先上代码:
javascript
function getTickAtSqrtPrice(uint160 sqrtPriceX96) internal pure returns (int24 tick) {
unchecked {
// Equivalent: if (sqrtPriceX96 < MIN_SQRT_PRICE || sqrtPriceX96 >= MAX_SQRT_PRICE) revert InvalidSqrtPrice();
// second inequality must be >= because the price can never reach the price at the max tick
// if sqrtPriceX96 < MIN_SQRT_PRICE, the `sub` underflows and `gt` is true
// if sqrtPriceX96 >= MAX_SQRT_PRICE, sqrtPriceX96 - MIN_SQRT_PRICE > MAX_SQRT_PRICE - MIN_SQRT_PRICE - 1
if ((sqrtPriceX96 - MIN_SQRT_PRICE) > MAX_SQRT_PRICE_MINUS_MIN_SQRT_PRICE_MINUS_ONE) {
InvalidSqrtPrice.selector.revertWith(sqrtPriceX96);
}
uint256 price = uint256(sqrtPriceX96) << 32;
uint256 r = price;
uint256 msb = BitMath.mostSignificantBit(r);
if (msb >= 128) r = price >> (msb - 127);
else r = price << (127 - msb);
int256 log_2 = (int256(msb) - 128) << 64;
assembly ("memory-safe") {
r := shr(127, mul(r, r))
let f := shr(128, r)
log_2 := or(log_2, shl(63, f))
r := shr(f, r)
}
assembly ("memory-safe") {
r := shr(127, mul(r, r))
let f := shr(128, r)
log_2 := or(log_2, shl(62, f))
r := shr(f, r)
}
assembly ("memory-safe") {
r := shr(127, mul(r, r))
let f := shr(128, r)
log_2 := or(log_2, shl(61, f))
r := shr(f, r)
}
assembly ("memory-safe") {
r := shr(127, mul(r, r))
let f := shr(128, r)
log_2 := or(log_2, shl(60, f))
r := shr(f, r)
}
assembly ("memory-safe") {
r := shr(127, mul(r, r))
let f := shr(128, r)
log_2 := or(log_2, shl(59, f))
r := shr(f, r)
}
assembly ("memory-safe") {
r := shr(127, mul(r, r))
let f := shr(128, r)
log_2 := or(log_2, shl(58, f))
r := shr(f, r)
}
assembly ("memory-safe") {
r := shr(127, mul(r, r))
let f := shr(128, r)
log_2 := or(log_2, shl(57, f))
r := shr(f, r)
}
assembly ("memory-safe") {
r := shr(127, mul(r, r))
let f := shr(128, r)
log_2 := or(log_2, shl(56, f))
r := shr(f, r)
}
assembly ("memory-safe") {
r := shr(127, mul(r, r))
let f := shr(128, r)
log_2 := or(log_2, shl(55, f))
r := shr(f, r)
}
assembly ("memory-safe") {
r := shr(127, mul(r, r))
let f := shr(128, r)
log_2 := or(log_2, shl(54, f))
r := shr(f, r)
}
assembly ("memory-safe") {
r := shr(127, mul(r, r))
let f := shr(128, r)
log_2 := or(log_2, shl(53, f))
r := shr(f, r)
}
assembly ("memory-safe") {
r := shr(127, mul(r, r))
let f := shr(128, r)
log_2 := or(log_2, shl(52, f))
r := shr(f, r)
}
assembly ("memory-safe") {
r := shr(127, mul(r, r))
let f := shr(128, r)
log_2 := or(log_2, shl(51, f))
r := shr(f, r)
}
assembly ("memory-safe") {
r := shr(127, mul(r, r))
let f := shr(128, r)
log_2 := or(log_2, shl(50, f))
}
int256 log_sqrt10001 = log_2 * 255738958999603826347141; // Q22.128 number
// Magic number represents the ceiling of the maximum value of the error when approximating log_sqrt10001(x)
int24 tickLow = int24((log_sqrt10001 - 3402992956809132418596140100660247210) >> 128);
// Magic number represents the minimum value of the error when approximating log_sqrt10001(x), when
// sqrtPrice is from the range (2^-64, 2^64). This is safe as MIN_SQRT_PRICE is more than 2^-64. If MIN_SQRT_PRICE
// is changed, this may need to be changed too
int24 tickHi = int24((log_sqrt10001 + 291339464771989622907027621153398088495) >> 128);
tick = tickLow == tickHi ? tickLow : getSqrtPriceAtTick(tickHi) <= sqrtPriceX96 ? tickHi : tickLow;
}
}
公式推导
首先回忆一下uniswap v3/v4的价格公式
这个方法顾名思义是通过价格的平方根求出tick。
对于这类数学计算方法的解析首先要从公式的推导开始,了解其计算步骤才能逐步拆解代码。
整个计算步骤基于的是对数换底公式:
以 a 为底的 b 的对数,等于以 c 为底的 b 的对数除以以 c 为底的 a 的对数。
(其中 a,b,c>0且 a,b≠1, c 是任意可选的新底数。)
总结下来就是:
高中的数学知识,这里稍微回忆一下:
设:,也就是说
两边同时取以c 为底的对数得到:
进一步得出
所以:
为了后续的计算可以转换成位运算,我们用2作为底数,整个计算就可以转换成:
作为一个常数,我们线下计算出定义成代码中的常量即可。整个方法的关键就是计算
。
我们假设,m是整数部分,n是小数部分,接下来的计算主要分为三步:
- 避免浮点型运算,对sqrtPrice做一些前置处理,将其转换成整数
- 计算整数部分
- 计算小数部分
精度转换
这个方法只有一个参数sqrtPriceX96,x96的意思代表此参数是一个小数位精度96的数字,什么意思呢?我们知道计算机在处理计算的过程中所有的数字都用二进制保存,而sqrtPriceX96这个数字的小数是用96位的二进制数保存,精度可以说相当的高了。
@param sqrtPriceX96 The sqrt price for which to compute the tick as a Q64.96
通过代码的注释,我们也可以看出 sqrtPriceX96是一个Q64.96 格式的定点数,就是说整数部分是64位,小数部分是96位。
64和96位数的制定是有一些来由的,这里要从tick说起,我们知道tick的范围是-887272到887272
于是我们得出:
进一步推导得出:
所以
也就是说sqrtPrice的小数部分最多是64位,整数部分最多也是64位,所以sqrtPrice的精度制定最起码需要Q64.64,而实际上Uniswap的许多计算(例getTickAtSqrtPrice
和 getSqrtPriceAtTick
)涉及多次乘法、除法,这些计算的中间结果其精度可能会超过64,为了减小误差,需要把小数部分的精度进一步扩大最终定在Q64.96,这就是sqrtPriceX96参数名称的由来。
接下来看第一行代码:
javascript
if ((sqrtPriceX96 - MIN_SQRT_PRICE) > MAX_SQRT_PRICE_MINUS_MIN_SQRT_PRICE_MINUS_ONE) {
InvalidSqrtPrice.selector.revertWith(sqrtPriceX96);
}
这里的几个常量定义如下:
javascript
uint160 internal constant MIN_SQRT_PRICE = 4295128739;
uint160 internal constant MAX_SQRT_PRICE = 1461446703485210103287273052203988822378723970342;
uint160 internal constant MAX_SQRT_PRICE_MINUS_MIN_SQRT_PRICE_MINUS_ONE =
1461446703485210103287273052203988822378723970342 - 4295128739 - 1;
首先要明确Qn.m定点数的表示方式,为了避免浮点型运算,所有的小数都会被转换成整数,打个比方2.7这个小数转换成Q2.4是什么样呢?
首先2.7转换成二进制是大约是10.1011,接着我们将其转换成Q2.4,Q2.4就是说小数部分最多是4位,所以转换成整数先乘以2的四次方也就是101011后面4位代表小数,前面两位是整数。101011,转换成十进制是43,也就是说给你一个整数43告诉你他的是一个Q2.4格式的定点数,你需要反向推导出它其实是2.7
说这么多就是要解释这几个常量值的由来,前面推导过
转换成Q64.96格式的整数需要先乘以
,也就是
,于是
MIN_SQRT_PRICE = 4295128739;
而maxSqrtPrice转换成Q64.96则是,所以
MAX_SQRT_PRICE = 1461446703485210103287273052203988822378723970342
sqrtPriceX96 必然在这个范围中。
javascript
uint256 price = uint256(sqrtPriceX96) << 32;
为了方便后续的计算需要进一步扩展精度
uint160
→ uint256
,高位补零===》[96个0][64 位整数][96 位小数]
<< 32后===>[64个0][64 位整数][96 位小数][32个0]
此时小数和整数的分界点在第127位和第128位之间(从0位开始算)
对数整数部分的计算
先介绍一个概念:最高有效位(msb)
其代表位二进制格式中值为 1
的最高位的位置(从 0 开始计数)
比如说5转换成二进制是101,他的最高有效位msb=2
这里其实有个定律,假设,那么对数的整数部分m就是最高有效位的值
比如,各位读者也可以拿其他的数字尝试一下,所以要求
中m的值,第一步就是要计算出sqrtPrice的最高有效位(msb)
javascript
uint256 msb = BitMath.mostSignificantBit(r);
后面我会专门开一篇介绍mostSignificantBit方法的实现,这里只需要知晓其作用即可。
对数小数部分的计算
假设其中a代表整数部分,
那么
所以
我们知道msb就是对数的整数部分,然而这里的msb和a并不等价,前面我们说到小数和整数的分界点在第127位和第128位之间,
所以当msb>=128,则a=msb-127,
此时r = price >> (msb - 127)
当msb<128,则a=0,并且后面连续若干个b也为0,具体多少个b为0,取决于msb和127中间的差值,假设msb=125,则
这时为了方便后面的计算我们需要做一些调整,使
javascript
if (msb >= 128) r = price >> (msb - 127);
else r = price << (127 - msb);
故而这两行代码的意义在于计算sqrtPrice排除掉msb部分后的值r
int256 log_2 = (int256(msb) - 128) << 64;
接下来我们要学习一下逐位逼近法,后面的一大段代码都是基于这个算法来的。
javascript
assembly ("memory-safe") {
r := shr(127, mul(r, r))
let f := shr(128, r)
log_2 := or(log_2, shl(63, f))
r := shr(f, r)
}
根据逐位逼近法的思想要计算
第一步计算,前面的推导可以看出
sqrtPrice在去除掉msb部分后是一个127位的数(从0开始算),其小于
,所以
的位数为255位,我们取最高位也就是255位的值 f 作为
的值,假设此时
,
也就是
,我们需要将
的整数部分化掉,所以两边同时除以2,相当于
这就是r := shr(f, r)的作用,如果计算出f=0,则,一定要仔细阅读**逐位逼近法,**否则很难理解!
整数部分和小数部分都计算出得到,我们的目的是计算是计算
,根据对数换底公式
在计算小数部分之前,的结果log_2已经被定义成了一个 Q64.64 格式的数
javascript
int256 log_2 = (int256(msb) - 128) << 64;
我们知道在进行乘法运算之前先要进行小数点对齐,所以要先转换成一个小数点部分有64位的数字:
,
两个64位小数的二进制数相乘会得到一个128位小数的数字,将128位的小数去除掉就是真正的tick值
javascript
// Magic number represents the ceiling of the maximum value of the error when approximating log_sqrt10001(x)
int24 tickLow = int24((log_sqrt10001 - 3402992956809132418596140100660247210) >> 128);
// Magic number represents the minimum value of the error when approximating log_sqrt10001(x), when sqrtPrice is from the range (2^-64, 2^64). This is safe as MIN_SQRT_PRICE is more than 2^-64. If MIN_SQRT_PRICE is changed, this may need to be changed too
int24 tickHi = int24((log_sqrt10001 + 291339464771989622907027621153398088495) >> 128);
误差补偿
由于直接计算的成本过高,整个计算过程是通过
计算的,中间有好几步近似转换,这期间必然会产生误差,所以在方法的背后还需要做一些误差补偿。
TickMath这个文件里面还有一个方法getSqrtPriceAtTick,通过tick计算sqrtPrice,我们把每个tick都通过getSqrtPriceAtTick方法计算出sqrtPrice,再通过getTickAtSqrtPrice方法计算出tick,这里记为tick1,我们统计每一个tick和tick1之间的误差,最终统计出log_sqrt10001 的上限误差和下限误差。这就是最后两个魔法数的来历。
getSqrtPriceAtTick的方法解析看这里。
javascript
tick = tickLow == tickHi ? tickLow : getSqrtPriceAtTick(tickHi) <= sqrtPriceX96 ? tickHi : tickLow;
如果在考虑上限误差和下限误差后再去除掉小数点部分tickLow == tickHi则罢了。否则通过getSqrtPriceAtTick方法做一次矫正。