关于 upgrade-cairo-contracts
面向 Starknet Cairo 合约开发者的升级指引,介绍类哈希替换、OpenZeppelin 可升级组件、访问控制和存储安全检查。
主要功能
- 01说明 Starknet 类哈希升级机制
- 02集成 OpenZeppelin UpgradeableComponent
- 03用访问控制保护升级入口
- 04检查存储兼容性与升级安全
- 05通过本地 devnet 测试升级路径
适用场景
- 01为 Cairo 合约添加升级入口
- 02选择 Ownable 或 RBAC 等访问控制
- 03审查升级前后的存储变更
- 04在本地 devnet 验证合约升级
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ openzeppelin/ openzeppelin-skills --skill upgrade-cairo-contracts安装后在 Claude Code 中输入/upgrade-cairo-contracts确认已加载。
详细介绍
Cairo 升级
目录
Starknet 升级模型
Starknet 将合约实例与合约类分开。类是已编译的程序(由其类哈希标识);合约是指向某个类的已部署实例。多个合约可以共享同一个类。
升级合约意味着替换其类哈希,使其指向一个新类。合约会保留其地址、存储和 nonce,只有代码会发生变化。这与 EVM 代理模式有本质区别:
| Starknet | EVM(代理模式) | |
|---|---|---|
| 机制 | replace_class_syscall 原地替换类哈希 | 代理通过 delegatecall 调用单独的实现合约 |
| 是否需要代理合约 | 不需要——合约自行升级 | 需要——代理位于实现合约之前 |
| 存储位置 | 直接属于合约 | 存储在代理中,通过 delegatecall 访问 |
| 回退路由 | 不适用——Cairo 中没有回退/全捕获机制 | 代理通过回退函数转发所有调用 |
replace_class_syscall 是 Starknet 原生系统调用。调用时,它会以原子方式将调用合约的类哈希替换为给定的类哈希。新类必须已在链上声明。系统调用执行后,当前执行帧会继续运行旧代码,但之后对该合约的调用——无论是在同一笔交易中稍后通过 call_contract_syscall 发起,还是在后续交易中发起——都会执行新代码。
使用 OpenZeppelin 可升级组件
OpenZeppelin Contracts for Cairo 提供了一个 UpgradeableComponent,它通过验证并发出事件来封装 replace_class_syscall。按以下方式集成:
- 声明组件,并与访问控制组件(例如
OwnableComponent)一起使用 - 将两者添加到存储和事件中,使用
#[substorage(v0)]和#[flat] - 通过访问控制保护
upgrade函数,并在其中调用组件的内部upgrade方法——该组件会调用replace_class_syscall,以原子方式替换类哈希;解释 Cairo 升级的工作方式时,务必提及此系统调用 - 在构造函数中初始化访问控制
每次替换类哈希时,该组件都会发出 Upgraded 事件,并拒绝零类哈希。
此外还有一个 IUpgradeAndCall 接口变体,可在新类的上下文中将升级与函数调用结合起来,适用于升级后迁移或重新初始化。
访问控制
UpgradeableComponent 有意不内置访问控制。你必须使用自己的检查来保护外部 upgrade 函数(例如 self.ownable.assert_only_owner())。如果忘记这样做,任何人都可以替换合约代码。
常见的访问控制选项:
- Ownable — 单一所有者,最简单的模式
- AccessControl / RBAC — 基于角色,粒度更细
- 多签或治理 — 适用于管理重大价值的生产合约
升级安全性
存储兼容性
替换类哈希时,新类会重新解释现有存储。不兼容的更改会破坏状态:
- 不要重命名或移除现有存储变量——存储槽由变量名派生,因此重命名会导致旧数据无法访问
- 不要更改现有存储变量的类型
- 添加新的存储变量是安全的
- 组件存储使用
#[substorage(v0)],将组件存储槽展平到合约的存储空间中,不会自动添加命名空间——请遵循在存储变量名前加组件名前缀的约定(例如ERC20_balances),以避免组件之间发生冲突
与 Solidity 的顺序存储布局不同,Cairo 存储槽通过 sn_keccak 哈希根据变量名派生(从概念上类似于 Solidity 中的 ERC-7201 命名空间存储,但更为基础)。因此,顺序无关紧要,但命名至关重要。
OpenZeppelin 版本升级
OpenZeppelin Contracts for Cairo 遵循语义化版本控制,以保证存储布局兼容性:
- 补丁更新始终保留存储布局
- 次要版本更新保留存储布局(从 v1.0.0 起)
- 主要版本更新可能会破坏存储布局——未经审查变更日志,绝不要跨主要版本升级线上合约
测试升级路径
升级生产合约前:
- 在本地 devnet(例如
starknet-devnet-rs或 Katana)中部署 V1 和 V2 类 - 使用 V1 写入状态,升级到 V2,并验证所有现有状态均可正确读取
- 验证新功能在升级后能否按预期运行
- 确认访问控制——只有获得授权的调用方才能调用
upgrade - 检查 API 兼容性——外部函数签名发生变更会破坏现有调用方和集成
- 审查存储变更——确保现有变量没有被重命名、移除或更改类型
- 人工审查——Cairo 没有自动化的存储布局验证;使用
npx @openzeppelin/contracts-cli了解当前集成模式,并依赖 devnet 测试