Smart ContractをUpgradableにする方法の1つであるProxy Patterについて、日本語でまとめた記事が見つからなくて皆さんさぞお困りだろうと思い書くことにしました。
zeppelin OSのこちらの記事を翻訳して少し噛み砕いた感じです。
Proxy Patternとは
ロジックを定義するコントラクトと、ストレージとして利用するコントラクトを分け、Proxy Contractからロジックコントラクトを呼び出すことで、コントラクトをアップデート可能なものにしよう、というものです。
ここまでは簡単ですが実際にコードまで日本語で解説したものがなかったので、以下サンプルコードを例に解説します。
Delegatecall
smart contractで、contractから別のcontractの関数を呼び出したい場合に、delegatecallという関数を利用します。
delegatecallを理解するにあたり重要な点が2つ
- contractがサポートしていない関数を読んだ時に、fallback関数が呼び出される。コントラクト内ではそのfallback関数をカスタマイズすることができる。
- contract Aがcontract Bの関数をdelegatecallで呼び出す場合、contractBの関数はcontractAの関数のコンテキストで呼び出される。(msg.sender,msg.value等の値は保持され、storageはcontractAのものが書き換えられる)
コンテキストとmsg.value,msg.senderの扱いはdelegatecall, callcode, callで異なりますが、それはこちら参照で。
Proxy Contract
Proxy Patternにはストレージの作り方にいくつか手法がありますが、今回はどのパターンにも共通して必要になるProxy Contractについて説明します。
まずコードは以下
contract Proxy {
function implementation() public view returns(address);
function () payable public {
address _impl = implementation();
require(_impl != address(0));
assembly {
let ptr := mload(0x40)
calldatacopy(ptr, 0, calldatasize)
let result := delegatecall(gas, _impl, ptr, calldatasize, 0, 0)
let size := returndatasize
returndatacopy(ptr, 0, size)
switch result
case 0 { revert(ptr, size) }
default { return(ptr, size) }
}
}
}
コントラクトからコントラクトを呼び出すためには、msg.dataにコントラクト呼び出しに必要なデータを渡す必要があります。
msg.dataとは?ということですが、msg.dataはコントラクト呼び出しに必要なデータです。これは後述するcalldataで取得することができます。
こちらの質問でありますが、
function getMsgData(
address _address,
bytes _bytes,
uint _int,
uint[] _array,
string _string
)
external
returns (bytes)
{
return msg.data;
}
上記の関数を次の引数で呼び出したとします。
contract.getMsgData(
someAddress,
web3.toHex('my bytes'),
12,
[1, 4, 412],
'thisislargerthanthirtytwobytesstring'
);
その時のmsg.dataが以下のようになります。
0x
d1621754 // (1) methodId
000000000000000000000000c6e012db5298275a4c11b3e07d2caba88473fce1 // (2) "_address"
00000000000000000000000000000000000000000000000000000000000000a0 // (3) location of start of "_bytes" data (item 7) = 160 bytes
000000000000000000000000000000000000000000000000000000000000000c // (4) "_val" = 12
00000000000000000000000000000000000000000000000000000000000000e0 // (5) location of start of "_array" data (item 9) = 224 bytes
0000000000000000000000000000000000000000000000000000000000000160 // (6) location of start of "_string" data (item 13) = 352 bytes
0000000000000000000000000000000000000000000000000000000000000008 // (7) size of "_bytes" data in bytes (32 bytes)
6d79206279746573000000000000000000000000000000000000000000000000 // (8) "_bytes" data padded to 32 bytes
0000000000000000000000000000000000000000000000000000000000000003 // (9) length of "_array" data = 3
0000000000000000000000000000000000000000000000000000000000000001 // (10) _array[0] value = 1
0000000000000000000000000000000000000000000000000000000000000004 // (11) _array[2] value = 4
000000000000000000000000000000000000000000000000000000000000019c // (12) _array[3] value = 412
0000000000000000000000000000000000000000000000000000000000000024 // (13) size of "_string" data in bytes (64 bytes)
7468697369736c61726765727468616e74686972747974776f6279746573737472696e670..0 // (14) "_string" data padded to 64 bytes
これは gethのeth_sendRawTransactionで渡すパラメータとも同じ形式ですね。(この辺りはこちらも参照)
つまり関数名と引数の値を指定するbyte型のRow dataです。コントラクトからコントラクトを呼び出すときはProxy Contractを通してロジックが定義してあるコントラクトにこのデータを渡す必要があるわけです。考えてみたら当たり前ですね。
次にProxy Contratの中身について解説します。
function implementation() public view returns(address);
上の関数では、ProxyContractから呼び出すコントラクトのaddressを入れておきます。Proxy Contractではimplementation()のinterfaceのみ定義してありますので、実際の実装ではProxy Contractを継承した関数にimplementation()の実体を定義します。
次に、calldatacopy(t,f,s)では、3つの引数を渡します。calldataのfの位置からサイズsのデータをmemory tの位置にコピーします。0x40というには、solidityのfree memory pointerという領域が0x40 ~ 0x5fまでと決まっていて、ここは初め何もデータが割り当てられていません。
ここではmsg.dataを全てコピーして、free memory pointer領域に格納しています。
let ptr := mload(0x40)
calldatacopy(ptr, 0, calldatasize)
いよいよ、delegatecallです。delgatecallには
- gas
- コントラクトのaddress
- コントラクトに渡すdata
- datasize
- 返り値のdata
- 返り値のdatasize
を渡します。
gasは、この段階でまだ残っているgasの値を取得します。ここでは残りのgas全てをdelegatecallに渡しています。
返り値の値は呼び出す関数がわかっていないので両方とも0としておきます。これはそれぞれ後述のreturndata, returndatasizeで取得できます。
またdelegatecallは成功時に1,失敗時に0を返しますので、resultには0 or 1が返ってきます。
let result := delegatecall(gas, _impl, ptr, calldatasize, 0, 0)
次に、returndatacopyで、delegatecallで返ってきた値をmemory領域にコピーします。
returndatacopy(ptr, 0, size)
最後にresultの値が0(失敗)の時にはrevert、それ以外の時にはreturnデータとそのサイズを返しています。
switch result
case 0 { revert(ptr, size) }
default { return(ptr, size) }
今回はここまで。次回はStorageとの連携について書きます。何か変なこと言ってたらご指摘お願いします。