Uniswap V3 第 9 章:闪电贷(Flash)
和 V2 的 flash swap 类似,V3 的
pool.flash让你从池子里借出任意数量的 token0 和/或 token1,只要在同一笔交易里归还本金 + 手续费即可。这一章讲清它的流程、手续费和回调。
目录
- 1. V3 闪电贷 vs V2 闪电兑换
- 2. pool.flash 的流程
- 3. uniswapV3FlashCallback 回调
- 4. 手续费怎么算
- 5. 案例:借 100 万 USDC
- 6. 安全检查
- 7. 本章小结
- 8. 动手练习
1. V3 闪电贷 vs V2 闪电兑换
| V2 flash swap | V3 flash | |
|---|---|---|
| 入口 | pair.swap(..., data) | 专门的 pool.flash(...) |
| 借出 | 借 amount0Out / amount1Out | 借 amount0 / amount1 |
| 回调 | uniswapV2Call | uniswapV3FlashCallback |
| 还款 | 让 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. 本章小结
pool.flash(recipient, amount0, amount1, data)让你同时借出 token0 和 token1,同笔交易归还。- 乐观模式:先转出 → 回调
uniswapV3FlashCallback(fee0, fee1, data)→ 校验余额恢复。 - 手续费 =
ceil(amount · fee / 1e6),用池子费率档位,向上取整,归 LP。 - 选费率最低的池子借最划算(0.05% 比 0.3% 便宜 6 倍)。
- 回调必须
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);
}
实现:
flash(uint amount0, uint amount1):编码data = abi.encode(msg.sender, amount0, amount1),调pool.flash(address(this), amount0, amount1, data)。uniswapV3FlashCallback(uint fee0, uint fee1, bytes data):require(msg.sender == address(pool))。- 解码 data 拿到借入额和原始 caller。
- (练习里可不做实际套利)从 caller 收取 fee0/fee1(
transferFrom),或直接用合约预存的币支付。 - 把
amount0 + fee0、amount1 + 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 的改进。