本次审计主要关注Era Contracts仓库中,EVM模拟器对modexp预编译的支持、指针方式处理字节码的优化,以及系统合约中对EIP-4337半抽象nonce的实现。审计发现了包括字节位移错误和内部变量遮蔽导致的返回错误等严重问题,并提出了代码优化、文档完善和测试加强等建议。
TypeL2 协议时间线 从 2025-03-20 到 2025-03-28
语言 Solidity + Yul
总问题 17 (15 个已解决, 1 个部分解决)
严重性问题
备注与附加信息 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 可以分为两个不同的项目:
在此 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 次调用来达到最大随机数。Arbitrary
排序的账户,则无法将该账户迁移到 KeyedSequential
。在审计时,Matter Labs 团队确认没有使用这种排序的账户。KeyedSequential
排序,无法更新为任何其他。此外,SsoAccount
合约当前使用 incrementMinNonceIfEquals
方法 在每个 Transaction
之后增加随机数。然而,此方法 仅允许 key 为零,这意味着 SsoAccount
合约无法利用新的关键随机数机制,该机制允许发送多个 Transaction
而不会因为相同的随机数而回滚。
考虑更新 SsoAccount
合约以使用新的 incrementMinNonceIfEqualsKeyed
方法。
在审计期间,基于此 PR 的更改作出了以下信任假设:
verbatim
语句中的函数调用,如 active_ptr_swap
、active_ptr_data_load
和 return_data_ptr_to_active
,属于 LLVM 编译器上下文。假设这些内联函数已正确实现且安全。在 modexpGasCost
和 mloadPotentiallyPaddedValue
函数中,代码以字节为单位计算移位量,但直接使用这些值与 EVM 的移位指令(按位操作)。这导致移位不完整或不准确,因为 1 字节等于 8 位。如果不将基于字节的值转换为其位等效,移位操作的行为是不正确的。
这种不匹配有几个负面影响:
考虑确保每个由字节差异推导出的移位量在进行任何移位操作之前乘以 8。这确保与 EVM 的位级移位语义对齐,从而避免由于部分移位引发的范围很广的下游问题。
更新: 在 pull request #1383 中解决了。
辅助函数 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
会影响位长度估算,从而扭曲迭代逻辑。考虑将调整后的值直接分配给返回变量,避免使用遮蔽的 let
声明。
更新: 在 pull request #1384 中解决了。
文档字符串是提高代码可读性和可维护性的重要因素。提供合约、函数(包括其参数和返回值)、事件和状态变量的清晰描述,帮助开发者和审计员更好地理解代码功能和用途。
在多个合约中发现了缺失或不完整文档字符串的实例,例如:
ContractDeployer
合约,其中并非所有函数都包含参数和返回值的文档字符串。
INonceHolder
接口,仅描述函数名称而未记录参数和返回值。
IContractDeployer
接口,缺少对多个函数、事件、参数和返回值的文档。
考虑全面记录所有合约、函数、事件和相关状态变量,并使用清晰、描述性的文档字符串。文档应遵循 以太坊自然规范格式 (NatSpec) 标准,以增强可读性,支持审计工作,并改善长期维护性。
更新: 在 pull request #1399 中解决,提交 250af39 和 ae0a314.
ContractDeployer
合约的新实现 防止 账户更新其随机数排序系统。此外,KeyedSequential
被指定为 默认排序,这使得 Arbitrary
排序选项完全被弃用。
然而,仍有一些地方未提及 Arbitrary
排序的弃用,这可能导致混淆,特别是:
考虑更新文档和 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 中解决,提交 aa15081 和 a44fc21。NonceIncreaseError
自定义错误也已修改,以通知最小可能值为 1。
辅助函数 MAX_MODEXP_INPUT_FIELD_SIZE
限制每个输入字段(Bsize
、Esize
、Msize
)的最大值为 32 字节。如果其中任何一个超出 32,modexpGasCost
通过返回 MAX_UINT64()
提前退出。
尽管有此限制,该函数包含一个 switch
,根据 Esize > 32
分支处理大指数。然而,由于执行的限制,该分支目前是不可达的,因此实际上是死代码。
这可能是为了未来的容错,但就目前而言,逻辑增加了不必要的复杂性。
考虑添加明确的文档,说明为什么此代码路径当前不可达,并在未来何种条件下可能变得相关。
更新: 在 pull request #1385 中解决。Matter Labs 团队表示:
_这确实是故意的 -
MAX_MODEXP_INPUT_FIELD_SIZE
可以是任意的,目前版本为 32 字节。然而,已添加更多注释。_
当前的 pull request #1359 在 era-contracts
仓库中与 pull request #3646 在 zksync-era
仓库中的关系通过 pull request #1299 连接。两个仓库均包含对 INonceHolder
接口的更改,但修改并未保持一致:
ValueSetUnderNonce
事件 和 isNonceUsed
函数 在 zksync-era
版本的接口 中不存在。setValueUnderNonce
和 getValueUnderNonce
函数仅在 zksync-era
仓库中存在,并已从 era-contracts
版本中移除。尽管 zksync-era
版本旨在用于测试,但保持两个仓库的一致性对于确保正确性、减少混淆和保持测试稳健性很重要。
考虑在两个仓库中对 INonceHolder
接口定义进行协调,或如果是故意的,则明确记录其差异。
更新: 在 e911061 提交中在 zksync-era 仓库中解决。现在两个仓库在 INonceHolder
接口定义上保持一致。
在整个代码库中,有几个实例表明现有的注释可能误导或过时。特别是:
Sequential
更新为 KeyedSequential
,但有多个注释仍引用先前的默认值。这一不一致出现在 ContractDeployer.sol
合约的第 312、437 和 465 行。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已修复。重新排序函数声明引入合并冲突,不值得为了微小的可读性收益而冒此风险。
在整个代码库中,随机数值的处理不一致,导致解释和使用的歧义。特别是:
NonceHolder
合约中的 getKeyedNonce
函数 在 key 为零 时从 rawNonce
映射 返回数据,但当提供非零 key 时返回 keyedNonces
映射 的数据。然而,在 isNonceUsed
函数中,此逻辑未得到一致应用——传递零 key 将导致同时查询 keyedNonces
和 rawNonce
映射,而不是仅仅查询 rawNonce
。incrementMinNonceIfEquals
函数将 _expectedNonce
输入用作 参数化的无序类型随机数 和 minNonce
类型随机数,导致对同一值的双重解释。尽管这些不一致目前并未引入安全漏洞,但使用多种方式解释或验证相同数据的潜在可能性,增加了未来错误的风险,降低了代码清晰度,并使维护更复杂。
考虑统一各函数处理随机数类型的逻辑,以改善一致性和代码可读性。
更新: 在 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 Wizard 和 ethereum-lists 这样的工具所采用。
更新: 在 pull request #1399 中解决,提交 bfbe2a8.
在整个代码库中,多处函数更新合约状态而未发出相应事件。例如:
NonceHolder.sol
中的 increaseMinNonce
函数NonceHolder.sol
中的 incrementMinNonceIfEquals
函数NonceHolder.sol
中的 incrementMinNonceIfEqualsKeyed
函数NonceHolder.sol
中的 incrementDeploymentNonce
函数考虑为所有状态更改操作发出事件,以提高透明度,支持链下索引,并减少潜在难以追踪或审计的静默状态变更风险。
更新: 认可,未解决。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 行 上隐式转换为 uint256
从 uint64
。
考虑显式将所有整数值转换为其预期类型,以提高可读性并减少未来更新中出现细微错误的风险。
更新: 在 pull request #1393 中解决。
在整个代码库中,所有变量遵循“camelCase”命名约定。然而,在计算 Modexp
预编译的Gas成本时,该约定有所违反。
在 获取 基数、指数和模数的字节长度时,这些变量分别命名为 Bsize
、Esize
和 Msize
。
考虑通过将这些重命名为 bSize
、eSize
和 mSize
来强制统一所有变量使用的命名约定。
更新: 在 pull request #1386 中解决。
本次审计的重点是 ModExp
预编译Gas成本计算的实现、模拟器中的调整以避免通过指针使用的字节码复制、以及根据 EIP-4337 在系统合约中引入的半抽象化随机数。
在审计过程中,发现了一个关键和一个高严重性问题。此外,还发现一些与优化、不充分的检查、低测试覆盖率和最佳实践相关的问题,虽然这些问题并不立即威胁系统安全,但可能影响性能、可维护性和Gas效率。
尽管发现了这些问题,但与团队的沟通明显快速友好,并且代码的模块化性质显示出整体上有条理的方法。尽管如此,在全面记录最近的更改和更强大的测试策略等方面仍有相当大的改进空间。解决这些问题将有利于该项目在未来的升级中获得更好的定位。
- 原文链接: blog.openzeppelin.com/ev...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!