15.Uniswap V3 Tick Bitmap 寻找下一个 Tick
目录
核心摘要 (Key Takeaways)
- Tick Bitmap 的核心作用:它是一个
uint256的位图,用于高效追踪 Uniswap V3 池中哪些价格点(Ticks)被用作了流动性区间的边界。1代表该 Tick 活跃(有流动性),0代表不活跃。 - 寻找下一个 Tick 的目的:在进行交易(Swap)时,价格会发生变动。当价格穿过一个活跃的 Tick,意味着当前流动性区间的流动性(L)将被耗尽,合约必须找到下一个有流动性的价格区间才能继续完成交易。
- 通过位运算实现高效查找:寻找下一个活跃 Tick 的过程利用了**掩码(Mask)和按位与(Bitwise AND)**运算。通过构建特定的掩码,可以快速地隔离出目标方向(价格升高或降低)上所有活跃的 Ticks。
- 交易方向决定搜索方向:
- 当交易导致价格上涨时(例如用 USDC 买 ETH),需要在 Tick Bitmap 中向左搜索(寻找更大的 Tick)。
- 当交易导致价格下跌时(例如卖出 ETH 换 USDC),需要在 Tick Bitmap 中向右搜索(寻找更小的 Tick)。
- Bitmap 内部方向与全局 Tick 轴方向的区别:在 Bitmap(一个
uint256)内部,索引位(bit position)从右到左增大,因此向左查找意味着寻找更大的 Tick 值。这与全局价格数轴上数值从左到右增大的直觉相反,需要注意区分。
I. Tick Bitmap 核心概念回顾
本节内容对应 TickBitmap.sol 库合约中的核心逻辑。
A. 什么是 Tick Bitmap
- 定义:Tick Bitmap 是一个映射(Mapping),其键(key)为一个
int16的word position,值(value)为一个uint256的数字。 - 数据转换:
- 一个
int24类型的 Tick 首先被转换成两个部分,这对应合约中的position函数。word position(int16): 决定了该 Tick 属于哪个uint256位图。计算方式为tick >> 8(相当于tick / 256)。bit position(uint8): 决定了该 Tick 在对应的uint256位图中的具体位置(从0到255)。计算方式为tick % 256。
- 一个
- 状态表示:
- 在
uint256的位图中,每一个比特位(0或1)代表一个 Tick 的状态。 1:表示该 Tick 被激活 (Active)。0:表示该 Tick 未被激活 (Inactive)。
- 在
B. Tick 的激活状态 (Active State)
- 激活定义:当一个 Tick 被用户选为添加流动性区间的上界 (
upper tick) 或下界 (lower tick) 时,该 Tick 就被认为是激活的。 - 非激活定义:如果没有任何流动性头寸使用某个 Tick 作为边界,那么该 Tick 就是非激活的。
C. 更新 Tick Bitmap
更新操作通过位运算实现,对应合约中的 flipTick 函数,确保只修改目标 Tick 对应的比特位,而不影响其他位。
-
添加流动性 (激活 Tick)
- 目标:将目标 Tick 对应的比特位从
0变为1。 - 方法:使用按位或 (Bitwise OR
|) 运算。 - 案例:
- 假设要在
tick = -200697处添加流动性,该 Tick 对应word_position = -784的uint256位图中的第7个比特位。 - 原始位图:
...01101**0**1...(第7位是0) - 构建掩码 (Mask): 创建一个只有第
7位是1,其他所有位都是0的uint256值 (...00000**1**0...)。 - 运算:
原始位图 | 掩码 - 结果:
...01101**1**1...(只有第7位被修改,其他位保持不变)
- 假设要在
- 目标:将目标 Tick 对应的比特位从
-
移除流动性 (取消激活 Tick)
- 目标:将目标 Tick 对应的比特位从
1变为0。 - 方法:使用按位与 (Bitwise AND
&) 运算。 - 案例:
- 假设要移除
tick = -200697处的流动性。 - 原始位图:
...01101**1**1...(第7位是1) - 构建掩码 (Mask): 创建一个只有第
7位是0,其他所有位都是1的uint256值 (...11111**0**1...)。 - 运算:
原始位图 & 掩码 - 结果:
...01101**0**1...(只有第7位被修改,其他位保持不变)
- 假设要移除
- 目标:将目标 Tick 对应的比特位从
II. 寻找下一个活跃 Tick (Finding Next Tick)
此过程是本节课的核心,对应 TickBitmap.sol 合约中的 nextInitializedTickWithinOneWord 函数。
function nextInitializedTickWithinOneWord(
mapping(int16 => uint256) storage self,
int24 tick,
int24 tickSpacing,
bool lte
) internal view returns (int24 next, bool initialized) {
int24 compressed = tick / tickSpacing;
if (tick < 0 && tick % tickSpacing != 0) compressed--; // round towards negative infinity
if (lte) {
(int16 wordPos, uint8 bitPos) = position(compressed);
// all the 1s at or to the right of the current bitPos
uint256 mask = (1 << bitPos) - 1 + (1 << bitPos);
uint256 masked = self[wordPos] & mask;
// if there are no initialized ticks to the right of or at the current tick, return rightmost in the word
initialized = masked != 0;
// overflow/underflow is possible, but prevented externally by limiting both tickSpacing and tick
next = initialized
? (compressed - int24(bitPos - BitMath.mostSignificantBit(masked))) * tickSpacing
: (compressed - int24(bitPos)) * tickSpacing;
} else {
// start from the word of the next tick, since the current tick state doesn't matter
(int16 wordPos, uint8 bitPos) = position(compressed + 1);
// all the 1s at or to the left of the bitPos
uint256 mask = ~((1 << bitPos) - 1);
uint256 masked = self[wordPos] & mask;
// if there are no initialized ticks to the left of the current tick, return leftmost in the word
initialized = masked != 0;
// overflow/underflow is possible, but prevented externally by limiting both tickSpacing and tick
next = initialized
? (compressed + 1 + int24(BitMath.leastSignificantBit(masked) - bitPos)) * tickSpacing
: (compressed + 1 + int24(type(uint8).max - bitPos)) * tickSpacing;
}
}A. 为什么需要寻找下一个 Tick?
在进行交易 (Swap) 时,池子的当前价格会移动。当价格移动到当前流动性区间的边界(即一个活跃的 Tick)时,该区间的流动性就被耗尽了。为了能继续执行交易,合约必须知道下一个有流动性的价格区间在哪里。因此,它需要从当前 Tick 出发,去寻找下一个被激活的 Tick 作为新的边界。
B. 寻找流程的可视化
以下流程图展示了在合约中根据交易方向(通过 lte 标志判断)寻找下一个活跃 Tick 的完整逻辑。
flowchart TD
A[开始: 提供 Current Tick] --> B{判断寻找方向: lte 参数是 true 吗?}
B -- "是 (lte=true), 价格下降" --> C[向右寻找更小的Tick]
C --> D[构建掩码: 当前位及其左侧为0, 右侧为1]
D --> E[将掩码与Tick Bitmap进行 按位与 运算]
E --> F[在运算结果中找到最左侧的 1]
F --> G[得到 Next Tick 的 bit position]
B -- "否 (lte=false), 价格上升" --> H[向左寻找更大的Tick]
H --> I[构建掩码: 当前位及其右侧为0, 左侧为1]
I --> J[将掩码与Tick Bitmap进行 按位与 运算]
J --> K[在运算结果中找到最左侧的 1]
K --> L[得到 Next Tick 的 bit position]
G --> M[计算Next Tick的具体值]
L --> M
M --> N[结束: 返回Next Tick]
flowchart TD
A[开始: 提供 Current Tick] --> B{判断寻找方向: lte 参数是 true 吗?}
B -- "是 (lte=true), 价格下降" --> C[向右寻找更小的Tick]
C --> D[构建掩码: 当前位及其左侧为0, 右侧为1]
D --> E[将掩码与Tick Bitmap进行 按位与 运算]
E --> F[在运算结果中找到最左侧的 1]
F --> G[得到 Next Tick 的 bit position]
B -- "否 (lte=false), 价格上升" --> H[向左寻找更大的Tick]
H --> I[构建掩码: 当前位及其右侧为0, 左侧为1]
I --> J[将掩码与Tick Bitmap进行 按位与 运算]
J --> K[在运算结果中找到最左侧的 1]
K --> L[得到 Next Tick 的 bit position]
G --> M[计算Next Tick的具体值]
L --> M
M --> N[结束: 返回Next Tick]
flowchart TD
A[开始: 提供 Current Tick] --> B{判断寻找方向: lte 参数是 true 吗?}
B -- "是 (lte=true), 价格下降" --> C[向右寻找更小的Tick]
C --> D[构建掩码: 当前位及其左侧为0, 右侧为1]
D --> E[将掩码与Tick Bitmap进行 按位与 运算]
E --> F[在运算结果中找到最左侧的 1]
F --> G[得到 Next Tick 的 bit position]
B -- "否 (lte=false), 价格上升" --> H[向左寻找更大的Tick]
H --> I[构建掩码: 当前位及其右侧为0, 左侧为1]
I --> J[将掩码与Tick Bitmap进行 按位与 运算]
J --> K[在运算结果中找到最左侧的 1]
K --> L[得到 Next Tick 的 bit position]
G --> M[计算Next Tick的具体值]
L --> M
M --> N[结束: 返回Next Tick]flowchart TD
A[开始: 提供 Current Tick] --> B{判断寻找方向: lte 参数是 true 吗?}
B -- "是 (lte=true), 价格下降" --> C[向右寻找更小的Tick]
C --> D[构建掩码: 当前位及其左侧为0, 右侧为1]
D --> E[将掩码与Tick Bitmap进行 按位与 运算]
E --> F[在运算结果中找到最左侧的 1]
F --> G[得到 Next Tick 的 bit position]
B -- "否 (lte=false), 价格上升" --> H[向左寻找更大的Tick]
H --> I[构建掩码: 当前位及其右侧为0, 左侧为1]
I --> J[将掩码与Tick Bitmap进行 按位与 运算]
J --> K[在运算结果中找到最左侧的 1]
K --> L[得到 Next Tick 的 bit position]
G --> M[计算Next Tick的具体值]
L --> M
M --> N[结束: 返回Next Tick]
流程解释:
- 起始点:从当前价格所在的
Current Tick开始。 - 方向判断:通过一个布尔值参数
lte(Less Than or Equal) 来决定搜索方向。true表示寻找比当前 Tick 小或相等的下一个活跃 Tick(价格下降),false表示寻找更大的 Tick(价格上升)。 - 构建掩码:根据搜索方向,生成一个特定的掩码。
- 位运算:将掩码与存储 Tick 状态的位图进行按位与操作。这个操作的结果是一个新的
uint256,其中只保留了目标搜索方向上的活跃 Tick。 - 定位:在运算结果中,找到最左边(最高位)的那个
1,其位置就是Next Tick的bit position。 - 计算:最后,根据找到的
bit position计算出Next Tick的确切整数值。
C. 场景一:寻找 ≤ 当前 Tick (价格下降)
- 目标:找到比当前 Tick 值更小(或相等)的、最近的一个活跃 Tick。
- 方向:在
uint256位图中向右查找。 - 方法:
- 构建掩码:以
current tick的bit position为界,其左侧(包括当前位置)全为0,右侧全为1。 - 运算:将此掩码与 Tick Bitmap 进行按位与 (
&) 运算。 - 结果:运算结果会滤掉所有比当前 Tick 大的活跃 Tick,保留所有更小的活跃 Tick。找到结果中最左侧的
1即可。
- 构建掩码:以
- 案例:
- 当前 Tick:
current_tick = -200697,其bit_position为7。 - Tick Bitmap (示例):
...001...0100100...(第10、7、2位为1) - 掩码 (Mask):
...000000001111111...(第7位及其左侧为0,右侧为1) - 运算结果:
...000...0000100...(只有第2位的1被保留) - 找到的 Next Tick:
bit_position为2。
- 当前 Tick:
- 计算 Next Tick 值:
- 公式:
- 案例计算:
- 新的价格区间边界为
-200702到-200697。
D. 场景二:寻找 > 当前 Tick (价格上升)
- 目标:找到比当前 Tick 值更大的、最近的一个活跃 Tick。
- 方向:在
uint256位图中向左查找。 - 方法:
- 构建掩码:以
current tick的bit position为界,其右侧(包括当前位置)全为0,左侧全为1。 - 运算:将此掩码与 Tick Bitmap 进行按位与 (
&) 运算。 - 结果:运算结果会滤掉所有比当前 Tick 小的活跃 Tick,保留所有更大的。找到结果中最左侧的
1。
- 构建掩码:以
- 案例:
- 当前 Tick:
current_tick = -200697,其bit_position为7。 - Tick Bitmap (示例):
...001...0100100...(第10、7、2位为1) - 掩码 (Mask):
...111000000000000...(第7位及其右侧为0,左侧为1) - 运算结果:
...001...0000000...(只有第10位的1被保留) - 找到的 Next Tick:
bit_position为10。
- 当前 Tick:
- 计算 Next Tick 值:
- 公式: (同上)
- 案例计算:
- 新的价格区间边界为
-200697到-200694。
III. 交易方向与 Tick 寻找方向的关联
合约如何确定是向左还是向右寻找?这取决于用户的交易行为对池子价格的影响。
-
案例 1: 用户使用 USDC 购买 ETH
- 池子变化: USDC 数量增加,ETH 数量减少。
- 价格影响: ETH 价格升高。
- 合约操作: 需要寻找大于当前 Tick 的下一个活跃 Tick。
- Bitmap 内部操作: 在位图中向左寻找。
-
案例 2: 用户卖出 ETH 换取 USDC
- 池子变化: ETH 数量增加,USDC 数量减少。
- 价格影响: ETH 价格降低。
- 合约操作: 需要寻找小于等于当前 Tick 的下一个活跃 Tick。
- Bitmap 内部操作: 在位图中向右寻找。
IV. 重要概念辨析:Tick 坐标轴方向 vs. Bitmap 内部方向
这是一个容易混淆的关键点,必须清晰区分:
-
全局 Tick 坐标轴:
- 这是一个逻辑上的、无限延伸的数轴。
- Tick 值从左到右递增(例如:…, -100, 0, 100, …)。
- 因此,价格上涨对应在坐标轴上向右移动。
-
Tick Bitmap 内部 (
uint256):- 这是一个具体的256位数据结构。
bit position(索引) 从右到左递增(最右边是第0位,最左边是第255位)。- 由于
Tick值与bit position相关,一个更大的Tick值会对应一个更大的bit position索引。 - 因此,价格上涨(寻找更大的Tick)对应在 Bitmap 内部向左寻找。
| 场景 | 价格变化 | 全局 Tick 轴移动方向 | Tick Bitmap 内部搜索方向 |
|---|---|---|---|
| 买入 ETH | 上涨 | 向右 | 向左 |
| 卖出 ETH | 下跌 | 向左 | 向右 |
V. 展望:Cross Tick 交易
- 视频结尾提及:当一笔交易消耗完当前价格区间内的所有流动性后,价格会穿越一个活跃的 Tick,进入下一个流动性区间。
- 后续主题:这个穿越 Tick 的过程被称为 Cross Tick。如何处理这种跨区间的交易,是更复杂的逻辑,将在后续课程中讲解。本次学习的核心是掌握如何找到下一个 Tick 的边界,为理解 Cross Tick 交易打下基础。