在之前的章节中,我们了解到该协议存储的是价格的平方根而不是价格本身。因此,有必要将 tick 与定点数 Q64.96 格式表示的价格平方根联系起来。
在本章中,我们将探讨如何在 sqrtPriceX96 和 tick 之间进行转换。
协议存储 Tick 索引
tick 是离散的价格,由公式 p ( i ) = 1.0001 i p(i)=1.0001^i p ( i ) = 1.000 1 i 给出,其中 i i i 被称为 tick 索引(tick index),或简称为 tick。拥有 tick 索引后,就可以唯一确定 tick 价格,反之亦然。由于存储 tick 索引 i i i 比存储 p ( i ) p(i) p ( i ) 占用的位数更少,因此协议存储的是 tick 索引而不是 tick 价格。
在本书中,我们将交替使用 tick (价格)和 tick 索引 这两个术语。在代码库中,名为 tick 的变量始终指代 tick 索引 i i i 。
给定 sqrtPriceX96 计算 Tick 索引
我们首先回顾一下价格的平方根与变量 sqrtPriceX96 之间的关系:
( sqrtPriceX96 2 96 ) = p \left( \frac{\text{sqrtPriceX96}}{2^{96}}\right) = \sqrt{p} ( 2 96 sqrtPriceX96 ) = p
因此,通过对两边求平方,即可得出价格与 sqrtPriceX96 之间的关系:
( sqrtPriceX96 2 96 ) 2 = p \left( \frac{\text{sqrtPriceX96}}{2^{96}}\right)^2 = p ( 2 96 sqrtPriceX96 ) 2 = p
由于价格和 tick 索引之间的关系为 p = 1.0001 i p = 1.0001^i p = 1.000 1 i ,我们可以得出
( sqrtPriceX96 2 96 ) 2 = 1.0001 i \left( \frac{\text{sqrtPriceX96}}{2^{96}}\right)^2 = 1.0001^i ( 2 96 sqrtPriceX96 ) 2 = 1.000 1 i
为了用 sqrtPriceX96 来确定 tick 索引 i i i ,我们对上述等式的两边取对数。
log ( ( sqrtPriceX96 2 96 ) 2 ) = log ( 1.0001 i ) 2 log ( sqrtPriceX96 2 96 ) = i log ( 1.0001 ) 利用 log ( a b ) = b log ( a ) i = 2 log ( sqrtPriceX96 2 96 ) log ( 1.0001 ) 两边同除以 log ( 1.0001 ) \begin{align*}
\log \left( \left( \frac{\text{sqrtPriceX96}}{2^{96}}\right)^2 \right) &= \log (1.0001^i) \\ 2 \, \log \left( \frac{\text{sqrtPriceX96}}{2^{96}}\right) &= i \, \log(1.0001) && \text{利用} \;\log(a^b) = b \; \log(a) \\ i &= \frac{2 \, \log \left( \frac{\text{sqrtPriceX96}}{2^{96}}\right)}{\log(1.0001)} && \text{两边同除以 } \log (1.0001)
\end{align*} log ( ( 2 96 sqrtPriceX96 ) 2 ) 2 log ( 2 96 sqrtPriceX96 ) i = log ( 1.000 1 i ) = i log ( 1.0001 ) = log ( 1.0001 ) 2 log ( 2 96 sqrtPriceX96 ) 利用 log ( a b ) = b log ( a ) 两边同除以 log ( 1.0001 )
对数总是基于某个底数计算的。例如,如果 b a = c b^a=c b a = c ,那么 log b c = a \log_b{c}=a log b c = a (以 b 为底取对数)。然而,上述等式中的对数可以以任意数值为底。对此的原因将在本章最后一节中解释。
Tick 索引是一个离散值
实际上,上述公式并不完全正确,原因在于:sqrtPriceX96 是一个连续值,而 tick 索引 i 是离散的(一个整数)。因此,该值需要向下取整。
这可以从下面的动画中看出来,其中我们使用了一个可以在我们关于 tick 的文章中找到的工具。请注意,虽然价格在不断变化,但该价格对应的 tick 始终是紧挨着它的下方那一个。
因此,tick 的精确公式为:
i = ⌊ 2 log ( sqrtPriceX96 2 96 ) log ( 1.0001 ) ⌋ i = \lfloor \frac{2 \, \log \left( \frac{\text{sqrtPriceX96}}{2^{96}}\right)}{\log(1.0001)} \rfloor i = ⌊ log ( 1.0001 ) 2 log ( 2 96 sqrtPriceX96 ) ⌋
其中符号 ⌊ . . . ⌋ \lfloor ...\rfloor ⌊ ... ⌋ 表示向下取整,例如,⌊ 3.14 ⌋ = 3 \lfloor 3.14 \rfloor = 3 ⌊ 3.14 ⌋ = 3 。
给定 Tick 索引计算 sqrtPriceX96
为了从 tick 索引计算价格的平方根,我们从以下公式开始:
( sqrtPriceX96 2 96 ) 2 = 1.0001 i \left( \frac{\text{sqrtPriceX96}}{2^{96}}\right)^2 = 1.0001^i ( 2 96 sqrtPriceX96 ) 2 = 1.000 1 i
然后,两边取平方根并乘以 2 96 2^{96} 2 96 ,
sqrtPriceX96 2 96 = 1.0001 i sqrtPriceX96 = 1.0001 i ⋅ 2 96 \begin{align*}
\frac{\text{sqrtPriceX96}}{2^{96}} &= \sqrt{1.0001^{i}} \\ \text{sqrtPriceX96} &= \sqrt{1.0001^{i}} \cdot 2^{96}
\end{align*} 2 96 sqrtPriceX96 sqrtPriceX96 = 1.000 1 i = 1.000 1 i ⋅ 2 96
在代码库中实现 Tick 与价格之间的相互转换
在 Solidity 中,tick 和 sqrtPriceX96 之间的转换是由 TickMath library 处理的,由于在 Solidity 中实现对数和平方根较为复杂,因此我们将在单独的一章中对其进行研究。
它包含两个函数:getSqrtRatioAtTick 和 getTickAtSqrtRatio,它们负责执行 sqrtPriceX96 与其对应的 tick 索引之间的相互转换。如下所示。
我们在上一节中已经了解了这些公式的数学原理。在本节中,我们将使用 Python 来执行这一计算。
getSqrtRatioAtTick 函数
Python 中的 getSqrtRatioAtTick 函数可以写为:
Copy def getSqrtRatioAtTick (i):
return math.sqrt( 1.0001 ** i) * 2 ** 96
举个例子,价格上限的 sqrtPriceX96 值可以使用 getSqrtRatioAtTick(887272) 来计算,结果为 1.4614467034780703e+48,这(大约)等于 TickMath 库中的 MAX_SQRT_RATIO 常量。
使用 Decimal 库
为了获得更精确的结果,我们可以使用 Python 中的 Decimal 库。下面是使用该库编写的相同公式,以及计算得出的 MAX_SQRT_RATIO 的值。
Copy import math
from decimal import Decimal, getcontext
getcontext().prec = 50
def getSqrtRatioAtTick (i):
base = Decimal( "1.0001" )
exponent = Decimal(i)
sqrt_value = base ** (exponent / 2 )
multiplier = Decimal( 2 ) ** 96
return sqrt_value * multiplier
print ( int (getSqrtRatioAtTick( 887272 ))) # 1461446703485210103244672773810124308346321380902
getTickAtSqrtRatio 函数
Python 中的 getTickAtSqrtRatio 函数可以写为:
Copy def getTickAtSqrtRatio (sqrtPriceX96):
return math.floor( 2 * math.log(sqrtPriceX96 / 2 ** 96 ) / math.log( 1.0001 ))
举个例子,sqrtPriceX96 下限值 4295128739 对应的 tick,可以使用 getTickAtSqrtRatio(4295128739) 来计算,结果为 -887272,这符合 tick 下限的预期。
Copy getTickAtSqrtRatio ( 4295128739 ) // -887272
改变对数的底数
当我们推导公式时:
i = ⌊ 2 log ( sqrtPriceX96 2 96 ) log ( 1.0001 ) ⌋ i = \lfloor \frac{2 \, \log \left( \frac{\text{sqrtPriceX96}}{2^{96}}\right)}{\log(1.0001)} \rfloor i = ⌊ log ( 1.0001 ) 2 log ( 2 96 sqrtPriceX96 ) ⌋
我们提到过对数可以以任意数值为底。在 Python 的公式实现中,我们使用了自然对数,但我们也可以使用以 10 为底或任何其他底数的对数,这都会产生相同的结果。
原因在于对数的一个基本性质(换底公式),它将不同底数的对数联系起来:
log b ( k ) = log a ( k ) log a ( b ) \log_{\boxed{b}}(k) = \frac{\log_a(k)}{\log_a(\boxed{b})} log b ( k ) = log a ( b ) log a ( k )
其中 a a a 和 b b b 是两个不同的底数,而 k k k 是我们想要计算的对数的真数。例如,将以自然数 e e e 为底的对数转换为以 10 为底的对数,我们得到:
log 10 ( k ) = log e ( k ) log e ( 10 ) \log_{10}(k) = \frac{\log_e(k)}{\log_e(10)} log 10 ( k ) = log e ( 10 ) log e ( k )
需要注意的关键点是,除数 log e ( 10 ) \log_e{(10)} log e ( 10 ) 仅取决于底数,而与真数无关。假设我们有一个分数:
log 10 ( x ) log 10 ( y ) \frac{\log_{10}(x)}{\log_{10}(y)} log 10 ( y ) log 10 ( x )
如果我们想将对数转换为某个任意底数 b,我们将分子和分母都除以 log b ( 10 ) \log_b(10) log b ( 10 ) 。然而,将一个分数的分子和分母都除以同一个值对最终答案没有影响,因为它会被消掉。
让我们将这一关系应用到我们的 tick 索引 i i i 公式中,并将其从自然底数转换为以 10 为底,来更清楚地说明这一点:
i = ⌊ 2 log e ( sqrtPriceX96 2 96 ) log e ( 1.0001 ) ⌋ i = ⌊ 2 log 10 ( sqrtPriceX96 2 96 ) / log 10 ( e ) log 10 ( 1.0001 ) / log 10 ( e ) ⌋ 从以 e 为底换算为以 10 为底 i = ⌊ 2 log 10 ( sqrtPriceX96 2 96 ) / log 10 ( e ) log 10 ( 1.0001 ) / log 10 ( e ) ⌋ i = ⌊ 2 log 10 ( sqrtPriceX96 2 96 ) log 10 ( 1.0001 ) ⌋ \begin{align*}
i &= \lfloor \frac{2 \, \log_e \left( \frac{\text{sqrtPriceX96}}{2^{96}}\right)}{\log_e(1.0001)} \rfloor \\i &= \lfloor \frac{2 \, \log_{10} \left( \frac{\text{sqrtPriceX96}}{2^{96}}\right) / \log_{10}(e)}{\log_{10}(1.0001) / \log_{10}(e)} \rfloor && \text{从以 e 为底换算为以 10 为底} \\ i &= \lfloor \frac{2 \, \log_{10} \left( \frac{\text{sqrtPriceX96}}{2^{96}}\right) / \cancel{\log_{10}(e)}}{\log_{10}(1.0001) / \cancel{\log_{10}(e)}} \rfloor \\ i &= \lfloor \frac{2 \, \log_{10} \left( \frac{\text{sqrtPriceX96}}{2^{96}}\right)}{\log_{10}(1.0001)} \rfloor
\end{align*} i i i i = ⌊ log e ( 1.0001 ) 2 log e ( 2 96 sqrtPriceX96 ) ⌋ = ⌊ log 10 ( 1.0001 ) / log 10 ( e ) 2 log 10 ( 2 96 sqrtPriceX96 ) / log 10 ( e ) ⌋ = ⌊ log 10 ( 1.0001 ) / log 10 ( e ) 2 log 10 ( 2 96 sqrtPriceX96 ) / log 10 ( e ) ⌋ = ⌊ log 10 ( 1.0001 ) 2 log 10 ( 2 96 sqrtPriceX96 ) ⌋ 从以 e 为底换算为以 10 为底
正如我们所看到的,转换因子被抵消了,无论底数是多少,计算结果都是一样的。
总结
协议需要在 sqrtPriceX96 和 tick 索引之间进行转换。这可以通过位于 TickMath 库中的 getSqrtRatioAtTick 和 getTickAtSqrtRatio 函数来完成。
要将 sqrtPriceX96 转换为 tick 索引,应使用以下公式:
i = ⌊ 2 log ( sqrtPriceX96 2 96 ) log ( 1.0001 ) ⌋ i = \lfloor \frac{2 \, \log \left( \frac{\text{sqrtPriceX96}}{2^{96}}\right)}{\log(1.0001)} \rfloor i = ⌊ log ( 1.0001 ) 2 log ( 2 96 sqrtPriceX96 ) ⌋
要将 tick 索引转换为 sqrtPriceX96,应使用以下公式:
1.0001 i ⋅ 2 96 \sqrt{1.0001^{i}} \cdot 2^{96} 1.000 1 i ⋅ 2 96
练习题
浏览 Uniswap V3 Pools 找到矿池地址,然后在区块链浏览器中打开该地址。找到公共变量 slot0 并将 tick 转换为 price(价格),反之亦然。
检查你的计算结果是否接近 Uniswap 提供的值,然后将 sqrtPriceX96 转换为美元金额。
对以下矿池进行练习:
以太坊上的 USDC/ETH
Base 上的 ETH/USDC
主网上的 WBTC/USDC
稍后我们将学习为什么智能合约直接使用 slot0 中的 sqrtPriceX96(可用于获取价格)和 tick 数据是不安全的,目前这仅仅是为了练习。