Uniswap的Q64.96详解:Hook开发者必备安全提示

  • zealynx
  • 发布于 2026-01-31 19:17
  • 阅读 6

本文深入探讨了Uniswap V3中Q64.96定点数的工作原理、重要性及其在Solidity智能合约中的应用。文章详细解释了如何表示和使用Q64.96数字,强调了溢出、精度损失和类型混淆等常见陷阱,并提供了使用FullMath和FixedPoint96库进行安全编码的最佳实践,最后介绍了模糊测试和形式化验证等高级测试策略以确保安全性。

各位 DeFi 爱好者们,大家好!如果你一直在 Uniswap 的世界里探索,你可能听说过 ticks、流动性池,甚至还有一些叫做 Q64.96 的数字。如果最后一个听起来像一个秘密代码,别担心——我们即将揭开它的神秘面纱!

你可能在想,“哦,太棒了,又一次深入探讨 Uniswap 的 ticks 吗?”但是请等一下!虽然我们会提到 ticks(因为,说实话,它们非常重要),但我们今天要讨论的是同样重要但经常被忽视的东西:Q64.96 数字。

“我为什么要关心这些 Q-whatever 数字?”我听到你这样问。

好吧,如果你正在 Uniswap 上构建或创建 hooks,了解 Q64.96 数字可能是构建一个安全、高效的合约和……嗯,我们只能说你不想站在等式的另一边之间的区别。

所以,系好安全带!我们将踏上 Q64.96 数字的世界之旅,探索如何安全地使用它们并避免沿途的一些讨厌的陷阱。

快速回顾:Uniswap 中的 Ticks 和 Q64.96 数字

好的,让我们快速复习一下。你了解 Uniswap v3 中的 ticks,对吗?不了解?好的,这里是 TL;DR:

Ticks 就像 Uniswap 流动性曲线上的价格点。每个 tick 代表 $0.01\%$ 的价格变化。这是 Uniswap 允许流动性提供者将其资金集中在特定价格范围内的巧妙方式。相当,是吧?

有趣的地方来了。你认为 Uniswap 是如何内部表示这些价格的?常规小数?整数?不!请看 Q64.96 数字。

“Q64.96?听起来像《星球大战》里的机器人!”你可能会开玩笑说。

接近,但并不完全是。Q64.96 数字是一种特殊的表示小数的方式,只使用整数。为什么?因为 Solidity(用于以太坊智能合约的语言)不擅长处理小数。

好奇吗?很好!因为我们即将更深入地探讨这些神秘的数字。

理解 Q64.96 数字

好的,让我们深入了解 Q64.96 数字。但是等等,它们到底是什么,为什么我们需要它们?

Q64.96 数字是一种特定类型的定点数表示。你可以把它们想象成在一个只理解整数的系统中表示小数的一种方式。

但为什么是 64 和 96?好问题!

“64”指的是用于整数部分的比特数,而“96”是用于小数部分的比特数。总共是 160 比特。你可能会想,“这难道不是杀鸡用牛刀吗?我们为什么需要这么多比特?”好吧,在 Uniswap 中,我们处理的是广泛的价格和流动性金额。我们需要足够的精度来处理微小的几分钱,但也要有足够的范围来表示数十亿美元。这就是 Q64.96 派上用场的地方。

让我们用一个例子来解释。我们如何将数字 $1.5$ 表示为 Q64.96 数字?

它是这样工作的:

  1. 从 $1.5$ 开始
  2. 将其乘以 $2^{96}$(即 $2$ 的 $96$ 次幂)
  3. 结果就是我们的 Q64.96 表示

在代码中,它看起来像这样:

1uint256 onePointFive = 1.5 * 2**96;
2// This equals 118842243771396506390315925504

现在你可能会想,“这只是为了表示 $1.5$ 而得到的巨大数字!”你说得对,确实如此。然而,这使我们能够执行精确的计算,而不会因为舍入误差而损失精度。

关键在于——我们如何在计算中实际使用这些数字?别担心,我们将在下一节中讲到。

常见陷阱和安全漏洞

现在我们了解了 Q64.96 数字是什么,让我们来谈谈在使用它们时可能出现的问题。毕竟,能力越大,责任越大,对吧?

首先:溢出和下溢

你可能对这些在常规整数数学中很熟悉,但对于 Q64.96,风险甚至更高。为什么?因为我们默认处理的是大得多的数字。

假设你正在尝试将两个 Q64.96 数字相乘。你可能认为,“没什么大不了的,我只会像普通整数一样将它们相乘。”但是请等一下!还记得这些数字有多大吗?直接将它们相乘几乎肯定会导致溢出。

以下是不该做的事情的例子:

1uint256 a = 1.5 * 2**96;  // Q64.96 representation of 1.5
2uint256 b = 2.0 * 2**96;  // Q64.96 representation of 2.0
3uint256 result = a * b;   // DON'T DO THIS!

你可能在想,“好的,那么我们如何安全地将它们相乘?”

好问题!我们需要使用特殊的函数来正确处理数学运算。我们将在最佳实践部分讨论这一点。

另一个常见问题是精度损失

你可能认为,“我最后只要除以 $2^{96}$ 就能回到普通数字。”但这没那么简单。Solidity 中的除法会截断结果,这意味着你可能会损失重要的精度。

例如:

1uint256 x = (1 * 2**96) / 3;  // Trying to represent 1/3
2uint256 y = x * 3;  // You might expect this to equal 1 * 2**96, but it won't!

最后,我们来谈谈类型混淆

很容易忘记你正在处理 Q64.96 数字,并将其视为常规整数。这可能会导致一些非常奇怪的结果。

例如:

1uint256 price = 1500 * 2**96;  // Q64.96 representation of 1500
2require(price > 1000, "Price too low");  // This will ALWAYS be true!

你可能在想,“但 $1500$ 显然大于 $1000$,那么问题出在哪里?”

问题在于我们将 Q64.96 数字与常规整数进行比较。由于乘以 $2^{96}$,Q64.96 数字大得多。

那么,我们如何避免这些陷阱呢?别担心,我们将在下一节的最佳实践中为你提供帮助。

使用 Q64.96 数字进行安全编码的最佳实践

好的,既然我们已经用所有可能出错的事情吓到你了,现在让我们来谈谈如何正确做事。别担心,并非一切都是厄运与悲观

首先,你可能在想,“我不能像处理常规整数一样使用 SafeMath 吗?”直觉!但不幸的是,SafeMath 并非为 Q64.96 数字设计。那么我们用什么代替呢?

FullMathFixedPoint96 库登场了!它们是你在使用 Q64.96 数字时的新好朋友。我们来看一个例子:

1import '@uniswap/v3-core/contracts/libraries/FullMath.sol';
2import '@uniswap/v3-core/contracts/libraries/FixedPoint96.sol';
3function multiplyQ64x96(uint256 a, uint256 b) internal pure returns (uint256) {
4    return FullMath.mulDiv(a, b, FixedPoint96.Q96);
5}

“但是等等,”你可能会说,“这个 mulDiv 函数是什么?为什么不直接乘法和除法?”

问题!mulDiv 是一个特殊函数,它以一种避免溢出保留精度的方式执行乘法和除法。

那么,除法呢?还记得我们之前遇到的精度损失问题吗?我们是这样处理的:

1function divideQ64x96(uint256 a, uint256 b) internal pure returns (uint256) {
2    return FullMath.mulDiv(a, FixedPoint96.Q96, b);
3}

看到我们做了什么吗?我们在除法之前乘以 Q96。这有助于保留精度

最后,我们来谈谈比较。还记得我们的类型混淆问题吗?这是正确比较 Q64.96 数字的方法:

1function isGreaterThan(uint256 a, uint256 b) internal pure returns (bool) {
2    return a > b;  // Assuming both a and b are Q64.96 numbers
3}

简单,是吧?关键是在比较之前确保两个数字都采用 Q64.96 格式。

测试和审计策略

此时,你可能在想,“我如何确保我的 Q64.96 实现真正安全?”

好问题!虽然单元测试很重要,但在 DeFi 安全方面,我们需要拿出大杀器:模糊测试和形式化验证。

模糊测试:发现未知未知

模糊测试就像向你的代码扔一千只猴子,看看什么会崩溃。好吧,不完全是这样,但差不多!它涉及生成大量随机(或半随机)输入来测试你的函数。这对于 Q64.96 操作尤其关键,因为边缘情况可能会非常棘手

例如,模糊器可能会使用极端值来测试你的 multiplyQ64x96 函数:

1function testFuzzMultiplyQ64x96(uint256 a, uint256 b) public {
2    vm.assume(a <= type(uint256).max / 2**96);
3    vm.assume(b <= type(uint256).max / 2**96);
4    uint256 result = multiplyQ64x96(a, b);
5    assert(result <= type(uint256).max);
6}

像 Echidna 或 Foundry 内置的模糊测试器这样的工具可以运行数千次此类测试,可能发现你从未考虑过的漏洞。

形式化验证:数学确定性

现在,如果模糊测试就像向你的代码扔猴子,那么形式化验证就像有一队数学家证明你的代码是正确的。它使用数学方法来证明或反驳你的算法的正确性。

对于 Q64.96 数字,形式化验证可能特别强大。它可以证明你的操作永远不会溢出、损失精度或违反你指定的任何其他属性,适用于所有可能的输入。

这里是真正有趣的地方:Zealynx Security 专门将这些高级技术应用于 DeFi 协议。我们已经使用模糊测试和形式化验证来发现其他方法遗漏的微妙错误。请看这个 Advanced Security Test Suite 是如何为这个 DeFi 协议实现的。

Advanced Security Test Suite

综合方法

当然,最好的策略是结合多种方法:

  1. 彻底的单元测试开始,以确保基本正确性。
  2. 应用模糊测试以发现意想不到的边缘情况。
  3. 使用形式化验证以获得数学确定性
  4. 最后,考虑进行专业审计,以发现你可能遗漏的任何问题。

请记住,在 DeFi 安全方面,再怎么小心都不为过。风险很高,即使 Q64.96 计算中的一个小错误也可能导致重大损失。

通过结合这些高级测试策略,你不仅是在编写安全代码——你还在为更健壮值得信赖的 DeFi 生态系统奠定基础。

结论

呼! 我们已经讲了很多内容,不是吗?让我们快速回顾一下:

  1. Q64.96 数字是 Uniswap 和类似 DeFi 协议中进行精确计算的强大工具。
  2. 它们有自己的一套陷阱,包括溢出/下溢风险和精度损失。
  3. 使用正确的库并遵循最佳实践可以帮助你避免这些问题。
  4. 彻底的测试和专业审计对于确保你的 Q64.96 计算的安全性至关重要。

请记住,在 DeFi 中,精度和安全性齐头并进。通过掌握 Q64.96 数字,你不仅是在编写更好的代码——你还在为更安全和可靠的 DeFi 生态系统做贡献。

联系我们:

https://x.com/TheBlockChainer

https://www.linkedin.com/in/carlos-vendrell-felici/

https://linktr.ee/bloqarl

如果你需要以下方面的帮助,请合作:

  • 智能合约审计 (Solidity, Rust, Cairo)
  • 高级安全测试套件 (Fuzz, Invariant tests + Formal Verification)
  • 智能合约开发
  • Web2 渗透测试

https://github.com/ZealynxSecurity

https://x.com/ZealynxSecurity

Footer Image


FAQ: Uniswap 中的 Q64.96 数字

1. 为什么 Uniswap 使用 Q64.96 而不是常规小数?

Solidity 不原生支持浮点数。Q64.96 是一种定点表示,使用 $64$ 比特表示整数部分,$96$ 比特表示小数部分,允许精确的价格计算,避免了仅使用整数运算可能出现的舍入误差。

2. 如果我直接将两个 Q64.96 数字相乘会发生什么?

直接将两个 Q64.96 数字相乘几乎肯定会导致 uint256 溢出,因为结果会按 $2^{192}$ 缩放。你必须使用 FullMath.mulDiv(),它执行中间的 $512$ 比特数学运算,以避免溢出同时保留精度。

3. 我可以将 Q64.96 数字与常规整数进行比较吗?

不能。代表 $1.0$ 的 Q64.96 数字存储为 $2^{96}$,这比整数 $1$ 大得多。在比较之前,两个值必须采用相同的格式,否则你将每次得到不正确的结果。

4. Ticks 与 Q64.96 数字有什么关系?

Ticks 是 Uniswap 集中流动性系统中的离散价格点,每个 tick 代表 $0.01\%$ (1 个基点) 的价格变化。内部存储的 sqrtPriceX96 是当前价格平方根的 Q64.96 表示,tick 数学在 ticks 和此 Q64.96 价格表示之间进行转换。

5. 模糊测试对发现 Q64.96 错误有效吗?

是的。模糊测试特别有效,因为 Q64.96 边缘情况难以手动枚举。模糊测试器可以测试极端值、接近溢出条件和精度损失场景,通过数千次随机输入,捕获单元测试遗漏的漏洞。


词汇表

术语 定义
Fixed-Point Arithmetic 一种通过为小数部分保留固定位数来使用整数表示小数的方法,避免了智能合约中的浮点不精确性。
FullMath 一个 Uniswap 库,以中间 $512$ 比特的精度执行乘法和除法,防止在处理大型定点数时溢出。
sqrtPriceX96 以 Q64.96 格式存储的代币对价格的平方根,由 Uniswap V3 和 V4 内部使用,用于高效的价格和流动性计算。
Fuzzing 一种自动化测试技术,生成大量随机或半随机输入,以发现代码中意想不到的行为、边缘情况和漏洞。
Tick Uniswap 集中流动性系统中的一个离散价格点,每个 tick 代表 $0.01\%$ (1 个基点) 的价格变化。

查看完整词汇表 →

  • 原文链接: zealynx.io/blogs/uniswap...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
zealynx
zealynx
江湖只有他的大名,没有他的介绍。