这是一份OpenZeppelin对Capyfi lending protocol的代码审计报告,代码仓库为LaChain/capyfi-sc,commit版本为cf47234。审计范围包括部署脚本、白名单机制的增加、CapyFi预言机的实现,以及部署脚本的安全性。报告中没有发现严重、高或中等严重程度的问题,但报告了一些低严重程度的问题,并提出了代码改进建议。
TypeDeFiTimelineFrom 2025-06-25To 2025-07-07LanguagesSolidityTotal Issues11 (9 resolved)Critical Severity Issues0 (0 resolved)High Severity Issues0 (0 resolved)Medium Severity Issues0 (0 resolved)Low Severity Issues4 (4 resolved)Notes & Additional Information7 (5 resolved)
OpenZeppelin 审计了 LaChain/capyfi-sc 仓库在 commit cf47234 上的代码。
审计范围包括以下文件:
src
├── contracts
│ ├── BaseJumpRateModelV2.sol
│ ├── CDaiDelegate.sol
│ ├── CErc20.sol
│ ├── CErc20Delegator.sol
│ ├── CErc20Immutable.sol
│ ├── CEther.sol
│ ├── CToken.sol
│ ├── CTokenInterfaces.sol
│ ├── Comptroller.sol
│ ├── ComptrollerG7.sol
│ ├── Governance
│ │ ├── Comp.sol
│ │ ├── GovernorAlpha.sol
│ │ ├── GovernorBravoDelegate.sol
│ │ ├── GovernorBravoDelegateG1.sol
│ │ ├── GovernorBravoDelegator.sol
│ │ └── GovernorBravoInterfaces.sol
│ ├── Lens
│ │ └── CompoundLens.sol
│ └── PriceOracle
│ └── CapyfiAggregatorV3.sol
└── script
├── DeployCapyfiProtocolAll.s.sol
└── capyfi
├── DeployInterestRateModels.s.sol
├── DeployWhitelist.s.sol
└── UpgradeWhitelist.s.sol
Capyfi 是一个借贷协议,旨在同时在以太坊和 LaChain 上运行,采用基于白名单的访问控制系统,能够执行 KYC/AML 策略。该协议采用基于市场的借贷架构,用户可以提供资产以赚取利息,并以其抵押品进行借贷。该系统利用计息代币(cTokens)来表示用户头寸,并使用自定义预言机来确定价格。
Whitelist
合约,实现为可升级的 UUPS 代理,为市场引入了基于角色的访问控制。定义了两个角色:ADMIN_ROLE
和 WHITELISTED_ROLE
。管理员拥有更高的权限,包括授予和撤销这两个角色以及升级代理实现。列入白名单的用户可以在指定的市场中铸造 cTokens
,而未使用列入白名单的用户仍然可以转移、赎回和清算资产。CapyfiAggregatorV3
合约允许 CapyFi 支持没有活跃 Chainlink 价格馈送的代币。虽然受到 Chainlink 模型的启发,但 CapyFi 价格馈送由具有权限的所有者更新,而不是从不同的节点运营商处聚合。
安全审查涵盖了部署脚本以及智能合约,以确保合约以正确的顺序和配置进行部署。
本节介绍已审查系统的安全模型以及其中的任何特权角色,以及相关的信任假设。
该系统设计为可以部署在以太坊主网和 LaChain 上。以太坊的配置正确地假设了 12 秒的区块时间,但 LaChain 使用 5 秒的区块时间。因此,利率模型或时间依赖逻辑的任何假设都必须考虑到这种差异。
部署脚本缺乏针对启动空市场的保护。虽然白名单可以为铸造提供一层访问控制,但初始化程序默认情况下不设置白名单合约。这引入了一个窗口,恶意行为者可以充当第一个铸币者,除非外部强制执行额外的保护措施。审计范围不包括将新市场部署到预先存在的部署。但是,建议在部署交易中进行一些 cToken 并销毁它们,以确保总供应量永远不会变为零。至关重要的是,市场部署和销毁发生在同一笔交易中。
存储布局与传统合约不兼容。与传统的 CErc20Delegate
合约相比,引入 Whitelist
功能修改了存储布局。因此,尝试将现有代理升级到这个新的实现会导致存储冲突,可能会破坏核心功能或暴露漏洞。但是,根据 CapyFi 团队的部署计划,此实现仅用于新的市场部署,而不是现有市场的升级路径。
CapyfiAggregatorV3
合约依赖于中心化机制来更新价格。因此,用户和集成商必须信任指定的报价提交者能够诚实且保持准确的定价。
CapyFiAggregatorV3
合约遵循 Chainlink 接口,但不一定像 Chainlink 预言机那样运行。集成商在使用预言机时应验证 CapyFi 预言机的特定行为。
Whitelist
合约中实现的访问控制机制赋予持有 ADMIN_ROLE
的帐户很大的权限。单个恶意或受损的管理员可以撤销所有其他用户的角色,并单方面控制系统,包括升级合约。因此,假设 ADMIN_ROLE
持有者具有适当的运营安全。此外,只有列入白名单的用户才能铸造 cTokens
。未列入白名单的用户仍然可以赎回、转移和清算 cTokens
。
DEFAULT_ADMIN_ROLE
对 ADMIN_ROLE
和 WHITELISTED_ROLE
的无限权力Whitelist
合约中预期的层级结构是,只有具有 ADMIN_ROLE
的用户才能使用 addAdmin
、removeAdmin
、addWhitelisted
和 removeWhitelisted
函数来授予和撤销 ADMIN_ROLE
和 WHITELISTED_ROLE
。但是,AccessControlUpgradeable
合约也暴露了 grantRole
和 revokeRole
public
函数,允许 DEFAULT_ADMIN_ROLE
自由管理这两个角色。这是因为这两个角色的管理员角色默认设置为 DEFAULT_ADMIN_ROLE
。因此,即使没有分配 ADMIN_ROLE
,他们也可以不受任何限制地管理这两个角色。
考虑实施以下解决方案之一:
addAdmin
、removeAdmin
、addWhitelisted
和 removeWhitelisted
函数,以依赖于继承的 grantRole
和 revokeRole
函数。此解决方案将需要正确设置 ADMIN_ROLE
和 WHITELISTED_ROLE
的角色管理员,以便只有特定角色的角色管理员才能管理它们。grantRole
和 revokeRole
函数并使它们无法访问来禁用它们。更新: 已在第 pull request #5 的 commit d4abd3 中解决。
getAnswer
函数中的不安全类型转换CapyFiAggreatorV3
合约的 getAnswer
函数接受 roundId
的任何 uint256
值。当 roundId
大于 uint80
时,该函数将始终恢复,但返回的错误会将返回的值错误地转换为 uint80
。
考虑在 RoundNotFound
错误中不将 roundId
转换为 uint80
,或者在输入参数大于 uint80
时返回不同的数据。
更新: 已在第 pull request #6 的 commit 5ac968 中解决。
Chainlink 的聚合器和 Capyfi 的聚合器之间存在一些实现差异:
roundId
由 phaseId
和 originalId
组成。phaseId
是一个计数器,每次引用新的聚合器时都会递增,originalId
是一个计数器,用于跟踪数据馈送中每次提交的价格。这两个 ID 被打包到同一个 uint80
中,偏移量为 uint80((phaseId << 64) + originalId)
。两个计数器都从 1 开始,因此,第一个有效的 roundId
应该是 18446744073709551617
。但是,在 Capyfi 的实现中,它从 1 开始,并且每次提交新价格时都会递增。如果外部集成商想要从一开始就获取历史数据,并且他们尝试将来自 Chainlink 聚合器的第一个有效轮次获取到 Capyfi 的实现中,它将恢复。getAnswer(uint256 roundId)
和 getTimestamp(uint256 roundId)
,它返回 0,对于 getRoundData(uint80 roundId)
,它为 answer
、startedAt
和 updatedAt
返回 0。但是,在 Capyfi 的实现中,它会恢复执行。这也可能会破坏外部集成。startedAt
始终与 updatedAt
相同。这是因为当调用 updateAnswer
时,该价格立即成为预言机的价格,而 Chainlink 会聚合多个预言机来源,这需要时间延迟。考虑在代码库中记录上述差异,以便集成商能够了解它们。
更新: 已在第 pull request #7 的 commit fb173f 中解决。
在整个代码库中,发现了多个函数实例,这些函数未验证新值是否与现有值实际不同,然后再进行更新:
Whitelist.sol
中的 activate
函数Whitelist.sol
中的 deactivate
函数CapyfiAggregatorV3.sol
中的 addAuthorizedAddress
函数CapyfiAggregatorV3.sol
中的 removeAuthorizedAddress
函数考虑添加验证检查,如果输入值与现有值匹配,则恢复交易。
更新: 已在第 pull request #8 的 commit 3c63ae 中解决。
在智能合约中提供特定的安全联系人(例如电子邮件或 ENS 名称)可以大大简化个人在代码中发现漏洞时进行通信的过程。这种做法非常有益,因为它允许代码所有者指定漏洞披露的沟通渠道,从而消除了由于缺乏相关知识而导致沟通不畅或未能报告的风险。此外,如果合约包含第三方库并且这些库中出现错误,维护人员可以更轻松地联系相关人员以了解问题并提供缓解说明。
审计范围内的合约没有安全联系人。
考虑在每个合约定义上方添加包含安全联系人的 NatSpec 注释。建议使用 @custom:security-contact
约定,因为它已被 OpenZeppelin Wizard 和 ethereum-lists 采用。
更新: 已确认但未解决。
自从 Solidity 0.8.18 以来,映射可以包含命名参数,以提供关于其用途的更多清晰度。命名参数允许以 mapping(KeyType KeyName? => ValueType ValueName?)
的形式声明映射。此功能增强了代码的可读性和可维护性。
在 CapyfiAggregatorV3.sol
中,发现了多个没有命名参数的映射实例:
考虑向映射添加命名参数,以提高代码库的可读性和可维护性。
更新: 已在第 pull request #9 的 commit d2c855 中解决。
在 Whitelist.sol
中,发现了多个缺少索引参数的事件实例:
为了提高链下服务搜索和筛选特定事件的能力,请考虑索引事件参数。
更新: 已在第 pull request #10 的 commit fb7ad5 中解决。
该协议依赖于 Chainlink 价格馈送进行资产估值。使用 Chainlink 的 latestRoundData
时,彻底验证所有返回的数据至关重要,以防止使用过时或不正确的价格。
ChainlinkPriceOracle.sol
中的 priceFeed.latestRoundData
调用不检查价格是否过时。
考虑充分验证 latestRoundData()
输出的结果,以确保数据馈送已返回最新且正确的价格。否则可能会引入重大风险,例如由于代币是针对价格过时的资产借入的,导致抵押不足的贷款。
更新: 已确认,未解决。
在整个代码库中,发现了多个缺少文档字符串的实例:
Whitelist.sol
中,ADMIN_ROLE
状态变量、WHITELISTED_ROLE
状态变量、WhitelistActivated
事件、WhitelistDeactivated
事件 和 WhitelistUpgraded
事件。AggregatorV3Interface.sol
中的所有函数和事件CapyfiAggregatorV3.sol
中,authorizedAddresses
状态变量、AuthorizedAddressAdded
事件 和 AuthorizedAddressRemoved
事件考虑彻底记录所有属于任何合约的公共 API 的函数(及其参数)。实现敏感功能的函数,即使不是公共的,也应清楚地记录。在编写文档字符串时,请考虑遵循 Ethereum Natural Specification Format (NatSpec)。
更新: 已在第 pull request #11 的 commit 4ca9de 中解决。
immutable
如果一个变量仅从合约的 constructor
中分配一个值,则可以将其声明为 immutable
。
在 CapyfiAggregatorV3.sol
中,发现了多个可以设置为 immutable
的变量实例:
为了更好地表达变量的预期用途并可能节省 gas,请考虑将 immutable
关键字添加到仅在构造函数中设置的变量。
更新: 已在第 pull request #12 的 commit 0dfc1e 中解决。
与底层 Compound V2 合约交互时,大多数函数都会返回值以指示错误,而不是恢复。在协议配置期间,尤其是在部署期间,必须仔细考虑此行为。如果诸如 _supportMarket
或 _setCollateralFactor
之类的特定操作失败并返回错误代码,则部署脚本仍将成功,但不会应用预期的配置。
为了避免静默失败,请考虑存储从这些函数调用返回的值,并显式检查它是否等于零(NO_ERROR
)。这确保了如果任何这些内部调用遇到错误,部署脚本会立即失败。
更新: 已在 pull request #14 的 commit 0acfdf 和 commit 244cd4 中解决。
审计范围包括 CapyFi 借贷协议的部署,特别强调了白名单机制的添加、CapyFi 预言机的实现以及部署脚本的安全性。此代码库实现了对已进行多次审计的协议的更改,并且最小化对原始代码库的修改使其能够从原始架构的安全性中受益。部署脚本涵盖了借贷协议、预言机和白名单的启动。虽然将新市场部署到已部署的合约不是本次审计的范围,但必须强调在启动时向空市场添加初始资产以防止膨胀攻击的重要性。
没有发现严重、高危或中危问题,这证明了代码库的稳健性。尽管如此,还是报告了一些低风险问题,并提出了各种代码改进建议。感谢 CapyFi 团队在整个参与过程中提供的卓越协作。该团队清楚地解释了合约,并提供了概述协议功能和他们特别关注领域的文档。
- 原文链接: blog.openzeppelin.com/ca...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!