14.Uniswap V3 Tick Bitmap
目录
核心摘要 (Key Takeaways)
- Tick BitMap 的核心目的: 为了高效、低成本地跟踪 Uniswap V3 池中哪些价格点(Ticks)被用作了流动性区间的边界,从而知道哪些价格范围是活跃的。
- 精巧的数据压缩:
Tick BitMap抛弃了为每个 Tick 单独存储状态的朴素方案(成本过高),而是通过位运算将 256 个连续的 Tick 状态压缩存储在一个uint256变量中,极大地节省了 Gas 费和存储空间。 - 关键转换公式: 任何一个
tick(类型为int24) 都可以通过数学运算定位到它在 BitMap 中的具体位置。其核心是两个坐标:wordPosition(确定在哪一个uint256组里) 和bitPosition(确定在该组的第几位)。wordPosition = tick / 256bitPosition = tick % 256
- 数据结构:
Tick BitMap在合约中实现为一个mapping(int16 => uint256)。其中key (int16)是wordPosition,value (uint256)是一个 256 位的位图,每一位代表一个tick的活跃状态(1 表示活跃,0 表示不活跃)。 - 状态更新机制: 添加或移除流动性时,通过生成一个特定的掩码 (Mask),并与 BitMap 中对应的
uint256值进行按位或 (OR) 或按位与 (AND) 运算,来精确地将某一位从 0 改为 1 或从 1 改为 0,而不影响其他位。
1. Tick 的回顾与作用
- 定义: Tick (也称 Tick Index) 是 Uniswap V3 中用来标记离散化价格点的单位。
- 价格公式: 价格
P与tickT的关系通过以下公式定义: 其中T就是tick的索引。当T增大时,价格P上升;当T减小时,价格P下降。 - Tick 范围:
- 合约中
tick的类型为int24,其理论范围是从-887272到+887272。 - 案例: 这个范围足以表示极其宽广的价格区间。
- 当
tick为-887272时,价格约为 。 - 当
tick为+887272时,价格约为 。 - 这个范围远超比特币等主流资产的价格波动范围,因此在设计上是足够用的。
- 当
- 合约中
2. 核心问题:如何跟踪活跃的流动性区间?
- 背景: 在 Uniswap V3 中,用户在自定义的价格区间
[PA, PB](对应[Tick_A, Tick_B]) 内添加流动性。 - 挑战: 一个活跃的交易对(如 BTC/USDT)可能有成千上万的用户在各种不同的、甚至重叠的价格区间添加流动性。合约需要一种高效的方式来跟踪哪些
tick成为了这些流动性区间的边界(即被激活)。- 案例: 假设一个用户在
tick范围[-10, -5]添加了流动性,而另一个用户在[-4, 1]添加了流动性。此外,还可能存在重叠区间,例如用户 A 在[6, 10]添加流动性,用户 B 在[7, 12]添加流动性,那么在[7, 10]区间内流动性是重叠的。合约必须有效管理这一切。
- 案例: 假设一个用户在
- 从 ERC20 到 ERC721:
- V2: 所有流动性提供者的 LP 凭证是同质化的 ERC20 代币。
- V3: 由于每个人的流动性价格区间和数量都可能不同,LP 凭证变成了非同质化的 ERC721 代币(NFT)。
- 案例: 一个 V3 LP NFT (通过
NonfungiblePositionManager合约管理) 记录了如tickLower、tickUpper和liquidity (uint128)等唯一信息,代表一个独一无二的流动性仓位。
- 根本问题: 尽管 NFT 记录了每个仓位的具体信息,但
Pool合约本身如何宏观地、低成本地知道在任意一个tick上,是否有流动性边界存在?
3. 一种被否决的朴素方案
- 思路: 设计一个简单的键值对(Key-Value)结构来记录每个
tick的状态。mapping(int24 => bool)Key:tick索引 (例如: -10000, 20000)Value:true(被用作边界) 或false(未被用作边界)
- 致命缺陷:
- 存储量巨大:
tick的总数约有 177 万个。为每一个tick都创建一个存储槽位在区块链上是无法承受的,成本极高。 - 计算量巨大 (高 Gas 费): 当一笔交易(swap)需要跨越成千上万个
tick时,如果需要逐一查询这些tick的状态,将会导致交易的 Gas 费极高,用户无法接受。
- 存储量巨大:
4. Uniswap V3 的精巧解决方案:Tick BitMap

4.1 Tick BitMap 的核心思想
Tick BitMap 是一种数据压缩技术,它没有为每个 tick 单独存储信息,而是将连续的 256 个 tick 的状态打包到一个 256 位的整数 (uint256) 中进行管理。
4.2 从 Tick 到 Word Position 和 Bit Position 的转换
为了定位任意一个 tick 在 BitMap 中的精确位置,需要将其转换为两个坐标:wordPosition 和 bitPosition。这本质上是将一个 int24 类型的 tick 值,在概念上拆分为两部分:
- 前 16 位: 用于确定
wordPosition,对应mapping的key(int16)。 - 后 8 位: 用于确定
bitPosition,对应value(uint256) 中的具体位置 (uint8)。
转换公式:
-
wordPosition(商): 决定了该tick属于哪一个uint256“字” (word)。 -
bitPosition(余数): 决定了该tick在其所属的uint256“字” 中的具体哪一位 (从 0 到 255)。 -
反向还原公式:
-
代码实现: 在 Solidity 合约中,为了节省 Gas,通常使用位运算实现:
- 除法
/ 256等价于 右移 8 位>> 8。 - 取余
% 256可以直接计算。
- 除法
-
案例 (视频中的核心例子):
- 假设
tick = -200697。 - 计算
wordPosition:-200697 / 256 = -783.97...,向负无穷方向取整得到 -784。 - 计算
bitPosition:-200697 % 256 = 7。 - 结论:
tick -200697的状态信息,存储在wordPosition为-784的那个uint256值的第 7 位上(索引从0开始)。
- 假设
4.3 数据结构与工作原理
- 合约内定义:
mapping(int16 => uint256) public tickBitmaps;key (int16): 就是计算出的wordPosition。value (uint256): 一个 256 位的位图。它本身不是一个有数值意义的数字,而是一个 标记序列 ,其中每一位(0或1)代表对应tick的活跃状态。
4.4 流动性的更新操作 (添加与移除)
更新特定 tick 的状态是通过位运算完成的,这比直接读写存储要高效得多。
-
添加流动性 (将某位置为 1)
- 目标: 将
wordPosition对应uint256值的第bitPosition位置为1,且不改变其他位。 - 生成掩码 (Mask): 创建一个
uint256的掩码,其中只有第bitPosition位是1,其余所有位都是0。(通过1 << bitPosition实现)。 - 执行运算: 将原始的
uint256值与这个掩码进行 按位或 (OR或|) 运算。X OR 1 = 1(将目标位置为 1)X OR 0 = X(其他位保持不变)
- 目标: 将
-
移除流动性 (将某位置为 0)
- 目标: 将
wordPosition对应uint256值的第bitPosition位置为0,且不改变其他位。 - 生成掩码 (Mask): 创建一个
uint256的掩码,其中只有第bitPosition位是0,其余所有位都是1。(通过对"添加"掩码进行按位取反 (NOT或~) 实现)。 - 执行运算: 将原始的
uint256值与这个掩码进行 按位与 (AND或&) 运算。X AND 0 = 0(将目标位置为 0)X AND 1 = X(其他位保持不变)
- 目标: 将
5. 可视化流程:更新 Tick BitMap
图表选择说明: 此处选择流程图 (Flowchart) 是最合适的。因为我们的目标是清晰地展示一个算法的内部处理步骤和决策逻辑。
- 时序图 (Sequence Diagram) 更侧重于不同对象/角色之间的消息传递和交互顺序,不适合展现内部计算逻辑。
- 甘特图 (Gantt Chart) 用于项目管理和时间规划,与此场景完全无关。
graph LR
A[开始: 更新Tick的活跃状态] --> B{输入 Tick 值};
B --> C[1:计算坐标
wordPosition = tick / 256
bitPosition = tick % 256];
C --> D[2:根据 wordPosition
从 tickBitmaps 中读取原始的 uint256 值];
D --> E{操作是添加还是移除流动性?};
E -- 添加 --> F[3a. 生成'添加'掩码 Mask
即第 bitPosition 位为1, 其余为0];
F --> G[4a. 执行按位'或'操作
新值 = 原始值 OR Mask];
E -- 移除 --> H[3b. 生成'移除'掩码 Mask
即第 bitPosition 位为0, 其余为1];
H --> I[4b. 执行按位'与'操作
新值 = 原始值 AND Mask];
G --> J[5: 将新值写回 tickBitmaps];
I --> J;
J --> K[结束];
graph LR
A[开始: 更新Tick的活跃状态] --> B{输入 Tick 值};
B --> C[1:计算坐标
wordPosition = tick / 256
bitPosition = tick % 256];
C --> D[2:根据 wordPosition
从 tickBitmaps 中读取原始的 uint256 值];
D --> E{操作是添加还是移除流动性?};
E -- 添加 --> F[3a. 生成'添加'掩码 Mask
即第 bitPosition 位为1, 其余为0];
F --> G[4a. 执行按位'或'操作
新值 = 原始值 OR Mask];
E -- 移除 --> H[3b. 生成'移除'掩码 Mask
即第 bitPosition 位为0, 其余为1];
H --> I[4b. 执行按位'与'操作
新值 = 原始值 AND Mask];
G --> J[5: 将新值写回 tickBitmaps];
I --> J;
J --> K[结束];
graph LR
A[开始: 更新Tick的活跃状态] --> B{输入 Tick 值};
B --> C[1:计算坐标
wordPosition = tick / 256
bitPosition = tick % 256];
C --> D[2:根据 wordPosition
从 tickBitmaps 中读取原始的 uint256 值];
D --> E{操作是添加还是移除流动性?};
E -- 添加 --> F[3a. 生成'添加'掩码 Mask
即第 bitPosition 位为1, 其余为0];
F --> G[4a. 执行按位'或'操作
新值 = 原始值 OR Mask];
E -- 移除 --> H[3b. 生成'移除'掩码 Mask
即第 bitPosition 位为0, 其余为1];
H --> I[4b. 执行按位'与'操作
新值 = 原始值 AND Mask];
G --> J[5: 将新值写回 tickBitmaps];
I --> J;
J --> K[结束];graph LR
A[开始: 更新Tick的活跃状态] --> B{输入 Tick 值};
B --> C[1:计算坐标
wordPosition = tick / 256
bitPosition = tick % 256];
C --> D[2:根据 wordPosition
从 tickBitmaps 中读取原始的 uint256 值];
D --> E{操作是添加还是移除流动性?};
E -- 添加 --> F[3a. 生成'添加'掩码 Mask
即第 bitPosition 位为1, 其余为0];
F --> G[4a. 执行按位'或'操作
新值 = 原始值 OR Mask];
E -- 移除 --> H[3b. 生成'移除'掩码 Mask
即第 bitPosition 位为0, 其余为1];
H --> I[4b. 执行按位'与'操作
新值 = 原始值 AND Mask];
G --> J[5: 将新值写回 tickBitmaps];
I --> J;
J --> K[结束];
流程解释:
- 当需要更新一个
tick的状态时,首先输入该tick的值。 - 合约通过除以 256 和对 256 取余,计算出该
tick对应的wordPosition和bitPosition。 - 使用
wordPosition作为键,从tickBitmaps这个mapping中找到对应的 256 位uint256值。 - 根据操作是添加还是移除流动性,生成不同的掩码(Mask)并执行相应的位运算(
OR或AND)。 - 最后,将运算后得到的新
uint256值更新回tickBitmaps中,完成状态更新。这个过程只修改了一位,效率极高。