关于定点数的更多内容
在这个额外的章节中,我想向你展示如何在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.sol
和PRBMathUD60x18.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)转换为上述两种定点数:
- Q64.96:或者使用位左移,
2 << 96
。结果是3327582825599102178928845914112。 - UD60.18:。结果是42000000000000000000。
现在让我们看看如何转换带小数部分的数字(42.1337):
- Q64.96:或
421337 << 92
。结果是2086359769329537075540689212669952。 - 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
位的原因。