本文详细介绍了以太坊虚拟机(EVM)代理和可升级性的安全检查清单,强调了在构建可升级合约时可能出现的五种关键失败情况,包括存储冲突、未受保护的初始化器、实现自毁、函数选择器冲突和升级授权绕过。文章还提供了涵盖33个安全检查项的清单,旨在帮助开发者和审计人员在开发和部署代理合约时避免常见的安全漏洞,确保用户资金安全。
构建可升级合约?以下是导致超过 3.5 亿美元代理合约漏洞利用的关键失败点:
此清单涵盖 8 个领域中的 33 项安全检查。在审计之前、开发期间以及作为所有代理合约实现发布前的关卡使用它。
🔐 交互式清单可用
我们创建了此清单的交互式版本,其中包含可展开的详细信息、代码示例和审计复选框。非常适合审计员和开发团队。
可升级的智能合约已成为 DeFi 协议的必备组件,这些协议需要在部署后进行迭代、修复错误和添加功能。代理模式通过将合约逻辑(实现)与状态(代理)分离来实现这一点,从而允许逻辑更新,同时保留用户数据和资金。
但这种能力带来了非凡的复杂性和风险。
数字说明了一切:
代理模式是最容易被利用的智能合约机制之一,因为它们违反了 Solidity 的许多安全性假设。与简单的合约不同,代理合约必须管理:
本清单是通过分析领先公司的 50 多份代理合约审计报告,并结合 DeFi 中主要代理合约漏洞利用的事后分析而提炼出来的。无论你是实现 OpenZeppelin 的可升级合约、构建自定义代理模式,还是审计可升级系统,本指南都将帮助你避免成为下一个受害者。
此清单分为八个关键领域:
每个部分包括:
对于使用特定代理模式的团队,我们提供了针对 UUPS 代理、透明代理 和 钻石模式 的专门指导。
存储冲突是代理合约漏洞的首要原因。当代理合约和实现合约使用重叠的存储槽时,升级可能会静默覆盖关键数据,如用户余额、所有权或协议参数。其影响通常是灾难性的且无法挽回。
与其他智能合约错误不同,存储冲突几乎不可能通过测试检测到,因为它们可能仅在特定升级序列后才 проявляться。到那时,用户资金已经丢失。
bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1) 作为实现地址存储bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1) 作为管理员地址存储bytes32(uint256(keccak256('eip1967.proxy.beacon')) - 1)0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc 的槽uint256[50] private __gap;uint256[50] 更改为 uint256[48]代理合约不能使用构造函数,因为代理的存储与实现隔离。这种根本区别产生了独特的初始化漏洞,这些漏洞在常规合约中不存在。未受保护的初始化器已导致完整的协议接管。
initializer 修饰符或等效的保护_disableInitializers() 不可变变量并调用onlyInitializing 修饰符调用所有父初始化器_disableInitializers() 以防止直接初始化_disableInitializers()不受限制的升级能力相当于让攻击者完全控制用户资金。薄弱的升级授权已导致无数次“rug pulls”,其中恶意实现耗尽协议资金。即使是合法的项目,当控制升级的单个密钥被泄露时也会受到影响。
proxiableUUID() 和升级函数Delegatecall 是使代理合约工作的机制,但它也是其最大的漏洞来源。当代理合约 delegatecall 实现合约时,实现合约代码在代理合约的存储上下文中运行。这种上下文切换会产生存储损坏、未经授权的访问和特定于实现的攻击的机会。
extcodesize > 0,以防止调用空地址代理合约会创建额外的访问控制复杂性层。代理合约有自己的管理员/所有者角色,而实现合约可能有不同的角色假设。配置错误的访问控制可能会允许未经授权的升级、绕过预期的限制或创建权限提升漏洞。
msg.sender,而不考虑代理合约上下文由于所有代理合约共享同一个实现合约,因此实现合约中的漏洞会影响每个代理合约实例。受损或销毁的实现合约可以同时破坏数百或数千个代理合约实例,从而将其影响放大到远远超出单个合约故障的范围。
代理合约根据函数选择器(函数签名的前 4 个字节)将函数调用转发给实现合约。当代理合约和实现合约具有具有相同选择器的函数时,代理合约函数优先,可能会绕过预期的访问控制或破坏预期的行为。
upgrade()、changeAdmin() 之类的函数某些漏洞模式在不同的代理合约实现中反复出现。这些常见的陷阱是导致大多数与代理合约相关的漏洞利用的原因,应该在任何代理合约实现中进行系统地检查。
对于实施通用可升级代理标准 (UUPS) 的团队:
0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc_authorizeUpgrade对于使用 OpenZeppelin 的透明代理合约模式的团队:
_disableInitializers()对于实施 EIP-2535 钻石标准的团队:
安全检查只有在你的测试良好时才有效。以下是你的测试套件应涵盖的内容:
代理模式解锁了在部署后迭代和改进智能合约的能力,但它们引入了如果处理不当可能会造成灾难性后果的复杂性。安全代理合约实现与 1 亿美元的漏洞利用之间的区别通常归结为细微的细节:缺少存储间隙、未受保护的初始化器或函数选择器冲突。
使代理合约具有强大功能的升级能力也使其具有独特的危险性。与包含错误的常规智能合约不同,代理合约漏洞可能会影响依赖合约和用户的整个生态系统。
将此清单用作你的基础,而不是你的上限。每个代理合约实现都是唯一的,并且你的安全分析应考虑你的特定升级模式、访问控制模型和集成要求。
代理合约安全性出错的成本以数亿美元和破坏的用户信任来衡量。正确的成本是什么?有条不紊的开发实践、彻底的测试和全面的审计。
明智地选择。你用户的资金取决于此。
构建可升级合约或审计代理合约实现?代理合约安全性不仅复杂,而且是专门化的。
在 Zealynx,我们已经审计了 UUPS、透明、钻石和自定义模式中的数十个代理合约实现。我们知道隐藏在存储布局、初始化序列和升级机制中的细微漏洞。
准备好保护你的可升级合约了吗?获取报价 或 直接联系 以讨论你的代理合约实现。
1. 代理合约漏洞的最常见原因是什么?
存储冲突和未受保护的初始化器占代理合约漏洞的 60% 以上。当代理合约和实现合约使用相同的存储槽时,会发生存储冲突,从而破坏数据。未受保护的初始化器允许任何人重新初始化合约并获得管理员访问权限。
2. 我应该使用 UUPS 还是透明代理合约模式?
UUPS 更节省 gas 且更灵活,但会将升级逻辑置于实现合约中(更复杂)。透明代理合约更简单、更安全,但会花费更多的 gas。对于新项目,如果你具有安全实施 UUPS 的专业知识,通常建议使用 UUPS。
3. 如何防止升级中的存储冲突?
遵循 ERC-1967 获取代理合约存储槽,在所有可升级合约中使用存储间隙(uint256[50] private __gap),记录存储布局,并使用 OpenZeppelin 的存储布局验证工具。永远不要将变量添加到现有存储布局的中间。
4. 我可以将代理合约升级到完全不同的合约吗?
从技术上讲,可以,但这非常危险。新的实现合约必须维护兼容的存储布局、函数接口和访问控制。添加新功能比完全替换现有逻辑更安全。
5. 如果我的实现合约被 self-destructed 会发生什么?
使用该实现合约的所有代理合约都将永久损坏。这就是为什么实现合约永远不应包含 selfdestruct 并且应被视为不可变基础设施的原因。始终在升级之前验证实现合约代码。
6. 代理合约实现应该获得多少次审计?
至少由具有代理合约模式经验的公司进行两次独立的审计。代理合约安全性是专门化的 —— 通用智能合约审计师可能会错过特定于代理合约的漏洞。对于高价值系统,请考虑进行其他审计。
| 术语 | 定义 |
|---|---|
| 实现合约 | 包含实际业务逻辑的合约,由代理合约通过 delegatecall 调用 |
| 升级授权 | 限制谁可以升级代理合约实现合约的访问控制机制 |
| 函数选择器冲突 | 当代理合约和实现合约具有相同的 4 字节函数签名时,会导致不可预测的行为 |
| 实现合约初始化器 | 替换代理合约模式中构造函数的函数,用于设置初始合约状态 |
| 非结构化存储 | 使用 keccak256 哈希派生槽的存储模式,以防止代理合约和实现合约变量之间的冲突 |
- 原文链接: zealynx.io/blogs/proxy-u...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!