0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

delegatecallの多段呼び出し

Last updated at Posted at 2024-09-10

はじめに

EthereumなどのEVM系のスマートコントラクトをSolidityで実装する場合、スマートコントラクトから別のスマートコントラクトの関数を呼び出す方法は複数あります。その中でも、ライブラリの呼び出しや、Proxyパターンなどで使われているdelegatecall関数による呼び出し方法について、その動作を検証してみました。
私が気になっていたのは、delegatecallで呼び出された関数からさらにdelegatecallで関数を呼び出す(delegatecallを多段呼び出しをする)とどうなるか? で、delegatecallの仕様であるEIP-7: DELEGATECALLを見ても、この辺りの動作が明記されてなかったので、実際にコードを書いて試してみました。

さまざまな関数の呼び出し方

通常は、呼び出し元のコントラクトから、呼び出し先のコントラクトの関数を呼び出す際は、

//直接呼び出し
con.func();

のように直接呼び出していると思いますが、この他に、

// call関数による呼び出し
address(con).call(abi.encodeWithSignature("func()"));

// callcode関数による呼び出し
address(con).callcode(abi.encodeWithSignature("func()"));

// delegatecall関数による呼び出し
address(con).delegatecall(abi.encodeWithSignature("func()"));

などの方法があるのはご存じでしょうか。
それぞれの呼び出し方で、動作の違いは色々あるのですが、大きく次の2点が注目すべき違いとなります。

  • ストレージ領域(スマートコントラクトの呼び出し元/先のどちらを使うのか?)
  • msg.sender(スマートコントラクトの呼び出し元のアドレスは何になるのか?)

それぞれの違いについて簡単に表にまとめると下記のようになります。

呼び出し方 ストレージ領域 msg.sender
直接(=callと同じ) 呼び出し先 呼び出し元
call 呼び出し先 呼び出し元
callcode 呼び出し元 呼び出し元
delegatecall 呼び出し元 呼び出し元を呼び出したアドレス(EOAなど)

今回注目している delegatecall は、ストレージ領域は「呼び出し元」でmsg.senderは「呼び出し元を呼び出したアドレス(EOAなど)」となります。
では、「呼び出し元」から「呼び出し先」をdelegatecallで呼び出した先で、さらにdelegatecallで「呼び出し先の呼び出し先」のコントラクトの関数を呼び出した場合、一体、ストレージ領域はどのコントラクトのものが使われ、msg.senderのアドレスはどこになるのでしょうか。
以下、この件について実際にコードを書いて、動かして試した結果をご紹介していきます。

まずは呼び出し元/呼び出し先だけでの動作確認

まずは呼び出し元と呼び出し先のスマートコントラクト間のみでの動作確認を行ってみます。
下記の実装コードでは、

  • 呼び出し元(First
  • 呼び出し先(Second

というスマートコントラクトを実装していて、Firstコントラクトの callTest()関数を呼び出すことで、Firstコントラクトから、SecondコントラクトのsetValue()関数を呼び出すテストを行っています。
Firstコントラクトの callTest()は、SecondコントラクトのsetValue()関数をdelegatecall関数ではなく、「直接呼び出し」で呼び出している実装となります。

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.24;

import "hardhat/console.sol";

contract Second {
  uint public value = 0;

  function printValue() public view {
    console.log("Second address: %s", address(this));
    console.log("Second value: %s", value);
  }
    
  function setValue() public {
    console.log("Second caller: %s", msg.sender);
    value = 1;
  }
}

contract First {
  uint public value = 0;

  function printValue() public view {
    console.log("First address: %s", address(this));
    console.log("First value: %s", value);
  }

  function callTest() public {
    console.log("First caller: %s", msg.sender);
    Second second = new Second();
    second.setValue();
    second.printValue();
    printValue();
  }
  
  function delegateCallTest() public {
    console.log("First caller: %s", msg.sender);
    Second second = new Second();
    address(second).delegatecall(abi.encodeWithSignature("setValue()"));
    second.printValue();
    printValue();
  }
}

実行結果は、下記のとおりとなります。

  console.log:
    First caller: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
    Second caller: 0x5fbdb2315678afecb367f032d93f642f64180aa3
    Second address: 0xa16e02e87b7454126e5e10d957a927a7f5b5d2be
    Second value: 1
    First address: 0x5fbdb2315678afecb367f032d93f642f64180aa3
    First value: 0

First callerは、Firstコントラクトを呼び出したEOAである「0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266」となっています。
Second callerは、Secondコントラクトを呼び出したFirstコントラクトのアドレス(First addressの「0x5fbdb2315678afecb367f032d93f642f64180aa3」)になっており、このことから、スマートコントラクトの直接呼び出しの場合は、msg.senderのアドレスが呼び出し元のスマートコントラクトのアドレスになっていることが分かると思います。
また、Second valueが「1」、First valueが「0」であることから、使用されたストレージ領域は呼び出し先であるSecondコントラクトであることが分かります。
これらの結果は、先の表で示したものと同じになっています。

では次に、今度はFirstコントラクトの delegateCallTest()関数の方を呼び出すことで、SecondコントラクトのsetValue()関数をdelegatecall関数で呼び出してみます。
実行結果は下記のとおりとなります。

  console.log:
    First caller: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
    Second caller: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
    Second address: 0xa16e02e87b7454126e5e10d957a927a7f5b5d2be
    Second value: 0
    First address: 0x5fbdb2315678afecb367f032d93f642f64180aa3
    First value: 1

First callerは、先と同じでFirstコントラクトを呼び出したEOAである「0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266」となっています。
しかし、Second callerは、Secondコントラクトを呼び出したFirstコントラクトのアドレス(First address)ではなく、First callerと同じEOAになっており、このことから、スマートコントラクトのdelegatecall関数での呼び出しの場合は、msg.senderのアドレスが呼び出し元を呼び出したアドレス(今回の場合EOA)になっていることが分かると思います。
また、今回はSecond valueが「0」、First valueが「1」であることから、使用されたストレージ領域は呼び出し元であるFirstコントラクトのものであることが分かります。
これらの結果は、先の表で示したものと同じになっています。

呼び出し元/呼び出し先/呼び出し先の先での動作確認

呼び出し元/呼び出し先でのdelegatecallの動作確認はできましたので、さらにその先の関数をdelegatecallで呼び出した場合の動作確認を行っていきます。
下記の実装コードでは、

  • 呼び出し元(First
  • 呼び出し先(Second
  • 呼び出し先の先(Third

というスマートコントラクトを実装していて、Firstコントラクトの delegateCall2Test()関数を呼び出すことで、Firstコントラクトから、SecondコントラクトのsetValue2()関数を呼び出し、さらにsetValue2()関数からThirdコントラクトのsetValue3()関数を呼び出すテストを行っています。
Firstコントラクトの delegateCall2Test()SecondコントラクトのsetValue2()関数をdelegatecall関数で呼び出し、さらにsetValue2()関数はsetValue3()関数をdelegatecall関数で呼び出している実装となります。

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.24;

import "hardhat/console.sol";

contract Third {
  uint public value = 0;

  function printValue() public view {
    console.log("Third address: %s", address(this));
    console.log("Third value: %s", value);
  }

  function setValue3() public {
    console.log("Third caller: %s", msg.sender);
    value = 1;
  }
}

contract Second {
  uint public value = 0;

  function printValue() public view {
    console.log("Second address: %s", address(this));
    console.log("Second value: %s", value);
  }

  function setValue2() public {
    console.log("Second caller: %s", msg.sender);
    Third third = new Third();
    address(third).delegatecall(abi.encodeWithSignature("setValue3()"));
    third.printValue();
  }
}

contract First {
  uint public value = 0;

  function printValue() public view {
    console.log("First address: %s", address(this));
    console.log("First value: %s", value);
  }

  function delegateCall2Test() public {
    console.log("First caller: %s", msg.sender);
    Second second = new Second();
    address(second).delegatecall(abi.encodeWithSignature("setValue2()"));
    second.printValue();
    printValue();
  }
}

実行結果は、下記のとおりとなります。

  console.log:
    First caller: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
    Second caller: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
    Third caller: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
    Third address: 0xb7a5bd0345ef1cc5e66bf61bdec17d2461fbd968
    Third value: 0
    Second address: 0xa16e02e87b7454126e5e10d957a927a7f5b5d2be
    Second value: 0
    First address: 0x5fbdb2315678afecb367f032d93f642f64180aa3
    First value: 1

First callerSecond callerは、先と同じでFirstコントラクトを呼び出したEOAである「0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266」となっています。さらに、Third callerも、同じEOAになっていることがこの結果から分かります。つまり、delegatecall関数を多段に経由した呼び出し方をしても、msg.senderはスマートコントラクトの大元の呼び出し元であるアドレス(今回の場合EOA)になることが動作確認により明らかになりました。
また、Third valueが「0」Second valueが「0」、First valueが「1」であることから、使用されたストレージ領域は大元の呼び出し元であるFirstコントラクトのものであることが分かります。つまり、呼び出し先の先であるThirdコントラクトは、直接の呼び出し元であるSecondコントラクトのストレージ領域ではなく、その大元の呼び出し元であるFirstコントラクトのストレージ領域を使うことが、今回の動作確認により明らかになりました。

まとめ

delegatecall が使用するストレージ領域は、呼び出し先のスマートコントラクトのものではなく、呼び出し元のスマートコントラクトのものになりますが、その「呼び出し元」とは、delegatecall関数を直前に呼び出したスマートコントラクトではなく、delegatecall関数を最初に呼び出した(=呼び出しの大元の)スマートコントラクトであることが、これではっきりしました。
Upgradableなスマートコントラクトを実装する際によく使われるProxyパターンではdelegatecallが使われていることが多いですが、そのProxyパターンを使用している際に、さらに自分で実装したスマートコントラクト内でdelegatecallを使って処理を行いたい場合に、使われるストレージ領域がどこのスマートコントラクトのものになるのかが気になり、今回、動作確認してみました。
この検証結果が、みなさんの知識の一助となれば幸いです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?