stellar_contract_utils/upgradeable/mod.rs
1//! # Lightweight upgradeability framework
2//!
3//! This module defines a minimal system for managing contract upgrades, with
4//! optional support for handling migrations in a structured and safe manner.
5//! The framework enforces correct sequencing of operations, e.g. migration can
6//! only be invoked after an upgrade.
7//!
8//! It is recommended to use this module via the `#[derive(Upgradeable)]` macro,
9//! or via the `#[derive(UpgradeableMigratable)]` when custom migration logic is
10//! additionally needed.
11//!
12//! If a rollback is required, the contract can be upgraded to a newer version
13//! where the rollback-specific logic is defined and performed as a migration.
14//!
15//! **IMPORTANT**: While the framework structures the upgrade flow, it does NOT
16//! perform deeper checks and verifications such as:
17//!
18//! - Ensuring that the new contract does not include a constructor, as it will
19//! not be invoked.
20//! - Verifying that the new contract includes an upgradability mechanism,
21//! preventing an unintended loss of further upgradability capacity.
22//! - Checking for storage consistency, ensuring that the new contract does not
23//! inadvertently introduce storage mismatches.
24//!
25//!
26//! Example for upgrade only:
27//! ```rust,ignore
28//! #[derive(Upgradeable)]
29//! #[contract]
30//! pub struct ExampleContract;
31//!
32//! impl UpgradeableInternal for ExampleContract {
33//! fn _require_auth(e: &Env, operator: &Address) {
34//! operator.require_auth();
35//! let owner = e.storage().instance().get::<_, Address>(&OWNER).unwrap();
36//! if *operator != owner {
37//! panic_with_error!(e, ExampleContractError::Unauthorized)
38//! }
39//! }
40//! }
41//! ```
42//!
43//! # Example for upgrade and migration:
44//! ```ignore,rust
45//! #[contracttype]
46//! pub struct Data {
47//! pub num1: u32,
48//! pub num2: u32,
49//! }
50//!
51//! #[derive(UpgradeableMigratable)]
52//! #[contract]
53//! pub struct ExampleContract;
54//!
55//! impl UpgradeableMigratableInternal for ExampleContract {
56//! type MigrationData = Data;
57//!
58//! fn _require_auth(e: &Env, operator: &Address) {
59//! operator.require_auth();
60//! let owner = e.storage().instance().get::<_, Address>(&OWNER).unwrap();
61//! if *operator != owner {
62//! panic_with_error!(e, ExampleContractError::Unauthorized)
63//! }
64//! }
65//!
66//! fn _migrate(e: &Env, data: &Self::MigrationData) {
67//! e.storage().instance().set(&DATA_KEY, data);
68//! }
69//! }
70//! ```
71//! Check in the "/examples/upgradeable/" directory for the full example, where
72//! you can also find a helper `Upgrader` contract that performs upgrade+migrate
73//! in a single transaction.
74
75mod storage;
76
77mod test;
78
79use soroban_sdk::{contractclient, contracterror, Address, BytesN, Env, FromVal, Val};
80
81pub use crate::upgradeable::storage::{
82 can_complete_migration, complete_migration, enable_migration, ensure_can_complete_migration,
83};
84
85/// High-level trait for contract upgrades.
86///
87/// This trait defines the external entry point and can be used in two ways:
88///
89/// 1. Standalone – Implement this trait directly when full control over access
90/// control and upgrade logic is required. In this case, the implementor is
91/// responsible for ensuring:
92/// - Proper authorization of the `operator`
93/// - Versioning management
94///
95/// 2. Framework-assisted usage – When using the lightweight upgrade framework
96/// provided in this module, you should NOT manually implement this trait.
97/// Instead:
98/// - Derive it using `#[derive(Upgradeable)]`
99/// - Provide access control by implementing [`UpgradeableInternal`] with
100/// your custom logic
101#[contractclient(name = "UpgradeableClient")]
102pub trait Upgradeable {
103 /// Upgrades the contract by setting a new WASM bytecode. The
104 /// contract will only be upgraded after the invocation has
105 /// successfully completed.
106 ///
107 /// # Arguments
108 ///
109 /// * `e` - Access to Soroban environment.
110 /// * `new_wasm_hash` - A 32-byte hash identifying the new WASM blob,
111 /// uploaded to the ledger.
112 /// * `operator` - The authorized address performing the upgrade.
113 fn upgrade(e: &Env, new_wasm_hash: BytesN<32>, operator: Address);
114}
115
116/// Trait to be implemented for a custom upgrade authorization mechanism.
117/// Requires defining access control logic for who can upgrade the contract.
118pub trait UpgradeableInternal {
119 /// Ensures the `operator` has signed and is authorized to perform the
120 /// upgrade.
121 ///
122 /// This must be implemented by the consuming contract.
123 ///
124 /// # Arguments
125 ///
126 /// * `e` - The Soroban environment.
127 /// * `operator` - The address attempting the upgrade. Can be G-account, or
128 /// another contract (C-account) such as timelock or governor.
129 fn _require_auth(e: &Env, operator: &Address);
130}
131
132/// High-level trait for a combination of upgrade and migration logic in
133/// upgradeable contracts.
134///
135/// This trait defines the external entry points for applying both, an upgrade
136/// and a migration. It is recommended to be used only as part of the
137/// lightweight upgrade framework provided in this module.
138///
139/// When using the framework, this trait is automatically derived with
140/// `#[derive(UpgradeableMigratable)]`, and should NOT be manually implemented.
141/// Instead, the contract must define access control via `_require_auth` and
142/// provide its custom migration logic by implementing
143/// `UpgradeableMigratableInternal`.
144pub trait UpgradeableMigratable: UpgradeableMigratableInternal {
145 /// Upgrades the contract by setting a new WASM bytecode. The
146 /// contract will only be upgraded after the invocation has
147 /// successfully completed.
148 ///
149 /// # Arguments
150 ///
151 /// * `e` - Access to Soroban environment.
152 /// * `new_wasm_hash` - A 32-byte hash identifying the new WASM blob,
153 /// uploaded to the ledger.
154 /// * `operator` - The authorized address performing the upgrade and the
155 /// migration.
156 fn upgrade(e: &Env, new_wasm_hash: BytesN<32>, operator: Address);
157
158 /// Entry point to handle a contract migration.
159 ///
160 /// # Arguments
161 ///
162 /// * `e` - The Soroban environment.
163 /// * `migration_data` - Arbitrary data passed to the migration logic.
164 /// * `operator` - The authorized address performing the upgrade and the
165 /// migration.
166 fn migrate(e: &Env, migration_data: Self::MigrationData, operator: Address);
167}
168
169/// Trait to be implemented for custom migration. Requires defining access
170/// control and custom business logic for a migration after an upgrade.
171pub trait UpgradeableMigratableInternal {
172 /// Type representing structured data needed during migration.
173 type MigrationData: FromVal<Env, Val>;
174
175 /// Applies migration logic using the given data.
176 ///
177 /// # Arguments
178 ///
179 /// * `e` - The Soroban environment.
180 /// * `migration_data` - Migration-specific input data.
181 fn _migrate(e: &Env, migration_data: &Self::MigrationData);
182
183 /// Ensures the `operator` has signed and is authorized to perform the
184 /// upgrade and the migration.
185 ///
186 /// This must be implemented by the consuming contract.
187 ///
188 /// # Arguments
189 ///
190 /// * `e` - The Soroban environment.
191 /// * `operator` - The address attempting the upgrade and the migration. Can
192 /// be a G-account, or another contract (C-account) such as timelock or
193 /// governor.
194 fn _require_auth(e: &Env, operator: &Address);
195}
196
197#[contracterror]
198#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
199#[repr(u32)]
200pub enum UpgradeableError {
201 /// When migration is attempted but not allowed due to upgrade state.
202 MigrationNotAllowed = 1100,
203}