当前位置: 首页 > news >正文

uniswap v4 TickBitmap库函数解析

我们知道uniswap v3/v4版本的智能合约需要管理887272*2个tick,为了高效的管理它们,采用了位图的数据结构:

mapping(int16 => uint256) public override tickBitmap;function position(int24 tick) private pure returns (int16 wordPos, uint8 bitPos) {wordPos = int16(tick >> 8);bitPos = uint8(tick % 256);
}

887272*2个tick合约中用int24来表示;int16(tick >> 8)代表取高16位,uint8(tick % 256)代表取低8位,在tickBitmap中高16位作为key,那么为什么用uint256作为value呢?剩下的低8位是2的8次方一共256个数。也就是说tickBitmap每一条记录需要管理256个tick状态,最高效的方法就是使用位图,把256个数转换成256个二进制数表示,也就是uint256,相应的位上为1代表当前的tick已经被初始化。

以上是tick的存储结构,本章介绍的就是对上述结构进行存取的类库。位置如下:

 compressed

这个方法的作用是:把 tick 映射到 tick bitmap 的压缩索引,即计算 compressed = tick / tickSpacing,即把 tick 映射到以 tickSpacing 为单位的整数索引。对于负数 tick,如果 tick 不是 tickSpacing 的整数倍,需要向下取整

    function compress(int24 tick, int24 tickSpacing) internal pure returns (int24 compressed) {// compressed = tick / tickSpacing;// if (tick < 0 && tick % tickSpacing != 0) compressed--;assembly ("memory-safe") {tick := signextend(2, tick)tickSpacing := signextend(2, tickSpacing)compressed :=sub(sdiv(tick, tickSpacing),// if (tick < 0 && tick % tickSpacing != 0) then tick % tickSpacing < 0, vice versaslt(smod(tick, tickSpacing), 0))}}

 伪代码:

compressed = tick / tickSpacing;
if (tick < 0 && tick % tickSpacing != 0) compressed--;

例如:tick = -7, tickSpacing = 5 ;-7 / 5 = -1.4,向下取整为 -2

该方法基于汇编实现:具体解析如下:

  • signextend (2, tick):用于把小位宽的有符号整数扩展为 256 位,保证正负号不变,其中2 表示 tick 占用 3 字节(24 位,0-based),即 int24
  • sdiv(tick, tickSpacing):有符号除法,得到商(但对负数不是地板除法)。
  • smod(tick, tickSpacing):有符号取模,得到余数。
  • slt(smod(...), 0):如果余数为负,返回 1,否则返回 0。
  • sub(...):如果 tick < 0 且余数不为 0,则商再减 1,实现地板除法。

flipTick

function flipTick(mapping(int16 => uint256) storage self, int24 tick, int24 tickSpacing) internal {// Equivalent to the following Solidity://     if (tick % tickSpacing != 0) revert TickMisaligned(tick, tickSpacing);//     (int16 wordPos, uint8 bitPos) = position(tick / tickSpacing);//     uint256 mask = 1 << bitPos;//     self[wordPos] ^= mask;assembly ("memory-safe") {tick := signextend(2, tick)tickSpacing := signextend(2, tickSpacing)// ensure that the tick is spacedif smod(tick, tickSpacing) {let fmp := mload(0x40)mstore(fmp, 0xd4d8f3e6) // selector for TickMisaligned(int24,int24)mstore(add(fmp, 0x20), tick)mstore(add(fmp, 0x40), tickSpacing)revert(add(fmp, 0x1c), 0x44)}tick := sdiv(tick, tickSpacing)// calculate the storage slot corresponding to the tick// wordPos = tick >> 8mstore(0, sar(8, tick))mstore(0x20, self.slot)// the slot of self[wordPos] is keccak256(abi.encode(wordPos, self.slot))let slot := keccak256(0, 0x40)// mask = 1 << bitPos = 1 << (tick % 256)// self[wordPos] ^= masksstore(slot, xor(sload(slot), shl(and(tick, 0xff), 1)))}}

先看下官方提供的等效代码:

function flipTick(mapping(int16 => uint256) storage self, int24 tick, int24 tickSpacing) internal {// 确保 tick 是 tickSpacing 的整数倍if (tick % tickSpacing != 0) {revert TickMisaligned(tick, tickSpacing);}// 计算压缩后的 tick 值tick /= tickSpacing;// 计算 wordPos 和 bitPosint16 wordPos = int16(tick >> 8); // tick / 256uint8 bitPos = uint8(tick % 256);// 计算掩码并翻转对应位uint256 mask = 1 << bitPos;self[wordPos] ^= mask;
}

逻辑很清晰:

  • 确保 tick 是 tickSpacing 的整数倍
  • 计算压缩后的 tick 值
  • 计算 wordPos 和 bitPos
  • 计算掩码并翻转对应位

下面是汇编代码的解析:

tick := signextend(2, tick)
tickSpacing := signextend(2, tickSpacing)
  • 使用 signextend 将 tick 和 tickSpacing 从 24 位扩展为 256 位。这是因为 EVM 的操作数默认是 256 位,signextend可以确保负数的符号位正确保留。
if smod(tick, tickSpacing) {let fmp := mload(0x40)mstore(fmp, 0xd4d8f3e6) // selector for TickMisaligned(int24,int24)mstore(add(fmp, 0x20), tick)mstore(add(fmp, 0x40), tickSpacing)revert(add(fmp, 0x1c), 0x44)
}
  • smod(tick, tickSpacing):计算 tick % tickSpacing。如果结果不为 0,说明 tick 不是 tickSpacing 的整数倍。此时需要抛出异常!

tick := sdiv(tick, tickSpacing)
  • 将 tick 除以 tickSpacing,得到压缩后的 tick 值。tickBitmap 只存储压缩后的 tick 状态,而不是原始的 tick
mstore(0, sar(8, tick))
mstore(0x20, self.slot)
let slot := keccak256(0, 0x40)
  • sar(8, tick):计算 wordPos,即tickBitMap的key。等价于 tick >> 8,将 tick 右移 8 位取高16位。

  • self.slotself 是 tickBitmap 的存储槽位置。

  • keccak256(0, 0x40):计算存储槽位置 slot,表示 tickBitmap[wordPos] 的存储位置。

在 Solidity 中,某一个key在mapping 的存储位置是通过这个公式计算的:

slot= keccak256(abi.encode(key, mappingSlot)) 

abi.encode(key, mappingSlot)的意思就是把key和mappingSlot连接到一起,前面两个部分攻占用了64个字节:[key (32 字节)][mappingSlot (32 字节)]

故而:keccak256(0, 0x40)=keccak256(abi.encode(key, mappingSlot)) 

也就说keccak256(0, 0x40)计算的是tick所在位图的存储位置

sstore(slot, xor(sload(slot), shl(and(tick, 0xff), 1)))
  • and(tick, 0xff):计算 bitPos,即 tick 在 256 位位图中的具体位置。等价于 tick % 256

  • shl(bitPos, 1):计算掩码 mask = 1 << bitPos,用于定位 tick 对应的位。

  • sload(slot):加载存储槽 slot 的当前值。

  • xor(sload(slot), mask):使用按位异或(^)翻转 tick 对应的位:如果该位原来是 0,则变为 1。如果该位原来是 1,则变为 0

  • sstore(slot, ...):将更新后的值存储回 slot

flipTick这个方法可以总结出两点:

  1. 用户初始化的tick必须是tickspace的整数倍
  2. tick的数量有887272*2个,但真正需要初始化的只有887272*2/tickSpace个

nextInitializedTickWithinOneWord

这个方法是 Uniswap v4 中用于查找下一个已初始化 tick 的核心逻辑。初始化过的tick 用于标记流动性变化的点,swap 时需要知道下一个会遇到的流动性边界。

    function nextInitializedTickWithinOneWord(mapping(int16 => uint256) storage self,int24 tick,int24 tickSpacing,bool lte) internal view returns (int24 next, bool initialized) {unchecked {int24 compressed = compress(tick, tickSpacing);if (lte) {(int16 wordPos, uint8 bitPos) = position(compressed);// all the 1s at or to the right of the current bitPosuint256 mask = type(uint256).max >> (uint256(type(uint8).max) - 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 wordinitialized = masked != 0;// overflow/underflow is possible, but prevented externally by limiting both tickSpacing and ticknext = initialized? (compressed - int24(uint24(bitPos - BitMath.mostSignificantBit(masked)))) * tickSpacing: (compressed - int24(uint24(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);// all the 1s at or to the left of the bitPosuint256 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 wordinitialized = masked != 0;// overflow/underflow is possible, but prevented externally by limiting both tickSpacing and ticknext = initialized? (compressed + int24(uint24(BitMath.leastSignificantBit(masked) - bitPos))) * tickSpacing: (compressed + int24(uint24(type(uint8).max - bitPos))) * tickSpacing;}}}

lte参数代表交易的方向,lte代表tick变小,我们需要寻找小于当前tick的第一个tick,这里我们以lte为例:

第一步是计算tick压当前缩后的值:

int24 compressed = compress(tick, tickSpacing);

由前面的flipTick方法可知,tick存储的时候进行了压缩处理,即(tick/tickSpace),所以在读取的时候也要先计算当前tick压缩后的值。

接下来对压缩后的值进行分解,前面我们说过所有的tick做了二级管理,第一级是的wordPos,使用tick的高16位作为key,后8位是一个位图转换成的数字也就是bitPos,

uint256 mask = type(uint256).max >> (uint256(type(uint8).max) - bitPos);
uint256 masked = self[wordPos] & mask;
  • type(uint256).max:代表 256 位全为 1 的二进制数(即 0xFFFF...FFFF,共 256 个 1)。

  • type(uint8).max:代表 8 位全为 1 的二进制数(即 255,0xFF)。

  • uint256(type(uint8).max) - bitPos:计算出从 bitPos 到 255 之间有多少位。

  • >>(右移):把高位的 1 右移,留下 bitPos 及其右侧的 1。

图解说明

假设 bitmap 的一个 word 是 256 位(bit),每一位代表一个 tick 是否被初始化:

word:  [b255][b254]...[b6][b5][b4][b3][b2][b1][b0]

index:   255   254 ...  6      5     4    3    2    1    0

假设 bitPos = 5,那么:mask = type(uint256).max >> (255 - 5):实际效果是:只保留 bit 5 及其右侧(低位)的所有位为 1,其余为 0

mask:  000...0001 1 1 1 1 1   (高位全0,低6位全1)
index: 255........65 4 3 2 1 0

masked = self[wordPos] & mask:只保留当前 word 中 bitPos 及其右侧的初始化状态。

 这样就可以可以快速查找从 bitPos 开始,向右(tick 变小)最近的已初始化 tick。

假设原始的word如下:

self[wordPos]: ... 0 1 0 0 1 1 0 1 0 0
index:              ... 9 8 7 6 5 4 3 2 1 0

 经过masked = self[wordPos] & mask:后

masked:       ...  0 0 0 0 1 0 1 0 0 1  (只看 5~0 的初始化状态)
index:           .... 9 8 7 6 5 4 3 2 1 0

 最后同过msb算法计算出masked中最左边的1所在的位置:

initialized = masked != 0;
next = initialized? (compressed - int24(uint24(bitPos - BitMath.mostSignificantBit(masked)))) * tickSpacing: (compressed - int24(uint24(bitPos))) * tickSpacing;

 首先判断在当前 word 内,从 bitPos(含)及其右侧是否有已初始化的 tick。masked 只保留了 bitPos 及其右侧的位,如果有任何一位是 1,说明有已初始化 tick。

如果找到了已初始化 tick(initialized == true

  • BitMath.mostSignificantBit(masked) 找到 masked 中最高位的 1(即最靠左的 1),这就是距离 bitPos 最近的已初始化 tick。
  • bitPos - ... 得到从当前 bitPos 到最近已初始化 tick 的距离(向右,tick 变小)。
  • compressed - ... 得到最近已初始化 tick 的 compressed 编号。
  • 乘以 tickSpacing 得到实际 tick 值。

如果没有找到已初始化 tick(initialized == false):

  • 说明当前 word 内 bitPos 及其右侧都没有已初始化 tick。返回当前 word 最右侧(最低位)的 tick(即 compressed - bitPos),作为“极限”tick。

非lte的算法和lte类似,唯一需要解释的是这行代码:
 

uint256 mask = ~((1 << bitPos) - 1);

假设 bitPos = 5

  • 1 << 5 = 0b0000000000000000000000000000000000000000000000000000000000100000
  • (1 << 5) - 1 = 0b0000000000000000000000000000000000000000000000000000000000011111
  • ~((1 << 5) - 1) = 0b1111111111111111111111111111111111111111111111111111111111100000

在 Solidity 里,1 默认是 uint256 类型(256 位)1 << bitPos 表示把最低位的 1 左移 bitPos 位,结果依然是 256 位的 uint256。

相关文章:

  • 力扣刷题——二分查找
  • 2022年TASE SCI2区,学习灰狼算法LGWO+随机柔性车间调度,深度解析+性能实测
  • 关于MySql深分页的问题及优化方案
  • 25年春招:携程java开发一面
  • 华为OD机试_2025 B卷_计算最大乘积(Python,100分)(附详细解题思路)
  • 第28节 Node.js 文件系统
  • C++ 引用
  • 【推荐算法课程一】推荐算法介绍-深度学习算法
  • 预测性去匿名化攻击(PDAA):重塑数据安全攻防边界
  • Redis核心数据结构详解与应用
  • 手搓transformer
  • day22
  • Python----神经网络发(神经网络发展历程)
  • Ubuntu系统Docker安装失败问题解决办法
  • 在Vue2项目中引入ElementUI详细步骤
  • 【论文阅读32】预期寿命预测(2024)
  • 用于在多个远程计算机上求解的 Ansoft RSM 设置 - Windows
  • 关于使用EasyExcel、 Vue3实现导入导出功能
  • SpringCloud + MybatisPlus:多租户模式与实现
  • 《网络安全与防护》知识点复习
  • 做网站常见程序/快速关键词排名首页
  • 贵阳建网站公司/seo快速排名案例
  • 高端网站建设收费为何比较贵/外贸公司一般怎么找客户
  • 网站建设谈单思路/北京搜索优化推广公司
  • 江西省上饶市城乡建设网站/高端网站建设哪家便宜
  • 甘肃网站开发/指数平台