升级和迁移
默认情况下,Soroban 合约是可变的。Stellar Soroban 上下文中的可变性是指智能合约修改其 WASM 字节码的能力,从而改变其函数接口、执行逻辑或元数据。
Soroban 提供了一种内置的、协议级别定义的合约升级机制,允许合约在显式设计为这样做时升级自身。它的优点之一是它为合约开发者提供了灵活性,他们可以选择通过简单地不提供可升级性机制来使合约不可变。另一方面,与缺乏原生升级支持的其他智能合约平台相比,在协议级别提供可升级性显着降低了风险面。
虽然 Soroban 的内置可升级性消除了许多与管理智能合约升级和迁移相关的挑战,但仍必须考虑某些注意事项。
概述
upgradeable 模块提供了一个轻量级的可升级性框架,并额外支持结构化和安全迁移。
它由两个主要组件组成:
-
Upgradeable
用于只需要更新 WASM 二进制文件的情况。 -
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);
}
}