EVM通用适配器审计

本次审计评估了Across协议的智能合约,包括移除白名单机制、引入通用适配器和SpokePool。审计发现了一些低风险问题,包括rootBundle可能被重复执行,以及Universal_SpokePool构造函数中未验证SOURCE_CHAIN_ID。同时,也提出了改进建议,例如添加安全联系方式、更正文档和变量名,以提高代码可读性和安全性。

目录

总结

TypeCross-ChainTimelineFrom 2025-03-31To 2025-04-09LanguagesSolidityTotal Issues5 (3 resolved)Critical Severity Issues0 (0 resolved)High Severity Issues0 (0 resolved)Medium Severity Issues0 (0 resolved)Low Severity Issues2 (0 resolved)Notes & Additional Information3 (3 resolved)

范围

OpenZeppelin 审计了 across-protocol/contracts 仓库的 pull request #916#926。所有更改都包含在 march-evm-audit-universal-adapter 分支中的 commit 9b58d8e

范围包括以下文件:

 contracts
├── chain-adapters
    ├── Universal_Adapter.sol
    ├── Solana_Adapter.sol
    └── utilities
        └── HubPoolStore.sol
├── external
    └── interfaces
        ├── IHelios.sol
├── interfaces
    └── SpokePoolInterface.sol
├── SpokePool.sol
└── Universal_SpokePool.sol

由于 Universal_SpokePool 依赖于 SP1Helios.sol 合约,因此也审计了部分合约。对 SP1Helios.sol 的完整审查包含在另一份审计报告中。

系统概述

Across 系统作为一个跨链传输加速器,通过实现跨各种区块链的即时 token 传输。这是通过激励第三方用户(称为“relayer”)使用他们自己的资金在目标链上填写跨链传输请求来实现的。然后,系统会退还给 relayer 填写的金额以及对其服务的奖励。退款过程使用链的规范桥,因此 relayer 本质上是将他们的资金借出一段时间。

以太坊主网上的 HubPool 合约是系统的核心及其流动性中心,而 SpokePool 合约部署在每个受支持的 L2 链上。SpokePool 合约既可以是传输请求的入口点,也可以是填写的目的地。HubPool 能够通过 Adapter 合约的通用接口向任何 SpokePool 发送消息,以便发送有关 SpokePool 之间资金再平衡、relayer 退款或慢速执行填写的指令。有关系统功能的更多详细信息,请参见以前的报告。

本次审查考虑了代码库中的两种更改:

  • SpokePool.sol 中删除 token/route 白名单,包含在 pull request #926 中。
  • 添加 Universal_AdapterUniversal_SpokePool,包含在 pull request #916 中。

移除 Token/Route 白名单

通过删除 SpokePool 合约的 deposit 函数中的相关检查,并将 enabledDepositRoutes 映射标记为已弃用,从而删除了从源到目标的 token 路由的白名单。作为一项对策,保护免受填写无价值 token 存款的责任转移到系统的链下组件。本质上,仅当 HubPool 中的 PoolRebalanceRoutes 映射包含已存入 token 的某些路由时,填写才会以不同于已存入 token 的退款 token 退款。否则,relayer 将被迫以存款来源链上的已存入 token 和金额退款。UMIP-179 将会更新,以正式指定这些规则。

Universal_AdapterUniversal_SpokePool

到目前为止,Across 协议支持的任何区块链都必须在 L1 上部署自己的 Adapter 合约,并在 L2 链上部署 SpokePool 合约。Adapter 合约负责使用支持每个 L2 链与 L1 通信的特定 L1 基础设施来 relay L1 -> L2 消息或 token 转移。这些消息允许 HubPoolSpokePool 通信,通常包括有关池再平衡、relayer 退款和慢速填写指示的 rootBundles 数据,以及 HubPool 合约的所有者 relay 的任何消息。

Universal_AdapterUniversal_SpokePool 允许为 EVM L2 链提供通用接口和跨链通信机制,而不是使用 L2 链的特定基础设施。本质上,只需要在 L1 上一个 Universal_Adapter,以及新的 HubPoolStore 合约,以便与 Universal_SpokePool 支持的任何 L2 链进行通信。

这是通过 SP1 零知识 VM (zkVM) 和 Helios 轻客户端在单个合约 SP1Helios 中组合来实现的。本质上,通过在 SP1 中运行 Reth 和 Revm,可以生成以太坊区块执行的 zk 证明。然后,在 Universal_SpokePool 支持的每个 L2 链上,都会部署一个 SP1Helios 合约。SP1Helios 充当 L2 区块链上的 L1 轻客户端,可以在其中提交和验证 SP1 区块执行证明,并通过这种方式与 L1 synchronize

从高层次上讲,L1 -> L2 通信是通过促进 Universal_AdapterSp1HeliosHubPoolStoreUniversal_SpokePool 来实现的,如下所示。当 HubPool 将消息 relay 到 Univeral_Adapter 时,消息数据 存储在 HubPoolStore 合约的特定存储槽中HubPoolStore 部署在 L1 上,是所有要 relay 到任何 L2 Universal_SpokePool 的消息的公共存储点。

SP1Helios 中,Helios 代码已扩展,以便 ProofOutputs 还包括 L1 合约的可验证存储槽值数组。在每次更新操作时,包含在 proof 输出中的所有存储槽值 都存储在 SP1Helios。在最后一步,当触发 L2 链上 Universal_SpokePool 中的 executeMessage 函数时,消息的有效性 通过调用 SP1Helios 并检查存储的数据来验证

根据 pull request #916,该系统还兼容将 RiscZero 与 Helios 轻客户端相结合的替代零知识设置。虽然没有审查 RiscZero + Helios 合约的特定实现,但预计它会实现与 SP1Helios 合约相同的 IHelios 接口。因此,只要底层轻客户端合约遵守预期的接口,Universal_SpokePool 就可以保持与 zkVM 无关。

安全模型和信任假设

本次审计是在某些信任假设下进行的,这些假设涉及系统中某些特权角色的行为以及系统依赖的链下组件的行为。

更具体地说,由于已经放弃了源/目标 token 的白名单,因此必须确保不使用合法的 token 填写无价值的已存入 token。UMA 团队已告知我们,链下填写相关规范将更新,以便检查 HubPoolPoolRebalanceRoutes 映射中的输入 token 以及请求的目标链 ID。如果它没有映射到输出 token,那么 relayer 将被迫以存款来源链上的已存入 token 和金额退款。我们相信,此规范将确实得到执行,直到部署更改。

此外,为了使 Universal_SpokePool 合约正常运行,应经常将验证 HubPoolStore 中存储更新的 SP1Helios 合约更新到最新的 L1 状态。我们相信,负责这些更新的 PROPOSER_ROLE 实体将执行一致的更新。

此外,假设 Universal_SpokePool 合约将部署在不同链上的不同地址。此假设对于减轻涉及管理消息的重放攻击至关重要。由于存储在 HubPoolStore 合约中的管理消息针对特定地址而不是特定链,因此在多个链上部署相同的合约地址将允许恶意行为者在一个链上重放用于另一个链的管理消息。通过确保每个 Universal_SpokePool 实例都部署在每个链的唯一地址上,该系统可以避免此类漏洞。

特权角色

在部署了 Universal_SpokePool 的链中,有两个特权角色能够触发关键功能:

  • HubPool 合约的所有者能够在 Universal_SpokePool 合约中两次执行 rootBundle。这是可能的,因为 HubPool 合约中的特殊 onlyOwner relaySpokePoolAdminFunction 与用于在 HubPoolStore 合约中存储管理消息的 distinct nonce counter 结合使用。本质上,如果管理员 relay 通过非所有者用户通过 HubPool.executeRootBundle 已经 relay 到 Universal_SpokePool 的执行消息,则管理员能够两次触发 rootBundles 执行。
  • 如果 SP1Helios 合约已超过 ADMIN_UPDATE_BUFFER 时间量未更新,则 Universal_SpokePool 的所有者能够 执行敏感的、访问受控的 SpokePool 操作。这仅在 SP1Helios 很长时间未更新的紧急情况下才有用,本质上会暂停 rootBundles 的执行。我们相信 ADMIN_UPDATE_BUFFER 将设置为接近 SP1Helios 更新阈值 的值,以尽可能限制所有者的行动自由。

我们相信,上述特权角色将以协议及其用户的最佳利益行事。

低风险

可能双重 relay 一个 rootBundle

HubPool 合约能够通过为此 L2 指定的 adapter 合约向任何 L2 SpokePool 发送跨链消息。在两种情况下,HubPool 会向一个或多个 L2 SpokePool 发送跨链消息。首先,允许 HubPool 的所有者 发送 任意消息到 SpokePool。其次,任何用户都可以通过将 rootBundle 作为消息 relay 到 SpokePool 来启动 L2 上的 rootBundle 执行。在这两种情况下,消息都 通过 adapter 合约 relay

当使用 Universal_Adapter 合约 relay L1 -> L2 消息时,它确保消息数据 存储HubPoolStore 中。反过来,在 HubPoolStore 中,存储 relay 数据的存储槽取决于 msg.sender。本质上,由 HubPool 所有者 relay 的消息被赋予一个计数器 uuid 作为 nonce,而由任何其他用户 relay 的消息被赋予 挑战期结束时间戳作为 nonce

因此,在 HubPool 的所有者在 L1 上为某些特定 L2 SpokePool 触发 rootBundle 的执行,然后用户将相同的 rootBundle relay 到另一个 L2 SpokePool 的情况下,rootBundle 的数据将在 HubPoolStore 中存储两次,在两个不同的槽中。这将允许对所有者也触发消息 relay 的 SpokePool 执行 rootBundle 中包含的 L2 操作两次。

考虑不允许在 HubPoolStore 的不同存储槽中存储相同的 rootBundle 数据,或者清楚地记录上述情况以及有关 HubPool 所有者的信任假设。

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

加粗这是按设计进行的。我们希望管理员能够 relay SpokePool 上已经存在的(或将来会存在的)rootBundle加粗

Universal_SpokePool 构造函数中未经验证的 SOURCE_CHAIN_ID

Universal_SpokePool 合约SP1Helios 合约集成,用于验证以太坊信标链状态更新,使用 SP1 零知识证明。SP1Helios 合约具有一个 immutable SOURCE_CHAIN_ID,通常对于以太坊设置为 1。但是,此设置可能会允许验证来自其他链的更新,从而引入可能的配置错误风险。

Universal_SpokePool 的构造函数不验证 SP1Helios 合约的 SOURCE_CHAIN_ID,而是假设其正确性。这种缺乏验证可能会导致接受来自意外链的更新,从而损害池的完整性。

为了减轻这种情况,建议在 Universal_SpokePool 构造函数中添加一个验证步骤,以确保 SP1Helios 合约的 SOURCE_CHAIN_ID 与预期的链 ID 匹配。这将通过验证来自正确源链的更新来防止配置错误并确保数据完整性。

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

加粗我们的目标是拥有 IHelios 合约的最小必需接口。我们可能希望将此 helios 合约与其他也需要实现 SOURCE_CHAIN_ID 的实现进行交换。本质上,HubPool 的管理员负责检查 Universal_SpokePool 是否已正确配置为使用从正确源链读取状态的 IHelios 合约。此检查应在执行任何管理操作到 HubPool.setCrossChainContracts 并正式“启用”此 spoke pool 之前进行。加粗

注意事项和其他信息

缺少安全联系人

在智能合约中提供特定的安全联系人(例如电子邮件或 ENS 名称)可以极大地简化个人在代码中发现漏洞时进行通信的过程。这种做法非常有益,因为它允许代码所有者指定漏洞披露的通信渠道,从而消除了因缺乏如何进行通信而导致沟通不畅或无法报告的风险。此外,如果合约包含第三方库并且这些库中出现错误,则其维护人员可以更轻松地联系到有关该问题的合适人员并提供缓解说明。

在整个代码库中,存在没有安全联系人的合约:

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

更新:pull request #951 中已解决。

误导性文档

在整个代码库中,发现了多个误导性文档的实例:

  • verifiedProofs 映射的 内联文档 可以澄清。虽然它目前描述了映射的内容,但它没有准确地反映出映射键对应于 nonce 本身(而不是 nonce 的哈希),它映射到存储在 HubPoolStore 中的 calldata 的哈希。
  • validateInternalCalls 修饰符和 _requireAdminSender 函数的 内联文档 引用了 receiveL1State 函数。但是,此函数在代码库中不存在,应替换为 executeMessage

考虑更正上述注释以提高代码库的整体清晰度和可读性。

更新:pull request #952 中已解决。

误导性变量名

在整个代码库中,发现了一些误导性或不清楚的变量名实例,这可能会阻碍理解并在开发和审查期间引起混淆:

  • 在 L1 上的 HubPoolStore 合约中,relayMessageCallData 映射 将 nonce 与 L2 上执行的 calldata 的哈希相关联。然后使用 getStorageSlot 函数 在特定区块号检索此映射中存储的值。但是,分配给 getStorageSlot 结果的变量名为 slotValueHash,这可能会产生误导。该函数返回存储槽的原始值,而不是存储槽值的哈希,这可能会造成混淆。考虑将 slotValueHash 重命名为 slotValue 以更好地反映其真实内容。此外,更新文档以阐明此值对应于 L2 calldata 的哈希,因为它最初存储在 relayMessageCallData 映射中。
  • Universal_SpokePool 合约中,verifiedProofs 映射实际上并不存储证明。相反,它将每个 nonce 映射到一个布尔值,以指示与此 nonce 链接的 calldata 是否已执行,以防止重放攻击。

考虑重命名上述高亮显示的变量,以更准确地反映其目的和内容,从而提高代码可读性并减少潜在的误解。

更新:pull request #952 中已解决。

结论

作为可扩展的跨链传输系统,Across 协议不断发展,从而可以在以太坊和各种 L2 链上实现快速安全的 token 传输。Relayer 使用自己的资金填写用户发起的传输,稍后通过规范桥梁报销,以太坊上的 HubPool 合约与部署在目标链上的 SpokePool 合约协调流动性和消息传递。

本次审计审查了该协议的最新更新,包括删除 token 和路由白名单,以及引入 Universal_AdapterUniversal_SpokePool 合约。这些更改表明了向通用化和模块化的明显推动,通过 SP1 或 RiscZero 等兼容替代方案支持 zkVM 驱动的跨链通信。这种设计反映了强大的架构远见,因为该协议旨在将 Across 支持扩展到其他基于 EVM 的 L2。

感谢 Risk Labs 团队在此过程中做出快速响应并提供详细的背景信息。

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

0 条评论

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