Stellar 升级

本文详细介绍了如何使用OpenZeppelin的upgradeable模块在Stellar/Soroban区块链上升级智能合约。文章阐述了Soroban原生WASM字节码替换的升级机制,并对比了其与EVM代理模式的区别。内容涵盖了两种升级模式(仅WASM更新与包含存储迁移),强调了访问控制、存储兼容性等安全考量,并提供了原子升级迁移模式的实现方法及测试指南。

目录

Soroban 升级模型

Soroban 合约默认可变。可变性指智能合约修改其自身 WASM 字节码、改变其函数接口、执行逻辑或元数据的能力。Soroban 提供了一种内置的协议级机制用于合约升级——无需代理模式。

合约如果明确设计为可升级,则可以自行升级。反之,合约如果未提供任何升级功能,则变为不可变。这与 EVM 代理模式有着根本性的不同:

Soroban EVM (代理模式) Starknet
机制 原生 WASM 字节码替换 代理 delegatecall 到实现合约 replace_class_syscall 就地交换类哈希
是否需要代理合约 否 — 合约自行升级 是 — 代理位于实现合约之前 否 — 合约自行升级
存储位置 直接属于合约 存在于代理中,通过 delegatecall 访问 直接属于合约
选择性不可变 不暴露升级函数 不部署代理 不调用系统调用

协议级可升级性的一个优势是,与需要代理合约和 delegatecall 转发的平台相比,风险面显著降低。

新实现仅在当前调用完成之后才生效。这意味着如果迁移逻辑定义在新实现中,它不能在与升级相同的调用中执行。辅助的 Upgrader 合约可以封装这两个调用以实现原子性(见下文)。

使用 OpenZeppelin Upgradeable 模块

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/ 目录,了解 UpgradeableUpgradeableMigratable 的完整工作集成示例,包括 Upgrader 模式。

访问控制

upgradeable 模块有意嵌入访问控制本身。你必须在 UpgradeableInternalUpgradeableMigratableInternal_require_auth 方法中定义授权。忘记这一点将允许任何人替换你的合约代码。

常见的访问控制选项:

  • Ownable — 单一所有者,最简单的模式(在 access 包中可用)
  • AccessControl / RBAC — 基于角色,更细粒度(在 access 包中可用)
  • Multisig 或治理 — 适用于管理重要价值的生产合约

升级安全性

注意事项

该框架构建了升级流程,但执行更深层次的检查:

  • 新合约的构造函数不会被调用 — 任何初始化都必须通过迁移或单独的调用进行
  • 没有自动检查新合约是否包含升级机制 — 升级到一个没有升级机制的合约将永久失去可升级性
  • 存储一致性未经验证 — 新合约可能会无意中引入存储不匹配

存储兼容性

替换 WASM 二进制文件时,现有存储会被新代码重新解释。不兼容的更改会损坏状态:

  • 不要移除或重命名现有存储键
  • 不要更改现有键下存储值的类型
  • 添加新的存储键是安全的
  • Soroban 存储使用显式字符串键(例如 symbol_short!("OWNER")),因此键命名至关重要——与 EVM 顺序槽不同,不存在顺序依赖性

版本跟踪

derive 宏自动从 Cargo.toml 中提取 crate 版本,并将其作为二进制版本嵌入到 WASM 元数据中,遵循 SEP-49。这使得链上版本跟踪成为可能,并可用于协调升级路径。

测试升级路径

在升级生产合约之前:

  • [ ] 在本地 Soroban 测试网(例如,使用本地网络的 stellar-cli)上部署 V1
  • [ ] 使用 V1 写入状态,升级到 V2,并验证所有现有状态读取正确
  • [ ] 验证升级后新功能按预期工作
  • [ ] 确认访问控制 — 只有授权调用者才能调用 upgrade
  • [ ] 检查 V2 是否包含升级机制 — 否则可升级性将永久丢失
  • [ ] 验证存储键兼容性 — 确保没有移除、重命名或更改现有键的类型
  • [ ] 如果需要迁移,使用 Upgrader 模式测试原子升级和迁移
  • [ ] 手动审查 — Soroban 没有自动存储兼容性验证;使用 derive 宏进行安全的升级脚手架,并依赖测试网测试
  • 原文链接: github.com/OpenZeppelin/...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
OpenZeppelin
OpenZeppelin
江湖只有他的大名,没有他的介绍。