Uniswap V2 - AMM - Liquidity Pool
深入例子与解析 —— Uniswap V2(恒积 AMM)流动性池各方面细节
下面我把 Uniswap V2(或 PancakeSwap V2 类似实现)的原理 + 合约调用 + 数学计算 + 常见问题用若干详细、可复现的例子彻底讲清楚(所有算术按步骤逐位计算,避免四则运算偷步造成错误)。
1. 恒积公式与价格基础(x × y = k)
- 池子保存两个代币的储备:
reserveA(Token A)与reserveB(Token B)。 - 恒积不变:
reserveA × reserveB = k(在没有手续费前,任何交换都应保持 k 不变)。 - 即瞬时价格(A 的价格以 B 计)近似为
price_A_in_B = reserveB / reserveA(注意是池中瞬时比值,不是离散市场深度)。
2. 示例:创建池子(第一次添加流动性)
假设项目方首次把 1000 个 TokenA(记作 A)和 2000 个 TokenB(记作 B)放入池子。
- 初始 reserves:
reserveA = 1000,reserveB = 2000。 - 初始价格
P_A=reserveB / reserveA = 2000 / 1000 = 2(即 1 A = 2 B)。
LP 代币铸造(首次):
Uniswap V2 首次提供流动性时,mint 的 LP 数量为:
$$
LP\text{_initial} = \sqrt{amountA \times amountB} - \text{MINIMUM_LIQUIDITY}
$$
其中通常 MINIMUM_LIQUIDITY = 1000(被锁定燃烧掉以避免除以 0 的问题)。
计算:
-
计算乘积
amountA * amountB = 1000 * 2000 = 2,000,000。 -
平方根
sqrt(2,000,000)。逐位算:sqrt(2,000,000) ≈ 1414.2135623730950488016887242097...(常见值)
-
初始 LP minted ≈
1414.213562373095 - 1000 = 414.213562373095(约 414.2136 LP 代币 发给最初提供者)。
(注:生产实现取整/小数位通常由合约逻辑决定)
3. 例子:执行一次 Swap(含手续费 0.3%)
池当前:reserveA = 1000, reserveB = 2000。用户想用 100 A 换取 B(swap A -> B)。Uniswap V2 的手续费是 0.30%,实际流入池的 amountInWithFee = amountIn * 997 / 1000(997 = 1000 - 3)。
步骤(逐位算):
-
amountIn = 100(A)。 -
计算含费后的数值:
amountInWithFee = 100 * 997 / 1000- 先算
100 * 997 = 99,700 - 然后
99,700 / 1000 = 99.7
→amountInWithFee = 99.7(A 的有效输入)
-
计算 amountOut(B)用公式:
$$
amountOut = \frac{amountInWithFee \times reserveB}{reserveA + amountInWithFee}
$$把数值代入:
- 分子:
99.7 × 2000 = 199,400 - 分母:
1000 + 99.7 = 1,099.7 - 除法:
199,400 / 1,099.7 ≈ 181.182897...
- 分子:
-
所以用户大约能收到
181.182897B(合约会四舍五入到可精度的整数或 token 的最小单位)。
更新后池子储备:
- 新
reserveA = 1000 + 100 = 1100(注意实际池里增加的是 amountIn 全额 100,而计算中用 amountInWithFee 来保持恒积关系) - 新
reserveB = 2000 - amountOut ≈ 2000 - 181.182897 = 1818.817103 - 检查恒积近似:
1100 * 1818.817103 ≈ 2,000,698.813(和原来2,000,000比较差异来自手续费留在池中,k 会增加一点)
4. LP 的增删(添加流动性到已有池)
已存在池:reserveA = 1000, reserveB = 2000, totalSupplyLP = S(例如最初 S=1414.2136)。新提供者想加 xA = 100 A 与 xB = ?。
-
必须按当前比例添加以避免改变价格:
$$
xB = xA \times \frac{reserveB}{reserveA}
$$例如:
xB = 100 × 2000 / 1000 = 200 -
若提供者按这个比例加入,获得的 LP token 数量为:
$$
liquidity = \frac{xA \times totalSupplyLP}{reserveA} = \frac{xB \times totalSupplyLP}{reserveB}
$$(两者应相等)
示例计算(若 totalSupplyLP = 2000 假设):
liquidity = 100 * 2000 / 1000 = 200(也等于200 * 2000 / 2000 = 200)
若未按比例加入(只提供一种或比例不同),Router 会计算最小值并退回多余资产(或按最小比率扣除)。
5. 移除流动性(退出)
-
移除
L个 LP token,退出比例为:L / totalSupplyLP -
提取出的代币数量:
$$
amountA = \frac{reserveA \times L}{totalSupplyLP}, \quad amountB = \frac{reserveB \times L}{totalSupplyLP}
$$ -
例:若
L = 200,reserveA = 1100,reserveB = 1818.817103,totalSupplyLP = 2000:amountA = 1100 * 200 / 2000 = 110amountB = 1818.817103 * 200 / 2000 = 181.8817103
6. 无常损失(Impermanent Loss)详细数值示例
这是理解 LP 风险的关键,实际算例最能说明问题。
初始:
- 用户提供流动性:
reserveA = 10 A,reserveB = 100 B(于是初始价格P_A = 100/10 = 10 B,即 1 A = 10 B)。 - k =
10 * 100 = 1000.
价格变动:假设外部市场使得 A 的价格从 10 B 变为 20 B(价格翻倍)。交易者与套利者会对池做套利使池内价格等于市场价(即 reserveB / reserveA = 20),在保持 k 不变下:
解方程:
reserveB = 20 * reserveA- 恒积
reserveA * reserveB = reserveA * (20 * reserveA) = 20 * reserveA^2 = 1000 - 所以
reserveA^2 = 1000 / 20 = 50 reserveA = sqrt(50) ≈ 7.0710678118654752440084436210485(保留精度)reserveB = 20 * reserveA ≈ 20 * 7.071067811865475 = 141.4213562373095
LP(若持有原来池的全部)在此时拥有的资产:
amountA = reserveA ≈ 7.071067811865475amountB = reserveB ≈ 141.4213562373095
以当前市场价(1 A = 20 B)计价,总价值为:
value_in_B = amountA * 20 + amountB = 7.071067811865475 * 20 + 141.4213562373095- 先算
7.071067811865475 * 20 = 141.4213562373095 - 再加上
141.4213562373095得到282.842712474619(B 单位)
如果不作为 LP,而是原样持有(HODL):
-
最开始持有的是
10 A + 100 B,价格翻倍后价值:A部分:10 * 20 = 200 (B)B部分:100 (B)- 总价值 =
200 + 100 = 300 (B)
无常损失 = 相对差距:
- 绝对差距 =
300 - 282.842712474619 = 17.157287525381 - 相对损失 =
17.157287525381 / 300 ≈ 0.057191 = 5.7191%
→ 即使池中包含手续费,LP 的收益需与手续费收入合并比较才能判断最终是否优于直接 HODL。
7. 费用与对 LP 收益的影响
- 每笔交易收取 0.30% 费用,直接加入池中(增加 k)。这些费用会累积到
reserveA/reserveB中,从而逐步补偿 LP 的无常损失并给 LP 带来收益。 - 当交易量高且手续费收入大时,LP 收益可能覆盖甚至超越无常损失。
8. 路由与多段路径(swap path)
Router 提供的常见函数:
swapExactTokensForTokens(amountIn, amountOutMin, path[], to, deadline)swapTokensForExactTokens(amountOut, amountInMax, path[], to, deadline)swapExactETHForTokens、swapExactTokensForETH等(处理原生币时的包装)
路径示例:若没有直接 A/B 池,但存在 A/WETH 与 WETH/B 池,路由 path = [A, WETH, B] 会先 A->WETH 再 WETH->B。Router 会组合多池实现最优兑换(但用户需要关注滑点与累计手续费)。
9. 合约内部常见函数(Uniswap V2 的关键点)
Factory.createPair(tokenA, tokenB):若 pair 不存在则创建 pair(返回 pair 合约地址)。Pair.mint(to):在添加流动性时 mint LP token。Pair.burn(to):移除流动性时烧毁 LP 并转回 token。Pair.swap(amount0Out, amount1Out, to, data):执行 swap;注意 swap 要求最后balance0 * balance1 >= k(含手续费)以防篡改。Pair.sync():外部改变 token 余额后强行更新 reserves(很少手动调用)。Pair.skim(to):把多余 token(误发送)转到 to。
闪兑(Flash swap):swap 可允许借出任意数量(0…reserve),并在同一交易内回传等值资产或支付相应费用 —— 类似 flash loan 的一个形式。开发者与审计需小心。
10. TWAP 与价格 Oracle(Uniswap v2 的实现)
- Pair 合约维护
price0CumulativeLast与price1CumulativeLast(累积(price * time))以便离链或链上通过差分计算时间加权平均价格(TWAP)。 - 计算方法概念:在两个时间点
t0与t1,TWAP =(priceCumulative(t1) - priceCumulative(t0)) / (t1 - t0)。 - 因为 cumulative 是累加 price×时间,取差除以时间即可得到时间加权平均价,抵抗短期操纵。
11. 前置与防护(实务注意)
- 滑点容忍(amountOutMin):全部 swap 都应设置最小可接受输出以避免因前置/价格波动导致巨额损失。
- deadline 参数:避免交易长时间挂起,被复议。
- 重入、批准浪费、审批最小化:在合约层需注意
approve的使用与最小权限原则。 - 前置/MEV风险:交易可能被矿工/验证者重排序或夹带,从而产生更差的执行(前置/插队)。可用保护策略(取消/限制滑点、高 gas 优先)但难以完全避免。
- 闪电贷攻击面:任何外部脚本可在单一 tx 中影响价格并操控 oracle,需在系统设计中考虑价格源多样化与检查(例如 TWAP 的窗口长度)。
12. 典型调用示例(伪代码)
-
创建池(如果不存在):
1
2// 返回 pair 地址
factory.createPair(tokenA, tokenB); -
添加流动性(router):
1
2
3
4
5
6router.addLiquidity(
tokenA, tokenB,
amountADesired, amountBDesired,
amountAMin, amountBMin,
to, deadline
); -
用 ETH/BNB 添加:
1
2
3
4
5
6router.addLiquidityETH(
token, amountTokenDesired,
amountTokenMin, amountETHMin,
to, deadline,
{ value: amountETH }
); -
交换(确保先 approve token):
1
2
3router.swapExactTokensForTokens(
amountIn, amountOutMin, path, to, deadline
);
13. 小结与最佳实践(工程及策略)
- 初学者用例:在测试网做
addLiquidity、swap、removeLiquidity的完整流程来观察 reserves、LP mint、手续费累计与价格变化。 - 提供流动性前请评估:预计交易量(fee 收入),代币波动性(IL 风险),以及是否需要锁定 LP Token 提升可信度。
- 使用 Router 而非直接 Pair:Router 帮你处理路径和多池组合,简化调用。
- 监控:持续监控池深度、滑点、套利窗口与手续费收入,适时调整策略。
- 安全:审计自制合约(如 Router/Helper)并尽量减少私钥暴露,避免自动化工具导入大量热私钥。