本次审计涵盖了三个不同的代码库,它们共同为 Open Intents Framework (OIF) 生态系统中的跨链互操作性和数据验证奠定了基础。第一个是广播合约,负责在源链上发出可验证的消息。第二个是 RLP 库,用于处理符合以太坊规范编码格式的数据序列化和反序列化。第三个是BroadcasterOracle 合约,旨在实现广播消息和链上验证者之间可靠的通信。
概要 类型: 库 时间线: 从 2025-10-27 → 到 2025-10-31
语言: Solidity
发现
总问题数:18 (13 个已解决,1 个部分解决)
严重:1 (1 个已解决) · 高:1 (1 个已解决) · 中:0 (0 个已解决) · 低:5 (2 个已解决)
备注 & 附加信息
提出 11 条备注 (9 个已解决,1 个部分解决)
OpenZeppelin 审计了 3 个不同的范围。
第一个是 openintentsframework/broadcaster 仓库,提交版本为 3522b4c。
范围内的文件如下:
contracts
├── interfaces
│ ├── IBlockHashProver.sol
│ ├── IBlockHashProverPointer.sol
│ ├── IBroadcaster.sol
│ └── IReceiver.sol
├── libraries
│ └── ProverUtils.sol
├── BlockHashProverPointer.sol
├── Broadcaster.sol
└── Receiver.sol
第二个是 OpenZeppelin/openzeppelin-contracts 仓库,提交版本为 d9f966f。
范围内的文件如下:
contracts
└── utils
└── RLP.sol
第三个是 openintentsframework/oif-contracts 仓库,提交版本为 acc7f9c。
范围内的文件如下:
src
└── integrations
└── oracles
└── broadcaster
└── BroadcasterOracle.sol
更新: 对于与 Broadcaster 合约相关的第一个范围,本报告中所有与发现相对应的修复已在提交版本 7afc0d2 中合并。 对于与 OpenZeppelin 合约相关的第二个范围,所有修复已在提交版本 6271317 中合并。 最后,对于与 OIF 合约相关的第三个范围,所有修复已在提交版本 035fa57 中合并。
当前的审计包括三个不同的范围,重点关注 Open Intents Framework (OIF) 生态系统中跨链互操作性和数据验证的基础组件。这些范围共同旨在提供可靠的消息验证、标准化数据编码以及跨异构区块链环境的安全广播机制。
第一个范围侧重于 ERC-7888 标准,该标准定义了跨链消息验证的通用框架。此实现引入了三个核心合约 Broadcaster、Receiver 和 BlockHashProverPointer 以及一个支持库 ProverUtils。
Broadcaster 合约负责在源链上发出可验证的消息,从而锚定相关区块链网络之间的通信。另一方面,Receiver 合约促进目标链上的消息摄取,确保仅处理来自可信来源的经过验证和最终确定的数据。
BlockHashProverPointer 提供了一种灵活的引用机制,该机制链接到特定的 BlockHashProver 实现。这些 Prover 通过验证状态根和存储 Merkle Patricia 尝试中的帐户数据和存储槽,充当链之间的密码桥梁。
通过模块化验证逻辑,ERC-7888 支持自适应跨链通信,该通信可以随着链升级或替代验证机制而发展,同时保持强大的真实性和一致性保证。
第二个范围涉及开发一个专用的 RLP(递归长度前缀)库,以根据以太坊的规范编码格式处理数据序列化和反序列化。该库提供了将结构化数据编码为 RLP 格式以及将 RLP 编码的有效负载解码回其组成元素的有效方法。正确的 RLP 实现对于互操作性至关重要,因为它确保了跨依赖以太坊兼容编码的系统和合约的确定性数据解释。
BroadcasterOracle第三个范围侧重于 BroadcasterOracle 合约的实现,该合约专为 Open Intents Framework (OIF) 而设计,OIF 是一种基于模块化意图的跨链协议。OIF 使用户能够定义和执行复杂的跨链意图,支持可定制的资产交付和验证条件,这些条件可以由开放的求解器以无需许可的方式来实现。
BroadcasterOracle 合约作为 OIF 智能合约层的一个组件运行,从而在广播消息和链上验证器之间建立可靠的通信。它与 OIF 的输出-输入分离模型保持一致,允许独立的资产收集和交付流程,例如通过资源锁定或托管机制的“先输出”和“后输入”。通过这种架构,BroadcasterOracle 有助于建立一个无需许可、可扩展的结算基础设施,该基础设施能够支持混合和跨链金融工作流程。
每个范围都引入了独特的信任假设和操作约束,这些假设和约束共同定义了系统的安全模型。
指针所有权和升级:BlockHashProverPointer 合约依赖于其所有者来正确更新对有效 BlockHashProver 实现的引用。恶意或疏忽的所有者可能会通过将指针重定向到欺诈性的 Prover 来进行 DoS 攻击或促进伪造的消息。
链一致性:更新到新的 BlockHashProver 时,Home 链和目标链必须与之前的配置保持一致。这是一个无法以编程方式验证的属性。
链升级:协议安全性取决于稳定的链存储结构。如果链升级修改了块哈希的存储位置(例如,重新利用父链上的映射),则较旧的 BlockHashProver 可能会产生无效或过时的块哈希,从而可能允许接收者接收伪造的数据。
Message 保证:ERC 确保消息可以被读取(给定最终确定),但不保证它们会被读取。由于最终确定在链上按顺序发生,因此消息的可用性取决于沿路由的累积最终确定时间。
与 RLP 库相关的主要风险涉及布尔解码语义。将布尔值解码为整数会引入与单字节编码期望的潜在不匹配。这可能会导致下游逻辑中的不一致解释,其中布尔值的二进制长度具有语义重要性。
BroadcasterOracle 和路由约束在 BroadcasterOracle 实现中,所有者有权跨目标链设置广播者 ID。一旦约束了路由,就无法更新。因此,如果链随后更改其结算层并需要不同的路由才能到达广播者,则该路由将不可逆转地损坏,从而阻止进一步的消息传播并有效地锁定该链的通信。
假设基于此构建的应用程序实现相应的检查以防止双重支出、多次跨链验证等。
在整个系统中,已识别出以下特权角色:
BlockHashProverPointer 所有者:维护对 Prover 引用的管理控制。负责确保对指针引用的更新是有效且兼容的 BlockHashProver 实现。未能正确管理可能会导致消息伪造或 DoS 情况。
BroadcasterOracle 所有者:拥有配置广播者 ID 和定义跨目标链的消息路由的权限。必须谨慎行使此角色,因为约束路由是不可变的,并且不正确的配置可能会永久中断链间连接。
Receiver 调用者:虽然在管理意义上不是特权的,但 Receiver 调用者有责任选择性地读取有效的消息,因为 ERC-7888 不强制执行消息活跃度或交付保证。
这些角色和假设共同定义了已审计组件的操作安全模型,强调谨慎的升级实践、负责任的所有权以及协议级别保证与系统级别完整性之间的一致性。
当提交填充的有效载荷的证明时,source 引用了已证明数据的应用程序的地址。但是,广播消息缺少有关应用程序的任何信息。因此,当用户在另一条链上验证消息时,他们可能会在 messageData 中提供任意应用程序。由于消息不包含任何用于标识应用程序的信息,因此此任意值直接用于 _attestations 映射中,无需验证。
考虑将 application 信息添加到消息哈希中,以便在消息验证期间对其进行验证。
更新: 已在提交 1872a01 的 pull request #160 中解决。
updateBlockHashProverCopy 函数允许将远程链 Prover 副本的地址更新为本地链中的新版本。在更新实现之前,此函数确保新地址处的 Prover 版本大于旧地址处的 Prover 版本。
但是,出现了一个问题,因为 _blockHashProverCopies 映射被初始化为零地址。因此,在零地址上调用 version getter 时,任何更新 Prover 副本的尝试都会恢复,从而导致更新被阻止。此限制阻止了 Receiver 合约正确验证来自涉及多个路由的链的消息。
考虑仅在已设置副本的实现地址时才执行版本检查。
更新: 已在提交 b66e918 的 pull request #29 中解决。
当 BlockHashProverPointer 首次设置实现地址时,它根本不执行任何验证。但是,所有后续的实现更改都会验证新的 version 是否与旧的 version 相比持续增加。如果初始实现不支持 version 函数,则指针将无法再次设置新地址。这是因为当它尝试在旧实现上调用 version 方法时,增加版本的检查将失败。
考虑检查初始实现是否支持 version 方法。
更新: 已在提交 807810f 的 pull request #38 中解决。
目前,可以提交给 BroadcasterOracle 的有效负载数量没有限制。但是,在消息验证过程中,系统[仅使用 2 字节的数据 来提取有效负载数组的长度。因此,如果提交给 Oracle 的有效负载数量超过了 2 个字节可以表示的限制,则该消息将无法在目标链上验证。
考虑限制 submit 函数上允许的有效负载数量。
更新: 已在提交 fb9575a 的 pull request #159 中解决。
RLP 库当前将 address 编码为 20 字节的数组。此表示形式可以包含前导零字节。
这本身不一定是问题。但是,以太坊黄皮书指出:
在解释 RLP 数据时,如果预期的片段被解码为标量,并且在字节序列中找到前导零,则客户端需要将其视为非规范数据,并以与其他无效 RLP 数据相同的方式处理它,完全忽略它。
这种模糊性可能会导致将 address 视为标量值的实现,在解码包含带有前导零的 address 的 RLP 数据时失败。
为了获得更好的兼容性并与规范保持一致,请考虑将 address 视为标量值,并使用其 uint256 表示形式对其进行编码。在这种情况下,任何前导零都不会包含在编码的字节数组中。
更新: 已确认,但未解决。该团队表示:
经过一番审查,结论是,不带前导零的编码与当前的以太坊生态系统不一致。如果有人想要编码一个不带前导零的地址,他们可以手动进行到 uint256 的转换,然后调用相应的编码函数。但是,这不应该是默认编码。
RLP 库的 address 解码函数目前仅允许长度为 1 字节(对于 address(0) 到 address(127))或 21 字节(0x94 前缀后跟 address 的 20 个字节)的编码地址。
这本身不一定是问题。但是,这种严格的检查意味着该实现不会将 address 视为标量。 以太坊黄皮书指出:
在解释 RLP 数据时,如果预期的片段被解码为标量,并且在字节序列中找到前导零,则客户端需要将其视为非规范数据,并以与其他无效 RLP 数据相同的方式处理它,完全忽略它。
这种模糊性可能会导致解码器在处理由其他实现编码为标量值的地址时失败,这些实现可能会省略前导零,因此具有不同的长度。
为了获得更好的兼容性并与规范保持一致,请考虑将 address 视为标量值,并使用其 uint256 表示形式对其进行解码。在这种情况下,该实现将支持解码表示为具有任意长度的标量的地址,并且长度检查可以简化为 length <= 21。
更新: 已确认,但未解决。该团队表示:
经过一番审查,结论是,不带前导零的编码与当前的以太坊生态系统不一致。如果有人想要编码一个不带前导零的地址,他们可以手动进行到 uint256 的转换,然后调用相应的编码函数。但是,这不应该是默认编码。
BroadcasterOracle 合约的所有者负责为特定链设置 broadcasterId。此设置是不可变的,这意味着,在首次设置后无法更改。
考虑到 L2 可能会更改其结算层,这种不可变性存在问题。例如,ZKchains 的结算层从以太坊迁移到网关 说明了这种情况。当 L2 更改其父链时,验证消息的路由会添加一个新的指针。这将导致 broadcasterId 累加器发生变化。因此,如果映射不可更新,则新的累加器将与存储的 broadcasterId 不匹配,这将停止该链的 Oracle 验证。
考虑添加一种机制来在链更改其父链时更新链的 broadcasterId。
更新: 已确认,但未解决。该团队表示:
该团队理解这个问题,但这是一种设计选择,即将特定链的
broadcasterId设置为不可变。其想法是在 Oracle 上具有尽可能少的信任要求。在这种情况下,尽管我们需要一个所有者来更新映射,但为了减少信任假设,我们认为最好不允许对其进行更新,因此用户和求解器可以确保 Oracle 不会更改。我们还认为,链更改其父链可能是一个罕见的事件,如果发生这种情况,我们始终可以部署一个新的 Oracle。
在 BlockHashProverPointer 合约中,在 setImplementationAddress 函数中,在同一范围内两次获取 _implementationAddress 存储变量。这会导致不必要的 sload 操作。
考虑缓存 _implementationAddress 以避免额外的存储读取。
更新: 已在提交 807810f 的 pull request #38 中解决。
在整个代码库中,发现了多个不完整的文档字符串实例:
BlockHashProverPointer.sol 中,implementationCodeHash 函数没有返回值的文档。Broadcaster.sol 中,hasBroadcasted 函数没有参数的文档。Receiver.sol 中,blockHashProverCopy 函数没有参数或返回值的文档。IReceiver.sol 中,blockHashProverCopy 函数没有 bhpPointerId 参数或返回值的文档。即使接口直接从 EIP 规范中提取,也强烈建议添加此文档。考虑彻底记录作为合约公共 API 的一部分的所有函数/事件(及其参数或返回值)。在编写文档字符串时,请考虑遵循以太坊自然规范格式 (NatSpec)。
更新: 已在提交 2d7bf91 的 pull request #39 中解决。
应修复 Pragma 指令,以清楚地标识将使用其编译合约的 Solidity 版本。
在整个代码库中,发现了多个浮动 Pragma 指令实例:
BlockHashProverPointer.sol 具有 solidity ^0.8.27 浮动 Pragma 指令。Broadcaster.sol 具有 solidity ^0.8.27 浮动 Pragma 指令。Receiver.sol 具有 solidity ^0.8.27 浮动 Pragma 指令。BroadcasterOracle.sol 具有 solidity ^0.8.26 浮动 Pragma 指令。考虑使用固定的 Pragma 指令。
更新: 已在提交 f811731 的 pull request #40 中部分解决。
在整个代码库中,发现了多个缺少文档字符串的实例:
BlockHashProverPointer.sol 中,BlockHashProverPointer 合约BlockHashProverPointer.sol 中,implementationAddress 函数BlockHashProverPointer.sol 中,setImplementationAddress 函数Broadcaster.sol 中,Broadcaster 合约Broadcaster.sol 中,broadcastMessage 函数Receiver.sol 中,Receiver 合约Receiver.sol 中,verifyBroadcastMessage 函数Receiver.sol 中,updateBlockHashProverCopy 函数考虑彻底记录作为任何合约公共 API 的一部分的所有函数(及其参数)。即使不是公共函数,实现敏感功能的函数也应明确记录。在编写文档字符串时,请考虑遵循以太坊自然规范格式 (NatSpec)。
更新: 已在提交 141e3da 和 411487c 的 pull request #41 中解决。
自 Solidity 版本 0.8.4 以来,自定义错误提供了一种更简洁、更经济高效的方式来向用户解释操作失败的原因。
在 ProverUtils.sol 和 RLP 中发现了多个 revert 和/或 require 消息实例:
ProverUtils 中,require(blockHash == keccak256(rlpBlockHeader), "Block hash does not match") 语句ProverUtils 中,require(accountExists, "Account does not exist") 语句RLP 中,require(bytes1(item.load(0)) != 0x00) 语句为了简洁和节省 Gas,请考虑使用自定义错误替换 require 和 revert 消息。
更新: 已在提交 720d8a1 的 pull request #42 中解决。
在整个代码库中,发现了多个返回值不一致的实例:
BroadcasterOracle.sol 中,_hashPayloadHashes 函数的命名返回值BroadcasterOracle.sol 中,_getMessage 函数的命名返回值RLP.sol 中,encode(Encoder memory self) 函数的命名返回值RLP.sol 中,[_decodeLength](https://github.com/OpenZeppelin/考虑删除具有命名返回的函数中多余的 return 语句,以提高代码清晰度和可维护性。更新: 已在 pull request #161 的 commit cd44a53 和 pull request #6106 的 commit 47c8048 中解决。
bytes[] 编码的含糊不清的文档RLP 库为 bytes[] 类型的值提供了一个 encode 函数。该实现只是简单地连接输入中提供的字节数组。但是,此实现可能会产生误导。一种简单的解释可能表明该函数对原始字节字符串数组进行编码。这种方法会导致关于每个单独字节数组长度的信息丢失。根据黄皮书规范,一个数组被编码为它的项目的编码的连接。encode(bytes[] memory input) 函数实际上期望一个已经编码的项目的列表。这要求用户首先在每个项目上调用 encode(string memory input) (或类似的 encode 函数),然后将结果数组传递给 encode(bytes[] memory input)。
考虑改进 encode(bytes[] memory input) 函数的文档字符串。该文档应明确说明该函数期望一个已经编码的字节字符串数组,而不是原始字符串,以防止潜在的误用和混淆。
更新: 已在 pull request #6106 的 commit 78f643d 中解决。
在 RLP 库的 _decodeLength 函数中,存在多个不可达的 bytes1(item.load(0)) != 0x00 检查。项目的第一个字节对应于 RLP 前缀。在这种情况, 如果这个字节是 0x00, 执行流会在函数的前两个 if 语句 (prefix < LONG_OFFSET 和 prefix < SHORT_OFFSET) 中已经分支, 所以这个检查永远不会被执行到。
考虑修改检查以检查第二个元素(索引 1)而不是第一个元素(索引 0)。这将正确地验证数据的长度的 big-endian 表达式是否为非零。
更新: 已在 pull request #6051 的 commit d3c84f5 和 3e96235 中解决。
在 RLP 合约中,readBytes 函数中的一个注释声明“长度由 {toBytes} 检查”。然而,这是具有误导性的。长度检查不是由 toBytes 函数直接执行的,而是由 slice 函数执行的,toBytes 调用了 slice 函数。
考虑更新注释以准确地反映 slice 函数执行长度检查的事实。
更新: 已在 pull request #6106 的 commit 55b33a0 中解决。
RLP 库的长字符串解码函数接受包含前导零字节的长度说明。但是,这些编码被认为是非规范的。此行为与其他著名的 RLP 实现(例如 Go-ethereum (geth))不同,后者不接受它们。这种差异可能会导致互操作性问题,即数据被此库认为是有效的,但被其他标准 Ethereum 客户端认为是无效的。
考虑在提供这些非规范编码时进行 revert,以与标准 RLP 实现行为保持一致。
更新: 已确认,未解决。 团队表示:
正如 L-03 和 L-04 中提到的,为了与生态系统中的其他库(例如 ethers.js)保持一致,我们选择接受带有前导零的非规范编码。
在 RLP.sol 库中,RLP 前缀赋值是使用内联汇编执行的。这些赋值的整数基数不一致。十进制和十六进制表示法在整个库中交替使用。已确定以下十六进制基数的实例:
考虑一致地使用十进制整数基数,以提高代码清晰度。
更新: 已在 pull request #6106 的 commit 61b695f 中解决。
目前的 Open Intents Framework (OIF) 审计涵盖了三个基本组件,旨在实现安全、标准化和无需许可的跨链互操作性:ERC-7888 实现、RLP 库和 BroadcasterOracle 合约。 ERC-7888 合约为真实的跨链消息传递建立了一个模块化验证框架。 RLP 库确保与 Ethereum 的规范格式一致的高效且确定性的数据编码,而 BroadcasterOracle 合约将消息广播和验证集成到 OIF 的基于意图的协议中,通过模块化执行模型支持复杂的多链结算流程。
在审计过程中,在 Receiver 合约中发现了一个高危问题,影响了多路由消息验证,以及与 BroadcasterOracle 中的应用程序验证相关的中危问题。 此外,还注意到了一些信任假设以及改进代码清晰度、可维护性和整体一致性的机会,并提出了加强验证边界和减少对受信任组件的依赖的建议。 总体而言,发现代码库结构良好、模块化且文档清晰,从而增强了 OIF 跨链生态系统中的可审计性和集成性。
OIF 团队在整个审查过程中表现出强大的技术能力和响应能力。 他们愿意提供详细的解释、阐明架构决策以及在问题解决方面进行协作,这极大地促进了评估的有效性,并反映了交付强大且可扩展的互操作性框架的明确承诺。
准备好保护你的代码了吗?
- 原文链接: openzeppelin.com/news/oi...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!