Open code423n4 opened 2 years ago
Reward tokens are now whitelisted in our mainnet deployment
The warden has shown how a feeOnTransfer token can cause accounting issues and cause a loss of rewards for end users.
Because of the open-ended nature of the bribes contract, as well as the real risk of loss of promised rewards, I believe the finding to be valid and of Medium Severity
Lines of code
https://github.com/code-423n4/2022-05-velodrome/blob/main/contracts/contracts/Bribe.sol#L50-L51 https://github.com/code-423n4/2022-05-velodrome/blob/main/contracts/contracts/Bribe.sol#L83-L90
Vulnerability details
Impact
Should a fee-on-transfer token be added as a reward token and deposited, the tokens will be locked in the
Bribe
contract. Voters will be unable to withdraw their rewards.Proof of Concept
Tokens are deposited into the
Bribe
contract usingnotifyRewardAmount()
, whereamount
of tokens are transferred, then added directly totokenRewardsPerEpoch[token][adjustedTstamp]
:Tokens are transferred out of the
Bribe
contract usingdeliverReward()
, which attempts to transfertokenRewardsPerEpoch[token][epochStart]
amount of tokens out.If
token
happens to be a fee-on-transfer token,deliverReward()
will always fail. For example:notifyRewardAmount()
, withtoken
as token that charges a 2% fee upon any transfer, andamount = 100
:_safeTransferFrom()
only transfers 98 tokens to the contract due to the 2% feeepochRewards = 0
,tokenRewardsPerEpoch[token][adjustedTstamp]
becomes100
deliverReward()
is called with the sametoken
andepochStart
:rewardPerEpoch = tokenRewardsPerEpoch[token][epochStart] = 100
_safeTransfer
attempts to transfer 100 tokens out of the contractdeliverReward()
revertsThe following test, which implements a MockERC20 with fee-on-transfer, demonstrates this:
Additional Impact
On a larger scale, a malicious attacker could temporarily DOS any
Gauge
contract. This can be done by:Bribe
contract, usingnotifyRewardAmount()
, and adding it as a reward token.deliverBribes()
to fail whenever it is called, thus no one would be able to withdraw any reward tokens from theGauge
contract.The only way to undo the DOS would be to call
swapOutBribeRewardToken()
and swap out the fee-on-transfer token for another valid token.Recommended Mitigation
The amount of tokens received should be added to
epochRewards
and stored intokenRewardsPerEpoch[token][adjustedTstamp]
, instead of the amount stated for transfer. For example: