スマートコントラクトのアップグレード
最終更新: 2025年10月21日
イーサリアムのスマートコントラクトは、イーサリアム仮想マシン(EVM)で実行される自己実行プログラムです。 これらのプログラムは、不変になるように設計されています。そのため、一度デプロイしたコントラクトのビジネスロジックを後から変更することはできません。
この不変性は、トラストレス、分散化、スマートコントラクトのセキュリティを実現するために必要ですが、場合によっては欠点になることもあります。 例えば、このコードの不変性により、デベロッパーは脆弱なコントラクトを修正できないことがあります。
しかし、スマートコントラクトの改善に向けた研究が進み、いくつかのアップグレードパターンが開発されました。 これらのアップグレードでは、デベロッパーが別のコントラクトにビジネスロジックを配置することで、スマートコントラクトを(不変性を維持しつつ)アップグレードすることができます。
前提条件
「スマートコントラクト」、「スマートコントラクトの構造」、「イーサリアム仮想マシン(EVM)」について、十分に理解しておく必要があります。 さらに、このガイドでは、読者がスマートコントラクトのプログラミングを理解していることを前提としています。
スマートコントラクトのアップグレードとは
スマートコントラクトをアップグレードするには、コントラクトの状態を維持したまま、スビジネスロジックを変更する必要があります。 特にアップグレード可能性と不変性は、スマートコントラクトのコンテキストでは、必ずしも一致するものではありません。
イーサリアムネットワーク上のアドレスにデプロイされたプログラムは、変更できません。 しかし、ユーザーがスマートコントラクトとやり取りする際に、実行されるコードは変更することができます。
これについては、次の方法を用いて行います。
-
複数のバージョンのスマートコントラクトを作成し、古いコントラクトから新しいコントラクトのインスタンスに状態(データ)を移行する。
-
ビジネスロジックおよび状態を保存するセパレートコントラクトを作成する。
-
プロキシパターンを使い、不変のプロキシコントラクトから修正可能なロジックコントラクトへの関数の呼び出しを委任する。
-
特定の関数を実行する柔軟なサテライトコントラクトに依存し、インターフェースで接続する不変のメインコントラクトを作成する。
-
ダイヤモンドパターンを使い、プロキシコントラクトからロジックコントラクトへの関数の呼び出しを委任する。
アップグレードメカニズム #1: コントラクトマイグレーション
スマートコントラクトのマイグレーションは、バージョン管理に基づいています。バージョン管理とは、同一のソフトウェアの固有の状態を作成・管理する考え方です。 コントラクトマイグレーションでは、従来のスマートコントラクトを新しいインスタンスにデプロイし、ストレージと残高を新しいコントラクトに転送します。
新しくデプロイされたコントラクトのストレージは空です。そのため、旧コントラクトからデータを復元して、新しい実装に書き込むことができます。 その後、旧コントラクトとやり取りしたすべてのコントラクトを、新しいコントラクトのアドレスに更新する必要があります。
コントラクトマイグレーションの最終ステップは、新しいコントラクトに切り替えてもらうようユーザーを説得することです。 新しいコントラクトでは、不変性が維持されており、ユーザーの残高とアドレスが引き継がれています。 トークンベースのコントラクトの場合は、旧コントラクトの使用を停止し、新しいコントラクトを使用する旨を取引所に通知する必要もあります。
コントラクトマイグレーションは、ユーザーとのやり取りを中断することなく、スマートコントラクトをアップグレードできる、比較的簡単で安全な方法です。 ただし、ユーザーのストレージと残高を新しいコントラクトに手動で移行するのに時間がかかり、高額なガス代が発生する可能性があります。
コントラクトマイグレーションの詳細 (opens in a new tab)
アップグレードメカニズム #2: データセパレーション
スマートコントラクトのアップグレード方法として、ビジネスロジックとデータストレージを別々のコントラクトに分離する方法があります。 この方法では、ユーザーはロジックコントラクトとやり取りし、データはストレージコントラクトに保存されます。
ロジックコントラクトは、アプリケーションとやり取りする際に実行されるコードを含んでいます。 また、ロジックコントラクトは、ストレージコントラクトのアドレスを保持しており、データの取得や設定を処理します。
一方で、ストレージコントラクトは、ユーザー残高やアドレスなどのスマートコントラクトに関連する状態を保持します。 ストレージコントラクトは、ロジックコントラクトによって管理されます。また、デプロイ時にロジックコントラクトのアドレスが設定されることに注意してください。 これにより、認可されていないコントラクトがストレージコントラクトを呼び出したり、ストレージコントラクトのデータを更新したりすることを防止できます。
デフォルトでは、ストレージコントラクトは不変ですが、ロジックコントラクトは新しい実装に置き換えることで、 ストレージと残高をそのままに保ちつつ、EVMで実行されるコードを変更することができます。
このアップグレード方法を使用するには、ストレージコントラクト内のロジックコントラクトのアドレスを更新する必要があります。 また、前述した理由により、ストレージコントラクトのアドレスを使用して、新しいロジックコントラクトを構成する必要があります。
データセパレーションパターンは、コントラクトマイグレーションと比較して、簡単に実装できます。 ただし、スマートコントラクトを悪意のあるアップグレードから保護するには、複数のコントラクトを管理し、複雑な認可スキームを実装する必要があります。
アップグレードメカニズム #3: プロキシパターン
プロキシパターンでも、ビジネスロジックとデータを別々のコントラクトに保持するために、データセパレーションを使います。 しかし、プロキシパターンでは、コードの実行中に、ストレージコントラクト(プロキシと呼ばれる)がロジックコントラクトを呼び出します。 これは、ロジックコントラクトがストレージコントラクトを呼び出すデータセパレーションとは逆の方法です。
プロキシパターンでは、以下の処理が行われます。
-
ユーザーは、データを格納するプロキシコントラクトとやり取りします。ただし、プロキシコントラクトにはビジネスロジックは含まれていません。
-
プロキシコントラクトでは、ロジックコントラクトのアドレスを格納し、
delegatecall関数を使って、すべての関数の呼び出しを(ビジネスロジックを持つ)ロジックコントラクトに委任します。 -
呼び出しがロジックコントラクトに転送された後、ロジックコントラクトから返されたデータを取得し、ユーザーに戻します。
プロキシパターンを使用するには、delegatecall関数を理解する必要があります。 基本的に、delegatecallはコントラクトが別のコントラクトを呼び出すことを可能にするオペコードですが、実際のコード実行は呼び出し元コントラクトのコンテキストで行われます。 プロキシパターンでdelegatecallを使用すると、プロキシコントラクトがストレージの読み取りと書き込みを行い、まるで内部関数を呼び出しているかのように、ロジックコントラクトのロジックを実行することになります。
Solidityドキュメント (opens in a new tab)より:
delegatecallという、メッセージコールの特別な亜種が存在します。これは、ターゲットアドレスのコードが呼び出し元コントラクトのコンテキスト(すなわちアドレス)で実行され、
msg.senderとmsg.valueの値が変わらない点を除けば、メッセージコールと同一です。 これは、コントラクトが実行時に異なるアドレスからコードを動的にロードできることを意味します。 ストレージ、現在のアドレス、残高は呼び出し元のコントラクトを参照しており、コードのみが呼び出し先のアドレスから取得されます。_
プロキシコントラクトにはfallback関数が組み込まれているため、ユーザーが関数を呼び出すと、常にdelegatecallが呼び出されます。 Solidityプログラミングでは、関数呼び出しがコントラクトで指定された関数と一致しない場合にフォールバック関数 (opens in a new tab)が実行されます。
プロキシパターンを機能させるには、プロキシコントラクトがサポートしていない関数の呼び出しを処理する方法を指定したカスタムフォールバック関数を作成する必要があります。 この場合、プロキシのフォールバック関数は、delegatecallを起動し、ユーザーのリクエストを現在のロジックコントラクト実装に切り替えるようにプログラムされます。
プロキシコントラクトは、デフォルトでは不変ですが、新しいロジックコントラクトを作成することで、ビジネスロジックを更新することができます。 アップグレードを実行するには、プロキシコントラクトが参照しているロジックコントラクトのアドレスを変更する必要があります。
プロキシコントラクトが新しいロジックコントラクトを参照するように変更することで、ユーザーがプロキシコントラクトの関数を呼び出すときに実行されるコードを変更することができます。 これにより、ユーザーは新しいコントラクトとやり取りする必要がなくなり、コントラクトのロジックをアップグレードすることができます。
プロキシパターンは、コントラクトのマイグレーションを容易にすることから、アップグレードで広く普及している方法です。 しかし、プロキシパターンは使用がより複雑であり、不適切に使用されると関数セレクタの衝突 (opens in a new tab)などの重大な欠陥を引き起こす可能性があります。
プロキシパターンの詳細 (opens in a new tab)
アップグレードメカニズム #4: ストラテジーパターン
このテクニックはストラテジーパターン (opens in a new tab)の影響を受けています。これは、特定の機能を実装するために他のプログラムとインターフェースするソフトウェアプログラムを作成することを推奨するものです。 ストラテジパターンをイーサリアム開発に適用すると、他のコントラクトから関数を呼び出すスマートコントラクトを構築することになります。
この場合、メインコントラクトはコアとなるビジネスロジックを持ちますが、他のスマートコントラクト(「サテライトコントラクト」) のインターフェースで、特定の機能を実行します。 このメインコントラクトには、各サテライトコントラクトのアドレスが格納されており、サテライトコントラクトの実装を切り替えることができます。
新しいサテライトコントラクトを構築して、メインコントラクトにその新しいコントラクトアドレスを設定することで、 これにより、スマートコントラクトの_ストラテジー_(すなわち、新しいロジックの実装)を変更できます。
先ほどのプロキシパターンと似ていますが、ストラテジパターンでは、ユーザーがやり取りするメインコントラクトがビジネスロジックを持っています。これは、プロキシパターンと異なる点です。 このパターンを使用すると、コアインフラストラクチャに影響を与えることなく、スマートコントラクトに限定的な変更を加えることができます。
このパターンの欠点は、マイナーアップグレードにしか対応できないことです。 また、メインコントラクトが(ハッキングなどにより)侵害された場合は、このアップグレード方法は使用できません。
アップグレードメカニズム #5: ダイヤモンドパターン
ダイヤモンドパターンは、プロキシパターンの改良版と言えます。 ダイヤモンドパターンでは、ダイヤモンドプロキシコントラクトが、関数呼び出しを複数のロジックコントラクトに委任できることができます。これは、プロキシパターンではできなかったことです。
ダイヤモンドパターンのロジックコントラクトは、_ファセット_として知られています。 ダイヤモンドパターンを機能させるには、プロキシコントラクト内で関数セレクタ (opens in a new tab)をさまざまなファセットアドレスにマッピングするマッピングを作成する必要があります。
ユーザーが関数を呼び出すと、プロキシコントラクトはマッピングをチェックして、その関数の実行を担当するファセットを見つけます。 次に、(fallback関数を使用して)delegatecallを呼び出し、その呼び出しを適切なロジックコントラクトにリダイレクトします。
このダイヤモンドアップグレードパターンは、従来のプロキシアップグレードパターンに比べて、以下の利点があります。
-
コード全体を変更しなくとも、コントラクトの一部をアップグレード可能。 プロキシパターンを使ってアップグレードすると、マイナーアップグレードであっても、新しいロジックコントラクトを最初から作成しなければなりません。
-
すべてのスマートコントラクト(プロキシパターンで使用されるロジックコントラクトを含む)には、24KBのサイズ制限がある。この制限は、特に、多くの機能を必要とする複雑なコントラクトで適用されます。 ダイヤモンドパターンを使えば、関数を複数のロジックコントラクトに分割できるため、この問題を簡単に解決できます。
-
プロキシパターンは、アクセス制御に対して、一括管理するアプローチを採用。 アップグレード機能にアクセスできるエンティティは、コントラクト_全体_を変更できます。 一方、ダイヤモンドパターンを使用することで、モジュールごとにアクセス許可を設定することができます。これにより、エンティティがスマートコントラクト内の特定の機能をアップグレードすることを制限できます。
ダイヤモンドパターンの詳細 (opens in a new tab)
スマートコントラクトをアップグレードする際の長所と短所
| 長所 | 短所 |
|---|---|
| スマートコントラクトのアップグレードにより、デプロイ後に発見された脆弱性を修正しやすくなります。 | スマートコントラクトをアップブレードすると、コードの不変性が失われ、分散化とセキュリティに悪影響を与える可能性があります。 |
| デベロッパーは、ロジックのアップグレードを使い、分散アプリケーションに新しい機能を追加できます。 | ユーザーは、デベロッパーがスマートコントラクトを勝手に変更しないことを信頼しなければなりません。 |
| スマートコントラクトのアップグレードにより、バグを迅速に修正できます。これにより、エンドユーザーの安全性が向上します。 | スマートコントラクトにアップグレード機能をプログラミングすると、より複雑になり、重大な欠陥が発生するリスクが高まります。 |
| コントラクトのアップグレードにより、デベロッパーは、さまざまな機能を試したり、時間とともにDappを改良したりすることができます。 | スマートコントラクトをアップグレードできるため、デベロッパーは、開発段階でデューデリジェンスを行わずに、プロジェクトを早く開始してしまうかもしれません。 |
| スマートコントラクトがセキュリティが低下したアクセスコントロールや集中化のリスクにさらされることで、悪意のある攻撃者が、認可されていないアップグレードを実行しやすくなる可能性があります。 |
スマートコントラクトをアップグレードする際の考慮事項
-
特にプロキシパターン、ストラテジパターン、データセパレーションを使う場合、安全なアクセスコントロール/認可メカニズムを用いて、認可されていないスマートコントラクトへのアップグレードを防止します。 例えば、コントラクトの所有者のみがアップグレード関数を呼び出せるように、アップグレード関数へのアクセスを限定します。
-
スマートコントラクトのアップグレードは複雑な作業です。また、脆弱性の侵入を防ぐためには、細心の注意が必要です。
-
アップグレードを実行するプロセスを分散化することで、信頼の仮定を軽減します。 考えられる戦略としては、マルチシグウォレットコントラクトを使用してアップグレードを管理したり、アップグレードの承認についてDAOのメンバーに投票を求めたりする方法があります。
-
コントラクトのアップグレードにコストがかかることに注意します。 例えば、コントラクトのマイグレーション中に、古いコントラクトから新しいコントラクトへ状態(例: ユーザーの残高)をコピーするには、複数のトランザクションが必要になる場合があります。これにより、ガス代が高くなります。
-
ユーザーを保護するためにタイムロックの実装を検討してください。 タイムロックによって、システムへの変更を強制的に遅延させることができます。 タイムロックとマルチシグによるガバナンスシステムを組み合わせることで、アップグレードをコントロールすることができます。これにより、提案されたアクションが承認に必要なしきい値に達しても、タイムロックで事前に定めた遅延期間が経過するまでは実行されません。
タイムロックは、ユーザーが提案された変更(例: ロジックのアップグレードや新しい料金体系など)に同意しない場合、ユーザーに対してシステムから離脱する猶予期間を与える機能です。 タイムロックがないと、ユーザーは、デベロッパーが事前通知なしでスマートコントラクトを勝手に変更しないという信頼に頼らざるを得ません。 タイムロックの欠点としては、脆弱性に対するパッチの適用が遅れる可能性があることです。
参考リソース
OpenZeppelin Upgrades Plugins - アップグレード可能なスマートコントラクトをデプロイし、安全性を確保するためのツールスイート。
チュートリアル
- スマートコントラクトのアップグレード | YouTubeチュートリアル (opens in a new tab) (Patrick Collins氏)
- イーサリアムスマートコントラクトの移行チュートリアル (opens in a new tab) (Austin Griffith氏)
- UUPSプロキシパターンを使用したスマートコントラクトのアップグレード (opens in a new tab) (Pranesh A.S氏)
- Web3チュートリアル: OpenZeppelinを使用してアップグレード可能なスマートコントラクト(プロキシ)を作成 (opens in a new tab) (fangjun.eth氏)
参考リンク
- スマートコントラクトアップグレードの現状 (opens in a new tab) (Santiago Palladino氏)
- Solidityスマートコントラクトをアップグレードする複数の方法 (opens in a new tab) - Crypto Market Poolブログ
- 学習: スマートコントラクトのアップグレード (opens in a new tab) - OpenZeppelinドキュメント
- Solidityコントラクトのアップグレード可能性のためのプロキシパターン: トランスペアレントプロキシ vs UUPSプロキシ (opens in a new tab) (Naveen Sahu氏)
- ダイヤモンドアップグレードの仕組み (opens in a new tab) (Nick Mudge氏)