Curve V1 第2章 数学基础:StableSwap 不变量与牛顿法求 D

拆解 Curve V1 (StableSwap) 的核心不变量方程,讲透参数 A 如何在恒定和与恒定乘积间插值,以及合约如何用牛顿迭代求解 D 和 y。

10 分钟阅读
Curve V1 第2章 数学基础:StableSwap 不变量与牛顿法求 D

Curve V1 第 2 章:数学基础(Math)

Curve V1(StableSwap)是专为锚定资产(如 DAI/USDC/USDT 都≈1 美元)设计的 AMM。它的全部魔法都浓缩在一条不变量方程里。这一章用通俗的语言把这条方程、参数 A 的作用,以及合约里靠”牛顿迭代法”求解 D 和 y 的过程彻底讲清楚。吃透这一章,后面的合约代码都只是它的工程实现。


目录


1. Curve V1 要解决什么问题

设想你要在 DAI 和 USDC 之间兑换。它们都应该≈1 美元,理想情况下 1 DAI 换 1 USDC。

  • 如果用 Uniswap(恒定乘积):哪怕换一大笔,价格也会明显偏离 1,产生不必要的滑点。对”本该 1:1”的稳定币来说,这种滑点是纯粹的浪费。
  • 如果用 恒定和(x+y=常数):能做到 1:1 零滑点,但一旦市场上 DAI 稍微脱锚,套利者会把池子里便宜的那种币全部买空

Curve V1 的目标

对锚定资产,在价格接近 1:1 时提供接近零滑点的兑换;但当价格严重失衡时,又能像 Uniswap 一样永不被掏空

它的做法就是把上面两条曲线”缝”在一起——这正是本章的主线。


2. 两条基础曲线的回顾

设池子里两种币数量为 xy

曲线公式滑点会被掏空吗
恒定和x + y = D零(价格恒为 1)
恒定乘积x · y = (D/2)²不会
  • 恒定和:图像是一条直线,斜率 -1,所以价格永远是 1,但一端可以被取到 0。
  • 恒定乘积:图像是一条双曲线,永远不碰坐标轴(x、y 都取不到 0),所以不会被掏空,但价格随比例剧烈变化。

Curve 想要:“在中间(x≈y)像直线一样平,在两端像双曲线一样翘起来。“


3. StableSwap 不变量:把两条曲线缝起来

Curve V1 的不变量方程(N 种币,3pool 里 N=3):

A · n^n · Σxᵢ  +  D  =  A · D · n^n  +  D^(n+1) / (n^n · Πxᵢ)

记号说明:

  • xᵢ:第 i 种币的余额(已统一到 18 位精度,见第 3 章 _xp)。
  • n:币种数量(3pool 是 3)。
  • Σxᵢ:所有币余额之和(恒定和的影子)。
  • Πxᵢ:所有币余额之积(恒定乘积的影子)。
  • A:放大系数(amplification coefficient)。
  • D:不变量,代表池子”完全平衡时的总价值”。

怎么理解这条方程”缝合了两条曲线”?看 A 的两个极端:

  • A → ∞:等式左边 A·n^n·Σxᵢ 和右边 A·D·n^n 占绝对主导,其它项可忽略,方程退化成 Σxᵢ = D恒定和(零滑点)。
  • A → 0:含 A 的项全消失,只剩 D = D^(n+1)/(n^n·Πxᵢ),整理后得 Πxᵢ = (D/n)^n恒定乘积(不被掏空)。

所以 A 就是那个”缝合旋钮”:A 越大越像恒定和(适合锚定资产),A 越小越像恒定乘积。Curve 给稳定币池设一个很大的 A(3pool 历史上 A≈2000),让它在 1:1 附近非常平。


4. 参数 A:放大系数(曲线有多”平”)

直觉图景:把不变量画成 x-y 曲线。

  • A 大 → 曲线在中间(x≈y)那一段非常平,几乎是斜率 -1 的直线 → 这一段内交易几乎零滑点。
  • A 小 → 曲线整体接近 Uniswap 双曲线 → 处处有明显滑点。

但注意:A 大不等于永远零滑点。一旦池子被推得很失衡(比如某种币占比远超其它),曲线就会”翘起来”,重新变得像双曲线,从而保护池子不被掏空。A 只是把”低滑点的甜区”做得很宽。


5. 案例:A 大和 A 小时报价的差别

设两币池,DAI 和 USDC 各 100 万(完全平衡,D≈200 万)。你要卖 10 万 DAI 换 USDC。

  • A 很大(如 2000):曲线在这一带极平,你大约能换到 ≈99,990 USDC(几乎 1:1,滑点仅约 0.01%)。
  • A 很小(≈0,退化成 Uniswap):按 x·y=k 计算,卖 10 万 DAI(x: 100万→110万),新 USDC = 100万·100万/110万 ≈ 909,090,换出 ≈ 90,909 USDC(滑点高达约 9%!)。

同样一笔交易,A 大的池子滑点 0.01%,A 小的池子滑点 9%。这就是 Curve 为稳定币兑换省下的钱。 这也解释了为什么稳定币交易者偏爱 Curve。


6. 不变量 D 是什么

D 可以这样理解:

D ≈ 池子完全平衡时的代币总量。 平衡时每种币各持有 D/n,总价值就是 D。

  • 100 万 DAI + 100 万 USDC + 100 万 USDT 的平衡池,D ≈ 300 万。
  • 交易不改变 D(这是”不变量”的含义)——交易只是沿着曲线移动,把一种币换成另一种。
  • 增/减流动性会改变 D(池子变大/变小)。
  • D 还用来衡量 LP 份额价值:virtual_price = D / LP总量(第 3 章)。

问题来了:给定各币余额 xᵢ,怎么从那条复杂方程里解出 D?方程里 D 的最高次幂是 D^(n+1)(3pool 是 D⁴),没有求根公式。于是要用数值方法——牛顿迭代法。


7. 为什么要用牛顿迭代法

牛顿法(Newton’s method)是一种”猜一个初值,然后反复修正、越来越接近真解”的数值方法。它适合解那些没有解析解的方程。

基本思想:

  1. 先猜一个 D 的初值(Curve 用 D = Σxᵢ 作起点,因为平衡时 D≈Σxᵢ)。
  2. 用一个递推公式把 D 修正得更准。
  3. 重复,直到两次结果几乎不变(差 ≤ 1),就认为收敛了。

Curve 的实现里最多迭代 255 次,但实际上通常几次就收敛(对稳定币池尤其快)。


8. 牛顿法求 D:合约公式拆解

合约 get_D 的核心循环(简化):

S = Σxᵢ                  # 所有余额之和
D = S                    # 初值
Ann = A · n              # 注意这里是 A·n(合约里 Ann = amp * N_COINS)
重复最多 255 次:
    D_P = D
    for each x in xp:
        D_P = D_P · D / (x · n)     # 这步迭代后 D_P ≈ D^(n+1)/(n^n·Πx)
    D = (Ann·S + D_P·n) · D / ((Ann-1)·D + (n+1D_P)
    if |D - D_prev| <= 1: break      # 收敛,退出

理解关键的两步:

  1. D_P 是什么:循环 D_P = D_P·D/(x·n) 跑完 n 次后,D_P = D^(n+1) / (n^n · Πxᵢ)。这正是不变量方程右边那个”恒定乘积项”。

  2. 更新公式 D = (Ann·S + D_P·n)·D / ((Ann-1)·D + (n+1)·D_P):这是把不变量方程用牛顿法整理后的迭代式。你不需要手推它,只要知道它每次都让 D 更接近真解。

实务上不必背公式,理解”D_P 代表乘积项、整个循环在数值逼近 D”即可。


9. 案例:手动迭代算一次 D

用一个小例子感受收敛。设 两币池(n=2),x = [100, 100](已平衡),A = 100,则 Ann = A·n = 200

  • S = 100 + 100 = 200
  • 初值 D = 200

第 1 次迭代:

D_P = D = 200
  乘 x[0]=100:  D_P = 200·200/(100·2) = 40000/200 = 200
  乘 x[1]=100:  D_P = 200·200/(100·2) = 200
(平衡时 D_P 正好 = D = 200)

D_new = (Ann·S + D_P·n)·D / ((Ann-1)·D + (n+1)·D_P)
      = (200·200 + 200·2)·200 / ((200-1)·200 + 3·200)
      = (40000 + 400)·200 / (39800 + 600)
      = 40400·200 / 40400
      = 200

D 一步就稳定在 200|200-200|=0 ≤ 1,收敛。这符合直觉:完全平衡的池子 D = Σxᵢ = 200。

如果池子不平衡(比如 x=[150,50]),第一次迭代 D 会偏离 200 一点,再迭代几次收敛到一个略小于 200 的值——因为失衡的池子”等效平衡价值”会略低。


10. 牛顿法求 y:swap 定价的核心

兑换时要回答:“我加进 dx 个币 i,为了让 D 不变,币 j 应该剩多少 y?” 这就是 get_y

它解的是固定 D、固定其它币、对 y 的方程。把不变量整理成关于 y 的二次方程

y² + (b - D)·y = c    →    y = (y² + c) / (2y + b - D)

其中(合约 get_y 里):

  • b = S' + D/AnnS' 是除 j 外其它币(含已加 dx 的 i)之和。
  • c = D^(n+1) / (n^n · Π其它币 · Ann)

同样用牛顿迭代 y = (y² + c)/(2y + b - D),初值 y = D,反复到收敛(差 ≤ 1)。

求出 y 后,换出量 dy = (旧的 xp[j] − y − 1)(−1 向下取整偏向池子),再换算回真实单位、扣手续费。这部分第 4 章详讲,这里只要知道:swap 的定价 = 用牛顿法解一个二次方程求 y。

对比记忆:get_D 解的是高次方程(求 D,初值 Σx);get_y 解的是二次方程(求某币余额,初值 D)。两者都用牛顿迭代,但 y 的方程简单得多(二次有明确递推)。


11. 本章小结

  1. Curve V1 为锚定资产设计,目标是平衡区近零滑点、失衡时不被掏空。
  2. 核心是 StableSwap 不变量A·n^n·Σxᵢ + D = A·D·n^n + D^(n+1)/(n^n·Πxᵢ),把恒定和与恒定乘积缝起来。
  3. 参数 A 是缝合旋钮:A→∞ 像恒定和(低滑点),A→0 像恒定乘积(不被掏空);A 越大低滑点甜区越宽。
  4. D 是不变量,≈池子平衡时的总币量;交易不改变 D。
  5. D 的方程是高次的、无解析解,用牛顿迭代 get_D 数值求解(初值 Σxᵢ,迭代到差≤1)。
  6. swap 定价用 get_y:固定 D 解关于某币的二次方程,同样牛顿迭代。

12. 动手练习

目标:先在脑子/脚本里把”牛顿法求 D”跑通,再到链上验证你的理解。

练习 1:用脚本实现 get_D

用 Python(或任意语言)实现上面第 8 节的 get_D 循环:

  1. 输入 xp = [1_000_000e18, 1_000_000e18, 1_000_000e18](3pool 三种币各 100 万,18 位精度),A = 2000n = 3
  2. 按公式迭代,打印每一轮的 D,观察它几轮收敛。
  3. 预期:平衡池 D ≈ 3,000,000e18。

练习 2:链上验证 D 与 A

主网分叉,针对 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);
}
  1. A(),打印(看看真实的 A 是多少)。
  2. balances(0/1/2)(DAI/USDC/USDT),把它们各自换算到 18 位精度(USDC/USDT 要乘 1e12),用你练习 1 的脚本算 D。
  3. get_virtual_price(),断言 >= 1e18(手续费让它只增不减)。
  4. (进阶)验证 D ≈ virtual_price · totalSupply / 1e18

进阶思考(选做)

  • 把池子改成严重失衡(如 xp=[2_800_000e18, 100_000e18, 100_000e18]),重跑 get_D,观察 D 比平衡时小,并思考为什么。
  • 把 A 从 2000 改成 1,重算同一笔 swap 的报价,量化滑点差异(呼应第 5 节)。

运行

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

下一章(第 3 章 合约总览)讲:A 怎么存储和缓慢调整(ramp_A)、_xp 怎么把不同精度的稳定币统一、get_virtual_pricecalc_token_amount 各自的用途。

💬 评论