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.