Curve V1 第 5 章:添加流动性(Add Liquidity)
这一章讲怎么把稳定币存进 3pool、换取 3CRV(LP token)。核心是搞懂 失衡费(imbalance fee):为什么”不按比例存币”会被多扣一点?理解它,就理解了 Curve 如何防止有人用”存单币”来规避兑换手续费、薅老 LP 的羊毛。
目录
- 1. 添加流动性的本质
- 2. 用 D 的增量决定铸多少 LP
- 3. add_liquidity 代码流程拆解
- 4. 失衡费:为什么不按比例存要被罚
- 5. 失衡费的计算公式
- 6. 案例:按比例存 vs 单币存的对比
- 7. 第一笔流动性的特殊处理
- 8. 本章小结
- 9. 动手练习
1. 添加流动性的本质
你往池子存入若干稳定币,池子给你铸造 3CRV(LP token),代表你的份额。将来可销毁 3CRV 取回对应份额的资产 + 累积的手续费。
核心问题:存这些币,应该铸多少 LP 才公平?
答案思想(和 V2 一致):
LP 数量正比于”你给池子增加了多少价值”,而 Curve 用不变量 D 的增长来衡量这个价值增量。
2. 用 D 的增量决定铸多少 LP
非首次添加的铸造公式:
mint_amount = totalSupply · (D2 - D0) / D0
D0:存入前的不变量。D2:存入后、扣掉失衡费的不变量。- 直觉:你的存入让 D 增长了多少百分比,就铸给你相当于现有 LP 总量同样百分比的新 LP。
这保证了”按比例存入时 virtual_price 不变”,老 LP 不被稀释。
注意这里有三个 D,是理解失衡费的关键:
D0:存入前。D1:存入后(不扣费,按你实际存入的余额算)。D2:存入后(扣掉失衡费,把每种币因失衡产生的 fee 减掉后再算)。
铸 LP 用的是 D2(扣费后),所以你存得越失衡、D2 越小于 D1、铸到的 LP 越少。
3. add_liquidity 代码流程拆解
def add_liquidity(amounts, min_mint_amount):
_fee = self.fee · N_COINS / (4 · (N_COINS - 1)) # 失衡费基准费率
D0 = get_D_mem(old_balances, A) # 存入前 D
# ① 把 amounts 转进池子,得到 new_balances
for i: transferFrom(coins[i], ...); new_balances[i] = old_balances[i] + amount
D1 = get_D_mem(new_balances, A) # 不扣费的存入后 D
assert D1 > D0
# ② 对每种币算失衡费(与"理想平衡比例"的偏离)
D2 = D1
if token_supply > 0: # 非首次
for i in range(N_COINS):
ideal_balance = D1 · old_balances[i] / D0 # 该币"理应"达到的余额
difference = |ideal_balance - new_balances[i]| # 实际偏离理想多少
fees[i] = _fee · difference / FEE_DENOMINATOR
self.balances[i] = new_balances[i] - fees[i]·admin_fee/...
new_balances[i] -= fees[i] # 扣掉失衡费
D2 = get_D_mem(new_balances, A) # 扣费后 D
# ③ 算铸造量
mint_amount = token_supply · (D2 - D0) / D0
assert mint_amount >= min_mint_amount, "Slippage screwed you"
self.token.mint(msg.sender, mint_amount)
关键在 ② 里的 ideal_balance:它是”如果你严格按池子当前比例存入,第 i 种币本应是多少”。你实际存的 new_balances[i] 偏离这个理想值越多,difference 越大,失衡费越高。
4. 失衡费:为什么不按比例存要被罚
设 3pool 现在三种币各占约 1/3。
- 小明按比例存:DAI/USDC/USDT 各存一点,池子还是 1:1:1 → 不破坏平衡。
- 小红只存 DAI:池子被推向”DAI 多、USDC/USDT 少”的失衡。
小红的”只存 DAI”,经济上等价于”按比例存入 + 顺便把一部分 DAI 换成了 USDC/USDT”。而那笔隐含的兑换本该付手续费!如果不收,小红就能用”存单币 → 马上按比例取走”来规避 swap 手续费,甚至套老 LP 的利。
所以规则是:
越偏离池子当前比例存入 → 失衡费越高;完全按比例存入 → 几乎不收费。
这笔失衡费留在池子里(扣除 admin_fee 部分后),归现有 LP,补偿他们因池子失衡承担的风险。和第 4 章 swap 手续费同一个哲学:谁把池子搞歪,谁付钱。
5. 失衡费的计算公式
基准费率:
_fee = fee · N / (4 · (N - 1))
对 3pool(N=3):_fee = fee · 3/8。这个 N/(4(N-1)) 系数是 Curve 的经验设计,让多币池的失衡费保持合理。
每种币的失衡费:
ideal_balance[i] = D1 · old_balances[i] / D0 # 按比例本应达到的余额
difference[i] = |ideal_balance[i] - new_balances[i]|
fees[i] = _fee · difference[i] / FEE_DENOMINATOR
- 完全按比例存:每种币的
new_balances[i]恰好等于ideal_balance[i],所以所有difference = 0,失衡费 = 0。 - 只存一种币:那种币远超理想、其它币低于理想,
difference都很大,失衡费就高。
扣费体现为 D2 < D1,最终铸到的 LP 变少。
6. 案例:按比例存 vs 单币存的对比
设 3pool 平衡,old_balances ≈ [DAI 100万, USDC 100万, USDT 100万](统一精度),D0 ≈ 300万。fee = 0.0001,则 _fee = 0.0001·3/8 = 0.0000375。
小明:按比例各存 10 万价值(共 30 万)
new_balances ≈ [110万, 110万, 110万],D1 ≈ 330万
ideal_balance[i] = 330万 · 100万 / 300万 = 110万
difference[i] = |110万 - 110万| = 0 (每种币都为 0)
→ 失衡费 ≈ 0,小明几乎拿到全额 LP
小红:只存 30 万 DAI(单币)
new_balances ≈ [130万, 100万, 100万],D1 ≈ 329.x万(略小于330万,因失衡)
ideal_balance[0] = 329万·100万/300万 ≈ 109.7万
difference[0] = |109.7万 - 130万| ≈ 20.3万 ← DAI 远超理想
difference[1] = |109.7万 - 100万| ≈ 9.7万 ← USDC 低于理想
difference[2] ≈ 9.7万 ← USDT 低于理想
fees = 0.0000375 · [20.3万, 9.7万, 9.7万] ≈ [7.6, 3.6, 3.6](统一精度单位)
→ 这些费让 D2 < D1,小红铸到的 LP < 全额
结论:同样投入 30 万价值,小红(单币)拿到的 3CRV 比小明(按比例)少,差额就是失衡费。
实务建议:想低成本做 LP,尽量按池子当前比例存;只有单一币种又想 LP,要预期到失衡费。可先用
calc_token_amount(第 3 章,不含费的估算)和实际铸量对比,量化这笔费。
7. 第一笔流动性的特殊处理
池子第一次注入流动性时(token_supply == 0):
if token_supply == 0:
assert in_amount > 0 # 首存必须三种币都 > 0
mint_amount = D1 # 直接用 D1 作为铸造量("take the dust")
self.balances = new_balances
- 首存要求三种币都存(
in_amount > 0),以建立初始平衡。 - 铸造量直接 = D1,使
virtual_price初始 ≈ 1.0。 - 首存不收失衡费(没有老 LP 需要保护)。
8. 本章小结
- 添加流动性 = 存稳定币换 3CRV;铸造量正比于不变量 D 的增长:
mint = totalSupply·(D2-D0)/D0。 - 有三个 D:
D0(存前)、D1(存后不扣费)、D2(存后扣失衡费);铸 LP 用 D2。 - 失衡费惩罚”不按比例存入”——单币存入等价于隐含了一笔该收费的兑换;费用留池归现有 LP。
- 失衡费 =
fee·N/(4(N-1))× “每种币偏离理想平衡余额的程度”;按比例存则各项偏离为 0、不收费。 - 首笔流动性要求三种币都存、不收失衡费、
virtual_price初始化为 1.0。
9. 动手练习
对应课程的 Add Liquidity 练习:向 3pool 单独存入 100 万 DAI。
练习目标
主网分叉,向 3pool(0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7)单独存入 100 万 DAI,铸造 3CRV,并验证拿到了 LP。
接口
interface IStableSwap3Pool {
function add_liquidity(uint256[3] memory amounts, uint256 min_mint_amount) external;
}
// 3CRV LP token: 0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490
解题思路(自己实现)
setUp:deal(DAI, address(this), 1e6 * 1e18);(100 万 DAI)并dai.approve(pool, max)。- 构造
amounts = [1e6 * 1e18, 0, 0](只存 DAI)。 - 调用
pool.add_liquidity(amounts, 1); - 断言
lp.balanceOf(address(this)) > 0(3CRV),打印铸到的数量。
进阶(强烈推荐,量化失衡费)
- 存入前先调
calc_token_amount([1e6*1e18,0,0], true),记下不含费的预估 LP。 - 真正
add_liquidity后,记下实际铸到的 LP。 - 计算
(预估 - 实际)/预估,这个比例 ≈ 你这笔单币存入承担的失衡费率。 - 对比实验:把 100 万拆成 DAI/USDC/USDT 各约 33 万(USDC/USDT 用 6 位小数,即
333_333e6)按比例存入,重复上面对比,失衡费应明显更小。
运行
forge test --evm-version cancun --fork-url $FORK_URL \
--match-path test/CurveV1AddLiquidity.t.sol -vvv
下一章(第 6 章 Remove Liquidity)讲三种取回方式:按比例(remove_liquidity,免费)、按指定数量(remove_liquidity_imbalance,收费)、只取一种币(remove_liquidity_one_coin,收费),以及它们各自的数学。