本文详细介绍了 Uniswap V3 如何存储和计算代币价格的平方根,主要通过一种固定点数格式 (Q64.96) 处理,以提高计算的 gas 效率。同时探讨了代币价格的上下限及其处理方式,深入分析了 Solidity 中不支持浮动小数的原因。
在 Uniswap V2 中,协议跟踪代币储备并推导现货价格 $p_x=y/x$ 和总流动性 $L=xy$, ,其中 $x$ 和 $y$ 是代币 X 和 Y 的储备。
而 Uniswap V3 则跟踪当前价格和流动性,并推导储备。这一计算较为复杂,后面的章节将对其进行详细说明。
Uniswap V3 实际上存储的是价格的平方根 $\sqrt{p}$,而不是价格本身。这样的方法提高了 gas 效率,具体内容将在后面的章节中详细探讨。通过这种方式,我们不会失去准确性,因为价格总是可以从其平方根推导出来。
本章的目标是讨论 Uniswap V3 存储价格平方根 $\sqrt{p}$ 的位置和方式,以及如何处理代币价格可能为小数值,而 Solidity 并没有浮点或小数类型的问题。
价格的平方根存储在 池合约 中的结构体 slot0
的字段变量 sqrtPriceX96
中,如下图所示。
结构体 slot0
还存储其他变量,如 tick
,表示当前刻度,如我们在 刻度章节 中看到的。slot0
中的其他变量与预言机、费用或合约安全性相关,后续将进行检查。
变量名 sqrtPriceX96
已经表明存储值是价格的平方根 (sqrtPrice
),以 Q96 数字格式 (X96
) 存储,特别是 Q64.96 格式。
Q 数字格式 的详细说明在前一章中提供。我们将在接下来的部分简要回顾其在 Uniswap V3 中的使用。
Uniswap V3 将价格的平方根存储为定点数。
定点数允许我们以高效的 gas 使用来表示分数。例如,假设我们需要存储 1.0050122696230506
,但只能存储整数。一个方法是将该值乘以一个大数,比如 $2^{96}$,结果为:
$$ 1.0050122696230506 \times 2^{96}=79625275426524700982079509374.66678 $$
然后我们丢弃小数部分,存储 79625275426524700982079509374
。
因此,定点表示 Q64.96 和(原始)数字之间的关系为:
$$ \text{value_in_Q64.96} = \text{floor}( \text{original_value} \times 2^{96}) $$
要恢复原始值,我们只需将定点表示除以 $2^{96}$,公式为:
$$ \text{original_value} \approx \frac{\text{value_in_Q64.96}}{2^{96} } $$
在我们的例子中,值 1.0050122696230506
表示为定点数:79625275426524700982079509374
。要恢复原始值,我们将该值除以 $2^96$,得到大约 1.0050122696230507
,这非常接近原始数字。
这正是 Uniswap V3 处理 Solidity 没有浮点或小数类型方法。存储代币价格平方根的变量 sqrtPriceX96
是通过将实际的、可能为分数的价格 p 的平方根乘以 $2^96$ 得到的定点数。
sqrtPriceX96
之间的关系$\sqrt{p}$ 和 sqrtPriceX96
之间的关系是,$\sqrt{p}$ 是价格的实际平方根,而 sqrtPriceX96
表示代币价格在 Q64.96 格式中的平方根。
这种关系可以用一个简单的公式表示。要将 $\sqrt{p}$ 转换为 sqrtPriceX96
,我们使用:
$$ \text{sqrtPriceX96} = \text{floor}(\sqrt{p} \times 2^{96}) $$
要将 sqrtPriceX96
转回 $\sqrt{p}$ ,我们使用:
$$ \text{sqrtPriceX96} = \text{floor}(\sqrt{p} \times 2^{96}) $$
sqrtPriceX96
下面是将价格转换为 sqrtPriceX96
并随后检索原始价格的 Python 代码:
from decimal import Decimal, getcontext
getcontext().prec = 100
price = Decimal('1.0050122696230506')
sqrtPriceX96 = price * Decimal(2) ** 96 # Decimal('79625275426524700982079509374.6667867672150016')
original_price = sqrtPriceX96 / 2 ** 96 # Decimal('1.0050122696230506')
使用 decimal 库以增加计算精度。
sqrtPriceX96
值的示例我们来通过一些示例进行讲解。
sqrtPriceX96
中存储为:$$ 100 \times 2^{96}= 7922816251426433759354395033600 $$
sqrtPriceX96
中存储为:$$ 323.002 \times 2^{96} = 25590854948432409571389883046428 $$
要恢复原始(实际)值,只需将上述值除以 $2^{96}$即可。
当前 Base 中 ETH:DAI 池的 sqrtPriceX96
值为 4552234755200983230583166215033
,如下图所示。
要将 sqrtPriceX96
转换为 $ \sqrt{p} $,我们将获得的值除以 $2^{96}$。因此:
$$ \sqrt{p} =\frac{4552234755200983230583166215033}{2^{ 96}}≈57.46942221802943 $$
通过平方价格的平方根,我们得到实际价格:
$$ p = (\sqrt{p})^2 = 57.46942221802943^2=3302.7344900741346 $$
这代表了在本文撰写时以 DAI 计价的以太币的价值。
作为另一个示例,我们检索 主网中的 USDC:ETH 池的 sqrtPriceX96
。这个示例与前一个示例不同,有两个原因:首先,价格是以以太币计算 USDC,而不是以稳定币计算以太币。其次,以太币有 18 位小数,而 USDC 只有 6 位,与前一个示例中两个代币都有 18 位小数不同。
如图所示,该值为 1506673274302120988651364689808458
。
值 $ \sqrt{p} $ 可以计算为:
$$ \sqrt{p}=\frac{1506673274302120988651364689808458}{2^{96}} = 19016.89028861243 $$
价格可以计算为:
$$ p = (\sqrt{p})^2= 19016.89028861243 ^2 \approx 361642116.2491218 $$
由于这是一个 USDC:ETH 池,我们计算的是 USDC 以至于 ETH 的价格。以太坊相对于 USDC 的价格由所获得价格的倒数给出:
$$ p_{y} = \frac{1}{p_x} \approx \frac{1}{361642116.2491218} \approx 2.7651646616046713 \times 10^{-9} $$
最后,USDC 有 6 位小数,而 ETH 有 18 位小数。我们需要考虑这个差异;要计算 ETH 以 USDC 计价的价格,我们必须将以上值乘以 10¹²。
$$ p \approx 2.7651646616046713 \times 10^{-9} \times 10^{12} \approx 2765.1646616046713 $$
这代表了在本文撰写时以 USDC 计价的以太币的价值。以太币在我们撰写时是极其波动的。
由于价格的平方根存储在 Q64.96
格式的数字中,它所能存储的最大整数约为 $2{64}$。与该平方根对应的价格可以通过平方该最大整数获得。
因此,协议能够处理的最大价格的平方根约为 $2^{64}$,且最大价格略低于 $2¹²⁸$。
定点数派生于 $2^{96}$ ,可以表示小至 $2⁻⁹⁶$ 的分数。这是因为 2⁻⁹⁶ 在乘以 $2^{96}$ 时,结果为 1
,可以存储为整数。
小于 $2⁻⁹⁶$ 的值超出范围。例如,$2⁻⁹⁷$ 转换为定点数,通过乘以 $2^{96}$,变为 $ 2⁻⁹⁷×2^{96}=2⁻¹$,或 0.5。由于只保留整数部分,向下取整为 0
,导致原始值的信息丢失。
因此,理论上,协议可以处理的最低价格是 $(2⁻⁹⁶)²$,或 $2⁻¹⁹²$。然而,正如我们将在下一章看到的那样,协议不允许如此低的价格。
协议在代币可以承受的最大价格平方根 $2^{64}$ 和最小价格平方根 由 $2⁻⁶⁴$ 限制之间施加了对称性。这个对称性是有道理的,因为代币 X 相对于 Y 的价值是代币 Y 相对于 X 的价值的倒数。
这并不是一个容易回答的问题,协议团队本可以选择其他的 Q 数字格式。由于他们决定将价格的平方根与刻度及其他信息打包在一个 256 位存储槽中,因此留给价格的平方根的空间为 160 位。
在下一节中,我们将展示如何使用 64 位来表示整数部分足以容纳真实世界场景中的价格,使协议能够支持一个币值数万亿美元的池,而另一个币只值几分钱。
正如我们所见,协议可以处理的最高价格约为 $2¹²⁸$ ($(2^{64})²$),或数字上的大约 $10³⁸$。由于代币的价格始终是相对于另一代币,池中两个代币之间的价格差异可达到 $10³⁸$ 个数量级。
我们还必须小心计算价格差异时考虑到小数。例如,具有 18 位小数的代币以 $10¹⁸$ 单位存储在其合约中,而具有 8 位小数的代币以 $10⁸$ 单位储存。因此,如果两个代币的美元价值相同,池中它们的价格差异已经因小数位的不同而有 10 个数量级的差异。
让我们考虑一个真实的例子,一个 WBTC:PEPE 池。目前,1 WBTC 的价值大约为 100,000 ($10⁵$) 美元,而 1 PEPE 的价值大约为 0.00001 ($10⁻⁵$) 美元,二者之间存在 10 个数量级的差异。
然而,我们还必须考虑小数位的不同。WBTC 有 8 位小数,而 PEPE 有 18 位小数,产生了 10 个数量级的差异,此外还存在由于价格差异导致的 10 个数量级的差异。
因此,WBTC 代币的最小单位(一 Satoshi,或 $10⁻⁸$ WBTC)价值约为 $10²⁰$(即一百亿亿)倍于 PEPE 最小单位(没有名称,但代表 $10⁻¹⁸$ PEPE)的价值。
二十个数量级是一个显著的差异,但 Uniswap V3 池允许多达 38 个数量级的差异。因此,WBTC 相对于 PEPE 的价格可以再增加 18 个数量级,才会达到 Uniswap V3 的限制。
假设 PEPE 保持在当前价格(0.00001 美元),那么比特币的价格必须达到 $10²³$ 美元,才会到达 Uniswap V3 的价格限制。类似地,如果比特币的价格达到 1 万亿美元,PEPE 的价格可以低至 0.0000000000000001 美元(即千分之一的万亿分之一美元),才会到达协议的价格限制。
推导这些限制是读者的练习。
- 原文链接: rareskills.io/post/unisw...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!