FCHAIN验证者和质押合约审计

本文对F Foundation的FCHAIN智能合约进行了审计,重点分析了其验证者注册和质押机制,揭示了一些关键的安全漏洞和优化建议。本文还详细描述了相关的重大问题和审计过程中的信任假设,建议F Foundation团队关注Ava Labs的最新进展,以便及时调整和改进其合约代码。

目录

总结

类型时间线 从 2025-03-03 到 2025-03-19 语言 Solidity 总问题 18 (6 已解决) 严重性关键问题 3 (2 已解决) 高严重性问题 1 (1 已解决) 中等严重性问题 2 (2 已解决) 低严重性问题 4 (1 已解决) 备注与附加信息 8 (0 已解决)

范围

我们审核了 0xFFoundation/fchain-contracts 仓库中的提交 11ffd45

范围内的文件如下:

 fchain-contracts/
└── src/
    ├── StakeManager.sol
    └── ValidatorRegistry.sol

系统概述

审核的代码库管理着 F Foundation 的“FCHAIN”的验证者集合,这是一个遵循 ACP-77 规范的子网,位于 Avalanche 生态系统中。范围内的合约是基于 Ava Lab 的 ICM 合约 的分支。代码库使用 Warp Messenger,这是一个使用预编译功能在子网和 Avalanche P-chain 之间发送消息的标准。消息可以由 P-chain 验证者签名,以管理 ACP-77 子网的官方验证者集合,例如 FCHAIN。

验证方案的设置涉及“许可证”,这些许可证是将在 FCHAIN 首次启动时出售或分配的 NFT。这些许可证将使用户能够成为 FCHAIN 的验证者。为了做到这一点,用户需要锁定他们的许可证 NFT。一旦完成,验证者可以开始为每个“周期”赚取奖励,只要他们有足够的正常运行时间。奖励将以 FCHAIN 的原生代币支付并自动重新质押。委托者也可以向验证者质押, earning some rewards 为自己和验证者,而无需进行验证的工作。对验证者和委托者的质押都需要至少一个已质押的许可证,但可以通过质押额外的许可证或原生代币来增加奖励。

为了防止对验证者方案的操控,加入和退出验证者或委托者集合的操作都有时间延迟。这为 P-chain 验证者提供了时间,以签署对 FCHAIN 验证者集合的任何更改,并防止验证者在部分周期内获得奖励。这通常通过对大多数状态转换具有“初始化”和“完成”功能来实现,在这两个阶段之间,P-chain 生成一条带签名的消息来验证转换。

此外,为了稳定系统,有一个为期一年的时间,在此期间,委托者无法提取他们的质押。这一时期对所有人而言均相同,并在 FCHAIN 启动后的一年结束。值得注意的是,此期间不适用于验证者,允许他们在停止验证后立即移除其质押。尽管委托者无法提取,但他们在此期间可以更改所选验证者并存入更多质押。

安全模型和信任假设

在审核过程中,作出了以下信任假设:

  • 假设 Warp messenger 预编译和 Warp 消息协议按照 ACP-77 规范 的规定运行。
  • 假设在每个周期的宽限期结束前,updateBalancesupdateMultipleBalances 函数将被调用,但在周期结束之前,针对每个 stakeID 进行活动验证。
  • 假设在每个周期的宽限期内,P-chain 验证者将对每个 stakeID 签署正常运行时间证明。类似地,假设在每个周期的宽限期内,将针对每个注册的 stakeID 调用 submitUptimeProofsubmitMultipleUptimeProofs
  • 假设 P-chain 验证者将及时签署 Warp 消息,如 ACP-77 中所述。

虽然正常运行时间和奖励积累方案是脆弱的,但它已被激励正常运作。计量正常运行时间和积累奖励的功能是开放可调用的,可以在多个周期内调用,也可能不会在每个周期内为每个验证者或委托者调用。然而,我们理解 F Foundation 团队意图使调用适用的功能自动化。因此,我们在安全评估中做出了以下假设:

  • 假设自动化服务将在每个周期结束前为所有验证者提交正常运行时间证明。
  • 假设正常运行时间证明将每个周期只创建一次,每个验证者只创造一次。
  • 假设余额更新将在每个周期的宽限期结束后并在下一个周期开始前为每个验证者和每个委托者提交,每个周期一次。
  • P-chain 验证者集合能够发送 Warp 消息,这些消息可以用来影响 FCHAIN 的验证者和委托者集合。此外,假设 P-chain 验证者将仅对有效的 Warp 消息进行签名,并在签名与之相关的消息之前检查 FCHAIN 的状态。

特权角色

在整个代码库中,识别出了以下特权角色:

  • StakeManager 合约中,仅 DEFAULT_ADMIN_ROLE 被授权升级合约。
  • ValidatorRegistry 合约中,VALIDATOR_ADMIN_ROLE 可以:

    • 初始化验证者注册。
    • 完成验证者注册。
    • 初始化结束验证。
    • 完成结束验证。
    • 铸造原生代币。
    • 设置验证者权重。
  • ValidatorRegistry 合约中,DEFAULT_ADMIN_ROLE 可以授予或撤销 VALIDATOR_ADMIN_ROLE

正在开发的ICM合约

需要注意的是,审核的代码库的分支来自 ICM 合约,这些合约仍在积极开发中。因此,建议 F Foundation 团队关注 ICM 合约代码库的最新发展,并审核任何变更以便与审核的代码库集成。F Foundation 团队应检查 ICM 合约的新版本及其相关讨论。此外,他们还应监控与 ACP-77ACP-99 相关的讨论。

严重性关键

锁定的许可证可以转移

持有 FNode 许可证是成为 FCHAIN 验证者的必要条件。验证者必须 锁定 他们的 ERC-721 兼容的 FNode 代币到 StakeManager 合约中,才能被认定为活动验证者并获得激励,而委托者也可以 锁定 并将其 FNode 代币委托给活动验证者以获得奖励。

StakeManager 合约中,验证者和委托者通过调用 internal _lockLicenses 函数 在验证者注册、委托者注册或添加新质押的初始化阶段锁定其 FNode 许可证。此函数验证 调用者 (_msgSender) 是 ERC-721 代币的所有者,然后通过 更新 tokenLockedBy[tokenID] 映射来“锁定”代币。值得注意的是,只有 internal 映射被修改,代币本身并未转移到合约中。

这允许 FNode 代币的所有者通过调用 IERC721 接口的 transferFromsafeTransferFrom 函数转让其“锁定”许可证的所有权,这实际上取消了他们作为网络验证者的资格。然而,StakeManager 中的原始质押仍然继续获得奖励,因为许可证仍然被计算在验证者的总体权重中。此外,许可证的新所有者 无法重新质押 代币,因为它已在 tokenLockedBy 映射中记录。

考虑在锁定过程中将 FNode 代币转账到 StakeManager 合约中,并在解锁时将其返回给验证者或委托者。或者,考虑使 FNode 代币在作为活动验证者的权重的一部分时不可转让。

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

NFT将在一年内不可转让,因此我们目前不会更改这一部分。

如果验证者不活跃,存入的质押可以在StakeManager中锁定

initializeDeposit 函数 旨在接受许可证和质押金额,作为系统中验证者的质押。验证者可以调用此函数以增加自己的权重,或委托者可以调用此函数以增加他们所委托验证者的权重。然而,该函数在执行存款之前并未验证验证者的活动状态,导致质押者可能不小心将资金存入不活跃的验证者。该函数随后 锁定 存入的许可证和价值于 StakeManager 合约中,将新增加的权重添加到验证者的现有权重中,并 向 P-Chain 发送相应的设置权重消息。它还 pendingDeposits映射中创建了一项条目,这样在调用 completeDeposit 函数时,这些 质押将被添加到 nextEpochDeposit 映射中,该映射会在质押者的余额更新时被 处理

鉴于验证者已经不活跃,P-Chain 将拒绝添加权重的消息,并将没有相应的 messageIndex 可用于调用 completeDeposit 函数。因此,pendingDeposits 永远不会被计算为质押者的余额的一部分。此外,由于锁定的质押不属于质押者的余额,质押者将无法由于 createWithdrawalRequest 函数中的还原 而提取这些质押,从而不可避免地将这些质押锁定在 StakeManager 合约中。

考虑在 initializeDeposit 函数中检查验证者是否活跃,然后再接受存款。

更新:pull request #46 中已解决。initializeDeposit 函数在接受质押之前正确检查验证者是否活跃。此外,引入了一个 cancelDeposit 函数,允许用户在存款完成之前,如果验证者变得不活跃,可以提取任何已存入的质押。

验证者可以跳过 createEndRequest 并快速重新注册

在 ACP-77 规范中,明确指出验证者只能注册一次:

当知道特定的 validationID 未被注册且将不会被注册时,P-Chain 必须愿意为该 validationID 提交一个 L1ValidatorRegistrationMessage,标记为 false,这可能在消息过期后而未完成,或者验证者曾成功注册后被移除。

然而,验证者可以仅在 FCHAIN 上重新注册,这导致 FCHAIN 和 P-chain 上记录的验证者集之间存在差异。这是因为一旦验证者被注销,P-chain 必须拒绝再次注册它。

要做到这一点,验证者必须首先调用 initializeValidatorRegistration。一旦收到来自 P-chain 的 L1ValidatorRegistrationMessage,验证者可以调用 completeValidatorRegistration。在这一过程中,验证者可以快速调用 initializeEndValidationcompleteEndValidation,然后再次调用 initializeValidatorRegistrationcompleteValidatorRegistration,使用与原始注册相同的 messageIndex。这必须在调用 initializeEndValidation 之前未调用 _updateBalances 的情况下进行,因为这样会影响质押的 fNodesTokenIDs 映射

此外,所有操作都必须在 registrationExpiry 时间 到达之前完成。完成此过程后,验证者的 isValidationEnded 布尔值将被设置为 true。这将导致 该验证者的结束被永久阻止,阻止 正常运行时间证明的处理,导致 P-chain 和 FCHAIN 上记录的验证者集之间的差异。需要注意的是,这一漏洞的另一个特性是,验证者能够完全跳过调用 createEndRequest。这显然不是预期的功能。

考虑在验证者注册流程中检查 isValidationEnded 的状态。同时,考虑在所有情况下强制要求在验证者可以初始化结束验证之前,至少调用一次 _updateBalances。可以通过标记任何新的验证者为“需要余额更新”,或者检查该验证者的 startEpochnextEpochDeposit 值的方式来实施这一点。

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

高严重性

对Warp消息检查不足

completeValidatorChangecompleteDelegatorRegistration 函数中,Warp 消息被吸收并用于验证逻辑。然而,这些 Warp 消息并没有经过充分的检查。

completeValidatorChange 中,任何值都可以指定为 oldNonceTargetnewNonceTarget,但这些值仅被 使用一次,用于与 oldNonceMessagenewNonceMessage 的检查。因此,可以指定任何消息索引,这使得该函数在没有预期的 Warp 消息的情况下轻松执行。这可能允许某些委托者快速更换其验证者,而无需 P-chain 的批准。这同样可以被利用来触发调用者不控制的验证者的更改,从而影响其他用户的奖励。

completeDelegatorRegistration 中,Warp 消息中唯一使用的值是 validationID。因此,任何包含相同 validationID 的错误消息都可能被用来使函数成功执行。这可能允许任何委托者在没有 P-chain 批准的情况下向验证者注册。

在这两种情况下,这位委托者所积累的奖励都可以被轻松操控。因此,考虑在 initiateValidatorChange 中记录验证者更改的随机数,并在 completeValidatorChange 中使用这些随机数来确保使用的消息比初始请求更新。这也可以用于防止用户更换其不知晓的委托者的验证者。类似地,考虑在 initializeDelegatorRegistration 中存储获取的 nonce 并记录从 Warp 消息中获得的 nonce completeDelegatorRegistration 中解析。这些值应进行相互比较,以确保 P-Chain Warp 消息比初始化委任请求更新。

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

中等严重性

由于验证者状态转换导致的潜在质押锁定和不一致

StakeManager 合约促进 委托者的注册过程,以便对活动验证者进行质押。它还包括 createEndRequest 函数,允许验证者发起从网络中的退出。当委托者在通过 createEndRequest 开始退出过程但尚未更新其余额的验证者上启用质押时,将会产生一个问题,导致其 isValidationActive 布尔值保持为 true。此标志仅在创建验证结束请求后通过余额更新设为 false

在此期间,委托者可以执行 initializeDelegatorRegistration 函数,这 将质押的权重添加到验证者向 P-Chain 广播消息,并通过 _lockStakesAndLicensesSetTotalStats internal 函数 在合约内锁定质押。这些质押预计将在下一个周期调用 completeDelegatorRegistration 时反映在质押者的余额中,该余额经过更新。

然而,如果验证者在委托者完成注册之前继续更新其余额并结束其验证,那么验证者的权重可能降至零。这样一系列操作引入了两个重要问题:

  • 如果来自 initializeDelegatorRegistration 的权重更新消息在验证者退出后因为中继器延迟采集或 P-Chain 顺序处理问题而被处理,它可能会被拒绝,因为验证者在 P-Chain 上被标记为不活跃。这意味着将没有 messageIndex 完成委托者的注册,从而有效地使质押资金锁定于 StakeManager 合约中,无处可去。
  • 如果 initializeDelegatorRegistration 消息及时处理,提供有效的 messageIndex,但验证者在执行 completeDelegatorRegistration 之前退出,则系统可能仍会完成质押登记。这将导致不一致,因为验证者的权重为零,但系统承认存在一个质押注册。因此,委托者由于无法验证验证者的正常运行时间而无法赚取奖励,也无法解锁其代币,除非 delegationLockDeadline 已过,迫使他们重新分配其委任。

此漏洞还涉及到 initializeDepositcompleteDeposit 函数,从而在质押机制中呈现出更大的攻击向量。

考虑为正在退出的验证者添加一个 PendingRemoved 状态,并在 completeDelegatorRegistrationcompleteDeposit 函数中实施条件检查,以自行释放质押,如果验证者处于这种过渡状态。这样做有助于保护防止意外的质押锁定,并确保验证者和委托者状态之间的一致性。

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

需要重发失败的设置权重消息功能

如果 Warp 消息未被至少 67% 的 FCHAIN 验证者签名,则发送到 P-chain 的 Warp 消息可能会被视为无效 ,从而导致问题。在 StakeManager 中存在很多依赖于 L1ValidatorWeightMessage 的操作。然而,如果这些消息被 P-chain 丢弃,这些流程可能会冻结,从而给用户带来问题。例如,如果 L1ValidatorWeightMessage 从未发送,则 completeWithdrawal 函数将无法使用,令代币锁定在 StakeManager 合约中。

考虑创建一个类似于 resendRegisterValidatorMessageresendEndValidatorMessage 函数的函数,用于 L1ValidatorWeightMessage。这将允许在重要的验证者权重消息被 P-chain 丢弃的情况下采取行动。

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

低严重性

不完整的文档字符串在代码库中,发现了多个不完整文档字符串的实例:

  • StakeManager.sol 中的 createWithdrawalRequest 函数文档字符串中,stakeIDamountfNodesTokenIDs 参数没有被文档化。

  • ValidatorRegistry.sol 中的 addValidatorAdmin 函数文档字符串中,account 参数没有被文档化。

  • ValidatorRegistry.sol 中的 removeValidatorAdmin 函数文档字符串中,account 参数没有被文档化。

请务必全面文档化合约公共 API 中所有函数/事件(及其参数或返回值)。撰写文档字符串时,请考虑遵循 Ethereum Natural Specification Format(NatSpec)。

更新: 已确认,将解决。

initializeDeposit 中验证不足

initializeDeposit 函数 允许存入原生代币和许可证的锁定。然而,在 fNodesTokenIDs 为空msg.value == 0 的情况下,该函数将成功执行,而不会对验证者或委托者的股份做出任何更改,从而导致不必要的Gas消耗。

遵循“尽早和大声失败”的原则,请考虑检查 fNodesTokenIDsmsg.value 的长度,如果这两个值均为 0,则回退并给出描述性错误。

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

缺乏内联文档

StakeManagerValidatorRegistry 合约在内联文档中存在显著不足,尤其是许多函数及其输入变量缺少文档字符串。此缺陷不仅妨碍代码库的可读性和可维护性,还对确保其安全性和功能性带来了重大的挑战。

请考虑在整个代码库中添加内联文档,以确保所有函数,特别是那些是公共或外部的,都附有详细的文档字符串。这些应包括对函数目的、参数、返回值、发出的事件以及其行为的任何副作用或重要说明的描述。撰写文档字符串时,请考虑遵循 Ethereum Natural Specification Format(NatSpec)。

更新: 已确认,将解决。

initialize... 函数名称更新为 initiate...

StakeManager 合约 中,有 5 个函数遵循 initialize... 的命名约定,例如:

这些函数都有相应的 complete... 函数。然而,还有一个名称类似而不遵循此约定的函数:initiateValidatorChange,它有一个类似的 complete... 函数

请考虑将这 5 个已识别的函数名称更改为 initiate...。这将有助于保持代码库的一致性并便于理解。此外,这样做将有助于将这些函数与 initialize 函数 及与初始化相关的任何关联函数区分开来。最后,这将帮助与 最新版本的 icm-contracts ValidatorManager.sol 相匹配,该版本已经做出了相同的更改。

更新: 已确认,将解决。

备注及附加信息

未解决的 TODO 注释

在开发过程中,有描述良好的 TODO 注释将使跟踪和解决它们的过程更加容易。然而,如果未解决,这些注释可能会变老,开发完成时可能会遗忘与系统安全相关的重要信息。因此,TODO 注释应在项目的问题待办列表中跟踪,并在系统部署之前解决。

StakeManager.sol 的第 543 行中识别到一个 TODO 注释。

请考虑解决所有 TODO 注释的实例并在问题待办列表中跟踪它们。

更新: 已确认,将解决。

函数顺序不一致

StakeManagerValidatorRegistry 合约由于函数顺序不一致,违反了 Solidity 风格指南。例如,在 StakeManager 合约中,submitUptimeProof 外部函数 被放置在 _processUptimeProof 内部函数 之后。

为了提高项目的整体可读性,请考虑按照 Solidity Style Guide(函数顺序) 推荐的标准化顺序。

更新: 已确认,将解决。

映射中缺少命名参数

Solidity 0.8.18 起,开发者可以在映射中使用命名参数。这意味着映射可以采用形式 mapping(KeyType KeyName? => ValueType ValueName?)。这种更新的语法能更清晰地表示映射目的。

StakeManager.sol 中,tokenExists 映射没有命名参数。

请考虑在映射中添加命名参数,以提高代码库的可读性和可维护性。

更新: 已确认,将解决。

未使用的命名返回变量

命名返回变量是一种声明在函数体内使用并作为该函数输出返回的变量的方式。它们是明确的行内 return 语句的替代品。

StakeManager.sol 中发现多个未使用的命名返回变量的实例:

请考虑使用或移除任何未使用的命名返回变量。

更新: 已确认,将解决。

缺少安全联系人

在智能合约中提供特定的安全联系人(如电子邮件或 ENS 名称)可以大大简化他人识别代码漏洞时的沟通流程。这种做法非常有益,因为它允许代码所有者指定漏洞披露的沟通渠道,消除由于缺乏知识而导致的错误或未报告的风险。此外,如果合约包含第三方库,而在这些库中出现错误,维护者也可以更方便地联系适当的人员并提供缓解指令。

StakeManagerValidatorRegistry 合约中均未设置安全联系人。此外,ValidatorManager 合约当前 指定 的安全联系人为 ava-labs/icm-contracts,而非 0xFFoundation/fchain-contracts 的安全联系人。

请考虑在每个合约定义上方添加包含正确安全联系人的 NatSpec 注释。建议使用 @custom:security-contact 的约定,因为这一约定已经被 OpenZeppelin Wizardethereum-lists 采用。

更新: 已确认,将解决。

打字错误

StakeManager 合约中发现多个打字错误的实例:

  • 第 1301 行 中,"calalble" 应为 "callable"。
  • 第 1095 行 中,"decays by 82.5%" 应为 "decays by 17.5%"。

请考虑纠正任何打字错误,以提高代码库的清晰度和可读性。

更新: 已确认,将解决。

_disableInitializers 未被初始合约构造函数调用

在代理模式中,实施合约允许任何人调用其 initialize 函数。虽然这不是直接的安全问题,但防止实施合约初始化非常重要,因为这可能允许攻击者接管合约。这不会影响代理合约的功能,因为只有实施合约的存储会受到影响。

在代码库中发现多个初始合约,其中在构造函数中没有调用 _disableInitializers():

请考虑在可初始合约的构造函数内部调用 _disableInitializers() 以防止恶意行为者抢先初始化。

更新: 已确认,将解决。

函数可见性过于宽松

在代码库中,有多个函数的可见性不必要地宽松:

  • StakeManager.sol 中的 _validateEpoch 函数,internal 可见性可以限制为 private

  • StakeManager.sol 中的 _processUptimeProof 函数,internal 可见性可以限制为 private

  • StakeManager.sol 中的 _hasGracePeriodPassed 函数,internal 可见性可以限制为 private

  • StakeManager.sol 中的 _getEpochEndTime 函数,internal 可见性可以限制为 private

  • StakeManager.sol 中的 _getPChainWarpMessage 函数,internal 可见性可以限制为 private

  • StakeManager.sol 中的 _getWeight 函数,internal 可见性可以限制为 private

  • StakeManager.sol 中的 _getWeightSetLosses 函数,internal 可见性可以限制为 private

  • StakeManager.sol 中的 _lockStakesAndLicensesSetTotalStats 函数,internal 可见性可以限制为 private

  • StakeManager.sol 中的 _lock 函数,internal 可见性可以限制为 private

  • StakeManager.sol 中的 _lockLicenses 函数,internal 可见性可以限制为 private

  • StakeManager.sol 中的 _validateStakeAmount 函数,internal 可见性可以限制为 private

  • StakeManager.sol 中的 _unlock 函数,internal 可见性可以限制为 private

  • StakeManager.sol 中的 _unlockLicenses 函数,internal 可见性可以限制为 private

  • StakeManager.sol 中的 _processPendingValidatorChange 函数,internal 可见性可以限制为 private

  • StakeManager.sol 中的 _processPendingBalanceChanges 函数,internal 可见性可以限制为 private

  • StakeManager.sol 中的 _getEpochReward 函数,internal 可见性可以限制为 private

  • StakeManager.sol 中的 _getEpochRewardForValidator 函数,internal 可见性可以限制为 private

  • StakeManager.sol 中的 _getEpochRewardForDelegator 函数,internal 可见性可以限制为 private

  • StakeManager.sol 中的 _updateBalances 函数,internal 可见性可以限制为 private

  • StakeManager.sol 中的 _revertIfDeadlineNotPassed 函数,internal 可见性可以限制为 private

  • ValidatorRegistry.sol 中的 completeValidatorRegistration 函数,public 可见性可以限制为 external

为了更好地传达函数的预期用途,并可能实现一些额外的Gas节省,请考虑将函数的可见性更改为仅限于所需的权限。

更新: 已确认,将解决。

结论

经审核的代码库管理着 F Foundation 的 FCHAIN 验证者集,这是一个在 Avalanche 生态系统下运行的 ACP-77 规范的子网。审核的代码与 Ava Labs 的 ICM 合约 ValidatorManager 合约交互,该合约未包含在此次审计范围内。

发现了多个关键和高严重性的问题,主要与用于注册验证者和委托者的关键参数的验证有关。虽然在审核代码库的文档文件夹中提供了一些文档,但在合约中添加更多内联文档可以进一步增强清晰度。由于 ICM 合约仍在 Ava Labs 积极开发中,我们鼓励 F Foundation 团队保持对可能影响该代码库的任何更新的关注,并根据需要整合相关更改。

在审计过程中,客户反应非常迅速,及时解决疑虑,并帮助优先处理潜在问题。

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

0 条评论

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