UMA Across V2 审计 - OpenZeppelin 博客

OpenZeppelin 对 Across Protocol 的 contracts-v2 代码仓库进行了安全评估,发现了多个问题,包括客户端报告的 refunds 处理不当、slow fill 潜在的支付不足以及 recipient/message 功能失效等问题。

此安全评估由 OpenZeppelin 编写。

目录

摘要

类型桥梁时间范围从 2023-02-21 到 2023-03-06 语言 Solidity 总问题 11 (11 已解决)严重级别问题 0 (0 已解决)高严重性问题 0 (0 已解决)中严重性问题 0 (0 已解决)低严重性问题 4 (4 已解决)备注及附加信息 7 (7 已解决)

范围

我们审核了 across-protocol/contracts-v2 仓库,该仓库在 c01d48cdb2e72812b6b9780fe215d44faced50ba 提交下。

范围包括以下合约:

 contracts
├── BondToken.sol
├── Optimism_SpokePool.sol
├── SpokePool.sol
├── Succinct_SpokePool.sol
├── chain-adapters
│   ├── Optimism_Adapter.sol
│   └── Succinct_Adapter.sol
└── upgradeable
    └── EIP712CrossChainUpgradeable.sol

以下合约也在范围内,但仅涉及其可升级性:

 contracts
├── Arbitrum_SpokePool.sol
├── Boba_SpokePool.sol
├── Ethereum_SpokePool.sol
├── Ovm_SpokePool.sol
└── Polygon_SpokePool.sol

更新:作为修复评审过程的一部分,我们还审查了以下拉取请求:

  • PR #249:直接发射 FundsDeposited 事件
  • PR #264:修复在其他链上处理退款和部分填充
  • PR #267:修复在负 LP 费用情况下慢填充中的潜在少付
  • PR #268:对部分填充和全部填充进行一致的四舍五入

系统概述

Across 协议为 Layer 2 (L2) 链与以太坊主网之间的代币转账提供跨链桥加速机制,以及 L2 链之间的转移。通过为称为 relayers 的第三方参与者提供经济激励,实现快速的跨链转移,relayers 使用他们自己已经存在于目标链上的资金来满足用户的跨链资金转移请求。为了确保对于 relayer 退款有足够的流动性,流动性提供者通过向协议添加流动性获得费用回报。

协议的核心是一个 HubPool 合约和多个 SpokePool 合约。HubPool 部署在以太坊主网上,而每个支持的链上部署一个 SpokePool。SpokePools 在微调上彼此有所不同,以适应不同链的特定要求。

要使用 Across 完成跨链转移,用户在源链上存入资金,并指定他们希望接收资金的目标链。这会发出一个事件,该事件由离线 relayers 监控。然后,relayer 使用他们自己在目标链上的资金来填充存款,实际上将用户的资金从源转移到目标。随后,协议将对 relayer 进行报销。

为了激励 relayers 和流动性提供者,用户存入的资金的一部分以 relayer 和流动性提供者费用的形式留在协议中。此外,还有一个协议费用,目前设置为零,但预留给未来使用。

如果存款未被填充或仅部分填充,协议会使用自己的资金执行 slow relay 来填充请求。

协议实现了一个池重新平衡程序,以确保 SpokePools 拥有足够的资金来退款 relayers 并执行慢填充。重新平衡还将多余的资金从 SpokePools 移动到 HubPool。

通过 dataworker 提议根捆绑到 Optimistic Oracle 来实现 relayer 退款、慢中继和池重新平衡。根捆绑是根据 UMIP 规范构建的 Merkle 根。除非受到争议,否则根捆绑在生存期过后被乐观接受。

当执行根捆绑时,HubPool 通过提供 relayer 退款和慢中继的 Merkle 树根以及代币,向 SpokePools 发送跨链消息。在实际执行 relayer 退款时,提供并在 SpokePool 端验证 Merkle 证明。

变更摘要

如范围部分所述,此次审计集中于协议的最新新增内容,而不是整个代码库。这里介绍新主要功能的摘要:

  • SpokePools 已成为可升级合约。
  • 添加对没有规范桥接基础设施的链的支持,这通过利用 Telepathy 协议 来实现。由于无法将代币转账到 Succinct SpokePools,因此引入了负费用,以激励用户将资金从高流动性 SpokePools 转移到低流动性 SpokePools。负费用转换为向存款者的奖励以使用 Across 协议。作为对冲,协议补偿填充此类请求的 relayers。这些 relayer 奖励的计算在离线进行,并集成到退款提案数据中。
  • 引入了自定义 BondToken,允许白名单捆绑提议者。然而,任何人仍然可以提交争议。
  • 添加对 EIP-1271 和 EIP-712 签名的支持,特别是 EIP-1271 允许用户在提交后使存款更新失效。
  • 现在使用非规范桥接在 Optimism 上转移 SNX 代币。
  • 为存款和填充操作添加了暂停功能。

安全模型和信任假设

Across 协议依靠 Optimistic Oracle 以及 UMIP 规范、规范和非规范桥接,以及由 relayer 和 dataworker 实现的离线软件。

由 UMA 代币持有者控制的 Optimistic Oracle 能够解决根捆绑上的争议。这意味着如果它被攻破,争议可能无法正确解决,更重要的是,能够控制 Optimistic Oracle 的任何人都可以决定如何在系统内移动资金。这是更大 UMA 生态系统的一个特性,并且存在激励机制以保持 Optimistic Oracle 的诚实。

UMIP 规范描述了什么构成有效根捆绑,并且是至关重要的组成部分。它们中的漏洞或不一致可能导致可比于智能合约漏洞的后果。即使规范完美,离线软件中的错误(主要是指 relayer 和 dataworker 实现)仍然可能被滥用以实现类似效果。

最后,系统取决于规范和非规范桥接的安全性和行为。

特权角色

整个 Across 协议系统有一个管理员。该管理员可以决定哪些链有有效的 SpokePools,哪些代币被启用,以及一个链上的代币如何映射到另一个链上的代币。管理员还控制系统费用百分比等参数,费用的方向、新提议根捆绑的保证金金额、争议根捆绑的识别方式以及哪些代币被允许在系统内使用。最后,他们可以取消或删除根捆绑,并暂停存款和填充。

随着 BondToken 的添加,管理员还管理提议者的白名单。

客户报告的发现

在其他链上处理退款错误

客户报告: UMA在审计后识别了此问题。

fillCounter 旨在提供抢跑保护,通过跟踪对 SpokePool 余额的预期减少。例如,在简化的情况下,relayer 为 100 代币存款执行完全填充,此时链的 fillCounter 增加 100。如果 relayer 选择在同一目标链上获取他们的退款,存款者可以根据知道 100 代币将在 relayer 被退款时从该 SpokePool 中移除而预计算他们的存款激励费用。由于池的运行余额已减少,因此存款的激励以可预测的方式增加。如果该 relayer 而是执行部分填充,并且慢中继完成填充,逻辑也能正确工作——结果仍然是 SpokePool 余额减少 100。

但是,逻辑没有考虑到 relayers 可以请求在不同链上获取退款,即 repaymentChainId != destinationChainId。应用于上述示例,目标链的 SpokePool 上的 fillCounter 会增加,即使其余额不会发生变化。同样,退款链的 SpokePool 不会更新以反映其运行余额的变化。因此,在这种情况下,退款链上的存款者可能会被 relayers 超越,因为填充计数器没有被正确更新。

此外,在多次部分填充的情况下,_updateCountFromFill 函数在第一次部分填充发生时简单地按总填充量更新 fillCounter,尽管 relayers 可以提交后续的部分填充,并在不同链上退款。

更新:pull request #264 的提交 ef59e2a 中已解决。

在负 LP 费用的慢填充中可能发生的少付

客户报告: UMA 在审计后识别了此问题。

如果在具有负 LP 费用的存款上执行慢中继,则传递到 _fillRelay 函数中的 maxSendAmount 可能会导致 fillAmountPreFees 小于金额,这意味着它可能小于 amountRemainingInRelay。在这种情况下,发生了两个错误:a)慢填充未完成填充,导致接收者少付,以及 b)payoutAdjustmentPct 被忽略,因为它在比较 fillAmountPreFeesamountRemainingInRelayif 语句中。

更新:pull request #267 的提交 01e6ec4 中已解决。

更改接收者/消息功能无法正常工作

客户报告: UMA 在审计后识别了此问题。

有一个功能可以更改接收者和消息。该功能无法正常工作,因此如果有人向一个无法接收资金的地址存入资金,这些资金将被锁定。该错误仅在用户犯错误时发生——攻击者无法随意利用此错误来窃取资金。

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

OpenZeppelin 报告的发现(审计后)

从数据工作者处窃取 ETH

OpenZeppelin 报告: OpenZeppelin 在审计后识别了此问题。

攻击者可以创建一个带有接收者和消息的存款,这将不会被 relayers 填充(例如,因为 relayer 费用设置为零)。最终,慢填充过程将被触发。在过程结束时,dataworker 完成慢填充,该过程将回调到攻击者的合约,攻击者现在可以在 dataworker 的费用下执行代码。

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

低严重性

不正确或误导性的文档

发现代码库中的多个实例文档注释或注释存在错误。特别是:

  • SpokePool 合约的 _fillRelay 函数中, 第 1003 行 上的注释似乎与下一行的 if 语句无关。考虑修订注释。
  • docstringSpokePool 合约中的 pauseDeposits 声明“暂停存款和填充功能”,但通过调用 pauseFills 来暂停填充功能,而该调用没有文档注释。

考虑修订 pauseDeposits 的文档注释,并为 pauseFills 添加文档注释。

更新:pull request #251 的提交 21d9ed8 中已解决。

敏感操作后缺乏事件发射

setSuccinctTargetAmb 管理功能在更改 succinctTargetAmb 地址后未发射相关事件。

考虑在敏感变更后始终发射事件,以便于跟踪并通知关注协议合约活动的离线客户端。

_更新:pull request #252 的提交 10c1190 中已解决。此外,在 Polygon_SpokePoolSuccinct_SpokePool 的消息处理函数 (processMessageFromRoothandleTelepathy) 中添加了新的 ReceivedMessageFromL1 事件。_

缺少文档注释

代码库 中,有多个部分没有文档注释。特别是:

  • AdapterInterface.sol第 13-20 行:接口函数没有文档记录。
  • Arbitrum_Adapter.sol第 9-54 行ArbitrumL1InboxLikeArbitrumL1ERC20GatewayLike 接口及其函数没有文档记录。
  • LpTokenFactoryInterface.sol第 4-5 行:接口及其函数没有文档记录。
  • Optimism_Adapter.sol第 15-16 行SynthetixBridgeToOptimism 接口及其函数没有文档记录。
  • Polygon_Adapter.sol第 10-30 行IRootChainManagerIFxStateSenderDepositManager 接口及其函数没有文档记录。
  • Polygon_SpokePool.sol第 11 行processMessageFromRoot 函数没有文档记录。
  • SpokePool.sol第 169-196 行RelayExecutionRelayExecutionInfoDepositUpdate 结构具有未记录的成员。
  • Succinct_SpokePool.sol第 39 行initialize 函数没有文档注释。
  • Succinct_SpokePool.sol第 57 行handleTelepathy 函数没有文档注释。
  • WETH9Interface.sol第 4-11 行:接口及其函数没有文档记录。
  • ZkSync_Adapter.sol第 13-30 行ZkSyncLikeZkBridgeLike 接口及其函数没有文档记录。
  • ZkSync_SpokePool.sol第 6-12 行ZkBridgeLike 接口及其函数没有文档记录。

考虑彻底记录所有公共 API 的功能(及其参数)。即使不是公共的,实现敏感功能的函数也应该清晰记录。在编写文档注释时,考虑遵循 以太坊自然规范格式 (NatSpec)。

更新:pull request #253 的提交 5d1090apull request #242 的提交 4a8fe1c 以及 pull request #269 的提交 146f2f2 中已解决。

_ ZkSync_Adapter.solZkSync_SpokePool.sol 合约未进行更改。UMA 团队表示:_

除了 zkSync 合约外,所有建议都已实施,这些合约不在此次审计范围内,仍在进行中且尚未上线。

updatedRelayerFeePct 可能低于预期

SpokePool 合约中,speedUpDeposit 函数 执行以下检查updatedRelayerFeePct 变量:

 require(updatedRelayerFeePct < 0.5e18, "invalid relayer fee");

deposit 函数中的等效检查不同,在这种情况下未使用 SignedMath.abs 函数,这允许 updatedRelayerFeePct 值达到或低于下限 -0.5e18。relayer 填充此请求的尝试 将被拒绝_fillRelay 函数的费用检查。

考虑使用 SignedMath.abs 函数确保提供给 speedUpDepositupdatedRelayerFeePct 参数在预期限制范围内。

更新:pull request #254 的提交 857e787 中已解决。

备注和附加信息

接口文件不在 interfaces 目录中

Across Protocol V2 代码库 包含 interfaces 目录,但 HubPoolInterface.solSpokePoolInterface.sol 文件不在该位置。相反,它们位于父目录中。

考虑将所有非外部接口文件移动到 interfaces 目录,以便更容易找到接口。

更新:pull request #255 的提交 fa59467 中已解决。

在单个 require 语句中多条件

Succinct_SpokePool 合约的 handleTelepathy 函数中,有一个 require 语句 有多个条件。

考虑将每个条件隔离到一个单独的 require 语句中,并添加相应的错误消息,以便更容易识别失败的特定条件。

更新:pull request #256 的提交 c42ea4f 中已解决。

if 语句中的冗余条件

SpokePool 合约中,_updateCountFromFill 函数负责在填充发生时更新 fillCounter 数据。在以下三种情况下,无需更新:当操作是慢填充、初始零填充或该请求已发生部分填充。

然而,在零填充的情况下,执行流 不会到达该函数,这使得条件 endingFillAmount == 0 成为冗余。

考虑删除冗余条件以增加清晰度、可读性和节省 gas。为了额外的安全,考虑在 函数返回零填充的地方 添加注释,指出任何对这一部分的更新应考虑到函数 _updateCountFromFill

更新:pull request #264 的提交 ef59e2a 中已解决。endingFillAmount == 0 条件已删除,并添加了注释以澄清初始零填充不会达到代码中的此位置。

拼写错误

考虑更正以下拼写错误:

  • BondToken.sol
    • 第 11 行,“rootBundleProposer()” 应为 “rootBundleProposal()”。
    • 第 21 行:“permissiong” 应为 “permissioning”。
  • EIP712CrossChainUpgradeable.sol
    • 第 83 行:“its always” 应为 “it's always”。
  • MultiCallerUpgradeable.sol
    • 第 5 行:“@title MockSpokePool” 应为 “@title MultiCallerUpgradeable”。
  • Optimism_Adapter.sol
    • 第 23 行:“its only” 应为 “it's only”。
  • Polygon_SpokePool.sol
  • SpokePool.sol
    • 第 567 行:“speedUpRelay()” 应为 “speedUpDeposit()”。
    • 第 1177 行:“its always” 应为 “it's always”。
  • Succinct_Adapter.sol
    • 第 27 行:“destinatipn” 应为 “destination”。
    • 第 27 行:“the message..” 应为 “the message.”。
  • Succinct_SpokePool.sol
    • 第 22 行:“callValidated” 应为 “adminCallValidated”。
    • 第 24 行:“callValidated” 应为 “adminCallValidated”。
    • 第 35 行:“callValidated” 应为 “adminCallValidated”。

更新:pull request #258 的提交 43a0ea7 中已解决。

不必要的取模操作

MerkleLib.sol 中的 setClaimed1D 函数 执行了一个取模 256 操作,使结果在范围 [0, 255] 内。然而,index 是一个 uint8 变量,因此操作 index % 256 等同于 index

考虑删除冗余的取模操作。

更新:pull request #259 的提交 04c8488 中已解决。

未使用的导入

以下导入未使用:

为了提高可读性并避免混淆,考虑删除任何未使用的导入。

更新:pull request #260 的提交 4a94713 中已解决。

使用硬编码值

Ovm_SpokePool 合约中,_bridgeTokensToHubPool 函数包含 两个硬编码地址,这些地址未明确记录。

考虑创建常量来存储这些值,并赋予它们描述性的变量名称。

更新:pull request #261 的提交 6f8b6e3 中已解决。

结论

识别出几个低严重性的问题,以及一些提高代码库质量的备注。提出了一些更改,以确保遵循智能合约安全最佳实践。然而,Across 协议在很大程度上依赖于 relayer 和 dataworker UMIP,以及它们的离线实现和 Optimistic Oracle。如安全模型和信任假设部分所述,任何这些中的错误都可能导致严重的资金损失,因此建议进行全面系统的安全审计。

附录

可升级性考虑

以下是对 Across 草案 SpokePool 升级过程 中描述过程的补充,以确保安全迁移。

从不可升级的 SpokePools 迁移到可升级的 SpokePools 的程序应确保没有资金在过程中丢失或被锁定。在迁移之前,建议确保:

  • SpokePool 所在链上的所有代币路由均已禁用。
  • SpokePool 所在链上的所有未完成存款均已填充。
  • 所有退款操作均已执行。
  • 所有慢中继操作均已执行。
  • 留在 SpokePool 中的资金返回 HubPool。
  • 根捆绑为空。
  • 没有在传输中的根捆绑(即,跨链消息已发送但尚未执行)。

监控建议

虽然审计有助于识别潜在的安全风险,但建议 UMA 团队还应将链上合约活动的自动监控纳入其运营。对已部署合约的持续监控有助于识别影响生产环境的潜在威胁和问题。

为了确保没有意外的管理操作发生,并验证使用的正确值,考虑监控所有 SpokePool 管理事件,即从具有 onlyAdminonlyOwner 修饰符的函数发出的事件。特别是,考虑监控:

  • PausedDepositsPausedFillsEnabledDepositRoute 事件的发生,以确保池没有意外地处于部分禁用状态。

  • SetHubPoolSetXDomainAdminEnabledDepositRouteSetDepositQuoteTimeBufferEmergencyDeleteRootBundle 事件中发出的地址和其他数据,以验证它们与输入值匹配。

  • 特定 SpokePool 的事件的发生和数值,例如 Polygon_SpokePoolSetFxChildSetPolygonTokenBridger 事件。

桥接服务器在停机时可能会干扰 Across 协议的操作。考虑监控桥节点的上线/下线状态,例如 Arbitrum Sequencer 的可用性。

考虑监控异常大或小的存款和填充值,这可能表明正在进行攻击。

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

0 条评论

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