{"content":{"title":"Solidity 中的定点数运算（以 Solady、Solmate 和 ABDK 为例）","body":">- 原文链接：https://www.rareskills.io/post/solidity-fixed-point\r\n>- 译者：[AI翻译官](https://learnblockchain.cn/people/19584)\r\n>- 本文永久链接：[learnblockchain.cn/article…](https://learnblockchain.cn/article/8441)\r\n    \r\n定点数是一个仅存储分子部分的整数——而分母是隐含的。\r\n\r\n在大多数编程语言中，这种类型的算术运算是不必要的，因为它们有浮点数。在 Solidity 中则是必要的，因为 Solidity 只有整数，而我们经常需要进行分数运算。\r\n\r\n定点数在大多数 DeFi 智能合约中都能找到，因此理解它们是必须的。\r\n\r\n例如，如果“隐含分母”是 100，那么持有“10”的定点数被解释为 0.1。\r\n\r\nSolidity 中最常见的定点数是 10¹⁸：这是以太坊和大多数 ERC-20 代币的“小数位数”。当我们读取以太坊地址的余额时，我们隐含地将数字除以 10¹⁸ 来确定它们的以太币数量。例如，一个地址的余额为 10¹⁹ 被解释为拥有 10 个以太币——因为隐含地除以 10¹⁸。\r\n\r\n具有 10¹⁸ 分母的定点数如此常见，以至于 Solidity 社区的工程师称之为“Wad”（这个名字最早由 MakerDAO 引入）。有时，一个 18 位的定点数被解释为最右边的 18 位分配给小数位，例如，数字“10”如下所示：\r\n\r\n![Image 1: 10 ^ 18 with an emphasis on the 18 zeros](https://img.learnblockchain.cn/attachments/migrate/1718963979853)\r\n\r\n然而，我们发现这种心智模型使得理解固定点算术变得更难，因此本文将使用定点数持有分子，隐含 10¹⁸ 分母的心智模型。\r\n\r\n在本文中，我们将学习如何使用定点数进行算术运算，并解释流行的固定点库如何工作。\r\n\r\n将整数转换为定点数\r\n---------------------------------------------\r\n\r\n要将整数转换为定点数，将整数乘以隐含分母。例如，“2 ether”是 2 × 10¹⁸，因此将整数 2 转换为“2 ether”我们将其乘以 10¹⁸。隐含的 10¹⁸ 分母抵消了 10¹⁸。\r\n\r\n定点数的乘法\r\n-------------------------------\r\n\r\n要将两个定点数相乘，我们遵循分数相乘的规则：\r\n\r\n1.  将分子相乘\r\n    \r\n2.  将分母相乘\r\n    \r\n3.  简化结果。\r\n    \r\n\r\n例如：\r\n\r\n![Image 2: Fraction multiplication of 3/4 and 2/3](https://img.learnblockchain.cn/attachments/migrate/1718963979857)\r\n\r\n然而，在实际操作中我们可以优化这个计算，因为定点数的分母总是相同的。\r\n\r\n现在让我们考虑一组具有共同分母的分数：\r\n\r\n![Image 3: Fraction multiplication with common denominatiors](https://img.learnblockchain.cn/attachments/migrate/1718963979870)\r\n\r\n然而，我们不希望返回一个隐含分母为𝑑的结果，因为这与我们选择的隐含分母不兼容。因此，我们需要将分子和分母除以𝑑，以返回一个与我们选择的分母一致的定点数。\r\n\r\n![Image 4: Fraction multiplication and division relationship](https://img.learnblockchain.cn/attachments/migrate/1718963979873)\r\n\r\n因此，如果 𝑥 和 𝑦 是隐含分母为 𝑑² 的定点数，我们可以将它们的乘积计算为 (𝑥 × 𝑦)/𝑑。\r\n\r\n### 定点数乘法的代码示例\r\n\r\nSolady 库有一个`mulWad`数学操作，用于将两个隐含 Wad 分母（10¹⁸）的定点数相乘。下面，我们展示代码并解释它如何与我们之前的讨论相关：\r\n\r\n![Image 5: solady mulwad function code](https://img.learnblockchain.cn/attachments/migrate/1718963979875)\r\n\r\n核心算法在截图底部（绿色框内）。我们在那里计算 (𝑥 × 𝑦)/𝑑，其中 𝑑 是`WAD`或 10¹⁸（如截图顶部所示，`WAD`被声明）。\r\n\r\n### 现实世界的例子\r\n\r\n假设一个用户有 1 DAI（具有 18 个小数位），我们希望计算他们的余额，假设他们的存款赚取了 15%的利息。这是一个需要固定点算术的明确例子，因为我们不能在 Solidity 中直接将一个数字乘以 1.15。\r\n\r\n![Image 6: example contract using the solady mulwad library](https://img.learnblockchain.cn/attachments/migrate/1718963979992)\r\n\r\n输出是 1.15，在除以 1e18 之后。当然，我们不能实际除以 1e18，因为那样会抹去小数位。我们需要一个固定点表示，因为 1.15 不能表示为整数。上面的代码可以在 [Remix 上测试](https://remix.ethereum.org/#code=aW1wb3J0ICJodHRwczovL2dpdGh1Yi5jb20vVmVjdG9yaXplZC9zb2xhZHkvYmxvYi9tYWluL3NyYy91dGlscy9GaXhlZFBvaW50TWF0aExpYi5zb2wiOwoKY29udHJhY3QgQyB7CgogICAgdXNpbmcgRml4ZWRQb2ludE1hdGhMaWIgZm9yIHVpbnQyNTY7CgogICAgdWludDI1NiB0b2tlbkJhbGFuY2UgPSAxZTE4OwoKICAgIGZ1bmN0aW9uIGNvbXB1dGUxNVBJbnRlcmVzdCgpIHB1YmxpYyB2aWV3IHJldHVybnMgKHVpbnQyNTYpIHsKICAgICAgICByZXR1cm4gdG9rZW5CYWxhbmNlLm11bFdhZCgxLjE1ZTE4KTsKICAgIH0KfQ&lang=en&optimize=false&runs=200&evmVersion=null&version=soljson-v0.8.25+commit.b61c2a91.js) 。\r\n\r\n将定点数乘以整数\r\n----------------------------------------------\r\n\r\n将分数 x 乘以整数 y 与将 x 乘以 y/1 相同：\r\n\r\n![Image 7: Fraction and integer multiplication relationship](https://img.learnblockchain.cn/attachments/migrate/1718963980521)\r\n\r\n因此，当我们将定点数乘以整数时，我们不需要任何额外步骤。我们只需将返回值解释为分母不变的定点数。\r\n\r\n定点数的除法\r\n----------------------------\r\n\r\n要将分数相除，我们“翻转”第二个分数并将它们相乘。例如：\r\n\r\n![Image 8: Fraction division to multiplication conversion calculation](https://img.learnblockchain.cn/attachments/migrate/1718963980525)\r\n\r\n现在让我们考虑一个具有相同分母的例子：\r\n\r\n![Image 9: Fraction division between 6/10 and 3/10](https://img.learnblockchain.cn/attachments/migrate/1718963980550)\r\n\r\n注意，公共分母 10 被抵消了。如果我们想用隐含分母 10（即具有分母 10 的定点数）表示 2，我们需要再次将其乘以 10：\r\n\r\n![Image 10: 2 equals 2 times 10 divided by 10 fraction](https://img.learnblockchain.cn/attachments/migrate/1718963980695)\r\n\r\n因此，对于具有公共分母 𝑑 的一般 𝑥 和 𝑦，如果我们想用隐含分母 𝑑 表示输出，我们必须执行以下操作：\r\n\r\n![Image 11: derivation of the implied denominator equation ((x*d)/y)/d)](https://img.learnblockchain.cn/attachments/migrate/1718963980700)\r\n\r\n因此，如果 𝑥 和 𝑦 是隐含分母为𝑑的定点数，我们可以将它们的商计算为 (𝑥 × 𝑑)/𝑦。\r\n\r\n![Image 12: solady divwad function](https://img.learnblockchain.cn/attachments/migrate/1718963980734)\r\n\r\n如果我们将`mulWad()`和`divWad()`并排放置，我们可以看到它们之间的唯一区别（在计算步骤，而不是溢出检查），是 div 情况乘以一个倒数分数。![Image 13: solady mulwad() and muldiv() code side by side](https://img.learnblockchain.cn/attachments/migrate/1718963980936)\r\n\r\n将定点数除以整数\r\n----------------\r\n\r\n假设我们想将 2.5 除以 2（或一般情况下将某个分数除以整数）。不需要通过 (2 × 𝑑)/𝑑将 2 转换为定点数。\r\n\r\n将分数 x 除以整数 y 与将 x 的分子除以 y 是一样的。\r\n\r\n![Image 14: example of fraction division only effecting the numerator](https://img.learnblockchain.cn/attachments/migrate/1718963980949)\r\n\r\n注意 35 ÷ 3 = 11，而不是 11.666，因为我们使用的是整数除法，而不是浮点数。我们只需将定点数除以整数，并将结果解释为定点数。与将定点数乘以整数一样，分母保持不变。\r\n\r\n加减定点数\r\n-----------\r\n\r\n具有相同分母的分数相加减时，只需将分子相加减，分母忽略不计。我们将和解释为具有与加数相同隐含分母的定点数。例如，\r\n\r\n![Image 15: fraction addition formula with the same denominator](https://img.learnblockchain.cn/attachments/migrate/1718963981033)\r\n\r\n因此，当相加具有相同分母的定点数时，我们只需像处理普通整数一样将这些数相加。\r\n\r\n考虑一个隐含分母为 100 的例子：\r\n\r\n![Image 16: Fraction subtraction with a denominator of 100](https://img.learnblockchain.cn/attachments/migrate/1718963981059)\r\n\r\n计算时，我们只需做 50 - 40 = 10，不需要将 100 纳入计算。\r\n\r\n二进制与十进制定点数\r\n----------------------\r\n\r\n二进制定点数是分母可以表示为 2ⁿ 的定点数。二进制定点数通常用 Q 表示法表示。例如，UQ112x112 使用 2¹¹² 作为分母。U 表示“无符号”。用于存储 UQ112x112 的数据类型是 224。另一种解释方式是“小数部分”存储在最右边的 112 位，“整数部分”存储在最左边的 112 位。\r\n\r\n另一个例子，UQ64x64（或 UQ64.64）是一个`uint128`，其中“小数部分”存储在最低有效的 64 位，“整数部分”存储在最高有效的 64 位。这仍然可以解释为具有隐含分母 2⁶⁴，如下所示。\r\n\r\n二进制定点数的优点是我们可以使用 gas 高效的左移位操作代替乘以分母（当将整数转换为定点数时），或在除法时使用右移位操作。\r\n\r\n作为一个基本例子，考虑以下情况：\r\n(1) 2 的二进制表示是 10\r\n(2) 16 的二进制表示是 10000\r\n(3) 16 = 2 × 2³\r\n(4) binary(1000) = binary(10) << 3\r\n\r\n注意在(3)中 3 是指数，在(4)中是我们左移位的位数。\r\n\r\n位移操作与乘以 2ᵉ之间的关系通常成立。以下操作是等效的：\r\n\r\n```\r\n// x \\* 2¹¹² 等于 x 左移 112 位\r\nx \\* 2 \\*\\* 112 == x << 112\r\n\r\n// x / 2¹¹² 等于 x 右移 112 位\r\nx / 2 \\*\\* 112 == x >> 112\r\n```\r\n\r\nx 可以是任意数字，只要它适合无符号整数。\r\n\r\n[ABDK 库](https://github.com/abdk-consulting/abdk-libraries-solidity/tree/master)使用以下函数将无符号整数转换为定点数（隐含分母为 2⁶⁴）：\r\n\r\n![Image 17: ABDK library fromUInt function code](https://img.learnblockchain.cn/attachments/migrate/1718963981089)\r\n\r\nrequire 语句确保 x 小于`type(int64).max`，因为 ABDK 库使用有符号定点数。左移 64 位相当于乘以 2⁶⁴。\r\n\r\n类似地，当 ABDK 进行乘法运算时，它不是将 x 和 y 的乘积除以 2⁶⁴，而是右移 64 位：\r\n\r\n![Image 18: ABDK mul function code](https://img.learnblockchain.cn/attachments/migrate/1718963981468)\r\n\r\n### Uniswap V2 定点数库\r\n\r\n[Uniswap V2](http://rareskills.io/uniswap-v2-book)的定点数库非常简单，因为 Uniswap V2 对定点数的唯一操作是加法和将定点数除以整数。\r\n\r\n![Image 19: Uniswap v2 uq112x112 library ](https://img.learnblockchain.cn/attachments/migrate/1718963981507)\r\n\r\n`encode()`函数将一个`uint112`转换为存储在`uint224`中的定点数。Uniswap V2 使用隐含分母`2**112`。如果使用位移操作代替乘法，它可能会更节省 gas（这可能是 Uniswap 开发者的一个错误）。\r\n\r\n定点数存储在一个`uint224`中，其大小是与之交互的`uint112`的两倍。在编码操作期间，`uint112`数字的位实际上被移到`uint224`的最高有效 112 位。\r\n\r\n这种“编码”操作在较小的 uint 大小下更容易可视化。让我们使用一个假设的分母为 2⁸ 的定点数。下面，我们展示了将一个`uint8`编码为分母为 2⁸ 的定点数时发生的情况：\r\n\r\n[video](https://video.wixstatic.com/video/706568_91fa05a2c70a479a9846b50f7cb946de/1080p/mp4/file.mp4)\r\n\r\n从数字 125 开始，其二进制表示为`01111101`，如果将其乘以 2⁸，乘积为 32000，当存储在 16 位 uint 中时表示为`0111110100000000`。注意，将 125 乘以 2⁸与左移 8 位效果相同。\r\n\r\n`uqdiv()`函数只是将定点数除以整数，不需要额外步骤。\r\n\r\nUniswap 使用这个库来累积 TWAP Oracle 的价格。每次更新发生时，TWAP 将最新价格添加到累加器中（累加器用于计算平均价格，涉及额外步骤，不在本文范围内）。由于价格表示为分数，定点数是表示它们的理想方式。\r\n\r\n变量`_reserve0`和`_reserve1`保存池的最新代币余额，类型为 uint112。`price0CumulativeLast`和`price1CumulativeLast`是 UQ112x112（隐含分母为 2¹¹² 的定点数）。下面的 Uniswap V2 代码将分子转换为定点数（UQ112x112）并将其除以整数（分母未转换为 UQ112x112）。结果是一个定点数。\r\n\r\n![Image 20: _update() function in uniswap using the UQ112X112 encode function](https://img.learnblockchain.cn/attachments/migrate/1718963981510)\r\n\r\n向上取整与向下取整\r\n-------------------\r\n\r\n定点数库通常有在除法时向上取整的选项。例如，Solady 有：\r\n\r\n*   `mulWadUp` — 乘以两个定点数，但在除以 d 时向上取整。回想一下，乘以两个定点数的公式是(x × y) / d。\r\n    \r\n*   `mulDivUp` — 除以两个定点数，但在除法时向上取整\r\n    \r\n\r\nSolidity 除法总是向下取整，例如 10 / 3 = 3。然而，如果我们向上取整，10 / 3 将等于 4。当计算信用或价格时，应始终向协议有利、用户不利的方向取整。例如，如果我们在计算用户应支付的另一种资产的固定金额，我们应向上取整价格。\r\n\r\n*   10 / 3 向下取整是 3.3333\r\n\r\n*   10 / 3 向上取整是 3.3334（取决于我们的分母大小）\r\n\r\n向上取整简单来说就是如果余数不为零，则在结果上加 1。例如，9 / 3 = 3 正好，所以我们不应该返回 4。然而，10 / 3 和 11 / 3 的余数分别是 1 和 2，所以我们应该在除法结果上加 1。\r\n\r\n![Image 21: Solmate mulDivUp function](https://img.learnblockchain.cn/attachments/migrate/1718963981515)\r\n\r\n在绿色下划线部分，代码检查模数是否大于零。如果是，则在结果上加 1（向上取整），否则加 0（不取整）。\r\n\r\n> 本文由 AI 翻译，欢迎小伙伴们来[校对](https://github.com/lbc-team/Pioneer/blob/master/translations/8441.md)。"},"author":{"user":"https://learnblockchain.cn/people/20722","address":null},"history":null,"timestamp":1718964176,"version":1}