Upgrading Wasm bytecode for a deployed contract
Introduction
Upgrading a smart contract allows you to improve or modify your contract without changing its address. This guide will walk you through the process of upgrading a WebAssembly (Wasm) bytecode contract using the Soroban SDK.
Prerequisites:
- Basic understanding of Rust programming language. To brush up on Rust, check out Rustlings or The Rust book.
- Familiarity with Stellar smart contracts
- Installed Stellar CLI and Soroban SDK
Download the upgradeable contract example
The upgradeable contract example demonstrates how to upgrade a Wasm contract.
Code
The example contains both an "old" and "new" contract, where we upgrade from "old" to "new". The code below is for the "old" contract.
#![no_std]
use soroban_sdk::{contractimpl, contracterror, contracttype, Address, BytesN, Env};
#[contracttype]
#[derive(Clone)]
enum DataKey {
Admin,
}
#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[repr(u32)]
pub enum Error {
AlreadyInitialized = 1,
}
#[contract]
pub struct UpgradeableContract;
#[contractimpl]
impl UpgradeableContract {
pub fn init(e: Env, admin: Address) {
if e.storage().instance().has(&DataKey::Admin) {
return Err(Error::AlreadyInitialized);
}
e.storage().instance().set(&DataKey::Admin, &admin);
Ok(())
}
pub fn version() -> u32 {
1
}
pub fn upgrade(e: Env, new_wasm_hash: BytesN<32>) {
let admin: Address = e.storage().instance().get(&DataKey::Admin).unwrap();
admin.require_auth();
e.deployer().update_current_contract_wasm(new_wasm_hash);
}
}
How it works
When upgrading a contract, the key function used is e.deployer().update_current_contract_wasm
, which takes the Wasm hash of the new contract as a parameter. Here’s a step-by-step breakdown of how this process works:
- No change in contract ID: The contract's ID remains the same even after the upgrade. This ensures that all references to the contract stay intact.
- Admin authorization: Before upgrading, the contract checks if the action is authorized by an admin. This is crucial to prevent unauthorized upgrades. Only someone with admin rights can perform the upgrade.
- The upgrade function: Below is the function that handles the upgrade process:
pub fn upgrade(e: Env, new_wasm_hash: BytesN<32>) {
let admin: Address = e.storage().instance().get(&DataKey::Admin).unwrap();
admin.require_auth();
e.deployer().update_current_contract_wasm(new_wasm_hash);
}
e: Env
: The environment object representing the current state of the blockchain.new_wasm_hash: BytesN<32>
: The hash of the new Wasm code for the contract. The Wasm bytecode must already be installed/present on the ledger.- The function first retrieves the admin's address from the contract's storage.
- It then requires the admin's authorization (
admin.require_auth()
) to proceed. - Finally, it updates the contract with the new Wasm code (
e.deployer().update_current_contract_wasm(new_wasm_hash)
).
- The
update_current_contract_wasm
host function will also emit aSYSTEM
contract event that contains the old and new wasm reference, allowing downstream users to be notified when a contract they use is updated. The event structure will havetopics = ["executable_update", old_executable: ContractExecutable, old_executable: ContractExecutable]
anddata = []
.
Tests
Open the upgradeable_contract/old_contract/src/test.rs
file to follow along.
#![cfg(test)]
use crate::Error;
use soroban_sdk::{testutils::Address as _, Address, BytesN, Env};
mod old_contract {
soroban_sdk::contractimport!(
file =
"target/wasm32-unknown-unknown/release/soroban_upgradeable_contract_old_contract.wasm"
);
}
mod new_contract {
soroban_sdk::contractimport!(
file = "../new_contract/target/wasm32-unknown-unknown/release/soroban_upgradeable_contract_new_contract.wasm"
);
}
fn install_new_wasm(e: &Env) -> BytesN<32> {
e.install_contract_wasm(new_contract::Wasm)
}
#[test]
fn test() {
let env = Env::default();
env.mock_all_auths();
// Note that we use register_contract_wasm instead of register_contract
// because the old contracts Wasm is expected to exist in storage.
let contract_id = env.register_contract_wasm(None, old_contract::Wasm);
let client = old_contract::Client::new(&env, &contract_id);
let admin = Address::random(&env);
client.init(&admin);
assert_eq!(1, client.version());
let new_wasm_hash = install_new_wasm(&env);
client.upgrade(&new_wasm_hash);
assert_eq!(2, client.version());
// new_v2_fn was added in the new contract, so the existing
// client is out of date. Generate a new one.
let client = new_contract::Client::new(&env, &contract_id);
assert_eq!(1010101, client.new_v2_fn());
}
#[test]
fn test_cannot_re_init() {
let env = Env::default();
env.mock_all_auths();
// Note that we use register_contract_wasm instead of register_contract
// because the old contracts WASM is expected to exist in storage.
let contract_id = env.register_contract_wasm(None, old_contract::WASM);
let client = old_contract::Client::new(&env, &contract_id);
let admin = Address::generate(&env);
client.init(&admin);
// `try_init` is expected to return an error. Since client is generated from Wasm,
// this is a generic SDK error.
let err: soroban_sdk::Error = client.try_init(&admin).err().unwrap().unwrap();
// Convert the SDK error to the contract error.
let contract_err: Error = err.try_into().unwrap();
// Make sure contract error has the expected value.
assert_eq!(contract_err, Error::AlreadyInitialized);
}
We first import Wasm files for both contracts:
mod old_contract {
soroban_sdk::contractimport!(
file =
"target/wasm32-unknown-unknown/release/soroban_upgradeable_contract_old_contract.wasm"
);
}
mod new_contract {
soroban_sdk::contractimport!(
file = "../new_contract/target/wasm32-unknown-unknown/release/soroban_upgradeable_contract_new_contract.wasm"
);
}
We register the old contract, intialize it with an admin, and verify the version it returns. The note in the code below is important:
// Note that we use register_contract_wasm instead of register_contract
// because the old contracts Wasm is expected to exist in storage.
let contract_id = env.register_contract_wasm(None, old_contract::Wasm);
let client = old_contract::Client::new(&env, &contract_id);
let admin = Address::random(&env);
client.init(&admin);
assert_eq!(1, client.version());
We install the new contract's Wasm:
let new_wasm_hash = install_new_wasm(&env);
Then we run the upgrade, and verify that the upgrade worked:
client.upgrade(&new_wasm_hash);
assert_eq!(2, client.version());
Build the contract
To build the contract .wasm
files, run stellar contract build
in both upgradeable_contract/old_contract
and upgradeable_contract/new_contract
in that order.
Both .wasm
files should be found in both contract target
directories after building both contracts:
target/wasm32-unknown-unknown/release/soroban_upgradeable_contract_old_contract.wasm
target/wasm32-unknown-unknown/release/soroban_upgradeable_contract_new_contract.wasm
Run the contract
If you have stellar-cli
installed, you can invoke contract functions. Deploy the old contract and install the Wasm for the new contract.
Navigate to to upgradeable_contract/old_contract
stellar contract deploy \
--wasm target/wasm32-unknown-unknown/release/soroban_upgradeable_contract_old_contract.wasm \
--source alice \
--network testnet
- When you deploy a smart contract to a network, you need to specify an identity that will be used to sign the transactions. Change the
alice
to your own identity.
You should see this similar contract ID after you have ran the deploy command:
CAS6FKBXGVXFGU2SPPPJJOIULJNPMPR6NVKWLOQP24SZJPMB76TGH7Y3
Navigate to upgradeable_contract/new_contract
and run the following command:
stellar contract install \
--source-account alice \
--wasm target/wasm32-unknown-unknown/release/soroban_upgradeable_contract_new_contract.wasm \
--network testnet
You should see this Wasm hash from the install command:
aa24c81289997ad815489b29db337b53f284cca5aba86e9a8ae5cef7d31842c2
You also need to call the init
method so the admin
address is set. This requires us to setup some identities.
Given that we have previously setup an identity, to get the identity address, we run the following command:
stellar keys address alice
Example output:
GCJ2R5ST4UQP2D4F54Y3IIAQKPMLMEEZCNZ3PEDKY4AGDYEMYUC2MOO7
Navigate to upgradeable_contract/old_contract
and now call init
with this key (make sure to substitute with the key you generated):
stellar contract invoke \
--id CAS6FKBXGVXFGU2SPPPJJOIULJNPMPR6NVKWLOQP24SZJPMB76TGH7Y3 \
--source alice \
--network testnet \
-- \
init \
--admin GCJ2R5ST4UQP2D4F54Y3IIAQKPMLMEEZCNZ3PEDKY4AGDYEMYUC2MOO7
Our deployed old_contract
address is CAS6FKBXGVXFGU2SPPPJJOIULJNPMPR6NVKWLOQP24SZJPMB76TGH7Y3
. You may need to replace this value with your own. Invoke the version
function of the contract:
stellar contract invoke \
--id CAS6FKBXGVXFGU2SPPPJJOIULJNPMPR6NVKWLOQP24SZJPMB76TGH7Y3 \
--source alice \
--network testnet \
-- \
version
The following output should occur using the code above:
1
Now upgrade the contract. Notice the --source
must be the identity name matching the address passed to the init
function.
stellar contract invoke \
--id CAS6FKBXGVXFGU2SPPPJJOIULJNPMPR6NVKWLOQP24SZJPMB76TGH7Y3 \
--source alice \
--network testnet \
-- \
upgrade \
--new_wasm_hash aa24c81289997ad815489b29db337b53f284cca5aba86e9a8ae5cef7d31842c2
Invoke the version
function again.
stellar contract invoke \
--id CAS6FKBXGVXFGU2SPPPJJOIULJNPMPR6NVKWLOQP24SZJPMB76TGH7Y3 \
--source alice \
--network testnet \
-- \
version
Now that the contract was upgraded, you'll see a new version.
2
Hooray, our contract has been upgraded!
Guides in this category:
📄️ Using __check_auth in interesting ways
Two guides that walk through using __check_auth
📄️ Making cross-contract calls
Call a smart contract from within another smart contract
📄️ Deploy a contract from installed Wasm bytecode using a deployer contract
Deploy a contract from installed Wasm bytecode using a deployer contract
📄️ Deploy a SAC for a Stellar asset using code
Deploy a SAC for a Stellar asset using Javascript SDK
📄️ Organize contract errors with an error enum type
Manage and communicate contract errors using an enum struct stored as Status values
📄️ Extend a deployed contract’s TTL with code
How to extend the TTL of a deployed contract's Wasm code using JavaScript SDK
📄️ Upgrading Wasm bytecode for a deployed contract
Upgrade Wasm bytecode for a deployed contract
📄️ Write metadata for your contract
Use the contractmeta! macro in Rust SDK to write metadata in Wasm contracts