upgrade-solidity-contracts について
OpenZeppelin コントラクトをアップグレードする Solidity 開発者向けのガイドです。プロキシの種類、アップグレード可能なコントラクトの設計、Hardhat と Foundry の手順に加え、ストレージ互換性や検証警告への対処も説明します。
主な機能
- 01UUPS、Transparent、Beacon プロキシの解説
- 02初期化関数と実装コントラクトのロック規則
- 03ストレージレイアウトと ERC-7201 の説明
- 04Hardhat と Foundry のアップグレード手順
- 05検証問題への対処方針
活用シーン
- 01新規コントラクトのプロキシ方式を選定
- 02アップグレード前にストレージ互換性を確認
- 03Hardhat または Foundry でプロキシをデプロイ・更新
- 04検証警告や危険な操作を評価
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ openzeppelin/ openzeppelin-skills --skill upgrade-solidity-contractsインストール後、Claude Code で/upgrade-solidity-contractsを入力して読み込まれたか確認します。
詳細
Solidity のアップグレード
目次
- プロキシパターンの概要
- メジャーバージョン間のアップグレード制限(v4 → v5)
- アップグレード可能なコントラクトの作成
- Hardhat のアップグレードワークフロー
- Foundry のアップグレードワークフロー
- アップグレード検証の問題への対処
- アップグレードの安全性チェックリスト
プロキシパターンの概要
| パターン | アップグレードロジックの配置場所 | 最適な用途 |
|---|---|---|
UUPS(UUPSUpgradeable) | 実装コントラクト(_authorizeUpgrade をオーバーライド) | ほとんどのプロジェクト — 軽量なプロキシ、デプロイ時のガス代を削減 |
| Transparent | 独立した ProxyAdmin コントラクト | 管理者とユーザーの呼び出しの分離が重要な場合 — 管理者が誤って実装関数を呼び出すことはありません |
| Beacon | 共有ビーコンコントラクト | 複数のプロキシで1つの実装を共有する場合 — ビーコンをアップグレードすると、すべてのプロキシがアトミックにアップグレードされます |
3つのパターンはいずれも、実装アドレス、管理者、ビーコンに EIP-1967 ストレージスロットを使用します。
Transparent プロキシ — v5 でのコンストラクター変更: v5 では、
TransparentUpgradeableProxyが独自のProxyAdminコントラクトを自動的にデプロイし、その管理者アドレスをイミュータブル変数に格納します(コンストラクション時に設定され、以後変更できません)。2番目のコンストラクターパラメーターは、自動デプロイされたProxyAdminのオーナーアドレスです。既存のProxyAdminコントラクトアドレスをここに渡してはいけません。アップグレード権限の移転は、ProxyAdminの所有権を通じてのみ行います。これは、ProxyAdminを別途デプロイして、そのアドレスをプロキシのコンストラクターに渡す v4 の方式とは異なります。
メジャーバージョン間のアップグレード制限(v4 → v5)
OpenZeppelin Contracts v4 を使用する実装から v5 を使用する実装への、プロキシの実装のアップグレードはサポートされていません。
v4 は順次ストレージ(宣言順のスロット)を使用し、v5 は名前空間付きストレージ(ERC-7201、決定論的なスロットに配置された構造体)を使用します。v5 の実装では、v4 の実装が書き込んだ状態を安全に読み取ることができません。手動でのデータ移行は理論上可能ですが、多くの場合、実行は困難です。mapping のエントリーは列挙できないため、任意のキーで書き込まれた値を移動できません。
推奨される方法: v5 の実装を使った新しいプロキシをデプロイし、ユーザーを新しいアドレスへ移行してください。現在 v4 の実装を参照しているプロキシをアップグレードしてはいけません。
コードベースを v5 に更新することを推奨します。 上記の制限は、すでにデプロイされているプロキシにのみ適用されます。v5 をベースにした新しいデプロイと、同じメジャーバージョン内でのアップグレードは、完全にサポートされています。
アップグレード可能なコントラクトの作成
コンストラクターの代わりにイニシャライザーを使用する
プロキシコントラクトは、実装に対して delegatecall を行います。コンストラクターが実行されるのは実装自体のデプロイ時のみであり、プロキシの作成時ではありません。コンストラクターをイニシャライザー関数に置き換えてください。
import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
contract MyToken is Initializable, ERC20Upgradeable, OwnableUpgradeable {
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers(); // lock the implementation
}
function initialize(address initialOwner) public initializer {
__ERC20_init("MyToken", "MTK");
__Ownable_init(initialOwner);
}
}
重要なルール:
- 最上位の
initializeにはinitializer修飾子を使用します - 親の初期化関数(
__X_init)は内部でonlyInitializingを使用します。これらは明示的に呼び出してください。コンパイラーはコンストラクターのようにイニシャライザーを自動で線形化しません - 攻撃者が実装を直接初期化するのを防ぐため、コンストラクターでは必ず
_disableInitializers()を呼び出します - フィールド宣言で初期値を設定しないでください(例:
uint256 x = 42)。これらはコンストラクターにコンパイルされるため、プロキシでは実行されません。constantはコンパイル時にインライン化されるため安全です。immutableの値はバイトコードに格納され、すべてのプロキシで共有されます。プラグインはデフォルトでこれを安全ではないものとして警告します。共有値として使用する場合は、/// @custom:oz-upgrades-unsafe-allow state-variable-immutableを使って許可してください
アップグレード可能なパッケージを使用する
基底コントラクト(例:ERC20Upgradeable、OwnableUpgradeable)を使う場合は、@openzeppelin/contracts-upgradeable からインポートしてください。インターフェースとライブラリは、@openzeppelin/contracts からインポートしてください。v5.5 以降では、Initializable と UUPSUpgradeable も @openzeppelin/contracts から直接インポートしてください。アップグレード可能パッケージ内のエイリアスは、次のメジャーリリースで削除されます。
ストレージレイアウトのルール
アップグレードする場合、新しい実装は古い実装とのストレージ互換性を維持する必要があります。
- 既存の状態変数の順序を変更、削除、または型を変更することは絶対にしないでください
- 既存の状態変数より前に新しい変数を挿入することは絶対にしないでください
- 新しい変数は末尾にのみ追加してください
- 基底コントラクトの継承順序は絶対に変更しないでください
名前空間付きストレージ(ERC-7201)
最新のアプローチです。すべての @openzeppelin/contracts-upgradeable コントラクト(v5 以降)で使用されています。状態変数は決定論的なストレージスロットにある構造体にまとめられ、各コントラクトのストレージが分離されるため、ストレージギャップが不要になります。基底コントラクトとしてインポートされる可能性があるすべてのコントラクトで推奨されます。
/// @custom:storage-location erc7201:example.main
struct MainStorage {
uint256 value;
mapping(address => uint256) balances;
}
// keccak256(abi.encode(uint256(keccak256("example.main")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant MAIN_STORAGE_LOCATION = 0x...;
function _getMainStorage() private pure returns (MainStorage storage $) {
assembly { $.slot := MAIN_STORAGE_LOCATION }
}
名前空間付きストレージの変数を使用する場合:
function _getBalance(address account) internal view returns (uint256) {
MainStorage storage $ = _getMainStorage();
return $.balances[account];
}
従来のストレージギャップと比べた利点:基底コントラクトへの変数の安全な追加、継承順序を変更してもレイアウトが壊れないこと、各コントラクトのストレージが完全に分離されること。
アップグレード時に、継承チェーンから削除して名前空間を削除することは絶対にしないでください。プラグインは削除された名前空間をエラーとして検出します。その名前空間に保存された状態は孤立します。データはオンチェーンに残りますが、新しい実装から読み書きできなくなります。名前空間を今後使用しない場合も、古いコントラクトを継承チェーンに残してください。未使用の名前空間は実行時コストもストレージ競合も発生させません。このエラーを抑制する個別のフラグはありません。唯一の回避策は unsafeSkipStorageCheck ですが、すべてのストレージレイアウト互換性チェックを無効にするため、危険な最終手段です。
ERC-7201 ストレージ位置の計算
名前空間付きストレージのコードを生成する際は、必ず実際の STORAGE_LOCATION 定数を計算してください。Bash ツールを使って以下のコマンドを実行し、実際の名前空間 ID を指定して、計算結果を生成コードに直接埋め込んでください。0x... のようなプレースホルダー値は絶対に残さないでください。
計算式は keccak256(abi.encode(uint256(keccak256(id)) - 1)) & ~bytes32(uint256(0xff)) です。ここで id は名前空間文字列(例:"example.main")です。
ethers を使う Node.js:
node -e "const{keccak256,toUtf8Bytes,zeroPadValue,toBeHex}=require('ethers');const id=process.argv[1];const h=BigInt(keccak256(toUtf8Bytes(id)))-1n;console.log(toBeHex(BigInt(keccak256(zeroPadValue(toBeHex(h),32)))&~0xffn,32))" "example.main"
"example.main" を実際の名前空間 ID に置き換えてコマンドを実行し、出力を定数値として使用してください。
安全でない操作
selfdestructは使用しないでください — Dencun 以前のチェーンでは実装を破棄し、すべてのプロキシを機能停止させます。Dencun 以降(EIP-6780)では、作成と同じトランザクション内で呼び出された場合のみselfdestructがコードを破棄しますが、プラグインは依然として危険な操作として検出します- 信頼できないコントラクトへの
delegatecallは使用しないでください — 悪意のあるターゲットはselfdestructを実行したり、ストレージを破損させたりする可能性があります
また、アップグレード可能なコントラクト内でコントラクトを作成するために new を使うことは避けてください。作成されたコントラクトはアップグレードできません。代わりに、デプロイ済みのアドレスを渡してください。
Hardhat のアップグレードワークフロー
プラグインをインストールします。
npm install --save-dev @openzeppelin/hardhat-upgrades
npm install --save-dev @nomicfoundation/hardhat-ethers ethers # peer dependencies
hardhat.config に登録します。
require('@openzeppelin/hardhat-upgrades'); // JS
import '@openzeppelin/hardhat-upgrades'; // TS
ワークフローの概要 — プラグインは upgrades オブジェクトに関数(deployProxy、upgradeProxy、deployBeacon、upgradeBeacon、deployBeaconProxy)を提供します。各関数は次の処理を行います。
- 実装が安全にアップグレードできるか検証します(ストレージレイアウト、初期化関数のパターン、安全でないオペコード)。
- 実装をデプロイします(すでにデプロイ済みの場合は再利用します)。
- プロキシまたはビーコンをデプロイまたは更新します。
- 初期化関数を呼び出します(デプロイ時)。
このプラグインは、ネットワークごとのファイルを .openzeppelin/ に保存し、デプロイ済みの実装を追跡します。開発用以外のネットワークのファイルは、バージョン管理にコミットしてください。
prepareUpgrade を使用すると、アップグレードを実行せずに、新しい実装を検証してデプロイできます。マルチシグやガバナンスコントラクトがアップグレード権限を持つ場合に便利です。
正確な API シグネチャとオプションはバージョンによって変わるため、インストールしたプラグインの README またはソースを確認してください。
Foundry のアップグレードワークフロー
依存関係をインストールします。
forge install foundry-rs/forge-std
forge install OpenZeppelin/openzeppelin-foundry-upgrades
forge install OpenZeppelin/openzeppelin-contracts-upgradeable
foundry.toml を設定します。
[profile.default]
ffi = true
ast = true
build_info = true
extra_output = ["storageLayout"]
Node.js が必要です。このライブラリは、検証のために OpenZeppelin Upgrades CLI をシェル経由で呼び出します。
スクリプトやテストにインポートして使用します。
import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol";
// Deploy
address proxy = Upgrades.deployUUPSProxy(
"MyContract.sol",
abi.encodeCall(MyContract.initialize, (args))
);
// IMPORTANT: Before upgrading, annotate MyContractV2 with: /// @custom:oz-upgrades-from MyContract
// Upgrade and call a function
Upgrades.upgradeProxy(proxy, "MyContractV2.sol", abi.encodeCall(MyContractV2.foo, ("arguments for foo")));
// Upgrade without calling a function
Upgrades.upgradeProxy(proxy, "MyContractV2.sol", "");
Hardhat との主な違い:
- コントラクトはファクトリーオブジェクトではなく、名前の文字列で参照します。
- 実装は自動で追跡されません。新しいバージョンには
@custom:oz-upgrades-fromを付けるか、Options構造体にreferenceContractを渡してください。 UnsafeUpgrades版では、すべての検証がスキップされます(名前ではなくアドレスを受け取ります)。本番用スクリプトでは決して使用しないでください。- スクリプトを実行する前に
forge cleanを実行するか、--forceを指定してください。
API と
Options構造体の全容については、インストールしたライブラリのUpgrades.solを確認してください。
アップグレード検証の問題への対処
プラグインが警告やエラーを報告した場合は、次の優先順位に沿って対処してください。
- 根本原因を解消します。 問題のあるパターンを取り除く、またはストレージをリファクタリングするなど、懸念を完全に解消できるようにコードを再構成できるか判断してください。まず最初に行うべきなのは、常にこの対応です。
- 本当に安全な場合は、コード内の注釈を使用します。 再構成が適切ではなく、報告されたパターンが実際には安全だと判断した場合、プラグインが提供する注釈を使って、その判断をソースに直接記録できます。利用できる機能については、インストールしたプラグインのドキュメントを確認してください。これらの注釈により、意図的な例外の明確な監査記録を残せます。安全性を評価したうえで使用し、近道として使わないでください。
- 注釈を使えない場合は、範囲を絞ったフラグを使用します。 変更できないサードパーティの基底コントラクトなど、ソース側で対処できないケースもあります。その場合は、該当する構文要素だけを対象とする、最も限定的なフラグを使用してください。
- 広範な無効化は、リスクを十分に理解したうえでの最後の手段とします。
UnsafeUpgrades(Foundry)や包括的なunsafeAllowエントリーなどのオプションを使うと、対象範囲内のすべての検証がスキップされます。使用する場合は理由をコメントに記載し、手動で検証してください。プラグインによる保護は受けられなくなります。
アップグレードの安全性チェックリスト
- ストレージ互換性:既存の変数の順序変更、削除、型の変更を行っていません。新しい変数は追加のみとし(または名前空間付き構造体にフィールドを追加します)。
- 初期化関数の保護:トップレベルの
initializeにinitializer修飾子を使用しています。実装コントラクトのコンストラクターで_disableInitializers()を呼び出しています。 - 親の初期化関数を呼び出す:継承するアップグレード可能な各コントラクトの
__X_initをinitialize内で必ず1回だけ呼び出しています。 - 安全でないオペコードを使用しない:
selfdestructや、信頼できないターゲットへのdelegatecallを使用していません。 - 関数セレクターの衝突:プロキシの管理関数と実装の関数で、同じセレクターを共有してはいけません。UUPS と Transparent のパターンでは設計上この問題に対処していますが、カスタムプロキシは手動で確認する必要があります。
- UUPS の
_authorizeUpgrade:適切なアクセス制御(例:onlyOwner)でオーバーライドしています。これを忘れると、プロキシがアップグレードできなくなるか、誰でもアップグレードできるようになります。 - アップグレードの経路をテストする:V1 をデプロイして V2 にアップグレードし、状態が保持され、新しいロジックが動作することを確認します。Hardhat と Foundry の両方のプラグインで、テストスイート内のアップグレードを検証できます。
- V2 以降のための再初期化関数:V2 で新しい初期化ロジックが必要な場合は、
reinitializer(2)修飾子を使用します(1回しか実行できないinitializerは使用しません)。 - 一意な ERC-7201 名前空間 ID:継承チェーン内の2つのコントラクトで、同じ名前空間 ID を共有していません。ID が衝突すると同じストレージスロットに対応し、気付かないままストレージが破損します。