ZeppelinOS: アップグレード可能なコントラクトを記述するためのフレームワーク (前半)

こみー / Ryo Komiyama
KYUZAN
Published in
8 min readSep 18, 2018
“photo of mountain during cloudy day” by Wolfgang Lutz on Unsplash

Ethereumにデプロイされたコントラクトのコードは、基本的に書き直すことができない。

コントラクトに機能を追加したい場合は新しいコントラクトを再度デプロイする必要があり、その際コントラクトのアドレスが変わってしまう。これでは、元のコントラクトをアップグレードしたとは言い難い。

しかし、実際にサービスを運営したい場合、コントラクトがアップグレード可能であることは重要な要素である。そうでなければ、リリース前に全てのバグをなくさなくてはならなくなる。この点には実際に我々も不自由さを感じたことがあり、以前EthoccerというDappsを開発した際、リリース前に通常のWebサービスでは考えられないほどの緊張感があった。

ZeppelinOSは、アップグレード可能なコントラクトを記述するためのフレームワークであり、この問題を解決する手助けをしてくれる。

前半である本記事では、ZeppelinOSを使ってアップグレード可能なコントラクトを実装する上で、押さえておくべきポイントを説明する。実際にZeppelinOSを使用する部分については、後半の記事にまとめる。

  1. アップグレード可能なコントラクトとは?
    1.1. 通常のスマートコントラクト
    1.2. アップグレード可能なスマートコントラクト
  2. 参考:アップグレード可能なコントラクトの詳細な理解

1. アップグレード可能なコントラクトとは?

コントラクトの「アップグレード」には、以下の3つが含まれる。

  • 既存の関数の実装変更
  • 新しい関数の追加
  • 新しい変数の追加

ここで注意しなくてはならないのが、「変数の削除」「変数の順番の変更」ができないことである。

アップグレード可能なスマートコントラクトの仕組みを理解することでそのような制約の原因もわかるのだが、それらを細部まで理解することは、意外と難しい。

なのでここでは、アップグレード可能なコントラクトの仕組みを感覚的に理解し、ZeppelinOSを使った開発ができるようになることを目的として説明しようと思う。

1.1. 通常のスマートコントラクト

ContractAの関数fを使いたいとき、ユーザーはContractAのアドレス0xAにアクセスする。 関数fを変更したい場合、上述のようにコントラクトをデプロイし直さなくてはならないが、そのとき0xAというアドレスが変わってしまう。これをユーザーは知るすべがないため、運用中のコントラクトの関数を変更することは難しい。

また、コントラクトは独自のストレージを持ち、ストレージ変数を(特別な指定なしで)定義すると、このストレージの先頭から順番に領域が割り当てられていく。そのため、新しいコントラクトをデプロイし直してしまうと、0xAにあるContractAのストレージ変数に保存された値は、次のコントラクトに引き継がれない。

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

1.2.1. 関数を変更・追加する仕組み

通常のコントラクトと同様に、ユーザーはContractAの関数fを使うために、0xAにアクセスする。 このとき、ユーザーからすると、ContractAに関数fがあるように見えるのだが、実際にはfの実装はContractBに記述されている。 ContractAは、fが呼ばれたとき、ContractBのfをdelegatecall*で参照するようになっている。(上図左)

* delegatecallとは「参照先のロジック」だけを使わせてもらうcallのことで、その関数実行でのストレージの操作は「呼び出し元のストレージ」に適用される。

このように設計することで、ContractAが参照するコントラクトのアドレスを変更すると、fの実装を変更することができるようになる(上図右)。参照を変更した後のコントラクト(ContractC)に新しい関数があれば、ContractAのアドレス0xAに対して、その関数を使用することも可能である。

1.2.2. ストレージ変数を追加する仕組み

ストレージ変数も、参照先のコントラクトに定義されている。

ここで注意しなくてはならないのが、「ContractAにはストレージ変数は定義されていないが、delegatecallで関数fを参照することによって、ContractAのストレージは、参照先のストレージ変数があるかのように値が書き込まれていく」ことである。図では、ストレージの色を対応させることで表現した。

このように設計することで、コントラクトをアップグレードしても(参照先のコントラクトがContractBからContractCに変わっても)、これまでストレージに保存していた値は影響を受けない。(常にContractAが保持している。)
また、ContractCで新しいストレージ変数zを追加しても、ContractAのストレージは、ストレージ変数zがあるかのように振る舞う。

ここまで見ると、「変数の追加」はできるが、「変数の削除」「変数の順番の変更」ができない理由も感覚的に理解できると思う。

これが、アップグレードの大まかな仕組みである。
ユーザーは常にContractAにアクセスすればよく、裏側を気にする必要はない。 開発者は、新しいContractCを作り、ContractAの参照先をContractCに変更することで、コントラクトをアップグレードすることができる。

この場合のContractAを、Proxy Contractと呼ぶ。

ZeppelinOSを使ってコントラクトをアップグレードするときに注意しなくてはならないことは、新しいコントラクトで

  • ストレージ変数の削除
  • ストレージ変数の順番の変更

をしないことである。

ここまでの理解で、ZeppelinOSを使った開発をしていくことはできるはずなので、まずは後半に進むといいかもしれない。

以下の章では、アップグレード可能なコントラクトに関するより詳細な理解をするために、必要な知識や参考になるサイトを紹介する。

2. 参考:アップグレード可能なコントラクトの詳細な理解

アップグレード可能なコントラクトのより詳細な理解には、以下の記事が役に立つ。ここでは、前章で「ZeppelinOSが使えるようになることに焦点を当て、抽象化して説明した」部分を、より具体的に説明してくれている。

この内容を理解するには、基本的なEthereumの知識・Solidityのプログラミングスキルに加えて、Proxyコントラクトを理解する必要がある。

そのための予備知識として、storage slot, delegatecallの説明が記事内でされているが、この他に、以下のassemblyを理解しておく必要がある。

記事内で説明されているproxy contractの仕組みを実際に実装したので、そのコードも載せておく。

後半では、実際にZeppelinOSを利用した実装について話していく。

--

--

こみー / Ryo Komiyama
KYUZAN
Editor for

🏔Kyuzan🏔 Blockchain Startup @ Tokyo ▼ex 東大 暦本研 ▼ex Rhizomatiks Research