正如我们在之前的文章中强调的那样,《Awesome Uniswap v4 Hooks》仓库[1]中超过 30% 的项目存在漏洞。值得注意的是,我们在此提及的漏洞是 Uniswap v4 交互特有的。因此,在本文中,我们将从以下两个角度对安全的 Hook 交互逻辑进行深入分析:
- 错误的访问控制
- 不当的输入验证
对于每个类别,我们将首先分析漏洞,并通过提供相应的概念验证 (PoC) 来演示其潜在的利用方式。随后将讨论潜在的缓解策略。
错误的访问控制
通常,与 Uniswap v4 Hooks 相关的交互可以根据 Hook 是作为锁定者(在 PoolManager 中获取锁以在池中执行操作)还是作为普通 Hook 来分类。两种主要的交互场景需要适当的访问控制:
- Hook-PoolManager 交互:这涉及官方回调函数与
PoolManager之间的交互。回调函数包括八个池操作回调(即initialize、modifyPosition、swap和donate)以及锁回调(即lockAcquired)。
- Hook-Internal 交互:这与 Hook 合约(作为锁定者)内部发生的交互有关。
Hook-PoolManager 交互相对直接。在此,Hook 完全充当 Hook,接受八个池操作回调。Hook 中的逻辑不影响相关池,这意味着 Hook 和池之间没有资金流动。回调函数提供的参数用于修改必要的存储或作为重要的函数参数。关键考虑因素是回调参数是否可以被操纵。
Hook-Internal 交互则更为复杂。在实际应用中,许多 Hook 原型不仅仅充当纯粹的 Hook。一些开发者允许 Hooks 为用户提供资金管理功能。这些功能可能未在 Hook 合约中实现,但在此上下文中,我们仍可将它们统称为 Hook。在这种情况下,Hook 接受用户资金并执行池操作,例如流动性管理或兑换。这意味着合约必须从 PoolManager 获取锁,将 Hook 转变为锁定者。
Uniswap 基金会已经考虑了这种情况,并在其 hook 模板中集成了一个函数。具体而言,BaseHook 模板提供了 lockAcquired 函数作为锁回调,如下所示:
function lockAcquired(bytes calldata data) external virtual poolManagerOnly
returns (bytes memory) {
(bool success, bytes memory returnData) = address(this).call(data);
if (success) return returnData;
if (returnData.length == 0) revert LockFailure();
// if the call failed, bubble up the reason
/// @solidity memory-safe-assembly
assembly {
revert(add(returnData, 32), mload(returnData))
}
}
为了执行自定义逻辑,lockAcquired 接受 data 字节,并使用该 data 对自身进行低级调用。data 取决于 Hook 的业务逻辑,并且可以被用户操纵,这可能由于 lockAcquired 触发的 Hook-Internal 交互而导致安全问题。请注意,Hook 的设计非常灵活,我们无法在此情境下涵盖所有可能的情况。我们在此主要关注Hook 获取锁及其后续的内部交互。深入探讨其他潜在的业务逻辑将使情况过于复杂,不适合本次讨论。
在这两种场景中,鉴于这些函数具有明确的交互实体,当务之急是解决可能导致利用的任何错误的访问控制。在接下来的子节中,我们将按顺序检查每种场景,并讨论必要的访问控制以确保更安全的交互逻辑。
漏洞分析
访问控制为许多项目提供了高效且直接的安全解决方案。如果一个函数被设计为由特定实体调用,它应该包含访问控制。最著名的访问控制示例是 OpenZeppelin 库的 Ownable 合约,它要求特权函数只能由合约所有者调用。很明显,我们上面讨论的两种场景都适合此类控制。
Hook-PoolManager 交互:为确保与 PoolManager 的安全交互,Hooks 应对这些回调函数强制执行必要的访问控制。具体来说,这些回调函数应只能由 PoolManager 调用,而不能由任何其他账户调用。未能建立此类控制可能会使这些敏感接口暴露于恶意行为者的潜在利用之下。
除了八个池操作回调之外,锁(即 lockAcquired)回调(在从 PoolManager 获取锁后执行自定义逻辑)也需要解决此问题。
Hook-Internal 交互:Hook 内部交互所涉及的函数也被设计为由特定调用者调用。如前所述,此场景包含两个阶段。首先,锁定者的 lockAcquired 函数由 PoolManager 调用,这表明该函数应该要求 msg.sender 是 PoolManager。其次,Hook 会相应地分派函数调用。根据 BaseHook 的设计,它是通过对 Hook 本身的低级调用来实现的。这表明这些函数必须定义为 external,并限制调用者必须是 Hook 的地址。
以 Awesome Uniswap v4 Hooks 仓库列出的一个示例,即 Stop Loss Order 为例[2]:
止损订单直接集成到 Uniswap V4 池中,并在链上发布并通过 afterSwap() hook 执行。不需要外部机器人或参与者来保证执行。
让我们看一下该 Hook 中的 afterSwap 回调函数:
![图 1:止损单的 afterSwap 函数[2]](https://assets.blocksec.com/frontend/blocksec-strapi-online/1_09ba973d9e.webp)
显然,上面的函数被设计用于执行敏感操作。然而,由于访问控制存在缺陷,恶意行为者可以通过操纵参数(例如 key 和 params)来利用它,从而导致意外行为。例如,afterSwap 回调可能假设兑换已在 PoolManager 中进行。在此之后,它可能会开始记录关键状态信息的操作,例如当前价格或收集的兑换费用。但是,如果 afterSwap 没有严格限制其调用者来自 PoolManager,恶意行为者可能会伪造 params 参数,导致记录的状态失真。
漏洞利用和 PoC
为简单起见,我们将使用一个基本的 PoC 来说明此访问控制问题。通常,Hook 的 beforeInitialize 接受一个 PoolKey 类型参数,该参数必须在其 hooks 字段中包含此 Hook 地址(因为 PoolManager 将使用此字段确定要调用的 Hook 地址)。
屏幕截图提供了一个 PoC,展示了 Hook 存在错误访问控制的利用方式,如 DiamondHookPoC [3] 中所示。
在 beforeInitialize 回调函数上没有访问限制的情况下,恶意行为者可以向该函数提供任意的 poolKey。Hook 不会验证此 poolKey 的 Hook 是否与当前 Hook 地址匹配。

虽然值得注意的是,此场景中的漏洞可能不会对 Hook 造成经济损失,但它仍然极大地凸显了 Hook 的状态可以通过未受保护的回调函数进行操纵。
如何缓解
为了确保 Hook-PoolManager 交互的安全性,Hook 回调和锁回调都应将它们的访问权限严格限制为 PoolManager。
幸运的是,Uniswap v4 通过其 v4-periphery 仓库中的 BaseHook 提供了最佳实践[4]。
BaseHook 提供了 poolManagerOnly 修饰符,以严格限制从 PoolManager 的调用:
/// @dev Only the pool manager may call this function
modifier poolManagerOnly() {
if (msg.sender != address(poolManager)) revert NotPoolManager();
_;
}
此修饰符可用于强制执行对敏感 Hook 和锁回调的正确访问控制。
另一方面,Hook-Internal 交互的存在要求通过 lockAcquired 回调(如 BaseHook 中指定的)调用的任何重要的状态更改函数都不能被随意调用。
为了满足此要求,BaseHook 提供了一个 selfOnly 修饰符。此修饰符将声明的函数的可访问性限制为 Hook 本身,阻止外部合约直接调用这些敏感函数以达到恶意目的。
/// @dev Only this address may call this function
modifier selfOnly() {
if (msg.sender != address(this)) revert NotSelf();
_;
}
总之,通过继承 BaseHook,自定义 Hook 可以利用这些内置的访问控制修饰符和回调来强制执行正确的访问控制。
不当的输入验证
v4-periphery[4] 中的 BaseHook 为更安全的交互逻辑提供了解决方案,Hook 的开发者可以利用它。然而,我们继续观察到不当使用的实例,这为现有 Hook 中的攻击向量开辟了新的可能性。
默认情况下,Hook 允许任何池通过 PoolManager 中的 initialize 函数进行注册。但是,如果 Hook 未能验证注册池中的底层资产,恶意用户可能会注册一个包含假冒代币的池,从而使他们能够通过代币的 transfer 函数重新进入 Hook。
此漏洞很微妙,因为 Hook 本身可能不执行恶意逻辑。然而,当 Hook 调用 PoolManager 时,PoolManager 与恶意池底层资产之间的交互可能会通过 PoolManager 中的 take 函数将控制流转移给攻击者。
/// @inheritdoc IPoolManager
function take(Currency currency, address to, uint256 amount) external override
noDelegateCall onlyByLocker {
_accountDelta(currency, amount.toInt128());
reservesOf[currency] -= amount;
currency.transfer(to, amount);
}
本质上,该漏洞源于对 Hook 用户计划交互的注册池存在不当验证。我们将通过一个具体示例来深入探讨此漏洞,并讨论潜在的缓解策略。
漏洞分析
Take Profits Hook[5] 是 Awesome Uniswap v4 Hooks 列出的一个 Hook:
在这个例子中,我们构建了一个允许用户设置“止盈”头寸的 Hook。例如,在 ETH/DAI 池中,如果当前 1 ETH = 1500 DAI,您可以设置一个止盈订单,如“当 1 ETH = 2000 DAI 时卖出我所有的 ETH”,该订单将自动执行。
让我们看一下该 Hook 中的 _handleSwap 函数。此函数在获取锁后执行兑换以填补止盈订单。
![图 3:止盈 Hook 的 _handleSwap 函数[5]](https://assets.blocksec.com/frontend/blocksec-strapi-online/3_53b046f4c7.webp)
您可能会注意到此函数不受任何访问控制修饰符的保护。然而,第 250 行有效地限制了访问,因此此函数只能在从 PoolManager 获取锁后调用。否则,poolManager.swap 将失败,因为操作者不是最新的锁定者。换句话说,_handleSwap 必须按特定顺序调用,前提是注册的池已验证。不幸的是,Hook 没有实现此类验证。
由于此实现存在缺陷,Hook 容易受到重入攻击。此漏洞可能允许攻击者利用用户存入的资金强制进行任意兑换。
漏洞利用和 PoC
具体来说,可以通过以下步骤发起攻击:
- 攻击者注册一个包含假冒代币的恶意池,并将止盈 Hook 指定为池的 Hook。
- 攻击者通过 Hook 在恶意池中设置一个止盈订单。
- 攻击者在恶意池中执行兑换,触发
afterSwap回调中的fillOrder来填补攻击者的止盈订单。 - Hook 调用
PoolManager的lock函数请求锁,并在lockAcquired回调中调用_handleSwap函数。 - 在
_handleSwap函数中,代币的转移触发了假冒代币合约中的恶意逻辑,该逻辑重新进入_handleSwap函数。这是可能的,因为_handleSwap是一个外部函数,没有任何可访问性限制。由于锁已获得,攻击者可以强制 Hook 在任何池上执行任意兑换,只要 Hook 持有足够的底层资产。攻击者然后可以夹挤兑换,以牺牲其他用户的利益来获利。
以下详细图说明了攻击流程。

如前所述,Hook 本身不执行恶意逻辑。唯一的错误是Hook 没有阻止不受信任的代币池在 PoolManager 合约中注册。间接而言,假冒代币合约中的恶意逻辑是通过代币转移操作调用的,这也算是一种不受信任的外部调用。
如何缓解
有三种可行的方法可以缓解由于不当输入验证而导致的潜在攻击:
-
适当的访问控制。通过利用
BaseHook的构建块,Hook 可以严格管理函数的可访问性。这可以防止任意账户调用敏感函数。 -
重入锁。在上述攻击场景中,此方法无疑可以防止恶意代币逻辑重新进入敏感函数。然而,在某些情况下,Hook 的设计要求 Hook 本身是可重入的。 具体来说,当 Hook 需要执行一些池操作时,它应该允许
PoolManager重新进入其回调以完成这些操作。重入锁可能会破坏这种预期的功能。 -
白名单方法。这将需要一个特权管理员在 Hook 中白名单化批准的池。管理员确保白名单化的池不会引入潜在风险。然而,限制是 Hook 用户只能通过 Hook 在有限数量的管理员批准的池上执行操作。 虽然白名单方法提高了安全性,但它严重限制了 Hook 的功能。
要找到一个在安全性和可用性之间取得平衡的完美解决方案对 Hook 来说是具有挑战性的。虽然我们讨论了几种缓解方法,但开发人员需要仔细考虑其 Hook 设计中的权衡。目标应该是尽可能缓解潜在风险,同时保留预期的功能。此外,我们的讨论仅涵盖可能存在于与 Uniswap v4 特性特别相关的交互中的漏洞。实际应用无疑将更加全面。务必确保您理解合约的每一行代码,并保持 SAFU!
结论
在本文中,我们探讨了在 Hook 交互逻辑中出现的漏洞,特别关注了两种情况:错误的访问控制和不当的输入验证。我们进行了详细的漏洞分析,展示了潜在的漏洞利用及其 PoC,并讨论了潜在的缓解策略。我们相信这些见解可以为 Hook 的安全开发和使用做出贡献,并指导未来漏洞检测的努力。
参考
[2] Stop Loss Order
[3] DiamondHookPoC
[4] v4-periphery
[5] Take Profits



