スマート・コントラクトのアップグレード
イーサリアム上のスマート・コントラクトは、Ethereum Virtual Machine (EVM) で実行される自己実行型プログラムです。これらのプログラムは設計上イミュータブルであり、コントラクトがデプロイされた後はビジネスロジックの更新ができません。
不変性はスマート・コントラクトのトラストレス性、分散化、およびセキュリティに不可欠ですが、特定のケースでは欠点となる場合があります。たとえば、イミュータブルなコードは、開発者が脆弱なコントラクトを修正することを不可能にする可能性があります。
しかし、スマート・コントラクトの改善に関する研究が進んだことで、いくつかのアップグレードパターンが導入されました。これらのアップグレードパターンにより、開発者はビジネスロジックを別のコントラクトに配置することで、(不変性を維持しながら) スマート・コントラクトをアップグレードできるようになります。
前提条件
スマート・コントラクト、スマート・コントラクトの構造、およびEthereum Virtual Machine (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)を異なるファセットアドレスにマッピングするマッピングを作成する必要があります。
ユーザーが関数呼び出しを行うと、プロキシ・コントラクトはマッピングをチェックして、その関数の実行を担当するファセットを見つけます。次に、(フォールバック関数を使用して) delegatecall を呼び出し、適切なロジックコントラクトに呼び出しをリダイレクトします。
ダイヤモンドアップグレードパターンには、従来のプロキシアップグレードパターンに比べていくつかの利点があります。
-
コード全体を変更することなく、コントラクトの小さな部分をアップグレードできます。アップグレードにプロキシパターンを使用する場合、マイナーなアップグレードであっても、まったく新しいロジックコントラクトを作成する必要があります。
-
すべてのスマート・コントラクト (プロキシパターンで使用されるロジックコントラクトを含む) には24KBのサイズ制限があり、特により多くの関数を必要とする複雑なコントラクトにとっては制限となる可能性があります。ダイヤモンドパターンは、関数を複数のロジックコントラクトに分割することで、この問題を簡単に解決します。
-
プロキシパターンは、アクセス制御に対して包括的なアプローチを採用しています。アップグレード関数へのアクセス権を持つエンティティは、コントラクト_全体_を変更できます。しかし、ダイヤモンドパターンはモジュール式の権限アプローチを可能にし、エンティティがスマート・コントラクト内の特定の関数のみをアップグレードできるように制限できます。
ダイヤモンドパターンの詳細 (opens in a new tab)
スマート・コントラクトのアップグレードのメリットとデメリット
| メリット | デメリット |
|---|---|
| スマート・コントラクトのアップグレードにより、デプロイ後のフェーズで発見された脆弱性の修正が容易になります。 | スマート・コントラクトのアップグレードはコードの不変性という考え方を否定するものであり、分散化とセキュリティに影響を与えます。 |
| 開発者はロジックのアップグレードを使用して、分散型アプリケーション (dapp) に新機能を追加できます。 | ユーザーは、開発者がスマート・コントラクトを恣意的に変更しないことを信頼する必要があります。 |
| バグを迅速に修正できるため、スマート・コントラクトのアップグレードはエンドユーザーの安全性を向上させることができます。 | スマート・コントラクトにアップグレード機能をプログラミングすると、複雑さの層が追加され、重大な欠陥の可能性が高まります。 |
| コントラクトのアップグレードにより、開発者はさまざまな機能を試したり、時間をかけてdappを改善したりする余地が広がります。 | スマート・コントラクトをアップグレードできる機会があることで、開発者が開発フェーズで十分なデューデリジェンスを行わずにプロジェクトを早く立ち上げることを助長する可能性があります。 |
| スマート・コントラクトにおける安全でないアクセス制御や中央集権化は、悪意のあるアクターが不正なアップグレードを実行しやすくする可能性があります。 |
スマート・コントラクトのアップグレードに関する考慮事項
-
特にプロキシパターン、ストラテジーパターン、またはデータの分離を使用する場合は、安全なアクセス制御/承認メカニズムを使用して、不正なスマート・コントラクトのアップグレードを防ぎます。例として、コントラクトの所有者のみが呼び出せるようにアップグレード関数へのアクセスを制限することが挙げられます。
-
スマート・コントラクトのアップグレードは複雑な作業であり、脆弱性の導入を防ぐために高いレベルの注意が必要です。
-
アップグレードを実装するプロセスを分散化することで、トラスト前提を減らします。考えられるストラテジーには、アップグレードを制御するためにマルチシグウォレットコントラクトを使用することや、アップグレードの承認についてDAOのメンバーに投票を要求することなどがあります。
-
コントラクトのアップグレードに伴うコストに注意してください。たとえば、コントラクトの移行中に古いコントラクトから新しいコントラクトに状態 (ユーザーの残高など) をコピーするには、複数のトランザクションが必要になる場合があり、これはより多くのガス代を意味します。
-
ユーザーを保護するために タイムロック の実装を検討してください。タイムロックとは、システムへの変更に強制される遅延を指します。タイムロックは、アップグレードを制御するためにマルチシグガバナンスシステムと組み合わせることができます。提案されたアクションが必要な承認しきい値に達した場合でも、事前に定義された遅延期間が経過するまで実行されません。
タイムロックは、提案された変更 (ロジックのアップグレードや新しい手数料スキームなど) に同意しない場合、ユーザーがシステムからエグジットするための時間を与えます。タイムロックがない場合、ユーザーは開発者が事前の通知なしにスマート・コントラクトに恣意的な変更を実装しないことを信頼する必要があります。ここでの欠点は、タイムロックが脆弱性に迅速にパッチを当てる能力を制限することです。
リソース
オープンツェッペリン Upgrades Plugins - アップグレード可能なスマート・コントラクトをデプロイし、保護するためのツールスイート。
チュートリアル
- スマート・コントラクトのアップグレード | ユーチューブチュートリアル (opens in a new tab) (Patrick Collins著)
- イーサリアムスマート・コントラクト移行チュートリアル (opens in a new tab) (Austin Griffith著)
- UUPSプロキシパターンを使用したスマート・コントラクトのアップグレード (opens in a new tab) (Pranesh A.S著)
- Web3チュートリアル: オープンツェッペリンを使用してアップグレード可能なスマート・コントラクト (プロキシ) を記述する (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) - オープンツェッペリンドキュメント
- Solidityコントラクトのアップグレード可能性のためのプロキシパターン: Transparent vs UUPSプロキシ (opens in a new tab) (Naveen Sahu著)
- ダイヤモンドアップグレードの仕組み (opens in a new tab) (Nick Mudge著)