Curve V1 第3章 合约总览:A、D、_xp 与关键视图函数

梳理 StableSwap3Pool 合约结构:参数 A 如何缓慢 ramp,_xp 如何统一精度,get_virtual_price 与 calc_token_amount 各自的用途。

8 分钟阅读
Curve V1 第3章 合约总览:A、D、_xp 与关键视图函数

Curve V1 第 3 章:合约总览(Contract Overview)

第 2 章讲了数学。这一章把视角切到代码:以主网经典的 3pool(DAI/USDC/USDT) 为例,看参数 A 怎么存储和缓慢调整、_xp 怎么把不同精度的稳定币拉到同一把尺子、get_virtual_pricecalc_token_amount 分别干什么。建立起对合约骨架的整体认识。


目录


1. 3pool:最经典的 Curve V1 池子

本课程练习基于以太坊主网的 3pool

  • 池子地址:0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7
  • LP token(3CRV):0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490
  • 三种币(下标固定):
    • coin[0] = DAI(18 位小数)
    • coin[1] = USDC(6 位小数)
    • coin[2] = USDT(6 位小数)

注意它和 Curve V2 TriCrypto 的最大区别:3pool 里三种币都≈1 美元(锚定),所以不需要 price_scale、不需要重锚。这让 V1 比 V2 简单很多。


2. 用户视角:你会调用哪些函数

函数作用章节
exchange(i, j, dx, min_dy)用 dx 个 coin[i] 换 coin[j]第 4 章
get_dy(i, j, dx)报价(只读)第 4 章
add_liquidity(amounts, min_mint)存币铸 LP第 5 章
remove_liquidity(amount, min_amounts)按比例取回三种币第 6 章
remove_liquidity_imbalance(amounts, max_burn)按指定数量取回(要付失衡费)第 6 章
remove_liquidity_one_coin(amount, i, min)只取一种币(要付失衡费)第 6 章
A() / balances(i)读放大系数 / 余额本章
get_virtual_price()每份 LP 的价值本章
calc_token_amount(amounts, deposit)预估存/取会铸/burn 多少 LP本章

3. 参数 A 的存储与缓慢调整(ramp_A)

A 决定曲线形状(第 2 章)。管理员有时要调整 A,但不能瞬间跳变。合约用四个状态变量记录一次”渐变”:

  • initial_A + initial_A_time:起点 A 值和起点时间。
  • future_A + future_A_time:目标 A 值和目标时间。

读取”当前 A”时(_A()),如果还在渐变区间内,就按时间比例线性插值

def _A() -> uint256:
    t1 = self.future_A_time
    A1 = self.future_A
    if block.timestamp < t1:      # 还在 ramp 中
        A0 = self.initial_A
        t0 = self.initial_A_time
        if A1 > A0:               # 升 A
            return A0 + (A1 - A0)·(now - t0)/(t1 - t0)
        else:                     # 降 A
            return A0 - (A0 - A1)·(now - t0)/(t1 - t0)
    else:                         # ramp 结束
        return A1

相关管理函数:

  • ramp_A(future_A, future_time):发起一次渐变。
  • stop_ramp_A():立即停止渐变,把 A 钉在当前插值出的值。

4. 案例:A 在 ramp 过程中的线性插值

假设管理员要把 A 从 2000 升到 5000,ramp 持续 7 天(604800 秒)t0=0, t1=604800

第 0 天(now=0):

A = 2000 + (5000-2000)·0/604800 = 2000   ← 起点

第 3.5 天(now=302400,正好一半):

A = 2000 + (5000-2000)·302400/604800 = 2000 + 3000·0.5 = 3500   ← 中点

第 7 天(now≥604800):

A = 5000   ← 终点(ramp 结束,直接返回 future_A)

A 平滑地从 2000 爬到 5000,没有任何跳变。


5. 为什么必须缓慢 ramp 而不能瞬间改

如果 A 瞬间从 2000 跳到 5000:

  • 不变量 D 会瞬间改变(同样的余额,不同 A 算出的 D 不同)。
  • 池子的报价(get_y 的结果)会瞬间跳动
  • 套利者可以精准夹击这个跳变:在跳变前后各做一笔交易,无风险套走价差,损失由 LP 承担。

缓慢 ramp 让 D 平滑变化,任何瞬间的套利空间都极小,从而保护 LP。这和 Curve V2 里 A/gamma 要 ramp 是同一个道理。


6. _xp:把不同精度的稳定币统一

3pool 里 DAI 是 18 位小数,USDC/USDT 是 6 位小数。直接拿 balances 丢进不变量会出错(合约会以为 1 个 DAI = 1e12 个 USDC)。

_xp() 把所有余额统一到 18 位精度

def _xp() -> uint256[N_COINS]:
    result = RATES                        # 每种币的换算率
    for i in range(N_COINS):
        result[i] = result[i] · balances[i] / LENDING_PRECISION
    return result

其中 RATES 把每种币乘到统一精度:

  • DAI(18 位):乘数相当于 ×1(已经是 18 位)。
  • USDC(6 位):乘数 ×1e12(补齐到 18 位)。
  • USDT(6 位):乘数 ×1e12。

注意:因为 3pool 全是稳定币(都≈$1),_xp 只做精度对齐,不像 V2 那样还要乘 price_scale。这是 V1 比 V2 简单的又一处体现。


7. 案例:USDC 的 6 位小数怎么对齐

假设 balances = [DAI: 1,000,000e18, USDC: 1,000,000e6, USDT: 1,000,000e6]

经过 _xp

xp[0] = 1,000,000e18 · 1     = 1,000,000e18   (DAI 已是 18 位)
xp[1] = 1,000,000e6  · 1e12  = 1,000,000e18   (USDC 补 12 位)
xp[2] = 1,000,000e6  · 1e12  = 1,000,000e18   (USDT 补 12 位)

三者都变成 1,000,000e18,量级一致,可以平等地参与不变量计算。如果不做这步,xp[0] 会比 xp[1]、xp[2] 大 1e12 倍,不变量会严重失真。


8. get_virtual_price:LP 份额值多少

def get_virtual_price() -> uint256:
    D = get_D(_xp(), _A())
    token_supply = token.totalSupply()
    return D · PRECISION / token_supply     # = D / LP总量,放大 1e18

含义:每一份 LP(3CRV)当前值多少(以”虚拟美元”计)。

  • 池子初始时 virtual_price = 1e18(即 1.0)。
  • 之后随着交易手续费累积进池子,D 增长但 LP 总量不变,virtual_price 缓慢上升。
  • 它是衡量 LP 真实收益 的核心指标,且理论上只增不减(手续费收入),所以常被用作判断”无损”的基准。

9. calc_token_amount:预估铸/burn 多少 LP

def calc_token_amount(amounts, deposit) -> uint256:
    D0 = get_D_mem(balances, A)              # 操作前的 D
    # 把 amounts 加上(存)或减去(取)
    D1 = get_D_mem(new_balances, A)          # 操作后的 D
    diff = |D1 - D0|
    return diff · totalSupply / D0           # 按 D 变化比例估算 LP 数量

用途:前端/集成方在存款或取款前预估会铸造/销毁多少 LP

⚠️ 注意合约注释明确说:这是简化估算,不计手续费,只看滑点,“用来防抢跑,不用于精确计算”。也就是说,它给的是一个不含失衡费的近似值,真实 add_liquidity 拿到的 LP 会因失衡费而略少(第 5 章)。所以它适合做滑点保护参数(min_mint_amount)的参考,但别把它当成精确结果。


10. 状态变量地图

┌──────────── 曲线形状参数(靠 ramp 缓变) ─────────────┐
│  A  放大系数      → initial_A / future_A (+ 对应 time) │
└──────────────────────────────────────────────────────┘

┌──────────── 余额与规模 ───────────────────────────────┐
│  balances[i]   三种币真实余额(原始精度)              │
│  _xp()         统一到 18 位精度的余额(喂给不变量)    │
│  D             不变量(≈平衡时总币量,由 get_D 求)    │
│  token (3CRV)  LP token,totalSupply = LP 总量         │
│  virtual_price D / totalSupply,每份 LP 价值           │
└──────────────────────────────────────────────────────┘

┌──────────── 费用参数 ─────────────────────────────────┐
│  fee         交易手续费率(swap 时收)                 │
│  admin_fee   手续费里归协议的比例                      │
│  失衡费 = fee · N/(4(N-1)),加/减不平衡流动性时收     │
└──────────────────────────────────────────────────────┘

11. 本章小结

  1. 经典池是 3pool:coin0=DAI(18位)、coin1=USDC(6位)、coin2=USDT(6位),全锚定 → 无需 price_scale/重锚。
  2. A 的调整必须 ramp(线性插值缓变),防止 D 突变被套利;ramp_A/stop_ramp_A 管理。
  3. _xp 把不同精度的稳定币统一到 18 位(USDC/USDT ×1e12),是不变量计算的前置。
  4. get_virtual_price = D/LP总量,衡量每份 LP 价值,理论上只增不减。
  5. calc_token_amount 预估存/取的 LP 数量,但不含手续费、仅供防抢跑参考,不精确。

12. 动手练习

目标:读出并理解 3pool 的核心状态,建立对合约的直观。

练习:读取 3pool 状态

主网分叉,针对 3pool(0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7):

interface IStableSwap3Pool {
    function A() external view returns (uint256);
    function balances(uint256 i) external view returns (uint256);
    function get_virtual_price() external view returns (uint256);
    function calc_token_amount(uint256[3] memory amounts, bool deposit) external view returns (uint256);
}

任务:

  1. 打印 A()(看真实 A 值)。
  2. 打印 balances(0/1/2),注意 DAI 是 18 位、USDC/USDT 是 6 位(数值量级差 1e12)。
  3. 打印 get_virtual_price(),断言 >= 1e18
  4. 调用 calc_token_amount([1_000_000e18, 0, 0], true),预估”存 100 万 DAI 能铸多少 3CRV”,打印出来。
  5. (进阶)下一章你会真正 add_liquidity 存 100 万 DAI,到时把实际铸到的 LP 和这里的预估值对比——实际应略少,差额就是失衡费。

运行

forge test --evm-version cancun --fork-url $FORK_URL \
  --match-path test/CurveV1Overview.t.sol -vvv

下一章(第 4 章 Swap)进入第一个动作:exchange 怎么完成兑换、get_y 怎么定价、get_dy 怎么报价,以及 Curve V1 在输出币上收手续费的细节。

💬 评论