メインコンテンツへスキップ

アップグレード可能なスマートコントラクト入門

更新日:
2025年11月26日

読了時間:7分

概要

デフォルトでは、イーサリアムブロックチェーン上にデプロイされたスマートコントラクトは不変です。これは分散化とセキュリティの確保には役立ちますが、スマートコントラクトの機能性を制限してしまう可能性があります。
この問題を解決するには、開発段階でアップグレード可能なスマートコントラクトのパターンを採用する必要があります。プロキシパターンを用いて作成されたアップグレード可能なスマートコントラクトにより、開発者はセキュリティや分散性を損なうことなく、デプロイ後にコントラクトの機能を変更することが可能になります。

私たちの取り組み

  • アップグレード可能なスマートコントラクトを定義し、そのユースケースをまとめる
  • アップグレード可能なスマートコントラクトの仕組みを理解する
  • アップグレード可能なスマートコントラクトを作成するために必要なことを学びましょう

必要なもの
以下の事項に関する基本的な知識:

  • スマートコントラクト
  • dApps
  • EVM
  • Solidity

スマートコントラクトとは何ですか?

アップグレード可能なスマートコントラクトについて詳しく説明する前に、スマートコントラクトとは何かを改めて確認しておきましょう。簡単に言えば、スマートコントラクトとは、ブロックチェーン上の特定のアドレスで実行されるプログラムのことです。スマートコントラクトは、適切な関数が呼び出されれば自動的に処理を実行できるという点で、自己実行型と言えます。例えば、特定の関数を呼び出すと、スマートコントラクトがあなたのアドレスにトークンを送信することができます。

イーサリアムのスマートコントラクトは、ブロックチェーン上でコードの実行を可能にするチューリング完全な仮想マシンであるイーサリアム仮想マシン(EVM)にデプロイされます。EVM環境の基本的なルールとして、コントラクトのコードはデプロイ後に変更できないことが挙げられ、これがスマートコントラクトが「不変」と呼ばれる理由です。

「アップグレード可能なスマートコントラクト」とは何ですか?

一般的な認識とは異なり、スマートコントラクトはアップグレード*可能です*。ただし、「アップグレード可能」と「変更可能」は異なる概念であることを明確にしておくことが重要です。

アップグレード可能なスマートコントラクトは、「プロキシパターン」と呼ばれる特別な機能を利用しており、これにより開発者はデプロイ後もコントラクトのロジックをある程度変更することが可能になります。以下に、プロキシコントラクトアーキテクチャを構成する要素の詳細を示します。

1. プロキシ契約
2. 実行契約またはロジック契約

最初のコントラクト(プロキシコントラクト)は開始時にデプロイされ、コントラクトのストレージと残高を保持しています。一方、2つ目のコントラクト(実行コントラクト)には、関数の実行に使用されるコントラクトロジックが格納されています。 
プロキシパターンでは、プロキシ契約が実行契約のアドレスを保持します。ユーザーがリクエストを送信すると、メッセージはプロキシ契約を経由して実行契約へと転送されます。その後、プロキシ契約は実行契約から計算結果を受け取り、それをユーザーに返します。
なお、プロキシ契約自体は変更できません。しかし、更新された契約ロジックを持つ追加の実行契約を作成し、メッセージ呼び出しを新しい契約に再ルーティングすることは可能です。プロキシパターンを使用すれば、スマートコントラクト自体をいかなる形でも変更する必要はありません。行うのは、新しいロジック契約をデプロイし、プロキシ契約に対して古い契約の代わりにそれを参照するよう指示することだけです。この新しいロジック契約には、追加機能や古いバグの修正が含まれている可能性があります。

なぜアップグレード可能な契約を利用するべきなのか?

イーサリアムの魅力の一部は、スマートコントラクトの不変性にあります。ユーザーは、開発者が恣意的な変更を加えることができないと信頼しており、それが分散型アプリケーションとの自由なやり取りを後押ししています。
したがって、アップグレード可能なスマートコントラクトの性質が変化することには、特に分散化やセキュリティの観点から懸念が生じかねません。しかし、スマートコントラクトにプロキシパターンを実装することで、次のような大きな改善が期待できます:

バグの修正

他のプログラムと同様、スマートコントラクトにも欠陥や脆弱性が存在することがよくあります。しかし、コントラクトをアップグレードする手段がなければ、致命的なバグの修正は困難であり、場合によっては完全に不可能なことさえあります。
アップグレード可能なスマートコントラクトを使用すれば、開発者は特定された問題をより容易に解決できます。必要なのは、新しい実行用コントラクトを作成し、プロキシコントラクトの設定を変更して古いコントラクトを参照しないようにすることだけです。
バグを修正する手段があれば、スマートコントラクトの開発者はユーザーに安全性を保証することができます。

 製品の改良

分散型アプリケーション(dapps)が主流になるにつれ、絶え間ない改善が必要となってくるでしょう。これを実現するためには、バックエンドとして機能するスマートコントラクトが、変更を受け入れられるものでなければなりません。

アップグレード可能なスマートコントラクトのパターンにより、新機能の追加やユーザー体験全体の向上が可能になります。ただし、悪意のある行為を防ぐためには、アップグレードの管理が分散化されていることが重要です。

スマートコントラクトをアップグレード可能にする方法

ほとんどのプロキシパターンはDELEGATECALLEVMオペコードに依存しているため、ここではデリゲートコールの仕組みについて簡単に説明します:
2つのコントラクト間の通常のメッセージ呼び出し(CALL)では、呼び出し元コントラクト(コントラクトA)がデータペイロードを呼び出し先コントラクト(コントラクトB)に送信します。その後、コントラクトBはコントラクトロジックを実行し、結果をコントラクトAに返します。通常のメッセージ呼び出しについては、次の2つの点を理解しておく必要があります:

  • 呼び出された契約(契約B)は、常に自身のストレージへの読み取りおよび書き込みを行います。
  • msg.sendermsg.valueといったメッセージ呼び出しに関する情報は、メッセージ呼び出しの途中で変更される可能性があります。

デリゲートコール(DELEGATECALL)では、呼び出されるコントラクト(コントラクト B)が、操作の実行に使用されるコードを保持しています。しかし、実際のロジックの実行は、呼び出し元コントラクト(コントラクト A)のコンテキスト内で行われます。これにより、次のような影響が生じます:

  • 契約Aは契約Bに格納されたロジックを、あたかも内部関数を呼び出すかのように実行できる。
  • 契約 Bで実行される読み取り・書き込み操作は、契約 A のストレージにのみ影響を及ぼします。
  • 呼び出し中、msg.sendermsg.valueは変化しません。

Solidityのドキュメントより:

この「デリゲートコール」とは、契約が実行時に別のアドレスからコードを動的に読み込むことができることを意味します。ストレージ、現在のアドレス、残高は引き続き呼び出し元の契約を指しますが、コードのみが呼び出されたアドレスから取得されます。

デリゲート呼び出しを理解すれば、プロキシパターンの概念もより理解しやすくなります。プロキシ契約はロジック契約のアドレスを保持し、すべての呼び出しをそこに委譲します。また、プロキシはDELEGATECALL関数を使用するため、操作にロジック契約のコードを利用することができます。
デリゲート呼び出しに加えて、アップグレード可能な契約には、アップグレードを制御するための別の関数が必要です。この関数を使用することで、プロキシ内で参照されている実行契約のアドレスを変更することができます。

簡単な方法としては、プロキシ契約にアップグレード機能を組み込み、`onlyAdmin`を使用して不正なアップグレードを防ぐことです。これにより、管理者レベルのアクセス権を持つエンティティのみが、プロキシを新しいロジック契約にアップグレードできるようになります。

プロキシがアップグレードロジックを保持し、実行契約が複雑な設計を必要とせずにデリゲート呼び出しを受け入れられるため、開発者はこのパターンを好む。しかし、単純なプロキシパターンを使用すると、「関数セレクタの競合」という問題が生じる。この問題を受けて、「透過プロキシ」と呼ばれる新しいパターンが開発された。

透過プロキシ

前述したように、単純なプロキシ契約では、DELEGATECALL関数を使用して、関数呼び出しを実行可能コードを保持するロジック契約へとリダイレクトします。また、プロキシ契約は、ロジック契約のアドレスを変更する関数も使用しており、これにより契約のアップグレードが可能になります。

しかし、プロキシのアップグレード管理関数が、ロジック契約内の別の関数と同じ識別子を持っている場合、このアプローチでは問題が生じる可能性があります。この場合、プロキシ契約とロジック契約のどちらが呼び出されたのかを見分けることが難しくなります。
関数セレクタの競合は、エラーや、さらに悪い場合には悪意のあるエクスプロイトにつながる可能性があります。この問題を解決するため、Open Zeppelinチームは「透過プロキシパターン」を開発しました。以下に、透過プロキシパターンの仕組みについて詳しく説明します。

透過プロキシパターンは、メッセージの呼び出しをその発信元(msg.sender)に基づいて解釈します:

  • msg.senderが外部アドレスを指している場合、プロキシは自動的にそれをロジックコントラクトに委譲します。これは、呼び出された関数がプロキシの内部関数のいずれかと類似している場合でも同様です。
  • msg.sender が管理者を指している場合、プロキシは呼び出しを委譲しません。その代わりに、(その関数を理解できる限り)その操作を実行します。

つまり、プロキシ管理関数を呼び出せるのは管理者だけであるということです。外部アドレスが競合するロジック契約関数を呼び出した場合、その操作は単にロールバックされます。透過プロキシパターンを使用する上での唯一の問題は、より高いガス料金を支払わなければならないことです。これは、EVMが各呼び出しごとに実行契約アドレスを読み込むため、また委任ロジックを実行するために、追加のガスを必要とするためです。さらに、透過プロキシパターン自体は、平均的なものよりもデプロイコストが高くなります。

ユニバーサル・アップグレード・プロキシ規格

もう1つ、あまり普及してはいないものの、プロキシパターンとして「ユニバーサル・アップグレード・プロキシ・スタンダード(UUPS)」があります。透過型プロキシパターンと同様に、ユニバーサル・アップグレード可能なプロキシもDELEGATECALL関数を使用します。主な違いは、アップグレード機能を管理するのがプロキシではなく、ロジック契約であるという点です。

このパターンでは、ロジック契約は依然としてプロキシ契約のストレージに書き込みを行い、そのアドレスはプロキシ契約に保存されます。ただし、すべてのロジック契約は、アップグレード機能を提供する「Proxiable」契約を継承しています。
実行契約に保存されている「Proxiable」契約を使用することで、プロキシ契約で参照されている契約アドレスを更新することができます。これにより、すべての実行契約が同じ「Proxiable」契約を継承している限り、新しい実行契約を引き続き使用し続けることが可能になります。

汎用的なアップグレード可能なプロキシは、関数セレクタの競合が発生する可能性を低減できるという点で有益です。Solidityコンパイラは、2つのコントラクト間で関数の競合を検出することはできませんが、同じコントラクト内で競合が発生した場合はそれを拒否することができます。さらに、この場合のプロキシコントラクトはストレージ使用量が少ないため、デプロイコストを削減できます。

ただし、汎用的なアップグレード可能なプロキシを使用することには代償が伴います。プロキシ契約を、アップグレード機能を継承していないロジック契約にアップグレードしてしまうと、将来的にそのプロキシをアップグレードすることは不可能になります。

結論

ここまでガイドを読み進めてくださった方、おめでとうございます!これで、アップグレード可能なスマートコントラクトの設計方法と、それをdAppに導入することがなぜ有益なのかを理解できたはずです。

イーサリアムに関する記事やガイドをもっと読みたい方は、ぜひニュースレターにご登録ください。ご意見やご感想がございましたら、Twitterからお気軽にお寄せください。また、Discordコミュニティサーバーでは、これまで出会った中で最もクールな開発者たちが集まっていますので、いつでもお気軽にチャットで交流してください :)

このガイドをシェアする