Uniswap V3中的平方根价格

本文详细介绍了 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 并没有浮点或小数类型的问题。

变量 sqrtPriceX96

价格的平方根存储在 池合约 中的结构体 slot0 的字段变量 sqrtPriceX96 中,如下图所示。

slot0 代码截图

结构体 slot0 还存储其他变量,如 tick,表示当前刻度,如我们在 刻度章节 中看到的。slot0 中的其他变量与预言机、费用或合约安全性相关,后续将进行检查。

变量名 sqrtPriceX96 已经表明存储值是价格的平方根 (sqrtPrice),以 Q96 数字格式 (X96) 存储,特别是 Q64.96 格式。

Q 数字格式 的详细说明在前一章中提供。我们将在接下来的部分简要回顾其在 Uniswap V3 中的使用。

价格的平方根$\sqrt{p}$ 作为定点值

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$ 得到的定点数。

$\sqrt{p}$ 和 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}) $$

使用 Python 计算 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 值的示例

我们来通过一些示例进行讲解。

  1. 如果代币的当前价格平方根 ($\sqrt{p}$) 为 100 ,则该值将在 sqrtPriceX96 中存储为:

$$ 100 \times 2^{96}= 7922816251426433759354395033600 $$

  1. 如果当前价格的平方根为 323.002,则它将在 sqrtPriceX96 中存储为:

$$ 323.002 \times 2^{96} = 25590854948432409571389883046428 $$

要恢复原始(实际)值,只需将上述值除以 $2^{96}$即可。

查询 slot0 中的 sqrtPriceX96 以找到池的价格

Base 中的 ETH:DAI 池

当前 Base 中 ETH:DAI 池的 sqrtPriceX96 值为 4552234755200983230583166215033,如下图所示。

sqrtPrice 截图

要将 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 池除

作为另一个示例,我们检索 主网中的 USDC:ETH 池的 sqrtPriceX96。这个示例与前一个示例不同,有两个原因:首先,价格是以以太币计算 USDC,而不是以稳定币计算以太币。其次,以太币有 18 位小数,而 USDC 只有 6 位,与前一个示例中两个代币都有 18 位小数不同。

如图所示,该值为 1506673274302120988651364689808458

USDCETH 池

值 $ \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 的价值的倒数。

为什么使用 Q64.96 来存储价格的平方根

这并不是一个容易回答的问题,协议团队本可以选择其他的 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 美元(即千分之一的万亿分之一美元),才会到达协议的价格限制。

推导这些限制是读者的练习。

总结

  • Uniswap V3 以 Q64.96 数字格式存储价格的平方根。这样做是因为 Solidity 中没有浮点数,使用定点数字在二进制中更具 gas 效率。
  • 协议可以存储的代币最高价格约为 $2¹²⁸$。为了保持对称性,其最低价格可以存储在 $2⁻¹²⁸$。池中代币价格不得超过这些值。
  • 原文链接: rareskills.io/post/unisw...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
RareSkills
RareSkills
https://www.rareskills.io/