Uniswap V3 第9章 闪电贷:pool.flash 借出与归还

讲解 Uniswap V3 的闪电贷 pool.flash:如何从池子借出任意代币、在同一笔交易内归还本金加手续费,以及回调机制。

4 分钟阅读
Uniswap V3 第9章 闪电贷:pool.flash 借出与归还

Uniswap V3 第 9 章:闪电贷(Flash)

和 V2 的 flash swap 类似,V3 的 pool.flash 让你从池子里借出任意数量的 token0 和/或 token1,只要在同一笔交易里归还本金 + 手续费即可。这一章讲清它的流程、手续费和回调。


目录


1. V3 闪电贷 vs V2 闪电兑换

V2 flash swapV3 flash
入口pair.swap(..., data)专门的 pool.flash(...)
借出借 amount0Out / amount1Out借 amount0 / amount1
回调uniswapV2CalluniswapV3FlashCallback
还款让 K 不等式成立归还本金 + 按费率档位的手续费
可同时借两种借一种为主可同时借 token0 和 token1

V3 把”闪电贷”做成了独立的 flash 函数(语义更清晰),而不是借用 swap。


2. pool.flash 的流程

function flash(address recipient, uint256 amount0, uint256 amount1, bytes calldata data) external {
    // ① 记录借出前的余额
    uint256 balance0Before = balance0();
    uint256 balance1Before = balance1();
    // ② 把 amount0 / amount1 乐观地转给 recipient
    if (amount0 > 0) TransferHelper.safeTransfer(token0, recipient, amount0);
    if (amount1 > 0) TransferHelper.safeTransfer(token1, recipient, amount1);
    // ③ 回调 recipient:你在这里用借来的钱做事 + 准备还款
    IUniswapV3FlashCallback(recipient).uniswapV3FlashCallback(fee0, fee1, data);
    // ④ 检查余额已恢复(本金 + 手续费)
    require(balance0() >= balance0Before + fee0);
    require(balance1() >= balance1Before + fee1);
    // ⑤ 多出来的手续费计入 feeGrowthGlobal,分给 LP
}

和 V2 一样是”先转出、再回调、后校验”的乐观模式。


3. uniswapV3FlashCallback 回调

你的合约要实现:

function uniswapV3FlashCallback(uint256 fee0, uint256 fee1, bytes calldata data) external {
    require(msg.sender == address(pool), "not pool");  // 安全检查
    // 解码 data,拿到借了多少、原始发起人等
    // 用借来的钱做事(套利/清算/再平衡...)
    // 归还:把 借入额 + fee 转回 pool
    if (amount0 + fee0 > 0) IERC20(token0).transfer(address(pool), amount0 + fee0);
    if (amount1 + fee1 > 0) IERC20(token1).transfer(address(pool), amount1 + fee1);
}

Pool 在回调里直接把该付的手续费 fee0/fee1 算好传给你,省得你自己算。


4. 手续费怎么算

手续费用池子的费率档位(第 5 章)算在借入额上:

fee0 = ceil(amount0 · fee / 1e6)
fee1 = ceil(amount1 · fee / 1e6)

fee 是池子的费率(如 500 = 0.05%,3000 = 0.3%),向上取整保证池子不亏。这笔手续费归 LP(计入 feeGrowthGlobal)。

注意:V3 闪电贷费率 = 该池子的 swap 费率档位。所以从 0.05% 池借比从 0.3% 池借便宜——选低费率池借更划算。


5. 案例:借 100 万 USDC

从一个 0.05% 档(fee=500)的池子借 1,000,000 USDC:

fee0 = ceil(1,000,000e6 · 500 / 1e6) = ceil(1,000,000e6 · 0.0005) = 500e6 = 500 USDC
还款 = 1,000,000 + 500 = 1,000,500 USDC

你借 100 万 USDC,结束前要还 100.05 万。这 500 USDC 必须来自你在回调里赚到的钱(套利/清算利润)。如果从 0.3% 档借同样金额,手续费就是 3000 USDC——所以选费率最低的池子借


6. 安全检查

回调里必须校验 msg.sender == address(pool)

require(msg.sender == address(pool), "not pool");

否则任何人都能调用你的 uniswapV3FlashCallback,骗你执行还款逻辑、转走你合约里的钱。这和 V2 flash swap 的安全检查是同一个道理。如果你的合约支持多个池子,还应校验 msg.sender 是某个合法的 V3 池(可用 Factory 的 getPool 反查,或对比 CREATE2 地址)。


7. 本章小结

  1. pool.flash(recipient, amount0, amount1, data) 让你同时借出 token0 和 token1,同笔交易归还。
  2. 乐观模式:先转出 → 回调 uniswapV3FlashCallback(fee0, fee1, data) → 校验余额恢复
  3. 手续费 = ceil(amount · fee / 1e6),用池子费率档位,向上取整,归 LP。
  4. 费率最低的池子借最划算(0.05% 比 0.3% 便宜 6 倍)。
  5. 回调必须 require(msg.sender == address(pool)) 防止被冒用。

8. 动手练习

对应课程的 Flash 练习:写一个合约从 V3 池借币再还。

练习:实现 UniswapV3Flash

interface IUniswapV3Pool {
    function flash(address recipient, uint256 amount0, uint256 amount1, bytes calldata data) external;
    function token0() external view returns (address);
    function token1() external view returns (address);
    function fee() external view returns (uint24);
}

实现:

  1. flash(uint amount0, uint amount1):编码 data = abi.encode(msg.sender, amount0, amount1),调 pool.flash(address(this), amount0, amount1, data)
  2. uniswapV3FlashCallback(uint fee0, uint fee1, bytes data)
    • require(msg.sender == address(pool))
    • 解码 data 拿到借入额和原始 caller。
    • (练习里可不做实际套利)从 caller 收取 fee0/fee1(transferFrom),或直接用合约预存的币支付。
    • amount0 + fee0amount1 + fee1 转回 pool。

测试:

  • setUp 给合约/caller 一点币用于支付手续费,approve。
  • flash(借入额),断言交易成功、池子余额恢复(≥ 借出前 + fee)。

运行

forge test --evm-version cancun --fork-url $FORK_URL \
  --match-path test/UniswapV3Flash.t.sol -vvv

下一章(第 10 章 TWAP)讲 V3 的时间加权平均价预言机:tickCumulative、observe,以及它相比 V2 TWAP 的改进。

💬 评论