深入Defi系列之Uniswap V3

Deep Defi 2021-04-28 08:49 1.43万
分享

Uniswap到目前为止历经了三次版本迭代,v1解决了任意token跟eth之间的兑换问题,v2解决了任意两种不同的token之间的兑换问题。但是v1和v2所使用的恒定乘积算法xy=k有两个问题:

稳定币之间兑换的滑点大

资金利用率较低

对于稳定币之间的兑换(比如USDT跟USDC)由于币值都是锚定美元,因此用户期望的滑点必须很小,在相同的流动性情况下Curve所采用的类似恒定求和的算法的滑点更小。虽然Curve在稳定币的兑换上比Uniswap更优,但两者都面临资金利用率低的问题,以Uniswap为例:

假如在A点添加流动性,则初始时Pool中的x数量是x(A),y数量是y(A)。当价格从A点移动到B点时y会从y(A)减少到y(B),也即是只需要y(A)-y(B)就能满足价格的变化,而剩下的y(B)并没有发挥作用。真实情况是大部分时候价格的波动都在有限的范围内,既不会突然跌为0,也不会突然涨到天上去,因此满足这个波动范围的资金就只需要占流动性很少的一部分,这就是Uniswap资金利用率低的原因。

集中流动性

为了提高资金的利用率最直观的做法是流动性曲线上不同价格段的流动性分别由不同的人提供,比如:

价格从A点波动到C点(池子中的x不断增加,y不断减少),Alice提供了AB之间的流动性y(A)-y(B),Bob提供了BC之间的流动性y(B)-y(C)。如果想把价格从A点打到C点,则需要往池子中增加的x就是x(C)-x(A)。

流动性是不可能一成不变的,假如Tom也跟Alice一样把流动性添加到AB价格段,比如:


注意图中A点和E的价格是相同的,B点和F点的价格也是相同的

绿色曲线上AB和BC之间的流动性公式都是xy=k,而由于Tom添加了跟Alice一样的流动性因此蓝色曲线上EF之间的公式为xy=2k。现在实际的流动性分成了两段,第一段EF流动性是2k,第二段BC流动性是k。由于y(E)-y(F)=2[y(A)-y(B)]因此根据几何上的等比特性可以得到x(F)-x(E)=2[x(B)-x(A)]。最后如果想把价格从A点打到C点,需要往池子中增加的x=2[x(B)-x(A)]+x(C)-x(B)。

我们通过数学证明来重复上述过程,令流动性,价格,由于,因此有

对于AB和EF价格段,有

Tom跟Alice提供的y资产数量是相同的,于是

反过来问Tom得注入多少y资产才能使得,那么答案就是提供跟Alice相同的y资产即可。

同样价格段上可以叠加任意多个不同值的流动性,不同的价格段发生重叠也没有关系,比如:

Alice在AB价格段提供的流动性是L(由绿色曲线表示),Tom在CD价格段提供的流动性是2L(由紫色曲线表示),由于线性叠加导致在AD价格段的流动性是3L(由黄色曲线表示)。整个CB价格段的流动性分为三段:CA,AD和DB,如果想将价格从C打到B,需要提供多少x资产呢?

不管有多少个价格段重叠的流动性,都可以采用这种方式来逐段计算。

离散的价格边界

虽然理论上Alice可以在类似于[0.65,1.23],[4.98,7.56]这个样的价格段上提供流动性,但实际上由于合约的存储空间是有限的无法实现价格段边界值的连续性。在V3的实现中将价格段边界值进行了离散化处理,每个边界p均为,tick为整数,tick的范围在[-887272,887272]之间,对应的p的最小值约为,最大值约为。

需要注意的是,两个相邻的p之间的间距并不是恒定的,在tick=0附近最小,离0越远间距就越大。比如tick=1和tick=0的间距是0.0001,而tick=10000与tick=10001之间的间距是0.00027。

价格段定义为Position,表示用户在什么价格区间提供了多少流动性。

{ "owner":"用户", "tickLower":"价格区间的下边界对应的tick", "tickUpper":"价格区间的上边界对应的tick", "liquitidy":"价格区间上的流动性" }

在V2中Pool由[token0,token1]来唯一标识,在V3中Pool则由[token0,token1,fee]来唯一标识,定义Pool为:

{ "token0":"交易对中的token,比如BTC", "token1":"交易对中的另外一个token,比如USDT", "fee":"费率,比如0.05%", "tickSpace":"Position价格上下边界的tick值必须是tickSpace的倍数" }

这样的定义意味着可以为类似于[BTC,USDT]这样的交易对创建多个不同费率的Pool。在V2中所有Pool的手续费都是0.3%,而Curve这样的稳定币兑换的手续费率则低至0.04%,所以在稳定币兑换上用户用脚投票都会选择Curve。V3灵活的费率给供需双方提供了更多的选择,用户自然会选择费率最低的Pool,而流动性提供者则更愿意进入到费率高的Pool中添加流动性。每个费率都有对应的tickSpace,合约里面提供了三个固定选项:

fee tickSpace

0.05% 10

0.3% 60

1% 200

V3的治理组织可以设置其它的[fee,tickSpace]选项

当tickSpace为10时,Positon可以是[0,10],[40,500],但不能是[3,5],[10,54],因为上下边界必须是10的整数倍。tickSpace的设置的大或者小影响的是swap时价格跨liquitidy的可能性。什么是跨liquitidy?

如上图所示,假定L(0,4)=L(0,2)+L(2,4),当改变Pool中x的数量(减少x,增加y)使得价格从P0上涨到P3时,对于tickSpace=4的Pool而言swap始终发生在L(0,4)这个流动性阶段内,而对于tickSpace=2的Pool则需要分段计算,先计算L(0,2)中的x是否足够,当L(0,2)的x不够时再计算L(2,4)的x是否足够。如果swap的过程中出现了流动性的切换,比如从L(0,2)进入L(2,4)就发生了跨liquitidy。跨liquitidy会导致swap需要的计算过程更多,消耗的gas也会更多。如果交易对的价格波动很小,比如稳定币,那么就可以设置更小一点的tickSpace,这样流动性可以更加集中。而一些新兴的币种价格波动性更大,因此就可以设置更大的tickSpace确保swap时尽可能少的发生跨liquitidy。

添加流动性

要swap首先要有流动性,在完成添加流动性前我们先定义两个数据结构:GlobalState和TickInfo,GlobalState是一个全局变量,TickInfo存储边界tick(边界tick就是能整除tickSpace的tick)的相关信息。定义GlobalState为:

{ "price":"当前的价格", "tick":"当前的tick", "liquitidy""当前的全局流动性" }

Pool在添加流动性前必须被初始化,初始化会设置GlobalState的price和tick值。tick的取值为最接近价格P但不超过P的那个,如下图所示:

当P为1.00022时最接近P值且不超过的tick是2,如果P为1.0003则tick值是3。

定义TickInfo为:

{ "tick":"是哪个边界tick", "liquidityNet":"tick上的流动性净值" }

当tickSpace为4时,只有tick=0,4,8等这类能整除tickSpace的tick才有可能会存储TickInfo。当Position的liquitidy发生变化时liquidityNet的更新规则如下:

Position的tickLower的净值会加上Position的liquitidyDelta

Position的tickUpper的净值会减去Position的liquitidyDelta

当价格穿越这些边界tick时全局liquitidy会叠加tick的净值,比如有两个Position分别是Postion0[-8,0,10](-8和0表示上下边界,10表示流动性)和Position1[-4,4,15]

假定价格从tick为-9波动到tick=5,初始流动性gL=0。

当价格穿越tick=-8时,Positon0提供的流动性开始发挥作用,gL+=netL(-8)=10

当价格继续穿越-4时,Position1提供的流动性开始发挥作用,gL+=net(-4)=25

当价格继续穿越0时,Position0提供的流动性已经全部用完,gl+=net(0)=15

当价格继续穿越4时,Position1提供的流动性已经全部用完,gl+=net(5)=0

在完成Position信息和TickInfo信息的设置后需要解决一个问题,在流动性和价格上下边界已知的情况下用户需要往Pool中存放多少资产?如果Position在当前全局价格gPrice之上,如下所示

只需要往Pool中添加x资产,因为只有当价格上涨(y变多,x变少)的时候才会用到Position上的流动性,通过前文的数学公式可以算出需要存入的x的数量为:

如果Position在当前全局价格gPrice之下,如下所示

因为只有当价格下降(y变少,x变多)的时候才会用到Position上的流动性,因此只需要往Pool中添加y资产:

如果Position能覆盖当前全局gPrice,如下所示

需要分两段计算,从P(-4)到gPrice需要提供y资产,从gPrice到P(8)需要提供x资产

交易

假如Alice往Pool中注入一定数量的y资产yIn,那么能换出多少x资产呢?在集中流动性那一节中我们描述了如何进行逐段计算,结合跨tick时带来的全局流动性的增减,可以把交易通过如下的流程来计算:

(1)考察离gTick最近的边界tick对应的价格P,计算将价格从gPrice打到P所需要的y资产数量。如果比yIn要大,说明当往Pool中增加yIn的时候价格波动并没有导致跨liquitidy的产生。因此可以根据前文的变换公式先计算出,再计算出。

上图中在价格波动方向(输入y导致价格上涨)离gTick=2最近的边界tick是8,如果想要将价格从gPrice打到P(8)需要输入100个y,但实际输入的y数量是70,因此不会发生跨liquitidy。根据输入70个y可以计算出价格将会打到的位置(蓝色的gPrice标注的地方),然后根据价格的变化就算出能得到的x资产的数量。

(2)如果比yIn要小,说明当前流动性还满足不了兑换的需求,需要继续寻找下一个有流动性的地方来完成兑换。

在上图中有两个Position,这两个Position产生了交叉,实际的流动性分成了三段。将价格gPrice打到P(-4)需要30个y,而实际输入是150个y,因此需要继续考察是否还有流动性能提供兑换。然后发现在tick=-4和tick=0之间有流动性,能满足100个y的兑换需求,此时实际输入变成了150-30=120,依然无法满足,因此需要继续考察。然后发现在tick=0和tick=4之间有流动性能满足剩下的20个y的兑换需求。最后兑换的x的资产数量根据公式分段计算即可。

(3)如果流动性出现缺口如何处理?跟步骤(2)的算法类似,不再赘述。

流动性缺口会带来什么风险?攻击者将可以花费很小的代价使得价格产生较大的波动,对于依赖V3作为Oracle的一些期货合约项目来说需要关注这样的风险。

费用的捕获

V2中产生费用后会继续放在Pool中并生成LP Token,V3中由于流动性的分隔无法有一个统一的LP Token,因此V3中的仅仅只是记录每单位流动性捕获的手续费是多少。全局状态GlobleState有个属性fG用来记录每单位流动性捕获的手续费:

{ "fG":"每单位流动性捕获的手续费" }

不管交易发生在哪个流动性段内都会给fG累加单位手续费收益,因此这个值总是增长的。每个边界tick的TickInfo中也包含类似的属性fO:

{ "fO":"tick外部每单位流动性捕获的手续费" }

fO初始化时跟边界tick与gTick的位置有关,如下所示:

如果边界tick比gTick小,则fO=fG,否则fO=0。

fO的更新规则是当交易出现跨liquitidy时,跨过的那个边界tick的fO=fG-fO,如下所示:

当价格从gPrice波动到gPriceNew的过程中,根据前文所述出现了跨liquitidy,交易分成了两段。第一段从gPrice到P(0)的过程中产生手续费使得fG从5增长到了7。当跨到下一个liquitidy继续计算的时候会更新边界tick=0的fO,根据公式计算得到fO(0)=7。在第二阶段的交易过程中fG继续增长到了10,但是由于没有出现跨liquitidy所以没有任何一个边界tick的fO需要更新。

当价格又出现了反向波动时,第一阶段从gPrice波动到P(0),fG从10增长到了13,由于出现了跨liquitidy因此边界tick=0的fO需要更新,更新后的fO(0)=fG-fO(0)=13-7=6。第二阶段从P(0)波动到P(-4),fG从13增长到20,由于出现了跨liquitidy因此边界tick=-4的fO需要更新,更新后的fO(-4)=fG-fO(-4)=20-5=15。第三阶段从P(-4)波动到gPriceNew,fG从20增长到了23,由于没有出现跨liquitidy因此不需要更新任何边界tick的fO。

每个Position都有上边界(upper)和下边界(lower),有了边界tick的fO之后就可以计算任意一个Position捕获的手续费:

如果gPrice在Positon的下方,f(Position)=f(lower)-f(upper)。比如上图中的Position[0,4],则f[-4,4]=fO(-4)+fO(4)=6。检查一下6是否正确,当价格上涨时进入L(0,4)之前的fG是7,交易结束之后的fG是10,因此L(0,4)捕获的单位收益就是3。当价格下跌时,价格离开L(0,4)的时候fG=13,因此L(0,4)捕获的单位收益是3,所以两次捕获的总收益就是6。

如果gPrice在Position的上方,f(Position)=f(upper)-f(lower)。比如第一次价格上涨时,f[-8,-4]=fO[-4]+fO[-8]=0。

如果gPrice在Position内部,f(Position)=fG-f(lower)-f(upper)。比如第二次价格下降之后,f[-8,-4]=fG-fO[-8]-fO[-4]=23-5-15=3。检查一下,当价格从P(-4)波动到gPriceNew时fG增长了3,因此f[-8,-4]捕获的单位收益就是3。

结束语

UniswapV3是一次非常精彩的创新,这反应出Uniswap团队对于AMM的深刻理解。V3将极大的提高资金的利用率,灵活的费率设置也给用户更多的选择,至于是否能影响Curve的稳定币兑换的份额还需要实际观察。V3的Oracle由于使用了几何平均数会比V2的算术平均值更为准确。V交易网络手续费跟流动性集中的程度有关,如果流动性比较分散,则兑换时会出现跨liquitidy问题,带来的gas消耗会比V2高。流动性缺口的存在会使得低成本操控V3价格存在可能性,依赖V3作为Oracle的合约、抵押借贷项目都需要注意这样的风险。

本文来源:Deep Defi 原文作者:Deep Defi 责任编辑:Seven
声明:奔跑财经登载此文出于传递更多信息之目的,并不意味着赞同其观点或证实其描述。文章内容仅供参考,不构成投资建议。投资者据此操作,风险自担。

评论

还没有人评论,快来评论吧

相关新闻

比特币减半生存手册

2024-04-20 11:06
随着比特币完成减半,加密世界四年一次的「风暴时间」正式拉开序幕。>
奔跑财经 28149

Meme 造富热潮加持,2024年的公链之争,SOL能颠覆ETH吗?

2024-03-29 15:31
公链杀手还是FOMO情绪使然?>
奔跑财经 82385

加密世界探索:破顶的比特币还是否需要从ETN中获益?

2024-03-13 14:50
英国致力于将加密货币纳入监管框架,ETN可为监管提供实践案例参考。全球加密市场态度逐渐积极,但监管政策进步也至关重要。>
奔跑财经 101954