Upgradeable Contracts

Starknet has native upgradeability through a syscall that updates the contract source code, removing the need for proxies.

⚠️ WARNING Make sure you follow the security recommendations before upgrading.

How Upgradeability Works in Starknet

To better comprehend how upgradeability works in Starknet, it's important to understand the difference between a contract and its contract class.

Contract Classes represent the source code of a program. All contracts are associated to a class, and many contracts can be instances of the same one. Classes are usually represented by a class hash, and before a contract of a class can be deployed, the class hash needs to be declared.

A contract instance is a deployed contract corresponding to a class, with its own storage.

Replacing Contract Classes

The replace_class_syscall

The replace_class syscall allows a contract to update its source code by replacing its class hash once deployed.

To upgrade a contract, expose an entry point that executes replace_class_syscall with the new class hash as an argument:

use core::num::traits::Zero;
use starknet::{ClassHash, syscalls};

fn upgrade(new_class_hash: ClassHash) {
    assert(!new_class_hash.is_zero(), 'Class hash cannot be zero');
    syscalls::replace_class_syscall(new_class_hash).unwrap();
}

Listing 17-3: Exposing replace_class_syscall to update the contract's class

📌 Note: If a contract is deployed without this mechanism, its class hash can still be replaced through library calls.

⚠️ WARNING: Thoroughly review changes and potential impacts before upgrading, as it's a delicate procedure with security implications. Don't allow arbitrary addresses to upgrade your contract.

OpenZeppelin's Upgradeable Component

OpenZeppelin Contracts for Cairo provides the Upgradeable component that can be embedded into your contract to make it upgradeable. This component is a simple way to add upgradeability to your contract while relying on an audited library.

Usage

Upgrades are often very sensitive operations, and some form of access control is usually required to avoid unauthorized upgrades. The Ownable component is used in this example to restrict the upgradeability to a single address, so that the contract owner has the exclusive right to upgrade the contract.

#[starknet::contract]
mod UpgradeableContract {
    use openzeppelin_access::ownable::OwnableComponent;
    use openzeppelin_upgrades::UpgradeableComponent;
    use openzeppelin_upgrades::interface::IUpgradeable;
    use starknet::{ClassHash, ContractAddress};

    component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);
    component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent);

    // Ownable Mixin
    #[abi(embed_v0)]
    impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl<ContractState>;
    impl OwnableInternalImpl = OwnableComponent::InternalImpl<ContractState>;

    // Upgradeable
    impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl<ContractState>;

    #[storage]
    struct Storage {
        #[substorage(v0)]
        ownable: OwnableComponent::Storage,
        #[substorage(v0)]
        upgradeable: UpgradeableComponent::Storage,
    }

    #[event]
    #[derive(Drop, starknet::Event)]
    enum Event {
        #[flat]
        OwnableEvent: OwnableComponent::Event,
        #[flat]
        UpgradeableEvent: UpgradeableComponent::Event,
    }

    #[constructor]
    fn constructor(ref self: ContractState, owner: ContractAddress) {
        self.ownable.initializer(owner);
    }

    #[abi(embed_v0)]
    impl UpgradeableImpl of IUpgradeable<ContractState> {
        fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {
            // This function can only be called by the owner
            self.ownable.assert_only_owner();

            // Replace the class hash upgrading the contract
            self.upgradeable.upgrade(new_class_hash);
        }
    }
}

Listing 17-4 Integrating OpenZeppelin's Upgradeable component in a contract

The UpgradeableComponent provides:

  • An internal upgrade function that safely performs the class replacement
  • An Upgraded event emitted when the upgrade is successful
  • Protection against upgrading to a zero class hash

For more information, please refer to the OpenZeppelin docs API reference.

Security Considerations

Upgrades can be very sensitive operations, and security should always be top of mind while performing one. Please make sure you thoroughly review the changes and their consequences before upgrading. Some aspects to consider are:

  • API changes that might affect integration. For example, changing an external function's arguments might break existing contracts or offchain systems calling your contract.
  • Storage changes that might result in lost data (e.g. changing a storage slot name, making existing storage inaccessible), or data corruption (e.g. changing a storage slot type, or the organization of a struct stored in storage).
  • Storage collisions (e.g. mistakenly reusing the same storage slot from another component) are also possible, although less likely if best practices are followed, for example prepending storage variables with the component's name.
  • Always check for backwards compatibility before upgrading between versions of OpenZeppelin Contracts.