Solidityの多重継承やダイヤモンド継承を攻略する(Solidity0.8.0対応)


こんにちはヤマピーブラックです。
ブロックチェーンゲーム会社のCryptoGamesdouble jump.tokyoでコントラクト開発をしています。

今回はSolidityにおける多重継承、ダイヤモンド継承の書き方、挙動について解説します。

特にSolidity0.6.0以降、継承したfunctionにはoverrideを記載しなければならず、書き方もひとクセあります。そのあたりも含めて解説します。

多重継承における実行順

以下のような構成を考えます。

※console.logはhardhatを使用

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
import "hardhat/console.sol";

contract A {
    function foo() virtual public {
        console.log("foo A");
    }
}

contract B {
    function foo() virtual public {
        console.log("foo B");
    }
}

contract C is A, B {
    function foo() override(A,B) public {
        console.log("foo C");
    }
}

solidity0.6.0から継承元を列挙、override(A,B)と記載する必要があります。

実行結果

foo C

当然ですがsuperをつけない場合はCが実行されて終わりです。

superあり

superつけた場合はどうなるでしょう。

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
import "hardhat/console.sol";

contract A {
    function foo() virtual public {
        console.log("foo A");
    }
}

contract B {
    function foo() virtual public {
        console.log("foo B");
    }
}

contract C is A, B {
    function foo() override(A,B) public {
        console.log("foo C");
        super.foo();
    }
}

実行結果

foo C
foo B

contract C is A, Bの後に書いたほう、Bが後勝ちとなる形です。
contract C is B, Aと逆に書くと、Aが実行されます。(割愛します)

overrideを逆順に

ここで、contract C is A, Bのままoverride(B,A)とoverrideのところだけ逆に記載するとどうなるでしょうか。

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
import "hardhat/console.sol";

contract A {
    function foo() virtual public {
        console.log("foo A");
    }
}

contract B {
    function foo() virtual public {
        console.log("foo B");
    }
}

contract C is A, B {
    function foo() override(B,A) public {
        console.log("foo C");
        super.foo();
    }
}

実行結果

foo C
foo B

結果は変わりませんでした。つまり、contract C is A,Bの順番が重要で、overrideに記載する順番はどうでもよいということです。

まあ、functionの順番だけ器用に変更するのは無理でしょうかね。訳わかんなくなりそうですし。

ダイヤモンド継承における実行順

続いてダイヤモンド継承を考えます。B,C,Dにsuperをつけました。

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
import "hardhat/console.sol";

contract A {
    function foo() virtual public {
        console.log("foo A");
    }
}

contract B is A {
    function foo() override virtual public {
        console.log("foo B");
        super.foo();
    }
}

contract C is A {
    function foo() override virtual public {
        console.log("foo C");
        super.foo();
    }
}

contract D is B,C {
    function foo() override(B,C) public {
        console.log("foo D");
        super.foo();
    }
}

実行結果

foo D
foo C
foo B
foo A

D→C→B→A。このような順番で実行されます。Cの親はAですが、兄弟クラスであるBが先に評価されるのが特徴です。

一部superなし

続きまして、Bのsuperを削除します。

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
import "hardhat/console.sol";

contract A {
    function foo() virtual public {
        console.log("foo A");
    }
}

contract B is A {
    function foo() override virtual public {
        console.log("foo B");
    }
}

contract C is A {
    function foo() override virtual public {
        console.log("foo C");
        super.foo();
    }
}

contract D is B,C {
    function foo() override(B,C) public {
        console.log("foo D");
        super.foo();
    }
}

実行結果

foo D
foo C
foo B

D→C→BといってBでSTOPします。

一部function削除

さらに、Bのfoo()自体を削除してみるとどうなるでしょう。

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
import "hardhat/console.sol";

contract A {
    function foo() virtual public {
        console.log("foo A");
    }
}

contract B is A {
}

contract C is A {
    function foo() override virtual public {
        console.log("foo C");
        super.foo();
    }
}

contract D is B,C {
    function foo() override(A,C) public {
        console.log("foo D");
        super.foo();
    }
}

実行結果

foo D
foo C
foo A

今度はAがちゃんと実行されます。また、Dはoverride(A,C)と記載しなければエラーとなります。

まとめ

・contract is A,B... 後から書いたものから実行される
・親より兄弟が先に実行される
・superがないとそこでSTOP
・兄弟のfunctionがない場合は親にいく

以上で一応理解はできましたが、hardhatでconsoleを出しながらやるのが一番かもしれませんね。
constructorの実行についてもいずれ書きたいと思います。