【Ethereum】【Solidity】low-level callを使ってコントラクトの関数を実行する
Solidityには、アカウント(アドレス)に対して直接バイトコードを実行する為に、address型の変数に対してcall
という関数が用意されています。(low-level call
と呼ばれるようです。)
http://solidity.readthedocs.io/en/develop/types.html#members-of-addresses
これは、ConsenSysなどが公開しているMultiSigWalletで使われている仕組みです。
MultiSigWalletは、特定の処理を行う為のコントラクトのアドレスと実行する関数のバイトコードをTransactionとして事前に登録し、規定の人数の承認が得られたタイミングで対象コントラクトの関数を実行する、ということをこのlow-level callを使って実現しています。
具体的な構文としては、以下になります。
<アドレス>.call.(<バイトデータ>)
これを実際にサンプルのContractで動かしてみます。
呼び出し元
pragma solidity ^0.4.17; contract Caller { function callFunc(address destination, bytes data) public { if (!destination.value(data)) { revert(); } } }
- 引数destinationは関数を実行する対象のコントラクトのアドレスを指定します
- dataには関数の実行バイトデータを指定します
実行するターゲットのコントラクト
pragma solidity ^0.4.17; contract Test { uint public num; function setNum(uint n) public { num = n; } }
- Testコントラクトは
setNum
という関数を一つもち、変数numに引数の値をセットしています
テストコード
- 検証のテストコードは、truffleを使っています。
const Caller = artifacts.require('../contracts/Caller.sol'); const Test = artifacts.require('../contracts/Test.sol'); contract('LowLevelCall', (accounts) => { it('Low level call test.', async () => { caller = await Caller.new(); test = await Test.new(); // 実行前のnumの値 const before = await test.num(); console.log("before: " + before); // 関数のバイトコードを取り出す const data = test.contract.setNum.getData(10); console.log("bytecode: " + data); // LowLevelCall await caller.callFunc(test.address, 0, data); // 実行後のnumの値 const after = await test.num(); console.log("after: " + after); }); });
- コントラクトのオブジェクトを生成して
<コントラクトオブジェクト>.contract.<関数名>.getData(<引数>)
でバイトコードを取得します- ※ truffleの場合は、上記の方法で取得していますが、web3などで取得する場合は若干異なります
テスト実行
truffle(develop)> test Using network 'develop'. Contract: LowLevelCall before: 0 bytecode: 0xcd16ecbf000000000000000000000000000000000000000000000000000000000000000a after: 10 ✓ Low level call test. (202ms) 1 passing (214ms)
- Testコントラクトのnum変数が更新されています
同時にEthも送る
call関数は、バイトコードの実行と同時にETHも送ることが可能です。
MultiSigWalletは、その名のとおりWalletなのでバイトコードの実行というよりもこちらの使い方をするほうが多いかと思います。
<アドレス>.call.value(<送信するETH>)(<バイトデータ>)
呼び出し元コントラクトの修正
pragma solidity ^0.4.17; contract Caller { // 引数に送信するETHのvalueを追加 function callFunc(address destination, uint value, bytes data) public { if (!destination.call.value(value)(data)) { revert(); } } // ETHの送信をCaller自体が受け取れる用にpayableの関数を定義 function () payable public { } }
- 関数callFuncにETHの送信量を指定する変数
value
を追加します - コントラクトにETHを送る為のpayableの無名関数を追加します。これはvalue()を実行した時のETHの送り元がCallerコントラクト自身であるため、Callerへ事前にETHを送るためのものです
ターゲットコントラクトの修正
pragma solidity ^0.4.17; contract Test { uint public num; // ETHを受け取れる用にpayable修飾子を追加 function setNum(uint n) payable public { num = n; } }
- 実行される関数
setNum
にpayable修飾子を追加します
テストコード
const Caller = artifacts.require('../contracts/Caller.sol'); const Test = artifacts.require('../contracts/Test.sol'); contract('LowLevelCall', (accounts) => { it('Low level call test.', async () => { caller = await Caller.new(); test = await Test.new(); // 実行前 const beforeNum = await test.num(); const beforeBalance = web3.eth.getBalance(test.address); console.log("beforeNum: " + beforeNum); console.log("beforeBalance: " + beforeBalance); // 予めCallerコントラクトに20wei送っておく await caller.send(20); // 関数のバイトコードを取り出す const data = test.contract.setNum.getData(10); console.log("bytecode: " + data); // LowLevelCall ※10weiも一緒に送る await caller.callFunc(test.address, 20, data); // 実行後 const afterNum = await test.num(); const afterBalance = web3.eth.getBalance(test.address); console.log("afterNum: " + afterNum); console.log("afterBalance: " + afterBalance); }); });
- まずは、Callerコントラクトに予め送信するための20weiを送っています
- あとは、callFuncに送信する量(20wei)を追加しているだけです
テスト実行
truffle(develop)> test Compiling ./contracts/Migrations.sol... Compiling ./contracts/lowLevelCall/Caller.sol... Compiling ./contracts/lowLevelCall/Test.sol... Contract: LowLevelCall beforeNum: 0 beforeBalance: 0 bytecode: 0xcd16ecbf000000000000000000000000000000000000000000000000000000000000000a afterNum: 10 afterBalance: 20 ✓ Low level call test. (455ms) 1 passing (466ms)
- コントラクトTestに20weiが送信されていることが確認できました