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)为一个 int16word position,值(value)为一个 uint256 的数字。
  • 数据转换
    • 一个 int24 类型的 Tick 首先被转换成两个部分,这对应合约中的 position 函数。
      1. word position (int16): 决定了该 Tick 属于哪个 uint256 位图。计算方式为 tick >> 8 (相当于 tick / 256)。
      2. bit position (uint8): 决定了该 Tick 在对应的 uint256 位图中的具体位置(从0到255)。计算方式为 tick % 256
  • 状态表示
    • uint256 的位图中,每一个比特位(01)代表一个 Tick 的状态。
    • 1:表示该 Tick 被激活 (Active)
    • 0:表示该 Tick 未被激活 (Inactive)

B. Tick 的激活状态 (Active State)

  • 激活定义:当一个 Tick 被用户选为添加流动性区间的上界 (upper tick)下界 (lower tick) 时,该 Tick 就被认为是激活的
  • 非激活定义:如果没有任何流动性头寸使用某个 Tick 作为边界,那么该 Tick 就是非激活的。

C. 更新 Tick Bitmap

更新操作通过位运算实现,对应合约中的 flipTick 函数,确保只修改目标 Tick 对应的比特位,而不影响其他位。

  1. 添加流动性 (激活 Tick)

    • 目标:将目标 Tick 对应的比特位从 0 变为 1
    • 方法:使用按位或 (Bitwise OR |) 运算。
    • 案例
      • 假设要在 tick = -200697 处添加流动性,该 Tick 对应 word_position = -784uint256 位图中的第 7 个比特位。
      • 原始位图: ...01101**0**1... (第7位是0)
      • 构建掩码 (Mask): 创建一个只有第 7 位是 1,其他所有位都是 0uint256 值 (...00000**1**0...)。
      • 运算: 原始位图 | 掩码
      • 结果: ...01101**1**1... (只有第7位被修改,其他位保持不变)
  2. 移除流动性 (取消激活 Tick)

    • 目标:将目标 Tick 对应的比特位从 1 变为 0
    • 方法:使用按位与 (Bitwise AND &) 运算。
    • 案例
      • 假设要移除 tick = -200697 处的流动性。
      • 原始位图: ...01101**1**1... (第7位是1)
      • 构建掩码 (Mask): 创建一个只有第 7 位是 0,其他所有位都是 1uint256 值 (...11111**0**1...)。
      • 运算: 原始位图 & 掩码
      • 结果: ...01101**0**1... (只有第7位被修改,其他位保持不变)

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]

流程解释:

  1. 起始点:从当前价格所在的 Current Tick 开始。
  2. 方向判断:通过一个布尔值参数 lte (Less Than or Equal) 来决定搜索方向。true 表示寻找比当前 Tick 小或相等的下一个活跃 Tick(价格下降),false 表示寻找更大的 Tick(价格上升)。
  3. 构建掩码:根据搜索方向,生成一个特定的掩码。
  4. 位运算:将掩码与存储 Tick 状态的位图进行按位与操作。这个操作的结果是一个新的 uint256,其中只保留了目标搜索方向上的活跃 Tick。
  5. 定位:在运算结果中,找到最左边(最高位)的那个 1,其位置就是 Next Tickbit position
  6. 计算:最后,根据找到的 bit position 计算出 Next Tick 的确切整数值。

C. 场景一:寻找 当前 Tick (价格下降)

  • 目标:找到比当前 Tick 值更小(或相等)的、最近的一个活跃 Tick。
  • 方向:在 uint256 位图中向查找。
  • 方法
    1. 构建掩码:以 current tickbit position 为界,其左侧(包括当前位置)全为 0,右侧全为 1
    2. 运算:将此掩码与 Tick Bitmap 进行按位与 (&) 运算。
    3. 结果:运算结果会滤掉所有比当前 Tick 大的活跃 Tick,保留所有更小的活跃 Tick。找到结果中最左侧的 1 即可。
  • 案例
    • 当前 Tick: current_tick = -200697,其 bit_position7
    • Tick Bitmap (示例): ...001...0100100... (第10、7、2位为1)
    • 掩码 (Mask): ...000000001111111... (第7位及其左侧为0,右侧为1)
    • 运算结果: ...000...0000100... (只有第2位的1被保留)
    • 找到的 Next Tick: bit_position2
  • 计算 Next Tick 值
    • 公式: NextTick=CurrentTickBitPositioncurrent+BitPositionnext \text{NextTick} = \text{CurrentTick} - \text{BitPosition}_{\text{current}} + \text{BitPosition}_{\text{next}}
    • 案例计算: 2006977+2=200702 -200697 - 7 + 2 = -200702
    • 新的价格区间边界为 -200702-200697

D. 场景二:寻找 > 当前 Tick (价格上升)

  • 目标:找到比当前 Tick 值更大的、最近的一个活跃 Tick。
  • 方向:在 uint256 位图中向查找。
  • 方法
    1. 构建掩码:以 current tickbit position 为界,其右侧(包括当前位置)全为 0,左侧全为 1
    2. 运算:将此掩码与 Tick Bitmap 进行按位与 (&) 运算。
    3. 结果:运算结果会滤掉所有比当前 Tick 小的活跃 Tick,保留所有更大的。找到结果中最左侧的 1
  • 案例
    • 当前 Tick: current_tick = -200697,其 bit_position7
    • Tick Bitmap (示例): ...001...0100100... (第10、7、2位为1)
    • 掩码 (Mask): ...111000000000000... (第7位及其右侧为0,左侧为1)
    • 运算结果: ...001...0000000... (只有第10位的1被保留)
    • 找到的 Next Tick: bit_position10
  • 计算 Next Tick 值
    • 公式: (同上) NextTick=CurrentTickBitPositioncurrent+BitPositionnext \text{NextTick} = \text{CurrentTick} - \text{BitPosition}_{\text{current}} + \text{BitPosition}_{\text{next}}
    • 案例计算: 2006977+10=200694 -200697 - 7 + 10 = -200694
    • 新的价格区间边界为 -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 内部方向

这是一个容易混淆的关键点,必须清晰区分:

  1. 全局 Tick 坐标轴:

    • 这是一个逻辑上的、无限延伸的数轴。
    • Tick 值从左到右递增(例如:…, -100, 0, 100, …)。
    • 因此,价格上涨对应在坐标轴上向移动。
  2. 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 交易打下基础。