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

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。

http://www.xdnf.cn/news/13922.html

相关文章:

  • git报错fatal: 远端意外挂断了
  • 利用亚马逊 API 实现商品详情实时数据采集(开发接入示例)
  • 价格性价比高系列的高性能单片机MS32C001-C
  • 多设备联动,canopen转Ethercat网关设备接入国产 PLC 控制系统方案落地
  • 将python脚本打包进docker
  • Java并发编程实战 Day 20:响应式编程与并发
  • STM32F103x6启动代码的详细分析
  • 如何在python中实现简单的app自动化测试框架?
  • 梯度下降相关
  • Git 首次使用完整设置指南
  • 【专业数据库探索 03】图数据库实战:Neo4j构建智能推荐与关系网络分析系统
  • 动态规划3——背包类动态规划详解
  • 一个圆的周长是如何进行推演计算的?都有哪几种方式?为啥要计算圆的周长?-六年级上册(还需要再学习和思考)
  • Python开发功能项目
  • ‌CDGP|数据治理与AI人工智能:相互依存,互相赋能的新篇章
  • uni-app项目怎么实现多服务环境切换
  • 企业不同发展阶段平衡品牌建设和利润获取的策略-中小企实战运营和营销工作室博客
  • 方法 | B2B营销主题品牌化
  • [vela os_1] docs | Kconfig
  • ff数据解析和解码
  • 多模态AI爬虫:文本+图像智能抓取实战
  • 【cv学习笔记】YOLO系列笔记
  • FFmpeg是什么?
  • 怎么轻松实现报表跨库移植
  • 循环数组中相邻元素的最大差值
  • DEVICENET转MODBUS TCP网关连接ABB机器人配置案例
  • 【android bluetooth 框架分析 04】【bt-framework 层详解 5】【AbstractionLayer介绍】
  • JAVA:深入理解 wait() 和 sleep() 的区别与实战
  • 78Qt窗口_QStatusBar的基本使用
  • centos6.5 老旧系统编译glib-2.58.3.tar.bz2