Uniswap V2 第 6 章:闪电兑换(Flash Swap)
Uniswap V2 一个强大且独特的特性:你可以先从池子拿走代币、在同一笔交易里再还回去(还本 + 手续费),中间想干啥都行。这就是 flash swap(闪电兑换/闪电贷的雏形)。它是第 8 章套利的基础。
目录
- 1. 什么是 flash swap
- 2. 乐观转账:pair.swap 的关键设计
- 3. uniswapV2Call 回调
- 4. 还多少:flash swap 的手续费
- 5. 案例:借 10,000 DAI 要还多少
- 6. 安全检查:两道 require
- 7. 完整流程图
- 8. 本章小结
- 9. 动手练习
1. 什么是 flash swap
普通 swap:你先给输入币,才拿输出币。
flash swap:池子先把你要的币转给你,在同一笔交易结束前,你只要保证把”该付的”补齐(让 x·y=k 的不等式成立)即可。你可以:
- 借出来去别处套利,用赚到的钱还款(第 8 章)。
- 借 A 币、最后用 B 币还(真正的”兑换”,无需预先持有输入币)。
- 借出来还闪电贷、清算、再平衡等。
关键:这一切必须在一笔交易内完成。 如果交易结束时没还够,整笔交易 revert,就像什么都没发生。
2. 乐观转账:pair.swap 的关键设计
回忆第 2 章 pair.swap 的顺序:
function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) {
// ① 先把输出币乐观地转给 to
if (amount0Out > 0) _safeTransfer(token0, to, amount0Out);
if (amount1Out > 0) _safeTransfer(token1, to, amount1Out);
// ② 如果 data 非空,回调 to 合约的 uniswapV2Call(把控制权交给你)
if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
// ③ 读取转账后的实际余额
balance0 = IERC20(token0).balanceOf(address(this));
balance1 = IERC20(token1).balanceOf(address(this));
// ④ 校验 K 不等式(扣 0.3% 费后乘积不减)
require(balance0Adjusted · balance1Adjusted >= reserve0 · reserve1 · 1000²);
}
“先转出(①)、再回调(②)、最后校验(④)” 就是 flash swap 的精髓:
- 你在回调 ② 里已经拿到了币,可以自由使用。
- 只要在回调结束前把还款转回 Pair,让 ④ 的不等式成立即可。
- 普通 swap 不传
data,跳过回调,必须事先转入输入币才能让 ④ 通过。
3. uniswapV2Call 回调
当你调用 pair.swap(..., data) 且 data 非空,Pair 会回调你的合约:
function uniswapV2Call(address sender, uint256 amount0, uint256 amount1, bytes calldata data) external {
// 此时你已经收到了借出的币,在这里:
// - 做你的事(套利/清算/...)
// - 计算还款额
// - 把还款转回 pair
}
sender:发起pair.swap的地址(应是你自己的合约)。amount0/amount1:池子借给你的两种币数量(通常只有一个 > 0)。data:你在flashSwap里编码进去的自定义数据(比如借的是哪种币、原始调用者是谁)。
4. 还多少:flash swap 的手续费
flash swap 沿用 swap 的 0.3% 费率。如果你借出 amount 个某币、最后用同一种币还,需要还:
fee = amount · 3 / 997 + 1 (向上取整)
还款额 = amount + fee
为什么是 3/997 而不是 3/1000?因为 0.3% 费率定义在”输入”上:要让池子的调整后余额满足不等式,借出 amount 需补回的最小费用恰好是 amount·3/997(可由 K 不等式反推)。+1 向上取整保证足额,避免因舍入差 1 wei 导致 revert。
注意:如果你借 A、用 B 还(真正的兑换式 flash swap),费率体现为 swap 公式本身,不必单独算这个 fee。上面的公式针对”借某币、还同币”的场景(最常见的闪电贷用法)。
5. 案例:借 10,000 DAI 要还多少
amount = 10,000 DAI
fee = 10,000 · 3 / 997 + 1 = 30,000/997 + 1 ≈ 30.09 + 1 ≈ 31.09 → 取整后约 31 DAI(实现里整数运算)
还款额 = 10,000 + 31 = 10,031 DAI
也就是说,你借 1 万 DAI,在交易结束前要还回约 10,031 DAI(多还约 31 DAI 作为手续费,归 LP)。这 31 DAI 必须来自你在回调里赚到的钱——如果套利赚了 100 DAI,扣掉 31 还净赚 69。
6. 安全检查:两道 require
写 flash swap 合约时,回调里必须做两道防护:
function uniswapV2Call(address sender, ...) external {
// ① 调用者必须是 Pair 合约(防止任何人随便调你的回调)
require(msg.sender == address(pair), "not pair");
// ② sender 必须是你自己(防止别人借用你的合约去触发 Pair 回调你)
require(sender == address(this), "not sender");
...
}
为什么需要 ②?因为 pair.swap 的 to 可以是任意地址。如果 Eve 调用 pair.swap(to=你的合约),你的回调会被触发、sender 会是 Eve。检查 sender == address(this) 确保只有你自己发起的 flash swap 才会执行还款逻辑,防止被人薅走你合约里的钱。
7. 完整流程图
你 → 你的FlashSwap合约.flashSwap(token, amount)
│ 编码 data = (token, msg.sender)
▼
pair.swap(amountOut, to=你的合约, data)
│ ① 先把 amount 个 token 转给你的合约(乐观转账)
│ ② 回调 你的合约.uniswapV2Call(sender, ...)
▼
uniswapV2Call:
│ require msg.sender==pair, sender==自己
│ 用借来的币做事(套利等)
│ 算 fee = amount·3/997 + 1
│ 把 amount+fee 转回 pair
▼
回到 pair.swap ④:校验 K 不等式 ✓ → 交易成功
(若还款不足 → 不等式不成立 → 整笔 revert)
8. 本章小结
- flash swap = 先从池子拿币、同一笔交易内还本付费;中间可自由使用借来的币。
- 基础是
pair.swap的 “乐观转账 → 回调 → 末尾校验 K” 顺序。 data非空时触发uniswapV2Call回调,你在里面拿到币并安排还款。- 借某币还同币的费用:
fee = amount·3/997 + 1,还款 =amount + fee,归 LP。 - 回调里必须
require(msg.sender==pair)和require(sender==自己)两道防护。 - 还款不足则 K 不等式不成立,整笔交易 revert(无副作用)。
9. 动手练习
对应课程的 Flash Swap 练习:写一个合约,从 Pair 借币再还。
练习:实现 UniswapV2FlashSwap
interface IUniswapV2Pair {
function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external;
function token0() external view returns (address);
function token1() external view returns (address);
}
实现两个函数:
-
flashSwap(address token, uint amount):- 校验
token是 token0 或 token1。 - 根据 token 决定
amount0Out/amount1Out(借哪种,另一种为 0)。 - 编码
data = abi.encode(token, msg.sender)。 - 调用
pair.swap(amount0Out, amount1Out, address(this), data)。
- 校验
-
uniswapV2Call(address sender, uint amount0, uint amount1, bytes data):require(msg.sender == address(pair))和require(sender == address(this))。- 解码
data得到token和原始caller。 - 确定借到的
amount(amount0 或 amount1 里 > 0 的那个)。 - 算
fee = amount*3/997 + 1,amountToRepay = amount + fee。 - 从
caller收取 fee(transferFrom(caller, this, fee)),然后把amountToRepay转回 pair。
测试要点:
setUp给 caller 充一点币用于支付 fee,并 approve 给你的 FlashSwap 合约。- 调
flashSwap(DAI, 10000e18),断言交易成功、pair 储备恢复(拿回了本+费)。
运行
forge test --evm-version cancun --fork-url $FORK_URL \
--match-path test/UniswapV2FlashSwap.t.sol -vvv
下一章(第 7 章 TWAP)讲:如何用 Uniswap V2 的累积价格构建抗操纵的时间加权平均价预言机。