Damn Vulnerable DeFi Challenge #5 Solution — The rewarder
Throughout numerous challenges, you will build the skills to become a bug hunter or security auditor in the space.
Challenge #5 — The rewarder
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.
Oh, by the way, rumours say a new pool has just landed on mainnet. Isn’t it offering DVT tokens in flash loans?
The attacker end goal
We start with zero DVT token, and the end goal of this challenge is to steal all the Pool’s reward, or at least most of them. To do that as the challenge description suggest we have to leverage the lending pool that offer flashloans without fee.
Study the contracts
This is the Lending pool contract, nothing wrong here. It offers a flashloan function called
flashLoan. It’s a pretty standard function where you specify the amount, it checks to have enough token before sending them to you, execute
receiveFlashLoan(uint256) on the
msg.sender and then check that the sender has repaid the loan.
This is the Reward ERC20 contract. Also here nothing special, when it’s created it set up a couple of roles and only the minter role can mint tokens toward an account. Both the Admin and Minter are the
msg.sender that created the contract.
Is an ERC20 contract that inherit from OpenZeppelin’s ERC20Snapshot. Directly from the OZ documentation:
This contract extends an ERC20 token with a snapshot mechanism. When a snapshot is created, the balances and total supply at the time are recorded for later access.
This can be used to safely create mechanisms based on token balances such as trustless dividends or weighted voting. In naive implementations it’s possible to perform a “double spend” attack by reusing the same balance from different accounts. By using snapshots to calculate dividends or voting power, those attacks no longer apply. It can also be used to create an efficient ERC20 forking mechanism.
AccountingToken contract allows the
TheRewarderPool contract to manage the amount of DVT token that have been deposited/withdrawn and the snapshot logic.
This is the main contract we are interested into. Let’s dive into it and see what’s going on function by function:
function deposit(uint256 amountToDeposit) external
- check if the amount is > 0
- mint the
msg.senderto this the deposited amount of DVT tokens and check the transfer result
function withdraw(uint256 amountToWithdraw) external
- burn the amount from
AccountingToken(it’s an ERC20 contract, so it will fail if the
msg.senderhas not enough balance deposited)
- transfer back the withdrawn DVT to
msg.senderchecking the result of the operation
function isNewRewardsRound() public view returns (bool)
The logic here is pretty simple:
return block.timestamp >= lastRecordedSnapshotTimestamp + REWARDS_ROUND_MIN_DURATION;
It checks if from the last reward distribution time (
lastRecordedSnapshotTimestamp) registered by
_recordSnapshot() has at least passed
REWARDS_ROUND_MIN_DURATION (5 days). Basically, it’s a new round if from the previous distribution has passed at least 5 days.
function distributeRewards() public returns (uint256)
- Check if it’s a new reward round calling
isNewRewardsRound()(has passed 5 days). If so, call
- Get the total amount of DVT token deposited in the pool on the last snapshot
- Get the amount of DVT token deposited by the user on the pool
- Calculate the amount of reward token to be rewarded to the user based on the percentage of contribution
rewards = (amountDeposited * 100 * 10 ** 18) / totalDeposits;
- If he gets some rewards and those rewards are not yet distributed to the user, the contract mint those rewards and send them to the
Ok, now we have a good understanding of the scenario. For the next round, we need to have enough token deposited in the pool to get the vast majority of the rewards. The pool is not checking for how long we have deposited our tokens to distribute a fair amount of token, so we just need to have them deposited for the time had to get the rewards.
First we have to create a new Contract because as you can see, only a contract can execute and receive the flash loans.
This temporary contract will:
- Wait for the amount of time needed to start a new round and be able to make the Rewarder Pool trigger the
_recordSnapshotat deposit time
- Check the amount of DVT token we can borrow with a flashloan from the Flashloan Pool
- Flashloan the max amount (we are not paying any fees)
- Deposit all the DVT token we just loaned. The
depositfunction will trigger
distributeRewardsfunction that will take a snapshot before distributing tokens to our account. Because we are the bigger staker in the pool, we are going to get the vast majority of reward tokens.
- Withdraw all the deposited DVT from the pool. We don’t need them anymore because we already got all the rewards needed, and we also need to repay back the loan!
- Repay back the loan to the Lending Pool
- Transfer all the rewards to the attacker
Here’s the code of the Attacker’s contract explained in the section above.
You can find the full solution on GitHub, looking at TheRewarderTest.t.sol
If you want to try yourself locally, just execute
forge test --match-contract TheRewarderTest -vv
All Solidity code, practices and patterns in this repository are DAMN VULNERABLE and for educational purposes only.
DO NOT USE IN PRODUCTION