4、Uniswap V2核心机制:恒定乘积公式、交易对与流动性池、swap与addLiquidity函数解析、滑点与价格影响
Uniswap V2 的核心,说白了就一个公式:x * y = k。这个公式看起来简单,但背后藏着整套自动做市商(AMM)的逻辑。我当年第一次看白皮书时,心想「就这?」。后来真正写合约、跑模拟、踩坑之后才发现,越简单的公式,越容易在细节上翻车。
4.1 恒定乘积公式:AMM的数学根基
恒定乘积公式是 Uniswap 的灵魂。它保证池子里两种资产的数量乘积始终不变(不考虑手续费)。
公式:x * y = k
- x:代币 A 的储备量
- y:代币 B 的储备量
- k:恒定常数
举个例子:假设池子里有 100 个 ETH 和 200,000 个 USDC。那么 k = 100 * 200,000 = 20,000,000。如果有人用 1 个 ETH 换 USDC,池子里的 ETH 变成 101,新的 USDC 数量就是 20,000,000 / 101 ≈ 198,019.8。所以他能拿到 200,000 - 198,019.8 = 1,980.2 个 USDC。
嗯,这里要注意:实际交易还要扣 0.3% 的手续费。但核心逻辑就是「买的人越多,价格越贵」。你想想看,如果池子深度不够,大额交易会直接把价格推到天上。
我的经验:我在做套利机器人时,经常需要预计算价格影响。如果直接用 x * y = k 算,忽略手续费,最终成交价会差 0.3%。别小看这 0.3%,高频交易里这就是利润空间。
4.2 交易对与流动性池:池子是怎么组成的?
每个交易对就是一个独立的流动性池。比如 ETH/USDC 是一个池,ETH/DAI 是另一个池。每个池子部署一个独立的合约,里面只存两种代币。
池子的创建是通过 UniswapV2Factory 完成的。调用 createPair 函数,传入两个代币地址,工厂合约就会部署一个新的交易对合约。
// 创建交易对的示例
function createPair(address tokenA, address tokenB) external returns (address pair) {
// 内部逻辑:部署新合约,初始化储备量为0
pair = address(new UniswapV2Pair());
// 记录交易对信息
}
我个人习惯在创建池子后,先检查一下储备量是否真的为 0。曾经有个项目方在测试网部署时,因为代币地址传反了,导致池子里的两种代币数量完全错乱。嗯,这种低级错误其实挺常见的。
4.3 swap 函数解析:交易到底怎么执行的?
swap 函数是 Uniswap V2 最核心的函数。它负责执行代币兑换。调用者需要指定「输出代币的数量」或者「输入代币的数量」,合约会按恒定乘积公式计算实际成交价。
// swap 函数核心逻辑(简化版)
function swap(uint256 amount0Out, uint256 amount1Out, address to) external {
// 检查输出数量不能同时为0
require(amount0Out > 0 || amount1Out > 0, "UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT");
// 更新储备量
uint256 balance0 = IERC20(token0).balanceOf(address(this));
uint256 balance1 = IERC20(token1).balanceOf(address(this));
// 计算实际输入量
uint256 amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
uint256 amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
// 验证恒定乘积
uint256 balance0Adjusted = balance0 * 1000 - amount0In * 3;
uint256 balance1Adjusted = balance1 * 1000 - amount1In * 3;
require(balance0Adjusted * balance1Adjusted >= uint256(_reserve0) * _reserve1 * (1000**2), "UniswapV2: K");
}
这段代码里有个细节:balance0Adjusted * balance1Adjusted >= ... 这行是在检查扣掉 0.3% 手续费后,乘积是否仍然大于等于原来的 k。如果小于,说明有人试图操纵价格,交易会被回滚。
避坑指南:我曾经在写套利合约时,直接调用 swap 函数但忘了传正确的 to 地址。结果代币转到了合约自己手里,锁死了。嗯,从那以后我每次调用 swap 都会再三确认 to 参数。
4.4 addLiquidity 函数解析:怎么添加流动性?
添加流动性是 LP(流动性提供者)的核心操作。调用 addLiquidity 时,你需要同时提供两种代币,且比例必须和当前池子里的比例一致。
// addLiquidity 核心逻辑(简化版)
function addLiquidity(address tokenA, address tokenB, uint256 amountADesired, uint256 amountBDesired) external {
// 计算实际应添加的数量
(uint256 reserveA, uint256 reserveB) = getReserves();
uint256 amountBOptimal = quote(amountADesired, reserveA, reserveB);
// 如果最优数量小于期望数量,按最优数量执行
if (amountBOptimal <= amountBDesired) {
// 添加流动性
} else {
uint256 amountAOptimal = quote(amountBDesired, reserveB, reserveA);
// 添加流动性
}
}
这里有个关键点:quote 函数用来计算最优比例。如果你提供的比例不对,合约会自动调整,只取你能提供的那部分。比如池子里 ETH/USDC 是 1:2000,你只有 1 ETH 和 1000 USDC,那合约只会收你 0.5 ETH 和 1000 USDC,剩下的 0.5 ETH 退给你。
我的建议:添加流动性前,最好先调用 getReserves 查看当前储备量,然后按比例准备代币。否则多出来的代币会被退回,白白浪费 gas。
4.5 滑点与价格影响:为什么大额交易会亏钱?
滑点(Slippage)是指你预期的成交价和实际成交价之间的差异。价格影响(Price Impact)是指你的交易对池子价格造成的改变。
这两个概念其实是同一枚硬币的两面。滑点是结果,价格影响是原因。
| 交易规模 | 价格影响 | 滑点风险 |
|---|---|---|
| 小额(< 1% 池子深度) | 几乎为 0 | 低 |
| 中等(1%-10% 池子深度) | 1%-5% | 中等 |
| 大额(> 10% 池子深度) | 5%-20%+ | 高 |
举个例子:一个深度 100 ETH 的池子,你买 10 ETH 的代币,价格影响大约在 10% 左右。也就是说,你实际付出的成本比预期高了 10%。
为什么会这样?因为恒定乘积公式决定了:你买的越多,单价越贵。这就像在沙漠里买水——第一瓶水便宜,最后一瓶水贵得离谱。
避坑指南:我曾经在写套利策略时,没有设置滑点保护。结果一笔大额交易因为链上拥堵,实际滑点达到了 15%,直接把利润吃光了。从那以后,我所有交易都强制设置 amountOutMin 参数,低于预期就回滚。
4.6 知识体系总览
下面这张图总结了 Uniswap V2 的核心机制。你可以看到,所有操作都围绕着恒定乘积公式展开。
这张图把 Uniswap V2 的核心机制串起来了。你想想看,从公式出发,衍生出交易对、流动性池、swap、addLiquidity,再到滑点和价格影响——所有东西都是一体的。
总结一下:
- 恒定乘积公式是 Uniswap 的数学基础,所有交易都围绕它展开
- 每个交易对是一个独立的池子,储备量决定价格
- swap 函数执行兑换,addLiquidity 函数添加流动性
- 滑点和价格影响是大额交易必须考虑的因素
- 设置滑点保护是套利机器人的基本操作
嗯,这一章的内容就到这里。记住,理解 Uniswap V2 的核心机制,是搭建套利机器人的第一步。后面的章节我们会深入代码实现和实战策略。