本文へ移動

プロキシのストレージ衝突:アップグレードがコントラクト状態を破壊する仕組み

プロキシは状態を保持したまま実装コードを変更します。互換性のないストレージレイアウトが残高、所有者、アップグレード制御を上書きする仕組みと、安全な検証方法を解説します。

更新日

教育目的の参考情報であり、投資助言ではありません。投資により損失が生じる可能性があります。

直接の回答

プロキシのストレージ衝突は、既存状態を作成したレイアウトとは異なる意味で、実装コードがプロキシのストレージスロットを読み書きすると発生します。その結果、アップグレードによって残高がアドレスとして扱われたり、所有者が消去されたり、マッピングの基点スロットやアップグレード制御データが破損したりします。

delegatecall では、実装のバイトコードがプロキシのコンテキストで実行されます。ストレージ、残高、address(this) はプロキシに属します。変数名はオンチェーンに保存されないため、EVM は新しいコードが計算したスロットとバイトオフセットだけに従います。

したがって、アップグレードでは、単にコンパイルに成功し同じ関数を公開するだけでなく、デプロイ済みのレイアウトを維持する必要があります。承認前に、コンパイラが生成したレイアウトを実際にデプロイされた正確なバージョンと比較してください。

仕組み

Solidity は通常、継承関係を C3 線形化した後、宣言順にスロット 0 から状態変数を配置します。32 バイト未満の値は同じスロットにパックされることがあります。構造体と配列には追加の配置規則があり、マッピングと動的配列は基点スロットからデータ位置を導出します。基点スロットがずれると、その派生データの位置も変わります。

監査すべき衝突境界は次の四種類です。

  • プロキシと実装: 実装アドレスや管理者など、プロキシ固有のフィールドがアプリケーション状態のスロットを占有してはいけません。ERC-1967 は、実装、ビーコン、管理者のデータに、通常のコンパイラ割り当てを回避する標準スロットを定めています。
  • 旧実装と新実装: 既存のアプリケーション変数は、互換性のあるスロット、オフセット、型を維持する必要があります。末尾への追加は安全な場合がありますが、挿入、並べ替え、削除、型変更は既存のワードを別の意味で解釈させます。
  • 継承: 基底コントラクトへの状態追加や継承順序の変更は、派生側のソースが変わらなくても、そのストレージを移動させることがあります。
  • 予約領域または名前空間ストレージ: 正しく消費されるストレージギャップは基底コントラクト用の領域を予約でき、ERC-7201 形式の名前空間はレイアウトを分離できます。ただし、どちらも既存レイアウト内部の任意変更を許可するものではありません。

バージョン 1 が次のレイアウトだとします。

uint256 totalAssets; // slot 0
address owner;       // slot 1

バージョン 2 は誤って先頭に変数を挿入します。

bool paused;         // slot 0, offset 0
uint256 totalAssets; // slot 1
address owner;       // slot 2

アップグレード後、paused は旧 totalAssets の下位バイトを読み、新しい totalAssets は旧 owner のワードを整数として読み、owner はスロット 2 に元からあった値(多くの場合はゼロ)を読みます。元のワードはストレージに残っていますが、新しいコードが異なる意味を与えます。トランザクションが成功しても、誤って解釈された状態で認可や会計処理が実行される可能性があります。

リスクとアップグレード確認

  • 両方の実装についてストレージレイアウト出力を生成し、実際にデプロイされた参照コントラクトを基準に、スロット、オフセット、型、継承情報を比較します。
  • 従来型の線形レイアウトでは、新しい変数は末尾にだけ追加します。互換性を証明できない限り、既存フィールドの並べ替え、型変更、削除と再利用、基底コントラクトの変更を行わないでください。
  • ストレージギャップを使う場合は、使用した予約スロット数だけ正確に縮小します。名前空間ストレージでは識別子を一意に保ち、既存の各名前空間内の変更を検証します。
  • ERC-1967 を完全な保護と考えないでください。これはプロキシのメタデータをコンパイラ割り当てのアプリケーションスロットから分離しますが、二つの実装レイアウトを互換にするものではありません。
  • フォークまたは状態スナップショット上で、アップグレードと再初期化処理をテストします。実行前後の所有者、ロール、残高、アローワンス、マッピング項目、一時停止状態、実装スロット、ロールバックまたは緊急制御を確認します。

アップグレードがすでに衝突を起こした疑いがある場合、ガバナンス上可能なら追加のアップグレードと状態変更呼び出しを停止します。アップグレード前のブロック番号と実装バイトコードを保存し、影響するスロットの生のストレージを比較して、適格なコントラクト技術者による移行設計と独立レビューを行ってください。検証済みのストレージマップなしにアップグレードを繰り返すと、回復可能な状態をさらに破壊しかねません。

よくある誤解

  • 「変数名が同じならレイアウトは安全だ。」 名前は保存位置を決めません。型、順序、パッキング、継承、名前空間の規則が決めます。
  • 「変数を削除すればスロットを再利用できる。」 プロキシのストレージは残り続けます。十分にレビューされた移行で旧値を消去または変換しない限り、再利用は古いワードに新しい意味を与えるだけです。
  • 「テストトランザクションが成功すれば互換性は証明された。」 テストが触れるスロットは一部だけかもしれません。レイアウト検証と状態差分テストでは、特権フィールド、パック値、マッピング、配列、継承ストレージを網羅する必要があります。

関連トピック

出典

ナビゲーション

Wiki を検索...