[Solidityのはじめ方] Part9: コントラクトの定義と継承

Solidityの旅、Step 3へようこそ!🎉 ここでは、スマートコントラクトの骨組みとなる「コントラクトの定義」と、コードの再利用性を高める「継承」について学びます。これらを理解することで、より構造化され、効率的なスマートコントラクトを作成できるようになります。

1. コントラクトの定義 🧱

Solidityにおける「コントラクト」は、Ethereumブロックチェーン上に存在するプログラムの基本単位です。オブジェクト指向プログラミングにおけるクラスに似ており、状態変数(データを格納する変数)や関数(ロジックを実行するコード)をまとめたものです。

コントラクトを定義するには、contract キーワードを使用します。基本的な構造は以下のようになります。

📝 ポイント:

  • SPDXライセンス識別子: コードのライセンスを明確にするために、ファイルの先頭にコメント形式で記載することが推奨されています (例: // SPDX-License-Identifier: MIT)。
  • pragma solidity: 使用するSolidityコンパイラのバージョンを指定します。特定のバージョン範囲を指定することで、意図しないバージョンの違いによる問題を避けることができます (例: pragma solidity ^0.8.20;)。これは0.8.20以上、0.9.0未満のバージョンを意味します。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

// 'MyContract' という名前のコントラクトを定義
contract MyContract {
    // 状態変数 (State Variables): コントラクトの状態を保持する変数
    uint256 public myNumber; // publicにすると自動的にゲッター関数が生成される
    address public owner;
    string private myString = "Hello"; // private変数はコントラクト内部からのみアクセス可能

    // コンストラクタ (Constructor): コントラクトがデプロイされる時に一度だけ実行される特別な関数
    constructor(uint256 _initialNumber) {
        myNumber = _initialNumber;
        owner = msg.sender; // msg.senderはトランザクションを送信したアドレス
    }

    // 関数 (Functions): コントラクトのロジックを定義
    function updateNumber(uint256 _newNumber) public {
        // ownerだけがこの関数を呼び出せるように制限 (簡単なアクセス制御)
        require(msg.sender == owner, "Only owner can update the number.");
        myNumber = _newNumber;
    }

    function getMyString() public view returns (string memory) {
        // view関数は状態を変更しない読み取り専用関数
        return myString;
    }

    // private関数: コントラクト内部からのみ呼び出し可能
    function _internalLogic() private pure returns (uint256) {
        // pure関数は状態の読み取りも行わない関数
        return 1 + 1;
    }
}

この例では、MyContract という名前のコントラクトを定義しています。これには、数値を格納する myNumber、コントラクトの所有者アドレスを格納する owner、文字列を格納する myString という3つの状態変数があります。また、初期値を設定する constructor、数値を更新する updateNumber 関数、文字列を取得する getMyString 関数などが含まれています。

2. 継承 (Inheritance) 👨‍👩‍👧‍👦

継承は、既存のコントラクト(親コントラクトまたはベースコントラクト)の機能を引き継いで、新しいコントラクト(子コントラクトまたは派生コントラクト)を作成する仕組みです。これにより、コードの重複を避け、再利用性を高めることができます。

Solidityでは、is キーワードを使って継承関係を示します。

以下は、基本的な継承の例です。Owned コントラクトを作成し、その機能を MyToken コントラクトが継承します。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

// 親コントラクト: 所有権管理機能を持つ
contract Owned {
    address public owner;

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    constructor() {
        owner = msg.sender;
    }

    // modifier: 関数の実行前に特定の条件をチェックする仕組み
    modifier onlyOwner() {
        require(msg.sender == owner, "Caller is not the owner");
        _; //修飾された関数の本体を実行する場所を示す
    }

    function transferOwnership(address newOwner) public virtual onlyOwner {
        require(newOwner != address(0), "New owner cannot be the zero address");
        emit OwnershipTransferred(owner, newOwner);
        owner = newOwner;
    }
}

// 子コントラクト: Ownedコントラクトを継承
contract MyToken is Owned {
    string public name = "My Awesome Token";
    string public symbol = "MAT";
    uint256 public totalSupply = 1000000;

    // Ownedコントラクトのコンストラクタも自動的に呼び出される

    // transferOwnership関数も継承されるが、onlyOwner修飾子も引き継がれる
    // 例: トークンの発行なども onlyOwer 修飾子で制限できる
    function mint(address _to, uint256 _amount) public onlyOwner {
        // mint関数はownerだけが実行可能になる
        totalSupply += _amount;
        // 実際には残高管理のロジックも必要
    }

    // 親コントラクトの関数をオーバーライドする場合
    // 親の関数に `virtual`、子の関数に `override` が必要
    function transferOwnership(address newOwner) public override onlyOwner {
        // ここで独自のロジックを追加することも可能
        // (例: 所有権移転前に特定の条件をチェックするなど)
        super.transferOwnership(newOwner); // 親コントラクトの関数を呼び出す
    }
}

💡 継承のポイント:

  • 子コントラクトは、親コントラクトの public および internal な状態変数と関数にアクセスできます。private なメンバーにはアクセスできません。
  • 親コントラクトのコンストラクタは、子コントラクトがデプロイされる際に自動的に実行されます。もし親コンストラクタが引数を必要とする場合、子コントラクトのコンストラクタで明示的に指定する必要があります。
  • modifier も継承されます。

子コントラクトで、親コントラクトから継承した関数と同じ名前・引数の関数を定義し、動作を上書きすることができます。これを「オーバーライド」と呼びます。

Solidity 0.6.0以降、関数をオーバーライド可能にするには親コントラクトの関数に virtual キーワードを、オーバーライドする子コントラクトの関数には override キーワードを明示的に付ける必要があります。

上の MyToken の例では、Owned コントラクトの transferOwnership 関数に virtual を付け、MyToken の同名関数に override を付けています。

子コントラクトのオーバーライドした関数内で、親コントラクトの元の関数を呼び出したい場合は super キーワードを使用します(例: super.transferOwnership(newOwner);)。

Solidityでは、複数のコントラクトを継承することも可能です。is キーワードの後にカンマ区切りで継承したいコントラクト名を列挙します。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

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

contract Base2 {
     function foo() public virtual pure returns (string memory) {
        return "Base2";
    }
     function bar() public virtual pure returns (string memory) {
        return "Bar from Base2";
    }
}

// Base1 と Base2 を継承 (継承順序が重要)
contract MultiDerived is Base1, Base2 {
    // Base1 と Base2 の両方に foo() があるため、オーバーライドが必要
    // 継承順序 (Base1, Base2) により、デフォルトでは Base2.foo が優先されることが多いが、
    // 明示的にどちらを呼び出すか、あるいは新しい実装を提供する必要がある。
    // C3線形化というルールに基づいて解決される。
    function foo() public pure override(Base1, Base2) returns (string memory) {
        // super.foo() は最も右の親 (Base2) の foo を呼び出す
        // return super.foo(); // "Base2" を返す
        return "MultiDerived foo"; // 独自の 実装
    }

     function bar() public pure override returns (string memory) {
         return "Overridden bar in MultiDerived";
     }
}

⚠️ 多重継承の注意点:

  • 継承順序: is の後に書くコントラクトの順序は重要です。特に、複数の親コントラクトに同じ名前の関数や修飾子が存在する場合、SolidityはC3線形化(C3 Linearization)というアルゴリズムに基づいて継承の優先順位を決定します。基本的には、is リストの後ろ(右側)にあるコントラクトが優先されます。
  • ダイヤモンド問題: あるコントラクトが、同じ基底コントラクトを継承する複数のコントラクトを継承する場合(菱形継承、ダイヤモンド問題)、どの親の実装を使用するかが曖昧になる可能性があります。SolidityのC3線形化はこの問題を解決しますが、複雑な継承関係はコードの理解を難しくする可能性があるため、慎重に設計する必要があります。
  • オーバーライド指定: 複数の親から同じ関数を継承する場合、override キーワードの後に、どの親コントラクトをオーバーライドするのかを括弧内にカンマ区切りで指定する必要があります (例: override(Base1, Base2))。

まとめ ✨

今回は、スマートコントラクトの基本的な構成要素である「コントラクト定義」と、コードの再利用性を高める「継承」について学びました。

  • contract キーワードでコントラクトを定義します。
  • コントラクトには状態変数、関数、コンストラクタ、修飾子などを含めることができます。
  • is キーワードで他のコントラクトを継承し、機能を引き継ぐことができます。
  • virtualoverride を使って、親コントラクトの関数を子コントラクトで上書きできます。
  • 多重継承も可能ですが、継承順序やオーバーライド指定に注意が必要です。

これらの概念を理解することで、より整理され、効率的で、保守しやすいスマートコントラクトを構築するための基礎が固まります。次のステップでは、コントラクトの初期化を行う「コンストラクタ」について詳しく見ていきましょう!🚀

参考情報 📚