Solidity 中定点数的运算

  • 0xE
  • 更新于 2024-09-24 15:36
  • 阅读 452

本文我们将讲解十进制定点数和二进制定点数的运算,以及看看相关库的编写和使用。

定点数这个概念对大家来说应该并不陌生,其特点是小数点的位置是隐含的固定值,或者说隐含了一个分母。在 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 定点数库

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 定点数库

由于 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 保存池的最新代币余额,类型为 uint112price0CumulativeLastprice1CumulativeLastUQ112x112(即隐含分母为 $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);
    }

参考链接:

  1. https://www.rareskills.io/post/solidity-fixed-point
  2. https://github.com/Vectorized/solady/blob/main/src/utils/FixedPointMathLib.sol
  3. https://github.com/transmissions11/solmate/blob/main/src/utils/FixedPointMathLib.sol
  4. https://github.com/abdk-consulting/abdk-libraries-solidity/blob/master/ABDKMath64x64.sol
  5. https://github.com/Uniswap/v2-core/blob/master/contracts/libraries/UQ112x112.sol
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
0xE
0xE
0x59f6...a17e
17年进入币圈,做过FHE,联盟链,现在是智能合约开发者。 刨根问底探链上真相,品味坎坷悟Web3人生。