本文是关于ZeppelinOS智能合约的审计报告,重点介绍了zOS-lib和Kernel组件的审计结果,发现了一些高、中、低严重程度的问题,并提出了改进建议。
没有多少团队比 Zeppelin 更清楚在新的智能合约代码上需要一个新视角的重要性。随着他们旗舰产品的发布日期临近,Nomic Labs 被要求对 ZeppelinOS 智能合约进行源代码审计。
我们发现代码质量非常高,文档完善,并且配备了一整套非常全面的自动化测试。
代码结构围绕两个主要组件:zOS-lib 和 Kernel。
zOS-lib 是该平台的核心。它负责为开发者提供可升级性支持和版本管理功能。
另一方面,ZeppelinOS 内核负责注册系统标准库的不同版本,处理这些版本的治理机制,并为贡献者的工作提供奖励。
在这两个组件中,我们发现:
需要强调的是,我们发现的那个高严重性问题存在于行业内采用的最先进的可升级性方法中,并且大多数实施可升级性的大型项目在某种程度上也受到影响。请阅读我们的 教育文章 来解释这一发现。
Zeppelin 团队根据我们的发现所做的修复可以在 这个拉取请求 上找到,位于 Github。
完整报告
zOS-lib
审计审计是在版本 0.1.5 上进行的,该版本可以在 GitHub 上找到。
未发现关键问题。
UpgradeabilityProxy
易受功能选择器冲突影响,zos-lib 使用 无结构存储代理模式 实现可升级合约。和所有其他的 代理模式 一样,它基于将消息转发到实现合约,通过 DELEGATECALL
。
这一转发逻辑在 Proxy
的 回退函数 中实现,仅当当前消息的数据不以 Proxy
的 功能选择器 开头时会被调用。这意味着,如果实现中某个功能的选择器与 Proxy
的选择器发生冲突,则后者将优先并且消息不会被转发。
这一行为可能会破坏合约,但也可以被 zos-lib 的用户利用来隐藏恶意代码。在 ZeppelinOS 的情况下,这可以用于提议藏有后门的库升级。
你可以在 这篇文章 中阅读我们对这一漏洞的深入解释。
更新: Zeppelin 团队已经 实施了修复,限制了 Proxy
的功能仅限于其所有者,转发来自其他用户的每条消息到实现合约。
未发现中等严重性问题。
Initializable
合约不支持多重继承
如果一个合约有多个 Initializable
直接父类,便无法调用两个初始化函数,因为 initialized
变量的管理会导致冲突。
更新: Zeppelin 团队已经意识到这个问题,他们计划在不久的将来实现一个新的初始化机制。在此期间,相关文档已被添加。
ContractDirectory#setImplementation
未检查实现是否为合约
此函数可以通过任何有效地址调用,包括零地址和 外部拥有账户,这可能导致 ContractDirectory
被破坏。
更新: 在 拉取请求 #102 中修复。
迁移可以以任意顺序应用
Migratable
用于在版本之间实施可升级合约的状态更新。它允许开发人员实现可能需要先执行另一个特定迁移的迁移。
如果两个迁移依赖于同一个迁移,它们可以以任意顺序应用。这可能会破坏合约的状态,因为第一个迁移可能已经以第二个迁移的作者无法预料的方式改变了它。
更新: Zeppelin 团队计划在不久的将来实现一个新的初始化机制。在此期间,相关文档已被添加。
BaseAppManager#upgradeTo
和 BaseAppManager#upgradeToAndCall
可以使用不正确的合约名称调用
这些方法用于升级代理合约,并将应升级到的实现名称作为输入。这一名称从未经过正确性检查,如果不正确,合约几乎肯定会被损坏。
更新: 这些函数旨在通过 zOS 的 CLI 调用,CLI 会对此进行验证。正在评估未来的链上验证。
Proxy
的回退函数应具有 extern
可见性。请注意,这在下一个主要版本的 Solidity 中将是强制性的。这个功能也可以简化,因为没有必要在其内联汇编块内部管理自由内存指针;并且可以优化,因为它总是包含对 keccak
的调用,这源自 solc
嵌入的 implementationSlot
常量字段。OwnedUpgradeabilityProxy
和 UpgradeabilityProxyFactory
中存在这些用法的实例。OwnedUpgradeabilityProxy#setUpgradeabilityOwner
的名称与合约中其他函数不一致。它应该命名为 setProxyOwner
。Package#getVersion
在请求的版本不可用时将无效返回。这是 zos-lib 中唯一具有此行为的 getter 可能会造成混淆。其他 getters 返回 address(0)
来表示“不可用 ”。pragma experimental “v0.5.0”
,正如 Solidity 在 0.4.21
版本中推荐的那样。这个 pragma 选项对于即将到来的破坏性更改进行预选。特别是,它使用 STATICCALL
来调用外部 pure
和 view
函数,从而使一类攻击不可能。审计是在版本 0.1.3 上进行的,该版本可以在 GitHub 上找到。
未发现关键问题。
未发现高严重性问题。
Kernel#register
接受任何合约,而不仅仅是 Release
当开发者想要提议一个新版本的标准库时,他们必须注册一个新的发布,这由一个合约表示。
虽然 Kernel
合约声明发布类型为 Release
,但这一信息仅在编译时可用,并用于检查合约类型,而不是在运行时验证接收的地址。这使攻击者能够构建含有后门的发布,可能会危害系统。
解决此问题的一种可能方案是在 Kernel
内创建 Release
合约,并验证只有这些合约可以被注册。
更新: Zeppelin 团队通知我们,他们意识到了这一行为,这一设计旨在为开发者在创建新发布时提供更多灵活性。为了阻止这种情况被用于攻击 zOS 用户,令牌持有者将被指示审核发布的合约,而不仅仅是提议的标准库的更改。
未发现中等严重性问题。
Vouching
合约具有字段名称以 _
开头,这通常用于参数,可能会造成混淆。Kernel#developerFraction
使用困难,可能不够细致以满足其要求。考虑用百万分之一 (PPM) 或类似记号表达这一比例。Kernel#_payoutAndVouch
强制开发者的支付至少为 1 个 ZEP 分数(即 1E-18 ZEP)。我们认为这并不可必要,因为转移如此小额 ZEP 的 gas 费用是高昂的。此外,这一功能的实现方式确保如果去掉这项要求,不会损失任何 ZEP 的分数。ERC721Token
实现已过时,应该使用 Migratable
。pragma experimental “v0.5.0
。发现一个高严重性问题。提出了一些更改以减少潜在攻击面,Zeppelin 团队已应用上述修复。
- 原文链接: medium.com/nomic-foundat...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!