EVM模拟器与半抽象Nonce更新审计

本次审计主要关注Era Contracts仓库中,EVM模拟器对modexp预编译的支持、指针方式处理字节码的优化,以及系统合约中对EIP-4337半抽象nonce的实现。审计发现了包括字节位移错误和内部变量遮蔽导致的返回错误等严重问题,并提出了代码优化、文档完善和测试加强等建议。

目录

摘要

TypeL2 协议时间线 从 2025-03-20 到 2025-03-28

语言 Solidity + Yul

总问题 17 (15 个已解决, 1 个部分解决)

严重性问题

  • 关键严重性问题 1 (1 个已解决)
  • 高严重性问题 1 (1 个已解决)
  • 中等严重性问题 0 (0 个已解决)
  • 低严重性问题 4 (4 个已解决)

备注与附加信息 11 (9 个已解决, 1 个部分解决)

客户端报告的问题 0 (0 个已解决)

范围

我们审计了 matter-labs/era-contracts 仓库的 pull request #1359,提交 ID 为 cc1619c.

审计范围包含以下文件:

 system-contracts
├── contracts
│   ├── Constants.sol
│   ├── ContractDeployer.sol
│   ├── EvmEmulator.yul
│   ├── EvmGasManager.yul
│   ├── NonceHolder.sol
│   ├── SystemContractErrors.sol
│   └── interfaces
│       ├── IContractDeployer.sol
│       └── INonceHolder.sol
└── evm-emulator
    ├── EvmEmulator.template.yul
    ├── EvmEmulatorFunctions.template.yul
    ├── EvmEmulatorLoop.template.yul
    └── calldata-opcodes
       └── RuntimeScope.template.yul

注意: 只审计了 pull request 中引入的更改,未完整审查列出文件的全部内容。

系统概述

审计的 pull request 可以分为两个不同的项目:

  • 系统合约中的半抽象化随机数实现
  • EVM 模拟器更新

EVM 模拟器更新

通过指针处理字节码增强效率

在此 pull request 引入的更改后,EvmEmulator 在调用中不再复制 EVM 字节码,而是直接通过指针读取它们。此更改更积极地利用指针,从而带来优化改进。通过消除复制字节码的需要并使用指针,模拟器提高了效率,减少了内存使用,并简化了字节码处理。

支持 modexp 预编译

此 pull request 为 EVM 模拟器添加了对 modexp 预编译的支持,并根据输入值实现了气体计算,遵循 EIP-2565 的规范。

半抽象化随机数实现

要求一个单一的顺序随机数值限制了发送者在交易排序方面定义自定义逻辑的能力。特别是,ZKsync SSO 的会话模块要求能够并行发送多个交易,而不让它们彼此覆盖或取消。

此 pull request 中引入的一个关键更改是支持 EIP-4337 的半抽象化随机数。在此更改之前,ZKChains 上的账户可以选择两种随机数排序模式:顺序和任意。经过此 pull request 后,任意排序已被弃用,而顺序排序已升级为 KeyedSequential

这种新的排序类型允许并行交易执行而不相互冲突,通过将完整的 uint256 随机数字段分为两个值:一个 192 位的 key 后跟一个 64 位的 sequence。在相同的 key 下,sequence 字段遵循经典的顺序,userOperation 必须以严格顺序执行。然而,多个 key 可以并行使用,而不相互影响。

为了支持此更改,旧的通过 setValueUnderNonce 函数在随机数下设置值的功能已被移除。此功能允许通过在映射中设置值来特定地使某个随机数失效。

需要注意的一点是,关键顺序排序是向后兼容的,因此所有的顺序排序账户被视为直到现在一直使用 key 值为零。更新随机数排序不再可能,KeyedSequential 是账户创建时的默认值。

集成注意事项

此更改引入了几个集成者应注意的考虑因素,例如:

  • 可以通过 increaseMinNonce 函数 增加随机数到任意值,最多 2^64,这意味着理论上有人可以通过确切增加其随机数 2^32 的 2^32 次调用来达到最大随机数。
  • 在此更改之前,理论最大随机数为 2^128,而现在为每个 Key 2^64。
  • 如果在更新部署之前配置了使用 Arbitrary 排序的账户,则无法将该账户迁移到 KeyedSequential。在审计时,Matter Labs 团队确认没有使用这种排序的账户。
  • 目前有 3 个不同的映射跟踪随机数系统。其中一个已被弃用,因为它通过设置值来跟踪随机数失效,但仍然存在于代码库中。第二个跟踪随机数,key 被设置为零。最后一个跟踪每个非零 key 的不同随机数序列。
  • 当前协议中没有模块使用关键随机数特定的功能。
  • 此更新后的每个新账户,将默认具有 KeyedSequential 排序,无法更新为任何其他。

此外,SsoAccount 合约当前使用 incrementMinNonceIfEquals 方法 在每个 Transaction 之后增加随机数。然而,此方法 仅允许 key 为零,这意味着 SsoAccount 合约无法利用新的关键随机数机制,该机制允许发送多个 Transaction 而不会因为相同的随机数而回滚。

考虑更新 SsoAccount 合约以使用新的 incrementMinNonceIfEqualsKeyed 方法。

安全模型和信任假设

在审计期间,基于此 PR 的更改作出了以下信任假设:

  • LLVM 编译器内联函数verbatim 语句中的函数调用,如 active_ptr_swapactive_ptr_data_loadreturn_data_ptr_to_active,属于 LLVM 编译器上下文。假设这些内联函数已正确实现且安全。

关键严重性

移位操作中的字节与位不匹配

modexpGasCostmloadPotentiallyPaddedValue 函数中,代码以字节为单位计算移位量,但直接使用这些值与 EVM 的移位指令(按位操作)。这导致移位不完整或不准确,因为 1 字节等于 8 位。如果不将基于字节的值转换为其位等效,移位操作的行为是不正确的。

这种不匹配有几个负面影响:

  • 参数读取失真:当用于修整或孤立组件(如底数、指数或模数)时,不正确的移位可能会残留不必要的位。这可能导致大小膨胀、气体计算偏差或数值损坏。
  • 可利用风险:恶意用户可能会提供触发这些不正确移位的输入,从而可能操纵Gas成本或绕过边界检查,导致未定义或可利用的行为。
  • 内存解释错误:意图遮蔽或清理特定字节的代码路径可能会保留残余位。这可能在解释内存内容时导致逻辑错误。
  • 数值不稳定:移位结果对齐错误可能导致后续逻辑中的值溢出或下溢。例如,从指数解析派生的格式错误的位长度可能导致循环过度运行或不足。

考虑确保每个由字节差异推导出的移位量在进行任何移位操作之前乘以 8。这确保与 EVM 的位级移位语义对齐,从而避免由于部分移位引发的范围很广的下游问题。

更新:pull request #1383 中解决了。

高严重性

内部变量遮蔽导致 mloadPotentiallyPaddedValue 返回错误

辅助函数 mloadPotentiallyPaddedValue 旨在从内存中读取 32 字节的字,并清零超出指定内存边界的任何字节。然而,由于在 if 块中不正确使用 let 声明,调整的值实际上并未返回。

 function mloadPotentiallyPaddedValue(index, memoryBound) -> value {
    value := mload(index)

    if lt(memoryBound, add(index, 32)) {
        memoryBound := getMax(index, memoryBound)
        let shift := sub(add(index, 32), memoryBound)
        let value := shl(shift, shr(shift, value)) // 内部 `value` 遮蔽外部的
    }
}

if 块中,使用 let 声明了一个名为 value 的新局部变量,遮蔽了作为函数返回变量的外部 value。因此,在块内应用的任何变换仅影响内部 value,而不是函数的输出。这导致该函数返回原始未修改的 mload(index) 结果,即使读取部分超出了指定内存区域。

此外,虽然 Yul 中不允许变量遮蔽,但当前编译器并未执行此规则并未能发出错误。这导致了这种微妙的逻辑错误,代码看似正确但由于静默遮蔽表现得异常。

此问题对 modexpGasCost 函数的Gas成本估算有下游影响,因为该函数依赖于 mloadPotentiallyPaddedValue 来提取受限参数。如果这些值未被正确调整,则计算将使用不准确的输入进行:

  • 参数大小不正确:当内存边界被超出时,超出界限的字节会保留在值中,导致大小误解。
  • 错误的指数迭代计数:不正确的 Esize 会影响位长度估算,从而扭曲迭代逻辑。
  • 不精确的气体计量:Gas成本可能被低估或高估,这违背了精确计量的目的,并可能导致可利用性或拒绝服务。

考虑将调整后的值直接分配给返回变量,避免使用遮蔽的 let 声明。

更新:pull request #1384 中解决了。

低严重性

缺失文档字符串

文档字符串是提高代码可读性和可维护性的重要因素。提供合约、函数(包括其参数和返回值)、事件和状态变量的清晰描述,帮助开发者和审计员更好地理解代码功能和用途。

在多个合约中发现了缺失或不完整文档字符串的实例,例如:

  • ContractDeployer 合约,其中并非所有函数都包含参数和返回值的文档字符串。

  • INonceHolder 接口,仅描述函数名称而未记录参数和返回值。

  • IContractDeployer 接口,缺少对多个函数、事件、参数和返回值的文档。

考虑全面记录所有合约、函数、事件和相关状态变量,并使用清晰、描述性的文档字符串。文档应遵循 以太坊自然规范格式 (NatSpec) 标准,以增强可读性,支持审计工作,并改善长期维护性。

更新:pull request #1399 中解决,提交 250af39ae0a314.

任意排序的弃用未明确说明

ContractDeployer 合约的新实现 防止 账户更新其随机数排序系统。此外,KeyedSequential 被指定为 默认排序,这使得 Arbitrary 排序选项完全被弃用。

然而,仍有一些地方未提及 Arbitrary 排序的弃用,这可能导致混淆,特别是:

  • IContractDeployer 接口中与 Arbitrary 类型相关的元素在 enum 中的 文档和名称
  • 合约的 广泛文档 在引用随机数排序时。

考虑更新文档和 enum 值,以正确反映该随机数排序系统现已弃用,以改善代码可读性,避免混淆,并使当前设计选择明确。此外,即使当前没有配置 Arbitrary 排序的账户,仍有可能在代码部署前设置它。考虑添加一个函数,使任何配置为使用 Arbitrary 排序的账户能够严格迁移到 KeyedSequential。这将为这些账户提供在无意中更新为 Arbitrary 之前正确迁移的方式。

更新:pull request #1387 中解决,提交 051b360。Matter Labs 团队表示:

由于 KeyedSequential 排序的假设,禁止从 Arbitrary 排序迁移回 KeyedSequential。即,如果随机数键 K 的随机数值为 V,假设是 V 以上的值未被使用。如果账户从 Arbitrary 排序迁移,这一假设将破坏。

缺乏输入验证

NonceHolder 合约中的 increaseMinNonce 函数_value 参数缺乏输入验证,调用时应严格大于零。

考虑实施验证检查以确保 _value > 0,以防止意外行为。

更新:pull request #1388 中解决,提交 aa15081a44fc21NonceIncreaseError 自定义错误也已修改,以通知最小可能值为 1。

不可达代码

辅助函数 MAX_MODEXP_INPUT_FIELD_SIZE 限制每个输入字段(BsizeEsizeMsize)的最大值为 32 字节。如果其中任何一个超出 32,modexpGasCost 通过返回 MAX_UINT64() 提前退出。

尽管有此限制,该函数包含一个 switch,根据 Esize > 32 分支处理大指数。然而,由于执行的限制,该分支目前是不可达的,因此实际上是死代码。

这可能是为了未来的容错,但就目前而言,逻辑增加了不必要的复杂性。

考虑添加明确的文档,说明为什么此代码路径当前不可达,并在未来何种条件下可能变得相关。

更新:pull request #1385 中解决。Matter Labs 团队表示:

_这确实是故意的 - MAX_MODEXP_INPUT_FIELD_SIZE 可以是任意的,目前版本为 32 字节。然而,已添加更多注释。_

备注与附加信息

兄弟代码库间接口不一致

当前的 pull request #1359era-contracts 仓库中与 pull request #3646zksync-era 仓库中的关系通过 pull request #1299 连接。两个仓库均包含对 INonceHolder 接口的更改,但修改并未保持一致:

尽管 zksync-era 版本旨在用于测试,但保持两个仓库的一致性对于确保正确性、减少混淆和保持测试稳健性很重要。

考虑在两个仓库中对 INonceHolder 接口定义进行协调,或如果是故意的,则明确记录其差异。

更新:e911061 提交中在 zksync-era 仓库中解决。现在两个仓库在 INonceHolder 接口定义上保持一致。

误导性文档

在整个代码库中,有几个实例表明现有的注释可能误导或过时。特别是:

  • 默认排序系统已从 Sequential 更新为 KeyedSequential,但有多个注释仍引用先前的默认值。这一不一致出现在 ContractDeployer.sol 合约的第 312437465 行。
  • ContractDeployer.sol第 26 行,注释指出 AccountInfo 值对于 EOA 和简单合约将为零。这可以澄清为指定零值对应于默认的 None 账户抽象版本和 KeyedSequential 随机数排序。

考虑更新这些内联注释,以准确反映当前系统行为。这将提高可读性,并减少未来开发或审查过程中的混淆风险。

更新:pull request #1399 中解决,提交 ffcf289.

接口与实现不匹配

在整个代码库中,接口与其关联实现之间存在一些不匹配。

IContractDeployer 接口 和其在 ContractDeployer 合约 的实现具有以下不同之处:

  • 接口包含一个命名参数,而实现中省略了它,以指示该函数不应使用,因为它将始终回滚。
  • 实现文档字符串指出随机数排序系统无法更新,而接口暗示可以更新。

NonceHolder 合约引入了新的 getKeyedNonce 函数,该函数未包含在关联的 INonceHolder 接口 中。此外,接口中的函数排序与实现不匹配。

考虑应用以下一致性改进:

  • 从接口版本的 updateNonceOrdering 中移除参数名称,以与实现保持一致。
  • 更新接口文档字符串,以反映该函数已弃用且将始终回滚的事实。
  • getKeyedNonce 函数添加到 INonceHolder 接口中。
  • 将接口中的函数排序与相应实现合约对齐。

这些更改将有助于减少混淆,并避免在代码库中出现意外的使用模式。

更新:pull request #1392 中部分解决,提交 58acf0c。Matter Labs 团队表示:

点1-3已修复。重新排序函数声明引入合并冲突,不值得为了微小的可读性收益而冒此风险。

随机数类型处理不一致

在整个代码库中,随机数值的处理不一致,导致解释和使用的歧义。特别是:

尽管这些不一致目前并未引入安全漏洞,但使用多种方式解释或验证相同数据的潜在可能性,增加了未来错误的风险,降低了代码清晰度,并使维护更复杂。

考虑统一各函数处理随机数类型的逻辑,以改善一致性和代码可读性。

更新:pull request #1395 中解决,提交 e04af13 以及在 pull request #1403 中,提交 6eb1fb2.

函数可见性过于宽松

在整个代码库中,有多个函数的可见性级别过于宽松:

  • NonceHolder.sol 中的 getKeyedNonce 函数标记为 public,但可以限制为 external
  • NonceHolder.sol 中的 getRawNonce 函数标记为 public,但可以限制为 external
  • NonceHolder.sol 中的 increaseMinNonce 函数标记为 public,但可以限制为 external
  • NonceHolder.sol 中的 _splitRawNonce 函数标记为 internal,但可以限制为 private

考虑将函数可见性限制为最低必要水平,以更好地反映预期用途,并可能减少Gas成本。

更新:pull request #1391 中解决。

缺乏安全联系

在智能合约中提供特定的安全联系(如电子邮件或 ENS 名称)可以显著简化个人报告漏洞的过程。此做法使代码所有者能够定义喜爱的沟通渠道,以进行负责披露,从而降低误沟通或漏报的风险。此外,在使用第三方库的情况下,维护者可以在需要时轻松联系以获取缓解指导。

在整个代码库中,有合约未包含安全联系:

考虑在每个合约定义上方添加一个NatSpec注释,包含安全联系。建议使用 @custom:security-contact 标签,因为它已被诸如 OpenZeppelin Wizardethereum-lists 这样的工具所采用。

更新:pull request #1399 中解决,提交 bfbe2a8.

函数在没有事件发射的情况下更新状态

在整个代码库中,多处函数更新合约状态而未发出相应事件。例如:

考虑为所有状态更改操作发出事件,以提高透明度,支持链下索引,并减少潜在难以追踪或审计的静默状态变更风险。

更新: 认可,未解决。Matter Labs 团队表示:

因为 EVM 不为随机数递增发出事件,所以决定在 NonceHolder.sol 中不发出它们,以防止开发者依赖它们。自定义 AA 账户仍然可以决定从它们自己的合约中发出这些事件,如果需要的话。

未使用的事件

INonceHolder 接口中,定义了 ValueSetUnderNonce 事件,但在整个代码库中并未使用。

考虑删除未使用的事件,以提高代码可读性,减少不必要的接口杂乱。

更新:pull request #1390 中解决。

冗余返回语句

为了提高代码库的可读性,建议从具有命名返回的函数中删除冗余的返回语句。

在整个代码库中,有多个冗余返回语句的实例。其中一些超出了当前审计范围;但突出它们也是有益的:

  • ContractDeployer.sol 中的 getAccountInfo 函数中的 第 41 行 应分配给 info 返回变量。

  • ContractDeployer.sol 中的 precreateEvmAccountFromEmulator 函数中的 第 217 行 是冗余的。

  • ContractDeployer.sol 中的 _evmDeployOnAddress 函数中的 第 417 行 应将最终值分配给 constructorReturnEvmGas 返回变量。

  • ContractDeployer.sol 中的 _performDeployOnAddressEVM 函数中的 第 473-480 行 应将内部函数输出分配给 constructorReturnEvmGas 返回变量。

  • NonceHolder.sol 中的 getDeploymentNonce 函数中的 第 180 行 是冗余的。

考虑在具有命名返回的函数中删除冗余返回语句,以改善合约的可读性。

更新:pull request #1394 中解决。

隐式转换

缺乏显式转换削弱了代码可读性,并使代码库难以维护且容易出错。

_combineKeyedNonce 函数中,nonceValue 参数在 第 119 行 上隐式转换为 uint256uint64

考虑显式将所有整数值转换为其预期类型,以提高可读性并减少未来更新中出现细微错误的风险。

更新:pull request #1393 中解决。

变量命名不一致

在整个代码库中,所有变量遵循“camelCase”命名约定。然而,在计算 Modexp 预编译的Gas成本时,该约定有所违反。

获取 基数、指数和模数的字节长度时,这些变量分别命名为 BsizeEsizeMsize

考虑通过将这些重命名为 bSizeeSizemSize 来强制统一所有变量使用的命名约定。

更新:pull request #1386 中解决。

结论

本次审计的重点是 ModExp 预编译Gas成本计算的实现、模拟器中的调整以避免通过指针使用的字节码复制、以及根据 EIP-4337 在系统合约中引入的半抽象化随机数。

在审计过程中,发现了一个关键和一个高严重性问题。此外,还发现一些与优化、不充分的检查、低测试覆盖率和最佳实践相关的问题,虽然这些问题并不立即威胁系统安全,但可能影响性能、可维护性和Gas效率。

尽管发现了这些问题,但与团队的沟通明显快速友好,并且代码的模块化性质显示出整体上有条理的方法。尽管如此,在全面记录最近的更改和更强大的测试策略等方面仍有相当大的改进空间。解决这些问题将有利于该项目在未来的升级中获得更好的定位。

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

0 条评论

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