本文详细介绍了如何使用OpenZeppelin的upgradeable模块在Stellar/Soroban区块链上升级智能合约。文章阐述了Soroban原生WASM字节码替换的升级机制,并对比了其与EVM代理模式的区别。内容涵盖了两种升级模式(仅WASM更新与包含存储迁移),强调了访问控制、存储兼容性等安全考量,并提供了原子升级迁移模式的实现方法及测试指南。
Soroban 合约默认可变。可变性指智能合约修改其自身 WASM 字节码、改变其函数接口、执行逻辑或元数据的能力。Soroban 提供了一种内置的协议级机制用于合约升级——无需代理模式。
合约如果明确设计为可升级,则可以自行升级。反之,合约如果未提供任何升级功能,则变为不可变。这与 EVM 代理模式有着根本性的不同:
| Soroban | EVM (代理模式) | Starknet | |
|---|---|---|---|
| 机制 | 原生 WASM 字节码替换 | 代理 delegatecall 到实现合约 |
replace_class_syscall 就地交换类哈希 |
| 是否需要代理合约 | 否 — 合约自行升级 | 是 — 代理位于实现合约之前 | 否 — 合约自行升级 |
| 存储位置 | 直接属于合约 | 存在于代理中,通过 delegatecall 访问 |
直接属于合约 |
| 选择性不可变 | 不暴露升级函数 | 不部署代理 | 不调用系统调用 |
协议级可升级性的一个优势是,与需要代理合约和 delegatecall 转发的平台相比,风险面显著降低。
新实现仅在当前调用完成之后才生效。这意味着如果迁移逻辑定义在新实现中,它不能在与升级相同的调用中执行。辅助的 Upgrader 合约可以封装这两个调用以实现原子性(见下文)。
OpenZeppelin Stellar Soroban Contracts 在 contract-utils 包中提供了一个 upgradeable 模块,包含两个主要组件:
| 组件 | 何时使用 |
|---|---|
Upgradeable |
只需要更新 WASM 二进制文件——无需存储迁移 |
UpgradeableMigratable |
WASM 二进制文件和特定存储条目需要在升级期间修改 |
推荐的使用方式是通过 derive 宏:#[derive(Upgradeable)] 和 #[derive(UpgradeableMigratable)]。这些宏处理必要函数的实现,并将 Cargo.toml 中的 crate 版本设置为 WASM 元数据中的二进制版本,与 SEP-49 指南保持一致。
在合约结构体上 derive Upgradeable,然后使用一个必需方法实现 UpgradeableInternal:
_require_auth(e: &Env, operator: &Address) — 验证操作员是否被授权执行升级(例如,对照存储的 owner 地址进行检查)operator 参数是升级函数的调用者,可用于基于角色的访问控制。
在合约结构体上 derive UpgradeableMigratable,然后使用以下方法实现 UpgradeableMigratableInternal:
MigrationData 类型,定义传递给迁移函数的数据_require_auth(e, operator) — 与上述相同的授权检查_migrate(e: &Env, data: &Self::MigrationData) — 使用提供的迁移数据执行存储修改derive 宏确保迁移只能在成功升级之后才能调用,防止状态不一致和存储损坏。
因为新实现仅在当前调用完成后才生效,所以新合约中的迁移逻辑不能与升级在同一调用中运行。辅助的 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,
);
}
}
使用适当的访问控制(例如,来自 access 包的 Ownable 并带有 #[only_owner])来保护 upgrade_and_migrate。
如果需要回滚,可以将合约升级到一个更新的版本,在该版本中定义回滚专用逻辑并作为迁移执行。
示例:请参阅 stellar-contracts 仓库的
examples/目录,了解Upgradeable和UpgradeableMigratable的完整工作集成示例,包括Upgrader模式。
upgradeable 模块有意不嵌入访问控制本身。你必须在 UpgradeableInternal 或 UpgradeableMigratableInternal 的 _require_auth 方法中定义授权。忘记这一点将允许任何人替换你的合约代码。
常见的访问控制选项:
access 包中可用)access 包中可用)该框架构建了升级流程,但不执行更深层次的检查:
替换 WASM 二进制文件时,现有存储会被新代码重新解释。不兼容的更改会损坏状态:
symbol_short!("OWNER")),因此键命名至关重要——与 EVM 顺序槽不同,不存在顺序依赖性derive 宏自动从 Cargo.toml 中提取 crate 版本,并将其作为二进制版本嵌入到 WASM 元数据中,遵循 SEP-49。这使得链上版本跟踪成为可能,并可用于协调升级路径。
在升级生产合约之前:
stellar-cli)上部署 V1upgradeUpgrader 模式测试原子升级和迁移
- 原文链接: github.com/OpenZeppelin/...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!