Uniswap Hook库里程碑 1 审计

本文对OpenZeppelin的Uniswap Hooks库进行了审计,涵盖了各类Hook的安全性及逻辑实现,详细列出关键问题,包括安全漏洞、逻辑缺陷和潜在改进意见。审计指出必须采用必要的访问控制与文档化措施来确保合约的安全性,并提供了关于如何改进合约实施的建议。

目录

摘要

TypeDeFi时间线从 2025-01-07 到 2025-01-17 语言 Solidity 总问题数 22 (16 解决, 2 部分解决) 关键严重性问题 1 (1 解决) 高严重性问题 2 (2 解决) 中严重性问题 6 (5 解决, 1 部分解决) 低严重性问题 4 (3 解决, 1 部分解决) 备注与附加信息 9 (5 解决)

范围

我们审计了 OpenZeppelin/uniswap-hooks 仓库,提交号为 1db9646

范围内的文件如下:

 src/
├── base/
│   ├── BaseCustomAccounting.sol
│   ├── BaseCustomCurve.sol
│   ├── BaseHook.sol
│   └── BaseAsyncSwap.sol (从 BaseNoOp.sol 重命名)
├── fee/
│   ├── BaseDynamicFee.sol
│   ├── BaseOverrideFee.sol
│   └── DynamicAfterFee.sol
└── lib/
    └── CurrencySettler.sol

系统概述

OpenZeppelin Uniswap Hooks 库提供了多种抽象合约,可以被继承并作为基础实现 hooks,为在一个或多个 Uniswap V4 流动性池中执行特定操作提供结构化的基础。

BaseHook

此 hook 包含了整个 hook 接口,仅可由 PoolManager 合约调用。此外,它要求继承合约在 getHookPermissions() 函数中指定允许的 hook 调用,并重写相应的内部 hook 调用函数。已部署地址的最后 14 位需为每个授予的 hook 权限设置正确的标志。以下所有 hook 都继承自此基础 hook。

BaseDynamicFee

此 hook 仅包含对 afterInitialize 回调的单一权限,以根据继承合约指定的值更新基础池中的动态 LPFee。继承合约需要重写 _getFee 函数,以返回每个支持池的费用。此外,任何人都可以通过此 hook 合约对已初始化的池进行触发,将其动态 LPFee 更新为 _getFee 返回的值。

BaseOverrideFee

此 hook 对 afterInitializebeforeSwap 回调具有权限。如果池未设置为动态费用,则初始化将被回滚。在每次交换之前,提供一个用于该交换的替代费用值,作为 LPFee。继承合约在其 _getFee 函数中指定新的费用金额。

DynamicAfterFee

此 hook 具有 afterSwapafterSwapReturnDelta 权限。每次确切输入交换后,以输出代币为单位的费用将捐赠给池。捐赠金额为输出金额与预定目标金额之间的差额。继承合约需要在每次交换之前设置每个支持池的目标金额,因为此 hook 在每次交换时会重置目标金额。

BaseAsyncSwap

此 hook 由 BaseNoOp 重命名,具有 beforeSwapbeforeSwapReturnDelta 权限。在每次确切输入交换之前,hook 会取走整个指定金额,并完全跳过底层 Uniswap V4 池中的交换逻辑,包括收取 LPFee 和协议费用。

BaseCustomAccounting

此 hook 预期仅与一个池配合使用。因此,它具有 beforeInitialize 回调的权限,在回调中将 poolKey 状态变量初始化为池的地址。此 hook 旨在拥有底层池中的全部流动性,仅可通过此 hook 进行流动性修改。为了实现这一点,hook 指定 beforeAddLiquiditybeforeRemoveLiquidity 回调的权限,这些回调在其他任何人(除 hook 本身外)尝试通过 PoolManager 合约修改流动性时会回滚。

用户只能通过 hook 直接提供流动性,调用 addLiquidityremoveLiquidity 函数。继承合约需要重写 _getAddLiquidity_getRemoveLiquidity 函数,以允许自定义计算逻辑将输入金额转换为流动性。继承合约还需要重写 _mint_burn 函数,以允许自定义逻辑将 Uniswap 的金额/流动性转换为 hook 用户的 hook 份额。

BaseCustomCurve

此 hook 继承自 BaseCustomAccounting hook,但重写了一些已经实现的内部函数,如 _modifyLiquidity_unlockCallback。在此 hook 中,当添加流动性时,hook 会调用 PoolManager 合约的 mint 函数,将调用者提供的 ERC-20 代币(由虚拟 _getAmountIn 函数指定)转换为底层 ERC-6909 领取代币,然后为调用者铸造份额。类似地,当流动性被移除时,它调用 PoolManager.burn 函数,燃焼它持有的自定义数量(由虚拟 _getAmountOut 函数指定)的 ERC-6909 代币,之后将底层的 ERC-20 代币从池提取给调用者,并最终燃烧调用者的份额。

继承合约需要在 _mint_burn 函数中放置自定义计算和访问控制,以实现份额转换。此外,此 hook 拥有 beforeSwapbeforeSwapReturnDelta 权限,允许 hook 在每次交换时将指定金额归零,并通过虚拟 _getAmount 函数返回一个自定义不确定金额,从而在交换上实现自定义曲线的效果。交换是使用 hook 合约拥有的领取代币进行结算的。

安全模型和信任假设

OpenZeppelin Uniswap Hooks 库主要由不打算单独部署的抽象合约组成。因此,安全性的负担最终落在继承合约上,以对敏感函数添加访问控制,限制外部无权限函数,并在重写函数中实施正确的逻辑。

集成指南

继承合约应注意一些一般安全考虑。

单池与多池

带有 hook 地址的任何池可以在未获得 hook 同意的情况下使用,除非故意拒绝。如果 hook 意图仅与单一池交互,考虑明确限制其他池使用该 hook。如果 hook 意图支持多个池,则需特别小心以避免从多个允许池覆盖池状态。这将要求继承合约为相关 hook 状态分隔 poolId 空间。

跳过的回调

当调用者为 hook 自身时,hooks 库会跳过一些权限的 hook 回调。例如,如果一个 hook 发起对 PoolManager.modifyLiquidity 函数的调用,将跳过 beforeAddLiquiditybeforeRemoveLiquidity hook 回调。如果一个 hook 发起一笔交换,则将跳过 beforeSwapbeforeSwapReturnsDelta 回调。因此,如果继承合约打算允许发起调用到 PoolManager,可能会跳过其某些权限回调。然而,对于其他用户直接调用 PoolManager 的情况下,其权限回调仍然有效。

构建 unlock Calldata

打算与 poolManager 交互的 hooks 包含一个用户自定义输入的 unlockCallback 函数。该函数将在 hook 合约上由 poolManager 调用。根据解锁函数的 calldata,这可以用于调用 hook 上的任何函数。因此,任何继承合约都应限制其用户能够构建 unlock calldata,以免暴露 hook 上的不必要函数。

Hooks 修改流动性

当 hook 尝试修改池中的头寸流动性时,返回的增量包含来自该头寸累积的费用。这会影响在添加流动性时通常假设的符号。因此,处理返回增量的任何逻辑应考虑所有可能返回的值。此外,由于池的当前 tick 可能在流动性操作之前不利地更新,考虑在这两种情况下也具有滑点保护。如果 hook 支持本币池,则应能够接受和结算本币。

Hooks 管理流动性

当 hook 管理具有份额的代币时,特别重要的是区分 Uniswap 流动性与 hook 份额。考虑保留“流动性”一词仅指 Uniswap 流动性,并使用其他术语(例如“份额”)来指代任何 hook 所有的份额代币。如果 hook 合约拥有流动性,则应存在允许以访问控制或正确会计方式移除锁定代币的函数。

动态费用 Hooks

支持动态费用的 hooks 必须实现一个函数,允许设置新费用用于交换。如果此函数依赖于某些可能暂时被操控的外部因素(例如,流动性或池中的代币数量),则应具有访问控制,以阻止恶意操控费用。

自定义交换逻辑

由于 Uniswap V4 支持带有本币的池,这引入了重入的可能性,无论是到 PoolManager 合约还是到 hook 自身。在设计可能依赖底层代币余额的任何自定义交换逻辑时,应考虑这些场景,以避免潜在的价格操控。

特定范围内 Hook 注意事项

DynamicAfterFee hook 将整个交换后费用捐赠给底层池。由于捐赠适用于包括当前 tick 在内的所有头寸,这使得套利交易者可以在交换之前提供临时流动性,稍微围绕交换后的最终价格 tick。因此,他们将从捐赠中获益,尽管他们提供的流动性几乎没有参与(或根本没有参与)的交换。在从 DynamicAfterFee hook 继承时,应考虑这一点。

继承自 BaseCustomAccounting hook 的合约需要正确实施负责计算份额和流动性修改参数的函数,例如 _getRemoveLiquidity_getAddLiquidity。这些函数应考虑底层池中代币数量可能会随时间变化,这可能会引入舍入误差。

BaseAsyncSwapDynamicAfterFee hooks 只在确切输入交换中应用特殊逻辑。在确切输出交换时,它们根据 Uniswap V4 逻辑执行常规交换。这可能会改变底层池中的价格和代币数量。如果这不是期望的行为,继承合约还应将自定义逻辑应用于确切输出交换。

严重性 - 关键

无显式多池支持允许覆盖 Hook 状态

BaseCustomAccounting hook 应仅支持由 poolKey hook 状态变量定义的单个 UniswapV4 池。poolKey_beforeInitialize hook 函数中初始化,这是唯一可以设置 poolKey 变量的地方。

然而,poolKey 变量可以在将此 hook 合约实例注册到另一个池时被覆盖。由于池与 hook 之间的基本不对称性,任何人都可以使用任何有效 hook 地址与任何池交互,除非 hook 明确拒绝。因此,用户通过 hook 提供的初始池中的所有流动性将被锁定,因为 BaseCustomAccounting 合约将仅在新的注册池上操作,该池未来再次可以被覆盖。

_beforeInitialize 函数中,考虑确保只能注册一个池,并对任何后续调用回滚,以防 hook 仅打算支持一个池。此外,对于每个 hook,考虑清晰地记录底层无显式多池支持,即允许将 hook 注册到多个池。这可以帮助继承合约小心避免来自多个关联池的 hook 状态意外被覆盖。

更新: 已在 pull request #31 和提交 388fcfb 中解决。

严重性 - 高

Hooks 不支持本币

Uniswap V4 池支持本币,例如 ETH,无需将其包装。 CurrencySettler 也支持在其 settle 函数 内进行本币转移,该函数在需要向 Uniswap V4 的 PoolManager 合约转移代币时由 hooks 调用。在本币转移的情况下,指定数量的本币 被转移 ,使用 payablePoolManager.settle 函数。

然而,没有任何 hook 合约能够接收本币。这意味着 hook 合约的本币余额始终为 0。因此,包含 ETH 作为其中一个代币的池将不被支持。例如,将无法通过 BaseCustomAccounting hook 向池提供流动性。

考虑允许 hooks 接收正确数量的本币,以便可以用于对 PoolManager 合约的 settle 调用。

更新: 已在 pull request #32 和提交 4be1870 中解决。

滑点检查不足

BaseCustomAccounting hook 要求用户仅通过这个 hook 修改相关的 UniswapV4 池流动性。可以使用无权限的外部 addLiquidityremoveLiquidity 函数与 UniswapV4 PoolManagermodifyLiquidity 函数进行交互。这两个函数的滑点检查不足,因为这种改变流动性的函数可以被前向运行的交换所打断,这可能会改变池的当前 tick,影响预定流动性操作所需或返回的 currency0currency1 的数量。

特别的是,当移除流动性时没有滑点检查。此外,虽然在添加流动性时有滑点检查,但它也包含来自现有头寸的累积费用,因为 PoolManager.modifyLiquidity 函数返回的增量 是主增量和累积的费用的。如果累积的费用大于主增量的绝对值,则得到的增量可能是正值。因此,将负值的转换不安全,有可能完全绕过滑点检查。

考虑在添加或移除流动性时仅使用主增量进行滑点检查。

更新: 已在 pull request #33 和提交 6887feb 中解决。

严重性 - 中

误导性评论

在整个代码库中,发现了多处误导性评论:

  • 这种评论 及这评论 提示 _getFee 函数返回的费用应以百分之一计。然而,在 Uniswap V4 中,费用以百分之一的 bip 表示,这是一个比评论中指定的单位小100倍的单位。
  • 这种评论 暗示 onlyValidPools 修改器确保使用它的函数仅能被有效的池调用,而实际上它并未验证 msg.sender。它只是验证该函数是被有效池调用的。
  • 这种评论 说明 LockFailure 错误在 hook 未解锁时抛出。然而,关于 hook 解锁的含义并不清楚,而且该错误在调用unlockCallback 内失败且没有错误数据时被抛出。可以更改 LockFailure 错误的描述以更好地反映此用例。
  • 这种评论 暗示在 _mint 函数中,将流动性单位铸造给发送者。然而,传递给该函数的 params 参数中包含to 成员,用于表示流动性单位的接收者,而不是 msg.sender。该评论可以更改为说明流动性单位铸造给 params.to 地址,或者如果没有意图使用可以删除 AddLiquidityParams.to
  • 这种评论 和这评论 并未提及底层 hook 的所有权限。
  • 重写的 _getAddLiquidity 函数并未返回 编码版本的 ModifyLiquidityParams,而该评论在 _getAddLiquidity 的上方有指定。有关_getRemoveLiquidity 函数也是如此。
  • 这种评论 暗示在 BaseCustomCurve._unlockCallback 函数中从池中要添加或减少流动性。然而,过程中不涉及任何 Uniswap V4 池流动性,只是代币在 PoolManager 合约之间转移。
  • 这种评论 说明 BaseCustomCurve._modifyLiquidity 函数返回的增量源于 PoolManager。然而,该增量在与 PoolManager 没有交互的情况下确定。
  • 这种评论 暗示在确切输入交换时,amount0 被指定,而 amount1 不被指定。然而,这不一定是正确的,因为这也取决于 zeroToOne 的值。对于这个评论 也是如此。

考虑更正上述评论以提高清晰性和代码库的可维护性。

更新: 已在 pull request #34 和提交 a40dd63 中解决。

动态后交换费用可能无法按预期工作

DynamicAfterFee 合约 引入了交换后的费用,该费用基于 交换增量和在交换之前指定的目标增量进行计算。然而,用户可以通过指定所需的输出代币数量轻松避免支付费用,因为收费逻辑仅适用于确切输入交换

此外,目标增量在每次交换后重置,且该合约没有内置更新功能。如果未及时更新,可能会导致一些无意后果。例如,有可能用微量输入进行抢跑交换,从而导致随后用户交换的整个输出金额 被用作费用。当在每次交换前以透明的方式更新 targetDelta(例如,在 beforeSwap hook 中)时,有可能将交换金额设置为低于目标增量,以完全避免支付交换后的费用。

考虑对确切输出交换施加费用,可能通过 beforeSwap hook 函数。此外,考虑提供关于目标增量的全面文档,特别是关于何时以及如何更新。

更新: 已在 pull request #34 和提交 081d92a 中解决。

由于累积费用导致的不安全转换

在向 BaseCustomAccounting hook 添加或移除流动性时,hook 会调用 PoolManager.modifyLiquidity 函数。返回的增量是主增量feesAccrued 之和。因此,在向现有头寸添加流动性时,返回的 delta 不一定对 amount0amount1 都是负值。根据累积的费用量以及待铸造的流动性,有可能使 amount0amount1 为正数或负数。因此,当其中一个数量为正时,这种强制转换 将导致错误的值。可能出现两种情况:

  • 错误强制转换的 uint256(int256(-amount)) 可能会很大,当 amount 为小的正值时,转移该大金额时可能会导致回滚。
  • 当累积费用足够大(对于大量流动性)且错误强制转换的值可以转移时,这将导致用户支付超过的费用,而不是收回相应的费用。

考虑添加额外逻辑以处理这一合法场景,而不是假设。这可以通过直接检查返回增量的符号来实现,当它为正时调用 take,当它为负时调用 settle

更新: 已在 pull request #33 和提交 6d1e94b 中解决。

BaseDynamicFee Hook 可以被任意强制触发

BaseDynamicFee hook 合约 具有 virtualexternalpoke 函数,允许任何池在任何时间(重新)设置更新的 LPFee。更新的 LPFee 来自 _getFee(poolKey) 虚拟函数,可以根据当前池状态或其他外部条件返回任何费用。然而,如果 _getFee 实现依赖于某些外部因素,这将有可能操控 LPFee 并在单个事务中进行交换。例如,假设 V4 池的 _getFee 依赖于同一Token对的 Uniswap V3 池的Token余额。同时,V4 池返回的费用与相应的 V3 池的Token余额成反比。也就是说,当 V3 池拥有大量余额时,_getFee 返回较低的值,反之则返回较高的值。现在,攻击者可以通过闪电贷,暂时存入 V3 池,然后调用 V4 池的 poke。由于 V3 池的余额将增加,交换费用将降低,而攻击者可以在后续交换中支付更少的费用。如果后续用户没有提前 poke,他们可以享受低廉的费用,而无需进行任何操控。

考虑对 poke 函数施加访问控制,以便无法被任意调用。否则,可以将 poke 函数设为 internal,允许继承契约决定如何将其与自身逻辑适当地结合在一起。或者,考虑更新 poke 函数的文档,以便上述风险对继承的合同是清晰的。

更新:pull request #34 的提交 7e81272 中解决了该问题。

BaseAsyncSwap 操作不明确

BaseAsyncSwap Hook通过 minting 相应的 ERC-6909 索赔代币到自身,跳过了 Uniswap 池中的任何交换操作。然而,这仅适用于精确输入交换。没有文档描述这个Hook中该数量如何分发、使用、提取或退款给用户。考虑增加更多的文档,或者指明 virtual 函数的覆盖意图。

此外,BaseAsyncSwap Hook可以在每次精确输入交换时 接收 来自多个池的 ERC-6909 代币。然而,它没有实现任何机制来区分来自不同池和不同用户的相同代币。例如,可以记录代币的数量、池的地址和发起交换的用户的地址,以便能够执行与持有的代币相关的后续逻辑。

此外,BaseAsyncSwap Hook可以通过指定交换中的输出金额来跳过。因此,要实现精确输出交换的相应逻辑,beforeSwapDelta 回调需要返回一种负值以将输出金额归零。在这种情况下,Hook必须解决相关金额。这可能需要一些额外的机制来正确处理涉及代币的流入和流出。

考虑为 BaseAsyncSwap Hook提供更清晰的实用程序,以便继承合约可以清楚地识别将其自身逻辑纳入的用例。

更新:pull request #34 的提交 1605117 中部分解决。文档已扩展,但发起交换的用户地址仍未被记录,并且该Hook仍仅在精确输入交换中工作。计划在未来修改该合同,以解决这些问题。

直观性不足的返回增量

BaseCustomCurve Hook允许用户为特定池添加和移除流动性,但仅通过Hook本身。它还仅允许使用Hook中的可用流动性进行交换。该Hook 继承 BaseCustomAccounting 并覆盖其 _modifyLiquidity_unlockCallback 函数的实现,以重用 addLiquidityremoveLiquidity 函数中的逻辑。

被覆盖的 _unlockCallback 函数 总是 返回两个负值,无论是从 addLiquidity 还是 removeLiquidity 调用。此行为不直观,可能导致在调用待实现的 _mint_burn 函数时出错。实际上,_mint 函数 期望返回的增量与 poolManager.modifyLiquidity 具有相同的符号(即,如果不考虑费用,则为负值),而_burn函数期望返回的增量为正值。

考虑在移除流动性时返回 正值,以与继承合同所期望的符号保持一致。这样,任何继承合同都可以以更直观的方式实现 _mint_burn 函数。

更新:pull request #36 的提交 45edf24 中已解决。

低严重性

冗余的 virtual 函数修饰符

BaseHook 合同的 validateHookAddress 函数 确保Hook具有预期的权限。该函数基于 Uniswap的 v4-periphery 仓库中的 validateHookAddress 函数,具有 virtual 修饰符。这允许继承自 BaseHook 的合同覆盖该函数。然而,从 v4-periphery 仓库对 validateHookAddress 函数引入的 virtual 函数修饰符 为了方便测试,否则并非必要。

考虑从 validateHookAddress 函数中删除 virtual 修饰符,以提高代码库的可读性,并避免函数被错误覆盖的情况。

更新:pull request #35 的提交 1c1f186 中得到解决。

文档不足

在代码库中,发现多个文档不足的实例:

  • BaseCustomAccounting 合同的 _getAddLiquidity_getRemoveLiquidity 函数 返回的值会 作为参数传递到 PoolManager.modifyLiquidity 函数。为了防止用户能够提取不同用户指定的流动性,这些参数(特别是 ModifyLiquidityParams 对象)应包含每个流动性提供者独特设置的盐。否则,当流动性被移除时,任何人都可以指定要删除的流动性和 tick 范围,只要他们在Hook中拥有足够的流动性单位,他们就可以移除其他人提供的流动性,可能会盗取累积的费用。可以在 _getAddLiquidity_getRemoveLiquidity 函数的文档中记录这一事实。
  • DynamicAfterFee 合同 允许施加额外费用,该费用将在交易后计入。该费用基于目标代币和实际代币的增量值,并且会 捐赠 给基础池。每当向池进行捐赠时,所有在范围内的流动性提供者都有受益。攻击者可以利用这一点,攻击者可以在交换后刚好在最后的 tick 周围提供流动性。这样的流动性在交换中几乎没有参与(或根本没有参与),但攻击者将获得大量的捐赠。考虑记录这种可能性,或许可以采用类似于 Uniswap V4 中的方式 的方式。
  • BaseCustomCurve 合同将受益于额外的文档。特别是 _beforeSwap_unlockCallback 函数中的 takesettle 操作可以更详细地解释,使它们执行的操作更易于理解。
  • BaseHook 合同的 unlockCallback 函数 没有提供返回值的文档。此外,像 BaseCustomCurve 合同的 _getAmountOutFromExactInput 函数 等多个 internal 函数未记录其参数或返回值。
  • BaseAsyncSwapDynamicAfterFee Hook的预期使用案例在它们的文档中未被纳入,实施这些Hook的合约需要定义附加函数。考虑记录这些合同的预期用例,可能提供一个可触碰的示例,说明它们可能如何使用。此外,考虑提供需要继承合同实现的函数的示例实现。

考虑解决上述所有文档不足实例,以提高代码库的清晰性和可维护性。

更新:pull request #35 的提交 315e0e4 中得到解决。

论据不足

由于库中的所有Hook均为通用模板Hook,因此一些Hook中的 virtual 函数,设计为可以被继承合同重写,可以受益于更多的参数。例如:

考虑实现上述建议的更改,以为Hook提供更广泛和更通用的功能。

更新:pull request #35 的提交 4be1870 中部分解决。未实现第一个建议,因为 currentTicksqrtPricex96 值可以从池中获取。

BaseHook 的 unlockCallback 不必要

最小Hook不需要 unlockCallback 函数,因为只有打算对 PoolManager 合同发起调用的Hook才需要解锁它。因此,在 BaseHook 实现中不必要有 unlockCallback 函数internal_unlockCallback 函数。

此外,_unlockCallback 函数使用了通用低级调用,并被所有范围内的合约覆盖,因为没有用于与池管理器交互的参数编码/解码工具,很难使用。此外,保留当前默认的 unlockCallback 实现可能会产生意想不到的后果,开启了继承合同重新进入Hook的可能性。

考虑从 BaseHook 合同中删除 unlockCallback 函数,而允许继承合同在需要时添加该函数。

更新:pull request #35 的提交 9ee7d09 中解决。

备注与附加信息

书写错误

在整个代码库中,发现多个书写错误的实例:

  • BaseCustomAccounting.sol第 22 行 中,"implementator" 应改为 "implementer" 而 "Aditionally" 应为 "Additionally"。
  • BaseCustomAccounting.sol第 40 行 中,"An" 应改为 "A"。
  • CurrencySettler.sol第 13 行 中,"make" 应改为 "may" 或 "may make"。

考虑纠正所有的书写错误,以提高代码库的清晰性和可读性。

更新:pull request #36 的提交 130875c 中已经解决。

不存在的命名返回变量

unlockCallback 函数 的文档提到 delta 返回参数,但在该函数中并不存在此返回参数。

考虑向 unlockCallback 函数添加名为 delta 的返回参数,以符合文档要求。

更新:pull request #35 的提交 9ee7d09 中已解决。此项通过在查找 L-04 时移除 unlockCallback 进行了处理。

可优化状态读取

BaseCustomCurve 合同中,poolKey 存储读取 可以通过缓存 poolKey 并在 memory 对象上操作来优化。

考虑减少不必要的 SLOAD 操作,并通过缓存 poolKey 变量的值来节省 gas。

更新:pull request #36 的提交 24d8c6c 中已解决。

未使用的修饰符

BaseHook 合同中定义的 onlySelf 修饰符未在代码库的任何地方使用。

为提高代码库的整体清晰度、目的性和可读性,考虑使用或删除任何未使用的修饰符。

更新: 已确认,未解决。团队表示:

这是作为助手实现的,类似于在 onlyValidPools 中的功能。

缺乏安全联系信息

在智能合约中提供特定的安全联系信息(例如电子邮件或ENS名称)可以显著简化个人在发现代码中的漏洞时的沟通过程。这种做法非常有益,因为它允许代码所有者决定漏洞披露的通信渠道,消除由于缺乏如何这样做的知识而导致的误沟通或未能报告的风险。此外,如果合约包含第三方库,而这些库中出现bug,那么其维护人员也可以更轻松地联系相关人员并提供解决方案。

在整个代码库中,没有合同包含安全联系信息。

考虑在每个合同定义上方添加包含安全联系信息的 NatSpec 注释。建议使用 @custom:security-contact 的约定,因为它已被 OpenZeppelin Wizardethereum-lists 采用。

更新: 已确认,未解决。团队表示:

实施者应添加他们自己的安全联系信息。

PoolManager 不同的 Pragma 版本

所有范围内的基础合约都使用浮动 pragma( ^0.8.24)。

考虑使用与 PoolManager 合约中相同的固定 pragma 版本,即 0.8.26。这将有助于提高一致性并避免意外的编译器行为。

更新: 已确认,未解决。团队表示:

此代码库使用 IPoolManager,其具有 ^0.8.24,与我们的 pragma 版本匹配。

命名建议

在整个代码库中,发现多个改善命名的机会:

  • CurrencySettler 库中的 manager 变量 可以重命名为 poolManager,因为该名称用于指代其他范围内合约中的 Uniswap V4 单体 PoolManager 合同。
  • DynamicAfterFee 合同名称 可以更改为 BaseDynamicAfterFee,以更好地反映它指代一个抽象合同,并与其他抽象合同的名称保持一致。
  • 与内部Hook的流动性单位相关的 命名为 liquidity 的变量 可以重命名为 shares,并更新相关注释,以便与 Uniswap V4 流动性区分开。
  • _getAmount 函数的 amountIn 参数 指的是指定代币的数量,该代币不一定必须是输入代币。实际上,在 _getAmountInForExactOutput 函数 中,相同的参数被称为 amountOut。考虑将 _getAmount 函数的 amountIn 参数重命名为 amountSpecified

考虑实施上述命名建议以提高代码库的可读性和清晰性。

更新:pull request #36 的提交 9cef475 中得到解决。

函数在未发出事件的情况下更新状态

在整个代码库中,发现多个在未发出事件的情况下更新状态的函数示例:

考虑在发生状态更改时发出事件,以改善代码库的可读性并减少出错的可能性。

更新: 已确认,未解决。团队表示:

这将不在此代码库中修复,以允许实施者在必要时添加自己的事件。

_getAmount virtual 函数的不必要限制

BaseCustomCurve Hook的 _getAmount 函数 包含计算交换金额的全部逻辑,应该同时包括费用机制、输入金额、代币计算、交换方向等逻辑。因此,该函数可能在其一般形式下相当复杂。然而,参数的影响和实施该 virtual 函数时的内部限制可能会导致不必要的困惑。

_getAmount 函数的参数不简练。如果指定了 inputoutput 货币,则无需指定 zeroForOne。事实上,实际上根本没有必要指定 inputoutput 货币,因为该合约仅通过继承 BaseCustomAccounting 来工作于一个 Uniswap V4 池。因此,仅需要有 zeroForOne 参数来确定交换方向。直接传入所有原始交换参数将足够。此外,通过基于 exactInput 将函数拆分以实施约束是没有必要的。作为一个复杂的函数,继承合同可能无论如何需要根据他它适合实现的方式进行拆分。

考虑直接将原始 IPoolManager.SwapParams 传递给 _getAmount 函数,并去除不必要的限制(即,_getAmountOutFromExactInput_getAmountInForExactOutputvirtual 函数)。同时考虑记录费用机制的必要性,因为交换需要在Hook内部发生。

更新:pull request #36 的提交 31a8430 中已解决。

结论

审核的代码库提供了可被继承以实现 UniswapV4 池定制Hook的基础合同。因此,继承合约实现所有所需的 virtual 函数是至关重要的。该报告的介绍部分已经包含了准则和潜在的风险领域。

该代码库可以通过进一步开发和额外文档以改善当前及潜在Hook原型的实用性、清晰性和安全性而受益。特别是,可以实现更全面的测试套件,以测试Hook和 PoolManager 之间的更广泛交互。此外,还可以扩展 BaseAsyncSwapDynamicAfterFee Hook合同,增加额外的代码,负责处理所有用例,例如指定精确输出量的交换,以提供完整的基础功能给继承合同。最后,能够提供实际的Hook实现示例,利用基础Hook合同以展示它们的潜在用例,以及它们的虚拟函数如何以安全的方式被重写。

请求审计

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

0 条评论

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