OpenZeppelin Uniswap Hooks v1.1.0 RC 2 审计报告

本文是对 OpenZeppelin 开发的 Uniswap V4 hooks 代码的审计报告,重点关注 AntiSandwichHook、LiquidityPenaltyHook 和 LimitOrderHook 三个合约,旨在增强 Uniswap V4 流动性池的功能性和安全性。

目录

总结

TypeDeFiTimelineFrom 2025-06-16To 2025-06-26LanguagesSolidityTotal Issues8 (8 已解决)Critical Severity Issues0 (0 已解决)High Severity Issues1 (1 已解决)Medium Severity Issues1 (1 已解决)Low Severity Issues3 (3 已解决)Notes & Additional Information3 (3 已解决)

范围

本审计报告详细说明了对一组自定义 Uniswap V4 Hook执行的全面分析。 这些Hook经过专门设计,旨在增强 Uniswap V4 流动性池的功能和安全性。

本次审计针对 release-v1.1.0-rc.2 分支上 OpenZeppelin/uniswap-hooks 仓库的 3e9fa22 提交进行了。 虽然相同的范围已经在 release-v1.1-rc.1 分支的 0879747 提交上进行了 审计,但由于发现的大量重要问题以及所需的后续重构,简单的修复审查被认为是不够的,因此建议重新审计。

先前报告 中存在但在本报告中不存在的发现已在代码库重构期间得到解决。

以下文件在范围内:

 src
├── base
│   ├── BaseAsyncSwap.sol
│   ├── BaseCustomAccounting.sol
│   ├── BaseCustomCurve.sol
│   └── BaseHook.sol
├── fee
│   └── BaseDynamicAfterFee.sol
├── general
│   ├── AntiSandwichHook.sol
│   ├── LimitOrderHook.sol
│   └── LiquidityPenaltyHook.sol
└── interfaces
    └── IHookEvents.sol
└── utils
    └── CurrencySettler.sol

除了 general 目录(已对其执行完整的逐行审计)之外,其余范围已根据提交 cb6d90c 在其 差异 上进行了审计。

更新: 本报告中解决的所有发现的修复程序已在 main 分支的提交 67ddcdf 中合并。

系统概述

AntiSandwichHook

AntiSandwichHook 合约 实现了防 三明治 攻击的自动做市商(AMM)设计,旨在缓解 三明治 攻击,即恶意行为者利用区块内的交易排序来提取价值,从而损害诚实用户的利益。 这是通过强制执行以下条件来实现的:任何交换的执行价格均不得优于当前区块开始时的价格。

在每个区块的开始,Hook 会记录池价格和状态的检查点。 当区块的第一次交换发生时,此检查点将被保存并用作参考。 然后,将同一区块内的后续交换与此初始检查点进行比较。 对于 !zeroForOne 方向的交易,Hook 会限制执行,以确保不会获得优于基线的价格。 如果交易产生的收益优于检查点价格允许的收益,则会扣留多余的 代币 并进行处理,以防止 交易者 提取价值。

LiquidityPenaltyHook

LiquidityPenaltyHook 合约 旨在保护 Uniswap V4 池免受 即时流动性(JIT)攻击。 这些攻击涉及对手在大型交易之前立即短暂注入流动性,收取费用,并在同一区块或下一个区块内提取流动性,从而有效地提取价值而不承担市场风险。 这种行为损害了 LTC 长期流动性提供者 (LPs)的利益,并破坏了公平的费用分配。

为了应对这种情况,Hook 强制执行基于添加和随后移除流动性的区块号的基于时间的惩罚机制。 如果过早移除流动性(在可配置的 blockNumberOffset 过去之前),则会 applied 惩罚。 这种惩罚采取费用捐赠的形式:收集到的部分(或全部)费用 redirected back 到池中,并在范围内的LTC之间分配,从而阻止滥用的短期流动性供应。

LimitOrderHook

LimitOrderHook 合约 允许用户通过在 Uniswap V4 池中创建范围外流动性头寸来表达限价单。 当用户创建 tick 范围宽度为 1 tickSpacing 的范围外流动性头寸时,只需要两种资产中的一种,从而有效地模拟在特定价格水平(tick)的单边限价单。

一旦池价格超过该 tick(即,它变为范围内),流动性就会被交换消耗,并且该订单被视为 filledHook 监听交换,并且在检测到价格交叉时,它会自动移除流动性,并将收到的 代币 铸造给自己,以供以后提取。

该合约包括以下功能:

  • 订单聚合:在同一 tick 且在同一方向 placed 的订单被分组到单个订单 ID 中。 每个参与者的流动性都会被单独跟踪,但 contribute 收益的共享池。
  • 订单取消:用户可以通过 cancelOrder 函数取消其未成交订单。 如果用户是该订单的最后一个剩余 LP,则赚取的任何费用都会返还给他们。 否则,应计费用将 allocated 到共享订单池,从而使剩余的参与者受益。
  • 提款:订单成交后,参与者可以通过 withdraw 函数申领其output 代币的按比例份额。
  • Fee Sniping Prevention:为了防止新参与者不公平地申领先前应计的费用,该合约在添加流动性时实施了按用户费用检查点。 只有在用户的检查点之后应计的费用才会 considered 在其提款中。

与 v1.0 的差异

除了添加新合约之外,还对现有合约进行了一些更改:

  • 添加了 IHooksEvents 接口,该接口定义了一些常见的事件发射。 此外,已修改 BaseAsyncSwap, BaseCustomAccounting, BaseCustomCurve, 和 BaseDynamicAfterFee 合约以在适当的情况下发出相应的事件。
  • CurrencySettler 已被修改为包含 SafeERC20 的使用,并且当金额为 0 时提前返回,因为某些 代币 可能会使用此类值恢复。
  • BaseAsyncSwap 合约 现在具有一个 internaloverride able _calculateSwapFee 函数,该函数可以返回要应用的交换费金额(如果需要)(当前返回 0)。
  • BaseCustomAccounting 合约 现在支持对流动性 头寸 使用 salt,以便用户可以通过提供 salt 值来标记其独特的 头寸。 它还具有一个新的 _handleAccruedFees 函数来处理流动性 头寸 中应计的费用。
  • 同样,BaseCustomCurve 合约 现在具有一个 override able _getSwapFeeAmount 函数来计算交换收取的费用。

与 v1.1.0-rc.1 的差异

审计的 Hook 已经存在于代码库的 release-v1.1.0-rc.1 版本中。 但是,此后对其实现进行了重构,以提高 Hook 的鲁棒性和安全性。 以下是 release-v1.1-rc.1 和审计的 release-v1.1-rc.2 之间引入的主要更改。

LiquidityPenaltyHook

  • 添加期间的费用扣留:与仅在移除流动性时应用惩罚的 previous version 不同,更新后的合约 withholds 如果该 头寸 是最近创建的,则在添加流动性时会产生应计费用。 这些费用保留在 Hook 本身中,从而禁用过早收集并防止攻击者通过重复添加/移除少量资金来规避惩罚。
  • 统一的费用会计和结算:新的逻辑通过 summing feeDelta(在移除期间生成)和 withheldFees(来自添加)来统一费用管理,以计算应受惩罚的总金额。 这确保了考虑 头寸 的整个生命周期,并防止通过零散的流动性供应进行操纵。
  • 空池的故障安全:如果应支付惩罚,但池的有效流动性为零,则 Hookreverts 以避免将费用捐赠到空池中(否则可能会导致意外行为或导致扣留资金永久损失)。 即使在极端情况下,这也强制执行了经济上的正确性。

AntiSandwichHook

  • 通过 _handleCollectedFees 进行自定义费用处理:新版本中的一个主要变化是引入了一个 virtual 函数,_handleCollectedFees,该函数将处理多余收集金额的责任委托给继承合约。 此更改为开发人员提供了灵活性,可以确定如何处理多余的费用(无论它们是否应该被捐赠、重新分配、发送到金库或以其他方式处理)。
  • 固定输出交换的输入调整:在使用指定 确切输出 (即固定输出交换)的情况下,如果执行会导致优于检查点的价格,则 Hook 现在会向上 adjusts 输入金额,从而确保用户至少支付目标价格。
  • 明确的保护范围:现在已明确记录和强制执行仅保护 zeroForOne == false 的交换(通常出售 token1 以换取 token0)。 在另一个方向,交换在 AMM 曲线下正常运行,并且不受检查点的约束。

LimitOrderHook

  • 检查点:为了防止费用被盗并进行更稳健的会计处理的边缘案例,添加了 checkpoint 以更好地跟踪订单placement。 此更改影响了下订单和提款时费用的管理方式。
  • 取消清理改进:取消过程中引入了更改,以更好地反映何时删除订单中的整个流动性。

高危

由于当前 Tick 未对齐导致的 Tick 迭代中的无限循环

AntiSandwichHook 合约通过存储每个区块开始时的池状态快照来实现 反 MEV 机制。 作为此过程的一部分,_beforeSwap 函数 iterates 从最后一个检查点到当前 ticktick 索引,以更新流动性和费用数据。 此迭代在 for 循环中使用固定 step 等于池的 tickSpacing 执行,并继续只要 tick != currentTick

但是,currentTick 可能并不总是与池配置的 tickSpacing 对齐。 由于池中价格变动的动态性,会自然发生这种未对齐,这会导致当前 tick 落在任何任意值上,而不是 tick 间距的倍数上。 发生这种情况时,循环条件 tick != currentTick 将永远不会满足,因为使用 tickSpacing 的递增或递减将跳过未对齐的当前 tick 。 因此,循环变为无限循环,消耗所有可用的 gas 并使交易无效。 这会创建一个拒绝服务 (DoS) 向量,因为用户无法再在受影响的池中执行交换。

考虑修改 tick 迭代逻辑以基于方向和当前 tick 与检查点 tick 之间的差异动态计算步骤,确保循环可靠地到达当前 tick,即使它未与 tick 间距对齐也是如此。

更新: 已在提交 5e42129pull request #80 中解决。 团队表示:

_为了解决无限循环迭代问题,我们现在在更新 _lastCheckpoint.state.slot0 之前缓存 lastTick 值,而不是比较 currentTick != lastTick,这可能会导致错位,我们检查 currentTick <= lastTickcurrentTick >= lastTick。 我们还在natspec上添加了一条注释,警告可能会出现较大的 tick 差异,这可能会导致较大的for循环(尽管不是无限的),这可能会导致极端情况下的 MemoryOOG 错误(小的 tick 间距和非常大的 tick 差异)。_

中危

unspecifiedAmount 代表输入而不是输出时的不正确费用应用

BaseDynamicAfterFee 合约通过将交换的 unspecifiedAmount 与目标值进行比较并 charging 差额作为费用来启用动态费用强制执行。 此逻辑假设 unspecifiedAmount 始终代表交换的输出。 但是,如果用户执行精确的输出交换,则 unspecifiedAmount 实际上表示用户必须支付的输入金额(unspecifiedAmount < 0)。

如果 unspecifiedAmount 对应于输入,则当前实现会错误地 applies 如果输入超过目标输出,则收取费用,从而导致用户被多收费用。 发生这种情况是因为费用始终计算为 feeAmount = uint128(unspecifiedAmount) - targetOutput,即使 unspecifiedAmount 是输入。 因此,用户可能会支付与收到的任何输出无关的不必要的额外费用。

在当前的 AntiSandwichHook 实现中,尚未确定利用此问题的明确方法。 但是,由于 BaseDynamicAfterFee 是一个抽象合约,旨在由未来的 Hook 扩展,因此请考虑修改 BaseDynamicAfterFee 中的逻辑,以便仅当 unspecifiedAmount 代表交换的输出端时才应用费用。 这可确保用户不会根据其输入金额被收取费用,并保留 交换后 费用强制执行的预期语义。

更新: 已在提交 2678eb9pull request #86 中解决。 团队表示:

_我们更新了 BaseDynamicAfterFee 逻辑,以区分 exactInputexactOutput 交换,更明确地处理 unspecifiedAmount 而不是仅处理输出。 为了更加明确,我们将 _getTargetOutput 重命名为 _getTargetUnspecified。 在 AntiSandwichHook 级别,我们删除了 _handleCollectedFees 函数,将 代币 的处理留给直接在 _afterSwapHandler 上编写。_

低危

缺少文档字符串

在整个代码库中,发现了多个缺少文档字符串的实例:

考虑彻底记录所有属于任何合约公共 API 的函数 (及其参数)。 即使不是公共的,实施敏感功能的函数也应明确记录。 编写文档字符串时,请考虑遵循 Ethereum Natural Specification Format (NatSpec)。

更新: 已在提交 933feb7pull request #85 中解决。

流动性惩罚可以通过使用辅助账户来规避

LiquidityPenaltyHook 旨在通过在短时间内 ( blockNumberOffset)添加和移除流动性时惩罚费用收取来缓解 JIT 流动性攻击。 在此期间应计的费用将重定向到活跃的范围内LTC,因此不会刺激掉期周围的投机性流动性供应。 但是,在特定条件下,涉及多个账户的协同攻击仍然可以用来绕过此惩罚机制。

此漏洞的核心在于操纵谁接收惩罚捐赠的能力。 由于捐赠的费用会分配给任何在移除流动性时在范围内的人,因此攻击者可以使用辅助账户策略性地在其他空 tick 范围内存放流动性。 攻击过程如下:

  1. 设置: 攻击者 (账户 B) 在一个遥远或低流量的 tick 范围内提供少量流动性,其中不存在其他流动性。
  2. JIT 执行: 几个区块之后,主要攻击者账户 (账户 A) 在当前 tick 周围添加大量流动性 头寸,预计会有用户进行掉期。 这些掉期会产生应计给 A 的费用,但由于 JIT 窗口而被 Hook 扣留。
  3. 费用重定向: 在生成费用后,A 通过掉期将池的价格移入 B 的 tick 范围。 当 A 移除流动性时,Hook 会通过将费用捐赠给范围内的 头寸 (B 的 头寸) 来惩罚该行为。 然后,B 可以立即提取捐赠的费用。
  4. 可选的回复: A 可以选择再次交换以将池恢复到其原始状态。

虽然这种策略在技术上是可行的,但在现实世界中很少是切实可行的。 该攻击依赖于攻击者能够将价格移入特定的 tick 范围,随着池流动性的增加,这种情况的成本和难度会大大增加。 在高度流动的池中,这种操纵的成本通常超过了从收取的费用中获得的潜在收益。 此外,为了提取有意义的利润,攻击者需要在 JIT 窗口内拦截大量的用户掉期,这会引入额外的不确定性和复杂性。

由于这些限制,虽然该机制在理论上仍然可以利用,但它的实际可行性很低。 攻击者必须付出高昂的代价来控制价格变动,并且依赖于适时大量的用户活动,这两者都降低了漏洞的盈利能力和可行性。 因此,此问题已 categor 作为低严重性问题。 它突出了 Hook 的费用捐赠逻辑中的一个细微限制,但在正常的市场条件下并不构成实际威胁。 尽管如此,开发人员和协议设计人员应始终了解通过协同账户行为重定向惩罚的可能性,尤其是在低流动性池中。

考虑扩展 Hook 的文档字符串,以明确提及串通的多账户策略重定向惩罚的可能性,尤其是在低流动性环境中,以确保下游集成商了解此边缘情况。

更新: 已在提交 bd7b885pull request #89 中解决。

getTargetOutput 中的误导性命名可能导致开发者混淆

getTargetOutput 函数根据区块开始时的池状态计算掉期中未指定的金额。 此值可以表示交易的输入或输出,具体取决于掉期方向以及它是精确输入掉期还是精确输出掉期。 尽管如此,函数名称暗示它始终返回输出金额,这并不能准确反映其行为。

考虑将该函数重命名为 getTargetUnspecifiedAmount,以更清楚地表明返回值可能是输入或输出。 这将减少开发人员的潜在混淆,并提高代码的整体清晰度和可维护性。

更新: 已在 pull request #86 中解决。 团队表示:

_我们将 _getTargetOutput 重命名为 _getTargetUnspecified。_

注意 & 附加信息

getLastAddedLiquidityBlock 函数中的文档不匹配

LiquidityPenaltyHook 合约中 getLastAddedLiquidityBlock 函数上方的注释错误地表明它跟踪了流动性 头寸withheldFees更新: 已在pull request #85的 commit 66231a1中解决。 团队声明:

指定的分支没有任何返回值,但为了防止未定义的行为,我们更新了函数以便在该分支中返回 ZERO_BYTES

结论

经过审计的代码库引入了三个新的 Hook 合约:AntiSandwichHook 用于防止三明治攻击,LimitOrderHook 用于下限价单,以及 LiquidityPenaltyHook 用于处理 JIT 攻击的潜在惩罚。 此外,自上一版本以来引入的一些小的更改也包含在此范围内。

在对版本 v1.1.0-rc.1 进行初始审计后,发现了多个高危和严重级别的漏洞,主要与 Uniswap v4 内部机制的细微差别有关。 代码库经过重构,之前发现的问题已得到解决,因此代码库再次经过审计,其输出就是本报告。 审计发现了一个高危问题,而之前报告的问题已得到解决。

感谢 Uniswap 基金会团队在整个审计期间的积极响应和乐于助人。 提供的文档也足以向审计团队提供必要的背景信息。

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

0 条评论

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