28912 - [SC - Critical] Attackers can control the vote result and ampli...
Submitted on Mar 1st 2024 at 17:54:55 UTC by @offside0011 for Boost | ZeroLend
Report ID: #28912
Report type: Smart Contract
Report severity: Critical
Target: https://github.com/zerolend/governance
Impacts:
Manipulation of governance voting result deviating from voted outcome and resulting in a direct change from intended effect of original results
Description
Brief/Intro
There is on lock on PoolVoter.sol. The voting results can be manipulated by repeatedly staking and unstaking in OmnichainStaking.
Vulnerability Details
Users can obtain NFTs by locking their zero tokens in either lockerLp or lockerToken. After acquiring the NFT, they can stake it in the OmnichainStaking to earn the corresponding token. Subsequently, they gain the ability to vote through PoolVoter, allowing them to control the share of the respective pool. When users vote, the PoolVoter.sol directly uses their balance in OmnichainStaking to determine their voting weight.
Although there are some checks in OmnichainStaking to avoid transfer between users
function transfer(address, uint256) public pure override returns (bool) {
// don't allow users to transfer voting power. voting power can only
// be minted or burnt and act like SBTs
require(false, "transfer disabled");
return false;
}
function transferFrom(
address,
address,
uint256
) public pure override returns (bool) {
// don't allow users to transfer voting power. voting power can only
// be minted or burnt and act like SBTs
require(false, "transferFrom disabled");
return false;
}
This check can be bypassed by unstaking directly and then staking it for another user.
if (data.length > 0) from = abi.decode(data, (address));
// if the stake is from the LP locker, then give 4 times the voting power
if (msg.sender == address(lpLocker)) {
lpPower[tokenId] = lpLocker.balanceOfNFT(tokenId);
_mint(from, lpPower[tokenId] * 4);
}
// if the stake is from a regular token locker, then give 1 times the voting power
else if (msg.sender == address(tokenLocker)) {
tokenPower[tokenId] = tokenLocker.balanceOfNFT(tokenId);
_mint(from, tokenPower[tokenId]);
} else require(false, "invalid operator");
Impact Details
The voting results can be manipulated and amplified, and the gauge pool rewards weight is based on the results of the voting. Therefore, attackers can exploit this to gain additional profits.
TIP1: Through auditing the code, another issue in the profit distribution process may be discovered. By manipulating the voting ratio at that moment, attackers can gain more profits. However, this second vulnerability would be analyzed more after fixing the first one.
TIP2: There is a bug in PoolVoter.sol, the bool check is wrong
function registerGauge(
address _asset,
address _gauge
) external onlyOwner returns (address) {
if (isPool[_asset]) {
_pools.push(_asset);
isPool[_asset] = true;
}
TIP3 Another bug in voters.ts, the governance.vestedZeroNFT.target and lending.protocolDataProvider.target is wrong.