好想法,坏设计:钻石标准为何不及格
TL;DR: 我们审计了钻石标准合约升级方案的实现,目前无法推荐该方案——但请参考我们的建议和升级策略指南。
我们最近审计了钻石标准代码的实现,这是一种新的升级模式。虽然这是值得称赞的尝试,但钻石提案和实现存在诸多问题。代码过度工程化,包含大量不必要的复杂性,目前我们不推荐使用。
当然,该提案仍处于草案阶段,有改进空间。一个可行的升级标准应该包含:
- 清晰简单的实现。标准应易于阅读,以简化与第三方应用的集成
- 完整的升级程序清单。升级是高风险过程,必须详细解释
- 针对最常见升级错误的链上缓解措施,包括函数遮蔽和冲突
- 相关风险列表。升级很困难,可能隐藏安全考虑或暗示风险很小
- 与最常见测试平台集成的测试
遗憾的是,钻石提案未能解决这些问题。
钻石提案范式
钻石提案是EIP 2535中定义的一个进行中的工作。该草案声称基于delegatecall提出了一种新的合约升级范式。
查找表
基于delegatecall的升级性主要包含两个组件——代理和实现:
用户与代理交互,代理delegatecall到实现。实现代码被执行,而存储保留在代理中。
使用查找表允许delegatecall到多个合约实现,根据要执行的函数选择适当的实现。
任意存储指针
提案还建议使用Solidity最近引入的功能:任意存储指针,允许将存储指针分配到任意位置。
由于存储保留在代理上,实现的存储布局必须遵循代理的存储布局。EIP提议每个实现都有一个关联结构来保存实现变量,以及一个指向存储结构的任意存储位置的指针。
审计发现和建议
我们的审计发现:
过度工程化的代码
虽然EIP中提出的模式很直接,但其实际实现难以阅读和审查,增加了问题的可能性。
例如,许多链上保存的数据很繁琐。虽然提案只需要一个从函数签名到实现地址的查找表,但EIP定义了许多需要存储额外数据的接口。
存储指针风险
尽管声称如果基指针不同就不可能发生冲突,但恶意合约可能与另一个实现的变量发生冲突。
函数遮蔽
可升级合约通常在代理中有遮蔽应该被委托的函数的函数。对这些函数的调用永远不会被委托,因为它们将在代理中执行。
无合约存在检查
另一个常见错误是缺少对合约代码存在性的检查。如果代理委托到错误的地址或已被销毁的实现,对实现的调用将返回成功,即使没有代码被执行。
不必要的钻石词汇
钻石提案严重依赖其新创建的词汇。这容易出错,使审查更加困难,并且对开发人员没有好处。
建议
- 始终追求简单,尽可能将代码保持在链下
- 编写新标准时,保持代码可读且易于理解
- 在实施优化之前分析需求
- 使用crytic.io或slither-check-upgradeability来捕获遮蔽实例
- 调用任意合约时始终检查合约存在性
- 使用常见、众所周知的词汇,不需要时不发明术语
升级性是否可行?
多年来,我们审查了许多可升级合约并发布了多篇相关分析。升级性困难、容易出错并增加风险,我们通常仍不推荐它作为解决方案。但需要合约升级性的开发人员应该:
- 考虑不需要delegatecall的升级设计
- 彻底审查现有解决方案及其局限性
- 使用crytic.io或在CI中添加slither-check-upgradeability