本文解释了 Uniswap V3 TickMath 库中的 getSqrtRatioAtTick() 函数是如何工作的。getSqrtRatioAtTick() 函数接收一个 tick 索引,并返回该 tick 处精确的平方根价格,格式为 Q64.96 的 Q-number。该函数计算:
其中 是 tick 索引。以下是该函数的截图:

本教程假设读者已经理解了我们对平方乘算法(square-and-multiply algorithm)的探讨,这也是 getSqrtRatioAtTick() 所依赖的算法。在这里我们会经常引用该教程中的概念,因此建议读者先复习一下那篇文章。
getSqrtPriceRatioAtTick 概述
该函数包含以下步骤:
- 使用代码
uint256 absTick = tick < 0 ? uint256(-int256(tick)) : uint256(int256(tick));计算 tick 的绝对值。 - 检查 tick 是否在最小和最大 tick 范围内,如果越界则回退(revert):
require(absTick <= uint256(MAX_TICK), 'T');。 - 使用平方乘算法计算 ,结果为 Q128.128 格式。
- 如果原始 tick 是正数,则计算
- 通过
>> 32将 Q128.128 数字转换为 Q64.96 数字
以下是代码中概述的步骤:

第 1/5 部分:为什么 Uniswap V3 要计算 tick 的绝对值,即
该函数的第一行代码计算了 tick 的绝对值:
uint256 absTick = tick < 0 ? uint256(-int256(tick)) : uint256(int256(tick));
Tick 索引可以是正数也可以是负数。为了避免分别处理这两种情况,getSqrtRatioAtTick() 中的平方乘部分仅计算负数 tick。如果原始 tick 是正数,那么会在平方乘算法的结果基础上计算其倒数。
例如,如果原始 tick 是正 5,算法会计算 -5 的 tick:
然后计算倒数:
观察到通常情况下:
因此,该函数首先计算:
接着,如果 tick 本来是正数,它会通过返回倒数来对答案求逆:
如果 tick 本来是负数,代码则不会计算倒数。
第 2/5 部分:检查 tick 是否在范围内
函数中的第二行代码不言自明:
require(absTick <= uint256(MAX_TICK), 'T');
MAX_TICK 是文件中的一个常量 887272,我们在关于 Tick Limits 的文章中推导过这个值。我们不需要检查 tick 是否小于 MIN_TICK,因为我们已经计算了 tick 的绝对值,所以 absTick 不可能是负数。
第 3/5 部分:使用平方乘计算价格
在本节中,我们将展示该函数是如何使用平方乘算法的,并推导函数中的大常数。
用于计算价格的变量是 ratio,但返回的变量是 sqrtPriceX96。
以下是该函数中使用平方乘的相关部分:
uint256 ratio = absTick & 0x1 != 0 ? 0xfffcb933bd6fad37aa2d162d1a594001 : 0x100000000000000000000000000000000;
if (absTick & 0x2 != 0) ratio = (ratio * 0xfff97272373d413259a46990580e213a) >> 128;
if (absTick & 0x4 != 0) ratio = (ratio * 0xfff2e50f5f656932ef12357cf3c7fdcc) >> 128;
if (absTick & 0x8 != 0) ratio = (ratio * 0xffe5caca7e10e4e61c3624eaa0941cd0) >> 128;
if (absTick & 0x10 != 0) ratio = (ratio * 0xffcb9843d60f6159c9db58835c926644) >> 128;
if (absTick & 0x20 != 0) ratio = (ratio * 0xff973b41fa98c081472e6896dfb254c0) >> 128;
if (absTick & 0x40 != 0) ratio = (ratio * 0xff2ea16466c96a3843ec78b326b52861) >> 128;
if (absTick & 0x80 != 0) ratio = (ratio * 0xfe5dee046a99a2a811c461f1969c3053) >> 128;
if (absTick & 0x100 != 0) ratio = (ratio * 0xfcbe86c7900a88aedcffc83b479aa3a4) >> 128;
if (absTick & 0x200 != 0) ratio = (ratio * 0xf987a7253ac413176f2b074cf7815e54) >> 128;
if (absTick & 0x400 != 0) ratio = (ratio * 0xf3392b0822b70005940c7a398e4b70f3) >> 128;
if (absTick & 0x800 != 0) ratio = (ratio * 0xe7159475a2c29b7443b29c7fa6e889d9) >> 128;
if (absTick & 0x1000 != 0) ratio = (ratio * 0xd097f3bdfd2022b8845ad8f792aa5825) >> 128;
if (absTick & 0x2000 != 0) ratio = (ratio * 0xa9f746462d870fdf8a65dc1f90e061e5) >> 128;
if (absTick & 0x4000 != 0) ratio = (ratio * 0x70d869a156d2a1b890bb3df62baf32f7) >> 128;
if (absTick & 0x8000 != 0) ratio = (ratio * 0x31be135f97d08fd981231505542fcfa6) >> 128;
if (absTick & 0x10000 != 0) ratio = (ratio * 0x9aa508b5b7a84e1c677de54f3e99bc9) >> 128;
if (absTick & 0x20000 != 0) ratio = (ratio * 0x5d6af8dedb81196699c329225ee604) >> 128;
if (absTick & 0x40000 != 0) ratio = (ratio * 0x2216e584f5fa1ea926041bedfe98) >> 128;
if (absTick & 0x80000 != 0) ratio = (ratio * 0x48a170391f7dc42444e8fa2) >> 128;
ratio 是 Q128.128 而 sqrtPriceX96 是 Q64.96
为了最大化精度,getSqrtRatioAtTick() 在执行平方乘算法时内部使用了 Q128.128 格式的数字,但将结果作为 Q64.96 返回。价格的内部表示形式即为 ratio 变量。
平方乘预计算回顾
为了解释 Uniswap V3 是如何推导出上述大常数的,我们必须首先回顾平方乘算法。
平方乘算法依赖于对底数的幂进行预计算。我们现在将展示,代码中的大常数是通过对 进行重复平方推导出来的。
使用平方乘算法,getSqrtRatioAtTick() 预计算了:
Uniswap V3 中的最小和最大 tick 分别为 -887,272 和 887,272。然而,由于 getSqrtPriceRatioAtTick 仅直接计算负数部分,并通过倒数来计算正数 tick,因此它只需要计算范围在 [-887,272, 0] 的 tick。编码最大至 887,273(887,272 加上 0)的数字所需的位数为 20,因为 。这就是为什么预计算的值范围是从 , , 直到 。
举个例子,假设我们要计算 tick -100 处的价格。我们可以将预计算的 -64、-32 和 -4 的 tick 值相乘,如下所示:
将大常数值推导为 Q128.128 格式数字
现在我们将展示 Uniswap V3 是如何推导这些大常数的。
tick 0 的平方根价格
大常数 0x100000000000000000000000000000000(紫色框)是 Q128.128 格式的定点数 (等价于 2 << 128)。

这对应于 tick 0,。考虑到如果 tick = 0,那么第 27 行(橙色框)的 absTick & 0x1 != 0 将为假(false),从而触发三元运算符的第二部分。
如果该 tick 是 0,则其他位都不会是 1,因此随后的任何条件 if (absTick & 0xXX !=0) 均不会为真(true),这意味着 ratio 将等于 0x100000000000000000000000000000000 且不进行进一步修改。这是符合预期的,因为任何数字的零次方都返回 1。
tick -1 的平方根价格
如果我们使用 Python 计算 Q128.128 格式的 tick -1 的价格,会得到以下结果:
>>>
0xfffcb933bd6f b0000000000000000000 # gap added for clarity
转换为十六进制时,它接近于下方高亮的魔数(magic number),但很明显我们上面的估计值在末尾有更多的零,这意味着它存在精度损失:

以下是 Uniswap 用于 的常数与我们的估计值的比较:
# Uniswap's constant
>>> hex(0xfffcb933bd6fad37aa2d162d1a594001)
0xfffcb933bd6f ad37aa2d162d1a594001 # gap added for clarity
# Our constant
>>> hex(int(2**128 * math.sqrt(1.0001**-1)))
0xfffcb933bd6f b0000000000000000000 # gap added for clarity
# note that Uniswap's and our estimate differ after the gap
在下一节中,我们将展示如何改进我们的估计值以匹配 Uniswap V3 的结果。
使用 Decimal 和重排除法来改进常数计算
默认情况下,Python 浮点数没有足够的精度来计算具有 128 位精度的数字,但我们可以通过使用 Decimal 库来解决这个问题,它能提供任意所需的精度:
from decimal import *
getcontext().prec = 100 # use 100 decimals of precision is ~333 bits
此外,为了消除涉及小数的不精确性,我们可以将:
计算为:
这样就消除了小数 1.0001。
我们可以通过避免负指数(负指数意味着隐含的除法)来再次提高精度。观察到我们可以通过翻转分子和分母来去掉负指数:
展望 tick -2,对于第二个负数 tick,我们不再计算 Decimal(10001)**Decimal(-2/2)(或 ),而是将其直接简化为 Decimal(10001)**Decimal(-1),以尽量减少引入不必要的除法操作。
有了这个改动,我们现在可以更准确地复现 Uniswap V3 的预计算值。下面的值乘以了 2**128 以将它们转换为定点数:
from decimal import *
getcontext().prec = 100
# tick -1
print(hex(int(Decimal(10000)**Decimal(1/2) * 2**128 / Decimal(10001)**Decimal(1/2))))
# estim: 0xfffcb933bd6fad37aa2d162d1a594001
# uniV3: 0xfffcb933bd6fad37aa2d162d1a594001
# tick -2
print(hex(int(Decimal(10000)**Decimal(1) * 2**128 / Decimal(10001)**Decimal(1))))
# estim: 0xfff97272373d413259a46990580e2139
# uniV3: 0xfff97272373d413259a46990580e213a
# tick -4
print(hex(int(Decimal(10000)**Decimal(2) * 2**128 / Decimal(10001)**Decimal(2))))
# estim: 0xfff2e50f5f656932ef12357cf3c7fdcb
# uniV3: 0xfff2e50f5f656932ef12357cf3c7fdcc
# tick -8
print(hex(int(Decimal(10000)**Decimal(4) * 2**128 / Decimal(10001)**Decimal(4))))
# estim: 0xffe5caca7e10e4e61c3624eaa0941ccf
# uniV3: 0xffe5caca7e10e4e61c3624eaa0941cd0
# tick -16
print(hex(int(Decimal(10000)**Decimal(8) * 2**128 / Decimal(10001)**Decimal(8))))
# estim: 0xffcb9843d60f6159c9db58835c926643
# univ3: 0xffcb9843d60f6159c9db58835c926644
请注意,我们在上述代码中计算出的常数仅比实际代码值少了 1,这意味着 Uniswap V3 在获取其常数时对小数进行了向上取整(rounding up)。
总而言之,getSqrtRatioAtTick() 中的每一个“魔数”都是以下数字表示为 128 位定点数向上取整的结果(除了 tick 1)。
第 4/5 部分:用 Q128.128 格式数字计算倒数
如下所示的代码行在原始 tick 为正数时计算其倒数。

现在我们解释为什么代码在分子中使用了 type(uint256).max。需要注意的是 ratio 是表示价格的 Q128.128 格式数字。
当用一个定点数 除以另一个定点数 时,我们需要将分子乘以缩放因子(scaling factor),以防止缩放因子相互抵消。
在 Q128.128 中,“1” 的值是 1 << 128 或 。要使用 Q128.128 计算 1 / ratio,我们这样做:
我们需要将分子乘以缩放因子,即 。这会得到:
然而,值 无法在 Solidity 中进行编码,能编码的最大值是 或 type(uint256).max。
这意味着为正数 tick 计算的价格会稍微向下取整(rounded down)。这带来的影响将在最后进行讨论。
第 5/5 部分:将 ratio 转换为 sqrtPriceX96 并向上取整
最后一行代码将作为 Q128.128 格式数字的 ratio 转换为 Q64.96 格式数字,同时进行向上取整并返回值。
sqrtPriceX96 = uint160((ratio >> 32) + (ratio % (1 << 32) == 0 ? 0 : 1));
本文未涉及的重要细节
我们对本文中的代码进行了以下观察:
- 除了 tick -1 外,常数都被向上取整
- 由于代码将 近似为
type(uint256).max,计算正数 tick 的倒数会产生轻微的向下取整。 - 如果
ratio除以1 << 32不能整除,则从 Q128.128 到 Q64.96 的最终转换会进行向上取整(注意上述代码片段中的三元运算符,如果ratio不能完美整除1 << 32,则会加上 1)。
精度的经验测试
测试该 Solidity 函数的方法很简单,只需将代码复制到 IDE 中即可。
我们可以按如下方式在 Python 中创建一个参考实现,以获得以下公式的正确值:
实现如下:
from decimal import *
getcontext().prec = 1000 # set the precision very high
# 2**96 * (1.0001)^(tick/2)
math.ceil(Decimal(2**96)*(Decimal(10001)/Decimal(10000))**(Decimal(tick)/2))
(注意:也可以使用在线全精度计算器)。
我们可以比较极端 tick 时的输出:
# tick 887272 (MAX_TICK)
solidity: 1461446703485210103287273052203988822378723970342
python : 1461446703485210103244672773810124308346321380903
# tick 0
solidity: 79228162514264337593543950336
python : 79228162514264337593543950336
# tick -887272 (MIN_TICK)
solidity: 4295128739
python : 4295128739
如果我们检查 MAX_TICK,会发现 Solidity 代码高估了真实值:
solidity: 14614467034852101032 87273052203988822378723970342
python : 14614467034852101032 44672773810124308346321380903
这个误差是否严重取决于下游逻辑如何使用该函数。
当流动性提供者(liquidity providers)添加或移除流动性,以及交易者进行跨越 tick 的兑换(swap)时,会使用到 getSqrtRatioAtTick()。由于在目前的 Uniswap V3 教程 阶段我们还未讨论这两种机制,因此我们推迟对该函数进行误差分析。
总结
为了计算某个 tick 的平方根价格,getSqrtRatioAtTick() 首先计算 tick()的绝对值,然后循环遍历 最重要的 20 个位来计算 。如果原始 tick 是正数,它会将价格重新计算为 。最后,它将 128 位定点数表示形式转换为 96 位表示形式,并将其作为价格返回。