本文主要探讨了在 Uniswap v4 中设计 Hook 合约时需要考虑的关键问题。
Uniswap v4 的 PoolManager 中的最新创新为定制的扩展带来了巨大的潜力,可以根据各个池子进行定制。这些扩展以 Hook 合约的形式出现,可以插入到流动性操作流程中,以自定义池子的行为。本指南概述了设计 Hook 以满足你的特定需求时的一些关键考虑因素。
在编写 Uniswap v4 Hook 时,考虑池子和 Hook 之间的关系至关重要。默认情况下,当在 PoolManager 中创建一个池子时,任何符合条件的 Hook 合约都可以在未经 Hook 同意的情况下添加到 PoolKey
中。这意味着池子总是可以通过检查 PoolKey
来确定它正在调用哪个 Hook,但是 Hook 本身并不知道哪个池子正在调用它,除非明确地设计为跟踪该信息。
仅限单池
如果你的 Hook 是为单个池子设计的,请确保实施一种机制,以防止其他池子调用它。实现此目的的一种简单方法是允许仅通过 afterInitialize
回调来初始化 Hook 一次。
非显式多池支持
如果你的 Hook 没有明确限制为单个池子,则默认情况下可以被多个池子使用。在这种情况下,你必须考虑对 Hook 状态的影响。例如,如果 Hook 的状态通过池子回调进行修改:
为了确保正确的核算,每个池子都应该在 Hook 中有自己独立的存储空间。
非显式多池支持也会影响你的回调函数中的逻辑。如果你的 Hook 从 PoolManager 获取金额,你需要说明这些金额来自哪个池子。确保你的回调函数能够正确处理这些情况。
如果你的 Hook 发起对 PoolManager 的调用(即,它不只是响应回调),你需要仔细管理 unlockCallback
函数以及可能随之而来的任何回调的实现。
关于 unlockCallback
数据
任何调用 PoolManager 的合约都需要先解锁它,这需要你的 Hook 实现 unlockCallback
函数并创建适当的 calldata。此 calldata 可用于调用 Hook 上的任何函数,因此你需要限制构造此 calldata 的能力。确保用户无法构造任意的 unlockCallback
数据,因为它可能会暴露意外的函数。特别是在使用 Assembly 代码时,务必注意编码和解码回调数据。
跳过的回调
如果调用者是 Hook 本身,则有权限的回调函数将不会触发。但是,如果任何其他调用者与 PoolManager 交互,则 Hook 的有权限的回调将触发。确保你的回调逻辑考虑了 Hook 是调用者的情况。
modifyLiquidity
吗?如果你的 Hook 调用 PoolManager.modifyLiquidity
函数,它将拥有它管理的流动性。你需要考虑 Hook 如何管理所有权以及从它代表用户拥有的流动性中获得的费用。Uniswap/v4-periphery 存储库可以作为一个有用的参考。具体来说,请确保你注意以下事项:
modifyLiquidity
返回值的目的和定义。管理累积的费用至关重要,特别是对于通过创建和管理范围外流动性仓位来模拟复杂金融工具(如限价单)的 Hook。跟踪、归属和分配这些费用的逻辑必须是完美的,以防止费用被误导或无法访问的情况。还必须注意的是,流动性仓位上的费用累积可以由任何人随时触发,这意味着及时的流动性修改可能会与 Hook 的自定义费用累积逻辑发生冲突。
此外,如果你的 Hook 实施经济激励或惩罚,以阻止可能不利于长期提供者的瞬时流动性策略(Just-In-Time 流动性攻击),则此机制的完整性至关重要。触发惩罚的条件和计算逻辑必须能够抵御复杂的绕过尝试。在这种情况下,应注意确认费用如何在各个仓位之间分配,以及如何跨越和管理 ticks。
铸造份额的 Hook
如果你的 Hook 管理 PoolManager 中的仓位,你可能需要为你的用户铸造份额。这就需要区分 Uniswap v4 流动性和 Hook 铸造的份额。
命名约定是这里的关键。为了避免混淆,请将术语“流动性”专门用于 Uniswap v4 “流动性”,并将任何 Hook 发布的份额代币称为“份额”。请注意用户输入(amount
)如何转换为 Uniswap v4 liquidity
,然后再转换为 Hook 发布的 shares
。这三个术语具有不同的单位,并且可能会引入舍入问题。
在设计你的 Hook 时,重要的是要考虑 Swap 逻辑中的对称性。由于可以通过多种方式执行 Swap,你必须确保你的 Hook 可以触发所有情况下的 Swap 逻辑。
如果你的 Hook 更改了 Swap 的返回 delta(例如,调整费用、奖励或自定义计算),它可能会修改 specifiedAmount
(由用户设置)或 unspecifiedAmount
(基于 specifiedAmount
计算)。根据它是否为 exact-output
或 exact-input
相对于 zeroForOne
布尔值指定,这两个金额可以是正数或负数。Swap 逻辑中的对称性对于处理所有情况都是必要的。
通过 PoolManager 的实现,specifiedAmount
只能在 beforeSwap
Hook 中更改,而 unspecifiedAmount
只能在 afterSwap
Hook 中更改。 你需要确保你的 Hook 可以处理 before 和 after Swap Hook 以保持对称性。
该逻辑还应考虑 exact-output 与 exact-input Swap 规范。请务必检查 specifiedAmount
的符号以处理这两种情况。
自定义的 Swap 逻辑
如果你的 Hook 引入了自定义的 Swap 逻辑,这可能涉及根据 specifiedAmount
和其他池子或 Hook 状态计算 unspecifiedAmount
。像这样的自定义逻辑会引入价格操纵的风险,特别是如果返回的金额取决于可能被操纵的底层余额或可能被利用的舍入误差。因此,应仔细审查自定义的 Swap 逻辑,以避免产生意想不到的后果。
对于引入自定义 Swap 逻辑的 Hook,例如那些旨在通过基于参考价格(更难操纵的价格,例如区块开始时的价格)调整 Swap 输出以减轻抢跑或三明治攻击的 Hook,在处理指定和未指定的 Swap 金额之间的区别时,需要格外注意细节。 确定何时以及如何进行调整的条件逻辑中的缺陷,可能会使保护措施无效,或者更糟的是,引入新的利用途径。
如果你的 Hook 旨在支持原生代币池,它应该能够:
msg.value
并将任何多余的 msg.value
返回给他们。与原生代币交互会带来 PoolManager 或 Hook 本身重入的风险。这可能会无意中改变池子或 Hook 的状态,特别是如果自定义核算逻辑依赖于底层代币余额。请注意这种漏洞,因为它可能会使系统面临价格操纵的风险。
访问控制机制对于确保你的 Hook 按预期运行并且安全地防止未经授权的交互至关重要。 除了 PoolManager
的交互之外,请考虑应该允许谁或什么来调用特定函数或修改 Hook 的状态。
调用者验证和 Hook 权限
最近的 1000 万美元以上的 Cork 协议攻击是由于 Hook 函数之一中缺乏访问控制,这给开发人员敲响了警钟,需要始终仔细审查代码库中的访问控制结构。 虽然从技术上讲,任何池子都可以调用 Hook,但你的 Hook 合约可能需要验证某些操作的调用者。 例如,msg.sender
是 PoolManager
吗? 还是经过授权的池子? 或者更糟糕的是,它是否可以被恶意行为者或手动制作的合约调用?
你的 Hook 是否需要管理角色(例如,所有者)? 这个角色可能负责:
保护敏感函数
确保使用正确的可见性(public、external、internal、private)声明函数。 不需要从外部调用的函数应受到限制,以防止滥用。 此外,如问题 2 中强调的那样,传递给 unlockCallback
的数据可以调用 Hook 上的任何函数。 仔细检查此数据的形成方式以及它可以定位哪些函数,有效地充当通过 unlock 启动的操作的访问控制层。
配置和可升级性
如果 Hook 具有可配置的参数,谁有权更改它们? 变更是否立即生效,还是需要经过时间锁或治理流程?
如果你的 Hook 被设计为可升级的(例如,通过代理模式),请明确定义谁有权执行升级。 这是一项强大的功能,应受到严格控制。
仔细考虑访问控制有助于防止未经授权的状态更改、资金挪用和其他潜在的漏洞利用,从而确保 Hook 合约的完整性和可靠性。
虽然已经知道 Hook 的一些潜在用例,但随着时间的推移可能会出现许多新颖的设计。 同样,过去关于代码在 Uniswap v3 中的行为方式的假设在特定的 Hook 逻辑下可能不再有效。 新颖的设计可能会带来新颖的攻击途径,因此,应特别注意确保即使添加了自订逻辑,理论上的协议激励系统仍然成立。
我们希望这 5 个问题可以帮助你形成一个关于 Hook 设计的基本框架。 这些只是一个开始,绝非全面。 在实施过程中,每个单独的 Hook 都可能需要考虑功能、安全性和效率方面的进一步特定权衡。 随着 Uniswap v4 周围生态系统的发展,将会出现新的最佳实践和策略。 与此同时,继续迭代、测试和完善你的 Hook!
- 原文链接: blog.openzeppelin.com/6-...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!