$npx -y skills add OpenZeppelin/openzeppelin-skills --skill upgrade-stellar-contractsUpgrade Stellar/Soroban smart contracts using OpenZeppelin's upgradeable module. Use when users need to: (1) make Soroban contracts upgradeable via native WASM replacement, (2) use Upgradeable or UpgradeableMigratable derive macros, (3) implement atomic upgrade-and-migrate patter
| 1 | # Stellar Upgrades |
| 2 | |
| 3 | ## Contents |
| 4 | |
| 5 | - [Soroban Upgrade Model](#soroban-upgrade-model) |
| 6 | - [Using the OpenZeppelin Upgradeable Module](#using-the-openzeppelin-upgradeable-module) |
| 7 | - [Access Control](#access-control) |
| 8 | - [Upgrade Safety](#upgrade-safety) |
| 9 | |
| 10 | ## Soroban Upgrade Model |
| 11 | |
| 12 | Soroban contracts are **mutable by default**. Mutability refers to the ability of a smart contract to modify its own WASM bytecode, altering its function interface, execution logic, or metadata. Soroban provides a **built-in, protocol-level mechanism** for contract upgrades — no proxy pattern is needed. |
| 13 | |
| 14 | A contract can upgrade itself if it is explicitly designed to do so. Conversely, a contract becomes immutable simply by not provisioning any upgrade function. This is fundamentally different from EVM proxy patterns: |
| 15 | |
| 16 | | | Soroban | EVM (proxy pattern) | Starknet | |
| 17 | |---|---|---|---| |
| 18 | | **Mechanism** | Native WASM bytecode replacement | Proxy `delegatecall`s to implementation contract | `replace_class_syscall` swaps class hash in-place | |
| 19 | | **Proxy contract needed** | No — the contract upgrades itself | Yes — a proxy sits in front of the implementation | No — the contract upgrades itself | |
| 20 | | **Storage location** | Belongs to the contract directly | Lives in the proxy, accessed via delegatecall | Belongs to the contract directly | |
| 21 | | **Opt-in to immutability** | Don't expose an upgrade function | Don't deploy a proxy | Don't call the syscall | |
| 22 | |
| 23 | One advantage of protocol-level upgradeability is a significantly reduced risk surface compared to platforms that require proxy contracts and delegatecall forwarding. |
| 24 | |
| 25 | The new implementation only becomes effective **after the current invocation completes**. This means if migration logic is defined in the new implementation, it cannot execute within the same call as the upgrade. An auxiliary `Upgrader` contract can wrap both calls to achieve atomicity (see below). |
| 26 | |
| 27 | ## Using the OpenZeppelin Upgradeable Module |
| 28 | |
| 29 | OpenZeppelin Stellar Soroban Contracts provides an `upgradeable` module in the `contract-utils` package with two main components: |
| 30 | |
| 31 | | Component | Use when | |
| 32 | |-----------|----------| |
| 33 | | **`Upgradeable`** | Only the WASM binary needs to be updated — no storage migration required | |
| 34 | | **`UpgradeableMigratable`** | The WASM binary and specific storage entries need to be modified during the upgrade | |
| 35 | |
| 36 | The recommended way to use these is through derive macros: `#[derive(Upgradeable)]` and `#[derive(UpgradeableMigratable)]`. These macros handle the implementation of necessary functions and set the crate version from `Cargo.toml` as the binary version in WASM metadata, aligning with SEP-49 guidelines. |
| 37 | |
| 38 | ### Upgrade only |
| 39 | |
| 40 | Derive `Upgradeable` on the contract struct, then implement `UpgradeableInternal` with a single required method: |
| 41 | |
| 42 | - `_require_auth(e: &Env, operator: &Address)` — verify the operator is authorized to perform the upgrade (e.g., check against a stored owner address) |
| 43 | |
| 44 | The `operator` parameter is the invoker of the upgrade function and can be used for role-based access control. |
| 45 | |
| 46 | ### Upgrade and migrate |
| 47 | |
| 48 | Derive `UpgradeableMigratable` on the contract struct, then implement `UpgradeableMigratableInternal` with: |
| 49 | |
| 50 | - An associated `MigrationData` type defining the data passed to the migration function |
| 51 | - `_require_auth(e, operator)` — same authorization check as above |
| 52 | - `_migrate(e: &Env, data: &Self::MigrationData)` — perform storage modifications using the provided migration data |
| 53 | |
| 54 | The derive macro ensures that migration can only be invoked **after** a successful upgrade, preventing state inconsistencies and storage corruption. |
| 55 | |
| 56 | ### Atomic upgrade and migration |
| 57 | |
| 58 | Because the new implementation only takes effect after the current invocation completes, migration logic in the new contract cannot run in the same call as the upgrade. An auxiliary `Upgrader` contract wraps both calls atomically: |
| 59 | |
| 60 | ```rust |
| 61 | use soroban_sdk::{contract, contractimpl, symbol_short, Address, BytesN, Env, Val}; |
| 62 | use stellar_contract_utils::upgradeable::UpgradeableClient; |
| 63 | |
| 64 | #[contract] |
| 65 | pub struct Upgrader; |
| 66 | |
| 67 | #[contractimpl] |
| 68 | impl Upgrader { |
| 69 | pub fn upgrade_and_migrate( |
| 70 | env: Env, |
| 71 | contract_address: Address, |
| 72 | operator: Address, |
| 73 | wasm_hash: BytesN<32>, |
| 74 | migration_data: soroban_sdk::Vec<Val>, |
| 75 | ) { |
| 76 | operator.require_auth(); |
| 77 | let contract_client = UpgradeableClient::new(&env, &contract_address); |
| 78 | contract_client.upgrade(&wasm_hash, &operator); |
| 79 | env.invoke_contract: |