朋友们大家好,今天我们继续读UniswapV3中library中的文件。这一系列的library是帮助更好地理解pool的机制,希望你能深呼吸耐下性子,我们一起读完。
LiquidityMath.sol
这个文件非常短只有17行!
从名字可以看出来这是一个用于计算流动性的库。用于完成流动性的增加或者减少。

函数主体只有五行,x是初始流动性,y是要增加或者减少的流动性,最终输出的是完成计算后的最新流动性。
当y小于零,也就是要减少流动性,就从x中减少-y,如果结果溢出了返回LS代表liquidity Subtraction算减法的时候出错。当y大于或者等于0,z=x+y,如果结果溢出了,返回LA意思是liquidity Addition,提示算加法溢出了。
非常简单!继续看Tick.sol。
Tick.sol
这里是集中流动性的总账本,每个tick的状态都存在这里,包括流动性总额、净流动性、手续费增长、预言机数据等。理解了这个库,就能理解仓位区间是被如何记录和更新的。
Tick.sol导入的四个库文件,分别是LowGasSafeMath、SafeCast、TickMath和LiquidityMath,这些我们都已经分析过了,不必慌张。
13行和14行,把安全计算和安全转换加载在int256格式上,使其可以调用对应的函数。

17到37行定义了一个结构体叫Info,一共8个字段。

liquidityGross:该tick对应的总流动性,所有把这个tick设为区间上界或下界的仓位的流动性总和。
liquidityNet:需要增加或减少的流动性
feeGrowthOutside0X128:这里有个关键词是Outside,在价格轴上以tick为界,当前价格所在的一侧是inside,另外一边是Outside,所以这个字段是指和当前价格不在一侧的,从tick到正无穷或者从0到tick这段价格对应的手续费。
feeGrowthOutside1X128:和上边字段的差别是池中两种代币,是token0还是token1。
tickCumulativeOutside:tick乘以时间的累计值
secondsPerLiquidityOutsideX128:每单位流动性的秒数积累
secondsOutside:纯粹的时间积累
initialized:初始化,当第一次有仓位把这个tick设为边界时,initialized被标记为true,当所有引用这个tick的仓位都被移除时,initialized变回false。
43行进入函数tickSpacingToMaxLiquidityPerTick,从名字推测这是一个基于给定的刻度间距,计算每个Tick上允许存储的最大流动性。

传入的参数是tickSpacing,出参是返回一个uint128的数值意思是对应每个tick最大的最大流动性。
限制单个tick的最大流动性是为了防止溢出,因为单个tick流动性过大时,再和别的tick相加时,可能会导致溢出,所以这里是在提前算出来安全上限。
45和46行在基于实际的tickSpacing计算实际的最大和最小tick是多少。TickMath.MIN_TICK和TickMath.MAX_TICK在TickMath.sol中有介绍,分别是-887272和887272。为什么是887272,因为1.0001的887272次方结果最接近2的128次方的值,在V3中用Q64.96的定点数(整数部分占64位,小数部分占96位)来存储代币价格的平方根√P,理论上√P的最大值就是2的64次方,因此价格P的理论最大值对应2的128次方。
887272是理论最大值,但是由于tickSpacing不同,实际上的最大最小值会比这个小。举个例子:
比如tickSpacing=10,TickMath.MAX_TICK=887272,887272 / 10 = 88727.2,整数除法向零截断为88727,再乘10得到实际的maxTick=887270,比理论最大值略小。
47行在计算numTicks,这是一个小学数据经典问题:已知一条街总长度和每两棵树之间的距离,求解这条街一共能种几棵树?
48行最终返回值是每个tick上的最大流动性,也就是要把liquidityGross均分给每一个tick,由于liquidityGross是uint128类型,所以每个tick的最大流动性等于type(uint128).max / numTicks。
函数getFeeGrowthInside
函数名从字面意思来解释是说这是一个查询某个价格区间内累计手续费的函数。全局手续费分布在整个价格轴上,计算某个区间的手续费=全局手续费-下限以下手续费-上限以上手续费。

入参6个字段,self是映射到tick的info存储了每个价格边界点的数据、tickLower和tickUpper是价格区间的上界和下界、tickCurrent池子当前价格、feeGrowthGlobal0X128和feeGrowthGlobal1X128是池子中两种代币全局积累的手续费。
出参返回两个字段,分别是token0和token1在指定价格区间内每单位流动性对应的累计手续费。


定义6个变量,分别是价格上下限对应tick的info和上界以上&下界以下的累计手续费。

先看72行开始如何计算价格下界以下的手续费。
如果当前价格比下界高,current在lower上方,所以lower以下的部分是outside,正好是需要的下界以下的手续费。
如果反过来当前价格比下界低,current在lower下方,lower上方的部分是outside,如果我们还需要算下界以下的手续费就用全部手续费减掉outside的部分。

同理算tickAbove也是一样的意思,从总手续费中减掉下界以下和上界以上,获得的就是区间lower到above区间内的累计手续费。
函数update
从函数名字来看,很明显这是一个更新函数。

入参11个字段,分别是:
self:一个mapping关系,根据传入的tick可以获得tick的info信息
tick:传入的tick值
liquidityDelta:本次流动性的变化量
feeGrowthGlobal0X128:token0累计的全局手续费
feeGrowthGlobal1X128:token1累计的全局手续费
secondsPerLiquidityCumulativeX128:每单位流动性的秒数积累
tickCumulative:当前tick乘以时间的累计和,主要用于计算tick的算术平均
time:当前区块的时间戳
upper:这是一个布尔类型的值,用于表示当前更新的tick是仓位的上界还是下界,true表示上界,false表示下界
maxLiquidity:最大流动性
出参返回一个布尔值类型的flipped,字面含义是翻转,意思返回该tick是否发生了翻转——流动性从无变有或者从有变无。

123行进入函数体,获取tick对应的info信息存入info字段中。
定义了两个字段记录流动性的变化前后,分别是before和after,before可以直接读取对应tick的info中存储的总流动性费;after就是之前的总流动性加上这次的变量,因此调用了liquidityMath方法,把这次的变量delta加上去。
128行的校验要求更新后的总流动性依然要小于或等于单个tick的最大流动性上限。
flipped是输出变量,tick是否发生了翻转:当总流动性前后有一个为0时,flipped为真。

132行,是在做首次初始化tick的特殊处理,所以首先校验liquidityGrossBefore之前的总流动性是否为0。
这里有一个核心假设:无论历史上手续费在价格轴上怎么分布,现在都统一认为它们在当前tick的下方。其实对于之前没有流动性的tick来说,历史手续费发生在哪里根本与它无关,规定都在其价格下方是方便后续价格穿过tick的时候,能够计算出区间内手续费。
所以当前价格大于或等于tick时,也就是tickCurrent在tick上方时,outside是tick的下方恰好可以赋值为feeGrowthGlobal0X128。
如果当前价格小于tick,此时feeoutside指的是tick到无穷大部分,而全局手续费依然是指tick下方部分,所以feeoutside=0。代码实际上跳过了赋值,因为存储默认就是0。
在理解手续费之后时间参数的传入并更新就没有什么特别的了。

144行在基础信息传入后,将更新后的流动性传入写进总流动性中。
147行是在更新净流动性,如果upper是LP提供流动性的下界,那么价格从左向右穿过意味着价格上涨到了区间内,需要增加流动性,价格从右向左穿过tick意味着价格跌出区间需要移除流动性。反之如果upper是价格区间的上界,增加或者移除流动性就是反过来的。
函数clear
这是一个clear函数,很明显是清除数据用的,删除了对应tick的info信息。

函数cross
函数名叫cross,是指当价格“穿过”tick时被调用的。主要完成两个动作,一是翻转tick的所有积累值,让它们反映新的另一侧,二是返回liquidityNet便于后续更新池子中中流动性。

178和179是在做outside的翻转,当前价格穿过tick,对于tick来说outside的位置就变化了,需要基于全量的手续费算出“另一侧”的累计手续费。
流动性加权的秒数累计和tick的时间累计、纯秒数累计和手续费翻转逻辑一致,也是用全局-旧外部累计。
最后183行返回净流动性,调用cross的pool拿到这个值之后,会更新当前的总流动性。
uniswapV3-core代码库里的tick和liquidityMath我们就分析完了,tick是超级核心的库文件,弄明白outside、翻转机制、增加或者减少流动性对于我们彻底搞明白pool在做什么非常重要。朋友们下次再见!
夜雨聆风