本次审核了across-protocol/contracts代码仓库,主要关注L3支持、ZkStack支持、可预测的中继哈希、支持最新版本的ERC-7683以及World Chain支持。发现了多个安全问题,包括缺少访问控制、错误的函数调用、不正确的ETH传输处理,以及潜在的重放攻击等。建议改进代码,增加测试,并确保代码符合最新的ERC-7683标准。
SpokePool
的fill
函数执行格式错误的调用](#spokepools-fill-function-performs-malformed-call)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 仓库。
范围包括五个部分,如下所示。
以下文件在范围内,于提交 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 #629 中 contracts/SpokePool*.sol
文件直到提交 6e86b70 所做的所有更改。
以下文件在范围内,于提交 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
对提交 108be77 中的以下文件所做的更改在范围内:
contracts/
├── SpokePool.sol
├── erc7683/
│ ├── ERC7683.sol
│ ├── ERC7683Across.sol
│ ├── ERC7683OrderDepositor.sol
│ └── ERC7683OrderDepositorExternal.sol
└── interfaces/
└── V3SpokePoolInterface.sol
以下文件在范围之内,直到提交 51c45b2 审计了在 PR #646 中以及直到提交 d4416cd 审计了在 PR #647 中,对以下文件所做的更改:
contracts/
├── WorldChain_SpokePool.sol
└── chain-adapters/
└── WorldChain_Adapter.sol
Across 是一种基于意图的跨链桥接协议,允许用户在不同的区块链之间快速转移他们的 token。有关协议如何工作的更多详细信息,请参阅 我们之前的审计报告之一。
到目前为止,Across 协议仅支持直接与以太坊通信的区块链,例如 L2 网络。为了与其他区块链上部署的 SpokePool 进行通信,部署在以太坊上的 HubPool 使用了包含允许它发送跨链消息的逻辑的 adapter。但是,它无法与通过 L2 网络间接与以太坊通信的区块链进行通信。在本审计报告中,此类区块链将被称为 L3 区块链,添加到仓库中的最新合约引入了对它们的支持。
为了支持 L3 区块链,添加了两组合约:
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。
引入了两个新的 adapter:ZkStack_Adapter
和 ZkStack_CustomGasToken_Adapter
。ZkStack_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
标准已提出新的变更。在这方面,引入了两种新的订单类型:GaslessCrossChainOrder
,旨在脱链创建,以及 OnchainCrossChainOrder
,可以直接链上使用。表示这两种类型的 Struct 都可以包含特定于实现的数据,然后用于构造 ResolvedCrossChainOrder
Struct,其中包含订单 fillers 所需的信息。无论何时打开任何类型的订单,都必须在事件中发出此 Struct。
此范围部分的合约中所做的更改实现了 ERC-7683
标准的新要求。
World Chain 实现了 Circle 的桥接 USDC 标准,允许将来将桥接的 USDC 升级为 Circle 发行的原生 USDC。因此,L1 和 L2 标准桥都不能用于 USDC 存款和取款。
此范围部分的 pull request 使用部署在以太坊和 World Chain 上的特殊 USDC 桥,以便在 HubPool 和 WorldChain_SpokePool
之间正确桥接 USDC token。
Across 协议依赖于许多不同的外部组件,例如不同区块链之间的桥和消息传递机制。此外,本次审计仅限于整个代码库的一部分。因此,审计是在一定的信任假设下进行的。
在整个审计过程中,我们假设范围内合约交互的所有合约都能正常工作。特别是,我们假设桥按预期工作并正确地桥接区块链之间的资产,并且在 HubPool 和 SpokePool 合约上调用的 view
函数(例如 tokenBridges
、remoteL1Tokens
和 poolRebalanceRoute
)返回正确的结果。我们还假设只会桥接桥和目标区块链都支持的资产。
此外,我们还假设 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_LIMIT
和 L1_GAS_TO_L2_GAS_PER_PUB_DATA_LIMIT
)设置为允许在每个受支持的区块链上正确执行所有交易的值。
多个新引入的特权角色也在本次审计的范围内:
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 中已解决。
SpokePool
的 fill
函数执行格式错误的调用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 中已解决。
WithdrawalHelperBase
和 ForwarderBase
合约旨在部署在 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。
考虑向 ForwarderBase
和 WithdrawalHelperBase
合约添加 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 相同的格式。
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
函数将调用分组在一起。
ForwarderBase
合约 包含几个只能通过源自 crossDomainAdmin
的跨链消息调用的函数。这些函数包括 setCrossDomainAdmin
和 updateAdapter
函数。ForwarderBase
合约和从中继承的合约预计通过部署在 L1 上的 Router_Adapter
合约 进行通信。
但是,Router_Adapter
不提供调用 forwarder 合约的任何函数(除了 relayMessage
和 relayTokens
)的方式。这意味着如果需要更改 crossDomainAdmin
或更改 forwarder 合约使用的 adapter,只能通过用提供此类功能的另一个 adapter 替换 Router_Adapter
来完成。
考虑实现允许通过 Router_Adapter
合约调用 forwarder 合约的其他特权函数的逻辑。或者,考虑实现专用的 adapter,这将能够从 HubPool 调用这些特权函数。
更新: 在 pull request #665 中已解决。团队表示:
我们仍在决定如何与
ForwarderBase
和WithdrawalHelper
合约进行通信,但是,目前,我们认为对这些合约的升级将很少见。考虑到这一点,我们有几种方法来解决此问题:- 在假设我们只需要在初始化新的 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 中解决。_
ERC7683OrderDepositorExternal
合约实现了 _deposit
函数来完成 Across V3 存款的创建。为此,该函数在订单详情中指定的 inputToken
上 调用 了 safeIncreaseAllowance
函数。此机制将适用于任何 token,其假设是整个授权将由 SpokePool 在depositV3
函数调用 中使用。safeIncreaseAllowance
函数也用于 ZkStack_Adapter
和 ZkStack_CustomGasToken_Adapter
合约中,以及其他一些适配器,如 ZkSync_Adapter,这些适配器不在本次审计范围内。
但是,如果出于任何原因,在批准后未使用整个授权,则任何进一步尝试使用禁止从非零值到非零值进行任何批准更改的 token(如 USDT)的 safeIncreaseAllowance
最终都会失败。作为实际影响的一个例子,问题 M08 的第二个例子可能会产生一个场景,即后续使用 USDT 作为自定义 gas token 的调用将失败,从而阻止整个 ZkStack_CustomGasToken_Adapter
的功能。
考虑使用 SafeERC20
库的 forceApprove
函数,以与在从非零值到非零值的批准时恢复的 token 兼容。
更新: 已在 pull request #734 的提交 ea59869 中解决。
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
结构体的 exclusiveRelayer
和 depositNonce
成员也是如此,它们 未包含 在其哈希的计算中。此外,用于创建 witness
的 CROSS_CHAIN_ORDER_TYPE
变量包含 GaslessCrossChainOrder
结构体 的不正确编码,因为 originChainId
成员 被指定为 uint32
类型,而不是 uint64
。
考虑更正上述错误,以保持与 EIP-712
和 Permit2
的兼容性。
更新: 已在 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 中解决。
ZkStack_Adapter
的 relayTokens
函数 旨在促进从以太坊主网上的 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
函数中,发送的 value
是 amount + txBaseCost
,而 mintValue
仅仅是 txBaseCost
。
requestL2TransactionDirect
函数 要求 mintValue
字段等于调用的 msg.value
,但是 mintValue
将始终仅设置为 txBaseCost
,这意味着检查将始终失败。此外,requestL2TransactionDirect
的 l2Value
在合约内部 固定为 0,这意味着 l2Contract
将永远不会收到任何 ETH。因此,无法成功将 WETH 转移到使用 ETH 作为基础 token 的 ZkStack 链。
对于具有自定义 gas token 的 ZkStack 链的 relayTokens
函数中也存在类似的问题。在要桥接的 token 是 gas token 的情况下,只有 支付交易成本所需的金额将被转移,因为 relayTokens
函数的 amount
参数将被忽略。
考虑根据 L1 到 L2 桥接 的指南修改 ZkStack 链上的实现。对于 ZkStack_Adapter
和 ZkStack_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
成员使用强制转换为 uint32
的 block.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_Adapter
和 ZkStack_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 中解决。
在整个代码库中,发现了多个可以改进注释的机会:
考虑实施上述注释改进建议,以提高代码库的整体清晰度和可读性。
更新: 已在 pull request #728 的提交 30b1ecb 和 pull request #646 的提交 f39418a 中解决。
在整个代码库中,发现了多个没有索引参数的事件实例:
WithdrawalHelperBase.sol
中的 SetXDomainAdmin
事件ZkStack_Adapter.sol
中的 ZkStackMessageRelayed
事件ZkStack_CustomGasToken_Adapter.sol
中的 ZkStackMessageRelayed
事件为了提高链下服务搜索和过滤特定事件的能力,请考虑索引事件参数。
更新: 已在 pull request #728 的提交 e13dd1f 中解决。
在整个代码库中,发现了多个可以改进命名的地方:
CROSS_CHAIN_ORDER_TYPE
变量 可以重命名为 GASLESS_CROSS_CHAIN_ORDER_TYPE
。CROSS_CHAIN_ORDER_EIP712_TYPE
变量 可以重命名为 GASLESS_CROSS_CHAIN_ORDER_EIP712_TYPE
。CROSS_CHAIN_ORDER_TYPE_HASH
变量 可以重命名为 GASLESS_CROSS_CHAIN_ORDER_TYPE_HASH
。_callDeposit
函数的 exclusivityDeadline
参数 可以重命名为 exclusivityPeriod
,以便与 AcrossOrderData
结构体保持一致。我们还建议确保 SpokePool
合同中的代码与此更改保持一致(即,该参数确实被视为一段时间,而不是时间戳)。考虑重命名上面指定的变量以提高代码可读性。
更新: 已在 pull request #728 的提交 03a0d1a 中解决。
ERC7683Across.sol
文件名与 ERC7683Permit2Lib
库名不匹配。
为了使开发人员和审查人员更容易理解代码库,请考虑重命名文件以匹配库名。
更新: 已在 pull request #728 的提交 3df9450 中解决。
在 ZKStack 适配器中,当 ETH 用作 gas 代币时,address(1)
用于表示 ETH。在这两个合约中,该值被多次使用,可以声明为一个常量,类似于 ETH_TOKEN_ADDRESS
常量,它在 Bridgehub
合约中被使用。
为了提高代码库的可读性,请考虑将 address(1)
声明为一个具有描述性名称的常量。
更新: 已在 pull request #728 的提交 813bf95 中解决。
为了清楚地识别合约将使用哪个 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
)。resolve
和 resolveFor
函数的返回参数应在 IOriginSettler
中命名,就像它们在实现中存在一样。考虑使接口和实现彼此保持一致,以提高代码可读性。
更新: 已在 pull request #728 的提交 88ae26a 中部分解决。该团队表示:
我们最终只解决了这个问题的第一点。这样做的动机是,我们希望
resolve
和resolveFor
不要在接口级别定义返回变量。附加的提交解决了第一点,但没有解决第二点。
中继哈希的可预测性使填充者能够在目标链上填充存款,然后再在源链上创建存款。当在源链上创建存款时,当前的区块时间戳会经过验证,因此存款必须在未来的填充截止日期之前存入。
但是,可能会出现这样一种情况,即存款已发生预填充,但在填充截止日期过去之前尚未存入。例如,这可能是由于区块链高度拥堵或区块链停止导致。在这种情况下,将无法再创建存款,这将导致预填充者的资产损失。
更新: 该团队在 pull request #870 的提交 3b21fea 中解决了这个问题,允许在填充截止日期过后进行存款。作为该修复的副作用,现在可以在填充截止日期过后创建一个尚未预填充的存款。这将导致存款人临时转移资产,但之后将退还资产。
从 L1 到 L3 的通信需要使用中间 L2 上的适配器合约。Across 团队表示,这些调用的适配器合约将基于当前的适配器合约,但应牢记一些关键差异。
Router_Adapter
合约支持从 L1 发送跨链消息到 L2,然后再发送到 L3。例如,在 Arbitrum_Adapter
合约中,relayMessage
和 relayTokens
函数在 函数逻辑 中包含 L2 执行所需的 gas。这假设调用合约(L1 上的 HubPool)持有足够的 ETH 来支付此 gas 成本。这是通过 relayMessage
和 relayTokens
函数中的最低余额检查来强制执行的。
但是,在 L2 上,转发器合约是适配器合约的调用者。ForwarderBase
合约的 relayMessage
函数没有检查以确保存在执行 L2 到 L3 调用所需的 gas 代币数量。此外,目前似乎没有任何自动化逻辑来为转发器合约提供执行 L3 交易的 gas 所需的资产。
鉴于以上情况,建议确保 L2 上的适配器合约针对目标 L3 定制,同时考虑到目标链的 gas 代币和桥接逻辑。还建议确保转发器合约始终有足够的资产来成功执行 relayMessage
和 relayTokens
函数。
为了提高代码库的质量和安全性,有必要实施全面的测试套件。这应包括单元测试(用于隔离测试每个组件)和集成测试(用于确保系统不同部分之间以及系统与外部组件之间的交互产生所需的结果)。可以通过在特定区块上 fork 一个区块链并与已部署和配置的合约(例如桥)进行交互来实现集成测试。在整个审计过程中,发现了多个问题,表明当前的测试套件不足。
考虑为代码库实施全面的测试套件。这将有助于确保更好的代码质量,并大大减少将来代码库中出现的问题数量。
经过审计的代码库在 Across 协议中引入了对 L3 区块链的支持,并根据 ERC-7683
标准进行了新的更改。此外,还添加了几个新的适配器,引入了对新区块链的支持,并修改了与计算 SpokePool 中存款 ID 相关的逻辑。
考虑到协议的复杂性和它所依赖的外部组件数量,我们认为代码库将从实施集成测试中获得极大的好处,这将有助于识别与不正确使用桥和消息传递机制相关的许多错误。我们认为,在此次参与过程中发现的大多数中等和较高严重程度的问题本可以在开发过程中通过适当的集成测试套件轻松检测到,该套件超越了模拟外部组件。此外,鉴于 ERC-7683
标准目前正在进行修改,并且在审计后可能会对其进行新的更改,我们建议确保代码符合标准的最终版本。
Risk Labs 团队在整个参与过程中一直非常有帮助,及时回答了我们所有的问题,并彻底解释了协议的细节。
- 原文链接: blog.openzeppelin.com/ac...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!