アップグレード可能なスマートコントラクトを簡単に作りたい!
Ethereumでデプロイしたコントラクトはアップグレードすることができないため、バグや機能拡張があった際にどうすることもできません、、、
ですが、Proxyコントラクト(Proxyパターン)を利用することで、アップグレード可能なコントラクトを作成することが可能です!
具体的には、Proxy用コントラクトと処理コントラクトを分けて作成することで、
Proxy用コントラクトからdelegatecallで処理用コントラクトを外部呼出しし、データ保持用と処理用を切り分け、更新したい場合は処理用コントラクトを更新、Proxyコントラクトが外部呼出しを行う際の参照先を変更することで、アップグレード可能なスマートコントラクトを作成できるようになるみたいです。
詳しくは、以下のサイトでまとめてくださっているのでご参照ください。
アップグレード可能なスマートコントラクトを実現する具体的なアプローチ。
上記方法をとることで、自前でアップグレード可能なコントラクトを作成することができると思います。
ですが、コントラクトのバージョン管理やバグなくアップグレードできるように作成するのが手間かつ不安です、、、
そんな中見つけたのがZeppelinOSです!(知らなかったですが、かなり前からあったみたいです...)
ZeppelinOS :https://docs.zeppelinos.org/docs/start.html
簡単に導入できそうなのでZeppelinOSを使ってスマートコントラクトをアップグレードさせてみました!
実施環境
Win10
Docker
やってみる
環境作成
とりあえずDockerでtruffleとzosをインストール
truffle:スマートコントラクト開発用フレームワーク
zos:プロキシコントラクト用のフレームワーク
nan:zosインストール中エラーが出たので追加でインストール
zos-lib:zosで利用するライブラリ
$ docker run -it --name zos-test node:10.15.3 //bin/bash
$ cd ~/
$ mkdir test
$ cd test
$ npm install -y truffle nan zos zos-lib
ZeppelinOSを初期化。
$ npx zos init
? Welcome to ZeppelinOS! Choose a name for your project test
? Initial project version
Project initialized. Write a new contract in the contracts folder and run 'zos create' to deploy it.
$ tree -L 1
.
|-- contracts
|-- networks.js
|-- node_modules
|-- package-lock.json
`-- zos.json
networks.jsとzos.jsonファイルが作成されています。
接続先とzosの管理ファイルのようです。
module.exports = {
networks: {
development: {
protocol: 'http',
host: 'localhost',
port: 8545,
gas: 5000000,
gasPrice: 5e9,
networkId: '*',
},
},
};
{
"zosversion": "2.2",
"contracts": {},
"dependencies": {},
"name": "test",
"version": "0.1.0"
}
truffleを使いたいのでtruffleを初期化。
$npx truffle init
$tree -L 1
.
|-- contracts
|-- migrations
|-- networks.js
|-- node_modules
|-- package-lock.json
|-- test
|-- truffle-config.js
`-- zos.json
truffleのファイルが作成されました。
ZeppelinOSで作成したnetworks.jsと、
truffleで作成truffle-config.jsで接続先設定が複数存在することになりますが、どちらが優先されるのでしょうか?
あとあとデプロイするので、その時確認していきます。
コントラクトを作る
コントラクトを作成します。
zos-libのInitializable.solを継承しinitializerを利用することで、デプロイ初回のみ稼働する処理を作成できるみたいです。
pragma solidity ^0.5.0;
import "zos-lib/contracts/Initializable.sol";
contract MyContract is Initializable {
uint256 public x;
function initialize(uint256 _x) initializer public {
x = _x;
}
function get() public view returns (uint256) {
return x;
}
function set(uint256 _x) public {
x = _x;
}
}
デプロイ
いよいよデプロイしていきます。
テストネットにはGanacheを利用します。
Ganacheを起動してワークスペース作成。
設定画面 > SERVER > HostNameでイーサネットを設定します。
truffle-config.jsに立ち上げたGanacheのRPC SERVER設定を追記します。
networks: {
development: {
host: 'XXX.XXX.XXX.XXX',
port: 7545,
network_id: '*'
}
}
念のため、truffleのコンソールを立ち上げてつながるか確認。
$ npx truffle console
truffle(development)>
undefined
truffle(development)> .exit
準備が完了したので、デプロイしてみます。
zosプロジェクトにコントラクトをaddします。
$ npx zos add MyContract
✓ Compiling contracts with Truffle, using settings from truffle.js file
Truffle output:
Compiling your contracts...
===========================
> Compiling ./contracts/MyContract.sol
> Compiling zos-lib/contracts/Initializable.sol
> Artifacts written to /share/test/build/contracts
> Compiled successfully using:
- solc: 0.5.8+commit.23d335f2.Emscripten.clang
✓ Added contract MyContract
addすることで、コントラクトのコンパイル & zos.jsonにaddしたコントラクト情報が追加されるみたいです。
{
"zosversion": "2.2",
"contracts": {
"MyContract": "MyContract"
},
"dependencies": {},
"name": "test",
"version": "0.1.0",
"compiler": {
"manager": "truffle",
"compilerSettings": {
"optimizer": {}
}
}
}
次にpushします。
$ npx zos push
✓ Compiling contracts with Truffle, using settings from truffle.js file
Truffle output:
Compiling your contracts...
===========================
> Compiling ./contracts/MyContract.sol
> Compiling zos-lib/contracts/Initializable.sol
> Artifacts written to /share/test/build/contracts
> Compiled successfully using:
- solc: 0.5.8+commit.23d335f2.Emscripten.clang
? Pick a network (Use arrow keys)
❯ development
Could not connect to the development Ethereum network on http://localhost:8545. Please check your networks.js configuration file. Error: Invalid JSON RPC response: "".
ネットワーク設定でdevelopmentを選択すると、localhost:8545でエラーになります。
truffle-config.jsの設定がnetworks.jsで上書きされているようにみえます。
truffle-config.jsのネットワーク名をdevelopment_trfに変更して再チャレンジ!
$ npx zos push
✓ Compiling contracts with Truffle, using settings from truffle.js file
Truffle output:
Compiling your contracts...
===========================
> Compiling ./contracts/MyContract.sol
> Compiling zos-lib/contracts/Initializable.sol
> Artifacts written to /share/test/build/contracts
> Compiled successfully using:
- solc: 0.5.8+commit.23d335f2.Emscripten.clang
? Pick a network
❯ development
上書きというか、参照されてないみたいです。
truffleのネットワーク設定を参照させたいので、networks.jsを削除して再チャレンジ。
$ npx zos push
✓ Compiling contracts with Truffle, using settings from truffle.js file
Truffle output:
Compiling your contracts...
===========================
> Compiling ./contracts/MyContract.sol
> Compiling zos-lib/contracts/Initializable.sol
> Artifacts written to /share/test/build/contracts
> Compiled successfully using:
- solc: 0.5.8+commit.23d335f2.Emscripten.clang
? Pick a network development_trf
✓ Contract MyContract deployed
All contracts have been deployed
上手くいきました!
development_trfを選択してデプロイ完了です。
デプロイすることでzosのデプロイ情報ファイルが作成されるみたいです。
{
"contracts": {
"MyContract": {
"address": "0x6684f937f2182801bcDAF4E87b58A75C35D399Be",
"constructorCode": "608060405234801561001057600080fd5b50610275806100206000396000f3fe",
"bodyBytecodeHash": "e569853a7786790e009487234e007962c5a833f976699426916e9e62977381f4",
"localBytecodeHash": "3b95e7a22280d4a0e2a6ed4a92d4bdfd244bdc0acd21db038f65bd0909cc3c2f",
"deployedBytecodeHash": "3b95e7a22280d4a0e2a6ed4a92d4bdfd244bdc0acd21db038f65bd0909cc3c2f",
"types": {
"t_bool": {
"id": "t_bool",
"kind": "elementary",
"label": "bool"
},
"t_uint256": {
"id": "t_uint256",
"kind": "elementary",
"label": "uint256"
},
===省略=============================================
},
"storage": [
{
"contract": "Initializable",
"path": "zos-lib/contracts/Initializable.sol",
"label": "initialized",
"astId": 41,
"type": "t_bool",
"src": "757:24:1"
},
{
"contract": "Initializable",
"path": "zos-lib/contracts/Initializable.sol",
"label": "initializing",
"astId": 43,
"type": "t_bool",
"src": "876:25:1"
},
===省略=============================================
],
"warnings": {
"hasConstructor": false,
"hasSelfDestruct": false,
"hasDelegateCall": false,
"hasInitialValuesInDeclarations": false,
"uninitializedBaseContracts": []
}
}
},
"solidityLibs": {},
"proxies": {},
"zosversion": "2.2",
"version": "0.1.0"
}
proxiesが空なので、普通にコントラクトがデプロイされただけみたいです。
Gnacheに登録されていると思うので動作を見てみます。
npx truffle console --network development_trf
truffle(development_trf)> myContract = await MyContract.at('0x6684f937f2182801bcDAF4E87b58A75C35D399Be')
truffle(development_trf)> myContract.get()
<BN: 0>
truffle(development_trf)> myContract.set(2)
{ tx:
'0xa21783ecb6fcc6d69d8707b293a394ec7c630ccb51df288becd8a282eccaa512',
receipt:
{ transactionHash:
'0xa21783ecb6fcc6d69d8707b293a394ec7c630ccb51df288becd8a282eccaa512',
transactionIndex: 0,
blockHash:
'0x16beb8af590b0e27c1e260432ee7ce7c16674cbcf5735cfe7f78ec68d24099c4',
blockNumber: 273,
from: '0xf500dfd70aaea58cc75ee657e7cbdf7282cc92fc',
to: '0x6684f937f2182801bcdaf4e87b58a75c35d399be',
gasUsed: 41706,
cumulativeGasUsed: 41706,
contractAddress: null,
logs: [],
status: true,
logsBloom:
'0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
v: '0x1b',
r:
'0x6c8e0f2ba433796334322f54faa462487ba2d91030b40b323c21ae96253d0890',
s:
'0x7b7c0269b7e61e1d077ec39237c2da249384fce03e199f7dc4b9ea66736725a0',
rawLogs: [] },
logs: [] }
truffle(development_trf)> myContract.get()
<BN: 2>
登録されているみたいです。
次にcreateを実行してみます。
これでプロキシコントラクトが作成され、先ほどのコントラクトが参照されるのだと思います。
$ npx zos create MyContract --init initialize --args 42
✓ Compiling contracts with Truffle, using settings from truffle.js file
Truffle output:
Compiling your contracts...
===========================
> Compiling ./contracts/MyContract.sol
> Compiling zos-lib/contracts/Initializable.sol
> Artifacts written to /share/test/build/contracts
> Compiled successfully using:
- solc: 0.5.8+commit.23d335f2.Emscripten.clang
? Pick a network development_trf
All contracts are up to date
✓ Setting everything up to create contract instances
✓ Instance created at 0x9dcee86c9F0B308cE3d580391174f406a9369563
0x9dcee86c9F0B308cE3d580391174f406a9369563
デプロイ完了です。
zos.dev-5777.jsonの差分を見てみます。(※差分以外は省略しています。)
"proxies": {
"test/MyContract": [
{
"address": "0x9dcee86c9F0B308cE3d580391174f406a9369563",
"version": "0.1.0",
"implementation": "0x6684f937f2182801bcDAF4E87b58A75C35D399Be",
"kind": "Upgradeable"
}
]
},
"zosversion": "2.2",
"version": "0.1.0",
"proxyAdmin": {
"address": "0x1374a26FF0cFCA967a3A9a8AC5EcB19520055De7"
}
proxies以降の部分に値が追加されました。
addressがプロキシのアドレス。
implementationが先ほどデプロイしたコントラクトのアドレスみたいです。
proxyAdminがプロキシアドレス管理用のアカウントみたいです。ZeppelinOS ProxyAdmin
それでは実際に動かしてみます。
まずは、pushの時にデプロイしたコントラクトから。
truffle(development_trf)> myContract = await MyContract.at('0x6684f937f2182801bcDAF4E87b58A75C35D399Be')
undefined
truffle(development_trf)> myContract.get()
<BN: 2>
当たり前ですが、先ほどコンソールでsetした値が保持され続けています。
次にプロキシコントラクトを動かしてみます。
truffle(development_trf)> myContract2 = await MyContract.at('0x9dcee86c9F0B308cE3d580391174f406a9369563')
undefined
truffle(development_trf)> myContract2.get()
<BN: 2a>
initialize時に設定した値が設定されています。
プロキシアドレスの値を変えてみます。※set時のトランザクション部分は省略しています。
truffle(development_trf)> myContract2.set(111)
truffle(development_trf)> myContract2.get()
<BN: 6f>
上手く更新できました。
あたりまえですが、pushでデプロイしたコントラクトには影響はないです。
truffle(development_trf)> myContract.get()
<BN: 2>
アップグレードしてみる
アップグレードしてみます。
アップグレード内容としては、Functionを増やしただけです。
pragma solidity ^0.5.0;
import "zos-lib/contracts/Initializable.sol";
contract MyContract is Initializable {
uint256 public x;
function initialize(uint256 _x) initializer public {
x = _x;
}
function get() public view returns (uint256) {
return x;
}
function set(uint256 _x) public {
x = _x;
}
function get2() public view returns (uint256) {
return x;
}
function set2(uint256 _x) public {
x = _x;
}
}
まずはpush。
npx zos push
✓ Compiling contracts with Truffle, using settings from truffle.js file
Truffle output:
Compiling your contracts...
===========================
> Compiling ./contracts/MyContract.sol
> Compiling zos-lib/contracts/Initializable.sol
> Artifacts written to /share/test/build/contracts
> Compiled successfully using:
- solc: 0.5.8+commit.23d335f2.Emscripten.clang
? Pick a network development_trf
✓ Contract MyContract deployed
All contracts have been deployed
pushしたので、zos.dev-5777.jsonを見てみます。
※★マークが差分箇所
{
"contracts": {
"MyContract": {
★ "address": "0xEEBE33b05E2a254Cc9dD5c3DD53Cd3B2723F5FbF",
★ "constructorCode": "608060405234801561001057600080fd5b506102eb806100206000396000f3fe",
★ "bodyBytecodeHash": "bfd43f1f360ee66693f641cd11005eafc65d5f06246f2de800069565d523728b",
★ "localBytecodeHash": "bce1409ee0ff584a19c39e94b736cbc6a0e8bae6f4f846bfe326e3a4b72a071b",
★ "deployedBytecodeHash": "bce1409ee0ff584a19c39e94b736cbc6a0e8bae6f4f846bfe326e3a4b72a071b",
"types": {
"t_bool": {
"id": "t_bool",
"kind": "elementary",
"label": "bool"
},
"t_uint256": {
"id": "t_uint256",
"kind": "elementary",
"label": "uint256"
},
"t_array:50<t_uint256>": {
"id": "t_array:50<t_uint256>",
"valueType": "t_uint256",
"length": "50",
"kind": "array",
"label": "uint256[50]"
}
},
"storage": [
{
"contract": "Initializable",
"path": "zos-lib/contracts/Initializable.sol",
"label": "initialized",
★ "astId": 59,
"type": "t_bool",
"src": "757:24:1"
},
{
"contract": "Initializable",
"path": "zos-lib/contracts/Initializable.sol",
"label": "initializing",
★ "astId": 61,
"type": "t_bool",
"src": "876:25:1"
},
{
"contract": "Initializable",
"path": "zos-lib/contracts/Initializable.sol",
"label": "______gap",
★ "astId": 117,
"type": "t_array:50<t_uint256>",
"src": "1951:29:1"
},
{
"contract": "MyContract",
"path": "contracts/MyContract.sol",
"label": "x",
"astId": 6,
"type": "t_uint256",
"src": "118:16:0"
}
],
"warnings": {
"hasConstructor": false,
"hasSelfDestruct": false,
"hasDelegateCall": false,
"hasInitialValuesInDeclarations": false,
"uninitializedBaseContracts": [],
★ "storageUncheckedVars": [],
★ "storageDiff": []
}
}
},
"solidityLibs": {},
"proxies": {
"test/MyContract": [
{
"address": "0x9dcee86c9F0B308cE3d580391174f406a9369563",
"version": "0.1.0",
"implementation": "0x6684f937f2182801bcDAF4E87b58A75C35D399Be",
"kind": "Upgradeable"
}
]
},
"zosversion": "2.2",
"version": "0.1.0",
"proxyAdmin": {
"address": "0x1374a26FF0cFCA967a3A9a8AC5EcB19520055De7"
}
}
新たにデプロイしたので、MyConstract部分が変更されました。
astId部分はStrageのIDっぽいですが、使われ方がよくわかりません。
次にupdateをしていきます。
$ npx zos update MyContract
? Pick a network development_trf
✓ Compiling contracts with Truffle, using settings from truffle.js file
Truffle output:
Compiling your contracts...
===========================
> Compiling ./contracts/MyContract.sol
> Compiling zos-lib/contracts/Initializable.sol
> Artifacts written to /share/test/build/contracts
> Compiled successfully using:
- solc: 0.5.8+commit.23d335f2.Emscripten.clang
All contracts are up to date
? Do you want to call a function on the instance after upgrading it? No
✓ Instance upgraded at 0x9dcee86c9F0B308cE3d580391174f406a9369563. Transaction receipt: 0x0076479db3f80f6ce8ea8fc683d475bfbfd3f4e6aa733ccd13635a582a8b0982
上手くアップデートできました。
アップデートの最後にアップデートしたインスタンスの動作確認もできるみたいです。(私はnoでしませんでした。)
zos.dev-5777.jsonの差分を見ていきます。
"implementation": "0xEEBE33b05E2a254Cc9dD5c3DD53Cd3B2723F5FbF",
一か所だけです。プロキシが呼び出すコントラクトがpushでデプロイしたコントラクトに変更されたみたいです。
では、実際に動作させていきます。
myContract1が初めにデプロイしたコントラクト
myContract2が今回update時にデプロイしたコントラクト
myContractproがプロキシコントラクト
です。
npx truffle console --network development_trf
truffle(development_trf)> myContract1 = await MyContract.at('0x6684f937f2182801bcDAF4E87b58A75C35D399Be')
undefined
truffle(development_trf)> myContract2 = await MyContract.at('0xEEBE33b05E2a254Cc9dD5c3DD53Cd3B2723F5FbF')
undefined
truffle(development_trf)> myContractpro = await MyContract.at('0x9dcee86c9F0B308cE3d580391174f406a9369563')
undefined
truffle(development_trf)> myContract1.get()
<BN: 2>
truffle(development_trf)> myContract2.get()
<BN: 0>
truffle(development_trf)> myContractpro.get()
<BN: 6f>
プロキシコントラクト(myContractpro)を見ると、アップデート前に更新された値が正しく返却されていることがわかります。
myContract2については、setしていないため初期値です。
次にFunctionを見ていきます。
truffle(development_trf)> myContract1.get2()
Error: Returned error: VM Exception while processing transaction: revert
at XMLHttpRequest._onHttpResponseEnd (/share/test/node_modules/truffle/build/webpack:/~/xhr2-cookies/dist/xml-http-request.js:318:1)
at XMLHttpRequest._setReadyState (/share/test/node_modules/truffle/build/webpack:/~/xhr2-cookies/dist/xml-http-request.js:208:1)
at XMLHttpRequestEventTarget.dispatchEvent (/share/test/node_modules/truffle/build/webpack:/~/xhr2-cookies/dist/xml-http-request-event-target.js:34:1)
at XMLHttpRequest.request.onreadystatechange (/share/test/node_modules/truffle/build/webpack:/~/web3-providers-http/src/index.js:96:1)
at /share/test/node_modules/truffle/build/webpack:/packages/truffle-provider/wrapper.js:112:1
at /share/test/node_modules/truffle/build/webpack:/~/web3-core-requestmanager/src/index.js:140:1
at Object.ErrorResponse (/share/test/node_modules/truffle/build/webpack:/~/web3-core-requestmanager/~/web3-core-helpers/src/errors.js:29:1)
truffle(development_trf)> myContract2.get2()
<BN: 0>
truffle(development_trf)> myContractpro.get2()
<BN: 6f>
truffle(development_trf)>
strageの値を保持したままFunctionが追加されていること(アップデートされていること)が確認できました!
トランザクション発行者を見てみる
外部コントラクトをCallするFunctionとCallされるコントラクトを作成して、発行者が誰か見てみます。
Proxyコントラクトがdelegatecallしてロジック呼び出してCallするので、発行者はProxyコントラクトアドレスになるはずです。
pragma solidity ^0.5.0;
import "zos-lib/contracts/Initializable.sol";
contract MyContract is Initializable {
==省略==================
function call2(address _address) public{
_address.call(abi.encodeWithSignature("setSender()"));
}
}
pragma solidity ^0.5.0;
import "zos-lib/contracts/Initializable.sol";
contract CallContract is Initializable {
address public msgSender;
function setSender() public {
msgSender = msg.sender;
}
function get() public view returns (address) {
return msgSender;
}
}
やってみます。確認に使うコントラクトはどちらもProxyコントラクトです。
$ npx truffle console --network development_trf
truffle(development_trf)> Caller = await MyContract.at('0x9dcee86c9F0B308cE3d580391174f406a9369563')
undefined
truffle(development_trf)> Call = await CallContract.at('0xdD4Bf0c508fD2EfF2336056dd7d596726b865b81')
undefined
truffle(development_trf)> Call.get()
'0xF500dFd70aaEa58CC75eE657e7cBDF7282Cc92fc'
truffle(development_trf)> Caller.call2('0xdD4Bf0c508fD2EfF2336056dd7d596726b865b81')
{ tx:
'0x431c2c0a612423be942362412ee6c4eb0bc5b958fef9945af744ac84174b4fb5',
receipt:
{ transactionHash:
'0x431c2c0a612423be942362412ee6c4eb0bc5b958fef9945af744ac84174b4fb5',
transactionIndex: 0,
blockHash:
'0x978cb062f1e3536683c3c90607a22fb2c88dfc097681fa96e18998f41e96b798',
blockNumber: 300,
from: '0xf500dfd70aaea58cc75ee657e7cbdf7282cc92fc',
to: '0x9dcee86c9f0b308ce3d580391174f406a9369563',
gasUsed: 32593,
cumulativeGasUsed: 32593,
contractAddress: null,
logs: [],
status: true,
logsBloom:
'0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
v: '0x1b',
r:
'0x3a3adeb65f9faf1eb6971b6d3ee66f76695cbf836bae2e60396cee5ddb30d521',
s:
'0x68f0d8ec3f794a9221154caf66b72c748f44f610b8cc7b07a1c4040ed0353ed2',
rawLogs: [] },
logs: [] }
truffle(development_trf)> Call.get()
'0x9dcee86c9F0B308cE3d580391174f406a9369563'
想定していた通り、発行者はプロキシコントラクトになっていました。
まとめ
アップグレード可能なZeppelinOSを試してみました。
導入自体は簡単にできましたが、
アップグレード前のコントラクトもブロックチェーン上では生き続けるので注意が必要だと感じました。