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

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

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

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

这一章讲怎么把稳定币存进 3pool、换取 3CRV(LP token)。核心是搞懂 失衡费(imbalance fee):为什么”不按比例存币”会被多扣一点?理解它,就理解了 Curve 如何防止有人用”存单币”来规避兑换手续费、薅老 LP 的羊毛。


目录


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

  1. 添加流动性 = 存稳定币换 3CRV;铸造量正比于不变量 D 的增长mint = totalSupply·(D2-D0)/D0
  2. 有三个 D:D0(存前)、D1(存后不扣费)、D2(存后扣失衡费);铸 LP 用 D2。
  3. 失衡费惩罚”不按比例存入”——单币存入等价于隐含了一笔该收费的兑换;费用留池归现有 LP。
  4. 失衡费 = fee·N/(4(N-1)) × “每种币偏离理想平衡余额的程度”;按比例存则各项偏离为 0、不收费。
  5. 首笔流动性要求三种币都存、不收失衡费、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

解题思路(自己实现)

  1. setUpdeal(DAI, address(this), 1e6 * 1e18);(100 万 DAI)并 dai.approve(pool, max)
  2. 构造 amounts = [1e6 * 1e18, 0, 0](只存 DAI)。
  3. 调用 pool.add_liquidity(amounts, 1);
  4. 断言 lp.balanceOf(address(this)) > 0(3CRV),打印铸到的数量。

进阶(强烈推荐,量化失衡费)

  1. 存入前先调 calc_token_amount([1e6*1e18,0,0], true),记下不含费的预估 LP
  2. 真正 add_liquidity 后,记下实际铸到的 LP
  3. 计算 (预估 - 实际)/预估,这个比例 ≈ 你这笔单币存入承担的失衡费率。
  4. 对比实验:把 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,收费),以及它们各自的数学。

💬 评论