本文通过代数运算,逐步展示了 get_D() 和 get_y() 的代码是如何从 StableSwap 不变量中推导出来的。
已知 StableSwap 不变量:
我们希望用它来进行两个常用的数学操作:
- 在给定固定的 值以及储备量 的情况下计算 。请注意,(即资金池支持的代币种类数量)在资金池部署时是固定的。这就是
get_D()函数的作用。 - 在给定 的情况下,我们希望将其中一个储备量 增加到一个新值 ,并计算出另一个储备量 需要减少多少以保持等式平衡。这就是
get_y()函数的作用。这里的 “y” 指的是 。
在 Curve StableSwap 中,这些操作分别被称为 get_D() 和 get_y()。
get_D() 的目标
在 Curve V1(StableSwap)中,D 的作用类似于 Uniswap V2 中的 k —— D 越大,储备量就越多,价格曲线也就越向外推移。在添加或移除流动性,或者手续费改变了资金池余额之后,D 会发生变化——并且需要重新计算。这就是 get_D() 函数的用途。在给定资金池当前储备量的情况下,它会计算出 D。
如果一个 Curve 资金池包含两种代币 x 和 y,则 StableSwap 不变量为
就我们的目的而言,“放大系数”(amplification factor)A 可以被视为一个常数。
get_y() 的目标
get_y() 函数在代币兑换(swap)期间使用。与 Uniswap V2 中的 k 类似,D 在兑换期间必须保持不变(忽略手续费)。具体来说,在给定 x 的新值时,它会计算出能使等式保持平衡的 y 值。因此,这是一个重要的子程序,用于计算“如果我向资金池中投入这么多代币 x,可以取出多少代币 y?”
Curve 资金池可以包含 2 种以上的代币(例如 3pool 包含 USDT、USDC 和 DAI)。Curve 通过数组中的索引来识别代币。因此,在这种情况下,x 和 y 指的是该数组中的特定代币。在此背景下,get_y() 的意思是改变特定代币 x 的余额,保持其他余额不变,但允许另一个代币 y 的值发生变化。然后,在给定 x 的特定变化量时,计算 y 应如何变化以保持不变量平衡。
n 种代币的不变量为:
为简便起见,在本文其余部分,我们将使用 代替求和符号,使用 代替求积符号,因此不变量变为:
其中 是代币余额之和(), 是余额的乘积(), 是代币 i 的余额。
在白皮书中, 被写为 , 被写为 。白皮书中的公式复现如下:
我们将使用 和 来代替求和与求积符号。
我们假设资金池可以包含任意数量为 的代币,因此公式将反映这一点。然而在实践中, 必须是一个较小的数,否则 项容易发生溢出。
使用 get_D() 计算
在 get_D() 中,已知一组余额 x_0, x_1, ..., x_n,我们需要计算 D。
我们无法通过代数方法直接求解
得到 。相反,我们需要应用牛顿法来进行数值求解。为此,我们构造一个函数 ,当等式平衡时,该函数为 0。
并且我们计算其关于 的导数 如下:
牛顿法公式
我们可以使用以下公式迭代求解 :
将 表示为带有分母 的形式会很有帮助。首先,我们将定义 的左侧分数分子和分母同时乘以 。
然后将 合并为一个分数:
我们可以将牛顿法公式重写为具有公分母的形式:
将之前的 和 代入重写后的牛顿法公式中,我们得到:
由于我们重排了 使其分母为 , 项将很好地被约掉:
展开所有项以消除分子中的括号:
这会产生大量的相消:
我们将分子和分母同乘 :
如果我们将 定义为:
并代入 ,我们得到:
与原始源代码的比较
这与 Vyper 代码 中的内容完全一致:

变量 的定义如下:
D_P: uint256 = D # D_P = S
for _x in xp:
D_P = D_P * D / (_x * N_COINS)
xp 是代币的数量,因此循环将执行 n 次。因此,在分母中我们有 n 个 相乘:
使用 get_y() 计算 y
其思路是,我们强制其中一个 取一个新值(代码中称之为 x),并计算出另一个 (其中 )的正确值,从而使等式保持平衡。其他代币的余额保持不变。 在此处被称为 。
尽管 StableSwap 资金池可能包含多种代币,但使用 get_y() 每次只能交换其中的两种代币。
同样,我们有相同的不变量:
、 和 是固定的,但我们将改变 和 中的两个值:
因此,我们需要对公式稍作调整,因为 和 包含了我们正在计算的值。
- 将是所有余额的总和,不包括我们试图求解的代币 的新余额。
- 将是所有代币余额的乘积,不包括我们试图求解的那一个代币。
换句话说:
为了与代码保持一致,我们将试图计算其新余额的那个代币称为 。
公式随之变为:
同样,我们推导出一个当等式平衡时为 0 的 ,以及它关于 y 的导数:
下面再次给出牛顿法公式:
将 和 代入牛顿法后,我们得到:
从分母中提取出 :
乘以 (如下方方框所示)以获得公分母:
在左侧项中展开 :
将具有公分母的和式进行合并:
从原始不变量中构造代换
这个等式似乎无法进一步化简,但如果我们回看原始的不变量:
我们可以求解出 ,得到:
然后,如果我们将 代入到我们最新的 公式分子中,我们得到:
会发生大量的相消:
最后我们留下了一个小得多的等式:
我们将分子分母同乘 :
回到我们的不变量,我们可以求解出分母中的分数项:
然后我们可以将其代入 ynext 的等式中:
然后我们可以展开 并化简分母:
通过去掉括号并将两个 加在一起,进一步化简分母:
在原始代码中,Curve 定义了额外的变量:
将其代入 的公式后,我们得到:
与原始源代码的比较
这与 Curve 代码完全一致,请见下方的紫色方框:

Ann 与 Anⁿ 之间的不匹配
令人困惑的是,Curve 白皮书使用的不变量是 ,但代码库中使用的是 。也就是说,代码库似乎在计算 A * n * n 而不是 A * n ** n。造成这种差异的原因是,代码库将 存储为了 。由于 在部署时是固定的,预先计算 可以让代码避免在链上进行指数运算,因为这种运算的成本更高。
总结
Curve 的核心不变量不允许对变量 或 进行符号求解。相反,这些项必须通过数值方法进行求解。
从这次推导中我们可以得出一个结论,那就是优秀的代数变换是一种非常有效的Gas优化技巧。Curve 的开发者们能够计算出比简单粗暴地代入 及其导数要小得多的牛顿法公式。
引用与致谢
在撰写本文时参考了以下资源:
StableSwap - efficient mechanism for Stablecoin liquidity, Michael Egorov, https://resources.curve.fi/pdf/curve-stableswap.pdf
Understanding the Curve AMM, Part -1: StableSwap Invariant, Atul Agarwal https://atulagarwal.dev/posts/curveamm/stableswap/
Curve Finance Discord, “chanho”
https://discord.com/channels/729808684359876718/729812922649542758/1126630568004698132
Curve - Code Explaind - get_y() | DeFi, Smart Contract Programmer https://www.youtube.com/watch?v=jAhKbxoeskQ