正如我们上一篇文章所强调的,在 Awesome Uniswap v4 Hooks 仓库[1] 中,超过 30% 的项目存在漏洞。值得注意的是,我们此处提到的漏洞是 Uniswap v4 交互所特有的。因此,在本文中,我们将从以下两个角度审视安全的 Hook 交互逻辑:
- 访问控制缺陷
- 输入验证不当
对于每一类漏洞,我们都将首先进行分析,并通过提供相应的概念验证 (PoC) 来演示其潜在的利用方式。随后,我们将讨论潜在的缓解策略。
访问控制缺陷
通常,与 Uniswap v4 Hook 相关的交互可以根据 Hook 是否充当“锁定者”(locker)——即在 PoolManager 中获取锁以在资金池中执行操作——来进行分类。有两种主要的交互场景需要适当的访问控制:
- Hook-PoolManager 交互:这涉及官方回调函数与
PoolManager之间的交互。回调函数包括八个资金池操作回调(即initialize、modifyPosition、swap和donate)以及锁定回调(即lockAcquired)。
- Hook-内部 交互:这涉及在 Hook 合约内部(充当锁定者时)发生的交互。
Hook-PoolManager 交互相对直接。在此场景中,Hook 纯粹充当 Hook 的角色,接收八个资金池操作回调。Hook 中的逻辑不会影响相关资金池,这意味着 Hook 和资金池之间没有资金流动。回调函数提供的参数用于修改必要的存储或作为重要的函数参数。关键考虑因素是 回调参数是否可以被篡改。
Hook-内部 交互则稍显复杂。实际上,许多 Hook 原型不仅仅充当纯粹的 Hook。一些开发者允许 Hook 为其用户提供资金管理功能。这些功能可能并未在 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-内部 交互而导致安全问题。请注意,Hook 的设计非常灵活,我们无法涵盖此情况下的所有可能场景。我们此处的主要关注点是 Hook 获取锁及其随后的内部交互。深入探讨其他可能的业务逻辑会使情况过于复杂,超出了本讨论的范围。
在这两种场景中,优先事项都是解决任何可能导致漏洞利用的缺陷访问控制,因为这些函数具有明确的交互实体。在随后的子章节中,我们将依次检查每个场景,并讨论确保更安全交互逻辑所需的必要访问控制。
漏洞分析
对于许多项目而言,访问控制是高效且直接的安全解决方案。如果一个函数被设计为由特定实体调用,它就应该包含访问控制。访问控制最著名的例子是 OpenZeppelin 库的 Ownable 合约,它要求特权函数只能由合约所有者调用。显然,我们上面讨论的两个场景都是此类控制的适用案例。
Hook-PoolManager 交互:为了与 PoolManager 进行安全交互,Hook 应该对这些回调函数实施必要的访问控制。具体来说,这些回调函数应该仅能由 PoolManager 调用,而不能由任何其他账户调用。未能建立此类控制可能会使这些敏感接口暴露给恶意行为者,从而面临被利用的风险。
除了八个资金池操作回调外,在从 PoolManager 获取锁后执行自定义逻辑的锁(即 lockAcquired)回调也需要解决此问题。
Hook-内部 交互:涉及 Hook 内部交互的函数同样被设计为由特定的调用者触发。正如我们之前所说,此场景包含两个阶段。首先,锁定者的 lockAcquired 函数由 PoolManager 调用,这表明该函数应该要求 msg.sender 是 PoolManager。其次,Hook 相应地分发函数调用。基于 BaseHook 的设计,它是通过对 Hook 自身进行底层调用来实现的。这表明这些函数必须定义为 external,并限制调用者必须是 Hook 的地址。
以 Awesome Uniswap v4 Hooks 仓库列出的示例之一,即 止损订单 (Stop Loss Order) 为例[2]:
止损订单直接集成到 Uniswap V4 资金池中,发布在链上并通过 afterSwap() Hook 执行。无需外部机器人或参与者来保证执行。
让我们检查它的 afterSwap 回调函数:
![Figure 1: The afterSwap function of Stop Loss Order[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 地址)。
截图提供了一个展示利用具有访问控制缺陷的 Hook 的 PoC,正如在 DiamondHookPoC [3] 中所见。
由于 beforeInitialize 回调函数上缺乏访问限制,恶意行为者可以向此函数输入任意的 poolKey。Hook 不会验证此 poolKey 的 Hook 是否与当前 Hook 地址匹配。

虽然需要注意的是,这种情况下的利用可能不会导致 Hook 的经济损失,但它依然剧烈地凸显了如何通过未受保护的回调函数来操纵 Hook 的状态。
如何缓解
为了确保 Hook-PoolManager 交互的安全性,Hook 回调和锁定回调都应将其可访问性严格限制为仅限 PoolManager。
幸运的是,Uniswap v4 通过其 v4-periphery 仓库[4] 中的 BaseHook 提供了最佳实践。
BaseHook 提供了 poolManagerOnly 修饰符,以强制仅从 PoolManager 进行调用:
/// @dev Only the pool manager may call this function
modifier poolManagerOnly() {
if (msg.sender != address(poolManager)) revert NotPoolManager();
_;
}
此修饰符可有效地用于对敏感的 Hook 和锁定回调实施适当的访问控制。
另一方面,Hook-内部 交互的存在要求任何通过 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 用户计划交互的已注册资金池缺乏适当的验证。我们将使用一个具体的例子来深入探讨此漏洞,并讨论潜在的缓解策略。
漏洞分析
止盈 Hook (Take Profits Hook)[5] 是 Awesome Uniswap v4 Hooks 列出的一个 Hook:
在此示例中,我们构建了一个允许用户设置“止盈”头寸的 Hook。例如,在 ETH/DAI 资金池中,如果当前 1 ETH = 1500 DAI,您可以设置一个止盈订单为“在 1 ETH = 2000 DAI 时卖出我所有的 ETH”,它将被自动执行。
让我们看看此 Hook 中的 _handleSwap 函数。此函数在获得锁后执行代币交换以完成止盈订单。
![Figure 3: The _handleSwap function of Take Profits Hook[5]](https://assets.blocksec.com/frontend/blocksec-strapi-online/3_53b046f4c7.webp)
您可能会注意到此函数没有受到任何访问控制修饰符的保护。然而,第 250 行有效地限制了访问权限,使得此函数只能在从 PoolManager 获取锁后才能被调用。否则,poolManager.swap 将会失败,因为操作者不是最近的锁定者。换句话说,_handleSwap 必须在特定顺序下被调用,前提是已注册的资金池经过了验证。遗憾的是,该 Hook 并未实现此类验证。
由于这种有缺陷的实现,该 Hook 容易受到重入攻击。此漏洞可能允许攻击者利用用户存入的资金强制进行任意的代币交换。
利用与 PoC
具体来说,攻击可以通过以下步骤发起:
- 攻击者使用假代币注册一个恶意资金池,并将 止盈 Hook (Take Profits Hook) 指定为该资金池的 Hook。
- 攻击者通过此 Hook 在恶意资金池中下单 止盈 订单。
- 攻击者在恶意资金池中执行代币交换,从而触发
afterSwap回调中的fillOrder以成交攻击者的止盈订单。 - 该 Hook 在
lockAcquired回调中调用PoolManager的lock函数来请求锁,并调用_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



