升级和迁移

默认情况下,Soroban 合约是可变的。Stellar Soroban 上下文中的可变性是指智能合约修改其 WASM 字节码的能力,从而改变其函数接口、执行逻辑或元数据。

Soroban 提供了一种内置的、协议级别定义的合约升级机制,允许合约在显式设计为这样做时升级自身。它的优点之一是它为合约开发者提供了灵活性,他们可以选择通过简单地不提供可升级性机制来使合约不可变。另一方面,与缺乏原生升级支持的其他智能合约平台相比,在协议级别提供可升级性显着降低了风险面。

虽然 Soroban 的内置可升级性消除了许多与管理智能合约升级和迁移相关的挑战,但仍必须考虑某些注意事项。

概述

upgradeable 模块提供了一个轻量级的可升级性框架,并额外支持结构化和安全迁移。

它由两个主要组件组成:

  1. Upgradeable 用于只需要更新 WASM 二进制文件的情况。

  2. UpgradeableMigratable 适用于更高级的场景,除了 WASM 二进制文件之外,还必须在升级过程中修改(迁移)特定的存储条目。

推荐的使用此模块的方式是通过 #[derive(Upgradeable)]#[derive(UpgradeableMigratable)] 宏。

它们处理必要函数的实现,允许开发者仅专注于管理授权和访问控制。这些派生宏还利用合约 Cargo.toml 中的 crate 版本,并将其设置为 WASM 元数据中的二进制版本,与 SEP-49 中概述的指南保持一致。

虽然该框架构建了升级流程,但它不执行更深入的检查和验证,例如:

  • 确保新合约不包含构造函数,因为它不会被调用。

  • 验证新合约是否包含可升级性机制,防止意外丢失进一步的可升级性能力。

  • 检查存储一致性,确保新合约不会无意中引入存储不匹配。

用法

仅升级

Upgradeable

当只需要升级 WASM 二进制文件并且不需要额外的迁移逻辑时,开发者应该实现 UpgradeableInternal trait。此 trait 定义了授权和自定义访问控制逻辑,指定谁可以执行升级。这种最小的实现使重点仅集中在控制升级权限上。

use soroban_sdk::{
    contract, contracterror, contractimpl, panic_with_error, symbol_short, Address, Env,
};
use stellar_contract_utils::upgradeable::UpgradeableInternal;
use stellar_macros::Upgradeable;

#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[repr(u32)]
pub enum ExampleContractError {
    Unauthorized = 1,
}

#[derive(Upgradeable)]
#[contract]
pub struct ExampleContract;

#[contractimpl]
impl ExampleContract {
    pub fn __constructor(e: &Env, admin: Address) {
        e.storage().instance().set(&symbol_short!("OWNER"), &admin);
    }
}

impl UpgradeableInternal for ExampleContract {
    fn _require_auth(e: &Env, operator: &Address) {
        operator.require_auth();
        // `operator` 是升级函数的调用者,如果已实现,可用于执行基于角色的访问控制
        let owner: Address = e.storage().instance().get(&symbol_short!("OWNER")).unwrap();
        if *operator != owner {
            panic_with_error!(e, ExampleContractError::Unauthorized)
        }
    }
}

升级和迁移

UpgradeableMigratable

当需要修改 WASM 二进制文件和特定存储条目作为升级过程的一部分时,应该实现 UpgradeableMigratableInternal trait。除了定义访问控制和迁移逻辑之外,开发者还必须指定一个关联类型,表示迁移所需的数据。

#[derive(UpgradeableMigratable)] 宏管理操作的顺序,确保迁移只能在成功升级后调用,防止潜在的状态不一致和存储损坏。

use soroban_sdk::{
    contract, contracterror, contracttype, panic_with_error, symbol_short, Address, Env,
};
use stellar_contract_utils::upgradeable::UpgradeableMigratableInternal;
use stellar_macros::UpgradeableMigratable;

#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[repr(u32)]
pub enum ExampleContractError {
    Unauthorized = 1,
}

#[contracttype]
pub struct Data {
    pub num1: u32,
    pub num2: u32,
}

#[derive(UpgradeableMigratable)]
#[contract]
pub struct ExampleContract;

impl UpgradeableMigratableInternal for ExampleContract {
    type MigrationData = Data;

    fn _require_auth(e: &Env, operator: &Address) {
        operator.require_auth();
        let owner: Address = e.storage().instance().get(&symbol_short!("OWNER")).unwrap();
        if *operator != owner {
            panic_with_error!(e, ExampleContractError::Unauthorized)
        }
    }

    fn _migrate(e: &Env, data: &Self::MigrationData) {
        e.storage().instance().set(&symbol_short!("DATA_KEY"), data);
    }
}
如果需要回滚,可以将合约升级到更新的版本,其中回滚特定逻辑被定义并作为迁移执行。

原子升级和迁移

执行升级时,新的实现只有在当前调用完成后才会生效。这意味着如果新的实现中包含迁移逻辑,则无法在同一调用中执行它。为了解决这个问题,可以使用一个名为 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);
        // 该合约不知道迁移函数的参数类型,因此我们需要用 invoke_contract 调用它。
        env.invoke_contract::<()>(&contract_address, &symbol_short!("migrate"), migration_data);
    }
}