本次OpenZeppelin对Across协议的合约仓库进行了差异化审计,重点关注SpokePoolPeriphery合约及其相关组件。审计发现了包括高、中、低风险在内的多个问题,主要集中在智能合约中swap逻辑,签名处理,EIP-712编码,重放攻击等方面,所有发现的问题均已被修复。本次审计旨在提升Across协议的跨链桥功能和用户体验,确保资产转移的安全性和效率。
Type跨链时间线 从 2025-05-15 到 2025-05-26 语言 Solidity 总问题 13 (13 个已解决) 严重问题 0 (0 个已解决) 高危问题 1 (1 个已解决) 中危问题 3 (3 个已解决) 低危问题 3 (3 个已解决) 注释 & 补充信息 6 (6 个已解决)
OpenZeppelin 对 across-protocol/contracts 仓库进行了差异审计,基础版本为提交 7362cd0 (master),头部版本为提交 b84dbfa。
以下文件在审计范围内:
contracts
├── external
│ └── interfaces
│ ├── IERC20Auth.sol
│ └── IPermit2.sol
├── handlers
│ └── MulticallHandler.sol
├── interfaces
│ └── SpokePoolPeripheryInterface.sol
├── libraries
│ └── PeripherySigningLib.sol
└── SpokePoolPeriphery.sol
Across 协议是一种跨链桥,旨在实现跨各种网络的 ERC-20 代币和原生资产的快速且经济高效的转移。 它允许用户(存款人)在源链上锁定资产,然后由中继者在目标链上提供给他们,这些中继者预付自己的资金。 该协议通过发送中继者选择的链上可用的资金,或者在特定链没有足够资金时利用以太坊上的 HubPool 来偿还中继者。 本次审计的重点是一组新的外围智能合约,旨在增强与 Across V3 生态系统交互的功能、灵活性和用户体验。
SpokePoolPeriphery
SpokePoolPeriphery
合约充当 Across 协议面向用户的入口点,显着扩展了启动跨链转移的可用选项。 其核心功能包括:
swapToken
,在指定的外部交易所执行交易以将其转换为 Across 接受的 inputToken
,然后启动桥接存款——所有这些都在一个原子交易中完成。 此功能支持按比例调整输出,如果交换产生的 inputToken
多于用户指定的最小值,则可以按比例增加在目标链上收到的代币数量。transferFrom
msg.value
,则会自动包装成其 WETH 等价物permit
permitWitnessTransferFrom
),用于通过规范的 Permit2
合约进行批量批准和更高级的基于签名的权限receiveWithAuthorization
,用于支持此 ERC-20 扩展的代币SwapProxy
进行隔离的交换执行:为了增强安全性和模块化,所有交换操作都委托给专用的 SwapProxy
合约。 SpokePoolPeriphery
部署此代理并将代币转账给它以进行交换。 然后,SwapProxy
处理对指定交易所或 Permit2 合约的代币批准,并在目标交易所上执行交换 calldata。 最后,输出代币被转移回 SpokePoolPeriphery
合约。MulticallHandler
变更对 MulticallHandler
合约所做的更改包括添加了 makeCallWithBalance
函数,该函数可用于用 MulticallHandler
合约的指定代币余额填充给定的 calldata,并使用此修改后的 calldata 调用目标合约。 每当指定 calldata 时,目标链上到达的代币数量未知时,此功能非常有用,当使用来自 SpokePoolPeriphery
合约的 交换和桥接 功能时,可能会出现这种情况,并且存款人在签署存款数据时不知道交换的输出金额。
值得注意的是,存款人自己负责提供正确的代币和偏移量,应在其中填充余额,请记住余额可以用小于 uint256
的类型表示,并且指定错误的偏移量可能会导致意想不到的后果,例如资金损失。 用户还应记住,makeCallWithBalance
函数不适用于需要提供负代币数量作为参数的交易所,因为它只能用非负余额填充 calldata。 鼓励所有存款人研究 makeCallWithBalance
函数的文档,以了解其所有风险和限制。
PeripherySigningLib
PeripherySigningLib
是一个库,支持 SpokePoolPeriphery
的基于签名的功能。 它的贡献包括:
BaseDepositData
、Fees
、DepositData
和 SwapAndDepositData
结构体提供计算符合 EIP-712 规范的类型化数据哈希的函数。 这确保了一致且安全的签名生成和验证。v, r, s
组件,从而简化主合约逻辑中的签名处理。这些外围合约的引入扩展了 Across 协议的功能,因此,为其安全模型和特定信任假设引入了新的元素:
swapAndBridge
功能依赖于用户(或受信任的前端)指定的外部交易所。 交换期间用户资金的安全性取决于所选交易所的安全性以及所提供的 routerCalldata
的完整性。 受损的交易所或恶意 calldata 可能会导致资金损失。MulticallHandler
指令和 EIP-712 签名消息,因为不正确或恶意的输入可能导致交易失败、资金损失或意外交互。 假设用户仅使用受信任的交易所,指定合理的最小代币输出数量,并提供正确的 EIP-712 签名。 此外,假设他们为 MulticallHandler
合约指定了正确的 calldata,并且他们注意在每次与该合约交互结束时将剩余在该合约中的任何代币转账到他们的帐户。Permit2
合约的安全性和运营完整性的信任。 假设此合约以正确的方式运行。 同样重要的是要注意,SpokePoolPeriphery
合约依赖于在其部署的链上存在 Permit2
合约。 我们假设 SpokePoolPeriphery
合约将仅部署在 Permit2
合约存在的区块链上。swapProxy
使用任意 routerCalldata
盲目调用用户指定的 exchange
,因此恶意的签名者可以将其指向一个合约,例如,该合约进入无限循环或执行返回炸弹攻击,在恢复之前耗尽所有 gas。 如果没有模拟,中继者将承担失败调用的全部 gas 成本(gas 恶意攻击),并且不会获得任何补偿。Permit2.permit
函数的 Nonce 不正确SwapProxy
合约的 performSwap
函数允许使用几种不同的方法为交换提供代币给指定的交易所。 特别是,它允许通过 Permit2
合约批准交换的代币。 为了做到这一点,它将给定的代币数量批准给 Permit2
合约,并调用 Permit2
合约的 permit
函数。
然而,为该调用指定的 nonce对于整个合约来说是全局的,而 Permit2
合约为每个(所有者、代币、支出者)元组存储一个单独的 nonce。 因此,任何尝试使用与第一次 performSwap
函数调用中使用的(代币,支出者)对不同的对,都将由于 nonce 不匹配而恢复。
考虑在 SwapProxy
合约中为每个(代币,支出者)对存储和使用单独的 nonce。
更新: 已在提交 3cd99c4
的 pull request #1013 中解决。
SpokePoolPeriphery
上发生重放攻击SpokePoolPeriphery
合约允许用户将代币存入或交换并存入 SpokePool。 为了做到这一点,资产首先从存款人的帐户转移,可以选择交换为不同的代币,然后最终存入 SpokePool。
可以从存款人的帐户以几种不同的方式转移资产,包括批准,然后transferFrom
调用,通过 ERC-2612 permit
函数批准,然后 transferFrom
,通过 Permit2
合约转移,以及通过 ERC-3009 receiveWithAuthorization
函数转移。 后三种方法需要额外的用户签名,并且可以由任何人代表给定的用户执行。 然而,对于使用 ERC-2612 permit
和 ERC-3009 receiveWithAuthorization
进行存款或交换和存款的签名数据不包含 nonce,因此,用于这些方法的签名可以稍后重放。
如果受害者签署了依赖于 ERC-2612 permit
函数的函数的数据,并且想再次使用相同的方法和代币存款在由 depositQuoteTimeBuffer
参数确定的时间窗口内,则可以执行攻击。 在这种情况下,攻击者可以首先代表受害者批准代币,然后调用 swapAndBridgeWithPermit
函数或 depositWithPermit
函数,提供过去存款或交换和存储的签名,其中包括比批准金额少的代币。
因此,代币将被存入并可能交换,使用来自旧签名的数据,迫使受害者执行意外的交换或将代币桥接到与预期不同的链。 此外,由于攻击消耗了 permit
批准的一部分,因此在存款人再次完全批准代币之前,无法代表存款人使用新签名来存款代币。 在依赖于 ERC-3009 receiveWithAuthorization
函数的函数的情况下,也可以进行类似的攻击,但这需要转移的代币数量与过去的数量相同。
考虑向 SwapAndDepositData
和 DepositData
结构体添加一个 nonce 字段,并在 SpokePoolPeriphery
合约中为每个用户存储一个 nonce,当签名被验证和接受时,应该递增 nonce。
更新: 已在 pull request #1015 中解决。 Across 团队添加了一个 permitNonces
映射,并使用 nonce
字段扩展了 SwapAndDepositData
和 DepositData
。 在 swapAndBridgeWithPermit
和 depositWithPermit
中,合约现在在验证 EIP-712 签名之前调用 _validateAndIncrementNonce(signatureOwner, nonce)
,从而确保每个基于许可的操作只能执行一次。 ERC-3009 路径继续依赖于代币自己的 nonce; 这里的重放需要代币同时实现 ERC-2612 和 ERC-3009,用户在两个签名中重用完全相同的 nonce,并且两者都在狭窄的 fillDeadlineBuffer
内执行。 鉴于这些条件不太可能收敛,因此实际风险可以忽略不计。
Permit2
对交换进行 DoS 攻击SwapProxy
合约包含 performSwap
函数,允许调用者以两种方式执行交换:通过批准或将代币发送到指定的交易所,或者通过 Permit2
合约批准代币。 然而,由于可以提供任何地址作为 exchange
参数,并通过 performSwap
函数的 routerCalldata
参数提供任何调用数据,因此可能会强制 SwapProxy
合约对任意地址执行任意调用。
攻击者可以利用这一点,迫使 SwapProxy
合约调用 Permit2
合约的 invalidateNonces
函数,指定任意的支出者和高于当前 nonce 的 nonce。 因此,给定(代币,支出者)对的 nonce 将被更新。 如果稍后再次调用 performSwap
函数,它将尝试使用后续 nonce,该 nonce 已被攻击者无效化,并且 Permit2
中的代码将由于 nonce 不匹配而恢复。
由于 performSwap
函数是传递给 Permit2
合约的 nonce 被更新的唯一位置,因此在某个交易所交换给定代币的可能性将被永久阻止,这会影响 SpokePoolPeriphery
合约中与交换代币相关的所有函数。 可以为许多不同的(代币,交易所)对执行攻击。
考虑不允许 exchange
参数等于 Permit2
合约地址。
更新: 已在提交 713e76b
的 pull request #1016 中解决。
PeripherySigningLib
库包含某些类型的 EIP-712 编码,以及生成符合 EIP-712 规范的哈希数据的辅助函数。 然而,SwapAndDepositData
结构体的数据类型不正确,因为它包含 枚举类型的 TransferType
成员,EIP-712 标准不支持该成员。
考虑替换用于生成 SwapAndDepositData
结构体数据类型的 TransferType
枚举名称,使用 uint8
以符合 EIP-712。
更新: 已在提交 c9aaec6
的 pull request #1017 中解决。
deposit
将不适用于非 EVM 目标链SpokePoolPeriphery
合约的 deposit
函数允许用户将原生价值存入 SpokePool。 然而,它的 recipient
和 exclusiveRelayer
参数都是 address
类型,并且强制转换为 bytes32
。 因此,无法将包装的原生代币桥接到非 EVM 区块链。
考虑更改 deposit
函数的 recipient
和 exclusiveRelayer
参数的类型,以便允许调用者为存款指定非 EVM 地址。
更新: 已在提交 3f34af6
的 pull request #1018 中解决。
_swapAndBridge
中的整数溢出在 _swapAndBridge
函数中,调整后的输出金额计算为depositData.outputAmount
和 returnAmount
的乘积除以 minExpectedInputTokenAmount
。 如果 depositData.outputAmount * returnAmount
超过 2^256–1
,则交易将在乘法步骤中立即恢复,即使最终除法结果可以容纳。 这种中间溢出对于用户来说是不可见的,他们只能看到没有解释性错误消息的通用失败。
考虑使用 OpenZeppelin 的 Math.mulDiv(a, b, c)
来计算 floor(a*b/c)
而没有中间溢出。 或者,考虑记录可能的溢出场景。
更新: 已通过记录潜在的溢出场景在提交 e872f04
的 pull request #1020 中解决。
目前,每个 DepositData
和 SwapAndDepositData
有效负载必须包含一个硬编码的费用接收者地址,并且在成功存款或交换和桥接后,外围设备会将提交费用支付给该确切地址。 虽然这确保了用户提前确切地知道谁将收到他们的费用,但它也阻止了开放的中继器竞争或在所选的中继器表现不佳或不可用时的回退选项。
考虑在 SwapAndDepositData
中保留显式费用接收者字段选项,但引入“零地址”约定:
msg.sender
作为付款人。recipient
。更新: 已在提交 f2218c0
的 pull request #1021 中解决。
SpokePoolPeriphery
合约的 deposit
函数允许用户将原生价值存入 SpokePool。 虽然可以指定 inputToken
参数,但无法通过此函数存入其他代币。 因此,可以将其重命名为 depositNative
或类似的名称,以明确这一事实。
考虑重命名 deposit
函数,以提高代码库的可读性。
更新: 已在提交 a69ad79
的 pull request #1019 中解决。
在整个代码库中,发现了多个代码优化机会:
permit
调用的“0x”字符串可以用“”替换。MulticallHandler
合约的 makeCallWithBalance
函数 允许用当前的 token 或原生余额替换给定调用数据的指定偏移量。然而,这个函数的目的以及正确的使用方式可能对用户来说并不立即清楚。因此,该函数可以通过额外的文档来说明其目的、局限性和正确用法。可以列出的一个额外的局限性是,这个函数不能填充负余额。因此,将不支持需要输入 token 金额为负的去中心化交易所。swapAndBridge
, swapAndBridgeWithPermit
, swapAndBridgeWithPermit2
, 和 swapAndBridgeWithAuthorization
函数的文档可以提及它们不支持原生 value 作为 swap 的输出 token,因此,只能通过这些函数将非原生 token 存入 SpokePool。PeripherySigningLib
库不包含任何描述其目的、用法或任何相关细节的 NatSpec 注释。如果没有合约级的 NatSpec 注释块,读者和自动文档工具将无法获得关于该库用途或如何与其集成的简明概述。考虑扩展上述实例中的文档,以提高代码库的清晰度。
Update:** 在提交 047283e
的 pull request #1023 中已解决。
在整个代码库中,发现了多个拼写错误的实例:
MulticallHandler.sol
文件的 第 48 行 中,“calldData”应为“callData”。SpokePoolPeripheryInterface.sol
文件的 第 113 行 中,可以删除 "on"。SpokePoolPeriphery.sol
文件的 第 500 行 中,“depositData/swapAndDepositData”可以为“DepositData/SwapAndDepositData”。考虑更正所有拼写错误的实例,以提高代码库的清晰度和可读性。
Update:** 在提交 18296cb
的 pull request #1024 中已解决。
在整个代码库中,发现了多个未使用的代码实例:
SpokePoolPeriphery.sol
文件中,未使用的 InvalidSignatureLength
错误SpokePoolPeripheryInterface.sol
文件中,未使用的 import为了提高代码库的整体清晰度和可维护性,请考虑删除任何未使用的代码实例。
Update:** 在提交 767cb9f
的 pull request #1025 中已解决。
在整个代码库中,发现了多个具有误导性的文档实例:
permit
函数,swapAndBridgeWithPermit
和 depositWithPermit
函数的文档说明会失败。然而,实现与此声明相矛盾,因为在这两个函数中,对 permit
的调用都包含在 try/catch
块中,并且任何失败都会被默默忽略。transferWithAuthorization
函数,而它应该提到 receiveWithAuthorization
函数。SpokePoolPeriphery
合约 和 SpokePoolPeripheryInterface
接口 的文档包含一个过时的注释,声称某些变量未标记为 immutable 或在构造函数中设置,以允许确定性部署。这不再正确,因为变量现在是 immutable 的,并且在构造函数中设置。考虑修复上述实例,以提高代码库的清晰度。
Update:** 在提交 f8f484a
的 pull request #1026 中已解决。
对 periphery 合约进行的审查变更引入了将资产存入 SpokePool 的新可能性。它们使第三方实体能够代表任何提供有效签名的用户存入或 swap-and-deposit 资金。此外,它们可以保护用户在为存款指定不正确的 SpokePool 地址时不会丢失其原生 token。
虽然审计发现了一些与 swap 逻辑和签名处理相关的问题,但发现代码是可靠且组织良好的。感谢 Risk Labs 团队的积极响应并在整个审计过程中回答审计团队的问题。
- 原文链接: blog.openzeppelin.com/pe...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!