Skip to main content

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:

Download the upgradeable contract example

The upgradeable contract example demonstrates how to upgrade a Wasm contract.

Open in Codespaces

Open in Codeanywhere

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.

upgradeable_contract/old_contract/src/lib.rs
#![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;

Source: https://github.com/stellar/soroban-examples/blob/v23.0.0/upgradeable_contract/old_contract/src/lib.rs

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:

  1. 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.

  2. 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's upgrade(...) function.

  3. Admin authorization: Before upgrading, the contract checks if the action is authorized by the Admin address. This is crucial to prevent unauthorized upgrades. Only someone with admin rights can perform the upgrade.

  4. 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)).
  5. The update_current_contract_wasm host function will also emit a SYSTEM 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 have topics = ["executable_update", old_executable: ContractExecutable, old_executable: ContractExecutable] and data = [].

Tests

Open the upgradeable_contract/old_contract/src/test.rs file to follow along.

upgradeable_contract/old_contract/src/test.rs
#![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![]
}
)]
)
}

Source: https://github.com/stellar/soroban-examples/blob/v23.0.0/upgradeable_contract/old_contract/src/test.rs

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!