在 Uniswap V2 中,协议跟踪代币储备量并推导出即期价格 以及总流动性 ,其中 和 是代币 X 和代币 Y 的储备量。
相反,Uniswap V3 跟踪当前价格和流动性,并推导出储备量。这种计算比较复杂,将在后续章节中介绍。
Uniswap V3 实际上存储的是价格的平方根 ,而不是价格本身。这种方法提高了 gas 效率,我们将在后续章节中详细探讨。这样做不会损失精度,因为价格总是可以通过其平方根推导出来。
本章的目的在于讨论 Uniswap V3 在何处以及如何存储价格的平方根 ,以及它如何处理代币价格可能是小数值,而 Solidity 没有浮点数(float)或小数(decimal)数字类型的问题。
变量 sqrtPriceX96
价格的平方根存储在 池合约 中 slot0 结构体的字段变量 sqrtPriceX96 内,如下图所示。

slot0 结构体还存储了其他变量,例如 tick,它代表当前的 tick,正如我们在 关于 tick 的章节 中看到的那样。slot0 中的其他变量与预言机、手续费或合约安全有关,我们将在稍后进行研究。
变量名 sqrtPriceX96 已经表明,其存储的值是价格的平方根(sqrtPrice),采用 Q96 数字格式(X96),具体来说是 Q64.96 格式。
上一章详细解释了 Q 数字格式。在接下来的部分,我们将简要回顾它在 Uniswap V3 中的用法。
价格的平方根 作为定点数值
Uniswap V3 将价格的平方根存储为定点数。
回顾一下,定点数允许我们以极高的 gas 效率来表示小数。例如,假设我们需要存储 1.0050122696230506,但只能存储整数。一种方法是将该值乘以一个大数,比如 ,结果为:
然后我们丢弃小数部分,存储 79625275426524700982079509374。
因此,一个数字的定点表示 Q64.96 与(原始)数字之间的关系为
为了恢复原始值,我们只需将定点表示除以 ,即
在我们的例子中,数值 1.0050122696230506 表示为定点数:79625275426524700982079509374。要恢复原始值,我们将此数值除以 ,得出约 1.0050122696230507,这非常接近原始数字。
这正是 Uniswap V3 用来规避 Solidity 没有浮点数或小数类型的解决方案。用于保存代币价格平方根的变量 sqrtPriceX96 就是一个定点数,它是通过将实际的、可能带有小数的价格平方根 乘以 得到的。
与 sqrtPriceX96 之间的关系
和 sqrtPriceX96 之间的关系是: 是实际的价格平方根,而 sqrtPriceX96 表示代币价格在 Q64.96 格式下的平方根。
这种关系可以用一个简单的公式来表示。要将 转换为 sqrtPriceX96,我们使用:
要将 sqrtPriceX96 转换回 ,我们使用:
使用 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 取值示例
让我们来看一些具体的例子。
- 如果当前某代币价格的平方根 是 100,该值将以以下形式存储在变量
sqrtPriceX96中:
- 如果当前价格的平方根是 323.002,它在
sqrtPriceX96中将被存储为:
若要恢复原始(真实)数值,只需将上述值除以 即可。
查询 slot0 中的 sqrtPriceX96 以计算池子价格
Base 网络上的 ETH:DAI 池
Base 网络上的 ETH:DAI 池 当前的 sqrtPriceX96 值可以在下图中看到,为 4552234755200983230583166215033。

要将 sqrtPriceX96 转换为 ,我们把获得的值除以 。因此:
根据价格的平方根,我们通过对该值求平方来得到实际价格:
这代表了撰写这部分文本时,以 DAI 计价的 Ether 价值。
主网(mainnet)上的 USDC:ETH 池
作为另一个例子,让我们获取 主网上的 USDC:ETH 池 中的 sqrtPriceX96。这个例子与前一个不同的原因有两点:首先,这是以 Ether 计价的 USDC 价格,而不是以稳定币计价的 Ether 价格。其次,Ether 有 18 位小数,而 USDC 只有 6 位小数,这与上一个例子中两种代币都有 18 位小数的情况不同。
从图片中可以看出,该值为 1506673274302120988651364689808458。

的值可以计算为:
价格可以计算为:
由于这是一个 USDC:ETH 池,我们得到的是以 ETH 计价的 USDC 价格。以 USDC 计价的 ETH 价格是所获价格的倒数:
最后,USDC 有 6 位小数,而 ETH 有 18 位。我们需要考虑到这种差异;要计算以 USDC 计价的 ETH 价格,我们必须将上述值乘以 。
这代表了撰写这部分文本时以 USDC 计价的 Ether 价值。在我们撰写本文时,ETH 的价格波动极大。
协议中的最高价格
因为价格的平方根以 Q64.96 格式的数字存储,所以它能存储的最大整数大约是 。与此平方根相对应的价格可以通过对这个最大整数求平方来获得。
因此,协议可以处理的最大价格平方根约为 ,而对应的最高价格略低于 。
协议中的最低价格
基于 导出的定点数可以表示小至 的分数。这是因为当 乘以 时结果为 1,可以将其作为整数存储。
小于 的值超出了范围。例如, 在通过乘以 转换为定点数时,会变成 ,即 0.5。由于仅保留整数部分,因此会向下舍入为 0,从而导致原始数值的信息丢失。
因此,理论上,协议能处理的最低价格是 即 。然而,正如我们将在下一章看到的,它并不允许如此低的价格。
协议规定了一个对称性:代币可假设的最大价格平方根为 ,而强制规定的最小价格平方根为 。这种对称性是合理的,因为代币 X 相对代币 Y 的价值,与代币 Y 相对代币 X 的价值互为倒数。
为什么使用 Q64.96 来存储价格的平方根
这不是一个容易回答的问题,而且协议团队本可以选择不同的 Q 数字格式。因为他们决定将价格平方根与 tick 以及其他信息打包在一个 256 位的存储槽(storage slot)中,留给价格平方根的空间刚好是 160 位。
在下一节中,我们将说明为什么使用 64 位来表示整数部分足以适应现实场景中的价格,这使得协议能够支持这样一个池子:其中一种代币价值数万亿美元,而另一种代币仅值几分之一美分。
现实场景中代币的价格限制
正如我们所见,协议能够处理的最高价格约为 (),或者在数量级上大致相当于 。由于代币价格始终是相对于另一种代币的,池中两个代币之间的价格差距跨度可高达 个数量级。
在计算价格差异时,我们还必须注意将小数位(decimals)因素考虑进去。例如,一个具有 18 位小数的代币在其合约中被存储为 个单位,而一个具有 8 位小数的代币被存储为 个单位。因此,如果这两种代币的美元价值相同,仅仅由于小数位的关系,它们在池中的价格差异就已经具备了 10 个数量级。
让我们考虑一个现实世界的例子,WBTC:PEPE 池。目前,1 个 WBTC 大约价值 100,000()美元,而 1 个 PEPE 大约价值 0.00001()美元,两者相差 10 个数量级。
但是,我们还必须考虑小数位数的不同。WBTC 有 8 位小数,而 PEPE 有 18 位小数,这会产生 10 个数量级的差异;此外再加上由价格差异造成的另外 10 个数量级的差异。
因此,WBTC 代币的最小单位(一聪,即 WBTC)的价值是 PEPE 最小单位(虽然没有名字,但代表 PEPE)价值的 倍(即十万亿亿倍)。
20 个数量级是一个巨大的差异,但 Uniswap V3 的池子允许最多 38 个数量级的差异。因此,在达到 Uniswap V3 的极限之前,WBTC 相对 PEPE 的价格还可以再增加 18 个数量级。
假设 PEPE 保持在其当前价格(0.00001 美元),在触及 Uniswap V3 的价格上限之前,比特币的价格必须达到一万亿亿()美元。同样地,假设比特币达到 1 万亿美元,在达到协议的价格下限之前,PEPE 的价格可以低至 0.0000000000000001 美元(一千万亿分之一美元)。
推导这些限制就作为留给读者的练习了。
总结
- Uniswap V3 使用 Q64.96 数字格式来存储价格的平方根。它这样做的原因在于 Solidity 中没有浮点数,且在二进制下处理定点数具有较高的 gas 效率。
- 协议能够存储的代币最高价格约为 。为了保持对称性,它所能存储的最低价格为 。池中的代币价格不能超出这些数值范围。