04.Uniswap V2 添加移除流动性
目录
核心摘要 (Key Takeaways)
- 核心公式与流动性衡量: Uniswap V2 的流动性池基于 恒定乘积公式,其中 被用作衡量流动性大小的线性指标,便于 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),以增加池子的总流动性。
- 恒定乘积公式: 流动性池的核心机制是 ,其中 是一个常数。
- 流动性的衡量 (L):
- 为了实现 LP Token 的线性增发,Uniswap V2 使用 来衡量流动性,而非 。
- 与 的关系:。
- 案例: 这种开平方根的方式,使得 可以线性增长。无论用户提供流动性的时间早晚,只要对池子的贡献比例相同,获得的 LP Token 数量就相同。
- 价格不变性约束:
- 添加流动性时,池中代币的价格比不能发生变化。
- 初始价格比:
- 添加后价格比:
- 约束条件:。
- 几何表现: 在坐标系中,添加流动性的操作点会落在连接原点和当前价格点 的一条直线上。
- LP Token (LPT) 的获取:
- 用户投入
DX和DY两种代币。 - 用户获得
LP Token(也称 Share) 作为其流动性贡献的凭证,这些 LPT 后续可用于质押、流动性挖矿等。
- 用户投入
- LP Token 增发量 (S) 的计算:
- 基本原理: 新增发的 LP Token (S) 占增发后总量的比例,等于新增流动性占增发后总流动性的比例。
- 公式表达:
其中:
- (初始流动性)
- (添加后的流动性)
- (为本次操作新增发的 LP Token 数量)
- (初始 LP Token 总量,即
totalSupply)
- 推导简化结果: 结合价格不变性约束 ,可以推导出: 这意味着,新增发的 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 给用户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 给用户
流程解释:
- 用户发起请求: 用户调用路由合约的
addLiquidity方法。 - 查询交易对: 路由合约向工厂合约查询
tokenA和tokenB的交易对是否存在。 - 创建或获取交易对:
- 如果不存在,工厂合约返回零地址,路由合约随即调用工厂合约的
createPair方法来创建新的交易对合约,并获取其地址。 - 如果已存在,工厂合约直接返回该交易对的地址。
- 如果不存在,工厂合约返回零地址,路由合约随即调用工厂合约的
- 转移代币: 在用户已授权的前提下,路由合约调用两种代币的
transferFrom方法,将用户的DX和DY数量的代币转移到交易对合约中。 - 铸造 LP Token: 路由合约调用交易对合约的
mint方法,并将用户的地址作为接收方传入。 - 完成: 交易对合约内部完成 LPT 的计算和铸造,并将新生成的 LPT 发送给用户。
3. 代码实现细节与考量
- LPT 计算的精度处理:
- 在智能合约中,由于浮点数精度问题,
DX/X0 * T和DY/Y0 * T可能会有微小误差。 - 为确保公平和防止潜在攻击,合约会取两者之间的较小值作为最终增发的 LPT 数量:
其中
amount0和amount1对应DX和DY,reserve0和reserve1对应X0和Y0。
- 在智能合约中,由于浮点数精度问题,
minimum liquidity(最小流动性) 机制:- 当一个全新的交易对首次创建时(
totalSupply为零),Pair 合约会铸造 1000 个 LP Token 并发送到0x0地址(永久锁定)。 - 案例: 首次添加流动性时,计算公式为
liquidity = sqrt(amount0 * amount1) - MINIMUM_LIQUIDITY。 - 目的/原因:
- 防止除零错误: 确保
totalSupply始终大于零。 - 防止价格操纵: 提高对极低流动性池的初始操纵成本。
- 提高安全性: 防止某些极端情况下池子被完全抽干。
- 防止除零错误: 确保
- 当一个全新的交易对首次创建时(
- 代币地址排序:
- 在
Factory.createPair中,tokenA和tokenB会根据其地址(视为16进制数字)大小进行排序,小的作为token0,大的作为token1。 - 案例: 无论用户输入
(DAI, WETH)还是(WETH, DAI)来添加流动性,经过排序后,工厂合约都会指向同一个唯一的交易对地址,避免了重复创建或查找失败。
- 在
create2的使用:- 在
Factory.createPair中使用create2而非普通的create。 - 原因: 允许在交易被确认上链之前,就能确定新 Pair 合约的地址。这对于
addLiquidity流程中,需要提前将代币transferFrom到 Pair 合约地址的场景至关重要。
- 在
initialize方法与constructor的选择:- Pair 合约的
token0和token1地址不是在constructor中设置,而是在创建后通过initialize方法设置。 - 原因: 这种设计简化了
create2的使用(创建合约时无需传递参数),优先保证了 Pair 地址的快速获取,之后再进行初始化。
- Pair 合约的
移除流动性 (Removing Liquidity)
1. 核心概念与数学原理
- 定义: 用户将持有的 LP Token (S) 返还给流动性池,以换取相应比例的两种代币 (DX, DY)。
- 逻辑: 与添加流动性相反,用户将凭证 (LPT) 还给池子,池子则返还用户等比例的两种代币。
- 价格不变性约束: 移除流动性时,池中代币的价格比同样不能发生变化。
- 约束条件:。
- 返还代币数量 (DX, DY) 的计算:
- 基本原理: 用户收回的代币数量占池中总量的比例,等于其销毁的 LP Token 占总 LP Token 的比例。
- 公式表达:
其中:
- 是移除前的流动性, 是移除后的流动性 ()
- (用户销毁的 LP Token 数量)
- (LP Token 总量,即
totalSupply)
- 推导简化结果:
其中 和 是当前池中的代币储备量 (
reserve0和reserve1)。
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 数量的 token1sequenceDiagram
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
流程解释:
- 用户发起请求: 用户调用路由合约的
removeLiquidity方法,并指定要移除的 LP Token 数量。 - 转移 LP Token: 在用户已授权的前提下,路由合约调用交易对合约的
transferFrom方法,将用户指定数量的 LP Token 转移到交易对合约中。 - 销毁 LPT 并计算返还量: 路由合约调用交易对合约的
burn方法,并将用户地址作为接收方传入。 - 返还代币: 交易对合约内部计算出应返还给用户的两种代币数量
DX和DY,然后将这两笔代币转移给用户。
3. 代码实现细节与考量
burn方法的参数:- Pair 合约的
burn方法只接受一个address to参数,没有amount参数来指定销毁多少 LP Token。 - 隐藏逻辑:
burn方法会默认销毁 Pair 合约自身所持有的所有 LP Token 余额 (balanceOf(address(this)))。这意味着在调用burn之前,用户必须已经将要销毁的 LP Token 转移到 Pair 合约中。
- Pair 合约的
- 代币计算:
amount0(DX) 和amount1(DY) 的计算直接使用上述简化公式: 其中liquidity是 Pair 合约自身持有的、刚刚从用户那里接收的 LP Token 余额。