Solidityでdelegatecallのサンプルを試していた時に、通常の呼び出しだとuintやstringなどのいわゆる可変長な型を引数にとるメソッドのdelegatecallが思った通りに動いてくれなくて、その原因や実装方法について調べました。
具体的には以下のstackexchangeの記事のような現象です.
ethereum.stackexchange.com
ethereum.stackexchange.com
メソッドに可変長引数を含める場合は次のようなデータフォーマットになるみたいです。
以下はfunc(uint256, uint256)に対してfunc(3, [2,4])と呼び出した場合のtransaction inputdataです。
0x618affd6 // メソッドsignature 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000003 // 第1引数の値 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000040 // 第2引数の実データのあるaddress 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000002 // 第2引数のarray.length 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000002 // 第2引数の1つ目のデータ 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000004 // 第2引数の2つ目のデータ
inputdataをみやすいように32byteずつに区切ってみました。(最初の4byteだけmethod signatureという特殊データなので4byteで区切ってます。)
ざっくりとした自分の解釈だと、引数に可変長のデータ型が含まれる場合、メソッド引数のデータフォーマット次のような感じかなと思います。
{header part(可変長型の場合は実データのアドレスそれ以外の型は実データ), {array length, [array data1, array data2....array dataN]}, {array length, [array data1, array data2....array dataN]} .... }
で、この辺の仕様についてどこかにまとめられていないか探したところありました。
メソッド引数のデータフォーマットの仕様はABI仕様に含まれるみたいですね。Ethereum公式のwikiに載ってました。灯台下暗しでした。
github.com
んで、Solidity 0.4.18現在ではcallとdelegatecallに渡した引数についてはこのABI encode に沿った変換はしていなくて、ユーザが自分でフォーマット似合わせたデータを渡す必要があるみたいです。
自前でheader partを補完してdelegatecallを呼び出すコードは次の通りになります。
pragma solidity ^0.4.18; contract Callee { uint public result_; bytes public _calleedata; bytes public _callerdata; address constant public callee_ = 0x00; function foo (uint _offset, uint[] _nums) public returns (uint) { _calleedata = msg.data; result_ = _offset; for ( uint i = 0; i < _nums.length; ++i) result_ += _nums[i]; return result_; } } contract Caller { uint public result_; bytes public _calleedata; bytes public _callerdata; address constant public callee_ = <デプロイしたCalleeのcontract address>; function Call (uint _offset, uint[] _nums) public returns (bool) { _callerdata = msg.data; uint data_address = 2 * 32; // header partが2つなので return callee_.delegatecall(bytes4(sha3("foo(uint256,uint256[])")), _offset, data_address, _nums.length, _nums); } }
Call(3, [2,4])で呼び出した場合のそれぞれのメソッド呼び出しの時のinputdataが次の通り一致します。
collerdata
0x618affd600000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004
colleedata
0x2fbe983a00000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004
可変長引数を複数とる場合はarray address の計算がちょっとだけめんどくさいですけど。。。
ちなみに、libraryを使えばこの辺のことをコンパイラーがうまいことやってくれるのかなーと思って試してみたのですが、どうやら0.4.18現在structsを含めたコンパイルにバグがあるようで、状態をlibraryに渡すのができなくなってるみたいでした。なので現状は可変長引数を持つlibraryを呼び出す場合は上記のように自分でheader partを補完してdelegatecallを呼び出すしかないみたいです。
(追記):Solidityに載ってあるLibrariesのサンプルコードをで試したところ正常に動きました。libraryに状態を渡せないのは勘違いでした。
call, delegatecallについてはまだ議論中みたいですね。次のIssueが見つかりました。
Incorrect encoding of dynamic arrays in address.call · Issue #2269 · ethereum/solidity · GitHub
#ABI encode仕様にあわせてcallの引数を勝手にencodeされると、ユーザが引数の構造を制御できなくなるからどうしようか?みたいな感じなのかな。