关于定点数的更多内容

在这个额外的章节中,我想向你展示如何在Solidity中将价格转换为价格刻度。我们在主合约中不需要这样做,但在测试中有这样的函数会很有帮助,这样我们就不需要硬编码价格刻度,而可以写类似tick(5000)这样的代码——这使得代码更容易阅读,因为对我们来说,用价格思考比用价格刻度索引更方便。

回想一下,为了找到价格刻度,我们使用TickMath.getTickAtSqrtRatio函数,它以作为参数,而是一个Q64.96定点数。在智能合约测试中,我们需要在许多不同的测试用例中多次检查:主要是在铸造和交换之后。与其硬编码实际值,使用像sqrtP(5000)这样的辅助函数将价格转换为可能会更清晰。

那么,问题是什么?

问题是Solidity原生不支持平方根运算,这意味着我们需要一个第三方库。另一个问题是,价格通常是相对较小的数字,如10、5000、0.01等,我们不希望在取平方根时损失精度。

你可能记得我们在本书前面使用了PRBMath来实现一个在乘法过程中不会溢出的乘后除运算。如果你查看PRBMath.sol合约,你会注意到sqrt函数。然而,正如函数描述所说,该函数不支持定点数。你可以试一试,看看PRBMath.sqrt(5000)的结果是70,这是一个失去精度的整数(没有小数部分)。

如果你查看prb-math仓库,你会看到这些合约:PRBMathSD59x18.solPRBMathUD60x18.sol。啊哈!这些是定点数实现。让我们选择后者看看效果如何:PRBMathUD60x18.sqrt(5000 * PRBMathUD60x18.SCALE)返回70710678118654752440。这看起来很有趣!PRBMathUD60x18是一个实现了小数部分有18位小数的定点数的库。所以我们得到的数字是70.710678118654752440(使用cast --from-wei 70710678118654752440)。

然而,我们不能使用这个数字!

定点数和定点数是有区别的。Uniswap V3使用的Q64.96定点数是一个二进制数——64和96表示二进制位。但PRBMathUD60x18实现的是一个十进制定点数(合约名称中的UD表示"无符号,十进制"),其中60和18表示十进制位。这个差异是相当显著的。

让我们看看如何将任意数字(42)转换为上述两种定点数:

  1. Q64.96:或者使用位左移,2 << 96。结果是3327582825599102178928845914112。
  2. UD60.18:。结果是42000000000000000000。

现在让我们看看如何转换带小数部分的数字(42.1337):

  1. Q64.96:421337 << 92。结果是2086359769329537075540689212669952。
  2. UD60.18:。结果是42133700000000000000。

第二种变体对我们来说更有意义,因为它使用了我们从小学习的十进制系统。第一种变体使用二进制系统,对我们来说更难读懂。

但不同变体最大的问题是它们之间很难转换。

这一切意味着我们需要一个不同的库,一个实现二进制定点数并为其提供sqrt函数的库。幸运的是,有这样一个库:abdk-libraries-solidity。这个库实现了Q64.64,不完全是我们需要的(小数部分不是96位),但这不是问题。

以下是我们如何使用新库实现价格到价格刻度的函数:

function tick(uint256 price) internal pure returns (int24 tick_) {
    tick_ = TickMath.getTickAtSqrtRatio(
        uint160(
            int160(
                ABDKMath64x64.sqrt(int128(int256(price << 64))) <<
                    (FixedPoint96.RESOLUTION - 64)
            )
        )
    );
}

ABDKMath64x64.sqrt函数接受Q64.64格式的数字,所以我们需要将price转换为这种格式。价格预计不会有小数部分,因此我们将其左移64位。sqrt函数也返回一个Q64.64格式的数字,但TickMath.getTickAtSqrtRatio函数接受Q64.96格式的数字——这就是为什么我们需要将平方根操作的结果再左移96 - 64位的原因。