编写 Uniswap v4 Hook 前要问的六个问题

本文探讨了设计 Uniswap v4 Hook 时需要考虑的关键问题,包括 Hook 与 Pool 的关系、Hook 如何与 PoolManager 交互、如何处理流动性修改、Swap 的对称性、原生Token的支持以及访问控制的实现。文章旨在帮助开发者在设计 Hook 时形成基本框架,从而确保其功能、安全性和效率。

合作作者:Xaler & Bartosz Wodziński

引言

Uniswap v4 的 PoolManager 中的最新创新为定制扩展提供了巨大的潜力,这些扩展是为各个池子量身定制的。这些扩展以 hook 合约的形式出现,可以插入到流动性操作流程中,以自定义池的行为。本指南概述了设计 hook 以满足你的特定需求时的一些关键考虑因素。

问题 1:有多少个池子可以调用此 hook?

在编写 Uniswap v4 hook 时,务必考虑池子和 hook 之间的关系。默认情况下,当在 PoolManager 中创建池子时,任何符合条件的 hook 合约都可以添加到 PoolKey 中,而无需 hook 的同意。这意味着池子始终可以通过检查 PoolKey 来确定它正在调用哪个 hook,但 hook 本身并不知道哪个池子正在调用它,除非明确设计为跟踪该信息。

仅限一个池子

如果你的 hook 专为单个池子设计,请确保实施一种机制来防止其他池子调用它。实现此目的的一种简单方法是仅允许通过 afterInitialize 回调初始化 hook 一次。

非显式的多池子支持

如果你的 hook 没有明确限制为单个池子,则默认情况下可以被多个池子使用。在这种情况下,你必须考虑对 hook 状态的影响。例如,如果 hook 的状态通过池子回调进行修改:

  • 此状态是否可能被其他池子覆盖?
  • 如果从不同的池子调用 hook,状态是否会损坏?

为了确保正确的核算,每个池子都应该在 hook 中有自己独立的存储空间。

非显式的多池子支持也会影响回调函数中的逻辑。如果你的 hook 从 PoolManager 获取金额,你需要考虑这些金额来自哪个池子。确保你的回调函数正确处理这些情况。

问题 2:此 hook 是否会启动对 PoolManager 的调用?

如果你的 hook 启动对 PoolManager 的调用(即,它不仅仅是响应回调),你需要仔细管理 unlockCallback 函数以及任何可能随之而来的回调的实现。

关于 unlockCallback 数据

任何调用 PoolManager 的合约都需要先解锁它,这需要你的 hook 实现 unlockCallback 函数并创建适当的 calldata。此 calldata 可用于调用 hook 上的任何函数,因此你需要限制构造此 calldata 的能力。确保用户无法构造任意的 unlockCallback 数据,因为它可能会暴露不需要的函数。特别注意编码和解码回调数据,尤其是在使用汇编代码时。

跳过的回调

如果调用者是 hook 本身,则有权限的回调函数将不会触发。但是,如果任何其他调用者与 PoolManager 交互,则 hook 的有权限的回调将触发。确保你的回调逻辑考虑了 hook 是调用者的情况。

问题 3:此 hook 是否调用 modifyLiquidity

拥有仓位的 Hook

如果你的 hook 调用 PoolManager.modifyLiquidity 函数,它将拥有它管理的流动性。你需要考虑 hook 如何管理所有权以及从代表其用户拥有的流动性中产生的费用。Uniswap/v4-periphery 存储库可以作为一个有用的参考。具体来说,请确保你注意以下事项:

  • 返回的数据被正确地转换,尤其是由于应计费用可能导致添加流动性时的正 delta。在这方面,应特别注意理解 modifyLiquidity 返回值的目的和定义。
  • 处理 delta 的逻辑将费用 delta 与调用者 delta 分开。
  • 滑点仅适用于添加/移除流动性时的本金 delta。
  • salt 的形成是足够独特的,可以区分 hook 中的不同仓位。
  • 应计费用得到妥善管理,并根据 hook 逻辑分配给其合法所有者。

管理应计费用至关重要,尤其是对于模拟复杂的金融工具(如通过创建和管理范围外的流动性仓位来模拟限价单)的 hook。跟踪、归属和分配这些费用的逻辑必须是完美的,以防止费用被误导或无法访问的情况。还必须注意的是,流动性仓位上的费用应计可以由任何人在任何时间触发,这意味着即时流动性修改可能会与 hook 的自定义费用应计逻辑发生冲突。

此外,如果你的 hook 实施经济激励或惩罚,以阻止可能不利于长期提供者的瞬时流动性策略(即时流动性攻击),则此机制的完整性至关重要。触发惩罚的条件和计算逻辑必须能够抵御复杂的绕过尝试。在这种情况下,应注意确认费用如何在各个仓位之间分配,以及如何越过和管理 tick。

铸造份额的 Hook

如果你的 hook 管理 PoolManager 中的仓位,你可能需要为你的用户铸造份额。这引入了区分 Uniswap v4 流动性和 hook 铸造的份额的需求。

命名约定是这里的关键。为避免混淆,请专门为 Uniswap v4 "流动性" 保留术语“流动性”,并将任何 hook 发行的份额代币称为“份额”。注意用户输入 (amount) 如何转换为 Uniswap v4 liquidity,然后再转换为 hook 发行的 shares。这三个术语具有不同的单位,可能会引入舍入问题。

问题 4:swap 回调是否表现出某种对称性?

在设计你的 hook 时,重要的是要考虑 swap 逻辑中的对称性。由于 swap 可以通过多种方式执行,你必须确保你的 hook 可以触发所有情况的 swap 逻辑。

如果你的 hook 更改了 swap 的返回 delta(例如,调整费用、奖励或自定义计算),它可能会修改 specifiedAmount(由用户设置)或 unspecifiedAmount(根据 specifiedAmount 计算)。这两个金额可以是正数或负数,具体取决于相对于 zeroForOne 布尔值,它是为 exact-output 还是 exact-input 指定的。swap 逻辑中的对称性对于处理所有情况是必要的。

通过 PoolManager 的实现,specifiedAmount 只能在 beforeSwap hook 中更改,而 unspecifiedAmount 只能在 afterSwap hook 中更改。你需要确保你的 hook 可以处理 before 和 after swap hook 以保持对称性。

该逻辑还应考虑 exact-output 与 exact-input swap 规范。请务必检查 specifiedAmount 的符号以处理这两种情况。

自定义 Swapping 逻辑

如果你的 hook 引入了自定义 swap 逻辑,这可能涉及根据 specifiedAmount 和其他池或 hook 状态计算 unspecifiedAmount。像这样的自定义逻辑引入了价格操纵的风险,尤其是如果返回的金额取决于可能被操纵的底层余额或可能被利用的舍入误差。因此,应仔细审查自定义 swap 逻辑以避免意外后果。

对于引入自定义 swap 逻辑的 hook,例如那些旨在通过基于参考价格(一种更难操纵的价格,例如在区块开始时的价格)调整 swap 输出来减轻抢先交易或三明治攻击的 hook,在处理指定和未指定的 swap 金额之间的区别时,需要一丝不苟地关注细节。在确定何时以及如何进行调整的条件逻辑中的缺陷会使保护措施失效,或者更糟糕的是,引入新的利用途径。

问题 5:此 hook 是否支持原生代币?

如果你的 hook 旨在支持原生代币池,它应该能够:

  • 接受来自用户的 msg.value 并将任何多余的 msg.value 返回给他们。
  • 接收和结算与 PoolManager 的原生代币。

与原生代币交互会引入在 PoolManager 或 hook 本身存在重入风险的可能性。这可能会无意中更改池或hook状态,尤其是如果自定义会计逻辑依赖于底层代币余额。请注意此漏洞,因为它可能会使系统面临价格操纵风险。

问题 6:如何在你的 Hook 中实施访问控制?

访问控制机制对于确保你的 hook 按预期运行并且安全地防止未经授权的交互至关重要。除了 PoolManager 的交互之外,还要考虑应该允许谁或什么来调用特定函数或修改 hook 的状态。

调用者验证和 Hook 权限

最近的 1000 万美元以上的 Cork 协议攻击是由于一个 hook 函数中缺乏访问控制造成的,这给开发人员上了一课,即始终仔细审查代码库中的访问控制结构。虽然任何池子在技术上都可以调用 hook,但你的 hook 合约可能需要验证某些操作的调用者。例如,msg.senderPoolManager 吗?还是授权池?或者更糟糕的是,它可以被恶意行为者或手动制作的合约调用吗?

你的 hook 是否需要管理角色(例如,所有者)?此角色可能负责:

  • 初始化或更新关键参数。
  • 在紧急情况下暂停或取消暂停某些功能。
  • 管理 hook 收取的资金或费用(如果适用)。

保护敏感函数

确保使用正确的可见性(public、external、internal、private)声明函数。不需要从外部调用的函数应受到限制以防止滥用。此外,如问题 2 中所强调的,传递给 unlockCallback 的数据可以调用 hook 上的任何函数。仔细检查如何形成此数据以及它可以定位哪些函数,从而有效地充当通过 unlock 启动的操作的访问控制层。

配置和可升级性

如果你的 hook 具有可配置的参数,谁有权更改它们?变更是否立即生效,还是需要经过时间锁或治理流程?

如果你的 hook 被设计为可升级的(例如,通过代理模式),请明确定义谁有权执行升级。这是一种强大的功能,应严格控制。

仔细考虑访问控制有助于防止未经授权的状态更改、资金挪用和其他潜在的漏洞利用,从而确保你的 hook 合约的完整性和可靠性。

结论

虽然 hooks 的一些潜在用例是已知的,但随着时间的推移,可能会出现许多新颖的设计。因此,过去关于代码在 Uniswap v3 中如何运行的假设在特定的 hook 逻辑下可能不再有效。新颖的设计可能会带来新的攻击途径,因此,应特别注意确保即使添加了自定义逻辑,理论协议激励系统仍然成立。

我们希望这 5 个问题可以帮助你形成设计 hook 的基本框架。这些仅仅是开始,绝不是全面的。在实施过程中,每个单独的 hook 可能需要考虑功能、安全性和效率方面的更多特定权衡。随着 Uniswap v4 周围生态系统的发展,将会出现新的最佳实践和策略。与此同时,请继续迭代、测试和改进你的 hook!

了解更多

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

0 条评论

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