$npx -y skills add OpenZeppelin/openzeppelin-skills --skill upgrade-cairo-contractsUpgrade Cairo smart contracts using OpenZeppelin's UpgradeableComponent on Starknet. Use when users need to: (1) make Cairo contracts upgradeable via replace_class_syscall, (2) integrate the OpenZeppelin UpgradeableComponent, (3) understand Starknet's class-based upgrade model vs
| 1 | # Cairo Upgrades |
| 2 | |
| 3 | ## Contents |
| 4 | |
| 5 | - [Starknet Upgrade Model](#starknet-upgrade-model) |
| 6 | - [Using the OpenZeppelin Upgradeable Component](#using-the-openzeppelin-upgradeable-component) |
| 7 | - [Access Control](#access-control) |
| 8 | - [Upgrade Safety](#upgrade-safety) |
| 9 | |
| 10 | ## Starknet Upgrade Model |
| 11 | |
| 12 | Starknet separates **contract instances** from **contract classes**. A class is the compiled program (identified by its class hash); a contract is a deployed instance pointing to a class. Multiple contracts can share the same class. |
| 13 | |
| 14 | Upgrading a contract means **replacing its class hash** so it points to a new class. The contract keeps its address, storage, and nonce — only the code changes. This is fundamentally different from EVM proxy patterns: |
| 15 | |
| 16 | | | Starknet | EVM (proxy pattern) | |
| 17 | |---|---|---| |
| 18 | | **Mechanism** | `replace_class_syscall` swaps the class hash in-place | Proxy `delegatecall`s to a separate implementation contract | |
| 19 | | **Proxy contract needed** | No — the contract upgrades itself | Yes — a proxy sits in front of the implementation | |
| 20 | | **Storage location** | Belongs to the contract directly | Lives in the proxy, accessed via delegatecall | |
| 21 | | **Fallback routing** | Not applicable — no fallback/catch-all mechanism in Cairo | Proxy forwards all calls via fallback function | |
| 22 | |
| 23 | The `replace_class_syscall` is a native Starknet syscall. When called, it atomically replaces the calling contract's class hash with the provided one. The new class must already be declared on-chain. After the syscall, the current execution frame continues with the old code, but subsequent calls to the contract — whether via `call_contract_syscall` later in the same transaction or in future transactions — execute the new code. |
| 24 | |
| 25 | ## Using the OpenZeppelin Upgradeable Component |
| 26 | |
| 27 | OpenZeppelin Contracts for Cairo provides an `UpgradeableComponent` that wraps `replace_class_syscall` with validation and event emission. Integrate it as follows: |
| 28 | |
| 29 | 1. **Declare the component** alongside an access control component (e.g., `OwnableComponent`) |
| 30 | 2. **Add both to storage and events** using `#[substorage(v0)]` and `#[flat]` |
| 31 | 3. **Expose an `upgrade` function** behind access control that calls the component's internal `upgrade` method — the component calls `replace_class_syscall` to atomically swap the class hash; always mention this syscall when explaining how Cairo upgrades work |
| 32 | 4. **Initialize access control** in the constructor |
| 33 | |
| 34 | The component emits an `Upgraded` event on each class hash replacement and rejects zero class hashes. |
| 35 | |
| 36 | There is also an `IUpgradeAndCall` interface variant that couples the upgrade with a function call in the new class context — useful for post-upgrade migrations or re-initialization. |
| 37 | |
| 38 | ### Access control |
| 39 | |
| 40 | The `UpgradeableComponent` deliberately does **not** embed access control itself. You must guard the external `upgrade` function with your own check (e.g., `self.ownable.assert_only_owner()`). Forgetting this allows anyone to replace your contract's code. |
| 41 | |
| 42 | Common access control options: |
| 43 | - **Ownable** — single owner, simplest pattern |
| 44 | - **AccessControl / RBAC** — role-based, finer granularity |
| 45 | - **Multisig or governance** — for production contracts managing significant value |
| 46 | |
| 47 | ## Upgrade Safety |
| 48 | |
| 49 | ### Storage compatibility |
| 50 | |
| 51 | When replacing a class hash, existing storage is reinterpreted by the new class. Incompatible changes corrupt state: |
| 52 | |
| 53 | - **Do not rename or remove** existing storage variables — the slot is derived from the variable name, so renaming makes old data inaccessible |
| 54 | - **Do not change the type** of existing storage variables |
| 55 | - **Adding** new storage variables is safe |
| 56 | - **Component storage** uses `#[substorage(v0)]`, which flattens component slots into the contract's storage space without automatic namespacing — follow the convention of prefixing storage variable names with the component name (e.g., `ERC20_balances`) to avoid collisions across components |
| 57 | |
| 58 | Unlike Solidity's sequential storage layout, Cairo storage slots are derived from variable names via `sn_keccak` hashing (conceptually analogous to, but more fundamental than, ERC-7201 namespaced storage in Solidity). This makes ordering irrelevant but makes naming critical. |
| 59 | |
| 60 | ### OpenZeppelin version upgrades |
| 61 | |
| 62 | OpenZeppelin Contracts for Cairo follows semantic versioning for storage layout compatibility: |
| 63 | - **Patch** updates always preserve storage layout |
| 64 | - **Minor** updates preserve storage layout (from v1.0.0 onward) |
| 65 | - **Major** updates ma |