code-423n4 / 2022-05-velodrome-findings

0 stars 0 forks source link

Bribe.sol is not meant to handle fee-on-transfer tokens #222

Open code423n4 opened 2 years ago

code423n4 commented 2 years ago

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 using notifyRewardAmount(), where amount of tokens are transferred, then added directly to tokenRewardsPerEpoch[token][adjustedTstamp]:

    _safeTransferFrom(token, msg.sender, address(this), amount);
    tokenRewardsPerEpoch[token][adjustedTstamp] = epochRewards + amount;

Tokens are transferred out of the Bribe contract using deliverReward(), which attempts to transfer tokenRewardsPerEpoch[token][epochStart] amount of tokens out.

function deliverReward(address token, uint epochStart) external lock returns (uint) {
    require(msg.sender == gauge);
    uint rewardPerEpoch = tokenRewardsPerEpoch[token][epochStart];
    if (rewardPerEpoch > 0) {
        _safeTransfer(token, address(gauge), rewardPerEpoch);
    }
    return rewardPerEpoch;
}

If token happens to be a fee-on-transfer token, deliverReward() will always fail. For example:

The following test, which implements a MockERC20 with fee-on-transfer, demonstrates this:

// Note that the following test was adapted from Bribes.t.sol
function testFailFeeOnTransferToken() public {
    // Deploy ERC20 token with fee-on-transfer
    MockERC20Fee FEE_TOKEN = new MockERC20Fee("FEE", "FEE", 18);

    // Mint FEE token for address(this)
    FEE_TOKEN.mint(address(this), 1e25);

    // vote
    VELO.approve(address(escrow), TOKEN_1);
    escrow.create_lock(TOKEN_1, 4 * 365 * 86400);
    vm.warp(block.timestamp + 1);

    address[] memory pools = new address[](1);
    pools[0] = address(pair);
    uint256[] memory weights = new uint256[](1);
    weights[0] = 10000;
    voter.vote(1, pools, weights);

    // and deposit into the gauge!
    pair.approve(address(gauge), 1e9);
    gauge.deposit(1e9, 1);

    vm.warp(block.timestamp + 12 hours); // still prior to epoch start
    vm.roll(block.number + 1);
    assertEq(uint(gauge.getVotingStage(block.timestamp)), uint(Gauge.VotingStage.BribesPhase));

    vm.warp(block.timestamp + 12 hours); // start of epoch
    vm.roll(block.number + 1);
    assertEq(uint(gauge.getVotingStage(block.timestamp)), uint(Gauge.VotingStage.VotesPhase));

    vm.warp(block.timestamp + 5 days); // votes period over
    vm.roll(block.number + 1);

    vm.warp(2 weeks + 1); // emissions start
    vm.roll(block.number + 1);

    minter.update_period();
    distributor.claim(1); // yay this works

    vm.warp(block.timestamp + 1 days); // next votes period start
    vm.roll(block.number + 1);

    // get a bribe
    owner.approve(address(FEE_TOKEN), address(bribe), TOKEN_1);
    bribe.notifyRewardAmount(address(FEE_TOKEN), TOKEN_1);

    vm.warp(block.timestamp + 5 days); // votes period over
    vm.roll(block.number + 1);

    // Atttempt to claim tokens will revert
    voter.distro(); // bribe gets deposited in the gauge
}

Additional Impact

On a larger scale, a malicious attacker could temporarily DOS any Gauge contract. This can be done by:

  1. Depositing a fee-on-transfer token into its respective Bribe contract, using notifyRewardAmount(), and adding it as a reward token.
  2. This would cause deliverBribes() to fail whenever it is called, thus no one would be able to withdraw any reward tokens from the Gauge 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

pooltypes commented 2 years ago

Reward tokens are now whitelisted in our mainnet deployment

GalloDaSballo commented 2 years ago

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