はじめに
基本的にEthereumに一度デプロイされたコントラクトはアップグレードできないため、コードにバグがあるとプロダクトにとって致命傷となってしまいます。
ですが、Proxyパターンを利用することによりアップグレード可能なコントラクトを作成することが可能になります。
Proxyパターンの理解は以下の記事が非常に分かりやすくまとめてくださっているので、まずはこちらをご一読の上、続きをご覧いただければと思います。
■アップグレード可能なスマートコントラクトを実現する具体的なアプローチ
https://zoom-blc.com/how-to-develop-upgradable-contracts
今回はZeppelinOSというアップグレード可能なコントラクトを実装するためのフレームワークを利用してERC721トークンを作っていきます。(ZeppelinOSを使わなくてもProxyContractを自分で書くことで実現できますが、その作業が不要になったりと何かと嬉しいです)
まずは成果物
成果物はこちらに上げています。
https://github.com/shiruco/upgradable-ERC721-sample
内容を簡単に説明します。
以下、ローカル環境のプライベートチェーンで動作させるものとします。
- まず
SampleToken(ERC721) をデプロイ。 - 続けてSampleTokenのアップグレード版
SampleToken_v1(ERC721)をデプロイ。 - SampleTokenの実装がSampleToken_v1に変更されていることを確認
という流れです。
開発環境
私の開発環境は以下のようになっています。もし実際に試されるのであればバージョンを揃えていただいた方がいいかもしれません。
- ZeppelinOS 2.2.0
- Truffle v5.0.4 (core: 5.0.4)
- Solidity v0.5.0 (solc-js)
- Ganache CLI v6.3.0 (ganache-core: 2.4.0)
- Node v11.3.0
ZeppelinOSのインストールはこちら。 (私はグローバルに入れました)
npm install -g zos
or
yarn global add zos
中身の解説
contractディレクトリの中には SampleToken と SampleToken_v1 というコントラクトが入っています。それぞれのコードは以下のようになっており、差分はversion情報を返す関数の返却値が違うのみです。(SampleTokenはv0を。SampleToken_v1はv1を返します)
SampleToken
中身について軽く触れておくと、zos-libとopenzeppelin-ethというライブラリを使用しています。zos-libはアップグレード可能なコントラクトを記述するためのライブラリで、constructorの代わりにinitialize関数を実装します。
openzeppelin-ethはセキュアなスマートコントラクトを実装するためのライブラリです。 openzeppelin-solidityとの違いにちょっと混乱しましたが、リポジトリのREADMEを見ると、openzeppelin-solidityとの大きな違いはopenzeppelin-ethはアップグレードする可能性のあるコントラクトになっているようです。ZeppelinOSを利用するならopenzeppelin-ethを利用したほうがよいでしょう。
The main difference is that all contracts in this package are potentially upgradeable
:
All in all, you should use this package instead of openzeppelin-solidity if you are managing your project via ZeppelinOS.
pragma solidity ^0.5.0;
import 'zos-lib/contracts/Initializable.sol';
import 'openzeppelin-eth/contracts/token/ERC721/ERC721Full.sol';
import 'openzeppelin-eth/contracts/token/ERC721/ERC721Mintable.sol';
contract SampleToken is Initializable, ERC721Full, ERC721Mintable {
function initialize(string memory name, string memory symbol) public initializer {
ERC721.initialize();
ERC721Enumerable.initialize();
ERC721Metadata.initialize(name, symbol);
ERC721Mintable.initialize(msg.sender);
}
function version() public view returns (string memory) {
// set version
string memory ver = "v0";
return ver;
}
}
SampleToken_v1
pragma solidity ^0.5.0;
import "zos-lib/contracts/Initializable.sol";
import 'openzeppelin-eth/contracts/token/ERC721/ERC721Full.sol';
import 'openzeppelin-eth/contracts/token/ERC721/ERC721Mintable.sol';
contract SampleToken_v1 is Initializable, ERC721Full, ERC721Mintable {
function initialize(string memory name, string memory symbol) public initializer {
ERC721.initialize();
ERC721Enumerable.initialize();
ERC721Metadata.initialize(name, symbol);
ERC721Mintable.initialize(msg.sender);
}
function version() public view returns (string memory) {
// set version
string memory ver = "v1"; <=== ここが違う
return ver;
}
}
zosコマンドでデプロイ
では実際にこのコントラクトをデプロイして動作を確認していきます。
npmインストール後、下記のコマンドでZeppelinOSプロジェクトを初期化します。
プロジェクト名は好きに指定してください。
$ zos init {プロジェクト名}
初期化が終わるとプロジェクトルート直下にzos.jsonが生成されます。zos.jsonはZeppelinOSプロジェクトのコントラクト情報やバージョン情報などが記載されています。
続けて下記コマンドでZeppelinOSプロジェクトにSampleTokenコントラクトを追加します。
$ zos add SampleToken
次にSampleTokenをデプロイするのですが、デプロイ先として今回はローカルでプライベートチェーンを立てておきます。ganache-cliを利用して、7545番ポートで立ち上げます。
$ ganache-cli -p 7545 -d
準備が整ったので、下記コマンドでデプロイします。networkにはlocalを指定します。これはtruffle-config.jsに定義されている情報を参照します。
$ zos push --network local
成功するとzos.dev-{ネットワークID}.jsonファイルが生成されます。中身をみると、SampleTokenコントラクトにaddressが振られており、無事デプロイされていることが分かります。(この時点ではproxiesフィールドの中身は空です)
続いてこのコントラクトのProxyContractをデプロイしていきます。ProxyContractをデプロイするには以下のコマンドで行います。引数にnameとsymbolを渡しています。
$ zos create SampleToken --init initialize --args="SampleToken,STKN" --network local
無事にデプロイされると、zos.dev-{ネットワークID}.jsonのproxiesフィールドにProxyContractのaddressが入っているはずです。また、implementationがSampleTokenのaddressを指しています。
これでProxyContractを通してSampleTokenにアクセスできると思います。実際に試してみます。
$ truffle console --network local
truffle(local)> token = await SampleToken.at(<proxy-address>)
truffle(local)> token.version()
'v0'
version関数でv0が返ってきたのでうまくいっているようです。
ではSampleTokenをアップグレードします。SampleToken_v1がアップグレード版となっており、これを同様にデプロイします。(SampleToken_v1をSampleTokenのエイリアスとして追加し、デプロイしています。)
$ zos add SampleToken_v1:SampleToken
$ zos push --network local
ProxyContractはすでにデプロイされているのでcreateではなく、updateします。
$ zos update SampleToken --network local
これでアップグレード作業は完了です。
(zos.dev-{ネットワークID}.jsonのproxiesフィールドにあるimplementationのアドレスがSampleToken_v1のaddressを指すように変更された)
ではもう一度確認してみます。
$ truffle console --network local
truffle(local)> token = await SampleToken.at(<proxy-address>)
truffle(local)> token.version()
'v1'
v1が返ってきたので、無事アップグレードされていました。
Truffleでデプロイ
上記はzosコマンドでデプロイしていきましたが、サンプルにはmigrationファイルが用意されており、truffle migrate でも確認できるようになっていますので、試してみてください。
$ zos init {プロジェクト名}
$ truffle migrate --network local
$ truffle console --network local
truffle(local)> token = await SampleToken.at(<proxy-address>)
truffle(local)> token.version()
'v1'
参考
https://blog.zeppelinos.org/proxy-patterns/
https://zoom-blc.com/how-to-develop-upgradable-contracts