UniV3 Share
核心公式:
(x+PaL)∗(y+L∗Pb)=L2
核心公式可以看作是x∗y=K曲线的平移。偏移量xoffset=PaL,yoffset=L⋅Pb。
偏移量事实上反映了这个仓位的杠杆大小。偏移量越大,说明加的杠杆越大。
衍生公式:
tick的定义:
ic=⌊log1.0001P⌋,向下取整
*Price可以为任何值,但是tick只能是离散的值。
*x,y为包含虚拟流动性的部分,总的x,y
P=xy
已知L,P1,P2,求δx,δy
δx=P1L−P2L
δy=L⋅P1−L⋅P2
已知δx,L,P1, 求P2:
P2=L+δx⋅P1L⋅P1
已知δy,L,P1, 求P2:
P2=P1−Lδy
再一次SWAP过程中,L保持不变,手续费的积累不会影响到L的值。
已知δx,P1,P2, 求L:
L=P11−P21δx
已知δy,P1,P2,求L:
L=P1−P2δy
公式推导
L的含义: L反应的是价格变化的阻力,也就是流动性深度。L越大,价格变化的阻力也就越大。
L=dPdy
推导过程:
第一步:在一次swap过程中,L保持不变,公式可以写为如下
(x+xoffset)⋅(y+yoffset)=L2
则:
x=y+yoffsetL2−xoffset
y=x+xoffsetL2−yoffset
价格P的定义为:
P=−dxdy,dy<0
则:
P=(x+xoffset)2L2
则:
P=(x+xoffset)L
同理,带入x+xoffset=y+yoffsetL2得到:
P=Ly+yoffset
则y等于:
y=L∗P−yoffset
当实际流动性y=0时,价格达到PA,此时yoffset=L∗PA
当实际流动性x=0时,价格达到PB,此时xoffset=L/PB
则同时求导数可得:
dPdy=L
Tick的定义与模型
整个UniV3的核心逻辑都是围绕着价格展开,即Price。用户提供流动性时,需要指定价格区间;用户swap时,要么指定价格,要么指定数量。具体的swap过程中也是一个tick,一个tick的去swap。
tick可以看成是一条坐标轴,坐标轴上的每一个刻度都是一个tick。坐标轴的刻度是有限的。

tick的定义如下:
ic=⌊log1.0001P⌋,向下取整
tick的取值范围为:
(−887272,887271)
相对应的P的取值范围:
(4295128739,1461446703485210103287273052203988822378723970342),1018精度
在tick的模型设计里,其与流动性深度相关。在每一个已被初始化的tick上,都维护了如下的变量:
Type | Variable Name | Notation |
---|
int128 | liquidityNet | ΔL |
uint128 | liquidityGross | Lg |
uint256 | feeGrowthOutside0X128 | fo0 |
uint256 | feeGrowthOutside1X128 | fo1 |
uint256 | secondsOutside | so |
uint256 | tickCumulativeOutside | io |
uint256 | secondsPerLiquidityOutsideX128 | slo |
- liqudityNet与Liquidity Gross
这个值记录的是当cross 过一个tick时,流动性应该增减的部分。因为用户添加流动性时,指定了price range,也即是两个tick。
如下图所示,alice和bob分别在a-c和b-d的范围内添加了流动性L1=500,L2=700.
则 UniV3更新tickA时,其ΔL = +500, Lg = 500;
针对tickC,其ΔL = -500, Lg = 500;
针对tickB,其ΔL = +700, Lg = 700;
针对tickD,其ΔL = -700, Lg = 700; 左右皆为闭区间。即左右的tick都会被初始化。

每一个tick都对应于一个全局变量liquidity,该全局变量liquidity是当前tick的总流动性。用于swap的计算
比如:
当tick < a时,liquidity = 0;
当tick > a, tick < b时,liquidity = 0+500;即cross tickA
当tick > b, tick < c时,liquidity = 500+700; 即cross tickB
当tick > c, tick < d时,liquidity = 1200+(-500);即cross tickC
当tick > d时,liquidity = 700+(-700), 即cross tickD
如果price反方向移动,即从右到左移动,则将加 liquidityDelta变为 减 liquidityDelta即可;
liquidity Gross的作用主要是用来判断当一个用户移除流动性之后,该tick上对应的流动性是不是已经为0;如果该tick上对应的流动为0,则需要删除该tick。
- feeGrowthOutside0x128和feeGrowthOutside1X128
这两个量主要是用于帮助收取手续费的。
UniV3里面的手续费与如下几个因素有关:1. 提供的price range;2. 提供的流动性;3. swap过程中是否经过了所提供的price range;4. 做LP的时间长度;
可以看到UniV3的手续费收取,跟UniV2的手续费收取的模式完全不一样。事实上,UniV3使用的是类似于MasterChef的模式来收取手续费。
R⋅l⋅t=t1∑t2⎩⎨⎧L(t)1,0,0,ilower<ic<iupperic<iloweric>iuppper⎭⎬⎫
上面的式子可以转换为:
R⋅l⋅(t=t1∑t2L(t)1−t=t1∑t2⎩⎨⎧L(t)1,0,ic<iloweric>=ilower⎭⎬⎫−t=t1∑t2⎩⎨⎧L(t)1,0,ic>=iloweric<ilower⎭⎬⎫)
即:
secondsPerLiquidityInside=secondsPerLiquidity−secondsPerLiquidityBelow(lowerTick)−secondsPerLiquidityAbove(upperTick)
其中,secondsPerLiquidity作为全局变量在oracle中存储,每一次操作,如swap,mint,burn都会更新该值。
对于secondsPerLiquidityBelow(lowerTick),UniV3的解决思路是,在tick中保存一个名为secondsPerLiquidityOutsideX128的变量。其具体的outside含义如下:
tick中保存的值,如secondsPerLiquidityOutsideX128,feeGrowthOutside0x128和feeGrowthOutside1X128等都是保存的outside的值。“outside”的含义是,当前价格的另一侧。

上图中,currentTick < tickI, 所以outside保存的是右侧的所有手续费与流动性之比之和fee
即:
fo(i)=i=i0∑imaxLfee

上图中,currentTick > tickI, 所以outside保存的是左侧的所有手续费之和fee
即:
fo(i)=i=imin∑i0Lfee

即:
fr=fg−fb(il)−fa(iu)fg=i=imin∑imaxLfee
*针对每一个tick,其L都是定值,可以认为是常量。
对于计算(lowerTick - upperTick)之间的所有手续费fee,此时currentTick在lowerTick和upperTick之间。则可以通过:
全局的fee - feeGrowthOutside0x128(lowerTick) - feeGrowthOutside1X128(upperTick)
那么,应该如何计算feeOutside呢?在整个UniV3的设计中,当且仅当currentTick穿过某一个tick时,才会导致该tick上记录的feeOutside翻转。
其公式为:
fo(i):=fg−fo(i)
即:
全局的手续费/L-左侧的所有手续费/L=右侧的所有手续费/L
Position的定义与模型
当一个用户像UniV3中添加流动性时,都会生成一个唯一的POSITION KEY:

key = keccak256(address(user),int24(leftTick),int24(rightTick))
每一个POSITION上,都会track如下变量:
Type | Variable Name | Notation |
---|
uint128 | liquidity | l |
uint256 | feeGrowthInside0LastX128 | fr,0(t0) |
uint256 | feeGrowthInside1LastX128 | fr,1(t0) |
uint256 | tokensOwed0 | t0 |
uint256 | tokensOwed1 | t1 |
其中,liquidity可以看作是用户mint时得到的lp token数量,也可以认为是x⋅y, 即L
POSITION的一大作用是,用于记录用户提供的流动性所应该收取的手续费数量。具体的手续费token数值会存储在tokensOwned中。
其中,feeGrowthInside0LastX128即为fr, 其计算过程如下:
fr=fg−fb(il)−fa(iu)fb(il)={fg−fb(il),fb(il),ic<iloweric>=ilower}fa(iu)={fb(il),fg−fb(il),ic<iupperic>=iupper}
具体的tokenOwned则为:
tokensOwned=(fr−fr‘)⋅l
Swap 过程

当在同一个tick里面进行swap时,会进行如下计算:
- 找到下一个tick(initialized) ticknext
- 根据当前tick tickcurr, ticknext及L,使用公式计算出δx,δy
即:已知L,P1,P2,求δx,δy
δx=P1L−P2L
δy=L⋅P1−L⋅P2
- 判断token remaining是否大于δx,
tokenremaining=tokenremaining−δx
在进入下一个循环。
- 如果小于,则:
已知δx=tokenremaining,L,P1, 求P2:
P2=L+δx⋅P1L⋅P1
- 然后根据求出的P2, 再次利用上述公式,求的δx,δy
即:已知L,P1,P2,求δx,δy
δx=P1L−P2L
δy=L⋅P1−L⋅P2
- 手续费扣除:
UniV3里面扣除手续费也是在tokenIn中,直接减去相应的手续费部分,然后剩余的参与计算。
- 全局变量更新:

state.feeGrowthGlobalX128 += FullMath.mulDiv(step.feeAmount, FixedPoint128.Q128, state.liquidity);
- tick更新:
如果不需要cross tick:
则:
ic=⌊log1.0001P2⌋,向下取整
如果需要cross tick:
则:
L=L+δL
tick=ticknext−1