SVM Spoke 审计

该文档是对Across协议的SVM Spoke Pool的审计报告,该协议将跨链桥接网络扩展到Solana生态系统。审计期间发现了19个问题,包括高、中、低严重性级别,涉及强制使用Claim Accounts、成本不对称利用、事件日志记录过度使用等,提出了改进建议。

目录

概要

TypeDeFiTimelineFrom 2024-11-26To 2024-12-18LanguagesRust & SolidityTotal Issues19 (10 resolved, 3 partially resolved)Critical Severity Issues0 (0 resolved)High Severity Issues2 (0 resolved, 1 partially resolved)Medium Severity Issues1 (0 resolved)Low Severity Issues9 (3 resolved, 2 partially resolved)Notes & Additional Information7 (7 resolved)

范围

我们审计了across-protocol/contracts仓库,commit 版本为401e24c

以下文件在审计范围内:

 contracts
├── chain-adapters
│   └── Solana_Adapter.sol
programs
├── multicall-handler
│   └── src
│       └── lib.rs
└── svm-spoke
    └── src
        ├── common
        │   ├── mod.rs
        │   └── v3_relay_data.rs
        ├── constants.rs
        ├── constraints.rs
        ├── error.rs
        ├── event.rs
        ├── instructions
        │   ├── admin.rs
        │   ├── bundle.rs
        │   ├── create_token_accounts.rs
        │   ├── deposit.rs
        │   ├── fill.rs
        │   ├── handle_receive_message.rs
        │   ├── instruction_params.rs
        │   ├── mod.rs
        │   ├── refund_claims.rs
        │   ├── slow_fill.rs
        │   └── token_bridge.rs
        ├── lib.rs
        ├── state
        │   ├── fill.rs
        │   ├── instruction_params.rs
        │   ├── mod.rs
        │   ├── refund_account.rs
        │   ├── root_bundle.rs
        │   ├── route.rs
        │   ├── state.rs
        │   └── transfer_liability.rs
        └── utils
            ├── bitmap_utils.rs
            ├── cctp_utils.rs
            ├── deposit_utils.rs
            ├── merkle_proof_utils.rs
            ├── message_utils.rs
            ├── mod.rs
            └── transfer_utils.rs

此外,我们还审查了这些 PR 中引入的变更:- PR 853,commit 为 25fc095 - PR 791,commit 为 5c49410 - PR 810,commit 为 739fb03

系统概述

SVM Spoke Pool 将 Across Protocol 的跨链桥网络扩展到 Solana 生态系统。该程序是对SpokePool.sol的功能性重新实现,专为 Solana 区块链量身定制,并结合了 Solana 兼容性所需的必要调整。它利用 Circle 的跨链传输协议 (CCTP) 来促进与以太坊主网之间的代币和消息桥接。与其基于 EVM 的对应物类似,该 Spoke Pool 在 EVM Hub Pool 的指导下运行,EVM Hub Pool 管理诸如池重新平衡和中继器报销等任务。

在保持与 EVM Spoke Pool 的功能对等的同时,Solana 的实现解决了基本的架构差异:

账户创建和生命周期

  • 需要显式创建账户并为所有存储预留租金。
  • 实施账户关闭模式,以便在不再需要数据时返还租金。

事件处理

  • 利用emit_cpi!宏而不是标准的emit!来克服 Solana 的日志大小限制和截断问题。
  • 确保捕获完整的事件数据以用于跨链通信。

通过 PDA 进行账户验证

  • 使用 PDA 进行金库所有权和状态管理。
  • 使用 PDA 种子进行安全账户验证。
  • 通过 PDA 推导路径强制执行授权关系。

代币账户管理

  • 遵循 Solana 模式,通过关联代币账户 (ATA) 管理代币。
  • 在需要时为填充期间的接收者创建 ATA。

Solana 链适配器

Solana 链适配器弥合了 EVM 和 Solana 环境之间的语义差距。它将 EVM 事件和消息转换为 Solana 指令数据,管理 CCTP 消息编码/解码,并处理以太坊和 Solana 地址格式之间的跨链标识符转换。

对于中继器和数据工作者,该适配器提供了直观的接口,抽象了 Solana 编程模型的复杂性。此抽象确保了现有的 Across Protocol 参与者可以将 Solana 操作无缝集成到其工作流程中。

适配器的设计适应了 Solana 的独特特征,同时保持了协议一致性。更具体地说,它处理:

  • 事件翻译和消息格式化
  • 跨链地址转换
  • 指令数据序列化
  • 账户管理抽象
  • PDA 推导和验证

安全模型和信任假设

SVM Spoke Pool 在 Across Protocol 既定的安全基础上构建,同时适应 Solana 的安全范例。CCTP 集成是跨链通信的支柱。该实现依赖 Circle 的消息证明框架来验证跨链消息并促进代币转账。这种关系产生了对 CCTP 安全模型的依赖,特别是对其消息发送器程序和证明验证机制的依赖。

账户授权通过精心设计的 PDA 层次结构流动。状态 PDA 充当金库管理的核心授权,而跨域管理员控制通过来自以太坊的经过验证的 CCTP 消息到达。此结构确保只有经过授权的实体才能启动状态更改或管理资金。程序验证利用 Anchor 的约束系统来强制执行账户关系和访问控制。

Bundle 验证与更广泛的 Across Protocol 保持一致。Merker 证明验证中继器退款和慢速填充,bundle 执行由以太坊上的 Hub Pool 协调。该实现在整个 bundle 处理过程中保持状态一致性,同时适应 Solana 的账户模型。

特权角色

SVM Spoke Pool 实现了清晰的特权层次结构:

跨域管理员(以太坊主网上的 HubPool):

  • 暂停存款和填充。
  • 管理启用的路由。
  • 设置跨域管理员。
  • 中继根 bundle 以供执行。
  • 紧急删除根 bundle。

状态所有者(Solana 上的多重签名):

  • 执行跨域管理员可用的所有操作。
  • 转移 Spoke Pool 的所有权。

高危

在某些情况下,强制所有中继器使用声明账户

当某个交易叶子中的中继器无法直接收到退款时(例如,由于黑名单导致代币转账失败),协议会使用声明账户 PDA 来处理退款。然而,这种回退机制对同一交易中的其他中继器施加了限制,强制所有中继器都通过 ClaimAccount PDA 声明退款,即使对于他们来说,直接退款是可能的。这会导致以下问题:

  • 存在达到计算预算的风险accrue_relayer_refunds过程涉及find_program_address,这在迭代尝试验证有效 bump 时,计算成本可能很高。由于此过程可用于多个账户,因此交易很可能会回滚。因此,中继器和用户可能无法从该叶子中检索到他们的退款。
  • 退款延迟:中继器可能会在其ClaimAccount PDA 中执行叶子之前立即声明其退款,从而减少了金库的总可用资金。由于协议可能在执行时缺乏足够的流动性,这可能会延迟其他用户的退款。
  • 增加执行开销:执行者负责确保所有中继器都已初始化声明帐户,并承担额外的计算和财务开销,包括支付租金豁免费用。
  • 与流动性相关的回滚:当中继池缺乏足够的流动性时,中继器可以尝试声明退款。这种情况可能会导致交易回滚,而不会提供有意义的错误消息,因为 Hub Pool 不跟踪未偿还的 Spoke Pool 债务。
  • 容易被滥用:由于任何人都能够调用该函数,恶意行为者可以抢先数据工作者,不必要地推迟退款,即使直接退款机制正在运行。此外,恶意的中继器可能会强制使用声明帐户,并延迟其退款声明,直到执行叶子之前,从而故意触发回滚以中断系统操作。

考虑实施逻辑以仅通过ClaimAccount PDA 为明确无法接收直接转账的中继器路由转账。这将优化操作,防止错误并减少开销。此外,考虑增强错误消息传递,以清楚地指示在声明尝试期间 Spoke Pool 中的流动性不足。此更改将确保更平稳的运营并提高用户的清晰度。

更新:pull request #847中部分解决。团队表示:

_我们承认这个问题,并且在最初开发 Solana Spoke 程序时也进行了广泛的考虑,但由于以下原因,已选择不完全引入建议的更改:- 达到计算预算的风险:即使不推迟退款,distribute_relayer_refunds方法也会在内部调用associated_token::get_associated_token_address_with_program_id,这涉及调用find_program_address来检查是否提供了有效的接收者 ATA,因此与distribute_relayer_refunds相比,accrue_relayer_refunds逻辑的成本不应有显着的额外成本。我们还测试过它可以处理最多 28 个中继器退款分配(同时到代币帐户和声明帐户),这高于当前MAX_POOL_REBALANCE_LEAF_SIZE的 25。即使达到此限制,瓶颈也是在发出ExecutedRelayerRefundRoot事件时内部指令大小限制,而不是计算预算。这进一步证实了find_program_address中的每次迭代都会消耗额外的 1500 个计算单元,并且考虑到典型 bump 分配的统计分布(99.9% 最多需要 10 次迭代),即使所有 25 个退款都深度为 10 次迭代(极不可能),也会消耗 375,000 个 CU,这仍然低于允许的最大 1,400,000 限制。- 退款延迟:与中继器立即收到退款的情况相比,我们不认为中继器声明其递延退款会对即将到来的execute_relayer_refund_leaf调用的 Spoke Pool 流动性产生负面影响。根捆绑包中的建议池重新平衡应确保 spoke 具有足够的流动性来支持中继器退款、慢速填充、取消的存款和对 HubPool 的还款。spoke 可能没有足够流动性的唯一可行情况是重新平衡代币的接收时间晚于接收桥接的relay_root_bundle调用,但如果由于某些代币帐户被阻止而导致中继器退款被递延,则不应进一步恶化。该协议旨在应对在执行中继器退款时流动性不足的情况,这通常发生在 EVM 链上,原因是代币桥和消息桥的时间不同步。它只是延迟了将代币到达目标 Spoke Pool 之前中继器获得退款所需的时间。- 增加执行开销:声明帐户初始化可以自动化,租金豁免费用在声明退款时收回。所提出的修复程序还通过将参数移动到帐户来优化退款声明相关指令,因此可以从交易编译的压缩中受益(多个指令中的帐户可能会使用对同一帐户的索引引用)。这允许在批量处理声明指令时在一个交易中声明最多 21 笔退款。- 与流动性相关的回滚:我们不认为在声明应计退款时需要引入额外的金库余额检查,因为代币程序的默认TokenError::InsufficientFunds错误应该足够清楚。- 容易被滥用:我们认识到,在最坏的情况下,数据工作者可以回退到初始化声明帐户,执行递延退款并代表所有可以收到代币帐户退款的中继器进行声明。这可以通过 UMIP 中的规范来进一步缓解,即退款必须按叶子中的价值进行排序,以便数据工作者可以选择不执行低价值退款的叶子,这些退款存在更大的恶意攻击风险。- 考虑实施逻辑以仅通过ClaimAccount PDA 为明确无法接收直接转账的中继器路由转账:我们最初尝试实施这种选择性逻辑,但 Solana 中转账 CPI 中缺少 try-catch 模式使以这种效率不高的方式执行此操作成为不可能。作为替代方案,我们考虑复制代币程序转账指令中的相同检查,以捕获代币帐户退款可能被阻止的情况,但认为这会引入过多的开销,并且在处理可能阻止转账的 Token-2022 程序扩展时可能会变得更加复杂。考虑到以上情况,我们已选择仅应用有限的修复程序,方法是将中继器退款声明指令的组成方式优化,方法是将其参数移动到帐户数据,这应减少声明中继器退款时的执行开销。_

利用跨链的成本不对称性,对退款执行施加高 Gas 成本

Across 协议是一种跨链桥,旨在快速有效地桥接受支持的源链和目标链之间的 ERC-20 代币。用户(称为存款人)通过将其代币存入源链上的协议来启动该过程。然后,中继器使用自己的资金在目标链上履行这些存款,并在扣除费用后交付指定的金额。为了补偿中继器,协议要么利用中继器选择的链上的可用资金,要么在特定链上没有足够流动性时访问以太坊上的HubPool

然而,恶意用户可能会利用链之间的成本不对称性,廉价地创建大量退款叶子,这些叶子必须在高成本链(例如以太坊)上执行,从而对执行这些叶子的任何人(通常是作为公共物品的数据工作者)施加巨大的财务负担。

详细的攻击步骤:

  1. 垃圾低成本的存款: 攻击者重复在低成本链(例如 Solana 或廉价 L2)上进行最小价值的存款,其中交易费用非常低。每个存款都将攻击者设置为独家中继器,确保只有他们才能在排他期内填写该存款。
  2. 在另一个廉价链上的廉价的填写: 想法是目标链也是低成本的。由于攻击者是独家中继器,他们可以填写自己的存款。这种填写过程也很便宜。
  3. 指定在高成本链上退款: 当攻击者填写这些存款时,他们指定将他们的退款发送到高成本链(例如以太坊主网)。
  4. 多个地址: 攻击者可以在这些填写中包含多个退款地址。地址的数量需要创建大量的叶子,每个叶子包含大约 25 个退款地址。

攻击者的最终结果:大量的叶子,每个叶子包含大量的退款(可忽略的和真实的退款的混合),这些退款在以太坊上执行成本很高。执行中继器退款叶子需要大量的资源,因为它涉及验证 Merkle 证明、执行多个代币转账以及运行各种验证。

数据工作者或这些叶子的任何其他执行者都面临着双输的局面:

  • 执行叶子并产生很高的 gas 成本,这是不可持续的。
  • 留下未执行的退款,导致这些叶子中的合法(但不幸地批量处理的)退款仍然未支付。
    • 在这种情况下,合法的重放者(他们希望在高成本链上支付退款)和与这些垃圾邮件条目共享叶子的用户(包括存款已过期的用户)除非有人支付以太坊上的执行成本,否则无法访问他们的合法退款。

除了这种直接的链上执行成本之外,数据工作者还必须处理广泛的链下任务,例如构建大型 Merkle 树、管理和存储更多事件数据,以及可能写入数据可用性层。所有这些都为数据工作者增加了大量的运营成本。此外,大量的微小、垃圾邮件式存款会使每个人的协议变得混乱。依赖于解析事件的重放者、区块浏览器和其他第三方现在面临着更难以区分真实现象与噪音的问题。

为了解决这个问题,请考虑实施以下措施:

  • 在低成本链上需要最低存款价值,以减少大规模垃圾邮件的可行性,从而确保仅处理具有经济意义的交易。
  • 更新 UMIP 以忽略或过滤掉垃圾邮件交易,确保只有合法交易才有资格执行。

中危

过度使用 emit_cpi! 宏进行事件日志记录

在整个代码库中,emit_cpi! 宏被广泛用于日志记录事件,即使在标准 emit! 宏也足够的情况下。虽然 emit_cpi! 提供了诸如通过利用 Solana 的指令数据存储来防止日志截断的好处,但其过度使用会引入不必要的计算开销。

emit_cpi! 宏的工作原理是通过使用事件数据执行自调用,从而确保日志完整无损,而不管大小如何。但是,这种方法存在权衡,因为创建和调用新指令会消耗交易中额外的计算预算。Solana 的交易具有严格的计算预算限制,过度使用 emit_cpi! 可能会导致超过这些限制,从而可能影响程序的功能。

为了缓解这些问题,请考虑将 emit_cpi! 的使用限制为以下情况:

  • 可能超过日志大小限制 (10 KB) 并且对于保留至关重要的事件。
  • 可能在恶意情况下遭受截断的事件。
  • 与其他日志一起发出且可能发生截断的事件。

对于非敏感事件或发生在假定善意的管理员操作之后的事件,emit! 宏更合适。这样做可以确保有效利用计算资源,而不会损害关键日志的可靠性。

更新:已确认,未解决。团队声明:

我们已决定不将某些 emit_cpi! 事件替换为原生 emit! 事件。此决定源于攻击者在调用 svm-spoke 函数之前预先添加指令填充日志的潜在风险,这可能导致日志被删除。为了减轻此风险,我们选择继续使用 emit_cpi!。虽然在某些情况下,可能可以安全使用原生 emit! 事件,但我们选择为所有事件保持 emit_cpi! 的一致性。此外,最好始终在所有上下文中使用相同的技术查询事件,而不需要某些代码使用 emit_cpi!,而其他代码使用 emit! 逻辑。

低危

错误处理超过1年的独占期限

exclusivity_parameter 可以作为存款的参数传递,它指定一个窗口,其中只有独家转播者将能够填写订单。如果该参数小于最大独占期(目前设置为1 年),则指定的时间窗口会添加到当前时间。如果窗口设置为大于 1 年,则独占截止日期设置为提供的值。如果此值小于当前时间,则它将具有与未设置独占参数相同的效果。

考虑使用错误处理此问题,以避免用户认为他们正在为独占设置一个大的截止期限,但实际上,他们允许任何转播者填写他们的订单。

更新:已确认,未解决。团队声明:

我们已决定确认问题,但不实施建议的更改。 我们希望模仿 EVM SpokePool 中的行为,当 exclusivity_parameter 小于当前时间且大于 1 年时,我们不会恢复,导致没有独占参数设置。 在合约级别阻止此配置被认为是没必要的。 为了参考,这是在 EVM 中提出 exclusivityParameter 的位置:https://github.com/across-protocol/contracts/pull/670。 相反,UI 将显示警告并阻止用户在 dApp 中提交此类错误配置。 值得注意的是,此问题只会影响打算设置超过一年的独占期的存款人,这不是协议的有效或预期用途。 因此,不允许此大小的值对任何用户都没有影响。

SVM Spoke Pool 中缺少 SpeedUpV3DepositFillV3RelayWithUpdatedDeposit 功能

speedUpV3Deposit 函数(在 EVM Spoke Pool 中实现)允许用户向中继器发出更新的输出金额、接收者地址和消息的信号。 但是,SVM Spoke Pool 中不存在此功能。 同样,fillV3RelayWithUpdatedDeposit 函数也未在 SVM Spoke Pool 中找到,它是用于填充存款的 fillV3Relay 函数的替代方案。 这两个函数的缺失会在 EVM 和 SVM Spoke Pool 之间产生功能不一致。 因此,这种不一致可能会使用户感到困惑,他们希望跨平台具有统一的功能,并会降低管理存款和转播的操作灵活性。

考虑在 SVM Spoke Pool 中实现 speedUpV3DepositfillV3RelayWithUpdatedDeposit 功能,以与 EVM Spoke Pool 保持一致。 或者,明确记录它们的缺失,以有效地管理用户的期望。

更新:pull request #848 中部分解决。团队声明:

我们承认这个问题,并将通过在代码中添加注释来解决它,以解释为什么我们决定放弃对 Solana 上加速的支持,考虑到以下限制:加速并不常用,尤其是在存款到期后。以前,它们用于更新存款参数,但现在到期有效地处理了它。由于不兼容的密码方案,在 Solana 上支持加速过于复杂。Solana 钱包 (Ed25519) 和以太坊 (ECDSA secp256k1) 使用不同的签名算法:虽然来自以太坊钱包的 ECDSA 签名可以在 Solana 上验证,但 Solana 钱包无法生成可以在以太坊上验证的 ECDSA 签名。加速需要存款人的签名在源链和目标链上都进行验证,这需要共享签名方案——这是 Solana 和以太坊之间不可行的。

在紧急情况下,暂停机制可能很慢

SVM Spoke Pool 允许本地所有者和远程 HubPool 独立暂停存款填充,从而提高了精细暂停操作的灵活性。但是,在紧急情况下,当前的设计可能并不理想。本地所有者将由多重签名控制,而 HubPool 需要执行 relaySpokePoolAdminFunction,这也依赖于多重签名。这些多重签名依赖关系可能会在关键情况下需要快速暂停时导致延迟。

考虑采用更有效的暂停机制,例如引入分配给系统帐户的专用PAUSER_ROLE。此角色可以快速响应紧急情况。为了平衡速度和安全性,请考虑执行以下操作:

  • 将合约所有者指定为具有暂停功能的备份帐户,以保持冗余。
  • 将暂停角色与所有者角色分开,以遵循最小特权原则并降低滥用风险。
  • 确保所有者可以撤销暂停角色,以便适应运营需求。

更新:已确认,未解决。团队声明:

我们确认问题/备注,并感谢你的投入。 然而,为了密切模仿 EVM 链上的协议设计,我们认为为 PAUSER_ROLE 引入管理员既没有必要也没有理由,因为这会偏离 EVM SpokePool 的设计。 在紧急情况下,我们相信跨链所有者和本地所有者的联合权限提供了足够的机制来及时有效地响应。 因此,我们已决定暂时不实施建议的机制。

不一致的路由启用验证

SetEnableRoute 函数允许启用或禁用从源Token到目标链 ID 的存款路由。 可以由两个不同的角色调用此函数:所有者和远程所有者(使用 CCTP 的 Mainnet 上的 HubPool)。

目前,Solana_Adapter 中存在验证,该验证限制远程所有者在源Token与 Solana 上的 USDC 地址不同时启用路由。 但是,当本地所有者直接在 svm-spoke 程序上调用该功能时,不存在此验证。 这种差异可能会导致不一致,并引入无意中启用错误Token的潜力。

考虑为本地所有者实施相同的验证,以确保角色之间的统一性。

更新:已确认,未解决。团队声明:

我们接受此问题作为预期行为,因为在 EVM Solana_Adapter 中,必须强制使用 USDC 才能将 EVM 截断的 Solana USDC 地址映射到其对应的 bytes32 地址。 但是,此要求不适用于 Solana 端,因为本地所有者(多重签名)可以直接根据需要提供完整的源Token地址。 当前实施的设计还使 SVM Spoke 能够在将来支持除 USDC 之外的Token,而无需更改 Spoke,只需更改 EVM Solana_Adapter 以存储额外的映射。 在 SVM 端进行硬编码和验证将强制新Token升级到 SVM Spoke。

handle_v3_across_message 中的低效 PDA 推导

multicall_handler 中的 handle_v3_across_message 函数依赖于 find_program_address 来推导 PDA 的公钥及其规范 bump 值,使用种子和程序 ID。 find_program_address 函数通过迭代调用 create_program_address 来计算有效的 PDA,从 255 的 bump 开始并递减 bump,直到找到有效的地址。 虽然这确保了派生出有效的 PDA,但迭代过程在计算上是昂贵的,这存在问题,尤其是在像 handle_v3_across_message 这样的函数中,这些函数可能会执行多个指令。 如果不进行优化,此方法有超出计算预算的风险。

为了缓解此问题,请考虑在初始化时将派生的 bump 值存储在帐户的数据字段中。 这种方法使后续指令能够重用存储的 bump,消除了重复调用 find_program_address 的需要。

更新:pull request #846 中部分解决。团队声明:

_我们承认这个问题,但是存储 bump 需要传递带有HandleV3AcrossMessage的附加状态帐户。 然后,存款人应该通过将此状态帐户添加到所有内部 CPI 所需的其他帐户来预先对消息进行编码,但是当 value > 0 时,与 LAMport 值转移到第一个帐户不兼容。要解决这个问题,将需要扩展AcrossPlusMessage结构,以区分传递给handle_v3_across_message的静态帐户(目前我们没有)与内部 CPI 中使用的所有剩余帐户。 这意味着更长的消息大小和存款人和填充器更多的复杂性。 作为替代措施,我们建议部署者选择这样的处理程序程序 ID,以最大程度地减少为handler_signer找到 PDA 所产生的计算成本,确保其 bump 是 255 的最大值。链接的 PR 在程序 ID 上添加了对此考虑的注释。_

不必要的 InitSpace 宏使用

在整个代码库中,以下结构不必要地使用了 InitSpace 宏:

  • [`考虑在敏感变更发生后发出事件,以方便链下跟踪和监控。

更新: 已在 pull 请求 #838 中解决。

没有状态变更时发出事件

冗余的事件发出可能会误导监控事件以跟踪状态变化的观察者,导致他们处理不代表实际修改的事件。这降低了事件作为状态转换的真实来源的可靠性。

在整个代码库中,发现了多个在没有任何状态变化的情况下发出事件的实例:

  1. pause_depositspause_fills 函数分别发出 PausedDepositsPausedFills 事件,而不管暂停状态是否改变。

  2. set_cross_domain_admin 函数每次被调用时都会发出 SetXDomainAdmin 事件,即使 HubPool 保持不变。

  3. 即使某个路由的 enabled 值与其之前的状态没有不同,set_enable_route 函数也会发出 EnabledDepositRoute 事件。

考虑更新这些函数,在发出各自的事件之前检查新状态是否与当前状态不同。这种调整将确保只有有意义的状态变化才会触发事件,从而提高事件可靠性并减少观察者不必要的处理。

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

我们承认这个问题,但我们决定不做更改,原因如下:在 EVM 中,我们遵循类似的模式,即无论之前的状态如何,都会发出事件。坚持这种方法可确保我们实现的一致性,并使代码更简单。通过始终发出事件,我们为观察者提供所有函数调用的清晰日志,即使没有发生状态更改,这对于调试和审计目的也很有价值。

instruction_params 账户在执行后保持打开状态

尽管一旦指令完成,instruction_params 账户变得不必要,但在 ExecuteRelayerRefundLeaf 指令中使用的 instruction_params 账户在执行后仍然处于打开状态。保持账户打开会导致资源效率低下。虽然 close_instruction_params 函数可以解决这个问题,但它引入了不必要的复杂性。更优雅和有效的方法是利用 Anchor 的 close 属性来自动处理账户关闭。Solana 中未关闭的账户会无限期地保留在链上,浪费存储资源并使租金中的 lamports 保持锁定状态,且无法用于其他操作。

考虑使用 Anchor 的 close 属性来确保 instruction_params 账户自动关闭,回收资源并简化代码。

更新: 已在 pull 请求 #840 中解决。

注意事项和补充信息

不完整或具有误导性的注释

在整个代码库中,发现了多个不完整或具有误导性的注释实例:

  • 上面这个 comment unsafe_deposit_v3 说明 deposit_nonce 不用于派生 deposit_id,而它确实被使用了
  • 上面这个 comment execute_relayer_refund_leaf 说明它只能被所有者调用,但实际上可以被任何人调用。对于上下文,relay_root_bundle 是一个 admin 函数,admin 在其中提供了最新的 root bundles 并增加了程序状态中的bundle ID。
  • 上面这个 comment execute_relayer_refund_leaf 说明 leaf 的结构和验证是在 UMIP 中定义的,但是没有 UMIP 的链接或编号。

更新: 已在 pull 请求 #843 中解决。

未使用的错误

在整个代码库中,发现了多个未使用的错误实例:

考虑删除任何未使用的错误。

更新: 已在 pull 请求 #841 中解决。

重复的逻辑

claim_relayer_refundclaim_relayer_refund_for 函数共享几乎相同的逻辑,主要区别在于退款地址。这种重复引入了可维护性方面的挑战,因为未来的更新可能需要同时应用于这两个函数,从而增加了不一致的风险。

为了简化代码并减少冗余,请考虑实施以下重构:

  • 从两个函数中提取公共逻辑到一个辅助函数中,该函数处理传输和事件发出。此辅助函数可以接受目标 token 账户和任何其他特定于上下文的数据等参数。
  • 或者,将这些指令合并到一个采用可选 refund_address 参数的单个函数中,从而在保持单个代码路径的同时实现灵活的用例。

实施这些方法中的任何一种都将简化未来的维护并减少潜在错误的范围。

更新: 已在 pull 请求 #845 中解决。

多重调用处理程序中可以使用不必要的 PDA 签名者

跨链存款可能包含必须在 中继器填写期间 处理的其他消息数据,在这种情况下,数据会 反序列化 并作为各种账户和指令处理,这些账户和指令在中继器数据中指定的 消息处理程序 上调用。在提供的 multicall_handler 示例中,handler_signer 可以作为从消息解码的 CPI 调用中的附加签名者包含在内。但是,在不必要时也可以包含它。这是因为,在每个程序调用中,如果调用中的任何帐户与 handler_signer 密钥匹配,则会 设置 use_handler_signer,但在检查后续调用之前,它永远不会重置为 false

考虑通过在最外面的 for 循环顶部重置 use_handler_signer 的值来纠正此逻辑,以避免在 CPI 调用中传递其他不必要的签名者。

更新: 已在 pull 请求 #837 中解决。

fill_deadline 的参数用法不明确

deposit_v3deposit_v3_now 函数使用相同的参数名称 fill_deadline,但语义不同。在 deposit_v3 中,它是一个绝对时间戳,而在 deposit_v3_now 中,它是一个添加到当前时间的偏移量。

文档缺乏关于这种区别的清晰说明,增加了出错的可能性。用户可能会错误地将绝对时间戳传递给 deposit_v3_now,从而导致计算出的截止日期(current_time + fill_deadline_offset)超过 current_time + state.fill_deadline_buffer。这会导致 事务失败

为了清晰起见,请考虑将 deposit_v3_now 中的 fill_deadline 重命名为 fill_deadline_offset。或者,考虑改进文档以明确说明语义上的差异,包括示例。

更新: 已在 pull 请求 #844 中解决。

不必要地使用 Instruction 属性

Accounts 派生宏包含一个名为 Instruction 的属性,该属性提供对利用该宏的结构中的指令参数的访问。但是,在 WriteInstructionParamsFragment 结构中,此属性是不必要的,因为 offsetfragment 参数都没有在该结构中使用。

这种包含可能会让处理代码库的开发人员感到困惑。因此,请考虑删除未使用的属性以提高代码清晰度和可维护性。

更新: 已在 pull 请求 #842 中解决。

代码中的 TODO 注释

在开发过程中,拥有描述良好的 TODO 注释将使跟踪和解决它们的过程更加容易。如果没有这些信息,这些注释可能会老化,并且在发布到生产环境时,可能会忘记对系统安全至关重要的信息。

在整个 代码库 中,发现了多个未处理的 TODO 注释实例:

考虑删除所有 TODO 注释的实例,而是在问题积压中跟踪它们。或者,考虑将每个内联 TODO 链接到相应的积压问题。

更新: 已在 pull 请求 #815 中解决。

结论

SVM Spoke Pool 代表了 Across Protocol 对 Solana 生态系统的重要扩展,重新实现了核心 spoke pool 功能,同时适应了 Solana 独特的编程模型。通过 CCTP 集成,它实现了 Solana 和 Ethereum 之间安全 token 桥接和消息传递,在保持与 EVM spoke pool 功能对等的同时,采用了 Solana 用于账户管理和程序交互的原生模式。

在我们的审计过程中,我们发现代码库结构合理,并仔细考虑了 Solana 的安全边界和编程约束。该实现证明了对 Solana 账户模型和 Across Protocol 架构要求的深刻理解。虽然我们发现了一些需要注意的问题,特别是围绕中继器退款 leaves 的执行,但总体实现是稳健且架构良好的。

在整个参与过程中,Risk Labs 团队始终乐于助人,及时回答我们的所有问题并彻底解释协议的详细信息。

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

0 条评论

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