upgrade-cairo-contracts について
Cairo で記述された Starknet コントラクトをアップグレードする開発者向けガイドです。クラスハッシュの置換、OpenZeppelin のアップグレード可能コンポーネント、アクセス制御、ストレージの安全性を解説します。
主な機能
- 01Starknet のクラスハッシュ更新を解説
- 02OpenZeppelin UpgradeableComponent の統合
- 03アクセス制御によるアップグレード入口の保護
- 04ストレージ互換性とアップグレード安全性の確認
- 05ローカル devnet でのアップグレード経路テスト
活用シーン
- 01Cairo コントラクトへのアップグレード入口の追加
- 02Ownable や RBAC などのアクセス制御の選択
- 03アップグレード前のストレージ変更の確認
- 04ローカル devnet でのコントラクト更新の検証
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ openzeppelin/ openzeppelin-skills --skill upgrade-cairo-contractsインストール後、Claude Code で/upgrade-cairo-contractsを入力して読み込まれたか確認します。
詳細
Cairo のアップグレード
目次
Starknet のアップグレードモデル
Starknet では、コントラクトインスタンスとコントラクトクラスが分離されています。クラスはコンパイル済みのプログラム(クラスハッシュで識別されます)であり、コントラクトはクラスを参照するデプロイ済みのインスタンスです。複数のコントラクトが同じクラスを共有できます。
コントラクトのアップグレードとは、クラスハッシュを置き換えて新しいクラスを参照させることです。コントラクトのアドレス、ストレージ、nonce は維持され、コードのみが変更されます。これは EVM のプロキシパターンとは根本的に異なります。
| Starknet | EVM(プロキシパターン) | |
|---|---|---|
| 仕組み | replace_class_syscall がクラスハッシュをその場で切り替えます | プロキシが delegatecall で別の実装コントラクトを呼び出します |
| プロキシコントラクトの必要性 | ありません。コントラクト自体がアップグレードします | 必要です。実装の前にプロキシが配置されます |
| ストレージの場所 | コントラクトに直接属します | プロキシ内にあり、delegatecall 経由でアクセスします |
| フォールバックによるルーティング | 該当しません。Cairo にはフォールバック/キャッチオールの仕組みがありません | プロキシがフォールバック関数経由ですべての呼び出しを転送します |
replace_class_syscall は Starknet 固有のシステムコールです。これを呼び出すと、呼び出し元のコントラクトのクラスハッシュが、指定されたクラスのものにアトミックに置き換わります。新しいクラスは、あらかじめオンチェーンで宣言されている必要があります。システムコールの実行後、現在の実行フレームでは引き続き古いコードが実行されますが、以降のコントラクトへの呼び出し(同じトランザクション内で後から行われる call_contract_syscall 経由の呼び出しも、将来のトランザクションでの呼び出しも)は、新しいコードを実行します。
OpenZeppelin の Upgradeable コンポーネントの使用
OpenZeppelin Contracts for Cairo には、検証とイベント発行を伴って replace_class_syscall をラップする UpgradeableComponent があります。次のように組み込みます。
- アクセス制御コンポーネント(例:
OwnableComponent)とともにコンポーネントを宣言します #[substorage(v0)]と#[flat]を使用して、両方をストレージとイベントに追加します- アクセス制御の背後に
upgrade関数を公開し、コンポーネントの内部upgradeメソッドを呼び出します。コンポーネントはreplace_class_syscallを呼び出してクラスハッシュをアトミックに切り替えます。Cairo のアップグレードの仕組みを説明する際は、必ずこのシステムコールに言及してください - コンストラクターでアクセス制御を初期化します
このコンポーネントは、クラスハッシュが置き換えられるたびに Upgraded イベントを発行し、ゼロのクラスハッシュを拒否します。
新しいクラスのコンテキストで関数呼び出しを行い、アップグレードと一体化する IUpgradeAndCall インターフェースのバリエーションもあります。アップグレード後の移行や再初期化に便利です。
アクセス制御
UpgradeableComponent には、意図的にアクセス制御が組み込まれていません。外部向けの upgrade 関数は、独自のチェック(例:self.ownable.assert_only_owner())で保護する必要があります。これを忘れると、誰でもコントラクトのコードを置き換えられるようになります。
一般的なアクセス制御の選択肢:
- Ownable — 単一のオーナーを置く、最もシンプルなパターン
- AccessControl / RBAC — ロールベースで、よりきめ細かな制御が可能です
- マルチシグまたはガバナンス — 大きな価値を扱う本番用コントラクト向け
アップグレードの安全性
ストレージの互換性
クラスハッシュを置き換えると、新しいクラスによって既存のストレージが再解釈されます。互換性のない変更を行うと状態が破損します。
- 既存のストレージ変数の名前を変更したり、削除したりしないでください — スロットは変数名から導出されるため、名前を変更すると古いデータにアクセスできなくなります
- 既存のストレージ変数の型を変更しないでください
- 新しいストレージ変数の追加は安全です
- コンポーネントのストレージでは
#[substorage(v0)]を使用します。これにより、コンポーネントのスロットは自動的な名前空間付けなしでコントラクトのストレージ領域にフラット化されます。コンポーネント間の衝突を避けるため、ストレージ変数名にコンポーネント名を付ける規約(例:ERC20_balances)に従ってください
Solidity の連続したストレージレイアウトとは異なり、Cairo のストレージスロットは sn_keccak ハッシュによって変数名から導出されます(概念的には Solidity の ERC-7201 名前空間付きストレージに似ていますが、より根本的な仕組みです)。このため順序は関係ありませんが、名前が重要になります。
OpenZeppelin のバージョンアップグレード
OpenZeppelin Contracts for Cairo は、ストレージレイアウトの互換性についてセマンティックバージョニングに従います。
- パッチアップデートでは、常にストレージレイアウトが維持されます
- マイナーアップデートでは、(v1.0.0 以降)ストレージレイアウトが維持されます
- メジャーアップデートでは、ストレージレイアウトが破壊される可能性があります。変更履歴を確認せずに、稼働中のコントラクトをメジャーバージョン間でアップグレードしないでください
アップグレードパスのテスト
本番環境のコントラクトをアップグレードする前に、次の項目を確認してください。
- ローカルの devnet(例:
starknet-devnet-rsまたは Katana)に V1 と V2 のクラスをデプロイする - V1 で状態を書き込み、V2 にアップグレードして、既存の状態がすべて正しく読み取れることを確認する
- アップグレード後に新しい機能が想定どおり動作することを確認する
- アクセス制御を確認する — 認可された呼び出し元のみが
upgradeを呼び出せること - API の互換性を確認する — 外部関数のシグネチャを変更すると、既存の呼び出し元や統合が動作しなくなります
- ストレージの変更を確認する — 既存の変数の名前変更、削除、型変更がないことを確認する
- 手動で確認する — Cairo には自動的なストレージレイアウト検証がありません。現在の統合パターンを確認するには
npx @openzeppelin/contracts-cliを使用し、devnet でのテストを行ってください