第三篇:Uniswap V3 价格机制与Tick系统源码分析
深入解析Tick系统的设计与实现,以及TickBitmap的极致优化
📋 目录
1. Tick系统概述
1.1 为什么需要Tick系统
V2的问题:
价格是连续的:P ∈ (0, ∞)
无法有效管理集中流动性
V3的解决方案:
将连续价格空间离散化
P = 1.0001^tick
tick ∈ [-887272, 887272]
1.2 Tick的核心作用
┌─────────────────────────────────────────────────┐
│ Tick系统的三大作用 │
├─────────────────────────────────────────────────┤
│ 1. 价格离散化 │
│ - 将无限价格空间映射到有限个Tick │
│ - 每个Tick代表一个精确的价格点 │
│ │
│ 2. 流动性管理 │
│ - 记录每个Tick的流动性变化 │
│ - 支持集中流动性的添加/移除 │
│ │
│ 3. 效率优化 │
│ - TickBitmap快速查找下一个激活的Tick │
│ - 避免遍历所有Tick │
└─────────────────────────────────────────────────┘
1.3 Tick间距的设计
// 不同手续费对应不同Tick间距
mapping(uint24 => int24) public override feeAmountTickSpacing;
初始值:
feeAmountTickSpacing[500] = 10; // 0.05% fee
feeAmountTickSpacing[3000] = 60; // 0.3% fee
feeAmountTickSpacing[10000] = 200; // 1% fee
为什么需要间距?
- 减少存储:
不使用间距:
可用Tick数 = 887272 - (-887272) + 1 = 1,774,545个
存储需求 = 1,774,545 * 256 bytes = 454 MB
使用间距60:
可用Tick数 = 1,774,545 / 60 = 29,576个
存储需求 = 29,576 * 256 bytes = 7.6 MB
节省:98.3%
- 匹配波动性:
低波动(稳定币):
- 需要精细价格控制
- tickSpacing = 10
高波动(山寨币):
- 不需要太精细
- tickSpacing = 200
- 防止溢出:
// 每个Tick的最大流动性
uint128 maxLiquidityPerTick = type(uint128).max / numTicks;
如果tickSpacing太小 -> numTicks太大 -> maxLiquidityPerTick太小
2. Tick数据结构详解
2.1 Tick.Info结构
struct Info {
// 1. 流动性数据(32 bytes)
uint128 liquidityGross; // 总流动性(所有仓位的和)
int128 liquidityNet; // 净流动性变化
// 2. 手续费追踪(64 bytes)
uint256 feeGrowthOutside0X128; // token0外部手续费增长
uint256 feeGrowthOutside1X128; // token1外部手续费增长
// 3. 预言机数据(32 bytes)
int56 tickCumulativeOutside; // 累计Tick
uint160 secondsPerLiquidityOutsideX128; // 每流动性秒数
uint32 secondsOutside; // 累计秒数
// 4. 状态标记(1 byte)
bool initialized; // 是否已初始化
}
// 总计:129 bytes(占用5个存储槽)
2.2 liquidityGross vs liquidityNet
liquidityGross(总流动性):
// 所有引用此Tick的仓位的流动性之和
liquidityGross = sum(所有仓位的liquidity)
用途:
1. 判断Tick是否初始化(liquidityGross > 0)
2. 检查是否超过maxLiquidityPerTick
liquidityNet(净流动性):
// 跨越Tick时全局流动性的变化量
liquidityNet = 向上跨越时的变化
计算规则:
- 作为下边界(tickLower): liquidityNet += liquidityDelta
- 作为上边界(tickUpper): liquidityNet -= liquidityDelta
示例:
场景:3个仓位
仓位A:[tick=100, tick=200], liquidity=1000
仓位B:[tick=100, tick=300], liquidity=500
仓位C:[tick=150, tick=200], liquidity=300
Tick 100(两个仓位的下边界):
liquidityGross = 1000 + 500 = 1500
liquidityNet = +1000 + 500 = +1500 (向上跨越时增加)
Tick 150(一个仓位的下边界):
liquidityGross = 300
liquidityNet = +300
Tick 200(两个仓位的上边界):
liquidityGross = 1000 + 300 = 1300
liquidityNet = -1000 - 300 = -1300 (向上跨越时减少)
Tick 300(一个仓位的上边界):
liquidityGross = 500
liquidityNet = -500
跨越Tick时的流动性更新:
// 向上跨越(zeroForOne = false)
if (price crosses tick upward) {
globalLiquidity += tick.liquidityNet;
}
// 向下跨越(zeroForOne = true)
if (price crosses tick downward) {
globalLiquidity -= tick.liquidityNet;
}
2.3 feeGrowthOutside的精妙设计
核心概念:相对位置追踪
feeGrowthOutside = "另一侧"的手续费增长
"另一侧"的定义取决于当前价格:
- 如果 currentTick >= tick: Outside = 下方
- 如果 currentTick < tick: Outside = 上方
初始化规则:
if (liquidityGrossBefore == 0) { // 首次初始化
if (tick <= tickCurrent) {
// Tick在当前价格下方
// Outside = 下方 = 从0到现在的所有手续费
info.feeGrowthOutside0X128 = feeGrowthGlobal0X128;
info.feeGrowthOutside1X128 = feeGrowthGlobal1X128;
} else {
// Tick在当前价格上方
// Outside = 上方 = 0(未来的手续费)
info.feeGrowthOutside0X128 = 0;
info.feeGrowthOutside1X128 = 0;
}
}
跨越时的翻转:
function cross(
mapping(int24 => Tick.Info) storage self,
int24 tick,
uint256 feeGrowthGlobal0X128,
uint256 feeGrowthGlobal1X128,
...
) internal returns (int128 liquidityNet) {
Info storage info = self[tick];
// 翻转Outside值(因为"另一侧"变了)
info.feeGrowthOutside0X128 = feeGrowthGlobal0X128 - info.feeGrowthOutside0X128;
info.feeGrowthOutside1X128 = feeGrowthGlobal1X128 - info.feeGrowthOutside1X128;
// 翻转预言机数据
info.tickCumulativeOutside = tickCumulative - info.tickCumulativeOutside;
info.secondsPerLiquidityOutsideX128 = secondsPerLiquidityCumulativeX128 - info.secondsPerLiquidityOutsideX128;
info.secondsOutside = time - info.secondsOutside;
return info.liquidityNet;
}
2.4 计算仓位内的手续费增长
function getFeeGrowthInside(
mapping(int24 => Tick.Info) storage self,
int24 tickLower,
int24 tickUpper,
int24 tickCurrent,
uint256 feeGrowthGlobal0X128,
uint256 feeGrowthGlobal1X128
) internal view returns (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) {
Info storage lower = self[tickLower];
Info storage upper = self[tickUpper];
// 步骤1:计算下方的手续费增长
uint256 feeGrowthBelow0X128;
if (tickCurrent >= tickLower) {
// 当前价格在tickLower之上
// Below = Outside(因为Outside指向下方)
feeGrowthBelow0X128 = lower.feeGrowthOutside0X128;
} else {
// 当前价格在tickLower之下
// Below = Total - Outside(因为Outside指向上方)
feeGrowthBelow0X128 = feeGrowthGlobal0X128 - lower.feeGrowthOutside0X128;
}
// 步骤2:计算上方的手续费增长
uint256 feeGrowthAbove0X128;
if (tickCurrent < tickUpper) {
// 当前价格在tickUpper之下
// Above = Outside(因为Outside指向上方)
feeGrowthAbove0X128 = upper.feeGrowthOutside0X128;
} else {
// 当前价格在tickUpper之上
// Above = Total - Outside(因为Outside指向下方)
feeGrowthAbove0X128 = feeGrowthGlobal0X128 - upper.feeGrowthOutside0X128;
}
// 步骤3:Inside = Total - Below - Above
feeGrowthInside0X128 = feeGrowthGlobal0X128 - feeGrowthBelow0X128 - feeGrowthAbove0X128;
feeGrowthInside1X128 = feeGrowthGlobal1X128 - feeGrowthBelow1X128 - feeGrowthAbove1X128;
}
图解:
tickUpper
↓
─────────────────────────┼───────────── feeGrowthAbove
│
tickCurrent
↓
┼
│ feeGrowthInside
tickLower
↓
─────────────────────────┼───────────── feeGrowthBelow
│
feeGrowthInside = feeGrowthGlobal - feeGrowthBelow - feeGrowthAbove
3. TickBitmap极致优化
3.1 问题的提出
场景:在swap中需要找到下一个有流动性的Tick
朴素方案:
// ❌ 极其低效
int24 nextTick = currentTick + tickSpacing;
while (ticks[nextTick].liquidityGross == 0) {
nextTick += tickSpacing;
}
时间复杂度:O(n),其中n可能达到29,576
Gas成本:每次SLOAD约2100 gas,总计可能数万gas
V3的方案:TickBitmap
// ✓ 极其高效
(int24 nextTick, bool initialized) = tickBitmap.nextInitializedTickWithinOneWord(...);
时间复杂度:O(1)或O(log n)
Gas成本:2-3次SLOAD,约6000 gas
3.2 TickBitmap数据结构
// 位图映射
mapping(int16 => uint256) public override tickBitmap;
结构:
- Key: int16(word位置)
- Value: uint256(256个bit)
每个bit代表一个Tick是否初始化:
- bit = 1: Tick已初始化(有流动性)
- bit = 0: Tick未初始化(无流动性)
索引计算:
function position(int24 tick) private pure returns (int16 wordPos, uint8 bitPos) {
// wordPos:哪个word(256个tick为一组)
wordPos = int16(tick >> 8); // tick / 256
// bitPos:word内的哪一位
bitPos = uint8(tick % 256);
}
示例:
tick = 1000:
wordPos = 1000 / 256 = 3
bitPos = 1000 % 256 = 232
tick = -500:
wordPos = -500 / 256 = -2
bitPos = -500 % 256 = 12
3.3 flipTick操作
function flipTick(
mapping(int16 => uint256) storage self,
int24 tick,
int24 tickSpacing
) internal {
// 确保tick是tickSpacing的倍数
require(tick % tickSpacing == 0);
// 计算位置
(int16 wordPos, uint8 bitPos) = position(tick / tickSpacing);
// 创建mask
uint256 mask = 1 << bitPos;
// XOR翻转对应bit
self[wordPos] ^= mask;
}
工作原理:
假设 bitPos = 5
mask = 1 << 5 = 0b...00100000
原值 = 0b...10101010
mask = 0b...00100000
XOR ─────────────────
结果 = 0b...10001010
↑ 第5位被翻转
应用场景:
// 添加流动性时
if (流动性从0变为非0) {
tickBitmap.flipTick(tickLower); // 设置为1
tickBitmap.flipTick(tickUpper);
}
// 移除流动性时
if (流动性从非0变为0) {
tickBitmap.flipTick(tickLower); // 设置为0
tickBitmap.flipTick(tickUpper);
}
3.4 nextInitializedTickWithinOneWord算法
目标:找到下一个初始化的Tick
function nextInitializedTickWithinOneWord(
mapping(int16 => uint256) storage self,
int24 tick,
int24 tickSpacing,
bool lte // less than or equal(向左查找)or greater than(向右查找)
) internal view returns (int24 next, bool initialized) {
// 压缩tick(考虑tickSpacing)
int24 compressed = tick / tickSpacing;
if (tick < 0 && tick % tickSpacing != 0) compressed--;
if (lte) {
// 向左查找(寻找 <= currentTick 的初始化Tick)
(int16 wordPos, uint8 bitPos) = position(compressed);
// 创建mask:保留bitPos及其右边的所有bit
// 例如 bitPos=5: mask = 0b...00111111
uint256 mask = (1 << bitPos) - 1 + (1 << bitPos);
// 只保留感兴趣的bit
uint256 masked = self[wordPos] & mask;
initialized = masked != 0;
next = initialized
? (compressed - int24(bitPos - BitMath.mostSignificantBit(masked))) * tickSpacing
: (compressed - int24(bitPos)) * tickSpacing;
} else {
// 向右查找(寻找 > currentTick 的初始化Tick)
(int16 wordPos, uint8 bitPos) = position(compressed + 1);
// 创建mask:保留bitPos及其左边的所有bit
// 例如 bitPos=5: mask = 0b11111111...11100000
uint256 mask = ~((1 << bitPos) - 1);
// 只保留感兴趣的bit
uint256 masked = self[wordPos] & mask;
initialized = masked != 0;
next = initialized
? (compressed + 1 + int24(BitMath.leastSignificantBit(masked) - bitPos)) * tickSpacing
: (compressed + 1 + int24(type(uint8).max - bitPos)) * tickSpacing;
}
}
3.5 BitMath库
mostSignificantBit(MSB):
// 找到最高位的1
function mostSignificantBit(uint256 x) internal pure returns (uint8 r) {
require(x > 0);
// 二分查找
if (x >= 0x100000000000000000000000000000000) { x >>= 128; r += 128; }
if (x >= 0x10000000000000000) { x >>= 64; r += 64; }
if (x >= 0x100000000) { x >>= 32; r += 32; }
if (x >= 0x10000) { x >>= 16; r += 16; }
if (x >= 0x100) { x >>= 8; r += 8; }
if (x >= 0x10) { x >>= 4; r += 4; }
if (x >= 0x4) { x >>= 2; r += 2; }
if (x >= 0x2) r += 1;
}
示例:
mostSignificantBit(0b...010100) = 4
↑ 最高位的1在第4位
leastSignificantBit(LSB):
// 找到最低位的1
function leastSignificantBit(uint256 x) internal pure returns (uint8 r) {
require(x > 0);
r = 255;
if (x & type(uint128).max > 0) { r -= 128; } else { x >>= 128; }
if (x & type(uint64).max > 0) { r -= 64; } else { x >>= 64; }
if (x & type(uint32).max > 0) { r -= 32; } else { x >>= 32; }
if (x & type(uint16).max > 0) { r -= 16; } else { x >>= 16; }
if (x & type(uint8).max > 0) { r -= 8; } else { x >>= 8; }
if (x & 0xf > 0) { r -= 4; } else { x >>= 4; }
if (x & 0x3 > 0) { r -= 2; } else { x >>= 2; }
if (x & 0x1 > 0) r -= 1;
}
示例:
leastSignificantBit(0b...010100) = 2
↑ 最低位的1在第2位
3.6 性能分析
场景对比:
场景:在10,000个Tick中找到下一个初始化的Tick
方案1:遍历查找
for (int24 i = currentTick; i <= MAX_TICK; i += tickSpacing) {
if (ticks[i].liquidityGross > 0) return i;
}
Gas成本:
- 最坏情况:10,000次SLOAD = 21,000,000 gas
- 平均情况:5,000次SLOAD = 10,500,000 gas
方案2:TickBitmap
tickBitmap.nextInitializedTickWithinOneWord(...)
Gas成本:
- 最好情况:1次SLOAD = 2,100 gas
- 最坏情况:2次SLOAD = 4,200 gas
性能提升:约5000倍!
4. Tick跨越机制
4.1 跨越流程
// 在swap函数中
while (state.amountSpecifiedRemaining != 0 && state.sqrtPriceX96 != sqrtPriceLimitX96) {
StepComputations memory step;
// 步骤1:找到下一个Tick
(step.tickNext, step.initialized) = tickBitmap.nextInitializedTickWithinOneWord(
state.tick,
tickSpacing,
zeroForOne
);
// 步骤2:计算在当前Tick内的交换
(state.sqrtPriceX96, step.amountIn, step.amountOut, step.feeAmount) = SwapMath.computeSwapStep(
state.sqrtPriceX96,
(zeroForOne ? step.tickNext < TickMath.MIN_TICK : step.tickNext > TickMath.MAX_TICK)
? sqrtPriceLimitX96
: TickMath.getSqrtRatioAtTick(step.tickNext),
state.liquidity,
state.amountSpecifiedRemaining,
fee
);
// 步骤3:更新累计值
state.amountSpecifiedRemaining -= (step.amountIn + step.feeAmount).toInt256();
state.amountCalculated = state.amountCalculated.sub(step.amountOut.toInt256());
// 步骤4:如果到达边界,跨越Tick
if (state.sqrtPriceX96 == TickMath.getSqrtRatioAtTick(step.tickNext)) {
if (step.initialized) {
// 跨越Tick,更新流动性
int128 liquidityNet = ticks.cross(
step.tickNext,
feeGrowthGlobal0X128,
feeGrowthGlobal1X128,
secondsPerLiquidityCumulativeX128,
tickCumulative,
time
);
// 更新全局流动性
if (zeroForOne) liquidityNet = -liquidityNet;
state.liquidity = LiquidityMath.addDelta(state.liquidity, liquidityNet);
}
// 移动到下一个Tick
state.tick = zeroForOne ? step.tickNext - 1 : step.tickNext;
} else {
// 没有到达边界,更新Tick(不跨越)
state.tick = TickMath.getTickAtSqrtRatio(state.sqrtPriceX96);
}
}
4.2 跨越时的流动性更新
价格向上移动(买入token1):
before: ────┼────[Position]────┼────
lower upper
↓
after: ────┼────[Position]────┼────
lower ←current upper
跨越tickLower:
globalLiquidity += tickLower.liquidityNet(正值)
跨越tickUpper:
globalLiquidity += tickUpper.liquidityNet(负值)
价格向下移动(卖出token1):
before: ────┼────[Position]────┼────
lower upper
↓
after: ────┼────[Position]────┼────
lower current→ upper
跨越tickUpper:
globalLiquidity -= tickUpper.liquidityNet(相当于加负的负值=正值)
跨越tickLower:
globalLiquidity -= tickLower.liquidityNet(相当于减正值)
5. 手续费在Tick中的追踪
5.1 全局手续费增长率
// 全局手续费增长率(每单位流动性)
uint256 public override feeGrowthGlobal0X128;
uint256 public override feeGrowthGlobal1X128;
// 每次swap后更新
feeGrowthGlobal0X128 += feeAmount0 * FixedPoint128.Q128 / liquidity;
feeGrowthGlobal1X128 += feeAmount1 * FixedPoint128.Q128 / liquidity;
5.2 feeGrowthOutside的维护
初始化时:
if (tick <= tickCurrent) {
// Tick在当前价格下方,Outside=下方=历史所有
feeGrowthOutside0X128 = feeGrowthGlobal0X128;
} else {
// Tick在当前价格上方,Outside=上方=0
feeGrowthOutside0X128 = 0;
}
跨越时:
// 翻转Outside值
info.feeGrowthOutside0X128 = feeGrowthGlobal0X128 - info.feeGrowthOutside0X128;
5.3 计算仓位应得手续费
// 步骤1:获取仓位内的手续费增长
(uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) =
ticks.getFeeGrowthInside(tickLower, tickUpper, tick, ...);
// 步骤2:计算增量
uint256 feeGrowthInside0DeltaX128 = feeGrowthInside0X128 - position.feeGrowthInside0LastX128;
uint256 feeGrowthInside1DeltaX128 = feeGrowthInside1X128 - position.feeGrowthInside1LastX128;
// 步骤3:计算应得手续费
uint128 tokensOwed0 = FullMath.mulDiv(feeGrowthInside0DeltaX128, position.liquidity, FixedPoint128.Q128);
uint128 tokensOwed1 = FullMath.mulDiv(feeGrowthInside1DeltaX128, position.liquidity, FixedPoint128.Q128);
6. 边界条件与安全检查
6.1 Tick范围限制
int24 internal constant MIN_TICK = -887272;
int24 internal constant MAX_TICK = 887272;
require(tickLower >= MIN_TICK && tickLower < MAX_TICK);
require(tickUpper > MIN_TICK && tickUpper <= MAX_TICK);
require(tickLower < tickUpper);
6.2 最大流动性限制
function tickSpacingToMaxLiquidityPerTick(int24 tickSpacing) internal pure returns (uint128) {
int24 minTick = (TickMath.MIN_TICK / tickSpacing) * tickSpacing;
int24 maxTick = (TickMath.MAX_TICK / tickSpacing) * tickSpacing;
uint24 numTicks = uint24((maxTick - minTick) / tickSpacing) + 1;
return type(uint128).max / numTicks;
}
// 检查
require(liquidityGrossAfter <= maxLiquidity, 'LO');
原因:
如果单个Tick的流动性过大:
1. liquidityNet可能溢出int128
2. 跨越Tick时全局流动性计算可能溢出
3. 影响价格计算精度
7. 实战案例分析
7.1 案例:添加流动性到[1000, 2000]
// 初始状态
currentTick = 1500
tickSpacing = 60
// 添加流动性
liquidity = 1000000
// 步骤1:更新tick 1000
ticks[1000].liquidityGross += 1000000
ticks[1000].liquidityNet += 1000000
if (之前liquidityGross == 0) {
ticks[1000].initialized = true
tickBitmap.flipTick(1000) // 设置bit为1
ticks[1000].feeGrowthOutside0X128 = feeGrowthGlobal0X128 // 因为1000 < 1500
}
// 步骤2:更新tick 2000
ticks[2000].liquidityGross += 1000000
ticks[2000].liquidityNet -= 1000000
if (之前liquidityGross == 0) {
ticks[2000].initialized = true
tickBitmap.flipTick(2000) // 设置bit为1
ticks[2000].feeGrowthOutside0X128 = 0 // 因为2000 > 1500
}
// 步骤3:更新全局流动性(因为当前价格在范围内)
globalLiquidity += 1000000
7.2 案例:Swap跨越多个Tick
// 初始状态
currentTick = 1000
currentPrice = 1.0001^1000
liquidity = 1000000
amountIn = 10000 token0
// Tick状态
tick 1000: initialized, liquidityNet = +500000
tick 1200: initialized, liquidityNet = +300000
tick 1500: initialized, liquidityNet = -400000
// Swap过程(token0 -> token1,价格上升)
// 第1步:在[1000, 1200)内交换
amountUsed1 = calculateSwapInTick(1000, 1200, liquidity=1000000)
amountRemaining = 10000 - amountUsed1
// 第2步:跨越tick 1200
liquidity += tick[1200].liquidityNet // +300000
currentLiquidity = 1300000
cross tick 1200(翻转feeGrowthOutside等)
// 第3步:在[1200, 1500)内交换
amountUsed2 = calculateSwapInTick(1200, 1500, liquidity=1300000)
amountRemaining -= amountUsed2
// 第4步:跨越tick 1500
liquidity += tick[1500].liquidityNet // -400000
currentLiquidity = 900000
cross tick 1500
// 继续...直到amountRemaining = 0
8. 总结与思考
8.1 核心要点
- Tick系统:将连续价格空间离散化,实现集中流动性
- liquidityNet:精妙地追踪跨越Tick时的流动性变化
- feeGrowthOutside:相对追踪手续费,避免每次更新所有仓位
- TickBitmap:位运算极致优化,实现O(1)查找
- 跨越机制:高效处理价格穿越多个Tick的情况
8.2 思考题
- 为什么feeGrowthOutside要在跨越时翻转,而不是重新计算?
- 如果tickSpacing = 1会有什么问题?
- TickBitmap的"within one word"限制会影响什么?
- liquidityGross和liquidityNet的区别本质是什么?
8.3 延伸阅读
- 下一篇:流动性管理核心代码解析
- 相关库:
本文是"Uniswap V3源码赏析系列"的第三篇