Curve Cryptoswap 第 2 章:数学基础(Math)
这一章是整门课的地基。Curve V2(Cryptoswap)之所以比普通 AMM 复杂,根源全在它的数学设计上。这一章我们用尽量通俗的语言,把「不变量公式、gamma、价格刻度 price_scale、流动性集中、重锚(repeg)、池子价值」这几个核心概念彻底讲清楚。看懂了这一章,后面所有合约代码都只是在「用代码实现这些数学」。
目录
- 1. 先搞清楚:Curve V2 到底想解决什么问题
- 2. 两种经典 AMM 的优缺点回顾
- 3. Curve V2 的核心思路:把两种曲线”缝”在一起
- 4. 不变量公式逐项拆解
- 5. K0 与 gamma:控制”集中程度”的两个旋钮
- 6. 案例:用具体数字算一遍 K 的变化
- 7. price_scale(价格刻度):把不同价格的币”拉到同一把尺子上”
- 8. 转换后余额(transformed balances)
- 9. 案例:把真实的 USDC/WBTC/WETH 余额转换一遍
- 10. 重锚(Repeg):让流动性自动跟着市场价走
- 11. 池子价值与 xcp、virtual_price
- 12. 重锚的代价:repegging loss(重锚损失)
- 13. 本章小结
- 14. 动手练习
1. 先搞清楚:Curve V2 到底想解决什么问题
Curve V1(Stableswap)是为锚定资产设计的,比如 USDC / USDT / DAI,它们的价格应该都≈1 美元。V1 假设”这几个币价格相等”,于是把流动性高度集中在 1:1 附近,做到极低滑点。
但现实里很多资产不锚定:ETH、BTC、各种波动币。它们之间没有固定汇率,而且价格会大幅变动。如果直接套用 V1,一旦价格偏离 1:1,池子就会被掏空、报价严重失真。
Curve V2 要解决的问题就是:
如何为「价格会持续变动、彼此不锚定」的波动资产,提供像 Stableswap 那样的低滑点交易,同时不需要人工去调整参数?
它的答案分两步:
- 流动性集中:把大部分流动性集中在「当前市场价」附近(而不是固定的 1:1)。
- 自动重锚(repeg):当市场价漂移时,让池子内部记录的”当前价”自动跟着移动,从而把流动性重新集中到新价格附近。
这两点就是整门课反复出现的主线。
2. 两种经典 AMM 的优缺点回顾
为了理解 V2,先快速回顾两条最基础的曲线。设池子里两种币的数量为 x 和 y。
2.1 恒定和(Constant Sum):x + y = D
- 含义:不管怎么换,两种币数量之和不变。
- 优点:零滑点,1 个 A 永远换 1 个 B。
- 致命缺点:价格被死死固定为 1。一旦外部价格不是 1,套利者会把便宜的那种币全部买空,池子直接被掏干。
2.2 恒定乘积(Constant Product):x · y = L²(Uniswap)
- 含义:两种币数量之积不变。
- 优点:永远不会被掏空(数量趋近 0 时价格趋近无穷,价格能适应任意比例)。
- 缺点:滑点大。哪怕只是小额交易,价格也会移动;大额交易滑点非常恐怖。
2.3 一句话对比
| 曲线 | 公式 | 滑点 | 会不会被掏空 | 适合谁 |
|---|---|---|---|---|
| 恒定和 | x + y = D | 零 | 会 | 完美锚定资产(理想情况) |
| 恒定乘积 | x · y = L² | 大 | 不会 | 任意资产,但效率低 |
| Stableswap(V1) | 两者混合 | 锚定附近极低 | 不会 | 锚定资产 |
| Cryptoswap(V2) | 两者混合 + 可移动的中心 | 当前价附近极低 | 不会 | 波动资产 |
3. Curve V2 的核心思路:把两种曲线”缝”在一起
Curve 的核心智慧是:在”价格接近中心”时表现得像恒定和(低滑点),在”价格远离中心”时表现得像恒定乘积(不被掏空)。
V1 已经做到了这一点,但 V1 的”中心”固定在 1:1。
V2 在 V1 基础上加了两样新东西:
- 一个可以移动的”中心价” —— 叫
price_scale(价格刻度)。V1 的中心永远是 1,V2 的中心可以是 60000(比如 1 BTC = 60000 USDC)。 - 一个更陡峭的过渡控制器 —— 叫
gamma。它控制”低滑点区间”有多窄。区间越窄,流动性越集中,资本效率越高,但越容易在价格波动时触发重锚。
直觉图景:想象一条”碗”形的流动性曲线。V1 的碗底固定在 1:1。V2 的碗可以整体平移到任意价格(靠 price_scale),而且碗壁可以做得更陡更窄(靠 gamma),把钱集中在碗底那一小块区域。市场价一旦走远,整个碗会被”搬”过去(repeg)。
4. 不变量公式逐项拆解
Curve V2 的不变量(invariant)写出来是这样的(N 是币种数量,三币池 N=3):
K · D^(N-1) · (x₀ + x₁ + ... + x_{N-1}) + x₀·x₁·...·x_{N-1} = K · D^N + (D/N)^N
别被它吓到,我们一项一项看。
- xᵢ:第 i 种币经过 price_scale 转换后的余额(后面第 7、8 节细讲,先理解成”统一单位后的余额”)。
- D:不变量,可以理解成”池子的总规模/总价值刻度”。所有币都平衡时,每种币大约持有
D/N。 - (D/N)^N 和 x₀·x₁·…·x_{N-1}:这就是恒定乘积的部分(积相关项)。
- K · D^(N-1) · Σxᵢ 和 K · D^N:这就是恒定和的部分(和相关项),前面乘了一个放大系数 K。
关键在 K。 K 就是那个”调节旋钮”:
- 当 K 非常大 → 和相关项占主导 → 曲线表现像恒定和 → 低滑点。
- 当 K 趋近 0 → 和相关项消失,只剩
x₀·x₁·…·x_{N-1} = (D/N)^N→ 曲线表现像恒定乘积 → 不会被掏空。
所以”把两条曲线缝在一起”这件事,本质就是让 K 随着池子是否平衡而自动变化。这就引出了 K0 和 gamma。
5. K0 与 gamma:控制”集中程度”的两个旋钮
5.1 K0:衡量”池子有多平衡”
K0 = (x₀ · x₁ · ... · x_{N-1}) · N^N / D^N
- 当池子完全平衡(每种币都恰好是
D/N)时,分子 =(D/N)^N · N^N = D^N,所以 K0 = 1。 - 当池子越来越不平衡(某种币快被换光),乘积变小,K0 → 0。
所以记住:K0 ∈ [0, 1],1 = 完全平衡,0 = 极度失衡。
5.2 gamma 与放大系数 K
K = A · K0 · ( gamma / (gamma + 1 - K0) )²
其中 A 是放大系数(amplification,跟 V1 一样,越大越”像恒定和”)。重点看后面那个分式 gamma / (gamma + 1 - K0):
- 池子平衡时 K0 = 1 → 分母 =
gamma + 1 - 1 = gamma→ 分式 =gamma/gamma = 1→ K = A(最大,曲线像恒定和,低滑点)。 - 池子失衡时 K0 → 0 → 分母 =
gamma + 1→ 分式 =gamma/(gamma+1),再平方后急剧变小,K 随之坍塌 → 曲线退化成恒定乘积。
gamma 的作用:它决定了”从低滑点区切换到恒定乘积区”的快慢/陡峭程度。
- gamma 很小(比如 1e-8):只要 K0 稍微偏离 1,分母
gamma + 1 - K0的相对变化就极大,K 迅速坍塌 → 低滑点区非常窄,流动性高度集中。 - gamma 较大:过渡平缓,低滑点区更宽,但资本效率低。
一句话:A 决定整体的”硬度”,gamma 决定”低滑点窗口的宽窄”。 Cryptoswap 用很小的 gamma 来实现”在当前价附近高度集中”。
6. 案例:用具体数字算一遍 K 的变化
我们用一个简化的两币例子(N=2),直观感受 K0 和 K 怎么变。设 A = 100,gamma = 0.01,D = 200(理想平衡时每种币各 100)。
情况 A:完全平衡,x₀ = 100, x₁ = 100
K0 = (100 · 100) · 2² / 200² = 10000 · 4 / 40000 = 1.0
分式 = gamma / (gamma + 1 - K0) = 0.01 / (0.01 + 1 - 1) = 0.01 / 0.01 = 1
K = A · K0 · 1² = 100 · 1 · 1 = 100 ← 很大,像恒定和,滑点极低
情况 B:轻度失衡,x₀ = 130, x₁ = 70
K0 = (130 · 70) · 4 / 40000 = 9100 · 4 / 40000 = 0.91
分母 = 0.01 + 1 - 0.91 = 0.10
分式 = 0.01 / 0.10 = 0.1
K = 100 · 0.91 · 0.1² = 100 · 0.91 · 0.01 = 0.91 ← 暴跌!
注意:余额只偏离了 30%,K 就从 100 掉到了不到 1,降了 100 多倍。这正是”小 gamma → 窗口窄 → 一旦失衡立刻退化成恒定乘积”的体现。
情况 C:重度失衡,x₀ = 190, x₁ = 10
K0 = (190 · 10) · 4 / 40000 = 1900 · 4 / 40000 = 0.19
分母 = 0.01 + 1 - 0.19 = 0.82
分式 = 0.01 / 0.82 ≈ 0.0122
K = 100 · 0.19 · 0.0122² ≈ 100 · 0.19 · 0.000149 ≈ 0.0028 ← 几乎为 0
K 几乎归零,此时不变量基本退化成 x₀·x₁ = (D/2)² = 100²,也就是纯恒定乘积,保证池子不会被掏空。
结论: 平衡时 K 巨大(低滑点),稍微失衡 K 就快速坍塌(自动切换到”不被掏空”模式)。gamma 越小,这个坍塌越早、越剧烈。
7. price_scale(价格刻度):把不同价格的币”拉到同一把尺子上”
前面的不变量公式里写的 xᵢ 并不是币的原始数量,而是转换后的余额。为什么需要转换?
考虑 TriCrypto 池:USDC(≈1 美元)、WBTC(≈6 万美元)、WETH(≈3 千美元)。如果直接把”数量”丢进不变量,公式会认为”1 个 WBTC 和 1 个 USDC 一样多”,完全错乱。
price_scale 就是池子内部记录的”当前汇率”,用来把每种币换算成同一个计价单位(通常以 coin0,即 USDC 为基准,单位是美元)。
price_scale[0]:WBTC 相对 USDC 的内部价格(例如 60000)。price_scale[1]:WETH 相对 USDC 的内部价格(例如 3000)。- coin0(USDC)本身就是基准,价格视为 1。
price_scale 不是市场实时价,而是池子”当前流动性集中在哪个价格”的记录。它通过重锚(repeg)慢慢向市场价靠拢。这正是 V2 区别于 V1 的关键——V1 的”price_scale”永远是 1。
8. 转换后余额(transformed balances)
转换公式(合约里 xp() 函数做的事):
转换后余额 xp[i] = 真实余额 balances[i] · precision[i] · price_scale[i]
precision[i]:把不同精度的代币统一到 18 位小数的乘数。比如 USDC 是 6 位小数,precision = 1e12;WBTC 是 8 位,precision = 1e10;WETH 18 位,precision = 1。price_scale[i]:上一节说的内部汇率(coin0 取 1)。
转换之后,所有 xp[i] 都变成了以”美元、18 位小数”为统一单位的数值,这时把它们丢进第 4 节的不变量公式才有意义。流动性也正是因此被集中在 price_scale 所代表的价格附近。
课程练习里实际写的 Solidity 代码就是这条公式(见第 14 节),非常值得亲手实现一遍。
9. 案例:把真实的 USDC/WBTC/WETH 余额转换一遍
假设某时刻 TriCrypto 池里:
| 币 | 真实余额 | 小数位 | precision | price_scale(美元) |
|---|---|---|---|---|
| USDC (coin0) | 10,000,000 USDC | 6 | 1e12 | 1(基准) |
| WBTC (coin1) | 150 WBTC | 8 | 1e10 | 60,000 |
| WETH (coin2) | 3,000 WETH | 18 | 1 | 3,000 |
转换(结果都化成”美元 × 1e18”的统一刻度,这里为了直观我省略 1e18,只看美元量级):
xp[0] = 10,000,000 · 1(precision抵消小数位后)· 1 = 10,000,000 美元
xp[1] = 150 · 1 · 60,000 = 9,000,000 美元
xp[2] = 3,000 · 1 · 3,000 = 9,000,000 美元
转换后三种币的”美元价值”分别是 1000 万、900 万、900 万,量级一致,可以放进同一个不变量里平等地参与计算。
而如果不转换,直接用 10,000,000 / 150 / 3,000 这三个数字,不变量会以为 USDC 多到爆、WBTC 少得可怜,给出完全错误的报价。这就是 price_scale + precision 的意义。
10. 重锚(Repeg):让流动性自动跟着市场价走
现在来到 V2 最精彩的部分。
流动性被集中在 price_scale 附近。可问题是——市场价会变。如果 BTC 从 6 万涨到 7 万,而 price_scale 还停在 6 万,那池子的流动性就集中错了地方,交易者会一直在”错误价格”附近交易,套利者获利、LP 受损。
重锚(repeg)就是把 price_scale 慢慢挪到新市场价,从而把流动性重新集中到正确价格的过程。 它分两个判断:
10.1 怎么知道”市场价变了”?——内部价格预言机 price_oracle
V2 内部维护一个**指数移动平均价(EMA)**叫 price_oracle,它平滑地跟踪最近成交价 last_prices。EMA 的细节是第 7 章的重点,这里只要知道:price_oracle 代表”池子认为的当前真实市场价”,price_scale 代表”流动性当前集中在哪”。 两者一旦明显分离,就说明该重锚了。
10.2 什么时候才允许重锚?——必须”有利可图”
重锚不是无条件的。移动 price_scale 会重排流动性,本身有代价(见第 12 节)。Curve 的规则是:
只有当池子累积的真实盈利(virtual_price 的增长)足够覆盖重锚的代价时,才执行重锚。
合约里的判断(简化)是:
if virtual_price · 2 − 1 > xcp_profit + 2 · allowed_extra_profit:
允许考虑重锚
直觉解释:virtual_price 是 LP 份额的真实价值,xcp_profit 是上次记录的盈利水平。只有当当前价值明显高过历史盈利的中点、且超出一个安全余量 allowed_extra_profit 时,才说明”赚的够多,可以拿一部分利润去支付重锚成本”。
然后还要看 price_oracle 和 price_scale 的向量距离是否足够大(norm > adjustment_step),距离太小就不动,避免无意义的频繁重排。
满足条件后,price_scale 会朝 price_oracle 方向移动一小步(adjustment_step 控制步长),而不是一步到位。多次小步移动,最终把流动性平滑地搬到新价格。
11. 池子价值与 xcp、virtual_price
要判断”重锚是否划算”,得先能量化池子的价值。Curve 用一个叫 xcp(x of constant product) 的量。
xcp = (xp₀ · xp₁ · ... · xp_{N-1})^(1/N) · N
它本质是把池子在”完全平衡状态”下看成一个恒定乘积池,算出它的恒定乘积流动性 L。直觉上 xcp 衡量”如果现在把池子拉到完全平衡,它值多少”。
由此定义每份 LP 的真实价值:
virtual_price = xcp / LP总供应量(totalSupply)
- 交易手续费会让 xcp 增长,从而
virtual_price缓慢上升 —— 这是 LP 的真实收益。 xcp_profit则累计记录virtual_price相对初始值(1.0)的增长倍数,用作”盈利水位线”。
重锚的逻辑就是:拿 LP 赚到的这部分真实利润,去”购买”把流动性搬到新价格的权利。 只赚不够就不搬,确保重锚永远不会让 LP 亏到本金。
12. 重锚的代价:repegging loss(重锚损失)
为什么重锚有代价?因为移动 price_scale 等于改变了每种币在不变量里的权重,相当于强制把池子在一个非市场价上做了一次”再平衡”,会产生类似无常损失的损耗。
可以这样理解(课程里用 Desmos 图展示):
- 在旧 price_scale 下,池子价值曲线(pool value 关于价格的函数)有一个峰值,集中在旧价格。
- 重锚把曲线峰值挪到新价格。挪动过程中,池子的总价值会短暂下降一点点——这就是 repegging loss。
Curve 的设计精髓在于:它用手续费收入(virtual_price 的增长)来补偿这个损失,并且通过 allowed_extra_profit、adjustment_step、ma_time 这三个参数严格控制,使得:
- 只有赚得够多才重锚(覆盖损失);
- 每次只移动一小步(损失可控);
- 用 EMA 平滑价格(不被瞬时操纵价格骗到)。
这三个参数(
allowed_extra_profit/adjustment_step/ma_time)是 Curve V2 调参的核心,后面第 7 章会反复出现。本章先记住它们各自管什么:允许动用的额外利润门槛 / 每次移动的步长 / EMA 的时间常数。
13. 本章小结
把这一章浓缩成几句话:
- V2 = 把**恒定和(低滑点)和恒定乘积(不被掏空)**缝在一起,且中心价可移动。
- 缝合靠不变量里的放大系数 K:平衡时 K 大(像恒定和),失衡时 K 坍塌(像恒定乘积)。
- K0 ∈ [0,1] 衡量平衡度,gamma 控制低滑点窗口的宽窄(gamma 越小越集中)。
- price_scale 是池子内部的”当前价”,配合 precision 把不同价格/精度的币转换成统一刻度的 transformed balances,流动性因此集中在 price_scale 附近。
- price_oracle(EMA) 跟踪真实市场价;当它与 price_scale 明显分离、且 LP 赚的利润足够时,触发 repeg,price_scale 小步移向 price_oracle。
- xcp / virtual_price 量化池子价值和 LP 收益;重锚有 repegging loss,用手续费利润来补偿,靠三个参数严格约束。
14. 动手练习
目标:亲手把”理论上的 transformed balances”用 Solidity + Foundry 在主网分叉上算出来。这正是课程的第一个练习(对应
CurveV2PriceScale的”计算转换后余额”)。
练习目标
在以太坊主网分叉环境下,读取真实的 TriCrypto(USDC/WBTC/WETH) 池子,计算它当前的转换后余额 xp[0..2],并打印出来、断言三者都 > 0。
池子地址(USDC/WBTC/WETH TriCrypto):0x7f86bf177dd4f3494b841a37e810a34dd56c829b
你需要用到的接口(自己声明)
interface ITriCrypto {
function balances(uint256 i) external view returns (uint256);
function price_scale(uint256 i) external view returns (uint256); // i=0 -> coin1, i=1 -> coin2
function precisions() external view returns (uint256[3] memory);
}
注意两个坑:
price_scale(i)的下标是 0、1,分别对应 coin1(WBTC)、coin2(WETH);coin0(USDC) 没有 price_scale,价格就是 1。price_scale本身是 18 位小数(1e18 = 价格 1),所以乘完要除回 1e18。
解题思路(自己写代码实现)
- 用
precisions()拿到三种币的精度乘数precisions[0..2]。 - coin0(USDC) 直接:
xp[0] = balances(0) · precisions[0](它是基准,不乘 price_scale)。 - coin1、coin2 循环处理:
xp[i] = balances(i) · price_scale(i-1) · precisions[i] / 1e18 - 用
console2.log("xp[%d] = %e", i, xp[i])打印三个值。 - 断言
xp[i] / 1e18 > 0,确认非零。
运行方式
# 需要一个归档节点 RPC(Alchemy/Infura 等),设为 FORK_URL
forge test \
--evm-version cancun \
--fork-url $FORK_URL \
--match-path test/CurveV2PriceScale.t.sol -vvv
验证你做对了
打印出来的三个 xp 值,量级应该都在”美元价值”上接近(就像第 9 节案例那样三者量级相当),因为 TriCrypto 池会通过重锚保持三种资产的价值大致平衡。如果某个 xp 比其它两个差了几个数量级,多半是 price_scale 或 precision 用错了。
进阶思考(选做)
- 把你算出的
xp三个值代入第 4 节的不变量公式,用 Python 数值求解D,再和合约的pool.D()对比,看看是否吻合。 - 改变其中一个
xp(模拟一次大额交易后的失衡),用第 5、6 节的方法手算K0和K,观察 K 坍塌的程度。
下一章(第 3 章 合约总览)会带你看:这些数学量(A、gamma、price_scale、D、xcp_profit……)在合约里是怎么存储的,以及要调用哪些函数来和池子交互。