skip to content

Curve v2 CryptoSwap: white paper

By 0xmc & 0xstan

understand Curve v2 CryptoSwap white paper

Curve v2 CryptoSwap 详解系列:

  1. Curve v2 CryptoSwap: white paper
  2. Curve v2 CryptoSwap: add and remove liquidity
  3. Curve v2 CryptoSwap: exchange and fee
  4. Curve v2 CryptoSwap: repegging
  5. Curve v2 CryptoSwap: math lib

Curve 一直是 Defi 领域重要的组成部分,我们将在接下来的系列文章中,从白皮书原理到实际业务代码,详细解读其 V2 的运行机制和业务逻辑的细节,领略 Curve 设计之精妙,代码实现之精巧。

有关 v1 的原理和代码详解可以参见我们之前的文章 https://0xreviews.xyz/2022/02/12/Curve-v1-StableSwap.

本篇将针对白皮书原理的重点和难点做进一步解读。

Transformed pegged invariants

v2 的 CryptoSwap 希望比 v1 StableSwap 更近一步,不仅仅只做锚定资产(例如稳定币或 ETH-sETH)之间的兑换。在 v1 中 D 的定义是在价格处于平衡点状态下:

xi=D\sum{x_i}=D

其中 x_i 代表 token 实际的数量(balance),可以替换为 b 来表示。但由于 v2 中并不是相互锚定的资产,波动比较大,需要将定义中的数量 b 替换为价值 b'。

我们引入一个内部缩放价格 p , 在代码中是 price_scale,令其作为锚定第一个资产的价格,例如 USDT-WBTC-WETH 池子,将会以 USDT 作为锚定标的,假设 WBTC 和 WETH 市价为 40000 和 3000 美元, 那么价格数组为 [1, 40000, 3000]。第一个价格始终是 1,因为该资产和自身锚定,比值永远是 1。

价值 b' 的定义是数量乘以价格 b=bpb'=bp

我们将所有资产的数量和余额分别整合到一起,组成 2 个向量 b 和 b',他们之间的转换关系如下

b=T(b,p)=(b0p0,b1p1,b2p2...)b=T(b',p)=(\frac{b'_0}{p_0},\frac{b'_1}{p_1},\frac{b'_2}{p_2}...)

b=T(b,p)=(b0p0,b1p1,b2p2...)b'=T(b,p)=(b_0 p_0,b_1 p_1,b_2 p_2 ...)

白皮书中上述两个定义式写反了,会造成 p 是价格的倒数的误会,经过反复比对代码和白皮书描述,可以确认表达式应为这里定义的形式

v2 中对于 D 的定义如下

D=NxeqD=N x_{eq}

当池子价格处于平衡点时(例如首次注入流动性时),每个资产的价值相等,其值为 x_eq

虽然定义了基于 x_eq 给出了 D 的定义,但价格总在变化,大部分情况不在平衡点,我们很难去求出当时的 x_eq。所以代码中的 D 是根据平衡方程利用牛顿法求解。

CurveCrypto invariant

curve v2 的平衡方程

KDN1xi+xi=KDN+(DN)NKD^{N-1}\sum{x_i}+\prod{x_i}=KD^N+(\frac{D}{N})^N

其中 K 的定义与 v1 中的 A 不同

K0=xiNNDNK_0=\frac{\prod{x_i}N^N}{D^N}

K=AK0γ2(γ+1K0)2K=AK_0\frac{\gamma^2}{(\gamma+1-K_0)^2}

A * K_0 实际就是 v1 的系数 χ\chi

AK0=AxiNNDN=χA*K_0=A\frac{\prod{x_i}N^N}{D^N}=\chi

v2 的系数 K 定义中多了关于 gamma 的调整系数

γ2(γ+1K0)2\frac{\gamma^2}{(\gamma+1-K_0)^2}

newton's method

为了求解 D 的值,和 v1 一样,将使用牛顿法迭代求解,在改变流动性时求解 D,当进行交易时,求解输出资产的数量 x_i 。将平衡方程写成 F(x, D)=0 的形式

F(x,D)=KDN1xi+xiKDN(DN)NF(x,D)=KD^{N-1}\sum{x_i}+\prod{x_i}-KD^N-(\frac{D}{N})^N

导函数 F'(x,D):

D 和 x_i 的牛顿法迭代初值分别设为如下将能更快的找到解

D0=N(xk)1ND_0=N(\prod{x_k})^{\frac{1}{N}}

xi,0=DNk!=ixkNNx_{i,0}=\frac{D^{N}}{\prod_{k!=i}{x_k}N^{N}}

白皮书中 x_i,0 初值的定义有误,分子中的 D 的幂写成 N-1,分母中 N 的幂写成 N-1,应该都为 N,代码中已经修正正确

白皮书中错误的初值定义: xi,0=DN1k!=ixkNN1x_{i,0}=\frac{D^{N-1}}{\prod_{k!=i}{x_k}N^{N-1}}

牛顿法函数在 EVM 中该过程将消耗约 35K gas。

Quantification of a repegging loss

为了衡量池子的总价值,量化利润或亏损,定义了 X_cp

Xcp=(DNpi)1NX_{cp}=(\prod{\frac{D}{Np_i}})^{\frac{1}{N}}

需要注意的一点是,p_iprice_scale , 而该价格只受 repegging 过程影响,所以这里在交易过程中,如果没有触发 repegging, 分母中的 p 是不变的,那么 X_cp 将只受到 D 的变化的影响。

TotalSupply&VirtualPrice

LP token 的发行总量 total_supply,在第一次注入流动性时将被赋值为 X_cp,之后增减流动性,将根据 D 的变化同比放缩。

TotalSupply={Xcp,D=0,TotalSupplyDafterDbefore,D>0TotalSupply= \begin{cases} X_{cp},&D=0, \newline TotalSupply\frac{D_{after}}{D_{before}},&D>0 \end{cases}

定义 virtual_price 为 Xcp/total_supply

pvirtual=XcpTotalSupplyp_{virtual}=\frac{X_{cp}}{TotalSupply}

白皮书中没有定义 virtual_price, 但在代码中比较重要

Algorithm for repegging

我们在流动性变化或交易时追踪 virtual_price 的变化,用 xcp_profit 来量化 LP 的损益情况。

Xcp,profit={1,D=0,Xcp,profitpvirtual,afterpvirtual,before,D>0X_{cp,profit}= \begin{cases} 1,&D=0, \newline X_{cp,profit}\frac{p_{virtual,after}}{p_{virtual,before}},&D>0 \end{cases}

每当发生交易时,包括不平衡的添加或移除流动性,对价格产生了影响,都会进入 repegging 逻辑(调整价格)。

undo repegging: 当利润不足时,会撤销 Repegging 操作,即只有当利润足够时才会执行 repegging 逻辑。

when virtual_price - 1 > (xcp_profit - 1) / 2 do repegging

virtual_price 代表了 xcp_profit_real, 这里的含义实际上是只有当 LP 每单位收益超过之前收益的一半时,进行 repegging,反言之,跌到之前收益一半以下,将不进行价格调整。

xcp_profit 在白皮书中定义有误。白皮书中定义为 xcp_after/xcp_before, 但是当流动性增加且不影响价格时, xcp 会同比增大,但这个时候总利润显然没有增长的,这并不能很好的表现收益情况。正确的定义应该根据 virtual_price 进行同比缩放,这一点在代码中已经修正

xcp_profit 会跟随 virtual_price 同比变化,且两者的初值都是 1,理论上来说两者值应该保持同步,但实际情况却不同,前者往往比后者要大,因为 xcp_profit 经常包含了未提取的 admin_fee,在此基础上同比放缩,两者的值不会完全同步。

每次交易产生的价格 last_price 经过时间加权组成一个预言机价格 price_oracle

注意: last_priceprice_scale 不同

  • price_scale 每种资产与第一个资产的缩放(锚定)价格
  • last_price 是交易时产生的价格,是每一笔交易输入和输出资产 xp 差量的比值;xp = balance * price_scale

定义时间权重系数 alpha,其中 t 是距离上次更新的时间间隔,T_1/2 是人为设定的 half_time, 对应代码中的 ma_half_time

oracle.1: α=2tT1/2\alpha=2^{-\frac{t}{T_{1/2}}}

price_oracle 预言机价格以时间加权的方式更新,即 EMA (Exponential Moving Average)

oracle.2: p=plast(1α)+αpprevp^*=p_{last}(1-\alpha)+\alpha p^*_{prev}

根据 price_oracle 来调整 price_scale

oracle.3: pipi,prev=1+s(pjpj,prev1)2(pipi,prev1)\frac{p_i}{p_{i,prev}}=1+\frac{s}{\sqrt{\sum{(\frac{p_j^*}{p_{j,prev}}-1)^2}}}(\frac{p_i^*}{p_{i,prev}}-1)

先将 oracle 公式稍作变形便于接下来的理解

pipi,prev1=s(pjpj,prev1)2(pipi,prev1)\frac{p_i}{p_{i,prev}}-1=\frac{s}{\sqrt{\sum{(\frac{p_j^*}{p_{j,prev}}-1)^2}}}(\frac{p_i^*}{p_{i,prev}}-1)

  1. 调整的思路是在每次交易后,让 price_scaleprice_oracle 做一定程度的偏移
  2. 构建一个 N 维的向量空位(N 是资产数量),我们可以将变化前后的价格的比值 p_i/p_i,before 组成的数组作为其中的向量,那么 1 即为坐标系的原点
  3. 将更新前后的价格的比值作为比较对象
    • pipi,prev1\frac{p_i}{p_{i,prev}}-1 price_scale 的前后比值
    • pipi,prev1\frac{p_i^*}{p_{i,prev}}-1 price_oracleprice_scale 的比值
    • 比值 - 1 实际上就是计算到向量和原点 1 的差值
  4. s(pjpj,prev1)2\frac{s}{\sqrt{\sum{(\frac{p_j^*}{p_{j,prev}}-1)^2}}}
    • s 是 step 的缩写,即调整步长;
    • 分母可以理解为向量到原点的距离(各个维度与原点的平方差)
  5. 最后根据这个调整系数来让 price_scaleprice_oracle 靠拢

Dynamic fees

手续费从 v1 的固定费率,改为动态调整,手续费会根据调整系数 g,在 f_mid 和 f_out 之间波动,距离平衡点越远,手续费将越高。

g=γfeeγfee+1xi(xi/N)Ng=\frac{\gamma_{fee}}{\gamma_{fee}+1-\frac{\prod{x_i}}{(\sum{x_i}/N)^N}}

f=gfmid+(1g)foutf=g \cdot f_{mid}+(1-g)\cdot f_{out}

下一篇我们将深入源码探究 Curve v2 关于流动性的逻辑。

Reference