delegatecallの多段呼び出し

# はじめに

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

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

通常は、呼び出し元のコントラクトから、呼び出し先のコントラクトの関数を呼び出す際は、
```javascript
//直接呼び出し
con.func();
```
のように直接呼び出していると思いますが、この他に、
```javascript
// 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関数ではなく、「直接呼び出し」で呼び出している実装となります。

```javascript
// 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();
  }
}
```

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

```javascript
  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関数で呼び出してみます。
実行結果は下記のとおりとなります。

```javascript
  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関数で呼び出している実装となります。

```javascript
// 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();
  }
}
```
実行結果は、下記のとおりとなります。

```javascript
  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 caller`と`Second 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を使って処理を行いたい場合に、使われるストレージ領域がどこのスマートコントラクトのものになるのかが気になり、今回、動作確認してみました。
この検証結果が、みなさんの知識の一助となれば幸いです。