本文我们将讲解十进制定点数和二进制定点数的运算,以及看看相关库的编写和使用。
定点数这个概念对大家来说应该并不陌生,其特点是小数点的位置是隐含的固定值,或者说隐含了一个分母。在 DeFi 开发中,我们经常需要处理定点数的运算。代码中常常出现 Wad
这个值,Uniswap 中也有 UQ112x112
类型,所以了解定点数的相关库和运算是很有必要的。
接下来,我们将讲解十进制定点数和二进制定点数的运算,以及看看相关库的编写和使用。
在十进制下,将一个整数转化为定点数的方式是将该整数乘以一个隐含的分母。例如,当我们说一个人的余额是 6 ether 时,实际上是在表示 6 * 10¹⁸ 的定点数值。
定点数的乘法可以类比为两个分数的乘法。由于 Wad
相同,即两个数的分母相同,最终我们需要得到的是同样 Wad
值的定点数。因此,运算过程如下:
$\frac{x}{d} \times \frac{y}{d} = \frac{z}{d^2} \div \frac{d}{d} = \frac{z/d}{d}$
因此,两个定点数相乘的计算方法是: (𝑥 × 𝑦) / 𝑑
。
相关库以及使用
在 Solady 定点数库 中,mulWad
函数用于执行两个定点数的乘法运算。以下是其实现代码:
uint256 internal constant WAD = 1e18;
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* SIMPLIFIED FIXED POINT OPERATIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev Equivalent to `(x * y) / WAD` rounded down.
function mulWad(uint256 x, uint256 y) internal pure returns (uint256 z) {
/// @solidity memory-safe-assembly
assembly {
// Equivalent to `require(y == 0 || x <= type(uint256).max / y)`.
if gt(x, div(not(0), y)) {
if y {
mstore(0x00, 0xbac65e5b) // `MulWadFailed()`.
revert(0x1c, 0x04)
}
}
z := div(mul(x, y), WAD)
}
}
类似地,在 Solmate 定点数库中提供了 mulWadDown
函数。其代码如下:
uint256 internal constant WAD = 1e18; // The scalar of ETH and most ERC20s.
function mulWadDown(uint256 x, uint256 y) internal pure returns (uint256) {
return mulDivDown(x, y, WAD); // Equivalent to (x * y) / WAD rounded down.
}
function mulDivDown(
uint256 x,
uint256 y,
uint256 denominator
) internal pure returns (uint256 z) {
/// @solidity memory-safe-assembly
assembly {
// Equivalent to require(denominator != 0 && (y == 0 || x <= type(uint256).max / y))
if iszero(mul(denominator, iszero(mul(y, gt(x, div(MAX_UINT256, y)))))) {
revert(0, 0)
}
// Divide x * y by the denominator.
z := div(mul(x, y), denominator)
}
}
可以看到,这两者的内联汇编部分最终的运算逻辑都是一致的,都是通过 z := div(mul(x, y), denominator)
来完成乘法运算。
以下是一个基于 Solady 的使用示例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "https://github.com/Vectorized/solady/blob/main/src/utils/FixedPointMathLib.sol";
contract Example {
using FixedPointMathLib for uint256;
uint256 tokenBalance = 6e18;
function computeMulWad() public view returns (uint256) {
return tokenBalance.mulWad(1.2e18);
// return 7200000000000000000
}
}
整数 y 可以写成 y/1: $\frac{x}{d} \times y = \frac{x}{d} \times \frac{y}{1} = \frac{x \times y}{d}$ 因此,当我们将定点数与整数相乘时,无需执行任何额外步骤。
两个定点数相除可以类比为两个分数相除。我们知道,除以一个数等于乘以该数的倒数,而由于定点数具有相同的分母,分母部分可以抵消。我们最终需要得到同样 Wad 值的定点数。推导过程如下:
$\frac{x}{d} \div \frac{y}{d} = \frac{x}{d} \times \frac{d}{y} = \frac{x}{y} = \frac{(x \times d) / y}{d}$
所以,我们可以将它们的商计算为 (𝑥 × 𝑑) / 𝑦
。
以下分别是 Solady 和 Solmate 关于定点数除法的代码。它们与定点数乘法的实现相似。
Solady 的 divWad
实现:
/// @dev Equivalent to `(x * WAD) / y` rounded down.
function divWad(uint256 x, uint256 y) internal pure returns (uint256 z) {
/// @solidity memory-safe-assembly
assembly {
// Equivalent to `require(y != 0 && x <= type(uint256).max / WAD)`.
if iszero(mul(y, lt(x, add(1, div(not(0), WAD))))) {
mstore(0x00, 0x7c5f487d) // `DivWadFailed()`.
revert(0x1c, 0x04)
}
z := div(mul(x, WAD), y)
}
}
Solmate 的 divWadDown
实现:
function divWadDown(uint256 x, uint256 y) internal pure returns (uint256) {
return mulDivDown(x, WAD, y); // Equivalent to (x * WAD) / y rounded down.
}
function mulDivDown(
uint256 x,
uint256 y,
uint256 denominator
) internal pure returns (uint256 z) {
/// @solidity memory-safe-assembly
assembly {
// Equivalent to require(denominator != 0 && (y == 0 || x <= type(uint256).max / y))
if iszero(mul(denominator, iszero(mul(y, gt(x, div(MAX_UINT256, y)))))) {
revert(0, 0)
}
// Divide x * y by the denominator.
z := div(mul(x, y), denominator)
}
}
类似于定点数与整数相乘,定点数除以整数时,只需对分子执行除法操作,分母保持不变,结果仍然表示为定点数。
$\frac{x}{d} \div y = \frac{x / y}{d}$
定点数的加减运算可以像普通分数一样进行。由于分母相同,只需对分子进行加减,分母保持不变。
$\frac{a}{d} + \frac{b}{d} = \frac{a + b}{d}$
二进制定点数使用的是 Q 格式,其特点是分母可以表示为 $2^n$ 的定点数。例如,UQ112x112
是一个 uint224
,它的分母为 $2^{112}$,其中 U 表示“无符号”。这种格式的解释是,“小数部分”存储在最右边的 112 位,而“整数部分”存储在最左边的 112 位。
另一个例子,UQ64x64
是一个 uint128
,其中“小数部分”存储在最低有效的 64 位,“整数部分”存储在最高有效的 64 位。
与十进制定点数相似,二进制定点数可以理解为一种 Wad
,即隐含分母为 $2^n$ 的数。但不同的是,在二进制下,可以使用左移操作代替乘法将整数转换为定点数,或通过右移操作进行除法运算,这样的位移运算能够节省 gas 费用。
ABDK 库提供了以下函数,将无符号整数转换为隐含分母为 $2^{64}$ 的定点数:
/**
* Convert unsigned 256-bit integer number into signed 64.64-bit fixed point
* number. Revert on overflow.
*
* @param x unsigned 256-bit integer number
* @return signed 64.64-bit fixed point number
*/
function fromUInt (uint256 x) internal pure returns (int128) {
unchecked {
require (x <= 0x7FFFFFFFFFFFFFFF);
return int128 (int256 (x << 64));
}
}
require 语句确保 𝑥 小于 type(int64).max,因为 ABDK 库使用有符号定点数。左移 64 位相当于乘以 $2^{64}$。
在十进制定点数的乘法运算中,计算公式为 (𝑥 × 𝑦) / 𝑑
。而 ABDK 库在执行乘法运算时,直接将乘积右移 64 位,代替了除以 $2^{64}$:
/**
* Calculate x * y rounding down. Revert on overflow.
*
* @param x signed 64.64-bit fixed point number
* @param y signed 64.64-bit fixed point number
* @return signed 64.64-bit fixed point number
*/
function mul (int128 x, int128 y) internal pure returns (int128) {
unchecked {
int256 result = int256(x) * y >> 64;
require (result >= MIN_64x64 && result <= MAX_64x64);
return int128 (result);
}
}
由于 Uniswap V2 对定点数的唯一操作是加法和定点数除以整数,因此该库相对简单。
pragma solidity =0.5.16;
// a library for handling binary fixed point numbers (https://en.wikipedia.org/wiki/Q_(number_format))
// range: [0, 2**112 - 1]
// resolution: 1 / 2**112
library UQ112x112 {
uint224 constant Q112 = 2**112;
// encode a uint112 as a UQ112x112
function encode(uint112 y) internal pure returns (uint224 z) {
z = uint224(y) * Q112; // never overflows
}
// divide a UQ112x112 by a uint112, returning a UQ112x112
function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) {
z = x / uint224(y);
}
}
encode()
函数将一个 uint112
转换为存储在 uint224
中的定点数。Uniswap V2 使用隐含分母 $2^{112}$。这个库其实可以用位移来代替乘法操作,会更省 gas。
uqdiv()
函数只是将定点数除以整数,和十进制定点数除以整数的逻辑相同,不需要额外步骤。
Uniswap 使用这个库表示价格,来累积 TWAP Oracle 的价格。每次更新发生时,TWAP 将最新价格添加到累加器中。
变量 _reserve0
和 _reserve1
保存池的最新代币余额,类型为 uint112
。price0CumulativeLast
和 price1CumulativeLast
是 UQ112x112
(即隐含分母为 $2^{112}$ 的定点数)。在 Uniswap V2 代码中,分子被转换为定点数(UQ112x112),然后再除以整数,结果仍然是一个定点数。
// update reserves and, on the first call per block, price accumulators
function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {
require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW');
uint32 blockTimestamp = uint32(block.timestamp % 2**32);
uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired
if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {
// * never overflows, and + overflow is desired
price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
}
reserve0 = uint112(balance0);
reserve1 = uint112(balance1);
blockTimestampLast = blockTimestamp;
emit Sync(reserve0, reserve1);
}
参考链接:
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!