← All standards
SEP-0049Source ↗

Upgradeable Contracts

5 min · 1,109 words

#Summary

Community guidelines and recommendations for safely upgrading the WASM bytecode of Soroban smart contracts.

#Motivation

Soroban contracts are mutable by default unlike many other smart contract platforms such as Ethereum. Mutability in the context of Soroban refers to the ability of a smart contract to modify its WASM bytecode, thereby altering its function interface, execution logic, or metadata.

However, there are cases where immutable contracts are preferable, such as when immutability guarantees are essential for trustless applications or when a contract’s logic must remain fixed to comply with regulatory requirements. In such scenarios, non-upgradability ensures that the contract's behavior remains predictable and secure.

Soroban provides a built-in, protocol-level defined mechanism for contract upgrades, allowing contracts to upgrade themselves if they are explicitly designed to do so, thus delegating the decision for it to the application layer.

This native upgrade mechanism offers significant advantages. One of them is the flexibility it offers to contract developers who can choose to make the contract immutable by simply not provisioning upgradability mechanics. On the other hand, providing upgradability on a protocol level significantly reduces the risk surface. This becomes particularly evident when compared to Ethereum, which lacks native support for upgradability, necessitating the development of various upgradability patterns, each with its own trade-offs. Despite the availability of tooling, handling upgrades in Ethereum remains a complex and error-prone process.

While Soroban’s built-in upgradability eliminates many of these challenges, certain caveats must still be considered.

#Abstract

This SEP defines a set of recommendations for Soroban smart contracts that includes:

  • Versioning through contract metadata
  • Handling of migrations during upgrades
  • Strategies for safe rollbacks
  • General guidance for ensuring a trustworthy and transparent upgrade process

#Recommendations

#Versioning

Each contract can include a version identifier in its metadata to facilitate tracking and validation. The version of the WASM bytecode can be stored using soroban_sdk::contractmeta! where:

  • "key": binver,
  • "value": a string that follows Semantic Versioning (SemVer) to differentiate between breaking and non-breaking changes.

The key binver is chosen for consistency with the already existing rsver and rssdkver in the metadata. Furthermore, the Soroban SDK may automatically embed this metadata by reading from the crate version to streamline adoption.

Versioning in metadata is preferred over defining a dedicated version() function for the following reasons:

  • the WASM binary format specifies a dedicated section for metadata as contractmetav0 that’s already well leveraged by the Soroban SDK and the executable version seems a logical candidate to get stored there
  • the version is static for a binary, and won’t change between different deployments
  • the slight overhead on the binary size.

#Upgrading

This SEP does not propose a standard upgrade() function signature, as its parameters may vary depending on the specific application or contract version. However, the following practices are recommended:

  1. Name the function explicitly upgrade() to make its purpose clear and recognizable.
  2. Ensure proper access control to restrict who can trigger an upgrade.
  3. It is NOT necessary to emit an application-specific event during an upgrade, since a system event is already emitted automatically when the contract's executable code is updated.

Example (pseudo-code):

#[contractimpl]
impl MyContract {
    pub fn upgrade(e: &Env, new_wasm_hash: BytesN<32>, operator: Address) {
        operator.require_auth();
        let owner: Address = e.storage().instance().get(&OWNER).unwrap();
        if (owner != operator) {
            panic_with_error!(e, ContractError::Unauthorized)
        }

        e.deployer().update_current_contract_wasm(new_wasm_hash);
    }
}

#Migrations

#What is a migration?

A migration refers to any change made to a contract’s storage layout. This is typically required after a contract upgrade that modifies how data is structured or interpreted.

#Handling migrations

Soroban’s architecture prevents calling functions in the newly deployed WASM within the same invocation. Therefore, pre- or post-upgrade housekeeping (such as data migrations) must be handled in a separate step or context.

Recommended best practices include:

  1. Define a dedicated migrate() function to handle data transformations after the upgrade. Ensure one-time execution of this function (if applicable) and proper access control.
  2. To ensure atomicity and avoid transitional inconsistencies, consider using a dedicated upgrader contract for orchestration that performs both the upgrade and the migration in the same invocation.

Example (pseudo-code):

pub const MIGRATE: Symbol = symbol_short!("migrate");

#[contractimpl]
impl Upgrader {
    pub fn upgrade_and_migrate(
        e: Env,
        contract_address: Address,
        operator: Address,
        wasm_hash: BytesN<32>,
        migration_data: soroban_sdk::Vec<Val>,
    ) {
        let contract_client = UpgradeableClient::new(&e, &contract_address);

        // the parameters of `upgrade()` can vary;
        // it is recommmended to explicitly name it `upgrade()`
        contract_client.upgrade(&wasm_hash, &operator);

        // If this contract is meant to be used for upgrading contracts with a defined `migrate()` funciton,
        // it can be invoked with a regular call:
        // `contract_client.migrate(param1, param2, param3)`
        e.invoke_contract::<()>(&contract_address, &MIGRATE, migration_data);
    }
}

  1. When atomic upgrade + migrate is not possible, pause external access to entry-point functions that modify storage between the upgrade and the migration. This avoids potential state corruption caused by interaction with inconsistent logic.

#Rollbacks

Once a smart contract is deployed, it cannot be rolled back like in traditional systems. Any incorrect logic must be corrected with a new contract version.

  1. It is of the utmost importance to define a rollback strategy, especially when storage layout mutations are involved. Ensure that future upgrades can restore the contract to a safe, consistent state.
  2. In any such situation, it is recommended to suspend the contract’s functionality temporarily (pausing) before deploying a fix.

#Transparency and Trustworthiness

To enhance safety and predictability when upgrading contracts, the following practices are recommended:

  1. Use a timelock mechanism which delays the effect of an upgrade, giving users time to review the change and exit safely if needed.
  2. Implement a governance model in which stakeholders vote on major upgrades or migrations, ensuring community oversight.
  3. Trigger upgrades or migrations through a multi-signature scheme, reducing the risk of a single point of failure if an admin account is compromised.
  4. Provide clear, timely communication to users when performing upgrades or migrations. This includes outlining the nature of the changes, potential risks and any required user actions.

#Further Considerations

To mitigate risks associated with contract upgrades, the following points shoud be also taken into account:

  1. Ensure that the new contract version does not rely on initializing new data from the constructor, as it will not be invoked after the upgrade.
  2. Verify that the new contract includes an upgradability mechanism, preventing an unintended loss of upgradability.
  3. Check for storage consistency, ensuring that the new contract does not inadvertently introduce storage mismatches.
  4. Compare version metadata between versions.

These checks can be implemented in upgrade tooling and libraries, providing warnings or requiring explicit confirmations for potential breaking changes.

#Changelog

  • 0.1.0: Initial draft
  • 0.2.0: Turn standardizations into guidelines