这篇文章逐步展示了 get_D()
和 get_y()
的代码是如何从 StableSwap 不变式推导而来的。
给定 StableSwap 不变式:
A⋅n∑xi+D=A⋅n⋅D+Dn+1⋅n∏xi
我们希望对其进行两种常见的数学操作:
- 在 A 和储备 x1,…,xn 的固定值的情况下计算 D。请注意,n,即池支持的代币数量,在池部署时是固定的。这正是函数
get_D()
的作用。
- 给定 D,我们希望将一个储备 xi 的值增加到一个新值 xi',并计算另一个储备 xj 需要减少多少,以保持方程的平衡。这正是函数
get_y()
的作用。在这里,“y”指的是 xi'。
在 Curve StableSwap 中,这些操作分别称为 get_D()
和 get_y()
。
get_D()
的目标
在 Curve V1 (StableSwap) 中,D
类似于 Uniswap V2 中的 k
— D
越大,储备就越多,价格曲线的“外推”越远。流动性添加或移除后,或者费用更改池余额后,D
会改变并需要重新计算。这就是函数 get_D()
的用途。给定当前储备的池,它计算 D
。
如果一个曲线池持有两个代币 x
和 y
,StableSwap 不变式为
4A(x+y)+D=4AD+D3/4xy
在我们讨论的情况下,“增幅因子” A
可以视为一个常数。
get_y()
的目标
get_y()
函数在交换过程中使用。与 Uniswap V2 中的 k
类似,交换过程中必须保持 D
为常数(忽略费用)。具体来说,给定 x
的新值,它计算出保持方程平衡的 y
的值。因此,它是计算“如果我将这数额的代币 x
放入池中,我可以取出多少代币 y
?”的一个重要子程序。
Curve 可以在池中持有超过 2 个代币(例如 3pool 持有 USDT、USDC 和 DAI)。Curve 通过一个数组中的索引来识别这些代币。因此,在这种情况下,x
和 y
指的是该数组中的特定代币。在这个上下文中,get_y()
意味着改变特定代币 x
的余额,同时保持其他余额不变,但允许另一个代币 y
的价值变化。然后,在给定特定的 x
变动后,计算 y
如何变化以保持不变式平衡。
n 个代币的恒等式为:
A⋅n∑xi+D=A⋅n⋅D+Dn+1⋅n∏xi
为简单起见,本文接下来将使用 S 代替求和,使用 P 代替乘积,因此不变式变为:
A⋅nS+D=A⋅Dn+Dn+1⋅nP
其中 S 是代币余额的总和(x0+x1+…+xn),P 是余额的乘积(x0x1…xn),而 xi 是代币 i 的余额。
在 白皮书 中,S 被写作 ∑xi,而 P 被写作 ∏xi。白皮书中的方程复制如下:
A⋅n∑xi+D=A⋅Dn+Dn+1⋅n∏xi
我们将使用 S 和 P 来代替求和和乘积符号。
我们假设池可以保存任意数量的 n 个代币,因此公式将反映这一点。然而,实际上,n 必须很小,否则 Dn+1 项可能会发生溢出。
使用 get_D()
计算 D
在 get_D()
中,我们得到了一组余额 x0,x1,...,xn,我们要计算 D
。
不可能代数求解
A⋅nS+D=A⋅Dn+Dn+1⋅nP
来找出 D。相反,我们需要应用Newton方法来数值解它。为此,我们创建一个函数 f(D),当方程平衡时它等于0。
0=A⋅Dn+Dn+1⋅nP−D−A⋅nS
0=Dn+1⋅nP+A⋅Dn−D−A⋅nS
f(D)=Dn+1⋅nP+A⋅nD−D−A⋅nS
然后我们对 D 计算导数 f′(D):
f′(D)=(n+1)Dn⋅nP+A⋅n−1
Newton 方法公式
我们可以通过下列公式迭代求解 D:
Dnext=D−f′(D)f(D)
将 f′(D) 表示为 D 的分母会比较有用。首先,我们将左侧分数的上下乘以 D。
f′(D)=(n+1)DDn+1⋅nP+A⋅n−1
然后,将 f′(D) 合并为单个分数:
f′(D)=D(n+1)Dn+1⋅nP+(A⋅n−1)D
我们可以重写 Newton 方法,使其具有相同的分母:
Dnext=D−f′(D)f(D)
=f′(D)D⋅f′(D)−f′(D)f(D)
=f′(D)D⋅f′(D)−f(D)
通过将前面得到的 f(D) 和 f′(D) 代入重写过的 Newton 方法公式,我们得到:
=(n+1)Dn+1⋅nP+(A⋅n−1)DD(n+1)Dn+1⋅nP+(A⋅n−1)D−(Dn+1⋅nP+A⋅nD−D−A⋅nS)
因为我们重新安排了 f′(D) 以使D 作为分母,所以 Df′(D) 项将很好地相消:
=(n+1)Dn+1⋅nP+(A⋅n−1)D(n+1)Dn+1⋅nP+(A⋅n−1)D−(Dn+1⋅nP+A⋅nD−D−A⋅nS)
=(n+1)Dn+1⋅nP+(A⋅n−1)D(n+1)Dn+1⋅nP+(A⋅n−1)D−(Dn+1⋅nP+A⋅nD−D−A⋅nS)
=(n+1)Dn+1⋅nP+(A⋅n−1)DnDn+1⋅nP+A⋅nS
我们将分子和分母都乘以 D:
=(A⋅n−1)D+(n+1)Dn+1⋅nP(A⋅nS+nDn+1⋅nP)D
如果我们定义 DP 为
DP=Dn+1⋅nP
并替换 DP,我们得到:
Dnext=(A⋅n−1)D+(n+1)DP(A⋅nS+nDP)D
与原始源代码的比较
这与在 Vyper 代码 中的内容完全匹配:

变量 D_P 被定义为:

D_P: uint256 = D
for _x in xp:
D_P = D_P * D / (_x * N_COINS)
xp
是代币的数量,因此循环将运行 n 次。因此,我们在分母中得到了 D 自身乘以 n 次。
DP=Dn+1⋅ni=1∏nxi
使用 get_y()
计算 y
这个想法是我们强制其中一个 xi 采取一个新值(代码称之为 x
),并计算出另一个 xj(其中 i≠j)的正确值,使方程保持平衡。其他代币的余额保持不变。xj 被称为 y。
尽管 StableSwap 池可以有多个代币,但一次只能使用 get_y()
交换两个代币。
同样地,我们有相同的不变式:
A⋅nS+D=A⋅Dn+Dn+1⋅nP
D、A 和 n 是固定的,但我们将改变 S 和 P 中的两个值。
S=x0+x1+…+xnP=x0x1…xn
因此,我们需要稍微调整公式,因为 S 和 P 包含我们正在计算的值。
- S' 将是所有余额的总和_除了_我们试图求解的代币 xi 的新余额。
- P 将是所有代币余额的乘积,_除了_我们试图求解的代币。
换句话说:
S=S′+yP=P′⋅y
为了与代码保持一致,我们将称我们正在尝试计算其新余额的代币为 y。
则公式变为:
A⋅n(S′+y)+D=A⋅Dn+Dn+1⋅nP′⋅y
同样,我们得出一个 f(y),当方程平衡时它会为0,以及关于 y 的导数:
f(y)=A⋅Dn+Dn+1⋅nP′⋅y−A⋅n(S′+y)f′(y)=−Dn+1⋅nP′⋅y2−A⋅n
这里再次是 Newton 方法的公式:
ynext=y−f′(y)f(y)
在将 f(y) 和 f′(y) 代入 Newton 方法后,我们得到:
ynext=y−−Dn+1⋅nP′⋅y2−A⋅nA⋅Dn+Dn+1⋅nP′⋅y−A⋅n(S′+y)
把 -1 移到分母外面:
ynext=y−(−A⋅Dn−Dn+1⋅nP′⋅y+A⋅n(S′+y))1Dn+1⋅nP′⋅y2+A⋅n
再次对 y 乘以分数,使其具有相同的分母:
ynext=y+Dn+1⋅nP′⋅y2+A⋅nA⋅Dn+Dn+1⋅nP′⋅y−A⋅n(S′+y)
ynext=y⋅(Dn+1⋅nP′⋅y2+A⋅n)+(A⋅Dn+Dn+1⋅nP′⋅y−A⋅n(S′+y))−Dn+1⋅nP′⋅y2−A⋅n
将 y 分配到左侧项中:
ynext=y⋅(Dn+1⋅nP′+A⋅ny+A⋅Dn+Dn+1⋅nP′⋅y−A⋅nS′−A⋅ny−Dn+1⋅nP′⋅y2−A⋅n)
结合同分母的总和:
ynext=A⋅Dn+2Dn+1⋅nP′⋅y−A⋅nS′−Dn+1⋅nP′⋅y2−A⋅n
从原始不变式创建一个替代项
看起来方程无法进一步简化,但如果我们重新审视我们的原始不变式:
A⋅n(S′+y)+D=A⋅Dn+Dn+1⋅nP′⋅y
我们可以求解 A⋅Dn,得到:
A⋅n(S′+y)+D−Dn+1⋅nP′⋅y=A⋅Dn
A⋅Dn=−Dn+1⋅nP′⋅y+A⋅n(S′+y)+D
然后,如果我们将 A⋅Dn 代入我们计算 ynext 的最新公式的分子中,我们得到:
ynext=A⋅Dn+2Dn+1⋅nP′⋅y−A⋅nS′−Dn+1⋅nP′⋅y2−A⋅n
ynext=(−Dn+1⋅nP′⋅y+A⋅n(S′+y)+D)+2Dn+1⋅nP′⋅y−A⋅nS′−Dn+1⋅nP′⋅y2−A⋅n
许多项发生相消:
ynext=−Dn+1⋅nP′⋅y+A⋅nS′+A⋅ny+D+2Dn+1⋅nP′⋅y−A⋅nS′−Dn+1⋅nP′⋅y2−A⋅n
我们剩下的就是一个更小的方程:
ynext=A⋅ny+Dn+1⋅nP′⋅y⋅Dn+1⋅nP′Dn+1⋅nP′⋅y2+A⋅n
我们将上下同时乘以 yA⋅n:
ynext=(Dn+1⋅nP′⋅y2+A⋅n)yA⋅n(A⋅ny+Dn+1⋅nP′)yA⋅n
回到我们的不变式,我们可以求解分母中的分数项:
A⋅n(S′+y)+D=A⋅Dn+Dn+1⋅nP′⋅y
Dn+1⋅nP′⋅y=A⋅n(S′+y)+D−A⋅Dn
然后我们可以将其代入 ynext 的公式中:
ynext=y2+Dn+1⋅nP′⋅An⋅(S′+y)+D−A⋅Dn
然后我们可以将 1/An 分配并简化分母:
ynext=y2+Dn+1⋅nP′⋅An(S′+y+D−A)/An
简化分母,去掉括号并将两个 y 相加:
ynext=y2+Dn+1⋅nP′⋅An/2y+S′+D/An−A
在原始代码中,Curve 定义了额外的变量:
c=Dn+1⋅nP′⋅Anb=S′+D/An
替代到 ynext 的公式中,我们得到:
ynext=y2+Dn+1⋅nP′⋅An/2y+S′+D/An−A
与原始源代码的比较
这与 Curve 代码完全匹配,请参阅下方紫色框:

Ann 和 Anⁿ 之间的不匹配
让人困惑的是,Curve 白皮书使用不变式 Ann,但代码库使用的是 Ann。也就是说,代码库似乎在计算 A⋅n⋅n 而不是 A⋅nn。这种差异的原因是,代码库将 A 存储为 A⋅n−1。由于 n 部署时是固定的,预先计算 nn−1 使代码能够避免在链上计算指数,这是一个更昂贵的操作。
总结
Curve 的核心不变式不允许通过符号方法求解变量 D 或 xi。相反,这些项必须通过数值方法来解决。
这次活动的一个启示是,良好的代数操作是一种非常有效的 Gas优化 技术。Curve 开发者能够计算出比直接代入 f 和它的导数相对更小的 Newton 方法公式。
引用与致谢
撰写本文时参考了以下资源:
StableSwap — 稳定币流动性高效机制,Michael Egorov, https://resources.curve.fi/pdf/curve-stableswap.pdf
理解 Curve AMM,第一部分:StableSwap 不变式,Atul Agarwal https://atulagarwal.dev/posts/curveamm/stableswap/
Curve Finance Discord, “chanho”
https://discord.com/channels/729808684359876718/729812922649542758/1126630568004698132
Curve – 代码解析 – get_y() | DeFi, 智能合约程序员 https://www.youtube.com/watch?v=jAhKbxoeskQ