Uniswap V2 第6章 闪电兑换:先拿币后付款

讲解 Uniswap V2 的 flash swap:pair.swap 的乐观转账机制、回调 uniswapV2Call、还款金额与手续费计算。

6 分钟阅读
Uniswap V2 第6章 闪电兑换:先拿币后付款

Uniswap V2 第 6 章:闪电兑换(Flash Swap)

Uniswap V2 一个强大且独特的特性:你可以先从池子拿走代币、在同一笔交易里再还回去(还本 + 手续费),中间想干啥都行。这就是 flash swap(闪电兑换/闪电贷的雏形)。它是第 8 章套利的基础。


目录


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.swapto 可以是任意地址。如果 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. 本章小结

  1. flash swap = 先从池子拿币、同一笔交易内还本付费;中间可自由使用借来的币。
  2. 基础是 pair.swap“乐观转账 → 回调 → 末尾校验 K” 顺序。
  3. data 非空时触发 uniswapV2Call 回调,你在里面拿到币并安排还款。
  4. 借某币还同币的费用:fee = amount·3/997 + 1,还款 = amount + fee,归 LP。
  5. 回调里必须 require(msg.sender==pair)require(sender==自己) 两道防护。
  6. 还款不足则 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);
}

实现两个函数:

  1. 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)
  2. 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 + 1amountToRepay = 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 的累积价格构建抗操纵的时间加权平均价预言机

💬 评论