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 = 1000reserveB = 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 的问题)。

计算:

  1. 计算乘积 amountA * amountB = 1000 * 2000 = 2,000,000

  2. 平方根 sqrt(2,000,000)。逐位算:

    • sqrt(2,000,000) ≈ 1414.2135623730950488016887242097...(常见值)
  3. 初始 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)。

步骤(逐位算)

  1. amountIn = 100(A)。

  2. 计算含费后的数值:

    • amountInWithFee = 100 * 997 / 1000
    • 先算 100 * 997 = 99,700
    • 然后 99,700 / 1000 = 99.7
      amountInWithFee = 99.7(A 的有效输入)
  3. 计算 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...
  4. 所以用户大约能收到 181.182897 B(合约会四舍五入到可精度的整数或 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 = 110
    • amountB = 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.071067811865475
  • amountB = 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)
  • swapExactETHForTokensswapExactTokensForETH 等(处理原生币时的包装)

路径示例:若没有直接 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 合约维护 price0CumulativeLastprice1CumulativeLast(累积(price * time))以便离链或链上通过差分计算时间加权平均价格(TWAP)。
  • 计算方法概念:在两个时间点 t0t1,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
    6
    router.addLiquidity(
    tokenA, tokenB,
    amountADesired, amountBDesired,
    amountAMin, amountBMin,
    to, deadline
    );
  • 用 ETH/BNB 添加:

    1
    2
    3
    4
    5
    6
    router.addLiquidityETH(
    token, amountTokenDesired,
    amountTokenMin, amountETHMin,
    to, deadline,
    { value: amountETH }
    );
  • 交换(确保先 approve token):

    1
    2
    3
    router.swapExactTokensForTokens(
    amountIn, amountOutMin, path, to, deadline
    );

13. 小结与最佳实践(工程及策略)

  • 初学者用例:在测试网做 addLiquidityswapremoveLiquidity 的完整流程来观察 reserves、LP mint、手续费累计与价格变化。
  • 提供流动性前请评估:预计交易量(fee 收入),代币波动性(IL 风险),以及是否需要锁定 LP Token 提升可信度。
  • 使用 Router 而非直接 Pair:Router 帮你处理路径和多池组合,简化调用。
  • 监控:持续监控池深度、滑点、套利窗口与手续费收入,适时调整策略。
  • 安全:审计自制合约(如 Router/Helper)并尽量减少私钥暴露,避免自动化工具导入大量热私钥。