Uniswap V2 第8章 应用:闪电兑换套利

把 swap 与 flash swap 串起来,构建一个跨池/跨 DEX 的套利合约:普通版与零本金的闪电兑换版。

6 分钟阅读
Uniswap V2 第8章 应用:闪电兑换套利

Uniswap V2 第 8 章:应用——闪电兑换套利(Arbitrage)

这是全课程的实战收官:把前面学的 swap(第 2 章)和 flash swap(第 6 章)组合起来,构建一个真实的跨池套利合约。先做需要本金的普通版,再做零本金的闪电兑换版。最后简单介绍如何求”最优投入量”。


目录


1. 套利的基本原理

同一对代币在两个池子里价格不同时,就存在套利机会:

便宜的池子买入,在的池子卖出,赚取差价。

例如 DAI/WETH 在 Uniswap 是 3000 DAI/WETH,在另一个 DEX(如 Sushiswap)是 3030 DAI/WETH。你可以:

  1. 在 Uniswap 用 3000 DAI 买 1 WETH。
  2. 在 Sushiswap 把 1 WETH 卖成 3030 DAI。
  3. 净赚约 30 DAI(扣手续费和滑点后)。

套利交易还会让两个池子的价格趋同——这是市场自我修正的机制。


2. 普通版套利:两次 swap

需要你先有本金(tokenIn)。逻辑(对应练习 Arb1 的 swap):

function swap(SwapParams calldata params) external {
    // 1. 从调用者拉入本金
    IERC20(params.tokenIn).transferFrom(msg.sender, address(this), params.amountIn);
    // 2. 执行两次兑换:router0 上 tokenIn→tokenOut,router1 上 tokenOut→tokenIn
    uint256 amountOut = _swap(params);
    // 3. 校验利润达标
    if (amountOut - params.amountIn < params.minProfit) revert InsufficientProfit();
    // 4. 把本金+利润还给调用者
    IERC20(params.tokenIn).transfer(msg.sender, amountOut);
}

_swap 内部:

在 router0 上:tokenIn --swapExactTokensForTokens--> tokenOut   (在便宜池买)
在 router1 上:tokenOut --swapExactTokensForTokens--> tokenIn   (在贵池卖回)
最终回到 tokenIn,数量 amountOut
若 amountOut > amountIn,差额就是利润

注意第二次 swap 设 amountOutMin = params.amountIn:保证至少换回本金,否则交易 revert(亏本不做)。


3. 案例:两个池子价差套利

设 tokenIn = DAI,tokenOut = WETH,amountIn = 30,000 DAI。

  • 池子 A(便宜,3000 DAI/WETH):30,000 DAI 买到约 9.95 WETH(含费滑点)。
  • 池子 B(贵,3030 DAI/WETH):9.95 WETH 卖回约 30,100 DAI(含费滑点)。
利润 ≈ 30,100 − 30,000 = 100 DAI(扣两次 0.3% 手续费和滑点后的净值)

如果价差太小、或你投入太多(滑点吃掉价差),amountOut 可能 < amountIn,这时 minProfit 检查会让交易 revert,保护你不亏本。


4. 零本金版:用 flash swap 套利

普通版要你先有 3 万 DAI。闪电兑换版连本金都不需要——从池子借出 tokenIn,套利赚钱,还本付费,剩下的就是纯利润(对应练习 Arb2)。

function flashSwap(address pair, bool isToken0, SwapParams calldata params) external {
    bytes memory data = abi.encode(msg.sender, pair, params);
    // 从 pair 借出 amountIn 个 tokenIn(借哪种由 isToken0 决定)
    IUniswapV2Pair(pair).swap(
        isToken0 ? params.amountIn : 0,
        isToken0 ? 0 : params.amountIn,
        address(this), data
    );
}

function uniswapV2Call(address sender, uint a0, uint a1, bytes calldata data) external {
    (address caller, address pair, SwapParams memory params) =
        abi.decode(data, (address, address, SwapParams));
    // 1. 用借来的 tokenIn 执行两次套利兑换
    uint256 amountOut = _swap(params);
    // 2. 算还款:本金 + flash swap 手续费
    uint256 fee = (params.amountIn * 3) / 997 + 1;
    uint256 amountToRepay = params.amountIn + fee;
    // 3. 利润校验
    uint256 profit = amountOut - amountToRepay;
    if (profit < params.minProfit) revert InsufficientProfit();
    // 4. 还款给 pair,利润给发起人
    IERC20(params.tokenIn).transfer(pair, amountToRepay);
    IERC20(params.tokenIn).transfer(caller, profit);
}

整个过程在一笔交易里完成,你不需要任何启动资金——这正是 DeFi 可组合性的魅力。


5. 两个版本的对比

普通版 swap闪电兑换版 flashSwap
需要本金✅ 要先有 amountIn❌ 零本金
资金来源你自己的钱从 pair 借
额外成本仅两次 swap 手续费两次 swap 费 + flash swap 费(3/997)
风险价格变动(已用 minProfit 兜底)同左 + 还款不足则整笔 revert
适合有资金的套利者任何人(机器人/MEV)

闪电版多付一点 flash swap 费,但换来”零本金”,对没有大额资金的人极有价值。


6. 最优投入量(选学)

投入太少 → 利润有限;投入太多 → 滑点把价差吃光甚至亏本。存在一个最优 amountIn 让利润最大。

对两个恒定乘积池,最优投入量有解析解(课程作为可选数学给出)。核心思路:把两个池子串起来表达”最终产出关于投入的函数”,求导令其为 0,解出最优投入。实践中也常用数值搜索(二分/三分)逼近。

本课程把它列为 optional,理解”存在最优点、过犹不及”这个直觉即可。生产级 MEV 机器人会精确求解或快速数值逼近。


7. 本章小结

  1. 套利 = 在便宜池买、贵池卖,赚价差,同时让两池价格趋同。
  2. 普通版transferFrom 拿本金 → 两次 swap(router0 买、router1 卖)→ minProfit 校验 → 还本+利润。
  3. 第二次 swap 设 amountOutMin = amountIn 保证不亏本。
  4. 闪电版:从 pair 借 tokenIn → 回调里两次套利 swap → 还 amountIn + amountIn·3/997 + 1 → 利润归发起人,零本金
  5. 投入量存在最优值(过多则滑点吃掉价差),可解析或数值求解。

8. 动手练习

对应课程的两个 Arbitrage 练习。

练习 1:普通版套利 UniswapV2Arb1.swap

实现 swap(SwapParams params)

  1. transferFrommsg.sender 拉入 amountIntokenIn
  2. 调内部 _swap(router0 上 tokenIn→tokenOut,router1 上 tokenOut→tokenIn)。
  3. if (amountOut - amountIn < minProfit) revert
  4. amountOut 全部转回 msg.sender

测试构造价差:在两个 Router(如 Uniswap 与 Sushiswap,或同一 Router 不同池)间制造价差,或用 deal + sync 人为造一个失衡池。

练习 2:零本金套利 UniswapV2Arb2.flashSwap

实现 flashSwapuniswapV2Call

  1. flashSwap:编码 data = (msg.sender, pair, params),调 pair.swap 借出 amountIn 个 tokenIn(按 isToken0 决定 amount0/1Out)。
  2. uniswapV2Call:解码 → _swap 套利 → 算 fee = amountIn*3/997+1amountToRepayprofit = amountOut - amountToRepayminProfit 校验 → 把 amountToRepay 还给 pair、profit 转给 caller。

断言:套利后发起人余额增加了 profit,且自己没出本金

运行

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

🎉 Uniswap V2 课程完结

恭喜走完 Uniswap V2!主线回顾:

  1. 总览:恒定乘积 x·y=k、Core/Periphery 架构。
  2. 兑换:含费的 amountOut/In 公式、pair.swap 守 k、滑点。
  3. 创建池:Factory、CREATE2 可预测地址、唯一池子。
  4. 添加流动性:几何平均铸 LP、取最小值、最小流动性锁定。
  5. 移除流动性:按比例 burn、无手续费。
  6. 闪电兑换:乐观转账 + 回调,先拿后还。
  7. TWAP:累积价格构建抗操纵预言机。
  8. 套利:swap + flash swap 实战,普通版与零本金版。

接下来学 Uniswap V3 会看到它如何用”集中流动性”把资本效率提升几个数量级——但 V2 的这些基础概念是理解 V3 的前提。

💬 评论