It’s now or never

IT系の技術ブログです。気になったこと、勉強したことを備忘録的にまとめて行きます。

【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>)(<バイトデータ>)
  • 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が送信されていることが確認できました