安全 - Across 审计

本次审核了across-protocol/contracts代码仓库,主要关注L3支持、ZkStack支持、可预测的中继哈希、支持最新版本的ERC-7683以及World Chain支持。发现了多个安全问题,包括缺少访问控制、错误的函数调用、不正确的ETH传输处理,以及潜在的重放攻击等。建议改进代码,增加测试,并确保代码符合最新的ERC-7683标准。

目录

概要

TypeDeFiTimelineFrom 2024-10-07To 2024-10-30LanguagesSolidityTotal Issues29 (26 resolved, 1 partially resolved)Critical Severity Issues1 (1 resolved)High Severity Issues2 (2 resolved)Medium Severity Issues8 (8 resolved)Low Severity Issues3 (2 resolved)Notes & Additional Information14 (13 resolved, 1 partially resolved)

范围

我们审计了 across-protocol/contracts 仓库。

范围包括五个部分,如下所示。

L3 支持

以下文件在范围内,于提交 5a0c67c 进行了审计:

 contracts/
├── chain-adapters/
│   ├── Router_Adapter.sol
│   ├── ForwarderBase.sol
│   ├── Ovm_Forwarder.sol
│   ├── Arbitrum_Forwarder.sol
│   └── l2/
│       ├── WithdrawalHelperBase.sol
│       ├── Ovm_WithdrawalHelper.sol
│       └── Arbitrum_WithdrawalHelper.sol
└── libraries/
    └── CrossDomainAddressUtils.sol

此外,我们还审计了 PR #629contracts/SpokePool*.sol 文件直到提交 6e86b70 所做的所有更改。

ZkStack 支持

以下文件在范围内,于提交 5a0c67c 进行了审计:

 contracts/
└── chain-adapters/
    ├── ZkStack_Adapter.sol
    └── ZkStack_CustomGasToken_Adapter.sol

可预测的中继哈希

以下文件在范围之内,直到提交 7641fbf 审计了在 PR #639 中对以下文件所做的更改:

 contracts/
├── SpokePool.sol
├── erc7683/
│   ├── ERC7683Across.sol
│   ├── ERC7683OrderDepositor.sol
│   └── ERC7683OrderDepositorExternal.sol
└── interfaces/
    └── V3SpokePoolInterface.sol

支持最新版本的 ERC-7683

对提交 108be77 中的以下文件所做的更改在范围内:

 contracts/
├── SpokePool.sol
├── erc7683/
│   ├── ERC7683.sol
│   ├── ERC7683Across.sol
│   ├── ERC7683OrderDepositor.sol
│   └── ERC7683OrderDepositorExternal.sol
└── interfaces/
    └── V3SpokePoolInterface.sol

World Chain 支持

以下文件在范围之内,直到提交 51c45b2 审计了在 PR #646 中以及直到提交 d4416cd 审计了在 PR #647 中,对以下文件所做的更改:

 contracts/
├── WorldChain_SpokePool.sol
└── chain-adapters/
    └── WorldChain_Adapter.sol

系统总览

Across 是一种基于意图的跨链桥接协议,允许用户在不同的区块链之间快速转移他们的 token。有关协议如何工作的更多详细信息,请参阅 我们之前的审计报告之一

变更概要

L3 支持

到目前为止,Across 协议仅支持直接与以太坊通信的区块链,例如 L2 网络。为了与其他区块链上部署的 SpokePool 进行通信,部署在以太坊上的 HubPool 使用了包含允许它发送跨链消息的逻辑的 adapter。但是,它无法与通过 L2 网络间接与以太坊通信的区块链进行通信。在本审计报告中,此类区块链将被称为 L3 区块链,添加到仓库中的最新合约引入了对它们的支持。

为了支持 L3 区块链,添加了两组合约:

  • Forwarder 合约。
  • Withdrawal helper 合约。

Forwarder 合约 旨在部署在 L2 区块链上,位于以太坊和 L3 区块链之间。它们旨在将从以太坊收到的所有消息传递到目标 L3 区块链。每个 forwarder 合约都能够处理与许多不同的 L3 区块链的通信。因此,对于每个 L2,部署其中一个就足够了。

通过使用 Router_Adapter 合约,可以实现以太坊和 forwarder 合约之间的通信。Router_Adapter 是一种特殊类型的 adapter,它包装了为 SpokePool 设计的消息,以便它们首先传递到 L2 上的 forwarder,然后发送到 L3 上的最终目标。每个受支持的 L3 区块链需要在以太坊上部署一个 Router_Adapter。这种设计允许在 HubPool 中将 L3 区块链视为 L2 区块链,因为 Router_Adapter 合约处理了整个 L3 相关逻辑,并且具有与现有 L2 adapter 完全相同的接口。

虽然 forwarder 合约启用了 L1->L3 通信,但 Withdrawal helper 合约 允许反向通信。它们负责将从 L3 区块链收到的 token 传递到以太坊上的 HubPool。

ZkStack 支持

引入了两个新的 adapter:ZkStack_AdapterZkStack_CustomGasToken_AdapterZkStack_Adapter 提供了对使用 ETH 作为 gas token 的 ZkStack 区块链的支持。另一方面,ZkStack_CustomGasToken_Adapter 设计用于与其余的 ZkStack 区块链一起使用。这两个合约都使能够发送自定义消息并将 token 转移到 L2 目标,类似于现有的 L2 adapter。

可预测的中继哈希

以前,SpokePool 合约中的存款 ID 计算为已完成的存款总数。但是,此设计不允许 relayers 完美地预测他们必须提供才能填充存款的中继哈希。这是因为如果在此之前已进行其他存款,则每个存款 ID 可能会更改。

当前的设计允许通过允许存款人指定 nonce 来预测存款 ID。然后,此 nonce 用于使用 keccak256 哈希函数创建确定性存款 ID。存款人负责不重复使用相同的 nonce,这可能导致 SpokePool 内的存款 ID 冲突。

支持最新版本的 ERC-7683

ERC-7683 标准已提出新的变更。在这方面,引入了两种新的订单类型:GaslessCrossChainOrder,旨在脱链创建,以及 OnchainCrossChainOrder,可以直接链上使用。表示这两种类型的 Struct 都可以包含特定于实现的数据,然后用于构造 ResolvedCrossChainOrder Struct,其中包含订单 fillers 所需的信息。无论何时打开任何类型的订单,都必须在事件中发出此 Struct。

此范围部分的合约中所做的更改实现了 ERC-7683 标准的新要求。

World Chain 支持

World Chain 实现了 Circle 的桥接 USDC 标准,允许将来将桥接的 USDC 升级为 Circle 发行的原生 USDC。因此,L1 和 L2 标准桥都不能用于 USDC 存款和取款。

此范围部分的 pull request 使用部署在以太坊和 World Chain 上的特殊 USDC 桥,以便在 HubPool 和 WorldChain_SpokePool 之间正确桥接 USDC token。

安全模型和信任假设

Across 协议依赖于许多不同的外部组件,例如不同区块链之间的桥和消息传递机制。此外,本次审计仅限于整个代码库的一部分。因此,审计是在一定的信任假设下进行的。

在整个审计过程中,我们假设范围内合约交互的所有合约都能正常工作。特别是,我们假设桥按预期工作并正确地桥接区块链之间的资产,并且在 HubPool 和 SpokePool 合约上调用的 view 函数(例如 tokenBridgesremoteL1TokenspoolRebalanceRoute)返回正确的结果。我们还假设只会桥接桥和目标区块链都支持的资产。

此外,我们还假设 Router_Adapter 使用的 L1 adapter 和 forwarder 合约使用的 L2 adapter 都能正常工作。特别是,我们假设它们正确地实现了 AdapterInterface,正确地验证了提供的 token 对以进行中继,并正确地桥接了 token 并在区块链之间发送消息。

我们还假设所有合约都只会部署在它们所设计的区块链上。例如,Ovm_WithdrawalHelper 合约在 OVM 区块链上无法正常工作,因为存在需要自定义逻辑才能进行桥接的 token,这与 Ovm_SpokePool 合约的 _bridgeTokensToHubPool 函数中包含的逻辑不同。例如,Optimism 和 Blast 就是这种情况。因此,我们假设 Ovm_WithdrawalHelper 不会部署在这样的区块链上。

此外,假设正在使用的每个 ZkStack 链都已正确配置,特别是,这意味着用于预估交易成本的 l2TransactionBaseCost 函数返回正确的值。这包括自定义 gas token 具有自定义小数位数的情况,在这种情况下,我们假设 基本 token 提名人和基本 token 分母参数 配置正确,以便在预估要收取的 L2 gas token 金额作为 gas 支出时应用适当的缩放。还值得注意的是,adapter 的当前实现始终使用 BridgeHub 中公开的共享桥。某些 token 可能需要 使用与共享桥不同的桥。我们假设此类 token 将不会使用现有的 adapter 进行桥接,并且只会桥接共享桥和目标 L2 网络都支持的 token。

还假设所有范围内合约都已正确初始化和配置。特别是,假设与 ZkStack 区块链上的 gas 计算相关的参数(例如 L2_GAS_LIMITL1_GAS_TO_L2_GAS_PER_PUB_DATA_LIMIT)设置为允许在每个受支持的区块链上正确执行所有交易的值。

特权角色

多个新引入的特权角色也在本次审计的范围内:

  • Forwarder 合约的管理员。此帐户能够随时更改 forwarder 合约的实现,从中转移 token,修改正在使用的 adapter,并代表它们发送消息。
  • Withdrawal helper 合约的管理员。此帐户能够随时更改 withdrawal helper 合约的实现。
  • ERC7683OrderDepositorExternal 合约的所有者。此帐户能够更改 ERC-7683 目标结算器,该结算器由 ERC7683OrderDepositor 合约使用,以便发出 ERC-7683 填充指令。

最重要的是,合约中还存在其他特权角色,这些角色已从本次参与的范围中排除,但可能会影响范围内合约(例如,HubPool 的所有者)。有关 Across 协议中存在的特权角色的完整列表,请参阅我们过去的审计。

最终,我们假设所有拥有上述角色的实体都将以负责任的方式行事,并以协议及其用户的最大利益为出发点。

严重问题

setDestinationSettler 缺少访问控制

ERC7683OrderDepositorExternal 合约包含 setDestinationSettler 函数,该函数提供了链 ID 到该链结算器合约地址的映射。可以通过同一合约的 _destinationSettler 函数访问此值,并且由继承的 ERC7683OrderDepositor 合约在构造 _resolveFor 函数中的 fill 指令 时使用。

问题在于,setDestinationSettler 函数 没有访问控制,并且可以由任何帐户更改为任何任意地址。因此,恶意用户可以将 destinationSettler 地址设置为恶意地址,该地址用于构造 fill 指令。destinationChain 上的 filler 需要向 destinationSettler 授予 token 批准才能执行 fill 调用。因此,恶意的 destinationSettler 能够从 filler 窃取资金。

由于 ERC7683OrderDepositorExternal 合约已经 继承Ownable 合约,请考虑将 onlyOwner 修饰符添加到其 setDestinationSettler 函数。

更新:pull request #733 的提交 8942780 中已解决。

高危问题

SpokePoolfill 函数执行格式错误的调用

SpokePool 合约的 fill 函数 旨在遵守 IDestinationSettler 接口,正如对 ERC-7683 规范 的最新更新所规定的那样。fill 函数旨在内部调用 fillV3Relay 函数 以处理订单数据,并且它通过 对自身的 fillV3Relay 函数进行 delegatecall 来实现这一点,并将 abi.encodePacked(originData, fillerData) 作为参数传递。

但是,fillV3Relay 函数接受两个参数,其中 repaymentChainId 作为第二个参数。由于调用是使用 encodeWithSelector 构建的,而不是类型安全的,因此编译器不会抱怨缺少参数。由于传递了不正确的参数数量,因此当尝试解码输入参数时,对 fillV3Relay 的调用将始终还原,从而破坏整个执行流程。此外,输入数据是使用 abi.encodePacked 编码的,强烈建议不要使用它,尤其是在处理 Struct 和动态类型(如数组)时。

考虑使用 encodeCall 而不是 encodeWithSelector 来确保类型安全,并分别提供 fillV3Relay 函数所需的参数。此外,考虑显式地使 SpokePool 合约从 IDestinationSettler 接口继承,正如 ERC-7683 标准所要求的那样。

更新:pull request #744 的提交 9f54455 中已解决。

Forwarder 和 Withdrawal Helper 合约无法正确处理 ETH 转账

WithdrawalHelperBaseForwarderBase 合约旨在部署在 L2 上,并协助将 token 和消息移入和移出 L3 链。这些合约由特定于链的合约继承,目前专为 Arbitrum 和基于 OVM 的区块链而设计。但是,这些合约都没有正确处理 ETH 转账。这是因为它们不包含 receive 函数,这会导致任何尝试将 ETH 转移到这些合约的尝试都失败。

对于 forwarder 合约,缺少 receive 函数意味着 WETH 转账(依赖于在桥接之前解包,例如 通过 Optimism_Adapter 进行的转账)将失败,导致 ETH 留在桥中,直到可以升级合约为止。对于 withdrawal helper 合约,缺少 receive 函数意味着它们将无法 在尝试将其转移到以太坊时解包 WETH。此外,它们将无法接收从 L3 桥接的 ETH。此外,虽然 withdrawal helper 合约包含 token 桥接逻辑,但它们不支持桥接 ETH。这意味着即使它们能够接收 ETH 并且 ETH 从 L3 桥接到它们,也无法将其路由到 L1。

考虑向 ForwarderBaseWithdrawalHelperBase 合约添加 receive 函数,以方便传入的 ETH 转账和桥接期间 WETH token 的解包。由于合约不支持直接桥接 ETH,因此 receive 函数应包含逻辑以确保正确处理传入的 ETH 并可以将其发送到目标链。

更新:pull request #725 的提交 705a276 中已解决,方法是将 receive 函数添加到 forwarder 和 withdrawal helper 合约。此外,两个合约都允许通过将其包装在需要 WETH 转账的情况下将 ETH 转出。团队表示:

我们采用了这种方法,以便我们可以保持与 L2-L3 桥接的 L1 adapter 和 L2-L1 提款的 L2 spoke pool 相同的格式。

中危问题

由 Forwarder 触发的 relayTokens 调用可能失败

Router_Adapter 合约 可用作 HubPool 合约的 adapter,以便将消息或 token 发送到 L3 区块链。为了将 token 发送到 L3,此合约向中间 L2 区块链发送两条消息:第一条消息 只是对 relayTokens 的调用,它将指定数量的 token 发送到 L2 上的相关 forwarder 合约,第二条消息 是对 relayMessage 的调用,它将在到达时在 L2 上的 forwarder 合约上执行 relayTokens 函数。这样,L2 上的 forwarder 合约将被指示在收到 token 后立即将收到的 token 发送到 L3。

但是,无法保证发送到 L2 上的 forwarder 合约的消息将以发送的相同顺序传递。特别是,某些 token 使用与消息使用的通道不同的通道发送到 L2。例如,在 Arbitrum 的情况下,USDC token 将通过 CCTP 协议进行桥接,但消息 将通过 Arbitrum Inbox 合约传递,该合约完全独立于 CCTP。这可能会导致某些指示 forwarder 合约将 token 中继到 L3 的消息在 L2 上失败,因为 token 可能会在尝试将其发送到 L3 后到达 L2。

考虑在 forwarder 中缓存失败的消息,以便将来任何人都可以重新执行它们,可能一次批量执行多个。

更新:pull request #664 的提交 d3e790f 中已解决,方法是在 relayTokens 函数中缓存所有消息。现在可以通过调用 executeRelayTokens 函数来执行缓存的 token 转账。可以通过使用 Multicaller 合约的 multicall 函数将调用分组在一起。

无法调用 Forwarder 的某些特权函数

ForwarderBase 合约 包含几个只能通过源自 crossDomainAdmin 的跨链消息调用的函数。这些函数包括 setCrossDomainAdminupdateAdapter 函数。ForwarderBase 合约和从中继承的合约预计通过部署在 L1 上的 Router_Adapter 合约 进行通信。

但是,Router_Adapter 不提供调用 forwarder 合约的任何函数(除了 relayMessagerelayTokens)的方式。这意味着如果需要更改 crossDomainAdmin 或更改 forwarder 合约使用的 adapter,只能通过用提供此类功能的另一个 adapter 替换 Router_Adapter 来完成。

考虑实现允许通过 Router_Adapter 合约调用 forwarder 合约的其他特权函数的逻辑。或者,考虑实现专用的 adapter,这将能够从 HubPool 调用这些特权函数。

更新:pull request #665 中已解决。团队表示:

我们仍在决定如何与 ForwarderBaseWithdrawalHelper 合约进行通信,但是,目前,我们认为对这些合约的升级将很少见。考虑到这一点,我们有几种方法来解决此问题:

- 在假设我们只需要在初始化新的 L3 时调用管理函数的情况下,我们可以在 hub pool 中调用 setCrossChainContracts 以将 L3 的链 ID 映射到 L2 forwarder 地址/withdrawal helper 地址和相关的 adapter(例如 OptimismAdapter)。然后可以使用此连接来配置 forwarder/withdrawal helper,之后我们调用 setCrossChainContracts,其中 L3 的链 ID 映射到正确的 adapter/spoke pool 对。

- 如果我们需要建立连接(临时或持久),我们可能还需要调用 setCrossChainContracts 以将 L3 链 ID 的某些函数映射到 forwarder/withdrawal helper 合约。否则(仅适用于临时连接)。如果我们只需要向 L2 合约发送一条消息,我们也可以通过调用 setCrossChainContracts 临时停止与 L3 spoke pool 的通信,其中更新: 已在 pull request #746 的提交 9eebe3d 中解决。_

某些合约可能无法与 USDT 授权正常工作

ERC7683OrderDepositorExternal 合约实现了 _deposit 函数来完成 Across V3 存款的创建。为此,该函数在订单详情中指定的 inputToken调用safeIncreaseAllowance 函数。此机制将适用于任何 token,其假设是整个授权将由 SpokePool 在depositV3 函数调用 中使用。safeIncreaseAllowance 函数也用于 ZkStack_AdapterZkStack_CustomGasToken_Adapter 合约中,以及其他一些适配器,如 ZkSync_Adapter,这些适配器不在本次审计范围内。

但是,如果出于任何原因,在批准后未使用整个授权,则任何进一步尝试使用禁止从非零值到非零值进行任何批准更改的 token(如 USDT)的 safeIncreaseAllowance 最终都会失败。作为实际影响的一个例子,问题 M08 的第二个例子可能会产生一个场景,即后续使用 USDT 作为自定义 gas token 的调用将失败,从而阻止整个 ZkStack_CustomGasToken_Adapter 的功能。

考虑使用 SafeERC20 库的 forceApprove 函数,以与在从非零值到非零值的批准时恢复的 token 兼容。

更新: 已在 pull request #734 的提交 ea59869 中解决。

在 ZK 适配器中可能存在恶意攻击

ZkStack_Adapter 合约的 _computeETHTxCost 函数 用于估算 L2 上的交易成本。每当消息从 L1 发送到 L2 时,此估算的交易成本会从 HubPool 转移 到原生 L1 收件箱。任何多余的值都应该 退还 给 L2 上的 L2_REFUND_ADDRESS,预计该地址受 Across 团队控制。但是,tx.gasprice(用于估计交易成本)是一个可以由交易发起者操纵的参数。这开辟了一种攻击途径,恶意用户可以膨胀 tx.gasprice,以便将 ETH 从 HubPool 转移到 L2 网络。

为了执行攻击,攻击者可以调用 HubPool 的 executeRootBundle 函数,导致 HubPool 调用适配器的 relayMessage 函数。由于 tx.gasprice 直接用于 所需的 gas 费用计算,攻击者可以将其设置为一个值,使估算的费用等于整个 HubPool 的 ETH 余额。然后,HubPool 将 ETH 转移到 L2。可以使用类似的攻击,以便使用 ZkStack_CustomGasToken_Adapter 合约 从 HubPool 转移自定义 gas token。

虽然攻击者通常很难膨胀整个交易的 tx.gasprice 参数(因为他们必须支付 gas 费用),但如果他们是执行攻击的区块的验证者,他们可以几乎收回所有投资的 ETH 金额。

考虑限制可用于 _computeETHTxCost_pullCustomGas 函数内部的 gas 费用计算的最大 tx.gasprice

更新: 已在 pull request #742 的提交 dc4337c 中解决,通过限制可用于 gas 费用计算的最大 tx.gasprice

传递给 permitWitnessTransferFrom 的参数不正确

PERMIT2_ORDER_TYPE 变量 存储见证数据的类型字符串,该字符串应作为 witnessTypeString 参数传递给 Permit2 合约的 permitWitnessTransferFrom 函数。因此,此变量应该定义 witness 参数(传递给该函数)从中哈希出的类型化数据。但是,它 指定 witness 参数是从 CrossChainOrder 类型哈希的,而实际上,它从 GaslessCrossChainOrder 类型哈希的

此外,指定的 witness 参数不正确,因为 计算它时 没有考虑 GaslessCrossChainOrder 结构体的 orderDataType 成员。对于 AcrossOrderData 结构体的 exclusiveRelayerdepositNonce 成员也是如此,它们 未包含 在其哈希的计算中。此外,用于创建 witnessCROSS_CHAIN_ORDER_TYPE 变量包含 GaslessCrossChainOrder 结构体 的不正确编码,因为 originChainId 成员 被指定为 uint32 类型,而不是 uint64

考虑更正上述错误,以保持与 EIP-712Permit2 的兼容性。

更新: 已在 pull request #745 的提交 b1b5904 和提交 98c761e 中解决。该团队表示:

在审计期间,ERC7682 发生了一些变化,因此这些修复分布在两次提交中。第一次提交(以及附加的 PR)解决了第一段和第二段的所有内容,除了 depositNonce 和将 originChainId 交换为 uint64。depositNonce 此后已被删除,并且由于第二次提交,原始链现在匹配。

尝试使用 ZkStack_CustomGasToken_Adapter 桥接 WETH 将失败

为了使用自定义 gas token 将 token 从 Ethereum 桥接到 ZkStack 区块链,可以使用 ZkStack_CustomGasToken_Adapter 合约的 relayTokens 函数。如果桥接的 token 是 WETH,则该 token 首先 转换 为 ETH,然后使用 BridgeHub 合约的 requestL2TransactionTwoBridges 函数 桥接该 ETH。然后,requestL2TransactionTwoBridges 函数使用调用者指定的 ETH 金额 调用第二个桥的 bridgeHubDeposit 函数

但是,bridgeHubDeposit 函数 要求指定的存款金额等于 0,在桥接 ETH 的情况下,它 在适配器的 relayTokens 函数内部指定为非零金额。这将导致任何尝试将 WETH 桥接到 L2 的操作都将恢复。

在桥接 WETH 的情况下,请考虑将第二个桥的 calldata 中使用的金额设置为 0。

更新: 已在 pull request #743 的提交 0bdad5b 中解决。

目标链的 Gas Token 的转移将失败

ZkStack_AdapterrelayTokens 函数 旨在促进从以太坊主网上的 HubPool 到以 ETH 作为 gas token 的 ZkStack 链的转移,通过 BridgeHub。ZkStack_CustomGasToken_Adapter 合约的 relayTokens 函数 能够为具有自定义 gas token 的链实现此功能。

对于 ZkStack_Adapter,当转移 WETH 时,relayTokens 函数 将 WETH 解包 为 ETH,并将此金额作为对 BridgeHub 合约的 requestL2TransactionDirect 调用的部分发送到 BridgeHub。对于 ETH 转移,requestL2TransactionDirect 函数检查随调用发送的值是否等于请求的 mintValue。但是,在 relayTokens 函数中,发送的 valueamount + txBaseCost,而 mintValue 仅仅是 txBaseCost

requestL2TransactionDirect 函数 要求 mintValue 字段等于调用的 msg.value,但是 mintValue 将始终仅设置为 txBaseCost,这意味着检查将始终失败。此外,requestL2TransactionDirectl2Value 在合约内部 固定为 0,这意味着 l2Contract 将永远不会收到任何 ETH。因此,无法成功将 WETH 转移到使用 ETH 作为基础 token 的 ZkStack 链。

对于具有自定义 gas token 的 ZkStack 链的 relayTokens 函数中也存在类似的问题。在要桥接的 token 是 gas token 的情况下,只有 支付交易成本所需的金额将被转移,因为 relayTokens 函数的 amount 参数将被忽略。

考虑根据 L1 到 L2 桥接 的指南修改 ZkStack 链上的实现。对于 ZkStack_AdapterZkStack_CustomGasToken_Adapter 合约,这将需要将 mintValue 设置为随对 requestL2TransactionDirect 的调用转移的总值,即 txBaseCost + amount。还应修改 l2Value 以反映要转移到 l2Contract 的值。

更新: 已在 pull request #739 的提交 8a05161 中解决。

低风险

更改 crossDomainAdmin 将禁止挂起的操作

ForwarderBase 合约具有 setCrossDomainAdmin 函数,该函数更改 crossDomainAdmin 状态 变量。此变量用于授予对合约中几乎所有函数的访问控制,包括那些用于完成 L1->L3 消息传递和资产转移的函数。调用 setCrossDomainAdmin 时,假设没有需要传递到另一个链的未完成操作。这是因为当操作到达 L2 时,这些操作的原始发送者是旧的 crossDomainAdmin,并且将被阻止进一步继续。

为了提高对此类行为的认识,请考虑在 setCrossDomainAdmin 函数中记录此边缘情况。

更新: 已在 pull request #729 的提交 4ba3439 中解决。

不正确的类型转换

ERC7683OrderDepositor 合约中的订单可以通过 _resolve 函数_resolveFor 函数 解决。这两个函数都接收一个订单并将其转换为 ResolvedCrossChainOrder 结构体,该结构体包含 Output[] 类型的 minReceived 成员。但是,在 _resolve_resolveFor 函数内部,minReceived 成员使用强制转换为 uint32block.chainId 初始化,尽管 Output.chainId 成员 的类型为 uint64。这意味着代码将对 chainID 不适合 uint32 的区块链进行恢复,尽管它应该适用于所有 chain ID 低于 type(uint64).max 的区块链。

在初始化 ResolvedCrossChainOrder.minReceived 时,考虑将 block.chainId 强制转换为 uint64 而不是 uint32

更新: 已在 pull request #736 的提交 eee4a75 中解决。该团队表示:

自从审计提交哈希以来,还有更多关于 ERC7683 的建议,因此对诸如 ResolvedCrossChainOrder 之类的结构的某些字段进行了更改。例如,现在,链 ID 表示为 uint256(参见 )。简而言之,在提议的 PR 中,强制转换现在已完全删除。

可能可变的变量被视为不可变

为了将 ERC-20 token 桥接到 L2,ZkStack_AdapterZkStack_CustomGasToken_Adapter 合约首先 批准 相关数量的 token 到 SHARED_BRIDGE,然后 调用 Bridgehub 合约的 requestL2TransactionTwoBridges 函数,他们在其中指定要用作 BRIDGE_HUB.sharedBridge() 的第二个桥。然后,requestL2TransactionTwoBridges 函数 调用第二个桥上的 bridgehubDeposit 函数,该函数 从最初调用 Bridge Hub 的合约转移 token

但是,适配器给出的 token 批准始终针对不可变的 SHARED_BRIDGE 地址,而指定为第二个桥的 BRIDGE_HUB.sharedBridge() 地址返回 sharedBridge 变量 的当前值。虽然该变量将来不太可能更改,但 它仍然是可能的,如果发生这种情况,则任何 ZkStack 适配器都将无法桥接 token,因为授权将给予先前的 sharedBridge 地址。

考虑删除 SHARED_BRIDGE 变量,并始终通过 BRIDGE_HUB.sharedBridge() 访问 sharedBridge 变量。

_更新: 已确认,未解决。已决定,如果 sharedBridge 变量发生更改,则重新部署适配器,并且不调用 BRIDGE_HUB.sharedBridge() 以节省 gas。为了进一步降低 gas 成本,在提交 3d260d7 中,BRIDGE_HUB.sharedBridge() 调用已被 SHARED_BRIDGE 变量访问替换。该团队表示:_

这是真的,但我们认为这可能是我们可能希望具有这种行为的少数情况之一。这是因为我们经常调用这些适配器,并且由于它们部署在 L1 上,因此它们可能会变得非常昂贵。因此,尽可能降低 gas 成本尤为重要,这也是我们采取的快捷方式之一。 [...] 特别是,如果桥 _确实 发生变化,适配器调用将只会恢复,我们需要重新部署。需要明确的是,如果共享桥确实发生变化,我们将需要部署一个新的适配器。希望从长远来看,重新部署的成本将低于在每个新交易的适配器上进行额外调用的gas成本。_

注释 & 附加信息

重复代码

ForwarderBase 合约的 _setCrossDomainAdmin internal 函数在只有管理员才能调用的 setCrossDomainAdmin 外部函数 内部 被调用。但是,这两个函数都实现相同的逻辑并发出相同的事件。

考虑从外部函数的主体中删除重复的逻辑,并将逻辑和事件发出保留在内部定义中。

更新: 已在 pull request #728 的提交 1ecbb3f 中解决。

无法从转发器合约中删除适配器

ForwarderBase 合约的 updateAdapter 函数允许将新的目标链 ID 与适当的适配器链接,以用于跨链转发。但是,如果链在将来某个时间变得不受支持,则 chainId 的目标不能设置为 address(0),也不能从 chainAdapters 映射中删除它。

考虑添加逻辑以删除给定目标链的适配器。

更新: 已在 pull request #728 的提交 b621adf 中解决。

未使用的结构体

ERC7683Across.sol 中声明的 AcrossDestinationFillerData 结构体 在代码库中的任何地方都未使用。

为了提高代码库的整体清晰度、意图性和可读性,请考虑使用或删除任何当前未使用的结构体。

更新: 已在 pull request #744 的提交 9f54455 中解决,方法是在 SpokePool 合约的 fill 函数中使用 AcrossDestinationFillerData 结构体。

误导性的文档

在整个代码更新: 已在 pull request #728 的提交 467a207 中解决。

注释改进建议

在整个代码库中,发现了多个可以改进注释的机会:

  • 这个注释专门提到了 WorldChain,但它也可能适用于其他区块链。考虑修改它,使其更通用。
  • 在这个注释中,L1 和 L2 的引用是小写的,这与其余使用大写字母引用的注释不一致。考虑将 "l1" 更改为 "L1",将 "l2" 更改为 "L2",以保持一致性。

考虑实施上述注释改进建议,以提高代码库的整体清晰度和可读性。

更新: 已在 pull request #728 的提交 30b1ecbpull request #646 的提交 f39418a 中解决。

缺少索引的事件参数

在整个代码库中,发现了多个没有索引参数的事件实例:

为了提高链下服务搜索和过滤特定事件的能力,请考虑索引事件参数

更新: 已在 pull request #728 的提交 e13dd1f 中解决。

命名建议

在整个代码库中,发现了多个可以改进命名的地方:

考虑重命名上面指定的变量以提高代码可读性。

更新: 已在 pull request #728 的提交 03a0d1a 中解决。

文件名和合约名不匹配

ERC7683Across.sol 文件名与 ERC7683Permit2Lib 库名不匹配。

为了使开发人员和审查人员更容易理解代码库,请考虑重命名文件以匹配库名。

更新: 已在 pull request #728 的提交 3df9450 中解决。

可以使用常量来表示 L2 上的 ETH

在 ZKStack 适配器中,当 ETH 用作 gas 代币时,address(1) 用于表示 ETH。在这两个合约中,该值被多次使用,可以声明为一个常量,类似于 ETH_TOKEN_ADDRESS 常量,它在 Bridgehub 合约中被使用

为了提高代码库的可读性,请考虑将 address(1) 声明为一个具有描述性名称的常量。

更新: 已在 pull request #728 的提交 813bf95 中解决。

使用了不明确的 Pragma 指令

为了清楚地识别合约将使用哪个 Solidity 版本进行编译,pragma 指令应该是固定的,并且在文件导入中保持一致。Ovm_WithdrawalHelper.sol 文件具有 pragma 指令 pragma solidity ^0.8.0;,并导入了 WithdrawalHelperBase.sol 文件,该文件具有不同的 pragma 指令 - ^0.8.19

这样做的意图似乎是将版本固定为低于 v0.8.20,这是引入 PUSH0 操作码的地方。但是,^0.8.19 将允许使用任何大于或等于该版本(且低于 v0.9.0)的版本。此外,Arbitrum_WithdrawalHelper 合约一条注释,指出 Arbitrum 仅支持 v0.8.19,但引用的文档另有说明,实际上表明 Arbitrum 现在支持 PUSH0 操作码。

考虑审查 pragma 指令以使其保持一致。如果有任何理由认为该版本应低于 v0.8.20,请使用 <= 代替 ^

更新: 已在 pull request #728 的提交 c335be2 中解决。

不正确的接口实现

ERC7683OrderDepositor 合约 确实实现了 ERC-7683 中声明的 IOriginSettler 接口。但是,最初在接口中指定的参数名称与 ERC7683OrderDepositor 合约中使用的名称之间存在几个不一致之处:

  • openFor 函数的 fillerData 参数名称与 IOriginSettler 接口中的名称不匹配(originFillerData)。
  • resolveresolveFor 函数的返回参数应在 IOriginSettler 中命名,就像它们在实现中存在一样。

考虑使接口和实现彼此保持一致,以提高代码可读性。

更新: 已在 pull request #728 的提交 88ae26a 中部分解决。该团队表示:

我们最终只解决了这个问题的第一点。这样做的动机是,我们希望 resolveresolveFor 不要在接口级别定义返回变量。附加的提交解决了第一点,但没有解决第二点。

客户端报告

填充截止日期后无法存款

中继哈希的可预测性使填充者能够在目标链上填充存款,然后再在源链上创建存款。当在源链上创建存款时,当前的区块时间戳会经过验证,因此存款必须在未来的填充截止日期之前存入。

但是,可能会出现这样一种情况,即存款已发生预填充,但在填充截止日期过去之前尚未存入。例如,这可能是由于区块链高度拥堵或区块链停止导致。在这种情况下,将无法再创建存款,这将导致预填充者的资产损失。

更新: 该团队在 pull request #870 的提交 3b21fea 中解决了这个问题,允许在填充截止日期过后进行存款。作为该修复的副作用,现在可以在填充截止日期过后创建一个尚未预填充的存款。这将导致存款人临时转移资产,但之后将退还资产。

建议

跨链调用可能因资产不足而失败

从 L1 到 L3 的通信需要使用中间 L2 上的适配器合约。Across 团队表示,这些调用的适配器合约将基于当前的适配器合约,但应牢记一些关键差异。

Router_Adapter 合约支持从 L1 发送跨链消息到 L2,然后再发送到 L3。例如,在 Arbitrum_Adapter 合约中,relayMessagerelayTokens 函数在 函数逻辑 中包含 L2 执行所需的 gas。这假设调用合约(L1 上的 HubPool)持有足够的 ETH 来支付此 gas 成本。这是通过 relayMessagerelayTokens 函数中的最低余额检查来强制执行的。

但是,在 L2 上,转发器合约是适配器合约的调用者。ForwarderBase 合约的 relayMessage 函数没有检查以确保存在执行 L2 到 L3 调用所需的 gas 代币数量。此外,目前似乎没有任何自动化逻辑来为转发器合约提供执行 L3 交易的 gas 所需的资产。

鉴于以上情况,建议确保 L2 上的适配器合约针对目标 L3 定制,同时考虑到目标链的 gas 代币和桥接逻辑。还建议确保转发器合约始终有足够的资产来成功执行 relayMessagerelayTokens 函数。

可以实施更彻底的测试

为了提高代码库的质量和安全性,有必要实施全面的测试套件。这应包括单元测试(用于隔离测试每个组件)和集成测试(用于确保系统不同部分之间以及系统与外部组件之间的交互产生所需的结果)。可以通过在特定区块上 fork 一个区块链并与已部署和配置的合约(例如桥)进行交互来实现集成测试。在整个审计过程中,发现了多个问题,表明当前的测试套件不足。

考虑为代码库实施全面的测试套件。这将有助于确保更好的代码质量,并大大减少将来代码库中出现的问题数量。

结论

经过审计的代码库在 Across 协议中引入了对 L3 区块链的支持,并根据 ERC-7683 标准进行了新的更改。此外,还添加了几个新的适配器,引入了对新区块链的支持,并修改了与计算 SpokePool 中存款 ID 相关的逻辑。

考虑到协议的复杂性和它所依赖的外部组件数量,我们认为代码库将从实施集成测试中获得极大的好处,这将有助于识别与不正确使用桥和消息传递机制相关的许多错误。我们认为,在此次参与过程中发现的大多数中等和较高严重程度的问题本可以在开发过程中通过适当的集成测试套件轻松检测到,该套件超越了模拟外部组件。此外,鉴于 ERC-7683 标准目前正在进行修改,并且在审计后可能会对其进行新的更改,我们建议确保代码符合标准的最终版本。

Risk Labs 团队在整个参与过程中一直非常有帮助,及时回答了我们所有的问题,并彻底解释了协议的细节。

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

0 条评论

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