关于 upgrade-stellar-contracts
使用 OpenZeppelin 可升级模块规划和实现 Soroban 合约升级的技能,讲解授权、可选存储迁移、原子升级与迁移模式,以及保持状态兼容的检查方法。
主要功能
- 01Soroban 原生升级模型
- 02仅升级与升级迁移模式
- 03授权实现指导
- 04原子升级与迁移模式
- 05存储兼容性与测试检查
适用场景
- 01设计 Soroban 合约升级路径
- 02为代码升级添加存储迁移
- 03审查升级授权控制
- 04上线前测试状态兼容性
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ openzeppelin/ openzeppelin-skills --skill upgrade-stellar-contracts安装后在 Claude Code 中输入/upgrade-stellar-contracts确认已加载。
详细介绍
Stellar 升级
目录
Soroban 升级模型
Soroban 合约默认可变。可变性是指智能合约修改自身 WASM 字节码的能力,这会改变其函数接口、执行逻辑或元数据。Soroban 提供了内置的协议级机制来升级合约,无需使用代理模式。
如果合约经过明确设计,就可以自行升级。反之,只要不提供升级函数,合约就会变为不可变。这与 EVM 代理模式有本质区别:
| Soroban | EVM(代理模式) | Starknet | |
|---|---|---|---|
| 机制 | 原生 WASM 字节码替换 | 代理通过 delegatecall 调用实现合约 | replace_class_syscall 原地替换类哈希 |
| 是否需要代理合约 | 不需要——合约自行升级 | 需要——代理位于实现合约之前 | 不需要——合约自行升级 |
| 存储位置 | 直接归属于合约 | 存储在代理中,通过 delegatecall 访问 | 直接归属于合约 |
| 选择不可变性 | 不公开升级函数 | 不部署代理 | 不调用系统调用 |
与需要代理合约和 delegatecall 转发的平台相比,协议级可升级性的一个优势是显著降低了风险面。
新实现只有在当前调用完成之后才会生效。这意味着,如果新实现中定义了迁移逻辑,该逻辑无法在执行升级的同一次调用中运行。可以使用辅助 Upgrader 合约封装这两个调用,以实现原子性(见下文)。
使用 OpenZeppelin 可升级模块
OpenZeppelin Stellar Soroban Contracts 在 contract-utils 包中提供了 upgradeable 模块,包含两个主要组件:
| 组件 | 适用场景 |
|---|---|
Upgradeable | 只需更新 WASM 二进制文件,无需迁移存储 |
UpgradeableMigratable | 升级期间需要修改 WASM 二进制文件和特定存储条目 |
推荐通过派生宏使用这些组件:#[derive(Upgradeable)] 和 #[derive(UpgradeableMigratable)]。这些宏会实现必要的函数,并将 Cargo.toml 中的 crate 版本设置为 WASM 元数据中的二进制版本,以符合 SEP-49 指南。
仅升级
在合约结构体上派生 Upgradeable,然后实现 UpgradeableInternal,并提供一个必需方法:
_require_auth(e: &Env, operator: &Address)— 验证操作员是否有权执行升级(例如,检查是否与存储的所有者地址相符)
operator 参数是升级函数的调用者,可用于基于角色的访问控制。
升级并迁移
在合约结构体上派生 UpgradeableMigratable,然后实现 UpgradeableMigratableInternal,并提供:
- 一个关联的
MigrationData类型,用于定义传递给迁移函数的数据 _require_auth(e, operator)— 与上述相同的授权检查_migrate(e: &Env, data: &Self::MigrationData)— 使用提供的迁移数据修改存储
派生宏可确保只能在成功升级之后调用迁移,从而防止状态不一致和存储损坏。
原子升级与迁移
由于新实现只有在当前调用完成后才会生效,新合约中的迁移逻辑无法与升级在同一次调用中运行。辅助 Upgrader 合约会将两个调用封装为一个原子操作:
use soroban_sdk::{contract, contractimpl, symbol_short, Address, BytesN, Env, Val};
use stellar_contract_utils::upgradeable::UpgradeableClient;
#[contract]
pub struct Upgrader;
#[contractimpl]
impl Upgrader {
pub fn upgrade_and_migrate(
env: Env,
contract_address: Address,
operator: Address,
wasm_hash: BytesN<32>,
migration_data: soroban_sdk::Vec<Val>,
) {
operator.require_auth();
let contract_client = UpgradeableClient::new(&env, &contract_address);
contract_client.upgrade(&wasm_hash, &operator);
env.invoke_contract::<()>(
&contract_address,
&symbol_short!("migrate"),
migration_data,
);
}
}
请为 upgrade_and_migrate 设置适当的访问控制(例如,使用 access 包中的 Ownable,并添加 #[only_owner])。
如果需要回滚,可以将合约升级到一个定义了回滚专用逻辑的新版本,并将其作为迁移来执行。
示例: 请参阅 stellar-contracts repository 的
examples/目录,查看Upgradeable和UpgradeableMigratable的完整集成示例,其中包括Upgrader模式。
访问控制
upgradeable 模块有意不内置访问控制。你必须在 UpgradeableInternal 或 UpgradeableMigratableInternal 的 _require_auth 方法中定义授权逻辑。若忘记这一点,任何人都可以替换你的合约代码。
常见的访问控制选项:
- Ownable — 单一所有者,最简单的模式(在
access包中提供) - AccessControl / RBAC — 基于角色,粒度更细(在
access包中提供) - 多重签名或治理 — 适用于管理重大资产的生产合约
升级安全性
注意事项
该框架会组织升级流程,但不会执行更深入的检查:
- 新合约的构造函数不会被调用——任何初始化都必须通过迁移或单独的调用完成
- 不会自动检查新合约是否包含升级机制——如果升级到不包含升级机制的合约,将永久失去升级能力
- 不会验证存储一致性——新合约可能会无意中引入存储不匹配
存储兼容性
替换 WASM 二进制文件时,现有存储会由新代码重新解释。不兼容的更改会破坏状态:
- 不要删除或重命名现有存储键
- 不要更改现有键下存储值的类型
- 添加新的存储键是安全的
- Soroban 存储使用显式字符串键(例如
symbol_short!("OWNER")),因此键名至关重要——与 EVM 的顺序槽位不同,这里不存在顺序依赖
版本跟踪
派生宏会自动从 Cargo.toml 中提取 crate 版本,并依据 SEP-49 将其作为二进制版本嵌入 WASM 元数据。这支持链上版本跟踪,也可用于协调升级路径。
测试升级路径
升级生产合约前:
- 在本地 Soroban 测试网上部署 V1(例如,使用
stellar-cli和本地网络) - 使用 V1 写入状态,升级到 V2,并验证所有现有状态均可正确读取
- 验证升级后新功能能否按预期运行
- 确认访问控制有效——只有经过授权的调用方才能调用
upgrade - 检查 V2 是否包含升级机制——否则将永久失去升级能力
- 验证存储键兼容性——确保没有删除、重命名现有键或更改其类型
- 如果需要迁移,请使用
Upgrader模式测试原子升级与迁移 - 手动审查——Soroban 没有自动的存储兼容性验证;请使用派生宏安全地搭建升级流程,并依靠测试网测试