スマートコントラクトのアップグレード
最終編集者: @HiroyukiNaito(opens in a new tab), 2023年8月15日
イーサリアムのスマートコントラクトは、イーサリアム仮想マシン(EVM)で実行される自己実行プログラムです。 これらのプログラムは、不変になるように設計されています。そのため、一度デプロイしたコントラクトのビジネスロジックを後から変更することはできません。
この不変性は、トラストレス、分散化、スマートコントラクトのセキュリティを実現するために必要ですが、場合によっては欠点になることもあります。 例えば、このコードの不変性により、デベロッパーは脆弱なコントラクトを修正できないことがあります。
しかし、スマートコントラクトの改善に向けた研究が進み、いくつかのアップグレードパターンが開発されました。 これらのアップグレードでは、デベロッパーが別のコントラクトにビジネスロジックを配置することで、スマートコントラクトを(不変性を維持しつつ)アップグレードすることができます。
前提知識
スマートコントラクト、スマートコントラクトの構造、 イーサリアム仮想マシン(EVM)について十分に理解している必要があります。 さらに、このガイドでは、読者がスマートコントラクトのプログラミングを理解していることを前提としています。
スマートコントラクトのアップグレードとは
スマートコントラクトをアップグレードするには、コントラクトの状態を維持したまま、スビジネスロジックを変更する必要があります。 特にアップグレード可能性と不変性は、スマートコントラクトのコンテキストでは、必ずしも一致するものではありません。
イーサリアムネットワーク上のアドレスにデプロイされたプログラムは、変更できません。 しかし、ユーザーがスマートコントラクトとやり取りする際に、実行されるコードは変更することができます。
これについては、次の方法を用いて行います。
複数のバージョンのスマートコントラクトを作成し、古いコントラクトから新しいコントラクトのインスタンスに状態(データ)を移行する。
ビジネスロジックおよび状態を保存するセパレートコントラクトを作成する。
プロキシパターンを使い、不変のプロキシコントラクトから修正可能なロジックコントラクトへの関数の呼び出しを委任する。
特定の関数を実行する柔軟なサテライトコントラクトに依存し、インターフェースで接続する不変のメインコントラクトを作成する。
ダイヤモンドパターンを使い、プロキシコントラクトからロジックコントラクトへの関数の呼び出しを委任する。
アップグレードメカニズム #1: コントラクトマイグレーション
スマートコントラクトのマイグレーションは、バージョン管理に基づいています。バージョン管理とは、同一のソフトウェアの固有の状態を作成・管理する考え方です。 コントラクトマイグレーションでは、従来のスマートコントラクトを新しいインスタンスにデプロイし、ストレージと残高を新しいコントラクトに転送します。
新しくデプロイされたコントラクトのストレージは空です。そのため、旧コントラクトからデータを復元して、新しい実装に書き込むことができます。 その後、旧コントラクトとやり取りしたすべてのコントラクトを、新しいコントラクトのアドレスに更新する必要があります。
コントラクトマイグレーションの最終ステップは、新しいコントラクトに切り替えてもらうようユーザーを説得することです。 新しいコントラクトでは、不変性が維持されており、ユーザーの残高とアドレスが引き継がれています。 トークンベースのコントラクトの場合は、旧コントラクトの使用を停止し、新しいコントラクトを使用する旨を取引所に通知する必要もあります。
コントラクトマイグレーションは、ユーザーとのやり取りを中断することなく、スマートコントラクトをアップグレードできる、比較的簡単で安全な方法です。 ただし、ユーザーのストレージと残高を新しいコントラクトに手動で移行するのに時間がかかり、高額なガス代が発生する可能性があります。
コントラクトマイグレーションの詳細(opens in a new tab)
アップグレードメカニズム #2: データセパレーション
スマートコントラクトのアップグレード方法として、ビジネスロジックとデータストレージを別々のコントラクトに分離する方法があります。 この方法では、ユーザーはロジックコントラクトとやり取りし、データはストレージコントラクトに保存されます。
ロジックコントラクトは、アプリケーションとやり取りする際に実行されるコードを含んでいます。 また、ロジックコントラクトは、ストレージコントラクトのアドレスを保持しており、データの取得や設定を処理します。
一方で、ストレージコントラクトは、ユーザー残高やアドレスなどのスマートコントラクトに関連する状態を保持します。 ストレージコントラクトは、ロジックコントラクトによって管理されます。また、デプロイ時にロジックコントラクトのアドレスが設定されることに注意してください。 これにより、認可されていないコントラクトがストレージコントラクトを呼び出したり、ストレージコントラクトのデータを更新したりすることを防止できます。
デフォルトでは、ストレージコントラクトは不変ですが、ロジックコントラクトは新しい実装に置き換えることで、 ストレージと残高をそのままに保ちつつ、EVMで実行されるコードを変更することができます。
このアップグレード方法を使用するには、ストレージコントラクト内のロジックコントラクトのアドレスを更新する必要があります。 また、前述した理由により、ストレージコントラクトのアドレスを使用して、新しいロジックコントラクトを構成する必要があります。
データセパレーションパターンは、コントラクトマイグレーションと比較して、簡単に実装できます。 ただし、スマートコントラクトを悪意のあるアップグレードから保護するには、複数のコントラクトを管理し、複雑な認可スキームを実装する必要があります。
アップグレードメカニズム #3: プロキシパターン
プロキシパターンでも、ビジネスロジックとデータを別々のコントラクトに保持するために、データセパレーションを使います。 しかし、プロキシパターンでは、コードの実行中に、ストレージコントラクト(プロキシと呼ばれる)がロジックコントラクトを呼び出します。 これは、ロジックコントラクトがストレージコントラクトを呼び出すデータセパレーションとは逆の方法です。
プロキシパターンでは、以下の処理が行われます。
ユーザーは、データを格納するプロキシコントラクトとやり取りします。ただし、プロキシコントラクトにはビジネスロジックは含まれていません。
プロキシコントラクトでは、ロジックコントラクトのアドレスを格納し、
delegatecall
関数を使って、すべての関数の呼び出しを(ビジネスロジックを持つ)ロジックコントラクトに委任します。呼び出しがロジックコントラクトに転送された後、ロジックコントラクトから返されたデータを取得し、ユーザーに戻します。
プロキシパターンを使うには、delegatecall関数を理解する必要があります。 基本的に、delegatecall
は、コントラクトが別のコントラクトを呼び出すことができるオペコードですが、実際のコードは呼び出し元のコントラクトのコンテキストで行われます。 プロキシパターンでdelegatecall
を使用すると、プロキシコントラクトがストレージの読み取りと書き込みを行い、まるで内部関数を呼び出しているかのように、ロジックコントラクトのロジックを実行することになります。
Solidityのドキュメント(opens in a new tab)の引用:
delegatecallという特別な可変メッセージ呼び出しがあります。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を改良したりすることができます。 | スマートコントラクトをアップグレードできるため、デベロッパーは、開発段階でデューデリジェンスを行わずに、プロジェクトを早く開始してしまうかもしれません。 |
スマートコントラクトがセキュリティが低下したアクセスコントロールや集中化のリスクにさらされることで、悪意のある攻撃者が、認可されていないアップグレードを実行しやすくなる可能性があります。 |
スマートコントラクトのアップグレードにおける考慮事項
特にプロキシパターン、ストラテジパターン、データセパレーションを使う場合、安全なアクセスコントロール/認可メカニズムを用いて、認可されていないスマートコントラクトへのアップグレードを防止します。 例えば、コントラクトの所有者のみがアップグレード関数を呼び出せるように、アップグレード関数へのアクセスを限定します。
スマートコントラクトのアップグレードは複雑な作業です。また、脆弱性の侵入を防ぐためには、細心の注意が必要です。
アップグレードを実行するプロセスを分散化することで、信頼の仮定を軽減します。 可能な戦略としては、マルチシグウォレットコントラクトを用いてアップグレードをコントロールしたり、DAOメンバーの投票によってアップグレードを承認したりするなどの方法があります。
コントラクトのアップグレードにコストがかかることに注意します。 例えば、コントラクトのマイグレーション中に、古いコントラクトから新しいコントラクトへ状態(例: ユーザーの残高)をコピーするには、複数のトランザクションが必要になる場合があります。これにより、ガス代が高くなります。
ユーザーの保護にタイムロックの実装を検討します。 タイムロックによって、システムへの変更を強制的に遅延させることができます。 タイムロックとマルチシグによるガバナンスシステムを組み合わせることで、アップグレードをコントロールすることができます。これにより、提案されたアクションが承認に必要なしきい値に達しても、タイムロックで事前に定めた遅延期間が経過するまでは実行されません。
タイムロックは、ユーザーが提案された変更(例: ロジックのアップグレードや新しい料金体系など)に同意しない場合、ユーザーに対してシステムから離脱する猶予期間を与える機能です。 タイムロックがないと、ユーザーは、デベロッパーが事前通知なしでスマートコントラクトを勝手に変更しないという信頼に頼らざるを得ません。 タイムロックの欠点としては、脆弱性に対するパッチの適用が遅れる可能性があることです。
リソース
OpenZeppelinアップグレードプラグイン - アップグレード可能なスマートコントラクトのデプロイおよび保護するためのツールスイート
チュートリアル
- スマートコントラクトのアップグレード | Patrick CollinsによるYouTubeチュートリアル(opens in a new tab)
- Austin Griffithによるイーサリアム・スマートコントラクト・マイグレーション・チュートリアル(opens in a new tab)
- Pranesh A.SによるUUPSプロキシパターンを使ったスマートコントラクトのアップグレード(opens in a new tab)
- fangjun.ethによるWeb3チュートリアル: OpenZeppelinを使用したアップグレード可能なスマートコントラクト(プロキシ)の作成(opens in a new tab)
参考文献
- スマートコントラクトの状態のアップグレード(opens in a new tab) | Santiago Palladino
- Solidityスマートコントラクトのさまざまなアップグレード方法(opens in a new tab) - Crypto Market Poolのブログ
- スマートコントラクトのアップグレードの学習(opens in a new tab) - OpenZeppelinドキュメント
- Solidityコントラクトのアップグレード可能なプロキシパターン: 透過プロキシとUUPSプロキシの比較(opens in a new tab) | Naveen Sahu
- ダイヤモンドアップグレードの動作原理(opens in a new tab) | Nick Mudge