Curve Cryptoswap 第 5 章:添加流动性(Add Liquidity)
这一章讲怎么把币存进池子、换取 LP token(流动性凭证)。重点搞懂一个看似不公平、其实很合理的机制——失衡费(imbalance fee):为什么你”不按比例存币”会被多扣一点?理解它,就理解了 Curve 如何保护已有 LP 不被新人占便宜。
目录
- 1. 添加流动性,本质是什么
- 2. LP token 该铸造多少:用 D 的增量来衡量
- 3. add_liquidity 代码流程拆解
- 4. 第一笔流动性的特殊处理
- 5. 失衡费(imbalance fee):为什么不按比例存要被罚
- 6. calc_token_fee 怎么算失衡费
- 7. 案例:按比例存 vs 单币存的对比
- 8. 添加流动性后也会触发重锚和管理费
- 9. 本章小结
- 10. 动手练习
1. 添加流动性,本质是什么
你往池子里存入若干币,池子给你铸造一些 LP token,代表你对池子的”份额所有权”。将来你可以销毁 LP token 取回对应份额的资产(加上这段时间累积的手续费)。
关键问题是:存入这些币,应该给你铸造多少 LP token 才公平?
答案的核心思想:
LP token 数量应该正比于”你给池子增加了多少价值”。而 Curve 用不变量 D 的增长比例来衡量这个价值增量。
2. LP token 该铸造多少:用 D 的增量来衡量
回忆:D 是池子的”规模刻度”。你存入币之后,池子的 D 会从 old_D 增长到 new_D。LP 铸造公式(非首次):
d_token = totalSupply · new_D / old_D − totalSupply
= totalSupply · (new_D − old_D) / old_D
直觉解释:
- 如果你的存入让 D 增长了 10%(
new_D = 1.1 · old_D),那你就应该拿到相当于现有 LP 总量 10% 的新 LP。 - 这保证了每份 LP 代表的价值(
virtual_price = xcp/totalSupply)在”按比例存入”时保持不变——老 LP 不亏,新 LP 不占便宜。
合约里对应:
old_D = self.D # 存入前的 D
D = MATH.newton_D(A_gamma, xp_new) # 存入后的 D(用牛顿迭代求)
d_token = token_supply · D / old_D − token_supply
这里
newton_D就是数学库里”已知各币余额、求不变量 D”的函数(用牛顿迭代逼近)。和第 4 章get_y(已知 D 求某币余额)正好是反过来的。
3. add_liquidity 代码流程拆解
def add_liquidity(amounts, min_mint_amount, use_eth, receiver) -> uint256:
参数:amounts 是三种币各存多少(可以某些为 0),min_mint_amount 是 LP 滑点保护。
① 读取状态、把存入量加进余额
xp = self.balances
xp_old = xp # 备份旧余额
for i in range(N_COINS):
bal = xp[i] + amounts[i]
xp[i] = bal
self.balances[i] = bal # 写回新余额
② 把新旧余额都转换成统一刻度(第 2 章公式)
xp[0] *= precisions[0]; xp_old[0] *= precisions[0]
for i in 1..N:
xp[i] = xp[i] · price_scale[i-1] · precisions[i] / PRECISION
xp_old[i] = xp_old[i] · price_scale[i-1] · precisions[i] / PRECISION
③ 把币真正转进池子(transferFrom),同时记录每种币的刻度增量:
amountsp[i] = xp[i] - xp_old[i] # 第 i 种币贡献的"刻度价值增量"
④ 求新旧 D,算应铸 LP
old_D = self.D(或 ramp 中重新算)
D = newton_D(A_gamma, xp)
d_token = token_supply · D / old_D − token_supply
⑤ 扣失衡费(见第 5、6 节)
d_token_fee = _calc_token_fee(amountsp, xp) · d_token / 1e10 + 1
d_token -= d_token_fee
self.mint(receiver, d_token)
⑥ 滑点检查 + 触发重锚 + 收管理费
assert d_token >= min_mint_amount, "Slippage"
tweak_price(...) # 顺手 repeg
self._claim_admin_fees() # 顺手把累计的管理费分给协议
4. 第一笔流动性的特殊处理
池子第一次被注入流动性时(old_D == 0),没有”增长比例”可言,于是:
if old_D > 0:
d_token = token_supply · D / old_D − token_supply # 常规
else:
d_token = self.get_xcp(D) # 首次:直接用 xcp
self.virtual_price = 1e18 # 初始每份 LP 价值 = 1.0
self.xcp_profit = 1e18
self.xcp_profit_a = 1e18
含义:第一个 LP 拿到的 LP 数量 = 池子的 xcp(恒定乘积价值),并把 virtual_price 初始化为 1.0。这就是为什么”每份 LP 初始值 = 1”,之后只会因手续费而上涨。
首次添加不收失衡费(没有老 LP 需要保护),也不需要重锚。
5. 失衡费(imbalance fee):为什么不按比例存要被罚
这是本章最重要的概念。
设想池子现在是平衡的 USDC/WBTC/WETH 各占约 1/3 价值。现在:
- 小明按当前比例同时存入三种币 → 池子还是平衡的。
- 小红只存 USDC(一种币)→ 池子被推向失衡(USDC 占比变高)。
小红的”只存一种币”,经济上等价于”先按比例存入 + 顺便做了一笔把池子推向失衡的兑换”。而那笔隐含的兑换,本该付交易手续费!如果不收,小红就能用”存单币再马上按比例提走”的方式来规避兑换手续费,甚至薅老 LP 的羊毛。
所以 Curve 的规则是:
按比例存入 → 几乎不收费;越偏离比例(越让池子失衡)→ 收的失衡费越高。
这笔失衡费不是给协议的,而是留在池子里、归属于所有现有 LP,补偿他们因池子失衡而承担的风险。这跟第 4 章的动态交易费是同一个哲学:谁让池子变歪,谁付钱。
6. calc_token_fee 怎么算失衡费
合约用 _calc_token_fee(amountsp, xp):
# 基础费率 = _fee(xp) (和第 4 章兑换用的同一个动态费率函数)
# 然后乘以一个系数 N / (4(N-1))
fee = _fee(xp) · N_COINS / (4 · (N_COINS - 1))
# 再对"各币存入量偏离平均的程度"加权求和
S = sum(amountsp) # 总存入(刻度)
avg = S / N # 平均每币应存
对每种币 i:
diff = |amountsp[i] - avg| # 该币偏离平均的程度
把 diff 累积进一个加权和
d_token_fee ≈ fee · (加权偏离和 / S) ...
抓住直觉即可,不必记死公式:
- 如果你完全按比例存(每种币的刻度增量都等于平均值),那么每个
diff = 0,失衡费 ≈ 0。 - 如果你只存一种币,其它币的增量是 0、这种币的增量很大,
diff很大,失衡费就高。 - 失衡费的”基准费率”用的就是第 4 章那个动态
_fee,所以池子本身越失衡,失衡费的基准也越高。
最后这笔费以”少铸一点 LP token”的形式体现:
d_token_fee = _calc_token_fee(amountsp, xp) · d_token / 1e10 + 1
d_token -= d_token_fee # 你最终拿到的 LP 变少了
7. 案例:按比例存 vs 单币存的对比
设池子平衡,转换后余额 xp = [USDC 1,000,000, WBTC 1,000,000, WETH 1,000,000](美元刻度),totalSupply 对应 old_D。假设动态基础费率此刻 ≈ 0.04%(池子平衡)。
小明:按比例存入(各 +30,000 美元价值)
amountsp = [30,000, 30,000, 30,000]
avg = 90,000 / 3 = 30,000
每种币 diff = |30,000 - 30,000| = 0
→ 失衡费 ≈ 0
小明拿到的 LP ≈ 全额(只损失可忽略的取整)
小红:只存 USDC(+90,000 美元价值,单币)
amountsp = [90,000, 0, 0]
avg = 90,000 / 3 = 30,000
diff = [|90,000-30,000|, |0-30,000|, |0-30,000|] = [60,000, 30,000, 30,000]
偏离很大 → 失衡费按基础费率(0.04%)放大后,叠加偏离权重,可能达到
存入价值的几个 bp 到几十 bp(取决于偏离比例和池子状态)
小红拿到的 LP < 全额,差额就是失衡费,留给所有老 LP
结论:同样存入 90,000 美元的价值,小红(单币)拿到的 LP 比小明(按比例)少。这个差额正好补偿了小红把池子推向失衡所”隐含的那笔兑换”。
实务建议:如果你想低成本提供流动性,尽量按池子当前比例存入;如果你只有单一资产又想 LP,要预期到失衡费的存在。
8. 添加流动性后也会触发重锚和管理费
和 exchange 一样,add_liquidity 在非首次时:
- 调用
tweak_price(...)更新内部价、视情况重锚(第 7 章)。 - 调用
_claim_admin_fees()把累计的管理费(来自 xcp_profit 增长的一部分)以铸造 LP 的形式分给协议的 fee_receiver。
所以”增加流动性”这个动作也参与了池子的价格维护机制,不只是单纯存钱。
9. 本章小结
- 添加流动性 = 存币换 LP token;铸造数量正比于不变量 D 的增长比例:
d_token = totalSupply·(new_D − old_D)/old_D。 - 这保证按比例存入时 virtual_price 不变,老 LP 不被稀释。
- 首笔流动性特殊处理:
d_token = xcp,并把virtual_price初始化为 1.0,不收失衡费。 - 失衡费惩罚”不按比例存入”——因为单币存入等价于隐含了一笔该收费的兑换;费用留在池中归现有 LP。
- 失衡费基于第 4 章同款动态
_fee,再按”各币偏离平均的程度”加权,体现为少铸 LP。 add_liquidity结尾同样会tweak_price重锚并_claim_admin_fees。
10. 动手练习
对应课程的 Add Liquidity 练习:向真实 TriCrypto 池存入 1000 USDC。
练习目标
主网分叉环境,向 TriCrypto(0x7f86bf177dd4f3494b841a37e810a34dd56c829b)单独存入 1,000 USDC,铸造 LP token,并验证拿到了 LP。
接口
interface ITriCrypto {
function add_liquidity(
uint256[3] memory amounts, uint256 min_lp,
bool use_eth, address receiver
) external payable returns (uint256);
function balanceOf(address) external view returns (uint256);
}
解题思路(自己实现)
setUp:deal(USDC, address(this), 1e3 * 1e6);(1000 USDC,6 位小数)并usdc.approve(pool, max)。- 构造
amounts = [1000e6, 0, 0](只存 USDC,coin0)。 - 调用:
pool.add_liquidity({ amounts: amounts, min_lp: 1, use_eth: false, receiver: address(this) }); - 断言
pool.balanceOf(address(this)) > 0,打印拿到的 LP 数量。
进阶(强烈推荐,体会失衡费)
设计一个对比实验:
- 实验 A:只存 3000 USDC 价值的单币(如上)。
- 实验 B:按池子当前比例,把这 3000 美元价值拆成 USDC/WBTC/WETH 三份分别存入(用
price_scale和balances估算当前比例,再deal对应数量)。 - 比较两次拿到的 LP「相对存入价值」的比率,B 应当略高于 A——差额就是失衡费。
- 也可以用 Views 合约的
calc_token_amount(amounts, true)预估 LP,再和实际对比。
提示:要精确按比例并不容易(价格会变),近似即可,目的是观察到失衡费的存在与方向。
运行
forge test --evm-version cancun --fork-url $FORK_URL \
--match-path test/CurveV2AddLiquidity.t.sol -vvv
下一章(第 6 章 Remove Liquidity)讲两种取回流动性的方式:按比例全取(remove_liquidity,无失衡费)和只取一种币(remove_liquidity_one_coin,要付失衡费),以及它们各自的数学。