ZeppelinOS: アップグレード可能なコントラクトを記述するためのフレームワーク (後半)
前半では、アップグレード可能なコントラクトの仕組みについて説明した。
後半である本記事では、実際にZeppelinOSを使用した実装について説明する。
- ZeppelinOSとは?
- ZeppelinOSを使ってアップグレード可能なコントラクトを実装する
2.1. zosアプリケーションのセットアップ
2.2. zos-libのインストール
2.3. MyContract.solを実装
2.4. MyContractをzosアプリケーションに登録
2.5. Ganacheにコントラクトをデプロイ
2.6. Proxy Contractのデプロイ
2.7. コントラクトのアップグレード - まとめ
1. ZeppelinOSとは?
ZeppelinOSは、アップグレード可能なコントラクトを実装するためのフレームワークで、そのためのライブラリやCLI (zosコマンド)を提供してくれている。
ZeppelinOSにおけるProxy Contractの設計パターンは、前半の「2. 参考:アップグレード可能なコントラクトの詳細な理解」で見た「非構造化ストレージパターン」になっている。それらの旨は、以下の記事で紹介されている。
2. ZeppelinOSを使ってアップグレード可能なコントラクトを実装する
実装の仕方は、以下の公式ドキュメントを参考にしている。
今回は、実際にコントラクトをデプロイし、そのコントラクトをアップグレードするところまでを説明する。ここでの実装は、以下に載せておいた。
2.1. zosアプリケーションのセットアップ
以下のコマンドで、zos (ZeppelinOSのCLI)を使ったアプリケーションをセットアップする。
$ mkdir my-app && cd my-app
$ npm init
$ zos init my-app
zos.jsonには、アプリケーションの設定が記述される。
2.2. zos-libのインストール
アップグレード可能なコントラクトを実現するための仕組みは、zos-libに実装されている。 npmでインストールする。
$ npm install zos-lib
2.3. MyContract.solの実装
以下のようにcontracts/MyContract.solを実装する。
pragma solidity ^0.4.21;
import "zos-lib/contracts/migrations/Migratable.sol";contract MyContract is Migratable {
uint256 public x;function initialize(uint256 _x) isInitializer("MyContract", "0") public {
x = _x;
}
}
zosを使う場合、自前のコントラクトはMigratableというコントラクトを継承させ、 constructorの代わりにinitializeを持たせる。
2.4. MyContractをzosアプリケーションに登録
実装したコントラクトをネットワーク上にデプロイする前に、zosアプリケーションに認知させる必要がある。そのために、 zos add
を使う。
$ zos add MyContract
これによって、zos.jsonに以下の行が追加され、MyContractがzosアプリケーションに組み込まれたことが分かる。
"contracts": {
"MyContract": "MyContract"
}
2.5. Ganacheにコントラクトをデプロイ
今回はテストネットワークとして、Ganacheを使用する。
zos init
でtruffle-config.jsが生成されているので、このポートをganacheが使用している7545に変更する。
以下のコマンドで、コードをネットワーク上にデプロイする。
$ zos push --network local
これによって、以下のzos.local.jsonが生成される。localの部分には、指定したネットワーク名が入る。
{
"contracts": {
"MyContract": {
"address": "0x1bfc7e8a8638c06e5993de2d9844f2f542a2fcba",
"constructorCode": "608060405234801561001057600080fd5b50610b0d806100206000396000f300",
"bodyBytecodeHash": "c829d0f89f2ee73f114364b2473387ebd483822f0418b301c33c988f531b989e",
"bytecodeHash": "83cb3b4e80f79c781380024e9118f0d16d33eab3701d5bec5e4822c4d9826b56"
}
},
"proxies": {},
"app": {
"address": "0x94dbc3a51dcb94bf2657fb7439437784d946cd23"
},
"version": "0.1.0",
"package": {
"address": "0x761930d1cc07ed590ce8563cfdf7dd46962432a7"
},
"provider": {
"address": "0x469f85f8c4deeb35faeeaf6ecb0f3f2dbbfaf7d4"
}
}
このファイルで注目すべきはcontractsプロパティとproxiesプロパティである。 現時点で、contractsプロパティにMyContractが追加されているが、proxiesプロパティには追加されていない。 つまり、今デプロイしたコードは、参照先のコントラクトであり、Proxy Contractはまだデプロイされていない。
また通常、 truffle migrate
コマンドを使ってコントラクトをデプロイすると、build/contracts/MyContract.jsonのaddressプロパティにデプロイしたコントラクトのアドレスが記述されるが、この段階ではまだaddressプロパティがないことにも注意する。これも、Proxy Contractがまだデプロイされていないことに由来する。
2.6. Proxy Contractのデプロイ
Proxy Contractをデプロイするためには、以下のように、 zos create
を使う。
$ zos create MyContract --init initialize --args 42 --network local
ここで、initオプションは、デプロイ後にinitialize関数を呼ぶことを意味し、argsオプションでその時の引数を指定している。
このコマンドによって、zos.local.jsonのproxiesプロパティに、MyContractの情報が追加される。 addressプロパティにProxy Contractのアドレスが、implementationプロパティに参照先コントラクトのアドレスが入っていることが確認できる。また、build/contracts/MyContract.jsonにも、addressプロパティが追加されている。ここに追加されたアドレスは、Proxy Contractのアドレスである。
truffle console
を使ってProxy Contractにアクセスしてみると、xの値である42が読み取れることがわかる。
$ npx truffle console --network=local
$ truffle(local)> myContract = MyContract.at('<your-proxy-address>')
$ truffle(local)> myContract.x()
BigNumber { s: 1, e: 1, c: [ 42 ] }
2.7. コントラクトのアップグレード
実際にコントラクトをアップグレードしてみる。
MyContractのxを書き換える手段がなかったので、その機能を追加するためにMyContract.solを以下のように書き換えるとする。
import "zos-lib/contracts/migrations/Migratable.sol";contract MyContract is Migratable {
uint256 public x;function initialize(uint256 _x) isInitializer("MyContract", "0") public {
x = _x;
}function increment() public {
x += 1;
}
}
これは参照先のコントラクトの変更なので、それをzosアプリケーションに伝えるには、もう一度 zos push
を使う。
$ zos push --network local
これによって、zos.local.jsonのcontractsが変更される。MyContractのアドレスが変わっているのが確認できる。 ただ、これだけでは、デプロイされたProxy Contractには伝わっていない。デプロイされたProxy Contractの参照先を変更するために、 zos update
を利用する。
$ zos update MyContract --network local
これによって、zos.local.jsonのproxiesプロパティにあるMyContractのimplementationプロパティが変更される。
これで、xを書き換えるために追加した関数が実行できる。
$ npx truffle console --network=local
$ truffle(local)> myContract = MyContract.at('<your-proxy-address>')
$ truffle(local)> myContract.x()
BigNumber { s: 1, e: 1, c: [ 42 ] }
$ truffle(local)> myContract.x()
$ truffle(local)> myContract.increment()
BigNumber { s: 1, e: 1, c: [ 43 ] }
3. まとめ
本投稿では、前半・後半に分けて、ZeppelinOSを使用してアップグレード可能なコントラクトを実装する、具体的な方法について説明した。
後半である本記事では、ZeppelinOSのセットアップから、一度デプロイしたコントラクトに、そのコントラクトのアドレスを変更することなく新しい関数を追加する手順までを説明した。
ここで、前半の記事を見るとわかるように、アップグレード可能なコントラクトは、自前で実装することも可能である。場合によっては、その方が融通がきいて、最適なコードを書くことができるかもしれない。
では、ZeppelinOSを利用する価値はどこにあるのだろうか。
それはやはり、OpenZeppelinの開発で有名なZeppelinの、信頼性の高いツールを簡単に使える、という点にあると思う。
特にSolidityによるコードは、金銭的な取引を含むコードが多いため、些細な実装ミスが深刻なトラブルを起こし兼ねない。そのような可能性を少しでも減らすために、信頼性のあるコードを利用することには、非常に大きな意味がある。
また、ZeppelinOSはオープンソースであるため、本当に必要な機能だけを、ZeppelinOSの上に自分で追加実装していくことが可能である。
これは例えば、ERC721準拠のTokenを開発するときに「OpenZeppelinのERC721を元にして、そこに自前の機能を追加していく」という開発方法とよく似ていて、合理的に思える。
こういった理由から、アップグレード可能なコントラクトを実装して見たい場合は、ZeppelinOSから始めてみるのがいいのではないかと思う。
そのための第一歩として、本記事が役に立てれば幸いである。