Uniswap V2 第 4 章:添加流动性(Add Liquidity)
这一章讲怎么把两种代币存进池子换取 LP token。重点搞懂三个数学点:首次铸造为什么用几何平均 √(x·y)、后续铸造为什么取两种比例的最小值、以及为什么要永久锁定一小撮 MINIMUM_LIQUIDITY。
目录
- 1. LP token 是什么
- 2. addLiquidity:先把两种币配平
- 3. 首次铸造:几何平均 √(x·y)
- 4. 为什么用几何平均而不是别的
- 5. 后续铸造:取最小值,防白嫖
- 6. 案例:两次添加流动性算 LP
- 7. 最小流动性锁定(MINIMUM_LIQUIDITY)
- 8. 本章小结
- 9. 动手练习
1. LP token 是什么
你向池子存入两种代币,池子(Pair,本身是个 ERC20)给你铸造 LP token,代表你在池子里的份额。
- 持有 LP token = 拥有池子里一定比例的两种代币 + 累积的交易手续费。
- 销毁 LP token(burn)= 按比例取回两种代币(第 5 章)。
- 因为手续费留在池中让储备增长,每个 LP token 对应的资产会随时间缓慢变多。
2. addLiquidity:先把两种币配平
你想存 DAI 和 WETH,但池子有固定的当前比例(比如 3000 DAI : 1 WETH)。如果你存的比例不对,Router 的 addLiquidity 会自动帮你按当前比例取舍:
你给出 amountADesired, amountBDesired(期望存入量)和 amountAMin, amountBMin(下限)
Router 计算:
- 若按 A 全额存,需要的 B = quote(amountADesired)
- 若这个 B ≤ amountBDesired,就用 (amountADesired, B)
- 否则反过来,按 B 全额存,算需要的 A
最终存入的是"按当前比例配平后"的一组数额
quote(amountA) = amountA · reserveB / reserveA 就是按当前储备比例换算。所以多出来的那种币不会被存入(留在你手上),保证你存入时不改变池子比例。
3. 首次铸造:几何平均 √(x·y)
池子第一次被注入流动性时(totalSupply == 0),没有”现有比例”可参考,于是 pair.mint 用:
liquidity = √(amount0 · amount1) − MINIMUM_LIQUIDITY
即存入两种币数量的几何平均数(再减去锁定的最小流动性,见第 7 节)。
4. 为什么用几何平均而不是别的
几何平均 √(x·y) 有一个关键性质:它对”代币单位的选择”不敏感,且正比于恒定乘积的”流动性深度”。
- 回忆
k = x·y,那么√k = √(x·y)正好衡量池子的”规模”。把 LP 数量定为√(x·y),意味着 LP 数量正比于 √k。 - 好处:当价格不变、只是流动性翻倍时(x、y 同时 ×2),
√(x·y)也正好 ×2,LP 数量翻倍——份额与投入成正比,公平。 - 如果用算术平均或单种币数量,会受代币精度/价格影响,不同池子无法统一衡量。
一句话:几何平均让 “LP 数量 ∝ √k ∝ 池子深度”,是衡量流动性最自然的尺子。
5. 后续铸造:取最小值,防白嫖
池子已有流动性时(totalSupply > 0),按你存入的两种币各自占储备的比例来铸:
liquidity = min(
amount0 · totalSupply / reserve0,
amount1 · totalSupply / reserve1
)
为什么取最小值(min)? 防止有人”存得不成比例”还想多拿 LP。
- 如果你严格按当前比例存,两个式子算出来一样,min 不影响。
- 如果你某一种币存多了(不成比例),多存的那种币不会让你多拿 LP——min 会以”存得少的那种”为准。多存的部分相当于白送给池子(送给所有 LP)。
这逼着大家按比例存,否则吃亏。Router 的配平(第 2 节)正是为了帮你避免这种亏损。
6. 案例:两次添加流动性算 LP
首次:小明建池,存 100 WETH + 300,000 DAI
liquidity = √(100 · 300,000) − MINIMUM_LIQUIDITY
= √30,000,000 − 1000 (单位简化)
≈ 5,477.2 − 0.001 ≈ 5,477.2 LP(忽略锁定的微量)
totalSupply ≈ 5,477.2
后续:小红按比例存 10 WETH + 30,000 DAI(正好 1/10 比例)
按 WETH:10 · 5477.2 / 100 = 547.72
按 DAI :30,000 · 5477.2 / 300,000 = 547.72
liquidity = min(547.72, 547.72) = 547.72 LP
小红存了池子规模的 10%,正好拿到约 10% 的 LP(547.72 / (5477.2+547.72) ≈ 9.1%,因为池子在她存入后变大了)。
如果小红不按比例:存 10 WETH + 40,000 DAI
按 WETH:10 · 5477.2 / 100 = 547.72
按 DAI :40,000 · 5477.2 / 300,000 = 730.3
liquidity = min(547.72, 730.3) = 547.72 LP
她多存了 10,000 DAI,却还是只拿 547.72 LP——多存的 DAI 白白送给了池子。所以一定要按比例存(用 Router 自动配平)。
7. 最小流动性锁定(MINIMUM_LIQUIDITY)
首次铸造时减去并永久锁定 MINIMUM_LIQUIDITY = 1000(铸给零地址,谁都取不回)。
为什么?防一种攻击:
- 如果不锁,攻击者可以先存极少量建池、拿到极少 LP,再直接给池子转入大量代币,把单个 LP token 的价值抬到极高,导致后来者因取整(除法向下取整)几乎拿不到 LP,被”挤出”。
- 锁定 1000 个最小单位 LP,让池子的 LP 总量永远有一个不可忽略的基数,使上述”通胀单份价值”的攻击在经济上不划算。
代价只是首个 LP 损失价值约 1000 个最小单位的份额,几乎可忽略,却堵住了一类攻击。
8. 本章小结
- 存两种币换 LP token(Pair 本身是 ERC20);持有 LP = 池子份额 + 累积手续费。
- Router 的
addLiquidity会按当前储备比例自动配平,多余的币不存入,避免你吃亏。 - 首次铸造
liquidity = √(amount0·amount1) − MINIMUM_LIQUIDITY,几何平均让 LP ∝ √k ∝ 池子深度。 - 后续铸造
liquidity = min(amount0·ts/reserve0, amount1·ts/reserve1),取最小值逼你按比例存。 - MINIMUM_LIQUIDITY=1000 永久锁定,防”通胀单份 LP 价值”的挤出攻击。
9. 动手练习
对应课程的 Add Liquidity 练习:向 DAI/WETH 池添加流动性。
练习:addLiquidity
主网分叉,Router = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D:
interface IUniswapV2Router02 {
function addLiquidity(
address tokenA, address tokenB,
uint256 amountADesired, uint256 amountBDesired,
uint256 amountAMin, uint256 amountBMin,
address to, uint256 deadline
) external returns (uint256 amountA, uint256 amountB, uint256 liquidity);
}
思路:
setUp:给 user 充值 100 WETH(weth.deposit)和 100 万 DAI(deal),都approve给 Router。- 调用
addLiquidity(DAI, WETH, 1_000_000e18, 100e18, 1, 1, user, block.timestamp)。 - 打印返回的
amountA(实际存入 DAI)、amountB(实际存入 WETH)、liquidity(铸到的 LP)。 - 观察配平:因为池子有固定比例,
amountA/amountB通常不会正好是你给的 100 万/100——有一种会被按比例缩减,多的留在你手上。打印你的 DAI/WETH 剩余余额验证。 - 断言
pair.balanceOf(user) > 0。
进阶
- 故意传一个严重偏离当前比例的
amountADesired/amountBDesired,观察 Router 如何配平、哪种币没被全额存入。 - 用
getReserves算出当前比例,预测会存入多少,再和实际对比。
运行
forge test --evm-version cancun --fork-url $FORK_URL \
--match-path test/UniswapV2Liquidity.t.sol -vvv
下一章(第 5 章 Remove Liquidity)讲:销毁 LP 怎么按比例取回两种币,以及 burn 的数学。