Uniswap V2 第 8 章:应用——闪电兑换套利(Arbitrage)
这是全课程的实战收官:把前面学的 swap(第 2 章)和 flash swap(第 6 章)组合起来,构建一个真实的跨池套利合约。先做需要本金的普通版,再做零本金的闪电兑换版。最后简单介绍如何求”最优投入量”。
目录
- 1. 套利的基本原理
- 2. 普通版套利:两次 swap
- 3. 案例:两个池子价差套利
- 4. 零本金版:用 flash swap 套利
- 5. 两个版本的对比
- 6. 最优投入量(选学)
- 7. 本章小结
- 8. 动手练习
1. 套利的基本原理
当同一对代币在两个池子里价格不同时,就存在套利机会:
在便宜的池子买入,在贵的池子卖出,赚取差价。
例如 DAI/WETH 在 Uniswap 是 3000 DAI/WETH,在另一个 DEX(如 Sushiswap)是 3030 DAI/WETH。你可以:
- 在 Uniswap 用 3000 DAI 买 1 WETH。
- 在 Sushiswap 把 1 WETH 卖成 3030 DAI。
- 净赚约 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. 本章小结
- 套利 = 在便宜池买、贵池卖,赚价差,同时让两池价格趋同。
- 普通版:
transferFrom拿本金 → 两次 swap(router0 买、router1 卖)→minProfit校验 → 还本+利润。 - 第二次 swap 设
amountOutMin = amountIn保证不亏本。 - 闪电版:从 pair 借 tokenIn → 回调里两次套利 swap → 还
amountIn + amountIn·3/997 + 1→ 利润归发起人,零本金。 - 投入量存在最优值(过多则滑点吃掉价差),可解析或数值求解。
8. 动手练习
对应课程的两个 Arbitrage 练习。
练习 1:普通版套利 UniswapV2Arb1.swap
实现 swap(SwapParams params):
transferFrom从msg.sender拉入amountIn个tokenIn。- 调内部
_swap(router0 上 tokenIn→tokenOut,router1 上 tokenOut→tokenIn)。 if (amountOut - amountIn < minProfit) revert。- 把
amountOut全部转回msg.sender。
测试构造价差:在两个 Router(如 Uniswap 与 Sushiswap,或同一 Router 不同池)间制造价差,或用 deal + sync 人为造一个失衡池。
练习 2:零本金套利 UniswapV2Arb2.flashSwap
实现 flashSwap 和 uniswapV2Call:
flashSwap:编码data = (msg.sender, pair, params),调pair.swap借出amountIn个 tokenIn(按isToken0决定 amount0/1Out)。uniswapV2Call:解码 →_swap套利 → 算fee = amountIn*3/997+1和amountToRepay→profit = amountOut - amountToRepay→minProfit校验 → 把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!主线回顾:
- 总览:恒定乘积
x·y=k、Core/Periphery 架构。 - 兑换:含费的 amountOut/In 公式、pair.swap 守 k、滑点。
- 创建池:Factory、CREATE2 可预测地址、唯一池子。
- 添加流动性:几何平均铸 LP、取最小值、最小流动性锁定。
- 移除流动性:按比例 burn、无手续费。
- 闪电兑换:乐观转账 + 回调,先拿后还。
- TWAP:累积价格构建抗操纵预言机。
- 套利:swap + flash swap 实战,普通版与零本金版。
接下来学 Uniswap V3 会看到它如何用”集中流动性”把资本效率提升几个数量级——但 V2 的这些基础概念是理解 V3 的前提。