🍇

[Solidity]継承の順番について検証してみた

2022/11/13に公開

複数のコントラクトを継承する際に、どの順番で書くべきなのか疑問に思ったので検証してみました。
複数パターンを試してみましたが、「この順番も追加して欲しい」ということであれば検証するので、リクエスト頂けると嬉しいです。

※最初に継承について簡単な説明があり、その後に大きく分けて2パターンの検証とその結果を記述しています。

継承とは

継承とは、他のコントラクトの機能を引き継ぐことができる機能です。
別のコントラクトに記述している状態変数や関数、modifierなどを継承先のコントラクトで使用することができるようになります。

基本的な書き方

継承はisキーワードを使用して記述します。
以下の例では、BコントラクトはAコントラクトを継承しています。

contract A {
    ...
}

contract B is A {
    ...
}

★パターン1


BがAを継承して、CがBを継承するという単純パターンです。
単純ではありますが、間のコントラクトBでoverrideする場合、しない場合の2パターンを見ていきましょう。

✅ BでAの関数をoverrideする場合

Bコントラクトでは、Aコントラクトのfoo関数をoverrideしています。

contract A {
    function foo() public pure virtual returns (string memory) {
        return "A";
    }
}

contract B is A {
    function foo() public pure virtual override returns (string memory) {
        return "B";
    }
}

contract C is B {
    function foo() public pure override returns (string memory) {
        return super.foo(); // "B"が返される
    }
}

Cコントラクトをデプロイしfoo関数を実行すると"B"が返ってくるので、親コントラクトBのfooが実行されていることが確認できました。

✅ BでAの関数をoverride"しない"場合

Bコントラクトでoverrideしない場合の動作を確認してみます。

contract A {
    function foo() public pure virtual returns (string memory) {
        return "A";
    }
}

contract B is A {
    // overrideしない
}

contract C is B {
    function foo() public pure override returns (string memory) {
        return super.foo(); // "A"が返される
    }
}

今度はBでoverrideしてないので、もう1つ親のAが返されました。

★パターン2


Aを継承したB,CをDで継承した場合、BとCのどちらが優先されるのか、検証してみました。
結論、一番右に書いたコントラクトから探索されます。

D is B,C

この書き方では「Cの関数が優先」してコールされます。

contract A {
    function foo() public pure virtual returns (string memory) {
        return "A";
    }
}

contract B is A {
    function foo() public pure virtual override returns (string memory) {
        return "B";
    }
}

contract C is A {
    function foo() public pure virtual override returns (string memory) {
        return "C";
    }
}

contract D is B, C {
    function foo() public pure override(B, C) returns (string memory) {
        return super.foo(); // "C"が返される
    }
}

Dコントラクトをデプロイしてfoo関数を実行するとCが返されました。
期待通り、一番右側に書いたコントラクトCの関数がコールされることを確認できたかと思います。

※ちなみにoverride(B, C)override(C, B)と書いても同じく"C"が返されました。

D is C,B も確認してみる

念の為、BとCを入れ替えても期待した値が返されるか検証してみましょう。

contract A {
    function foo() public pure virtual returns (string memory) {
        return "A";
    }
}

contract B is A {
    function foo() public pure virtual override returns (string memory) {
        return "B";
    }
}

contract C is A {
    function foo() public pure virtual override returns (string memory) {
        return "C";
    }
}

contract D is C, B {
    function foo() public pure override(C, B) returns (string memory) {
        return super.foo(); // "B"が返される
    }
}

期待通りBが返されました。
これで、一番右(最後)に記述したコントラクトから探索されることが検証できました。

D is C,B かつ、Bでoverrideしていない場合

1つ上のケースでは一番右のコントラクトBの関数がコールされました。
では、Bでfoo関数をoverrideしていない場合はどうなるのか?挙動を確認してみましょう。

結論、Cの関数がコールされます。

contract A {
    function foo() public pure virtual returns (string memory) {
        return "A";
    }
}

contract B is A {
    // overrideしない
}

contract C is A {
    function foo() public pure virtual override returns (string memory) {
        return "C";
    }
}

contract D is C, B {
    function foo() public pure override(A, C) returns (string memory) {
        return super.foo(); // "C"が返される
    }
}

今回はBコントラクトでは何もoverrideしていないため、次に優先されるCコントラクトのfoo関数がコールされました。

※ちなみに、override(A, C)と書いてある箇所をoverride(C)などとすると以下のようなエラーが出ます。
これまではBコントラクトをoverrideに含めていたので問題ありませんでしたが、Bコントラクトでoverrideしなくなったため、さらに親のAコントラクトをoverrideに含める必要があると注意されてます。

from solidity:
TypeError: Function needs to specify overridden contract "A".
  --> contracts/Zenn.sol:21:32:
   |
21 |     function foo() public pure override(C) returns (string memory) {
   |                                ^^^^^^^^^^^
Note: This contract: 
 --> contracts/Zenn.sol:4:1:
  |
4 | contract A {
  | ^ (Relevant source part starts here and spans across multiple lines).

D is C,B かつ、BでもCでもoverrideしていない場合

結論、Aの関数がコールされます。

contract A {
    function foo() public pure virtual returns (string memory) {
        return "A";
    }
}

contract B is A {
    // overrideしない
}

contract C is A {
    // overrideしない
}

contract D is C, B {
    function foo() public pure override returns (string memory) {
        return super.foo(); // "A"が返される
    }
}

まとめ

  • 一番右(最後)に記述したコントラクトから優先して探索される
  • overrideされていない場合は、さらに次のコントラクトが探索される

今回は私が検証したことをまとめてみましたが、補足や追加などあればコメント頂けると嬉しいです。
スマートコントラクトは1度デプロイしてしまうと変更ができないので、細かい挙動も正確に理解しておくことが大切だと改めて感じました。

Discussion