04.Uniswap V2 添加移除流动性

核心摘要 (Key Takeaways)

  • 核心公式与流动性衡量: Uniswap V2 的流动性池基于 X×Y=KX \times Y = K 恒定乘积公式,其中 L=KL = \sqrt{K} 被用作衡量流动性大小的线性指标,便于 LP Token (LPT) 的线性增发与销毁。
  • 价格不变性约束: 无论是添加还是移除流动性,操作前后池内两种代币的价格比(即兑换价格)必须保持不变,这是确保用户按当前市场价提供或撤出流动性的核心原则。
  • LPT 增发/销毁机制: 添加流动性时,新铸造的 LP Token 数量与新增流动性占总流动性的比例成正比;移除流动性时,用户收回的代币数量也与销毁的 LP Token 占总 LP Token 的比例成正比。
  • Uniswap V2 合约架构: 核心交互涉及 路由合约 (Router)工厂合约 (Factory)交易对合约 (Pair)。路由合约是用户入口,工厂合约负责创建交易对,交易对合约管理实际的流动性池和代币交换。
  • 关键工程实现: 智能合约实现中包含多项精巧设计,如使用 create2 预测交易对地址、对代币地址进行排序以确保交易对的唯一性、以及设置 minimum liquidity 机制以增强安全性和稳定性。

Uniswap V2 流动性管理概述

本节课主要围绕 Uniswap V2 中的添加流动性移除流动性无常损失(本节课不深入讲解)三个核心概念。目标是清晰梳理前两个概念的数学原理和合约实现流程。


添加流动性 (Adding Liquidity)

1. 核心概念与数学原理

  • 定义: 用户向流动性池中同时投入两种代币(X 和 Y),以增加池子的总流动性。
  • 恒定乘积公式: 流动性池的核心机制是 X×Y=KX \times Y = K,其中 KK 是一个常数。
  • 流动性的衡量 (L):
    • 为了实现 LP Token 的线性增发,Uniswap V2 使用 LL 来衡量流动性,而非 KK
    • LLKK 的关系:L=KL = \sqrt{K}
    • 案例: 这种开平方根的方式,使得 LL 可以线性增长。无论用户提供流动性的时间早晚,只要对池子的贡献比例相同,获得的 LP Token 数量就相同。
  • 价格不变性约束:
    • 添加流动性时,池中代币的价格比不能发生变化。
    • 初始价格比:P0=Y0/X0P_0 = Y_0 / X_0
    • 添加后价格比:P1=(Y0+DY)/(X0+DX)P_1 = (Y_0 + DY) / (X_0 + DX)
    • 约束条件:P0=P1Y0/X0=DY/DXP_0 = P_1 \Rightarrow Y_0 / X_0 = DY / DX
    • 几何表现: 在坐标系中,添加流动性的操作点会落在连接原点和当前价格点 (X0,Y0)(X_0, Y_0) 的一条直线上。
  • LP Token (LPT) 的获取:
    • 用户投入 DXDY 两种代币。
    • 用户获得 LP Token (也称 Share) 作为其流动性贡献的凭证,这些 LPT 后续可用于质押、流动性挖矿等。
  • LP Token 增发量 (S) 的计算:
    • 基本原理: 新增发的 LP Token (S) 占增发后总量的比例,等于新增流动性占增发后总流动性的比例。
    • 公式表达: L1L0L0=ST \frac{L_1 - L_0}{L_0} = \frac{S}{T} 其中:
      • L0=X0×Y0L_0 = \sqrt{X_0 \times Y_0} (初始流动性)
      • L1=(X0+DX)×(Y0+DY)L_1 = \sqrt{(X_0 + DX) \times (Y_0 + DY)} (添加后的流动性)
      • SS (为本次操作新增发的 LP Token 数量)
      • TT (初始 LP Token 总量,即 totalSupply)
    • 推导简化结果: 结合价格不变性约束 Y0/X0=DY/DXY_0/X_0 = DY/DX,可以推导出: S=DXX0×T=DYY0×T S = \frac{DX}{X_0} \times T = \frac{DY}{Y_0} \times T 这意味着,新增发的 LP Token 数量与用户投入的代币数量占池中现有代币数量的比例,再乘以总 LP Token 数量相等。

2. Uniswap V2 合约实现流程 (时序图)

sequenceDiagram
    participant User as 用户
    participant Router as 路由合约
    participant Factory as 工厂合约
    participant Pair as 交易对合约

    User->>Router: 1. 调用 addLiquidity(tokenA, tokenB, ...)
    Router->>Factory: 2. 调用 getPair(tokenA, tokenB) 查询是否存在
  
    alt 交易对不存在
        Factory-->>Router: 3a. 返回 address(0)
        Router->>Factory: 4a. 调用 createPair(tokenA, tokenB)
        Note right of Factory: 内部使用 create2 创建
并对代币地址排序 Factory-->>Router: 5a. 返回新的 Pair 合约地址 else 交易对已存在 Factory-->>Router: 3b. 返回已存在的 Pair 合约地址 end Note over User, Pair: 用户需提前授权(approve)Router
转移其代币 Router->>Pair: 6. 调用 transferFrom(用户, Pair, DX) Router->>Pair: 7. 调用 transferFrom(用户, Pair, DY) Router->>Pair: 8. 调用 mint(用户地址) Note right of Pair: 内部计算应增发的 LPT 数量 (S)
并铸造新的 LPT Pair-->>User: 9. 转移 S 数量的 LPT 给用户
sequenceDiagram
    participant User as 用户
    participant Router as 路由合约
    participant Factory as 工厂合约
    participant Pair as 交易对合约

    User->>Router: 1. 调用 addLiquidity(tokenA, tokenB, ...)
    Router->>Factory: 2. 调用 getPair(tokenA, tokenB) 查询是否存在
  
    alt 交易对不存在
        Factory-->>Router: 3a. 返回 address(0)
        Router->>Factory: 4a. 调用 createPair(tokenA, tokenB)
        Note right of Factory: 内部使用 create2 创建
并对代币地址排序 Factory-->>Router: 5a. 返回新的 Pair 合约地址 else 交易对已存在 Factory-->>Router: 3b. 返回已存在的 Pair 合约地址 end Note over User, Pair: 用户需提前授权(approve)Router
转移其代币 Router->>Pair: 6. 调用 transferFrom(用户, Pair, DX) Router->>Pair: 7. 调用 transferFrom(用户, Pair, DY) Router->>Pair: 8. 调用 mint(用户地址) Note right of Pair: 内部计算应增发的 LPT 数量 (S)
并铸造新的 LPT Pair-->>User: 9. 转移 S 数量的 LPT 给用户
sequenceDiagram
    participant User as 用户
    participant Router as 路由合约
    participant Factory as 工厂合约
    participant Pair as 交易对合约

    User->>Router: 1. 调用 addLiquidity(tokenA, tokenB, ...)
    Router->>Factory: 2. 调用 getPair(tokenA, tokenB) 查询是否存在
  
    alt 交易对不存在
        Factory-->>Router: 3a. 返回 address(0)
        Router->>Factory: 4a. 调用 createPair(tokenA, tokenB)
        Note right of Factory: 内部使用 create2 创建
并对代币地址排序 Factory-->>Router: 5a. 返回新的 Pair 合约地址 else 交易对已存在 Factory-->>Router: 3b. 返回已存在的 Pair 合约地址 end Note over User, Pair: 用户需提前授权(approve)Router
转移其代币 Router->>Pair: 6. 调用 transferFrom(用户, Pair, DX) Router->>Pair: 7. 调用 transferFrom(用户, Pair, DY) Router->>Pair: 8. 调用 mint(用户地址) Note right of Pair: 内部计算应增发的 LPT 数量 (S)
并铸造新的 LPT Pair-->>User: 9. 转移 S 数量的 LPT 给用户

流程解释:

  1. 用户发起请求: 用户调用路由合约addLiquidity 方法。
  2. 查询交易对: 路由合约向工厂合约查询 tokenAtokenB 的交易对是否存在。
  3. 创建或获取交易对:
    • 如果不存在,工厂合约返回零地址,路由合约随即调用工厂合约的 createPair 方法来创建新的交易对合约,并获取其地址。
    • 如果已存在,工厂合约直接返回该交易对的地址。
  4. 转移代币: 在用户已授权的前提下,路由合约调用两种代币的 transferFrom 方法,将用户的 DXDY 数量的代币转移到交易对合约中。
  5. 铸造 LP Token: 路由合约调用交易对合约的 mint 方法,并将用户的地址作为接收方传入。
  6. 完成: 交易对合约内部完成 LPT 的计算和铸造,并将新生成的 LPT 发送给用户。

3. 代码实现细节与考量

  • LPT 计算的精度处理:
    • 在智能合约中,由于浮点数精度问题,DX/X0 * TDY/Y0 * T 可能会有微小误差。
    • 为确保公平和防止潜在攻击,合约会取两者之间的较小值作为最终增发的 LPT 数量: Scontract=min(amount0reserve0×totalSupply,amount1reserve1×totalSupply) S_{contract} = \min \left( \frac{\text{amount0}}{\text{reserve0}} \times \text{totalSupply}, \frac{\text{amount1}}{\text{reserve1}} \times \text{totalSupply} \right) 其中 amount0amount1 对应 DXDYreserve0reserve1 对应 X0Y0
  • minimum liquidity (最小流动性) 机制:
    • 当一个全新的交易对首次创建时(totalSupply 为零),Pair 合约会铸造 1000 个 LP Token 并发送到 0x0 地址(永久锁定)。
    • 案例: 首次添加流动性时,计算公式为 liquidity = sqrt(amount0 * amount1) - MINIMUM_LIQUIDITY
    • 目的/原因:
      1. 防止除零错误: 确保 totalSupply 始终大于零。
      2. 防止价格操纵: 提高对极低流动性池的初始操纵成本。
      3. 提高安全性: 防止某些极端情况下池子被完全抽干。
  • 代币地址排序:
    • Factory.createPair 中,tokenAtokenB 会根据其地址(视为16进制数字)大小进行排序,小的作为 token0,大的作为 token1
    • 案例: 无论用户输入 (DAI, WETH) 还是 (WETH, DAI) 来添加流动性,经过排序后,工厂合约都会指向同一个唯一的交易对地址,避免了重复创建或查找失败。
  • create2 的使用:
    • Factory.createPair 中使用 create2 而非普通的 create
    • 原因: 允许在交易被确认上链之前,就能确定新 Pair 合约的地址。这对于 addLiquidity 流程中,需要提前将代币 transferFrom 到 Pair 合约地址的场景至关重要。
  • initialize 方法与 constructor 的选择:
    • Pair 合约的 token0token1 地址不是在 constructor 中设置,而是在创建后通过 initialize 方法设置。
    • 原因: 这种设计简化了 create2 的使用(创建合约时无需传递参数),优先保证了 Pair 地址的快速获取,之后再进行初始化。

移除流动性 (Removing Liquidity)

1. 核心概念与数学原理

  • 定义: 用户将持有的 LP Token (S) 返还给流动性池,以换取相应比例的两种代币 (DX, DY)。
  • 逻辑: 与添加流动性相反,用户将凭证 (LPT) 还给池子,池子则返还用户等比例的两种代币。
  • 价格不变性约束: 移除流动性时,池中代币的价格比同样不能发生变化。
    • 约束条件:Y0/X0=(Y0DY)/(X0DX)Y_0 / X_0 = (Y_0 - DY) / (X_0 - DX)
  • 返还代币数量 (DX, DY) 的计算:
    • 基本原理: 用户收回的代币数量占池中总量的比例,等于其销毁的 LP Token 占总 LP Token 的比例。
    • 公式表达: L0L1L0=ST \frac{L_0 - L_1}{L_0} = \frac{S}{T} 其中:
      • L0L_0 是移除前的流动性, L1L_1 是移除后的流动性 (L0>L1L_0 > L_1)
      • SS (用户销毁的 LP Token 数量)
      • TT (LP Token 总量,即 totalSupply)
    • 推导简化结果: DX=ST×X0 DX = \frac{S}{T} \times X_0 DY=ST×Y0 DY = \frac{S}{T} \times Y_0 其中 X0X_0Y0Y_0 是当前池中的代币储备量 (reserve0reserve1)。

2. Uniswap V2 合约实现流程 (时序图)

sequenceDiagram
    participant User as 用户
    participant Router as 路由合约
    participant Pair as 交易对合约

    User->>Router: 1. 调用 removeLiquidity(tokenA, tokenB, amountLP)
    Note over User, Pair: 用户需提前授权(approve)Router
转移其 LP Token Router->>Pair: 2. 调用 transferFrom(用户, Pair, amountLP) Router->>Pair: 3. 调用 burn(用户地址) Note right of Pair: 内部计算应返还的 DX, DY 数量
并销毁合约持有的 LPT Pair-->>User: 4. 转移 DX 数量的 token0 Pair-->>User: 5. 转移 DY 数量的 token1
sequenceDiagram
    participant User as 用户
    participant Router as 路由合约
    participant Pair as 交易对合约

    User->>Router: 1. 调用 removeLiquidity(tokenA, tokenB, amountLP)
    Note over User, Pair: 用户需提前授权(approve)Router
转移其 LP Token Router->>Pair: 2. 调用 transferFrom(用户, Pair, amountLP) Router->>Pair: 3. 调用 burn(用户地址) Note right of Pair: 内部计算应返还的 DX, DY 数量
并销毁合约持有的 LPT Pair-->>User: 4. 转移 DX 数量的 token0 Pair-->>User: 5. 转移 DY 数量的 token1
sequenceDiagram
    participant User as 用户
    participant Router as 路由合约
    participant Pair as 交易对合约

    User->>Router: 1. 调用 removeLiquidity(tokenA, tokenB, amountLP)
    Note over User, Pair: 用户需提前授权(approve)Router
转移其 LP Token Router->>Pair: 2. 调用 transferFrom(用户, Pair, amountLP) Router->>Pair: 3. 调用 burn(用户地址) Note right of Pair: 内部计算应返还的 DX, DY 数量
并销毁合约持有的 LPT Pair-->>User: 4. 转移 DX 数量的 token0 Pair-->>User: 5. 转移 DY 数量的 token1

流程解释:

  1. 用户发起请求: 用户调用路由合约removeLiquidity 方法,并指定要移除的 LP Token 数量。
  2. 转移 LP Token: 在用户已授权的前提下,路由合约调用交易对合约的 transferFrom 方法,将用户指定数量的 LP Token 转移到交易对合约中。
  3. 销毁 LPT 并计算返还量: 路由合约调用交易对合约的 burn 方法,并将用户地址作为接收方传入。
  4. 返还代币: 交易对合约内部计算出应返还给用户的两种代币数量 DXDY,然后将这两笔代币转移给用户。

3. 代码实现细节与考量

  • burn 方法的参数:
    • Pair 合约的 burn 方法只接受一个 address to 参数,没有 amount 参数来指定销毁多少 LP Token。
    • 隐藏逻辑: burn 方法会默认销毁 Pair 合约自身所持有的所有 LP Token 余额 (balanceOf(address(this)))。这意味着在调用 burn 之前,用户必须已经将要销毁的 LP Token 转移到 Pair 合约中。
  • 代币计算:
    • amount0 (DX) 和 amount1 (DY) 的计算直接使用上述简化公式: amount0=liquiditytotalSupply×reserve0 \text{amount0} = \frac{\text{liquidity}}{\text{totalSupply}} \times \text{reserve0} amount1=liquiditytotalSupply×reserve1 \text{amount1} = \frac{\text{liquidity}}{\text{totalSupply}} \times \text{reserve1} 其中 liquidity 是 Pair 合约自身持有的、刚刚从用户那里接收的 LP Token 余额。