本文对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 对 afterInitialize
和 beforeSwap
回调具有权限。如果池未设置为动态费用,则初始化将被回滚。在每次交换之前,提供一个用于该交换的替代费用值,作为 LPFee。继承合约在其 _getFee
函数中指定新的费用金额。
DynamicAfterFee
此 hook 具有 afterSwap
和 afterSwapReturnDelta
权限。每次确切输入交换后,以输出代币为单位的费用将捐赠给池。捐赠金额为输出金额与预定目标金额之间的差额。继承合约需要在每次交换之前设置每个支持池的目标金额,因为此 hook 在每次交换时会重置目标金额。
BaseAsyncSwap
此 hook 由 BaseNoOp
重命名,具有 beforeSwap
和 beforeSwapReturnDelta
权限。在每次确切输入交换之前,hook 会取走整个指定金额,并完全跳过底层 Uniswap V4 池中的交换逻辑,包括收取 LPFee 和协议费用。
BaseCustomAccounting
此 hook 预期仅与一个池配合使用。因此,它具有 beforeInitialize
回调的权限,在回调中将 poolKey
状态变量初始化为池的地址。此 hook 旨在拥有底层池中的全部流动性,仅可通过此 hook 进行流动性修改。为了实现这一点,hook 指定 beforeAddLiquidity
和 beforeRemoveLiquidity
回调的权限,这些回调在其他任何人(除 hook 本身外)尝试通过 PoolManager
合约修改流动性时会回滚。
用户只能通过 hook 直接提供流动性,调用 addLiquidity
或 removeLiquidity
函数。继承合约需要重写 _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 拥有 beforeSwap
和 beforeSwapReturnDelta
权限,允许 hook 在每次交换时将指定金额归零,并通过虚拟 _getAmount
函数返回一个自定义不确定金额,从而在交换上实现自定义曲线的效果。交换是使用 hook 合约拥有的领取代币进行结算的。
OpenZeppelin Uniswap Hooks 库主要由不打算单独部署的抽象合约组成。因此,安全性的负担最终落在继承合约上,以对敏感函数添加访问控制,限制外部无权限函数,并在重写函数中实施正确的逻辑。
继承合约应注意一些一般安全考虑。
带有 hook 地址的任何池可以在未获得 hook 同意的情况下使用,除非故意拒绝。如果 hook 意图仅与单一池交互,考虑明确限制其他池使用该 hook。如果 hook 意图支持多个池,则需特别小心以避免从多个允许池覆盖池状态。这将要求继承合约为相关 hook 状态分隔 poolId
空间。
当调用者为 hook 自身时,hooks 库会跳过一些权限的 hook 回调。例如,如果一个 hook 发起对 PoolManager.modifyLiquidity
函数的调用,将跳过 beforeAddLiquidity
和 beforeRemoveLiquidity
hook 回调。如果一个 hook 发起一笔交换,则将跳过 beforeSwap
和 beforeSwapReturnsDelta
回调。因此,如果继承合约打算允许发起调用到 PoolManager
,可能会跳过其某些权限回调。然而,对于其他用户直接调用 PoolManager
的情况下,其权限回调仍然有效。
unlock
Calldata打算与 poolManager
交互的 hooks 包含一个用户自定义输入的 unlockCallback
函数。该函数将在 hook 合约上由 poolManager
调用。根据解锁函数的 calldata,这可以用于调用 hook 上的任何函数。因此,任何继承合约都应限制其用户能够构建 unlock
calldata,以免暴露 hook 上的不必要函数。
当 hook 尝试修改池中的头寸流动性时,返回的增量包含来自该头寸累积的费用。这会影响在添加流动性时通常假设的符号。因此,处理返回增量的任何逻辑应考虑所有可能返回的值。此外,由于池的当前 tick 可能在流动性操作之前不利地更新,考虑在这两种情况下也具有滑点保护。如果 hook 支持本币池,则应能够接受和结算本币。
当 hook 管理具有份额的代币时,特别重要的是区分 Uniswap 流动性与 hook 份额。考虑保留“流动性”一词仅指 Uniswap 流动性,并使用其他术语(例如“份额”)来指代任何 hook 所有的份额代币。如果 hook 合约拥有流动性,则应存在允许以访问控制或正确会计方式移除锁定代币的函数。
支持动态费用的 hooks 必须实现一个函数,允许设置新费用用于交换。如果此函数依赖于某些可能暂时被操控的外部因素(例如,流动性或池中的代币数量),则应具有访问控制,以阻止恶意操控费用。
由于 Uniswap V4 支持带有本币的池,这引入了重入的可能性,无论是到 PoolManager
合约还是到 hook 自身。在设计可能依赖底层代币余额的任何自定义交换逻辑时,应考虑这些场景,以避免潜在的价格操控。
DynamicAfterFee
hook 将整个交换后费用捐赠给底层池。由于捐赠适用于包括当前 tick 在内的所有头寸,这使得套利交易者可以在交换之前提供临时流动性,稍微围绕交换后的最终价格 tick。因此,他们将从捐赠中获益,尽管他们提供的流动性几乎没有参与(或根本没有参与)的交换。在从 DynamicAfterFee
hook 继承时,应考虑这一点。
继承自 BaseCustomAccounting
hook 的合约需要正确实施负责计算份额和流动性修改参数的函数,例如 _getRemoveLiquidity
和 _getAddLiquidity
。这些函数应考虑底层池中代币数量可能会随时间变化,这可能会引入舍入误差。
BaseAsyncSwap
和 DynamicAfterFee
hooks 只在确切输入交换中应用特殊逻辑。在确切输出交换时,它们根据 Uniswap V4 逻辑执行常规交换。这可能会改变底层池中的价格和代币数量。如果这不是期望的行为,继承合约还应将自定义逻辑应用于确切输出交换。
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 中解决。
Uniswap V4 池支持本币,例如 ETH,无需将其包装。 CurrencySettler
库 也支持在其 settle
函数 内进行本币转移,该函数在需要向 Uniswap V4 的 PoolManager
合约转移代币时由 hooks 调用。在本币转移的情况下,指定数量的本币 被转移 ,使用 payable
的 PoolManager.settle
函数。
然而,没有任何 hook 合约能够接收本币。这意味着 hook 合约的本币余额始终为 0。因此,包含 ETH 作为其中一个代币的池将不被支持。例如,将无法通过 BaseCustomAccounting
hook 向池提供流动性。
考虑允许 hooks 接收正确数量的本币,以便可以用于对 PoolManager
合约的 settle
调用。
更新: 已在 pull request #32 和提交 4be1870 中解决。
BaseCustomAccounting
hook 要求用户仅通过这个 hook 修改相关的 UniswapV4 池流动性。可以使用无权限的外部 addLiquidity
和 removeLiquidity
函数与 UniswapV4 PoolManager
的 modifyLiquidity
函数进行交互。这两个函数的滑点检查不足,因为这种改变流动性的函数可以被前向运行的交换所打断,这可能会改变池的当前 tick,影响预定流动性操作所需或返回的 currency0
和 currency1
的数量。
特别的是,当移除流动性时没有滑点检查。此外,虽然在添加流动性时有滑点检查,但它也包含来自现有头寸的累积费用,因为 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
。_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
不一定对 amount0
和 amount1
都是负值。根据累积的费用量以及待铸造的流动性,有可能使 amount0
和 amount1
为正数或负数。因此,当其中一个数量为正时,这种强制转换 将导致错误的值。可能出现两种情况:
uint256(int256(-amount))
可能会很大,当 amount
为小的正值时,转移该大金额时可能会导致回滚。考虑添加额外逻辑以处理这一合法场景,而不是假设。这可以通过直接检查返回增量的符号来实现,当它为正时调用 take
,当它为负时调用 settle
。
更新: 已在 pull request #33 和提交 6d1e94b 中解决。
BaseDynamicFee
Hook 可以被任意强制触发BaseDynamicFee
hook 合约 具有 virtual
和 external
的 poke
函数,允许任何池在任何时间(重新)设置更新的 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
函数的实现,以重用 addLiquidity
和 removeLiquidity
函数中的逻辑。
被覆盖的 _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
函数中的 take
和 settle
操作可以更详细地解释,使它们执行的操作更易于理解。BaseHook
合同的 unlockCallback
函数 没有提供返回值的文档。此外,像 BaseCustomCurve
合同的 _getAmountOutFromExactInput
函数 等多个 internal
函数未记录其参数或返回值。BaseAsyncSwap
和 DynamicAfterFee
Hook的预期使用案例在它们的文档中未被纳入,实施这些Hook的合约需要定义附加函数。考虑记录这些合同的预期用例,可能提供一个可触碰的示例,说明它们可能如何使用。此外,考虑提供需要继承合同实现的函数的示例实现。考虑解决上述所有文档不足实例,以提高代码库的清晰性和可维护性。
更新: 在 pull request #35 的提交 315e0e4 中得到解决。
由于库中的所有Hook均为通用模板Hook,因此一些Hook中的 virtual
函数,设计为可以被继承合同重写,可以受益于更多的参数。例如:
BaseDynamicFee
Hook 中,_getFee
virtual
函数 可以具有 currentTick
和 sqrtPricex96
参数。PoolManager.modifyLiquidity
调用 的 feesAccrued
增量可以由 _modifyLiquidity
函数返回,并传递给 _mint
和 _burn
virtual
函数。BaseCustomAccounting
Hook 中,可以向 AddLiquidityParams
结构 添加一个 salt
值,以便可以将其传递给 _getAddLiquidity
函数,以允许用户在同一 tick 范围内拥有多个位置。类似地,可以在 RemoveLiquidityParams
结构 中添加成员 salt
。考虑实现上述建议的更改,以为Hook提供更广泛和更通用的功能。
更新: 在 pull request #35 的提交 4be1870 中部分解决。未实现第一个建议,因为 currentTick
和 sqrtPricex96
值可以从池中获取。
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 Wizard 和 ethereum-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
,以更好地反映它指代一个抽象合同,并与其他抽象合同的名称保持一致。liquidity
的变量 可以重命名为 shares
,并更新相关注释,以便与 Uniswap V4 流动性区分开。_getAmount
函数的 amountIn
参数 指的是指定代币的数量,该代币不一定必须是输入代币。实际上,在 _getAmountInForExactOutput
函数 中,相同的参数被称为 amountOut
。考虑将 _getAmount
函数的 amountIn
参数重命名为 amountSpecified
。考虑实施上述命名建议以提高代码库的可读性和清晰性。
更新: 在 pull request #36 的提交 9cef475 中得到解决。
在整个代码库中,发现多个在未发出事件的情况下更新状态的函数示例:
BaseCustomAccounting.sol
中的 _beforeInitialize
函数。BaseHook.sol
中的 constructor
函数。考虑在发生状态更改时发出事件,以改善代码库的可读性并减少出错的可能性。
更新: 已确认,未解决。团队表示:
这将不在此代码库中修复,以允许实施者在必要时添加自己的事件。
_getAmount
virtual
函数的不必要限制BaseCustomCurve
Hook的 _getAmount
函数 包含计算交换金额的全部逻辑,应该同时包括费用机制、输入金额、代币计算、交换方向等逻辑。因此,该函数可能在其一般形式下相当复杂。然而,参数的影响和实施该 virtual
函数时的内部限制可能会导致不必要的困惑。
_getAmount
函数的参数不简练。如果指定了 input
和 output
货币,则无需指定 zeroForOne
。事实上,实际上根本没有必要指定 input
和 output
货币,因为该合约仅通过继承 BaseCustomAccounting
来工作于一个 Uniswap V4 池。因此,仅需要有 zeroForOne
参数来确定交换方向。直接传入所有原始交换参数将足够。此外,通过基于 exactInput
将函数拆分以实施约束是没有必要的。作为一个复杂的函数,继承合同可能无论如何需要根据他它适合实现的方式进行拆分。
考虑直接将原始 IPoolManager.SwapParams
传递给 _getAmount
函数,并去除不必要的限制(即,_getAmountOutFromExactInput
和 _getAmountInForExactOutput
的 virtual
函数)。同时考虑记录费用机制的必要性,因为交换需要在Hook内部发生。
更新: 在 pull request #36 的提交 31a8430 中已解决。
审核的代码库提供了可被继承以实现 UniswapV4 池定制Hook的基础合同。因此,继承合约实现所有所需的 virtual
函数是至关重要的。该报告的介绍部分已经包含了准则和潜在的风险领域。
该代码库可以通过进一步开发和额外文档以改善当前及潜在Hook原型的实用性、清晰性和安全性而受益。特别是,可以实现更全面的测试套件,以测试Hook和 PoolManager 之间的更广泛交互。此外,还可以扩展 BaseAsyncSwap
和 DynamicAfterFee
Hook合同,增加额外的代码,负责处理所有用例,例如指定精确输出量的交换,以提供完整的基础功能给继承合同。最后,能够提供实际的Hook实现示例,利用基础Hook合同以展示它们的潜在用例,以及它们的虚拟函数如何以安全的方式被重写。
- 原文链接: blog.openzeppelin.com/un...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!