在前面的章节中,我们推导了计算代币 X 和代币 Y 在一个区间内两个价格之间真实准备金(real reserves)的公式。当用户添加或移除流动性以及进行兑换(swap)时,协议会使用这些公式。
在本章中,我们将展示这些公式在代码库中的具体位置。由于协议以 Q64.96 格式存储价格的平方根,因此必须调整公式以适应此格式。
在代码库中计算数量 和
代码库中包含用于在恒定流动性下计算两个价格之间代币 X 和代币 Y 数量的辅助函数。这些函数定义在 SqrtPriceMath.sol 库中,名称如下:
getAmount0Delta:计算给定流动性下,两个价格之间代币 X 的真实准备金。getAmount1Delta:计算给定流动性下,两个价格之间代币 Y 的真实准备金。
这些真实准备金的公式实际上依赖于价格的平方根,而不是价格本身。我们可以通过将该值平方来从平方根恢复出价格——但这仅在链下进行;在链上并没有这个必要。
为简便起见,我们将继续使用“价格”一词,但读者应注意,协议仅使用价格的平方根。例如,如果协议需要计算价格 100 和 200 之间的真实准备金数量,它将使用 Q64.96 格式的 和 作为参数来调用这些函数,而不是使用 100 和 200。
函数 getAmount0Delta 和 getAmount1Delta
SqrtPriceMath 库有两个名为 getAmount0Delta 的函数,其中一个具有以下签名,另一个将在后续章节中讨论。

它接收两个 Q64.96 格式的价格平方根(sqrtRatioAX96 和 sqrtRatioBX96)、流动性以及一个布尔值(roundUp),该布尔值指定最终数量应向上还是向下取整。
当该数量代表协议向用户支付的款项时(例如在兑换或移除流动性时),数量会向下取整;当它代表用户向协议存入的款项时(例如在兑换或提供流动性时),数量会向上取整。取整规则始终有利于协议而不利于用户。
此函数计算并返回在给定流动性 下,sqrtRatioAX96 和 sqrtRatioBX96 之间代币 X 的真实准备金数量,前提是假设该区间的流动性全部为代币 X。
协议中同样有两个 getAmount1Delta 函数,其中一个签名与 getAmount0Delta 非常相似,另一个将在后续章节中讨论。它的作用是计算在给定流动性 下,sqrtRatioAX96 和 sqrtRatioBX96 之间代币 Y 的真实准备金数量,前提是假设该区间的流动性全部为代币 Y。

与 getAmount0Delta 相同的取整行为也适用于此函数。
下面的交互式工具计算给定流动性下一个价格区间的数值,与 getAmount0Delta 和 getAmount1Delta 的计算结果相同。要使用它,请移动蓝色和紫色滑块来调整 sqrtRatioAX96 和 sqrtRatioBX96,并移动橙色滑块来调整流动性。amount0/amount1 单选按钮指示应执行哪个函数。如果需要对结果向上取整,请勾选 roundUp 复选框。
请注意,这些函数假设在两个价格点之间,流动性是恒定的,且仅由单一准备金构成。
在使用这些函数之前,我们必须提前了解这一信息,因为这些函数并不知晓整个可能价格区间内的流动性分布情况。
函数 getAmount0Delta
函数 getAmount0Delta 计算以下公式,该公式在有关真实准备金的章节中推导得出:
只不过它使用的是由 sqrtRatioAX96 和 sqrtRatioBX96 表示的、Q64.96 格式的价格平方根。
由于 和 sqrtRatioX96 之间的关系是
我们可以将该关系代入 的公式中如下:
最后一步在数学上并非严格必要,但在代码中除法会引入舍入误差,由于我们只执行了一次除法而不是两次,因此这提高了精度。
此公式可以在代码库的函数注释中找到(不包含 )

并在函数 getAmount0Delta 中实现:

上图中的橙色和绿色框对应我们刚刚推导公式中的橙色和绿色部分,它们构成了分子。蓝色框包含完整的表达式,其中分子的各项既相乘又被 sqrtRatioAX96 和 sqrtRatioBX96 除。
mulDiv 函数将前两个参数相乘,并将乘积除以第三个参数。mulDivRoundingUp 函数执行相同操作,但将最终结果向上取整。
divRoundingUp 执行向上取整的整数除法。
如前所述,roundUp 参数指示最终数量是否应向上取整。
函数 getAmount1Delta
此函数旨在计算以下公式
只不过用 sqrtRatioAX96 和 sqrtRatioBX96 代替了 和 。
回想一下, 和 sqrtRatioX96 之间的关系是
那么我们可以在 的公式中使用该关系:
我们再次重新排列公式以使其仅包含一次除法,这提高了精度,并与代码库中的内容相匹配。
下图展示了函数 getAmount1Delta。

Python 中的 getAmount0Delta 和 getAmount1Delta 函数
在本节中,我们将使用 Python 编写 getAmount0Delta 和 getAmount1Delta 函数,它们将用于计算一个区间的真实准备金。
为了简单起见,我们无需在脚本中担心舍入误差。
它们可以编写如下:
import math
def getAmount0Delta(sqrtRatioAX96, sqrtRatioBX96, liquidity, roundUp):
numerator1 = liquidity*2**96
numerator2 = sqrtRatioBX96 - sqrtRatioAX96
if roundUp:
return math.ceil(numerator1*numerator2 / (sqrtRatioBX96*sqrtRatioAX96))
else:
return math.floor(numerator1*numerator2 / (sqrtRatioBX96*sqrtRatioAX96))
def getAmount1Delta(sqrtRatioAX96, sqrtRatioBX96, liquidity, roundUp):
if roundUp:
return math.ceil(liquidity*(sqrtRatioBX96 - sqrtRatioAX96)/2**96)
else:
return math.floor(liquidity*(sqrtRatioBX96 - sqrtRatioAX96)/2**96)
假设我们想计算 tick -10 和 10 之间的真实准备金,且当前价格恰好在 tick 0,如下图所示。代币 Y 的真实准备金位于较低的 tick (-10) 和当前价格 (0) 之间,而代币 X 的真实准备金位于当前价格 (0) 和较高的 tick (10) 之间。

我们可以使用函数 getAmount0Delta 和 getAmount1Delta 来获取这些值。
为了确定是向上取整还是向下取整,我们需要考虑计算的目的。如果计算是为了提供流动性,结果将向上取整,反之,如果是为了移除流动性则向下取整。让我们在提供流动性时进行这些计算,此时需要向上取整。
计算过程和结果值如下方代码所示。
def getSqrtRatioAtTick(i):
return math.sqrt(1.0001 ** i) * 2**96;
current_price = getSqrtRatioAtTick(0)
upper_price = getSqrtRatioAtTick(10)
lower_price = getSqrtRatioAtTick(-10)
print(getAmount0Delta(current_price, upper_price, 1000000000, True)) # 499851
print(getAmount1Delta(lower_price, current_price, 1000000000, True)) # 499851
练习:在以下情况下,计算 tick -10 和 20 之间的真实准备金:
- 当前价格在 tick -20,
- 当前价格在 tick 0,
- 当前价格在 tick 20。
结论
- 协议提供了计算两个任意价格之间真实准备金以及在它们之间给定恒定流动性的函数。这些函数是
getAmount0Delta(用于代币 X)和getAmount1Delta(用于代币 Y)。 - 这些函数以 Q64.96 格式接收价格平方根,对应变量名为
sqrtRatioAX96和sqrtRatioBX96。 - 如果计算出的真实准备金数量代表协议将要收取的金额,则向上取整;如果它代表协议将要支付的金额,则向下取整。
本文是 Uniswap V3 系列文章的一部分。