コントラクト

Solidityのコントラクトは、オブジェクト指向言語のクラスに似ています。 コントラクトには、状態変数に格納された永続的なデータと、これらの変数を変更できる関数が含まれています。 別のコントラクト(インスタンス)の関数を呼び出すと、EVM関数呼び出しが実行され、呼び出したコントラクトの状態変数にアクセスできないようにコンテキストが切り替わります。 コントラクトとその関数は、何かが起こるために呼び出される必要があります。 Ethereumには、特定のイベントで自動的に関数を呼び出す「cron」の概念はありません。

コントラクトの作成

コントラクトは、Ethereumのトランザクションを介して「外部から」作成することも、Solidityのコントラクトから作成することもできます。

Remix に代表されるIDEは、UIによって作成プロセスをシームレスにします。

Ethereumでプログラマティックにコントラクトを作成する方法の一つとして、JavaScript APIの web3.js があります。 これにはコントラクトの作成を容易にする web3.eth.Contract という関数があります。

コントラクトが作成されると、その コンストラクタconstructor キーワードで宣言された関数)が一度だけ実行されます。

コンストラクタはオプションです。 ココンストラクタは1つしか許可されていないので、オーバーロードはサポートされていません。

コンストラクタが実行された後、コントラクトの最終コードがブロックチェーンに保存されます。 このコードには、すべてのパブリック関数と外部関数、および関数呼び出しによってそこから到達可能なすべての関数が含まれます。 デプロイされたコードには、コンストラクタのコードや、コンストラクタからのみ呼び出される内部関数は含まれません。

内部的にはコンストラクタの引数はコントラクト自体のコードの後に ABIエンコード を渡していますが、 web3.js を使用する場合はこれを気にする必要はありません。

あるコントラクトが別のコントラクトを作成したい場合、作成するコントラクトのソースコード(およびバイナリ)を作成者が知っていなければなりません。 これは周期的な作成の依存関係は不可能であることを意味します。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;

contract OwnedToken {
    // `TokenCreator` は、以下で定義されるコントラクトの型です。
    // 新しいコントラクトを作成するために使用しない限り、これを参照することは問題ありません。
    TokenCreator creator;
    address owner;
    bytes32 name;

    // 作成者と割り当てられた名前を登録するコンストラクタです。
    constructor(bytes32 name_) {
        // 状態変数には、その名前を通してアクセスします(`this.owner` などではない)。
        // 関数には、直接アクセスすることも、 `this.f` を介してアクセスすることもできますが、後者は関数への外部からのアクセスを提供します。
        // 特にコンストラクタでは、まだ関数が存在しないので、外部から関数にアクセスするべきではありません。
        // 詳しくは次のセクションを参照してください。
        owner = msg.sender;

        // `address` から `TokenCreator` への明示的な型変換を行い、
        // 呼び出したコントラクトの型が `TokenCreator` であることを仮定していますが、
        // 実際にそれを確認する方法はありません。
        // これは新しいコントラクトを作成するわけではありません。
        creator = TokenCreator(msg.sender);
        name = name_;
    }

    function changeName(bytes32 newName) public {
        // 作成者のみが名称を変更できます。
        // コントラクトの比較は、アドレスに明示的に変換することで取得できるアドレスをもとに行う。
        if (msg.sender == address(creator))
            name = newName;
    }

    function transfer(address newOwner) public {
        // トークンを送信できるのは、現在の所有者のみです。
        if (msg.sender != owner) return;

        // 以下に定義する `TokenCreator` コントラクトの関数を用いて、送金を進めるべきかどうかを作成者コントラクトに問い合わせます。
        // 呼び出しに失敗した場合(ガス欠など)、ここでの実行も失敗します。
        if (creator.isTokenTransferOK(owner, newOwner))
            owner = newOwner;
    }
}

contract TokenCreator {
    function createToken(bytes32 name)
        public
        returns (OwnedToken tokenAddress)
    {
        // 新しい `Token` コントラクトを作成し、そのアドレスを返す。
        // JavaScript側から見ると、この関数の戻り値の型は `address` です。
        // これはABIで利用可能な最も近い型だからです。
        return new OwnedToken(name);
    }

    function changeName(OwnedToken tokenAddress, bytes32 name) public {
        // ここでも、`tokenAddress` の外部型は単純に `address` です。
        tokenAddress.changeName(name);
    }

    // `OwnedToken`コントラクトにトークンを送信するかどうかのチェックを行う。
    function isTokenTransferOK(address currentOwner, address newOwner)
        public
        pure
        returns (bool ok)
    {
        // 送金を進めるかどうか、任意の条件をチェックします。
        return keccak256(abi.encodePacked(currentOwner, newOwner))[0] == 0x7f;
    }
}

ビジビリティとゲッター

状態変数のビジビリティ

public

パブリックな状態変数は内部変数と異なり、コンパイラが自動的に ゲッター関数 を生成し、他のコントラクトがその値を読み取ることを可能にします。 同じコントラクト内で使用する場合、外部アクセス(例えば this.x )はゲッターを呼び出しますが、内部アクセス(例えば x )はストレージから直接変数の値を取得します。 セッター関数は生成されないので、他のコントラクトが直接その値を変更することはできません。

internal

内部状態変数は、定義されているコントラクト内および派生コントラクトからのみアクセス可能です。 外部にアクセスすることはできません。 これは、状態変数のデフォルトのビジビリティレベルです。

private

プライベート状態変数は内部変数のようなものですが、派生コントラクトでは見えません。

警告

何かを private または internal にしても、他のコントラクトが情報を読んだり変更したりできなくなるだけで、まだブロックチェーンの外で世界中から見ることができます。

関数のビジビリティ

Solidityは、実際のEVMメッセージコールを作成する外部関数とそうでない内部関数の2種類の関数呼び出しを知っています。 さらに、内部関数は派生コントラクトにアクセスできないようにできます。 このため、関数のビジビリティには4つのタイプがあります。

external

外部関数はコントラクトインターフェースの一部であり、他のコントラクトやトランザクションを介して呼び出すことができることを意味します。 外部関数 f は、内部で呼び出すことはできません(すなわち、 f() は動作しませんが、 this.f() は動作します)。

public

パブリック関数は、コントラクトインターフェースの一部であり、内部またはメッセージコール経由で呼び出すことができます。

internal

内部機能は、現在のコントラクトまたはそこから派生するコントラクトの内部からのみアクセスできます。 外部にアクセスすることはできません。 コントラクトのABIを通じて外部に公開されないので、マッピングやストレージ参照などの内部型のパラメータを取ることができます。

private

プライベート関数は内部関数と同じですが、派生コントラクトでは見えません。

警告

何かを private または internal にしても、他のコントラクトが情報を読んだり変更したりできなくなるだけで、まだブロックチェーンの外で世界中から見ることができます。

ビジビリティ指定子は、状態変数の場合は型の後に、関数の場合はパラメータリストとリターンパラメータリストの間に与えられます。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract C {
    function f(uint a) private pure returns (uint b) { return a + 1; }
    function setData(uint a) internal { data = a; }
    uint public data;
}

次の例では、 Dc.getData() をコールしてステートのストレージ内にある data の値を取り出すことができますが、 f を呼び出すことはできません。 コントラクト EC から派生したものであるため、 compute を呼び出すことができます。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract C {
    uint private data;

    function f(uint a) private pure returns(uint b) { return a + 1; }
    function setData(uint a) public { data = a; }
    function getData() public view returns(uint) { return data; }
    function compute(uint a, uint b) internal pure returns (uint) { return a + b; }
}

// これはコンパイルできません
contract D {
    function readData() public {
        C c = new C();
        uint local = c.f(7); // error: member `f` is not visible
        c.setData(3);
        local = c.getData();
        local = c.compute(3, 5); // error: member `compute` is not visible
    }
}

contract E is C {
    function g() public {
        C c = new C();
        uint val = compute(3, 5); // 内部メンバへのアクセス(親コントラクトから派生したもの)
    }
}

ゲッター関数

コンパイラは、すべての public 状態変数のゲッター関数を自動的に作成します。 以下のコントラクトでは、コンパイラーは data という関数を生成します。 この関数は引数を取らず、状態変数 data の値である uint を返します。 状態変数は、宣言時に初期化できます。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract C {
    uint public data = 42;
}

contract Caller {
    C c = new C();
    function f() public view returns (uint) {
        return c.data();
    }
}

ゲッター関数は外部から見えるようになっています。 シンボルが内部的にアクセスされた場合(すなわち、 this. なし)、それは状態変数として評価されます。 外部からアクセスされた場合(つまり this. あり)、それは関数として評価されます。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;

contract C {
    uint public data;
    function x() public returns (uint) {
        data = 3; // internal access
        return this.data(); // 外部アクセス
    }
}

配列型の public 状態変数を持っている場合、生成されたゲッター関数を介して配列の単一要素を取り出すことしかできません。 このメカニズムは、配列全体を返すときの高いガスコストを避けるために存在します。 引数を使って、例えば myArray(0) のように、どの個別要素を返すかを指定できます。 一度の呼び出しで配列全体を返したい場合は、例えば、関数を書く必要があります。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract arrayExample {
    // パブリック状態変数
    uint[] public myArray;

    // コンパイラが生成するゲッター関数
    /*
    function myArray(uint i) public view returns (uint) {
        return myArray[i];
    }
    */

    // 配列全体を返す関数
    function getArray() public view returns (uint[] memory) {
        return myArray;
    }
}

これで、1回のコールで1つの要素を返す myArray(i) ではなく、 getArray() を使って配列全体を取り出すことができます。

次の例はもっと複雑です。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;

contract Complex {
    struct Data {
        uint a;
        bytes3 b;
        mapping(uint => uint) map;
        uint[3] c;
        uint[] d;
        bytes e;
    }
    mapping(uint => mapping(bool => Data[])) public data;
}

次のような形式の関数を生成します。 構造体のマッピングと配列(バイト配列を除く)は、個々の構造体メンバーを選択する、あるいはマッピングにキーを提供する良い方法がないため、省略されています。

function data(uint arg1, bool arg2, uint arg3)
    public
    returns (uint a, bytes3 b, bytes memory e)
{
    a = data[arg1][arg2][arg3].a;
    b = data[arg1][arg2][arg3].b;
    e = data[arg1][arg2][arg3].e;
}

関数モディファイア

モディファイアは、宣言的な方法で関数の動作を変更するために使用できます。 例えば、モディファイアを使って、関数を実行する前に自動的に条件をチェックできます。

モディファイアはコントラクトの継承可能なプロパティであり、派生コントラクトでオーバーライドできますが、 virtual マークが付いている場合に限ります。 詳細は、 モディファイアのオーバーライド を参照してください。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;
// 非推奨のselfdestructを使用するためwarningが出力されます。

contract owned {
    constructor() { owner = payable(msg.sender); }
    address payable owner;

    // このコントラクトはモディファイアを定義するだけで、それを使用することはありません。
    // 派生コントラクトで使用されます。
    // 関数本体は、モディファイアの定義にある特別な記号 `_;` が現れる場所に挿入されます。
    // これは、オーナーがこの関数を呼び出した場合は関数が実行され、そうでない場合は例外がスローされることを意味します。
    modifier onlyOwner {
        require(
            msg.sender == owner,
            "Only owner can call this function."
        );
        _;
    }
}

contract destructible is owned {
    // このコントラクトは `onlyOwner` モディファイアを `owned` から継承し、 `destroy` 関数に適用します。
    // これにより、 `destroy` への呼び出しは、保存されているオーナーによって実行された場合にのみ有効となります。
    function destroy() public onlyOwner {
        selfdestruct(owner);
    }
}

contract priced {
    // モディファイアは引数を受け取ることができます:
    modifier costs(uint price) {
        if (msg.value >= price) {
            _;
        }
    }
}

contract Register is priced, destructible {
    mapping(address => bool) registeredAddresses;
    uint price;

    constructor(uint initialPrice) { price = initialPrice; }

    // ここで `payable` キーワードを指定することも重要です。
    // さもなければ、この関数は送られてきたすべての Ether を自動的に拒否します。
    function register() public payable costs(price) {
        registeredAddresses[msg.sender] = true;
    }

    function changePrice(uint price_) public onlyOwner {
        price = price_;
    }
}

contract Mutex {
    bool locked;
    modifier noReentrancy() {
        require(
            !locked,
            "Reentrant call."
        );
        locked = true;
        _;
        locked = false;
    }

    /// この関数はミューテックスで保護されているので、 `msg.sender.call` 内からのリエントラントなコールは `f` を再び呼び出すことができません。
    /// `return 7` 文は戻り値に 7 を代入しますが、その後にモディファイアの `locked = false` という文は実行されます。
    function f() public noReentrancy returns (uint) {
        (bool success,) = msg.sender.call("");
        require(success);
        return 7;
    }
}

コントラクト C で定義されたモディファイア m にアクセスしたい場合は、 C.m を使って仮想ルックアップなしで参照できます。 現在のコントラクトまたはそのベースコントラクトで定義されたモディファイアのみを使用できます。 モディファイアはライブラリで定義することもできますが、その使用は同じライブラリの関数に限られます。

複数のモディファイアをホワイトスペースで区切ったリストで指定すると、その関数に適用され、提示された順序で評価されます。

モディファイアは、自分が修飾する関数の引数や戻り値に暗黙のうちにアクセスしたり変更したりできません。 モディファイアの値は、呼び出しの時点で明示的に渡されるだけです。

関数モディファイアでは、モディファイアが適用された関数をいつ実行させたいかを指定する必要があります。 プレースホルダ文(アンダースコア1文字 _ で示される)は、修飾される関数のボディが挿入されるべき場所を示すために使用されます。 プレースホルダ演算子は、アンダースコアを変数名の先頭や末尾に使用するのとは異なることに注意してください(これはスタイル上の選択です)。

モディファイアや関数本体からの明示的なリターンは、現在のモディファイアや関数本体のみを残します。 戻り値の変数は割り当てられ、コントロールフローは先行するモディファイアの _ の後に続きます。

警告

Solidityの以前のバージョンでは、モディファイアを持つ関数内の return 文の動作が異なっていました。

return; を持つモディファイアからの明示的なリターンは、関数が返す値に影響を与えません。 しかし、モディファイアは、関数本体を全く実行しないことを選択でき、その場合、関数本体が空であった場合と同様に、戻り値の変数は デフォルト値 に設定されます。

_ マークはモディファイアの中で複数回現れることがあります。 それぞれの出現箇所は、関数本体で置き換えられます。

モディファイアの引数には任意の式が許されており、このコンテキストでは、関数から見えるすべてのシンボルがモディファイアでも見えます。 モディファイアで導入されたシンボルは、(オーバーライドによって変更される可能性があるため)関数では見えません。

定数の状態変数とイミュータブルの状態変数

状態変数は constant または immutable として宣言できます。 どちらの場合も、コントラクトが構築された後は、変数を変更できません。 constant 変数の場合はコンパイル時に値を固定する必要がありますが、 immutable の場合はコンストラクション時にも値を代入できます。

また、ファイルレベルで constant 変数を定義することも可能です。

コンパイラはこれらの変数のためにストレージスロットを確保しておらず、出現するたびにそれぞれの値で置き換えられます。

通常の状態変数と比較して、定数変数やイミュータブル変数のガスコストは非常に低くなります。 定数変数の場合、それに割り当てられた式は、アクセスされるすべての場所にコピーされ、また毎回再評価されます。 これにより、局所的な最適化が可能になります。 イミュータブルの変数は、構築時に一度だけ評価され、その値はコード内のアクセスされるすべての場所にコピーされます。 これらの値のために、たとえそれより少ないバイト数で収まるとしても、32バイトが確保されます。 このため、定数値の方がイミュータブル値よりもコストが低い場合があります。

現時点では、定数やイミュータブルのすべての型が実装されているわけではありません。 サポートされているのは、 strings (定数のみ)と 値型 のみです。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.4;

uint constant X = 32**22 + 8;

contract C {
    string constant TEXT = "abc";
    bytes32 constant MY_HASH = keccak256("abc");
    uint immutable decimals;
    uint immutable maxBalance;
    address immutable owner = msg.sender;

    constructor(uint decimals_, address ref) {
        decimals = decimals_;
        // Assignments to immutables can even access the environment.
        maxBalance = ref.balance;
    }

    function isBalanceTooHigh(address other) public view returns (bool) {
        return other.balance > maxBalance;
    }
}

定数

constant 変数については、コンパイル時に値が定数である必要があり、変数が宣言された場所で代入されなければなりません。 ストレージ、ブロックチェーンデータ(例: block.timestampaddress(this).balanceblock.number )、実行データ( msg.valuegasleft() )にアクセスしたり、外部コントラクトを呼び出したりする式はすべて許可されていません。 メモリの割り当てに副作用を及ぼす可能性のある式は許可されますが、他のメモリオブジェクトに副作用を及ぼす可能性のある式は許可されません。 組み込み関数の keccak256sha256ripemd160ecrecoveraddmodmulmod は許可されています( keccak256 を除いて外部コントラクトをコールしていますが)。

メモリアロケータの副作用を許可した理由は、ルックアップテーブルなどの複雑なオブジェクトを構築できるようにするためです。 この機能はまだ完全には使用できません。

イミュータブル

immutable として宣言された変数は、 constant として宣言された変数よりも制限が緩いです。 具体的には、イミュータブルの変数は、コントラクトのコンストラクタや宣言の時点で、任意の値を代入できます。 それらは一度だけ代入でき、その時点からコンストラクション中でも読み取ることができます。

コンパイラが生成したコントラクト作成コードは、イミュータブルへのすべての参照をイミュータブルに割り当てられた値に置き換えることで、コントラクトのランタイムコードを返す前に修正します。 これは、コンパイラによって生成されたランタイムコードと、実際にブロックチェーンに保存されているランタイムコードを比較する場合に重要です。 コンパイラは、デプロイされたバイトコードのどこにこれらのイミュータブルがあるかを コンパイラのJSONスタンダードアウトプットimmutableReferences フィールドに出力します。

注釈

宣言時に代入されたイミュータブルは、コントラクトのコンストラクタが実行されて初めて初期化されたとみなされます。 つまり、他のイミュータブルに依存する値でイミュータブルをインラインで初期化できません。 ただし、コントラクトのコンストラクタの内部では初期化できます。

これは、状態変数の初期化とコンストラクタの実行の順序について、特に継承に関して異なる解釈がなされないようにするための措置です。

関数

関数は、コントラクトの内側にも外側にも定義できます。

コントラクト外の関数は「フリー関数」とも呼ばれ、常に暗黙の internal visibility を持っています。 そのコードは、内部のライブラリ関数と同様に、それらを呼び出したすべてのコントラクトに含まれます。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;

function sum(uint[] memory arr) pure returns (uint s) {
    for (uint i = 0; i < arr.length; i++)
        s += arr[i];
}

contract ArrayExample {
    bool found;
    function f(uint[] memory arr) public {
        // This calls the free function internally.
        // The compiler will add its code to the contract.
        uint s = sum(arr);
        require(s >= 10);
        found = true;
    }
}

注釈

コントラクトの外で定義された関数は、常にコントラクトのコンテキストで実行されます。 他のコントラクトを呼び出したり、Etherを送ったり、呼び出したコントラクトを破壊したりできます。 コントラクト内で定義された関数との主な違いは、フリー関数は変数 this やストレージ変数、自分のスコープにない関数に直接アクセスできないことです。

関数パラメータと返り値

関数は入力として型付けされたパラメータを受け取り、他の多くの言語とは異なり、出力として任意の数の値を返せます。

関数パラメータ

関数のパラメータは変数と同じように宣言され、使わないパラメータの名前は省略できます。

例えば、コントラクトが2つの整数で1種類の外部呼び出しを受け付けるようにしたい場合は、以下のようにします。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract Simple {
    uint sum;
    function taker(uint a, uint b) public {
        sum = a + b;
    }
}

関数パラメータは、他のローカル変数と同様に使用でき、また、それらを割り当てることもできます。

リターン変数

関数のリターン変数は、 returns キーワードの後に同じ構文で宣言されます。

例えば、関数のパラメータとして渡された2つの整数の和と積の2つの結果を返したい場合、次のように使います。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract Simple {
    function arithmetic(uint a, uint b)
        public
        pure
        returns (uint sum, uint product)
    {
        sum = a + b;
        product = a * b;
    }
}

リターン変数の名前は省略可能です。 リターン変数は、他のローカル変数と同様に使用でき、 デフォルト値 で初期化され、(再)代入されるまでその値を保持します。

上記のように明示的にリターン変数に代入してから関数を残すか、 return 文でリターン値(一個あるいは 複数個 )を直接指定できます。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract Simple {
    function arithmetic(uint a, uint b)
        public
        pure
        returns (uint sum, uint product)
    {
        return (a + b, a * b);
    }
}

return変数を持つ関数を終了するためにearly return を使用する場合は、return文と一緒にreturn値を指定する必要があります。

注釈

非内部関数から返せない型もあります。 これには、以下に挙げる型と、それらを再帰的に含む複合型が含まれます。

  • マッピング

  • 内部関数型

  • ロケーションが storage に設定されている参照型

  • 多次元配列( ABI coder v1 にのみ適用されます)

  • 構造体( ABI coder v1 にのみ適用されます)

ライブラリ関数は 内部ABI が異なるため、この制限は適用されません。

複数の値を返す

関数が複数の戻り値の型を持つ場合、 return (v0, v1, ..., vn) という文を複数の値を返すために使用できます。 構成要素の数は戻り値の変数の数と同じでなければならず、また、 暗黙の変換 の後にそれらの型は一致しなければなりません。

ステートのミュータビリティ

view関数

関数は view を宣言でき、その場合は状態を変更しないことが約束されます。

注釈

コンパイラのEVMのターゲットがByzantium以降(デフォルト)の場合、 view 関数が呼び出されるとオペコード STATICCALL が使用され、EVM実行の一部として状態が変更されないように強制されます。 ライブラリ view 関数では、 DELEGATECALLSTATICCALL の組み合わせがないため、 DELEGATECALL が使用されます。 つまり、ライブラリ view 関数には、状態の変更を防ぐランタイムチェックがありません。 ライブラリのコードは通常、コンパイル時に知られており、静的チェッカーはコンパイル時のチェックを行うため、このことがセキュリティに悪影響を及ぼすことはありません。

次のような記述は、状態の修正とみなされます。

  1. 状態変数への書き込み。

  2. イベントの発生

  3. 他のコントラクトの作成

  4. selfdestruct の使用。

  5. コールでのEtherの送金。

  6. view または pure と表示されていない関数の呼び出し。

  7. 低レベルコールの使用。

  8. 特定のオペコードを含むインラインアセンブリの使用。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;

contract C {
    function f(uint a, uint b) public view returns (uint) {
        return a * (b + 42) + block.timestamp;
    }
}

注釈

関数の constant は、かつては view の別名でしたが、バージョン0.5.0で廃止されました。

注釈

ゲッターメソッドは自動的に view とマークされます。

注釈

バージョン0.5.0以前のコンパイラでは、 view 関数に STATICCALL オペコードを使用していませんでした。 これにより、無効な明示的型変換を使用して、 view 関数の状態を変更できました。 view 関数に STATICCALL を使用することで、EVMのレベルで状態の変更を防ぐことができます。

pure関数

関数は pure を宣言でき、その場合、状態を読み取ったり変更したりしないことが約束されます。 特に、 pure 関数をコンパイル時に、入力と msg.data のみを与えて評価することが可能でなければなりませんが、現在のブロックチェーンの状態については一切知りません。 これは、 immutable 変数からの読み取りが非純粋な操作である可能性があることを意味します。

注釈

コンパイラのEVMターゲットがByzantium以降(デフォルト)の場合、オペコード STATICCALL が使用されます。 これは、状態が読み取られないことを保証するものではありませんが、少なくとも修正されないことを保証するものです。

上記で説明したステートの修飾文のリストに加えて、以下のものはステートからの読み取りとみなされます。

  1. 状態変数からの読み出し。

  2. address(this).balance または <address>.balance へのアクセス。

  3. blocktxmsgmsg.sigmsg.data を除く)のメンバーのいずれかにアクセスすること。

  4. pure とマークされていない関数を呼び出すこと。

  5. 特定のオペコードを含むインラインアセンブリの使用。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;

contract C {
    function f(uint a, uint b) public pure returns (uint) {
        return a * (b + 42);
    }
}

Pure関数は、 エラーが発生 したときに、 revert() および require() 関数を使って潜在的な状態変化をリバートできます。

viewpure の制限を受けていないコードで以前に行われた状態の変更のみがリバートされ、そのコードは revert をキャッチして渡さないというオプションを持っているため、状態の変更をリバートすることは「状態の修正」とはみなされません。

この動作は、 STATICCALL のオペコードとも一致しています。

警告

EVMのレベルで関数が状態を読み取るのを防ぐことはできず、状態に書き込むのを防ぐことしかできません(つまり、EVMのレベルで強制できるのは view だけで、 pure はできません)。

注釈

バージョン0.5.0以前のコンパイラでは、 pure 関数に STATICCALL オペコードを使用していませんでした。 これにより、無効な明示的型変換を使用して、 pure 関数の状態を変更できました。 pure 関数に STATICCALL を使用することで、EVMのレベルで状態の変更を防ぐことができます。

注釈

バージョン0.4.17以前では、コンパイラは pure が状態を読んでいないことを強制していませんでした。 これはコンパイル時の型チェックで、コントラクトの型の間で無効な明示的変換を行うことで回避できます。 コンパイラはコントラクトの型が状態を変更する操作を行わないことを検証できますが、実行時に呼び出されるコントラクトが実際にその型であることをチェックできないからです。

特殊な関数

receive Ether関数

コントラクトは最大で1つの receive 関数を持つことができ、 receive() external payable { ... } を使って宣言されます( function キーワードなし)。 この関数は、引数を持つことができず、何も返すことができず、 external のビジビリティと payable のステートミュータビリティを持たなければなりません。 この関数は仮想的であり、オーバーライドでき、モディファイアを持つことができます。

receive関数は、空のcalldataを持つコントラクトへの呼び出しで実行されます。 これは、プレーンなEther送金(例: .send() または .transfer() 経由)で実行される関数です。 このような関数が存在せず、payableな fallback関数 が存在する場合は、プレーンなEther送金時にフォールバック関数が呼び出されます。 receive Ether関数もpayable fallback関数も存在しない場合、コントラクトはpayableな関数呼び出しを表さないトランザクションを通じてEtherを受信できず、例外をスローします。

最悪の場合、 receive 関数は2300のガスが使えることに頼るしかなく( sendtransfer を使用した場合など)、基本的なロギング以外の操作を行う余裕はありません。 以下のような操作は、2300ガスの規定値よりも多くのガスを消費します。

  • ストレージへの書き込み

  • コントラクトの作成

  • 大量のガスを消費する外部関数の呼び出し

  • Etherの送信

警告

Etherがコントラクトに直接送信され(関数呼び出しなしで、送信者は send または transfer を使用)、受信側のコントラクトがreceive Ether関数またはpayable fallback関数を定義しない場合、例外が発生しEtherを送り返します(Solidity v0.4.0以前は異なっていました)。 コントラクトでEtherを受信したい場合は、receive Ether関数を実装する必要があります(Etherを受信するためにpayable fallback関数を使用することは推奨されていません)。

警告

Etherを受け取る関数を持たないコントラクトは、 coinbaseトランザクション (別名: minerブロックリワード )の受信者として、または selfdestruct の宛先としてEtherを受け取ることができます。

コントラクトは、そのようなEther送金に反応できず、したがって、それらを拒否することもできません。 これはEVMの設計上の選択であり、Solidityはこれを回避できません。

また、 address(this).balance は、コントラクトに実装されている手動の会計処理(receive Ether関数でカウンタを更新するなど)の合計よりも高くなる可能性があることを意味しています。

関数 receive を使用したSinkコントラクトの例です。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;

// このコントラクトは、送られてきたEtherをすべて保持し、それを取り戻す方法はありません。
contract Sink {
    event Received(address, uint);
    receive() external payable {
        emit Received(msg.sender, msg.value);
    }
}

fallback関数

コントラクトは最大で1つの fallback 関数を持つことができ、 fallback () external [payable] または fallback (bytes calldata input) external [payable] returns (bytes memory output) (いずれも function キーワードなし)を使って宣言されます。 この関数は external ビジビリティを持たなければなりません。 フォールバック関数は、仮想的であり、オーバーライドでき、モディファイアを持つことができます。

フォールバック関数は、他の関数が与えられた関数シグネチャに一致しない場合、またはデータが全く供給されず receive Ether関数 がない場合、コントラクトへの呼び出しで実行されます。 フォールバック関数は常にデータを受信しますが、Etherも受信するためには、 payable とマークされていなければなりません。

パラメータ付きバージョンを使用した場合、 input にはコントラクトに送信された完全なデータ( msg.data に等しい)が含まれ、 output でデータを返すことができます。 返されたデータはABIエンコードされません。 代わりに、修正なしで(パディングさえもしない)返されます。

最悪の場合、受信関数の代わりに支払い可能なフォールバック関数も使用されている場合、2300ガスが使用可能であることだけに頼ることができます(この意味については、 receive Ether関数 を参照してください)。

他の関数と同様に、fallback関数も、十分な量のガスが渡されている限り、複雑な処理を実行できます。

警告

payable フォールバック関数は、 receive Ether関数 が存在しない場合、プレーンなEther送金に対しても実行されます。 Ether送金をインターフェースの混乱と区別するために、payable fallback関数を定義する場合は、必ずreceive Ether関数も定義することをお勧めします。

注釈

入力データをデコードしたい場合は、最初の4バイトで関数セレクタをチェックし、 abi.decode と配列スライス構文を併用することで、ABIエンコードされたデータをデコードできます。 (c, d) = abi.decode(input[4:], (uint256, uint256)); この方法は最後の手段としてのみ使用し、代わりに適切な関数を使用すべきであることに注意してください。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.2 <0.9.0;

contract Test {
    uint x;
    // この関数はこのコントラクトに送られるすべてのメッセージに対して呼び出されます(他の関数は存在しません)。
    // このコントラクトにEtherを送信すると例外が発生します。
    // なぜなら、fallback関数が `payable` モディファイアを持たないからです。
    fallback() external { x = 1; }
}

contract TestPayable {
    uint x;
    uint y;
    // この関数は、プレーンなEther送金を除く、このコントラクトに送信されるすべてのメッセージに対して呼び出されます(受信関数以外の関数は存在しません)。
    // このコントラクトへの空でないcalldataを持つ呼び出しは、フォールバック関数を実行します(呼び出しと一緒にEtherが送信された場合でも同様です)。
    fallback() external payable { x = 1; y = msg.value; }

    // この関数は、プレーンなEther送金、すなわち空のcalldataを持つすべてのコールに対して呼び出されます。
    receive() external payable { x = 2; y = msg.value; }
}

contract Caller {
    function callTest(Test test) public returns (bool) {
        (bool success,) = address(test).call(abi.encodeWithSignature("nonExistingFunction()"));
        require(success);
        // test.xが1になる

        // address(test) は ``send`` を直接呼び出すことはできません。
        // なぜなら、 ``test`` には支払い可能なフォールバック関数がないからです。
        // その上で ``send`` を呼び出すには ``address payable`` 型に変換する必要があります。
        address payable testPayable = payable(address(test));

        // 誰かがそのコントラクトにEtherを送ると、送金は失敗します。
        // つまり、ここではfalseが返されます。
        return testPayable.send(2 ether);
    }

    function callTestPayable(TestPayable test) public returns (bool) {
        (bool success,) = address(test).call(abi.encodeWithSignature("nonExistingFunction()"));
        require(success);
        // test.xが1、test.yが0になります
        (success,) = address(test).call{value: 1}(abi.encodeWithSignature("nonExistingFunction()"));
        require(success);
        // test.xが1、test.yが1になります

        // 誰かがそのコントラクトにEtherを送ると、TestPayableのreceive関数が呼び出されます。
        // この関数はストレージに書き込むので、単純な ``send`` や ``transfer`` よりも多くのガスを消費します。
        // そのため、低レベルの呼び出しを使用する必要があります。
        (success,) = address(test).call{value: 2 ether}("");
        require(success);
        // test.xが2、test.yが2 etherになります。

        return true;
    }
}

関数のオーバーロード

コントラクトは、同じ名前でパラメータの種類が異なる複数の関数を持つことができます。 この処理は「オーバーロード」と呼ばれ、継承された関数にも適用されます。 次の例では、コントラクト A のスコープ内での関数 f のオーバーロードを示しています。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract A {
    function f(uint value) public pure returns (uint out) {
        out = value;
    }

    function f(uint value, bool really) public pure returns (uint out) {
        if (really)
            out = value;
    }
}

オーバーロードされた関数は、外部インターフェースにも存在します。 外部から見える2つの関数が、Solidityの型ではなく、外部の型で異なる場合はエラーになります。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

// これはコンパイルできません
contract A {
    function f(B value) public pure returns (B out) {
        out = value;
    }

    function f(address value) public pure returns (address out) {
        out = value;
    }
}

contract B {
}

上記の両方の f 関数のオーバーロードは、Solidity内では異なるものと考えられていますが、最終的にはABI用のアドレス型を受け入れます。

オーバーロードの解決と引数のマッチング

オーバーロードされた関数は、現在のスコープ内の関数宣言と、関数呼び出しで提供される引数を照合することで選択されます。 すべての引数が期待される型に暗黙的に変換できる場合、関数はオーバーロードの候補として選択されます。 正確に1つの候補がない場合、解決は失敗します。

注釈

オーバーロードの解決にリターンパラメータは考慮されません。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract A {
    function f(uint8 val) public pure returns (uint8 out) {
        out = val;
    }

    function f(uint256 val) public pure returns (uint256 out) {
        out = val;
    }
}

f(50) を呼び出すと、 50 は暗黙のうちに uint8 型と uint256 型の両方に変換できるため、型エラーが発生します。 一方、 f(256) は、 256 が暗黙のうちに uint8 に変換できないため、 f(uint256) のオーバーロードとなります。

イベント

Solidityのイベントは、EVMのロギング機能の上に抽象化を与えます。 アプリケーションは、EthereumクライアントのRPCインターフェースを介して、これらのイベントをサブスクライブし、リッスンできます。

イベントはコントラクトの継承可能なメンバーです。 イベントを呼び出すと、引数がトランザクションのログ(ブロックチェーンの特別なデータ構造)に保存されます。 これらのログはコントラクトのアドレスに関連付けられ、ブロックチェーンに組み込まれ、ブロックがアクセス可能である限りそこに留まります(現時点では永遠ですが、Serenityでは変わるかもしれません)。 ログとそのイベントデータはコントラクト内からはアクセスできません(ログを作成したコントラクトからもアクセスできません)。

ログのMerkle証明を要求することが可能なので、外部のエンティティがコントラクトにそのような証明を供給すれば、ログがブロックチェーン内に実際に存在することをチェックできます。 コントラクトは直近の256個のブロックハッシュしか見ることができないため、ブロックヘッダを提供する必要があります。

最大3つのパラメータに indexed 属性を追加すると、ログのデータ部分ではなく、 "topics" と呼ばれる特別なデータ構造に追加されます。 トピックは1つのワード(32バイト)しか保持できないため、インデックス付きの引数に 参照型 を使用した場合、値のKeccak-256ハッシュが代わりにトピックとして保存されます。

indexed 属性を持たないパラメータはすべて、ログのデータ部分に ABIエンコード されます。

トピックを使用すると、イベントを検索できます。 たとえば、一連のブロックを特定のイベントでフィルタリングする場合などです。 また、イベントを発したコントラクトのアドレスでイベントをフィルタリングすることもできます。

例えば、以下のコードでは、web3.jsの subscribe("logs") メソッド を使用して、特定のアドレス値を持つトピックにマッチするログをフィルタリングしています。

var options = {
    fromBlock: 0,
    address: web3.eth.defaultAccount,
    topics: ["0x0000000000000000000000000000000000000000000000000000000000000000", null, null]
};
web3.eth.subscribe('logs', options, function (error, result) {
    if (!error)
        console.log(result);
})
    .on("data", function (log) {
        console.log(log);
    })
    .on("changed", function (log) {
});

anonymous 指定子でイベントを宣言した場合を除き、イベントのシグネチャのハッシュはトピックの一つです。 つまり、特定の匿名イベントを名前でフィルタリングできず、コントラクトアドレスでしかフィルタリングできません。 匿名イベントの利点は、デプロイや呼び出しが安く済むことです。 また、インデックス付きの引数を3つではなく4つ宣言できます。

注釈

トランザクションログにはイベントデータのみが保存され、型は保存されないので、データを正しく解釈するためには、どのパラメータがインデックスされているか、イベントが匿名であるかなど、イベントの型を知る必要があります。 特に、匿名イベントを使って別のイベントのシグネチャを「偽装」することが可能です。

イベントのメンバー

  • event.selector: 匿名でないイベントの場合、デフォルトのトピックで使用されるイベントシグネチャの keccak256 ハッシュを含む bytes32

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.21 <0.9.0;

contract ClientReceipt {
    event Deposit(
        address indexed from,
        bytes32 indexed id,
        uint value
    );

    function deposit(bytes32 id) public payable {
        // イベントは `emit` を使って発行され、その後にイベント名と引数 (もしあれば) が括弧で囲まれます。
        // このような呼び出しは (深くネストされていても) JavaScript API から `Deposit` をフィルタリングすることで検出できます。
        emit Deposit(msg.sender, id, msg.value);
    }
}

JavaScript APIでの使用方法は以下の通りです。

var abi = /* コンパイラが生成するABI */;
var ClientReceipt = web3.eth.contract(abi);
var clientReceipt = ClientReceipt.at("0x1234...ab67" /* アドレス */);

var depositEvent = clientReceipt.Deposit();

// 変更を監視
depositEvent.watch(function(error, result){
    // resultには、`Deposit` の呼び出しに与えられたインデックス付けされていない引数とトピックが含まれます。
    if (!error)
        console.log(result);
});

// また、コールバックを渡すとすぐに監視を開始します。
var depositEvent = clientReceipt.Deposit(function(error, result) {
    if (!error)
        console.log(result);
});

上記の出力は以下のようになります(トリミング済み)。

{
   "returnValues": {
       "from": "0x1111…FFFFCCCC",
       "id": "0x50…sd5adb20",
       "value": "0x420042"
   },
   "raw": {
       "data": "0x7f…91385",
       "topics": ["0xfd4…b4ead7", "0x7f…1a91385"]
   }
}

イベントを理解するための追加資料

エラーとリバート文

Solidityのエラーは、操作が失敗した理由をユーザーに説明するための、便利でガス効率の良い方法です。 エラーはコントラクト(インターフェースやライブラリを含む)の内外で定義できます。

これらは、 リバート文 と一緒に使用する必要があります。 リバート文は、現在のコールのすべての変更をリバートし、エラーデータをコール側に戻します。

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;

/// 送金の残高不足
/// `required`必要だが、`available`しか利用可能でない
/// @param available 利用可能な残高
/// @param required 送金の要求額
error InsufficientBalance(uint256 available, uint256 required);

contract TestToken {
    mapping(address => uint) balance;
    function transfer(address to, uint256 amount) public {
        if (amount > balance[msg.sender])
            revert InsufficientBalance({
                available: balance[msg.sender],
                required: amount
            });
        balance[msg.sender] -= amount;
        balance[to] += amount;
    }
    // ...
}

エラーはオーバーロードやオーバーライドできませんが、継承されます。 スコープが異なっている限り、同じエラーを複数の場所で定義できます。 エラーのインスタンスは、 revert 文を使ってのみ作成できます。

エラーが発生すると、データが作成され、リバート操作で呼び出し側に渡され、オフチェーンコンポーネントに戻るか、 try/catch文 でキャッチされます。 エラーをキャッチできるのは、外部からの呼び出しの場合のみで、内部呼び出しや同じ関数内で発生したリバートはキャッチできないことに注意してください。

パラメータを何も与えなければ、エラーは4バイトのデータだけで済み、上記のように NatSpec を使ってエラーの理由をさらに説明できますが、これはチェーンには保存されません。 これにより、非常に安価で便利なエラー報告機能を同時に実現しています。

具体的には、エラーインスタンスは、同じ名前と型を持つ関数への関数呼び出しと同じ方法でABIエンコードされ、 revert オペコードのリターンデータとして使用されます。 つまり、データは4バイトのセレクタとそれに続く ABIエンコード されたデータで構成されています。 セレクタは、エラー型のシグネチャのkeccak256ハッシュの最初の4バイトで構成されています。

注釈

コントラクトが同じ名前の異なるエラーでリバートすることは可能ですし、呼び出し元では区別できない異なる場所で定義されたエラーであっても可能です。 外部、つまりABIにとっては、エラーの名前だけが重要であり、そのエラーが定義されているコントラクトやファイルは関係ありません。

require(condition, "description"); という文は、 error Error(string) を定義できれば if (!condition) revert Error("description") と同じになります。 ただし、 Error は組み込み型であり、ユーザーが提供するコードでは定義できないことに注意してください。

同様に、 assert が失敗した場合や同様の条件の場合は、ビルトイン型の Panic(uint256) エラーでリバートします。

注釈

エラーデータは、失敗の兆候を示すためにのみ使用すべきで、コントロールフローの手段としては使用しないでください。 その理由は、インナーコールのリバートデータは、デフォルトでは外部呼び出しのチェーンを通じて伝搬されるからです。 つまり、内側の呼び出しは、それを呼び出したコントラクトから来たように見えるリバートデータを「偽造」できるということです。

エラーのメンバー

  • error.selector: エラーセレクタを含む bytes4 の値。

継承

Solidityは、ポリモーフィズムを含む多重継承をサポートしています。

ポリモーフィズムとは、関数の呼び出し(内部および外部)が、継承階層の中で最も派生したコントラクト内の同名の関数(およびパラメータ型)を常に実行することを意味します。 この機能は、 virtual キーワードと override キーワードを使って、階層内の各関数で明示的に有効にする必要があります。 詳細は 関数オーバーライド を参照してください。

ContractName.functionName() を使ってコントラクトを明示的に指定したり、フラット化された継承階層の1つ上のレベルの関数を呼び出したい場合は super.functionName() を使うことで、継承階層のさらに上のレベルの関数を内部で呼び出すことができます(以下参照)。

コントラクトが他のコントラクトを継承する場合、ブロックチェーン上には1つのコントラクトのみが作成され、作成されたコントラクトにはすべてのベースコントラクトのコードがコンパイルされます。 つまり、ベースコントラクトの関数に対する内部呼び出しも、すべて内部関数呼び出しを使用するだけです( super.f(..) はJUMPを使用し、メッセージ呼び出しではありません)。

状態変数のシャドーイングはエラーとみなされます。 派生コントラクトは、そのベースのいずれかに同名の可視状態変数が存在しない場合にのみ、状態変数 x を宣言できます。

一般的な継承システムは Pythonのもの と非常に似ていますが、複数の継承に関しては、 異なる点 もあります。

詳細は以下の例を参照してください。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
// 非推奨のselfdestructを使用するためwarningが出力されます。

contract Owned {
    constructor() { owner = payable(msg.sender); }
    address payable owner;
}

// 他のコントラクトから派生させるには、`is`を使用します。
// 派生したコントラクトは、内部関数や状態変数を含む、プライベートでないすべてのメンバにアクセスできます。
// しかし、これらは `this` を介して外部からアクセスすることはできません。
contract Destructible is Owned {
    // キーワード `virtual` は、その関数が派生クラスでその振る舞いを変更できる (「オーバーライド」) ことを意味します。
    function destroy() virtual public {
        if (msg.sender == owner) selfdestruct(owner);
    }
}

// これらの抽象コントラクトは、コンパイラにインターフェースを知らせるためにのみ提供されています。
// ボディを持たない関数に注意してください。
// コントラクトがすべての関数を実装していない場合、インターフェースとしてのみ使用できます。
abstract contract Config {
    function lookup(uint id) public virtual returns (address adr);
}

abstract contract NameReg {
    function register(bytes32 name) public virtual;
    function unregister() public virtual;
}

// 多重継承が可能です。
// `Owned` は `Destructible` のベースクラスでもあるが、 `Owned` のインスタンスは一つしかないことに注意(C++ の仮想継承と同じ)。
contract Named is Owned, Destructible {
    constructor(bytes32 name) {
        Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
        NameReg(config.lookup(1)).register(name);
    }

    // 関数は、同じ名前、同じ入力の数/型の別の関数によってオーバーライドできます。
    // オーバーライドされた関数が異なる型の出力パラメータを持っている場合、それはエラーの原因となります。
    // ローカル関数とメッセージベースの関数呼び出しの両方が、これらのオーバーライドを考慮に入れています。
    // 関数をオーバーライドしたい場合は、`override` キーワードを使用する必要があります。
    // また、この関数を再びオーバーライドしたい場合は、`virtual`キーワードを指定する必要があります。
    function destroy() public virtual override {
        if (msg.sender == owner) {
            Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
            NameReg(config.lookup(1)).unregister();
            // 特定のオーバーライドされた関数を呼び出すことは可能です。
            Destructible.destroy();
        }
    }
}

// コンストラクタが引数を取る場合、派生コントラクトのコンストラクタでヘッダまたはモディファイアを呼び出すスタイルで提供する必要があります(下記参照)。
contract PriceFeed is Owned, Destructible, Named("GoldFeed") {
    function updateInfo(uint newInfo) public {
        if (msg.sender == owner) info = newInfo;
    }

    // ここでは、 `override` のみを指定し、 `virtual` は指定しません。
    // これは、 `PriceFeed` から派生したコントラクトは、もう `destroy` の挙動を変更できないことを意味します。
    function destroy() public override(Destructible, Named) { Named.destroy(); }
    function get() public view returns(uint r) { return info; }

    uint info;
}

上記では、破壊要求を「送金」するために Destructible.destroy() をコールしていることに注意してください。 この方法は、次の例に見られるように、問題があります。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
// 非推奨のselfdestructを使用するためwarningが出力されます。

contract owned {
    constructor() { owner = payable(msg.sender); }
    address payable owner;
}

contract Destructible is owned {
    function destroy() public virtual {
        if (msg.sender == owner) selfdestruct(owner);
    }
}

contract Base1 is Destructible {
    function destroy() public virtual override { /* cleanup 1 */ Destructible.destroy(); }
}

contract Base2 is Destructible {
    function destroy() public virtual override { /* cleanup 2 */ Destructible.destroy(); }
}

contract Final is Base1, Base2 {
    function destroy() public override(Base1, Base2) { Base2.destroy(); }
}

Final.destroy() への呼び出しは、最終的なオーバーライドで明示的に指定しているので Base2.destroy を呼び出しますが、この関数は Base1.destroy をバイパスします。 これを回避する方法は、 super を使うことです。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
// 非推奨のselfdestructを使用するためwarningが出力されます。

contract owned {
    constructor() { owner = payable(msg.sender); }
    address payable owner;
}

contract Destructible is owned {
    function destroy() virtual public {
        if (msg.sender == owner) selfdestruct(owner);
    }
}

contract Base1 is Destructible {
    function destroy() public virtual override { /* cleanup 1 */ super.destroy(); }
}

contract Base2 is Destructible {
    function destroy() public virtual override { /* cleanup 2 */ super.destroy(); }
}

contract Final is Base1, Base2 {
    function destroy() public override(Base1, Base2) { super.destroy(); }
}

Base2super の関数を呼び出す場合、単純にそのベースコントラクトの1つでこの関数を呼び出すのではありません。 むしろ、最終的な継承グラフの次のベースコントラクトでこの関数を呼び出すので、 Base1.destroy() を呼び出すことになります(最終的な継承順序は--最も派生したコントラクトから始まることに注意してください: Final、Base2、Base1、Destructible、owned)。 superを使うときに呼び出される実際の関数は、型はわかっていても、使われるクラスのコンテキストではわかりません。 これは通常の仮想メソッドの検索でも同様です。

関数オーバーライド

ベース関数は、コントラクトを継承することでオーバーライドでき、 virtual としてマークされている場合は、その動作を変更できます。 オーバーライドされた関数は、関数ヘッダーで override キーワードを使用しなければなりません。 オーバーライドされた関数は、オーバーライドされた関数のビジビリティを external から public に変更するだけです。 ミュータビリティは、順序に従って、より厳密なものに変更できます。 nonpayableviewpure でオーバーライドでき、 viewpure でオーバーライドできます。 payable は例外で、他のミュータビリティに変更できません。

次の例では、mutabilityとvisibilityの変更を行っています。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

contract Base
{
    function foo() virtual external view {}
}

contract Middle is Base {}

contract Inherited is Middle
{
    function foo() override public pure {}
}

多重継承では、同じ関数を定義する最も派生したベースコントラクトを、 override キーワードの後に明示的に指定する必要があります。 言い換えれば、同じ関数を定義し、まだ別のベースコントラクトによってオーバーライドされていないすべてのベースコントラクトを指定しなければなりません(継承グラフのあるパス上で)。 さらに、コントラクトが複数の(関連性のない)ベースから同じ関数を継承する場合は、明示的にオーバーライドしなければなりません。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;

contract Base1
{
    function foo() virtual public {}
}

contract Base2
{
    function foo() virtual public {}
}

contract Inherited is Base1, Base2
{
    // foo()を定義している複数のベースから派生しているので、明示的にオーバーライドする必要があります。
    function foo() public override(Base1, Base2) {}
}

関数が共通のベースコントラクトで定義されている場合や、共通のベースコントラクトに他のすべての関数をすでにオーバーライドする固有の関数がある場合は、明示的なオーバーライド指定子は必要ありません。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;

contract A { function f() public pure{} }
contract B is A {}
contract C is A {}
// 明示的なオーバーライドは必要ありません
contract D is B, C {}

より正式には、シグネチャのすべてのオーバーライドパスの一部であるベースコントラクトがあり、(1)そのベースが関数を実装しており、現在のコントラクトからベースへのパスでそのシグネチャを持つ関数に言及しているものがないか、(2)そのベースが関数を実装しておらず、現在のコントラクトからベースへのすべてのパスで関数に言及しているものが多くても1つである場合、複数のベースから継承された関数をオーバーライドする必要はありません。

この意味で、シグネチャのオーバーライドパスとは、対象となるコントラクトから始まり、オーバーライドしないそのシグネチャを持つ関数に言及しているコントラクトで終わる、継承グラフを通るパスのことです。

オーバーライドする関数を virtual としてマークしていない場合、派生コントラクトはもはやその関数の動作を変更できません。

注釈

private のビジビリティを持つ関数は virtual にできません。

注釈

実装のない関数は、インターフェースの外では virtual とマークされなければなりません。 インターフェースでは、すべての関数は自動的に virtual とみなされます。

注釈

Solidity 0.8.8からは、複数のベースで定義されている場合を除き、インターフェース関数をオーバーライドする際に override キーワードは必要ありません。

パブリックな状態変数は、関数のパラメータと戻り値の型が変数のゲッター関数と一致する場合、外部関数をオーバーライドできます。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;

contract A
{
    function f() external view virtual returns(uint) { return 5; }
}

contract B is A
{
    uint public override f;
}

注釈

パブリックな状態変数は、外部関数をオーバーライドできますが、それ自体をオーバーライドできません。

モディファイアのオーバーライド

関数のモディファイアはお互いにオーバーライドできます。 これは、 関数オーバーライド と同じように動作します(モディファイアにオーバーロードがないことを除く)。 virtual キーワードはオーバーライドするモディファイアに使用し、 override キーワードはオーバーライドするモディファイアに使用しなければなりません。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;

contract Base
{
    modifier foo() virtual {_;}
}

contract Inherited is Base
{
    modifier foo() override {_;}
}

多重継承の場合は、すべての直接のベースコントラクトを明示的に指定する必要があります。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;

contract Base1
{
    modifier foo() virtual {_;}
}

contract Base2
{
    modifier foo() virtual {_;}
}

contract Inherited is Base1, Base2
{
    modifier foo() override(Base1, Base2) {_;}
}

コンストラクタ

コンストラクタは、 constructor キーワードで宣言されたオプションの関数で、コントラクトの作成時に実行され、コントラクトの初期化コードを実行できます。

コンストラクタのコードが実行される前に、状態変数は、インラインで初期化した場合は指定した値に、初期化しなかった場合は デフォルト値 に初期化されます。

コンストラクタの実行後、コントラクトの最終コードがブロックチェーンにデプロイされます。 コードのデプロイには、コードの長さに応じた追加のガスがかかります。 このコードには、パブリックインターフェースの一部であるすべての関数と、そこから関数呼び出しによって到達可能なすべての関数が含まれます。 コンストラクタのコードや、コンストラクタからしか呼び出されない内部関数は含まれません。

コンストラクタがない場合、コントラクトはデフォルトコンストラクタを想定しますが、これは constructor() {} と同等です。 例えば、以下のようになります。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

abstract contract A {
    uint public a;

    constructor(uint a_) {
        a = a_;
    }
}

contract B is A(1) {
    constructor() {}
}

コンストラクタで内部パラメータを使用できます(たとえば、ストレージポインタなど)。 この場合、コントラクトは abstract マークを付けなければなりません。 なぜなら、これらのパラメータは外部から有効な値を割り当てることができず、派生コントラクトのコンストラクタを通してのみ有効だからです。

警告

バージョン0.4.22より前のバージョンでは、コンストラクタはコントラクトと同じ名前の関数として定義されていました。 この構文は非推奨で、バージョン0.5.0ではもう認められていません。

警告

バージョン0.7.0より前のバージョンでは、コンストラクタのビジビリティを internal または public のいずれかに指定する必要がありました。

ベースコンストラクタの引数

すべてのベースコントラクトのコンストラクタは、以下に説明する線形化規則に従って呼び出されます。 ベースコントラクトのコンストラクタに引数がある場合、派生コントラクトはそのすべてを指定する必要があります。 これは2つの方法で行うことができます。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

contract Base {
    uint x;
    constructor(uint x_) { x = x_; }
}

// 継承リストに直接指定するか...
contract Derived1 is Base(7) {
    constructor() {}
}

// または派生コンストラクタの"モディファイア"を介して行われるか、
contract Derived2 is Base {
    constructor(uint y) Base(y * y) {}
}

// or declare abstract...
abstract contract Derived3 is Base {
}

// and have the next concrete derived contract initialize it.
contract DerivedFromDerived is Derived3 {
    constructor() Base(10 + 10) {}
}

1つの方法は、継承リストに直接記載する方法です( is Base(7) )。 もう1つは、派生したコンストラクタの一部としてモディファイアを呼び出す方法です( Base(y * y) )。 コンストラクタの引数が定数で、コントラクトの動作を定義したり、記述したりする場合は、最初の方法が便利です。 ベースのコンストラクタの引数が派生コントラクトの引数に依存する場合は、2 番目の方法を使用する必要があります。 引数は、継承リストで指定するか、派生するコンストラクタのモディファイアスタイルで指定する必要があります。 両方の場所で引数を指定するとエラーになります。

派生したコントラクトが、そのベースコントラクトのすべてのコンストラクタの引数を指定しない場合、abstractとして宣言されなければなりません。 その場合、他のコントラクトがそこから派生するとき、その他のコントラクトの継承リストまたはコンストラクタは、パラメータが指定されていないすべてのベースクラスに対して必要なパラメータを提供しなければなりません(さもなければ、その他のコントラクトも同様に抽象化されなければなりません)。 例えば、上記のコードスニペットでは、 Derived3DerivedFromDerived を見てください。

多重継承とリニアライゼーション

多重継承が可能な言語は、いくつかの問題を抱えています。 ひとつは「 Diamond Problem 」です。 SolidityはPythonに似ていますが、ベースクラスの有向非巡回グラフ(Directed Acyclic Graph; DAG)に特定の順序を強制するために「 C3 Linearization 」を使用しています。 この結果、単調性という望ましい特性が得られますが、いくつかの継承グラフが使えなくなります。 特に、 is 指令でのベースクラスの順序は重要で、「最もベースに近いもの」から「最も派生したもの」の順に直接ベースコントラクトをリストアップする必要があります。 この順序は、Pythonで使われている順序とは逆であることに注意してください。

これを別の方法で簡単に説明すると、異なるコントラクトで複数回定義された関数が呼び出された場合、与えられたベースは右から左(Pythonでは左から右)へと深さ優先で検索され、最初にマッチしたもので停止します。 もしベースコントラクトが既に検索されていたら、その部分はスキップされます。

以下のコードでは、Solidityが「Linearization of inheritance graph impossible」というエラーを出します。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;

contract X {}
contract A is X {}
// これはコンパイルできません
contract C is A, X {}

その理由は、 CXA のオーバーライドを要求している( A, X をこの順番で指定することで)が、 A 自身は X のオーバーライドを要求しており、解決できない矛盾を抱えているからです。

複数のベースから継承された関数を独自にオーバーライドせずに明示的にオーバーライドする必要があるため、C3線形化は実際にはあまり重要ではありません。

継承の直線化が特に重要でありながら、あまり明確ではないのが、継承階層に複数のコンストラクタが存在する場合です。 コンストラクタは、継承するコントラクトのコンストラクタで引数が提供された順番に関係なく、常に線形化された順番で実行されます。 例えば、以下のようになります。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

contract Base1 {
    constructor() {}
}

contract Base2 {
    constructor() {}
}

// コンストラクターは、以下の順序で実行されます。
//  1 - Base1
//  2 - Base2
//  3 - Derived1
contract Derived1 is Base1, Base2 {
    constructor() Base1() Base2() {}
}

// コンストラクターは、以下の順序で実行されます。
//  1 - Base2
//  2 - Base1
//  3 - Derived2
contract Derived2 is Base2, Base1 {
    constructor() Base2() Base1() {}
}

// コンストラクターは、変わらず以下の順序で実行されます。
//  1 - Base2
//  2 - Base1
//  3 - Derived3
contract Derived3 is Base2, Base1 {
    constructor() Base1() Base2() {}
}

同じ名前の異なる種類のメンバーの継承

コントラクト内の以下のペアが継承により同じ名前になっている場合はエラーとなります。
  • 関数とモディファイア

  • 関数とイベント

  • イベントとモディファイア

例外として、状態変数のゲッターが外部関数をオーバーライドできます。

抽象コントラクト

コントラクトは、その関数の少なくとも1つが実装されていない場合、またはすべてのベースコントラクトコンストラクタに引数を提供しない場合、abstractとマークする必要があります。 そうでない場合でも、コントラクトを直接作成するつもりがない場合などには、コントラクトをabstractとマークすることがあります。 抽象コントラクトは インターフェース と似ていますが、インターフェースは宣言できる内容がより限定されています。

抽象コントラクトは、次の例に示すように abstract キーワードを使用して宣言します。 関数 utterance() が宣言されているが、実装が提供されていない(実装本体 { } が与えられていない)ため、このコントラクトは抽象として定義する必要があることに注意してください。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;

abstract contract Feline {
    function utterance() public virtual returns (bytes32);
}

このような抽象コントラクトは、直接インスタンス化できません。 これは、抽象コントラクト自体がすべての定義された関数を実装している場合にも当てはまります。 ベースクラスとしての抽象コントラクトの使い方を以下の例で示します。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;

abstract contract Feline {
    function utterance() public pure virtual returns (bytes32);
}

contract Cat is Feline {
    function utterance() public pure override returns (bytes32) { return "miaow"; }
}

コントラクトが抽象コントラクトを継承し、オーバーライドによって未実装関数を全て実装していない場合は、抽象としてマークする必要があります。

なお、実装のない関数は、構文がよく似ていても、 関数型 とは異なるものです。

実装のない関数(関数宣言)の例:

function foo(address) external returns (address);

型が関数型である変数の宣言の例:

function(address) external returns (address) foo;

抽象コントラクトは、コントラクトの定義とその実装を切り離し、より良い拡張性と自己文書化を提供し、 テンプレートメソッド のようなパターンを促進し、コードの重複を取り除きます。 抽象コントラクトは、インターフェースでメソッドを定義することが有用であるのと同じ方法で有用です。 抽象コントラクトの設計者が「私の子供はこのメソッドを実装しなければならない」と言える方法です。

注釈

抽象コントラクトは、実装済みの仮想関数を未実装の仮想関数で上書きできません。

インターフェース

インターフェースは、抽象コントラクトと似ていますが、いかなる関数も実装できません。 さらに以下の制限があります。

  • 他のコントラクトを継承できませんが、他のインターフェースを継承できます。

  • 宣言された関数は、コントラクトでpublicであっても、インターフェースではexternalでなければなりません。

  • コンストラクタを宣言できません。

  • 状態変数を宣言できません。

  • モディファイアを宣言できません。

これらの制限の一部は、将来解除される可能性があります。

インターフェースは基本的にコントラクトABIが表現できる内容に限定されており、ABIとインターフェースの間の変換は情報を失うことなく可能でなければなりません。

インターフェースは、次のように定義されます。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.2 <0.9.0;

interface Token {
    enum TokenType { Fungible, NonFungible }
    struct Coin { string obverse; string reverse; }
    function transfer(address recipient, uint amount) external;
}

コントラクトは、他のコントラクトを継承するように、インターフェースを継承できます。

インターフェースで宣言されたすべての関数は暗黙のうちに virtual となり、それをオーバーライドする関数には override キーワードは必要ありません。 これは、オーバーライドされた関数が再びオーバーライドできることを自動的に意味するものではありません。 オーバーライドされた関数が virtual とマークされている場合にのみ、それは可能です。

インターフェースは、他のインターフェースを継承できます。 これには通常の継承と同じルールがあります。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.2 <0.9.0;

interface ParentA {
    function test() external returns (uint256);
}

interface ParentB {
    function test() external returns (uint256);
}

interface SubInterface is ParentA, ParentB {
    // Parentと互換性があることを主張するために、testを再定義する必要があります。
    function test() external override(ParentA, ParentB) returns (uint256);
}

インターフェースや他のコントラクトに似た構造の中で定義された型は、他のコントラクトからアクセスできます。 Token.TokenType または Token.Coin

警告

インターフェースは Solidity version 0.5.0 以降 enum 型をサポートしています。 プラグマバージョンが最低限このバージョンを指定していることを確認してください。

ライブラリ

ライブラリはコントラクトに似ていますが、その目的は、特定のアドレスに一度だけデプロイされ、そのコードはEVMの DELEGATECALL (Homesteadまでの CALLCODE )機能を使って再利用されることです。 ライブラリ関数が呼び出された場合、そのコードは呼び出したコントラクトのコンテキストで実行されます。 つまり this は呼び出したコントラクトを指し、呼び出したコントラクトのストレージにアクセスできます。 ライブラリは独立したソースコードの一部なので、呼び出し元のコントラクトの状態変数が明示的に提供されている場合にのみアクセスできます(そうでない場合は名前を付ける方法がありません)。 ライブラリはステートレスであると想定されているため、ライブラリ関数は、ステートを変更しない場合( view または pure 関数の場合)にのみ、直接(つまり DELEGATECALL を使用せずに)呼び出すことができます。 ライブラリを破壊することはできません。

注釈

バージョン0.4.20までは、Solidityの型システムを回避してライブラリを破壊できました。 このバージョンから、ライブラリには状態を変更する関数を直接(つまり DELEGATECALL なしで)呼び出すことを禁止する メカニズム が含まれるようになりました。

ライブラリは、それを使用するコントラクトの暗黙のベースコントラクトと見なすことができます。 継承階層では明示的には見えませんが、ライブラリ関数への呼び出しは、明示的なベースコントラクトの関数への呼び出しと同じように見えます( L.f() のような修飾されたアクセスを使用)。 もちろん、内部関数への呼び出しは内部呼び出し規約を使用します。 つまり、すべての内部型を渡すことができ、 メモリに保存された 型は参照によって渡され、コピーされません。 EVMでこれを実現するために、コントラクトから呼び出される内部ライブラリ関数のコードとそこから呼び出されるすべての関数は、コンパイル時に呼び出し元のコントラクトに含まれ、 DELEGATECALL の代わりに通常の JUMP 呼び出しが使用されます。

注釈

継承のアナロジーは、パブリック関数になると破綻します。 パブリックライブラリ関数を L.f() で呼び出すと、外部呼び出しになります(正確には DELEGATECALL )。 対して A.f() は、 A が現在のコントラクトのベースコントラクトである場合、内部呼び出しとなります。

次の例では、ライブラリを使用する方法を説明しています(ただし、マニュアルの方法を使用しており、集合を実装するためのより高度な例については、必ず using for を参照してください)。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;

// 呼び出し側のコントラクトでそのデータを保持するために使用される新しい構造体のデータ型を定義します。
struct Data {
    mapping(uint => bool) flags;
}

library Set {
    // 最初のパラメータは「ストレージ参照」型であるため、呼び出しの一部として、そのストレージアドレスのみが渡され、その内容は渡されないことに注意してください。
    // これはライブラリ関数の特別な機能です。
    // もし関数がそのオブジェクトのメソッドとみなすことができるならば、最初のパラメータを `self` と呼ぶのが慣例となっています。
    function insert(Data storage self, uint value)
        public
        returns (bool)
    {
        if (self.flags[value])
            return false; // 既に存在する
        self.flags[value] = true;
        return true;
    }

    function remove(Data storage self, uint value)
        public
        returns (bool)
    {
        if (!self.flags[value])
            return false; // 存在しない
        self.flags[value] = false;
        return true;
    }

    function contains(Data storage self, uint value)
        public
        view
        returns (bool)
    {
        return self.flags[value];
    }
}

contract C {
    Data knownValues;

    function register(uint value) public {
        // 「インスタンス」は現在のコントラクトになるため、ライブラリの関数は特定のインスタンスなしで呼び出すことができます。
        require(Set.insert(knownValues, value));
    }
    // このコントラクトでは、必要であれば、knownValues.flagsに直接アクセスすることもできます。
}

もちろん、このような方法でライブラリを使用する必要はありません。 構造体のデータ型を定義せずにライブラリを使用することもできます。 また、関数はストレージの参照パラメータなしで動作し、複数のストレージの参照パラメータを任意の位置に持つことができます。

Set.containsSet.insertSet.remove の呼び出しは、すべて外部のコントラクト/ライブラリへの呼び出し( DELEGATECALL )としてコンパイルされています。 ライブラリを使用している場合は、実際の外部関数の呼び出しが行われることに注意してください。 msg.sendermsg.valuethis は、この呼び出しでも値が保持されますが(ホームステッド以前は、 CALLCODE を使用していたため、 msg.sendermsg.value は変化していましたが)。

次の例は、外部関数呼び出しのオーバーヘッドなしにカスタム型を実装するために、 メモリに保存された型 とライブラリの内部関数を使用する方法を示しています。

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

struct bigint {
    uint[] limbs;
}

library BigInt {
    function fromUint(uint x) internal pure returns (bigint memory r) {
        r.limbs = new uint[](1);
        r.limbs[0] = x;
    }

    function add(bigint memory a, bigint memory b) internal pure returns (bigint memory r) {
        r.limbs = new uint[](max(a.limbs.length, b.limbs.length));
        uint carry = 0;
        for (uint i = 0; i < r.limbs.length; ++i) {
            uint limbA = limb(a, i);
            uint limbB = limb(b, i);
            unchecked {
                r.limbs[i] = limbA + limbB + carry;

                if (limbA + limbB < limbA || (limbA + limbB == type(uint).max && carry > 0))
                    carry = 1;
                else
                    carry = 0;
            }
        }
        if (carry > 0) {
            // 残念、limbを追加しなくてはいけません
            uint[] memory newLimbs = new uint[](r.limbs.length + 1);
            uint i;
            for (i = 0; i < r.limbs.length; ++i)
                newLimbs[i] = r.limbs[i];
            newLimbs[i] = carry;
            r.limbs = newLimbs;
        }
    }

    function limb(bigint memory a, uint index) internal pure returns (uint) {
        return index < a.limbs.length ? a.limbs[index] : 0;
    }

    function max(uint a, uint b) private pure returns (uint) {
        return a > b ? a : b;
    }
}

contract C {
    using BigInt for bigint;

    function f() public pure {
        bigint memory x = BigInt.fromUint(7);
        bigint memory y = BigInt.fromUint(type(uint).max);
        bigint memory z = x.add(y);
        assert(z.limb(1) > 0);
    }
}

ライブラリ型を address 型に変換して、つまり address(LibraryName) を使ってライブラリのアドレスを取得することが可能です。

コンパイラは、ライブラリが配置されるアドレスを知らないため、コンパイルされた16進コードには __$30bbc0abd4d6364515865950d3e0d10953$__ という形式のプレースホルダーが含まれます。 このプレースホルダーは、完全修飾されたライブラリ名のkeccak256ハッシュの16進エンコーディングの34文字のプレフィックスであり、例えば、ライブラリが libraries/ ディレクトリの bigint.sol というファイルに格納されている場合は libraries/bigint.sol:BigInt となります。 このようなバイトコードは不完全なので、デプロイしてはいけません。 プレースホルダーを実際のアドレスに置き換える必要があります。 これを行うには、ライブラリのコンパイル時にコンパイラに渡すか、リンカを使用して既にコンパイルされたバイナリを更新する必要があります。 リンク用のコマンドラインコンパイラの使用方法については、 ライブラリのリンク を参照してください。

コントラクトと比較して、ライブラリには以下のような制限があります。

  • 状態変数を持つことはできません。

  • 継承することも継承されることもできません。

  • Etherを受け取れません。

  • 壊すことができません。

(これらは後の段階で解除されるかもしれません)

ライブラリの関数シグネチャと関数セレクタ

パブリックライブラリ関数や外部ライブラリ関数の外部呼び出しは可能ですが、そのような呼び出しのための呼び出し規約はSolidity内部のものとみなされ、通常の コントラクトABI に指定されているものとは異なります。 外部ライブラリ関数は、再帰的構造体やストレージポインタなど、外部コントラクト関数よりも多くの引数型をサポートしています。 そのため、4バイトセレクタの計算に使用される関数シグネチャは、内部のネーミングスキーマに従って計算され、コントラクトABIでサポートされていない型の引数は、内部のエンコーディングを使用します。

シグネチャの型には、以下の識別子が使われています。

  • 値型、非ストレージ string 、非ストレージ bytes はコントラクトABIと同じ識別子を使用しています。

  • 非ストレージ型の配列型はコントラクトABIと同じ規則に従っています。 すなわち、動的配列は <type>[]M 要素の固定サイズ配列は <type>[M] です。

  • ストレージを持たない構造体は、完全修飾名で参照されます。

  • ストレージポインターマッピングでは、 <keyType><valueType> がそれぞれマッピングのキー型とバリュー型の識別子である mapping(<keyType> => <valueType>) storage を使用します。

  • 他のストレージポインタ型は、対応する非ストレージ型の型識別子を使用しますが、それに1つのスペースとそれに続く storage が追加されます。

引数のエンコーディングは、通常のコントラクトABIと同じです。 ただし、ストレージポインタは、それが指し示すストレージスロットを参照する uint256 値としてエンコーディングされます。

コントラクトABIと同様に、セレクタは署名のKeccak256ハッシュの最初の4バイトで構成されています。 その値は、 .selector メンバーを使ってSolidityから以下のように取得できます。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.14 <0.9.0;

library L {
    function f(uint256) external {}
}

contract C {
    function g() public pure returns (bytes4) {
        return L.f.selector;
    }
}

ライブラリのためのコールプロテクション

冒頭で述べたように、 DELEGATECALLCALLCODE ではなく CALL を使ってライブラリのコードを実行すると、 viewpure の関数が呼ばれない限りリバートします。

EVMは、コントラクトが CALL を使用して呼び出されたかどうかを検出する直接的な方法を提供していませんが、コントラクトは ADDRESS オペコードを使用して、現在「どこで」実行されているかを調べることができます。 生成されたコードは、このアドレスをコンストラクション時に使用されたアドレスと比較して、呼び出しのモードを決定します。

具体的には、ライブラリのランタイムコードは常にプッシュ命令で始まり、コンパイル時には20バイトのゼロになっています。 デプロイコードが実行されると、この定数がメモリ上で現在のアドレスに置き換えられ、この変更されたコードがコントラクトに格納されます。 実行時には、これによりデプロイ時のアドレスがスタックにプッシュされる最初の定数となり、ディスパッチャコードは、ビューではない、ピュアではない関数の場合、現アドレスとこの定数を比較します。

つまり、ライブラリのためにチェーンに保存された実際のコードは、コンパイラが deployedBytecode として報告したコードとは異なるということです。

using for

using A for B ディレクティブは、コントラクトの文脈において、関数( A )を演算子としてユーザー定義の値型にに、あるいはメンバー関数として任意の型( B )にアタッチするために使用できます。 そのメンバー関数は、呼び出されたオブジェクトを最初のパラメータとして受け取ります(Pythonの self 変数のようなものです)。 演算子関数は、オペランドをパラメータとして受け取ります。

ファイルレベルでも、コントラクト内でも、コントラクトレベルで有効です。

最初の部分である A には、以下のいずれかを指定できます:

  • 関数のリストで、オプションで演算子名を指定できます(例: using {f, g as +, h, L.t} for uint )。 演算子が指定されていない場合、関数はライブラリ関数かフリー関数のどちらかになり、メンバ関数として型にアタッチされます。 それ以外の場合は、自由関数でなければならず、型に対するその演算子の定義となります。

  • ライブラリの名前(例: using L for uint) - ライブラリのプライベートでない関数はすべてメンバ関数として型にアタッチされます。

ファイルレベルでは、2番目の部分である B は明示的な型でなければなりません(データロケーションの指定はありません)。 コントラクトの中では、型の代わりに * を使用することもできます(例: using L for *;)。

ライブラリを指定した場合、そのライブラリに含まれる すべての 非プライベート関数は、最初のパラメータの型がオブジェクトの型と一致しないものであっても、アタッチされます。 型は関数が呼び出された時点でチェックされ、関数のオーバーロード解消が実行されます。

関数のリストを使用する場合(例: using {f, g, h, L.t} for uint )、型( uint )はこれらの関数のそれぞれの第1パラメータに暗黙的に変換可能である必要があります。 このチェックは、これらの関数のいずれもが呼び出されない場合でも行われます。 ライブラリのプライベート関数は、 using for がライブラリ内にある場合にのみ指定できることに注意してください。

演算子を定義する場合(例: using {f as +} for T )、型( T )は ユーザー定義の値型 で、定義は pure 関数である必要があります。 演算子の定義はグローバルでなければなりません。 この方法で定義できる演算子は以下の通りです。

カテゴリー

演算子

可能なシグネチャ

Bitwise

&

function (T, T) pure returns (T)

|

function (T, T) pure returns (T)

^

function (T, T) pure returns (T)

~

function (T) pure returns (T)

Arithmetic

+

function (T, T) pure returns (T)

-

function (T, T) pure returns (T)

function (T) pure returns (T)

*

function (T, T) pure returns (T)

/

function (T, T) pure returns (T)

%

function (T, T) pure returns (T)

Comparison

==

function (T, T) pure returns (bool)

!=

function (T, T) pure returns (bool)

<

function (T, T) pure returns (bool)

<=

function (T, T) pure returns (bool)

>

function (T, T) pure returns (bool)

>=

function (T, T) pure returns (bool)

単項と二項の - は別々の定義が必要であることに注意してください。 コンパイラは演算子がどのように呼び出されるかに基づいて、正しい定義を選択します。

using A for B; ディレクティブは、現在のスコープ(コントラクトまたは現在のモジュール/ソースユニット)内においてのみ有効で、そのすべての関数内を含み、それが使用されているコントラクトまたはモジュール外には影響を与えません。

このディレクティブがファイルレベルで使用され、同じファイル内でファイルレベルで定義されたユーザー定義型に適用される場合、最後に global という単語を追加できます。 これにより、using文の範囲だけでなく、その型が利用可能な場所(他のファイルも含む)で、関数や演算子がその型に付加されるという効果があります。

ライブラリ セクションにある集合の例を、ライブラリ関数の代わりにファイルレベルの関数を使って、このように書き換えてみましょう。

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.13;

struct Data { mapping(uint => bool) flags; }
// Now we attach functions to the type.
// The attached functions can be used throughout the rest of the module.
// If you import the module, you have to repeat the using directive there, for example as
//   import "flags.sol" as Flags;
//   using {Flags.insert, Flags.remove, Flags.contains}
//     for Flags.Data;
using {insert, remove, contains} for Data;

function insert(Data storage self, uint value)
    returns (bool)
{
    if (self.flags[value])
        return false; // already there
    self.flags[value] = true;
    return true;
}

function remove(Data storage self, uint value)
    returns (bool)
{
    if (!self.flags[value])
        return false; // not there
    self.flags[value] = false;
    return true;
}

function contains(Data storage self, uint value)
    view
    returns (bool)
{
    return self.flags[value];
}

contract C {
    Data knownValues;

    function register(uint value) public {
        // ここでは、Data型のすべての変数に対応するメンバ関数があります。
        // 以下の関数呼び出しは、 `Set.insert(knownValues, value)` と同じです。
        require(knownValues.insert(value));
    }
}

また、そのようにしてビルトイン型(値型)を拡張することも可能です。 この例では、ライブラリを使用します。

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.13;

library Search {
    function indexOf(uint[] storage self, uint value)
        public
        view
        returns (uint)
    {
        for (uint i = 0; i < self.length; i++)
            if (self[i] == value) return i;
        return type(uint).max;
    }
}
using Search for uint[];

contract C {
    uint[] data;

    function append(uint value) public {
        data.push(value);
    }

    function replace(uint from, uint to) public {
        // これは、ライブラリ関数呼び出しを実行します
        uint index = data.indexOf(from);
        if (index == type(uint).max)
            data.push(to);
        else
            data[index] = to;
    }
}

すべての外部ライブラリ呼び出しは、実際のEVM関数呼び出しであることに注意してください。 つまり、メモリや値の型を渡す場合は、 self 変数であってもコピーが実行されます。 コピーが行われない唯一の状況は、ストレージ参照変数が使用されている場合や、内部ライブラリ関数が呼び出されている場合です。

ユーザー定義型にカスタム演算子を定義する方法を示す別の例:

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.19;

type UFixed16x2 is uint16;

using {
    add as +,
    div as /
} for UFixed16x2 global;

uint32 constant SCALE = 100;

function add(UFixed16x2 a, UFixed16x2 b) pure returns (UFixed16x2) {
    return UFixed16x2.wrap(UFixed16x2.unwrap(a) + UFixed16x2.unwrap(b));
}

function div(UFixed16x2 a, UFixed16x2 b) pure returns (UFixed16x2) {
    uint32 a32 = UFixed16x2.unwrap(a);
    uint32 b32 = UFixed16x2.unwrap(b);
    uint32 result32 = a32 * SCALE / b32;
    require(result32 <= type(uint16).max, "Divide overflow");
    return UFixed16x2.wrap(uint16(a32 * SCALE / b32));
}

contract Math {
    function avg(UFixed16x2 a, UFixed16x2 b) public pure returns (UFixed16x2) {
        return (a + b) / UFixed16x2.wrap(200);
    }
}