Skip to content
Document

Upgradable Smart Contracts

Partisia Blockchain allows public-only contract upgrades. This article outlines the process for upgrading smart contracts and provides crucial considerations for ensuring safe and efficient upgrades.

A public-only contract can be upgraded if it meets the following conditions:

  • Contract must be a normal public-only contract, without REAL functionality.
  • Contract must be compiled with SDK version 16.74.0 or newer.
  • Contract must implement specific invocations for permission checking and state upgrading.

Examples

The example contracts repository contains several example contracts for the upgrade flow:

  • upgradable-v1: Can be upgraded to a different contract, using a simple permission system. This contract is the first version of a smart contract.
  • upgradable-v2: Can be upgraded to, and can be upgraded from, using a hash verification system. This contract is an intermediate version of a smart contract.
  • upgradable-v3: Can be upgraded to, but does not define any way to be upgraded from. This contract implements a way to be the final version of the smart contract.

Feel free to experiment using the contracts. One exercise could be:

  1. Deploy upgradable-v1. Ensure that you set upgrader to your own address.
  2. Use the "increment counter by one" invocation, to see the behavior of the version 1.
  3. Upgrade to upgradable-v2.
    • This is possible either through the upgrade button on the contract's page in the Browser.
    • Through the CLI, with the command: cargo pbc transaction upgrade <contract-address> <bytecode-file>
    • Or by using the "upgrade contract" invocation on the WASM Deploy contract.
  4. The action "increment counter by one" should now be replaced with an "increment counter by two". Use it to see the new behavior.
  5. Upgrade to upgradable-v3.
  6. The action "increment counter by two" should now be replaced with an "increment counter by four". Use it to see the new behavior.
  7. You should now be unabled to upgrade the contract further, as the upgrade button has disappeared from the contract's page, and WASM Deploy refuses the upgrade

Writing Upgradeable Smart Contracts

Upgradable contracts offer flexibility but require strict permission checks to prevent unauthorized changes. They must handle data structure updates and ensure state compatibility to maintain functionality across versions.

Danger

Contract upgrading replaces your contract, and there is no undo. It can be used as a powerful attack vector, and will result in stolen assets or worse, if hacked.

Permission Checks

Upgradable contracts are fully responsible for their own upgrade flow, which means that it must itself perform the needed permission checks. This allows for arbitrarily complex permission flows, but makes the upgrade process very sensitive. Care must be used when constructing the permission flow.

Below example is based on upgradable-v1:

/// Example: Trust a single user with full upgrade permissions.
#[upgrade_is_allowed]
pub fn is_upgrade_allowed(
    // Standard context: When and who attempted the contract upgraded?
    context: ContractContext,
    // State of the version 1 contract.
    state: ContractState,
    // Hashes of the contract currently running this code.
    current_contract_hashes: ContractHashes,
    // Hashes of the contract being upgraded to.
    new_contract_hashes: ContractHashes,
    // RPC for the contract upgrade, if needed.
    new_contract_rpc: Vec<u8>,
) -> bool {
    // Whomever is attempting to upgrade must be the specified contract upgrader.
    context.sender == state.upgrader
}

Updating Data Structures

Contract upgrades will often require making changes to the state data structures. This is useful for cases where a contract requires additional information to be maintained or where certain information has become unnecessary. This means that the contract being upgraded must be capable of performing the required updates to the contract state itself.

Below example is based on upgradable-v2:

pub struct UpgradableV1State {
    upgrader: Address,
    counter: u32,
}

#[upgrade]
pub fn upgrade_from_v1(
    // Standard context: When and who attempted the contract upgraded?
    context: ContractContext,
    // State of the original version 1 contract.
    state: UpgradableV1State,
) -> MyUpgradedContractState {
    // Here MyUpgradedContractState is a different from ContractState.
    MyUpgradedContractState  {
        upgrader: state.upgrader,
        counter: state.counter,
        new_map: AvlTreeMap::new(),
    }
}

Version-to-version State Compatibility

Upgraded contracts must be able to transform the previous version of the state to it's own new state. Therefore, we must ensure that the upgraded version of the contract parses the initial contract's state in precisely the same manner the initial contract does. This is accomplished with a special check on the contract types, which ensures that you don't accidentally create a malformed contract state. This is automatically handled for you.

Below example is based on both upgradable-v1 and upgradable-v2:

// contract_v1.rs

/// State In original contract
pub struct UpgradableV1State {
    upgrader: Address,
    counter: u32,
}

//---------------------------------------------------//
// contract_v2.rs

/// What version 2 thinks the version 1 state looked like

/// Notice the incorrect order of fields.
pub struct UpgradableV1State {
    counter: u32,
    upgrader: Address,
}

#[upgrade]
pub fn upgrade_from_v1(
    context: ContractContext,
    state: UpgradableV1State,
) -> MyUpgradedContractState {
    // This code will never be run. The contract fails beforehand,
    // because of the state mismatch.
    MyUpgradedContractState { ... }
}

Upgrade flow: Detailed overview

The following details how the contract upgrade flows through the blockchain:

Upgrade flow

  1. Any user can initiate upgrade of a contract, through the public deploy contract.
  2. The pub deploy contract communicates with the blockchain to ensure that the contract upgrade is performed in a single atomic step. Any failure will result in the complete rollback of the contract; as if no upgrade had taken place.
  3. The version 1 (currently deployed version) of the contract is informed of the upgrade attempt through the #[upgrade_is_allowed] macro, and is asked whether it should allow the upgrade. It is given the hashes of the currently running code, and the hashes of the code that is being upgraded to, along with the initialization RPC of the upgrade function.
  4. The upgrade is aborted if the #[upgrade_is_allowed] macro panics, or returns false. A state type witness is returned from the contract, which is a succinct description of the state type, which will be used later on.
  5. The version 2 (upgraded version) of the contract is instructed to upgrade version 1 state to version 2 state, through the #[upgrade] macro. The type witness from step 4 is compared with the type being upgraded from, to ensure version 2 can correctly deserialize the version 1 state; the upgrade is aborted if they do not match.
  6. The upgrade is aborted if the #[upgrade] macro panics. The contract is upgraded if the macro returns successfully.

About Upgrade Governance

Issues to consider when implementing upgrade permission checking and governance:

  • The #[upgrade_is_allowed] invocation should not be trivially true. Otherwise anybody can overwrite the existing contract.
  • The #[upgrade_is_allowed] invocation should be trivially false, if upgrading is not needed for this contract. This prevents upgrading by anybody, including the contract deployer.
  • When the #[upgrade_is_allowed] is not trivially false, it should be backed by a suitable governance structure, such as a DAO or other consensus mechanism. This backing mechanism should not directly upgrade the contract, but should rather supply the required hash, to minimize complexity in the governance layer.
  • It is not recommended that a smart contract (such as a DAO) invokes the upgrade, as it would require the invoking contract to store the upgraded contract code, which is likely to invoke significant storage costs. A hash-based solution is recommended instead.
  • While it is possible to trust a single user to perform upgrades it might represent a security issue, as they have full control of the contract.