Curve Cryptoswap 第5章 添加流动性:LP 铸造与失衡费

讲解如何用不变量 D 的增量决定 LP 铸造数量,以及为什么不按比例存入会被收取失衡费来保护现有 LP。

9 分钟阅读
Curve Cryptoswap 第5章 添加流动性:LP 铸造与失衡费

Curve Cryptoswap 第 5 章:添加流动性(Add Liquidity)

这一章讲怎么把币存进池子、换取 LP token(流动性凭证)。重点搞懂一个看似不公平、其实很合理的机制——失衡费(imbalance fee):为什么你”不按比例存币”会被多扣一点?理解它,就理解了 Curve 如何保护已有 LP 不被新人占便宜。


目录


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. 本章小结

  1. 添加流动性 = 存币换 LP token;铸造数量正比于不变量 D 的增长比例d_token = totalSupply·(new_D − old_D)/old_D
  2. 这保证按比例存入时 virtual_price 不变,老 LP 不被稀释。
  3. 首笔流动性特殊处理:d_token = xcp,并把 virtual_price 初始化为 1.0,不收失衡费。
  4. 失衡费惩罚”不按比例存入”——因为单币存入等价于隐含了一笔该收费的兑换;费用留在池中归现有 LP。
  5. 失衡费基于第 4 章同款动态 _fee,再按”各币偏离平均的程度”加权,体现为少铸 LP
  6. 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);
}

解题思路(自己实现)

  1. setUpdeal(USDC, address(this), 1e3 * 1e6);(1000 USDC,6 位小数)并 usdc.approve(pool, max)
  2. 构造 amounts = [1000e6, 0, 0](只存 USDC,coin0)。
  3. 调用:
    pool.add_liquidity({
        amounts: amounts, min_lp: 1,
        use_eth: false, receiver: address(this)
    });
  4. 断言 pool.balanceOf(address(this)) > 0,打印拿到的 LP 数量。

进阶(强烈推荐,体会失衡费)

设计一个对比实验:

  • 实验 A:只存 3000 USDC 价值的单币(如上)。
  • 实验 B:按池子当前比例,把这 3000 美元价值拆成 USDC/WBTC/WETH 三份分别存入(用 price_scalebalances 估算当前比例,再 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,要付失衡费),以及它们各自的数学。

💬 评论