用bitmap来建立一个 ticks 的索引
contract UniswapV3Pool {
using TickBitmap for mapping(int16 => uint256);
mapping(int16 => uint256) public tickBitmap;
...
}
这里的存储方式是一个mapping,key 的类型是 int16
,value 的类型是 uint256
。想象一个无穷的 0/1 数组:
举例计算某个tick的位置:
tick = 85176
word_pos = tick >> 8 # or tick // 2**8
bit_pos = tick % 256
print(f"Word {word_pos}, bit {bit_pos}")
# Word 332, bit 184
也就是说位于85176的tick,在用bitmap存储的时候是tickBitmap[332]=184;
在uniswap V3用position表示这个tick的位置
function position(int24 tick) private pure returns (int16 wordPos, uint8 bitPos) {
wordPos = int16(tick >> 8);
bitPos = uint8(uint24(tick % 256));
}
function flipTick(
mapping(int16 => uint256) storage self,
int24 tick,
int24 tickSpacing
) internal {
require(tick % tickSpacing == 0); // ensure that the tick is spaced
(int16 wordPos, uint8 bitPos) = position(tick / tickSpacing);
uint256 mask = 1 << bitPos;
self[wordPos] ^= mask;
}
//mask 二进制表示 从右到左的184位为1
10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
tickBitmap[332]的初始值全为0的情况下,self[wordPos] ^= mask;之后的结果是第184位变成1。
在 swap 过程中,我们需要找到现在 tick 左边或者右边的下一个有流动性的 tick。因为同一个堆栈里面左边是高位,右边是低位,所以在同一个word内,左边的价格比右边高。
当卖出 token x (在这里即 ETH)时,找到在同一个 word 内当前tick的右边下一个有流动性的tick。
当卖出 token y (在这里即 USDC)时,找到在同一个 word 内当前tick的左边下一个有流动性的tick。
在nextInitializedTickWithinOneWord函数里面,
if (lte) { //lte为true表示卖出x
(int16 wordPos, uint8 bitPos) = position(compressed);
//(1 << bitPos) - 1 表示类似于1000-1=111,(1 << bitPos)=1000,相加为1111,即当前位及所有右边的位为 1
uint256 mask = (1 << bitPos) - 1 + (1 << bitPos);
//将掩码覆盖到当前 word 上,得出右边的所有位
uint256 masked = self[wordPos] & mask;
//如果右边有为1的tick,那么表示右边有流动性。
initialized = masked != 0;
//下一个有流动性的 tick 位置,或者下一个 word 的最左边一位
next = initialized
? (compressed - int24(uint24(bitPos - BitMath.mostSignificantBit(masked)))) * tickSpacing
: (compressed - int24(uint24(bitPos))) * tickSpacing;
} else { //买入x的时候方向相反
(int16 wordPos, uint8 bitPos) = position(compressed + 1);
//当前位置所有左边的位为 1;
uint256 mask = ~((1 << bitPos) - 1);
uint256 masked = self[wordPos] & mask;
...
按照图中的点a-f,假设在进入a之前没有流动性,那么从左到右:
进入a的时候,流动性Liquidity= 10ETH
到达b的时候,流动性Liquidity+20ETH=30ETH
到达c的时候,流动性Liquidity-10ETH=20ETH
到达d的时候,流动性Liquidity+20ETH=40ETH
到达e的时候,流动性Liquidity-20ETH=20ETH
到达f的时候,流动性Liquidity-20ETH=20ETH
如果我是a-c的10个ETH的提供者,那么在a点我有10个eth,在c点全部换成了USDC,如果我是在ac的重点开始提供流动性,那么我将提供一半eth,一半USDC。
function update(
mapping(int24 => Tick.Info) storage self,
int24 tick,
int128 liquidityDelta,
bool upper
) internal returns (bool flipped) {
...
tickInfo.liquidityNet = upper
? int128(int256(tickInfo.liquidityNet) - liquidityDelta)
: int128(int256(tickInfo.liquidityNet) + liquidityDelta);
}//当经过上限时,是增加还是减少取决于方向
在做swap的时候需要循环遍历找到两个tick之间的所有tick,十分消耗gas。为了节省gas,用tickSpacing来调节间隔,tickSpacing越大,越节省gas。但是tick 间隔越大,精度越低。价格波动大的交易对可以容忍价格精度低,但是价格波动小的交易对就需要高的价格精度。所以Uniswap设置了10,60,200作为tickSpacing的选项。