Upgrades and Migrations
Soroban contracts are mutable by default. Mutability in the context of Stellar Soroban refers to the ability of a smart contract to modify its WASM bytecode, thereby altering its function interface, execution logic, or metadata.
Soroban provides a built-in, protocol-level defined mechanism for contract upgrades, allowing contracts to upgrade themselves if they are explicitly designed to do so. One of the advantages of it is the flexibility it offers to contract developers who can choose to make the contract immutable by simply not provisioning upgradability mechanics. On the other hand, providing upgradability on a protocol level significantly reduces the risk surface, compared to other smart contract platforms, which lack native support for upgradability.
While Soroban’s built-in upgradability eliminates many of the challenges, related to managing smart contract upgrades and migrations, certain caveats must still be considered.
Overview
The upgradeable module provides a lightweight upgradeability framework with additional support for structured and safe migrations.
It consists of two main components:
-
Upgradeable
for cases where only the WASM binary needs to be updated. -
UpgradeableMigratable
for more advanced scenarios where, in addition to the WASM binary, specific storage entries must be modified (migrated) during the upgrade process.
The recommended way to use this module is through the #[derive(Upgradeable)]
and #[derive(UpgradeableMigratable)]
macros.
They handle the implementation of the necessary functions, allowing developers to focus solely on managing authorizations
and access control. These derive macros also leverage the crate version from the contract’s Cargo.toml
and set it as
the binary version in the WASM metadata, aligning with the guidelines outlined in
SEP-49.
While the framework structures the upgrade flow, it does NOT perform deeper checks and verifications such as:
|
Usage
Upgrade Only
Upgradeable
When only the WASM binary needs to be upgraded and no additional migration logic is required, developers should implement
the UpgradeableInternal
trait. This trait is where authorization and custom access control logic are defined,
specifying who can perform the upgrade. This minimal implementation keeps the focus solely on controlling upgrade
permissions.
use soroban_sdk::{
contract, contracterror, contractimpl, panic_with_error, symbol_short, Address, Env,
};
use stellar_upgradeable::UpgradeableInternal;
use stellar_upgradeable_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` is the invoker of the upgrade function and can be used
// to perform a role-based access control if implemented
let owner: Address = e.storage().instance().get(&symbol_short!("OWNER")).unwrap();
if *operator != owner {
panic_with_error!(e, ExampleContractError::Unauthorized)
}
}
}
Upgrade and Migrate
UpgradeableMigratable
When both the WASM binary and specific storage entries need to be modified as part of the upgrade process, the
UpgradeableMigratableInternal
trait should be implemented. In addition to defining access control and migration
logic, the developer must specify an associated type that represents the data required for the migration.
The #[derive(UpgradeableMigratable)]
macro manages the sequencing of operations, ensuring that the migration can
only be invoked after a successful upgrade, preventing potential state inconsistencies and storage corruption.
use soroban_sdk::{
contract, contracterror, contracttype, panic_with_error, symbol_short, Address, Env,
};
use stellar_upgradeable::UpgradeableMigratableInternal;
use stellar_upgradeable_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);
}
}
If a rollback is required, the contract can be upgraded to a newer version where the rollback-specific logic is defined and performed as a migration. |
Atomic upgrade and migration
When performing an upgrade, the new implementation only becomes effective after the current invocation completes.
This means that if migration logic is included in the new implementation, it cannot be executed within the same
call. To address this, an auxiliary contract called Upgrader
can be used to wrap both invocations, enabling an
atomic upgrade-and-migrate process. This approach ensures that the migration logic is executed immediately after the
upgrade without requiring a separate transaction.
use soroban_sdk::{contract, contractimpl, symbol_short, Address, BytesN, Env, Val};
use stellar_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);
// The types of the arguments to the migrate function are unknown to this
// contract, so we need to call it with invoke_contract.
env.invoke_contract::<()>(&contract_address, &symbol_short!("migrate"), migration_data);
}
}