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 the 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::{contract, contractimpl, contracttype, Address, BytesN, Env};
#[contracttype]
#[derive(Clone)]
enum DataKey {
Admin,
}
#[contract]
pub struct UpgradeableContract;
#[contractimpl]
impl UpgradeableContract {
pub fn __constructor(env: Env, admin: Address) {
env.storage().instance().set(&DataKey::Admin, &admin);
}
pub fn version() -> u32 {
1
}
pub fn upgrade(env: Env, new_wasm_hash: BytesN<32>) {
let admin: Address = e.storage().instance().get(&DataKey::Admin).unwrap();
admin.require_auth();
env.deployer().update_current_contract_wasm(new_wasm_hash);
}
}
mod test;
How it works
When upgrading a contract, the key function used is env.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 address: The contract's address remains the same after the upgrade. This ensures that all references to the contract stay intact.
-
The Wasm executable must already be uploaded: The upgrade depends on the compiled executable (identified by the
new_wasm_hash) being uploaded and available on the blockchain. This must be done prior to invoking the contract'supgrade(...)function. -
Admin authorization: Before upgrading, the contract checks if the action is authorized by the
Adminaddress. 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(env: Env, new_wasm_hash: BytesN<32>) {
let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap();
admin.require_auth();
env.deployer().update_current_contract_wasm(new_wasm_hash);
}env: 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 bytecode (
env.deployer().update_current_contract_wasm(new_wasm_hash)).
-
The
update_current_contract_wasmhost function will also emit aSYSTEMcontract 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)]
extern crate std;
use soroban_sdk::{
symbol_short,
testutils::{Address as _, AuthorizedFunction, AuthorizedInvocation},
Address, BytesN, Env, IntoVal,
};
use crate::{UpgradeableContract, UpgradeableContractClient};
mod new_contract {
soroban_sdk::contractimport!(
file = "../new_contract/target/wasm32v1-none/release/soroban_upgradeable_contract_new_contract.wasm"
);
}
fn install_new_wasm(env: &Env) -> BytesN<32> {
env.deployer().upload_contract_wasm(new_contract::WASM)
}
#[test]
fn test() {
let env = Env::default();
env.mock_all_auths();
let admin = Address::generate(&env);
let contract_id = env.register(UpgradeableContract, (&admin,));
let client = UpgradeableContractClient::new(&env, &contract_id);
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());
// New contract version requires the `NewAdmin` key to be initialized, but since the constructor
// hasn't been called, it is not initialized, thus calling try_upgrade won't work.
let new_update_result = client.try_upgrade(&new_wasm_hash);
assert!(new_update_result.is_err());
// `handle_upgrade` sets the `NewAdmin` key properly.
client.handle_upgrade();
// Now upgrade should succeed (though we are not actually changing the Wasm).
client.upgrade(&new_wasm_hash);
// The new admin is the same as the old admin, so the authorization is still performed for
// the `admin` address.
assert_eq!(
env.auths(),
std::vec![(
admin,
AuthorizedInvocation {
function: AuthorizedFunction::Contract((
contract_id.clone(),
symbol_short!("upgrade"),
(new_wasm_hash,).into_val(&env),
)),
sub_invocations: std::vec![]
}
)]
)
}
We first import the compiled Wasm file for the new contract:
mod new_contract {
soroban_sdk::contractimport!(
file = "../new_contract/target/wasm32v1-none/release/soroban_upgradeable_contract_new_contract.wasm"
);
}
We register the old contract, initialize it with an admin, and verify the version it returns. The note in the code below is important:
let admin = Address::generate(&env);
let contract_id = env.register(UpgradeableContract, (&admin,));
let client = UpgradeableContractClient::new(&env, &contract_id);
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/wasm32v1-none/release/soroban_upgradeable_contract_old_contract.wasm
target/wasm32v1-none/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.
First, navigate to the upgradeable_contract/old_contract directory, and deploy an instance of the old contract. We're providing the alice identity to the __constructor function, so it will be the contract's Admin address. Create and provide your own identities, where necessary.
This command will output the contract address it was deployed to.
stellar contract deploy \
--wasm target/wasm32v1-none/release/soroban_upgradeable_contract_old_contract.wasm \
--source-account alice \
--network testnet \
-- --admin alice
# CAS6FKBXGVXFGU2SPPPJJOIULJNPMPR6NVKWLOQP24SZJPMB76TGH7Y3
Then, navigate to upgradeable_contract/new_contract and upload the compiled executable file for the new contract. This command will output the Sha256 hash of the executable, which will be used later for the new_wasm_hash parameter.
stellar contract upload \
--source-account alice \
--wasm target/wasm32v1-none/release/soroban_upgradeable_contract_new_contract.wasm \
--network testnet
# aa24c81289997ad815489b29db337b53f284cca5aba86e9a8ae5cef7d31842c2
Our deployed old_contract address is CAS6FKBXGVXFGU2SPPPJJOIULJNPMPR6NVKWLOQP24SZJPMB76TGH7Y3. You may need to replace this value with your own. Invoke the version function of the contract, to see the current deployed version.
stellar contract invoke \
--id CAS6FKBXGVXFGU2SPPPJJOIULJNPMPR6NVKWLOQP24SZJPMB76TGH7Y3 \
--source-account alice \
--network testnet \
-- version
# 1
Now upgrade the contract. Notice the --source-account should be the identity name matching the address passed to the __constructor function, when the contract was deployed.
stellar contract invoke \
--id CAS6FKBXGVXFGU2SPPPJJOIULJNPMPR6NVKWLOQP24SZJPMB76TGH7Y3 \
--source-account alice \
--network testnet \
-- \
upgrade \
--new_wasm_hash aa24c81289997ad815489b29db337b53f284cca5aba86e9a8ae5cef7d31842c2
Invoke the version function again. Now that the contract was upgraded, you'll see a new version.
stellar contract invoke \
--id CAS6FKBXGVXFGU2SPPPJJOIULJNPMPR6NVKWLOQP24SZJPMB76TGH7Y3 \
--source-account alice \
--network testnet \
-- version
# 2
Hooray, our contract has been upgraded!
Guides in this category:
📄️ 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
📄️ 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
📄️ Workspaces
Organize contracts using Cargo workspaces