相信 Uniswap v4 不久就会和大家见面了!
这一次 Uniswap 团队可谓目标宏大,计划引入众多全新功能[1],包括每个交易对支持无限数量的流动性池和动态费用、单例设计、闪电记账、Hook,以及支持 ERC1155 代币标准。利用 EIP-1153 引入的瞬态存储,Uniswap v4 预计将在以太坊坎昆升级之后发布。
在诸多创新中,Hook 机制因其强大潜力引起了广泛关注。Hook 机制支持在流动性池生命周期中的特定点执行特定代码,大大增强了池子的可扩展性和灵活性。
然而,Hook 机制也可能是一把双刃剑。虽然它功能强大且灵活,但安全使用 Hook 同样是一个不小的挑战。Hook 的复杂性不可避免地带来了新的潜在攻击向量。因此,我们希望撰写一个系列文章,来系统介绍与 Hook 机制相关的安全问题与潜在风险,以此推动社区的安全发展,相信这些见解将有助于构建安全的 Uniswap v4 Hook。
作为该系列文章的开篇之作,本文介绍了与 Uniswap v4 中 Hook 机制相关的概念,并概述了Hook 机制存在的安全风险。
Uniswap V4的机制
在深入探讨之前,我们需要对 Uniswap v4 的机制有一个基本的了解。根据官方公告[1]和白皮书[2],Hook、单例架构和闪电记账是实现自定义流动性池和跨多个池子实现高效路由的三个重要功能。
1.1 Hook
Hook 指的是在流动性资金池生命周期的不同阶段运行的合约,Uniswap 团队希望通过引入 Hook使任何人都能做出权衡决策。通过这种方式,可以实现原生支持动态费用、添加链上限价单、或者通过时间加权平均做市商(TWAMM)分散大订单。
目前有八个Hook 回调,分为四组(每组包含一对回调):
-
beforeInitialize/afterInitialize
-
beforeModifyPosition/afterModifyPosition
-
beforeSwap/afterSwap
-
beforeDonate/afterDonate
下文是白皮书[2]中介绍的交换 Hook 的流程。
图 1:交换 Hook 流程
Uniswap 团队用一些示例(例如 TWAMM Hook[3])介绍了操作方法,社区参与者也做出了一些贡献。官方文档[4]还链接到了Awesome Uniswap v4 Hooks[5] 仓库,该仓库收集了更多的 Hook 示例。
1.2 单例、闪电记账和锁机制
单例(singleton)架构和闪电记账(flash accounting)旨在通过降低成本和确保效率来提高性能。它引入了一种新的 singleton 合约,即所有流动性池都保存在同一个智能合约中。这个单例设计依赖一个 PoolManager 来存储和管理所有池子的状态。
在 Uniswap 协议的早期版本中,兑换或添加流动性等操作涉及直接代币转移,v4 版本则有所不同,在于其引入了闪电记账和锁机制(lock mechanism)。
锁机制的运作方式如下:
1. 某个locker合约在 PoolManager 上请求lock。
2. PoolManager 将该locker合约的地址添加到 lockData 队列,并调用其 lockAcquired 回调。
3. 该locker合约在回调中执行其逻辑。在执行过程中,locker合约与池子的交互可能导致非零的货币增量。然而,在执行结束时,所有增量必须结算为零。另外,如果 lockData 队列不为空,只有最后一个locker合约可以执行操作。
4. PoolManager 检查 lockData 队列和货币增量的状态。验证后,PoolManager 将删除该locker合约。
总结来说,锁机制防止了并发访问,并保证了所有的交易都能被清算。而locker合约按顺序申请lock,然后通过 lockAcquired 回调执行交易。在每次池操作前后会触发对应的Hook回调。最后,PoolManager 会检查状态。
这种方法意味着操作调整的是内部净余额(即delta),而不是执行即时转账。任何修改都会记录在池子的内部余额中,实际的转账则在操作(即lock)结束时进行。这个过程保证了没有未清算的代币,从而维持了资金的完整性。
由于锁机制的存在,外部所有账户 (EOA) 不能直接与 PoolManager 进行交互。相反,任何交互都必须通过一个合约进行。该合约作为一个中间的locker,在进行任何池操作之前都需要请求lock。
主要存在两种合约交互场景:
-
某个locker合约来自官方的代码库,或者由用户部署。在这种情况下,我们可以将交互视为通过路由器进行。
-
某个locker合约和 Hook 集成到同一个合约中,或由第三方实体控制。对于这种情况,我们可以将交互视为通过Hook进行。在这种情况下,Hook既扮演了locker合约的角色,又负责处理回调。
威胁模型
在讨论相关的安全问题之前,我们需要确定威胁模型。我们主要考虑以下两种情况:
-
威胁模型 I:Hook 本身是良性的,但存在漏洞。
-
威胁模型 II:Hook 本身就是恶意的。
在接下来的部分,我们将根据这两种威胁模型讨论潜在的安全问题。
2.1 威胁模型 I 中的安全问题
威胁模型 I 关注的是与 Hook 本身相关的漏洞。这个威胁模型假设开发者及其 Hook 是无恶意的。然而,智能合约现有的已知漏洞也可能出现在 Hook 中。例如,如果 Hook 是作为可升级合约实现的,那么它可能会遇到类似于 OpenZeppelin 的 UUPSUpgradeable 漏洞的相关问题。
鉴于以上因素,我们选择聚焦于v4版本特有的潜在漏洞。在 Uniswap v4 中,Hook 是能够在核心池操作(包括初始化、修改位置、交换和收集)之前或之后执行自定义逻辑的智能合约。虽然 Hook 预计将实现标准的接口,但它也允许包含自定义逻辑。因此,我们的讨论范围将限制在涉及标准 Hook 接口的逻辑。然后,我们将尝试找出可能的漏洞来源,例如,Hook 可能如何滥用这些标准 Hook 函数。
具体来说,我们将关注以下两种 Hook:
-
第一种 hook, 保管用户资金。在这种情况下,攻击者可能会攻击这个 hook 来转移资金,造成资产损失。
-
第二种 hook, 存储用户或其他协议依赖的关键状态数据。在这种情况下,攻击者可能会试图改变关键状态。当其他用户或协议使用错误状态时,可能会带来潜在风险。
请注意,这两种范围之外的 hook 不在我们的讨论范围内。
由于本文撰写时还没有 Hook 的真实用例,我们将从 Awesome Uniswap v4 Hooks 仓库中获取一些信息。
在对Awesome Uniswap v4 Hooks仓库(提交哈希为3a0a444922f26605ec27a41929f3ced924af6075)进行深入研究后,我们发现了几个严重的漏洞。这些漏洞主要源于 hook、PoolManager 以及外部第三方之间的风险交互,主要可以分为两类:访问控制问题和输入验证问题。具体发现请见下表:
总的来说,我们发现了22个相关项目(排除与Uniswap v4无关的项目)。在这些项目中,我们认为有8个(36%)项目是存在漏洞的。在这8个有漏洞的项目中,6个存在访问控制问题,2个容易受到不受信任的外部调用。
2.1.1 访问控制问题
在这部分讨论中,我们主要关注的是 v4 中的回调函数可能导致的问题,包括 8 个 hook 回调和 lock 回调。当然,还有其他情况需要验证,但这些情况因设计而异,暂不在我们的讨论范围之内。
这些函数应该只能被 PoolManager 调用,不能被其他地址(包括EOA和合约)调用。例如,在奖励由资金池密钥分发的情况下,如果相应的函数可以由任意账户调用,那么奖励可能会被错误地领取。
因此,对于hook来说,建立强大的访问控制机制是至关重要的,尤其是它们可以被除了池子本身之外的其他方调用。通过严格管理访问权限,流动性池可以显著降低与hook未授权交互或恶意交互的风险。
2.1.2 输入验证问题
在Uniswap v4中,由于存在锁机制,用户在执行任何资金池操作之前必须通过合约获得一个lock。这确保了当前参与交互的合约是最新的locker合约。
尽管如此,仍然存在一个可能的攻击场景,即由于在一些易受攻击的 Hook 实现中输入验证不当而导致的不受信任的外部调用:
-
首先,hook并未验证用户打算交互的资金池。这可能是一个含有虚假代币并执行有害逻辑的恶意资金池。
-
其次,一些关键的hook函数允许任意的外部调用。
不受信任的外部调用极其危险,因为它可能导致各种类型的攻击,包括我们熟知的重入攻击。
为了攻击这些易受攻击的hook,攻击者可以为自己的虚假代币注册一个恶意资金池,然后调用hook在资金池执行操作。在与资金池交互时,恶意代币逻辑劫持控制流以便进行不良行为。
2.1.3 针对威胁模型 I 的防范措施
为了规避与hook相关的此类安全问题,通过适当执行对敏感的外部/公共函数的必要访问控制,并对输入参数进行验证,从而对交互进行验证是至关重要的。此外,重入保护可能有助于确保 hook 不会在标准逻辑流程中被重复执行。通过实施适当的安全防护措施,资金池可以降低与此类威胁相关的风险。
2.2 威胁模型 II 中的安全问题
在这个威胁模型中,我们假设开发者及其 hook 是恶意的。鉴于涉及范围很广,我们仅关注与 v4 版本相关的安全问题。因此,关键在于提供的 hook 是否能够处理用户转账或授权的加密资产。
由于访问 hook 的方法决定了可能赋予 hook 的权限,我们据此将 hook 分为两类:
-
托管型 Hook(Managed Hooks):hook 不是入口点。用户必须通过路由器(可能由 Uniswap 提供)与 hook 进行交互。
-
独立型 Hook(Standalone Hooks):hook 是入口点,允许用户直接与之交互。
图 2:恶意 Hook 的例子
2.2.1 托管型 Hook
在这种情况下,用户的加密资产(包括原生代币和其他代币)被转账或授权给 router 。由于 PoolManager 执行了余额检查,恶意 hook 不容易直接窃取这些资产。然而,仍然存在潜在的攻击面。例如,v4 版本的费用管理机制可能会被攻击者通过 hook 进行操纵。
2.2.2 独立型 Hook
当 Hook 被用作入口点时,情况就更加复杂。在这种情况下,由于用户可以直接与 hook 进行交互,hook 获得了更多的权力。理论上,hook 可以通过这种交互执行想要的任何操作。
在 v4 版本中,代码逻辑的验证是非常关键的。主要问题在于是否可以操纵代码逻辑。如果 hook 是可升级的,这意味着一个看似安全的 hook 可能会在升级之后成为恶意的,从而构成重大风险。这些风险包括:
-
可升级的代理(可以被直接攻击);
-
带有自毁逻辑。在联合调用 selfdestruct 和 create2 的情况下可能被攻击。
2.2.3 针对威胁模型 II 的防范措施
至关重要的一点在于评估 hook 是否是恶意的。具体来说,对于托管型 hook,我们应该关注费用管理的行为;而对于独立型 hook,主要的关注点在于它们是否可升级。
结论
在本文中,我们首先简要概述了与 Uniswap v4 的 Hook 安全问题相关的核心机制。随后,我们提出了两种威胁模型并简要概述了相关的安全风险。
在后续文章中,我们将对每种威胁模型下的安全问题进行深入分析。