Solidity

Solidityは、スマートコントラクトを実装するための、オブジェクト指向の高級言語です。 スマートコントラクトとは、Ethereumのアカウントの動作を制御するプログラムのことです。

Solidityは、 Ethereum Virtual Machine (EVM)をターゲットに設計されている カーリーブラケット言語 です。 C++、Python、JavaScriptの影響を受けています。 Solidityがどの言語から影響を受けているかについては、 言語の影響 のセクションで詳しく説明しています。

Solidityは、静的型付け、継承、ライブラリ、複雑なユーザー定義型などの機能をサポートしています。

Solidityでは、投票、クラウドファンディング、ブラインドオークション、マルチシグネチャウォレットなどの用途に応じたコントラクトを作成できます。

コントラクトをデプロイする際には、Solidityの最新のリリースバージョンを使用すべきです。 例外的なケースを除いて、最新バージョンには セキュリティフィックス が施されています。 さらに、破壊的な変更や新機能も定期的に導入されています。 私たちは現在、 この速いペースでの変更を示すため に、0.y.zというバージョン番号を使用しています。

警告

Solidityは最近バージョン0.8.xをリリースしましたが、変更点が多くあります。 必ず その詳細なリスト を読んでください。

Solidityやこのドキュメントを改善するためのアイデアはいつでも歓迎します。 詳細は コントリビューターガイド を読んでください。

ヒント

このドキュメントは、左下のバージョン表示メニューをクリックして、希望のダウンロード形式を選択すると、PDF、HTML、Epubのいずれかでダウンロードできます。

はじめに

1. スマートコントラクトの基本を理解する

スマートコントラクトの概念を初めて知る方には、まず「スマートコントラクトの紹介」を掘り下げて読むことをお勧めします。 これには次のコンテンツが含まれています。

2. Solidityを知る

基本的な操作に慣れてきたら、 「Solidity by Example」や「言語仕様」にあるセクションを読んで、言語のコアコンセプトを理解することをお勧めします。

3. Solidityコンパイラをインストールする

Solidityコンパイラをインストールするには様々な方法があります。 好みのオプションを選択して、 インストールページ に記載されている手順に従ってください。

ヒント

Remix IDE を使えば、ブラウザ上で直接コードの例を試すことができます。 RemixはWebブラウザベースのIDEで、Solidityをローカルにインストールすることなく、Solidityスマートコントラクトの作成、デプロイ、管理を行うことができます。

警告

人間がソフトウェアを書くと、バグが発生することがあります。 そのため、スマートコントラクトを作成する際には、確立されたソフトウェア開発のベストプラクティスに従うべきです。 これには、コードレビュー、テスト、監査、およびコレクトネス(実装が仕様に沿っていること)の証明が含まれます。 スマートコントラクトのユーザーは、コードの作成者よりもコードを信頼している場合があります。 また、ブロックチェーンやスマートコントラクトには、注意すべき特有の問題があるため、本番のコードに取り組む前に、必ず セキュリティへの配慮 のセクションを読んでください。

4. さらに学ぶ

Ethereumでの分散型アプリケーションの構築について詳しく知りたい場合は、 Ethereum Developer Resources で、Ethereumに関するさらに一般的なドキュメントや、幅広い種類のチュートリアル、ツール、開発フレームワークを紹介しています。

疑問点があれば、検索したり、 Ethereum StackExchangeGitterチャンネル で聞いてみると良いでしょう。

翻訳

このドキュメントは、コミュニティのコントリビューターによって、いくつかの言語に翻訳されています。 これらの言語は、完成度と最新度が異なります。 正確な情報を得たい場合は、英語版を参考にしてください。

左下のフライアウトメニューをクリックし、好みの言語を選択することで言語を切り替えることができます。

注釈

私たちは、コミュニティの取り組みを効率化するために、GitHubのオーガナイゼーションと翻訳ワークフローをセットアップしました。 新しい言語での翻訳を始めたり、コミュニティの翻訳に貢献する方法については、 solidity-docsオーガナイゼーション にある翻訳ガイドを参照してください。

コンテンツ

キーワードインデックス, 検索ページ

スマートコントラクトの紹介

シンプルなスマートコントラクト

まずは、他のコントラクトが変数の値を読み書きできる基本的なコントラクトの例から始めましょう。 今はまだ全てを理解しなくても構いません、後でもっと詳しく説明します。

ストレージの例

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

contract SimpleStorage {
    uint storedData;

    function set(uint x) public {
        storedData = x;
    }

    function get() public view returns (uint) {
        return storedData;
    }
}

最初の行は、ソースコードがGPLバージョン3.0でライセンスされていることを示しています。 このライセンス指定子は機械的に読み取り可能であり、ソースコードの公開がデフォルトとなっている環境では重要です。

2行目では、ソースコードがSolidityバージョン0.4.16からバージョン0.9.0の前までのバージョンで書かれたものであることを示しています(バージョン0.9.0は含みません)。 これは、コントラクトが新しい(破壊的変更があった)コンパイラのバージョンでコンパイルできないことを保証するためです。 pragma は、コンパイラに対してソースコードをどのように扱うかを規定する指示であり、Solidityに限らず一般的に使われているものです(例: pragma once )。

Solidityにおけるコントラクトとは、Ethereumブロックチェーン上の特定のアドレスに存在するコード( 関数 )とデータ( ステート )の集合です。 uint storedData; という行は、 uint (256ビットの非負整数)型の storedData という状態変数を宣言しています。 これは、データベースの1つのスロットとして考えることができます。 データベースを管理するコードの関数を呼び出すと、そのスロットの取得や変更ができるようなイメージです。 この例では、コントラクトによって、変数の値を変更したり取得したりするのに使用できる関数 setget が定義されています。

現在のコントラクトのメンバ(状態変数など)にアクセスする場合、通常は this. という接頭辞を付けずに、その名前で直接アクセスします。 他のいくつかの言語とは異なり、これを省略することは単なるスタイルの問題ではなく、メンバへのアクセス方法が全く異なるのです。

このコントラクトを使えば、(Ethereumが構築したインフラにより)世界中の誰もがアクセス可能な1つの番号を誰もが保存できます。 その番号の公開を防ぐ現実的な方法はないのです。 ただし、この用途を除けば大したことはできません。 誰もが set に別の値で再度コールをし、あなたの番号を上書きできますが、元の番号はブロックチェーンの履歴に保存されたままです。 後で、自分だけが番号を変更できるようにアクセス制限をかける方法を見てみましょう。

警告

Unicodeテキストを使用する際には、見た目が似ている(あるいは同じ)文字でもコードポイントが異なる場合があり、その場合は異なるバイト配列としてエンコードされるので注意が必要です。

注釈

すべての識別子(コントラクト名、関数名、変数名)は、ASCII文字に制限されています。 文字列変数にUTF-8でエンコードされたデータを格納することは可能です。

サブ通貨の例

以下のコントラクトは、最も単純な形の暗号通貨を実装したものです。 このコントラクトでは、作成者のみが新しいコインを作成できます(異なる発行スキームに変えることが可能です)。 誰もがユーザー名とパスワードを登録することなく、Ethereumのキーペアさえあればコインを送り合うことができます。

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

contract Coin {
    // キーワード「public」は、変数を他のコントラクトからアクセスできるようにします
    address public minter;
    mapping(address => uint) public balances;

    // イベントは、コントラクトの特定の変更にクライアントが反応できるようにします
    event Sent(address from, address to, uint amount);

    // コンストラクタのコードは、コントラクトが作成されるときにのみ実行されます
    constructor() {
        minter = msg.sender;
    }

    // 指定した量のコインを新しく作成して、指定したアドレスの残高に追加します
    // コントラクトの作成者のみが呼び出せます
    function mint(address receiver, uint amount) public {
        require(msg.sender == minter);
        balances[receiver] += amount;
    }

    // エラーは、操作に失敗した理由の情報を提供できます
    // エラーは関数のコール側に返されます
    error InsufficientBalance(uint requested, uint available);

    // コールしてきたアカウントから指定したアドレスに指定した量のコインを送ります
    function send(address receiver, uint amount) public {
        if (amount > balances[msg.sender])
            revert InsufficientBalance({
                requested: amount,
                available: balances[msg.sender]
            });

        balances[msg.sender] -= amount;
        balances[receiver] += amount;
        emit Sent(msg.sender, receiver, amount);
    }
}

今回のコントラクトでは、いくつかの新しい概念が導入されています。 それらを一つずつ見ていきましょう。

address public minter; という行は、 address という型の状態変数を宣言しています。 address 型は160ビットの値で、算術演算を行うことができません。 コントラクトのアドレスや、 外部アカウント に属するキーペアの公開鍵のハッシュを格納するのに適しています。

キーワード public を指定すると、コントラクトの外部から状態変数の現在の値にアクセスできる関数が自動的に生成されます。 このキーワードがないと、他のコントラクトはその変数にアクセスする方法がありません。 コンパイラが生成する関数のコードは以下のようになります(今のところ externalview は無視してください)。

function minter() external view returns (address) { return minter; }

上記のような関数を自分で追加することもできはしますが、関数と状態変数が同じ名前になってしまいます。 コンパイラが同じことを代わりにやってくれるので、そのようなことはする必要はありません。

次の行の mapping(address => uint) public balances; もパブリックな状態変数を作成しますが、より複雑なデータ型です。 この mapping 型は、アドレスを 符号なし整数 にマッピングします。

マッピングは、考えられるすべてのキーが最初から存在し、バイト表現がすべてゼロである値にマッピングされるように仮想的に初期化された ハッシュテーブル と見なすことができます。 しかし、マッピングのすべてのキーのリストを得ることも、すべての値のリストを得ることもできません。 マッピングに追加したものを記録するか、そのようなことが必要ないコンテキストで使用してください。 あるいは、リストで保持するか、より適切なデータ型を使用することをお勧めします。

public キーワードで作成した ゲッター関数 は、マッピングの場合は複雑です。 次のようになります。

function balances(address account) external view returns (uint) {
    return balances[account];
}

この関数を使って、ある1つのアカウントの残高を取得できます。

event Sent(address from, address to, uint amount); という行は、 イベント を宣言しており、このイベントは関数 send の最後の行で発生します。 WebアプリケーションなどのEthereumクライアントは、ブロックチェーン上で発せられるこれらのイベントを、それほどコストをかけずにリッスンできます。 イベントが発せられると同時に、リスナーは引数の from, to, amount を受け取るため、トランザクションの追跡が可能になります。

このイベントをリッスンするには、次のJavaScriptコードを使用します。 web3.js を使って Coin のコントラクトオブジェクトを作成し、上記の関数定義から自動生成された balances 関数を呼び出します:

Coin.Sent().watch({}, '', function(error, result) {
    if (!error) {
        console.log("Coin transfer: " + result.args.amount +
            " coins were sent from " + result.args.from +
            " to " + result.args.to + ".");
        console.log("Balances now:\n" +
            "Sender: " + Coin.balances.call(result.args.from) +
            "Receiver: " + Coin.balances.call(result.args.to));
    }
})

コンストラクタ は、コントラクトの作成時に実行され、その後は呼び出すことができない特別な関数です。 上記のコントラクトの場合、コントラクトを作成した人のアドレスを永続的に保存します。 msg 変数は( txblock と一緒に) 特別なグローバル変数 であり、ブロックチェーンへのアクセスを可能にするプロパティを含んでいます。 msg.sender は常に、現在の(外部の)関数呼び出しが行われたアドレスです。

コントラクトを構成する関数で、ユーザーやコントラクトが呼び出すことのできるものは、 mintsend です。

mint 関数は、指定した量のコインを新しく作成して、指定したアドレスに追加します。 require 関数のコールでは、条件を定義し、満たされない場合はすべての変更をリバートします。 この例では、 require(msg.sender == minter); により、コントラクトの作成者だけが mint を呼び出せるようになっています。 一般的には、作成者は好きなだけトークンをミントできますが、ある時点で「オーバーフロー」と呼ばれる現象が発生します。 デフォルトの 算術チェック により、式 balances[receiver] += amount; がオーバーフローした場合、つまり、任意精度の算術演算で balances[receiver] + amountuint の最大値( 2**256 - 1 )よりも大きくなった場合には、トランザクションはリバートしてしまうことに注意してください。 これは、関数 send の中の balances[receiver] += amount; という文にも当てはまります。

エラー を使うと、条件や演算が失敗したときに呼び出し側に詳しい情報を提供できます。 エラーは revert文 と一緒に使用されます。 revert 文は require 関数と同様にすべての変更を無条件に中止してリバートさせますが、エラーの名前や、呼び出し側(最終的にはフロントエンドアプリケーションやブロックエクスプローラ)に提供される追加データを提供することもできるので、失敗をデバッグしたり対処したりすることがより簡単にできます。

send 関数は、(すでにコインを持っている人なら)誰でも他の人にコインを送るために使えます。 送金者が送金するのに十分なコインを持っていない場合は、 if の条件がtrueと評価されます。 結果として、 revert は操作を失敗させ、送金者には InsufficientBalance というエラーの詳細を伝えます。

注釈

このコントラクトを使ってあるアドレスにコインを送っても、ブロックチェーンエクスプローラではそのアドレスを見ても何もわかりません。 なぜなら、コインを送ったという記録と変更された残高は、このコインコントラクトのデータストレージにのみ保存されているからです。 イベントを使えば、新しいコインのトランザクションや残高を追跡する「ブロックチェーンエクスプローラ」を作ることができますが、コインの所有者のアドレスを調べるのではなく、コインコントラクトのアドレスを調べる必要があります。

ブロックチェーンの基本

概念としてのブロックチェーンは、プログラマーにとってはそれほど難しいものではありません。 なぜなら、複雑な仕組み(マイニング、 ハッシュ楕円曲線暗号peer-to-peerネットワーク など)のほとんどは、プラットフォームに一定の機能や約束事を提供するために存在しているだけだからです。 これらの機能を当たり前のように受け入れれば、基盤となる技術について心配する必要はありません。 AmazonのAWSを使うためには、内部でどのように機能しているかを知る必要があるでしょうか?

トランザクション

ブロックチェーンとは、グローバルに共有されたトランザクション用のデータベースです。 つまり、ネットワークに参加するだけで、誰もがデータベースのエントリーを読むことができるのです。 データベース内の何かを変更したい場合は、いわゆるトランザクションを作成し、他のすべての人に受け入れられなければなりません。 トランザクションという言葉は、あなたが行いたい変更(2つの値を同時に変更したいと仮定)が、全く行われないか、完全に適用されるかのどちらかであることを意味しています。 さらに、あなたのトランザクションがデータベースに実行されている間は、他のトランザクションは干渉できません。

例として、ある電子通貨のすべてのアカウントの残高を一覧にしたテーブルがあるとします。 あるアカウントから別のアカウントへの送金がリクエストされた場合、データベースのトランザクションの性質上、あるアカウントから金額が差し引かれた場合、必ず別のアカウントに追加されます。 何らかの理由で対象となるアカウントに金額を追加できない場合は、元のアカウントも変更されません。

さらに、トランザクションは常に送信者(作成者)によって暗号学的に署名されています。 これにより、データベースの特定の変更に対するアクセスを簡単に保護できます。 電子通貨の例では、簡単なチェックで、アカウントの鍵を持っている人だけがそのアカウントからその通貨を送金できるようになっています。

ブロック

克服しなければならない大きな障害のひとつが、ビットコイン用語で「二重支出攻撃」と呼ばれるものです。 ネットワーク上に2つのトランザクションが存在し、どちらもアカウントを空にしようとしていたらどうなるでしょうか? 有効なトランザクションは1つだけで、通常は最初に受け入れられたトランザクションが有効です。 問題は、peer-to-peerネットワークでは「最初」という言葉が客観的ではないことです。

これに対する抽象的な答えは、「気にする必要はない」というものです。 グローバルに決められているトランザクションの順序が選択され、そのコンフリクトを解決してくれます。 トランザクションは「ブロック」と呼ばれるものにまとめられ、実行されて参加しているすべてのノードに分配されることになります。 2つのトランザクションが互いに矛盾する場合、2番目になった方が拒否され、ブロックに含まれません。

これらのブロックは、時間的に直線的な列を形成しており、これが「ブロックチェーン」という言葉の由来となっています。 ブロックは一定の間隔でチェーンに追加されますが、この間隔は将来変更される可能性があります。 最新の情報については、 Etherscan などでネットワークをモニタリングすることをお勧めします。

「オーダーセレクションメカニズム」(これを「マイニング」と呼びます)の一環として、ブロックが時々リバートされることがありますが、それはチェーンの「端」に限ったことです。 特定のブロックの上にブロックが追加されればされるほど、そのブロックがリバートされる可能性は低くなります。 つまり、あなたのトランザクションがリバートされ、さらにはブロックチェーンから削除されることもあるかもしれませんが、時間が経てば経つほど、その可能性は低くなります。

注釈

トランザクションが次のブロックや将来の特定のブロックに含まれることは保証されていません。 なぜなら、そのトランザクションがどのブロックに含まれるかを決めるのは、トランザクションの提出者ではなく、マイナーに任されているからです。 コントラクトの将来の呼び出しをスケジュールしたい場合は、 スマートコントラクトの自動化ツールやオラクルサービスを利用できます。

Ethereum Virtual Machine

概要

Ethereum Virtual Machine(EVM)は、Ethereumにおけるスマートコントラクトの実行環境です。 EVMはサンドボックス化されているだけでなく、完全に隔離されています。 つまり、EVM内で実行されるコードは、ネットワーク、ファイルシステム、または他のプロセスにアクセスできません。 スマートコントラクトは、他のスマートコントラクトへのアクセスも制限されています。

アカウント

Ethereumには、同じアドレス空間を共有する2種類のアカウントがあります。 それは、公開鍵と秘密鍵のペア(つまり人間)によって管理される 外部アカウント と、アカウントと一緒に保存されているコードによって管理される コントラクトアカウント です。

外部アカウントのアドレスは公開鍵から決定されますが、コントラクトのアドレスはコントラクトが作成された時点で決定されます(作成者のアドレスとそのアドレスから送信されたトランザクションの数、いわゆる「nonce」から導き出されます)。

アカウントにコードが格納されているかどうかにかかわらず、EVMでは2つの型が同じように扱われます。

すべてのアカウントには、256ビットのワードと256ビットのワードをマッピングする永続的なキーバリューストアがあり、これを ストレージ と呼びます。

さらに、すべてのアカウントはEther(正確には「Wei」で、 1 ether10**18 wei )で 残高 を持っており、Etherを含むトランザクションを送信することで更新されます。

トランザクション

トランザクションとは、あるアカウントから別のアカウント(同じアカウントの場合もあれば、空のアカウントの場合もある、以下参照)に送信されるメッセージです。 このメッセージには、バイナリデータ(これを「ペイロード」と呼びます)とEtherが含まれます。

対象となるアカウントにコードが含まれている場合、そのコードが実行され、ペイロードが入力データとして提供されます。

対象となるアカウントが設定されていない(トランザクションに受取人がいない、または受取人が「null」に設定されている)場合、そのトランザクションは 新しいコントラクト を作成します。 すでに述べたように、そのコントラクトのアドレスはゼロのアドレスではなく、送信者とその送信したトランザクション数から得られるアドレス(「nonce」)です。 このようなコントラクト作成トランザクションのペイロードは、EVMバイトコードとみなされ、実行されます。 この実行の出力データは、コントラクトのコードとして永続的に保存されます。 つまり、コントラクトを作成するためには、コントラクトの実際のコードを送信するのではなく、実際には、実行されるとその実際のコードを返すコードを送信することになります。

注釈

コントラクトが作成されている間、そのコードはまだ空です。 そのため、コンストラクタの実行が終了するまで、作成中のコントラクトにコールバックしてはいけません。

ガス

トランザクションの作成時に、各トランザクションには一定量の ガス がチャージされ、トランザクションの作成者( tx.origin )が支払う必要があります。 EVMがトランザクションを実行している間、ガスは特定のルールに従って徐々に減っていきます。

いずれかの時点でガスが使い切られると(つまりマイナスになると)、ガス切れの例外が発生して、実行が停止し、現在のコールフレームでステートに加えられたすべての変更がリバートされます。

このメカニズムは、EVMの実行時間の経済的な使用を奨励し、EVMのエグゼキューター(すなわち、マイナーあるいはステーカー)の作業に対する補償を行うものです。 各ブロックには最大量のガスがあるため、ブロックの検証に必要な作業量も制限されます。

ガスプライス はトランザクションの作成者が設定する値であり、作成者はEVM実行者に gas_price * gas を前払いする必要があります。 実行後にガスが残っている場合、それはトランザクションの作成者に返金されます。 変更をリバートする例外が発生した場合、既に使用されたガスは払い戻されません。

EVMのエグゼキューターはトランザクションを含めるかどうかを選択できるため、トランザクション送信者は低いガス価格を設定することでシステムを悪用することはできません。

ストレージ、メモリ、スタック

Ethereum Virtual Machineには、データを保存できる3つの領域「ストレージ」「メモリ」「スタック」があります。

各アカウントには ストレージ と呼ばれるデータ領域があり、関数呼び出しやトランザクション間で永続的に使用されます。 storageは256ビットのワードを256ビットのワードにマッピングするkey-value storeです。 コントラクト内からストレージを列挙できず、読み込みには比較的コストがかかり、ストレージの初期化や変更にはさらにコストがかかります。 このコストのため、永続的なストレージに保存するものは、コントラクトが実行するために必要なものに限定するべきです。 派生する計算、キャッシング、アグリゲートなどのデータはコントラクトの外に保存します。 コントラクトは、コントラクト以外のストレージに対して読み書きできません。

2つ目のデータ領域は メモリ と呼ばれ、コントラクトはメッセージを呼び出すたびにクリアされたばかりのインスタンスを取得します。 メモリは線形で、バイトレベルでアドレスを指定できますが、読み出しは256ビットの幅に制限され、書き込みは8ビットまたは256ビットの幅に制限されます。 メモリは、これまで手つかずだったメモリワード(ワード内の任意のオフセット)にアクセス(読み出しまたは書き込み)すると、ワード(256ビット)単位で拡張されます。 拡張時には、ガスによるコストを支払わなければなりません。 メモリは大きくなればなるほどコストが高くなります(二次関数的にスケールする)。

EVMはレジスタマシンではなく、スタックマシンなので、すべての計算は スタック と呼ばれるデータ領域で行われます。 スタックの最大サイズは1024要素で、256ビットのワードを含みます。 スタックへのアクセスは次のように上端に制限されています。 一番上の16個の要素の1つをスタックの一番上にコピーしたり、一番上の要素をその下の16個の要素の1つと入れ替えたりすることが可能です。 それ以外の操作では、スタックから最上位の2要素(操作によっては1要素、またはそれ以上)を取り出し、その結果をスタックにプッシュします。 もちろん、スタックの要素をストレージやメモリに移動させて、スタックに深くアクセスすることは可能ですが、最初にスタックの最上部を取り除かずに、スタックの深いところにある任意の要素にアクセスすることはできません。

命令セット

EVMの命令セットは、コンセンサスの問題を引き起こす可能性のある不正確な実装や矛盾した実装を避けるために、最小限に抑えられています。 すべての命令は、基本的なデータ型である256ビットのワード、またはメモリのスライス(または他のバイトアレイ)で動作します。 通常の算術演算、ビット演算、論理演算、比較演算が可能です。 条件付きおよび無条件のジャンプが可能です。 さらにコントラクトでは、番号やタイムスタンプなど、現在のブロックの関連プロパティにアクセスできます。

完全なリストについては、インラインアセンブリのドキュメントの一部である オペコードの一覧 を参照してください。

メッセージコール

コントラクトは、メッセージコールによって、他のコントラクトを呼び出したり、コントラクト以外のアカウントにEtherを送金できます。 メッセージコールは、ソース、ターゲット、データペイロード、Ether、ガス、およびリターンデータを持つという点で、トランザクションと似ています。 実際、すべてのトランザクションは、トップレベルのメッセージコールで構成されており、そのメッセージコールがさらにメッセージコールを作成できます。

コントラクトは、その残りの ガス のうち、どれだけを内部メッセージ呼び出しで送信し、どれだけを保持したいかを決定できます。 内側の呼び出しでガス切れの例外(またはその他の例外)が発生した場合は、スタックに置かれたエラー値によって通知されます。 この場合、呼び出しと一緒に送られたガスだけが使い切られます。 Solidityでは、このような状況では、呼び出し側のコントラクトがデフォルトで手動例外を発生させ、例外がコールスタックを「バブルアップ」するようにしています。

すでに述べたように、呼び出されたコントラクト(呼び出し側と同じ場合もある)は、メモリのクリアされたばかりのインスタンスを受け取り、呼び出しペイロード( calldata と呼ばれる別の領域に提供される)にアクセスできます。 実行終了後、呼び出し元のメモリ内で呼び出し元が事前に割り当てた場所に保存されるデータを返すことができます。 このような呼び出しはすべて完全に同期しています。

呼び出しの深さは1024までに 制限 されます。 つまり、より複雑な操作を行う場合には、再帰的な呼び出しよりもループの方が望ましいということです。 さらに、メッセージコールではガスの63/64だけを転送できるため、実際には1000よりも少し少ない深さの制限が発生します。

delegatecallとライブラリ

メッセージコールには、 delegatecall という特別なバリエーションがあります。 これは、ターゲットアドレスのコードが呼び出し元のコントラクトのコンテキスト(すなわち、そのアドレス)で実行され、 msg.sendermsg.value の値が変更されないという点を除けば、メッセージコールと同じです。

これは、ターゲットアドレスのコードが呼び出し元のコントラクトのコンテキスト(つまりアドレス)で実行され、 msg.sendermsg.value が値を変えないという事実を除けば、メッセージコールと同じである delegatecall という特殊なバリエーションが存在します。

これは、コントラクトが実行時に異なるアドレスからコードを動的にロードできることを意味します。 ストレージ、現在のアドレス、バランスは依然として呼び出したコントラクトのものを参照しており、コードだけが呼び出されたアドレスから取得されます。

これにより、Solidityに「ライブラリ」機能を実装することが可能になりました。 再利用可能なライブラリコードで、複雑なデータ構造を実装するためにコントラクトのストレージに適用することなどが可能です。

ログ

ブロックレベルまでマッピングされた特別なインデックス付きのデータ構造にデータを保存することが可能です。 この ログ と呼ばれる機能は、Solidityでは イベント を実装するために使用されています。 コントラクトはログデータが作成された後はアクセスできませんが、ブロックチェーンの外部から効率的にアクセスできます。 ログデータの一部は Bloom Filter に格納されているため、効率的かつ暗号的に安全な方法でこのデータを検索することが可能であり、ブロックチェーン全体をダウンロードしないネットワークピア(いわゆる「ライトクライアント」)でもこれらのログを見つけることができます。

create

コントラクトは、特別なオペコードを使用して他のコントラクトを作成することもできます(つまり、トランザクションのように単純にゼロアドレスを呼び出すわけではありません)。 これらの createコール と通常のメッセージコールとの唯一の違いは、ペイロードデータが実行され、その結果がコードとして保存され、呼び出し側/作成側がスタック上の新しいコントラクトのアドレスを受け取ることです。

非アクティブ化と自己破壊

ブロックチェーンからコードを削除する唯一の方法は、そのアドレスのコントラクトが selfdestruct 命令を実行することです。 そのアドレスに保存されている残りのEtherは、指定されたターゲットに送られ、その後、ストレージとコードがステートから削除されます。 理論的にはコントラクトを削除することは良いアイデアのように聞こえますが、削除されたコントラクトに誰かがEtherを送ると、そのEtherは永遠に失われてしまうため、潜在的には危険です。

警告

バージョン 0.8.18 以降、Solidity と Yul の両方で selfdestruct を使用すると、 EIP-6049 で述べられているように、 SELFDESTRUCT オペコードがいずれ動作に破壊的変更を受けるため、非推奨の警告が発せられます。

警告

selfdestruct によってコントラクトが削除されたとしても、それはブロックチェーンの歴史の一部であり、おそらくほとんどのEthereumノードが保持しています。 そのため、 selfdestruct を使うことは、ハードディスクからデータを削除することと同じではありません。

注釈

コントラクトのコードに selfdestruct の呼び出しが含まれていなくても、 delegatecallcallcode を使ってその操作を行うことができます。

コントラクトを非アクティブ化したい場合、すべての関数をリバートするように内部状態を変更することで実現できます。 これにより、コントラクトは即座にEtherを返すようになるため、使用できなくなります。

プリコンパイル済みコントラクト

コントラクトのアドレスの中には、特別なものがあります。 1 から 8 までのアドレスには「プリコンパイル済みコントラクト」が含まれており、他のコントラクトと同様に呼び出すことができますが、その動作(およびガス消費量)は、そのアドレスに格納されているEVMコードによって定義されるのではなく(コードが含まれていない)、EVMの実行環境自体に実装されています。

EVMと互換性のあるチェーンでは、異なるプリコンパイル済みコントラクトのセットを使用する可能性があります。 また、将来Ethereumのメインチェーンに新しいプリコンパイル済みコントラクトが追加される可能性もありますが、常に 1 から 0xffff (包括的)の範囲内であると考えるのが妥当でしょう。

Solidity by Example

投票

次のコントラクトは非常に複雑ですが、Solidityの機能の多くを紹介しています。 これは、投票コントラクトを実装しています。 もちろん、電子投票の主な問題点は、いかにして正しい人に投票権を割り当てるか、いかにして操作を防ぐかです。 ここですべての問題を解決するわけではありませんが、少なくとも、投票数のカウントが 自動的 に行われ、同時に 完全に透明 であるように、委任投票を行う方法を紹介する予定です。

アイデアとしては、1つの投票用紙に対して1つのコントラクトを作成し、それぞれの選択肢に短い名前をつけます。 そして、議長を務めるコントラクトの作成者が、各アドレスに個別に投票権を与えます。

そして、そのアドレスを持つ人は、自分で投票するか、信頼できる人に投票を委任するかを選ぶことができます。

投票時間終了後、最も多くの票を獲得したプロポーザルを winningProposal() に返します。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
/// @title 委任による投票
contract Ballot {
    // 新しい複合型を宣言し、後で変数に使用します。
    // 一人の投票者を表します。
    struct Voter {
        uint weight; // ウェイトは委任により蓄積される
        bool voted;  // trueならすでにその人は投票済み
        address delegate; // 委任先
        uint vote;   // 投票したプロポーザルのインデックス
    }

    // 1つのプロポーザルの型です。
    struct Proposal {
        bytes32 name;   // 短い名前(上限32バイト)
        uint voteCount; // 投票数
    }

    address public chairperson;

    // アドレスごとに `Voter` 構造体を格納する状態変数を宣言しています。
    mapping(address => Voter) public voters;

    // `Proposal` 構造体の動的サイズの配列
    Proposal[] public proposals;

    /// `proposalNames` のいずれかを選択するための新しい投票を作成します。
    constructor(bytes32[] memory proposalNames) {
        chairperson = msg.sender;
        voters[chairperson].weight = 1;

        // 指定されたプロポーザル名ごとに新しいプロポーザルオブジェクトを作成し、配列の末尾に追加します。
        for (uint i = 0; i < proposalNames.length; i++) {
            // `Proposal({...})` は一時的なプロポーザルオブジェクトを作成し、 `proposals.push(...)` はそれを `proposals` の末尾に追加します。
            proposals.push(Proposal({
                name: proposalNames[i],
                voteCount: 0
            }));
        }
    }

    // `voter` にこの投票用紙に投票する権利を与えます。
    // `chairperson` だけが呼び出すことができます。
    function giveRightToVote(address voter) external {
        // `require` の第一引数の評価が `false` の場合、実行は終了し、状態やEther残高のすべての変更がリバートされます。
        // これは、古いEVMのバージョンでは全てのガスを消費していましたが、今はそうではありません。
        // 関数が正しく呼び出されているかどうかを確認するために、 `require` を使用するのは良いアイデアです。
        // 第二引数として、何が悪かったのかについての説明を記述することもできます。
        require(
            msg.sender == chairperson,
            "Only chairperson can give right to vote."
        );
        require(
            !voters[voter].voted,
            "The voter already voted."
        );
        require(voters[voter].weight == 0);
        voters[voter].weight = 1;
    }

    /// 投票者 `to` に投票を委任します。
    function delegate(address to) external {
        // 参照を代入
        Voter storage sender = voters[msg.sender];
        require(sender.weight != 0, "You have no right to vote");
        require(!sender.voted, "You already voted.");

        require(to != msg.sender, "Self-delegation is disallowed.");

        // `to` もデリゲートされている限り、デリゲートを転送します。
        // 一般的に、このようなループは非常に危険です。
        // なぜなら、ループが長くなりすぎると、ブロック内で利用できる量よりも多くのガスが必要になる可能性があるからです。
        // この場合、デリゲーションは実行されません。
        // しかし、他の状況では、このようなループによってコントラクトが完全に「スタック」してしまう可能性があります。
        while (voters[to].delegate != address(0)) {
            to = voters[to].delegate;

            // 委任でループを発見した場合、委任は許可されません。
            require(to != msg.sender, "Found loop in delegation.");
        }

        Voter storage delegate_ = voters[to];

        // 投票者は、投票できないアカウントに委任できません。
        require(delegate_.weight >= 1);

        // `sender` は参照なので、`voters[msg.sender]` を修正します。
        sender.voted = true;
        sender.delegate = to;

        if (delegate_.voted) {
            // 代表者が既に投票している場合は、直接投票数に加算する
            proposals[delegate_.vote].voteCount += sender.weight;
        } else {
            // 代表者がまだ投票していない場合は、その人の重みに加える
            delegate_.weight += sender.weight;
        }
    }

    /// あなたの投票権(あなたに委任された投票権を含む)をプロポーザル `proposals[proposal].name` に与えてください。
    function vote(uint proposal) external {
        Voter storage sender = voters[msg.sender];
        require(sender.weight != 0, "Has no right to vote");
        require(!sender.voted, "Already voted.");
        sender.voted = true;
        sender.vote = proposal;

        // もし `proposal` が配列の範囲外であれば、自動的にスローされ、すべての変更が取り消されます。
        proposals[proposal].voteCount += sender.weight;
    }

    /// @dev 以前の投票をすべて考慮した上で、当選案を計算します。
    function winningProposal() public view
            returns (uint winningProposal_)
    {
        uint winningVoteCount = 0;
        for (uint p = 0; p < proposals.length; p++) {
            if (proposals[p].voteCount > winningVoteCount) {
                winningVoteCount = proposals[p].voteCount;
                winningProposal_ = p;
            }
        }
    }

    // winningProposal()関数をコールして、プロポーザルの配列に含まれる当選案のインデックスを取得し、当選案の名前を返します。
    function winnerName() external view
            returns (bytes32 winnerName_)
    {
        winnerName_ = proposals[winningProposal()].name;
    }
}

改良の可能性

現状では、すべての参加者に投票権を割り当てるために多くのトランザクションが必要です。 また、2つ以上の提案の投票数が同じ場合、 winningProposal() は同票を登録できません。 これらの問題を解決する方法を考えてみてください。

ブラインドオークション

このセクションでは、Ethereum上で完全なブラインドオークションコントラクトをいかに簡単に作成できるかを紹介します。 まず、誰もが入札を見ることができるオープンオークションから始めて、このコントラクトを、入札期間が終了するまで実際の入札を見ることができないブラインドオークションに拡張します。

シンプルなオープンオークション

以下のシンプルなオークションコントラクトの一般的な考え方は、入札期間中に誰もが入札を行うことができるというものです。 入札には、入札者を拘束するために対価(例えばEther)を送ることが含まれています。 最高入札額が上がった場合、それまでの最高入札者はEtherを返してもらいます。 入札期間の終了後、受益者がEtherを受け取るためには、コントラクトを手動で呼び出さなければなりません。 コントラクトは自ら動作することはできません。

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract SimpleAuction {
    // オークションのパラメータ。
    // 時刻は絶対的なunixタイムスタンプ(1970-01-01からの秒数)または秒単位の時間です。
    address payable public beneficiary;
    uint public auctionEndTime;

    // オークションの現状
    address public highestBidder;
    uint public highestBid;

    // 以前の入札のうち取り下げを許可したもの
    mapping(address => uint) pendingReturns;

    // 最後にtrueを設定すると、一切の変更が禁止されます。
    // デフォルトでは `false` に初期化されています。
    bool ended;

    // 変更時に発信されるイベント
    event HighestBidIncreased(address bidder, uint amount);
    event AuctionEnded(address winner, uint amount);

    // 失敗を表すエラー

    // トリプルスラッシュのコメントは、いわゆるnatspecコメントです。
    // これらは、ユーザーがトランザクションの確認を求められたときや、エラーが表示されたときに表示されます。

    /// オークションはすでに終了しています。
    error AuctionAlreadyEnded();
    /// すでに上位または同等の入札があります。
    error BidNotHighEnough(uint highestBid);
    /// オークションはまだ終了していません。
    error AuctionNotYetEnded();
    /// 関数 auctionEnd はすでに呼び出されています。
    error AuctionEndAlreadyCalled();

    /// 受益者アドレス `beneficiaryAddress` に代わって `biddingTime` 秒の入札時間を持つシンプルなオークションを作成します。
    constructor(
        uint biddingTime,
        address payable beneficiaryAddress
    ) {
        beneficiary = beneficiaryAddress;
        auctionEndTime = block.timestamp + biddingTime;
    }

    /// この取引と一緒に送られたvalueでオークションに入札します。
    /// 落札されなかった場合のみ、valueは返金されます。
    function bid() external payable {
        // 引数は必要なく、すべての情報はすでにトランザクションの一部となっています。
        // キーワードpayableは、この関数がEtherを受け取ることができるようにするために必要です。

        // 入札期間が終了した場合、コールをリバートします。
        if (block.timestamp > auctionEndTime)
            revert AuctionAlreadyEnded();

        // 入札額が高くなければ、Etherを送り返します(リバート文は、Etherを受け取ったことを含め、この関数の実行のすべての変更をリバートします)。
        if (msg.value <= highestBid)
            revert BidNotHighEnough(highestBid);

        if (highestBid != 0) {
            // highestBidder.send(highestBid) を使って単純に送り返すと、信頼できないコントラクトを実行する可能性があり、セキュリティ上のリスクがあります。
            // 受取人が自分でEtherを引き出せるようにするのが安全です。
            pendingReturns[highestBidder] += highestBid;
        }
        highestBidder = msg.sender;
        highestBid = msg.value;
        emit HighestBidIncreased(msg.sender, msg.value);
    }

    /// 最高入札額より低い入札を取り下げます。
    function withdraw() external returns (bool) {
        uint amount = pendingReturns[msg.sender];
        if (amount > 0) {
            // 受信者は `send` が戻る前に、受信コールの一部としてこの関数を再び呼び出すことができるので、これをゼロに設定することが重要です。
            pendingReturns[msg.sender] = 0;

            // msg.sender is not of type `address payable` and must be
            // explicitly converted using `payable(msg.sender)` in order
            // use the member function `send()`.
            if (!payable(msg.sender).send(amount)) {
                // ここでコールを投げる必要はなく、ただリセットすれば良いです。
                pendingReturns[msg.sender] = amount;
                return false;
            }
        }
        return true;
    }

    /// オークションを終了し、最高入札額を受益者に送付します。
    function auctionEnd() external {
        // 他のコントラクトと相互作用する関数(関数をコールしたり、Etherを送ったりする)は、3つのフェーズに分けるのが良いガイドラインです。
        // 1. 条件をチェックする
        // 2. アクションを実行する(条件を変更する可能性がある)。
        // 3. 他のコントラクトと対話する
        // これらのフェーズが混在すると、他のコントラクトが現在のコントラクトにコールバックして状態を変更したり、エフェクト(エーテル払い出し)を複数回実行させたりする可能性があります。
        // 内部で呼び出される関数に外部コントラクトとの相互作用が含まれる場合は、外部コントラクトとの相互作用も考慮しなければなりません。

        // 1. 条件
        if (block.timestamp < auctionEndTime)
            revert AuctionNotYetEnded();
        if (ended)
            revert AuctionEndAlreadyCalled();

        // 2. エフェクト
        ended = true;
        emit AuctionEnded(highestBidder, highestBid);

        // 3. インタラクション
        beneficiary.transfer(highestBid);
    }
}

ブラインドオークション

前回のオープンオークションは、次のようにブラインドオークションに拡張されます。 ブラインドオークションの利点は、入札期間の終わりに向けての時間的プレッシャーがないことです。 透明なコンピューティングプラットフォーム上でブラインドオークションを行うというのは矛盾しているように聞こえるかもしれませんが、暗号技術がその助けとなります。

入札期間 中、入札者は自分の入札を実際には送信せず、ハッシュ化したものだけを送信します。 現在のところ、ハッシュ値が等しい2つの(十分に長い)値を見つけることは実質的に不可能であると考えられているため、入札者はそれによって入札にコミットします。 入札期間の終了後、入札者は自分の入札を明らかにしなければなりません。 入札者は自分の値を暗号化せずに送信し、コントラクトはそのハッシュ値が入札期間中に提供されたものと同じであるかどうかをチェックします。

もう一つの課題は、いかにしてオークションの バインディングとブラインド を同時に行うかということです。 落札した後にEtherを送らないだけで済むようにするには、入札と一緒に送らせるようにするしかありません。 Ethereumでは価値の移転はブラインドできないため、誰でも価値を見ることができます。

以下のコントラクトでは、最高額の入札よりも大きな値を受け入れることで、この問題を解決しています。 もちろん、これは公開段階でしかチェックできないため、いくつかの入札は 無効 になるかもしれませんが、これは意図的なものです(高額な送金で無効な入札を行うための明示的なフラグも用意されています)。 入札者は、高額または低額の無効な入札を複数回行うことで、競争を混乱させることができます。

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract BlindAuction {
    struct Bid {
        bytes32 blindedBid;
        uint deposit;
    }

    address payable public beneficiary;
    uint public biddingEnd;
    uint public revealEnd;
    bool public ended;

    mapping(address => Bid[]) public bids;

    address public highestBidder;
    uint public highestBid;

    // 以前の入札のうち取り下げを許可したもの
    mapping(address => uint) pendingReturns;

    event AuctionEnded(address winner, uint highestBid);

    // 失敗を表すエラー

    /// この関数は早く呼び出されすぎました。
    /// `time` 秒後にもう一度試してください。
    error TooEarly(uint time);
    /// この関数を呼び出すのが遅すぎました。
    /// `time` 秒後に呼び出すことはできません。
    error TooLate(uint time);
    /// 関数 auctionEnd はすでに呼び出されています。
    error AuctionEndAlreadyCalled();

    // モディファイアは、関数への入力を検証するための便利な方法です。
    // 以下の `onlyBefore` は `bid` に適用されます。
    // 新しい関数の本体はモディファイアの本体で、 `_` が古い関数の本体に置き換わります。
    modifier onlyBefore(uint time) {
        if (block.timestamp >= time) revert TooLate(time);
        _;
    }
    modifier onlyAfter(uint time) {
        if (block.timestamp <= time) revert TooEarly(time);
        _;
    }

    constructor(
        uint biddingTime,
        uint revealTime,
        address payable beneficiaryAddress
    ) {
        beneficiary = beneficiaryAddress;
        biddingEnd = block.timestamp + biddingTime;
        revealEnd = biddingEnd + revealTime;
    }

    /// `blindedBid` = keccak256(abi.encodePacked(value, fake, secret)) でブラインド入札を行います。
    /// 送信されたEtherは、リビールフェーズで入札が正しくリビールされた場合にのみ払い戻されます。
    /// 入札と一緒に送られたEtherが少なくとも「value」であり、「fake」がtrueでない場合、入札は有効です。
    /// 「fake」をtrueに設定し、正確な金額を送らないことで、本当の入札を隠しつつ、必要なデポジットを行うことができます。
    /// 同じアドレスで複数の入札を行うことができます。
    function bid(bytes32 blindedBid)
        external
        payable
        onlyBefore(biddingEnd)
    {
        bids[msg.sender].push(Bid({
            blindedBid: blindedBid,
            deposit: msg.value
        }));
    }

    /// ブラインドした入札を公開します。
    /// 正しくブラインドされた無効な入札と、完全に高い入札を除くすべての入札の払い戻しを受けることができます。
    function reveal(
        uint[] calldata values,
        bool[] calldata fakes,
        bytes32[] calldata secrets
    )
        external
        onlyAfter(biddingEnd)
        onlyBefore(revealEnd)
    {
        uint length = bids[msg.sender].length;
        require(values.length == length);
        require(fakes.length == length);
        require(secrets.length == length);

        uint refund;
        for (uint i = 0; i < length; i++) {
            Bid storage bidToCheck = bids[msg.sender][i];
            (uint value, bool fake, bytes32 secret) =
                    (values[i], fakes[i], secrets[i]);
            if (bidToCheck.blindedBid != keccak256(abi.encodePacked(value, fake, secret))) {
                // 入札は実際にリビールされていません。
                // デポジットを返金しません。
                continue;
            }
            refund += bidToCheck.deposit;
            if (!fake && bidToCheck.deposit >= value) {
                if (placeBid(msg.sender, value))
                    refund -= value;
            }
            // 送信者が同じデポジットを再クレームできないようにします。
            bidToCheck.blindedBid = bytes32(0);
        }
        payable(msg.sender).transfer(refund);
    }

    /// オーバーな入札を引き出す。
    function withdraw() external {
        uint amount = pendingReturns[msg.sender];
        if (amount > 0) {
            // これをゼロに設定することが重要です。
            // なぜなら、受信者は `transfer` が戻る前にリシーブしているコールの一部としてこの関数を再び呼び出すことができるからです(前で述べた 条件 -> エフェクト -> インタラクション に関する記述を参照してください)。
            pendingReturns[msg.sender] = 0;

            payable(msg.sender).transfer(amount);
        }
    }

    /// オークションを終了し、最高入札額を受益者に送ります。
    function auctionEnd()
        external
        onlyAfter(revealEnd)
    {
        if (ended) revert AuctionEndAlreadyCalled();
        emit AuctionEnded(highestBidder, highestBid);
        ended = true;
        beneficiary.transfer(highestBid);
    }

    // これは「内部」関数であり、コントラクト自身(または派生コントラクト)からしか呼び出すことができないことを意味します。
    function placeBid(address bidder, uint value) internal
            returns (bool success)
    {
        if (value <= highestBid) {
            return false;
        }
        if (highestBidder != address(0)) {
            // 前回の最高額入札者に払い戻しを行います。
            pendingReturns[highestBidder] += highestBid;
        }
        highestBid = value;
        highestBidder = bidder;
        return true;
    }
}

安全なリモート購入

現在、遠隔地で商品を購入するには、複数の当事者がお互いに信頼し合う必要があります。 最もシンプルなのは、売り手と買い手の関係です。 買い手は売り手から商品を受け取り、売り手はその見返りとして対価(例えばEther)を得たいと考えます。 問題となるのは、ここでの発送です。 商品が買い手に届いたかどうかを確実に判断する方法がありません。

この問題を解決するには複数の方法がありますが、いずれも1つまたは他の方法で不足しています。 次の例では、両当事者がアイテムの2倍の価値をエスクローとして コントラクトに入れなければなりません。 これが起こるとすぐに、買い手がアイテムを受け取ったことを確認するまで、Etherはコントラクトの中にロックされたままになります。 その後、買い手にはvalue(デポジットの半分)が返却され、売り手にはvalueの3倍(デポジット + value)が支払われます。 これは、双方が事態を解決しようとするインセンティブを持ち、そうしないとEtherが永遠にロックされてしまうという考えに基づいています。

このコントラクトはもちろん問題を解決するものではありませんが、コントラクトの中でステートマシンのような構造をどのように使用できるかの概要を示しています。

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract Purchase {
    uint public value;
    address payable public seller;
    address payable public buyer;

    enum State { Created, Locked, Release, Inactive }
    // state変数のデフォルト値は、最初のメンバーである`State.created`です。
    State public state;

    modifier condition(bool condition_) {
        require(condition_);
        _;
    }

    /// 買い手だけがこの機能を呼び出すことができます。
    error OnlyBuyer();
    /// 売り手だけがこの機能を呼び出すことができます。
    error OnlySeller();
    /// 現在の状態では、この関数を呼び出すことはできません。
    error InvalidState();
    /// 提供される値は偶数でなければなりません。
    error ValueNotEven();

    modifier onlyBuyer() {
        if (msg.sender != buyer)
            revert OnlyBuyer();
        _;
    }

    modifier onlySeller() {
        if (msg.sender != seller)
            revert OnlySeller();
        _;
    }

    modifier inState(State state_) {
        if (state != state_)
            revert InvalidState();
        _;
    }

    event Aborted();
    event PurchaseConfirmed();
    event ItemReceived();
    event SellerRefunded();

    // `msg.value` が偶数であることを確認します。
    // 割り算は奇数だと切り捨てられます。
    // 奇数でなかったことを乗算で確認します。
    constructor() payable {
        seller = payable(msg.sender);
        value = msg.value / 2;
        if ((2 * value) != msg.value)
            revert ValueNotEven();
    }

    /// 購入を中止し、Etherを再クレームします。
    /// コントラクトがロックされる前に売り手によってのみ呼び出すことができます。
    function abort()
        external
        onlySeller
        inState(State.Created)
    {
        emit Aborted();
        state = State.Inactive;
        // ここではtransferを直接使っています。
        // この関数の最後のコールであり、すでに状態を変更しているため、reentrancy-safeになっています。
        seller.transfer(address(this).balance);
    }

    /// 買い手として購入を確認します。
    /// 取引には `2 * value` のEtherが含まれていなければなりません。
    /// EtherはconfirmReceivedが呼ばれるまでロックされます。
    function confirmPurchase()
        external
        inState(State.Created)
        condition(msg.value == (2 * value))
        payable
    {
        emit PurchaseConfirmed();
        buyer = payable(msg.sender);
        state = State.Locked;
    }

    /// あなた(買い手)が商品を受け取ったことを確認します。
    /// これにより、ロックされていたEtherが解除されます。
    function confirmReceived()
        external
        onlyBuyer
        inState(State.Locked)
    {
        emit ItemReceived();
        // 最初に状態を変更することが重要です。
        // そうしないと、以下の `send` を使用して呼び出されたコントラクトが、ここで再び呼び出される可能性があるからです。
        state = State.Release;

        buyer.transfer(value);
    }

    /// この機能は、売り手に返金する、つまり売り手のロックされた資金を払い戻すものです。
    function refundSeller()
        external
        onlySeller
        inState(State.Release)
    {
        emit SellerRefunded();
        // otherwise, the contracts called using `send` below
        // can call in again here.
        // 最初に状態を変更することが重要です。
        // そうしないと、以下の `send` を使用して呼び出されたコントラクトが、ここで再び呼び出される可能性があるからです。
        state = State.Inactive;

        seller.transfer(3 * value);
    }
}

マイクロペイメントチャネル

このセクションでは、ペイメントチャネルの実装例を構築する方法を学びます。 これは、暗号化された署名を使用して、同一の当事者間で繰り返されるEtherの送金を、安全かつ瞬時に、トランザクション手数料なしで行うものです。 この例では、署名と検証の方法を理解し、ペイメントチャネルを設定する必要があります。

署名の作成と検証

アリスがボブにEtherを送りたい、つまりアリスが送信者でボブが受信者であるとします。

アリスは暗号化されたメッセージをオフチェーンで(例えばメールで)ボブに送るだけでよく、小切手を書くのと同じようなものです。

アリスとボブは署名を使ってトランザクションを承認しますが、これはEthereumのスマートコントラクトで可能です。 アリスはEtherを送信できるシンプルなスマートコントラクトを構築しますが、支払いを開始するために自分で関数を呼び出すのではなく、ボブにそれをさせ、その結果、トランザクション手数料を支払うことになります。

コントラクト内容は以下のようになっています。

  1. アリスは ReceiverPays コントラクトをデプロイし、行われるであろう支払いをカバーするのに十分なEtherを取り付けます。

  2. アリスは、自分の秘密鍵でメッセージを署名することで、支払いを承認します。

  3. アリスは、暗号署名されたメッセージをボブに送信します。 メッセージは秘密にしておく必要はなく(後で説明します)、送信の仕組みも問題ありません。

  4. Bobはスマートコントラクトに署名済みのメッセージを提示して支払いを請求し、スマートコントラクトはメッセージの真正性を検証した後、資金を放出します。

署名の作成

アリスはトランザクションに署名するためにEthereumネットワークと対話する必要はなく、プロセスは完全にオフラインです。 このチュートリアルでは、他にも多くのセキュリティ上の利点があるため、 EIP-712 で説明した方法を用いて、 web3.jsMetaMask を使ってブラウザ上でメッセージを署名します。

/// ハッシュ化を先に行うことで、より簡単になります。
var hash = web3.utils.sha3("message to sign");
web3.eth.personal.sign(hash, web3.eth.defaultAccount, function () { console.log("Signed"); });

注釈

web3.eth.personal.sign はメッセージの長さを署名済みデータの前に付けます。 最初にハッシュ化することで、メッセージは常に正確な32バイトの長さになるため、この長さのプレフィックスは常に同じになります。

署名するもの

支払いを履行するコントラクトの場合、署名されたメッセージには以下が含まれていなければなりません。

  1. 受信者のアドレス

  2. 送金される金額

  3. リプレイアタックへの対策

リプレイアタックとは、署名されたメッセージが再利用されて、2回目のアクションの認証を主張することです。 リプレイアタックを避けるために、私たちはEthereumのトランザクション自体と同じ技術、いわゆるnonceを使用していますが、これはアカウントから送信されたトランザクションの数です。 スマートコントラクトは、nonceが複数回使用されているかどうかをチェックします。

別のタイプのリプレイアタックは、所有者が ReceiverPays スマートコントラクトをデプロイし、いくつかの支払いを行った後、コントラクトを破棄した場合に発生します。 しかし、新しいコントラクトは前回のデプロイで使用されたnonceを知らないため、攻撃者は古いメッセージを再び使用できます。

アリスはメッセージにコントラクトのアドレスを含めることでこの攻撃から守ることができ、コントラクトのアドレス自体を含むメッセージだけが受け入れられます。 このセクションの最後にある完全なコントラクトの claimPayment() 関数の最初の2行に、この例があります。

引数のパッキング

さて、署名付きメッセージに含めるべき情報がわかったところで、メッセージをまとめ、ハッシュ化し、署名する準備が整いました。 簡単にするために、データを連結します。 ethereumjs-abi ライブラリは、 abi.encodePacked でエンコードされた引数に適用されるSolidityの keccak256 関数の動作を模倣した soliditySHA3 という関数を提供しています。 以下は、 ReceiverPays の例で適切な署名を作成するJavaScriptの関数です。

// recipient は、支払いを受けるべきアドレスです。
// amount (wei) は、どれだけのEtherを送るべきかを指定します。
// nonce は、リプレイアタックを防ぐために任意の一意な番号を指定します。
// contractAddress は、クロスコントラクトのリプレイアタックを防ぐために使用します。
function signPayment(recipient, amount, nonce, contractAddress, callback) {
    var hash = "0x" + abi.soliditySHA3(
        ["address", "uint256", "uint256", "address"],
        [recipient, amount, nonce, contractAddress]
    ).toString("hex");

    web3.eth.personal.sign(hash, web3.eth.defaultAccount, callback);
}
Solidityでメッセージの署名者の復元

一般的に、ECDSA署名は rs という2つのパラメータで構成されています。 Ethereumの署名には、 v という3つ目のパラメータが含まれており、どのアカウントの秘密鍵がメッセージの署名に使われたか、トランザクションの送信者を確認するために使用できます。 Solidityには、メッセージと rsv の各パラメータを受け取り、メッセージの署名に使用されたアドレスを返す組み込み関数 ecrecover があります。

署名パラメータの抽出

web3.jsが生成する署名は、 rsv を連結したものなので、まずはこれらのパラメータを分割する必要があります。 これはクライアントサイドでもできますが、スマートコントラクト内で行うことで、署名パラメータを3つではなく1つだけ送信すればよくなります。 バイト配列を構成要素に分割するのは面倒なので、 splitSignature 関数(このセクションの最後にあるフルコントラクトの3番目の関数)の中で、インラインアセンブリ を使ってその作業を行います。

メッセージハッシュの計算

スマートコントラクトは、どのパラメータが署名されたかを正確に知る必要があるため、パラメータからメッセージを再作成し、それを署名検証に使用する必要があります。 prefixed 関数と recoverSigner 関数は、 claimPayment 関数でこれを行います。

コントラクト全体
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
// 非推奨のselfdestructを使用するためwarningが出力されます。
contract ReceiverPays {
    address owner = msg.sender;

    mapping(uint256 => bool) usedNonces;

    constructor() payable {}

    function claimPayment(uint256 amount, uint256 nonce, bytes memory signature) external {
        require(!usedNonces[nonce]);
        usedNonces[nonce] = true;

        // this recreates the message that was signed on the client
        bytes32 message = prefixed(keccak256(abi.encodePacked(msg.sender, amount, nonce, this)));

        require(recoverSigner(message, signature) == owner);

        payable(msg.sender).transfer(amount);
    }

    /// destroy the contract and reclaim the leftover funds.
    function shutdown() external {
        require(msg.sender == owner);
        selfdestruct(payable(msg.sender));
    }

    /// signature methods.
    function splitSignature(bytes memory sig)
        internal
        pure
        returns (uint8 v, bytes32 r, bytes32 s)
    {
        require(sig.length == 65);

        assembly {
            // first 32 bytes, after the length prefix.
            r := mload(add(sig, 32))
            // second 32 bytes.
            s := mload(add(sig, 64))
            // final byte (first byte of the next 32 bytes).
            v := byte(0, mload(add(sig, 96)))
        }

        return (v, r, s);
    }

    function recoverSigner(bytes32 message, bytes memory sig)
        internal
        pure
        returns (address)
    {
        (uint8 v, bytes32 r, bytes32 s) = splitSignature(sig);

        return ecrecover(message, v, r, s);
    }

    /// builds a prefixed hash to mimic the behavior of eth_sign.
    function prefixed(bytes32 hash) internal pure returns (bytes32) {
        return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash));
    }
}

シンプルなペイメントチャンネルの書き方

アリスは今、シンプルだが完全なペイメントチャネルの実装を構築しています。 ペイメントチャネルは、暗号化された署名を使用して、安全に、瞬時に、トランザクション手数料なしで、Etherの反復送金を行えます。

ペイメントチャネルとは

ペイメントチャンネルでは、参加者はトランザクションを使わずにEtherの送金を繰り返し行うことができます。 つまり、トランザクションに伴う遅延や手数料を回避できます。 ここでは、2人の当事者(AliceとBob)の間の単純な一方向性の支払いチャネルを調べてみます。 それには3つのステップがあります。

  1. アリスはスマートコントラクトにEtherで資金を供給します。 これにより、ペイメントチャネルを「オープン」します。

  2. アリスは、そのEtherのうちどれだけの量を受信者に支払うかを指定するメッセージに署名します。 このステップは支払いごとに繰り返されます。

  3. Bob はペイメントチャネルを「クローズ」し、自分の分のEtherを引き出し、残りのEtherを送信者に送り返します。

注釈

ステップ1とステップ3のみがEthereumのトランザクションを必要とし、ステップ2は送信者が暗号化されたメッセージをオフチェーン方式(例: 電子メール)で受信者に送信することを意味します。 つまり、2つのトランザクションだけで、任意の数の送金をサポートできます。

スマートコントラクトはEtherをエスクローし、有効な署名付きメッセージを尊重するので、ボブは資金を受け取ることが保証されています。 また、スマートコントラクトはタイムアウトを強制しているため、受信者がチャネルを閉じることを拒否した場合でも、アリスは最終的に資金を回収できることが保証されています。 ペイメントチャネルの参加者は、そのチャネルをどのくらいの期間開いておくかを決めることができます。 例えば、インターネットカフェにネットワーク接続料を支払うような短時間のトランザクションの場合、ペイメントチャネルは限られた時間しか開いていないかもしれません。 一方、従業員に時給を支払うような定期的な支払いの場合は、数ヶ月または数年にわたってペイメントチャネルを開いておくことができます。

ペイメントチャネルのオープン

ペイメントチャネルを開くために、アリスはスマートコントラクトをデプロイし、エスクローされるイーサを添付し、意図する受取人とチャネルが存在する最大期間を指定します。 これが、このセクションの最後にあるコントラクトの関数 SimplePaymentChannel です。

ペイメントの作成

アリスは、署名されたメッセージをボブに送ることで支払いを行います。 このステップは、Ethereumネットワークの外で完全に実行されます。 メッセージは送信者によって暗号化されて署名され、受信者に直接送信されます。

各メッセージには以下の情報が含まれています。

  • スマートコントラクトのアドレス。 クロスコントラクトのリプレイアタックを防ぐために使用されます。

  • これまでに受取人に支払われたEtherの合計額。

ペイメントチャネルは、一連の送金が終わった時点で一度だけ閉じられます。 このため、送信されたメッセージのうち1つだけが償還されます。 これが、各メッセージが、個々のマイクロペイメントの金額ではなく、支払うべきEtherの累積合計金額を指定する理由です。 受信者は当然、最新のメッセージを償還することを選択しますが、それは最も高い合計額を持つメッセージだからです。 スマートコントラクトは1つのメッセージのみを尊重するため、メッセージごとのnonceはもう必要ありません。 スマートコントラクトのアドレスは、あるペイメントチャネル用のメッセージが別のチャネルで使用されるのを防ぐために使用されます。

前述のメッセージを暗号化して署名するためのJavaScriptコードを修正したものです。

function constructPaymentMessage(contractAddress, amount) {
    return abi.soliditySHA3(
        ["address", "uint256"],
        [contractAddress, amount]
    );
}

function signMessage(message, callback) {
    web3.eth.personal.sign(
        "0x" + message.toString("hex"),
        web3.eth.defaultAccount,
        callback
    );
}

// contractAddressは、クロスコントラクトリプレイ攻撃を防ぐために使用されます。
// amount (wei)は、送信されるべきEtherの量を指定します。

function signPayment(contractAddress, amount, callback) {
    var message = constructPaymentMessage(contractAddress, amount);
    signMessage(message, callback);
}
ペイメントチャネルのクローズ

ボブが資金を受け取る準備ができたら、スマートコントラクトの close 関数をコールしてペイメントチャネルを閉じる時です。 チャネルを閉じると、受取人に支払うべきEtherが支払われ、コントラクトが破棄され、残っているEtherがAliceに送り返されます。 チャネルを閉じるために、BobはAliceが署名したメッセージを提供する必要があります。

スマートコントラクトは、メッセージに送信者の有効な署名が含まれていることを検証する必要があります。 この検証を行うためのプロセスは、受信者が使用するプロセスと同じです。 Solidityの関数 isValidSignaturerecoverSigner は、前のセクションのJavaScriptの対応する関数と同じように動作しますが、後者の関数は ReceiverPays コントラクトから借用しています。

close 関数を呼び出すことができるのは、ペイメントチャネルの受信者のみです。 受信者は当然、最新のペイメントメッセージを渡します。 なぜなら、そのメッセージには最も高い債務総額が含まれているからです。 もし送信者がこの関数を呼び出すことができれば、より低い金額のメッセージを提供し、受信者を騙して債務を支払うことができます。

この関数は、署名されたメッセージが与えられたパラメータと一致するかどうかを検証します。 すべてがチェックアウトされれば、受信者には自分の分のEtherが送られ、送信者には selfdestruct 経由で残りの分が送られます。 close 関数はコントラクト全体で見ることができます。

チャネルの有効期限

ボブはいつでも支払いチャネルを閉じることができますが、それができなかった場合、アリスはエスクローされた資金を回収する方法が必要です。 コントラクトのデプロイ時に 有効期限 が設定されました。 その時間に達すると、アリスは claimTimeout をコールして資金を回収できます。 claimTimeout 関数は コントラクト全文で見ることができます。

この関数が呼び出されると、BobはEtherを受信できなくなるため、期限切れになる前にBobがチャネルを閉じることが重要です。

コントラクト全体
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
// 非推奨のselfdestructを使用するためwarningが出力されます。
contract SimplePaymentChannel {
    address payable public sender;      // 支払いを送信するアカウント
    address payable public recipient;   // 支払いを受けるアカウント
    uint256 public expiration;  // 受信者が閉じない場合のタイムアウト

    constructor (address payable recipientAddress, uint256 duration)
        payable
    {
        sender = payable(msg.sender);
        recipient = recipientAddress;
        expiration = block.timestamp + duration;
    }

    /// 受信者は送信者から署名された金額を提示することで、いつでもチャンネルを閉じることができます。
    /// 受信者はその金額を送信し、残りは送信者に戻ります。
    function close(uint256 amount, bytes memory signature) external {
        require(msg.sender == recipient);
        require(isValidSignature(amount, signature));

        recipient.transfer(amount);
        selfdestruct(sender);
    }

    /// 送信者はいつでも有効期限を延長できます。
    function extend(uint256 newExpiration) external {
        require(msg.sender == sender);
        require(newExpiration > expiration);

        expiration = newExpiration;
    }

    /// 受信者がチャネルを閉じることなくタイムアウトに達した場合、Etherは送信者に戻されます。
    function claimTimeout() external {
        require(block.timestamp >= expiration);
        selfdestruct(sender);
    }

    function isValidSignature(uint256 amount, bytes memory signature)
        internal
        view
        returns (bool)
    {
        bytes32 message = prefixed(keccak256(abi.encodePacked(this, amount)));

        // 署名が支払い送信者のものであることを確認します。
        return recoverSigner(message, signature) == sender;
    }

    /// これ以下の関数はすべて「署名の作成と検証」の章から引用しているだけです。

    function splitSignature(bytes memory sig)
        internal
        pure
        returns (uint8 v, bytes32 r, bytes32 s)
    {
        require(sig.length == 65);

        assembly {
            // first 32 bytes, after the length prefix
            r := mload(add(sig, 32))
            // second 32 bytes
            s := mload(add(sig, 64))
            // final byte (first byte of the next 32 bytes)
            v := byte(0, mload(add(sig, 96)))
        }

        return (v, r, s);
    }

    function recoverSigner(bytes32 message, bytes memory sig)
        internal
        pure
        returns (address)
    {
        (uint8 v, bytes32 r, bytes32 s) = splitSignature(sig);

        return ecrecover(message, v, r, s);
    }

    /// eth_sign の動作を模倣して、接頭辞付きハッシュを構築します。
    function prefixed(bytes32 hash) internal pure returns (bytes32) {
        return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash));
    }
}

注釈

関数 splitSignature は、すべてのセキュリティチェックを使用していません。 実際の実装では、openzepplinの バージョン のように、より厳密にテストされたライブラリを使用する必要があります。

ペイメントの検証

前述のセクションとは異なり、ペイメントチャネル内のメッセージはすぐには償還されません。 受信者は最新のメッセージを記録しておき、決済チャネルを閉じるときにそのメッセージを引き換えることになります。 つまり、受信者がそれぞれのメッセージに対して独自の検証を行うことが重要です。 そうしないと、受信者が最終的に支払いを受けることができるという保証はありません。

受信者は、以下のプロセスで各メッセージを確認する必要があります。

  1. メッセージ内のコントラクトアドレスがペイメントチャネルと一致していることを確認します。

  2. 新しい合計金額が期待通りの金額であることを確認します。

  3. 新しい合計がエスクローされたEtherの量を超えていないことを確認します。

  4. 署名が有効であり、ペイメントチャネルの送信者からのものであることを確認します。

この検証には ethereumjs-util ライブラリを使って書きます。 最後のステップはいくつかの方法で行うことができますが、ここではJavaScriptを使用します。 次のコードは、上の署名用 JavaScriptコード から constructPaymentMessage 関数を借りています。

// これは eth_sign JSON-RPC メソッドのプリフィックス動作を模倣しています。
function prefixed(hash) {
    return ethereumjs.ABI.soliditySHA3(
        ["string", "bytes32"],
        ["\x19Ethereum Signed Message:\n32", hash]
    );
}

function recoverSigner(message, signature) {
    var split = ethereumjs.Util.fromRpcSig(signature);
    var publicKey = ethereumjs.Util.ecrecover(message, split.v, split.r, split.s);
    var signer = ethereumjs.Util.pubToAddress(publicKey).toString("hex");
    return signer;
}

function isValidSignature(contractAddress, amount, signature, expectedSigner) {
    var message = prefixed(constructPaymentMessage(contractAddress, amount));
    var signer = recoverSigner(message, signature);
    return signer.toLowerCase() ==
        ethereumjs.Util.stripHexPrefix(expectedSigner).toLowerCase();
}

モジュラーコントラクト

モジュラーアプローチでコントラクトを構築すると、複雑さを軽減し、読みやすさを向上させることができ、開発やコードレビューの際にバグや脆弱性を特定するのに役立ちます。 各モジュールの動作を個別に指定して制御する場合、考慮しなければならない相互作用はモジュールの仕様間のものだけで、コントラクトの他のすべての可動部分ではありません。 以下の例では、コントラクトは Balances ライブラリmove メソッドを使用して、アドレス間で送信された残高が期待したものと一致するかどうかをチェックしています。 このように、 Balances ライブラリはアカウントの残高を適切に追跡する独立したコンポーネントを提供しています。 Balances ライブラリが負の残高やオーバーフローを決して生成せず、すべての残高の合計がコントラクトのライフタイムにわたって不変であることを簡単に確認できます。

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

library Balances {
    function move(mapping(address => uint256) storage balances, address from, address to, uint amount) internal {
        require(balances[from] >= amount);
        require(balances[to] + amount >= balances[to]);
        balances[from] -= amount;
        balances[to] += amount;
    }
}

contract Token {
    mapping(address => uint256) balances;
    using Balances for *;
    mapping(address => mapping(address => uint256)) allowed;

    event Transfer(address from, address to, uint amount);
    event Approval(address owner, address spender, uint amount);

    function transfer(address to, uint amount) external returns (bool success) {
        balances.move(msg.sender, to, amount);
        emit Transfer(msg.sender, to, amount);
        return true;

    }

    function transferFrom(address from, address to, uint amount) external returns (bool success) {
        require(allowed[from][msg.sender] >= amount);
        allowed[from][msg.sender] -= amount;
        balances.move(from, to, amount);
        emit Transfer(from, to, amount);
        return true;
    }

    function approve(address spender, uint tokens) external returns (bool success) {
        require(allowed[msg.sender][spender] == 0, "");
        allowed[msg.sender][spender] = tokens;
        emit Approval(msg.sender, spender, tokens);
        return true;
    }

    function balanceOf(address tokenOwner) external view returns (uint balance) {
        return balances[tokenOwner];
    }
}

Solidityコンパイラのインストール

バージョニング

Solidityのバージョンは セマンティックバージョニング に従っています。 さらに、メジャーリリース0(つまり0.x.y)のパッチレベルのリリースには、破壊的な変更が含まれません。 つまり、バージョン0.x.yでコンパイルされたコードは、0.x.z(z > y)でコンパイルされることが期待できます。

リリースに加え、 nightly development builds を提供することで、開発者が簡単に次期機能を試し、早期にフィードバックを提供できるようにしています。 ただし、ナイトリービルドは通常非常に安定していますが、開発ブランチからの最先端のコードを含んでおり、常に動作することを保証するものではないことに注意してください。 私たちの最善の努力にもかかわらず、文書化されていない、あるいは破壊的変更が含まれている可能性があり、実際のリリースの一部にはなりません。 また、本番環境での使用は想定していません。

コントラクトをデプロイする場合は、Solidityの最新リリースバージョンを使用する必要があります。 これは、新しい機能やバグ修正だけでなく、破壊的な変更が定期的に導入されるからです。 現在、0.xのバージョン番号を使用していますが、これは この速いペースでの変更を示すため です。

Remix

小規模なコントラクトやSolidityを短期間で習得するにはRemixをお勧めします。

Remixにオンラインでアクセスする 場合、何もインストールする必要はありません。 インターネットに接続せずに使用したい場合は、https://github.com/ethereum/remix-live/tree/gh-pages#readme に行き、そのページの指示に従ってください。 Remixは、複数のSolidityバージョンをインストールせずにnightlyビルドをテストするのに便利なオプションでもあります。

このページの他のオプションでは、お使いのコンピュータにコマンドラインのSolidityコンパイラソフトウェアをインストールする方法について説明しています。 大規模なコントラクトに取り組む場合や、より多くのコンパイルオプションを必要とする場合は、コマンドラインコンパイラを選択してください。

npm / Node.js

solcjs プログラムは、Solidityのコンパイラである solcjs をインストールするための便利でポータブルな方法として使用します。 solcjs プログラムは、このページの下の方で説明されているコンパイラへのアクセス方法よりも機能が少なくなっています。 コマンドラインコンパイラの使い方 のドキュメントでは、フル機能のコンパイラである solc を使用していることを前提としています。 solcjs の使い方は、独自の リポジトリ の中で説明されています。

注: solc-jsプロジェクトは、Emscriptenを使用してC++ solc から派生しており、両者は同じコンパイラのソースコードを使用しています。 solc-js はJavaScriptプロジェクト(Remixなど)で直接使用できます。 使用方法はsolc-jsのリポジトリを参照してください。

npm install -g solc

注釈

コマンドラインの実行ファイルの名前は solcjs です。

solcjs のコマンドラインオプションは solc と互換性がなく、 solc の動作を想定したツール( geth など)は solcjs では動作しません。

Docker

SolidityのビルドのDockerイメージは、 ethereum オーガナイゼーションの solc イメージを使って利用できます。 最新のリリースバージョンには stable タグを、developブランチの不安定な可能性のある変更には nightly タグを使用してください。

Dockerイメージはコンパイラ実行ファイルを実行するので、すべてのコンパイラ引数を渡すことができます。 例えば、以下のコマンドは、ステーブル版の solc イメージ(まだ持っていない場合)を取り出し、 --help 引数を渡して新しいコンテナで実行します。

docker run ethereum/solc:stable --help

タグには、0.5.4リリースのように、リリースのビルドバージョンを指定することもできます。

docker run ethereum/solc:0.5.4 --help

ホストマシンでSolidityのファイルをコンパイルするためにDockerイメージを使用するには、入出力用のローカルフォルダーをマウントし、コンパイルするコントラクトを指定します。 例えば、以下のようになります。

docker run -v /local/path:/sources ethereum/solc:stable -o /sources/output --abi --bin /sources/Contract.sol

また、標準のJSONインターフェースを使用することもできます(コンパイラとツールを使用する場合は、このインターフェースを使用することをお勧めします)。 このインターフェースを使用する場合、JSON入力が自己完結している限り、ディレクトリをマウントする必要はありません(つまり、importコールバックによって読み込まれる 必要がある外部ファイルを参照しない)。

docker run ethereum/solc:stable --standard-json < input.json > output.json

Linuxパッケージ

solidity/releases ではSolidityのバイナリパッケージが用意されています。

また、Ubuntu用のPPAも用意しているので、以下のコマンドで最新のステーブル版を入手できます。

sudo add-apt-repository ppa:ethereum/ethereum
sudo apt-get update
sudo apt-get install solc

nightlyバージョンは、以下のコマンドでインストールできます。

sudo add-apt-repository ppa:ethereum/ethereum
sudo add-apt-repository ppa:ethereum/ethereum-dev
sudo apt-get update
sudo apt-get install solc

さらに、一部のLinuxディストリビューションでは、独自のパッケージが提供されています。 これらのパッケージは私たちが直接メンテナンスしているわけではありませんが、基本的にそれぞれのパッケージメンテナによって最新に保たれています。

例えば、Arch Linuxでは、最新の開発版のパッケージがAURパッケージとして用意されています。 soliditysolidity-bin です。

注釈

AUR パッケージはユーザーが作成したコンテンツであり、非公式パッケージです。 使用するときは注意してください。

snapパッケージ もありますが、 現在メンテナンスされていませんサポートされているLinuxディストリビューション すべてでインストール可能です。 最新の安定版のsolcをインストールするには:

sudo snap install solc

最新のデベロップメント版Solidityを最新の変更点でテストすることに協力したい方は、以下をご利用ください。

sudo snap install solc --edge

注釈

solc スナップはstrict confinementを使用します。 これはスナップパッケージにとって最も安全なモードですが、 /home/media ディレクトリ内のファイルにしかアクセスできないなどの制限があります。 詳細については、 Demystifying Snap Confinement を参照してください。

macOSパッケージ

私たちは、SolidityコンパイラをHomebrewを通じて、build-from-sourceバージョンとして配布しています。 ビルド済みのボトルは現在サポートされていません。

brew update
brew upgrade
brew tap ethereum/ethereum
brew install solidity

最新の0.4.x / 0.5.xバージョンのSolidityをインストールするには、それぞれ brew install solidity@4brew install solidity@5 を使用することもできます。

Solidityの特定のバージョンが必要な場合は、Githubから直接Homebrew式をインストールできます。

solidity.rb commits on Github を見てください。

欲しいバージョンのコミットハッシュをコピーして、自分のマシンでチェックしてみましょう。

git clone https://github.com/ethereum/homebrew-ethereum.git
cd homebrew-ethereum
git checkout <your-hash-goes-here>

brew を使ってインストールします。

brew unlink solidity
# eg. Install 0.4.8
brew install solidity.rb

静的バイナリ

solc-bin では、サポートしているすべてのプラットフォーム用の過去および現在のコンパイラバージョンの静的ビルドを含むリポジトリを管理しています。 ここにはnightlyビルドも置かれています。

リポジトリは、エンドユーザーがすぐに使えるバイナリを素早く簡単に入手できるだけでなく、サードパーティのツールとの親和性も考慮しています。

  • コンテンツは https://binaries.soliditylang.org にミラーリングされ、認証やレート制限、gitを使用する必要なく、HTTPSで簡単にダウンロードできます。

  • コンテンツは、正しい Content-Type ヘッダと寛大なCORS設定で提供され、ブラウザ上で動作するツールで直接読み込めるようになっています。

  • バイナリは、インストールや解凍の必要がありません(ただし、必要なDLLがバンドルされた古いWindowsビルドは例外です)。

  • 私たちは、高いレベルの後方互換性を確保するよう努めています。 一度追加されたファイルは、古い場所でシンボリックリンクやリダイレクトを提供することなく削除または移動されることはありません。 また、ファイルはその場で変更されることはなく、常にオリジナルのチェックサムと一致していなければなりません。 唯一の例外は、壊れたファイルや使用できないファイルで、そのままにしておくと害になる可能性があるものです。

  • ファイルは HTTP と HTTPS の両方で提供されます。ファイルリストを安全な方法(git、HTTPS、IPFS、またはローカルにキャッシュ)で取得し、バイナリをダウンロードした後にバイナリのハッシュを検証する限り、バイナリ自体にHTTPSを使用する必要はありません。

同じバイナリは、ほとんどの場合、 Solidity release page on Github で入手できます。 異なる点は、Githubのリリースページにある古いリリースを一般的には更新しないことです。 つまり、命名規則が変わっても名前を変えないし、リリース時にサポートされていなかったプラットフォーム用のビルドも追加しません。 これは solc-bin でのみ起こります。

solc-bin リポジトリには、複数のトップレベルのディレクトリがあり、それぞれが1つのプラットフォームを表しています。 それぞれのディレクトリには、利用可能なバイナリの一覧を示す list.json ファイルが含まれています。 例えば、 emscripten-wasm32/list.json にはバージョン0.7.4についての以下の情報があります。

{
  "path": "solc-emscripten-wasm32-v0.7.4+commit.3f05b770.js",
  "version": "0.7.4",
  "build": "commit.3f05b770",
  "longVersion": "0.7.4+commit.3f05b770",
  "keccak256": "0x300330ecd127756b824aa13e843cb1f43c473cb22eaf3750d5fb9c99279af8c3",
  "sha256": "0x2b55ed5fec4d9625b6c7b3ab1abd2b7fb7dd2a9c68543bf0323db2c7e2d55af2",
  "urls": [
    "bzzr://16c5f09109c793db99fe35f037c6092b061bd39260ee7a677c8a97f18c955ab1",
    "dweb:/ipfs/QmTLs5MuLEWXQkths41HiACoXDiH8zxyqBHGFDRSzVE5CS"
  ]
}

これは次のことを意味します。

  • 同じディレクトリに solc-emscripten-wasm32-v0.7.4+commit.3f05b770.js という名前でバイナリが置かれています。 このファイルはシンボリックリンクになっている可能性があるので、git を使ってダウンロードしていない場合やファイルシステムがシンボリックリンクをサポートしていない場合は、自分で解決する必要があります。

  • このバイナリは https://binaries.soliditylang.org/emscripten-wasm32/solc-emscripten-wasm32-v0.7.4+commit.3f05b770.js にもミラーされています。 この場合、git は必要ありません。シンボリックリンクは透過的に解決され、ファイルのコピーを提供するか HTTP リダイレクトを返します。

  • このファイルはIPFSの QmTLs5MuLEWXQkths41HiACoXDiH8zxyqBHGFDRSzVE5CS でも公開されています。

  • このファイルは、将来はSwarmの 16c5f09109c793db99fe35f037c6092b061bd39260ee7a677c8a97f18c955ab1 で公開されるかもしれません。

  • keccak256ハッシュを 0x300330ecd127756b824aa13e843cb1f43c473cb22eaf3750d5fb9c99279af8c3 と比較することで、バイナリの完全性を確認できます。 ハッシュは、 sha3sum が提供する keccak256sum ユーティリティーを使ってコマンドラインで計算するか、JavaScriptで keccak256() function from ethereumjs-util を使って計算できます。

  • また、sha256ハッシュを 0x2b55ed5fec4d9625b6c7b3ab1abd2b7fb7dd2a9c68543bf0323db2c7e2d55af2 と比較することで、バイナリの完全性を確認できます。

警告

強い後方互換性の要求により、リポジトリにはいくつかのレガシー要素が含まれていますが、新しいツールを書く際にはそれらを使用しないようにしてください。

  • 最高のパフォーマンスを求めるのであれば、 bin/ ではなく emscripten-wasm32/emscripten-asmjs/ へのフォールバック機能あり)を使用してください。バージョン0.6.1まではasm.jsのバイナリのみを提供していました。 0.6.2からは、パフォーマンスが大幅に向上した WebAssembly builds に切り替えました。古いバージョンを wasm 用に作り直しましたが、オリジナルの asm.js ファイルは bin/ に残っています。 新しいファイルは、名前の衝突を避けるために別のディレクトリに置く必要がありました。

  • wasmとasm.jsのどちらのバイナリをダウンロードしているかを確認したい場合は、 bin/wasm/ ディレクトリではなく、 emscripten-asmjs/emscripten-wasm32/ を使用してください。

  • list.jslist.txt の代わりに list.json を使用します。JSONリスト形式には、旧来のものからすべての情報が含まれています。

  • https://solc-bin.ethereum.org の代わりに https://binaries.soliditylang.org を使用してください。物事をシンプルにするために、コンパイラに関連するほとんどすべてのものを新しい soliditylang.org ドメインの下に移動しましたが、これは solc-bin にも当てはまります。新しいドメインを推奨しますが、古いドメインも完全にサポートされており、同じ場所を指すことが保証されています。

警告

バイナリは https://ethereum.github.io/solc-bin/ にもありますが、このページはバージョン0.7.2のリリース直後に更新が停止しており、プラットフォームを問わず、新しいリリースやnightlyビルドを受け取ることはなく、また、非emscripten のビルドを含む新しいディレクトリ構造にも対応していません。

使用している場合は、ドロップインで置き換え可能な https://binaries.soliditylang.org に切り替えてください。 これにより、基盤となるホスティングの変更を透明性のある方法で行い、混乱を最小限に抑えることができます。 私たちがコントロールできない ethereum.github.io ドメインとは異なり、 binaries.soliditylang.org は長期的に機能し、同じURL構造を維持することが保証されています。

ソースからのビルド

前提知識 - 全オペレーティングシステム共通

以下は、Solidityのすべてのビルドに依存しています。

ソフトウェア

ノート

CMake (Windowsではバージョン3.21.3+、その他では3.13+)

クロスプラットフォームのビルドファイルジェネレーター。

Boost (Windowsではバージョン1.77、その他では1.65+)

C++ライブラリ。

Git

ソースコードを取得するためのコマンドラインツール。

z3 (バージョン4.8.16+, オプション)

SMTチェッカーと併用する場合。

cvc4 (オプション)

SMTチェッカーと併用する場合。

注釈

Solidityのバージョンが0.5.10以前の場合、Boostのバージョン1.70以上に対して正しくリンクできないことがあります。 これを回避するには、cmakeコマンドを実行してSolidityを設定する前に、一時的に <Boost install path>/lib/cmake/Boost-1.70.0 の名前を変更することが考えられます。

0.5.10以降、Boost 1.70以上とのリンクは手動での操作なしに動作します。

注釈

デフォルトのビルド構成では、特定のZ3バージョン(コードが最後に更新された時点での最新のもの)が必要です。 Z3のリリース間に導入された変更により、わずかに異なる(ただし有効な)結果が返されることがよくあります。 私たちのSMTテストはこれらの違いを考慮しておらず、書かれたバージョンとは異なるバージョンで失敗する可能性があります。 これは、異なるバージョンを使用したビルドが欠陥であることを意味するものではありません。 CMakeに -DSTRICT_Z3_VERSION=OFF オプションを渡しておけば、上の表にある要件を満たす任意のバージョンでビルドできます。 ただし、この場合、SMT テストをスキップするために scripts/tests.sh--no-smt オプションを渡すことを忘れないでください。

注釈

デフォルトでは、ビルドは pedantic mode で実行され、余分な警告を有効にし、すべての警告をエラーとして扱うようにコンパイラに指示します。 これにより、開発者は警告が発生したときに修正することを余儀なくされ、「後で修正する」ことが蓄積されることがありません。 もしあなたがリリースビルドを作ることにしか興味がなく、そのような警告に対処するためにソースコードを修正するつもりがないのであれば、CMakeに -DPEDANTIC=OFF オプションを渡してこのモードを無効にすることが可能です。 この方法は一般的な使用では推奨されませんが、私たちがテストしていないツールチェーンを使用する場合や、古いバージョンを新しいツールでビルドしようとする場合には必要かもしれません。 もしこのような警告に遭遇したら、 それらを報告すること を検討してください。

最小コンパイラバージョン

以下のC++コンパイラとその最小バージョンでSolidityのコードベースを構築できます。

  • GCC 、バージョン8以上

  • Clang 、バージョン7以上

  • MSVC 、バージョン2019以上

前提知識 - macOS

macOSでビルドする場合は、最新版の Xcodeがインストールされていること を確認してください。 Xcodeを初めてインストールする場合や、新しいバージョンをインストールしたばかりの場合は、コマンドラインでのビルドを行う前にライセンスに同意する必要があります。

sudo xcodebuild -license accept

私たちのOS Xのビルドスクリプトは、外部の依存関係をインストールするために Homebrew パッケージマネージャーを使用しています。 もし、最初からやり直したいと思ったときのために、 Homebrewのアンインストール の方法を紹介します。

前提知識 - Windows

SolidityのWindowsビルドには、以下の依存関係をインストールする必要があります。

Software

Notes

Visual Studio 2019 Build Tools

C++コンパイラ

Visual Studio 2019 (Optional)

C++コンパイラと開発環境

Boost (version 1.77)

C++ライブラリ

すでに1つのIDEを持っていて、コンパイラとライブラリだけが必要な場合は、Visual Studio 2019 Build Toolsをインストールできます。

Visual Studio 2019は、IDEと必要なコンパイラとライブラリの両方を提供します。 そのため、IDEを持っておらず、Solidityを開発したい場合は、すべてのセットアップを簡単に行うことができるVisual Studio 2019を選択するとよいでしょう。

ここでは、「Visual Studio 2019 Build Tools」または「Visual Studio 2019」にインストールされるべきコンポーネントのリストを示します。

  • Visual Studio C++のコア関数

  • VC++ 2019 v141ツールセット(x86,x64)

  • Windows Universal CRT SDK

  • Windows 8.1 SDK

  • C++/CLIのサポート

必要な外部依存パッケージをすべてインストールするためのヘルパースクリプトを用意しています。

scripts\install_deps.ps1

これにより、 boostcmakedeps サブディレクトリにインストールされます。

リポジトリのクローン

ソースコードをクローンするには、以下のコマンドを実行します。

git clone --recursive https://github.com/ethereum/solidity.git
cd solidity

もしSolidityの開発に協力したいのであれば、Solidityをフォークして、自分の個人的なフォークをセカンドリモートとして追加してください。

git remote add personal git@github.com:[username]/solidity.git

注釈

この方法では、プレリリースビルドの結果、そのようなコンパイラで生成された各バイトコードにフラグが設定されるなどの問題が発生します。 リリースされたSolidityコンパイラを再構築したい場合は、githubのリリースページにあるソースtarballを使用してください。

https://github.com/ethereum/solidity/releases/download/v0.X.Y/solidity_0.X.Y.tar.gz

(GitHubで提供されている「ソースコード」ではありません)。

コマンドラインビルド

ビルドする前に、必ず外部依存関係(上記参照)をインストールしてください。

Solidityプロジェクトでは、CMakeを使ってビルドの設定を行います。 繰り返しのビルドを高速化するために、 ccache をインストールするとよいでしょう。 CMakeはそれを自動的にピックアップします。 Solidityのビルドは、Linux、macOS、その他のUnicesでもよく似ています。

mkdir build
cd build
cmake .. && make

あるいはLinuxやmacOSではもっと簡単に実行できます:

#note: this will install binaries solc and soltest at usr/local/bin
./scripts/build.sh

警告

BSDビルドは動作するはずですが、Solidityチームではテストしていません。

そして、Windows用のビルドは、以下のコマンドを実行します:

mkdir build
cd build
cmake -G "Visual Studio 16 2019" ..

scripts\install_deps.ps1 がインストールしたバージョンのブーストを使用したい場合は、 cmake の呼び出しの引数として -DBoost_DIR="deps\boost\lib\cmake\Boost-*"-DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded を追加で渡す必要があります。

これにより、そのビルドディレクトリに solidity.sln が作成されるはずです。 そのファイルをダブルクリックすると、Visual Studioが起動します。 Release 構成での構築をお勧めしますが、その他の構成でも動作します。

あるいは、次のようにコマンドラインでWindows用にビルドすることもできます。

cmake --build . --config Release

CMakeオプション

もし、CMakeのオプションに興味があれば、 cmake .. -LH を実行してください。

SMTソルバー

SolidityはSMTソルバーに対してビルドでき、システムで見つかった場合、デフォルトでそうします。 それぞれのソルバーは cmake オプションで無効にできます。

注: 場合によっては、ビルドに失敗したときの回避策としても有効です。

ビルドフォルダ内では、デフォルトで有効になっているので、無効にできます。

# disables only Z3 SMT Solver.
cmake .. -DUSE_Z3=OFF

# disables only CVC4 SMT Solver.
cmake .. -DUSE_CVC4=OFF

# disables both Z3 and CVC4
cmake .. -DUSE_CVC4=OFF -DUSE_Z3=OFF

バージョン文字列の詳細

Solidityバージョンの文字列は、4つの部分で構成されています。

  • バージョン番号

  • プレリリースのタグ。通常は develop.YYYY.MM.DD または nightly.YYYY.MM.DD に設定されています。

  • コミット。フォーマットは commit.GITHASH です。

  • プラットフォーム。任意の数の項目を持ち、プラットフォームとコンパイラに関する詳細を含むます。

ローカルに変更があった場合、そのコミットは .mod でポストフィックスされます。

これらのパーツはSemVerの要求に応じて組み合わせられます。 SolidityのプレリリースタグはSemVerのプレリリースに相当し、Solidityのコミットとプラットフォームを組み合わせてSemverのビルドメタデータを構成します。

リリース例: 0.4.8+commit.60cc1668.Emscripten.clang

プレリリースの例: 0.4.9-nightly.2017.1.17+commit.6ecb4aa3.Emscripten.clang

バージョニングについての重要な情報

リリースが行われた後、パッチレベルの変更のみが続くと想定されるため、パッチのバージョンレベルをバンプさせています。 変更がマージされたときには、SemVerと変更の重要度に応じてバージョンを上げる必要があります。 最後に、リリースは常に現在のnightlyビルドのバージョンで作成されますが、 prerelease 指定子はありません。

例:

  1. 0.4.0のリリースを行います。

  2. nightlyビルドのバージョンが今後0.4.1になります。

  3. 非破壊的な変更があった場合 --> バージョンの変更なし。

  4. 破壊的な変更があった場合 --> バージョンは0.5.0にバンプされます。

  5. 0.5.0のリリースを行います。

この動作は version pragma と相性が良いです。

Solidityソースファイルのレイアウト

ソースファイルには、任意の数の コントラクト定義importpragmausing for ディレクティブと structenumfunctionerrorconstant variable の定義を含めることができます。

SPDXライセンス識別子

スマートコントラクトの信頼性は、そのソースコードが利用可能であれば、より確立されます。 ソースコードを公開することは、著作権に関する法的な問題に常に触れることになるため、Solidityコンパイラでは、機械が解釈可能な SPDXライセンス識別子(SPDX license identifier) の使用を推奨しています。 すべてのソースファイルは、そのライセンスを示すコメントで始まるべきです。

// SPDX-License-Identifier: MIT

コンパイラはライセンスが SPDXで許可されたリスト の一部であることを検証しませんが、供給された文字列は bytecodeメタデータ に含まれます。

ライセンスを指定したくない場合や、ソースコードがオープンソースでない場合は、特別な値 UNLICENSED を使用してください。 UNLICENSED (使用不可、SPDXライセンスリストに存在しない)は、 UNLICENSE (すべての権利をすべての人に与える)とは異なることに注意してください。 Solidity は npm recommendation に従っています。

もちろん、このコメントを提供することで、各ソースファイルに特定のライセンスヘッダーを記載しなければならないとか、オリジナルの著作権者に言及しなければならないといった、ライセンスに関する他の義務から解放されるわけではありません。

コメントは、ファイルレベルではファイルのどこにあってもコンパイラに認識されますが、ファイルの先頭に置くことをお勧めします。

SPDXライセンス識別子の使用方法の詳細は、 SPDXのWebサイト に記載されています。

pragma

pragma キーワードは、特定のコンパイラの機能やチェックを有効にするために使用されます。 pragmaディレクティブは、常にソースファイルに局所的に適用されるため、プロジェクト全体で有効にしたい場合は、すべてのファイルにpragmaを追加する必要があります。 他のファイルを インポート した場合、そのファイルのpragmaは自動的にインポートしたファイルには適用されません。

バージョンpragma

ソースファイルには、互換性のない変更が加えられる可能性のある将来のバージョンのコンパイラでのコンパイルを拒否するために、バージョンpragmaで注釈を付けることができます(また、そうすべきです)。 私たちはこれらの変更を最小限にとどめ、セマンティクスの変更がシンタックスの変更を必要とするような方法で導入するようにしていますが、これは必ずしも可能ではありません。 このため、少なくとも変更点を含むリリースについては、変更履歴に目を通すことをお勧めします。 これらのリリースには、常に 0.x.0 または x.0.0 という形式のバージョンがあります。

バージョンpragmaは次のように使用されます: pragma solidity ^0.5.2;

上記の行を含むソースファイルは、バージョン0.5.2以前のコンパイラではコンパイルできず、バージョン0.6.0以降のコンパイラでも動作しません(この2番目の条件は ^ を使用することで追加されます)。 また、バージョン0.6.0以降のコンパイラでは動作しません。 コンパイラの正確なバージョンは固定されていないので、バグフィックスリリースも可能です。

コンパイラバージョンには、より複雑なルールを指定できますが、これらは npm で使用されているのと同じ構文に従います。

注釈

バージョンpragmaを使用しても、コンパイラのバージョンを変更することはありません。 また、コンパイラの機能を有効にしたり無効にしたりすることもありません。 コンパイラに対して、そのバージョンがpragmaで要求されているものと一致するかどうかをチェックするように指示するだけです。 一致しない場合、コンパイラはエラーを発行します。

ABIコーダーpragma

pragma abicoder v1 または pragma abicoder v2 を使用すると、ABIエンコーダおよびデコーダの2つの実装を選択できます。

新しいABIコーダー(v2)は、任意にネストされた配列や構造体をエンコードおよびデコードできます。 より多くの型をサポートするだけでなく、より広範な検証と安全チェックを伴うため、ガスコストが高くなる可能性がありますが、セキュリティも強化されます。 Solidity 0.6.0の時点では非実験的とみなされ、Solidity 0.8.0からデフォルトで有効になりました。 古いABIコーダーは pragma abicoder v1; を使用して選択できます。

新しいエンコーダーがサポートする型のセットは、古いエンコーダーがサポートする型の厳密なスーパーセットです。 このエンコーダーを使用するコントラクトは、制限なしに使用しないコントラクトと相互作用できます。 逆は、 abicoder v2 ではないコントラクトが、新しいエンコーダでのみサポートされている型のデコードを必要とするような呼び出しを行わない限り可能です。 コンパイラはこれを検知してエラーを出します。 コントラクトで abicoder v2 を有効にするだけで、このエラーは解消されます。

注釈

このpragmaは、コードが最終的にどこに到達するかにかかわらず、このpragmaが有効になっているファイルで定義されたすべてのコードに適用されます。 つまり、ソースファイルがABIコーダーv1でコンパイルするように選択されているコントラクトでも、他のコントラクトから継承することで新しいエンコーダを使用するコードを含むことができます。 これは、新しい型が内部的にのみ使用され、外部の関数の署名に使用されない場合に許可されます。

注釈

Solidity 0.7.4までは、 pragma experimental ABIEncoderV2 を使用してABIコーダーv2を選択できましたが、coder v1がデフォルトであるため、明示的に選択できませんでした。

実験的pragma

2つ目のpragmaは、実験的pragmaです。 これは、デフォルトではまだ有効になっていないコンパイラや言語の機能を有効にするために使用できます。 現在、以下の実験的pragmaがサポートされています。

ABIEncoderV2

ABIコーダーv2は実験的なものではなくなったので、Solidity 0.7.4から pragma abicoder v2 (上記参照)で選択できるようになりました。

SMTChecker

このコンポーネントは、Solidityコンパイラのビルド時に有効にする必要があるため、すべてのSolidityバイナリで利用できるわけではありません。 build instructions では、このオプションを有効にする方法を説明しています。 ほとんどのバージョンのUbuntu PPAリリースでは有効になっていますが、Dockerイメージ、Windowsバイナリ、静的ビルドのLinuxバイナリでは有効になっていません。 SMTソルバーがローカルにインストールされていて、ブラウザではなくnode経由でsolc-jsを実行している場合、 smtCallback 経由でsolc-jsを有効にできます。

pragma experimental SMTChecker; を使用する場合は、SMTソルバーへの問い合わせによって得られる追加の safety warnings を取得します。 このコンポーネントは、Solidity言語のすべての機能をサポートしておらず、多くの警告を出力する可能性があります。 サポートされていない機能が報告された場合、解析が完全にはうまくいかない可能性があります。

他のソースファイルのインポート

シンタックスとセマンティクス

Solidityは、JavaScript(ES6以降)と同様に、コードをモジュール化するためのインポート文をサポートしています。 しかし、Solidityは default export の概念をサポートしていません。

グローバルレベルでは、次のような形式のインポート文を使用できます。

import "filename";

filename の部分は、 importパス と呼ばれます。 この文は、"filename"からのすべてのグローバルシンボル(およびそこでインポートされたシンボル)を、現在のグローバルスコープにインポートします(ES6とは異なりますが、Solidityでは後方互換性があります)。 この形式は、予測できないほど名前空間を汚染するので、使用を推奨しません。 "filename"の中に新しいトップレベルのアイテムを追加すると、"filename"からこのようにインポートされたすべてのファイルに自動的に表示されます。 特定のシンボルを明示的にインポートする方が良いでしょう。

次の例では、 "filename" のすべてのグローバルシンボルをメンバーとする新しいグローバルシンボル symbolName を作成しています。

import * as symbolName from "filename";

と入力すると、すべてのグローバルシンボルが symbolName.symbol 形式で利用できるようになります。

この構文のバリエーションとして、ES6には含まれていませんが、便利なものがあります。

import "filename" as symbolName;

となっており、これは import * as symbolName from "filename"; と同じです。

名前の衝突があった場合、インポート中にシンボルの名前を変更できます。 例えば、以下のコードでは、新しいグローバルシンボル aliassymbol2 を作成し、それぞれ "filename" の内部から symbol1symbol2 を参照しています。

import {symbol1 as alias, symbol2} from "filename";

インポートパス

すべてのプラットフォームで再現可能なビルドをサポートするために、Solidityコンパイラは、ソースファイルが保存されているファイルシステムの詳細を抽象化する必要があります。 このため、インポートパスはホストファイルシステム上のファイルを直接参照しません。 代わりに、コンパイラは内部データベース( バーチャルファイルシステム(virtual filesystem) あるいは略して VFS )を維持し、各ソースユニットに不透明で構造化されていない識別子である一意の ソースユニット名 を割り当てます。 インポート文で指定されたインポートパスは、ソースユニット名に変換され、このデータベース内の対応するソースユニットを見つけるために使用されます。

Standard JSON APIを使用すると、すべてのソースファイルの名前と内容を、コンパイラの入力の一部として直接提供できます。 この場合、ソースユニット名は本当に任意です。 しかし、コンパイラが自動的にソースコードを見つけてVFSにロードしたい場合は、ソースユニット名を import callback が見つけられるように構造化する必要があります。 コマンドラインコンパイラを使用する場合、デフォルトのインポートコールバックはホストファイルシステムからのソースコードのロードのみをサポートしているため、ソースユニット名はパスでなければなりません。 環境によっては、より汎用性の高いカスタムコールバックを提供しています。 例えば、 Remix IDE は、 HTTP、IPFS、SwarmのURLからファイルをインポートしたり、NPMレジストリのパッケージを直接参照したりできる ものを提供しています。

バーチャルファイルシステムとコンパイラが使用するパス解決ロジックの完全な説明は、 Path Resolution を参照してください。

コメント

一行コメント (//) と複数行コメント (/*...*/) が使用可能です。

// これは一行コメントです。

/*
これは
複数行コメントです。
*/

注釈

一行コメントは、UTF-8エンコーディングの任意のunicode行終端記号(LF、VF、FF、CR、NEL、LS、PS)で終了します。 ターミネーターはコメントの後もソースコードの一部であるため、ASCIIシンボル(NEL、LS、PS)でない場合はパーサーエラーになります。

さらに、NatSpecコメントと呼ばれる別の種類のコメントがあり、その詳細は style guide に記載されています。 このコメントは、トリプルスラッシュ( /// )またはダブルアスタリスクブロック( /** ... */ )で記述され、関数宣言や文の上で使用されます。

コントラクトの構造

Solidityのコントラクトは、オブジェクト指向言語のクラスに似ています。 各コントラクトは、 状態変数関数関数モディファイアイベントエラー構造体型列挙型 の宣言を含むことができます。 さらに、コントラクトは他のコントラクトを継承できます。

また、 ライブラリインターフェース と呼ばれる特別な種類のコントラクトもあります。

コントラクト のセクションには、このセクションよりも詳細な情報が記載されており、概要を説明するための役割を担っています。

状態変数

状態変数は、コントラクトストレージに値が永続的に保存される変数です。

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

contract SimpleStorage {
    uint storedData; // 状態変数
    // ...
}

有効な状態変数の型については セクションを、ビジビリティについての可能な選択肢については ビジビリティとゲッター を参照してください。

関数

関数は、実行可能なコードの単位です。 関数は通常、コントラクトの中で定義されますが、コントラクトの外で定義することもできます。

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

contract SimpleAuction {
    function bid() public payable { // 関数
        // ...
    }
}

// コントラクトの外で定義されたヘルパー関数
function helper(uint x) pure returns (uint) {
    return x * 2;
}

関数呼び出し は内部または外部で起こり、他のコントラクトに対して異なるレベルの ビジビリティ を持つことができます。 関数 は、それらの間でパラメータと値を渡すために パラメータと返り値 を受け入れます。

関数モディファイア

関数モディファイアを使うと、宣言的に関数のセマンティクスを変更できます(コントラクトセクションの 関数モディファイア を参照)。

オーバーロード、つまり、同じモディファイア名で異なるパラメータを持つことはできません。

関数と同様、モディファイアも overridden にできます。

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

contract Purchase {
    address public seller;

    modifier onlySeller() { // モディファイア
        require(
            msg.sender == seller,
            "Only seller can call this."
        );
        _;
    }

    function abort() public view onlySeller { // モディファイアの使用
        // ...
    }
}

イベント

イベントは、EVMのログ機能を使った便利なインターフェースです。

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

contract SimpleAuction {
    event HighestBidIncreased(address bidder, uint amount); // イベント

    function bid() public payable {
        // ...
        emit HighestBidIncreased(msg.sender, msg.value); // イベントのトリガー
    }
}

イベントがどのように宣言され、dapp内でどのように使用されるかについては、コントラクトセクションの イベント を参照してください。

エラー

エラーは障害が発生したときの記述的な名前とデータを定義できます。 エラーは リバート文 で使用できます。 文字列による説明に比べて、エラーははるかに安価で、追加データをエンコードできます。 NatSpecを使って、ユーザーにエラーを説明できます。

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

// 送金資金の不足。要求したのは`requested`だが、利用可能なのは`available`だけ。
error NotEnoughFunds(uint requested, uint available);

contract Token {
    mapping(address => uint) balances;
    function transfer(address to, uint amount) public {
        uint balance = balances[msg.sender];
        if (balance < amount)
            revert NotEnoughFunds(amount, balance);
        balances[msg.sender] -= amount;
        balances[to] += amount;
        // ...
    }
}

詳しくは、コントラクト編の エラーとリバート文 を参照してください。

構造体型

構造体(struct)は、複数の変数をグループ化できるカスタム定義の型です(型の項の 構造体 を参照)。

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

contract Ballot {
    struct Voter { // 構造体
        uint weight;
        bool voted;
        address delegate;
        uint vote;
    }
}

列挙型

列挙(enum)は、有限の「定数値」を持つカスタム型を作成するために使用できます(型の項の 列挙 を参照)。

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

contract Purchase {
    enum State { Created, Locked, Inactive } // 列挙
}

Solidityは静的型付け言語です。 つまり、各変数(ステートとローカル)の型を指定する必要があります。 Solidityにはいくつかの基本的な型があり、それらを組み合わせて複雑な型を作ることができます。

また、演算子を含む式では、型同士が相互に作用できます。 様々な演算子のクイックリファレンスについては、 演算子の優先順位 を参照してください。

Solidityには "undefined"や"null"の値の概念はありませんが、新しく宣言された変数は常にその型に依存した デフォルト値 を持ちます。 予期せぬ値を処理するには、 リバート を使ってトランザクション全体をリバートするか、成功を示す bool 値を2番目に持つタプルを返す必要があります。

値型

下記で紹介するものは、変数が常に値で渡されるため、値型と呼ばれます。 つまり、関数の引数や代入で使われるときは、常にコピーされます。

ブーリアン

bool: 可能な値は定数 truefalse です。

演算子:

  • ! (論理的否定)

  • && (論理積、"and")

  • || (論理和、"or")

  • == (等号)

  • != (不等号)

演算子 ||&& は、共通の短絡ルールを適用します。 つまり、式 f(x) || g(y) において、 f(x)true と評価された場合、 g(y) はたとえ副作用があったとしても評価されません。

整数

int / uint: さまざまなサイズの符号付きおよび符号なし整数。 キーワード uint8uint256 (8~256ビットの符号なし)と int8int2568 の間隔で。 uintint は、それぞれ uint256int256 のエイリアス。

演算子:

  • 比較: <=, <, ==, !=, >=, > ( bool に評価)

  • ビット演算子: &, |, ^ (ビットごとの排他的論理和), ~ (ビットごとの否定)

  • シフト演算子: << (左シフト), >> (右シフト)

  • 算術演算子: +, -, 単項 - (符号付き整数の場合のみ), *, /, % (モジュロ), ** (指数)

整数型の X の場合、 type(X).mintype(X).max を使って、その型で表現できる最小値と最大値にアクセスできます。

警告

Solidityの整数は、ある範囲に制限されています。 例えば、 uint32 の場合、 0 から 2**32 - 1 までとなります。 これらの型に対して算術演算を行うには2つのモードがあります。 "ラッピング"または"チェックなし"モードと"チェックあり"モードです。 デフォルトでは、演算は常に"チェック"されます。 つまり、演算結果が型の値の範囲外になると、呼び出しは failing assertion でリバートされます。 unchecked { ... } を使って"チェックなし"モードに切り替えることができます。 詳細は unchecked の項を参照してください。

比較

比較の値は、整数値を比較して得られる値です。

ビット演算

ビット演算は、数値の2の補数表現に対して行われます。 これは例えば、 ~int256(0) == int256(-1) だということです。

シフト

シフト演算の結果は、左オペランドの型を持ち、型に合わせて結果を切り捨てます。 右のオペランドは符号なしの型でなければならず、符号ありの型でシフトしようとするとコンパイルエラーになります。

シフトは、以下の方法で2の累乗を使って"シミュレート"できます。 なお、左オペランドの型への切り捨ては常に最後に行われますが、明示的には言及されていません。

  • x << y は数学的表現で x * 2**y に相当します。

  • x >> y は、数学的表現で x / 2**y を負の無限大に向けて丸めたものに相当します。

警告

バージョン 0.5.0 以前では、負の x の右シフト x >> y は、ゼロに向かって丸められた数学的表現 x / 2**y に相当していました。 つまり、右シフトでは、(負の無限大に向かって)切り捨てるのではなく、(ゼロに向かって)切り上げられていたのです。

注釈

シフト演算では、算術演算のようなオーバーフローチェックが行われません。 その代わり、結果は常に切り捨てられます。

加算、減算、乗算

加算、減算、乗算には通常のセマンティクスがあり、オーバーフローとアンダーフローに関しては2つの異なるモードがあります。

デフォルトでは、すべての演算はアンダーフローまたはオーバーフローをチェックしますが、 unchecked block を使ってこれを無効にでき、結果としてラッピング演算が行われます。 詳細はこのセクションを参照してください。

-x という表現は、 Tx の型として (T(0) - x) と同等です。 これは、符号付きの型にのみ適用できます。 x が負であれば、 -x の値は正になります。 また、2の補数表現にはもう1つの注意点があります。

int x = type(int).min; の場合は、 -x は正の範囲に当てはまりません。 つまり、 unchecked { assert(-x == x); } は動作し、 -x という表現をcheckedモードで使用すると、アサーションが失敗するということになります。

除算

演算の結果の型は常にオペランドの1つの型であるため、整数の除算は常に整数になります。 Solidityでは、除算はゼロに向かって丸められます。 これは、 int256(-5) / int256(2) == int256(-2) を意味します。

これに対し、 リテラル での除算では、任意の精度の分数値が得られることに注意してください。

注釈

ゼロによる除算は、 パニックエラー を引き起こします。 このチェックは unchecked { ... } で無効にできます。

注釈

type(int).min / (-1) という式は、除算でオーバーフローが発生する唯一のケースです。 チェックされた算術モードでは、これは失敗したアサーションを引き起こしますが、ラッピングモードでは、値は type(int).min になります。

モジュロ

モジュロ演算 a % n では、オペランド a をオペランド n で除算した後の余り r が得られますが、ここでは q = int(a / n)r = a - (n * q) が使われています。 つまり、モジュロの結果は左のオペランドと同じ符号(またはゼロ)になり、 a % n == -(-a % n) は負の a の場合も同様です。

  • int256(5) % int256(2) == int256(1)

  • int256(5) % int256(-2) == int256(1)

  • int256(-5) % int256(2) == int256(-1)

  • int256(-5) % int256(-2) == int256(-1)

注釈

ゼロでのモジュロは パニックエラー を引き起こします。 このチェックは unchecked { ... } で無効にできます。

指数

指数計算は、指数が符号なしの型の場合のみ可能です。 指数計算の結果の型は、常に基底の型と同じです。 結果を保持するのに十分な大きさであることに注意し、潜在的なアサーションの失敗やラッピングの動作に備えてください。

注釈

チェックされたモードでは、指数計算は小さな基底に対して比較的安価な exp というオペコードしか使いません。 x**3 の場合には x*x*x という表現の方が安いかもしれません。 いずれにしても、ガスコストのテストとオプティマイザの使用が望まれます。

注釈

なお、 0**0 はEVMでは 1 と定義されています。

固定小数点

警告

固定小数点数はSolidityではまだ完全にはサポートされていません。 宣言できますが、代入したり、代入解除したりできません。

fixed / ufixed: さまざまなサイズの符号付きおよび符号なしの固定小数点数。 キーワード ufixedMxNfixedMxNM は型で取るビット数、 N は小数点以下の数を表します。 M は8で割り切れるものでなければならず、8から256ビットまであります。 N は0から80までの値でなければなりません。 ufixedfixed は、それぞれ ufixed128x18fixed128x18 のエイリアスです。

演算子:

  • 比較: <=, <, ==, !=, >=, > ( bool に評価)

  • 算術演算子: +, -, 単項 -, *, /, % (モジュロ)

注釈

浮動小数点(多くの言語では floatdouble 、正確にはIEEE754の数値)と固定小数点の主な違いは、整数部と小数部(小数点以下の部分)に使用するビット数が、前者では柔軟に設定できるのに対し、後者では厳密に定義されていることです。 一般に、浮動小数点では、ほぼすべての空間を使って数値を表現するが、小数点の位置を決めるのは少数のビットです。

アドレス

アドレス型には2つの種類がありますが、ほとんど同じです。

  • address: 20バイトの値(Ethereumのアドレスのサイズ)を保持します。

  • address payable: address と同じですが、メンバの transfersend が追加されます。

この区別の背景にある考え方は、 address payable はEtherを送ることができるアドレスであるのに対し、プレーンな address はEtherを送ることが想定されない、というものです。 例えば、 address の変数に格納されたアドレスがEtherを受信できるように構築されていないスマートコントラクトである可能性があります。

型変換:

address payable から address への暗黙の変換は許されますが、 address から address payable への変換は payable(<address>) を介して明示的に行う必要があります。

uint160 、整数リテラル、 bytes20 、コントラクト型については、 address との明示的な変換が可能です。

address 型とコントラクト型の式のみが、明示的な変換 payable(...) によって address payable 型に変換できます。 コントラクト型については、コントラクトがEtherを受信できる場合、つまりコントラクトが receive またはpayableのフォールバック関数を持っている場合にのみ、この変換が可能です。 payable(0) は有効であり、このルールの例外であることに注意してください。

注釈

address 型の変数が必要で、その変数にEtherを送ろうと思っているなら、その変数の型を address payable と宣言して、この要求を見えるようにします。 また、この区別や変換はできるだけ早い段階で行うようにしてください。 address``と ``address payable``の区別はバージョン0.5.0で導入されました。 また、そのバージョンからコントラクトは暗黙的に ``address 型に変換できませんが、receiveまたはpayable fallback関数があれば、明示的に address または address payable 型に変換できます。

演算子:

  • <=, <, ==, !=, >=, >

警告

より大きなバイトサイズを使用する型、例えば bytes32 などを address に変換した場合、 address は切り捨てられます。 変換の曖昧さを減らすために、バージョン0.4.24以降のコンパイラでは、変換時に切り捨てを明示することを強制するようになっています。 例えば、32バイトの値 0x111122223333444455556666777788889999AAAABBBBCCCCDDDDEEEEFFFFCCCC を考えてみましょう。

address(uint160(bytes20(b))) を使うと 0x111122223333444455556666777788889999aAaa になり、 address(uint160(uint256(b))) を使うと 0x777788889999AaAAbBbbCcccddDdeeeEfFFfCcCc になります。

注釈

EIP-55 に準拠した大文字小文字混在の16進数は自動的に address 型のリテラルとして扱われます。 アドレスリテラル を参照してください。

アドレスのメンバ

全メンバーのアドレスの早見表は、 アドレス型のメンバー を参照してください。

  • balancetransfer

プロパティ balance を使ってアドレスの残高を照会したり、 transfer 関数を使って支払先のアドレスにイーサ(wei単位)を送信したりすることが可能です。

address payable x = payable(0x123);
address myAddress = address(this);
if (x.balance < 10 && myAddress.balance >= 10) x.transfer(10);

transfer 関数は、現在のコントラクトの残高が十分でない場合や、Ether送金が受信アカウントで拒否された場合に失敗します。 transfer 関数は失敗するとリバートします。

注釈

x がコントラクトアドレスの場合、そのコード(具体的には、 receive Ether関数 があればその receive Ether関数fallback関数 があればその fallback関数 )が transfer コールとともに実行されます(これはEVMの機能であり、防ぐことはできません)。 その実行がガス欠になるか、何らかの形で失敗した場合、Ether送金はリバートされ、現在のコントラクトは例外的に停止します。

  • send

send は、 transfer の低レベルのカウンターパートです。 実行に失敗した場合、現在のコントラクトは例外的に停止しませんが、 sendfalse を返します。

警告

send の使用にはいくつかの危険性があります。 コールスタックの深さが1024の場合(これは常に呼び出し側で強制できます)、送金は失敗し、また、受信者がガス欠になった場合も失敗します。 したがって、安全なEther送金を行うためには、 send の戻り値を常にチェックするか、 transfer を使用するか、あるいはさらに良い方法として、受信者がEtherを引き出すパターンを使用してください。

  • call, delegatecall, staticcall

ABIに準拠していないコントラクトとのインターフェースや、エンコーディングをより直接的に制御するために、関数 calldelegatecallstaticcall が用意されています。 これらの関数はすべて1つの bytes memory パラメータを受け取り、成功条件( bool )と戻りデータ( bytes memory )を返します。 関数 abi.encodeabi.encodePackedabi.encodeWithSelectorabi.encodeWithSignature は、構造化データのエンコードに使用できます。

例:

bytes memory payload = abi.encodeWithSignature("register(string)", "MyName");
(bool success, bytes memory returnData) = address(nameReg).call(payload);
require(success);

警告

これらの関数はすべて低レベルの関数であり、注意して使用する必要があります。 特に、未知のコントラクトは悪意を持っている可能性があり、それを呼び出すと、そのコントラクトに制御を渡すことになり、そのコントラクトが自分のコントラクトにコールバックする可能性があるので、コールが戻ってきたときの自分の状態変数の変化に備えてください。 他のコントラクトとやりとりする通常の方法は、コントラクトオブジェクト( x.f() )の関数を呼び出すことです。

注釈

以前のバージョンのSolidityでは、これらの関数が任意の引数を受け取ることができ、また、 bytes4 型の第1引数の扱いが異なっていました。 これらのエッジケースはバージョン0.5.0で削除されました。

gas モディファイアで供給ガスを調整することが可能です。

address(nameReg).call{gas: 1000000}(abi.encodeWithSignature("register(string)", "MyName"));

同様に、送金するEtherの値も制御できます。

address(nameReg).call{value: 1 ether}(abi.encodeWithSignature("register(string)", "MyName"));

最後に、これらのモディファイアは組み合わせることができます。 その順番は問題ではありません。

address(nameReg).call{gas: 1000000, value: 1 ether}(abi.encodeWithSignature("register(string)", "MyName"));

同様の方法で、関数 delegatecall を使用できます。 違いは、与えられたアドレスのコードのみが使用され、他のすべての側面(ストレージ、残高、...)は、現在のコントラクトから取得されます。 delegatecall の目的は、別のコントラクトに保存されているライブラリコードを使用することです。 ユーザーは、両方のコントラクトのストレージのレイアウトが、delegatecallを使用するのに適していることを確認しなければなりません。

注釈

Homestead以前のバージョンでは、 callcode という限定されたバリアントのみが利用可能で、オリジナルの msg.sendermsg.value の値にアクセスできませんでした。 この関数はバージョン0.5.0で削除されました。

Byzantiumから staticcall も使えるようになりました。 これは基本的に call と同じですが、呼び出された関数が何らかの形で状態を変更するとリバートされます。

calldelegatecallstaticcall の3つの関数は、非常に低レベルな関数で、Solidityの型安全性を壊してしまうため、 最後の手段 としてのみ使用してください。

gas オプションは3つの方式すべてで利用できますが、 value オプションは call でのみ利用できます。

注釈

スマートコントラクトのコードでは、状態の読み書きにかかわらず、ハードコードされたガスの値に依存することは、多くの落とし穴があるので避けたほうがよいでしょう。 また、ガスへのアクセスが将来変わる可能性もあります。

  • codecodehash

スマートコントラクトのデプロイコードをクエリできます。 .code を使用すると、EVMバイトコードを bytes memory として取得できます。 そのコードのkeccak-256ハッシュを取得するには .codehash を使用します( bytes32 として)。 なお、 addr.codehashkeccak256(addr.code) を使用するよりもコストが小さいです。

注釈

すべてのコントラクトは address 型に変換できるので、 address(this).balance を使って現在のコントラクトの残高を照会することが可能です。

コントラクト型

すべての コントラクト はそれ自身の型を定義します。 コントラクトを、それらが継承するコントラクトに暗黙的に変換できます。 コントラクトは、 address 型との間で明示的に変換できます。

address payable 型との間の明示的な変換は、コントラクト型にreceiveまたはpayableのフォールバック関数がある場合にのみ可能です。 変換は address(x) を使用して行われます。 コントラクト型にreceiveまたはpayment fallback関数がない場合、 address payable への変換は payable(address(x)) を使用して行うことができます。 詳細は、アドレス型 の項を参照してください。

注釈

バージョン0.5.0以前は、コントラクトはアドレス型から直接派生し、 addressaddress payable の区別はありませんでした。

コントラクト型( MyContract c )のローカル変数を宣言すると、そのコントラクトで関数を呼び出すことができます。 ただし、同じコントラクト型のどこかから代入するように注意してください。

また、コントラクトをインスタンス化することもできます(新規に作成することを意味します)。 詳細は 'Contracts via new' の項を参照してください。

コントラクトのデータ表現は address 型と同じで、この型は ABI でも使用されています。

コントラクトは、いかなる演算子もサポートしません。

コントラクト型のメンバは、 public とマークされた状態変数を含むコントラクトの外部関数です。

コントラクト C の場合は、 type(C) を使ってコントラクトに関する 型情報 にアクセスできます。

固定長バイト列

bytes1 , bytes2 , bytes3 , ..., bytes32 の値は、1から最大32までのバイト列を保持します。

演算子:

  • 比較: <=, <, ==, !=, >=, > ( bool に評価)

  • ビット演算子: &, |, ^ (ビットごとの排他的論理和), ~ (ビットごとの否定)

  • シフト演算子: << (左シフト), >> (右シフト)

  • インデックスアクセス: x が型 bytesI の場合、 0 <= k < I において x[k]k 番目のバイトを返します(読み取り専用)。

シフト演算子は、右オペランドに符号なし整数型を指定して動作します(ただし、左オペランドの型を返します)が、この型はシフトするビット数を表します。 符号付きの型でシフトするとコンパイルエラーになります。

メンバー:

  • .length は、バイト列の固定長を出力します(読み取り専用)。

注釈

bytes1[] 型はバイトの配列ですが、パディングのルールにより、各要素ごとに31バイトのスペースを無駄にしています(ストレージを除く)。 代わりに bytes 型を使うのが良いでしょう。

注釈

バージョン0.8.0以前では、 bytebytes1 のエイリアスでした。

動的サイズのバイト列

bytes:

動的なサイズのバイト配列。 配列 を参照。 値型ではありません!

string:

動的サイズのUTF-8エンコードされた文字列。 配列 を参照。 値型ではありません!

アドレスリテラル

アドレスチェックサムテストに合格した16進数リテラル(例: 0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF )は address 型です。 16進数リテラルの長さが39桁から41桁の間で、チェックサムテストに合格しない場合はエラーになります。 エラーを取り除くには、ゼロを前置(整数型の場合)または後置(bytesNN型の場合)する必要があります。

注釈

混合ケースのアドレスチェックサムフォーマットは EIP-55 で定義されています。

有理数リテラルと整数リテラル

整数リテラルは、0~9の範囲の数字の列で構成されます。 小数点以下の数字として解釈されます。 例えば、 69 は69を意味します。 Solidityには8進数のリテラルは存在せず、先頭のゼロは無効です。

10進数の小数リテラルは小数点 . の後に少なくとも1つの数字で形成されます。 例としては .11.3 があります(ただし 1. は含まれません)。

2e10 のような科学的表記(指数表記)もサポートされています。 仮数は小数でも構いませんが、指数は整数でなければなりません。 MeE というリテラルは M * 10**E と同じ意味です。 例としては、 2e10-2e102e-102.5e1 などがあります。

アンダースコアは、読みやすくするために数値リテラルの桁を区切るのに使用できます。 例えば、10進法の 123_000 、16進法の 0x2eff_abde 、科学的10進法の 1_2e345_678 はすべて有効です。 アンダースコアは2つの数字の間にのみ使用でき、連続したアンダースコアは1つしか使用できません。 アンダースコアを含む数値リテラルには、追加の意味はなく、アンダースコアは無視されます。

数値リテラル式は、非リテラル型に変換されるまで(すなわち、数値リテラル式以外のもの(ブーリアンリテラルなど)と一緒に使用するか、明示的に変換することにより)、任意の精度を保持します。 このため、数値リテラル式では、計算がオーバーフローしたり、除算が切り捨てられたりすることはありません。

例えば、 (2**800 + 1) - 2**800 の結果は定数 1uint8 型)になりますが、中間の結果はマシンのワードサイズに収まりません。 さらに、 .5 * 8 の結果は整数の 4 になります(ただし、その間には非整数が使われています)。

警告

ほとんどの演算子はリテラルに適用するとリテラル式を生成するが、このパターンに従わない演算子もあります:

  • 三項演算子( ... ? ... : ...

  • 配列の添字( <array>[<index>]

255 + (true ? 1 : 0)255 + [1, 2, 3][0] のような式は、256というリテラルを直接使うのと同じだと思うかもしれませんが、実際には uint8 型の中で計算されるためオーバーフローする可能性があります。

整数に適用できる演算子は、オペランドが整数であれば、数リテラル式にも適用できます。 2つのうちいずれかが小数の場合、ビット演算は許可されず、指数が小数の場合、指数演算は許可されません(非有理数になってしまう可能性があるため)。

リテラル数を左(またはベース)オペランドとし、整数型を右(指数)オペランドとするシフトと指数計算は、右(指数)オペランドの型にかかわらず、常に uint256 (非負のリテラルの場合)または int256 (負のリテラルの場合)型で実行されます。

警告

バージョン0.4.0以前のSolidityでは、整数リテラルの除算は切り捨てられていましたが、有理数に変換されるようになりました。 つまり、 5 / 22 とはならず、 2.5 となります。

注釈

Solidityでは、有理数ごとに数リテラル型が用意されています。 整数リテラルと有理数リテラルは、数リテラル型に属します。 また、すべての数リテラル式(数リテラルと演算子のみを含む式)は、数リテラル型に属します。 つまり、数リテラル式 1 + 22 + 1 は、有理数3に対して同じ数リテラル型に属しています。

注釈

数リテラル式は、非リテラル式と一緒に使われると同時に、非リテラル型に変換されます。 型に関係なく、以下の b に割り当てられた式の値は整数と評価されます。 auint128 型なので、 2.5 + a という式は適切な型を持っていなければなりませんが。 2.5uint128 の型には共通の型がないので、Solidityのコンパイラはこのコードを受け入れません。

uint128 a = 1;
uint128 b = 2.5 + a + 0.5;

文字列リテラルと文字列型

文字列リテラルは、ダブルクオートまたはシングルクオート( "foo" または 'bar' )で記述され、連続した複数の部分に分割することもできます( "foo" "bar""foobar" に相当)。 これは長い文字列を扱う際に便利です。 また、C言語のように末尾にゼロを付けることはなく、 "foo" は4バイトではなく3バイトです。 整数リテラルと同様に、その型は様々ですが、 bytes1, ..., bytes32 に暗黙のうちに変換され、それが適合する場合は、 bytesstring に変換されます。

例えば、 bytes32 samevar = "stringliteral" では文字列リテラルが bytes32 型に割り当てられると、生のバイト形式で解釈されます。

文字列リテラルには、印刷可能なASCII文字のみを含めることができます。 つまり、0x20から0x7Eまでの文字です。

さらに、文字列リテラルは以下のエスケープ文字にも対応しています。

  • \<newline> (実際の改行のエスケープ)

  • \\ (バックスラッシュ)

  • \' (シングルクォート)

  • \" (ダブルクォート)

  • \n (改行)

  • \r (キャリッジリターン)

  • \t (タブ)

  • \xNN (ヘックスエスケープ、下記参照)

  • \uNNNN (Unicodeエスケープ、下記参照)

\xNN は16進数の値を受け取り、適切なバイトを挿入します。 \uNNNN はUnicodeコードポイントを受け取り、UTF-8シーケンスを挿入します。

注釈

バージョン0.8.0までは、さらに3つのエスケープシーケンスがありました。 \b\f\v です。 これらは他の言語ではよく使われていますが、実際にはほとんど必要ありません。 もし必要であれば、他のASCII文字と同じように16進数のエスケープ、すなわち \x08\x0c\x0b を使って挿入できます。

次の例の文字列の長さは10バイトです。 この文字列は、改行バイトで始まり、ダブルクォート、シングルクォート、バックスラッシュ文字、そして(セパレータなしで)文字列 abcdef が続きます。

"\n\"\'\\abc\
def"

改行ではないUnicodeの行終端記号(LF、VF、FF、CR、NEL、LS、PSなど)は、文字列リテラルを終了するものとみなされます。 改行が文字列リテラルを終了させるのは、その前に \ がない場合のみです。

Unicodeリテラル

通常の文字列リテラルはASCIIのみを含むことができますが、Unicodeリテラル(キーワード unicode を前に付けたもの)は、有効なUTF-8シーケンスを含むことができます。 また、Unicodeリテラルは、通常の文字列リテラルと同じエスケープシーケンスにも対応しています。

string memory a = unicode"Hello 😃";

16進数リテラル

16進数リテラルは、キーワード hex を前に付け、ダブルクオートまたはシングルクオートで囲みます( hex"001122FF"hex'0011_22_FF' )。 リテラルの内容は16進数でなければならず、バイト境界のセパレータとしてアンダースコアを1つ使用することも可能です。 リテラルの値は、16進数をバイナリ表現したものになります。

空白で区切られた複数の16進数リテラルは、1つのリテラルに連結されます。 hex"00112233" hex"44556677"hex"0011223344556677" と同じです。

16進数リテラルはある意味では、 文字列リテラル のように振る舞いますが、暗黙的に string 型に変換されるわけではありません。

列挙

列挙(enum)はSolidityでユーザー定義型を作成する一つの方法です。 すべての整数型との間で明示的に変換できますが、暗黙的な変換はできません。 整数型からの明示的な変換は、実行時に値が列挙型の範囲内にあるかどうかをチェックし、そうでない場合は パニックエラー を発生させます。 列挙型は少なくとも1つのメンバーを必要とし、宣言時のデフォルト値は最初のメンバーです。 列挙型は256以上のメンバーを持つことはできません。

データ表現は、C言語のenumと同じです。 オプションは、 0 から始まる後続の符号なし整数値で表されます。

type(NameOfEnum).mintype(NameOfEnum).max を使えば、与えられたenumの最小値と最大値を得ることができます。

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

contract test {
    enum ActionChoices { GoLeft, GoRight, GoStraight, SitStill }
    ActionChoices choice;
    ActionChoices constant defaultChoice = ActionChoices.GoStraight;

    function setGoStraight() public {
        choice = ActionChoices.GoStraight;
    }

    // 列挙型はABIの一部ではないため、Solidityの外部に対して、「getChoice」の署名は自動的に「getChoice() returns (uint8)」に変更されることになります。
    function getChoice() public view returns (ActionChoices) {
        return choice;
    }

    function getDefaultChoice() public pure returns (uint) {
        return uint(defaultChoice);
    }

    function getLargestValue() public pure returns (ActionChoices) {
        return type(ActionChoices).max;
    }

    function getSmallestValue() public pure returns (ActionChoices) {
        return type(ActionChoices).min;
    }
}

注釈

Enumは、コントラクトやライブラリの定義とは別に、ファイルレベルで宣言することもできます。

ユーザー定義の値型

ユーザー定義の値型は、基本的な値型をゼロコストで抽象化して作成できます。 これは、エイリアスに似ていますが、型の要件がより厳しくなっています。

ユーザー定義の値型は、 type C is V を使って定義されます。 C は新しく導入される型の名前で、 V は組み込みの値の型(「基礎となる型」)でなければなりません。 関数 C.wrap は、基礎となる型からカスタム型への変換に使用されます。 同様に、関数 C.unwrap はカスタム型から基礎型への変換に使用されます。

C 型には、演算子や付属のメンバ関数がありません。 特に、演算子 == も定義されていません。 他の型との間の明示的および暗黙的な変換は許されません。

このような型の値のデータ表現は、基礎となる型から継承され、基礎となる型はABIでも使用されます。

次の例では、18桁の10進数固定小数点型を表すカスタム型 UFixed256x18 と、その型に対して算術演算を行うための最小限のライブラリを示しています。

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

// 18進数、256ビット幅の固定小数点型をユーザー定義の値型を使用して表現します。
type UFixed256x18 is uint256;

/// UFixed256x18に対して固定小数点演算を行うための最小限のライブラリ
library FixedMath {
    uint constant multiplier = 10**18;

    /// 2つのUFixed256x18の値を足します。
    /// uint256のチェックされた算術に依存して、オーバーフローでリバートします。
    function add(UFixed256x18 a, UFixed256x18 b) internal pure returns (UFixed256x18) {
        return UFixed256x18.wrap(UFixed256x18.unwrap(a) + UFixed256x18.unwrap(b));
    }
    /// UFixed256x18の値とuint256の値を掛けます。
    /// uint256のチェックされた算術に依存して、オーバーフローでリバートします。
    function mul(UFixed256x18 a, uint256 b) internal pure returns (UFixed256x18) {
        return UFixed256x18.wrap(UFixed256x18.unwrap(a) * b);
    }
    /// UFixed256x18の数のフロアを取ります。
    /// @return `a` を超えない最大の整数。
    function floor(UFixed256x18 a) internal pure returns (uint256) {
        return UFixed256x18.unwrap(a) / multiplier;
    }
    /// uint256 を同じ値の UFixed256x18 に変換します。
    /// 整数が大きすぎる場合はリバートします。
    function toUFixed256x18(uint256 a) internal pure returns (UFixed256x18) {
        return UFixed256x18.wrap(a * multiplier);
    }
}

UFixed256x18.wrapFixedMath.toUFixed256x18 は同じ署名を持っていますが、全く異なる2つの処理を行っていることに注目してください。 UFixed256x18.wrap 関数は入力と同じデータ表現の UFixed256x18 を返すのに対し、 toUFixed256x18 は同じ数値を持つ UFixed256x18 を返します。

関数型

関数型は、関数の型です。 関数型の変数は、関数から代入でき、関数型のパラメータは、関数呼び出しに関数を渡したり、関数呼び出しから関数を返したりするのに使われます。 関数型には、 内部(internal) 関数と 外部(external) 関数の2種類があります。

内部関数は、現在のコントラクトのコンテキストの外では実行できないため、現在のコントラクトの内部(より具体的には、現在のコードユニットの内部で、内部ライブラリ関数や継承された関数も含む)でのみ呼び出すことができます。 内部関数の呼び出しは、現在のコントラクトの関数を内部で呼び出す場合と同様に、そのエントリーラベルにジャンプすることで実現します。

外部関数は、アドレスと関数シグネチャで構成されており、外部関数呼び出しを介して渡したり、外部関数呼び出しから返したりできます。

関数型は以下のように表記されています。

function (<parameter types>) {internal|external} [pure|view|payable] [returns (<return types>)]

パラメータ型とは対照的に、リターン型は空にできません。 関数型が何も返さない場合は、 returns (<return types>) の部分をすべて省略しなければなりません。

デフォルトでは、関数型は内部的なものなので、 internal キーワードは省略できます。 これは関数型にのみ適用されることに注意してください。 コントラクトで定義された関数については、ビジビリティを明示的に指定する必要があり、デフォルトはありません。

変換:

関数型 A は、それらのパラメータ型が同一であり、戻り値の型が同一であり、それらの内部/外部プロパティが同一であり、 A の状態の変更可能性が B の状態の変更可能性よりも制限されている場合に限り、関数型 B に暗黙的に変換可能です。 具体的には以下です。

  • pure 関数を viewnon-payable 関数に変換可能

  • view 関数から non-payable 関数への変換が可能

  • payable 関数から non-payable 関数への変換が可能

これら以外の関数型間の変換はできません。

payablenon-payable のルールは少しわかりにくいかもしれませんが、要するにある関数が payable であれば、ゼロのEtherの支払いも受け入れるということなので、 non-payable でもあるということです。 一方で、 non-payable 関数は送られてきたEtherを拒否するので、 non-payable 関数を payable 関数に変換することはできません。 明確にするために、etherを拒否することは、etherを拒否しないことよりも制限されます。 つまり、payableな関数をnon-payableな関数で上書きすることは可能ですが、その逆はできません。

さらに、 non-payable な関数ポインタを定義した場合、コンパイラは指定した関数が実際にEtherを拒否することを強制するわけではありません。 その代わりに、その関数ポインタは決して ether を送るために使われないことを強制します。 そのため、 payable な関数ポインタを non-payable な関数ポインタに割り当てることで、両方の型が同じように動作する、つまり、どちらもEtherを送信するために使用できないことを保証することが可能になります。

関数型変数が初期化されていない場合、それを呼び出すと パニックエラー になります。 また、関数に delete を使用した後に関数を呼び出した場合も同様です。

外部関数型がSolidityのコンテキスト外で使用される場合は、 function 型として扱われ、アドレスに続いて関数識別子をまとめて1つの bytes24 型にエンコードします。

現在のコントラクトのパブリック関数は、内部関数としても外部関数としても使用できることに注意してください。 f を内部関数として使用したい場合は f を、外部関数として使用したい場合は this.f を使用してください。

内部型の関数は、どこで定義されているかに関わらず、内部関数型の変数に代入できます。 これには、コントラクトとライブラリの両方のプライベート関数、内部関数、パブリック関数のほか、フリーの関数も含まれます。 一方、外部関数型は、パブリック関数と外部コントラクト関数にのみ対応しています。

注釈

calldata パラメータを持つ外部関数は、 calldata パラメータを持つ外部関数型と互換性がありません。 代わりに memory パラメータを持つ対応する型と互換性があります。 例えば、 function (string calldata) external 型の値が指すことのできる関数はありませんが、 function (string memory) externalfunction f(string memory) external {}function g(string calldata) external {} を指すことができます。 これは、どちらの場所でも、引数が同じように関数に渡されるからです。 呼び出し元はcalldataを直接外部関数に渡すことはできず、常に引数をメモリにABIエンコードします。 パラメータを calldata としてマークすることは、外部関数の実装にのみ影響し、呼び出し側の関数ポインタでは意味を持ちません。

ライブラリは、 delegatecallセレクタへの異なるABI規約 の使用を必要とするため、除外されます。 インターフェースで宣言された関数は定義を持たないので、それを指し示すことも意味がありません。

メンバー:

外部(またはパブリック)関数には、次のようなメンバーを持ちます。

  • .address は、関数のコントラクトのアドレスを返します。

  • .selectorABI関数セレクタ を返します。

注釈

外部(またはパブリック)関数には、追加のメンバー .gas(uint).value(uint) がありました。 これらはSolidity 0.6.2で非推奨となり、Solidity 0.7.0で削除されました。 代わりに {gas: ...}{value: ...} を使って、それぞれ関数に送られるガスの量やweiの量を指定してください。 詳細は 外部関数呼び出し を参照してください。

メンバーの使用法を示す例:

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

contract Example {
    function f() public payable returns (bytes4) {
        assert(this.f.address == address(this));
        return this.f.selector;
    }

    function g() public {
        this.f{gas: 10, value: 800}();
    }
}

内部関数型の使用法を示す例:

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

library ArrayUtils {
    // 内部関数は、同じコードコンテキストの一部となるため、内部ライブラリ関数で使用できる
    function map(uint[] memory self, function (uint) pure returns (uint) f)
        internal
        pure
        returns (uint[] memory r)
    {
        r = new uint[](self.length);
        for (uint i = 0; i < self.length; i++) {
            r[i] = f(self[i]);
        }
    }

    function reduce(
        uint[] memory self,
        function (uint, uint) pure returns (uint) f
    )
        internal
        pure
        returns (uint r)
    {
        r = self[0];
        for (uint i = 1; i < self.length; i++) {
            r = f(r, self[i]);
        }
    }

    function range(uint length) internal pure returns (uint[] memory r) {
        r = new uint[](length);
        for (uint i = 0; i < r.length; i++) {
            r[i] = i;
        }
    }
}

contract Pyramid {
    using ArrayUtils for *;

    function pyramid(uint l) public pure returns (uint) {
        return ArrayUtils.range(l).map(square).reduce(sum);
    }

    function square(uint x) internal pure returns (uint) {
        return x * x;
    }

    function sum(uint x, uint y) internal pure returns (uint) {
        return x + y;
    }
}

外部関数型を使用するもう一つの例:

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

contract Oracle {
    struct Request {
        bytes data;
        function(uint) external callback;
    }

    Request[] private requests;
    event NewRequest(uint);

    function query(bytes memory data, function(uint) external callback) public {
        requests.push(Request(data, callback));
        emit NewRequest(requests.length - 1);
    }

    function reply(uint requestID, uint response) public {
        // Here goes the check that the reply comes from a trusted source
        requests[requestID].callback(response);
    }
}

contract OracleUser {
    Oracle constant private ORACLE_CONST = Oracle(address(0x00000000219ab540356cBB839Cbe05303d7705Fa)); // known contract
    uint private exchangeRate;

    function buySomething() public {
        ORACLE_CONST.query("USD", this.oracleResponse);
    }

    function oracleResponse(uint response) public {
        require(
            msg.sender == address(ORACLE_CONST),
            "Only oracle can call this."
        );
        exchangeRate = response;
    }
}

注釈

ラムダ関数やインライン関数が予定されていますが、まだサポートされていません。

参照型

参照型の値は、複数の異なる名前で変更できます。 値型の変数が使われるたびに独立したコピーを得ることができる値型とは対照的です。 そのため、参照型は値型よりも慎重に扱う必要があります。 現在、参照型は構造体、配列、マッピングで構成されています。 参照型を使用する場合は、その型が格納されているデータ領域を常に明示的に提供する必要があります。 memory (ライフタイムが外部の関数呼び出しに限定される)、 storage (状態変数が格納されている場所で、ライフタイムがコントラクトのライフタイムに限定される)、 calldata (関数の引数が格納されている特別なデータの場所)のいずれかになります。

データロケーションを変更する割り当てや型変換は、常に自動コピー操作が発生しますが、同じデータロケーション内の割り当ては、ストレージ型の場合、一部のケースでしかコピーされません。

データロケーション

すべての参照型には、それがどこに保存されているかについて、「データロケーション」という追加のアノテーションがあります。 データロケーションは3つあります。 memorystoragecalldata です。 コールデータは、関数の引数が格納される、変更不可能で永続性のない領域で、ほとんどメモリのように動作します。

注釈

できれば、データの場所として calldata を使うようにしましょう。 コピーを避けることができますし、データが変更されないようにすることもできます。 calldata 型のデータを持つ配列や構造体は、関数から返すことができますが、そのような型を代入することはできません。

注釈

バージョン0.6.9以前では、参照型引数のデータロケーションは、外部関数では calldata 、パブリック関数では memory 、内部およびプライベート関数では memory または storage に制限されていました。 現在では、 memorycalldata は、そのビジビリティに関わらず、すべての関数で許可されています。

注釈

バージョン0.5.0までは、データロケーションを省略でき、変数の種類や関数の種類などに応じて異なるロケーションをデフォルトとしていましたが、現在ではすべての複合型でデータのロケーションを明示的に指定しなければなりません。

データロケーションと代入の挙動

データロケーションは、データの永続性だけでなく、代入のセマンティクスにも関係します。

  • storagememory の間(または calldata から)の代入では、常に独立したコピーが作成されます。

  • memory から memory への代入では、参照のみが作成されます。 つまり、あるメモリ変数の変更は、同じデータを参照している他のすべてのメモリ変数にも反映されるということです。

  • storage から ローカル ストレージ変数への代入もまた、参照のみを代入します。

  • その他の storage への代入は、常にコピーされます。 このケースの例としては、状態変数やストレージ構造体型のローカル変数のメンバへの代入がありますが、ローカル変数自体が単なる参照であっても同様です。

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

contract C {
    // xのデータロケーションはstorageです。
    // データロケーションを省略できるのはここだけです。
    uint[] x;

    // memoryArray のデータロケーションはメモリです。
    function f(uint[] memory memoryArray) public {
        x = memoryArray; // 配列全体をストレージにコピーします。
        uint[] storage y = x; // ポインタが代入され、yのデータロケーションはストレージになります。
        y[7]; // 8番目の要素を返します。
        y.pop(); // yを通してxも修正されます。
        delete x; // 配列がクリアされ、yも修正されます。
        // ストレージに新しい一時的な無名配列を作成する必要がありますが、ストレージは「静的」に割り当てられているため、次のようにするとうまくいきません。
        // y = memoryArray;
        // Similarly, "delete y" is not valid, as assignments to local variables
        // referencing storage objects can only be made from existing storage objects.
        // It would "reset" the pointer, but there is no sensible location it could point to.
        // For more details see the documentation of the "delete" operator.
        // delete y;
        g(x); // gを呼び出し、xへの参照を渡します。
        h(x); // hを呼び出し、独立した一時的なコピーをメモリ上に作成します。
    }

    function g(uint[] storage) internal pure {}
    function h(uint[] memory) public pure {}
}

配列

配列は、コンパイル時に固定されたサイズも動的なサイズも持つことができます。

固定サイズ k 、要素型 T の配列の型は T[k] 、動的サイズの配列は T[] と書きます。

例えば、 uint の動的配列を5個並べた配列は uint[][5] と書きます。 この表記法は、他のいくつかの言語とは逆になっています。 Solidityでは、たとえ X がそれ自体配列であっても、 X[3] は常に X 型の3つの要素を含む配列です。 これは、Cなどの他の言語ではそうではありません。

インデックスはゼロベースで、アクセスは宣言とは逆方向になります。

例えば、変数 uint[][5] memory x がある場合、3番目の動的配列の7番目の uint にアクセスするには x[2][6] を使い、3番目の動的配列にアクセスするには x[2] を使います。 繰り返しになりますが、配列にもなる T 型に対して配列 T[5] a がある場合、 a[2] は常に T 型です。

配列の要素は、マッピングや構造体など、どのような型でもよいです。 一般的な型の制限が適用され、マッピングは storage データの場所にしか保存できず、一般に公開されている関数には ABI型 のパラメータが必要となります。

状態変数の配列に public をマークして、Solidityに ゲッター を作成させることが可能です。 数値インデックスは、getterの必須パラメータとなります。

配列の終端を超えてアクセスすると、アサーションが失敗します。 メソッド .push().push(value) は、動的なサイズの配列の末尾に新しい要素を追加するために使用できます。

注釈

動的なサイズの配列は、ストレージ内でのみサイズを変更できます。 メモリ上では、このような配列は任意のサイズにできますが、配列が割り当てられるとサイズを変更できません。

配列としての bytesstring

bytes 型と string 型の変数は、特殊な配列です。 bytes 型は bytes1[] と似ていますが、calldataとメモリにしっかりと詰め込まれています。 stringbytes と同じですが、長さやインデックスのアクセスはできません。

Solidityには文字列操作関数はありませんが、サードパーティ製の文字列ライブラリがあります。 また、 keccak256(abi.encodePacked(s1)) == keccak256(abi.encodePacked(s2)) を使って2つの文字列をそのkeccak256-hashで比較したり、 string.concat(s1, s2) を使って2つの文字列を連結できます。

memorybytes1[] を使うと要素間に31個のパディングバイトを追加するので、 bytes1[] よりも bytes を使用した方が安価です。 なお、 storage では、タイトパッキングのためパディングは存在しません。 bytesとstring を参照してください。 原則として、任意の長さの生バイトデータには bytes を、任意の長さの文字列(UTF-8)データには string を使用してください。 長さを一定のバイト数に制限できる場合は、値型 bytes1bytes32 のいずれかを必ず使用してください。 その方がはるかに安価です。

注釈

s という文字列のバイト表現にアクセスしたい場合は、 bytes(s).length / bytes(s)[7] = 'x'; を使います。 UTF-8表現の低レベルバイトにアクセスしているのであって、個々の文字にアクセスしているわけではないことに注意してください。

bytes.concat 関数と string.concat 関数

string.concat を使えば、任意の数の string の値を連結できます。 この関数は、引数の内容をパディングせずに格納した単一の string memory を返します。 暗黙のうちに string に変換できない他の型のパラメータを使用したい場合は、まず string に変換する必要があります。

同様に、 bytes.concat 関数は、任意の数の bytes または bytes1 ... bytes32 値を連結させることができます。 この関数は、引数の内容をパディングせずに格納した単一の bytes memory を返します。 文字列パラメータや、暗黙のうちに bytes に変換できない他の型を使用したい場合は、最初に bytes または bytes1/.../bytes32 に変換する必要があります。

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

contract C {
    string s = "Storage";
    function f(bytes calldata bc, string memory sm, bytes16 b) public view {
        string memory concatString = string.concat(s, string(bc), "Literal", sm);
        assert((bytes(s).length + bc.length + 7 + bytes(sm).length) == bytes(concatString).length);

        bytes memory concatBytes = bytes.concat(bytes(s), bc, bc[:2], "Literal", bytes(sm), b);
        assert((bytes(s).length + bc.length + 2 + 7 + bytes(sm).length + b.length) == concatBytes.length);
    }
}

引数なしで string.concat あるいは bytes.concat を呼び出すと、空の配列が返されます。

メモリ配列のアロケート

動的な長さを持つメモリ配列は、 new 演算子を使って作成できます。 ストレージ配列とは対照的に、メモリ配列のサイズを変更できません(例えば、 .push メンバ関数は使用できません)。 必要なサイズを事前に計算するか、新しいメモリ配列を作成してすべての要素をコピーする必要があります。

Solidityのすべての変数と同様に、新しく割り当てられた配列の要素は、常に デフォルト値 で初期化されます。

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

contract C {
    function f(uint len) public pure {
        uint[] memory a = new uint[](7);
        bytes memory b = new bytes(len);
        assert(a.length == 7);
        assert(b.length == len);
        a[6] = 8;
    }
}
配列リテラル

配列リテラルは、1つまたは複数の式を角括弧( [...] )で囲んだコンマ区切りのリストです。 例えば、 [1, a, f(3)] です。 配列リテラルの型は以下のように決定されます。

これは、常に静的サイズのメモリ配列で、その長さは式の数です。

配列の基本型は、リストの最初の式の型で、他のすべての式が暗黙的に変換できるようになっています。 これができない場合は型エラーとなります。

すべての要素に変換できる型があるだけでは不十分です。 要素の一つがその型でなければなりません。

下の例では、それぞれの定数の型が uint8 であることから、 [1, 2, 3] の型は uint8[3] memory となります。 結果を uint[3] memory 型にしたい場合は、最初の要素を uint に変換する必要があります。

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

contract C {
    function f() public pure {
        g([uint(1), 2, 3]);
    }
    function g(uint[3] memory) public pure {
        // ...
    }
}

配列リテラル [1, -1] が無効なのは、最初の式の型が uint8 であるのに対し、2番目の式の型が int8 であり、両者を暗黙的に変換できないからです。 これを動作させるには、例えば [int8(1), -1] を使用します。

異なる型の固定サイズのメモリ配列は、(基底型が変換できても)相互に変換できないため、二次元配列リテラルを使用する場合は、常に共通の基底型を明示的に指定する必要があります。

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

contract C {
    function f() public pure returns (uint24[2][4] memory) {
        uint24[2][4] memory x = [[uint24(0x1), 1], [0xffffff, 2], [uint24(0xff), 3], [uint24(0xffff), 4]];
        // 以下のようにすると、内側の配列の一部が正しい型でないため、うまくいきません。
        // uint[2][4] memory x = [[0x1, 1], [0xffffff, 2], [0xff, 3], [0xffff, 4]];
        return x;
    }
}

固定サイズのメモリ配列を、動的サイズのメモリ配列に代入することはできません。 つまり、以下のことはできません。

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

// これではコンパイルできません。
contract C {
    function f() public {
        // 次の行は、uint[3]メモリをuint[]メモリに変換できないため、型エラーが発生します。
        uint[] memory x = [uint(1), 3, 4];
    }
}

将来はこの制限を解除する予定ですが、ABIでの配列の渡し方の関係で複雑になっています。

動的なサイズの配列を初期化したい場合は、個々の要素を代入する必要があります。

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

contract C {
    function f() public pure {
        uint[] memory x = new uint[](3);
        x[0] = 1;
        x[1] = 3;
        x[2] = 4;
    }
}
配列のメンバー
length:

配列は、要素数を表す length メンバを持ちます。 メモリー配列の長さは、作成時に固定されます(ただし、動的、つまり実行時のパラメータに依存することがあります)。

push():

動的ストレージ配列と bytesstring ではありません)には、 push() というメンバ関数があり、配列の最後にゼロ初期化された要素を追加するのに使用できます。 この関数は、要素への参照を返すので、 x.push().t = 2x.push() = b のように使用できます。

push(x):

動的ストレージ配列と bytes``( ``string ではありません)には、 push(x) というメンバ関数があり、配列の最後に与えられた要素を追加するのに使用できます。 この関数は何も返しません。

pop():

動的ストレージ配列と bytesstring ではありません)には pop() というメンバ関数があり、配列の最後から要素を削除するのに使用できます。 この関数は、削除された要素に対して delete を暗黙的に呼び出します。 この関数は何も返しません。

注釈

push() をコールしてストレージ配列の長さを増加させると、ストレージがゼロ初期化されるため、ガスコストが一定になります。 一方、 pop() をコールして長さを減少させると、削除される要素の「サイズ」に依存するコストが発生します。 その要素が配列の場合は、 delete を呼び出すのと同様に、削除された要素を明示的にクリアすることが含まれるため、非常にコストがかかります。

注釈

配列の配列を(publicではなく)外部関数で使用するには、ABI coder v2を有効にする必要があります。

注釈

Byzantium以前のEVMバージョンでは、関数呼び出しから返される動的配列にアクセスできませんでした。 動的配列を返す関数を呼び出す場合は、必ずByzantiumモードに設定されたEVMを使用してください。

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

contract ArrayContract {
    uint[2**20] aLotOfIntegers;
    // 以下は動的配列のペアではなく、ペアの動的配列(つまり長さ2の固定サイズ配列のペア)であることに注意してください。
    // In Solidity, T[k] and T[] are always arrays with elements of type T, even if T itself is an array.
    // Because of that, bool[2][] is a dynamic array of elements that are bool[2]. This is different from other languages, like C.
    // すべての状態変数のデータロケーションはストレージです。
    bool[2][] pairsOfFlags;

    // newPairsはメモリに格納されます - パブリックコントラクト関数の引数として唯一の選択肢です。
    function setAllFlagPairs(bool[2][] memory newPairs) public {
        // ストレージ配列への代入は、 ``newPairs`` のコピーを実行し、完全な配列 ``pairsOfFlags`` を置き換えます。
        pairsOfFlags = newPairs;
    }

    struct StructType {
        uint[] contents;
        uint moreInfo;
    }
    StructType s;

    function f(uint[] memory c) public {
        // ``s`` への参照を ``g`` に格納します。
        StructType storage g = s;
        // ``s.moreInfo`` も変更します。
        g.moreInfo = 2;
        // コピーを代入します。
        // なぜなら ``g.contents`` はローカル変数ではなく、ローカル変数のメンバだからです。
        g.contents = c;
    }

    function setFlagPair(uint index, bool flagA, bool flagB) public {
        // 存在しないインデックスにアクセスすると、例外が発生します。
        pairsOfFlags[index][0] = flagA;
        pairsOfFlags[index][1] = flagB;
    }

    function changeFlagArraySize(uint newSize) public {
        // 配列の長さを変更するには、push と pop を使用するのが唯一の方法です。
        if (newSize < pairsOfFlags.length) {
            while (pairsOfFlags.length > newSize)
                pairsOfFlags.pop();
        } else if (newSize > pairsOfFlags.length) {
            while (pairsOfFlags.length < newSize)
                pairsOfFlags.push();
        }
    }

    function clear() public {
        // これらは、配列を完全にクリアします。
        delete pairsOfFlags;
        delete aLotOfIntegers;
        // これも同じ効果です。
        pairsOfFlags = new bool[2][](0);
    }

    bytes byteData;

    function byteArrays(bytes memory data) public {
        // バイト配列("bytes")はパディングなしで格納されるため異なりますが、"uint8[]"と同じように扱うことができます。
        byteData = data;
        for (uint i = 0; i < 7; i++)
            byteData.push();
        byteData[3] = 0x08;
        delete byteData[2];
    }

    function addFlag(bool[2] memory flag) public returns (uint) {
        pairsOfFlags.push(flag);
        return pairsOfFlags.length;
    }

    function createMemoryArray(uint size) public pure returns (bytes memory) {
        // 動的メモリ配列は `new` を用いて作成します。
        uint[2][] memory arrayOfPairs = new uint[2][](size);

        // インライン配列は常に静的サイズであり、リテラルのみを使用する場合は、少なくとも1つの型を提供する必要があります。
        arrayOfPairs[0] = [uint(1), 2];

        // 動的バイト列を作成します。
        bytes memory b = new bytes(200);
        for (uint i = 0; i < b.length; i++)
            b[i] = bytes1(uint8(i));
        return b;
    }
}
文字列の要素へのダングリング参照

文字列を扱う場合、ぶら下がり参照を避けるように注意する必要があります。 ぶら下がり参照とは、もはや存在しないものを指す参照、あるいは参照を更新せずに移動された参照のことです。 例えば、ローカル変数に配列の要素への参照を格納した後、格納されている配列から .pop() を行うと、ダングリング参照が発生する可能性があります:

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

contract C {
    uint[][] s;

    function f() public {
        // Stores a pointer to the last array element of s.
        uint[] storage ptr = s[s.length - 1];
        // Removes the last array element of s.
        s.pop();
        // Writes to the array element that is no longer within the array.
        ptr.push(0x42);
        // Adding a new element to ``s`` now will not add an empty array, but
        // will result in an array of length 1 with ``0x42`` as element.
        s.push();
        assert(s[s.length - 1][0] == 0x42);
    }
}

ptr.push(0x42) の書き込みは、 ptr がもはや s の有効な要素を指していないにもかかわらず、 リバートしません 。 コンパイラは、未使用のストレージは常にゼロになると仮定しているので、その後の s.push() はストレージに明示的にゼロを書き込むことはありません。 したがって、 push() の後の s の最後の要素は、長さが 1 で、最初の要素として 0x42 を含むことになります。

Solidityでは、ストレージ内の値型への参照を宣言できないことに注意してください。 これらの種類の明示的なダングリング参照は、ネストされた参照型に制限されます。 しかし、タプル代入で複雑な式を使用する場合、ダングリング参照も一時的に発生する可能性があります:

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

contract C {
    uint[] s;
    uint[] t;
    constructor() {
        // Push some initial values to the storage arrays.
        s.push(0x07);
        t.push(0x03);
    }

    function g() internal returns (uint[] storage) {
        s.pop();
        return t;
    }

    function f() public returns (uint[] memory) {
        // The following will first evaluate ``s.push()`` to a reference to a new element
        // at index 1. Afterwards, the call to ``g`` pops this new element, resulting in
        // the left-most tuple element to become a dangling reference. The assignment still
        // takes place and will write outside the data area of ``s``.
        (s.push(), g()[0]) = (0x42, 0x17);
        // A subsequent push to ``s`` will reveal the value written by the previous
        // statement, i.e. the last element of ``s`` at the end of this function will have
        // the value ``0x42``.
        s.push();
        return s;
    }
}

ストレージへの代入はステートメントごとに1回だけにして、代入の左辺に複雑な式を書かないようにするのが常に安全です。

bytes配列の要素への参照を扱う場合は特に注意が必要です。 bytes配列に .push() を行うと、ストレージ内のレイアウトが shortからlongに切り替わる 可能性があるからです。

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

// This will report a warning
contract C {
    bytes x = "012345678901234567890123456789";

    function test() external returns(uint) {
        (x.push(), x.push()) = (0x01, 0x02);
        return x.length;
    }
}

ここで、最初の x.push() が評価されたとき、 x はまだショートレイアウトで格納されており、 x.push()x の最初の格納スロットの要素への参照を返します。 しかし、2番目の x.push() はバイト配列をラージレイアウトに切り替えます。 このとき、 x.push() が参照していた要素は配列のデータエリアにあるのですが、参照は元の体の元を指しています。 安全のために、1回の代入でバイト配列を最大1要素だけ大きくし、同じステートメントで同時に配列にインデックスアクセスしないようにしてください。

上記は、現在のバージョンのコンパイラにおけるダングリングストレージ参照の動作について説明したものですが、ダングリング参照を含むコードはすべて、 未定義の動作 を持つと考えるべきです。 特に、将来のバージョンのコンパイラーは、ダングリング参照を含むコードの動作を変更する可能性があるということです。

コードの中でダングリング参照を避けるようにしてください!

配列のスライス

配列のスライスは、配列の連続した部分のビューです。 スライスは x[start:end] と書き、 startend はuint256型になる(または暗黙のうちに変換できる)式です。 スライスの最初の要素は x[start] で、最後の要素は x[end - 1] です。

startend より大きい場合や、 end が配列の長さより大きい場合は、例外が発生します。

startend はどちらもオプションです。 start はデフォルトで 0end はデフォルトで配列の長さになります。

配列スライスは、メンバーを持ちません。 スライスは、基礎となる型の配列に暗黙的に変換可能で、インデックスアクセスをサポートします。 インデックスアクセスは、基礎となる配列での絶対的なものではなく、スライスの開始点からの相対的なものです。

配列スライスには型名がありません。 つまり、どの変数も配列スライスを型として持つことはできず、中間式にのみ存在することになります。

注釈

現在、配列スライスはcalldata配列に対してのみ実装されています。

配列スライスは、関数のパラメータで渡された二次データをABIデコードするのに便利です。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.5 <0.9.0;
contract Proxy {
    /// @dev プロキシ(すなわちこのコントラクト)で管理するクライアントコントラクトのアドレス
    address client;

    constructor(address client_) {
        client = client_;
    }

    /// 引数のアドレスの基本的な検証を行った後、クライアントが実装する"setOwner(address)"のフォワードコール
    function forward(bytes calldata payload) external {
        bytes4 sig = bytes4(payload[:4]);
        // 切り捨て処理のため、bytes4(payload)も同じ処理
        // bytes4 sig = bytes4(payload);
        if (sig == bytes4(keccak256("setOwner(address)"))) {
            address owner = abi.decode(payload[4:], (address));
            require(owner != address(0), "Address of owner cannot be zero.");
        }
        (bool status,) = client.delegatecall(payload);
        require(status, "Forwarded call failed.");
    }
}

構造体

Solidityでは、構造体の形で新しい型を定義する方法を提供しており、次の例のようになります。

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

// 2つのフィールドを持つ新しい型を定義します。
// 構造体をコントラクトの外部で宣言すると、複数のコントラクトで共有できるようになります。
// ここでは、これはあまり必要ありません。
struct Funder {
    address addr;
    uint amount;
}

contract CrowdFunding {
    // 構造体はコントラクトの内部で定義することもでき、その場合、その内部および派生コントラクトでのみ認識できるようになります。
    struct Campaign {
        address payable beneficiary;
        uint fundingGoal;
        uint numFunders;
        uint amount;
        mapping(uint => Funder) funders;
    }

    uint numCampaigns;
    mapping(uint => Campaign) campaigns;

    function newCampaign(address payable beneficiary, uint goal) public returns (uint campaignID) {
        campaignID = numCampaigns++; // campaignIDは返り値です。
        // "campaigns[campaignID] = Campaign(beneficiary, goal, 0, 0)"は、
        // 右側がマッピングを含むメモリ構造体"Campaign"を作成するため、使用することはできません。
        Campaign storage c = campaigns[campaignID];
        c.beneficiary = beneficiary;
        c.fundingGoal = goal;
    }

    function contribute(uint campaignID) public payable {
        Campaign storage c = campaigns[campaignID];
        // 指定された値で初期化された新しい一時的なメモリ構造体を作成し、ストレージにコピーします。
        // Funder(msg.sender, msg.value) を使用して初期化することもできます。
        c.funders[c.numFunders++] = Funder({addr: msg.sender, amount: msg.value});
        c.amount += msg.value;
    }

    function checkGoalReached(uint campaignID) public returns (bool reached) {
        Campaign storage c = campaigns[campaignID];
        if (c.amount < c.fundingGoal)
            return false;
        uint amount = c.amount;
        c.amount = 0;
        c.beneficiary.transfer(amount);
        return true;
    }
}

このコントラクトは、クラウドファンディングのコントラクトの機能をすべて提供するものではありませんが、構造体を理解するために必要な基本的な概念が含まれています。 構造体はマッピングや配列の内部で使用でき、構造体自身もマッピングや配列を含むことができます。

構造体に自身の型のメンバーを含めることはできませんが、構造体自体をマッピングメンバーの値の型にしたり、構造体にその型の動的サイズの配列を含めることはできます。 構造体のサイズは有限である必要があるため、この制限は必要です。

すべての関数で、構造体型がデータロケーション storage のローカル変数に代入されていることに注目してください。 これは構造体をコピーするのではなく、参照を保存するだけなので、ローカル変数のメンバーへの代入は実際にステートに書き込まれます。

もちろん、 campaigns[campaignID].amount = 0 のようにローカル変数に代入せずに、構造体のメンバーに直接アクセスすることもできます。

注釈

Solidity 0.7.0までは、ストレージのみの型(マッピングなど)のメンバーを含むメモリ構造が許可されており、上の例の campaigns[campaignID] = Campaign(beneficiary, goal, 0, 0) のような代入が機能し、それらのメンバーを静かにスキップしていました。

マッピング型

マッピング型は mapping(KeyType KeyName? => ValueType ValueName?) という構文を使い、マッピング型の変数は mapping(KeyType KeyName? => ValueType ValueName?) VariableName という構文を使います。 KeyType は組み込みの値型、 bytesstring 、コントラクト型やenum型が使用できます。 マッピング、構造体、配列型など、その他のユーザー定義型や複雑な型は使用できません。 ValueType にはマッピング、配列、構造体を含む任意の型を指定できます。 KeyNameValueName はオプションであり、型以外の任意の有効な識別子を指定できます( mapping(KeyType => ValueType) も同様に機能します)。

マッピングは ハッシュテーブル と考えることができ、ありとあらゆるキーが存在するように仮想的に初期化され、バイト表現がすべてゼロである値(型の デフォルト値 )にマッピングされています。 キーデータはマッピングには保存されず、 keccak256 ハッシュのみが値の検索に使用されるという点で似ています。

このため、マッピングには長さや、キーや値が設定されているという概念がなく、割り当てられたキーに関する余分な情報( マッピングのクリア 参照)がないと消すことができません。

マッピングのデータロケーションは storage のみであるため、状態変数、関数内のストレージ参照型、ライブラリ関数のパラメータとして使用できます。 これらは、一般に公開されているコントラクト関数のパラメータやリターンパラメータとしては使用できません。 これらの制限は、マッピングを含む配列や構造体にも当てはまります。

マッピング型の状態変数を public としてマークすると、Solidityはあなたに代わって ゲッター を作成します。 KeyTypeKeyName (指定された場合)という名前のゲッターのパラメータになります。 もし ValueType が値型または構造体であれば、ゲッターは ValueTypeValueName という名前で返します。 もし ValueType が配列またはマッピングの場合、ゲッターは各 KeyType に対して再帰的に1つのパラメータを持ちます。

以下の例では、 MappingExample コントラクトがパブリック balances マッピングを定義しており、キー型は address 、値型は uint で、Ethereumアドレスを符号なし整数値にマッピングしています。 uint は値型なので、ゲッターは型にマッチした値を返しますが、 MappingUser コントラクトでは指定されたアドレスの値を返しているのがわかります。

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

contract MappingExample {
    mapping(address => uint) public balances;

    function update(uint newBalance) public {
        balances[msg.sender] = newBalance;
    }
}

contract MappingUser {
    function f() public returns (uint) {
        MappingExample m = new MappingExample();
        m.update(100);
        return m.balances(address(this));
    }
}

下の例は、 ERC20トークン を簡略化したものです。 _allowances は、別のマッピング型の中にマッピング型がある例です。

以下の例では、オプションの KeyNameValueName をマッピングに指定しています。 これはコントラクトの機能やバイトコードには影響せず、マッピングのゲッターのABIの入力と出力に name フィールドを設定するだけです。

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

contract MappingExampleWithNames {
    mapping(address user => uint balance) public balances;

    function update(uint newBalance) public {
        balances[msg.sender] = newBalance;
    }
}

以下の例では、 _allowances を使って、他の人があなたのアカウントから引き出すことができる金額を記録しています。

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

contract MappingExample {

    mapping(address => uint256) private _balances;
    mapping(address => mapping(address => uint256)) private _allowances;

    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);

    function allowance(address owner, address spender) public view returns (uint256) {
        return _allowances[owner][spender];
    }

    function transferFrom(address sender, address recipient, uint256 amount) public returns (bool) {
        require(_allowances[sender][msg.sender] >= amount, "ERC20: Allowance not high enough.");
        _allowances[sender][msg.sender] -= amount;
        _transfer(sender, recipient, amount);
        return true;
    }

    function approve(address spender, uint256 amount) public returns (bool) {
        require(spender != address(0), "ERC20: approve to the zero address");

        _allowances[msg.sender][spender] = amount;
        emit Approval(msg.sender, spender, amount);
        return true;
    }

    function _transfer(address sender, address recipient, uint256 amount) internal {
        require(sender != address(0), "ERC20: transfer from the zero address");
        require(recipient != address(0), "ERC20: transfer to the zero address");
        require(_balances[sender] >= amount, "ERC20: Not enough funds.");

        _balances[sender] -= amount;
        _balances[recipient] += amount;
        emit Transfer(sender, recipient, amount);
    }
}

イテレート可能なマッピング

マッピングはイテレートできません。 つまり、キーを列挙することもできません。 しかし、マッピングの上にデータ構造を実装し、その上で反復処理を行うことは可能です。 例えば、以下のコードでは、 IterableMapping ライブラリを実装し、 User コントラクトがデータを追加し、 sum 関数がすべての値を合計するために反復処理を行います。

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

struct IndexValue { uint keyIndex; uint value; }
struct KeyFlag { uint key; bool deleted; }

struct itmap {
    mapping(uint => IndexValue) data;
    KeyFlag[] keys;
    uint size;
}

type Iterator is uint;

library IterableMapping {
    function insert(itmap storage self, uint key, uint value) internal returns (bool replaced) {
        uint keyIndex = self.data[key].keyIndex;
        self.data[key].value = value;
        if (keyIndex > 0)
            return true;
        else {
            keyIndex = self.keys.length;
            self.keys.push();
            self.data[key].keyIndex = keyIndex + 1;
            self.keys[keyIndex].key = key;
            self.size++;
            return false;
        }
    }

    function remove(itmap storage self, uint key) internal returns (bool success) {
        uint keyIndex = self.data[key].keyIndex;
        if (keyIndex == 0)
            return false;
        delete self.data[key];
        self.keys[keyIndex - 1].deleted = true;
        self.size --;
    }

    function contains(itmap storage self, uint key) internal view returns (bool) {
        return self.data[key].keyIndex > 0;
    }

    function iterateStart(itmap storage self) internal view returns (Iterator) {
        return iteratorSkipDeleted(self, 0);
    }

    function iterateValid(itmap storage self, Iterator iterator) internal view returns (bool) {
        return Iterator.unwrap(iterator) < self.keys.length;
    }

    function iterateNext(itmap storage self, Iterator iterator) internal view returns (Iterator) {
        return iteratorSkipDeleted(self, Iterator.unwrap(iterator) + 1);
    }

    function iterateGet(itmap storage self, Iterator iterator) internal view returns (uint key, uint value) {
        uint keyIndex = Iterator.unwrap(iterator);
        key = self.keys[keyIndex].key;
        value = self.data[key].value;
    }

    function iteratorSkipDeleted(itmap storage self, uint keyIndex) private view returns (Iterator) {
        while (keyIndex < self.keys.length && self.keys[keyIndex].deleted)
            keyIndex++;
        return Iterator.wrap(keyIndex);
    }
}

// 使用方法
contract User {
    // データを保持する構造体
    itmap data;
    // データ型にライブラリ関数を適用します。
    using IterableMapping for itmap;

    // 何かを挿入する
    function insert(uint k, uint v) public returns (uint size) {
        // これは IterableMapping.insert(data, k, v) を呼び出します。
        data.insert(k, v);
        // 構造体のメンバーにアクセスすることは可能ですが、構造体をいじらないように注意する必要があります。
        return data.size;
    }

    // 保存されているすべてのデータの合計を計算します。
    function sum() public view returns (uint s) {
        for (
            Iterator i = data.iterateStart();
            data.iterateValid(i);
            i = data.iterateNext(i)
        ) {
            (, uint value) = data.iterateGet(i);
            s += value;
        }
    }
}

演算子

算術演算子やビット演算子は、2つのオペランドが同じ型でなくても適用できます。 例えば、 y = x + zxuint8 で、 zuint32 という型を持っていても計算できます。 このような場合、次のようなメカニズムで、演算が計算される型(これはオーバーフローの場合に重要です)と演算子の結果の型を決定します:

  1. 右オペランドの型が左オペランドの型に暗黙のうちに変換できる場合、左オペランドの型を使用します。

  2. 左オペランドの型が右オペランドの型に暗黙のうちに変換できる場合、右オペランドの型を使用します。

  3. それ以外の場合は、演算ができません。

オペランドの一方が リテラルの数値 の場合、まずその値を保持できる最小の型である「モバイル型」に変換されます(同じビット幅の符号なし型は符号付き型より「小さい」とみなされます)。 両方がリテラルの数値の場合、演算は事実上無制限の精度で計算されます。 つまり、式は必要な限りの精度で評価され、結果がリテラルでない型で使われたときに失われることはありません。

演算子の結果の型は、結果が常に bool になる比較演算子を除いて、演算が行われた型と同じです。

演算子 ** (指数)、 <<>> は、演算と結果において左オペランドの型を使用します。

三項演算子

三項演算子は、 <expression> ? <trueExpression> : <falseExpression> という形式の式で使われます。 これは、メインの <expression> の評価結果に応じて、後者の2つの式のうち1つを評価するものです。 もし <expression>true と評価されれば <trueExpression> が評価され、そうでなければ <falseExpression> が評価されます。

三項演算子の結果は、たとえすべてのオペランドが有理数リテラルであっても、有理数型を持ちません。 結果の型は、上記と同じように2つのオペランドの型から決定され、必要であれば最初にその移動型に変換されます。

その結果、 255 + (true ? 1 : 0) は算術オーバーフローによりリバートされてしまいます。 これは (true ? 1 : 0)uint8 型であるため、加算も uint8 で行う必要があり、256はこの型で許される範囲を超えているためです。

もう一つの結果として、 1.5 + 1.5 のような式は有効だが、 1.5 + (true ? 1.5 : 2.5) は無効です。 これは、前者が無制限の精度で評価される有理式であり、その最終値のみが重要だからです。 後者は小数有理数から整数への変換を伴うので、現在では認められていません。

複合演算子、インクリメント/デクリメント演算子

a がLValue(すなわち変数や代入可能なもの)の場合、以下の演算子がショートハンドとして利用できます。

a += ea = a + e と同等です。 演算子 -=*=/=%=|=&=^=<<=>>= はそれぞれ定義されています。 a++a--a += 1 / a -= 1 に相当しますが、表現自体は a の前の値のままです。 一方、 --a++a は、 a に同じ効果を与えますが、変更後の値を返します。

delete

delete a は、 a に型の初期値を割り当てます。 つまり、整数の場合は a = 0 と同じですが、配列にも使用でき、長さ0の動的配列や、すべての要素を初期値に設定した同じ長さの静的配列を割り当てます。 delete a[x] は、配列のインデックス x の項目を削除し、他のすべての要素と配列の長さはそのままにします。 これは特に、配列にギャップを残すことを意味します。 アイテムを削除する予定なら、 マッピング の方が良いでしょう。

構造体の場合は、すべてのメンバーがリセットされた構造体が割り当てられます。 つまり、 delete a 後の a の値は、以下の注意点を除いて、 a が代入なしで宣言された場合と同じになります。

delete はマッピングには影響を与えません(マッピングのキーは任意である可能性があり、一般的には不明であるため)。 そのため、構造体を削除すると、マッピングではないすべてのメンバーがリセットされ、またマッピングでない限り、そのメンバーに再帰します。 しかし、個々のキーとそれが何にマッピングされているかは削除できます。 a がマッピングであれば、 delete a[x]x に格納されている値を削除します。

delete a は実際には a への代入のように振る舞います。 つまり、 a に新しいオブジェクトを格納するということに注意することが重要です。 この違いは、 a が参照変数の場合に見られます。 delete aa 自体をリセットするだけで、以前参照していた値はリセットしません。

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

contract DeleteExample {
    uint data;
    uint[] dataArray;

    function f() public {
        uint x = data;
        delete x; // xを0にセットし、dataには影響を与えない
        delete data; // dataを0にセットし、xには影響を与えない
        uint[] storage y = dataArray;
        delete dataArray; // これは dataArray.length を 0 にするものですが、
        // uint[] は複合オブジェクトであるため、ストレージオブジェクトのエイリアスである y にも影響が及びます。
        // ストレージオブジェクトを参照するローカル変数への代入は、既存のストレージオブジェクトからしか行えないため、"delete y"は有効ではありません。
        assert(y.length == 0);
    }
}

演算子の優先順位

演算子の優先順位を評価順に並べると以下のようになります。

優先順位

説明

演算子

1

後置インクリメントとデクリメント

++, --

New式

new <typename>

配列添字アクセス

<array>[<index>]

メンバアクセス

<object>.<member>

関数的呼び出し

<func>(<args...>)

括弧

(<statement>)

2

前置インクリメントとデクリメント

++, --

単項マイナス

-

単項演算子

delete

論理否定

!

ビット単位の否定

~

3

累乗

**

4

乗算、除算、剰余

*, /, %

5

加算、減算

+, -

6

ビット単位のシフト演算

<<, >>

7

ビット単位のAND

&

8

ビット単位のXOR

^

9

ビット単位のOR

|

10

不等号演算子

<, >, <=, >=

11

等号演算子

==, !=

12

論理AND

&&

13

論理OR

||

14

三項演算子

<conditional> ? <if-true> : <if-false>

代入演算子

=, |=, ^=, &=, <<=, >>=, +=, -=, *=, /=, %=

15

カンマ演算子

,

初等型間の変換

暗黙的な変換

暗黙の型変換は、代入時、関数に引数を渡すとき、および演算子を適用するときに、コンパイラによって自動的に適用される場合があります。 一般に、セマンティック的に正しく、情報が失われないのであれば、値型間の暗黙の変換は可能です。

例えば、 uint8uint16 に、 int128int256 に変換できますが、 uint256-1 のような値を保持できないため、 int8uint256 に変換できません。

演算子が異なる型に適用される場合、コンパイラはオペランドの一方を他方の型に暗黙のうちに変換しようとします(代入の場合も同様)。 つまり、演算は常に一方のオペランドの型で実行されることになります。

どのような暗黙の変換が可能であるかの詳細については、型自体に関するセクションを参照してください。

下の例では、加算のオペランドである yz は同じ型ではありませんが、 uint8 は暗黙のうちに uint16 に変換でき、その逆はできません。 そのため、 yz の型に変換されてから、 uint16 の型で加算が行われます。 その結果、式 y + z の型は uint16 となります。 uint32 型の変数に代入されているため、加算の後に別の暗黙の変換が行われます。

uint8 y;
uint16 z;
uint32 x = y + z;

明示的な変換

コンパイラが暗黙的な変換を許可していないが、変換がうまくいくと確信している場合、明示的な型変換が可能な場合があります。 この場合、予期しない動作をしたり、コンパイラのセキュリティ機能を迂回したりすることがありますので、結果が期待通りのものであることを必ずテストしてください。

次の例では、マイナスの intuint に変換しています。

int  y = -3;
uint x = uint(y);

このコードスニペットの最後に、 x は256ビットの2の補数表現で-3である 0xfffff..fd (64の16進文字)という値を持つことになります。

整数を明示的に小さい型に変換すると、高次のビットが削られます。

uint32 a = 0x12345678;
uint16 b = uint16(a); // b は 0x5678 になる

整数がより大きな型に明示的に変換された場合、その整数は左に(すなわち高次の端に)パディングされます。 変換の結果は、元の整数と同じになります。

uint16 a = 0x1234;
uint32 b = uint32(a); // b は 0x00001234 になる
assert(a == b);

固定サイズのバイト、変換時の挙動が異なります。 これらは個々のバイトのシーケンスと考えることができ、より小さな型に変換するとシーケンスが切断されます。

bytes2 a = 0x1234;
bytes1 b = bytes1(a); // b は 0x12 になる

固定サイズのバイト型が明示的に大きな型に変換された場合は、右にパディングされます。 固定インデックスでバイトにアクセスすると、変換前と変換後で同じ値になります(インデックスがまだ範囲内にある場合)。

bytes2 a = 0x1234;
bytes4 b = bytes4(a); // b は 0x12340000 になる
assert(a[0] == b[0]);
assert(a[1] == b[1]);

整数と固定サイズのバイト配列は、切り捨てやパディングの際に異なる動作をするので、 整数と固定サイズのバイト配列の間の明示的な変換は、両者が同じサイズである場合にのみ許されます。 異なるサイズの整数と固定サイズのバイト配列の間で変換したい場合は、必要な切り捨てとパディングの規則を明示する中間変換を使用しなければなりません。

bytes2 a = 0x1234;
uint32 b = uint16(a); // b は 0x00001234 になる
uint32 c = uint32(bytes4(a)); // c は 0x12340000 になる
uint8 d = uint8(uint16(a)); // d は 0x34 になる
uint8 e = uint8(bytes1(a)); // e は 0x12 になる

bytes 配列と bytes calldata sliceは、明示的に固定バイト型( bytes1 / ... / bytes32 )に変換できます。 配列が対象となる固定バイト型よりも長い場合は、末尾の切り捨てが行われます。 配列が対象となる固定バイト型よりも短い場合は、末尾にゼロが詰められます。

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

contract C {
    bytes s = "abcdefgh";
    function f(bytes calldata c, bytes memory m) public view returns (bytes16, bytes3) {
        require(c.length == 16, "");
        bytes16 b = bytes16(m);  // m の長さが 16 より大きい場合、切り捨てが発生します。
        b = bytes16(s);  // 右詰めしたもので、結果は "abcdefgh\0\0\0\0\0\0\0\0"
        bytes3 b1 = bytes3(s); //切り捨て、b1は"abc"に等しい。
        b = bytes16(c[:8]);  // ゼロ埋めされる
        return (b, b1);
    }
}

リテラルと初等型間の変換

整数型

10進数や16進数のリテラルは、切り捨てずに表現できる大きさの整数型に暗黙のうちに変換できます。

uint8 a = 12; // OK
uint32 b = 1234; // OK
uint16 c = 0x123456; // 失敗、0x3456に切り捨てなければならないため。

注釈

バージョン0.8.0以前では、10進数や16進数のリテラルを明示的に整数型に変換できました。 0.8.0からは、このような明示的な変換は暗黙的な変換と同様に厳格になりました。

固定サイズバイト列

10進数リテラルを固定サイズのバイト列に暗黙的に変換できません。 16進数リテラルは変換できますが、それは16進数の桁数がバイト型のサイズにぴったり合う場合に限られます。 例外として、0の値を持つ10進数リテラルと16進数リテラルは、任意の固定サイズのバイト型に変換できます。

bytes2 a = 54321; // NG
bytes2 b = 0x12; // NG
bytes2 c = 0x123; // NG
bytes2 d = 0x1234; // OK
bytes2 e = 0x0012; // OK
bytes4 f = 0; // OK
bytes4 g = 0x0; // OK

文字列リテラルと16進文字列リテラルは、その文字数がバイト型のサイズと一致する場合、暗黙のうちに固定サイズのバイト配列に変換できます。

bytes2 a = hex"1234"; // OK
bytes2 b = "xy"; // OK
bytes2 c = hex"12"; // NG
bytes2 d = hex"123"; // NG
bytes2 e = "x"; // NG
bytes2 f = "xyz"; // NG

アドレス

アドレスリテラル で説明したように、チェックサムテストに合格した正しいサイズの16進数リテラルは address 型となります。 他のリテラルは暗黙的に address 型に変換できません。

明示的に address に変換できるのは bytes20uint160 だけです。

address apayable(a) によって address payable に明示的に変換できます。

注釈

バージョン0.8.0以前では、任意の整数型(符号あり、符号なし、サイズは問わない)から address または address payable への明示的な変換が可能でした。 0.8.0からは uint160 からの変換のみが可能になりました。

単位とグローバルで利用可能な変数

Etherの単位

リテラルの数に weigweiether の接尾辞を付けて、Etherの別の単位を指定できますが、接尾辞のないEtherの数はWeiとみなされます。

assert(1 wei == 1);
assert(1 gwei == 1e9);
assert(1 ether == 1e18);

単位の接尾辞は、10の累乗にだけ対応しています。

注釈

バージョン0.7.0では、単位 finneyszabo が削除されました。

時間の単位

リテラルの数の後に secondsminuteshoursdaysweeks などの接尾辞をつけると、時間の単位を指定できます。 秒を基本単位とし、単位は次のように単純なものです。

  • 1 == 1 seconds

  • 1 minutes == 60 seconds

  • 1 hours == 60 minutes

  • 1 days == 24 hours

  • 1 weeks == 7 days

これらの単位を使ってカレンダーの計算を行う場合、 うるう秒 のために1年が365日ではなく、1日が24時間でもないので注意が必要です。 うるう秒が予測できないため、正確なカレンダーライブラリは外部のオラクルで更新する必要があります。

注釈

上記の理由により、バージョン0.5.0では接尾語の years が削除されました。

これらの接尾辞は、変数には適用できません。 例えば、関数のパラメータを日単位で解釈したい場合は、以下のようになります。

function f(uint start, uint daysAfter) public {
    if (block.timestamp >= start + daysAfter * 1 days) {
        // ...
    }
}

特別な変数と関数

グローバルな名前空間に常に存在し、主にブロックチェーンに関する情報を提供するために使用されたり、汎用的なユーティリティー関数である特別な変数や関数があります。

ブロックとトランザクションのプロパティ

  • blockhash(uint blockNumber) returns (bytes32): blocknumber が直近256個のブロックの一つである場合は、与えられたブロックのハッシュ、そうでない場合はゼロを返す

  • block.basefee (uint): カレントブロックのベースフィー(base fee)( EIP-3198EIP-1559)

  • block.chainid (uint): カレントブロックのチェーンID

  • block.coinbase (address payable): カレントブロックのマイナーのアドレス

  • block.difficulty (uint): 現在のブロックの難易度( EVM < Paris )。 EVMの他のバージョンでは、 block.prevrandao の非推奨のエイリアスとして動作ます( EIP-4399 )。

  • block.gaslimit (uint): カレントブロックのガスリミット

  • block.number (uint): カレントブロックの番号

  • block.prevrandao (uint): ビーコンチェーンが提供する乱数( EVM >= Paris

  • block.timestamp ( uint ): カレントブロックのタイムスタンプ(Unixエポックからの秒数)

  • gasleft() returns (uint256): 残りのガス

  • msg.data (bytes calldata): 完全なコールデータ

  • msg.sender (address): メッセージの送信者(現在のコール)

  • msg.sig (bytes4): コールデータの最初の4バイト(すなわち関数識別子)

  • msg.value (uint): メッセージと一緒に送られたweiの数

  • tx.gasprice (uint): トランザクションのガスプライス

  • tx.origin (address): トランザクションの送信者(フルコールチェーン)

注釈

msg.sendermsg.value を含む msg のすべてのメンバーの値は、 外部(external) 関数を呼び出すたびに変わる可能性があります。 これには、ライブラリ関数の呼び出しも含まれます。

注釈

コントラクトが、ブロックに含まれるトランザクションのコンテキストではなく、オフチェーンで評価される場合、 block.*tx.* が特定のブロックやトランザクションの値を参照していると仮定すべきではありません。 これらの値は、コントラクトを実行するEVM実装によって提供され、任意のものとなり得ます。

注釈

自分が何をしているか分かっていない限り、ランダムネスのソースとして block.timestampblockhash に頼らないでください。

タイムスタンプもブロックハッシュも、ある程度はマイナーの影響を受ける可能性があります。 マイニングコミュニティの悪質なアクターは、例えば、選択したハッシュでカジノのペイアウト関数を実行し、対価(例えばEther)を受け取れなかった場合は別のハッシュで再試行できます。

現在のブロックのタイムスタンプは、最後のブロックのタイムスタンプよりも厳密に大きくなければなりませんが、唯一の保証は、正規のチェーンで連続する2つのブロックのタイムスタンプの間のどこかになるということです。

注釈

ブロックハッシュは、スケーラビリティの観点から、すべてのブロックで利用できるわけではありません。 アクセスできるのは最新の256ブロックのハッシュのみで、その他の値はすべてゼロになります。

注釈

関数 blockhash は、以前は block.blockhash と呼ばれていましたが、バージョン0.4.22で非推奨となり、バージョン0.5.0で削除されました。

注釈

gasleft 関数は、以前は msg.gas と呼ばれていましたが、バージョン0.4.21で非推奨となり、バージョン0.5.0で削除されました。

注釈

バージョン0.7.0では、 now (block.timestamp)というエイリアスを削除しました。

ABIエンコーディングおよびデコーディングの関数

  • abi.decode(bytes memory encodedData, (...)) returns (...): ABIは与えられたデータをデコードしますが、型は第2引数として括弧内に与えます。 例: (uint a, uint[2] memory b, bytes memory c) = abi.decode(data, (uint, uint[2], bytes))

  • abi.encode(...) returns (bytes memory): 与えられた引数をABIエンコードします。

  • abi.encodePacked(...) returns (bytes memory): 与えられた引数の packed encoding を実行します。 パックされたエンコーディングは曖昧になる可能性があることに注意してください。

  • abi.encodeWithSelector(bytes4 selector, ...) returns (bytes memory): 与えられた引数を2番目から順にABIエンコードし、与えられた4バイトのセレクタを前に付加します。

  • abi.encodeWithSignature(string memory signature, ...) returns (bytes memory): abi.encodeWithSelector(bytes4(keccak256(bytes(signature))), ...) に相当。

  • abi.encodeCall(function functionPointer, (...)) returns (bytes memory): functionPointer``の呼び出しを、タプルで見つかった引数でABIエンコードします。 完全な型チェックを行い、型が関数のシグネチャと一致することを確認します。 結果は ``abi.encodeWithSelector(functionPointer.selector, (...)) に等しいです。

注釈

これらのエンコーディング関数は、実際に外部関数を呼び出すことなく、外部関数呼び出しのためにデータを細工するために使用できます。 さらに、 keccak256(abi.encodePacked(a, b)) は構造化されたデータのハッシュを計算する方法でもあります(ただし、異なる関数パラメータタイプを使って「ハッシュの衝突」を工作することが可能なので注意が必要です)。

エンコーディングの詳細については、 ABI および タイトにパックするエンコーディング に関するドキュメントを参照してください。

bytesのメンバー

stringのメンバー

エラーハンドリング

エラー処理の詳細や、いつどの関数を使うかについては、 assertとrequire の専用セクションを参照してください。

assert(bool condition)

条件が満たされないとパニックエラーを引き起こし、状態変化が戻ります - 内部エラーに使用されます。

require(bool condition)

条件が満たされないとリバートします - 入力や外部コンポーネントのエラーに使用されます。

require(bool condition, string memory message)

条件が満たされないとリバートします - 入力や外部コンポーネントのエラーに使用します。 また、エラーメッセージも表示されます。

revert()

実行を中止し、状態変化をリバートします。

revert(string memory reason)

実行を中止し、状態変化をリバートするために、説明用の文字列を提供します。

数理的関数と暗号学的関数

addmod(uint x, uint y, uint k) returns (uint)

任意の精度で加算が実行され、 2**256 で切り捨てられない (x + y) % k を計算します。 バージョン0.5.0から k != 0 であることをアサートします。

mulmod(uint x, uint y, uint k) returns (uint)

任意の精度で乗算が実行され、 2**256 で切り捨てられない (x * y) % k を計算します。 バージョン0.5.0から k != 0 であることをアサートします。

keccak256(bytes memory) returns (bytes32)

入力のKeccak-256ハッシュを計算します。

注釈

以前は sha3 という keccak256 のエイリアスがありましたが、バージョン0.5.0で削除されました。

sha256(bytes memory) returns (bytes32)

入力のSHA-256ハッシュを計算します。

ripemd160(bytes memory) returns (bytes20)

入力のRIPEMD-160ハッシュを計算します。

ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address)

楕円曲線の署名から公開鍵に紐づくアドレスを復元するか、エラーで0を返します。 この関数のパラメータは、署名のECDSA値に対応しています。

  • r = 署名の最初の32バイト

  • s = 署名の2番目の32バイト

  • v = 署名の最後の1バイト

ecrecoveraddress を返し、 address payable を返しません。 復旧したアドレスに送金する必要がある場合は、 address payable を参照して変換してください。

詳しくは 使用例 を参照してください。

警告

ecrecover を使用している場合、対応する秘密鍵を知らなくても、有効な署名を別の有効な署名に変えることができることに注意してください。 Homesteadのハードフォークでは、この問題は _transaction_ signaturesで修正されましたが( EIP-2 参照)、ecrecover関数は変更されませんでした。

これは、署名を一意にする必要がある場合や、アイテムを識別するために使用する場合を除き、通常は問題になりません。 OpenZeppelinには、この問題なしに ecrecover のラッパーとして使用できる ECDSAヘルパーライブラリ があります。

注釈

sha256ripemd160ecrecoverプライベートブロックチェーン で実行すると、Out-of-Gasに遭遇することがあります。 これは、これらの関数が「プリコンパイル済みコントラクト」として実装されており、最初のメッセージを受信して初めて実際に存在するからです(ただし、コントラクトコードはハードコードされています)。 存在しないコントラクトへのメッセージはより高価であるため、実行時にOut-of-Gasエラーが発生する可能性があります。 この問題を回避するには、実際のコントラクトで使用する前に、まず各コントラクトにWei(例: 1)を送信することです。 これは、メインネットやテストネットでは問題になりません。

コントラクト関連

this (現在のコントラクト型)

現在のコントラクトで、 アドレス に明示的に変換可能なもの

super

継承階層の1つ上のレベルのコントラクト

selfdestruct(address payable recipient)

現在のコントラクトを破棄し、その資金を所定の アドレス に送り、実行を終了します。 selfdestruct はEVMから引き継いだいくつかの特殊性を持っていることに注意してください。

  • 受信側コントラクトのレシーブ関数が実行されません。

  • コントラクトが実際に破壊されるのはトランザクション終了時であり、 revert はその破壊を「リバートする」かもしれません。

さらに、現在のコントラクトのすべての関数は、現在の関数を含めて直接呼び出すことができます。

警告

バージョン0.8.18以降、SolidityとYulの両方で selfdestruct を使用すると、非推奨の警告が発生します。 というのも、 SELFDESTRUCT オペコードは、 EIP-6049 で述べられているように、いずれ動作が大きく変わることになるからです。

注釈

バージョン0.5.0以前では、 selfdestruct と同じセマンティクスを持つ suicide という関数がありました。

型情報

type(X) という式を使って、 X という型に関する情報を取り出すことができます。 現在のところ、この機能のサポートは限られていますが( X はコントラクト型か整数型のどちらかです)、将来は拡張されるかもしれません。

コントラクト型 C には以下のプロパティがあります。

type(C).name

コントラクトの名称です。

type(C).creationCode

コントラクトの作成バイトコードを含むメモリバイト列。 これはインラインアセンブリで使用でき、特に create2 オペコードを使用してカスタム作成ルーチンを構築できます。 このプロパティは、コントラクト自体または派生コントラクトでアクセスできま せん。 これにより、バイトコードはコールサイトのバイトコードに含まれることになり、そのような循環参照はできません。

type(C).runtimeCode

コントラクトのランタイムバイトコードを含むメモリバイト列。 これは、通常、 C のコンストラクタによってデプロイされるコードです。 C のコンストラクタがインラインアセンブリを使用している場合、これは実際にデプロイされるバイトコードとは異なる可能性があります。 また、ライブラリはデプロイ時にランタイムのバイトコードを変更し、正規の呼び出しを防ぐことにも注意してください。 このプロパティにも、 .creationCode と同様の制限が適用されます。

上記のプロパティに加えて、インターフェース型 I では以下のプロパティが利用可能です。

type(I).interfaceId:

bytes4 値で、与えられたインターフェース IEIP-165 インターフェース識別子を含む。 この識別子は、インターフェース自身の中で定義されたすべての関数セレクタの XOR として定義され、すべての継承された関数は除外されます。

整数型の T には以下のプロパティがあります。

type(T).min

T で表現可能な最小の値です。

type(T).max

T で表現可能な最大の値です。

予約語

これらのキーワードはSolidityで予約されています。 将来は構文の一部になるかもしれません。

after, alias, apply, auto, byte, case, copyof, default, define, final, implements, in, inline, let, macro, match, mutable, null, of, partial, promise, reference, relocatable, sealed, sizeof, static, supports, switch, typedef, typeof, var

式と制御構造

制御構造

カーリーブレース言語で知られている制御構造のほとんどがSolidityで利用可能です。

ifelsewhiledoforbreakcontinuereturn 、また、CやJavaScriptで知られている通常のセマンティクスがあります。

Solidityは、 try/catch 文の形での例外処理もサポートしていますが、 外部関数呼び出し とコントラクト作成の呼び出しにのみ対応しています。 エラーは リバート文 を使って作成できます。

条件式では括弧を省略できませんが、単一の文であれば中括弧を省略できます。

なお、CやJavaScriptのように、非ブール型からブール型への型変換はありませんので、 if (1) { ... } はSolidityとしては有効では ありません

関数呼び出し

内部関数呼び出し

コントラクトの関数はそのコントラクト内の関数を、直接(「内部的に」)、再帰的に呼び出すことができます。

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

// これは警告を通知します。
contract C {
    function g(uint a) public pure returns (uint ret) { return a + f(); }
    function f() internal pure returns (uint ret) { return g(7) + f(); }
}

これらの関数呼び出しは、EVM内部で単純なジャンプに変換されます。 これは、現在のメモリがクリアされないという効果があります。 つまり、内部で呼び出された関数にメモリ参照を渡すことは非常に効率的です。 同じコントラクトインスタンスの関数のみが内部で呼び出されます。

すべての内部関数呼び出しは少なくとも1つのスタックスロットを使用し、利用可能なスロットは1024個しかないため、過度な再帰は避けるべきです。

外部関数呼び出し

関数呼び出しには、 this.g(8);c.g(2); の記法を使うこともできます。 c はコントラクトのインスタンス、 gc に属する関数です。 いずれかの方法で関数 g を呼び出すと、ジャンプを介して直接呼び出されるのではなく、メッセージコールを使用して「外部から」呼び出されることになります。 this の関数呼び出しはコンストラクタでは使用できないことに注意してください。 実際のコントラクトはまだ作成されていないためです。

他のコントラクトの関数は、外部から呼び出す必要があります。 外部呼び出しの際には、すべての関数の引数をメモリにコピーする必要があります。

注釈

あるコントラクトから別のコントラクトへの関数呼び出しは、独自のトランザクションを作成するものではなく、全体のトランザクションの一部としてのメッセージ呼び出しです。

他のコントラクトの関数を呼び出す場合、特別なオプション {value: 10, gas: 10000} で呼び出しとともに送られるweiまたはガスの量を指定できます。 なお、ガスの値を明示的に指定することは推奨されません。 オペコードのガスコストは将来変更される可能性があるからです。 コントラクトに送ったWeiは、そのコントラクトの総残高に追加されます。

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

contract InfoFeed {
    function info() public payable returns (uint ret) { return 42; }
}

contract Consumer {
    InfoFeed feed;
    function setFeed(InfoFeed addr) public { feed = addr; }
    function callFeed() public { feed.info{value: 10, gas: 800}(); }
}

info 関数にモディファイア payable を使用する必要があります。 そうしないと、 value オプションは使用できません。

警告

注意すべきなのは、 feed.info{value: 10, gas: 800} は関数呼び出しで value と送信される gas の量をローカルに設定しているだけで、最後の括弧内は実際の呼び出しを実行しているということです。 そのため、 feed.info{value: 10, gas: 800} は関数をコールして valuegas の設定が失われることはなく、 feed.info{value: 10, gas: 800}() のみが関数呼び出しを実行します。

EVMでは、存在しないコントラクトへの呼び出しは常に成功すると考えられているため、Solidityは extcodesize オペコードを使用して、呼び出されようとしているコントラクトが実際に存在する(コードが含まれている)かどうかをチェックし、存在しない場合は例外を発生させます。 このチェックは、呼び出し後にリターンデータがデコードされる場合にはスキップされ、存在しないコントラクトのケースをABIデコーダがキャッチします。

なお、コントラクトインスタンスではなく、アドレスを操作する 低レベル呼び出し の場合は、このチェックは行われません。

注釈

プリコンパイル済みコントラクト のハイレベルな呼び出しを使用する際には、コードを実行してデータを返すことができるにもかかわらず、コンパイラは上記の論理に従ってそれらを存在しないものとみなすため、注意が必要です。

また、関数呼び出しは、呼び出されたコントラクト自身が例外を投げたり、ガス欠になったりした場合にも例外を発生させます。

警告

他のコントラクトとの相互作用は、特にコントラクトのソースコードが事前にわからない場合、潜在的な危険をもたらします。 現在のコントラクトは呼び出されたコントラクトに制御を渡し、そのコントラクトはあらゆることを行う可能性があります。 呼び出されたコントラクトが既知の親コントラクトを継承している場合でも、継承しているコントラクトは正しいインターフェースを持っていることだけが要求されます。 しかし、コントラクトの実装は完全に恣意的なものになる可能性があり、危険を伴います。 さらに、システムの他のコントラクトを呼び出したり、最初の呼び出しが戻る前に呼び出し元のコントラクトに戻ったりする場合にも備えてください。 つまり、呼び出されたコントラクトは、その関数を介して呼び出したコントラクトの状態変数を変更できるということです。 コントラクトがreentrancyエクスプロイトに対して脆弱でないように、例えば外部関数への呼び出しがコントラクト内の状態変数の変更後に行われるように、関数を記述してください。

注釈

Solidity 0.6.2以前は、valueとgasを指定する方法として、 f.value(x).gas(g)() を使用することが推奨されていました。 これはSolidity 0.6.2で非推奨となり、Solidity 0.7.0からはできなくなりました。

名前付きパラメータを使った関数呼び出し

関数呼び出しの引数は、次の例のように { } で囲まれていれば、任意の順序で名前を与えることができます。 引数リストは、関数宣言のパラメータリストと名前が一致していなければなりませんが、任意の順序にできます。

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

contract C {
    mapping(uint => uint) data;

    function f() public {
        set({value: 2, key: 3});
    }

    function set(uint key, uint value) public {
        data[key] = value;
    }
}

関数定義での名前の省略

関数宣言のパラメータや戻り値の名前は省略できます。 名前が省略された項目はスタック上に存在しますが、名前からアクセスすることはできません。 省略された戻り値名は、 return 文を使用することで、呼び出し元に値を返すことは可能です。

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

contract C {
    // 省略されたパラメータ名
    function func(uint k, uint) public pure returns(uint) {
        return k;
    }
}

new を使ったコントラクト作成

コントラクトは、 new キーワードを使って他のコントラクトを作成できます。 作成されるコントラクトの完全なコードは、作成するコントラクトがコンパイルされるときに知られていなければならないので、再帰的な作成依存は不可能です。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract D {
    uint public x;
    constructor(uint a) payable {
        x = a;
    }
}

contract C {
    D d = new D(4); // C のコンストラクタの一部として実行されます。

    function createD(uint arg) public {
        D newD = new D(arg);
        newD.x();
    }

    function createAndEndowD(uint arg, uint amount) public payable {
        // 作成と一緒にEtherを送る
        D newD = new D{value: amount}(arg);
        newD.x();
    }
}

例に見られるように、 value オプションを使用して D のインスタンスを作成中にEtherを送信することは可能ですが、ガスの量を制限できません。 作成に失敗した場合(スタック不足、残高不足、その他の問題)、例外が発生します。

ソルトされたコントラクト作成 / create2

コントラクトを作成する際、コントラクトのアドレスは、作成するコントラクトのアドレスと、コントラクトを作成するたびに増加するカウンタから計算されます。

オプションの salt (bytes32の値)を指定した場合、コントラクトの作成では、別のメカニズムで新しいコントラクトのアドレスを計算します。

作成したコントラクトのアドレス、与えられたソルト値、作成したコントラクトの(作成)バイトコード、コンストラクタの引数からアドレスを計算します。

特に、カウンター("nonce")は使用されません。 これにより、コントラクトをより柔軟に作成できます。 新しいコントラクトが作成される前に、そのアドレスを導き出すことができます。 さらに、コントラクトを作成している間に他のコントラクトを作成した場合にも、このアドレスに依存できます。

ここでの主なユースケースは、オフチェーンでのやりとりの判断材料となるコントラクトで、紛争が発生した場合にのみ作成する必要があります。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract D {
    uint public x;
    constructor(uint a) {
        x = a;
    }
}

contract C {
    function createDSalted(bytes32 salt, uint arg) public {
        // この複雑な式は、アドレスがどのように事前計算されるかを示しているに過ぎません。
        // これは説明のために存在するだけです。
        // 実際には ``new D{salt: salt}(arg)`` だけが必要です。
        address predictedAddress = address(uint160(uint(keccak256(abi.encodePacked(
            bytes1(0xff),
            address(this),
            salt,
            keccak256(abi.encodePacked(
                type(D).creationCode,
                abi.encode(arg)
            ))
        )))));

        D d = new D{salt: salt}(arg);
        require(address(d) == predictedAddress);
    }
}

警告

ソルトされた作成に関しては、いくつかの特殊性があります。 コントラクトは破壊された後、同じアドレスで再作成できます。 しかし、新しく作成されたコントラクトは、作成時のバイトコードが同じであっても、デプロイ時のバイトコードが異なる可能性があります(そうしないとアドレスが変わってしまうため、これは必須条件です)。 これは、コンストラクタが2つの作成の間に変更された可能性のある外部状態を照会し、それを格納する前にデプロイされたバイトコードに組み込むことができるという事実によるものです。

式の評価順序

式の評価順序は指定されていません(より正式には、式ツリーのあるノードの子が評価される順序は指定されていませんが、もちろんそのノード自身よりも先に評価されます)。 文が順番に実行されることが保証されているだけであり、ブーリアン式の短絡は行われます。

代入

代入のデストラクティングと複数の値のリターン

Solidityは内部的にタプル型を許可しています。 つまり、潜在的に異なる型のオブジェクトのリストで、その数はコンパイル時に一定となります。 これらのタプルは、同時に複数の値を返すために使用できます。 これらの値は、新たに宣言された変数や既存の変数(または一般的なLValue)に代入できます。

タプルはSolidityでは適切な型ではなく、式の構文的なグループ化をするためにのみ使用されます。

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

contract C {
    uint index;

    function f() public pure returns (uint, bool, uint) {
        return (7, true, 2);
    }

    function g() public {
        // 型付きで宣言され、返されたタプルから代入される変数は、すべての要素を指定する必要はありません(ただし、数は一致しなければなりません)。
        (uint x, , uint y) = f();
        // 値をスワップする一般的なトリック -- 値を持たないストレージ型では機能しません。
        (x, y) = (y, x);
        // コンポーネントは省略可能です(変数宣言の場合も同様)。
        (index, , ) = f(); // indexを7に設定
    }
}

変数の宣言と非宣言の代入を混在させることはできません。 つまり、次のようなものは有効ではありません。 (x, uint y) = (1, 2);

注釈

バージョン0.5.0以前では、より小さなサイズのタプルに、左側または右側(どちらかが空の場合)を埋めるように割り当てることができました。 これは現在では禁止されており、両側とも同じ数のコンポーネントを持たなければなりません。

警告

参照型が含まれる場合に複数の変数に同時に代入すると、予期しないコピー動作になることがあるので注意が必要です。

配列と構造体の複雑さ

代入のセマンティクスは、 bytesstring などの配列や構造体などの非値型ではより複雑になりますが。 詳細は データロケーションと代入の動作 を参照してください。

以下の例では、 g(x) の呼び出しは、メモリ内にストレージの値の独立したコピーを作成するため、 x に影響を与えません。 しかし、 h(x) はコピーではなく参照のみが渡されるため、 x の変更に成功しています。

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

contract C {
    uint[20] x;

    function f() public {
        g(x);
        h(x);
    }

    function g(uint[20] memory y) internal pure {
        y[2] = 3;
    }

    function h(uint[20] storage y) internal {
        y[3] = 4;
    }
}

スコーピングと宣言

宣言された変数は、バイト表現がすべてゼロである初期のデフォルト値を持ちます。 変数の「デフォルト値」は、その型が何であれ、典型的な「ゼロ状態」です。 例えば、 bool のデフォルト値は false です。 uint 型や int 型のデフォルト値は 0 です。 静的なサイズの配列や bytes1 から bytes32 では、個々の要素はその型に対応するデフォルト値に初期化されます。 動的なサイズの配列や bytesstring では、デフォルト値は空の配列または文字列です。 enum 型では、初期値はその最初のメンバーです。

Solidityのスコーピングは、C99(および他の多くの言語)で広く採用されているスコーピングルールに従っています。 変数は、その宣言の直後から、その宣言を含む最小の { } ブロックの終わりまで見ることができます。 この規則の例外として、for-loopの初期化部分で宣言された変数は、for-loopの終わりまでしか見えません。

パラメータのような変数(関数パラメータ、モディファイアパラメータ、キャッチパラメータなど)は、次のコードブロックの中に表示されます。 関数パラメータとモディファイアパラメータの場合は関数/モディファイアのボディ、キャッチパラメータの場合はキャッチブロックです。

コードブロックの外で宣言された変数やその他のアイテム(例えば、関数、コントラクト、ユーザー定義型など)は、宣言される前から見ることができます。 つまり、宣言される前の状態の変数を使用したり、関数を再帰的に呼び出したりできます。

その結果、以下の例では、2つの変数は同じ名前ですが、スコープが異なっているため、警告を出さずにコンパイルできます。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;
contract C {
    function minimalScoping() pure public {
        {
            uint same;
            same = 1;
        }

        {
            uint same;
            same = 3;
        }
    }
}

C99のスコープルールの特別な例として、以下では、 x への最初の代入が実際には内側の変数ではなく外側の変数を代入することに注意してください。 いずれにしても、外側の変数がシャドーイングされているという警告が表示されます。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;
// これは警告を報告します
contract C {
    function f() pure public returns (uint) {
        uint x = 1;
        {
            x = 2; // これは外側の変数に代入されます
            uint x;
        }
        return x; // xは2になります
    }
}

警告

バージョン0.5.0以前のSolidityは、JavaScriptと同じスコープルールに従っていました。 つまり、関数内の任意の場所で宣言された変数は、どこで宣言されたかに関わらず、関数全体のスコープになります。 次の例は、バージョン0.5.0以降、コンパイル時にエラーが発生するコードスニペットです。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;
// これはコンパイルできません
contract C {
    function f() pure public returns (uint) {
        x = 2;
        uint x;
        return x;
    }
}

算術演算のチェックをするかしないか

オーバーフローまたはアンダーフローとは、制限のない整数に対して算術演算を実行したときに、結果の値が結果の型の範囲外になってしまうことです。

Solidity 0.8.0以前では、アンダーフローやオーバーフローが発生した場合、算術演算は常にラップするため、追加のチェックを導入するライブラリが広く使用されていました。

Solidity 0.8.0以降、すべての算術演算はデフォルトでオーバーフローとアンダーフローでリバートするため、これらのライブラリを使用する必要はありません。

以前のような動作を得るためには、 unchecked ブロックを使用できます。

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract C {
    function f(uint a, uint b) pure public returns (uint) {
        // この引き算はアンダーフローでラップされます
        unchecked { return a - b; }
    }
    function g(uint a, uint b) pure public returns (uint) {
        // この引き算はアンダーフローでリバートされます
        return a - b;
    }
}

f(2, 3) を呼び出すと 2**256-1 が返され、 g(2, 3) を呼び出すとアサーションに失敗することになります。

unchecked ブロックは、ブロックの中であればどこでも使えますが、ブロックの代わりにはなりません。 また、入れ子にすることもできません。

この設定は、構文的にブロックの内部にある文にのみ影響します。 unchecked ブロック内から呼び出された関数は、このプロパティを継承しません。

注釈

曖昧さを避けるため、 unchecked ブロック内で _; を使用できません。

以下の演算子は、オーバーフローまたはアンダーフロー時にアサーションの失敗を引き起こし、チェックされていないブロック内で使用された場合はエラーなしでラップされます。

++, --, +, 二項 -, 単項 -, *, /, %, **

+=, -=, *=, /=, %=

警告

unchecked ブロックを使ってもゼロ除算やゼロによる剰余のチェックは無効にできません。

注釈

ビット演算子はオーバーフローやアンダーフローのチェックを行いません。 これは、整数の除算や2の累乗の代わりにビット単位のシフト( <<>><<=>>= )を使用する場合に特に顕著です。

注釈

int x = type(int).min; -x; の2つ目の文は、負の範囲が正の範囲よりも1つ多くの値を保持できるため、オーバーフローになります。

明示的な型変換は常に切り捨てられ、整数型からenum型への変換を除いて、アサーションの失敗は発生しません。

エラーハンドリング: Assert, Require, Revert, Exception

Solidityでは、エラーの処理にステートをリバートする例外を使用します。 このような例外は、現在の呼び出し(およびそのすべてのサブコール)で行われた状態への変更をすべてリバートし、呼び出し側にエラーを通知します。

サブコールで例外が発生した場合、 try/catch 文で捕捉されない限り、自動的に「バブルアップ」(例外が再スローされる)します。 このルールの例外は、 send と低レベル関数の calldelegatecallstaticcall です。 これらの関数は、例外が発生した場合、「バブルアップ」するのではなく、 false を最初の戻り値として返します。

警告

低レベル関数の calldelegatecallstaticcall は、EVMの設計の一環として、呼び出されたアカウントが存在しない場合、最初の戻り値として true を返します。 必要に応じて、呼び出す前にアカウントの存在を確認する必要があります。

例外にはエラーデータを含めることができ、 error instances の形で呼び出し側に戻されます。 組み込みエラーの Error(string)Panic(uint256) は、以下に説明するように特別な関数で使用されます。 Error は「通常の」エラー状態に使用され、 Panic はバグのないコードでは存在してはならないエラーに使用されます。

assert を介したパニックと require を介したエラー

コンビニエンス関数である assertrequire は、条件をチェックし、条件を満たさない場合は例外を投げることができます。

assert 関数では、 Panic(uint256) 型のエラーが発生します。 以下のような特定の状況では、コンパイラによって同じエラーが発生します。

Assertは、内部エラーのテストや不変性のチェックにのみ使用します。 適切に機能しているコードは、外部からの不正な入力に対してもパニックを起こさないはずです。 もしそうなってしまったら、コントラクトにバグがあるので修正する必要があります。 言語解析ツールは コントラクトを評価し、パニックを引き起こす条件や関数呼び出しを特定します。

パニック例外は次のような場合に発生します。 エラーデータとともに提供されるエラーコードは、パニックの種類を示します。

  1. 0x00: 一般的なコンパイラの挿入されたパニックに使用されます。

  2. 0x01: falseと評価される引数で assert を呼び出した場合。

  3. 0x11: unchecked { ... } ブロックの外で、演算結果がアンダーフローまたはオーバーフローになった場合。

  4. 0x12; 0で除算や剰余をした場合(例: 5 / 023 % 0 )。

  5. 0x21: 大きすぎる値や負の値を列挙型に変換した場合。

  6. 0x22: 正しくエンコードされていないストレージのバイト配列にアクセスした場合。

  7. 0x31: 空の配列で .pop() を呼び出した場合。

  8. 0x32: 境界外または負のインデックス( x[i]i >= x.lengthi < 0 など)で配列、 bytesN 、または配列スライスにアクセスした場合。

  9. 0x41: メモリの割り当てが多すぎたり、大きすぎる配列を作成した場合。

  10. 0x51: 内部関数型のゼロ初期化変数を呼び出した場合。

require 関数は、データのないエラーを作成するか、 Error(string) 型のエラーを作成します。 require 関数は、実行時まで検出できない有効な条件を保証するために使用する必要があります。 これには、入力に対する条件や、外部コントラクトへの呼び出しからの戻り値が含まれます。

注釈

現在、 require との組み合わせでカスタムエラーを使用できません。 代わりに if (!condition) revert CustomError(); を仕様してください。

Error(string) 例外(またはデータのない例外)は、以下のような場合にコンパイラによって生成されます。

  1. xfalse に評価されるとき require(x) を呼び出す。

  2. revert()revert("description") を使う場合。

  3. コードを含まないコントラクトを対象とした外部関数呼び出しを行った場合。

  4. payable モディファイアのないパブリック関数(コンストラクタ、フォールバック関数を含む)を介してコントラクトがEtherを受け取る場合。

  5. コントラクトがパブリックゲッター関数でEtherを受け取る場合。

以下のケースでは、外部の呼び出しからのエラーデータ(提供されている場合)が送金されます。 これは、 Error または Panic (またはその他の何かが与えられた場合)を引き起こす可能性があることを意味します。

  1. .transfer() が失敗した場合。

  2. メッセージコールで関数を呼び出したが、正しく終了しなかった場合(ガス欠、一致する関数がない、自分自身で例外をスローするなど)。 低レベルの操作 callsenddelegatecallcallcodestaticcall を使用した場合を除きます。 低レベルの操作は、例外を投げることはありませんが、 false を返すことで失敗を示します。

  3. new キーワードを使ってコントラクトを作成しても、コントラクトの作成が 正しく終了しない場合

require にはオプションでメッセージ文字列を指定できますが、 assert には指定できません。

注釈

require に文字列の引数を与えない場合、エラーセレクタを含めずに空のエラーデータでリバートします。

次の例では、 require で入力の状態を確認し、 assert で内部のエラーチェックを行うことができます。

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

contract Sharer {
    function sendHalf(address payable addr) public payable returns (uint balance) {
        require(msg.value % 2 == 0, "Even value required.");
        uint balanceBeforeTransfer = address(this).balance;
        addr.transfer(msg.value / 2);
        // transferに失敗すると例外がスローされ、ここにコールバックすることはできないので、半分のEtherを送金せず保持する方法はないはずです。
        assert(address(this).balance == balanceBeforeTransfer - msg.value / 2);
        return address(this).balance;
    }
}

内部的には、Solidityはリバートする操作(命令 0xfd )を行います。 これにより、EVMは状態に加えられたすべての変更をリバートします。 リバートする理由は、期待した効果が発生しなかったために、実行を継続する安全な方法がない場合です。 トランザクションのアトミック性を維持したいので、最も安全なアクションはすべての変更をリバートし、トランザクション全体(または少なくともコール)を効果なしにすることです。

どちらの場合も、呼び出し側はそのような失敗に対して try / catch を使って反応できますが、呼び出された側の変更は必ずリバートされます。

注釈

パニック例外は、Solidity 0.8.0以前は invalid オペコードを使用していましたが、これは呼び出しに使用可能なすべてのガスを消費していました。 require を使用する例外は、Metropolisリリースの前まではすべてのガスを消費していました。

revert

リバートは、 revert 文と revert 関数を使って直接トリガーできます。

revert 文では、カスタムエラーを括弧なしで直接引数として受け取ります。

revert CustomError(arg1, arg2);

後方互換性を考慮して、括弧を使用して文字列を受け取る revert() 関数もあります。

revert(); revert("description");

エラーデータは呼び出し側に戻されるので、そこでキャッチできます。 revert() を使うとエラーデータなしでリバートしますが、 revert("description") を使うと Error(string) エラーが発生します。

カスタムエラーのインスタンスを使用すると、通常、文字列による説明よりもはるかにコストが小さくなります。 これは、わずか4バイトでエンコードされるエラーの名前を使用して説明できるからです。 より長い記述はNatSpecを介して提供できますが、これには一切のコストがかかりません。

次の例では、エラー文字列とカスタムエラーインスタンスを、 revert と同等の require と一緒に使用しています。

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

contract VendingMachine {
    address owner;
    error Unauthorized();
    function buy(uint amount) public payable {
        if (amount > msg.value / 2 ether)
            revert("Not enough Ether provided.");
        // 別の方法:
        require(
            amount <= msg.value / 2 ether,
            "Not enough Ether provided."
        );
        // 購入を実行する
    }
    function withdraw() public {
        if (msg.sender != owner)
            revert Unauthorized();

        payable(msg.sender).transfer(address(this).balance);
    }
}

if (!condition) revert(...);require(condition, ...); の2つの方法は、 revertrequire への引数が副作用を持たない限り、例えば単なる文字列であれば、等価です。

注釈

require 関数は、他の関数と同様に評価されます。 これは、関数自体が実行される前に、すべての引数が評価されることを意味します。 特に require(condition, f()) では、 condition が真であっても関数 f が実行されます。

提供された文字列は、あたかも関数 Error(string) の呼び出しであるかのように ABIエンコード されます。 上記の例では、 revert("Not enough Ether provided."); はエラーの返り値として次の16進数を返します。

0x08c379a0                                                         // Error(string)の関数セレクタ
0x0000000000000000000000000000000000000000000000000000000000000020 // データオフセット
0x000000000000000000000000000000000000000000000000000000000000001a // 文字列の長さ
0x4e6f7420656e6f7567682045746865722070726f76696465642e000000000000 // 文字列データ

与えたメッセージは、以下のように try / catch を使って呼び出し側が取り出すことができます。

注釈

かつて、 revert() と同じ意味を持つ throw というキーワードがありましたが、バージョン0.4.13で非推奨となり、バージョン0.5.0で削除されました。

try/catch

外部呼び出しの失敗は、以下のようにtry/catch文を使ってキャッチできます。

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

interface DataFeed { function getData(address token) external returns (uint value); }

contract FeedConsumer {
    DataFeed feed;
    uint errorCount;
    function rate(address token) public returns (uint value, bool success) {
        // 10個以上のエラーが発生した場合、この機構を永久に無効にします。
        require(errorCount < 10);
        try feed.getData(token) returns (uint v) {
            return (v, true);
        } catch Error(string memory /*reason*/) {
            // これは、getDataの内部でrevertが呼び出され、文字列reasonが提供された場合に実行されます。
            errorCount++;
            return (0, false);
        } catch Panic(uint /*errorCode*/) {
            // これはパニック、すなわちゼロによる除算やオーバーフローのような重大なエラーが発生した場合に実行されます。
            // エラーコードからエラーの種類を判断できます。
            errorCount++;
            return (0, false);
        } catch (bytes memory /*lowLevelData*/) {
            // revert()が使用された場合に実行されます。
            errorCount++;
            return (0, false);
        }
    }
}

try キーワードの後には、外部関数呼び出しやコントラクトの作成( new ContractName() )を表す式が続く必要があります。 式の内部のエラーは捕捉されず(例えば、内部の関数呼び出しを含む複雑な式の場合)、外部呼び出し自体の内部で起こるリバートのみが捕捉されます。 続く returns 部(オプション)では、外部呼び出しが返す型に一致するリターン変数を宣言します。 エラーがなかった場合、これらの変数が代入され、コントラクトの実行は最初の成功ブロック内で継続されます。 成功ブロックの終わりに達した場合は、 catch ブロックの後に実行が続きます。

Solidityでは、エラーの種類に応じて様々な種類のキャッチブロックをサポートしています。

  • catch Error(string memory reason) { ... }: このキャッチ句は、エラーの原因が revert("reasonString") または require(false, "reasonString") (またはこのような例外を引き起こす内部エラー)であった場合に実行されます。

  • catch Panic(uint errorCode) { ... }: エラーがパニックによって引き起こされた場合、つまり、 assert の失敗、ゼロによる除算、無効な配列アクセス、算術オーバーフローなどによって引き起こされた場合、このキャッチ句が実行されます。

  • catch (bytes memory lowLevelData) { ... }: この句は、エラーのシグネチャが他の句と一致しない場合、エラーメッセージのデコード中にエラーが発生した場合、または例外でエラーデータが提供されなかった場合に実行されます。 宣言された変数は、その場合の低レベルのエラーデータへのアクセスを提供します。

  • catch { ... }: エラーデータに興味がないのであれば、前の句の代わりに catch { ... } を(唯一のcatch句としても)使用すればよいでしょう。

将来は、他の型のエラーデータにも対応する予定です。 文字列 ErrorPanic は、現在、そのまま解析され、識別子としては扱われません。

すべてのエラーケースをキャッチするためには、少なくとも catch { ...} 句または catch (bytes memory lowLevelData) { ... } 句が必要です。

returns 句と catch 句で宣言された変数は、それに続くブロックでのみスコープに入ります。

注釈

try/catch文の中でリターンデータのデコード中にエラーが発生した場合、現在実行中のコントラクトで例外が発生し、そのためcatch句ではキャッチされません。 catch Error(string memory reason) のデコード中にエラーが発生し、低レベルのcatch句がある場合は、このエラーはそこでキャッチされます。

注釈

実行がキャッチブロックに到達した場合、外部呼び出しの状態変化の影響はリバートされています。 実行が成功ブロックに到達した場合、その効果はリバートされていません。 効果がリバートした場合、実行はcatchブロック内で継続されるか、try/catch文の実行自体がリバートします(例えば、上述のようなデコードの失敗や、低レベルのcatch句を提供していないことが原因です)。

注釈

失敗したコールの原因はさまざまです。 エラーメッセージが呼び出されたコントラクトから直接来ていると思わないでください。 エラーはコールチェーンのより深いところで発生し、呼び出されたコントラクトがそれをフォワードしただけかもしれません。 また、意図的なエラー状態ではなく、ガス欠状態が原因である可能性もあります。 コール側は常に1/64以上のガスを保持しているため、コールされたコントラクトがガス欠になっても、コール側にはガスが残っています。

コントラクト

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);
    }
}

インラインアセンブリ

Ethereum Virtual Machineの言語に近い言語で、Solidityの文にインラインアセンブリを挟むことができます。 これにより、より細かな制御が可能となり、特にライブラリを書いて言語を強化する場合に有効です。

Solidityのインラインアセンブリに使用される言語は Yul と呼ばれ、詳細はそのセクションに書かれています。 このセクションでは、インラインアセンブリのコードが周囲のSolidityコードとどのように連携するかについてのみ説明します。

警告

インラインアセンブリは、Ethereum Virtual Machineに低レベルでアクセスする方法です。 これは、Solidityのいくつかの重要な安全機能とチェックをバイパスします。 必要なタスクにのみ使用し、使用に自信がある場合のみ使用してください。

インラインアセンブリブロックは assembly { ... } で示され、中括弧内のコードは Yul 言語のコードです。

インラインのアセンブリコードは、以下のようにSolidityのローカル変数にアクセスできます。

異なるインラインアセンブリブロックは、名前空間を共有しません。 つまり、異なるインラインアセンブリブロックで定義されたYul関数を呼び出したり、Yul変数にアクセスしたりできません。

次の例では、他のコントラクトのコードにアクセスし、それを bytes 変数にロードするライブラリコードを提供しています。 これは「素のSolidity」でも <address>.code を使えば可能です。 しかし、ここでのポイントは、再利用可能なアセンブリライブラリは、コンパイラを変更することなくSolidity言語を強化できるということです。

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

library GetCode {
    function at(address addr) public view returns (bytes memory code) {
        assembly {
            // コードのサイズを取得します。これはアセンブリが必要です。
            let size := extcodesize(addr)
            // 出力バイト配列を確保します。
            // これは、code = new bytes(size) を用いて、アセンブリなしで行うこともできます。
            code := mload(0x40)
            // パディングを含む新しい"memory end"です。
            mstore(0x40, add(code, and(add(add(size, 0x20), 0x1f), not(0x1f))))
            // メモリにコードサイズを格納します。
            mstore(code, size)
            // 実際のコードを取得します。これはアセンブリが必要です。
            extcodecopy(addr, add(code, 0x20), 0, size)
        }
    }
}

インラインアセンブリは、オプティマイザが効率的なコードを生成できない場合などにも有効です。

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

library VectorSum {
    // この関数は、現在、オプティマイザが配列アクセスにおける境界チェックを除去しないため、効率が悪くなっています。
    function sumSolidity(uint[] memory data) public pure returns (uint sum) {
        for (uint i = 0; i < data.length; ++i)
            sum += data[i];
    }

    // 列へのアクセスは境界内だけであることが分かっているので、チェックを回避できます。
    // 最初のスロットに配列の長さが入っているので、0x20を配列に追加する必要があります。
    function sumAsm(uint[] memory data) public pure returns (uint sum) {
        for (uint i = 0; i < data.length; ++i) {
            assembly {
                sum := add(sum, mload(add(add(data, 0x20), mul(i, 0x20))))
            }
        }
    }

    // 上記と同じですが、コード全体をインラインアセンブリで実現します。
    function sumPureAsm(uint[] memory data) public pure returns (uint sum) {
        assembly {
            // 長さ(最初の32バイト)を読み込みます。
            let len := mload(data)

            // 長さのフィールドをスキップします。
            //
            // in-placeでインクリメントできるように一時的な変数を保持します。
            //
            // 注: data をインクリメントすると、このアセンブリブロックの後では data 変数は使用できなくなります。
            let dataElementLocation := add(data, 0x20)

            // 上限に達するまで反復します。
            for
                { let end := add(dataElementLocation, mul(len, 0x20)) }
                lt(dataElementLocation, end)
                { dataElementLocation := add(dataElementLocation, 0x20) }
            {
                sum := add(sum, mload(dataElementLocation))
            }
        }
    }
}

外部変数、外部関数、外部ライブラリへのアクセス

Solidityの変数やその他の識別子は、その名前を使ってアクセスできます。

値型のローカル変数は、インラインアセンブリで直接使用できます。 読み込みと代入の両方が可能です。

メモリを参照するローカル変数は、値そのものではなく、メモリ内の変数のアドレスを評価します。 このような変数は代入することもできますが、代入はポインタを変更するだけでデータを変更するわけではないので、Solidityのメモリ管理を尊重する責任があることに注意してください。 Solidityの慣習 を参照してください。

同様に、静的なサイズのcalldata配列やcalldata構造体を参照するローカル変数は、値そのものではなく、calldata内の変数のアドレスに評価されます。 変数に新しいオフセットを割り当てることもできますが、変数が calldatasize() を超えてポイントしないことを保証するための検証は行われないことに注意してください。

外部関数ポインターの場合、アドレスと関数セレクタは x.addressx.selector を使ってアクセスできます。 セレクタは右揃えの4バイトで構成されています。 どちらの値も代入可能です。 例えば、以下のようになります。

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

contract C {
    // 返り値を格納する変数 @fun に新しいセレクタとアドレスを代入します。
    function combineToFunctionPointer(address newAddress, uint newSelector) public pure returns (function() external fun) {
        assembly {
            fun.selector := newSelector
            fun.address  := newAddress
        }
    }
}

動的なcalldata配列の場合、 x.offsetx.length を使ってcalldataのオフセット(バイト単位)と長さ(要素数)にアクセスできます。 両方の式は代入することもできますが、静的の場合と同様に、結果として得られるデータ領域が calldatasize() の範囲内にあるかどうかの検証は行われません。

ローカルストレージ変数や状態変数の場合、必ずしも1つのストレージスロットを占有しているわけではないので、単一のYul識別子では不十分です。 そのため、変数の「アドレス」は、スロットとそのスロット内のバイトオフセットで構成されます。 変数 x が指すスロットを取得するには x.slot を、バイトオフセットを取得するには x.offset を使います。 x をそのまま使うとエラーになります。

また、ローカルストレージの変数ポインタの .slot 部に代入することもできます。 これら(構造体、配列、マッピング)の場合、 .offset 部は常にゼロです。 ただし、状態変数の .slot または .offset 部分に代入できません。

ローカルSolidityの変数は代入に利用できます。 例:

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

contract C {
    uint b;
    function f(uint x) public view returns (uint r) {
        assembly {
            // ストレージスロットのオフセットは無視します。
            // この特別なケースではゼロであることが分かっています。
            r := mul(x, sload(b.slot))
        }
    }
}

警告

256ビット未満の型( uint64addressbytes16 など)の変数にアクセスする場合、その型のエンコーディングに含まれないビットを仮定することはできません。 特に、それらをゼロと仮定してはいけません。 安全のために、このことが重要な文脈で使用する前に、必ずデータを適切にクリアしてください。 uint32 x = f(); assembly { x := and(x, 0xffffffff) /* now use x */ } 符号付きの型をクリーンにするには、 signextend オペコードを使用できます。 オペコード: assembly { signextend(<num_bytes_of_x_minus_one>, x) }

Solidity 0.6.0以降、インラインアセンブリ変数の名前は、インラインアセンブリブロックのスコープ内で見える宣言(変数宣言、コントラクト宣言、関数宣言を含む)をシャドーイングできません。

Solidity 0.7.0以降、インラインアセンブリブロック内で宣言された変数や関数は . を含むことができませんが、インラインアセンブリブロックの外からSolidityの変数にアクセスするために . を使用することは有効です。

避けるべきこと

インラインアセンブリは、かなりハイレベルな見た目をしていますが、実際には極めてローレベルです。 関数呼び出し、ループ、if、スイッチは簡単な書き換えルールで変換され、その後、アセンブラがしてくれるのは、関数型オペコードの再配置、変数アクセスのためのスタックの高さのカウント、ブロックの終わりに達したときのアセンブリローカル変数のスタックスロットの削除だけです。

Solidityの慣習

型のある変数の値

EVMアセンブリとは対照的に、Solidityには、 uint24 などの256ビットよりも小さい型があります。 効率化のため、ほとんどの算術演算では、型が256ビットよりも短い可能性があるという事実は無視され、高次のビットは必要に応じて、つまり、メモリに書き込まれる直前や比較が実行される前に、クリーニングされます。 つまり、インラインアセンブリ内でこのような変数にアクセスする場合、最初に高次ビットを手動でクリーニングする必要があるかもしれません。

メモリー管理

Solidityは次のような方法でメモリを管理しています。 メモリの位置 0x40 に「フリーメモリポインタ」があります。 メモリを確保したい場合は、このポインタが指す位置から始まるメモリを使用し、更新します。 このメモリが以前に使用されていないという保証はないので、その内容が0バイトであると仮定できません。 割り当てられたメモリを解放するメカニズムは組み込まれていません。 以下は、上記のプロセスに沿ってメモリを割り当てるために使用できるアセンブリスニペットです。

function allocate(length) -> pos {
  pos := mload(0x40)
  mstore(0x40, add(pos, length))
}

メモリの最初の64バイトは、短期的に割り当てられる「スクラッチスペース」として使用できます。 フリーメモリポインタの後の32バイト(つまり 0x60 から始まる)は、永久にゼロであることを意味し、空の動的メモリ配列の初期値として使用されます。 つまり、割り当て可能なメモリは、フリーメモリポインタの初期値である 0x80 から始まります。

Solidityのメモリ配列の要素は、常に32バイトの倍数を占めています(これは bytes1[] でも当てはまりますが、 bytesstring では当てはまりません)。 多次元のメモリ配列は、メモリ配列へのポインタです。 動的配列の長さは、配列の最初のスロットに格納され、その後に配列要素が続きます。

警告

静的サイズのメモリ配列にはlengthフィールドがありませんが、静的サイズの配列と動的サイズの配列の間でより良い変換を可能にするために、後に追加されるかもしれませんので、これに頼らないようにしてください。

メモリ安全性

インラインアセンブリを使用しない場合、コンパイラはメモリが常にwell-definedな状態に保たれることに依存できます。 これは特に Yul IRによる新しいコード生成パイプライン に関連しています。 このコード生成パスは、メモリの使用に関する特定の仮定に依存できる場合、スタックからメモリにローカル変数を移動してStack Too Deepを回避し、追加のメモリの最適化を実行できます。

Solidityのメモリモデルを常に尊重することをお勧めしますが、インラインアセンブリでは互換性のない方法でメモリを使用できます。 したがって、スタック変数をメモリに移動する処理やその他のメモリ最適化は、メモリ操作またはメモリにSolidity変数を割り当てる操作を含むインラインアセンブリブロックの存在下でデフォルトでグローバルに無効になっています。

ただし、次のようにアセンブリブロックに特別な注釈を付けて、Solidityのメモリモデルを尊重していることを示すことができます:

assembly ("memory-safe") {
    ...
}

特に、メモリセーフなアセンブリブロックは、以下のメモリ範囲にのみアクセスできます:

  • 上記の allocate 関数のようなメカニズムを使用して自分で割り当てたメモリ。

  • Solidityによって割り当てられたメモリ(例: 参照するメモリ配列の境界内のメモリ)。

  • 先述したメモリオフセット0と64の間のスクラッチスペース。

  • アセンブリブロックの開始時点のフリーメモリポインタの値より に位置する一時的なメモリ。 すなわち、フリーメモリポインタを更新することなく、フリーメモリポインタに「割り当て」られたメモリ。

さらに、アセンブリブロックがメモリ上にSolidity変数を割り当てる場合、Solidity変数へのアクセスがこれらのメモリ範囲にのみアクセスすることを保証する必要があります。

これは主にオプティマイザに関するものなので、アセンブリブロックがリバートしたり終了したりしても、これらの制限に従う必要があります。 例として、次のアセンブリスニペットはメモリセーフではありません。 なぜなら returndatasize() の値はスクラッチスペースの範囲である64バイトを超える可能性があるからです:

assembly {
  returndatacopy(0, 0, returndatasize())
  revert(0, returndatasize())
}

一方、次のコード メモリセーフです。 なぜなら、フリーメモリポインタが指す位置より先のメモリは、一時的なスクラッチスペースとして安全に使用できるからです。

assembly ("memory-safe") {
  let p := mload(0x40)
  returndatacopy(p, 0, returndatasize())
  revert(p, returndatasize())
}

次の割り当てがない場合は、フリーメモリポインタを更新する必要はありませんが、フリーメモリポインタが与える現在のオフセットから始まるメモリのみを使用できることに注意してください。

メモリ操作で長さ0を使用する場合は、任意のオフセットを使用しても問題ありません(スクラッチスペースに該当する場合のみではありません):

assembly ("memory-safe") {
  revert(0, 0)
}

インラインアセンブリ自体のメモリ操作だけでなく、メモリ上の参照型のSolidity変数への代入もメモリセーフにならないことがあることに注意してください。 例えば以下のようなものはメモリセーフではありません:

bytes memory x;
assembly {
  x := 0x40
}
x[0x20] = 0x42;

メモリにアクセスする操作や、メモリ上のSolidity変数への代入を行わないインラインアセンブリは、自動的にメモリセーフとみなされ、アノテーションを付ける必要はありません。

警告

アセンブリが実際にメモリモデルを満たしているかどうかを確認するのは、あなたの責任です。 アセンブリブロックをメモリセーフとアノテーションしても、メモリの前提条件の1つに違反した場合、テストでは容易に発見できない不正確で未定義の動作につながるでしょう。

Solidityの複数のバージョンで互換性のあるライブラリを開発する場合、特別なコメントを使用してアセンブリブロックをメモリセーフとして注釈できます:

/// @solidity memory-safe-assembly
assembly {
    ...
}

なお、コメントによるアノテーションは、将来のブレーキングリリースで禁止する予定です。 したがって、古いコンパイラのバージョンとの後方互換性にこだわらない場合は、方言文字列を使用することをお勧めします。

チートシート

演算子の優先順位

演算子の優先順位を評価順に並べると以下のようになります。

優先順位

説明

演算子

1

後置インクリメントとデクリメント

++, --

New式

new <typename>

配列添字アクセス

<array>[<index>]

メンバアクセス

<object>.<member>

関数的呼び出し

<func>(<args...>)

括弧

(<statement>)

2

前置インクリメントとデクリメント

++, --

単項マイナス

-

単項演算子

delete

論理否定

!

ビット単位の否定

~

3

累乗

**

4

乗算、除算、剰余

*, /, %

5

加算、減算

+, -

6

ビット単位のシフト演算

<<, >>

7

ビット単位のAND

&

8

ビット単位のXOR

^

9

ビット単位のOR

|

10

不等号演算子

<, >, <=, >=

11

等号演算子

==, !=

12

論理AND

&&

13

論理OR

||

14

三項演算子

<conditional> ? <if-true> : <if-false>

代入演算子

=, |=, ^=, &=, <<=, >>=, +=, -=, *=, /=, %=

15

カンマ演算子

,

ABIのエンコード関数とデコード関数

  • abi.decode(bytes memory encodedData, (...)) returns (...): 与えたデータを ABI デコードします。 型は第2引数として括弧内に与えられます。 例 (uint a, uint[2] memory b, bytes memory c) = abi.decode(data, (uint, uint[2], bytes))

  • abi.encode(...) returns (bytes memory): 与えた引数を ABI エンコードします。

  • abi.encodePacked(...) returns (bytes memory): 与えた引数の packed encoding を実行します。 このエンコーディングは曖昧になる可能性があることに注意してください!

  • abi.encodeWithSelector(bytes4 selector, ...) returns (bytes memory): 与えた引数を2番目から順に ABI エンコードし、与えた4バイトのセレクタを前に付加します。

  • abi.encodeCall(function functionPointer, (...)) returns (bytes memory): タプルに含まれる引数を用いて functionPointer の呼び出しをABIエンコードします。 完全な型チェックを行い、型が関数のシグネチャと一致することを保証します。 結果は abi.encodeWithSelector(functionPointer.selector, (...)) に等くなります。

  • abi.encodeWithSignature(string memory signature, ...) returns (bytes memory): abi.encodeWithSelector(bytes4(keccak256(bytes(signature))), ...) と同等です。

bytesstring のメンバー

address のメンバー

  • <address>.balance (uint256): アドレス の残高(Wei)

  • <address>.code (bytes memory): アドレス のコード(空にもなり得る)

  • <address>.codehash (bytes32): アドレス のコードハッシュ

  • <address payable>.send(uint256 amount) returns (bool): 指定した量のWeiを アドレス に送り、失敗したら false を返します。

  • <address payable>.transfer(uint256 amount): 指定した量のWeiを アドレス に送り、失敗したらリバートします。

ブロックとトランザクションのプロパティ

  • blockhash(uint blockNumber) returns (bytes32): 指定したブロックのハッシュ。最新の256ブロックに対してのみ動作します。

  • block.basefee (uint): カレントブロックのベースフィー(base fee)( EIP-3198EIP-1559 )。

  • block.chainid (uint): カレントブロックのチェーンID。

  • block.coinbase (address payable): カレントブロックのマイナーのアドレス。

  • block.difficulty (uint): カレントブロックの難易度( EVM < Paris )。 EVMの他のバージョンでは、 block.prevrandao の非推奨のエイリアスとして動作し、次のブレーキングリリースで削除される予定です。

  • block.gaslimit (uint): カレントブロックのガスリミット。

  • block.number (uint): カレントブロックの番号。

  • block.prevrandao (uint): ビーコンチェーンが提供する乱数( EVM < Paris )。 EIP-4399 を参照してください。

  • block.timestamp ( uint ): Unixエポックからのカレントブロックのタイムスタンプ(秒)。

  • gasleft() returns (uint256): 残りのガス。

  • msg.data (bytes): 完全なコールデータ。

  • msg.sender (address): メッセージの送信者(現在のコール)。

  • msg.sig (bytes4): コールデータの最初の4バイト(すなわち関数識別子)。

  • msg.value (uint): メッセージと一緒に送られたweiの数。

  • tx.gasprice (uint): トランザクションのガスプライス。

  • tx.origin (address): トランザクションの送信者(フルコールチェーン)。

バリデーションとアサーション

  • assert(bool condition): 条件が false の場合、実行を中止し、ステートの変化をリバートします(内部エラーに使用)。

  • require(bool condition): 条件が false の場合、実行を中止し、ステートの変化をリバートします(不正な入力や外部コンポーネントのエラーに使用)。

  • require(bool condition, string memory message): 条件が false の場合、実行を中止し、ステートの変化をリバートします(不正な入力や外部コンポーネントのエラーに使用)。また、エラーメッセージを表示します。

  • revert(): 実行を中止し、状態の変化をリバートします。

  • revert(string memory message): 実行を中止し、説明文字列を提供してステートの変化をリバートします。

数学的関数と暗号学的関数

  • keccak256(bytes memory) returns (bytes32): 入力のKeccak-256ハッシュを計算します。

  • sha256(bytes memory) returns (bytes32): 入力のSHA-256ハッシュを計算します。

  • ripemd160(bytes memory) returns (bytes20): 入力のRIPEMD-160ハッシュを計算します。

  • ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address): 楕円曲線署名から公開鍵に関連したアドレスをリカバリーします。エラー時は0を返します。

  • addmod(uint x, uint y, uint k) returns (uint): 任意の精度で加算が実行され、 2**256 で切り捨てられない (x + y) % k を計算します。バージョン0.5.0から k != 0 であることをアサートします。

  • mulmod(uint x, uint y, uint k) returns (uint): 任意の精度で乗算が実行され、 2**256 で切り捨てられない (x * y) % k を計算します。バージョン0.5.0から k != 0 であることをアサートします。

コントラクト関連

  • this (現在のコントラクトの型): 現在のコントラクトで、 address または address payable に明示的に変換できるもの。

  • super: 継承階層の1つ上の階層のコントラクト。

  • selfdestruct(address payable recipient): 現在のコントラクトを破棄し、その資金を指定されたアドレスに送ります。

型情報

  • type(C).name (string): コントラクトの名前。

  • type(C).creationCode ( bytes memory ): 与えられたコントラクトの作成バイトコード、 型情報 を参照。

  • type(C).runtimeCode ( bytes memory ): 与えられたコントラクトのランタイムのバイトコード、 型情報 を参照。

  • type(I).interfaceId (bytes4): 指定されたインターフェースのEIP-165インターフェース識別子を含む値、 型情報 を参照。

  • type(T).min (T): 整数型 T で表現可能な最小値、 型情報 を参照。

  • type(T).max (T): 整数型 T で表現可能な最大値、 型情報 を参照。

関数のビジビリティ指定子

function myFunction() <visibility specifier> returns (bool) {
    return true;
}
  • public: 外部にも内部にも見えます(ストレージ/状態変数の ゲッター関数 を作成する)。

  • private: 現在のコントラクトでのみ見えます。

  • external: 外部にしか見えません(関数のみ)。つまり、メッセージコールしかできません( this.func 経由)。

  • internal: 内部でのみ見えます。

モディファイア

  • 関数の pure: 状態の変更やアクセスを禁止します。

  • 関数の view: 状態の変更を不可とします。

  • 関数の payable: コールと同時にEtherを受信できるようにします。

  • 状態変数の constant: 初期化を除き、代入を禁止し、ストレージスロットを占有しません。

  • 状態変数の immutable: コンストラクション時に正確に1つの代入を可能にし、その後は一定です。コードに格納されます。

  • イベントの anonymous: イベントのシグネチャをトピックとして保存しません。

  • イベントパラメータの indexed: パラメータをトピックとして保存します。

  • 関数やモディファイアの virtual: 関数やモディファイアの動作を派生コントラクトで変更できるようにします。

  • override: この関数、モディファイア、パブリックの状態変数が、ベースコントラクト内の関数やモディファイアの動作を変更することを示します。

言語の文法

parser grammar SolidityParser

Solidityは、Ethereumプラットフォーム上でスマートコントラクトを実装するための、静的型付けでコントラクト指向の高水準言語です。

rule source-unit

Solidityでは、プラグマ、importディレクティブ、コントラクト、インターフェース、ライブラリ、構造体、列挙型、定数などの定義が可能です。

pragmapragma-token;import-directiveusing-directivecontract-definitioninterface-definitionlibrary-definitionfunction-definitionconstant-variable-declarationstruct-definitionenum-definitionuser-defined-value-type-definitionerror-definitioneof

rule import-directive

importディレクティブは、異なるファイルから識別子をインポートします。

importpathasidentifiersymbol-aliasesfrompath*asidentifierfrompath;

rule path

インポートするファイルのパス。

non-empty-string-literal

rule symbol-aliases

インポートするシンボルのエイリアスのリスト。

{identifierasidentifier,}

rule contract-definition

コントラクトのトップレベルの定義。

abstractcontractidentifierisinheritance-specifier,{contract-body-element}

rule interface-definition

インターフェースのトップレベルの定義。

interfaceidentifierisinheritance-specifier,{contract-body-element}

rule library-definition

ライブラリのトップレベルの定義。

libraryidentifier{contract-body-element}

rule inheritance-specifier

コントラクトとインターフェースの継承指定子です。 オプションでベースコンストラクタの引数を与えることができます。

identifier-pathcall-argument-list

rule contract-body-element

コントラクト、インターフェース、ライブラリで使用可能な宣言。

インターフェースとライブラリはコンストラクタを含むことができず、インターフェースは状態変数を含むことができず、ライブラリはフォールバック、receive関数、非定数の状態変数を含むことができないことに注意してください。

constructor-definitionfunction-definitionmodifier-definitionfallback-function-definitionreceive-function-definitionstruct-definitionenum-definitionuser-defined-value-type-definitionstate-variable-declarationevent-definitionerror-definitionusing-directive

rule call-argument-list

関数や類似の呼び出し可能なオブジェクトを呼び出す際の引数。 引数はカンマで区切られたリストか、名前付き引数のマップとして与えられます。

(expression,{identifier:expression,})

rule identifier-path

適格な名称。

identifier.

rule modifier-invocation

モディファイアの呼び出し。 モディファイアが引数を取らない場合、引数リストは完全にスキップすることができます(開閉括弧を含む)。

identifier-pathcall-argument-list

rule visibility

関数と関数型のビジビリティ。

internalexternalprivatepublic

rule parameter-list

関数の引数や戻り値などのパラメータのリスト。

type-namedata-locationidentifier,

rule constructor-definition

コンストラクタの定義。 常に実装を提供する必要があります。 internalあるいはpublicの指定は非推奨であることに注意してください。

constructor(parameter-list)modifier-invocationpayableinternalpublicblock

rule state-mutability

関数型に対するミュータビリティの指定。 ミュータビリティが指定されていない場合は、デフォルトのミュータビリティ「non-payable」が指定される。

pureviewpayable

rule override-specifier

関数、モディファイア、状態変数に使用されるオーバーライド指定子。 オーバーライドされる複数のベースコントラクトにあいまいな宣言がある場合、基本コントラクトの完全なリストを指定する必要があります。

override(identifier-path,)

rule function-definition

コントラクト関数、ライブラリ関数、インターフェース関数、フリー関数の定義。 関数が定義されているコンテキストによっては、さらなる制約が適用される場合があります。 例えば、インターフェイスの関数は未実装、つまりボディブロックを含んではなりません。

functionidentifierfallbackreceive(parameter-list)visibilitystate-mutabilitymodifier-invocationvirtualoverride-specifierreturns(parameter-list);block

rule modifier-definition

モディファイアの定義。 モディファイアの本体ブロック内では、アンダースコアは識別子として使用できませんが、モディファイアが適用される関数本体のプレースホルダー文として使用できることに注意してください。

modifieridentifier(parameter-list)virtualoverride-specifier;block

rule fallback-function-definition

fallback関数の定義。

fallback(parameter-list)externalstate-mutabilitymodifier-invocationvirtualoverride-specifierreturns(parameter-list);block

rule receive-function-definition

receive関数の定義。

receive()externalpayablemodifier-invocationvirtualoverride-specifier;block

rule struct-definition

構造体の定義。ソースユニット、コントラクト、ライブラリ、インターフェースのトップレベルで定義できます。

structidentifier{struct-member}

rule struct-member

名前付き構造体メンバの宣言。

type-nameidentifier;

rule enum-definition

enumの定義。ソースユニット、コントラクト、ライブラリ、インターフェースのトップレベルで定義できます。

enumidentifier{identifier,}

rule user-defined-value-type-definition

ユーザー定義の値型を定義。ソースユニット、コントラクト、ライブラリ、インターフェースのトップレベルで定義できます。

typeidentifieriselementary-type-name;

rule state-variable-declaration

状態変数の宣言。

type-namepublicprivateinternalconstantoverride-specifierimmutableidentifier=expression;

rule constant-variable-declaration

定数変数の宣言。

type-nameconstantidentifier=expression;

rule event-parameter

イベントのパラメータ。

type-nameindexedidentifier

rule event-definition

イベントの定義。コントラクト、ライブラリ、インターフェースで定義できます。

eventidentifier(event-parameter,)anonymous;

rule error-parameter

エラーのパラメータ。

type-nameidentifier

rule error-definition

エラーの定義。

erroridentifier(error-parameter,);

rule user-definable-operator

Operators that users are allowed to implement for some types with using for.

&~|^+/%*-==>>=<<=!=

rule using-directive

Using directive to attach library functions and free functions to types. Can occur within contracts and libraries and at the file level.

usingidentifier-path{identifier-pathasuser-definable-operator,}for*type-nameglobal;

rule type-name

型名には、基本型、関数型、マッピング型、ユーザ定義型(コントラクトや構造体など)、配列型があります。

elementary-type-namefunction-type-namemapping-typeidentifier-pathtype-name[expression]

rule elementary-type-name

addressaddresspayableboolstringbytessigned-integer-typeunsigned-integer-typefixed-bytesfixedufixed

rule function-type-name

function(parameter-list)visibilitystate-mutabilityreturns(parameter-list)

rule variable-declaration

単一の変数の宣言。

type-namedata-locationidentifier

rule data-location

memorystoragecalldata

rule expression

複合式。 インデックスアクセス、インデックス範囲アクセス、メンバーアクセス、関数呼び出し(関数呼び出しオプション付き)、型変換、単項式または二項式、比較または代入、三項式、new式(コントラクトの作成または動的メモリ配列の割り当て)、タプル、インライン配列、一次式(識別子、リテラル、型名など)であることが可能です。

expression[expression]expression[expression:expression]expression.identifieraddressexpression{identifier:expression,}expressioncall-argument-listpayablecall-argument-listtype(type-name)++--!~delete-expressionexpression++--expression**expressionexpression*/%expressionexpression+-expressionexpression<<>>>>>expressionexpression&expressionexpression^expressionexpression|expressionexpression<><=>=expressionexpression==!=expressionexpression&&expressionexpression||expressionexpression?expression:expressionexpression=|=^=&=<<=>>=>>>=+=-=*=/=%=expressionnewtype-nametuple-expressioninline-array-expressionidentifierliteralliteral-with-sub-denominationelementary-type-name

rule tuple-expression

(expression,)

rule inline-array-expression

インライン配列式は、含まれる式の共通型の静的な大きさの配列を示します。

[expression,]

rule identifier

通常の非キーワード識別子以外に、'from' や 'error' などのキーワードも識別子として使用することができます。

identifierfromerrorrevertglobal

rule literal

string-literalnumber-literalboolean-literalhex-string-literalunicode-string-literal

rule literal-with-sub-denomination

number-literalsub-denomination

rule boolean-literal

truefalse

rule string-literal

完全な文字列リテラルは、1つまたは複数の連続した引用符で囲まれた文字列で構成されています。

non-empty-string-literalempty-string-literal

rule hex-string-literal

1つまたは複数の連続した16進文字列で構成される完全な16進文字列リテラル。

hex-string

rule unicode-string-literal

1つまたは複数の連続したUnicode文字列で構成される完全なUnicode文字列リテラル。

unicode-string-literal

rule number-literal

数値リテラルは10進数または16進数で、単位は任意です。

decimal-numberhex-number

rule block

波括弧で囲まれた文のブロック。独自のスコープを持ちます。

{statementunchecked-block}

rule unchecked-block

uncheckedblock

rule statement

blockvariable-declaration-statementexpression-statementif-statementfor-statementwhile-statementdo-while-statementcontinue-statementbreak-statementtry-statementreturn-statementemit-statementrevert-statementassembly-statement

rule if-statement

if文。else部はオプション。

if(expression)statementelsestatement

rule for-statement

for文。init、condition、post-loop部はオプション。

for(variable-declaration-statementexpression-statement;expression-statement;expression)statement

rule while-statement

while(expression)statement

rule do-while-statement

dostatementwhile(expression);

rule continue-statement

continue文。for、while、do-whileループ内でのみ使用可能。

continue;

rule break-statement

break文。for、while、do-whileループ内でのみ使用可能。

break;

rule try-statement

try文。含まれる式は、外部関数呼び出しまたはコントラクトの作成である必要があります。

tryexpressionreturns(parameter-list)blockcatch-clause

rule catch-clause

try文のcatch句。

catchidentifier(parameter-list)block

rule return-statement

returnexpression;

rule emit-statement

emit文。含まれる式は、イベントを参照する必要があります。

emitexpressioncall-argument-list;

rule revert-statement

revert文。含まれる式は、エラーを参照する必要があります。

revertexpressioncall-argument-list;

rule assembly-statement

インラインアセンブリブロック。 インラインアセンブリブロックのコンテンツは、別の字句解析器(scanner/lexer)を使用します。 つまり、インラインアセンブリブロックの内部では、キーワードと許可された識別子のセットが異なります。

assembly'"evmasm"'assembly-flags{yul-statement}

rule assembly-flags

Assembly flags. Comma-separated list of double-quoted strings as flags.

(assembly-flag-string,)

rule variable-declaration-tuple

変数宣言で使用される変数名のタプルです。 空のフィールドを含むことができます。

(,variable-declaration,variable-declaration)

rule variable-declaration-statement

変数宣言文。 単一の変数は初期値なしで宣言できますが、変数のタプルは初期値付きでしか宣言できません。

variable-declaration=expressionvariable-declaration-tuple=expression;

rule expression-statement

expression;

rule mapping-type

mapping(mapping-key-typeidentifier=>type-nameidentifier)

rule mapping-key-type

マッピングのキーとして使用できるのは、基本型またはユーザー定義型のみです。

elementary-type-nameidentifier-path

rule yul-statement

インラインアセンブリブロック内のYul文。 continue文とbreak文は、forループ内でのみ有効です。 leave文は、関数のボディの中でのみ有効です。

yul-blockyul-variable-declarationyul-assignmentyul-function-callyul-if-statementyul-for-statementyul-switch-statementleavebreakcontinueyul-function-definition

rule yul-block

{yul-statement}

rule yul-variable-declaration

1つまたは複数のYul変数の宣言で、初期値は任意。 複数の変数が宣言されている場合、初期値として有効なのは関数呼び出しのみです。

letyul-identifier:=yul-expressionletyul-identifier,:=yul-function-call

rule yul-assignment

どんな式でも1つのYul変数に代入できますが、複数代入する場合は右辺に関数呼び出しが必要です。

yul-path:=yul-expressionyul-path,yul-path:=yul-function-call

rule yul-if-statement

ifyul-expressionyul-block

rule yul-for-statement

foryul-blockyul-expressionyul-blockyul-block

rule yul-switch-statement

Yul switch文は、default-caseのみ(非推奨)、または1つ以上のnon-default case(オプションでdefault-caseが続く)から構成できます。

switchyul-expressioncaseyul-literalyul-blockdefaultyul-blockdefaultyul-block

rule yul-function-definition

functionyul-identifier(yul-identifier,)->yul-identifier,yul-block

rule yul-path

インラインアセンブリ内ではドットのない識別子しか宣言できませんが、ドットを含むパスはインラインアセンブリブロックの外の宣言を参照できます。

yul-identifier.yul-identifieryul-evm-builtin

rule yul-function-call

戻り値のある関数の呼び出しは、代入や変数宣言の右辺としてのみ発生します。

yul-identifieryul-evm-builtin(yul-expression,)

rule yul-boolean

truefalse

rule yul-literal

yul-decimal-numberyul-string-literalyul-hex-numberyul-booleanhex-string

rule yul-expression

yul-pathyul-function-callyul-literal

lexer grammar SolidityLexer
rule fixed-bytes

固定長のバイト型。

'bytes1''bytes2''bytes3''bytes4''bytes5''bytes6''bytes7''bytes8''bytes9''bytes10''bytes11''bytes12''bytes13''bytes14''bytes15''bytes16''bytes17''bytes18''bytes19''bytes20''bytes21''bytes22''bytes23''bytes24''bytes25''bytes26''bytes27''bytes28''bytes29''bytes30''bytes31''bytes32'

rule sub-denomination

数値の単位表記。

'wei''gwei''ether''seconds''minutes''hours''days''weeks''years'

rule signed-integer-type

サイズが決められた符号付き整数型。 intはint256のエイリアスです。

'int''int8''int16''int24''int32''int40''int48''int56''int64''int72''int80''int88''int96''int104''int112''int120''int128''int136''int144''int152''int160''int168''int176''int184''int192''int200''int208''int216''int224''int232''int240''int248''int256'

rule unsigned-integer-type

サイズが決められた符号無し整数型。 uintはuint256のエイリアスです。

'uint''uint8''uint16''uint24''uint32''uint40''uint48''uint56''uint64''uint72''uint80''uint88''uint96''uint104''uint112''uint120''uint128''uint136''uint144''uint152''uint160''uint168''uint176''uint184''uint192''uint200''uint208''uint216''uint224''uint232''uint240''uint248''uint256'

rule non-empty-string-literal

印字可能な文字に制限された、クォートで囲まれた空でない文字列リテラル。

'"'double-quoted-printableescape-sequence'"''\''single-quoted-printableescape-sequence'\''

rule empty-string-literal

空の文字列リテラル

'"''"''\'''\''

rule single-quoted-printable

シングルクォート、バックスラッシュ以外の印字可能な文字。

[\u0020-\u0026\u0028-\u005B\u005D-\u007E]

rule double-quoted-printable

ダブルクォート、バックスラッシュ以外の印刷可能な文字。

[\u0020-\u0021\u0023-\u005B\u005D-\u007E]

rule escape-sequence

エスケープシーケンス。 一般的な1文字のエスケープシーケンスとは別に、改行もエスケープできます。 また、4桁の16進数のUnicodeエスケープ uXXXX と2桁の16進数のエスケープシーケンス xXX が使用可能です。

'\\'['"\\nrt\n\r]'u'[0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f]'x'[0-9A-Fa-f][0-9A-Fa-f]

rule unicode-string-literal

任意のUnicode文字を使用できるシングルクォートで囲まれた文字列リテラル。

'unicode''"'~["\r\n\\]escape-sequence'"''\''~['\r\n\\]escape-sequence'\''

rule hex-string

16進文字列は、偶数長の16進数の数字で構成され、アンダースコアを用いてグループ化できる必要があります。

'hex''"'[0-9A-Fa-f][0-9A-Fa-f]'_''"''\''[0-9A-Fa-f][0-9A-Fa-f]'_''\''

rule hex-number

16進数は、プレフィックスと、アンダースコアで区切られた任意の数の16進数の数字で構成されています。

'0''x'[0-9A-Fa-f]'_'

rule decimal-number

10進数リテラルは、アンダースコアで区切られた10進数の数字と、オプションで正または負の指数で構成されています。 桁に小数点が含まれている場合、リテラルは固定小数点型となります。

[0-9]'_'[0-9]'_''.'[0-9]'_'[eE]'-'[0-9]'_'

rule identifier

Solidityの識別子は、アルファベット、ドル記号、アンダースコアで始まる必要があり、最初の記号の後であれば数字を含むことができます。

[a-zA-Z$_][a-zA-Z0-9$_]

rule yul-evm-builtin

EVMのオペコードに対応するYulのビルトイン関数。

'stop''add''sub''mul''div''sdiv''mod''smod''exp''not''lt''gt''slt''sgt''eq''iszero''and''or''xor''byte''shl''shr''sar''addmod''mulmod''signextend''keccak256''pop''mload''mstore''mstore8''sload''sstore''msize''gas''address''balance''selfbalance''caller''callvalue''calldataload''calldatasize''calldatacopy''extcodesize''extcodecopy''returndatasize''returndatacopy''extcodehash''create''create2''call''callcode''delegatecall''staticcall''return''revert''selfdestruct''invalid''log0''log1''log2''log3''log4''chainid''origin''gasprice''blockhash''coinbase''timestamp''number''difficulty''prevrandao''gaslimit''basefee'

rule yul-identifier

Yul識別子は、アルファベット、ドル記号、アンダースコア、数字で構成されますが、数字で始めることはできません。 インラインアセンブリでは、ユーザー定義識別子にドットを使用することはできません。 ドットを含む識別子で構成される式については、yulPathを参照してください。

[a-zA-Z$_][a-zA-Z0-9$_]

rule yul-hex-number

Yulの16進数リテラルは、プレフィックスと1桁以上の16進数で構成されています。

'0''x'[0-9a-fA-F]

rule yul-decimal-number

Yulの10進数リテラルは、0または先頭の0を除いた任意の10進数の数字列です。

'0'[1-9][0-9]

rule yul-string-literal

Yulの文字列リテラルは、1つ以上のダブルクォートまたはシングルクォートで囲まれた文字列からなり、それぞれエスケープされていない改行やエスケープされていないダブルクォートやシングルクォート以外のエスケープシーケンスや印字可能な文字が含まれることがある。

'"'double-quoted-printableescape-sequence'"''\''single-quoted-printableescape-sequence'\''

rule pragma-token

Pragmaトークン。 セミコロン以外のあらゆる種類の記号を含むことができます。 現在、Solidityパーサーはこのサブセットしか許さないことに注意してください。

コンパイラの使い方

コマンドラインコンパイラの使い方

注釈

このセクションは solcjs には適用されません。 たとえコマンドラインモードで使用してもです。

基本的な使い方

Solidityリポジトリのビルドターゲットの1つは、Solidityのコマンドラインコンパイラである solc です。 solc --help を実行すると、すべてのオプションの説明を見ることができます。 コンパイラは、抽象構文木(パースツリー)上の単純なバイナリやアセンブリから、ガス使用量の推定値まで、様々な出力を行うことができます。 単一のファイルをコンパイルしたいだけなら、 solc --bin sourceFile.sol として実行すれば、バイナリを出力します。 solc のより高度な出力を得たい場合は、 solc -o outputDirectory --bin --ast-compact-json --asm sourceFile.sol を使ってすべてを別々のファイルに出力するように指示したほうがよいでしょう。

オプティマイザオプション

コントラクトをデプロイする前に、 solc --optimize --bin sourceFile.sol を使ってコンパイルをする際にオプティマイザを有効にできます。 デフォルトでは、オプティマイザは、コントラクトがそのライフタイム全体で200回呼び出されると仮定して最適化します(より具体的には、各オペコードが約200回実行されると仮定します)。 最初のコントラクトデプロイを安価にし、後の関数実行を高価にしたい場合は、 --optimize-runs=1 を設定してください。 多くのトランザクションが予想され、デプロイコストや出力サイズが高くなっても気にしない場合は、 --optimize-runs を高い数値に設定してください。 このパラメータは以下に影響を与えます(将来変更される可能性があります)。

  • 関数ディスパッチのルーティンにおける二分検索のサイズ

  • 大きな数値や文字列などの定数の保存方法

ベースパスとインポートのリマッピング

コマンドラインコンパイラは、インポートされたファイルをファイルシステムから自動的に読み込みますが、以下のように prefix=path を使って パスのリダイレクト をすることも可能です。

solc github.com/ethereum/dapp-bin/=/usr/local/lib/dapp-bin/ file.sol

これは基本的に、 github.com/ethereum/dapp-bin/ で始まるものを /usr/local/lib/dapp-bin の下で検索するようにコンパイラに指示するものです。

インポートを検索するためにファイルシステムにアクセスする際、 ./または../で始まらないパス--base-path および --include-path オプションで指定されたディレクトリ(ベースパスが指定されていない場合はカレントワーキングディレクトリ)からの相対パスとして扱われます。 また、これらのオプションで追加されたパスの部分は、コントラクトのメタデータには表示されません。

セキュリティ上の理由から、コンパイラは アクセスできるディレクトリに制限 を設けています。 コマンドラインで指定されたソースファイルのディレクトリと、リマッピングのターゲットパスは、ファイルリーダーからのアクセスが自動的に許可されますが、それ以外はデフォルトで拒否されます。 --allow-paths /sample/path,/another/sample/path スイッチで追加のパス(およびそのサブディレクトリ)を許可できます。 --base-path で指定されたパスの中のものは常に許可されます。

上記は、コンパイラがインポートパスをどのように処理するかを簡単に説明したものです。 例を交えた詳細な説明やコーナーケースについては、 パスの解決 のセクションを参照してください。

ライブラリのリンク

コントラクトで ライブラリ を使用している場合、バイトコードに __$53aea86b7d70b31448b230b20ae141a537$__ のような部分文字列が含まれていることに気づくでしょう。 これは、実際のライブラリアドレスのプレースホルダーです。 プレースホルダーは、完全に修飾されたライブラリ名のkeccak256ハッシュの16進数エンコーディングの34文字のプレフィックスです。 また、バイトコードファイルには、プレースホルダーがどのライブラリを表しているかを識別するために、最後に // <placeholder> -> <fq library name> という形式の行が含まれます。 完全に修飾されたライブラリ名は、そのソースファイルのパスとライブラリ名を : で区切ったものであることに注意してください。 solc をリンカーとして使用すると、これらの箇所にライブラリのアドレスを挿入してくれます。

--libraries "file.sol:Math=0x1234567890123456789012345678901234567890 file.sol:Heap=0xabCD567890123456789012345678901234567890" をコマンドに追加して各ライブラリのアドレスを指定するか(セパレータにはカンマまたはスペースを使用)、文字列をファイルに保存して(1行に1ライブラリ)、 --libraries fileName を使って solc を実行するかの2つの方法があります。

注釈

Solidity 0.8.1から、ライブラリとアドレスの間のセパレータとして = を採用することになり、セパレータとしての : は非推奨となりました。 これは将来削除される予定ですが、現在は --libraries "file.sol:Math:0x1234567890123456789012345678901234567890 file.sol:Heap:0xabCD567890123456789012345678901234567890" でも動作します。

solc--standard-json オプション付きで呼び出された場合、標準入力に(以下に説明する)JSONの入力を受け取り、標準出力にJSONの出力を返します。 これは、より複雑な用途、特に自動化された用途に推奨されるインターフェースです。 プロセスは常に「成功」の状態で終了し、エラーがあればJSON出力で報告されます。 オプション --base-path もstandard-jsonモードで処理されます。

solc がオプション --link 付きで呼ばれた場合、すべての入力ファイルは、上記で与えられた __$53aea86b7d70b31448b230b20ae141a537$__ 形式のリンクされていないバイナリ(16進コード)と解釈され、その場でリンクされます(入力が標準入力から読み込まれた場合は、標準出力に書き込まれます)。 この場合、 --libraries 以外のオプションはすべて無視されます( -o も含む)。

警告

生成されたバイトコード上でライブラリを手動でリンクすることは、コントラクトのメタデータが更新されないため、推奨されません。 メタデータにはコンパイル時に指定されたライブラリのリストが含まれており、バイトコードにはメタデータのハッシュが含まれているため、リンクを実行するタイミングによって異なるバイナリが得られることになります。

コントラクトのコンパイル時にライブラリをリンクするようにコンパイラに依頼するには、 solc--libraries オプションを使用するか、コンパイラへの標準JSONインターフェースを使用する場合は libraries キーを使用する必要があります。

注釈

ライブラリのプレースホルダーは、以前はライブラリのハッシュではなく、ライブラリ自体の完全修飾名でした。 この形式は solc --link ではまだサポートされていますが、コンパイラでは出力されなくなりました。 この変更は、完全修飾ライブラリ名の最初の36文字しか使用できないため、ライブラリ間の衝突の可能性を減らすために行われました。

EVMのバージョンをターゲットに設定

コントラクトコードをコンパイルする際に、特定の機能や動作を避けるためにコンパイルするEthereum Virtual Machineのバージョンを指定できます。

警告

EVMのバージョンを間違えてコンパイルすると、間違った動作、おかしな動作、失敗することがあります。 特にプライベートチェーンを実行している場合は、一致するEVMバージョンを使用するようにしてください。

コマンドラインでは、以下のようにEVMのバージョンを選択できます。

solc --evm-version <VERSION> contract.sol

標準JSONインターフェース では、 "settings" フィールドに "evmVersion" キーを使用します。

{
  "sources": {/* ... */},
  "settings": {
    "optimizer": {/* ... */},
    "evmVersion": "<VERSION>"
  }
}

ターゲットオプション

以下は、対象となるEVMのバージョンと、各バージョンで導入されたコンパイラ関連の変更点の一覧です。 各バージョン間の下位互換性は保証されていません。

  • homestead

    • (最も古いバージョン)

  • tangerineWhistle

    • 他のアカウントへのアクセスのためのガスコストが増加しました。 ガスの推定とオプティマイザに関係します。

    • 外部からのコールに対しては、デフォルトですべてのガスが送信されますが、従来は一定量を保持する必要がありました。

  • spuriousDragon

    • exp オペコードのガスコストが増加しました。 ガスの推定とオプティマイザに関係します。

  • byzantium

    • オペコード returndatacopyreturndatasizestaticcall がアセンブリで利用可能になりました。

    • staticcall オペコードは、ライブラリではないview関数やpure関数を呼び出す際に使用され、関数がEVMレベルでステートを変更することを防ぎます。 つまり、無効な型変換を使用している場合でも適用されます。

    • 関数呼び出しから返された動的データにアクセスすることが可能になりました。

    • revert のオペコードが導入されたことで、 revert() がガスを無駄にしないようになりました。

  • constantinople

    • オペコード create2 , extcodehash , shl , shr , sar がアセンブリで使用可能になりました。

    • シフト演算子が、シフトオペコードを使用するため、より少ないガスで済みます。

  • petersburg

    • コンパイラの動作はconstantinopleの場合と同じです。

  • istanbul

    • オペコード chainidselfbalance がアセンブリで使用可能になりました。

  • berlin

    • SLOAD*CALLBALANCEEXT*SELFDESTRUCT のガス代が増加しました。 コンパイラーは、このような操作に対してcoldのガスコストを仮定します。 これは、ガスの推定とオプティマイザに関係します。

  • london

    • ブロックのベースフィー( EIP-3198 および EIP-1559 )は、グローバルな block.basefee またはインラインアセンブリで basefee() を介してアクセスできます。

  • paris

    • prevrandao()``と ``block.prevrandao を導入し、現在では非推奨となっている block.difficulty のセマンティクスを変更し、インラインアセンブリの difficulty() を禁止しました( EIP-4399 を参照してください)。

  • shanghaiデフォルト

    • push0 の導入により、コードサイズが小さくなり、ガスが節約できるようになりました( EIP-3855 を参照)。

コンパイラの入出力JSONの説明

Solidityコンパイラとのインターフェースとして、特に複雑な自動化されたセットアップには、いわゆるJSON-input-outputインターフェースを使用することをお勧めします。 このインターフェースは、コンパイラのすべてのディストリビューションで提供されています。

フィールドは一般的に変更される可能性があり、いくつかの項目はオプションですが(前述のとおり)、後方互換性のある変更のみを行うようにしています。

コンパイラAPIは、JSON形式の入力を期待し、コンパイル結果をJSON形式の出力で出力します。 標準のエラー出力は使用されず、エラーがあった場合でも、常に「成功」の状態で処理が終了します。 エラーは常にJSON出力の一部として報告されます。

以下のサブセクションでは、例を挙げてフォーマットを説明します。 もちろん、コメントは許可されておらず、ここでは説明のためにのみ使用されています。

入力の説明

{
  // 必須: Source code language. Currently supported are "Solidity", "Yul" and "SolidityAST" (experimental).
  "language": "Solidity",
  // 必須
  "sources":
  {
    // The keys here are the "global" names of the source files, imports can use other files via remappings (see below).
    "myFile.sol":
    {
      // オプション: keccak256 hash of the source file
      // It is used to verify the retrieved content if imported via URLs.
      "keccak256": "0x123...",
      // 必須(「content」が使用されている場合を除く): ソースファイルのURL。
      // URL(s) should be imported in this order and the result checked against the keccak256 hash (if available).
      // If the hash doesn't match or none of the URL(s) result in success, an error should be raised.
      // Using the commandline interface only filesystem paths are supported.
      // With the JavaScript interface the URL will be passed to the user-supplied read callback, so any URL supported by the callback can be used.
      "urls":
      [
        "bzzr://56ab...",
        "ipfs://Qma...",
        "/tmp/path/to/file.sol"
        // If files are used, their directories should be added to the command-line via
        // `--allow-paths <path>`.
      ]
      // If language is set to "SolidityAST", an AST needs to be supplied under the "ast" key.
      // Note that importing ASTs is experimental and in particular that:
      // - importing invalid ASTs can produce undefined results and
      // - no proper error reporting is available on invalid ASTs.
      // Furthermore, note that the AST import only consumes the fields of the AST as
      // produced by the compiler in "stopAfter": "parsing" mode and then re-performs
      // analysis, so any analysis-based annotations of the AST are ignored upon import.
      "ast": { ... } // formatted as the json ast requested with the ``ast`` output selection.
    },
    "destructible":
    {
      // オプション: ソースファイルのkeccak256ハッシュ
      "keccak256": "0x234...",
      // Required (unless "urls" is used): literal contents of the source file
      "content": "contract destructible is owned { function shutdown() { if (msg.sender == owner) selfdestruct(owner); } }"
    }
  },
  // オプション
  "settings":
  {
    // オプション: Stop compilation after the given stage. Currently only "parsing" is valid here
    "stopAfter": "parsing",
    // オプション: Sorted list of remappings
    "remappings": [ ":g=/dir" ],
    // オプション: Optimizer settings
    "optimizer": {
      // Disabled by default.
      // NOTE: enabled=false still leaves some optimizations on. See comments below.
      // WARNING: Before version 0.8.6 omitting the 'enabled' key was not equivalent to setting
      // it to false and would actually disable all the optimizations.
      "enabled": true,
      // Optimize for how many times you intend to run the code.
      // Lower values will optimize more for initial deployment cost, higher
      // values will optimize more for high-frequency usage.
      "runs": 200,
      // Switch optimizer components on or off in detail.
      // The "enabled" switch above provides two defaults which can be
      // tweaked here. If "details" is given, "enabled" can be omitted.
      "details": {
        // 何も指定しない場合、peepholeオプティマイザは常にオンになります。
        "peephole": true,
        // 何も指定しない場合、inlinerは常にオンになります。
        "inliner": false,
        // The unused jumpdest remover is always on if no details are given, use details to switch it off.
        "jumpdestRemover": true,
        // Sometimes re-orders literals in commutative operations.
        "orderLiterals": false,
        // Removes duplicate code blocks
        "deduplicate": false,
        // Common subexpression elimination, this is the most complicated step but can also provide the largest gain.
        "cse": false,
        // Optimize representation of literal numbers and strings in code.
        "constantOptimizer": false,
        // 新しいYulオプティマイザ。
        // 主にABIコーダーv2とインラインアセンブリのコードで動作します。
        // It is activated together with the global optimizer setting and can be deactivated here.
        // Before Solidity 0.6.0 it had to be activated through this switch.
        "yul": false,
        // Tuning options for the Yul optimizer.
        "yulDetails": {
          // 変数のスタックスロットの割り当てを改善し、スタックスロットを早めに解放できます。
          // Yulオプティマイザが有効な場合、デフォルトで有効になります。
          "stackAllocation": true,
          // Select optimization steps to be applied.
          // It is also possible to modify both the optimization sequence and the clean-up sequence.
          // Instructions for each sequence are separated with the ":" delimiter and the values are provided in the form of optimization-sequence:clean-up-sequence.
          // For more information see "The Optimizer > Selecting Optimizations".
          // This field is optional, and if not provided, the default sequences for both optimization and clean-up are used.
          // If only one of the sequences is provided the other will not be run.
          // If only the delimiter ":" is provided then neither the optimization nor the clean-up sequence will be run.
          // If set to an empty value, only the default clean-up sequence is used and no optimization steps are applied.
          "optimizerSteps": "dhfoDgvulfnTUtnIf..."
        }
      }
    },
    // Version of the EVM to compile for.
    // Affects type checking and code generation. Can be homestead,
    // tangerineWhistle, spuriousDragon, byzantium, constantinople, petersburg, istanbul, berlin, london or paris
    "evmVersion": "byzantium",
    // オプション: Change compilation pipeline to go through the Yul intermediate representation.
    // This is false by default.
    "viaIR": true,
    // オプション: Debugging settings
    "debug": {
      // How to treat revert (and require) reason strings. Settings are
      // "default", "strip", "debug" and "verboseDebug".
      // "default" does not inject compiler-generated revert strings and keeps user-supplied ones.
      // "strip" removes all revert strings (if possible, i.e. if literals are used) keeping side-effects
      // "debug" injects strings for compiler-generated internal reverts, implemented for ABI encoders V1 and V2 for now.
      // "verboseDebug" even appends further information to user-supplied revert strings (not yet implemented)
      "revertStrings": "default",
      // オプション: How much extra debug information to include in comments in the produced EVM
      // assembly and Yul code. Available components are:
      // - `location`: Annotations of the form `@src <index>:<start>:<end>` indicating the
      //    location of the corresponding element in the original Solidity file, where:
      //     - `<index>` is the file index matching the `@use-src` annotation,
      //     - `<start>` is the index of the first byte at that location,
      //     - `<end>` is the index of the first byte after that location.
      // - `snippet`: A single-line code snippet from the location indicated by `@src`.
      //     The snippet is quoted and follows the corresponding `@src` annotation.
      // - `*`: Wildcard value that can be used to request everything.
      "debugInfo": ["location", "snippet"]
    },
    // メタデータの設定(オプション)
    "metadata": {
      // The CBOR metadata is appended at the end of the bytecode by default.
      // Setting this to false omits the metadata from the runtime and deploy time code.
      "appendCBOR": true,
      // Use only literal content and not URLs (false by default)
      "useLiteralContent": true,
      // Use the given hash method for the metadata hash that is appended to the bytecode.
      // The metadata hash can be removed from the bytecode via option "none".
      // The other options are "ipfs" and "bzzr1".
      // If the option is omitted, "ipfs" is used by default.
      "bytecodeHash": "ipfs"
    },
    // Addresses of the libraries. If not all libraries are given here,
    // it can result in unlinked objects whose output data is different.
    "libraries": {
      // The top level key is the the name of the source file where the library is used.
      // If remappings are used, this source file should match the global path
      // after remappings were applied.
      // If this key is an empty string, that refers to a global level.
      "myFile.sol": {
        "MyLib": "0x123123..."
      }
    },
    // The following can be used to select desired outputs based
    // on file and contract names.
    // If this field is omitted, then the compiler loads and does type checking,
    // but will not generate any outputs apart from errors.
    // The first level key is the file name and the second level key is the contract name.
    // An empty contract name is used for outputs that are not tied to a contract
    // but to the whole source file like the AST.
    // A star as contract name refers to all contracts in the file.
    // Similarly, a star as a file name matches all files.
    // To select all outputs the compiler can possibly generate, use
    // "outputSelection: { "*": { "*": [ "*" ], "": [ "*" ] } }"
    // but note that this might slow down the compilation process needlessly.
    //
    // The available output types are as follows:
    //
    // File level (needs empty string as contract name):
    //   ast - AST of all source files
    //
    // Contract level (needs the contract name or "*"):
    //   abi - ABI
    //   devdoc - Developer documentation (natspec)
    //   userdoc - User documentation (natspec)
    //   metadata - Metadata
    //   ir - Yul intermediate representation of the code before optimization
    //   irAst - AST of Yul intermediate representation of the code before optimization
    //   irOptimized - Intermediate representation after optimization
    //   irOptimizedAst - AST of intermediate representation after optimization
    //   storageLayout - Slots, offsets and types of the contract's state variables.
    //   evm.assembly - New assembly format
    //   evm.legacyAssembly - Old-style assembly format in JSON
    //   evm.bytecode.functionDebugData - Debugging information at function level
    //   evm.bytecode.object - Bytecode object
    //   evm.bytecode.opcodes - Opcodes list
    //   evm.bytecode.sourceMap - Source mapping (useful for debugging)
    //   evm.bytecode.linkReferences - Link references (if unlinked object)
    //   evm.bytecode.generatedSources - Sources generated by the compiler
    //   evm.deployedBytecode* - Deployed bytecode (has all the options that evm.bytecode has)
    //   evm.deployedBytecode.immutableReferences - Map from AST ids to bytecode ranges that reference immutables
    //   evm.methodIdentifiers - The list of function hashes
    //   evm.gasEstimates - Function gas estimates
    //
    // Note that using a using `evm`, `evm.bytecode`, etc. will select every
    // target part of that output. Additionally, `*` can be used as a wildcard to request everything.
    //
    "outputSelection": {
      "*": {
        "*": [
          "metadata", "evm.bytecode" // Enable the metadata and bytecode outputs of every single contract.
          , "evm.bytecode.sourceMap" // Enable the source map output of every single contract.
        ],
        "": [
          "ast" // Enable the AST output of every single file.
        ]
      },
      // Enable the abi and opcodes output of MyContract defined in file def.
      "def": {
        "MyContract": [ "abi", "evm.bytecode.opcodes" ]
      }
    },
    // The modelChecker object is experimental and subject to changes.
    "modelChecker":
    {
      // Chose which contracts should be analyzed as the deployed one.
      "contracts":
      {
        "source1.sol": ["contract1"],
        "source2.sol": ["contract2", "contract3"]
      },
      // Choose how division and modulo operations should be encoded.
      // When using `false` they are replaced by multiplication with slack
      // variables. This is the default.
      // Using `true` here is recommended if you are using the CHC engine
      // and not using Spacer as the Horn solver (using Eldarica, for example).
      // See the Formal Verification section for a more detailed explanation of this option.
      "divModNoSlacks": false,
      // Choose which model checker engine to use: all (default), bmc, chc, none.
      "engine": "chc",
      // Choose whether external calls should be considered trusted in case the
      // code of the called function is available at compile-time.
      // For details see the SMTChecker section.
      "extCalls": "trusted",
      // Choose which types of invariants should be reported to the user: contract, reentrancy.
      "invariants": ["contract", "reentrancy"],
      // Choose whether to output all proved targets. The default is `false`.
      "showProved": true,
      // Choose whether to output all unproved targets. The default is `false`.
      "showUnproved": true,
      // Choose whether to output all unsupported language features. The default is `false`.
      "showUnsupported": true,
      // Choose which solvers should be used, if available.
      // See the Formal Verification section for the solvers description.
      "solvers": ["cvc4", "smtlib2", "z3"],
      // Choose which targets should be checked: constantCondition,
      // underflow, overflow, divByZero, balance, assert, popEmptyArray, outOfBounds.
      // If the option is not given all targets are checked by default,
      // except underflow/overflow for Solidity >=0.8.7.
      // See the Formal Verification section for the targets description.
      "targets": ["underflow", "overflow", "assert"],
      // Timeout for each SMT query in milliseconds.
      // If this option is not given, the SMTChecker will use a deterministic
      // resource limit by default.
      // A given timeout of 0 means no resource/time restrictions for any query.
      "timeout": 20000
    }
  }
}

出力の説明

{
  // オプション: not present if no errors/warnings/infos were encountered
  "errors": [
    {
      // オプション: Location within the source file.
      "sourceLocation": {
        "file": "sourceFile.sol",
        "start": 0,
        "end": 100
      },
      // オプション: Further locations (e.g. places of conflicting declarations)
      "secondarySourceLocations": [
        {
          "file": "sourceFile.sol",
          "start": 64,
          "end": 92,
          "message": "Other declaration is here:"
        }
      ],
      // 必須: Error type, such as "TypeError", "InternalCompilerError", "Exception", etc.
      // See below for complete list of types.
      "type": "TypeError",
      // 必須: Component where the error originated, such as "general" etc.
      "component": "general",
      // Mandatory ("error", "warning" or "info", but please note that this may be extended in the future)
      "severity": "error",
      // オプション: unique code for the cause of the error
      "errorCode": "3141",
      // Mandatory
      "message": "Invalid keyword",
      // オプション: the message formatted with source location
      "formattedMessage": "sourceFile.sol:100: Invalid keyword"
    }
  ],
  // This contains the file-level outputs.
  // It can be limited/filtered by the outputSelection settings.
  "sources": {
    "sourceFile.sol": {
      // Identifier of the source (used in source maps)
      "id": 1,
      // The AST object
      "ast": {}
    }
  },
  // This contains the contract-level outputs.
  // It can be limited/filtered by the outputSelection settings.
  "contracts": {
    "sourceFile.sol": {
      // If the language used has no contract names, this field should equal to an empty string.
      "ContractName": {
        // The Ethereum Contract ABI. If empty, it is represented as an empty array.
        // See https://docs.soliditylang.org/en/develop/abi-spec.html
        "abi": [],
        // See the Metadata Output documentation (serialised JSON string)
        "metadata": "{/* ... */}",
        // User documentation (natspec)
        "userdoc": {},
        // Developer documentation (natspec)
        "devdoc": {},
        // Intermediate representation before optimization (string)
        "ir": "",
        // AST of intermediate representation before optimization
        "irAst":  {/* ... */},
        // Intermediate representation after optimization (string)
        "irOptimized": "",
        // AST of intermediate representation after optimization
        "irOptimizedAst": {/* ... */},
        // See the Storage Layout documentation.
        "storageLayout": {"storage": [/* ... */], "types": {/* ... */} },
        // EVM-related outputs
        "evm": {
          // Assembly (string)
          "assembly": "",
          // Old-style assembly (object)
          "legacyAssembly": {},
          // Bytecode and related details.
          "bytecode": {
            // Debugging data at the level of functions.
            "functionDebugData": {
              // Now follows a set of functions including compiler-internal and
              // user-defined function. The set does not have to be complete.
              "@mint_13": { // Internal name of the function
                "entryPoint": 128, // Byte offset into the bytecode where the function starts (optional)
                "id": 13, // AST ID of the function definition or null for compiler-internal functions (optional)
                "parameterSlots": 2, // Number of EVM stack slots for the function parameters (optional)
                "returnSlots": 1 // Number of EVM stack slots for the return values (optional)
              }
            },
            // The bytecode as a hex string.
            "object": "00fe",
            // Opcodes list (string)
            "opcodes": "",
            // The source mapping as a string. See the source mapping definition.
            "sourceMap": "",
            // Array of sources generated by the compiler. Currently only
            // contains a single Yul file.
            "generatedSources": [{
              // Yul AST
              "ast": {/* ... */},
              // Source file in its text form (may contain comments)
              "contents":"{ function abi_decode(start, end) -> data { data := calldataload(start) } }",
              // Source file ID, used for source references, same "namespace" as the Solidity source files
              "id": 2,
              "language": "Yul",
              "name": "#utility.yul"
            }],
            // If given, this is an unlinked object.
            "linkReferences": {
              "libraryFile.sol": {
                // Byte offsets into the bytecode.
                // Linking replaces the 20 bytes located there.
                "Library1": [
                  { "start": 0, "length": 20 },
                  { "start": 200, "length": 20 }
                ]
              }
            }
          },
          "deployedBytecode": {
            /* ..., */ // The same layout as above.
            "immutableReferences": {
              // There are two references to the immutable with AST ID 3, both 32 bytes long. One is
              // at bytecode offset 42, the other at bytecode offset 80.
              "3": [{ "start": 42, "length": 32 }, { "start": 80, "length": 32 }]
            }
          },
          // The list of function hashes
          "methodIdentifiers": {
            "delegate(address)": "5c19a95c"
          },
          // Function gas estimates
          "gasEstimates": {
            "creation": {
              "codeDepositCost": "420000",
              "executionCost": "infinite",
              "totalCost": "infinite"
            },
            "external": {
              "delegate(address)": "25000"
            },
            "internal": {
              "heavyLifting()": "infinite"
            }
          }
        }
      }
    }
  }
}
エラータイプ
  1. JSONError: JSON入力が要求されたフォーマットに適合していません。 例: 入力がJSONオブジェクトでない、言語がサポートされていない、など。

  1. IOError: 解決できないURLや提供されたソースのハッシュの不一致など、IOおよびインポート処理のエラーです。

  1. ParserError: ソースコードが言語ルールに準拠していません。

  1. DocstringParsingError: コメントブロック内のNatSpecタグが解析できません。

  1. SyntaxError: for ループの外で continue が使われているなど、構文上のエラーです。

  1. DeclarationError: 無効な、解決不可能な、または衝突した識別子名があります。 例: Identifier not found

  1. TypeError: 無効な型変換、無効な代入など、型システム内のエラーです。

  1. UnimplementedFeatureError: この機能はコンパイラではサポートされていませんが、将来のバージョンではサポートされる予定です。

  1. InternalCompilerError: コンパイラの内部バグが発生しました。

  1. Exception: コンパイル時に不明な障害が発生しました - これはイシューとして報告すべきです。

  1. CompilerError: コンパイラースタックの無効な使用 - これはイシューとして報告すべきです。

  1. FatalError: 致命的なエラーが正しく処理されていない - これはイシューとして報告すべきです。

  1. YulException: Yulコード生成時のエラー - これはイシューとして報告すべきです。

  1. Warning: 警告。コンパイルは停止しなかったが、できれば対処すべきです。

  1. Info: コンパイラが、ユーザーが役に立つかもしれないと考えている情報です。 しかし、危険ではないので、必ず対処する必要はありません。

コンパイラの出力の解析

コンパイラが生成したアセンブリコードを見ると便利なことが多くあります。 生成されたバイナリ、すなわち solc --bin contract.sol の出力は、一般に読みにくいです。 フラグ --asm を使用して、アセンブリ出力を分析することをお勧めします。 大規模なコントラクトであっても、変更前と変更後のアセンブリの視覚的なdiffを見ることは、しばしば非常に有益です。

次のようなコントラクトを考えます(名前は contract.sol とします)。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;
contract C {
    function one() public pure returns (uint) {
        return 1;
    }
}

solc --asm contract.sol の出力は以下のようになります。

======= contract.sol:C =======
EVM assembly:
    /* "contract.sol":0:86  contract C {... */
  mstore(0x40, 0x80)
  callvalue
  dup1
  iszero
  tag_1
  jumpi
  0x00
  dup1
  revert
tag_1:
  pop
  dataSize(sub_0)
  dup1
  dataOffset(sub_0)
  0x00
  codecopy
  0x00
  return
stop

sub_0: assembly {
        /* "contract.sol":0:86  contract C {... */
      mstore(0x40, 0x80)
      callvalue
      dup1
      iszero
      tag_1
      jumpi
      0x00
      dup1
      revert
    tag_1:
      pop
      jumpi(tag_2, lt(calldatasize, 0x04))
      shr(0xe0, calldataload(0x00))
      dup1
      0x901717d1
      eq
      tag_3
      jumpi
    tag_2:
      0x00
      dup1
      revert
        /* "contract.sol":17:84  function one() public pure returns (uint) {... */
    tag_3:
      tag_4
      tag_5
      jump  // in
    tag_4:
      mload(0x40)
      tag_6
      swap2
      swap1
      tag_7
      jump  // in
    tag_6:
      mload(0x40)
      dup1
      swap2
      sub
      swap1
      return
    tag_5:
        /* "contract.sol":53:57  uint */
      0x00
        /* "contract.sol":76:77  1 */
      0x01
        /* "contract.sol":69:77  return 1 */
      swap1
      pop
        /* "contract.sol":17:84  function one() public pure returns (uint) {... */
      swap1
      jump  // out
        /* "#utility.yul":7:125   */
    tag_10:
        /* "#utility.yul":94:118   */
      tag_12
        /* "#utility.yul":112:117   */
      dup2
        /* "#utility.yul":94:118   */
      tag_13
      jump  // in
    tag_12:
        /* "#utility.yul":89:92   */
      dup3
        /* "#utility.yul":82:119   */
      mstore
        /* "#utility.yul":72:125   */
      pop
      pop
      jump  // out
        /* "#utility.yul":131:353   */
    tag_7:
      0x00
        /* "#utility.yul":262:264   */
      0x20
        /* "#utility.yul":251:260   */
      dup3
        /* "#utility.yul":247:265   */
      add
        /* "#utility.yul":239:265   */
      swap1
      pop
        /* "#utility.yul":275:346   */
      tag_15
        /* "#utility.yul":343:344   */
      0x00
        /* "#utility.yul":332:341   */
      dup4
        /* "#utility.yul":328:345   */
      add
        /* "#utility.yul":319:325   */
      dup5
        /* "#utility.yul":275:346   */
      tag_10
      jump  // in
    tag_15:
        /* "#utility.yul":229:353   */
      swap3
      swap2
      pop
      pop
      jump  // out
        /* "#utility.yul":359:436   */
    tag_13:
      0x00
        /* "#utility.yul":425:430   */
      dup2
        /* "#utility.yul":414:430   */
      swap1
      pop
        /* "#utility.yul":404:436   */
      swap2
      swap1
      pop
      jump  // out

    auxdata: 0xa2646970667358221220a5874f19737ddd4c5d77ace1619e5160c67b3d4bedac75fce908fed32d98899864736f6c637827302e382e342d646576656c6f702e323032312e332e33302b636f6d6d69742e65613065363933380058
}

また、上記の出力は、コントラクトをコンパイルした後、 Remix のオプション「Compilation Details」からも得ることができます。

asm の出力は、作成/コンストラクタのコードで始まることに注意してください。 配置コードは、サブオブジェクトの一部として提供されます(上記の例では、サブオブジェクト sub_0 の一部です)。 auxdata フィールドはコントラクトの メタデータ に対応しています。 アセンブリ出力のコメントは、ソースの位置を示しています。 #utility.yul は、フラグ --combined-json generated-sources,generated-sources-runtime を使用して取得できるユーティリティー関数の内部生成ファイルであることに注意してください。

同様に、最適化されたアセンブリは、次のコマンドで得ることができます: solc --optimize --asm contract.sol 。 しばしば、Solidityの2つの異なるソースが同じ最適化されたコードになるかどうかを確認することは興味深いことです。 例えば、 (a * b) / c , a * b / c という式が同じバイトコードを生成するかどうかを確認できます。 これは、ソースの位置を参照するコメントを削除した後、対応するアセンブリ出力の diff を取ることで簡単に行うことができます。

注釈

--asm 出力は機械で読めるようには設計されていません。 そのため、solcのマイナーバージョン間では、出力に変更がある可能性があります。

Solidity IRベースのCodegenの変更点

Solidityは、2つの異なる方法でEVMバイトコードを生成できます。 Solidityから直接EVMのオペコードを生成する方法「old codegen」と、Yulの中間表現「IR」を介して生成する方法(「new codegen」または「IR-based codegen」)です。

IRベースのコードジェネレーターを導入したのは、コード生成の透明性や監査性を高めるだけでなく、関数を跨いだより強力な最適化パスを可能にすることを目的としています。

コマンドラインで --via-ir を使って有効にしたり、スタンダードJSONで {"viaIR": true} オプションを使って有効にできますので、ぜひ皆さんに試していただきたいと思います。

いくつかの理由により、従来のコードジェネレーターとIRベースのコードジェネレーターの間にはわずかなセマンティックな違いがありますが、そのほとんどは、いずれにしても人々がこの動作に頼ることはないだろうと思われる領域です。 このセクションでは、旧来のコードジェネレーターとIRベースのコードジェネレーターの主な違いを紹介します。

セマンティックのみの変更

このセクションでは、セマンティックのみの変更点をリストアップしています。 そのため、既存のコードの中に新しい、あるいは異なる動作が隠されている可能性があります。

  • 継承した場合の状態変数の初期化の順序が変更されました。

    以前の順序:

    • すべての状態変数は、最初にゼロ初期化されます。

    • ベースコンストラクタの引数を、最も派生したものから最もベースとなるコントラクトまで評価します。

    • 最も基本的なものから最も派生的なものまで、継承階層全体のすべての状態変数を初期化します。

    • 最も基本的なものから最も派生したものまで、線形化された階層内のすべてのコントラクトについて、コンストラクタが存在する場合はそれを実行します。

    新しい順序:

    • すべての状態変数が最初にゼロ初期化されます。

    • ベースコンストラクタの引数を、最も派生したコントラクトから最もベースとなるコントラクトまで評価します。

    • 線形化された階層で、最も基本から最も派生した順に、すべてのコントラクトについて:

      1. 状態変数を初期化します。

      2. コンストラクタを実行します(存在する場合)。

    このため、コントラクトで状態変数の初期値が異なる場合があります。 変数は、別のコントラクトのコンストラクタの結果に依存しています:

    // SPDX-License-Identifier: GPL-3.0
    pragma solidity >=0.7.1;
    
    contract A {
        uint x;
        constructor() {
            x = 42;
        }
        function f() public view returns(uint256) {
            return x;
        }
    }
    contract B is A {
        uint public y = f();
    }
    

    これは、最初に状態変数を初期化することに起因しています: まず、 x は0に設定され、 y を初期化する際に、 f() は0を返し、 y も0になります。 新しいルールでは、 y は 42 に設定されます。 まず x を 0 に初期化し、次に A のコンストラクタをコールして x を 42 に設定します。 最後に y を初期化する際に f() が 42 を返すので y は 42 になります。

  • ストレージ構造体が削除されると、その構造体のメンバーを含むすべてのストレージスロットが完全にゼロになります。 以前は、パディングスペースはそのまま残されていました。 そのため、構造体内のパディングスペースがデータの保存に使用されている場合(コントラクトのアップグレードなど)、 delete では追加されたメンバーもクリアされてしまうことに注意する必要があります(以前はクリアされませんでしたが)。

    // SPDX-License-Identifier: GPL-3.0
    pragma solidity >=0.7.1;
    
    contract C {
        struct S {
            uint64 y;
            uint64 z;
        }
        S s;
        function f() public {
            // ...
            delete s;
            // s occupies only first 16 bytes of the 32 bytes slot
            // delete will write zero to the full slot
        }
    }
    

    暗黙の削除は、例えば構造体の配列が短くなったときにも同じ動作をします。

  • 関数モディファイアは、関数のパラメータとリターン変数に関して、若干異なる方法で実装されています。 これは特に、プレースホルダー _; がモディファイアの中で複数回評価される場合に影響を及ぼします。 古いコードジェネレーターでは、各関数パラメータとリターン変数はスタック上に固定されたスロットを持っています。 もし _; が複数回使われたり、ループ内で使われたりして関数が複数回実行されると、関数パラメータの値やリターン変数の値の変化は、関数の次の実行で見えるようになります。 新しいコードジェネレータでは、実際の関数を使用してモディファイアを実装し、関数パラメータを渡します。 つまり、関数本体を複数回評価しても、パラメータは同じ値になり、リターン変数は実行ごとにデフォルト(ゼロ)値にリセットされるという効果があります。

    // SPDX-License-Identifier: GPL-3.0
    pragma solidity >=0.7.0;
    contract C {
        function f(uint a) public pure mod() returns (uint r) {
            r = a++;
        }
        modifier mod() { _; _; }
    }
    

    古いコードジェネレータで f(0) を実行すると 1 が返され、新しいコードジェネレータを使うと 0 が返されます。

    // SPDX-License-Identifier: GPL-3.0
    pragma solidity >=0.7.1 <0.9.0;
    
    contract C {
        bool active = true;
        modifier mod()
        {
            _;
            active = false;
            _;
        }
        function foo() external mod() returns (uint ret)
        {
            if (active)
                ret = 1; // Same as ``return 1``
        }
    }
    

    関数 C.foo() は以下の値を返します:

    • 古いコードジェネレータ: 戻り値の変数である 1 は、最初の _; 評価の前に一度だけ 0 に初期化され、その後 return 1; によって上書きされます。 2回目の _; 評価では再び初期化されず、 foo() も明示的に代入しないので( active == false のため)、最初の値を保持します。

    • 新しいコードジェネレータ: 0 は、リターンパラメータを含むすべてのパラメータが、各 _; 評価の前に再初期化されるからです。

  • 旧コードジェネレータの場合、式の評価順は不定です。 新しいコードジェネレータでは、ソース順(左から右)に評価するようにしていますが、保証はしません。 このため、意味上の差異が生じることがあります。

    例えば、以下のようなものです:

    // SPDX-License-Identifier: GPL-3.0
    pragma solidity >=0.8.1;
    contract C {
        function preincr_u8(uint8 a) public pure returns (uint8) {
            return ++a + a;
        }
    }
    

    関数 preincr_u8(1) は、以下の値を返します: - 古いコード生成器: 3 (1 + 2)。ただし、一般に戻り値は不定です。 - 新しいコードジェネレーター: 4 (2 + 2)。ただし、戻り値は保証されません。

    一方、関数の引数の式は、グローバル関数 addmodmulmod を除いて、両方のコードジェネレータで同じ順序で評価されます。 例えば、以下のようになります:

    // SPDX-License-Identifier: GPL-3.0
    pragma solidity >=0.8.1;
    contract C {
        function add(uint8 a, uint8 b) public pure returns (uint8) {
            return a + b;
        }
        function g(uint8 a, uint8 b) public pure returns (uint8) {
            return add(++a + ++b, a + b);
        }
    }
    

    関数 g(1, 2) は以下の値を返します:

    • 古いコードジェネレータ: 10 (add(2 + 3, 2 + 3))。ただし、一般に戻り値は不特定です。

    • 新しいコードジェネレーター: 10 。ただし、戻り値は保証されません。

    グローバル関数 addmodmulmod の引数は、古いコードジェネレータでは右から左に、新しいコードジェネレータでは左から右に評価されます。 例えば、以下のようになります:

    // SPDX-License-Identifier: GPL-3.0
    pragma solidity >=0.8.1;
    contract C {
        function f() public pure returns (uint256 aMod, uint256 mMod) {
            uint256 x = 3;
            // Old code gen: add/mulmod(5, 4, 3)
            // New code gen: add/mulmod(4, 5, 5)
            aMod = addmod(++x, ++x, x);
            mMod = mulmod(++x, ++x, x);
        }
    }
    

    関数 f() は以下の値を返します:

    • 旧コードジェネレーター: aMod = 0mMod = 2 です。

    • 新しいコードジェネレーター: aMod = 4mMod = 0 です。

  • 新しいコードジェネレーターでは、空きメモリポインタの上限が type(uint64).max (0xffffffffffff) に設定されました。 この制限を越えて値を増やすような割り当ては、リバートされます。 古いコードジェネレーターには、この制限はありません。

    例えば:

    // SPDX-License-Identifier: GPL-3.0
    pragma solidity >0.8.0;
    contract C {
        function f() public {
            uint[] memory arr;
            // allocation size: 576460752303423481
            // assumes freeMemPtr points to 0x80 initially
            uint solYulMaxAllocationBeforeMemPtrOverflow = (type(uint64).max - 0x80 - 31) / 32;
            // freeMemPtr overflows UINT64_MAX
            arr = new uint[](solYulMaxAllocationBeforeMemPtrOverflow);
        }
    }
    

    関数 f() は以下のような挙動をします: - 古いコードジェネレータ: 大きなメモリ割り当ての後、配列の内容をゼロにするときにガス欠になります。 - 新しいコードジェネレータ: フリーメモリポインタのオーバーフローによりリバートします(ガス欠はしない)。

内部構造

内部の関数ポインタ

古いコードジェネレータは、内部の関数ポインタの値にコードオフセットまたはタグを使用しています。 特に、これらのオフセットは構築時とデプロイ後では異なり、値はストレージを介してこの境界を越えることができるので、これは複雑です。 そのため、構築時には両方のオフセットが同じ値に(異なるバイトに)エンコードされます。

新しいコードジェネレータでは、関数ポインタは、順番に割り当てられる内部IDを使用します。 ジャンプによる呼び出しができないため、関数ポインタによる呼び出しは、常に switch 文を使って正しい関数を選択する内部ディスパッチ関数を使用する必要があります。

ID 0 は、初期化されていない関数ポインタ用に予約されており、このポインタが呼び出されると、ディスパッチ関数でパニックが発生します。

古いコードジェネレータでは、内部関数ポインタは、常にパニックを起こす特別な関数で初期化されます。 このため、ストレージ内の内部関数ポインタの構築時にストレージへの書き込みが発生します。

クリーンアップ

古いコードジェネレータは、ダーティビットの値によって結果が影響を受ける可能性のある操作の前にのみ、クリーンアップを行います。 新しいコードジェネレータでは、ダーティビットが発生する可能性のある操作の後にクリーンアップを行います。 オプティマイザが強力になり、冗長なクリーンアップ処理がなくなることを期待しています。

例えば、以下のようになります。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.1;
contract C {
    function f(uint8 a) public pure returns (uint r1, uint r2)
    {
        a = ~a;
        assembly {
            r1 := a
        }
        r2 = a;
    }
}

関数 f(1) は以下の値を返します。

  • 古いコードジェネレータ: ( fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe , 00000000000000000000000000000000000000000000000000000000000000fe )

  • 新しいコードジェネレータ: ( 00000000000000000000000000000000000000000000000000000000000000fe , 00000000000000000000000000000000000000000000000000000000000000fe )

なお、新コードジェネレータとは異なり、旧コードジェネレータでは、ビットの否定(not)の割り当て( a = ~a )の後にクリーンアップを行いません。 このため、新旧のコードジェネレータでは、インラインアセンブリブロック内で戻り値 r1 に割り当てられる値が異なります。 しかし、どちらのコードジェネレータも、 a の新しい値が r2 に割り当てられる前に、クリーンアップを実行します。

ストレージ内の状態変数のレイアウト

コントラクトの状態変数はストレージにコンパクトに格納され、複数の値が同じストレージスロットを使用することがあります。 動的なサイズの配列やマッピング(後述)を除き、データはスロット 0 に格納された最初の状態変数から順に連続して格納されます。 各変数には、その型に応じてバイト単位のサイズが決定されます。 32バイトに満たない複数の連続したアイテムは、以下のルールに従って、可能な限り1つのストレージスロットにまとめられます。

  • ストレージスロットの最初のアイテムは、下位にアラインされ格納されます。

  • 値型はそれを格納するのに必要な数のバイトしか使用しません。

  • 値型がストレージスロットの残りの部分に収まらない場合は、次のストレージスロットに格納されます。

  • 構造体や配列データは、常に新しいスロットで開始し、そのアイテムはこれらのルールに従って密にパッキングされます。

  • 構造体や配列データに続くアイテムは、常に新しいストレージスロットで開始します。

継承を使用しているコントラクトでは、状態変数の順序は、最も下位のコントラクトから始まるコントラクトのC3線形化された順序によって決定されます。 上記のルールで許可されていれば、異なるコントラクトの状態変数が同じストレージスロットを共有できます。

構造体や配列の要素は、あたかも個々の値が与えられたかのように、それぞれの要素の後に格納されます。

警告

32バイト以下の要素を使用する場合、コントラクトのガス使用量が多くなる場合があります。 これは、EVMが一度に32バイトで動作するためです。 そのため、要素がそれよりも小さい場合、EVMは要素のサイズを32バイトから希望のサイズに縮小するために、より多くの操作を行う必要があります。

コンパイラは複数の要素を1つのストレージスロットにまとめ、複数の読み書きを1つの操作にまとめるため、ストレージの値を扱う場合はサイズの小さい型を使用することが有益な場合があります。 しかし、スロット内のすべての値を同時に読み書きしない場合、これは逆効果になります。 複数の値を持つストレージスロットに1つの値が書き込まれた場合、ストレージスロットを最初に読み込んで、同じスロットの他のデータが破壊されないように新しい値と結合する必要があります。

関数の引数やメモリの値を扱う場合は、コンパイラがこれらの値をパックしないので、本質的なメリットはありません。

最後に、EVMに最適化させるために、ストレージ変数と struct メンバーの順番を工夫して、しっかりと詰め込むようにしてください。 例えば、ストレージ変数を uint128, uint256, uint128 ではなく uint128, uint128, uint256 の順に宣言すると、前者はストレージのスロットを2つしか使用しないのに対し、後者は3つ使用することになります。

注釈

ストレージの状態変数のレイアウトは、ストレージへのポインタをライブラリに渡すことができるため、Solidityの外部インターフェースの一部とみなされます。 つまり、このセクションで説明されているルールを変更することは、言語の破壊的な変更とみなされ、その重大な性質のため、実行する前に非常に慎重に検討する必要があります。 このような変更があった場合、コンパイラが古いレイアウトをサポートするバイトコードを生成する互換モードをリリースしたいと思います。

マッピングと動的配列

マッピングや動的なサイズの配列型は、そのサイズが予測できないため、前後の状態変数の間に格納することはできません。 その代わりに、それらは 上記のルール に対して32バイトしか占有しないとみなされ、含まれる要素はKeccak-256ハッシュを使用して計算された別のストレージスロットから開始して格納されます。

マッピングや配列の格納場所が、 ストレージのレイアウトルール を適用した後にスロット p になったとします。 動的配列の場合、このスロットには、配列の要素数が格納されます(バイト配列と文字列は例外で、 ここ を参照してください)。 マッピングの場合、このスロットは空のままですが、2つのマッピングが隣り合っていても、その内容が異なる保存場所になることを保証するために必要です。

配列データは keccak256(p) から始まり、静的なサイズの配列データと同じように配置されています。 要素の長さが16バイト以下であれば、ストレージスロットを共有できる可能性があります。 動的配列の動的配列は、このルールを再帰的に適用します。 x の型が uint24[][] である要素 x[i][j] の位置は、次のように計算されます(ここでも、 x 自身がスロット p に格納されていると仮定します)。 スロットは keccak256(keccak256(p) + i) + floor(j / floor(256 / 24)) であり、要素は (v >> ((j % floor(256 / 24)) * 24)) & type(uint24).max を用いてスロットデータ v から得ることができます。

マッピングキー k に対応する値は keccak256(h(k) . p) に位置し、 . は連結、 h はキーの型に応じて適用される関数です。

  • 値型の場合、 h はメモリに値を格納するときと同じように、値を32バイトにパディングします。

  • 文字列やバイト配列の場合、 h(k) は、パディングされていないデータです。

マッピング値が非値型の場合、計算されたスロットがデータの開始を示します。 例えば、値が構造体型の場合は、構造体のメンバーに到達するために、構造体のメンバーに対応するオフセットを追加する必要があります。

一例として、次のようなコントラクトを考えてみましょう。

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

contract C {
    struct S { uint16 a; uint16 b; uint256 c; }
    uint x;
    mapping(uint => mapping(uint => S)) data;
}

data[4][9].c の格納位置を計算してみましょう。 マッピング自体の位置は 1 です(32バイトの変数 x が先に存在しています)。 つまり、 data[4]keccak256(uint256(4) . uint256(1)) に格納されます。 data[4] の型は再びマッピングで、 data[4][9] のデータはスロット keccak256(uint256(9) . keccak256(uint256(4) . uint256(1))) から始まります。 ab は1つのスロットにパックされているので、構造体 S 内のメンバー c のスロットオフセットは 1 です。 つまり、 data[4][9].c のスロットは keccak256(uint256(9) . keccak256(uint256(4) . uint256(1))) + 1 です。 値型は uint256 なので、1つのスロットを使用します。

bytesstring

bytesstring は同じようにエンコードされます。 一般的には、配列自体を格納するスロットと、そのスロットの位置の keccak256 ハッシュを使って計算されるデータ領域があるという意味で、 bytes1[] と同様のエンコーディングになっています。 ただし、短い値(32バイトよりも)の場合は、配列の要素が長さとともに同じスロットに格納されます。

具体的には、データが最大で 31 バイトの場合、上位バイトに要素が格納され(左詰め)、下位バイトには値 length * 2 が格納されます。 32 バイト以上のデータを格納するバイト配列では、メインスロット plength * 2 + 1 が格納され、データは通常通り keccak256(p) に格納されます。 つまり、最下位ビットがセットされているかどうかで、short(セットされていない)、long(セットされている)と、短い配列と長い配列を見分けることができるのです。

注釈

無効にエンコードされたスロットの処理は現在サポートされていませんが、将来追加される可能性があります。 IR経由でコンパイルしている場合、不正にエンコードされたスロットを読むと Panic(0x22) エラーが発生します。

JSON出力

コントラクトのストレージレイアウトは、 標準JSONインターフェース を介して要求できます。 出力されるのは、 storagetypes の2つのキーを含むJSONオブジェクトです。 storage オブジェクトは配列で、各要素は次のような形をしています。

{
    "astId": 2,
    "contract": "fileA:A",
    "label": "x",
    "offset": 0,
    "slot": "0",
    "type": "t_uint256"
}

上記の例は、ソースユニット fileA から contract A { uint x; } のストレージレイアウトと

  • astId は状態変数の宣言のASTノードのIDです。

  • contract はコントラクトの名前で、プレフィックスとしてパスを含みます。

  • label は状態変数の名前です。

  • offset はエンコーディングに応じたストレージスロット内のバイト単位のオフセットです。

  • slot は、状態変数が存在する、あるいは、開始するストレージスロットです。 この数値は非常に大きくなる可能性があるため、JSONの値は文字列として表されます。

  • type は、変数の型情報のキーとなる識別子です(以下に記載)。

与えられた type 、この場合 t_uint256 は、 types の中の要素を表しており、その形は

{
    "encoding": "inplace",
    "label": "uint256",
    "numberOfBytes": "32",
}

ここで

  • encoding は、データがストレージでどのようにエンコードされているかを示すもので、可能な値は以下の通りです。

    • inplace: データがストレージに連続してレイアウトされている( 上記 参照)。

    • mapping: Keccak-256ハッシュベースの方式( 上記 参照)。

    • dynamic_array: Keccak-256ハッシュベースの方式( 上記 参照)。

    • bytes: シングルスロット、あるいは、データサイズに応じたKeccak-256ハッシュベース( 上記 参照)。

  • label は正規化された型名です。

  • numberOfBytes は使用されたバイト数(10進数の文字列)です。 numberOfBytes > 32 の場合は、複数のスロットが使用されていることを意味することに注意してください。

いくつかの型は、上記の4つの情報以外にも追加の情報を持っています。 マッピングには key 型と value 型があり(ここでも型のマッピングのエントリを参照しています)、配列には base 型があり、構造体には members 型がトップレベルの storage 型と同じ形式で記載されています( 上記 参照)。

注釈

コントラクトのストレージレイアウトのJSON出力フォーマットはまだ実験的なものと考えられており、Solidityの非破壊的なリリースで変更される可能性があります。

次の例では、値型と参照型、エンコードされたパック型、ネストされた型を含むコントラクトとそのストレージのレイアウトを示しています。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract A {
    struct S {
        uint128 a;
        uint128 b;
        uint[2] staticArray;
        uint[] dynArray;
    }

    uint x;
    uint y;
    S s;
    address addr;
    mapping(uint => mapping(address => bool)) map;
    uint[] array;
    string s1;
    bytes b1;
}
{
  "storage": [
    {
      "astId": 15,
      "contract": "fileA:A",
      "label": "x",
      "offset": 0,
      "slot": "0",
      "type": "t_uint256"
    },
    {
      "astId": 17,
      "contract": "fileA:A",
      "label": "y",
      "offset": 0,
      "slot": "1",
      "type": "t_uint256"
    },
    {
      "astId": 20,
      "contract": "fileA:A",
      "label": "s",
      "offset": 0,
      "slot": "2",
      "type": "t_struct(S)13_storage"
    },
    {
      "astId": 22,
      "contract": "fileA:A",
      "label": "addr",
      "offset": 0,
      "slot": "6",
      "type": "t_address"
    },
    {
      "astId": 28,
      "contract": "fileA:A",
      "label": "map",
      "offset": 0,
      "slot": "7",
      "type": "t_mapping(t_uint256,t_mapping(t_address,t_bool))"
    },
    {
      "astId": 31,
      "contract": "fileA:A",
      "label": "array",
      "offset": 0,
      "slot": "8",
      "type": "t_array(t_uint256)dyn_storage"
    },
    {
      "astId": 33,
      "contract": "fileA:A",
      "label": "s1",
      "offset": 0,
      "slot": "9",
      "type": "t_string_storage"
    },
    {
      "astId": 35,
      "contract": "fileA:A",
      "label": "b1",
      "offset": 0,
      "slot": "10",
      "type": "t_bytes_storage"
    }
  ],
  "types": {
    "t_address": {
      "encoding": "inplace",
      "label": "address",
      "numberOfBytes": "20"
    },
    "t_array(t_uint256)2_storage": {
      "base": "t_uint256",
      "encoding": "inplace",
      "label": "uint256[2]",
      "numberOfBytes": "64"
    },
    "t_array(t_uint256)dyn_storage": {
      "base": "t_uint256",
      "encoding": "dynamic_array",
      "label": "uint256[]",
      "numberOfBytes": "32"
    },
    "t_bool": {
      "encoding": "inplace",
      "label": "bool",
      "numberOfBytes": "1"
    },
    "t_bytes_storage": {
      "encoding": "bytes",
      "label": "bytes",
      "numberOfBytes": "32"
    },
    "t_mapping(t_address,t_bool)": {
      "encoding": "mapping",
      "key": "t_address",
      "label": "mapping(address => bool)",
      "numberOfBytes": "32",
      "value": "t_bool"
    },
    "t_mapping(t_uint256,t_mapping(t_address,t_bool))": {
      "encoding": "mapping",
      "key": "t_uint256",
      "label": "mapping(uint256 => mapping(address => bool))",
      "numberOfBytes": "32",
      "value": "t_mapping(t_address,t_bool)"
    },
    "t_string_storage": {
      "encoding": "bytes",
      "label": "string",
      "numberOfBytes": "32"
    },
    "t_struct(S)13_storage": {
      "encoding": "inplace",
      "label": "struct A.S",
      "members": [
        {
          "astId": 3,
          "contract": "fileA:A",
          "label": "a",
          "offset": 0,
          "slot": "0",
          "type": "t_uint128"
        },
        {
          "astId": 5,
          "contract": "fileA:A",
          "label": "b",
          "offset": 16,
          "slot": "0",
          "type": "t_uint128"
        },
        {
          "astId": 9,
          "contract": "fileA:A",
          "label": "staticArray",
          "offset": 0,
          "slot": "1",
          "type": "t_array(t_uint256)2_storage"
        },
        {
          "astId": 12,
          "contract": "fileA:A",
          "label": "dynArray",
          "offset": 0,
          "slot": "3",
          "type": "t_array(t_uint256)dyn_storage"
        }
      ],
      "numberOfBytes": "128"
    },
    "t_uint128": {
      "encoding": "inplace",
      "label": "uint128",
      "numberOfBytes": "16"
    },
    "t_uint256": {
      "encoding": "inplace",
      "label": "uint256",
      "numberOfBytes": "32"
    }
  }
}

メモリ内のレイアウト

Solidityは4つの32バイトスロットを確保しており、特定のバイト範囲(エンドポイントを含む)は以下のように使用されます。

  • 0x00 - 0x3f (64バイト): ハッシュ化のためのスクラッチ領域

  • 0x40 - 0x5f (32バイト): 現在割り当てられているメモリサイズ(別名: フリーメモリポインタ)

  • 0x60 - 0x7f (32バイト): ゼロスロット

スクラッチ領域は、文の間(インラインアセンブリ内)で使用できます。 ゼロスロットは、動的メモリ配列の初期値として使用され、決して書き込まれてはいけません(フリーメモリポインタは初期値として 0x80 を指します)。

Solidityは常に新しいオブジェクトをフリーメモリポインタに配置し、メモリは解放されません(これは将来変更されるかもしれません)。

Solidityのメモリ配列の要素は、常に32バイトの倍数を占めています(これは bytes1[] でも当てはまりますが、 bytesstring では当てはまりません)。 多次元のメモリ配列は、メモリ配列へのポインタです。 動的配列の長さは、配列の最初のスロットに格納され、その後に配列要素が続きます。

警告

Solidityには、64バイト以上の一時的なメモリ領域を必要とする操作があり、そのためスクラッチ領域には収まりません。 これらはフリーメモリが指す場所に配置されますが、その寿命が短いため、ポインタは更新されません。 メモリはゼロになってもならなくても構いません。 このため、フリーメモリがゼロアウトされたメモリを指していると思ってはいけません。

確実にゼロになったメモリ領域に到達するために msize を使用するのは良いアイデアに思えるかもしれませんが、空きメモリポインタを更新せずにそのようなポインタを非一時的に使用すると、予期しない結果になることがあります。

ストレージ内のレイアウトとの違い

以上のように、メモリ上のレイアウトと ストレージ 上のレイアウトは異なります。 以下、いくつかの例を紹介します。

配列における違いの例

次の配列は、ストレージでは32バイト(1スロット)ですが、メモリでは128バイト(32バイトずつの4つのアイテム)を占有します。

uint8[4] a;

構造体レイアウトにおける違いの例

次の構造体は、ストレージでは96バイト(32バイトのスロットが3つ)ですが、メモリでは128バイト(32バイトずつのアイテムが4つ)を占有します。

struct S {
    uint a;
    uint b;
    uint8 c;
    uint8 d;
}

コールデータのレイアウト

関数呼び出しの入力データは、 ABIの仕様 で定義されたフォーマットであることが前提となっています。 とりわけ、ABI仕様では、引数を32バイトの倍数にパディングすることが求められています。 内部の関数呼び出しでは、これとは異なる規則が用いられています。

コントラクトのコンストラクタの引数は、コントラクトのコードの最後にABIエンコーディングで直接追加されます。 コンストラクタはハードコードされたオフセットを使ってアクセスしますが、コードにデータを追加するときに変更されるため、 codesize オペコードは使用しません。

変数のクリーンアップ

値が256ビットよりも短い場合、場合によっては残りのビットをクリーンアップする必要があります。 最終的に、EVM内のすべての値は256ビットのワードで保存されます。 したがって、値の型が256ビット未満である場合、残りのビットをクリーニングする必要があるケースがあります。 Solidityのコンパイラは、残りのビットに含まれる潜在的なゴミによって悪影響を受ける可能性のある演算の前に、このようなクリーニングを行うように設計されています。

例えば、値をメモリに書き込む前に、残りのビットをクリアする必要があります。 これは、メモリの内容がハッシュの計算に使用されたり、メッセージコールのデータとして送信されたりする可能性があるためです。 同様に、ストレージに値を格納する前に、残りのビットを消去する必要があります。 そうしないと、文字化けした値が観測される可能性があるからです。

なお、インラインアセンブリによるアクセスはそのような操作とはみなされません。 インラインアセンブリを使用して256ビットより短いSolidity変数にアクセスした場合、コンパイラは値が適切にクリーンアップされることを保証しません。

また、直後の演算に影響がない場合は、ビットのクリーニングは行いません。 例えば、 JUMPI 命令では0以外の値は true とみなされるので、 JUMPI の条件として使われる前のブーリアン値のクリーニングは行いません。

上記の設計原理に加えて、Solidityのコンパイラは、入力データがスタックに読み込まれる際に、入力データをクリーニングします。

次の表は、異なるタイプに適用されるクリーニングルールを説明するもので、 higher bits は、タイプが256ビット未満の場合の残りのビットを指します。

有効な値

無効な値のクリーンアップ

n個のメンバーのenum

0 から n-1

例外を投げる

bool

0 あるいは 1

1になる

符号付き整数

higher bits set to the sign bit

currently silently signextends to a valid value, i.e. all higher bits are set to the sign bit; may throw an exception in the future

符号なし整数

上位ビットがゼロ

currently silently masks to a valid value, i.e. all higher bits are set to zero; may throw an exception in the future

有効な値と無効な値は、その型サイズに依存することに注意してください。 符号なし8ビット型である uint8 を考えてみると、次のような有効な値があります:

0000...0000 0000 0000
0000...0000 0000 0001
0000...0000 0000 0010
....
0000...0000 1111 1111

無効な値は、上位ビットが0に設定されます:

0101...1101 0010 1010   invalid value
0000...0000 0010 1010   cleaned value

符号付き8ビット型である int8 の場合、有効な値は次のとおりです:

Negative

1111...1111 1111 1111
1111...1111 1111 1110
....
1111...1111 1000 0000

Positive

0000...0000 0000 0000
0000...0000 0000 0001
0000...0000 0000 0010
....
0000...0000 1111 1111

コンパイラは符号ビットを signextend します。 符号ビットは負の値を1、正の値を0とし、上位ビットを上書きします:

Negative

0010...1010 1111 1111   invalid value
1111...1111 1111 1111   cleaned value

Positive

1101...0101 0000 0100   invalid value
0000...0000 0000 0100   cleaned value

ソースマッピング

コンパイラは、ASTの出力の一部として、ASTの各ノードで表現されるソースコードの範囲を提供します。 これは、ASTに基づいてエラーを報告する静的解析ツールや、ローカル変数とその用途を強調するデバッグツールなど、さまざまな目的に利用できます。

さらに、コンパイラは、バイトコードから、その命令を生成したソースコードの範囲へのマッピングを生成することもできます。 これは、バイトコードレベルで動作する静的解析ツールや、デバッガ内でソースコードの現在の位置を表示したり、ブレークポイントを処理する際にも重要です。 このマッピングには、ジャンプタイプやモディファイアの深さなど、他の情報も含まれています(後述)。

どちらのソースマッピングも、ソースファイルの参照には整数の識別子を使用します。 ソースファイルの識別子は output['sources'][sourceName]['id'] に格納され、 output はJSONとして解析されたstandard-jsonコンパイラインターフェースの出力です。 一部のユーティリティルーチンでは、コンパイラーは元の入力の一部ではなく、ソースマッピングから参照される「内部」ソースファイルを生成します。 これらのソースファイルは、その識別子とともに、 output['contracts'][sourceName][contractName]['evm']['bytecode']['generatedSources'] を通じて入手できます。

注釈

特定のソースファイルに関連付けられていない命令の場合、ソースマッピングでは -1 という整数の識別子が割り当てられます。 これは、コンパイラによって生成されたインラインアセンブリ文に由来するバイトコードセクションで発生する可能性があります。

AST内部のソースマッピングは以下の表記を使用しています。

s:l:f

s はソースファイル内の範囲の開始点へのバイトオフセット、 l はソース範囲の長さ(バイト)、 f は前述のソースインデックスです。

バイトコードのソースマッピングでのエンコーディングはもっと複雑です。 それは ; で区切られた s:l:f:j:m のリストです。 これらの要素はそれぞれ命令に対応しています。 つまり、バイトオフセットを使用することはできず、命令オフセットを使用する必要があります(プッシュ命令は1バイトよりも長い)。 フィールド slf は上記の通りです。 jio- のいずれかで、ジャンプ命令が関数に入るのか、関数から戻るのか、ループなどの一部としての通常のジャンプなのかを示します。 最後のフィールド m は、「モディファイアの深さ」を示す整数です。 この深さは、モディファイアにプレースホルダー文( _ )が入力されるたびに増加し、再び入力されると減少します。 これにより、同じモディファイアが2回使われたり、1つのモディファイアに複数のプレースホルダー文が使われたりするようなトリッキーなケースをデバッガーが追跡できます。

特にバイトコードの場合、これらのソースマッピングを圧縮するために、以下のルールが使われています。

  • フィールドが空の場合は、直前の要素の値が使用されます。

  • : がない場合、以下のすべてのフィールドは空であるとみなされます。

これは、次のソースマッピングが同じ情報を表していることを意味します。

1:2:1;1:9:1;2:1:2;2:1:2;2:1:2

1:2:1;:9;2:1:2;;

重要なのは、 verbatim ビルトインを使用すると、ソースマッピングが無効になることです。 ビルドインは複数の命令ではなく、1つの命令とみなされます。

オプティマイザ

Solidityコンパイラは、2つの異なるオプティマイザモジュールを使用しています。 オペコードレベルで動作する「旧」オプティマイザと、Yul IRコードで動作する「新」オプティマイザです。

オペコードベースのオプティマイザは、オペコードに 簡略化ルール を適用します。 また、同じコードセットを組み合わせたり、使われていないコードを削除したりします。

Yulベースのオプティマイザは、関数呼び出しをまたいで動作できるのでより強力です。 例えば、Yulでは任意のジャンプができないため、各関数の副作用を計算できます。 2つの関数呼び出しを考えてみましょう。 1つ目はストレージを変更せず、2つ目はストレージを変更します。 それらの引数と戻り値がお互いに依存しない場合、関数呼び出しを並べ替えることができます。 同様に、ある関数に副作用がなく、その実行結果にゼロをかける場合は、その関数呼び出しを完全に削除できます。

現在、パラメータ --optimize は、生成されたバイトコードにはオペコードベースのオプティマイザを、ABI coder v2などで内部的に生成されたYulコードにはYulオプティマイザを適用します。 solc --ir-optimized --optimize は、Solidityのソースに対して最適化されたYul IRを生成するために使用できます。 同様に、 solc --strict-assembly --optimize はスタンドアローンのYulモードに使用できます。

注釈

peepholeオプティマイザ はデフォルトで常に有効になっており、 Standard JSON によってのみオフにできます。

オプティマイザモジュールとその最適化ステップの詳細は以下の通りです。

Solidityコードを最適化するメリット

全体的に、オプティマイザは複雑な式を単純化しようとします。 これにより、コードサイズと実行コストの両方が削減されます。 つまり、コントラクトのデプロイやコントラクトへの外部呼び出しに必要なガスを削減できます。 また、関数の特殊化やインライン化も行います。 特に関数のインライン化は、コードサイズが大きくなる可能性がある操作ですが、より単純化できる機会があるため、よく行われます。

最適化コードと非最適化コードの違い

一般的に最も目に見える違いは、定数式がコンパイル時に評価されることです。 ASMの出力に関しても、同じあるいは重複するコードブロックが減っていることがわかります(フラグ --asm--asm --optimize の出力を比較してみてください)。 しかし、Yul/中間表現になると大きな差が出ることがあります。 例えば、冗長性をなくすために、関数がインライン化されたり、結合されたり、書き換えられたりすることがあります(フラグ --ir--optimize --ir-optimized の出力を比較してみてください)。

オプティマイザの実行回数パラメータ

実行回数( --optimize-runs )は、デプロイされたコードの各オペコードがコントラクトのライフタイム中にどのくらいの頻度で実行されるかを大まかに指定します。 つまり、コードサイズ(デプロイコスト)とコード実行コスト(デプロイ後のコスト)のトレードオフパラメータとなります。 実行回数パラメータが1の場合、コードは短いものの実行時にコストのかかるコードが生成されます。 一方、実行回数パラメータを大きくすると、コードは長いもののガス効率の良いコードが生成されます。 パラメータの最大値は 2**32-1 です。

注釈

よくこのパラメータがオプティマイザの反復回数を指定すると誤解されますが、これは違います。 オプティマイザは、コードが改善される限り、常に何度でも実行されます。

オペコードベースのオプティマイザモジュール

オペコードベースのオプティマイザモジュールは、アセンブリコード上で動作します。 このモジュールは、一連の命令を JUMPsJUMPDESTs の基本ブロックに分割します。 これらのブロックの中で、オプティマイザは命令を解析し、スタックやメモリ、ストレージに対するすべての変更を、命令と他の式へのポインタである引数のリストからなる式として記録します。

さらに、オペコードベースのオプティマイザでは、「CommonSubexpressionEliminator」というコンポーネントを使用しています。 他のタスクの中で、(すべての入力に対して)常に等しい式を見つけ出し、それらを式クラスにまとめるというものです。 まず、既知の式のリストから新しい式を見つけようとします。 もしそのような式が見つからなければ、 constant + constant = sum_of_constantsX * 1 = X のようなルールに従って式を簡略化します。 これは再帰的なプロセスであるため、第2因子が常に1と評価されることがわかっているより複雑な式の場合、後者のルールを適用することもできます。

オプティマイザの一部のステップでは、ストレージやメモリの位置をシンボリックに追跡します。 例えば、この情報は、コンパイル時に評価できるKeccak-256ハッシュの計算に使用されます。 次のシーケンスを考えてみましょう。

PUSH 32
PUSH 0
CALLDATALOAD
PUSH 100
DUP2
MSTORE
KECCAK256

または、同等の処理をする次のYulコードを考えてみましょう。

let x := calldataload(0)
mstore(x, 100)
let value := keccak256(x, 32)

この場合、オプティマイザはメモリ位置 calldataload(0) の値を追跡し、コンパイル時にKeccak-256ハッシュを評価できることを認識します。 これがうまくいくのは、 mstorekeccak256 の間にメモリを変更する他の命令がない場合です。 つまり、メモリ(またはストレージ)に書き込む命令があれば、現在のメモリ(またはストレージ)の知識を消去する必要があるのです。 しかし、この消去には例外があり、その命令がある場所に書き込まれていないことが容易にわかる場合です。

例えば、次のコードです。

let x := calldataload(0)
mstore(x, 100)
// Current knowledge memory location x -> 100
let y := add(x, 32)
// Does not clear the knowledge that x -> 100, since y does not write to [x, x + 32)
mstore(y, 200)
// This Keccak-256 can now be evaluated
let value := keccak256(x, 32)

そのため、ストレージやメモリの位置(例えば位置 l )を変更する場合、 l に等しい可能性のあるストレージやメモリの位置に関する知識を消去しなければなりません。 具体的には、ストレージについては、 l に等しい可能性のあるシンボリックロケーションの知識をすべて消去し、メモリについては、少なくとも32バイト離れていない可能性のあるシンボリックロケーションの知識をすべて消去しなければなりません。 m が任意の位置を示す場合、この消去の判断は値 sub(l, m) を計算することで行われます。 ストレージの場合、この値がゼロではないリテラルと評価されれば、 m に関する知識は維持されます。 メモリの場合、この値が 322**256 - 32 の間のリテラルと評価されるならば、 m に関する知識が保持されます。 それ以外の場合は、 m に関する知識は消去されます。

このプロセスを経て、最後にどの式がスタック上になければならないかがわかり、メモリとストレージの修正リストができました。 これらの情報は基本ブロックと一緒に保存され、ブロックの連結に使用されます。 さらに、スタック、ストレージ、メモリの構成に関する知識は、次のブロック(複数可)に転送されます。

すべての JUMP 命令と JUMPI 命令のターゲットがわかっていれば、プログラムの完全なコントロールフローグラフを作成できます。 一つだけわからないターゲットがある場合(ジャンプターゲットは原理的に入力から計算できるため、このようなことが起こりうる)、ブロックの入力状態に関する知識をすべて消去しなければなりません。 なぜなら、そのブロックは未知の JUMP のターゲットになりうるからです。 オペコードベースのオプティマイザモジュールは、条件が定数で評価される JUMPI を見つけた場合、それを無条件ジャンプに変換します。

最後のステップとして、各ブロックのコードが再生成されます。 オプティマイザは、ブロックの最後のスタック上の式から依存関係のグラフを作成し、このグラフに含まれないすべての操作を削除します。 メモリやストレージの変更を元のコードの順番通りに適用するコードを生成します(必要ないと判断された変更は削除します)。 最後に、スタック上に必要なすべての値を正しい位置に生成します。

これらのステップは各基本ブロックに適用され、新しく生成されたコードの方が小さい場合には置き換えとして使用されます。 基本ブロックが JUMPI で分割され、解析中にその条件が定数と評価された場合、 JUMPI は定数の値に基づいて置換されます。 したがって、以下のようなコードは

uint x = 7;
data[7] = 9;
if (data[x] != x + 2) // this condition is never true
  return 2;
else
  return 1;

は次のように簡略化されます。

data[7] = 9;
return 1;

単純なインライン化

Solidityのバージョン0.8.2以降、オプティマイザのステップとして、「ジャンプ」で終わる「単純な」命令を含むブロックへの特定のジャンプを、これらの命令のコピーに置き換えるものがあります。 これは、単純で小さなSolidityやYulの関数のインライン化に相当します。 特に、シーケンス PUSHTAG(tag) JUMP は、 JUMP が関数への「ジャンプ」としてマークされ、 tag の後ろに、関数からの「ジャンプ」としてマークされた別の JUMP で終わる基本ブロック(「CommonSubexpressionEliminator」で前述したように)がある場合には、置き換えられる可能性があります。

具体的には、Solidityの内部関数をコールした際に生成されるアセンブリの典型的な例を以下に示します。

  tag_return
  tag_f
  jump      // in
tag_return:
  ...opcodes after call to f...

tag_f:
  ...body of function f...
  jump      // out

関数のボディが連続した基本ブロックである限り、「Inliner」は tag_f jumptag_f のブロックで置き換えることができ、結果として以下のようになります。

  tag_return
  ...body of function f...
  jump
tag_return:
  ...opcodes after call to f...

tag_f:
  ...body of function f...
  jump      // out

ここで理想的なのは、上述の他のオプティマイザのステップにより、リターンタグのプッシュが残りのジャンプの方に移動し、結果として、

  ...body of function f...
  tag_return
  jump
tag_return:
  ...opcodes after call to f...

tag_f:
  ...body of function f...
  jump      // out

この場合、「PeepholeOptimizer」はリターンジャンプを削除します。 理想的には、すべての tag_f への参照に対してこれを行い、未使用のまま、削除できるようにできます。

...body of function f...
...opcodes after call to f...

そのため、関数 f の呼び出しはインライン化され、 f の元の定義は削除できます。

このようなインライン化は、インライン化しないよりもインライン化した方がコントラクトのライフタイムの中で安くなるというヒューリスティックな提案がある場合に試みられます。 このヒューリスティックは、関数本体のサイズ、そのタグへの他の参照の数(関数のコール回数に近似)、コントラクトの予想実行回数(グローバルオプティマイザのパラメータ「runs」)に依存します。

Yulベースのオプティマイザモジュール

Yulベースのオプティマイザは、いくつかのステージとコンポーネントで構成されており、これらがすべて意味的に同等の方法でASTを変換します。 最終的には、コードを短くするか、少なくともわずかに長くするだけで、さらなる最適化を可能にすることが目標です。

警告

オプティマイザは現在鋭意開発中のため、ここに掲載されている情報は古いものになっている可能性があります。

特定の機能に依存している場合は、チームに直接お問い合わせください。

現在、オプティマイザは純粋に貪欲な戦略をとり、バックトラックは一切行いません。

Yulベースのオプティマイザモジュールの全構成要素を以下に説明します。 以下の変換ステップが主な構成要素です。

  • SSA Transform

  • Common Subexpression Eliminator

  • Expression Simplifier

  • Redundant Assign Eliminator

  • Full Inliner

オプティマイザのステップ

これは、Yulベースのオプティマイザの全ステップをアルファベット順に並べたリストです。 個々のステップとそのシーケンスについては、以下で詳しく説明しています。

Abbreviation

Full name

f

BlockFlattener

l

CircularReferencesPruner

c

CommonSubexpressionEliminator

C

ConditionalSimplifier

U

ConditionalUnsimplifier

n

ControlFlowSimplifier

D

DeadCodeEliminator

E

EqualStoreEliminator

v

EquivalentFunctionCombiner

e

ExpressionInliner

j

ExpressionJoiner

s

ExpressionSimplifier

x

ExpressionSplitter

I

ForLoopConditionIntoBody

O

ForLoopConditionOutOfBody

o

ForLoopInitRewriter

i

FullInliner

g

FunctionGrouper

h

FunctionHoister

F

FunctionSpecializer

T

LiteralRematerialiser

L

LoadResolver

M

LoopInvariantCodeMotion

r

RedundantAssignEliminator

m

Rematerialiser

V

SSAReverser

a

SSATransform

t

StructuralSimplifier

p

UnusedFunctionParameterPruner

S

UnusedStoreEliminator

u

UnusedPruner

d

VarDeclInitializer

いくつかのステップは BlockFlattener, FunctionGrouper, ForLoopInitRewriter によって確保されるプロパティに依存しています。 このため、Yulオプティマイザは、ユーザーが提供したステップを適用する前に、常にそれらを適用します。

最適化の選択

デフォルトでは、オプティマイザは、生成されたアセンブリに対して、事前に定義された最適化ステップのシーケンスを適用します。 --yul-optimizations オプションを使用すると、このシーケンスを上書きして、独自のシーケンスを指定できます。

solc --optimize --ir-optimized --yul-optimizations 'dhfoD[xarrscLMcCTU]uljmul:fDnTOc'

ステップの順番は重要で、アウトプットの品質に影響します。 さらに、あるステップを適用することで、すでに適用した他のステップの新たな最適化の機会が発見されることもあり、ステップを繰り返すことが有益なことも多くあります。

[...] 内のシーケンスは、Yulコードが変化しないか、最大ラウンド数(現在は12ラウンド)に達するまで、ループで複数回適用されます。 括弧( [] )は連続して複数回使用できますが、入れ子にすることはできません。

注意すべき重要な点は、ユーザーから提供されたシーケンス(ユーザーから提供されなかった場合はデフォルトのシーケンス)の前後に常に実行されるハードコードされたステップがいくつかあることです。

クリーンアップシーケンスの区切り文字 : はオプションで、デフォルトのクリーンアップシーケンスを置き換えるために、カスタムクリーンアップシーケンスを指定するために使用します。 省略された場合、オプティマイザはデフォルトのクリーンアップシーケンスを適用します。 また、デリミターをユーザーが指定したシーケンスの先頭に置くと、最適化シーケンスは空になり、逆にシーケンスの末尾に置くと、空のクリーンアップシーケンスとして扱われます。

前処理

前処理コンポーネントは、プログラムを作業しやすい特定の正規形に変換します。

この正規形は、最適化プロセスの残りの部分でも保たれます。

Disambiguator

DisambiguatorはASTを受け取り、すべての識別子が入力ASTの中でユニークな名前を持つようなコピーを新たに作って返します。 これは、他の全てのオプティマイザのステージの前提条件となります。 利点は、識別子の検索にスコープを考慮する必要がないため、他のステップで必要な分析が簡単になることです。

それ以降のステージでは、すべての名前が一意に保たれるという特性があります。 つまり、新しい識別子を導入する必要がある場合は、新しいユニークな名前が生成されます。

FunctionHoister

FunctionHoisterは、すべての関数定義を最上位のブロックの最後に移動させます。 これは、曖昧さを解消するステージの後に実行される限り、意味的に同等の変換です。 その理由は、定義を上位のブロックに移動しても、そのビジビリティを低下させることはできず、また、別の関数で定義された変数を参照することもできないからです。

このステージの利点は、関数の定義をより簡単に調べることができ、ASTを完全にトラバースすることなく関数を単独で最適化できることです。

FunctionGrouper

FunctionGrouperは、DisambiguatorとFunctionHoisterの後に適用しなければなりません。 その効果は、関数定義ではないすべての最上位要素が、ルートブロックの最初の文である単一のブロックに移動されることです。

このステップを経て、プログラムは次のような正規形になります。

{ I F... }

I は関数定義を(再帰的にも)含まない(空になる可能性のある)ブロックであり、 F は関数定義のリストでどの関数も関数定義を含まないようになっています。

このステージの利点は、関数のリストがどこから始まるかを常に把握できることです。

ForLoopConditionIntoBody

この変換は、forループのループ反復条件をループ本体に移動させるものです。 ExpressionSplitter は反復条件式(以下の例では C )には適用されないため、この変換が必要です。

for { Init... } C { Post... } {
    Body...
}

上記のコードは次の処理に変換されます。

for { Init... } 1 { Post... } {
    if iszero(C) { break }
    Body...
}

ループ不変条件の不変量をループの外に出すことができるため、この変換は LoopInvariantCodeMotion と組み合わせても有効です。

ForLoopInitRewriter

この変換により、for-loopの初期化部分がループの前に移動します。

for { Init... } C { Post... } {
    Body...
}

上記のコードは次の処理に変換されます。

Init...
for {} C { Post... } {
    Body...
}

これにより、forループ初期化ブロックの複雑なスコープルールを無視できるため、残りの最適化プロセスが容易になります。

VarDeclInitializer

このステップでは、変数の宣言を書き換えて、すべての変数が初期化されるようにします。 let x, y のような宣言は、複数の宣言文に分割されます。

今のところ、ゼロリテラルでの初期化のみをサポートしています。

疑似SSAトランスフォーム

このコンポーネントの目的は、プログラムをより長い形式にして、他のコンポーネントがより簡単に作業できるようにすることです。 最終的な表現は、Static-Single-Assignment (SSA)形式に似ていますが、コントロールフローの異なるブランチからの値を結合する明示的な「phi」関数を使用しないという違いがあります(そのような機能はYul言語には存在しません)。 代わりに、コントロールフローがマージされる際に、いずれかのブランチで変数が再代入されると、その現在の値を保持する新しいSSA変数が宣言されるため、以下の式では依然としてSSA変数を参照するだけでよいです。

変換例は以下の通りです。

{
    let a := calldataload(0)
    let b := calldataload(0x20)
    if gt(a, 0) {
        b := mul(b, 0x20)
    }
    a := add(a, 1)
    sstore(a, add(b, 0x20))
}

以下の変換ステップをすべて適用すると、プログラムは以下のようになります。

{
    let _1 := 0
    let a_9 := calldataload(_1)
    let a := a_9
    let _2 := 0x20
    let b_10 := calldataload(_2)
    let b := b_10
    let _3 := 0
    let _4 := gt(a_9, _3)
    if _4
    {
        let _5 := 0x20
        let b_11 := mul(b_10, _5)
        b := b_11
    }
    let b_12 := b
    let _6 := 1
    let a_13 := add(a_9, _6)
    let _7 := 0x20
    let _8 := add(b_12, _7)
    sstore(a_13, _8)
}

このスニペットで再代入されている変数は b のみであることに注意してください。 b はコントロールフローに応じて異なる値を持つため、この再代入を避けることはできません。 他のすべての変数は、一度定義されるとその値が変わることはありません。 この特性の利点は、新しいコンテキストでこれらの値が有効である限り、変数を自由に移動させたり、変数への参照を初期値で交換したりできることです(その逆も同様)。

もちろん、このコードは最適化されたものとは程遠いです。 それどころかずっと長くなっています。 ここで期待することは、このコードが作業しやすくなり、さらに、これらの変更をリバートし、最後に再びコードをコンパクトにするオプティマイザのステップがあることです。

ExpressionSplitter

ExpressionSplitterは、 add(mload(0x123), mul(mload(0x456), 0x20)) のような式を、その式のサブ式に代入られた一意の変数の宣言の列に変え、各関数呼び出しが引数として変数のみを持つようにします。

上記の式は次のように変換されます。

{
    let _1 := 0x20
    let _2 := 0x456
    let _3 := mload(_2)
    let _4 := mul(_3, _1)
    let _5 := 0x123
    let _6 := mload(_5)
    let z := add(_6, _4)
}

なお、この変換はオペコードや関数のコールの順番を変えるものではありません。

これは、ループのコントロールフローが、すべてのケースで内部式の「アウトライン化」を許可していないため、ループの反復条件には適用されません。 ForLoopConditionIntoBody を適用して反復条件をループ本体に移動させることで、この制限を回避できます。

最終的なプログラムは、(ループ条件を除いて)関数呼び出しを式の中に入れ子にすることはできず、関数呼び出しの引数はすべて変数でなければならないという形にしなければなりません。

この形式の利点は、オペコードの順序を変更するのがかなり容易であることと、関数呼び出しのインライン化を実行するのも容易であることです。 さらに、式の個々の部分を置き換えたり、「式ツリー」を再編成したりするのも簡単です。 難点は、人間にとって読みにくいコードであることです。

SSATransform

このステージでは、既存の変数への繰り返しの代入を、新しい変数の宣言で可能な限り置き換えようとします。 再代入は残っていますが、再代入された変数へのすべての参照は、新しく宣言された変数に置き換えられます。

例:

{
    let a := 1
    mstore(a, 2)
    a := 3
}

は、次のコードに変換されます。

{
    let a_1 := 1
    let a := a_1
    mstore(a_1, 2)
    let a_3 := 3
    a := a_3
}

厳密なセマンティクス:

コードのどこかに代入されている変数 a (値が宣言されていて再代入されない変数は変更されない)について、以下の変換を行います。

  • let a := vlet a_i := v   let a := a_i で置き換えます。

  • a := vlet a_i := v   a := a_i に置き換えます。 ここで ia_i にまだ使われていない数です。

さらに、 a に使われている i の現在の値を常に記録し、 a への各参照を a_i に置き換えます。 変数 a の現在値のマッピングは、それが代入された各ブロックの終了時、およびforループ本体やポストブロック内で代入された場合はforループのinitブロックの終了時にクリアされます。 上記のルールで変数の値がクリアされ、その変数がブロック外で宣言された場合、ループのポスト/ボディブロックの先頭や、If/Switch/ForLoop/Block文の直後など、コントロールフローが合流する位置に新たなSSA変数が作成されます。

このステージの後、不要な中間代入を削除するために、Redundant Assign Eliminatorを使用することをお勧めします。

このステージでは、Expression SplitterとCommon Subexpression Eliminatorが直前に実行されると、過剰な量の変数が生成されないため、最良の結果が得られます。 一方、Common Subexpression EliminatorはSSAトランスフォームの後に実行した方がより効率的です。

RedundantAssignEliminator

SSAトランスフォームでは、次の例のように多くのケースで不要な場合があっても、常に a := a_i 形式の割り当てが生成されます。

{
    let a := 1
    a := mload(a)
    a := sload(a)
    sstore(a, 1)
}

SSAトランスフォームでは、このスニペットを次のように変換します。

{
    let a_1 := 1
    let a := a_1
    let a_2 := mload(a_1)
    a := a_2
    let a_3 := sload(a_2)
    a := a_3
    sstore(a_3, 1)
}

Redundant Assign Eliminatorは、 a の値が使用されていないため、 a への3つの割り当てをすべて削除し、このスニペットを厳密なSSAフォームにします。

{
    let a_1 := 1
    let a_2 := mload(a_1)
    let a_3 := sload(a_2)
    sstore(a_3, 1)
}

もちろん、代入が冗長であるかどうかを判断する複雑な部分は、コントロールフローの結合につながっています。

このコンポーネントは、詳しくは以下のように動作します。

ASTは、情報収集のステップと実際の削除のステップの2回にわたって走査されます。 情報収集のステップでは、代入文から「unused」「undecided」「used」の3つの状態へのマッピングを保持しています。 これは、代入された値が後でその変数への参照によって使われるかどうかを示すものです。

代入が訪問されると、「undecided」状態のマッピングに追加され(後述のforループに関する記述を参照)、「undecided」状態のままの同じ変数への他のすべての代入は「unused」に変更されます。 ある変数が参照されると、「undecided」状態にあるその変数へのすべての割り当ての状態は "used"に変更されます。

コントロールフローが分岐するポイントでは、マッピングのコピーが各ブランチに引き渡されます。 コントロールフローが合流するポイントでは、2つのブランチから送られてきた2つのマッピングが次のようにして結合されます。 1つのマッピングにしかない文や同じ状態の文は、変更されずに使用されます。 相反する値は次のようにして解決されます。

  • 「unused」「undecided」 -> 「undecided」

  • 「unused」「used」 -> 「used」

  • 「undecided」「used」 -> 「used」

for-loopでは、condition、body、post-partを2回訪れ、conditionでのコントロールフローの結合を考慮します。 つまり、3つのコントロールフローの経路を作ります。 つまり、0回のループ、1回のループ、2回のループの3つのコントロールフローを作成し、最後にそれらを結合します。

3回目以降のシミュレーションは不要であることは、次のように考えられます。

反復開始時の割り当ての状態は、反復終了時のその割り当ての状態を決定論的にもたらします。 この状態マッピング関数を f とします。 上記で説明した3つの異なる状態 unusedundecidedused の組み合わせは、 unused = 0undecided = 1used = 2max 演算です。

適切な方法は、次のように計算します。

max(s, f(s), f(f(s)), f(f(f(s))), ...)

をループ後の状態とします。 f は3つの異なる値の範囲を持っているだけなので、これを反復すると、最大で3回の反復後にサイクルに到達しなければならず、したがって f(f(f(s)))sf(s)f(f(s)) のいずれかと等しくなければならず、したがって

max(s, f(s), f(f(s))) = max(s, f(s), f(f(s)), f(f(f(s))), ...).

要するに、3つの異なる状態があるだけなので、ループを最大2回実行すれば十分です。

defaultのケースを持つswitch文では、スイッチをスキップするコントロールフローの部分はありません。

変数がスコープ外に出ると、まだ「undecided」の状態にあるすべての文が「unused」に変更されます。 ただし、その変数が関数のリターンパラメータである場合は、「used」に変更されます。

2回目のトラバーサルでは、「unused」の状態にあるすべての代入が削除されます。

このステップは通常、SSAトランスフォームの直後に実行され、疑似SSAの生成を完了します。

ツール

Movability

movabilityは、式の特性の一つです。 大まかに言うと、その式は副作用がなく、その評価は変数の値と環境のコールコンスタントな状態にのみ依存するということです。 ほとんどの式はmovableです。 以下の部分が式をnon-movableにしています。

  • 関数の呼び出し(関数内のすべての文がmovableであれば、将来緩和される可能性あり)

  • 副作用のある(可能性のある)オペコード( callselfdestruct など)

  • メモリ、ストレージ、外部の状態情報を読み書きするオペコード

  • 現在のPC、メモリサイズ、リターンデータのサイズに依存するオペコード

DataflowAnalyzer

Dataflow Analyzerは、それ自体はオプティマイザではありませんが、他のコンポーネントのツールとして使用されます。 ASTをトラバースしながら、各変数の現在の値を追跡します(その値がmovableな式である限り)。 各変数に現在割り当てられている式の一部である変数を記録します。 変数 a に代入されるたびに、 a の現在の格納値が更新され、 ab の現在格納されている式の一部であるときは、すべての変数 b のすべての格納値がクリアされます。

コントロールフローの分岐点では、コントロールフローのいずれかの経路で代入された、または代入される可能性のある変数についての知識がクリアされます。 たとえば、forループに入ると、bodyまたはpostブロックで代入される予定のすべての変数がクリアされます。

式スケールの単純化

これらの簡略化パスは、表現を変更し、同等の、できればより単純な表現に置き換えます。

CommonSubexpressionEliminator

このステップでは、Dataflow Analyzer を使用して、構文的に変数の現在の値と一致する部分式を、その変数への参照に置き換えます。 このような部分式はmovableでなければならないため、これは等価変換です。

識別子であるすべての部分式は、その値が識別子である場合、現在の値で置き換えられます。

上記の2つのルールの組み合わせにより、ローカルな値のナンバリングを計算できます。 これは、2つの変数が同じ値を持つ場合、そのうちの1つは常に使用されないことを意味します。 Unused PrunerやRedundant Assign Eliminatorは、このような変数を完全に排除できます。

このステップは、式分割機が前に実行されている場合、特に効率的です。 コードが疑似SSA形式であれば、変数の値はより長い時間利用可能であるため、式が置換可能になる可能性が高くなります。

式単純化装置は、その直前に共通部分式除去装置が実行されていれば、より良い置換を行うことができます。

ExpressionSimplifier

Expression Simplifierは、Dataflow Analyzerを使用し、 X + 0 -> X のような式に対する等価変換のリストを利用してコードを単純化します。

X + 0 のようなパターンを各部分式でマッチさせようとします。 また、コードが疑似SSA形式であっても、より深い入れ子のパターンにマッチできるように、マッチング処理中に変数を現在割り当てられている式に解決します。

X - X -> 0 のようないくつかのパターンは、式 X がmovableである限り適用できます。 そうでなければ、その潜在的な副作用を取り除くことになるからです。 変数参照は、現在の値がそうでないかもしれないとしても、常にmovableであるため、式の簡略化は、分割または疑似SSAの形で再び強力になります。

LiteralRematerialiser

ドキュメント化予定。

LoadResolver

sload(x) 型と mload(x) 型の式を、現在ストレージやメモリに格納されている値で置き換える最適化ステージ。

コードがSSA形式の場合に最適です。

前提条件: Disambiguator、ForLoopInitRewriter。

文スケールの単純化

CircularReferencesPruner

このステージでは、相互に呼び出しているが、外部から参照されておらず、一番外側のコンテキストからも参照されていない関数を削除します。

ConditionalSimplifier

条件付きシンプリファイアは、コントロールフローから値が決定できる場合、条件変数への割り当てを挿入します。

SSAフォームを破棄します。

現在のところ、このツールは非常に限定されています。 主な理由は、ブーリアン型をまだサポートしていないからです。 条件は式がゼロでないことをチェックするだけなので、特定の値を割り当てることはできません。

現在の機能:

  • スイッチケースで「<condition> := <caseLabel>」を挿入します。

  • 終了コントロールフローのif文の後に、「<条件> := 0」を挿入します。

今後の機能:

  • 「1」による置き換えを可能にします。

  • ユーザー定義関数の終了を考慮に入れます。

SSA形式で、かつデッドコード除去を実行したことがある場合に最適です。

前提条件: Disambiguator。

ConditionalUnsimplifier

Conditional Simplifierの逆です。

ControlFlowSimplifier

いくつかのコントロールフロー構造を簡素化をします:

  • pop(condition)でifを空のボディに置き換える

  • 空のデフォルトのスイッチケースを削除する

  • デフォルトのケースが存在しない場合、空のスイッチケースを削除する

  • ケースのないswitchをpop(expression)で置き換える

  • シングルケースのスイッチをifに変える

  • pop(expression)とbodyでデフォルトケースのみのswitchに変更する

  • スイッチを、ケースボディが一致するconst exprに置き換える

  • for を終端コントロールフローに置き換える、 if による他のブレーク/コンティニューなしで

  • 関数の最後にある leave を削除する

これらの操作はいずれもデータフローには依存しません。 StructuralSimplifierは、データフローに依存する同様のタスクを実行します。

ControlFlowSimplifierは、トラバーサル中に break 文と continue 文の有無を記録します。

前提条件: Disambiguator、FunctionHoister、ForLoopInitRewriter。

重要: EVMオペコードを導入しているため、現在はEVMコードにのみ使用可能です。

DeadCodeEliminator

この最適化ステージでは、到達できないコードを削除します。

到達不可能なコードとは、ブロック内のコードのうち、leave、return、invalid、break、continue、selfdestruct、revert、または無限に再帰するユーザー定義関数の呼び出しが先行するものを指します。

関数定義は、以前のコードから呼び出される可能性があるため、到達可能とみなされて保持されます。

forループのinitブロックで宣言された変数は、そのスコープがループ本体にまで及ぶため、このステップの前にForLoopInitRewriterを実行する必要があります。

前提条件: ForLoopInitRewriter、Function Hoister、Function Grouper。

EqualStoreEliminator

このステップは、 mstore(k, v) / sstore(k, v) の呼び出しが過去にあり、その間に他のストアがなく、 kv の値が変更されていない場合に、 mstore(k, v)sstore(k, v) の呼び出しを削除します。

この単純なステップは、SSA変換とCommon Subexpression Eliminatorの後に実行すると効果的です。 SSAは変数が変更されないことを確認し、Common Subexpression Eliminatorは値が同じであることが分かっている場合、まったく同じ変数を再利用するからです。

前提条件: Disambiguator、ForLoopInitRewriter。

UnusedPruner

このステップでは、参照されることのないすべての関数の定義を削除します。

また、決して参照されない変数の宣言も削除されます。 宣言が移動不可能な値を割り当てている場合、式は保持されますが、その値は破棄されます。

movableな式の文(割り当てられていない式)はすべて削除されます。

StructuralSimplifier

これは、構造的なレベルで様々な種類の単純化を行う一般的なステップです。

  • if文を pop(condition) による空のボディに置き換える

  • 真の条件を持つif文をそのボディで置き換える

  • 偽の条件を持つif文は削除する

  • シングルケースのスイッチをifに変える

  • スイッチを pop(expression) とボディのデフォルトケースのみに置き換える

  • 大文字小文字を一致させてスイッチをリテラル表現に置き換える

  • 偽条件のforループを初期化部分で置き換える

このコンポーネントは、Dataflow Analyzerを使用します。

BlockFlattener

このステージでは、内側のブロックの文を外側のブロックの適切な場所に挿入することで、入れ子になったブロックを解消します。 このステージはFunctionGrouperに依存しており、FunctionGrouperによって生成されたフォームを維持するために、一番外側のブロックをフラットにしません。

{
    {
        let x := 2
        {
            let y := 3
            mstore(x, y)
        }
    }
}

は、次の処理に変換されます。

{
    {
        let x := 2
        let y := 3
        mstore(x, y)
    }
}

曖昧さを排除したコードであれば、変数のスコープは大きくなる一方なので、問題はありません。

LoopInvariantCodeMotion

この最適化により、移動可能なSSA変数の宣言はループの外側に移動します。

考慮されるのは、ループの本体またはポストブロック内のトップレベルの文のみです。 つまり、条件分岐内の変数宣言はループの外に移動されません。

要件:

  • Disambiguator、ForLoopInitRewriter、FunctionHoisterは前もって実行する必要があります。

  • より良い結果を得るためには、ExpressionSplitterとSSAトランスフォームを前もって実行する必要があります。

関数レベルの最適化

FunctionSpecializer

このステップでは、関数をリテラルの引数で特殊化します。

例えば function f(a, b) { sstore (a, b) } という関数が、例えば f(x, 5) というリテラルの引数で呼ばれ、 x が識別子である場合、1つの引数しか取らない f_1 という新しい関数を作ることで、特化できます。

function f_1(a_1) {
    let b_1 := 5
    sstore(a_1, b_1)
}

他の最適化ステップでは、関数をより単純化できます。 最適化ステップは、主にインライン化されないような関数に有効です。

前提条件: Disambiguator、FunctionHoister。

LiteralRematerialiserは、正しさのために必要ではないにもかかわらず、前提条件として推奨されています。

UnusedFunctionParameterPruner

このステップでは、関数内の未使用のパラメータを削除します。

cyfunction f(a,b,c) -> x, y { x := div(a,b) } になっているように、パラメータが使われていない場合は、パラメータを削除して、次のように新しい「リンク」関数を作成します。

function f(a,b) -> x { x := div(a,b) }
function f2(a,b,c) -> x, y { x := f(a,b) }

そして、 f へのすべての参照を f2 に置き換えます。 インライナーは、その後に実行して、 f2 へのすべての参照が f に置き換えられていることを確認する必要があります。

前提条件: Disambiguator、FunctionHoister、LiteralRematerialiser。

LiteralRematerialiserというステップは正しさのために必要ではありません。 以下のようなケースに対処するのに役立ちます。 function f(x) -> y { revert(y, y} } はリテラル y がその値 0 に置き換えられるので、関数を書き換えることができます。

UnusedStoreEliminator

冗長な sstore ステートメントやメモリストアステートメントを削除するオプティマイザコンポーネントです。 ストア sstore の場合、(明示的な revert()invalid() 、または無限再帰によって)すべての出力コードパスがリバートするか、オプティマイザが最初のストアを上書きすると判断できる別の sstore につながる場合、ステートメントは削除されます。 しかし、最初の sstore とリバート、または上書きされる sstore の間に読み取り操作がある場合、ステートメントは削除されません。 このような読み取り操作には、外部呼び出し、ストレージにアクセスするユーザー定義関数、最初の sstore が書き込んだスロットと異なることを証明できないスロットの sload が含まれます。

例えば、次のコードは、

{
    let c := calldataload(0)
    sstore(c, 1)
    if c {
        sstore(c, 2)
    }
    sstore(c, 3)
}

Unused Store Eliminatorステップが実行されると、以下のコードに変換されます。

{
    let c := calldataload(0)
    if c { }
    sstore(c, 3)
}

メモリストア操作の場合、一般的には、少なくとも一番外側のYulブロックでは、そのようなステートメントは、どのコードパスでも読み込まれることがなければ、すべて削除されるので単純です。 しかし、関数解析レベルでは、関数のスコープを離れるとメモリロケーションが読み込まれるかどうかわからないので、ステートメントはすべてのコードパスがメモリの上書きにつながる場合にのみ削除されます。

SSA形式で最も効果があります。

前提条件: Disambiguator、ForLoopInitRewriter。

EquivalentFunctionCombiner

2つの関数が構文的に同等で、変数名の変更は可能だが順序変更はできない場合、一方の関数への参照は他方の関数で置き換えられます。

実際に関数を取り除くのは、Unused Prunerが行います。

関数のインライン化

ExpressionInliner

オプティマイザのこのコンポーネントは、関数式の中にあるインライン化できる関数、つまり以下のような関数をインライン化することで、制限付きの関数のインライン化を行います。

  • 単一の値を返す

  • r := <functional expression> のようなボディを持つ

  • 自分自身も r も右辺で参照しない

さらに、すべてのパラメータについて、以下のすべてが真である必要があります。

  • 引数がmovableである

  • パラメータの参照回数が関数ボディ内で2回以下であるか、または引数のコストがかなり低い("コスト"は最大でも1で、0xffまでの定数のようなもの)

例: インライン化される関数は function f(...) -> r { r := E } という形式で、 Er を参照していない式で、関数呼び出しのすべての引数はmovableな式です。

このインライン化の結果は、常に単一の式となります。

このコンポーネントは、固有の名前を持つソースにのみ使用できます。

FullInliner

Full Inlinerでは、特定の関数の特定の呼び出しを関数の本体に置き換えます。 これはコードサイズが大きくなるだけで、ほとんどの場合あまり役に立ちません。 コードは通常非常に高価なものであり、効率の良いコードよりも短いコードの方が良い場合が多いのです。 しかし、いくつかのケースでは、関数のインライン化が後続のオプティマイザのステップにプラスの効果をもたらすことがあります。 例えば、関数の引数の1つが定数の場合です。

インライン化の際には、関数呼び出しをインライン化すべきかどうかを判断するヒューリスティックな手法が用いられます。 現在のヒューリスティックでは、呼び出される関数が小さなものでない限り、「大きな」関数にはインライン化されません。 一度しか使用されない関数はインライン化され、中規模の関数もインライン化されますが、定数の引数を持つ関数呼び出しでは少し大きな関数が使用できます。

将来は、関数をすぐにインライン化するのではなく、関数を特殊化するバックトラックコンポーネントを組み込むことも考えています。 その後、この特殊化された関数に対してオプティマイザを実行します。 その結果、大きな利益が得られた場合は、特化された関数を残し、そうでない場合は元の関数を代わりに使用します。

クリーンアップ

クリーンアップは、オプティマイザの実行の最後に行われます。 分割された式を再び深く入れ子にして結合しようとしたり、変数を極力排除してスタックマシンでの「コンパイル性」を向上させたりします。

ExpressionJoiner

これは、式分割器とは逆の動作です。 正確に1つの参照を持つ変数宣言のシーケンスを複雑な式に変えます。 このステージでは、関数の呼び出しとオペコードの実行の順序が完全に保持されます。 オペコードの可換性に関する情報は利用しません。 変数の値を使用する場所に移動することで、関数呼び出しやオペコードの実行順序が変わる場合は、変換を行いません。

ただし、変数の代入や複数回参照されている変数の代入値は、コンポーネントでは移動しません。

スニペット let x := add(0, 2) let y := mul(x, mload(2)) は変換されません。 オペコード addmload の呼び出し順序が入れ替わってしまうからです。 ただし、 add はmovableなので違いはありません。

このようにオペコードを並び替える場合、変数参照やリテラルは無視されます。 そのため、リテラル 3 の評価後に add のオペコードが実行されるにもかかわらず、スニペット let x := add(0, 2) let y := mul(x, 3)let y := mul(add(0, 2), 3) に変換されてしまいます。

SSAReverser

これは、Common Subexpression EliminatorやUnused Prunerと組み合わせることで、SSAトランスフォームの効果を元に戻すのに役立つ小さな一歩です。

私たちが生成するSSAフォームは、多くのローカル変数を生成するため、コード生成に悪影響を及ぼします。 新しい変数を宣言する代わりに、既存の変数を代入して再利用する方が良いでしょう。

SSAトランスフォームは、

let a := calldataload(0)
mstore(a, 1)

を、次の処理に書き換えます。

let a_1 := calldataload(0)
let a := a_1
mstore(a_1, 1)
let a_2 := calldataload(0x20)
a := a_2

問題は、 a が参照されるたびに、 a の代わりに a_1 という変数が使われることです。 SSAトランスフォームでは、このような形式の文を、宣言と代入を入れ替えるだけで変更します。 上のスニペットは次のように変わります。

let a := calldataload(0)
let a_1 := a
mstore(a_1, 1)
a := calldataload(0x20)
let a_2 := a

これは非常に単純な同値変換ですが、次にCommon Subexpression Eliminatorを実行すると、 a_1 のすべての出現箇所が a に置き換えられます( a が再割り当てされるまで)。 その後、Unused Prunerが変数 a_1 を完全に除去し、SSAトランスフォームを完全に逆にします。

StackCompressor

Ethereum Virtual Machineのコード生成を難しくしている問題の1つは、式スタックを下にたどり着くためのスロットが16個という厳しい制限があることです。 これは多かれ少なかれ、16個のローカル変数に制限があることに通じます。 スタックコンプレッサは、YulのコードをEVMバイトコードにコンパイルします。 スタックの差が大きくなると、この現象がどの関数で起きたかを記録します。

このような問題を起こした関数ごとに、Rematerialiserに特別な依頼をして、値のコスト順にソートされた特定の変数を積極的に排除してもらいます。

失敗した場合は、この手続きを複数回繰り返します。

Rematerialiser

再物質化ステージでは、変数の参照を、その変数に最後に割り当てられた式で置き換えようとします。 これはもちろん、この式が比較的安価に評価できる場合にのみ有益です。 さらに、代入時点と使用時点の間で式の値が変化していない場合にのみ、意味的に等価となります。 このステージの主な利点は、変数を完全に排除することにつながる場合、スタックスロットを節約できることですが(後述)、式が非常に安価な場合、EVM上のDUPオペコードを節約することもできます。

Rematerialiserは、Dataflow Analyzerを使用して、常にmovableな変数の現在の値を追跡します。 値が非常に安い場合や、変数の削除が明示的に要求された場合、変数の参照はその現在の値で置き換えられます。

ForLoopConditionOutOfBody

ForLoopConditionIntoBodyの変換の逆です。

どのようなmovableな c に対しても、

for { ... } 1 { ... } {
if iszero(c) { break }
...
}

を、

for { ... } c { ... } {
...
}

にし、また、

for { ... } 1 { ... } {
if c { break }
...
}

を、

for { ... } iszero(c) { ... } {
...
}

にします。

LiteralRematerialiserは、このステップの前に実行する必要があります。

コントラクトのメタデータ

Solidityコンパイラは、自動的にJSONファイルを生成します。 このJSONファイルには、コンパイルされたコントラクトに関する2種類の情報が含まれています。

  • コントラクトとの対話方法: ABIとNatSpecドキュメント。

  • コンパイルを再現し、デプロイされたコントラクトを検証する方法: コンパイラのバージョン、コンパイラの設定、使用したソースファイル。

コンパイラはデフォルトで、メタデータファイルのIPFSハッシュを、各コントラクトの実行時バイトコード(生成バイトコードだけではない)の最後に追加します。 これにより、公開された場合、中央集権的なデータプロバイダーに頼ることなく、認証された方法でファイルを取得できるようになります。 他に利用可能なオプションとして、Swarmハッシュと、バイトコードにメタデータハッシュを付加しないものがあります。 これらは 標準JSONインターフェース で設定できます。

そのメタデータファイルは、IPFSやSwarmなどのサービスに公開して、他の人がアクセスできるようにする必要があります。 このファイルは solc --metadata コマンドと --output-dir パラメータを使用して作成します。 パラメータを指定しないと、メタデータは標準出力に書き出されます。 メタデータにはソースコードへのIPFSとSwarmの参照が含まれるので、メタデータファイルに加えてすべてのソースファイルをアップロードする必要があります。 IPFSの場合、 ipfs add が返すCIDに含まれるハッシュ(ファイルの直接の sha2-256 ハッシュではない)は、バイトコードに含まれるハッシュと一致しなければなりません。

メタデータファイルの形式は以下の通りです。 以下の例は、人間が読める形で表示されています。 適切にフォーマットされたメタデータは、引用符を正しく使用し、ホワイトスペースを可能な限り無くし、すべてのオブジェクトのキーをアルファベット順にソートして、正規化されたフォーマットにならなければなりません。 コメントは許可されておらず、ここでは説明のためにのみ使用されています。

{
  // 必須: コンパイラの詳細。内容は各言語に固有のもの。
  "compiler": {
    // オプション: この出力を生成したコンパイラのバイナリのハッシュ
    "keccak256": "0x123...",
    // Solidityには必須: コンパイラのバージョン
    "version": "0.8.2+commit.661d1103"
  },
  // 必須:ソースコードの言語。基本的に仕様の「サブバージョン」を選択。
  "language": "Solidity",
  // 必須: コントラクトについて生成される情報
  "output": {
    // 必須: コントラクトのABI定義。「Contract ABI Specification」を参照。
    "abi": [/* ... */],
    // 必須: コントラクトのNatSpec開発者ドキュメント。詳しくは https://docs.soliditylang.org/en/latest/natspec-format.html を参照。
    "devdoc": {
      // コントラクトの @author NatSpecフィールドの内容
      "author": "John Doe",
      // コントラクトの @dev NatSpecフィールドの内容
      "details": "Interface of the ERC20 standard as defined in the EIP. See https://eips.ethereum.org/EIPS/eip-20 for details",
      "errors": {
        "MintToZeroAddress()" : {
          "details": "Cannot mint to zero address"
        }
      },
      "events": {
        "Transfer(address,address,uint256)": {
          "details": "Emitted when `value` tokens are moved from one account (`from`) toanother (`to`).",
          "params": {
            "from": "The sender address",
            "to": "The receiver address",
            "value": "The token amount"
          }
        }
      },
      "kind": "dev",
      "methods": {
        "transfer(address,uint256)": {
          // メソッドの @dev NatSpecフィールドの内容
          "details": "Returns a boolean value indicating whether the operation succeeded. Must be called by the token holder address",
          // メソッドの @param NatSpecフィールドの内容
          "params": {
            "_value": "The amount tokens to be transferred",
            "_to": "The receiver address"
          },
          // メソッドの @return NatSpecフィールドの内容
          "returns": {
            // 存在する場合、var名(ここでは "success")を返す。戻り値が無名の場合、"_0 "をキーとして返す。
            "success": "a boolean value indicating whether the operation succeeded"
          }
        }
      },
      "stateVariables": {
        "owner": {
          // 状態変数の @dev NatSpecフィールドの内容
          "details": "Must be set during contract creation. Can then only be changed by the owner"
        }
      },
      // コントラクトの @title NatSpecフィールドの内容
      "title": "MyERC20: an example ERC20",
      "version": 1 // NatSpecバージョン
    },
    // 必須: コントラクトのNatSpecユーザードキュメント。「NatSpec Format」を参照。
    "userdoc": {
      "errors": {
        "ApprovalCallerNotOwnerNorApproved()": [
          {
            "notice": "The caller must own the token or be an approved operator."
          }
        ]
      },
      "events": {
        "Transfer(address,address,uint256)": {
          "notice": "`_value` tokens have been moved from `from` to `to`"
        }
      },
      "kind": "user",
      "methods": {
        "transfer(address,uint256)": {
          "notice": "Transfers `_value` tokens to address `_to`"
        }
      },
      "version": 1 // NatSpecバージョン
    }
  },
  // 必須: コンパイラの設定。コンパイル時のJSON入力の設定が反映。
  // 標準JSON入力の「settings」フィールドのドキュメントを参照。
  "settings": {
    // Solidityには必須: このメタデータの作成対象となるコントラクトまたはライブラリのファイルパスおよび名前。
    "compilationTarget": {
      "myDirectory/myFile.sol": "MyContract"
    },
    // Solidityには必須
    "evmVersion": "london",
    // Solidityには必須: 使用するライブラリのアドレス
    "libraries": {
      "MyLib": "0x123123..."
    },
    "metadata": {
      // 入力のjsonで使用されている設定を反映、デフォルトは「true」
      "appendCBOR": true,
      // 入力のjsonで使用されている設定を反映、デフォルトは「ipfs」
      "bytecodeHash": "ipfs",
      // 入力のjsonで使用されている設定を反映、デフォルトは「false」
      "useLiteralContent": true
    },
    // オプション: オプティマイザの設定。
    // 「enabled」および「runs」フィールドは非推奨であり、後方互換性のためにのみ与えられています。
    "optimizer": {
      "details": {
        "constantOptimizer": false,
        "cse": false,
        "deduplicate": false,
        // inlinerのデフォルトは「true」
        "inliner": true,
        // jumpdestRemoverのデフォルトは「true」
        "jumpdestRemover": true,
        "orderLiterals": false,
        // peepholeのデフォルトは「true」
        "peephole": true,
        "yul": true,
        // オプション: "yul"が"true"の場合にのみ存在
        "yulDetails": {
          "optimizerSteps": "dhfoDgvulfnTUtnIf...",
          "stackAllocation": false
        }
      },
      "enabled": true,
      "runs": 500
    },
    // Solidityには必須: ソースファイルのインポートのリマッピング。
    "remappings": [ ":g=/dir" ]
  },
  // 必須: コンパイルされたソースファイル/ソースユニット。キーはファイルパス。
  "sources": {
    "destructible": {
      // 必須(「url」が使用されていない場合): ソースファイルのリテラルコンテンツ
      "content": "contract destructible is owned { function destroy() { if (msg.sender == owner) selfdestruct(owner); } }",
      // 必須: ソースファイルのkeccak256ハッシュ
      "keccak256": "0x234..."
    },
    "myDirectory/myFile.sol": {
      // 必須: ソースファイルのkeccak256ハッシュ
      "keccak256": "0x123...",
      // オプション: ソースファイルに与えられるSPDXライセンス識別子
      "license": "MIT",
      // 必須(「content」が使用されていない場合、下記参照): ソースファイルへのソートされたURL。
      // プロトコルはほぼ任意であるが、IPFSのURLを推奨。
      "urls": [ "bzz-raw://7d7a...", "dweb:/ipfs/QmN..." ]
    }
  },
  // 必須: メタデータフォーマットのバージョン
  "version": 1
}

警告

結果として得られるコントラクトのバイトコードには、デフォルトでメタデータのハッシュが含まれているため、メタデータを変更すると、バイトコードも変更される可能性があります。 これにはファイル名やパスの変更も含まれ、メタデータには使用されたすべてのソースのハッシュが含まれているため、たったひとつのホワイトスペースの変更でもメタデータが変わり、バイトコードも異なるものになります。

注釈

上記のABIの定義は、固定された順序はありません。 コンパイラのバージョンによって変わる可能性があります。 しかし、Solidityのバージョン0.5.12からは、この列は一定の順序を保っています。

バイトコードにおけるメタデータハッシュのエンコーディング

現在のデフォルトでは、コンパイラは正規メタデータファイルの IPFS hash (in CID v0) とコンパイラバージョンをバイトコードの最後に追加します。 オプションとして、IPFSの代わりにSwarmハッシュ、または実験的なフラグが使用されます。 以下は、使用可能なすべてのフィールドです:

{
  "ipfs": "<metadata hash>",
  // コンパイラの設定で "bytecodeHash" が "ipfs" ではなく "bzzr1" だった場合
  "bzzr1": "<metadata hash>",
  // 以前のバージョンでは "bzzr1" の代わりに "bzzr0" を使用していた
  "bzzr0": "<metadata hash>",
  // コード生成に影響を与える実験的機能が使用されている場合
  "experimental": true,
  "solc": "<compiler version>"
}

将来、メタデータファイルを取り出す他の方法をサポートするかもしれないので、この情報は CBOR エンコードされて格納されます。 バイトコードの最後の2バイトは、CBORエンコードされた情報の長さを示します。 この長さを見ることで、バイトコードの関連部分をCBORデコーダでデコードできます。

solcのリリースビルドでは、上記のようにバージョンを3バイト(メジャー、マイナー、パッチのバージョン番号を各1バイト)でエンコードしていますが、プレリリースビルドでは、コミットハッシュとビルド日を含む完全なバージョン文字列を使用します。

コマンドラインフラグ --no-cbor-metadata を使用すると、デプロイされたバイトコードの最後に追加されるメタデータをスキップできます。 同様に、Standard JSON入力のブーリアンフィールド settings.metadata.appendCBOR をfalseに設定できます。

注釈

CBORマッピングには他のキーも含まれている可能性があるため、CBORの長さについてはバイトコードの最後を見てデータを完全にデコードし、適切なCBORパーサーを使う方がよいです。 0xa2640xa2 0x64 'i' 'p' 'f' 's' で始まることはは当てになりません。

インターフェースの自動生成とNatSpecの使用方法

メタデータは次のように使用されます: コントラクトと対話したいコンポーネント(例えばウォレット)は、コントラクトのコードを取得します。 その際、メタデータファイルのIPFS/Swarmハッシュを含むCBORエンコードされたセクションをデコードします。 そのハッシュで、メタデータファイルを取得します。 そのファイルはJSONデコードされ、上記のような構造になります。

このコンポーネントは、ABIを使ってコントラクトの初歩的なユーザーインターフェースを自動的に生成できます。

さらに、ウォレットはNatSpecユーザードキュメントを使用して、ユーザーがコントラクトと対話する際には必ずヒューマンリーダブルな確認メッセージを表示し、併せてトランザクション署名の承認を要求できます。

詳しくは、 Ethereum Natural Language Specification (NatSpec) フォーマット を参照してください。

ソースコード検証の方法

ピン/パブリッシュされた場合、コントラクトのメタデータをIPFS/Swarmから取得することが可能です。 メタデータファイルには、ソースファイルのURLやIPFSハッシュ、コンパイル設定、つまりコンパイルを再現するために必要なすべての情報が含まれています。

この情報があれば、コンパイルを再現し、コンパイルのバイトコードとデプロイされたコントラクトのバイトコードを比較することで、コントラクトのソースコードを検証できます。

メタデータのハッシュはバイトコードの一部であり、ソースコードのハッシュもメタデータの一部であるため、メタデータも自動的に検証されます。 ファイルや設定に変更があれば、メタデータのハッシュは異なるものになります。 ここでのメタデータはコンパイル全体のフィンガープリントとして機能します。

Sourcify はこの機能を「完全/完璧な検証」のために利用するだけでなく、メタデータのハッシュでアクセスできるようにファイルをIPFSに公開することもできます。

コントラクトABIの仕様

基本設計

コントラクトのApplication Binary Interface (ABI)は、Ethereumエコシステム内のコントラクトと対話するためのスタンダードな方法であり、ブロックチェーンの外側からも、コントラクト間の対話のためにも使用されます。 データは、この仕様に記載されているように、その型に応じてエンコードされます。 エンコーディングは自己記述的ではないため、デコードするためにはスキーマが必要です。

コントラクトのインタフェース関数が強く型付けされ、それがコンパイル時に知られており、かつ静的であるとしています。 また、すべてのコントラクトは、それらが呼び出すコントラクトのインタフェース定義をコンパイル時に利用可能であるとしています。

この仕様では、インターフェースが動的である、あるいは、実行時にしかわからないコントラクトは扱いません。

関数セレクタ

関数呼び出しのコールデータの最初の4バイトは、呼び出される関数を指定します。 これは、関数のシグネチャのKeccak-256ハッシュの最初(左、ビッグエンディアンの高次)の4バイトです。 シグネチャは、データロケーション指定子のない基本のプロトタイプの正規表現として定義されています。 つまり、関数名と括弧で囲まれたパラメータ型のリストです。 パラメータ型はコンマで分割され、スペースは使用されません。

注釈

関数の戻り値の型は、シグネチャには含まれません。 Solidityの関数オーバーロード では戻り値の型は考慮されません。 その理由は、関数呼び出しの解決をコンテキストに依存しないようにするためです。 しかし、 ABIのJSONの内容 には入力と出力の両方が含まれます。

引数のエンコーディング

5バイト目からは、エンコードされた引数が続きます。 このエンコーディングは他の場所でも使用されています。 例えば、戻り値やイベントの引数も同じようにエンコーディングされます。 ただし、関数を指定する4バイトのセレクタはありません。

次のような基本型があります。

  • uint<M>: M ビット、 0 < M <= 256M % 8 == 0 の符号なし整数型。 例えば uint32uint8uint256

  • int<M>: M ビット、 0 < M <= 256M % 8 == 0 の2の補数の符号付き整数型。

  • address: 仮定される解釈と言語の型付けを除き、 uint160 と同等。 関数セレクタの計算には address を使用。

  • uint, int: それぞれ uint256int256 の同義語。 関数セレクタの計算には uint256int256 を使用。

  • bool: 0と1に限定された uint8 と同等。 関数セレクタの計算には bool を使用。

  • fixed<M>x<N>: M ビット、 8 <= M <= 256M % 8 == 00 < N <= 80 の符号付き固定小数点10進数で、 v の値を v / (10 ** N) と表記。

  • ufixed<M>x<N>: fixed<M>x<N> の符号なしのバリアント。

  • fixed, ufixed: それぞれ fixed128x18ufixed128x18 の同義語。 関数セレクタの計算には、 fixed128x18ufixed128x18 を使用。

  • bytes<M>: M バイトのバイナリ型、 0 < M <= 32

  • function: アドレス(20バイト)の後に関数セレクタ(4バイト)が続く。 bytes24 と同じようにエンコード。

次のような(固定サイズの)配列型が存在します。

  • <type>[M]: 与えられた型の M 個の要素の固定長の配列。 M >= 0

    注釈

    このABI仕様では、要素数が0の固定長の配列を表現できますが、コンパイラではサポートされていません。

次のような非固定サイズの型が存在します。

  • bytes: 動的サイズのバイトシーケンス。

  • string: UTF-8でエンコードされていると仮定した動的サイズのユニコード文字列。

  • <type>[]: 指定された型の要素を持つ可変長の配列。

型は、カンマで区切って括弧で囲むことでタプルにまとめることができます。

  • (T1,T2,...,Tn): T1 , ..., Tn の各型からなるタプル。 n >= 0

タプルのタプル、タプルの配列などを作ることが可能です。 また、ゼロタプルを作ることも可能です( n == 0 )。

Solidityの型からABIの型へのマッピング

Solidityでは、タプルを除いて、上記で紹介したすべての型を同じ名前でサポートしています。 一方で、Solidityの型の中には、ABIではサポートされていないものもあります。 次の表は、左の列にABIがサポートしていないSolidityの型を、右の列にそれに対応するABIの型を示しています。

Solidity

ABI

address payable

address

contract

address

enum

uint8

user defined value types

元となる値型

struct

tuple

警告

バージョン 0.8.0 以前のenumは256個以上のメンバーを持つことができ、任意のメンバーの値を保持するのに十分な大きさの最小の整数型で表されていました。

エンコーディングの設計基準

このエンコーディングは、以下のような特性を持つように設計されており、いくつかの引数が入れ子になった配列である場合には、特に便利です。

  1. 値にアクセスするために必要な読み取り回数は、最大でも引数配列構造内の値の深さ分であり、すなわち a_i[k][l][r] を取得するためには4回の読み取りが必要です。 以前のバージョンのABIでは、最悪の場合、読み取り回数は動的パラメータの総数に比例していました。

  2. 変数や配列要素のデータは、他のデータとインターリーブされておらず、相対的な「アドレス」のみを使用する、リロケータブルなものです。

エンコーディングの形式的な仕様

静的な型と動的な型を区別します。 静的型はインプレースでエンコードされ、動的型は現在のブロックの後に別個に割り当てられた場所でエンコードされます。

定義: 次のような型を「動的」と呼びます。

  • bytes

  • string

  • 任意の T に対して T[]

  • 任意の動的 T と任意の k >= 0 に対する T[k]

  • ある 1 <= i <= k に対して Ti が動的である場合の (T1,...,Tk)

それ以外の型は「静的」と呼ばれます。

定義: len(a) は、2進数の文字列 a のバイト数です。 len(a) の型は uint256 とします。

実際のエンコーディングである enc は、ABI型の値をバイナリ文字列にマッピングしたものと定義し、 X の型が動的である場合に限り、 len(enc(X))X の値に依存するようにします。

定義: 任意のABI値 X に対して、 X の型に応じて enc(X) を再帰的に定義します。

  • k >= 0 と任意の型 T1 , ..., Tk に対する (T1,...,Tk)

    enc(X) = head(X(1)) ... head(X(k)) tail(X(1)) ... tail(X(k))

    ここで、 X = (X(1), ..., X(k))headtail は、 Ti に対して次のように定義されます。

    Ti が静的である場合:

    head(X(i)) = enc(X(i))tail(X(i)) = "" (空の文字列)

    それ以外の場合、つまり Ti が動的な場合:

    head(X(i)) = enc(len( head(X(1)) ... head(X(k)) tail(X(1)) ... tail(X(i-1)) )) tail(X(i)) = enc(X(i))

    なお、動的なケースでは、head部分の長さは型にのみ依存し、値には依存しないため、 head(X(i)) はwell-definedです。 head(X(i)) の値は、 enc(X) の開始位置に対する tail(X(i)) の開始位置のオフセットです。

  • 任意の Tk に対する T[k]:

    enc(X) = enc((X[0], ..., X[k-1]))

    つまり、同じ型の k 要素を持つタプルであるかのようにエンコードされます。

  • Xk の要素を持つ T[]kuint256 型とします)。

    enc(X) = enc(k) enc((X[0], ..., X[k-1]))

    つまり、同じ型の k 個の要素を持つタプル(静的サイズ k の配列)であるかのようにエンコードされ、その前に要素数が付きます。

  • 長さ kbytes (これは型 uint256 であると仮定されます)。

    enc(X) = enc(k) pad_right(X) 、すなわち、バイト数は uint256 に続いて X の実際の値をバイト列として符号化し、その後に len(enc(X)) が32の倍数になるような最小数のゼロバイトが続きます。

  • string:

    enc(X) = enc(enc_utf8(X)) 、つまり X はUTF-8でエンコードされ、この値は bytes 型と解釈され、さらにエンコードされます。 なお、この後のエンコードで使用する長さは、文字数ではなく、UTF-8でエンコードされた文字列のバイト数です。

  • uint<M>: enc(X) は、 X のビッグエンディアンのエンコーディングで、高次(左)側に0バイトをパディングし、長さが32バイトになるようにしたものです。

  • address: uint160 と同様です。

  • int<M>: enc(X)X のビッグエンディアンの2の補数で、高次(左)側に負の X には 0xff バイト、非負の X には0バイトをパディングし、長さが32バイトになるようにしたものです。

  • bool: uint8 と同様に、 true には 1false には 0 が使われます。

  • fixed<M>x<N>: enc(X)enc(X * 10**N)X * 10**Nint256 と解釈されます。

  • fixed: fixed128x18 と同様です。

  • ufixed<M>x<N>: enc(X)enc(X * 10**N)X * 10**Nuint256 と解釈されます。

  • ufixed: ufixed128x18 と同様です。

  • bytes<M>: enc(X) は、 X のバイト列に末尾にゼロバイトを追加して32バイトにしたものです。

なお、任意の X に対して、 len(enc(X)) は32の倍数です。

関数セレクタと引数エンコーディング

つまり、パラメータ a_1, ..., a_n を持つ関数 f の呼び出しは、次のようにエンコードされます。

function_selector(f) enc((a_1, ..., a_n))

となり、 f の戻り値 v_1, ..., v_k は次のようにエンコードされます。

enc((v_1, ..., v_k))

つまり、値がタプルにまとめられ、エンコードされます。

次のコントラクトを考える:

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

contract Foo {
    function bar(bytes3[2] memory) public pure {}
    function baz(uint32 x, bool y) public pure returns (bool r) { r = x > 32 || y; }
    function sam(bytes memory, bool, uint[] memory) public pure {}
}

Foo の例では、 69true というパラメータで baz を呼び出す場合、合計68バイトを渡すことになり、その内訳は以下の通りです。

  • 0xcdcd77c0: メソッドID。シグネチャ baz(uint32,bool) のASCII形式のKeccakハッシュの最初の4バイトです。

  • 0x0000000000000000000000000000000000000000000000000000000000000045: 第1パラメータ。 32バイトにパディングされたuint32の値 69

  • 0x0000000000000000000000000000000000000000000000000000000000000001: 第2パラメータ。 32バイトにパディングされたboolの値 true

合わせると、

0xcdcd77c000000000000000000000000000000000000000000000000000000000000000450000000000000000000000000000000000000000000000000000000000000001

この関数は単一の bool を返します。 例えば、 false を返すとしたら、その出力は単一のバイト列 0x0000000000000000000000000000000000000000000000000000000000000000 であり、これは単一のboolです。

bar["abc", "def"] の引数で呼び出す場合、合計68バイトを渡すことになり、その内訳は以下の通りです。

  • 0xfce353f6: メソッドID。シグネチャ bar(bytes3[2]) から得られます。

  • 0x6162630000000000000000000000000000000000000000000000000000000000: 第1パラメータの最初の部分で、 bytes3 の値 "abc" (左寄せ)。

  • 0x6465660000000000000000000000000000000000000000000000000000000000: 第1パラメータの2番目の部分で、 bytes3 の値 "def" (左寄せ)。

合わせると、

0xfce353f661626300000000000000000000000000000000000000000000000000000000006465660000000000000000000000000000000000000000000000000000000000

引数 "dave"true[1,2,3]sam を呼び出したい場合、合計292バイトを渡すことになり、その内訳は以下の通りです。

  • 0xa5643bf2: メソッドID。シグネチャ sam(bytes,bool,uint256[]) から得られます。 uint はその正規の表現である uint256 に置き換えられていることに注意してください。

  • 0x0000000000000000000000000000000000000000000000000000000000000060: 第1引数(動的型)のデータ部の位置で、引数ブロックの先頭からのバイト数で表します。この場合は 0x60

  • 0x0000000000000000000000000000000000000000000000000000000000000001: 第2引数: boolでtrue。

  • 0x00000000000000000000000000000000000000000000000000000000000000a0: 第3引数(動的型)のデータ部の位置で、単位はバイトです。この場合は 0xa0

  • 0x0000000000000000000000000000000000000000000000000000000000000004: 第1引数のデータ部で、バイト配列の要素数から始まります。この場合は4。

  • 0x6461766500000000000000000000000000000000000000000000000000000000: 第1引数の内容: "dave" のUTF-8(ここではASCIIと同等)エンコードで右をパディングして32バイトにしたもの。

  • 0x0000000000000000000000000000000000000000000000000000000000000003: 第3引数のデータ部で、配列の要素数から始まります。この場合は3。

  • 0x0000000000000000000000000000000000000000000000000000000000000001: 第3引数の最初のエントリ。

  • 0x0000000000000000000000000000000000000000000000000000000000000002: 第3引数の2番目のエントリ。

  • 0x0000000000000000000000000000000000000000000000000000000000000003: 第3引数の3番目のエントリ。

合わせると、

0xa5643bf20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000464617665000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003

動的型の使用法

シグネチャが f(uint256,uint32[],bytes10,bytes) で値が (0x123, [0x456, 0x789], "1234567890", "Hello, world!") である関数呼び出しは、以下のようにエンコードされます。

まず keccak("f(uint256,uint32[],bytes10,bytes)") の最初の4バイト、つまり 0x8be65246 を取ります。 そして、4つの引数すべてのヘッド部分をエンコードします。 静的型の uint256bytes10 については、これらが直接渡したい値となりますが、動的型の uint32[]bytes については、値のエンコードの開始(つまり、関数シグネチャのハッシュを含む最初の4バイトを数えない)から測定した、データ領域の開始までのオフセットをバイト単位で使用します。

  • 0x0000000000000000000000000000000000000000000000000000000000000123 (32バイトにパディングした 0x123

  • 0x0000000000000000000000000000000000000000000000000000000000000080 (第2パラメータのデータ部の開始位置へのオフセット。4*32バイトでちょうどヘッド部のサイズ)

  • 0x3132333435363738393000000000000000000000000000000000000000000000 (32バイトに右をパディングした "1234567890"

  • 0x00000000000000000000000000000000000000000000000000000000000000e0 (第4パラメータのデータ部の先頭へのオフセット = 第1動的パラメータのデータ部の先頭へのオフセット + 第1動的パラメータのデータ部のサイズ = 4 * 32 + 3 * 32(後述))。

これに続いて、最初の動的引数のデータ部、 [0x456, 0x789] は次のようになります。

  • 0x0000000000000000000000000000000000000000000000000000000000000002 (配列の要素数が2)

  • 0x0000000000000000000000000000000000000000000000000000000000000456 (最初の要素)

  • 0x0000000000000000000000000000000000000000000000000000000000000789 (2番目の要素)

最後に、2番目の動的引数である "Hello, world!" のデータ部をエンコードします。

  • 0x000000000000000000000000000000000000000000000000000000000000000d (要素数(ここではバイト): 13)

  • 0x48656c6c6f2c20776f726c642100000000000000000000000000000000000000"Hello, world!" は32バイトで右をパディング)

すべてを合わせると、エンコーディングは次のようになります(わかりやすくするために、関数セレクタと各32バイトの後に改行しています)。

0x8be65246
  0000000000000000000000000000000000000000000000000000000000000123
  0000000000000000000000000000000000000000000000000000000000000080
  3132333435363738393000000000000000000000000000000000000000000000
  00000000000000000000000000000000000000000000000000000000000000e0
  0000000000000000000000000000000000000000000000000000000000000002
  0000000000000000000000000000000000000000000000000000000000000456
  0000000000000000000000000000000000000000000000000000000000000789
  000000000000000000000000000000000000000000000000000000000000000d
  48656c6c6f2c20776f726c642100000000000000000000000000000000000000

同じ原理で、シグネチャ g(uint256[][],string[]) を持つ関数のデータを値 ([[1, 2], [3]], ["one", "two", "three"]) でエンコードしてみましょう。 ただし、エンコードの最も基本的な部分から始めます。

まず最初に、第1のルート配列 [[1, 2], [3]] の第1の埋め込み動的配列 [1, 2] の長さとデータをエンコードします。

  • 0x0000000000000000000000000000000000000000000000000000000000000002 (1番目の配列の要素数は2で、要素は 12

  • 0x0000000000000000000000000000000000000000000000000000000000000001 (最初の要素)

  • 0x0000000000000000000000000000000000000000000000000000000000000002 (2番目の要素)

そして、第1のルート配列 [[1, 2], [3]] の第2の埋め込み動的配列 [3] の長さとデータを符号化します。

  • 0x0000000000000000000000000000000000000000000000000000000000000001 (2番目の配列の要素数は1で、要素は 3

  • 0x0000000000000000000000000000000000000000000000000000000000000003 (最初の要素)

次に、それぞれの動的配列 [1, 2][3] に対するオフセット ab を求める必要があります。 このオフセットを計算するために、最初のルート配列 [[1, 2], [3]] のエンコードデータを見て、エンコードの各行を列挙します。

0 - a                                                                - offset of [1, 2]
1 - b                                                                - offset of [3]
2 - 0000000000000000000000000000000000000000000000000000000000000002 - count for [1, 2]
3 - 0000000000000000000000000000000000000000000000000000000000000001 - encoding of 1
4 - 0000000000000000000000000000000000000000000000000000000000000002 - encoding of 2
5 - 0000000000000000000000000000000000000000000000000000000000000001 - count for [3]
6 - 0000000000000000000000000000000000000000000000000000000000000003 - encoding of 3

オフセット a は、2行目(64バイト)の配列 [1, 2] の内容の先頭を指しているので、 a = 0x0000000000000000000000000000000000000000000000000000000000000040 となります。

オフセット b は、配列 [3] の内容の先頭である5行目(160バイト)を指しているので、 b = 0x00000000000000000000000000000000000000000000000000000000000000a0 となります。

次に、2番目のルート配列の埋め込み文字列をエンコードします。

  • 0x0000000000000000000000000000000000000000000000000000000000000003 (単語 "one" の文字数)

  • 0x6f6e650000000000000000000000000000000000000000000000000000000000 (単語 "one" のutf8表現)

  • 0x0000000000000000000000000000000000000000000000000000000000000003 (単語 "two" の文字数)

  • 0x74776f0000000000000000000000000000000000000000000000000000000000 (単語 "two" のutf8表現)

  • 0x0000000000000000000000000000000000000000000000000000000000000005 (単語 "three" の文字数)

  • 0x7468726565000000000000000000000000000000000000000000000000000000 (単語 "three" のutf8表現)

最初のルート配列と並行して、文字列は動的な要素なので、そのオフセット cde を見つける必要があります。

0 - c                                                                - offset for "one"
1 - d                                                                - offset for "two"
2 - e                                                                - offset for "three"
3 - 0000000000000000000000000000000000000000000000000000000000000003 - count for "one"
4 - 6f6e650000000000000000000000000000000000000000000000000000000000 - encoding of "one"
5 - 0000000000000000000000000000000000000000000000000000000000000003 - count for "two"
6 - 74776f0000000000000000000000000000000000000000000000000000000000 - encoding of "two"
7 - 0000000000000000000000000000000000000000000000000000000000000005 - count for "three"
8 - 7468726565000000000000000000000000000000000000000000000000000000 - encoding of "three"

オフセット c は、3行目(96バイト)である文字列 "one" の内容の開始点を指しているので、 c = 0x0000000000000000000000000000000000000000000000000000000000000060 となります。

オフセット d は、5行目(160バイト)の文字列 "two" の内容の始まりを指しているので、 d = 0x00000000000000000000000000000000000000000000000000000000000000a0 となります。

オフセット e は、7行目(224バイト)である文字列 "three" のコンテンツの開始を指しているので、 e = 0x00000000000000000000000000000000000000000000000000000000000000e0

なお、ルート配列の埋め込み要素の符号化は互いに依存しておらず、シグネチャ g(string[],uint256[][]) を持つ関数では同じ符号化になります。

そして、最初のルート配列の長さをエンコードします。

  • 0x0000000000000000000000000000000000000000000000000000000000000002 (最初のルート配列の要素数2、要素自体は [1, 2][3] )

そして、2番目のルートの配列の長さをエンコードします。

  • 0x0000000000000000000000000000000000000000000000000000000000000003 (2番目のルート配列に含まれる文字列の数3、文字列自体は "one""two""three"

最後に、それぞれのルート動的配列 [[1, 2], [3]]["one", "two", "three"] のオフセット fg を見つけ、正しい順序でパーツを組み立てます。

0x2289b18c                                                            - function signature
 0 - f                                                                - offset of [[1, 2], [3]]
 1 - g                                                                - offset of ["one", "two", "three"]
 2 - 0000000000000000000000000000000000000000000000000000000000000002 - count for [[1, 2], [3]]
 3 - 0000000000000000000000000000000000000000000000000000000000000040 - offset of [1, 2]
 4 - 00000000000000000000000000000000000000000000000000000000000000a0 - offset of [3]
 5 - 0000000000000000000000000000000000000000000000000000000000000002 - count for [1, 2]
 6 - 0000000000000000000000000000000000000000000000000000000000000001 - encoding of 1
 7 - 0000000000000000000000000000000000000000000000000000000000000002 - encoding of 2
 8 - 0000000000000000000000000000000000000000000000000000000000000001 - count for [3]
 9 - 0000000000000000000000000000000000000000000000000000000000000003 - encoding of 3
10 - 0000000000000000000000000000000000000000000000000000000000000003 - count for ["one", "two", "three"]
11 - 0000000000000000000000000000000000000000000000000000000000000060 - offset for "one"
12 - 00000000000000000000000000000000000000000000000000000000000000a0 - offset for "two"
13 - 00000000000000000000000000000000000000000000000000000000000000e0 - offset for "three"
14 - 0000000000000000000000000000000000000000000000000000000000000003 - count for "one"
15 - 6f6e650000000000000000000000000000000000000000000000000000000000 - encoding of "one"
16 - 0000000000000000000000000000000000000000000000000000000000000003 - count for "two"
17 - 74776f0000000000000000000000000000000000000000000000000000000000 - encoding of "two"
18 - 0000000000000000000000000000000000000000000000000000000000000005 - count for "three"
19 - 7468726565000000000000000000000000000000000000000000000000000000 - encoding of "three"

オフセット f は、2行目(64バイト)の配列 [[1, 2], [3]] の内容の先頭を指しているので、 f = 0x0000000000000000000000000000000000000000000000000000000000000040 となります。

オフセット g は、配列 ["one", "two", "three"] の内容の先頭である10行目(320バイト)を指しているので、 g = 0x0000000000000000000000000000000000000000000000000000000000000140 となります。

イベント

イベントは、Ethereumのログ/イベントウォッチングプロトコルを抽象化したものです。 ログエントリは、コントラクトのアドレス、最大4つのトピックのシリーズ、任意の長さのバイナリデータを提供します。 イベントは、既存の関数ABIを活用して、これを(インターフェース仕様とともに)適切に型付けされた構造として解釈します。

イベント名と一連のイベントパラメータが与えられると、それらを2つのサブシリーズに分割します。 インデックスが付けられているものは、最大で3つ(非匿名イベントの場合)または4つ(匿名イベントの場合)あり、イベント署名のKeccakハッシュと一緒にログエントリのトピックを形成するために使用されます。 インデックスが付けられていないものは、イベントのバイト配列を形成します。

事実上、このABIを使ったログエントリは次のように記述されます。

  • address: コントラクトのアドレス(Ethereumが本質的に提供するもの)。

  • topics[0]: keccak(EVENT_NAME+"("+EVENT_ARGS.map(canonical_type_of).join(",")+")")canonical_type_of は、与えられた引数の正規の型を単純に返す関数であり、例えば uint indexed foo の場合は uint256 を返す)。 この値は、イベントが anonymous として宣言されていない場合、 topics[0] にのみ存在します。

  • topics[n]: abi_encode(EVENT_INDEXED_ARGS[n]) - 1]) イベントが anonymous として宣言されていない場合は abi_encode(EVENT_INDEXED_ARGS[n])、宣言されている場合は abi_encode(EVENT_INDEXED_ARGS[n]) となります( EVENT_INDEXED_ARGS はインデックス化された EVENT_ARGS のシリーズです)。

  • data: EVENT_NON_INDEXED_ARGS のABIエンコーディング( EVENT_NON_INDEXED_ARGS はインデックスが付いていない一連の EVENT_ARGSabi_encode は上述のように関数から型付けされた一連の値を返すために使用されるABIエンコーディング関数)。

長さが最大32バイトのすべての型について、 EVENT_INDEXED_ARGS 配列には、通常のABIエンコーディングと同様に、32バイトにパディングまたは符号拡張された値が直接格納されます。 しかし、すべての "複雑な"型や動的な長さの型(すべての配列、 stringbytes 、構造体を含む)では、 EVENT_INDEXED_ARGS には直接エンコードされた値ではなく、特別なインプレースエンコードされた値の Keccakハッシュインデックスされたイベントパラメータのエンコーディング 参照)が格納されます。 これにより、アプリケーションは(エンコードされた値のハッシュをトピックとして設定することで)動的長型の値を効率的に問い合わせることができますが、アプリケーションは問い合わせていないインデックス化された値をデコードできなくなります。 動的長型の場合、アプリケーション開発者は、(引数がインデックス化されている場合の)所定の値の高速検索と(引数がインデックス化されていないことが必要な)任意の値の可読性との間でトレードオフの関係に直面します。 開発者はこのトレードオフを克服し、効率的な検索と任意の可読性の両方を達成するために、同じ値を保持することを意図した2つの引数(1つはインデックス化され、1つはインデックス化されない)を持つイベントを定義できます。

エラー

コントラクト内部で障害が発生した場合、コントラクトは特別なオペコードを使用して実行を中止し、すべての状態変化をリバートできます。 これらの効果に加えて、記述的データを呼び出し元に返すことができます。 この記述データは、関数呼び出しのデータと同じように、エラーとその引数をエンコードしたものです。

例として、 transfer 関数が常に「残高不足」というカスタムエラーでリバートしてしまう次のコントラクトを考えてみましょう。

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

contract TestToken {
    error InsufficientBalance(uint256 available, uint256 required);
    function transfer(address /*to*/, uint amount) public pure {
        revert InsufficientBalance(0, amount);
    }
}

戻りデータは、関数 InsufficientBalance(uint256,uint256) に対する関数呼び出し InsufficientBalance(0, amount) と同じ方法でエンコードされます。

エラーセレクタ 0x000000000xffffffff は将来のために予約されています。

警告

エラーデータを信用してはいけません。 デフォルトでは、エラーデータは外部呼び出しの連鎖を通じてバブリングします。 つまり、コントラクトは、直接呼び出すコントラクトのどれにも定義されていないエラーを受け取る可能性があります。 さらに、どのコントラクトも、エラーがどこにも定義されていなくても、エラー署名に一致するデータを返すことで、どんなエラーでも偽装できます。

JSON

コントラクトのインターフェースのJSONフォーマットは、関数、イベント、エラーの記述の配列で与えられます。 関数の記述は、フィールドを持つJSONオブジェクトです。

コンストラクタ、receive関数、fallback関数は nameoutputs を持ちません。 receive関数とfallback関数には inputs もありません。

注釈

non-payableな関数にEtherを送ると、トランザクションがrevertします。

注釈

ステートミュータビリティ nonpayable は、Solidityではステートミュータビリティモディファイアを指定しないことで設定されます。

イベントの記述は、同様のフィールドを持つJSONオブジェクトです。

  • type: 常に "event"

  • name: イベントの名前。

  • inputs: オブジェクトの配列で、それぞれのオブジェクトは次のものを含みます。

    • name: パラメータの名前。

    • type: パラメータの正規の型(詳細は後述)。

    • components: タプル型に使用(詳細は後述)。

    • indexed: フィールドがログのトピックの一部である場合は true 、ログのデータセグメントの一部である場合は false

  • anonymous: イベントが anonymous と宣言された場合は true

エラーの記述は以下の通りです。

  • type: 常に "error"

  • name: エラーの名前。

  • inputs: オブジェクトの配列で、それぞれのオブジェクトは次のものを含みます。

    • name: パラメータの名前。

    • type: パラメータの正規の型(詳細は後述)。

    • components: タプル型に使用(詳細は後述)。

注釈

スマートコントラクト内の異なるファイルからエラーが発生した場合や、別のスマートコントラクトから参照されている場合など、JSON配列内に同じ名前や同一の署名を持つ複数のエラーが存在する可能性があります。 ABIでは、エラー自体の名前だけが重要で、どこで定義されているかは関係ありません。

例えば、

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

contract Test {
    constructor() { b = hex"12345678901234567890123456789012"; }
    event Event(uint indexed a, bytes32 b);
    event Event2(uint indexed a, bytes32 b);
    error InsufficientBalance(uint256 available, uint256 required);
    function foo(uint a) public { emit Event(a, b); }
    bytes32 b;
}

は、次のJSONになります。

[{
"type":"error",
"inputs": [{"name":"available","type":"uint256"},{"name":"required","type":"uint256"}],
"name":"InsufficientBalance"
}, {
"type":"event",
"inputs": [{"name":"a","type":"uint256","indexed":true},{"name":"b","type":"bytes32","indexed":false}],
"name":"Event"
}, {
"type":"event",
"inputs": [{"name":"a","type":"uint256","indexed":true},{"name":"b","type":"bytes32","indexed":false}],
"name":"Event2"
}, {
"type":"function",
"inputs": [{"name":"a","type":"uint256"}],
"name":"foo",
"outputs": []
}]

タプル型のハンドリング

名前は意図的にABIエンコーディングの一部ではありませんが、エンドユーザーに表示するためにJSONに含めることには大きな意味があります。 構造は以下のように入れ子になっています。

メンバー nametype 、そして潜在的に components を持つオブジェクトは、型付けされた変数を記述します。 タプル型に到達するまでは正規の型が決定され、その時点までの文字列記述は tuple という単語を前置した type に格納されます。 つまり、 tuple の後に整数 k を持つ [][k] のシーケンスが続くことになります。 その後、タプルの構成要素はメンバー components に格納されます。 components は配列型で、そこに indexed が許されないことを除いて、トップレベルのオブジェクトと同じ構造を持っています。

一例として、次のコード

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.5 <0.9.0;
pragma abicoder v2;

contract Test {
    struct S { uint a; uint[] b; T[] c; }
    struct T { uint x; uint y; }
    function f(S memory, T memory, uint) public pure {}
    function g() public pure returns (S memory, T memory, uint) {}
}

は、次のJSONになります。

[
  {
    "name": "f",
    "type": "function",
    "inputs": [
      {
        "name": "s",
        "type": "tuple",
        "components": [
          {
            "name": "a",
            "type": "uint256"
          },
          {
            "name": "b",
            "type": "uint256[]"
          },
          {
            "name": "c",
            "type": "tuple[]",
            "components": [
              {
                "name": "x",
                "type": "uint256"
              },
              {
                "name": "y",
                "type": "uint256"
              }
            ]
          }
        ]
      },
      {
        "name": "t",
        "type": "tuple",
        "components": [
          {
            "name": "x",
            "type": "uint256"
          },
          {
            "name": "y",
            "type": "uint256"
          }
        ]
      },
      {
        "name": "a",
        "type": "uint256"
      }
    ],
    "outputs": []
  }
]

厳密なエンコーディングモード

厳密なエンコーディングモードとは、上記の正式な仕様で定義されているのと全く同じエンコーディングになるモードです。 つまり、データ領域にオーバーラップを生じさせないようにしながら、オフセットはできるだけ小さくしなければならず、したがってギャップは許されません。

通常、ABIデコーダはオフセットポインタに従うだけの素直な方法で書かれていますが、デコーダによってはストリクトモードを強制する場合があります。 SolidityのABIデコーダは、現在のところストリクトモードを強制していませんが、エンコーダは常にストリクトモードでデータを作成します。

非標準のパックモード

Solidityは、 abi.encodePacked() を通して、非標準のパックモードをサポートしています。

  • 32バイトより短い型は、パディングや符号拡張なしに、直接連結されます。

  • 動的型は、インプレースで長さの情報無しにエンコードされます。

  • 配列の要素はパディングされますが、インプレースでエンコードされます。

また、構造体や入れ子になった配列はサポートされていません。

例として、 int16(-1), bytes1(0x42), uint16(0x03), string("Hello, world!") をエンコードすると次のようになります。

0xffff42000348656c6c6f2c20776f726c6421
  ^^^^                                 int16(-1)
      ^^                               bytes1(0x42)
        ^^^^                           uint16(0x03)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^ string("Hello, world!") lengthフィールド無し

より具体的には、

  • エンコードの際、すべてがインプレースでエンコードされます。 つまり、ABIのエンコーディングのように、先頭と末尾の区別がなく、配列の長さもエンコードされません。

  • abi.encodePacked の直接引数は、配列(または stringbytes )でない限り、パディングなしでエンコードされます。

  • 配列のエンコーディングは、その要素のエンコーディングとパディングを連結したものです。

  • stringbytesuint[] のような動的なサイズの型は、長さフィールドなしでエンコードされます。

  • stringbytes のエンコーディングでは、配列や構造体の一部でない限り、末尾にパディングが適用されません(その場合、32バイトの倍数にパディングされます)。

一般的には、動的なサイズの要素が2つあると、長さのフィールドがないため、エンコーディングが曖昧になります。

パディングが必要な場合は、明示的な型変換を行うことができます: abi.encodePacked(uint16(0x12)) == hex"0012"

関数を呼び出すときにはpackedエンコーディングは使われないので、関数セレクタの前に付ける特別なサポートはありません。 また、エンコーディングが曖昧なため、デコード関数はありません。

警告

keccak256(abi.encodePacked(a, b)) を使っていて、 ab の両方が動的型の場合、 a の一部を b に移動させたり、逆に b の一部を a に移動させたりすることで、ハッシュ値の衝突を容易に工作できます。 より具体的には abi.encodePacked("a", "bc") == abi.encodePacked("ab", "c") です。 署名や認証、データの整合性のために abi.encodePacked を使用する場合は、常に同じ型を使用し、最大でもどちらかが動的型であることを確認してください。 やむを得ない理由がない限り、 abi.encode を優先すべきです。

インデックスされたイベントパラメータのエンコーディング

値型ではないインデックスされたイベントパラメータ(配列や構造体)は、直接保存されず、エンコーディングのkeccak256ハッシュが保存されます。 このエンコーディングは以下のように定義されています。

  • bytesstring の値のエンコーディングは、パディングや長さのプレフィックスを含まない文字列の内容だけになります。

  • 構造体のエンコーディングは、そのメンバーのエンコーディングを32バイトの倍数にパディングして連結したものです( bytesstring も同様)。

  • 配列のエンコーディング(動的サイズと静的サイズどちらも)は、その要素のエンコーディングを32バイトの倍数にパディングして連結したもので( bytesstring も)、長さのプレフィックスはありません。

上記では、通常と同じく負の数は符号拡張でパディングされ、ゼロパディングされません。 bytesNN 型は右が、 uintNN / intNN 型は左がパディングされます。

警告

構造体に複数の動的サイズの配列が含まれていると、エンコーディングが曖昧になります。 そのため、常にイベントデータを再確認し、インデックス化されたパラメータだけに基づく検索結果に頼らないようにしてください。

セキュリティへの配慮

想定通りに動作するソフトウェアを作るのは簡単ですが、想定外の使い方をされないようにチェックするのは非常に困難です。

Solidityでは、スマートコントラクトを使ってトークンや、もっと価値のあるものを扱うことができるので、セキュリティはさらに重要です。 加えて、スマートコントラクトの実行はすべて公開されており、ソースコードも公開されることがあります。

もちろん、セキュリティにどれだけの価値があるかは常に考えなければなりません: スマートコントラクトは、パブリックに公開されていて(つまり悪意のあるアクターにも公開されていて)オープンソースであることがあるWebサービスと比較できます。 そのウェブサービスに食料品のリストを保存するだけであれば、それほど注意を払う必要はないかもしれませんが、そのWebサービスを使って銀行口座を管理する場合には、より注意を払う必要があります。

このセクションでは、いくつかの落とし穴や一般的なセキュリティ上の推奨事項を挙げていきますが、もちろん完全なものではありません。 また、スマートコントラクトのコードにバグがない場合でも、コンパイラやプラットフォーム自体にバグがある可能性があることも覚えておいてください。 コンパイラのセキュリティ関連の既知のバグのリストは 既知のバグリスト に掲載されており、機械でも読めます。 なお、Solidityコンパイラのコードジェネレータを対象とした Bug Bounty Program があります。

いつものように、オープンソースのドキュメントでは、このセクションの拡張にご協力ください(特に、いくつかの例があれば問題ありません)。

注: 以下のリストの他にも、セキュリティに関する推奨事項やベストプラクティスが Guy Landoのリスト および ConsensysのGitHubリポジトリ に掲載されています。

落とし穴

プライベートの情報とランダム性

スマートコントラクト上で利用できるものはすべて公開されており、ローカル変数や private と書かれた状態変数も公開されています。

スマートコントラクトで乱数を使用することは、ブロックビルダーが不正行為をする可能性があるため、困難です。

Reentrancy

コントラクト(A)と別のコントラクト(B)とのインタラクションやEtherの送金は、コントラクト(B)に制御権を渡します。 これにより、このインタラクションが完了する前に、BがAにコールバックすることが可能になります。 例として、以下のコードにはバグが含まれています(これは単なるスニペットであり、完全なコントラクトではありません)。

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

// このコントラクトにはバグが含まれています - 使わないでください
contract Fund {
    /// @dev Etherシェアのマッピング
    mapping(address => uint) shares;
    /// シェアを引き出す
    function withdraw() public {
        if (payable(msg.sender).send(shares[msg.sender]))
            shares[msg.sender] = 0;
    }
}

この問題は、 send にガス制限があるため、それほど深刻ではありませんが、それでも脆弱性があります。 Etherの送金には常にコードの実行が含まれるため、受信者は withdraw にコールバックするコントラクトになる可能性があります。 これにより、複数回の払い戻しが可能となり、基本的にはコントラクト内のすべてのEtherを回収できます。 特に、以下のコントラクトは、デフォルトで残りのガスをすべて送金する call を使用しているため、攻撃者は複数回返金できます。

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

// このコントラクトにはバグが含まれています - 使わないでください
contract Fund {
    /// @dev Etherシェアのマッピング
    mapping(address => uint) shares;
    /// シェアを引き出す
    function withdraw() public {
        (bool success,) = msg.sender.call{value: shares[msg.sender]}("");
        if (success)
            shares[msg.sender] = 0;
    }
}

Re-entrancyを避けるために、以下のようなChecks-Effects-Interactionsパターンを使用できます。

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

contract Fund {
    /// @dev Etherシェアのマッピング
    mapping(address => uint) shares;
    /// シェアを引き出す
    function withdraw() public {
        uint share = shares[msg.sender];
        shares[msg.sender] = 0;
        payable(msg.sender).transfer(share);
    }
}

Checks-Effects-Interactionsパターンは、コントラクトを通るすべてのコードパスが、コントラクトの状態を変更する前に、供給されたパラメータの必要なチェックをすべて完了することを保証します(Checks)。 その後、コントラクトはステートに変更を加えます(Effects)。 計画されたステートの変更がすべてストレージに書き込まれた後、他のコントラクトの関数を呼び出すことができます(Interactions)。 これは、外部から呼び出された悪意のあるコントラクトが、トランザクションを確定する前に元のコントラクトにコールバックするロジックを使用することで、手当を二重に使ったり、残高を二重に引き出したりできる、 リエントランシー攻撃 を防ぐための一般的なフールプルーフな方法です。

Reentrancyは、Ether送金だけでなく、別のコントラクトでのあらゆる関数呼び出しの影響を受けることに注意してください。 さらに、複数のコントラクトを考慮しなければならない状況もあります。 呼び出されたコントラクトが、依存している別のコントラクトの状態を変更する可能性があります。

ガスリミットとループ

例えば、ストレージの値に依存するループなど、反復回数が固定されていないループは、慎重に使用する必要があります。 ブロックガスリミットにより、トランザクションは一定量のガスしか消費できません。 明示的に、または通常の操作によって、ループの反復回数がブロックガスリミットを超えてしまい、コントラクト全体がある時点で停止してしまうことがあります。 これは、ブロックチェーンからデータを読み取るためだけに実行される view 関数には当てはまらないかもしれません。 それでも、そのような関数はオンチェーン操作の一部として他のコントラクトから呼び出され、それらを引き伸ばすことができます。 このようなケースについては、コントラクトのドキュメントで明示してください。

Etherの送受信

  • コントラクトも「外部アカウント」も、誰かがEtherを送ってくるのを防ぐことは今のところできません。 コントラクトは、通常の送金に反応して拒否できますが、メッセージコールを作成せずにEtherを移動する方法があります。 ひとつはコントラクトのアドレスに単純に「マイニング」する方法で、もうひとつは selfdestruct(x) を使う方法です。

  • コントラクトが(関数が呼ばれずに)Etherを受信すると、 receive Ether または fallback 関数が実行されます。 receive 関数も fallback 関数も持たない場合、Etherは(例外を投げて)拒否されます。 これらの関数が実行されている間、コントラクトは、渡された「gas stipend」(2300ガス)がその時点で利用可能であることにのみ依存できます。 この供給量は、ストレージを変更するのに十分ではありません(将来のハードフォークで供給量が変更される可能性がありますので、これを鵜呑みにしてはいけません)。 コントラクトがこの方法でEtherを受け取ることができるかどうかを確認するには、receive関数とfallback関数のガス要件を確認してください(例えばRemixの「詳細」セクションに記載されています)。

  • addr.call{value: x}("") を使用して、より多くのガスを受信コントラクトに送金する方法があります。 これは基本的に addr.transfer(x) と同じですが、残りのガスをすべて送金し、受信側がより高価なアクションを実行できるようにします(また、自動的にエラーを伝播するのではなく、失敗コードを返します)。 これには、送信側のコントラクトにコールバックすることや、あなたが考えもしなかったような他の状態変化が含まれるかもしれません。 そのため、誠実なユーザーだけでなく、悪意のあるアクターにも大きな柔軟性を与えることができます。

  • weiの量を表す単位は、精度が低いために丸められたものは失われてしまうので、できるだけ正確な単位を使ってください。

  • address.transfer を使ってEtherを送信する場合、注意すべき点があります。

    1. 受信者がコントラクトの場合、そのreceive関数またはfallback関数を実行させ、その結果、送信側のコントラクトをコールバックできます。

    2. コールの深さが102以上になると、Etherの送信に失敗することがあります。

    3. 呼び出し側はコールの深さを完全にコントロールしているため、強制的に送金を失敗させることができます。 この可能性を考慮して send を使用するか、その戻り値を常に確認するようにしてください。 さらに言えば、受取人が代わりにEtherを引き出せるようなパターンでコントラクトを書いてください。

    4. Etherの送信は、受信者のコントラクトの実行に割り当てられた量以上のガスが必要となるため( requireassertrevert を使用して明示的に、または操作が高すぎるため)、「ガス欠」(OOG)となって失敗することもあります。 transfer または send を戻り値のチェックとともに使用すると、受信者が送信側のコントラクトの進行をブロックする手段となる可能性があります。 ここでも、 sendパターンの代わりにwithdrawパターン を使用するのがベストです。

コールスタックの深さ

外部関数の呼び出しは、コールスタックの最大サイズ制限である1024を超えるため、いつでも失敗する可能性があります。 このような状況では、Solidityは例外を投げます。 悪意のあるアクターは、コントラクトと対話する前にコールスタックを強制的に高い値にできるかもしれません。 Tangerine Whistle のハードフォーク以来、 63/64ルール はコールスタックの深さの攻撃を実用的ではないものにしていることに注意してください。 また、コールスタックとエクスプレッションスタックは、どちらも1024のスタックスロットというサイズ制限がありますが、無関係であることに注意してください。

.send() はコールスタックが枯渇した場合に例外を発生させず、 false を返すことに注意してください。 低レベル関数の .call().delegatecall().staticcall() も同じように動作します。

認可されたプロキシ

コントラクトがプロキシとして動作できる場合、つまり、ユーザーが提供したデータで任意のコントラクトを呼び出すことができる場合、ユーザーは基本的にプロキシのコントラクトのアイデンティティを仮定できます。 他の保護手段があったとしても、プロキシが(自分自身のためでさえも)いかなる許可も持たないようにコントラクトシステムを構築することが最善です。 必要であれば、第二のプロキシを使ってそれを達成できます。

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract ProxyWithMoreFunctionality {
    PermissionlessProxy proxy;

    function callOther(address addr, bytes memory payload) public
            returns (bool, bytes memory) {
        return proxy.callOther(addr, payload);
    }
    // その他の関数や機能
}

// これは完全なコントラクトであり、他の機能はなく、動作するために特権を必要としません。
contract PermissionlessProxy {
    function callOther(address addr, bytes memory payload) public
            returns (bool, bytes memory) {
        return addr.call(payload);
    }
}

tx.origin

認証に tx.origin を使用しないでください。 以下のようなウォレットコントラクトがあるとします。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
// このコントラクトにはバグが含まれています - 使わないでください
contract TxUserWallet {
    address owner;

    constructor() {
        owner = msg.sender;
    }

    function transferTo(address payable dest, uint amount) public {
        // バグはここにあります。tx.originの代わりにmsg.senderを使用する必要があります。
        require(tx.origin == owner);
        dest.transfer(amount);
    }
}

今度は誰かに騙されて、この攻撃用ウォレットのアドレスにEtherを送ってしまうとしましょう。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
interface TxUserWallet {
    function transferTo(address payable dest, uint amount) external;
}

contract TxAttackWallet {
    address payable owner;

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

    receive() external payable {
        TxUserWallet(msg.sender).transferTo(owner, msg.sender.balance);
    }
}

もしあなたのウォレットが msg.sender をチェックして承認を得ていたら、所有者のアドレスではなく、攻撃したウォレットのアドレスを得ることになります。 しかし、 tx.origin をチェックすると、トランザクションを開始した元のアドレスが取得され、それがオーナーのアドレスとなります。 攻撃されたウォレットは即座にあなたの資金をすべて使い果たしてしまいます。

2の補数 / アンダーフロー / オーバーフロー

多くのプログラミング言語と同様に、Solidityの整数型は実際には整数ではありません。 値が小さいときは整数に似ていますが、任意に大きな数値を表すことはできません。

以下のコードでは、加算結果が大きすぎて uint8 型に格納できないため、オーバーフローが発生します。

uint8 x = 255;
uint8 y = 1;
return x + y;

Solidityには、これらのオーバーフローを処理する2つのモードがあります。 チェックされたモードとチェックされていないモード、つまり「ラッピング」モードです。

デフォルトのチェックモードでは、オーバーフローを検出し、アサーションの失敗を引き起こします。 unchecked { ... } を使ってこのチェックを無効にすることで、オーバーフローを静かに無視できます。 上記のコードは、 unchecked { ... } でラップすると 0 を返します。

チェックモードであっても、オーバーフローのバグから守られていると思わないでください。 このモードでは、オーバーフローは必ずリバートします。 オーバーフローを回避できない場合、スマートコントラクトが特定の状態で立ち往生してしまう可能性があります。

一般的には、2の補数表現の限界について読んでみてください。 2の補数表現には、符号付きの数字に対するより特別なエッジケースもあります。

require を使って入力の大きさを合理的な範囲に制限し、 SMTチェッカー を使ってオーバーフローの可能性を見つけるようにしましょう。

マッピングのクリア

Solidityの型 mappingマッピング型 参照)は、ストレージのみのキーバリューデータ構造で、ゼロ以外の値が割り当てられたキーを追跡しません。 そのため、書き込まれたキーに関する余分な情報を持たないマッピングのクリーニングは不可能です。 mapping が動的ストレージ配列の基本型として使用されている場合、配列を削除したりポップしたりしても mapping の要素には影響しません。 例えば、動的ストレージ配列のベース型である struct のメンバーフィールドの型として mapping が使用されている場合も同様です。 また、 mapping を含む構造体や配列の代入においても、 mapping は無視されます。

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

contract Map {
    mapping(uint => uint)[] array;

    function allocate(uint newMaps) public {
        for (uint i = 0; i < newMaps; i++)
            array.push();
    }

    function writeMap(uint map, uint key, uint value) public {
        array[map][key] = value;
    }

    function readMap(uint map, uint key) public view returns (uint) {
        return array[map][key];
    }

    function eraseMaps() public {
        delete array;
    }
}

上の例で、次のような一連のコールを考えてみましょう: allocate(10), writeMap(4, 128, 256) 。 この時点で、 readMap(4, 128) を呼び出すと256を返します。 eraseMaps を呼び出すと、状態変数 array の長さはゼロになりますが、その mapping 要素はゼロにできないので、その情報はコントラクトのストレージの中で生き続けます。 array を削除した後、 allocate(5) を呼び出すと、再び array[4] にアクセスできるようになり、 readMap(4, 128) を呼び出すと、 writeMap を再度呼び出さなくても256を返します。

mapping の情報を削除する必要がある場合は、 iterable mapping と同様のライブラリを使用することを検討し、適切な mapping でキーをトラバースしてその値を削除できます。

マイナーな内容

  • 32バイトを完全に占有しない型には、「ダーティな高次ビット」が含まれている可能性があります。 これは msg.data にアクセスする場合に特に重要で、不正改造の危険性があります: 関数 f(uint8 x) を生のバイト引数 0xff000001 で呼び出すトランザクションと、 0x00000001 で呼び出すトランザクションを作ることができます。 両方ともコントラクトに供給され、 x に関しては両方とも 1 という数字に見えますが、 msg.data は異なるものになりますので、何かに keccak256(msg.data) を使うと、異なる結果になります。

推奨事項

警告を真摯に受け止める

コンパイラが何かを警告したら、それを変更すべきです。 その警告がセキュリティに影響するとは思わなくても、その下に別の問題が隠れているかもしれません。 私たちが発するコンパイラの警告は、コードを少し変更するだけで黙らせることができます。

最近導入されたすべての警告について通知を受けるには、常に最新バージョンのコンパイラを使用してください。

コンパイラが発行する info 型のメッセージは危険なものではなく、ユーザにとって有用であるとコンパイラが考える追加の提案やオプション情報を表しています。

Etherの量を制限する

スマートコントラクトに格納できるEther(または他のトークン)の量を制限します。 ソースコードやコンパイラ、プラットフォームにバグがあると、これらの資金が失われる可能性があります。 損失を制限したい場合は、Etherの量を制限してください。

小さくモジュール化する

コントラクトは小さく、理解しやすいものにしましょう。 関係のない機能は他のコントラクトやライブラリにまとめてください。 もちろん、ソースコードの品質に関する一般的な推奨事項も適用されます。 ローカル変数の量や関数の長さなどを制限してください。 また、あなたの意図が何であるか、それがコードが行うことと異なるかどうかを他の人が理解できるように、関数を文書化してください。

Checks-Effects-Interactionsパターンを使う

ほとんどの関数は最初にいくつかのチェックを行い、それらは最初に行うべきです。 例えば、誰が関数を呼び出したか、引数は範囲内か、十分なイーサを送ったか、その人はトークンを持っているか、などです。

2番目のステップとして、すべてのチェックがパスした場合、現在のコントラクトの状態変数への効果が作られるべきです。 他のコントラクトとのやりとりは、どの関数でも最後のステップにすべきです。

初期のコントラクトでは、いくつかの効果を遅らせ、外部の関数呼び出しが非エラー状態で戻ってくるのを待っていました。 これは、上で説明したReentrancyの問題のため、しばしば重大な誤りとなります。

なお、既知のコントラクトを呼び出すと、未知のコントラクトを呼び出す可能性もあるので、常にこのパターンを適用するのが良いでしょう。

フェイルセーフモードを搭載する

システムを完全に非中央集権化することで、仲介者を排除できますが、特に新しいコードには、何らかのフェイルセーフメカニズムを組み込むことが良いかもしれません。

スマートコントラクトの中に、「Etherが漏れていないか」「トークンの合計がコントラクトの残高と同じか」などの自己チェックを行う関数を追加できます。 そのためには、あまり多くのガスを使うことはできないので、オフチェーンの計算による助けが必要になるかもしれないことを覚えておいてください。

セルフチェックに失敗すると、コントラクトは自動的にある種の「フェイルセーフ」モードに切り替わります。 例えば、ほとんどの機能を無効にしたり、固定された信頼できる第三者にコントロールを委ねたり、あるいは単に「Etherを返してください」というコントラクトに変更したりします。

ピアレビューを依頼する

多くの人がコードを検証すればするほど、多くの問題が見つかります。 また、人にコードを見てもらうことで、コードがわかりやすいかどうかのクロスチェックにもなり、これは優れたスマートコントラクトにとって非常に重要な基準です。

既知のバグのリスト

以下に、Solidityコンパイラのセキュリティ関連の既知のバグをJSON形式でリストアップしています。 このファイルは Githubリポジトリ にあります。 このリストはバージョン0.3.0までさかのぼりますが、それ以前のバージョンにしか存在しないことがわかっているバグはリストに含まれていません。

また、 bugs_by_version.json というファイルがあり、特定のバージョンのコンパイラーに影響を与えるバグを確認できます。

コントラクトソース検証ツール、およびコントラクトと相互作用するその他のツールは、以下の基準に従ってこのリストを参照する必要があります。

  • コントラクトがリリースされたバージョンではなく、nightlyコンパイラのバージョンでコンパイルされた場合は、少し疑わしいです。 このリストでは、リリースされていないバージョンやnightlyバージョンの記録は取っていません。

  • また、コントラクトが作成された時点で最新ではないバージョンでコンパイルされていた場合、少し疑わしいです。 他のコントラクトから作成されたコントラクトについては、作成の連鎖をトランザクションまでさかのぼり、そのトランザクションの日付を作成日として使用する必要があります。

  • 既知のバグを含むコンパイラを使用してコントラクトを作成し、修正プログラムを含む新しいバージョンのコンパイラがすでにリリースされている時期にコントラクトが作成された場合、非常に疑わしいです。

以下の既知のバグのJSONファイルは、各バグに1つずつ、以下のキーを持つオブジェクトの配列です。

uid

SOL-<year>-<number> 形式でバグに与えられた一意の識別子。 同じuidで複数のエントリが存在する可能性があります。

name

バグに付けられたユニークな名前。

summary

バグの短い説明。

description

バグの詳細な説明。

link

より詳細な情報があるウェブサイトのURL。 オプション。

introduced

バグを含んで最初に公開されたコンパイラバージョン。 オプション。

fixed

バグを含まなくなって最初に公開されたコンパイラバージョン。

publish

バグが公に知られるようになった日付。 オプション。

severity

バグの深刻度: very low、low、medium、high。 コントラクトテストでの発見可能性、発生の可能性、悪用による被害の可能性を考慮しています。

conditions

バグを発生させるために満たさなければならない条件。 以下のキーが使用できます。 optimizer: ブール値。 バグを有効にするにはオプティマイザがオンになっていなければならないことを意味します。 evmVersion: どのEVMバージョンのコンパイラ設定がバグを引き起こすかを示す文字列。 この文字列には比較演算子を含めることができます。 例えば、 ">=constantinople" は EVM バージョンが constantinople 以降に設定されている場合にバグが発生することを意味します。

check

このフィールドには、スマートコントラクトにバグが含まれているかどうかを報告するさまざまなチェックが含まれます。 最初のタイプのチェックは、バグが存在する場合にソースコード(「source-regex」)に対してマッチされるJavaScriptの正規表現です。 一致しない場合は、バグが存在しない可能性が高いです。 一致するものがあれば、そのバグは存在する可能性があります。 精度を上げるためには、コメントを削除した後のソースコードにチェックを適用する必要があります。 2番目のタイプのチェックは、SolidityプログラムのコンパクトASTにチェックするパターンです(「ast-compact-json-path」)。 指定された検索クエリは、 JsonPath の式です。 SolidityのASTの少なくとも1つのパスがクエリにマッチする場合、バグが存在する可能性が高いです。

[
    {
        "uid": "SOL-2022-7",
        "name": "StorageWriteRemovalBeforeConditionalTermination",
        "summary": "Calling functions that conditionally terminate the external EVM call using the assembly statements ``return(...)`` or ``stop()`` may result in incorrect removals of prior storage writes.",
        "description": "A call to a Yul function that conditionally terminates the external EVM call could result in prior storage writes being incorrectly removed by the Yul optimizer. This used to happen in cases in which it would have been valid to remove the store, if the Yul function in question never actually terminated the external call, and the control flow always returned back to the caller instead. Conditional termination within the same Yul block instead of within a called function was not affected. In Solidity with optimized via-IR code generation, any storage write before a function conditionally calling ``return(...)`` or ``stop()`` in inline assembly, may have been incorrectly removed, whenever it would have been valid to remove the write without the ``return(...)`` or ``stop()``. In optimized legacy code generation, only inline assembly that did not refer to any Solidity variables and that involved conditionally-terminating user-defined assembly functions could be affected.",
        "link": "https://blog.soliditylang.org/2022/09/08/storage-write-removal-before-conditional-termination/",
        "introduced": "0.8.13",
        "fixed": "0.8.17",
        "severity": "medium/high",
        "conditions": {
            "yulOptimizer": true
        }
    },
    {
        "uid": "SOL-2022-6",
        "name": "AbiReencodingHeadOverflowWithStaticArrayCleanup",
        "summary": "ABI-encoding a tuple with a statically-sized calldata array in the last component would corrupt 32 leading bytes of its first dynamically encoded component.",
        "description": "When ABI-encoding a statically-sized calldata array, the compiler always pads the data area to a multiple of 32-bytes and ensures that the padding bytes are zeroed. In some cases, this cleanup used to be performed by always writing exactly 32 bytes, regardless of how many needed to be zeroed. This was done with the assumption that the data that would eventually occupy the area past the end of the array had not yet been written, because the encoder processes tuple components in the order they were given. While this assumption is mostly true, there is an important corner case: dynamically encoded tuple components are stored separately from the statically-sized ones in an area called the *tail* of the encoding and the tail immediately follows the *head*, which is where the statically-sized components are placed. The aforementioned cleanup, if performed for the last component of the head would cross into the tail and overwrite up to 32 bytes of the first component stored there with zeros. The only array type for which the cleanup could actually result in an overwrite were arrays with ``uint256`` or ``bytes32`` as the base element type and in this case the size of the corrupted area was always exactly 32 bytes. The problem affected tuples at any nesting level. This included also structs, which are encoded as tuples in the ABI. Note also that lists of parameters and return values of functions, events and errors are encoded as tuples.",
        "link": "https://blog.soliditylang.org/2022/08/08/calldata-tuple-reencoding-head-overflow-bug/",
        "introduced": "0.5.8",
        "fixed": "0.8.16",
        "severity": "medium",
        "conditions": {
            "ABIEncoderV2": true
        }
    },
    {
        "uid": "SOL-2022-5",
        "name": "DirtyBytesArrayToStorage",
        "summary": "Copying ``bytes`` arrays from memory or calldata to storage may result in dirty storage values.",
        "description": "Copying ``bytes`` arrays from memory or calldata to storage is done in chunks of 32 bytes even if the length is not a multiple of 32. Thereby, extra bytes past the end of the array may be copied from calldata or memory to storage. These dirty bytes may then become observable after a ``.push()`` without arguments to the bytes array in storage, i.e. such a push will not result in a zero value at the end of the array as expected. This bug only affects the legacy code generation pipeline, the new code generation pipeline via IR is not affected.",
        "link": "https://blog.soliditylang.org/2022/06/15/dirty-bytes-array-to-storage-bug/",
        "introduced": "0.0.1",
        "fixed": "0.8.15",
        "severity": "low"
    },
    {
        "uid": "SOL-2022-4",
        "name": "InlineAssemblyMemorySideEffects",
        "summary": "The Yul optimizer may incorrectly remove memory writes from inline assembly blocks, that do not access solidity variables.",
        "description": "The Yul optimizer considers all memory writes in the outermost Yul block that are never read from as unused and removes them. This is valid when that Yul block is the entire Yul program, which is always the case for the Yul code generated by the new via-IR pipeline. Inline assembly blocks are never optimized in isolation when using that pipeline. Instead they are optimized as a part of the whole Yul input. However, the legacy code generation pipeline (which is still the default) runs the Yul optimizer individually on an inline assembly block if the block does not refer to any local variables defined in the surrounding Solidity code. Consequently, memory writes in such inline assembly blocks are removed as well, if the written memory is never read from in the same assembly block, even if the written memory is accessed later, for example by a subsequent inline assembly block.",
        "link": "https://blog.soliditylang.org/2022/06/15/inline-assembly-memory-side-effects-bug/",
        "introduced": "0.8.13",
        "fixed": "0.8.15",
        "severity": "medium",
        "conditions": {
            "yulOptimizer": true
        }
    },
    {
        "uid": "SOL-2022-3",
        "name": "DataLocationChangeInInternalOverride",
        "summary": "It was possible to change the data location of the parameters or return variables from ``calldata`` to ``memory`` and vice-versa while overriding internal and public functions. This caused invalid code to be generated when calling such a function internally through virtual function calls.",
        "description": "When calling external functions, it is irrelevant if the data location of the parameters is ``calldata`` or ``memory``, the encoding of the data does not change. Because of that, changing the data location when overriding external functions is allowed. The compiler incorrectly also allowed a change in the data location for overriding public and internal functions. Since public functions can be called internally as well as externally, this causes invalid code to be generated when such an incorrectly overridden function is called internally through the base contract. The caller provides a memory pointer, but the called function interprets it as a calldata pointer or vice-versa.",
        "link": "https://blog.soliditylang.org/2022/05/17/data-location-inheritance-bug/",
        "introduced": "0.6.9",
        "fixed": "0.8.14",
        "severity": "very low"
    },
    {
        "uid": "SOL-2022-2",
        "name": "NestedCalldataArrayAbiReencodingSizeValidation",
        "summary": "ABI-reencoding of nested dynamic calldata arrays did not always perform proper size checks against the size of calldata and could read beyond ``calldatasize()``.",
        "description": "Calldata validation for nested dynamic types is deferred until the first access to the nested values. Such an access may for example be a copy to memory or an index or member access to the outer type. While in most such accesses calldata validation correctly checks that the data area of the nested array is completely contained in the passed calldata (i.e. in the range [0, calldatasize()]), this check may not be performed, when ABI encoding such nested types again directly from calldata. For instance, this can happen, if a value in calldata with a nested dynamic array is passed to an external call, used in ``abi.encode`` or emitted as event. In such cases, if the data area of the nested array extends beyond ``calldatasize()``, ABI encoding it did not revert, but continued reading values from beyond ``calldatasize()`` (i.e. zero values).",
        "link": "https://blog.soliditylang.org/2022/05/17/calldata-reencode-size-check-bug/",
        "introduced": "0.5.8",
        "fixed": "0.8.14",
        "severity": "very low"
    },
    {
        "uid": "SOL-2022-1",
        "name": "AbiEncodeCallLiteralAsFixedBytesBug",
        "summary": "Literals used for a fixed length bytes parameter in ``abi.encodeCall`` were encoded incorrectly.",
        "description": "For the encoding, the compiler only considered the types of the expressions in the second argument of ``abi.encodeCall`` itself, but not the parameter types of the function given as first argument. In almost all cases the abi encoding of the type of the expression matches the abi encoding of the parameter type of the given function. This is because the type checker ensures the expression is implicitly convertible to the respective parameter type. However this is not true for number literals used for fixed bytes types shorter than 32 bytes, nor for string literals used for any fixed bytes type. Number literals were encoded as numbers instead of being shifted to become left-aligned. String literals were encoded as dynamically sized memory strings instead of being converted to a left-aligned bytes value.",
        "link": "https://blog.soliditylang.org/2022/03/16/encodecall-bug/",
        "introduced": "0.8.11",
        "fixed": "0.8.13",
        "severity": "very low"

    },
    {
        "uid": "SOL-2021-4",
        "name": "UserDefinedValueTypesBug",
        "summary": "User defined value types with underlying type shorter than 32 bytes used incorrect storage layout and wasted storage",
        "description": "The compiler did not correctly compute the storage layout of user defined value types based on types that are shorter than 32 bytes. It would always use a full storage slot for these types, even if the underlying type was shorter. This was wasteful and might have problems with tooling or contract upgrades.",
        "link": "https://blog.soliditylang.org/2021/09/29/user-defined-value-types-bug/",
        "introduced": "0.8.8",
        "fixed": "0.8.9",
        "severity": "very low"
    },
    {
        "uid": "SOL-2021-3",
        "name": "SignedImmutables",
        "summary": "Immutable variables of signed integer type shorter than 256 bits can lead to values with invalid higher order bits if inline assembly is used.",
        "description": "When immutable variables of signed integer type shorter than 256 bits are read, their higher order bits were unconditionally set to zero. The correct operation would be to sign-extend the value, i.e. set the higher order bits to one if the sign bit is one. This sign-extension is performed by Solidity just prior to when it matters, i.e. when a value is stored in memory, when it is compared or when a division is performed. Because of that, to our knowledge, the only way to access the value in its unclean state is by reading it through inline assembly.",
        "link": "https://blog.soliditylang.org/2021/09/29/signed-immutables-bug/",
        "introduced": "0.6.5",
        "fixed": "0.8.9",
        "severity": "very low"
    },
    {
        "uid": "SOL-2021-2",
        "name": "ABIDecodeTwoDimensionalArrayMemory",
        "summary": "If used on memory byte arrays, result of the function ``abi.decode`` can depend on the contents of memory outside of the actual byte array that is decoded.",
        "description": "The ABI specification uses pointers to data areas for everything that is dynamically-sized. When decoding data from memory (instead of calldata), the ABI decoder did not properly validate some of these pointers. More specifically, it was possible to use large values for the pointers inside arrays such that computing the offset resulted in an undetected overflow. This could lead to these pointers targeting areas in memory outside of the actual area to be decoded. This way, it was possible for ``abi.decode`` to return different values for the same encoded byte array.",
        "link": "https://blog.soliditylang.org/2021/04/21/decoding-from-memory-bug/",
        "introduced": "0.4.16",
        "fixed": "0.8.4",
        "conditions": {
            "ABIEncoderV2": true
        },
        "severity": "very low"
    },
    {
        "uid": "SOL-2021-1",
        "name": "KeccakCaching",
        "summary": "The bytecode optimizer incorrectly re-used previously evaluated Keccak-256 hashes. You are unlikely to be affected if you do not compute Keccak-256 hashes in inline assembly.",
        "description": "Solidity's bytecode optimizer has a step that can compute Keccak-256 hashes, if the contents of the memory are known during compilation time. This step also has a mechanism to determine that two Keccak-256 hashes are equal even if the values in memory are not known during compile time. This mechanism had a bug where Keccak-256 of the same memory content, but different sizes were considered equal. More specifically, ``keccak256(mpos1, length1)`` and ``keccak256(mpos2, length2)`` in some cases were considered equal if ``length1`` and ``length2``, when rounded up to nearest multiple of 32 were the same, and when the memory contents at ``mpos1`` and ``mpos2`` can be deduced to be equal. You maybe affected if you compute multiple Keccak-256 hashes of the same content, but with different lengths inside inline assembly. You are unaffected if your code uses ``keccak256`` with a length that is not a compile-time constant or if it is always a multiple of 32.",
        "link": "https://blog.soliditylang.org/2021/03/23/keccak-optimizer-bug/",
        "fixed": "0.8.3",
        "conditions": {
            "optimizer": true
        },
        "severity": "medium"
    },
    {
        "uid": "SOL-2020-11",
        "name": "EmptyByteArrayCopy",
        "summary": "Copying an empty byte array (or string) from memory or calldata to storage can result in data corruption if the target array's length is increased subsequently without storing new data.",
        "description": "The routine that copies byte arrays from memory or calldata to storage stores unrelated data from after the source array in the storage slot if the source array is empty. If the storage array's length is subsequently increased either by using ``.push()`` or by assigning to its ``.length`` attribute (only before 0.6.0), the newly created byte array elements will not be zero-initialized, but contain the unrelated data. You are not affected if you do not assign to ``.length`` and do not use ``.push()`` on byte arrays, or only use ``.push(<arg>)`` or manually initialize the new elements.",
        "link": "https://blog.soliditylang.org/2020/10/19/empty-byte-array-copy-bug/",
        "fixed": "0.7.4",
        "severity": "medium"
    },
    {
        "uid": "SOL-2020-10",
        "name": "DynamicArrayCleanup",
        "summary": "When assigning a dynamically-sized array with types of size at most 16 bytes in storage causing the assigned array to shrink, some parts of deleted slots were not zeroed out.",
        "description": "Consider a dynamically-sized array in storage whose base-type is small enough such that multiple values can be packed into a single slot, such as `uint128[]`. Let us define its length to be `l`. When this array gets assigned from another array with a smaller length, say `m`, the slots between elements `m` and `l` have to be cleaned by zeroing them out. However, this cleaning was not performed properly. Specifically, after the slot corresponding to `m`, only the first packed value was cleaned up. If this array gets resized to a length larger than `m`, the indices corresponding to the unclean parts of the slot contained the original value, instead of 0. The resizing here is performed by assigning to the array `length`, by a `push()` or via inline assembly. You are not affected if you are only using `.push(<arg>)` or if you assign a value (even zero) to the new elements after increasing the length of the array.",
        "link": "https://blog.soliditylang.org/2020/10/07/solidity-dynamic-array-cleanup-bug/",
        "fixed": "0.7.3",
        "severity": "medium"
    },
    {
        "uid": "SOL-2020-9",
        "name": "FreeFunctionRedefinition",
        "summary": "The compiler does not flag an error when two or more free functions with the same name and parameter types are defined in a source unit or when an imported free function alias shadows another free function with a different name but identical parameter types.",
        "description": "In contrast to functions defined inside contracts, free functions with identical names and parameter types did not create an error. Both definition of free functions with identical name and parameter types and an imported free function with an alias that shadows another function with a different name but identical parameter types were permitted due to which a call to either the multiply defined free function or the imported free function alias within a contract led to the execution of that free function which was defined first within the source unit. Subsequently defined identical free function definitions were silently ignored and their code generation was skipped.",
        "introduced": "0.7.1",
        "fixed": "0.7.2",
        "severity": "low"
    },
    {
        "uid": "SOL-2020-8",
        "name": "UsingForCalldata",
        "summary": "Function calls to internal library functions with calldata parameters called via ``using for`` can result in invalid data being read.",
        "description": "Function calls to internal library functions using the ``using for`` mechanism copied all calldata parameters to memory first and passed them on like that, regardless of whether it was an internal or an external call. Due to that, the called function would receive a memory pointer that is interpreted as a calldata pointer. Since dynamically sized arrays are passed using two stack slots for calldata, but only one for memory, this can lead to stack corruption. An affected library call will consider the JUMPDEST to which it is supposed to return as part of its arguments and will instead jump out to whatever was on the stack before the call.",
        "introduced": "0.6.9",
        "fixed": "0.6.10",
        "severity": "very low"
    },
    {
        "uid": "SOL-2020-7",
        "name": "MissingEscapingInFormatting",
        "summary": "String literals containing double backslash characters passed directly to external or encoding function calls can lead to a different string being used when ABIEncoderV2 is enabled.",
        "description": "When ABIEncoderV2 is enabled, string literals passed directly to encoding functions or external function calls are stored as strings in the intemediate code. Characters outside the printable range are handled correctly, but backslashes are not escaped in this procedure. This leads to double backslashes being reduced to single backslashes and consequently re-interpreted as escapes potentially resulting in a different string being encoded.",
        "introduced": "0.5.14",
        "fixed": "0.6.8",
        "severity": "very low",
        "conditions": {
            "ABIEncoderV2": true
        }
    },
    {
        "uid": "SOL-2020-6",
        "name": "ArraySliceDynamicallyEncodedBaseType",
        "summary": "Accessing array slices of arrays with dynamically encoded base types (e.g. multi-dimensional arrays) can result in invalid data being read.",
        "description": "For arrays with dynamically sized base types, index range accesses that use a start expression that is non-zero will result in invalid array slices. Any index access to such array slices will result in data being read from incorrect calldata offsets. Array slices are only supported for dynamic calldata types and all problematic type require ABIEncoderV2 to be enabled.",
        "introduced": "0.6.0",
        "fixed": "0.6.8",
        "severity": "very low",
        "conditions": {
            "ABIEncoderV2": true
        }
    },
    {
        "uid": "SOL-2020-5",
        "name": "ImplicitConstructorCallvalueCheck",
        "summary": "The creation code of a contract that does not define a constructor but has a base that does define a constructor did not revert for calls with non-zero value.",
        "description": "Starting from Solidity 0.4.5 the creation code of contracts without explicit payable constructor is supposed to contain a callvalue check that results in contract creation reverting, if non-zero value is passed. However, this check was missing in case no explicit constructor was defined in a contract at all, but the contract has a base that does define a constructor. In these cases it is possible to send value in a contract creation transaction or using inline assembly without revert, even though the creation code is supposed to be non-payable.",
        "introduced": "0.4.5",
        "fixed": "0.6.8",
        "severity": "very low"
    },
    {
        "uid": "SOL-2020-4",
        "name": "TupleAssignmentMultiStackSlotComponents",
        "summary": "Tuple assignments with components that occupy several stack slots, i.e. nested tuples, pointers to external functions or references to dynamically sized calldata arrays, can result in invalid values.",
        "description": "Tuple assignments did not correctly account for tuple components that occupy multiple stack slots in case the number of stack slots differs between left-hand-side and right-hand-side. This can either happen in the presence of nested tuples or if the right-hand-side contains external function pointers or references to dynamic calldata arrays, while the left-hand-side contains an omission.",
        "introduced": "0.1.6",
        "fixed": "0.6.6",
        "severity": "very low"
    },
    {
        "uid": "SOL-2020-3",
        "name": "MemoryArrayCreationOverflow",
        "summary": "The creation of very large memory arrays can result in overlapping memory regions and thus memory corruption.",
        "description": "No runtime overflow checks were performed for the length of memory arrays during creation. In cases for which the memory size of an array in bytes, i.e. the array length times 32, is larger than 2^256-1, the memory allocation will overflow, potentially resulting in overlapping memory areas. The length of the array is still stored correctly, so copying or iterating over such an array will result in out-of-gas.",
        "link": "https://blog.soliditylang.org/2020/04/06/memory-creation-overflow-bug/",
        "introduced": "0.2.0",
        "fixed": "0.6.5",
        "severity": "low"
    },
    {
        "uid": "SOL-2020-1",
        "name": "YulOptimizerRedundantAssignmentBreakContinue",
        "summary": "The Yul optimizer can remove essential assignments to variables declared inside for loops when Yul's continue or break statement is used. You are unlikely to be affected if you do not use inline assembly with for loops and continue and break statements.",
        "description": "The Yul optimizer has a stage that removes assignments to variables that are overwritten again or are not used in all following control-flow branches. This logic incorrectly removes such assignments to variables declared inside a for loop if they can be removed in a control-flow branch that ends with ``break`` or ``continue`` even though they cannot be removed in other control-flow branches. Variables declared outside of the respective for loop are not affected.",
        "introduced": "0.6.0",
        "fixed": "0.6.1",
        "severity": "medium",
        "conditions": {
            "yulOptimizer": true
        }
    },
    {
        "uid": "SOL-2020-2",
        "name": "privateCanBeOverridden",
        "summary": "Private methods can be overridden by inheriting contracts.",
        "description": "While private methods of base contracts are not visible and cannot be called directly from the derived contract, it is still possible to declare a function of the same name and type and thus change the behaviour of the base contract's function.",
        "introduced": "0.3.0",
        "fixed": "0.5.17",
        "severity": "low"
    },
    {
        "uid": "SOL-2020-1",
        "name": "YulOptimizerRedundantAssignmentBreakContinue0.5",
        "summary": "The Yul optimizer can remove essential assignments to variables declared inside for loops when Yul's continue or break statement is used. You are unlikely to be affected if you do not use inline assembly with for loops and continue and break statements.",
        "description": "The Yul optimizer has a stage that removes assignments to variables that are overwritten again or are not used in all following control-flow branches. This logic incorrectly removes such assignments to variables declared inside a for loop if they can be removed in a control-flow branch that ends with ``break`` or ``continue`` even though they cannot be removed in other control-flow branches. Variables declared outside of the respective for loop are not affected.",
        "introduced": "0.5.8",
        "fixed": "0.5.16",
        "severity": "low",
        "conditions": {
            "yulOptimizer": true
        }
    },
    {
        "uid": "SOL-2019-10",
        "name": "ABIEncoderV2LoopYulOptimizer",
        "summary": "If both the experimental ABIEncoderV2 and the experimental Yul optimizer are activated, one component of the Yul optimizer may reuse data in memory that has been changed in the meantime.",
        "description": "The Yul optimizer incorrectly replaces ``mload`` and ``sload`` calls with values that have been previously written to the load location (and potentially changed in the meantime) if all of the following conditions are met: (1) there is a matching ``mstore`` or ``sstore`` call before; (2) the contents of memory or storage is only changed in a function that is called (directly or indirectly) in between the first store and the load call; (3) called function contains a for loop where the same memory location is changed in the condition or the post or body block. When used in Solidity mode, this can only happen if the experimental ABIEncoderV2 is activated and the experimental Yul optimizer has been activated manually in addition to the regular optimizer in the compiler settings.",
        "introduced": "0.5.14",
        "fixed": "0.5.15",
        "severity": "low",
        "conditions": {
            "ABIEncoderV2": true,
            "optimizer": true,
            "yulOptimizer": true
        }
    },
    {
        "uid": "SOL-2019-9",
        "name": "ABIEncoderV2CalldataStructsWithStaticallySizedAndDynamicallyEncodedMembers",
        "summary": "Reading from calldata structs that contain dynamically encoded, but statically-sized members can result in incorrect values.",
        "description": "When a calldata struct contains a dynamically encoded, but statically-sized member, the offsets for all subsequent struct members are calculated incorrectly. All reads from such members will result in invalid values. Only calldata structs are affected, i.e. this occurs in external functions with such structs as argument. Using affected structs in storage or memory or as arguments to public functions on the other hand works correctly.",
        "introduced": "0.5.6",
        "fixed": "0.5.11",
        "severity": "low",
        "conditions": {
            "ABIEncoderV2": true
        }
    },
    {
        "uid": "SOL-2019-8",
        "name": "SignedArrayStorageCopy",
        "summary": "Assigning an array of signed integers to a storage array of different type can lead to data corruption in that array.",
        "description": "In two's complement, negative integers have their higher order bits set. In order to fit into a shared storage slot, these have to be set to zero. When a conversion is done at the same time, the bits to set to zero were incorrectly determined from the source and not the target type. This means that such copy operations can lead to incorrect values being stored.",
        "link": "https://blog.soliditylang.org/2019/06/25/solidity-storage-array-bugs/",
        "introduced": "0.4.7",
        "fixed": "0.5.10",
        "severity": "low/medium"
    },
    {
        "uid": "SOL-2019-7",
        "name": "ABIEncoderV2StorageArrayWithMultiSlotElement",
        "summary": "Storage arrays containing structs or other statically-sized arrays are not read properly when directly encoded in external function calls or in abi.encode*.",
        "description": "When storage arrays whose elements occupy more than a single storage slot are directly encoded in external function calls or using abi.encode*, their elements are read in an overlapping manner, i.e. the element pointer is not properly advanced between reads. This is not a problem when the storage data is first copied to a memory variable or if the storage array only contains value types or dynamically-sized arrays.",
        "link": "https://blog.soliditylang.org/2019/06/25/solidity-storage-array-bugs/",
        "introduced": "0.4.16",
        "fixed": "0.5.10",
        "severity": "low",
        "conditions": {
            "ABIEncoderV2": true
        }
    },
    {
        "uid": "SOL-2019-6",
        "name": "DynamicConstructorArgumentsClippedABIV2",
        "summary": "A contract's constructor that takes structs or arrays that contain dynamically-sized arrays reverts or decodes to invalid data.",
        "description": "During construction of a contract, constructor parameters are copied from the code section to memory for decoding. The amount of bytes to copy was calculated incorrectly in case all parameters are statically-sized but contain dynamically-sized arrays as struct members or inner arrays. Such types are only available if ABIEncoderV2 is activated.",
        "introduced": "0.4.16",
        "fixed": "0.5.9",
        "severity": "very low",
        "conditions": {
            "ABIEncoderV2": true
        }
    },
    {
        "uid": "SOL-2019-5",
        "name": "UninitializedFunctionPointerInConstructor",
        "summary": "Calling uninitialized internal function pointers created in the constructor does not always revert and can cause unexpected behaviour.",
        "description": "Uninitialized internal function pointers point to a special piece of code that causes a revert when called. Jump target positions are different during construction and after deployment, but the code for setting this special jump target only considered the situation after deployment.",
        "introduced": "0.5.0",
        "fixed": "0.5.8",
        "severity": "very low"
    },
    {
        "uid": "SOL-2019-5",
        "name": "UninitializedFunctionPointerInConstructor_0.4.x",
        "summary": "Calling uninitialized internal function pointers created in the constructor does not always revert and can cause unexpected behaviour.",
        "description": "Uninitialized internal function pointers point to a special piece of code that causes a revert when called. Jump target positions are different during construction and after deployment, but the code for setting this special jump target only considered the situation after deployment.",
        "introduced": "0.4.5",
        "fixed": "0.4.26",
        "severity": "very low"
    },
    {
        "uid": "SOL-2019-4",
        "name": "IncorrectEventSignatureInLibraries",
        "summary": "Contract types used in events in libraries cause an incorrect event signature hash",
        "description": "Instead of using the type `address` in the hashed signature, the actual contract name was used, leading to a wrong hash in the logs.",
        "introduced": "0.5.0",
        "fixed": "0.5.8",
        "severity": "very low"
    },
    {
        "uid": "SOL-2019-4",
        "name": "IncorrectEventSignatureInLibraries_0.4.x",
        "summary": "Contract types used in events in libraries cause an incorrect event signature hash",
        "description": "Instead of using the type `address` in the hashed signature, the actual contract name was used, leading to a wrong hash in the logs.",
        "introduced": "0.3.0",
        "fixed": "0.4.26",
        "severity": "very low"
    },
    {
        "uid": "SOL-2019-3",
        "name": "ABIEncoderV2PackedStorage",
        "summary": "Storage structs and arrays with types shorter than 32 bytes can cause data corruption if encoded directly from storage using the experimental ABIEncoderV2.",
        "description": "Elements of structs and arrays that are shorter than 32 bytes are not properly decoded from storage when encoded directly (i.e. not via a memory type) using ABIEncoderV2. This can cause corruption in the values themselves but can also overwrite other parts of the encoded data.",
        "link": "https://blog.soliditylang.org/2019/03/26/solidity-optimizer-and-abiencoderv2-bug/",
        "introduced": "0.5.0",
        "fixed": "0.5.7",
        "severity": "low",
        "conditions": {
            "ABIEncoderV2": true
        }
    },
    {
        "uid": "SOL-2019-3",
        "name": "ABIEncoderV2PackedStorage_0.4.x",
        "summary": "Storage structs and arrays with types shorter than 32 bytes can cause data corruption if encoded directly from storage using the experimental ABIEncoderV2.",
        "description": "Elements of structs and arrays that are shorter than 32 bytes are not properly decoded from storage when encoded directly (i.e. not via a memory type) using ABIEncoderV2. This can cause corruption in the values themselves but can also overwrite other parts of the encoded data.",
        "link": "https://blog.soliditylang.org/2019/03/26/solidity-optimizer-and-abiencoderv2-bug/",
        "introduced": "0.4.19",
        "fixed": "0.4.26",
        "severity": "low",
        "conditions": {
            "ABIEncoderV2": true
        }
    },
    {
        "uid": "SOL-2019-2",
        "name": "IncorrectByteInstructionOptimization",
        "summary": "The optimizer incorrectly handles byte opcodes whose second argument is 31 or a constant expression that evaluates to 31. This can result in unexpected values.",
        "description": "The optimizer incorrectly handles byte opcodes that use the constant 31 as second argument. This can happen when performing index access on bytesNN types with a compile-time constant value (not index) of 31 or when using the byte opcode in inline assembly.",
        "link": "https://blog.soliditylang.org/2019/03/26/solidity-optimizer-and-abiencoderv2-bug/",
        "introduced": "0.5.5",
        "fixed": "0.5.7",
        "severity": "very low",
        "conditions": {
            "optimizer": true
        }
    },
    {
        "uid": "SOL-2019-1",
        "name": "DoubleShiftSizeOverflow",
        "summary": "Double bitwise shifts by large constants whose sum overflows 256 bits can result in unexpected values.",
        "description": "Nested logical shift operations whose total shift size is 2**256 or more are incorrectly optimized. This only applies to shifts by numbers of bits that are compile-time constant expressions.",
        "link": "https://blog.soliditylang.org/2019/03/26/solidity-optimizer-and-abiencoderv2-bug/",
        "introduced": "0.5.5",
        "fixed": "0.5.6",
        "severity": "low",
        "conditions": {
            "optimizer": true,
            "evmVersion": ">=constantinople"
        }
    },
    {
        "uid": "SOL-2018-4",
        "name": "ExpExponentCleanup",
        "summary": "Using the ** operator with an exponent of type shorter than 256 bits can result in unexpected values.",
        "description": "Higher order bits in the exponent are not properly cleaned before the EXP opcode is applied if the type of the exponent expression is smaller than 256 bits and not smaller than the type of the base. In that case, the result might be larger than expected if the exponent is assumed to lie within the value range of the type. Literal numbers as exponents are unaffected as are exponents or bases of type uint256.",
        "link": "https://blog.soliditylang.org/2018/09/13/solidity-bugfix-release/",
        "fixed": "0.4.25",
        "severity": "medium/high",
        "check": {"regex-source": "[^/]\\*\\* *[^/0-9 ]"}
    },
    {
        "uid": "SOL-2018-3",
        "name": "EventStructWrongData",
        "summary": "Using structs in events logged wrong data.",
        "description": "If a struct is used in an event, the address of the struct is logged instead of the actual data.",
        "link": "https://blog.soliditylang.org/2018/09/13/solidity-bugfix-release/",
        "introduced": "0.4.17",
        "fixed": "0.4.25",
        "severity": "very low",
        "check": {"ast-compact-json-path": "$..[?(@.nodeType === 'EventDefinition')]..[?(@.nodeType === 'UserDefinedTypeName' && @.typeDescriptions.typeString.startsWith('struct'))]"}
    },
    {
        "uid": "SOL-2018-2",
        "name": "NestedArrayFunctionCallDecoder",
        "summary": "Calling functions that return multi-dimensional fixed-size arrays can result in memory corruption.",
        "description": "If Solidity code calls a function that returns a multi-dimensional fixed-size array, array elements are incorrectly interpreted as memory pointers and thus can cause memory corruption if the return values are accessed. Calling functions with multi-dimensional fixed-size arrays is unaffected as is returning fixed-size arrays from function calls. The regular expression only checks if such functions are present, not if they are called, which is required for the contract to be affected.",
        "link": "https://blog.soliditylang.org/2018/09/13/solidity-bugfix-release/",
        "introduced": "0.1.4",
        "fixed": "0.4.22",
        "severity": "medium",
        "check": {"regex-source": "returns[^;{]*\\[\\s*[^\\] \\t\\r\\n\\v\\f][^\\]]*\\]\\s*\\[\\s*[^\\] \\t\\r\\n\\v\\f][^\\]]*\\][^{;]*[;{]"}
    },
    {
        "uid": "SOL-2018-1",
        "name": "OneOfTwoConstructorsSkipped",
        "summary": "If a contract has both a new-style constructor (using the constructor keyword) and an old-style constructor (a function with the same name as the contract) at the same time, one of them will be ignored.",
        "description": "If a contract has both a new-style constructor (using the constructor keyword) and an old-style constructor (a function with the same name as the contract) at the same time, one of them will be ignored. There will be a compiler warning about the old-style constructor, so contracts only using new-style constructors are fine.",
        "introduced": "0.4.22",
        "fixed": "0.4.23",
        "severity": "very low"
    },
    {
        "uid": "SOL-2017-5",
        "name": "ZeroFunctionSelector",
        "summary": "It is possible to craft the name of a function such that it is executed instead of the fallback function in very specific circumstances.",
        "description": "If a function has a selector consisting only of zeros, is payable and part of a contract that does not have a fallback function and at most five external functions in total, this function is called instead of the fallback function if Ether is sent to the contract without data.",
        "fixed": "0.4.18",
        "severity": "very low"
    },
    {
        "uid": "SOL-2017-4",
        "name": "DelegateCallReturnValue",
        "summary": "The low-level .delegatecall() does not return the execution outcome, but converts the value returned by the functioned called to a boolean instead.",
        "description": "The return value of the low-level .delegatecall() function is taken from a position in memory, where the call data or the return data resides. This value is interpreted as a boolean and put onto the stack. This means if the called function returns at least 32 zero bytes, .delegatecall() returns false even if the call was successful.",
        "introduced": "0.3.0",
        "fixed": "0.4.15",
        "severity": "low"
    },
    {
        "uid": "SOL-2017-3",
        "name": "ECRecoverMalformedInput",
        "summary": "The ecrecover() builtin can return garbage for malformed input.",
        "description": "The ecrecover precompile does not properly signal failure for malformed input (especially in the 'v' argument) and thus the Solidity function can return data that was previously present in the return area in memory.",
        "fixed": "0.4.14",
        "severity": "medium"
    },
    {
        "uid": "SOL-2017-2",
        "name": "SkipEmptyStringLiteral",
        "summary": "If \"\" is used in a function call, the following function arguments will not be correctly passed to the function.",
        "description": "If the empty string literal \"\" is used as an argument in a function call, it is skipped by the encoder. This has the effect that the encoding of all arguments following this is shifted left by 32 bytes and thus the function call data is corrupted.",
        "fixed": "0.4.12",
        "severity": "low"
    },
    {
        "uid": "SOL-2017-1",
        "name": "ConstantOptimizerSubtraction",
        "summary": "In some situations, the optimizer replaces certain numbers in the code with routines that compute different numbers.",
        "description": "The optimizer tries to represent any number in the bytecode by routines that compute them with less gas. For some special numbers, an incorrect routine is generated. This could allow an attacker to e.g. trick victims about a specific amount of ether, or function calls to call different functions (or none at all).",
        "link": "https://blog.soliditylang.org/2017/05/03/solidity-optimizer-bug/",
        "fixed": "0.4.11",
        "severity": "low",
        "conditions": {
            "optimizer": true
        }
    },
    {
        "uid": "SOL-2016-11",
        "name": "IdentityPrecompileReturnIgnored",
        "summary": "Failure of the identity precompile was ignored.",
        "description": "Calls to the identity contract, which is used for copying memory, ignored its return value. On the public chain, calls to the identity precompile can be made in a way that they never fail, but this might be different on private chains.",
        "severity": "low",
        "fixed": "0.4.7"
    },
    {
        "uid": "SOL-2016-10",
        "name": "OptimizerStateKnowledgeNotResetForJumpdest",
        "summary": "The optimizer did not properly reset its internal state at jump destinations, which could lead to data corruption.",
        "description": "The optimizer performs symbolic execution at certain stages. At jump destinations, multiple code paths join and thus it has to compute a common state from the incoming edges. Computing this common state was simplified to just use the empty state, but this implementation was not done properly. This bug can cause data corruption.",
        "severity": "medium",
        "introduced": "0.4.5",
        "fixed": "0.4.6",
        "conditions": {
            "optimizer": true
        }
    },
    {
        "uid": "SOL-2016-9",
        "name": "HighOrderByteCleanStorage",
        "summary": "For short types, the high order bytes were not cleaned properly and could overwrite existing data.",
        "description": "Types shorter than 32 bytes are packed together into the same 32 byte storage slot, but storage writes always write 32 bytes. For some types, the higher order bytes were not cleaned properly, which made it sometimes possible to overwrite a variable in storage when writing to another one.",
        "link": "https://blog.soliditylang.org/2016/11/01/security-alert-solidity-variables-can-overwritten-storage/",
        "severity": "high",
        "introduced": "0.1.6",
        "fixed": "0.4.4"
    },
    {
        "uid": "SOL-2016-8",
        "name": "OptimizerStaleKnowledgeAboutSHA3",
        "summary": "The optimizer did not properly reset its knowledge about SHA3 operations resulting in some hashes (also used for storage variable positions) not being calculated correctly.",
        "description": "The optimizer performs symbolic execution in order to save re-evaluating expressions whose value is already known. This knowledge was not properly reset across control flow paths and thus the optimizer sometimes thought that the result of a SHA3 operation is already present on the stack. This could result in data corruption by accessing the wrong storage slot.",
        "severity": "medium",
        "fixed": "0.4.3",
        "conditions": {
            "optimizer": true
        }
    },
    {
        "uid": "SOL-2016-7",
        "name": "LibrariesNotCallableFromPayableFunctions",
        "summary": "Library functions threw an exception when called from a call that received Ether.",
        "description": "Library functions are protected against sending them Ether through a call. Since the DELEGATECALL opcode forwards the information about how much Ether was sent with a call, the library function incorrectly assumed that Ether was sent to the library and threw an exception.",
        "severity": "low",
        "introduced": "0.4.0",
        "fixed": "0.4.2"
    },
    {
        "uid": "SOL-2016-6",
        "name": "SendFailsForZeroEther",
        "summary": "The send function did not provide enough gas to the recipient if no Ether was sent with it.",
        "description": "The recipient of an Ether transfer automatically receives a certain amount of gas from the EVM to handle the transfer. In the case of a zero-transfer, this gas is not provided which causes the recipient to throw an exception.",
        "severity": "low",
        "fixed": "0.4.0"
    },
    {
        "uid": "SOL-2016-5",
        "name": "DynamicAllocationInfiniteLoop",
        "summary": "Dynamic allocation of an empty memory array caused an infinite loop and thus an exception.",
        "description": "Memory arrays can be created provided a length. If this length is zero, code was generated that did not terminate and thus consumed all gas.",
        "severity": "low",
        "fixed": "0.3.6"
    },
    {
        "uid": "SOL-2016-4",
        "name": "OptimizerClearStateOnCodePathJoin",
        "summary": "The optimizer did not properly reset its internal state at jump destinations, which could lead to data corruption.",
        "description": "The optimizer performs symbolic execution at certain stages. At jump destinations, multiple code paths join and thus it has to compute a common state from the incoming edges. Computing this common state was not done correctly. This bug can cause data corruption, but it is probably quite hard to use for targeted attacks.",
        "severity": "low",
        "fixed": "0.3.6",
        "conditions": {
            "optimizer": true
        }
    },
    {
        "uid": "SOL-2016-3",
        "name": "CleanBytesHigherOrderBits",
        "summary": "The higher order bits of short bytesNN types were not cleaned before comparison.",
        "description": "Two variables of type bytesNN were considered different if their higher order bits, which are not part of the actual value, were different. An attacker might use this to reach seemingly unreachable code paths by providing incorrectly formatted input data.",
        "severity": "medium/high",
        "fixed": "0.3.3"
    },
    {
        "uid": "SOL-2016-2",
        "name": "ArrayAccessCleanHigherOrderBits",
        "summary": "Access to array elements for arrays of types with less than 32 bytes did not correctly clean the higher order bits, causing corruption in other array elements.",
        "description": "Multiple elements of an array of values that are shorter than 17 bytes are packed into the same storage slot. Writing to a single element of such an array did not properly clean the higher order bytes and thus could lead to data corruption.",
        "severity": "medium/high",
        "fixed": "0.3.1"
    },
    {
        "uid": "SOL-2016-1",
        "name": "AncientCompiler",
        "summary": "This compiler version is ancient and might contain several undocumented or undiscovered bugs.",
        "description": "The list of bugs is only kept for compiler versions starting from 0.3.0, so older versions might contain undocumented bugs.",
        "severity": "high",
        "fixed": "0.3.0"
    }
]

Solidity v0.5.0の破壊的変更点

このセクションでは、Solidityバージョン0.5.0で導入された主な変更点と、変更の理由、影響を受けるコードの更新方法について説明します。 完全なリストは リリースのチェンジログ を参照してください。

注釈

Solidity v0.5.0でコンパイルされたコントラクトは、古いバージョンでコンパイルされたコントラクトやライブラリを再コンパイルや再配置することなく、それらとインターフェースをとることができます。 データの場所やビジビリティとミュータビリティの指定子を含むようにインターフェースを変更すれば十分です。 以下の 従来のコントラクトとの相互運用性 セクションを参照してください。

セマンティックのみの変更点

このセクションでは、セマンティックのみの変更点をリストアップしています。 そのため、既存のコードの中に新しい、あるいは異なる動作が隠されている可能性があります。

  • 符号付き右シフトでは、ゼロに丸めるのではなく、負の無限大に丸めるなど、適切な算術的シフトが使用されるようになりました。 符号付きと符号なしのシフトは、Constantinopleでは専用のオペコードが用意されていますが、現時点ではSolidityでエミュレートされています。

  • do...while ループ内の continue 文は、条件にジャンプするようになりましたが、これはこのような場合の一般的な動作です。 以前は、ループ本体にジャンプしていました。 したがって、条件が偽の場合、ループは終了します。

  • 関数 .call().delegatecall().staticcall() は、単一の bytes パラメータが与えられた場合、パッドがなくなります。

  • EVMのバージョンがByzantium以降の場合、PureおよびView関数は CALL ではなく STATICCALL というオペコードで呼び出されるようになりました。 これにより、EVMレベルでの状態変更ができなくなります。

  • ABIエンコーダーは、外部関数呼び出しや abi.encode で使用される calldata ( msg.data および外部関数パラメーター)からのバイト配列や文字列を適切にパッドするようになりました。 パッドされていないエンコーディングには、 abi.encodePacked を使用してください。

  • ABIデコーダは、関数の先頭や abi.decode() で、渡されたcalldataが短すぎたり、境界外を指したりした場合には、リバートします。 なお、ダーティな高次ビットはまだ単純に無視されます。

  • Tangerine Whistleから始まる外部関数呼び出しで、利用可能なすべてのガスを転送します。

セマンティックかつシンタックスの変更点

このセクションでは、シンタックスとセマンティックに影響する変更点を紹介します。

  • 関数 .call().delegatecall()staticcall()keccak256()sha256()ripemd160() は、 bytes の引数を1つだけ受け付けるようになりました。 さらに、この引数はパディングされません。 これは、引数がどのように連結されるかをより明示的かつ明確にするために変更されました。 すべての .call() (およびファミリー)を .call("") に、すべての .call(signature, a, b, c).call(abi.encodeWithSignature(signature, a, b, c)) に変更しました(最後のものはvalue型でのみ機能します)。 すべての keccak256(a, b, c)keccak256(abi.encodePacked(a, b, c)) に変更しました。 壊すような変更ではありませんが、開発者は x.call(bytes4(keccak256("f(uint256)")), a, b)x.call(abi.encodeWithSignature("f(uint256)", a, b)) に変更することを提案します。

  • 関数 .call().delegatecall().staticcall()(bool, bytes memory) を返すようになり、戻りデータへのアクセスが可能になりました。 bool success = otherContract.call("f")(bool success, bytes memory data) = otherContract.call("f") に変更します。

  • Solidityは、関数のローカル変数にC99スタイルのスコープルールを実装しました。 つまり、変数は宣言された後にのみ使用でき、同じスコープまたはネストされたスコープ内でのみ使用できます。 for ループの初期化ブロックで宣言された変数は、ループ内のどの時点でも有効です。

明示的な要件

このセクションでは、コードをより明確にする必要がある変更点を示します。 ほとんどの項目では、コンパイラが提案をしてくれます。

  • 関数の明示的な可視化が必須になりました。 すべての関数とコンストラクタに public を追加し、ビジビリティを指定していないすべてのフォールバック関数やインターフェース関数に external を追加します。

  • 構造体(struct)、配列(array)、マッピング(mapping)型のすべての変数について、明示的なデータ配置が必須となりました。 これは、関数のパラメータやリターン変数にも適用されます。 例えば、 uint[] x = zuint[] storage x = z に、 function f(uint[][] x)function f(uint[][] memory x) に変更すると、 memory がデータロケーションとなり、 storagecalldata に適宜置き換えられます。 なお、 external 関数ではデータロケーションが calldata のパラメータが必要です。

  • 名前空間を分離するために、コントラクト型には address メンバーが含まれなくなりました。 そのため、 address メンバを使用する前に、コントラクト型の値を明示的にアドレスに変換する必要があります。 例: c がコントラクトの場合、 c.transfer(...)address(c).transfer(...) に、 c.balanceaddress(c).balance に変更します。

  • 関連性のないコントラクト型間の明示的な変換ができなくなりました。 あるコントラクト型から、そのベースまたは祖先の型の1つへの変換のみが可能です。 あるコントラクトが、変換したいコントラクト型を継承していないものの、互換性があると確信している場合、最初に address に変換することでこれを回避できます。 例: AB がコントラクト型で、 BA から継承されず、 bB 型のコントラクトである場合、 A(address(b)) を使って bA 型に変換できます。 なお、以下に説明するように、マッチングで支払い可能なフォールバック関数にも注意する必要があります。

  • address 型は addressaddress payable に分割され、 address payable のみが transfer 関数を提供しています。 address payable を直接 address に変換できますが、その逆はできません。 address から address payable への変換は、 uint160 による変換で可能です。 c がコントラクトの場合、 address(c) は、 c に支払い可能なフォールバック関数がある場合に限り、 address payable になります。 withdraw pattern を使用している場合、 transfer はストアドアドレスではなく msg.sender でのみ使用され、 msg.senderaddress payable になるので、コードを変更する必要はほとんどありません。

  • 異なるサイズの bytesXuintY の間の変換は、 bytesX のパディングが右に、 uintY のパディングが左にあるため、予期しない変換結果を引き起こす可能性があるため、許可されなくなりました。 変換の前に、型内でサイズを調整する必要があるようになりました。 例えば、 bytes4 (4バイト)を uint64 (8バイト)に変換するには、まず bytes4 の変数を bytes8 に変換し、次に uint64 に変換します。 uint32 で変換すると逆にパディングされてしまいます。 v0.5.0以前のバージョンでは、 bytesXuintY の間の変換は uint8X を経由します。 例えば、 uint8(bytes3(0x291807))uint8(uint24(bytes3(0x291807))) に変換されます(結果は 0x07 )。

  • 払えない関数で msg.value を使う(またはモディファイアで導入する)ことは、セキュリティ機能として認められていません。 関数を payable に変えるか、 msg.value を使用するプログラムロジックのために新しい内部関数を作成してください。

  • わかりやすくするために、コマンドラインインターフェースでは、標準入力をソースとして使用する場合、 - を要求するようになりました。

非推奨の要素

このセクションでは、以前の機能や構文を廃止する変更点を紹介します。 これらの変更点の多くは、実験モードの v0.5.0 ですでに有効になっていることに注意してください。

コマンドラインインターフェースとJSONインターフェース

  • コマンドラインオプションの --formal (さらなる形式検証のためにWhy3出力を生成するために使用)は非推奨であり、現在は削除されています。 新しいフォーマル検証モジュールであるSMTCheckerは、 pragma experimental SMTChecker; を介して有効になります。

  • 中間言語 JuliaYul に名称変更されたことに伴い、コマンドラインオプション --julia--yul に名称変更されました。

  • --clone-bin および --combined-json clone-bin コマンドラインオプションが削除されました。

  • 空のプレフィックスを持つリマッピングは許可されません。

  • JSON ASTフィールドの constantpayable が削除されました。 情報は stateMutability フィールドに存在するようになりました。

  • FunctionDefinition ノードのJSON ASTフィールド isConstructor が、 "constructor""fallback""function" の値を持つことができる kind というフィールドに置き換えられました。

  • リンクされていないバイナリ16進数ファイルでは、ライブラリアドレスのプレースホルダーが、完全修飾ライブラリ名のkeccak256ハッシュの最初の3616文字を $...$ で囲んだものになりました。 以前は、完全修飾ライブラリ名のみが使用されていました。 これにより、特に長いパスを使用している場合に、衝突の可能性が低くなります。 バイナリファイルには、これらのプレースホルダーから完全修飾名へのマッピングのリストも含まれるようになりました。

コンストラクタ

  • コンストラクタは、 constructor キーワードを使って定義する必要があります。

  • ベースコンストラクタを括弧なしで呼び出すことができなくなりました。

  • ベースコンストラクタの引数を同じ継承階層で複数回指定できなくなりました。

  • 引数を持つコンストラクタを、間違った引数数で呼び出すことはできなくなりました。 引数を与えずに継承関係だけを指定したい場合は、括弧を一切付けないでください。

関数

  • callcode 関数は、現在では使用できません( delegatecall に変更)。 ただし、インラインアセンブリで使用することは可能です。

  • suicide は( selfdestruct を優先して)不許可になりました。

  • sha3 は( keccak256 を優先して)不許可になりました。

  • throw は現在、( revertrequireassert に代わって)不許可となっています。

変換

  • 10進数のリテラルから bytesXX 型への明示的、暗黙的な変換ができなくなりました。

  • 16進数のリテラルから異なるサイズの bytesXX 型への明示的および暗黙的な変換ができなくなりました。

リテラルと接尾辞

  • 単位表記の years は、うるう年の複雑さと混乱のため、現在は認められていません。

  • 数字を含まない末尾のドットは使用できません。

  • 16進数と単位表記(例: 0x1e wei )の組み合わせができなくなりました。

  • 16進数の接頭辞 0X は使用できず、 0x のみ使用可能です。

変数

  • 空の構造体を宣言することは、わかりやすくするために禁止されました。

  • var キーワードを使用しないようにしたことで、明示性が確保されました。

  • コンポーネントの数が異なるタプル間の割り当てができなくなりました。

  • コンパイル時の定数ではない定数の値は許されません。

  • 値の数が不一致の複数変数の宣言ができなくなりました。

  • 初期化されていないストレージ変数が禁止されるようになりました。

  • 空のタプル構成要素が許されなくなりました。

  • 変数や構造体の周期的な依存関係の検出は、再帰的に256に制限されます。

  • 長さがゼロの固定サイズの配列が禁止されるようになりました。

シンタックス

  • constant を関数のステートミュータビリティのモディファイアとして使用できなくなりました。

  • ブール式では、算術演算は使えません。

  • 単項の + 演算子が使えなくなりました。

  • リテラルは、事前に明示的な型に変換することなく、 abi.encodePacked で使用できなくなりました。

  • 1つ以上の戻り値を持つ関数の空の戻り文は認められなくなりました。

  • つまり、ジャンプラベルやジャンプ、機能しない命令はもう使用できません。 代わりに新しい whileswitchif 構文を使ってください。

  • 実装のない関数では、モディファイアが使えなくなりました。

  • 名前付きの戻り値を持つ関数型が禁止されるようになりました。

  • ブロックではないif/while/forボディ内の単一の文の変数宣言が禁止されました。

  • 新しいキーワードです。 calldataconstructor です。

  • 新しい予約キーワードです: alias , apply , auto , copyof , define , immutable , implements , macro , mutable , override , partial , promise , reference , sealed , sizeof , supports , typedef , unchecked

従来のコントラクトとの相互運用性

0.5.0より前のバージョンのSolidityで書かれたコントラクトにインターフェースを定義することで、コントラクトとインターフェースを結ぶことができます。 以下の0.5.0以前のコントラクトがすでにデプロイされているとします。

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.4.25;
// This will report a warning until version 0.4.25 of the compiler
// This will not compile after 0.5.0
contract OldContract {
    function someOldFunction(uint8 a) {
        //...
    }
    function anotherOldFunction() constant returns (bool) {
        //...
    }
    // ...
}

これはSolidity v0.5.0ではコンパイルされなくなります。 ただし、互換性のあるインターフェースを定義することは可能です:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;
interface OldContract {
    function someOldFunction(uint8 a) external;
    function anotherOldFunction() external returns (bool);
}

オリジナルのコントラクトでは constant と宣言されていたにもかかわらず、 anotherOldFunctionview と宣言していないことに注意してください。 これは、Solidity v0.5.0から view 関数のコールに staticcall が使われるようになったことによります。 v0.5.0以前は constant キーワードが強制されていなかったため、 constant と宣言された関数を staticcall で呼び出しても、 constant 関数がストレージを変更しようとする可能性があるため、リバートする可能性があります。 したがって、古いコントラクトのインターフェースを定義する際には、その関数が staticcall で動作することが絶対的に確認できる場合にのみ、 constant の代わりに view を使用する必要があります。

上記で定義されたインターフェースがあれば、すでにデプロイされたpre-0.5.0のコントラクトを簡単に使用できます。

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

interface OldContract {
    function someOldFunction(uint8 a) external;
    function anotherOldFunction() external returns (bool);
}

contract NewContract {
    function doSomething(OldContract a) public returns (bool) {
        a.someOldFunction(0x42);
        return a.anotherOldFunction();
    }
}

同様に、0.5.0以前のライブラリも、実装せずにライブラリの関数を定義し、リンク時に0.5.0以前のライブラリのアドレスを指定することで使用できます(リンク時のコマンドラインコンパイラの使用方法については コマンドラインコンパイラの使い方 をご参照ください)。

// This will not compile after 0.6.0
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.5.0;

library OldLibrary {
    function someFunction(uint8 a) public returns(bool);
}

contract NewContract {
    function f(uint8 a) public returns (bool) {
        return OldLibrary.someFunction(a);
    }
}

次の例は、Solidity v0.5.0のコントラクトとそのアップデート版で、このセクションに記載されている変更点があります。

古いバージョンです:

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.4.25;
// This will not compile after 0.5.0

contract OtherContract {
    uint x;
    function f(uint y) external {
        x = y;
    }
    function() payable external {}
}

contract Old {
    OtherContract other;
    uint myNumber;

    // Function mutability not provided, not an error.
    function someInteger() internal returns (uint) { return 2; }

    // Function visibility not provided, not an error.
    // Function mutability not provided, not an error.
    function f(uint x) returns (bytes) {
        // Var is fine in this version.
        var z = someInteger();
        x += z;
        // Throw is fine in this version.
        if (x > 100)
            throw;
        bytes memory b = new bytes(x);
        y = -3 >> 1;
        // y == -1 (wrong, should be -2)
        do {
            x += 1;
            if (x > 10) continue;
            // 'Continue' causes an infinite loop.
        } while (x < 11);
        // Call returns only a Bool.
        bool success = address(other).call("f");
        if (!success)
            revert();
        else {
            // Local variables could be declared after their use.
            int y;
        }
        return b;
    }

    // No need for an explicit data location for 'arr'
    function g(uint[] arr, bytes8 x, OtherContract otherContract) public {
        otherContract.transfer(1 ether);

        // Since uint32 (4 bytes) is smaller than bytes8 (8 bytes),
        // the first 4 bytes of x will be lost. This might lead to
        // unexpected behavior since bytesX are right padded.
        uint32 y = uint32(x);
        myNumber += y + msg.value;
    }
}

新バージョンです:

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.5.0;
// This will not compile after 0.6.0

contract OtherContract {
    uint x;
    function f(uint y) external {
        x = y;
    }
    function() payable external {}
}

contract New {
    OtherContract other;
    uint myNumber;

    // Function mutability must be specified.
    function someInteger() internal pure returns (uint) { return 2; }

    // Function visibility must be specified.
    // Function mutability must be specified.
    function f(uint x) public returns (bytes memory) {
        // The type must now be explicitly given.
        uint z = someInteger();
        x += z;
        // Throw is now disallowed.
        require(x <= 100);
        int y = -3 >> 1;
        require(y == -2);
        do {
            x += 1;
            if (x > 10) continue;
            // 'Continue' jumps to the condition below.
        } while (x < 11);

        // Call returns (bool, bytes).
        // Data location must be specified.
        (bool success, bytes memory data) = address(other).call("f");
        if (!success)
            revert();
        return data;
    }

    using AddressMakePayable for address;
    // Data location for 'arr' must be specified
    function g(uint[] memory /* arr */, bytes8 x, OtherContract otherContract, address unknownContract) public payable {
        // 'otherContract.transfer' is not provided.
        // Since the code of 'OtherContract' is known and has the fallback
        // function, address(otherContract) has type 'address payable'.
        address(otherContract).transfer(1 ether);

        // 'unknownContract.transfer' is not provided.
        // 'address(unknownContract).transfer' is not provided
        // since 'address(unknownContract)' is not 'address payable'.
        // If the function takes an 'address' which you want to send
        // funds to, you can convert it to 'address payable' via 'uint160'.
        // Note: This is not recommended and the explicit type
        // 'address payable' should be used whenever possible.
        // To increase clarity, we suggest the use of a library for
        // the conversion (provided after the contract in this example).
        address payable addr = unknownContract.makePayable();
        require(addr.send(1 ether));

        // Since uint32 (4 bytes) is smaller than bytes8 (8 bytes),
        // the conversion is not allowed.
        // We need to convert to a common size first:
        bytes4 x4 = bytes4(x); // Padding happens on the right
        uint32 y = uint32(x4); // Conversion is consistent
        // 'msg.value' cannot be used in a 'non-payable' function.
        // We need to make the function payable
        myNumber += y + msg.value;
    }
}

// We can define a library for explicitly converting ``address``
// to ``address payable`` as a workaround.
library AddressMakePayable {
    function makePayable(address x) internal pure returns (address payable) {
        return address(uint160(x));
    }
}

Solidity v0.6.0の破壊的変更点

このセクションでは、Solidityバージョン0.6.0で導入された主な変更点と、変更の理由、影響を受けるコードの更新方法について説明します。 完全なリストは リリースチェンジログ を参照してください。

コンパイラが警告しない可能性のある変更点

このセクションでは、コンパイラーが教えてくれないのにコードの動作が変更される可能性のある変更点を挙げます。

  • 指数計算の結果として得られる型は、基数の型です。 以前は、対称演算のように、基数の型と指数の型の両方を保持できる最小の型でした。 さらに、指数の底には符号付きの型が許されています。

明示的な要件

このセクションでは、コードをより明確にする必要があるが、セマンティクスは変わらないという変更点を挙げます。 ほとんどの項目では、コンパイラが提案をしてくれます。

  • 関数は、 virtual キーワードでマークされているか、インターフェースで定義されている場合にのみオーバーライドできるようになりました。 インターフェースの外で実装されていない関数は、 virtual とマークされなければなりません。 関数やモディファイアをオーバーライドする際には、新しいキーワード override を使用しなければなりません。 複数の並列ベースで定義された関数やモディファイアをオーバーライドする場合、キーワードの後の括弧内にすべてのベースを以下のように記載する必要があります: override(Base1, Base2)

  • 配列の length へのメンバーアクセスは、ストレージ配列であっても常に読み取り専用になりました。 ストレージ配列の長さに新しい値を割り当ててサイズを変更できなくなりました。 代わりに push()push(value)pop() を使用するか、完全な配列を割り当てると、当然ながら既存のコンテンツは上書きされます。 この理由は、巨大なストレージ配列のストレージ衝突を防ぐためです。

  • 新しいキーワード abstract は、コントラクトを抽象的にマークするために使用できます。 これはコントラクトがそのすべての関数を実装していない場合に使用しなければなりません。 抽象コントラクトは new 演算子を使って作成できませんし、コンパイル時にコントラクト用のバイトコードを生成することもできません。

  • ライブラリは、内部だけでなく、すべての関数を実装しなければなりません。

  • インラインアセンブリで宣言された変数の名前は、 _slot_offset で終わることはありません。

  • インラインアセンブリ内の変数宣言は、インラインアセンブリブロック外の宣言を影で支えることはできません。 名前にドットが含まれている場合、ドットまでの接頭辞はインラインアセンブリブロック外の宣言と衝突してはなりません。

  • インラインアセンブリでは、引数をとらないオペコードは、独立した識別子ではなく、「組み込み関数」として表現されるようになりました。 つまり、 gasgas() になりました。

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

セマンティックかつシンタックスの変更点

このセクションでは、コードを修正する必要があり、その後に何か別のことが行われる変更点をリストアップしています。

  • 外部関数型から address への変換は認められなくなりました。 その代わりに、外部関数型は既存の selector メンバと同様に address というメンバを持ちます。

  • 動的ストレージ配列用の関数 push(value) は、新しい長さを返さなくなりました(何も返しません)。

  • 一般的に「fallback関数」と呼ばれる無名関数は、 fallback キーワードで定義される新しいfallback関数と、 receive キーワードで定義されるreceive Ether関数に分割されました。

  • 存在する場合、コールデータが空になるたびに(Etherを受信したかどうかに関わらず)receive Ether関数が呼び出されます。 この関数は暗黙のうちに payable です。

  • 新しいフォールバック関数は、他の関数がマッチしない場合に呼び出されます(receive Ether関数が存在しない場合は、コールデータが空のコールも含まれます)。 この関数を payable にするかどうかは自由です。 payable でない場合は、値を送信する他の関数にマッチしないトランザクションがリバートします。 新しいフォールバック関数を実装する必要があるのは、アップグレードやプロキシのパターンに従っている場合だけです。

新機能

このセクションでは、Solidity 0.6.0以前では実現できなかったことや、実現が困難だったことを挙げています。

  • try/catch文 では、失敗した外部呼び出しに反応できます。

  • struct および enum 型は、ファイルレベルで宣言できます。

  • 例えば abi.decode(msg.data[4:], (uint, uint)) は関数呼び出しのペイロードをデコードする低レベルな方法です。

  • Natspecは開発者向けドキュメントで複数のリターンパラメータをサポートし、 @param と同じネーミングチェックを実施します。

  • YulとInline Assemblyには、現在の関数を終了させる leave という新しい文があります。

  • address から address payable への変換は payable(x) を介して可能になりました。

インターフェースの変更点

このセクションでは、言語そのものとは関係なく、コンパイラーのインターフェースに影響を与える変更点を紹介します。 これらの変更により、コマンドラインでのコンパイラの使用方法、プログラマブルインターフェースの使用方法、コンパイラが生成した出力の分析方法が変わる可能性があります。

新しいエラーリポーター

新しいエラーレポーターが導入されました。 これは、コマンドライン上でよりアクセスしやすいエラーメッセージを生成することを目的としています。 デフォルトでは有効になっていますが、 --old-reporter を指定すると、非推奨の古いエラーレポーターに戻ります。

メタデータハッシュオプション

コンパイラは、メタデータファイルの IPFS ハッシュをデフォルトでバイトコードの最後に追加するようになりました(詳細については、 コントラクトメタデータ のドキュメントを参照してください)。 0.6.0より前のバージョンでは、コンパイラはデフォルトで Swarm ハッシュを付加していましたが、この動作を引き続きサポートするために、新しいコマンドラインオプション --metadata-hash が導入されました。 --metadata-hash コマンドラインオプションの値として ipfs または swarm を渡すことで、生成および付加されるハッシュを選択できます。 none という値を渡すと、ハッシュが完全に削除されます。

これらの変更は、 標準JSONインターフェース を介して使用することもでき、コンパイラによって生成されるメタデータJSONに影響を与えます。

推奨されるメタデータの読み方は、最後の2バイトを読んでCBORエンコーディングの長さを判断し、 メタデータのセクション で説明されているようにそのデータブロックに対して適切なデコーディングを行うことです。

Yulオプティマイザ

レガシーのバイトコードオプティマイザとともに、 Yul オプティマイザが --optimize でコンパイラーを呼び出したときにデフォルトで有効になりました。 これを無効にするには、 --no-optimize-yul でコンパイラを呼び出します。 これは主に ABI coder v2 を使用しているコードに影響します。

C APIの変更点

libsolc のC APIを使用するクライアントコードは、コンパイラが使用するメモリを制御するようになりました。 この変更に一貫性を持たせるために、 solidity_freesolidity_reset に改名され、関数 solidity_allocsolidity_free が追加され、 solidity_compilesolidity_free() を介して明示的に解放しなければならない文字列を返すようになりました。

コードのアップデート方法

このセクションでは、全ての変更点のために以前のコードを更新する方法を詳しく説明しています。

  • f が外部関数型のため、 address(f)f.address に変更してください。

  • function () external [payable] { ... }receive() external payable { ... }fallback() external [payable] { ... } のいずれか、または両方で置き換えてください。 可能な限り、 receive 関数のみを使用してください。

  • uint length = array.push(value)array.push(value); に変更してください。 新しい長さは array.length からアクセスできます。

  • ストレージ配列の長さを増やすには array.length++array.push() に変更し、減らすには pop() を使用してください。

  • 関数の @dev ドキュメントでは、名前のついたリターンパラメータごとに、パラメータの名前を最初の単語として含む @return エントリを定義します。 例えば、関数 f()function f() public returns (uint value) のように定義されていて、それに注釈をつけた @dev がある場合、その戻りパラメータを次のように文書化します: @return value The return value. 。 タプルの戻り値の型に表示されている順序で通知を行う限り、名前のある戻り値パラメータと名前のない戻り値パラメータの文書を混在させることができます。

  • インラインアセンブリ内の変数宣言には、インラインアセンブリブロック外の宣言と衝突しないように、一意の識別子を選択してください。

  • オーバーライドしようとするすべての非インタフェース関数に virtual を追加してください。 インターフェースの外にある実装のないすべての関数に virtual を追加してください。 単一継承の場合は、オーバーライドするすべての関数に override を追加してください。 多重継承の場合は、 override(A, B, ..) を追加し、オーバーライドする関数を定義するすべてのコントラクトを括弧内に列挙してください。 複数のベースが同じ関数を定義している場合、継承するコントラクトは、競合するすべての関数をオーバーライドしなければなりません。

  • インラインアセンブリでは、引数を受け付けないすべてのオペコードに () を追加してください。 例えば、 pcpc() に、 gasgas() に変更してください。

Solidity v0.7.0の破壊的変更点

このセクションでは、Solidityバージョン0.7.0で導入された主な変更点と、変更の理由、影響を受けるコードの更新方法について説明します。 完全なリストは リリースチェンジログ を参照してください。

セマンティクスのサイレントな変更点

  • リテラルの非リテラル( 1 << x2 ** x など)による指数化やシフトは、常に uint256 型(非負のリテラル用)または int256 型(負のリテラル用)を使用して演算を行います。 これまでは、シフト量/指数の型で演算を行っていたため、誤解を招く恐れがありました。

シンタックスの変更点

  • 外部関数やコントラクト作成コールで、Etherやガスが新しい構文で指定されるようになりました。 x.f{gas: 10000, value: 2 ether}(arg1, arg2) です。 従来の構文( x.f.gas(10000).value(2 ether)(arg1, arg2) )ではエラーになります。

  • グローバル変数 now は非推奨であり、代わりに block.timestamp を使用すべきです。 now という単一の識別子は、グローバル変数としては一般的すぎて、トランザクション処理中に変化するような印象を与える可能性がありますが、 block.timestamp は単なるブロックのプロパティであるという事実を正しく反映しています。

  • 変数に関するNatSpecコメントは、パブリックな状態の変数に対してのみ許可され、ローカルまたは内部の変数に対しては許可されません。

  • トークン gwei は、現在のキーワード(例えば 2 gwei を数字で指定するために使用)であり、識別子としては使用できません。

  • 文字列リテラルには、印刷可能なASCII文字のみを含めることができるようになり、16進数( \xff )やユニコードエスケープ( \u20ac )などの様々なエスケープシーケンスも含まれています。

  • Unicode文字列リテラルがサポートされ、有効なUTF-8シーケンスに対応できるようになりました。 これらは unicode という接頭語で識別されます: unicode"Hello 😃"

  • ステートミュータビリティ: 継承の際に、関数のステートミュータビリティを制限できるようになりました。 デフォルトのステートミュータビリティを持つ関数は、 pure および view 関数でオーバーライドでき、 view 関数は pure 関数でオーバーライドできます。 同時に、パブリックな状態変数は view とみなされ、定数であれば pure ともみなされます。

インラインアセンブリ

  • インラインアセンブリのユーザー定義関数および変数名に . を使用できないようにしました。 SolidityをYul-onlyモードで使用している場合も有効です。

  • ストレージポインタ変数 x のスロットとオフセットは、 x_slotx_offset ではなく x.slotx.offset でアクセスされます。

未使用または安全でない機能の削除

ストレージ外のマッピング

  • 構造体や配列にマッピングが含まれている場合、その構造体と配列はストレージでのみ使用できるようになりました。 これまでは、マッピングのメンバーはメモリ内では無視されていたため、混乱してエラーが発生しやすい状況になっていました。

  • ストレージ内の構造体や配列にマッピングが含まれていると代入が動作しなくなるようになりました。 これまでは、マッピングはコピー操作中に自動的に無視されていましたが、これは誤解を招きやすく、エラーが発生しやすい状況になっていました。

関数とイベント

  • コンストラクタにはビジビリティ( public / internal )は必要なくなりました。 コントラクトが作成されないようにするには、 abstract マークを付けることができます。 これにより、コンストラクタのビジビリティの概念は廃止されました。

  • 型チェッカー: ライブラリ関数の virtual を禁止します。 ライブラリは継承できないので、ライブラリ関数は仮想関数であってはなりません。

  • 同一の継承階層に同一名称、同一パラメータ型のイベントが複数存在することは認められません。

  • using A for B は、記載されているコントラクトにのみ影響を与えます。 以前は、この効果は継承されていました。 現在では、この関数を利用するすべての派生コントラクトで using 文を繰り返さなければなりません。

  • 符号付きの型によるシフトは禁止されています。 以前は、負の金額によるシフトは許可されていましたが、実行時にリバートされました。

  • finneyszabo のデノミネーションは削除されています。 これらはほとんど使用されず、実際の金額を容易に確認できません。 代わりに、 1e20 や非常に一般的な gwei のような明確な値を使用できます。

宣言

  • キーワード var が使用できなくなりました。 以前は、このキーワードは解析されますが、型エラーが発生し、どの型を使用すべきかの提案がありました。 現在は、パーサーエラーとなります。

インターフェースの変更点

  • JSON AST: 16進文字列リテラルを kind: "hexString" でマークするようになりました。

  • JSON AST: 値が null のメンバーをJSON出力から削除しました。

  • NatSpec: コンストラクタと関数に一貫したユーザードキュメントを出力するようにしました。

コードのアップデート方法

このセクションでは、変更のたびに先行コードを更新する方法を詳しく説明しています。

  • x.f.value(...)()x.f{value: ...}() に変更してください。同様に (new C).value(...)()new C{value: ...}() に、 x.f.gas(...).value(...)()x.f{gas: ..., value: ...}() にしてください。

  • nowblock.timestamp に変更してください。

  • シフト演算子の右オペランドの型を符号なしに変更してください。例えば、 x >> (256 - y)x >> uint(256 - y) に変更してください。

  • 必要に応じて、すべての派生コントラクトで using A for B 文を繰り返してください。

  • すべてのコンストラクタから public キーワードを削除してください。

  • すべてのコンストラクタから internal キーワードを削除し、コントラクトに abstract を追加してください(まだ存在しない場合)。

  • インラインアセンブリの _slot_offset の接尾辞をそれぞれ .slot.offset に変更してください。

Solidity v0.8.0の破壊的変更点

このセクションでは、Solidityのバージョン0.8.0で導入された主な変更点を紹介します。 完全なリストは リリースチェンジログ を参照してください。

セマンティクスのサイレントな変更点

このセクションでは、既存のコードがコンパイラーに通知されることなく動作を変更する変更点を示します。

  • 算術演算は、アンダーフローとオーバーフローでリバートします。 unchecked { ... } を使えば、以前の折り返し動作を使うことができます。

    オーバーフローのチェックは非常に一般的なものなので、多少ガス代が高くなってもコードの可読性を高めるためにデフォルトにしました。

  • ABI coder v2はデフォルトで起動しています。

    pragma abicoder v1; を使って古い動作を選択できます。 プラグマ pragma experimental ABIEncoderV2; はまだ有効ですが、非推奨であり、効果はありません。 明示的にしたい場合は、代わりに pragma abicoder v2; を使用してください。

    ABI coder v2は、v1よりも多くの型をサポートし、入力に対してより多くのサニティチェックを行うことに注意してください。 ABI coder v2では、一部の関数呼び出しがより高価になり、また、パラメータの型に適合しないデータが含まれている場合、ABI coder v1ではリバートしなかったコントラクトコールがリバートすることがあります。

  • つまり、 a**b**c という式は a**(b**c) として解析されます。 0.8.0以前は (a**b)**c と解析されていました。

    これは、指数演算子を解析する一般的な方法です。

  • ゼロ除算や算術オーバーフローなどの失敗したアサーションやその他の内部チェックは、invalid opcodeではなくrevert opcodeを使用します。 より具体的には、状況に応じたエラーコードを持つ Panic(uint256) への関数呼び出しと等しいエラーデータを使用します。

    これにより、エラー時のガスを節約できますが、静的解析ツールでは、このような状況を、 require の失敗のような無効な入力に対するリバートと区別できます。

  • ストレージのバイト配列の長さが正しくエンコードされていないものにアクセスすると、パニックが発生します。 コントラクトは、ストレージのバイト配列の生の表現を変更するためにインラインアセンブリを使用しない限り、このような状況に陥ることはありません。

  • 定数を配列の長さの式で使用する場合、以前のバージョンのSolidityでは、評価ツリーのすべての分岐で任意の精度を使用していました。 現在では、定数変数が中間式として使用されている場合、その値はランタイム式で使用されている場合と同様に適切に丸められます。

  • byte 型は削除されました。 これは bytes1 の別名でした。

新しい制約

このセクションでは、既存のコントラクトのコンパイルができなくなる可能性のある変更点を示します。

  • リテラルの明示的な変換に関連する新しい制限があります。 以下のようなケースでの従来の動作は、曖昧であったと思われます。

    1. 負のリテラルや type(uint160).max より大きいリテラルから address への明示的な変換は禁止されています。

    2. リテラルと整数型 T の間の明示的な変換は、リテラルが type(T).mintype(T).max の間にある場合にのみ許されます。 特に、 uint(-1) の使用を type(uint).max に置き換えてください。

    3. リテラルと列挙型の間の明示的な変換は、リテラルが列挙型の値を表すことができる場合にのみ許可されます。

    4. リテラルと address 型の間の明示的な変換(例: address(literal) )は、 address payable の代わりに address 型を持ちます。 明示的な変換を使用することで、payableなアドレス型を得ることができます。 すなわち、 payable(literal) です。

  • アドレスリテラル は、 address payable の代わりに address という型を持っています。 これらは、 payable(0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF) のような明示的な変換を用いることで、 address payable に変換できます。

  • 明示的な型変換には新しい制限があります。 変換は、符号、幅、または型カテゴリ( intaddressbytesNN など)に最大1つの変更がある場合にのみ許可されます。 複数の変更を行うには、複数の変換を使用します。

    ここで、 TS は型であり、 xS 型の任意の変数である、明示的な変換 T(x) を表すために T(S) という表記を使用してみましょう。 このような許されない変換の例としては、 uint16(int8) があります。 uint16(int8) は幅(8ビットから16ビット)と符号(符号付き整数から符号なし整数)の両方を変更するからです。 変換を行うためには、中間型を経由しなければなりません。 先ほどの例では、 uint16(uint8(int8)) または uint16(int16(int8)) となります。 この2つの変換方法では、例えば -1 の場合、異なる結果が得られることに注意してください。 以下は、この規則によって許されない変換の例です。

    • address(uint)uint(address): 型カテゴリーと幅の両方を変換します。 これはそれぞれ address(uint160(uint))uint(uint160(address)) に置き換えてください。

    • payable(uint160) , payable(bytes20), payable(integer-literal): 型カテゴリとステートミュータビリティの両方を変換します。 これはそれぞれ payable(address(uint160)) , payable(address(bytes20)) , payable(address(integer-literal)) に置き換えるてください。 なお、 payable(0) は有効であり、例外です。

    • int80(bytes10)bytes10(int80): 型カテゴリーと符号の両方を変換します。 これはそれぞれ int80(uint80(bytes10))bytes10(uint80(int80) に置き換えてください。

    • Contract(uint): 型カテゴリと幅の両方を変換しています。 これは Contract(address(uint160(uint))) に置き換えてください。

    これらの変換は、曖昧さを避けるために認められませんでした。 例えば、 uint16 x =   uint16(int8(-1)) という表現では、 x の値は、符号と幅のどちらの変換が最初に適用されるかに依存します。

  • 関数呼び出しのオプションは一度しか与えることができません。 つまり、 c.f{gas: 10000}{value: 1}() は無効で、 c.f{gas: 10000, value: 1}() に変更しなければなりません。

  • グローバル関数の log0log1log2log3log4 が削除されました。

    これらは、ほとんど使われていない低レベルの関数です。 これらの動作はインラインアセンブリからアクセスできます。

  • enum 定義は256個以上のメンバーを含むことはできません。

    これにより、ABIの基礎となる型が常に uint8 であると仮定しても安全になります。

  • thissuper_ という名前の宣言は、パブリック関数とイベントを除いて禁止されています。 この例外は、Solidity以外の言語で実装されたコントラクトのインターフェースを宣言できるようにするためのもので、このような関数名を許可しています。

  • コード内の \b\f\v のエスケープシーケンスのサポートを削除しました。 これらは、それぞれ \x08\x0c\x0b などの16進数のエスケープで挿入できます。

  • グローバル変数 tx.originmsg.sender の型は、 address payable ではなく address です。 これらを address payable に変換するには、明示的な変換を、すなわち payable(tx.origin) または payable(msg.sender) を用いてください。

    この変更は、これらのアドレスがpayableかどうかをコンパイラが判断できないため、この要件を可視化するために明示的な変換を必要とするようになりました。

  • address 型への明示的な変換は、常に支払い不可能な address 型を返します。 特に、以下の明示的な変換は、 address payable 型ではなく address 型になります。

    • address(u) ここで、 uuint160 型の変数です。 uaddress payable 型に変換するには、2つの明示的な変換、すなわち payable(address(u)) を用いてください。

    • address(b) ここで、 bbytes20 型の変数です。 baddress payable 型に変換するには、2つの明示的な変換、すなわち payable(address(b)) を用いてください。

    • address(c)c はコントラクト)。 以前は、この変換のリターン型は、コントラクトがEtherを受信できるかどうかに依存していました(受信関数または支払可能なフォールバック関数を持つことにより)。 payable(c) 変換は address payable 型で、コントラクト c がEtherを受け取ることができる場合にのみ許可されます。 一般的には、次の明示的な変換を用いることで、常に caddress payable 型に変換できます: payable(address(c))address(this) は、 address(c) と同じカテゴリーに属し、同じルールが適用されることに注意してください。

  • インラインアセンブリの chainid ビルトインは、 pure ではなく view とみなされるようになりました。

  • 単項否定は符号なし整数では使用できなくなり、符号付き整数でのみ使用できるようになりました。

インターフェースの変更

  • --combined-json の出力が変わりました。 JSONのフィールド abidevdocuserdocstorage-layout がサブオブジェクトになりました。 0.8.0以前では、これらは文字列としてシリアライズされていました。

  • 「レガシーAST」が削除されました(コマンドラインインターフェースでは --ast-json 、標準JSONでは legacyAST )。 代わりに「コンパクトAST」( --ast-compact--json 、標準JSONでは AST )を使用してください。

  • 旧エラーレポーター( --old-reporter )は削除されました。

コードのアップデート方法

  • ラッピング算術(オーバーフローを許容する算術)に頼っている場合は、各演算を unchecked { ... } で囲んでください。

  • オプション: SafeMathまたは同様のライブラリを使用している場合は、 x.add(y)x + yx.mul(y)x * y などに変更してください。

  • 古いABIコーダーを使用したい場合は、 pragma abicoder v1; を追加してください。

  • 冗長なので、オプションで pragma experimental ABIEncoderV2 または pragma abicoder v2 を削除してください。

  • bytebytes1 に変更してください。

  • 必要に応じて、中間の明示的な型変換を追加してください。

  • c.f{gas: 10000}{value: 1}()c.f{gas: 10000, value: 1}() に結合してください。

  • msg.sender.transfer(x)payable(msg.sender).transfer(x) に変更するか、 address payable 型のstored変数を使用してください。

  • x**y**z(x**y)**z に変更してください。

  • log0 、...、 log4 の代わりにインラインアセンブリを使用してください。

  • 符号なし整数を、その型の最大値から引いて1を加えて否定してください(例: type(uint256).max - x + 1 、ただし x はゼロではないことを確認してください)。

NatSpecフォーマット

Solidityコントラクトでは、特別な形式のコメントを使用して、関数や返り値の変数などに対してリッチなドキュメントを提供できます。 この特別な形式は、Ethereum Natural Language Specification Format (NatSpec)と名付けられています。

注釈

NatSpecは Doxygen に触発されて作られました。 Doxygenスタイルのコメントとタグを使用していますが、Doxygenとの厳密な互換性を維持する意図はありません。 下記にリストされているサポートされているタグをよく確認してください。

NatSpecのドキュメントは、開発者向けのメッセージと、エンドユーザー向けのメッセージに分けられます。 これらのメッセージは、エンドユーザー(人間)がコントラクトと対話する(すなわち、トランザクションに署名する)際に表示されることがあります。

Solidityのコントラクトは、全てのパブリックインターフェース(ABIにある全て)に対してNatSpecを使用して完全にアノテーションすることが推奨されます。

NatSpecには、スマートコントラクトの作成者が使用し、Solidityのコンパイラが理解するコメントのフォーマットが含まれています。 また、これらのコメントを機械で読める形式に抽出するSolidityコンパイラの出力も以下に示します。

NatSpecには、サードパーティのツールが使用するアノテーションが含まれることもあります。 これらは @custom:<name> タグを介して実現されることが多く、例えば、解析・検証ツールが使用しています。

ドキュメントの例

ドキュメントは、Doxygen記法のフォーマットを使用して、各 contractinterfacelibraryfunctionevent の上に挿入されます。 public の状態変数は、NatSpecの目的上、 function と同等です。

  • Solidityでは、1行のコメントに /// を、複数行のコメントに /** から始めて */ で終わるものを使えます。

  • Vyperでは、 """ を内側のコンテンツにインデントして、コメントをむき出しにして使います。 詳しくは Vyperのドキュメント を参照してください。

次の例は、利用可能なすべてのタグを使ったコントラクトと関数です。

注釈

Solidityのコンパイラは、タグがexternalまたはpublicの場合のみ解釈します。 internal関数やprivate関数に同様のコメントを使用することは可能ですが、それらはパースされません。

これは将来変更される可能性があります。

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

/// @title A simulator for trees
/// @author Larry A. Gardner
/// @notice You can use this contract for only the most basic simulation
/// @dev All function calls are currently implemented without side effects
/// @custom:experimental This is an experimental contract.
contract Tree {
    /// @notice Calculate tree age in years, rounded up, for live trees
    /// @dev The Alexandr N. Tetearing algorithm could increase precision
    /// @param rings The number of rings from dendrochronological sample
    /// @return Age in years, rounded up for partial years
    function age(uint256 rings) external virtual pure returns (uint256) {
        return rings + 1;
    }

    /// @notice Returns the amount of leaves the tree has.
    /// @dev Returns only a fixed number.
    function leaves() external virtual pure returns(uint256) {
        return 2;
    }
}

contract Plant {
    function leaves() external virtual pure returns(uint256) {
        return 3;
    }
}

contract KumquatTree is Tree, Plant {
    function age(uint256 rings) external override pure returns (uint256) {
        return rings + 2;
    }

    /// Return the amount of leaves that this specific kind of tree has
    /// @inheritdoc Tree
    function leaves() external override(Tree, Plant) pure returns(uint256) {
        return 3;
    }
}

タグ

すべてのタグはオプションです。 次の表では、各NatSpecタグの目的と使用される場所を説明しています。 特別なケースとして、タグが使用されていない場合、Solidityのコンパイラは /// または /** のコメントを @notice のタグが付いている場合と同じように解釈します。

タグ

コンテキスト

@title

コントラクトあるいはインターフェースを説明すべき名前

contract, library, interface

@author

オーサーの名前

contract, library, interface

@notice

これがどういうことを行うのか、エンドユーザー向けの説明

contract, library, interface, function, public state variable, event

@dev

開発者向けの追加の説明

contract, library, interface, function, state variable, event

@param

Doxygenのようなパラメータの説明(後ろにパラメータ名をつける必要がある)

function, event

@return

コントラクトの関数のリターン変数の説明

function, public state variable

@inheritdoc

ベース関数から不足しているタグを全てコピーする(後ろにコントラクト名をつける必要がある)

function, public state variable

@custom:...

カスタムタグ、セマンティクスはアプリケーションで定義

everywhere

(int quotient, int remainder) のように関数が複数の値を返す場合は、 @param 文と同じ形式で複数の @return 文を使用します。

カスタムタグは @custom: で始まり、その後に1つ以上の小文字またはハイフンを付ける必要があります。 ただし、ハイフンで始まることはできません。 カスタムタグは、あらゆる場所で使用でき、開発者向けドキュメントの一部となります。

動的表現

Solidityコンパイラは、SolidityソースコードからNatSpecドキュメントを経て、このガイドに記載されているJSON出力に変換します。 このJSON出力の使用者(エンドユーザーのクライアントソフトウェアなど)は、その出力をエンドユーザーに直接提示する場合もあれば、何らかの前処理を施す場合もあります。

例えば、一部のクライアントソフトではレンダリングを行います。

/// @notice This function will multiply `a` by 7

このドキュメントは、関数が呼び出され入力 a の値が 10 がである場合、次のようにエンドユーザーに提供されるかもしれません。

This function will multiply 10 by 7

継承に関する注意事項

NatSpecを持たない関数は、そのベースとなる関数のドキュメントを自動的に継承します。 この例外として次の場合があります。

  • パラメータ名が異なる場合。

  • 複数のベース関数がある場合。

  • どのコントラクトを継承すべきを指定する明示的な @inheritdoc タグがある場合。

ドキュメントの出力

上記の例のようなドキュメントは、コンパイラによって解析されると、2つの異なるJSONファイルが生成されます。 1つはエンドユーザーが関数実行時の通知として使用するもので、もう1つは開発者が使用するものです。

上記のコントラクトが ex1.sol として保存されていれば、以下の方法でドキュメントを作成できます。

solc --userdoc --devdoc ex1.sol

出力は以下のようになります。

注釈

Solidityバージョン0.6.11以降、NatSpec出力には versionkind フィールドが含まれています。 現在、 version1 に設定されており、 kinduser または dev のいずれかでなければなりません。 将来は、新しいバージョンが導入され、古いバージョンが廃止される可能性があります。

ユーザードキュメント

上記のドキュメントでは、以下のようなユーザードキュメントのJSONファイルが出力されます。

{
  "version" : 1,
  "kind" : "user",
  "methods" :
  {
    "age(uint256)" :
    {
      "notice" : "Calculate tree age in years, rounded up, for live trees"
    }
  },
  "notice" : "You can use this contract for only the most basic simulation"
}

なお、メソッドを見つけるためのキーは、単に関数名ではなく、 コントラクトABI で定義された関数の正規の署名であることに注意してください。

開発者ドキュメント

ユーザードキュメントファイルとは別に、開発者ドキュメントのJSONファイルも作成する必要があり、以下のような内容になります。

{
  "version" : 1,
  "kind" : "dev",
  "author" : "Larry A. Gardner",
  "details" : "All function calls are currently implemented without side effects",
  "custom:experimental" : "This is an experimental contract.",
  "methods" :
  {
    "age(uint256)" :
    {
      "details" : "The Alexandr N. Tetearing algorithm could increase precision",
      "params" :
      {
        "rings" : "The number of rings from dendrochronological sample"
      },
      "return" : "age in years, rounded up for partial years"
    }
  },
  "title" : "A simulator for trees"
}

SMTCheckerと形式検証

形式検証は、ソースコードがある形式的な仕様を満たしていることを、自動的に数学的に証明することを可能にします。 仕様はソースコードと同様に形式的なものですが、通常はよりシンプルなものになります。

形式検証は、「何をしたか(仕様)」と「どのようにしたか(実際の実装)」の違いを理解するためのものでしかないことに注意してください。 仕様が望んだものになっているかどうか、意図しない挙動を見逃していないかどうかをチェックする必要は依然としてあります。

Solidityでは、 SMT (Satisfiability Modulo Theories)Horn の解法に基づいた形式的な検証アプローチを実装しています。 SMTCheckerモジュールは、 require 文と assert 文で与えられた仕様をコードが満たしていることを自動的に証明しようとします。 つまり、 require 文を仮定とみなし、 assert 文の中の条件が常に真であることを証明しようとします。 アサーションの失敗が発見された場合、アサーションがどのように破られるかを示す反例がユーザーに与えられます。 SMTCheckerがあるプロパティに対して警告を出さない場合、そのプロパティは安全であることを意味します。

SMTCheckerがコンパイル時にチェックするその他の検証対象は以下の通りです。

  • 算術演算のアンダーフローとオーバーフロー。

  • ゼロによる除算。

  • トリビアルな条件と到達不可能なコード。

  • 空の配列に対するポップ。

  • 範囲外のインデックスアクセス。

  • 送金に必要な資金の不足。

上記のターゲットは、Solidity >=0.8.7のunderflowとoverflowを除き、すべてのエンジンが有効な場合、デフォルトで自動的にチェックされます。

SMTCheckerが報告する潜在的な警告は次のとおりです。

  • <failing  property> happens here これは、SMTCheckerがあるプロパティが失敗することを証明したことを意味します。 反例が示されることもありますが、複雑な状況では反例が示されないこともあります。 この結果は、SMTエンコーディングが、表現が困難または不可能なSolidityコードの抽象化を追加する場合、特定のケースでは誤検出となることもあります。

  • <failing property> might happen here これは、ソルバーが与えられたタイムアウト内にどちらのケースも証明できなかったことを意味します。 結果は不明なので、SMTCheckerは健全性のために潜在的な失敗を報告します。 これは、クエリのタイムアウトを増やすことで解決できるかもしれませんが、問題が単にエンジンにとって難しすぎるだけかもしれません。

SMTCheckerを有効にするには、デフォルトではエンジンなしとなっている 実行するエンジン を選択する必要があります。 エンジンを選択すると、すべてのファイルでSMTCheckerが有効になります。

注釈

Solidity 0.8.4以前では、SMTCheckerを有効にするデフォルトの方法は pragma experimental SMTChecker; を介したもので、プラグマを含むコントラクトのみが分析されました。 このプラグマは非推奨となっており、後方互換性のためにSMTCheckerを有効にしていますが、Solidity 0.9.0では削除されます。 また、1つのファイルでもプラグマを使用すると、すべてのファイルでSMTCheckerが有効になることに注意してください。

注釈

検証対象に対して警告が出ないということは、SMTCheckerや基盤となるソルバーにバグがないことを前提とした、議論の余地のない正しさの数学的証明を意味します。 これらの問題は、一般的なケースで自動的に解決することは 非常に難しく 、時には 不可能 であることに留意してください。 したがって、いくつかの特性は解決できないかもしれませんし、大規模なコントラクトでは誤検出につながるかもしれません。 すべての証明されたプロパティは重要な成果であると考えるべきです。 上級者向けには、 SMTChecker Tuning を参照して、より複雑なプロパティを証明するのに役立ついくつかのオプションを学んでください。

チュートリアル

オーバーフロー

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

contract Overflow {
    uint immutable x;
    uint immutable y;

    function add(uint x_, uint y_) internal pure returns (uint) {
        return x_ + y_;
    }

    constructor(uint x_, uint y_) {
        (x, y) = (x_, y_);
    }

    function stateAdd() public view returns (uint) {
        return add(x, y);
    }
}

上のコントラクトではオーバーフローチェックの例を示しています。 SMTCheckerはSolidity >=0.8.7ではデフォルトでアンダーフローとオーバーフローをチェックしないので、コマンドラインオプション --model-checker-targets "underflow,overflow" またはJSONオプション settings.modelChecker.targets = ["underflow", "overflow"] を使用する必要があります。 this section for targets configuration を参照してください。 ここでは、以下のように報告しています。

Warning: CHC: Overflow (resulting value larger than 2**256 - 1) happens here.
Counterexample:
x = 1, y = 115792089237316195423570985008687907853269984665640564039457584007913129639935
 = 0

Transaction trace:
Overflow.constructor(1, 115792089237316195423570985008687907853269984665640564039457584007913129639935)
State: x = 1, y = 115792089237316195423570985008687907853269984665640564039457584007913129639935
Overflow.stateAdd()
    Overflow.add(1, 115792089237316195423570985008687907853269984665640564039457584007913129639935) -- internal call
 --> o.sol:9:20:
  |
9 |             return x_ + y_;
  |                    ^^^^^^^

オーバーフローのケースをフィルタリングする require 文を追加すると、SMTCheckerはオーバーフローに到達しないことを(警告を報告しないことで)証明します。

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

contract Overflow {
    uint immutable x;
    uint immutable y;

    function add(uint x_, uint y_) internal pure returns (uint) {
        return x_ + y_;
    }

    constructor(uint x_, uint y_) {
        (x, y) = (x_, y_);
    }

    function stateAdd() public view returns (uint) {
        require(x < type(uint128).max);
        require(y < type(uint128).max);
        return add(x, y);
    }
}

アサート

アサーションとは、コードの不変性を表すもので、すべての入力値と保存値を含むすべてのトランザクションに対して*真でなければならないプロパティで、そうでなければバグがあることになります。

以下のコードでは、オーバーフローしないことを保証する関数 f を定義しています。 関数 inv は、 f が単調増加であるという仕様を定義しています: すべての可能なペア (a, b) に対して、もし b > a ならば f(b) > f(a) です。 f は確かに単調増加なので、SMTCheckerは我々の特性が正しいことを証明します。 この性質と関数の定義を使って、どんな結果が出るか試してみてください。

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

contract Monotonic {
    function f(uint x) internal pure returns (uint) {
        require(x < type(uint128).max);
        return x * 42;
    }

    function inv(uint a, uint b) public pure {
        require(b > a);
        assert(f(b) > f(a));
    }
}

また、ループの中にアサーションを追加して、より複雑なプロパティを検証することもできます。 次のコードでは、制限のない数値の配列の最大要素を検索し、検索された要素は配列のすべての要素と同じかそれ以上でなければならないというプロパティをアサートしています。

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

contract Max {
    function max(uint[] memory a) public pure returns (uint) {
        uint m = 0;
        for (uint i = 0; i < a.length; ++i)
            if (a[i] > m)
                m = a[i];

        for (uint i = 0; i < a.length; ++i)
            assert(m >= a[i]);

        return m;
    }
}

この例では、SMTCheckerは自動的に3つのプロパティを証明しようとすることに注意してください。

  1. 最初のループの ++i はオーバーフローしません。

  1. 2つ目のループの ++i はオーバーフローしません。

  1. アサーションは常に真です。

注釈

プロパティにはループが含まれているため、これまでの例よりも*はるかに*難しくなっていますので、ループにご注意ください。

すべてのプロパティの安全性が正しく証明されています。 プロパティを変更したり、配列に制限を加えることで、異なる結果を得ることができます。 例えば、コードを次のように変更すると

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

contract Max {
    function max(uint[] memory a) public pure returns (uint) {
        require(a.length >= 5);
        uint m = 0;
        for (uint i = 0; i < a.length; ++i)
            if (a[i] > m)
                m = a[i];

        for (uint i = 0; i < a.length; ++i)
            assert(m > a[i]);

        return m;
    }
}

が与えてくれます。

Warning: CHC: Assertion violation happens here.
Counterexample:

a = [0, 0, 0, 0, 0]
 = 0

Transaction trace:
Test.constructor()
Test.max([0, 0, 0, 0, 0])
  --> max.sol:14:4:
   |
14 |            assert(m > a[i]);

ステートのプロパティ

これまでの例では、特定の操作やアルゴリズムに関するプロパティを証明する、純粋なコードに対するSMTCheckerの使用方法を示しただけでした。 スマートコントラクトにおける一般的なプロパティの種類は、コントラクトの状態に関わるプロパティです。 このようなプロパティについてアサーションを失敗させるには、複数のトランザクションが必要になる場合があります。

例として、両軸の座標が(-2^128, 2^128 - 1)の範囲にある2Dグリッドを考えてみましょう。 ここで、ロボットを(0, 0)の位置に置きます。 ロボットは対角線上に1歩ずつしか移動できず、グリッドの外には出られません。 このロボットのステートマシンは、以下のスマートコントラクトで表すことができます。

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

contract Robot {
    int x = 0;
    int y = 0;

    modifier wall {
        require(x > type(int128).min && x < type(int128).max);
        require(y > type(int128).min && y < type(int128).max);
        _;
    }

    function moveLeftUp() wall public {
        --x;
        ++y;
    }

    function moveLeftDown() wall public {
        --x;
        --y;
    }

    function moveRightUp() wall public {
        ++x;
        ++y;
    }

    function moveRightDown() wall public {
        ++x;
        --y;
    }

    function inv() public view {
        assert((x + y) % 2 == 0);
    }
}

関数 inv は、 x + y が偶数でなければならないというステートマシンの不変量を表しています。 SMTCheckerは、ロボットにどんなに多くの命令を与えても、たとえ無限に与えても、不変量は*絶対に*失敗しないことを証明できます。 興味のある方は、手動でこの事実を証明することもできます。 ヒント: この不変量は帰納的なものです。

また、SMTCheckerを騙して、到達可能と思われるある位置までのパスを教えてもらうこともできます。 次のような関数を追加することで、(2, 4)は*not* reachableであるという性質を追加できます。

function reach_2_4() public view {
    assert(!(x == 2 && y == 4));
}

この性質は偽であり、SMTCheckerはこの性質が偽であることを証明しながら、(2, 4)に到達する方法を正確に*教えてくれます。

Warning: CHC: Assertion violation happens here.
Counterexample:
x = 2, y = 4

Transaction trace:
Robot.constructor()
State: x = 0, y = 0
Robot.moveLeftUp()
State: x = (- 1), y = 1
Robot.moveRightUp()
State: x = 0, y = 2
Robot.moveRightUp()
State: x = 1, y = 3
Robot.moveRightUp()
State: x = 2, y = 4
Robot.reach_2_4()
  --> r.sol:35:4:
   |
35 |            assert(!(x == 2 && y == 4));
   |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^

なお、上の経路は必ずしも決定論的ではなく、(2, 4)に到達する経路は他にもあるので注意が必要です。 どの経路を表示するかは、使用するソルバーやそのバージョンによって変わるかもしれませんし、ランダムに表示されるかもしれません。

外部呼び出しとReentrancy

すべての外部呼び出しは、SMTCheckerによって未知のコードへの呼び出しとして扱われます。 その理由は、たとえ呼び出されたコントラクトのコードがコンパイル時に利用可能であったとしても、デプロイされたコントラクトが実際にコンパイル時にインターフェースの元となったコントラクトと同じであるという保証はないからです。

場合によっては、外部から呼び出されたコードが呼び出し元のコントラクトを再入力するなど、何をしても真である状態変数のプロパティを自動的に推論することも可能です。

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

interface Unknown {
    function run() external;
}

contract Mutex {
    uint x;
    bool lock;

    Unknown immutable unknown;

    constructor(Unknown u) {
        require(address(u) != address(0));
        unknown = u;
    }

    modifier mutex {
        require(!lock);
        lock = true;
        _;
        lock = false;
    }

    function set(uint x_) mutex public {
        x = x_;
    }

    function run() mutex public {
        uint xPre = x;
        unknown.run();
        assert(xPre == x);
    }
}

上の例では、ミューテックスフラグを使用して再入を禁止したコントラクトを示しています。 ソルバーは、 unknown.run() が呼び出されたとき、コントラクトはすでに「ロック」されているので、未知の呼び出されたコードが何をしようと、 x の値を変更できないだろうと推測できます。

関数 setmutex モディファイアを使うことを「忘れた」場合、SMTCheckerは外部から呼び出されたコードの振る舞いを合成し、アサーションが失敗するようにします。

Warning: CHC: Assertion violation happens here.
Counterexample:
x = 1, lock = true, unknown = 1

Transaction trace:
Mutex.constructor(1)
State: x = 0, lock = false, unknown = 1
Mutex.run()
    unknown.run() -- untrusted external call, synthesized as:
        Mutex.set(1) -- reentrant call
  --> m.sol:32:3:
   |
32 |                assert(xPre == x);
   |                ^^^^^^^^^^^^^^^^^

SMTCheckerのオプションとチューニング

タイムアウト

SMTCheckerでは、ソルバーごとに選択されたハードコードされたリソース制限( rlimit )を使用していますが、これは時間とは正確には関係ありません。 rlimit オプションをデフォルトとして選択したのは、ソルバー内部の時間よりも決定性の保証が得られるからです。

このオプションを大まかに説明すると、1回のクエリにつき「数秒のタイムアウト」となります。 もちろん、多くのプロパティは非常に複雑で、決定論が問題にならないような解決に多くの時間を必要とします。 SMTCheckerがデフォルトの rlimit でコントラクトプロパティを解決できない場合、CLIオプション --model-checker-timeout <time> またはJSONオプション settings.modelChecker.timeout=<time> を介して、ミリ秒単位でタイムアウトを与えることができます。

検証ターゲット

SMTCheckerによって作成される検証ターゲットの種類は、CLIオプション --model-checker-target <targets> またはJSONオプション settings.modelChecker.targets=<targets> によってカスタマイズすることもできます。 CLIの場合、 <targets> は1つまたは複数の検証ターゲットのスペースなしコンマ区切りのリストで、JSON入力では1つまたは複数のターゲットを文字列として配列します。 ターゲットを表すキーワードは

  • アサーション: assert

  • 算術アンダーフロー: underflow

  • 算術オーバーフロー: overflow

  • ゼロによる除算: divByZero

  • トリビアルな条件と到達不可能なコード: constantCondition

  • 空の配列のポップ: popEmptyArray

  • 境界を越えた配列/固定バイトのインデックスアクセス: outOfBounds

  • 送金に必要な資金が不足しています: balance

  • 上記の全てです: default (CLIのみ)。

ターゲットの一般的なサブセットは、例えば次のようなものです: --model-checker-targets assert,overflow

デフォルトではすべてのターゲットがチェックされますが、Solidity >=0.8.7ではアンダーフローとオーバーフローがチェックされます。

検証対象をいつ、どのように分割するかについての正確なヒューリスティックはありませんが、特に大規模なコントラクトを扱う場合には有効です。

証明されたターゲット

証明されたターゲットがある場合、SMTCheckerはエンジンごとに、証明されたターゲットの数を示す警告を1回発行します。 もしユーザーが証明されたターゲットをすべて見たい場合は、CLIオプション --model-checker-show-proved とJSONオプション settings.modelChecker.showProved = true を使用できます。

証明されていないターゲット

証明されていないターゲットがある場合、SMTCheckerは証明されていないターゲットの数を示す1つの警告を発行します。 ユーザーが特定の未処理のターゲットをすべて表示したい場合は、CLIオプション --model-checker-show-unproved およびJSONオプション settings.modelChecker.showUnproved = true を使用できます。

未サポートの言語機能

SMTCheckerが適用するSMTエンコーディングでは、Solidity 言語の一部の機能が完全にサポートされていません(例えば、アセンブリブロック)。 サポートされていない構成は、健全性を保つために過近接によって抽象化されます。 つまり、この機能がサポートされていなくても、安全と報告されたプロパティは安全です。 しかし、このような抽象化は、対象となるプロパティがサポートされていない機能の正確な動作に依存している場合、誤検出を引き起こす可能性があります。 エンコーダがこのようなケースに遭遇した場合、デフォルトでは、サポートされていない機能をいくつ見たかを示す一般的な警告を報告することになります。 もしユーザーがサポートされていない機能をすべて見たい場合は、CLIオプション --model-checker-show-unsupported とJSONオプション settings.modelChecker.showUnsupported = true を使用できます(デフォルト値は false です)。

検証されたコントラクト

デフォルトでは、指定されたソース内のすべてのデプロイ可能なコントラクトが、デプロイされるものとして個別に分析されます。 これは、コントラクトが多くの直接および間接的な継承親を持つ場合、最も派生したものだけがブロックチェーン上で直接アクセスされるにもかかわらず、それらすべてが単独で分析されることを意味します。 これは、SMTCheckerとソルバーに不必要な負担をかけることになります。 このようなケースを支援するために、ユーザーはどのコントラクトをデプロイされたものとして分析すべきかを指定できます。 親コントラクトはもちろんまだ分析されますが、最も派生したコントラクトのコンテキストでのみ分析され、エンコーディングと生成されたクエリの複雑さが軽減されます。 抽象コントラクトはデフォルトではSMTCheckerによって最も派生したものとして分析されないことに注意してください。

選択されたコントラクトは、CLI: --model-checker-contracts "<source1.sol:contract1>,<source2.sol:contract2>,<source2.sol:contract3>" では<source>: <contract>のペアのコンマ区切りリスト(空白は許されない)を介して、 JSON input ではオブジェクト settings.modelChecker.contracts を介して、次のような形式で与えられます。

"contracts": {
    "source1.sol": ["contract1"],
    "source2.sol": ["contract2", "contract3"]
}

信頼した外部呼び出し

デフォルトでは、SMTCheckerは、コンパイル時に利用可能なコードと外部呼び出しの実行時コードが同じであることを想定していません。 次のコントラクトを例にとります:

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

contract Ext {
    uint public x;
    function setX(uint _x) public { x = _x; }
}
contract MyContract {
    function callExt(Ext _e) public {
        _e.setX(42);
        assert(_e.x() == 42);
    }
}

MyContract.callExt が呼び出されると、引数としてアドレスが与えられます。 デプロイ時には、アドレス _e が実際にコントラクト Ext のデプロイメントを含んでいるかどうかを確実に知ることはできません。 したがって、SMTChecker は、 _eExt 以外のコントラクトが含まれている場合、上記のアサーションに違反する可能性があることを警告します(これは真です)。

しかし、例えば、あるインターフェースの異なる実装が同じプロパティに適合しているかどうかをテストするために、これらの外部呼び出しを信頼できるものとして扱うことが有用な場合があります。 これは、アドレス _e が本当にコントラクト Ext としてデプロイされたと仮定することを意味します。 このモードはCLIオプション --model-checker-ext-calls=trusted またはJSONフィールド settings.modelChecker.extCalls: "trusted" で有効にできます。

このモードを有効にすると、SMTCheckerの解析に計算コストがかかることに注意してください。

このモードの重要な点は、コントラクトタイプとコントラクトへの高レベルの外部呼び出しに適用され、 calldelegatecall などの低レベルの呼び出しには適用されないという点です。 アドレスの保存はコントラクトタイプごとに行われ、SMTCheckerは外部から呼び出されたコントラクトは呼び出し元の式のタイプを持つと仮定しています。 したがって、 address やコントラクトを異なるコントラクト型にキャストすると、異なるストレージ値が得られ、以下の例のように仮定が矛盾している場合、健全でない結果を与えることがあります:

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

contract D {
    constructor(uint _x) { x = _x; }
    uint public x;
    function setX(uint _x) public { x = _x; }
}

contract E {
    constructor() { x = 2; }
    uint public x;
    function setX(uint _x) public { x = _x; }
}

contract C {
    function f() public {
        address d = address(new D(42));

        // `d` was deployed as `D`, so its `x` should be 42 now.
        assert(D(d).x() == 42); // should hold
        assert(D(d).x() == 43); // should fail

        // E and D have the same interface, so the following
        // call would also work at runtime.
        // However, the change to `E(d)` is not reflected in `D(d)`.
        E(d).setX(1024);

        // Reading from `D(d)` now will show old values.
        // The assertion below should fail at runtime,
        // but succeeds in this mode's analysis (unsound).
        assert(D(d).x() == 42);
        // The assertion below should succeed at runtime,
        // but fails in this mode's analysis (false positive).
        assert(D(d).x() == 1024);
    }
}

以上のことから、 address 型や contract 型の特定の変数に対する信頼できる外部呼び出しは、常に同じ呼び出し元の式の型を持つようにします。

また、継承の場合には、呼び出されたコントラクトの変数を最も派生した型の型としてキャストすることが有効です。

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

interface Token {
    function balanceOf(address _a) external view returns (uint);
    function transfer(address _to, uint _amt) external;
}

contract TokenCorrect is Token {
    mapping (address => uint) balance;
    constructor(address _a, uint _b) {
        balance[_a] = _b;
    }
    function balanceOf(address _a) public view override returns (uint) {
        return balance[_a];
    }
    function transfer(address _to, uint _amt) public override {
        require(balance[msg.sender] >= _amt);
        balance[msg.sender] -= _amt;
        balance[_to] += _amt;
    }
}

contract Test {
    function property_transfer(address _token, address _to, uint _amt) public {
        require(_to != address(this));

        TokenCorrect t = TokenCorrect(_token);

        uint xPre = t.balanceOf(address(this));
        require(xPre >= _amt);
        uint yPre = t.balanceOf(_to);

        t.transfer(_to, _amt);
        uint xPost = t.balanceOf(address(this));
        uint yPost = t.balanceOf(_to);

        assert(xPost == xPre - _amt);
        assert(yPost == yPre + _amt);
    }
}

関数 property_transfer では、外部呼び出しは変数 t に対して行われることに注意してください。

このモードのもう一つの注意点は、解析されたコントラクト以外のコントラクトタイプの状態変数への呼び出しです。 以下のコードでは、 BA をデプロイしているにもかかわらず、 B.a に格納されているアドレスが、 B 自身へのトランザクションの合間に、 B 以外の誰かによって呼び出される可能性もあります。 B.a に起こりうる変更を反映するために、エンコーディングは外部から B.a を無制限に呼び出すことができるようにします。 エンコーディングは B.a のストレージを追跡するので、アサーション(2)が成立するはずです。 しかし、現在のエンコーディングでは、このような呼び出しは概念的に B から行うことができるので、アサーション(3)は失敗します。 エンコーディングを論理的に強くすることは、トラステッドモードの拡張であり、現在開発中です。 もし B.aaddress 型を持つ場合、エンコーディングは B へのトランザクションの間にそのストレージが変更されないと仮定します。

pragma solidity >=0.8.0;

contract A {
    uint public x;
    address immutable public owner;
    constructor() {
        owner = msg.sender;
    }
    function setX(uint _x) public {
        require(msg.sender == owner);
        x = _x;
    }
}

contract B {
    A a;
    constructor() {
        a = new A();
        assert(a.x() == 0); // (1) should hold
    }
    function g() public view {
        assert(a.owner() == address(this)); // (2) should hold
        assert(a.x() == 0); // (3) should hold, but fails due to a false positive
    }
}

報告された推論された帰納的な不変量

CHCエンジンで安全性が証明された性質については、SMTCheckerは証明の一部としてHornソルバーによって推論された帰納的不変量を取得できます。 現在、2種類のみの不変量をユーザに報告できます。

  • コントラクト不変量: コントラクトの状態変数に関するプロパティで、コントラクトが実行する可能性のあるすべてのトランザクションの前後で真となるものです。 例えば、 xy がコントラクトの状態変数である場合、 x >= y となります。

  • 再帰性プロパティ: 未知のコードへの外部呼び出しがある場合のコントラクトの動作を表します。 これらのプロパティは、外部呼び出しの前と後の状態変数の値の間の関係を表現できます。 外部呼び出しは、分析されたコントラクトへのリエントラントな呼び出しを行うことを含め、何でも自由に行うことができます。 プライム化された変数は、前記外部呼び出し後の状態変数の値を表します。 例: lock -> x = x'

ユーザーは、CLIオプション --model-checker-invariants "contract,reentrancy" を使用して、または JSON input のフィールド settings.modelChecker.invariants で配列として報告される不変量の型を選択できます。 デフォルトでは、SMTCheckerはインバリアントを報告しません。

スラック変数を使った除算とモジュロ

SMTCheckerで使用されているデフォルトのHornソルバーであるSpacerは、Hornルール内の除算やモジュロ演算を嫌うことがあります。 そのため、デフォルトではSolidityの除算とモジュロ演算は a = b * d + m where d = a / b and m = a % b という制約を用いてエンコードされています。 しかし、Eldaricaのような他のソルバーは、構文的に正確な演算を好みます。 コマンドラインフラグ --model-checker-div-mod-no-slacks とJSONオプション settings.modelChecker.divModNoSlacks を使って、使用するソルバーの好みに応じてエンコーディングを切り替えることができます。

Natspec Function Abstraction

powsqrt などの一般的な数学手法を含む特定の関数は、完全に自動化された方法で分析するには複雑すぎる場合があります。 このような関数には、Natspecタグで注釈を付けることができます。 Natspecタグは、SMTCheckerに対して、これらの関数が抽象化されるべきであることを示します。 これは、関数の本体は使用されず、関数が呼び出されたときに

  • 非決定論的な値を返し、抽象化された関数がview/pureであれば状態変数を変更せずに、そうでなければ状態変数を非決定論的な値に設定します。 これは、アノテーション /// @custom:smtchecker abstract-function-nondet を介して使用できます。

  • 解釈されない関数として動作します。 これは、(ボディで与えられた)関数のセマンティクスが無視され、この関数が持つ唯一の特性は、同じ入力が与えられれば同じ出力が保証されるということです。 この関数は現在開発中で、アノテーション /// @custom:smtchecker abstract-function-uf から利用できるようになる予定です。

モデルチェックエンジン

SMTCheckerモジュールは、BMC(Bounded Model Checker)とCHC(Constrained Horn Clauses)という2種類の推論エンジンを実装しています。 両エンジンは現在開発中であり、それぞれ異なる特徴を持っています。 これらのエンジンは独立しており、すべてのプロパティの警告は、それがどのエンジンから来たかを示しています。 なお、上記の反例のある例はすべて、より強力なエンジンであるCHCから報告されています。

デフォルトでは、両方のエンジンが使用され、CHCが最初に実行され、証明されなかったすべてのプロパティがBMCに渡されます。 特定のエンジンを選択するには、CLIオプション --model-checker-engine {all,bmc,chc,none} またはJSONオプション settings.modelChecker.engine={all,bmc,chc,none} を使用します。

Bounded Model Checker (BMC)

BMCエンジンは、関数を単独で解析します。 つまり、各関数を解析する際に、複数のトランザクションにわたるコントラクトの全体的な動作を考慮しません。 ループも現時点ではこのエンジンでは無視されます。 内部の関数呼び出しは、直接的または間接的に再帰的でない限りインライン化されます。 外部関数呼び出しは可能な限りインライン化されます。 再帰性の影響を受ける可能性のある知識は消去されます。

上記のような特性から、BMCは誤検出を報告する傾向がありますが、軽量であるため、小さなローカルバグを素早く発見できるはずです。

Constrained Horn Clauses (CHC)

コントラクトのコントロールフローグラフ(CFG)は、Horn節のシステムとしてモデル化されており、コントラクトのライフサイクルは、すべてのpublic/external関数を非決定的に訪れることができるループで表現されています。 このようにして、任意の関数を解析する際には、無制限の数のトランザクションにおけるコントラクト全体の動作が考慮されます。 ループはこのエンジンで完全にサポートされています。 internal関数の呼び出しはサポートされており、external関数の呼び出しは、呼び出されたコードが未知であり、何でもできると仮定します。

CHCエンジンは、BMCよりも証明できる内容がはるかに多く、より多くの計算資源を必要とする可能性があります。

SMTソルバーとHornソルバー

上記の2つのエンジンは、自動定理証明器を論理的バックエンドとして使用しています。 BMCはSMTソルバーを使用し、CHCはHornソルバーを使用しています。 SMTソルバーを主とし、 Spacer をHornソルバーとして利用可能な z3 や、両方の機能を持つ Eldarica のように、同じツールが両方の役割を果たすこともよくあります。

ユーザーは、使用可能な場合、どのソルバーを使用するかを、CLIオプション --model-checker-solvers {all,cvc4,eld,smtlib2,z3} またはJSONオプション settings.modelChecker.solvers=[smtlib2,z3] で選択できます。

  • cvc4 は、 solc のバイナリがコンパイルされている場合にのみ使用できます。 cvc4 を使うのはBMCだけです。

  • eld は、システムにインストールされている必要があるバイナリを介して使用されます。 CHCだけが eld を使用し、 z3 が有効でない場合にのみ使用します。

  • smtlib2 はSMT/Hornのクエリを smtlib2 形式で出力します。 これをコンパイラの コールバックメカニズム と併用することで、システム内の任意のソルバーバイナリを採用して、クエリの結果をコンパイラに同期して返すことができます。 これは、どのソルバーを呼び出すかによって、BMCとCHCの両方で使用できます。

  • 以下の場合 z3 が使えます。

    • solc がz3とともにコンパイルされている場合。

    • Linuxシステムにバージョン>=4.8.xの動的 z3 ライブラリがインストールされている場合(Solidity 0.7.6以降)。

    • soljson.js (Solidity 0.6.9 以降)では静的に、つまりコンパイラのJavaScriptバイナリを使用しています。

注釈

z3バージョン4.8.16は、以前のバージョンとのABI互換性を壊し、solc <=0.8.13で使用できません。 もしz3 >=4.8.16を使用しているならば、solc>=0.8.14を使用してください。 逆に、古いz3は古いsolcリリースとしか使用できません。 また、SMTCheckerも最新のz3リリースを使用することをお勧めします。

BMCもCHCも z3 を採用しており、 z3 はブラウザを含めてより多様な環境で利用できるため、ほとんどのユーザーはこのオプションを気にする必要はないでしょう。 上級者であれば、より複雑な問題に対して別のソルバーを試すためにこのオプションを適用するかもしれません。

なお、選択したエンジンとソルバーの組み合わせによっては、SMTCheckerが何もしない場合があります。 例えば、CHCと cvc4 を選択した場合などです。

抽象化と偽陽性

SMTCheckerは、抽象化を不完全かつ健全な方法で実装しています。 バグが報告された場合、それは抽象化によってもたらされた誤検出である可能性があります(知識を消去したり、正確でない型を使用したため)。 検証対象が安全であると判断された場合、それは確かに安全であり、つまり(SMTCheckerにバグがない限り)偽陰性は存在しないのです。

ターゲットが証明できない場合は、前のセクションのチューニングオプションを使ってソルバーを助けることができます。 誤検出が確実な場合は、より多くの情報を含む require 文をコードに追加することで、ソルバーにさらなる力を与えることもできます。

SMTエンコーディングと型

SMTCheckerのエンコーディングは可能な限り正確を期しており、Solidityの型や表現を下の表のように最も近い SMT-LIB 表現にマッピングしています。

Solidityの型

SMT sort

Theories

Boolean

Bool

Bool

intN, uintN, address, bytesN, enum, contract

Integer

LIA, NIA

array, mapping, bytes, string

Tuple (Array elements, Integer length)

Datatypes, Arrays, LIA

struct

Tuple

Datatypes

他の型

Integer

LIA

まだサポートされていない型は、1つの256ビットの符号なし整数で抽象化され、サポートされていない操作は無視されます。

SMTエンコーディングの内部動作の詳細については、論文 SMT-based Verification of Solidity Smart Contracts を参照してください。

関数呼び出し

BMCエンジンでは、同じコントラクト(またはベースコントラクト)の関数呼び出しは、可能な場合、つまりその実装が利用可能な場合にインライン化されます。 他のコントラクトの関数の呼び出しは、そのコードが利用可能であってもインライン化されません。 これは、実際にデプロイされたコードが同じであることを保証できないからです。

CHCエンジンは、内部関数の呼び出しをサポートするために、呼び出された関数のサマリーを使用する非線形Horn句を作成します。 外部関数呼び出しは、リエントラント呼び出しの可能性も含め、未知のコードへの呼び出しとして扱われます。

複雑な純関数は、引数上の解釈されない関数(UF)によって抽象化されます。

関数

BMC/CHCの挙動

assert

Verification target.

require

仮定

internalコール

BMC: Inline function call. CHC: Function summaries.

既知のコードへのexternalコール

BMC: Inline function call or erase knowledge about state variables and local storage references. CHC: Assume called code is unknown. Try to infer invariants that hold after the call returns.

Storage array push/pop

Supported precisely. Checks whether it is popping an empty array.

ABI関数

Abstracted with UF.

addmod, mulmod

Supported precisely.

gasleft, blockhash, keccak256, ecrecover ripemd160

Abstracted with UF.

pure functions without implementation (external or complex)

Abstracted with UF

external functions without implementation

BMC: Erase state knowledge and assume result is nondeterminisc. CHC: Nondeterministic summary. Try to infer invariants that hold after the call returns.

transfer

BMC: Checks whether the contract's balance is sufficient. CHC: does not yet perform the check.

others

現在は未サポート

抽象化することは、正確な知識を失うことを意味しますが、多くの場合、証明力を失うことを意味しません。

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

contract Recover
{
    function f(
        bytes32 hash,
        uint8 v1, uint8 v2,
        bytes32 r1, bytes32 r2,
        bytes32 s1, bytes32 s2
    ) public pure returns (address) {
        address a1 = ecrecover(hash, v1, r1, s1);
        require(v1 == v2);
        require(r1 == r2);
        require(s1 == s2);
        address a2 = ecrecover(hash, v2, r2, s2);
        assert(a1 == a2);
        return a1;
    }
}

上の例では、SMTCheckerは実際に ecrecover を計算するほどの表現力はありませんが、関数呼び出しを解釈されない関数としてモデル化することで、同等のパラメータで呼び出された場合に戻り値が同じであることがわかります。 このことは、上記の主張が常に真であることを証明するのに十分です。

関数呼び出しをUFで抽象化することは、決定論的であることが知られている関数に対しては可能であり、純粋な関数に対しても簡単に行うことができます。 しかし、一般の外部関数では、状態変数に依存する可能性があるため、これを行うことは困難です。

参照型とエイリアス

Solidityでは、同じ データロケーション を持つ参照型に対してエイリアスを実装しています。 つまり、ある変数が同じデータ領域への参照を通じて変更される可能性があるということです。 SMTCheckerは、どの参照が同じデータを参照しているかを追跡しません。 これは、参照型のローカル参照または状態変数が割り当てられるたびに、同じ型およびデータロケーションの変数に関するすべての知識が消去されることを意味します。 型が入れ子になっている場合、知識の消去には、すべての前置基底型も含まれます。

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

contract Aliasing
{
    uint[] array1;
    uint[][] array2;
    function f(
        uint[] memory a,
        uint[] memory b,
        uint[][] memory c,
        uint[] storage d
    ) internal {
        array1[0] = 42;
        a[0] = 2;
        c[0][0] = 2;
        b[0] = 1;
        // Erasing knowledge about memory references should not
        // erase knowledge about state variables.
        assert(array1[0] == 42);
        // However, an assignment to a storage reference will erase
        // storage knowledge accordingly.
        d[0] = 2;
        // Fails as false positive because of the assignment above.
        assert(array1[0] == 42);
        // Fails because `a == b` is possible.
        assert(a[0] == 2);
        // Fails because `c[i] == b` is possible.
        assert(c[0][0] == 2);
        assert(d[0] == 2);
        assert(b[0] == 1);
    }
    function g(
        uint[] memory a,
        uint[] memory b,
        uint[][] memory c,
        uint x
    ) public {
        f(a, b, c, array2[x]);
    }
}

b[0] に割り当てられた後、 a については型( uint[] )とデータの場所(メモリ)が同じであるため、知識を消去する必要があります。 また、 c の基本型もメモリ上の uint[] であるため、 c に関する知識も消去する必要があります。 これは、ある c[i]ba と同じデータを参照する可能性があることを意味します。

arrayd は、型が uint[] であっても、ストレージに配置されているため、知識を消去しないことに注意してください。 しかし、もし d が割り当てられていたら、 array に関する知識をクリアする必要があり、その逆もまた然りです。

コントラクト残高

コントラクトは、デプロイトランザクションにおいて msg.value > 0 であれば、資金を送ってデプロイされるかもしれません。 しかし、コントラクトのアドレスは、デプロイ前にすでに資金を持っている可能性があり、それはコントラクトによって保持されます。 そのため、SMTCheckerはEVMルールとの整合性を取るために、コンストラクタで address(this).balance >= msg.value を想定しています。 また、コントラクトの残高は、以下の場合、コントラクトへの呼び出しをトリガすることなく増加することがあります。

  • selfdestruct は、分析されたコントラクトを残金の対象として、別のコントラクトで実行されます。

  • コントラクトは、あるブロックのコインベース(= block.coinbase )です。

これを適切にモデル化するために、SMTCheckerは、新しいトランザクションのたびに コントラクトの残高が少なくとも msg.value だけ増える可能性があると仮定しています。

実世界の仮定

SolidityやEVMでは表現できますが、実際には発生しないと思われるシナリオもあります。 そのようなケースの1つが、プッシュ時に動的ストレージの配列の長さがオーバーフローすることです。 push 操作が長さ2^256 - 1の配列に適用された場合、その長さは静かにオーバーフローします。 しかし、実際にはこのようなことは起こり得ません。 なぜなら、配列をそこまで成長させるために必要な演算を実行するには、何十億年もかかるからです。 SMTCheckerのもう一つの類似した仮定は、アドレスの残高がオーバーフローすることはないというものです。

同じようなアイデアが EIP-1985 でも紹介されていました。

Yul

Yul(以前はJULIAやIULIAとも呼ばれていました)は、さまざまなバックエンド用のバイトコードにコンパイルできる中間言語です。

スタンドアローンでも、Solidityで「インラインアセンブリ」としても使えます。 コンパイラは、IRベースのコードジェネレータ(「new codegen」または「IR-based codegen」)において、中間言語としてYulを使用します。 Yulは、すべてのターゲットプラットフォームに等しく恩恵を与えることができるハイレベルな最適化段階のための良いターゲットです。

モチベーションとハイレベルな記述

Yulの設計は、次の目標を達成しようと試みています。

  1. Yulで書かれたプログラムは、たとえそれがSolidityや他の高級言語のコンパイラで生成されたコードであっても、可読性がなければいけません。

  2. 手動での検査、形式的な検証、最適化に役立つように、コントロールフローは理解しやすいものでなければなりません。

  3. Yulからバイトコードへの変換は、可能な限り簡単に行う必要があります。

  4. Yulは、プログラム全体の最適化に適しているべきです。

1つ目と2つ目の目標を達成するために、Yulは for ループ、 if 文、 switch 文、関数呼び出しといったハイレベルな要素を提供しています。 アセンブリプログラムのコントロールフローを適切に表現するためには、これらで十分なはずです。 したがって、 SWAPDUPJUMPDESTJUMPJUMPI の明示的な文は用意されていません。 なぜなら、最初の2つはデータフローを難読化し、最後の2つはコントロールフローを難読化するからです。 さらに、 mul(add(x, y), 7) 形式の関数文は 7 y x add mul のような純粋なオペコード文よりも好まれます。 なぜなら、前の形式ではどのオペランドがどのオペコードに使用されているかがわかりやすいからです。

スタックマシン用に設計されているにもかかわらず、Yulはスタック自体の複雑さが現れることがありません。 プログラマーや監査人は、スタックのことを気にする必要はありません。

3つ目の目標は、ハイレベルな構造体を非常に規則的な方法でバイトコードにコンパイルすることで達成されます。 アセンブラが行う唯一の非ローカルな操作は、ユーザー定義の識別子(関数、変数、...)の名前のルックアップと、スタックからのローカル変数のクリーンアップです。

値なのか参照なのかというような混乱を避けるために、Yulは静的に型付けされています。 また、デフォルトの型(通常はターゲットマシンの整数ワード)があり、可読性のために常に省略できます。

言語をシンプルかつ柔軟に保つために、Yulは素の状態ではビルトインの演算や関数、型を持っていません。 これらはYulの方言を指定して初めて、そのセマンティクスとともに追加されます。 これにより、さまざまなターゲットプラットフォームや機能の要件に合わせて、Yulを特殊化できます。

現在、Yulの方言は1つだけあります。 この方言では、EVMのオペコードをビルトイン関数として使用し(下記参照)、EVMのネイティブな256ビット型である u256 型のみを定義しています。 そのため、以下の例では型を記述していません。

シンプルな例

以下のサンプルプログラムはEVM方言で書かれており、指数計算を行います。 solc --strict-assembly を使ってコンパイルできます。 ビルトイン関数 muldiv は、それぞれ積と除算を計算します。

{
    function power(base, exponent) -> result
    {
        switch exponent
        case 0 { result := 1 }
        case 1 { result := base }
        default
        {
            result := power(mul(base, base), div(exponent, 2))
            switch mod(exponent, 2)
                case 1 { result := mul(base, result) }
        }
    }
}

また、同じ関数を再帰ではなく、forループを使って実装することも可能です。 ここでは、 lt(a, b)ab より小さいかどうかを計算します。

{
    function power(base, exponent) -> result
    {
        result := 1
        for { let i := 0 } lt(i, exponent) { i := add(i, 1) }
        {
            result := mul(result, base)
        }
    }
}

セクションの最後 では、ERC-20規格の完全な実装が見られます。

スタンドアローンでの使用

Yulは、Solidityコンパイラを使用して、EVM方言をスタンドアローンの形で使用できます。 これは Yulオブジェクト記法 を使用するので、コードをデータとして参照してコントラクトをデプロイすることが可能です。 このYulモードは、コマンドラインコンパイラ( --strict-assembly を使用)と スタンダードJSONインターフェース で使用できます。

{
    "language": "Yul",
    "sources": { "input.yul": { "content": "{ sstore(0, 1) }" } },
    "settings": {
        "outputSelection": { "*": { "*": ["*"], "": [ "*" ] } },
        "optimizer": { "enabled": true, "details": { "yul": true } }
    }
}

警告

Yulは現在開発中で、バイトコード生成はEVM 1.0をターゲットとしたYulのEVM方言に対してのみ完全に実装されています。

Yulのインフォーマルな記述

以下では、Yul言語の個々の側面について説明します。 以下の例では、デフォルトのEVM方言を使用します。

シンタックス

YulはSolidityと同じようにコメント、リテラル、識別子を解析するため、例えば ///* */ をコメントとして使えます。 ただし、ひとつだけ例外があり、Yulの識別子はドット . を含むことができます。

Yulは、コード、データ、サブオブジェクトからなる「オブジェクト」を指定できます。 その詳細については下記の Yulオブジェクト を参照してください。 このセクションでは、そのようなオブジェクトのコード部分についてのみ説明します。 このコード部分は、常に中括弧で区切られたブロックで構成されています。 ほとんどのツールは、オブジェクトが期待される場所にコードブロックだけを指定することをサポートしています。

コードブロック内では、以下のような要素が使用できます(詳細は後述します)。

  • リテラル(最大32文字までの文字列)。例: 0x12342"abc"

  • ビルトイン関数の呼び出し。例: add(1, mload(0))

  • 変数宣言(初期値として0が代入される)。例: let x := 7let x := add(y, 3)let x

  • 識別子(変数)。例: add(3, x)

  • 代入。例: x := add(y, 3)

  • ローカル変数が内部にスコープされているブロック。例: { let x := 3 { let y := add(x, 1) } }

  • if文。例: if lt(a, b) { sstore(0, 1) }

  • スイッチ文。例: switch mload(0) case 0 { revert() } default { mstore(0, 1) }

  • forループ。例: for { let i := 0} lt(i, 10) { i := add(i, 1) } { mstore(i, 7) }

  • 関数定義。例: function f(a, b) -> c { c := add(a, b) }

複数の構文要素は、空白で区切られているだけで、互いに続けることができます。 つまり、終端の ; や改行は必要ありません。

リテラル

リテラルとして次のものを使用できます。

  • 10進数または16進数表記の整数定数。

  • ASCII文字列(例: "abc" )。 N が16進数である場合、16進数エスケープ \xNN とUnicodeエスケープ \uNNNN を含むことができます。

  • 16進数の文字列(例: hex"616263" )。

YulのEVM方言では、リテラルは次のように256ビットのワードを表します。

  • 10進数または16進数の定数は、 2**256 より小さい値でなければなりません。 これらの定数は、256ビットのワードのビッグエンディアンエンコーディングの符号なし整数として表します。

  • ASCII文字列は、まずバイト列として見ることができます。 すなわち、エスケープされていないASCII文字はASCIIコードを値とする1バイトと見なし、エスケープ \xNN はその値を持つ1バイトと見なし、エスケープ \uNNNN はそのコードポイントに対するUTF-8のバイト列と見なします。 バイト列は32バイトを超えてはなりません。 バイト列は32バイトになるように右に0をパディングします。 すなわち、文字列は左詰めで格納します。 パディングされたバイト列は、最上位8ビットが最初のバイトとなる256ビットのワードを表します。 すなわちビッグエンディアン形式で解釈されます。

  • 16進文字列は、まず隣り合う16進文字のペアを1バイトと見なして、バイト列として扱います。 バイト列は32バイト(つまり64個の16進文字)を超えてはならず、上記のように扱われます。

EVM用にコンパイルした場合、これは適切な PUSHi 命令に変換されます。 次の例では、 32 を足して5とし、文字列 "abc" のビット単位の and を計算しています。 最終的な値は、 x というローカル変数に代入されます。

上記の32バイトの制限は、リテラル引数を必要とするビルトイン関数に渡される文字列リテラルに対しては適用されません(例: setimmutableloadimmutable )。 それらの文字列は、生成されるバイトコードには含まれません。

let x := and("abc", add(3, 2))

デフォルトの型でない限り、リテラルの型はコロンの後に指定する必要があります。

// これはコンパイルできません(u32型とu256型はまだ実装されていません)。
let x := and("abc":u32, add(3:u256, 2:u256))

関数呼び出し

ビルトイン関数もユーザー定義関数(下記参照)も、前の例で示したのと同じ方法で呼び出すことができます。 関数が単一の値を返す場合は、再び式の中で直接使用できます。 複数の値を返す場合は、ローカル変数に代入する必要があります。

function f(x, y) -> a, b { /* ... */ }
mstore(0x80, add(mload(0x80), 3))
// ここで、ユーザ定義関数 `f` は2つの値を返す。
let x, y := f(1, mload(0))

EVMのビルトイン関数では、関数式はオペコードのストリームに直接変換されます。 式を右から左に読むだけでオペコードが得られます。 例の2行目の場合、 PUSH1 3 PUSH1 0x80 MLOAD ADD PUSH1 0x80 MSTORE です。

ユーザー定義関数の呼び出しでは、引数も右から左にスタックに置かれ、これが引数リストが評価される順序となります。 一方、戻り値は左から右へとスタックに置かれます。 つまり、この例では、 y がスタックの一番上に、 x がその下に置かれます。

変数宣言

let キーワードを使って変数を宣言できます。 変数は定義された {...} ブロックの中でのみ使えます。 EVMへのコンパイル時には、変数のために予約された新しいスタックのスロットが作成され、ブロックの終わりに達すると自動的に削除されます。 変数には初期値を指定できます。 値を指定しない場合は、変数はゼロに初期化されます。

変数はスタック上に格納されるため、メモリやストレージに直接影響を与えることはありませんが、ビルトイン関数 mstoremloadsstoresload でメモリやストレージの位置へのポインタとして変数を使用できます。 将来の方言では、このようなポインターのための特定の型が導入されるかもしれません。

変数を参照すると、その変数の現在の値がコピーされます。 EVMでは、これは DUP 命令に相当します。

{
    let zero := 0
    let v := calldataload(zero)
    {
        let y := add(sload(v), 1)
        v := y
    } // yが「deallocated」されます。
    sstore(v, zero)
} // vとzeroが「deallocated」されます。

宣言した変数の型がデフォルトの型と異なる場合は、コロンの後にその型を記述します。 また、複数の値を返す関数呼び出しから代入する場合、1つの文で複数の変数を宣言できます。

// これはコンパイルできません(u32型とu256型は未実装です)
{
    let zero:u32 := 0:u32
    let v:u256, t:u32 := f()
    let x, y := g()
}

オプティマイザの設定によっては、変数が最後に使用された後、まだスコープ内にあるにもかかわらず、コンパイラがスタックのスロットを解放することがあります。

代入

変数は、定義の後に := 演算子を使って代入できます。 複数の変数を同時に代入することも可能です。 そのためには、値の数と型が一致している必要があります。 複数のリターンパラメーターを持つ関数から返される値を代入する場合は、複数の変数を用意する必要があります。 代入の左辺に同じ変数を複数回使用できません。 例えば、 x, x := f() は無効です。

let v := 0
// vの再代入
v := 2
let t := add(v, 2)
function f() -> a, b { }
// 複数の値の代入
v, t := f()

if

if文は、条件付きでコードを実行するために使用できます。 elseブロックは定義できません。 複数の選択肢が必要な場合は、代わりにswitch(後述)の使用を検討してください。

if lt(calldatasize(), 4) { revert(0, 0) }

カーリーブレースは必須です。

switch

switch文は、if文の拡張版として使うことができます。 switch文は、式の値を受け取り、それをいくつかのリテラル定数と比較します。 そして、一致した定数に対応する分岐が実行されます。 他のプログラミング言語とは異なり、安全上の理由から、制御の流れは1つのケースから次のケースへとは続きません。 default と呼ばれるフォールバックまたはデフォルトケースがあり、リテラル定数のどれにもマッチしない場合に実行されます。

{
    let x := 0
    switch calldataload(4)
    case 0 {
        x := calldataload(0x24)
    }
    default {
        x := calldataload(0x44)
    }
    sstore(0, div(x, 2))
}

ケースのリスト自体は中括弧で囲まれていませんが、ケースの本文では中括弧が必要です。

ループ

Yulは、初期化パート・条件・イテレーション後のパートを含むヘッダーとボディから成るforループをサポートしています。 条件は式でなければならず、他の3つはブロックです。 初期化パートでトップレベルの変数が宣言されている場合、その変数のスコープはループの他のすべての部分にまで及びます。

break 文と continue 文は、それぞれループを終了させたり、イテレーション後のパートにスキップさせたりするためにボディで使用できます。

次の例では、メモリ上のある領域の和を計算します。

{
    let x := 0
    for { let i := 0 } lt(i, 0x100) { i := add(i, 0x20) } {
        x := add(x, mload(i))
    }
}

forループはwhileループの代用としても使用できます。 初期化部分と反復後の部分を空にするだけです。

{
    let x := 0
    let i := 0
    for { } lt(i, 0x100) { } {     // while(i < 0x100)
        x := add(x, mload(i))
        i := add(i, 0x20)
    }
}

関数宣言

Yulでは、関数の定義が可能です。 Solidityの関数と混同してはいけません。 Yulの関数はコントラクトの外部インターフェースの一部ではなく、Solidityの関数とは別の名前空間に属しているからです。

EVMでは、Yulの関数はスタックから引数(およびリターンされるPC)を取り、結果をスタックに置きます。 ユーザー定義関数やビルトイン関数も全く同じように呼び出されます。

関数はどこでも定義でき、宣言されたブロック内で利用できます。 関数の内部では、その関数の外部で定義されたローカル変数にアクセスできません。

関数はSolidityと同様に、パラメータとリターン変数を宣言します。 値を返すには、その値を戻り値の変数に代入します。

複数の値を返す関数を呼び出した場合は、 a, b := f(x)let a, b := f(x) を使って複数の変数に代入する必要があります。

leave 文は、現在の関数を終了するために使用できます。 他の言語の return 文と同じように動作しますが、戻り値を取らずに関数を終了し、関数は戻り値の変数に現在割り当てられている値を返します。

EVM方言には return というビルトイン関数があり、現在のYulの関数だけでなく、完全な実行コンテキスト(内部メッセージコール)を終了させることができることに注意してください。

次の例では、バイナリ法(square-and-multiply)でpower関数を実装しています。

{
    function power(base, exponent) -> result {
        switch exponent
        case 0 { result := 1 }
        case 1 { result := base }
        default {
            result := power(mul(base, base), div(exponent, 2))
            switch mod(exponent, 2)
                case 1 { result := mul(base, result) }
        }
    }
}

Yulの仕様

この章では、Yulのコードを形式的に説明します。 Yulコードは通常、Yulオブジェクトの中に配置されます。 Yulオブジェクトについては別の章で説明します。

Block = '{' Statement* '}'
Statement =
    Block |
    FunctionDefinition |
    VariableDeclaration |
    Assignment |
    If |
    Expression |
    Switch |
    ForLoop |
    BreakContinue |
    Leave
FunctionDefinition =
    'function' Identifier '(' TypedIdentifierList? ')'
    ( '->' TypedIdentifierList )? Block
VariableDeclaration =
    'let' TypedIdentifierList ( ':=' Expression )?
Assignment =
    IdentifierList ':=' Expression
Expression =
    FunctionCall | Identifier | Literal
If =
    'if' Expression Block
Switch =
    'switch' Expression ( Case+ Default? | Default )
Case =
    'case' Literal Block
Default =
    'default' Block
ForLoop =
    'for' Block Expression Block Block
BreakContinue =
    'break' | 'continue'
Leave = 'leave'
FunctionCall =
    Identifier '(' ( Expression ( ',' Expression )* )? ')'
Identifier = [a-zA-Z_$] [a-zA-Z_$0-9.]*
IdentifierList = Identifier ( ',' Identifier)*
TypeName = Identifier
TypedIdentifierList = Identifier ( ':' TypeName )? ( ',' Identifier ( ':' TypeName )? )*
Literal =
    (NumberLiteral | StringLiteral | TrueLiteral | FalseLiteral) ( ':' TypeName )?
NumberLiteral = HexNumber | DecimalNumber
StringLiteral = '"' ([^"\r\n\\] | '\\' .)* '"'
TrueLiteral = 'true'
FalseLiteral = 'false'
HexNumber = '0x' [0-9a-fA-F]+
DecimalNumber = [0-9]+

文法に関する制限

文法によって直接課せられるものとは別に、以下のような制限があります。

スイッチには、少なくとも1つのケース(デフォルトのケースを含む)が必要です。 すべてのケースの値は、同じ型で異なる値を持つ必要があります。 式の型のすべての可能な値がカバーされている場合、デフォルトケースは使用できません(つまり、trueとfalseの両方のケースを持つ bool 式のスイッチにはデフォルトケースを使えません)。

すべての式は0個以上の値で評価されます。 識別子とリテラルは正確に1つの値に評価され、関数呼び出しは呼び出された関数のリターン変数の数に等しい数の値に評価されます。

変数宣言や代入では、右辺の式(存在する場合)は、左辺の変数の数と同じ数の値に評価されなければなりません。 これが複数の値に評価される式が許される唯一のシチュエーションです。 代入や変数宣言の左辺には、同じ変数名を複数回使用できません。

文でもある式(ブロックレベル)は、0に評価されなければなりません。

それ以外のシチュエーションでは、式は正確に1つの値に評価されなければなりません。

continue 文または break 文は、以下のようにforループのボディ内でのみ使用できます。 文を含む最も内側のループを考えてみましょう。 ループと文は同じ関数内にあるか、または両方がトップレベルになければなりません。 文はループのボディブロック内に配置しなければならず、ループの初期化ブロックや更新ブロック内に配置できません。 この制限は、 continue 文または break 文を含む最も内側のループにのみ適用されることを強調しておきます。 この最も内側のループ、つまり continue 文または break 文は、外側ループのどこでも、おそらく外側ループの初期化ブロックや更新ブロックでも出現できます。 例えば、次の例は、 break が外側ループの更新ブロックにあるにもかかわらず、内側ループのボディブロックにあるため、合法です:

for {} true { for {} true {} { break } }
{
}

forループの条件部分は、正確に1つの値に評価されなければなりません。

leave 文は、関数内でのみ使用できます。

関数はforループの初期化ブロック内のどこにも定義できません。

リテラルはその型より大きくできません。 定義されている最大の型は256ビット幅です。

代入や関数呼び出しの際には、それぞれの値の型が一致していなければなりません。 暗黙の型変換はありません。 一般に、型の変換は、ある型の値を受け取り、異なる型の値を返す適切なビルトイン関数を方言が提供している場合にのみ実現します。

スコープのルール

Yulでは、スコープはブロックに紐付けられており(例外として、後述する関数やforループがあります)、すべての宣言( FunctionDefinitionVariableDeclaration )は、これらのスコープに新しい識別子を導入します。

識別子は、定義されているブロック(すべてのサブノードとサブブロックを含む)で使用できます。 関数はブロック全体(定義前も含む)で使用できますが、変数は VariableDeclaration の後の文からしか使用できません。

特に、変数は自分の変数宣言の右側では参照できません。 関数は、その宣言の前にすでに参照できます(関数が利用できる場合)。

一般的なスコープルールの例外として、forループの初期化パート(最初のブロック)のスコープは、forループの他のすべてのパートに及びます。 つまり、初期化パート(初期化パート内のブロックは含まない)で宣言された変数(および関数)は、forループの他のすべてのパートで使用できます。

forループの他の部分で宣言された識別子は、通常の構文上のスコープルールに従います。

これは、 for { I... } C { P... } { B... } という形式のforループが { I... for {} C { P... } { B... } } と同等であることを意味しています。

関数のパラメータとリターンパラメータは、関数ボディで利用でき、それらの名前は異なるものでなければなりません。

関数内では、その関数の外で宣言された変数を参照できません。

シャドーイングは禁止されています。 つまり、現在の関数の外で宣言されたために参照できなくても、同じ名前の別の識別子が見える場所で識別子を宣言できません。

形式的な仕様

ASTの様々なノード上でオーバーロードされた評価関数Eを提供することで、Yulを形式的に定めます。 ビルトイン関数には副作用があるため、Eは2つの状態オブジェクトとASTノードを受け取り、2つの新しい状態オブジェクトと可変数の他の値を返します。 2つの状態オブジェクトとは、グローバル状態オブジェクト(EVMの文脈では、ブロックチェーンのメモリ、ストレージ、状態)と、ローカル状態オブジェクト(ローカル変数の状態、つまりEVMのスタックのセグメント)です。

ASTノードが文の場合、Eは2つの状態オブジェクトと breakcontinueleave 文で使用される「モード」を返します。 ASTノードが式の場合、Eは2つの状態オブジェクトと式の評価値の数だけの値を返します。

グローバルな状態の正確な性質は、このハイレベルな説明では指定されていません。 ローカルステート L は、識別子 i から値 v へのマッピングであり、 L[i] = v と表記されます。

識別子 v に対して、識別子の名前を $v とします。

ここでは、ASTのノードにデストラクション記法を用います。

E(G, L, <{St1, ..., Stn}>: Block) =
    let G1, L1, mode = E(G, L, St1, ..., Stn)
    let L2 be a restriction of L1 to the identifiers of L
    G1, L2, mode
E(G, L, St1, ..., Stn: Statement) =
    if n is zero:
        G, L, regular
    else:
        let G1, L1, mode = E(G, L, St1)
        if mode is regular then
            E(G1, L1, St2, ..., Stn)
        otherwise
            G1, L1, mode
E(G, L, FunctionDefinition) =
    G, L, regular
E(G, L, <let var_1, ..., var_n := rhs>: VariableDeclaration) =
    E(G, L, <var_1, ..., var_n := rhs>: Assignment)
E(G, L, <let var_1, ..., var_n>: VariableDeclaration) =
    let L1 be a copy of L where L1[$var_i] = 0 for i = 1, ..., n
    G, L1, regular
E(G, L, <var_1, ..., var_n := rhs>: Assignment) =
    let G1, L1, v1, ..., vn = E(G, L, rhs)
    let L2 be a copy of L1 where L2[$var_i] = vi for i = 1, ..., n
    G1, L2, regular
E(G, L, <for { i1, ..., in } condition post body>: ForLoop) =
    if n >= 1:
        let G1, L1, mode = E(G, L, i1, ..., in)
        // mode has to be regular or leave due to the syntactic restrictions
        if mode is leave then
            G1, L1 restricted to variables of L, leave
        otherwise
            let G2, L2, mode = E(G1, L1, for {} condition post body)
            G2, L2 restricted to variables of L, mode
    else:
        let G1, L1, v = E(G, L, condition)
        if v is false:
            G1, L1, regular
        else:
            let G2, L2, mode = E(G1, L, body)
            if mode is break:
                G2, L2, regular
            otherwise if mode is leave:
                G2, L2, leave
            else:
                G3, L3, mode = E(G2, L2, post)
                if mode is leave:
                    G3, L3, leave
                otherwise
                    E(G3, L3, for {} condition post body)
E(G, L, break: BreakContinue) =
    G, L, break
E(G, L, continue: BreakContinue) =
    G, L, continue
E(G, L, leave: Leave) =
    G, L, leave
E(G, L, <if condition body>: If) =
    let G0, L0, v = E(G, L, condition)
    if v is true:
        E(G0, L0, body)
    else:
        G0, L0, regular
E(G, L, <switch condition case l1:t1 st1 ... case ln:tn stn>: Switch) =
    E(G, L, switch condition case l1:t1 st1 ... case ln:tn stn default {})
E(G, L, <switch condition case l1:t1 st1 ... case ln:tn stn default st'>: Switch) =
    let G0, L0, v = E(G, L, condition)
    // i = 1 .. n
    // Evaluate literals, context doesn't matter
    let _, _, v1 = E(G0, L0, l1)
    ...
    let _, _, vn = E(G0, L0, ln)
    if there exists smallest i such that vi = v:
        E(G0, L0, sti)
    else:
        E(G0, L0, st')

E(G, L, <name>: Identifier) =
    G, L, L[$name]
E(G, L, <fname(arg1, ..., argn)>: FunctionCall) =
    G1, L1, vn = E(G, L, argn)
    ...
    G(n-1), L(n-1), v2 = E(G(n-2), L(n-2), arg2)
    Gn, Ln, v1 = E(G(n-1), L(n-1), arg1)
    Let <function fname (param1, ..., paramn) -> ret1, ..., retm block>
    be the function of name $fname visible at the point of the call.
    Let L' be a new local state such that
    L'[$parami] = vi and L'[$reti] = 0 for all i.
    Let G'', L'', mode = E(Gn, L', block)
    G'', Ln, L''[$ret1], ..., L''[$retm]
E(G, L, l: StringLiteral) = G, L, str(l),
    where str is the string evaluation function,
    which for the EVM dialect is defined in the section 'Literals' above
E(G, L, n: HexNumber) = G, L, hex(n)
    where hex is the hexadecimal evaluation function,
    which turns a sequence of hexadecimal digits into their big endian value
E(G, L, n: DecimalNumber) = G, L, dec(n),
    where dec is the decimal evaluation function,
    which turns a sequence of decimal digits into their big endian value

EVM方言

Yulのデフォルトの方言は、現在選択されているEVMのバージョンのEVMの方言です。 この方言で使用できる型は、Ethereum Virtual Machineの256ビットのネイティブ型である u256 のみです。 これはこの方言のデフォルト型なので、省略できます。

次の表は、すべてのビルトイン関数(これはEVMバージョンに依存します)をリストアップし、関数/オペコードのセマンティクスの簡単な説明を提供しています。 このドキュメントは、Ethereum Virtual Machineの完全な説明を目的としていません。 正確なセマンティクスに興味がある場合は、別のドキュメントを参照してください。

- と書かれたオペコードは結果を返さず、その他のオペコードは正確に1つの値を返します。 FHBCILP と書かれたオペコードは、それぞれFrontier、Homestead、Byzantium、Constantinople、Istanbul、London、Parisから存在しています。

以下では、 mem[a...b) は位置 a から位置 b までのメモリのバイトを意味し、 storage[p] はスロット p のストレージ内容を意味します。

Yulはローカル変数やコントロールフローを管理しているため、これらの機能を阻害するオペコードは使用できません。 これには、 dupswap 命令のほか、 jump 命令、ラベル、 push 命令などが含まれます。

命令

説明

stop()

-

F

実行停止。return(0, 0)と同じ。

add(x, y)

F

x + y。

sub(x, y)

F

x - y。

mul(x, y)

F

x * y。

div(x, y)

F

x / y。ただしy == 0ならば0。

sdiv(x, y)

F

x / y(2の補数の符号付き数値用)。ただしy == 0ならば0。

mod(x, y)

F

x % y。ただしy == 0ならば 0。

smod(x, y)

F

x % y(2の補数の符号付き数値用)。ただしy == 0ならば0。

exp(x, y)

F

xのy乗。

not(x)

F

xのビット単位のnot(xの各ビットが否定される)。

lt(x, y)

F

x < yの場合は1、それ以外の場合は0。

gt(x, y)

F

x > yの場合は1、それ以外の場合は0。

slt(x, y)

F

2の補数の符号付き数値で、x < yの場合は1、それ以外の場合は0。

sgt(x, y)

F

2の補数の符号付き数値で、x > yの場合は1、それ以外の場合は0。

eq(x, y)

F

x == yの場合は1、それ以外の場合は0。

iszero(x)

F

x == 0の場合は1、それ以外の場合は0。

and(x, y)

F

xとyのビット単位のand。

or(x, y)

F

xとyのビット単位のor。

xor(x, y)

F

xとyのビット単位のxor。

byte(n, x)

F

xのn番目のバイト。最上位バイトが0番目。

shl(x, y)

C

yをxビット左シフト。

shr(x, y)

C

yをxビット右シフト。

sar(x, y)

C

yを符号付き数値としてxビット右シフト(算術シフト)。

addmod(x, y, m)

F

任意精度の(x + y) % m、m == 0の場合は0。

mulmod(x, y, m)

F

任意精度の(x * y) % m、m == 0の場合は0。

signextend(i, x)

F

xの(i*8+7)ビット目から最下位ビットまでを符号拡張。

keccak256(p, n)

F

keccak(mem[p...(p+n)))。

pc()

F

現在のコードの位置。

pop(x)

-

F

値xを破棄。

mload(p)

F

mem[p...(p+32))。

mstore(p, v)

-

F

mem[p...(p+32)) := v。

mstore8(p, v)

-

F

mem[p] := v & 0xff (単一バイトを修正するのみ)。

sload(p)

F

storage[p]。

sstore(p, v)

-

F

storage[p] := v。

msize()

F

メモリのサイズ、すなわちアクセスされた最大のメモリインデックス。

gas()

F

残りの実行可能なガス。

address()

F

現在のコントラクト/実行コンテキストのアドレス。

balance(a)

F

アドレスaのwei残高。

selfbalance()

I

balance(address())と同等だが、より安価。

caller()

F

コール送信者( delegatecall を除く)。

callvalue()

F

現在のコールと一緒に送信されたwei。

calldataload(p)

F

位置pから始まるコールデータ(32バイト)。

calldatasize()

F

コールデータのサイズ(バイト)。

calldatacopy(t, f, s)

-

F

位置fのコールデータから位置tのmemにsバイトコピー。

codesize()

F

現在のコントラクト/実行コンテキストのコードサイズ。

codecopy(t, f, s)

-

F

位置fのコードから位置tのmemにsバイトコピー。

extcodesize(a)

F

アドレスaのコードのサイズ。

extcodecopy(a, t, f, s)

-

F

codecopy(t, f, s)と似ているが、アドレスaのコードに対してのもの。

returndatasize()

B

直前のリターンデータのサイズ。

returndatacopy(t, f, s)

-

B

位置fのリターンデータから位置tのmemにsバイトコピー。

extcodehash(a)

C

アドレスaのコードハッシュ。

create(v, p, n)

F

mem[p...(p+n))のコードを持つ新しいコントラクトを作成し、 v Weiを送信し、新しいアドレスを返す。エラーの場合は0を返します。

create2(v, p, n, s)

C

mem[p...(p+n))のコードを持つ新しいコントラクトをアドレス keccak256(0xff . this . s . keccak256(mem[p...(p+n)))に作成し、 v Weiを送信し、新しいアドレスを返します。 0xff は1バイトの値、 this は現在のコントラクトのアドレス(20バイトの値)、 s はビッグエンディアンの256ビットの値です。 エラーの場合は0を返します。

call(g, a, v, in, insize, out, outsize)

F

アドレスaのコントラクトを、 mem[in...(in+insize))を入力として呼び出し、 g gasとv Weiを送信します。 出力をmem[out...(out+outsize))に書き込みます。 エラーの場合(例えば、out of gas)は0を、成功した場合は1を返します。 詳細

callcode(g, a, v, in, insize, out, outsize)

F

call と似ていますが、aのコードのみを使用し、 それ以外は現在のコントラクトのコンテキスト。 詳細

delegatecall(g, a, in, insize, out, outsize)

H

calldata と似ていますが、 callercallvalue も保持します。 詳細

staticcall(g, a, in, insize, out, outsize)

B

call(g, a, 0, in, insize, out, outsize) と似ていますが、 ステートの変更を許可しません。 詳細

return(p, s)

-

F

実行終了。リターンデータはmem[p...(p+s))。

revert(p, s)

-

B

実行終了。ステート変化をリバートします。リターンデータはmem[p...(p+s))。

selfdestruct(a)

-

F

実行終了。現在のコントラクトを破壊し、aに資金を送ります。(非推奨)

invalid()

-

F

invalid命令で実行を終了。

log0(p, s)

-

F

データがmem[p...(p+s))のログ。

log1(p, s, t1)

-

F

トピックt1、データがmem[p...(p+s))のログ。

log2(p, s, t1, t2)

-

F

トピックt1,t2、データがmem[p...(p+s))のログ。

log3(p, s, t1, t2, t3)

-

F

トピックt1,t2,t3、データがmem[p...(p+s))のログ。

log4(p, s, t1, t2, t3, t4)

-

F

トピックt1,t2,t3,t4、データがmem[p...(p+s))のログ。

chainid()

I

実行中のチェーンのID(EIP-1344)。

basefee()

L

現在のブロックのベースフィー(EIP-3198、EIP-1559)。

origin()

F

トランザクションの送信者。

gasprice()

F

トランザクションのガスプライス。

blockhash(b)

F

ブロック番号bのハッシュ - 現在を除く過去256ブロック分のみ。

coinbase()

F

現在のマイニングの受益者アドレス。

timestamp()

F

現在のブロックのタイムスタンプ(エポックからの秒数)。

number()

F

現在のブロックナンバー。

difficulty()

F

現在のブロックの難易度。(下記の注釈も参照)

prevrandao()

P

ビーコンチェーンによるランダム性(下記の注釈も参照)

gaslimit()

F

現在のブロックのブロックガスリミット。

注釈

call* 命令は、 out および outsize のパラメータを使用して、戻り値または失敗の値のデータを配置するメモリ内の領域を定義します。 この領域は、呼び出されたコントラクトが何バイト返すかに応じて、書き込まれます。 より多くのデータを返した場合は、最初の outsize バイトのみが書き込まれます。 残りのデータには returndatacopy オペコードでアクセスできます。 より少ないデータを返した場合は、残りのバイトにはまったく手をつけません。 このメモリ領域のどの部分にリターンデータが含まれているかを確認するには、 returndatasize オペコードを使用する必要があります。 残りのバイトは、呼び出し前の値を保持します。

注釈

Paris以降のEVMのバージョンでは、 difficulty() 命令が禁止されています。 Parisネットワークのアップグレードにより、以前は difficulty と呼ばれていた命令のセマンティクスが変更され、その命令は prevrandao に改名されました。 この命令は256ビットの全範囲の任意の値を返すことができるようになり、Ethash内で記録された最高難易度の値は54ビットでした。 この変更は EIP-4399 で説明されています。 コンパイラでどのEVMバージョンが選択されているかとは無関係に、命令のセマンティクスは最終的なデプロイの連鎖に依存することに注意してください。

警告

バージョン0.8.18以降、SolidityとYulの両方で selfdestruct を使用すると、非推奨であることを警告します。 というのも、 SELFDESTRUCT オペコードは、 EIP-6049 で述べられているように、いずれ動作が大きく変化することになるからです。

内部の方言では、追加関数が存在するものもあります。

datasize, dataoffset, datacopy

関数 datasize(x)dataoffset(x)datacopy(t, f, l) は、Yulオブジェクトの他の部分にアクセスするために使用されます。

datasizedataoffset は、文字列リテラル(他のオブジェクトの名前)のみを引数に取り、それぞれデータ領域のサイズとオフセットを返します。 EVMでは、 datacopy 関数は codecopy と同等です。

setimmutable, loadimmutable

関数 setimmutable(offset, "name", value)loadimmutable("name") はSolidityのimmutable機構に使用されており、純粋なYulにはうまくマッピングされていません。 setimmutable(offset, "name", value) の呼び出しは、指定された名前付きのimmutableコントラクトを含むランタイムコードがオフセット offset でメモリにコピーされたと仮定し、ランタイムコード内の loadimmutable("name") への呼び出しのために生成されたプレースホルダーを含むメモリ内のすべての位置( offset に対する相対位置)に value を書き込みます。

linkersymbol

関数 linkersymbol("library_id") は、リンカーが置換するアドレスリテラルのプレースホルダです。 その最初で唯一の引数は文字列リテラルでなければならず、挿入されるアドレスを一意的に表します。 識別子は任意ですが、コンパイラがSolidityのソースからYulコードを生成する場合、そのライブラリを定義するソースユニット名で修飾したライブラリ名を使用します。 特定のライブラリアドレスでコードをリンクするには、コマンドラインの --libraries オプションに同じ識別子を指定する必要があります。

例えば、次のコード

let a := linkersymbol("file.sol:Math")

は、 --libraries "file.sol:Math=0x1234567890123456789012345678901234567890 オプションを付けてリンカーを起動した場合に、次のコードと同じです。

let a := 0x1234567890123456789012345678901234567890

Solidityリンカーの詳細は Using the Commandline Compiler を参照してください。

memoryguard

この関数はEVM方言のオブジェクトで使用できます。 let ptr := memoryguard(size)size はリテラル数)のコール元は、範囲 [0, size) または ptr から始まる境界の無い範囲のいずれかのメモリのみを使用することを約束します。

memoryguard コールの存在は、すべてのメモリアクセスがこの制限に従っていることを示すので、オプティマイザは追加の最適化ステップを実行できます。 例えば、スタックリミットイベーダーは、他の方法では到達できないスタック変数をメモリに移動させようとします。

Yulオプティマイザは、目的のためにメモリ範囲 [size, ptr) のみを使用することを約束します。 オプティマイザがメモリを確保する必要がない場合は、その ptr == size を保持します。

memoryguard は複数回呼び出すことができますが、1つのYulサブオブジェクト内で同じリテラルを引数として持つ必要があります。 サブオブジェクトの中に少なくとも1つの memoryguard の呼び出しが見つかった場合、追加のオプティマイザのステップが実行されます。

verbatim

verbatim... ビルトイン関数群は、Yulコンパイラーが知らないオペコードのバイトコードを作成できます。 また、オプティマイザによって変更されることがないバイトコードシーケンスを作成することもできます。

関数は verbatim_<n>i_<m>o("<data>", ...) のようになります。 ここで、

  • n は0~99の10進数で、入力スタックのスロット/変数の数を指定します。

  • m は0~99の10進数で、出力スタックのスロット/変数の数を指定します。

  • data はバイト列を含む文字列リテラルです。

例えば、入力を2倍する関数を定義する際に、オプティマイザが定数2に触れないようにするには、次のようにします。

let x := calldataload(0)
let double := verbatim_1i_1o(hex"600202", x)

このコードでは、 x を取得するための dup1 オペコード(オプティマイザは calldataload オペコードの結果を直接再利用するかもしれませんが)が、 600202 に続いて表示されます。 このコードは、 x のコピーされた値を消費して、スタックの一番上に結果を生成すると想定されます。 その後、コンパイラは double 用のスタックスロットを割り当て、そこに結果を格納するコードを生成します。

他のオペコードと同様に、引数はスタック上に左端の引数が一番上になるように並べられ、戻り値は右端の変数がスタックの一番上になるように並べられるとされています。

verbatim は、任意のオペコードや、Solidityコンパイラにとって未知のオペコードを生成するために使用できるため、オプティマイザと verbatim を併用する際には注意が必要です。 オプティマイザがオフになっていても、コードジェネレーターはスタックレイアウトを決定しなければなりません。 つまり、 verbatim を使ってスタックの高さを変更すると、未定義の動作になる可能性があります。

以下は、コンパイラではチェックされない逐語的バイトコードの制限事項の非網羅的なリストです。 これらの制限に違反すると、未定義の動作を引き起こす可能性があります。

  • コントロールフローはverbatimブロックの中に飛び込んだり、外に出たりしてはいけませんが、同じverbatimブロックの中では飛び込むことができます。

  • 入力と出力パラメータ以外のスタックの内容にアクセスしてはいけません。

  • スタックの高さの差は、正確には m - n (出力スロットから入力スロットを引いたもの)です。

  • Verbatimのバイトコードは、周囲のバイトコードを想定できません。必要なパラメータはすべてスタック変数として渡さなければなりません。

オプティマイザはバイトコードを逐語的に分析せず、常に状態のすべての側面を修正することを前提としているため、 verbatim 関数呼び出し全体ではごくわずかな最適化しかできません。

オプティマイザは、バーベイタムバイトコードを不透明なコードブロックとして扱います。 分割はしませんが、移動、複製、同一のバーベイタムバイトコードブロックとの結合は可能です。 逐語的バイトコードブロックがコントロールフローから到達できない場合、そのブロックは削除されます。

警告

EVMの改善が既存のスマートコントラクトを破壊するかどうかを議論する際、 verbatim の機能はSolidityのコンパイラ自体が使用する機能と同じように考慮できません。

注釈

混乱を避けるため、文字列 verbatim で始まる識別子はすべて予約されており、ユーザー定義の識別子には使用できません。

Yulオブジェクトの仕様

Yulオブジェクトは、名前の付いたコードおよびデータセクションをグループ化するために使用されます。 関数 datasizedataoffsetdatacopy を使用して、コード内からこれらのセクションにアクセスできます。 16進文字列は、データを16進エンコーディングで、通常の文字列をネイティブエンコーディングで指定するために使用できます。 コードの場合、 datacopy はアセンブルされたバイナリ表現にアクセスします。

Object = 'object' StringLiteral '{' Code ( Object | Data )* '}'
Code = 'code' Block
Data = 'data' StringLiteral ( HexLiteral | StringLiteral )
HexLiteral = 'hex' ('"' ([0-9a-fA-F]{2})* '"' | '\'' ([0-9a-fA-F]{2})* '\'')
StringLiteral = '"' ([^"\r\n\\] | '\\' .)* '"'

上記の Block は、前章で説明したYulコード文法の Block を指します。

注釈

名前が _deployed で終わるオブジェクトは、Yulオプティマイザによってデプロイされたコードとして扱われます。 この唯一の結果は、オプティマイザのガスコストのヒューリスティックが異なるということです。

注釈

. を含む名前のデータオブジェクトやサブオブジェクトを定義できますが、 . は他のオブジェクトの内部にあるオブジェクトにアクセスするためのセパレータとして使用されるため、 datasizedataoffsetdatacopy を介してアクセスできません。

注釈

".metadata" というデータオブジェクトには特別な意味があります: コードからはアクセスできず、オブジェクト内の位置に関わらず、常にバイトコードの最後尾に付加されます。

今後、特別な意味を持つデータオブジェクトが他に追加されるかもしれませんが、その名前は常に . で始まります。

Yulオブジェクトの例を以下に示します。

// コントラクトは単一のオブジェクトで構成され、デプロイされるコードや作成できる他のコントラクトを表すサブオブジェクトがあります。
// 1つのcodeノードは、オブジェクトの実行可能なコードです。
// すべての(他の)名前の付いたオブジェクトやデータセクションはシリアライズされ、特別な組み込み関数datacopy / dataoffset / datasizeでアクセスできるようになります。
// カレントオブジェクト、サブオブジェクト、カレントオブジェクト内部のデータアイテムがスコープに入ります。
object "Contract1" {
    // コントラクトのコンストラクタコード
    code {
        function allocate(size) -> ptr {
            ptr := mload(0x40)
            // Note that Solidity generated IR code reserves memory offset ``0x60`` as well, but a pure Yul object is free to use memory as it chooses.
            if iszero(ptr) { ptr := 0x60 }
            mstore(0x40, add(ptr, size))
        }

        // 最初に"Contract2"を作成する
        let size := datasize("Contract2")
        let offset := allocate(size)
        // これは、EVMのコードコピーになる
        datacopy(offset, dataoffset("Contract2"), size)
        // コンストラクトパラメータは単一の数値0x1234
        mstore(add(offset, size), 0x1234)
        pop(create(0, offset, add(size, 32)))

        // ランタイムオブジェクトを返す(現在実行中のコードがコンストラクタのコード)
        size := datasize("Contract1_deployed")
        offset := allocate(size)
        // これは、EVMではコードコピーになる
        datacopy(offset, dataoffset("Contract1_deployed"), size)
        return(offset, size)
    }

    data "Table2" hex"4123"

    object "Contract1_deployed" {
        code {
            function allocate(size) -> ptr {
                ptr := mload(0x40)
                // Note that Solidity generated IR code reserves memory offset ``0x60`` as well, but a pure Yul object is free to use memory as it chooses.
                if iszero(ptr) { ptr := 0x60 }
                mstore(0x40, add(ptr, size))
            }

            // ランタイムコード

            mstore(0, "Hello, World!")
            return(0, 0x20)
        }
    }

    // 埋め込みオブジェクト。使用例としては、外側がファクトリーのコントラクトで、Contract2がファクトリーで作成されるコード。
    object "Contract2" {
        code {
            // code here ...
        }

        object "Contract2_deployed" {
            code {
                // code here ...
            }
        }

        data "Table1" hex"4123"
    }
}

Yulオプティマイザ

Yulオプティマイザは、Yulコード上で動作し、入力、出力、中間状態を同じ言語で表現します。 これにより、オプティマイザのデバッグや検証が容易になります。

各最適化ステージの詳細やオプティマイザの使用方法については、一般的な オプティマイザドキュメント を参照してください。

SolidityをスタンドアローンのYulモードで使いたい場合は、 --optimize でオプティマイザを起動し、オプションで --optimize-runs期待コントラクト実行回数 を指定します。

solc --strict-assembly --optimize --optimize-runs 200

Solidityモードでは、通常のオプティマイザと一緒にYulオプティマイザが実行されます。

最適化ステップシーケンス

最適化の順序や略語のリストに関する詳しい情報は オプティマイザのドキュメント にあります。

ERC20の完全な例

object "Token" {
    code {
        // スロット0にコントラクト作成者を格納
        sstore(0, caller())

        // コントラクトのデプロイ
        datacopy(0, dataoffset("runtime"), datasize("runtime"))
        return(0, datasize("runtime"))
    }
    object "runtime" {
        code {
            // Ether送信の保護
            require(iszero(callvalue()))

            // ディスパッチャー
            switch selector()
            case 0x70a08231 /* "balanceOf(address)" */ {
                returnUint(balanceOf(decodeAsAddress(0)))
            }
            case 0x18160ddd /* "totalSupply()" */ {
                returnUint(totalSupply())
            }
            case 0xa9059cbb /* "transfer(address,uint256)" */ {
                transfer(decodeAsAddress(0), decodeAsUint(1))
                returnTrue()
            }
            case 0x23b872dd /* "transferFrom(address,address,uint256)" */ {
                transferFrom(decodeAsAddress(0), decodeAsAddress(1), decodeAsUint(2))
                returnTrue()
            }
            case 0x095ea7b3 /* "approve(address,uint256)" */ {
                approve(decodeAsAddress(0), decodeAsUint(1))
                returnTrue()
            }
            case 0xdd62ed3e /* "allowance(address,address)" */ {
                returnUint(allowance(decodeAsAddress(0), decodeAsAddress(1)))
            }
            case 0x40c10f19 /* "mint(address,uint256)" */ {
                mint(decodeAsAddress(0), decodeAsUint(1))
                returnTrue()
            }
            default {
                revert(0, 0)
            }

            function mint(account, amount) {
                require(calledByOwner())

                mintTokens(amount)
                addToBalance(account, amount)
                emitTransfer(0, account, amount)
            }
            function transfer(to, amount) {
                executeTransfer(caller(), to, amount)
            }
            function approve(spender, amount) {
                revertIfZeroAddress(spender)
                setAllowance(caller(), spender, amount)
                emitApproval(caller(), spender, amount)
            }
            function transferFrom(from, to, amount) {
                decreaseAllowanceBy(from, caller(), amount)
                executeTransfer(from, to, amount)
            }

            function executeTransfer(from, to, amount) {
                revertIfZeroAddress(to)
                deductFromBalance(from, amount)
                addToBalance(to, amount)
                emitTransfer(from, to, amount)
            }

            /* ---------- コールデータのデコード関数 ----------- */
            function selector() -> s {
                s := div(calldataload(0), 0x100000000000000000000000000000000000000000000000000000000)
            }

            function decodeAsAddress(offset) -> v {
                v := decodeAsUint(offset)
                if iszero(iszero(and(v, not(0xffffffffffffffffffffffffffffffffffffffff)))) {
                    revert(0, 0)
                }
            }
            function decodeAsUint(offset) -> v {
                let pos := add(4, mul(offset, 0x20))
                if lt(calldatasize(), add(pos, 0x20)) {
                    revert(0, 0)
                }
                v := calldataload(pos)
            }
            /* ---------- コールデータのエンコード関数 ---------- */
            function returnUint(v) {
                mstore(0, v)
                return(0, 0x20)
            }
            function returnTrue() {
                returnUint(1)
            }

            /* -------- イベント ---------- */
            function emitTransfer(from, to, amount) {
                let signatureHash := 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
                emitEvent(signatureHash, from, to, amount)
            }
            function emitApproval(from, spender, amount) {
                let signatureHash := 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925
                emitEvent(signatureHash, from, spender, amount)
            }
            function emitEvent(signatureHash, indexed1, indexed2, nonIndexed) {
                mstore(0, nonIndexed)
                log3(0, 0x20, signatureHash, indexed1, indexed2)
            }

            /* -------- ストレージレイアウト ---------- */
            function ownerPos() -> p { p := 0 }
            function totalSupplyPos() -> p { p := 1 }
            function accountToStorageOffset(account) -> offset {
                offset := add(0x1000, account)
            }
            function allowanceStorageOffset(account, spender) -> offset {
                offset := accountToStorageOffset(account)
                mstore(0, offset)
                mstore(0x20, spender)
                offset := keccak256(0, 0x40)
            }

            /* -------- ストレージアクセス ---------- */
            function owner() -> o {
                o := sload(ownerPos())
            }
            function totalSupply() -> supply {
                supply := sload(totalSupplyPos())
            }
            function mintTokens(amount) {
                sstore(totalSupplyPos(), safeAdd(totalSupply(), amount))
            }
            function balanceOf(account) -> bal {
                bal := sload(accountToStorageOffset(account))
            }
            function addToBalance(account, amount) {
                let offset := accountToStorageOffset(account)
                sstore(offset, safeAdd(sload(offset), amount))
            }
            function deductFromBalance(account, amount) {
                let offset := accountToStorageOffset(account)
                let bal := sload(offset)
                require(lte(amount, bal))
                sstore(offset, sub(bal, amount))
            }
            function allowance(account, spender) -> amount {
                amount := sload(allowanceStorageOffset(account, spender))
            }
            function setAllowance(account, spender, amount) {
                sstore(allowanceStorageOffset(account, spender), amount)
            }
            function decreaseAllowanceBy(account, spender, amount) {
                let offset := allowanceStorageOffset(account, spender)
                let currentAllowance := sload(offset)
                require(lte(amount, currentAllowance))
                sstore(offset, sub(currentAllowance, amount))
            }

            /* ---------- ユーティリティ関数 ---------- */
            function lte(a, b) -> r {
                r := iszero(gt(a, b))
            }
            function gte(a, b) -> r {
                r := iszero(lt(a, b))
            }
            function safeAdd(a, b) -> r {
                r := add(a, b)
                if or(lt(r, a), lt(r, b)) { revert(0, 0) }
            }
            function calledByOwner() -> cbo {
                cbo := eq(owner(), caller())
            }
            function revertIfZeroAddress(addr) {
                require(addr)
            }
            function require(condition) {
                if iszero(condition) { revert(0, 0) }
            }
        }
    }
}

インポートパスの解決

全てのプラットフォームで再現可能なビルドをサポートするために、Solidityコンパイラはソースファイルが格納されるファイルシステムを抽象化する必要があります。 インポートで使用されるパスは、どこでも同じように動作しなければならない一方で、コマンドラインインターフェースは、良いユーザーエクスペリエンスを提供するために、プラットフォーム固有のパスを扱うことができなければなりません。 このセクションでは、Solidityがこれらの要件をどのように解決しているかを詳しく説明します。

バーチャルファイルシステム

コンパイラは内部データベース( virtual filesystem 、略して VFS )を保持しており、各ソースユニットには不透明で構造化されていない識別子である一意の ソースユニット名 が割り当てられています。 インポート文 を使用する際には、ソースユニット名を参照する インポートパス を指定します。

インポートコールバック

VFSには、コンパイラーが入力として受け取ったファイルのみが最初に入力されます。 使用するコンパイラの種類によって異なる インポートコールバック を使用して、コンパイル中に追加のファイルを読み込むことができます(後述)。 コンパイラは、VFS内のインポートパスに一致するソースユニット名が見つからない場合、コールバックを起動し、その名前で配置されるソースコードを取得する役割を果たします。 インポートコールバックは、ソースユニット名をパスとしてだけでなく、任意の方法で自由に解釈できます。 必要なときに利用可能なコールバックがない場合や、ソースコードの取得に失敗した場合は、コンパイルに失敗します。

コマンドラインコンパイラには、ソースユニット名をローカルファイルシステムのパスとして解釈する初歩的なコールバックである Host Filesystem Loader が用意されています。 JavaScriptインターフェース はデフォルトでは提供していませんが、ユーザーが提供することもできます。 このメカニズムを使用して、ローカルファイルシステム以外の場所からソースコードを取得できます(ブラウザでコンパイラを実行している場合など、アクセスできない場合もあります)。 例えば、 Remix IDE は汎用性の高いコールバックを提供しており、これを利用して HTTP、IPFS、SwarmのURLからファイルをインポートしたり、NPMレジストリのパッケージを直接参照したり できます。

注釈

Host Filesystem Loaderのファイル検索は、プラットフォームに依存します。 例えば、ソースユニット名のバックスラッシュは、ディレクトリセパレータとして解釈されるかどうか、また、検索では大文字と小文字が区別されるかどうかが、基礎となるプラットフォームに依存します。

移植性を考慮して、特定のインポートコールバックでのみ正しく動作するインポートパスや、特定のプラットフォームでのみ動作するインポートパスは使用しないことをお勧めします。 例えば、バックスラッシュをサポートするプラットフォームでもパスの区切りとして機能するフォワードスラッシュを常に使用するべきです。

Virtual Filesystemの初期コンテンツ

VFSの初期コンテンツは、コンパイラの起動方法によって異なります。

  1. solc / コマンドラインインターフェース

    コンパイラのコマンドラインインターフェースを使用してファイルをコンパイルする際に、Solidityコードを含むファイルへの1つまたは複数のパスを指定します。

    solc contract.sol /usr/local/dapp-bin/token.sol
    

    この方法で読み込まれたファイルのソースユニット名は、そのパスを正規の形式に変換し、可能であればベースパスまたはインクルードパスのいずれかからの相対パスとすることで構築されます。 この処理の詳細については、 CLI Path NormalizationとStripping を参照してください。

  1. Standard JSON

    Standard JSON APIを使用する場合( JavaScriptインターフェース または --standard-json コマンドラインオプションを使用)、すべてのソースファイルのコンテンツなどを含むJSONフォーマットの入力を提供します。

    {
        "language": "Solidity",
        "sources": {
            "contract.sol": {
                "content": "import \"./util.sol\";\ncontract C {}"
            },
            "util.sol": {
                "content": "library Util {}"
            },
            "/usr/local/dapp-bin/token.sol": {
                "content": "contract Token {}"
            }
        },
        "settings": {"outputSelection": {"*": { "*": ["metadata", "evm.bytecode"]}}}
    }
    

    sources 辞書は仮想ファイルシステムの初期コンテンツとなり、そのキーはソースユニット名として使用されます。

  1. Standard JSON (インポートコールバック経由)

    Standard JSONでは、ソースコードの取得にインポートコールバックを使用するようにコンパイラに指示することも可能です。

    {
        "language": "Solidity",
        "sources": {
            "/usr/local/dapp-bin/token.sol": {
                "urls": [
                    "/projects/mytoken.sol",
                    "https://example.com/projects/mytoken.sol"
                ]
            }
        },
        "settings": {"outputSelection": {"*": { "*": ["metadata", "evm.bytecode"]}}}
    }
    

    インポートコールバックが利用可能な場合、コンパイラーは urls で指定された文字列を一つずつ、読み込みに成功するかリストの最後に達するまで渡します。

    ソースユニット名は content を使用する場合と同じ方法で決定されます。 これらは sources 辞書のキーであり、 urls の内容はこれらに何ら影響を与えません。

  1. 標準入力

    コマンドラインでは、コンパイラの標準入力にソースを送信することも可能です。

    echo 'import "./util.sol"; contract C {}' | solc -
    

    引数の1つとして使われる - は、標準入力の内容を仮想ファイルシステムの特別なソースユニット名 <stdin> の下に置くようにコンパイラに指示します。

VFSが初期化された後も、インポートコールバックによってのみファイルを追加できます。

インポート

インポート文では インポートパス を指定します。 インポートパスの指定方法に基づいて、インポートは2つの種類に分けられます。

  • ダイレクトインポート: ソースユニットのフルネームを直接指定します。

  • 相対インポート: ./ または ../ で始まるパスを指定して、インポートファイルのソースユニット名と組み合わせます。

contracts/contract.sol
import "./math/math.sol";
import "contracts/tokens/token.sol";

上の例では、 ./math/math.solcontracts/tokens/token.sol がインポートパスで、変換後のソースユニット名はそれぞれ contracts/math/math.solcontracts/tokens/token.sol です。

ダイレクトインポート

./../ で始まらないインポートは、 ダイレクトインポート です。

import "/project/lib/util.sol";         // source unit name: /project/lib/util.sol
import "lib/util.sol";                  // source unit name: lib/util.sol
import "@openzeppelin/address.sol";     // source unit name: @openzeppelin/address.sol
import "https://example.com/token.sol"; // source unit name: https://example.com/token.sol

import remappings を適用すると、インポートパスは単にソースユニット名になります。

注釈

ソースユニット名は単なる識別子であり、その値がたまたまパスのように見えたとしても、シェルで一般的に期待される正規化ルールの対象にはなりません。 /.//../ のセグメントや複数のスラッシュのシーケンスがあっても、その一部として残ります。 ソースが標準のJSONインターフェースで提供されている場合、ディスク上の同じファイルを参照するソースユニット名に、異なるコンテンツを関連付けることができます。

ソースが仮想ファイルシステムで利用できない場合、コンパイラはソースユニット名をインポートコールバックに渡します。 ホストファイルシステムローダーはこの名前をパスとして使用し、ディスク上のファイルを検索しようとします。 このとき、プラットフォーム固有の正規化ルールが働き、VFSでは異なるとされていた名前が、実際には同じファイルが読み込まれることがあります。 例えば、 /project/lib/math.sol/project/lib/../lib///math.sol は、ディスク上の同じファイルを参照しているにもかかわらず、VFSでは全く異なるものとみなされます。

注釈

インポートコールバックがディスク上の同じファイルから2つの異なるソースユニット名のソースコードを読み込むことになっても、コンパイラーはそれらを別々のソースユニットと見なします。 重要なのはソースユニット名であって、コードの物理的な場所ではありません。

相対インポート

./ または ../ で始まるインポートは、相対インポート です。 このようなインポートは、インポート元のソースユニット名からの相対パスを指定します。

/project/lib/math.sol
import "./util.sol" as util;    // source unit name: /project/lib/util.sol
import "../token.sol" as token; // source unit name: /project/token.sol
lib/math.sol
import "./util.sol" as util;    // source unit name: lib/util.sol
import "../token.sol" as token; // source unit name: token.sol

注釈

相対インポートは 常に./ または ../ で始まるので、 import "util.sol"import "./util.sol" とは異なり、ダイレクトインポートとなります。 どちらのパスもホストファイルシステムでは相対パスとみなされますが、VFSでは util.sol が絶対パスとなります。

ここでは、セパレータを含まず、2つのパスセパレータで囲まれた空でない部分を パスセグメント と定義します。 セパレータとは、フォワードスラッシュや文字列の先頭/末尾のことです。 例えば、 ./abc/..// では3つのパスセグメントがあります: .abc..

コンパイラは、インポートパスに基づき、次のようにインポートをソースユニット名に解決します:

  1. インポートするソースユニット名から始めます。

  2. 解決された名前から、スラッシュが先行する最後のパスセグメントが削除されます。

  3. 次に、インポートパスの各セグメントについて、左端から順に説明します:

    • セグメントが . の場合、それはスキップされます。

    • セグメントが ... の場合、スラッシュが先行する最後のパスセグメントが解決された名前から削除されます。

    • それ以外の場合は、そのセグメント(解決された名前が空でない場合は、スラッシュが1つ先行する)が解決された名前に追加されます。

スラッシュが先行する最後のパスセグメントの削除は、以下のように動作すると理解されています。

  1. 最後のスラッシュから先はすべて削除されます(例: a/b//c.sola/b// になります)。

  2. 後続のスラッシュはすべて削除されます(例: a/b//a/b になります)。

つまり、 ... はすべて削除され、複数のスラッシュは1つにつぶされます。 一方、インポートモジュールのソースユニット名から来る部分は非正規化されたままです。 これにより、インポートファイルがURLで識別される場合、 protocol:// の部分が protocol:/ にならないようにできます。

インポートパスがすでに正規化されている場合は、上記のアルゴリズムで非常に直感的な結果を得ることができます。 以下は、正規化されていない場合の例です。

lib/src/../contract.sol
import "./util/./util.sol";         // source unit name: lib/src/../util/util.sol
import "./util//util.sol";          // source unit name: lib/src/../util/util.sol
import "../util/../array/util.sol"; // source unit name: lib/src/array/util.sol
import "../.././../util.sol";       // source unit name: util.sol
import "../../.././../util.sol";    // source unit name: util.sol

注釈

先行する .. セグメントを含む相対インポートの使用はお勧めできません。 同じ効果を得るには、 ベースパスとインクルードパス を含むダイレクトインポートを使用する方がより確実です。

ベースパスとインクルードパス

ベースパスとインクルードパスは、ホストファイルシステムローダがファイルをロードするディレクトリを表します。 ローダーにソースユニット名が渡されると、その前にベースパスが付けられ、ファイルシステムのルックアップが行われます。 ルックアップが成功しない場合は、インクルードパスリスト上のすべてのディレクトリに対して同様の処理を行います。

ベースパスをプロジェクトのルートディレクトリに設定し、インクルードパスを使って、プロジェクトが依存するライブラリを含む追加の場所を指定することをお勧めします。 これにより、プロジェクトのファイルシステム上の位置にかかわらず、これらのライブラリから統一的にインポートできます。 例えば、npmを使用してパッケージをインストールし、コントラクトが @openzeppelin/contracts/utils/Strings.sol をインポートする場合、これらのオプションを使用して、npmパッケージディレクトリのいずれかにライブラリが存在することをコンパイラに伝えることができます。

solc contract.sol \
    --base-path . \
    --include-path node_modules/ \
    --include-path /usr/local/lib/node_modules/

ライブラリをローカルパッケージディレクトリやグローバルパッケージディレクトリにインストールしても、あるいはプロジェクトルートの直下にインストールしても、コントラクトは(同じメタデータで)コンパイルされます。

デフォルトでは、ベースパスは空で、ソースユニット名は変更されません。 ソースユニット名が相対パスの場合、コンパイラを起動したディレクトリでファイルが検索されます。 また、ソースユニット名の絶対パスが実際にディスク上の絶対パスとして解釈される唯一の値です。 ベースパスが相対パスの場合は、コンパイラの現在の作業ディレクトリからの相対パスとして解釈されます。

注釈

インクルードパスは空の値を持つことはできず、空ではないベースパスと一緒に使用する必要があります。

注釈

インクルードパスとベースパスは、インポートの解決を曖昧にしない限り、重なっても構いません。 例えば、ベースパス内のディレクトリをインクルードディレクトリとして指定したり、別のインクルードディレクトリのサブディレクトリであるインクルードディレクトリを持つことができます。 ホストファイルシステムローダーに渡されたソースユニット名が、複数のインクルードパスまたはインクルードパスとベースパスの組み合わせで既存のパスを表している場合にのみ、コンパイラはエラーを発行します。

CLI Path NormalizationとStripping

コマンドラインでは、コンパイラは他のプログラムと同じように動作します。 プラットフォームに固有の形式でパスを受け取り、相対パスは現在の作業ディレクトリからの相対パスです。 しかし、コマンドラインでパスが指定されたファイルに割り当てられたソースユニット名は、プロジェクトが別のプラットフォームでコンパイルされていたり、コンパイラが別のディレクトリから起動されていたりしても、変更されるべきではありません。 そのためには、コマンドラインで指定されたソースファイルのパスを正規の形式に変換し、可能であればベースパスまたはインクルードパスからの相対パスにする必要があります。

正規化のルールは以下の通りです。

  • パスが相対パスの場合は、カレントワーキングディレクトリを先頭に置くことで絶対パスになります。

  • 内部の ... のセグメントは折りたたまれます。

  • プラットフォーム固有のパスセパレータは、フォワードスラッシュに置き換えられます。

  • 複数の連続したパスセパレータのシーケンスは、1つのセパレータに潰されます( UNCパス の先頭のスラッシュでない限り)。

  • パスにルート名(Windowsのドライブレターなど)が含まれていて、そのルートが現在の作業ディレクトリのルートと同じ場合は、ルートを / に置き換えます。

  • パスのシンボリックリンクは解決 されません

    • 唯一の例外は、相対パスを絶対パスにする際に、現在の作業ディレクトリへのパスを前置することです。 一部のプラットフォームでは、作業ディレクトリは常にシンボリックリンクが解決された状態で報告されるため、一貫性を保つためにコンパイラはすべての場所でシンボリックリンクを解決します。

  • ファイルシステムでは大文字と小文字を区別しない(case-insensitive)が、 case-preserving とディスク上の実際の大文字と小文字が異なる場合でも、パスの元の大文字と小文字は保存されます。

注釈

プラットフォームに依存しないパスを作ることができない場合があります。 例えば、Windowsでは、コンパイラが現在のドライブのルートディレクトリを / として参照することで、ドライブレターの使用を避けることができますが、他のドライブにつながるパスにはドライブレターが必要です。 このような状況を回避するには、すべてのファイルが同じドライブ上の単一のディレクトリツリーで利用できるようにする必要があります。

正規化後、コンパイラはソースファイルのパスを相対化しようとします。 まずベースパスを試し、次にインクルードパスを指定された順に試します。 ベースパスが空であったり、指定されていない場合は、カレントワーキングディレクトリへのパス(すべてのシンボリックリンクが解決されている)と同じであるかのように扱われます。 この結果は、正規化されたディレクトリパスが正規化されたファイルパスの正確なプレフィックスである場合にのみ受け入れられます。 そうでなければ、ファイルパスは絶対的なままです。 これにより、変換が曖昧にならず、相対パスが ../ で始まらないことが保証されます。 変換後のファイルパスがソースユニット名となります。

注釈

ストリッピングによって生成される相対パスは、ベースパスおよびインクルードパス内で一意でなければなりません。 例えば、次のコマンドで /project/contract.sol/lib/contract.sol の両方が存在する場合、コンパイラはエラーを発行します。

solc /project/contract.sol --base-path /project --include-path /lib

注釈

バージョン 0.8.8 より前の CLI では、パスストリッピングは行われず、適用される正規化はパスセパレータの変換のみでした。 古いバージョンのコンパイラーを使用する場合は、ベースパスからコンパイラーを起動し、コマンドラインでは相対パスのみを使用することをお勧めします。

許可されるパス

セキュリティ対策として、Host Filesystem Loaderは、デフォルトで安全とされるいくつかの場所以外からのファイルのロードを拒否します。

  • Standard JSONモード以外の場合。

    • コマンドラインで指定された入力ファイルを含むディレクトリ。

    • リマッピング ターゲットとして使用されるディレクトリ。 ターゲットがディレクトリでない場合( //./.. で終わらない場合)は、ターゲットを含むディレクトリが代わりに使用されます。

    • ベースパスとインクルードパス。

  • Standard JSONモードの場合。

    • ベースパスとインクルードパス。

--allow-paths オプションを使って、追加のディレクトリをホワイトリストに登録できます。 このオプションでは、コンマで区切ってパスのリストを指定できます。

cd /home/user/project/
solc token/contract.sol \
    lib/util.sol=libs/util.sol \
    --base-path=token/ \
    --include-path=/lib/ \
    --allow-paths=../utils/,/tmp/libraries

上記のコマンドでコンパイラを起動した場合、Host Filesystem Loaderは以下のディレクトリからのファイルのインポートを許可します。

  • /home/user/project/token/token/ には入力ファイルがあり、またベースパスでもあるため)。

  • /lib//lib/ はインクルードパスの一つであるため)。

  • /home/user/project/libs/libs/ はリマップ対象を含むディレクトリのため)。

  • /home/user/utils/../utils/--allow-paths にパスされたため)。

  • /tmp/libraries//tmp/libraries--allow-paths にパスされたため)。

注釈

コンパイラの作業ディレクトリは、デフォルトで許可されているパスのうち、たまたまベースパスであった場合(またはベースパスが指定されていないか空の値であった場合)にのみ許可されます。

注釈

コンパイラは、許可されたパスが実際に存在するかどうか、またそれらがディレクトリであるかどうかはチェックしません。 存在しないパスや空のパスは単に無視されます。 許可されたパスがディレクトリではなくファイルに一致した場合、そのファイルもホワイトリストとみなされます。

注釈

許可されたパスは、ファイルシステムがそうでない場合でも、大文字と小文字を区別します。 大文字と小文字は、インポートで使われているものと正確に一致しなければなりません。 例えば、 --allow-paths tokensimport "Tokens/IERC20.sol" とは一致しません。

警告

許可されているディレクトリからシンボリックリンクでのみアクセスできるファイルやディレクトリは、自動的にホワイトリストに登録されません。 例えば、上の例の token/contract.sol が実際には /etc/passwd を指すシンボリックリンクであった場合、 /etc/ が許可されたパスの一つでない限り、コンパイラはそれを読み込むことを拒否します。

インポートリマッピング

インポートリマッピングでは、インポートを仮想ファイルシステムの異なる場所にリダイレクトできます。 このメカニズムは、インポートパスとソースユニット名の間の変換を変更することで機能します。 例えば、仮想ディレクトリ github.com/ethereum/dapp-bin/library/ からのインポートを、代わりに dapp-bin/library/ からのインポートと見なすようなリマッピングを設定できます。

コンテキスト を指定することで、リマッピングの範囲を制限できます。 これにより、特定のライブラリまたは特定のファイルにあるインポートのみに適用されるリマッピングを作成できます。 コンテキストを指定しない場合、リマッピングは仮想ファイルシステム内のすべてのファイルにある、一致するすべてのインポートに適用されます。

インポートのリマッピングは context:prefix=target の形をしています。

  • context は、インポートを含むファイルのソースユニット名の先頭と一致する必要があります。

  • prefix は、インポート後のソースユニット名の先頭と一致する必要があります。

  • target は、プレフィックスが置き換えられる値です。

例えば、ローカルで https://github.com/ethereum/dapp-bin//project/dapp-bin にクローンして、コンパイラを実行した場合:

solc github.com/ethereum/dapp-bin/=dapp-bin/ --base-path /project source.sol

をソースファイルに記述できます。

import "github.com/ethereum/dapp-bin/library/math.sol"; // source unit name: dapp-bin/library/math.sol

コンパイラは、 dapp-bin/library/math.sol の下のVFSでファイルを探します。 そこにファイルがない場合は、ソースユニット名がHost Filesystem Loaderに渡され、Host Filesystem Loaderは /project/dapp-bin/library/iterable_mapping.sol を探します。

警告

リマッピングに関する情報はコントラクトメタデータに格納されています。 コンパイラが生成するバイナリにはメタデータのハッシュが埋め込まれているため、リマッピングを変更すると異なるバイトコードになります。

このため、リマッピングのターゲットにローカル情報が含まれないように注意する必要があります。 例えば、あなたのライブラリが /home/user/packages/mymath/math.sol にある場合、 @math/=/home/user/packages/mymath/ のようなリマッピングを行うと、あなたのホームディレクトリがメタデータに含まれることになります。 このようなリマッピングを行った同じバイトコードを別のマシンで再現するためには、ローカルのディレクトリ構造の一部をVFSに、(Host Filesystem Loaderに依存している場合は)ホストファイルシステムにも再現する必要があります。

ローカルのディレクトリ構造がメタデータに埋め込まれるのを避けるために、ライブラリを含むディレクトリを インクルードパス として指定することが推奨されます。 例えば、上記の例では、 --include-path /home/user/packages/ を指定すると、 mymath/ で始まるインポートを使用できます。 リマッピングとは異なり、このオプションだけでは mymath@math に見せることはできませんが、シンボリックリンクを作成したり、パッケージのサブディレクトリの名前を変更することで実現できます。

もっと複雑な例として、 /project/dapp-bin_old にチェックアウトした古いバージョンのdapp-binを使っているモジュールに依存しているとします。

solc module1:github.com/ethereum/dapp-bin/=dapp-bin/ \
     module2:github.com/ethereum/dapp-bin/=dapp-bin_old/ \
     --base-path /project \
     source.sol

つまり、 module2 のインポートはすべて旧バージョンを指しますが、 module1 のインポートは新バージョンを指します。

ここでは、リマッピングの動作に関する詳細なルールをご紹介します。

  1. リマッピングは、インポートパスとソースユニット名の間の変換にのみ影響します。

    その他の方法でVFSに追加されたソースユニット名は、リマッピングできません。 例えば、コマンドラインで指定したパスや、Standard JSONの sources.urls にあるパスは影響を受けません。

    solc /project/=/contracts/ /project/contract.sol # source unit name: /project/contract.sol
    

    上記の例では、コンパイラは /project/contract.sol からソースコードを読み込み、VFSの /contract/contract.sol の下ではなく、その正確なソースユニット名の下に置くことになります。

  2. コンテキストとプレフィックスは、インポートパスではなく、ソースユニット名と一致する必要があります。

    • つまり、 ./../ はソースユニット名への変換時に置き換えられてしまうため、直接リマップできませんが、置き換えられた部分をリマップすることは可能です。

      solc ./=a/ /project/=b/ /project/contract.sol # source unit name: /project/contract.sol
      
      /project/contract.sol
      import "./util.sol" as util; // source unit name: b/util.sol
      
    • ベースパスや、インポートコールバックによって内部的に追加されるだけのパスの部分をリマッピングすることはできません:

      solc /project/=/contracts/ /project/contract.sol --base-path /project # source unit name: contract.sol
      
      /project/contract.sol
      import "util.sol" as util; // source unit name: util.sol
      
  3. Targetはソースユニット名に直接挿入され、必ずしも有効なパスである必要はありません。

    • インポートコールバックがそれを処理できる限り、何でもよいのです。 ホストファイルシステムローダーの場合は、相対パスも含まれます。 JavaScriptインターフェースを使用する場合、コールバックが処理できるならば、URLや抽象的な識別子を使用することもできます。

    • リマッピングは、相対的なインポートがすでにソースユニット名に解決された後に行われます。 つまり、 ./../ で始まるターゲットは特別な意味を持たず、ソースファイルの位置ではなくベースパスに対する相対的なものです。

    • リマップ対象は正規化されていないので、 @root/=./a/b//@root/contract.sol./a/b//contract.sol にリマップし、 a/b/contract.sol にはなりません。

    • ターゲットがスラッシュで終わっていない場合、コンパイラは自動的にスラッシュを追加しません。

      solc /project/=/contracts /project/contract.sol # source unit name: /project/contract.sol
      
      /project/contract.sol
      import "/project/util.sol" as util; // source unit name: /contractsutil.sol
      
  4. コンテキストとプレフィックスはパターンであり、マッチは正確でなければなりません。

    • a//b=ca/b に合わせません。

    • ソースユニット名は正規化されていないので、 a/b=ca//b にもマッチしません。

    • ファイル名やディレクトリ名の一部もマッチします。 /newProject/con:/new=old/newProject/contract.sol と一致し、 oldProject/contract.sol にリマップされます。

  1. 1つのインポートに適用されるリマッピングは、最大で1つです。

    • 複数のリマッピングが同じソースユニット名と一致する場合、最も長く一致する接頭辞を持つものが選択されます。

    • プレフィックスが同一の場合は、最後に指定されたものが優先されます。

    • リマッピングは、他のリマッピングには作用しません。 例えば、 a=b b=c c=dad にリマッピングすることはありません。

  1. プレフィックスは空欄にできないが、コンテキストとターゲットは任意です。

    • target が空の文字列の場合、 prefix は単にインポートパスから削除されます。

    • 空の context は、リマッピングがすべてのソースユニットのすべてのインポートに適用されることを意味します。

インポートでのURLの使用

https://data:// のようなほとんどのURLプレフィックスは、インポートパスでは特別な意味を持ちません。 唯一の例外は file:// で、これはHost Filesystem Loaderによってソースユニット名から取り除かれます。

ローカルにコンパイルする場合、インポートリマッピングを使用して、プロトコルとドメインの部分をローカルパスに置き換えることができます。

solc :https://github.com/ethereum/dapp-bin=/usr/local/dapp-bin contract.sol

先頭の : に注目してください。 これは、リマッピングコンテキストが空の場合に必要です。 そうしないと、 https: の部分がコンパイラーによって文脈として解釈されてしまいます。

スタイルガイド

イントロダクション

このガイドは、Solidityのコーディング規約を提供することを目的としています。 このガイドは、有用な規約が発見されたり、古い規約が廃止されたりして、時間とともに変化していく文書として考えるべきです。

多くのプロジェクトでは、独自のスタイルガイドを導入しています。 コンフリクトが生じた場合は、プロジェクト独自のスタイルガイドが優先されます。

このスタイルガイドの構造や推奨事項の多くは、Pythonの pep8スタイルガイド から引用されています。

このガイドの目的は、Solidityのコードを書くための正しい方法や最良の方法であることではありません。 このガイドの目的は、 一貫性 です。 Pythonの pep8 からの引用はこのコンセプトをよく表しています。

注釈

スタイルガイドとは一貫性を保つためのものです。 このスタイルガイドとの一貫性は重要です。 プロジェクト内での一貫性はより重要です。 そして、最も重要な一貫性は、一つのモジュールや関数の中での一貫性です。

しかし、一番重要なことは、 一貫性がないことを自覚すること です。 時には、スタイルガイドをそのまま適用できないこともあります。 迷ったときは、自分のベストな判断に従ってください。 また、他の例を見て何がベストなのかを判断してください。 そして、迷わず質問してください!

コードレイアウト

インデント

インデントレベルごとに4つのスペースを使用してください。

タブかスペースか

スペースのほうが好ましいインデント方法です。

タブとスペースの混在は避けてください。

空行

Solidityのソースコードでは、トップレベルの宣言を2つの空行で囲んでください。

OK:

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

contract A {
    // ...
}

contract B {
    // ...
}

contract C {
    // ...
}

NG:

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

contract A {
    // ...
}
contract B {
    // ...
}

contract C {
    // ...
}

コントラクト内では、関数の宣言を1つの空行で囲みます。

関連する一行のグループにおいては、空行を省略しても良いです(抽象コントラクトの未実装の関数など)。

OK:

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

abstract contract A {
    function spam() public virtual pure;
    function ham() public virtual pure;
}

contract B is A {
    function spam() public pure override {
        // ...
    }

    function ham() public pure override {
        // ...
    }
}

NG:

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

abstract contract A {
    function spam() virtual pure public;
    function ham() public virtual pure;
}

contract B is A {
    function spam() public pure override {
        // ...
    }
    function ham() public pure override {
        // ...
    }
}

最大の行の長さ

行の長さは最大120文字であることを推奨しています。

ラップされる行は以下のガイドラインに沿ってください。

  1. 第1引数は、開始括弧に付けてはいけません。

  2. インデントは1つだけにしてください。

  3. それぞれの引数は、それぞれの行にあるべきものです。

  4. 終端要素である ); は、それだけを最後の行に配置する必要があります。

関数呼び出し

OK:

thisFunctionCallIsReallyLong(
    longArgument1,
    longArgument2,
    longArgument3
);

NG:

thisFunctionCallIsReallyLong(longArgument1,
                              longArgument2,
                              longArgument3
);

thisFunctionCallIsReallyLong(longArgument1,
    longArgument2,
    longArgument3
);

thisFunctionCallIsReallyLong(
    longArgument1, longArgument2,
    longArgument3
);

thisFunctionCallIsReallyLong(
longArgument1,
longArgument2,
longArgument3
);

thisFunctionCallIsReallyLong(
    longArgument1,
    longArgument2,
    longArgument3);

代入文

OK:

thisIsALongNestedMapping[being][set][toSomeValue] = someFunction(
    argument1,
    argument2,
    argument3,
    argument4
);

NG:

thisIsALongNestedMapping[being][set][toSomeValue] = someFunction(argument1,
                                                                   argument2,
                                                                   argument3,
                                                                   argument4);

イベント定義とイベントエミッタ

OK:

event LongAndLotsOfArgs(
    address sender,
    address recipient,
    uint256 publicKey,
    uint256 amount,
    bytes32[] options
);

LongAndLotsOfArgs(
    sender,
    recipient,
    publicKey,
    amount,
    options
);

NG:

event LongAndLotsOfArgs(address sender,
                        address recipient,
                        uint256 publicKey,
                        uint256 amount,
                        bytes32[] options);

LongAndLotsOfArgs(sender,
                  recipient,
                  publicKey,
                  amount,
                  options);

ソースファイルのエンコーディング

UTF-8あるいはASCIIのエンコーディングが望ましいです。

インポート

インポート文は、常にファイルの先頭に配置する必要があります。

OK:

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

import "./Owned.sol";

contract A {
    // ...
}


contract B is Owned {
    // ...
}

NG:

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

contract A {
    // ...
}

import "./Owned.sol";

contract B is Owned {
    // ...
}

関数の順番

順番を決めることで、読者はどの関数を呼び出すことができるかを識別し、コンストラクタやフォールバックの定義を見つけやすくなります。

関数はビジビリティに応じてグループ化し、順序立てて配置します。

  • constructor

  • receive 関数(ある場合)

  • fallback 関数(ある場合)

  • external 関数

  • public 関数

  • internal 関数

  • private 関数

グループ内では、 view 関数と pure 関数を最後に配置します。

OK:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract A {
    constructor() {
        // ...
    }

    receive() external payable {
        // ...
    }

    fallback() external {
        // ...
    }

    // External functions
    // ...

    // External functions that are view
    // ...

    // External functions that are pure
    // ...

    // Public functions
    // ...

    // Internal functions
    // ...

    // Private functions
    // ...
}

NG:

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

    // External functions
    // ...

    fallback() external {
        // ...
    }
    receive() external payable {
        // ...
    }

    // Private functions
    // ...

    // Public functions
    // ...

    constructor() {
        // ...
    }

    // Internal functions
    // ...
}

式中の空白文字

次の場合は、余計な空白を入れないようにしましょう。

括弧、大括弧、中括弧のすぐ内側(ただし、1行の関数の宣言は例外):

OK:

spam(ham[1], Coin({name: "ham"}));

NG:

spam( ham[ 1 ], Coin( { name: "ham" } ) );

例外:

function singleLine() public { spam(); }

コンマとセミコロンの直前:

OK:

function spam(uint i, Coin coin) public;

NG:

function spam(uint i , Coin coin) public ;

代入や他の演算子の周りに1つ以上の空白を入れての整列:

OK:

x = 1;
y = 2;
longVariable = 3;

NG:

x            = 1;
y            = 2;
longVariable = 3;

レシーブ関数とフォールバック関数に空白を入れてはいけません:

OK:

receive() external payable {
    ...
}

fallback() external {
    ...
}

NG:

receive () external payable {
    ...
}

fallback () external {
    ...
}

制御構造

コントラクト、ライブラリ、関数、構造体の本体を示す中括弧は、次のようにします。

  • 宣言と同じ行にオープンする

  • 宣言の先頭と同じインデントレベルで独立した行でクローズする

  • 冒頭のブレースの前に半角スペースを入れる

OK:

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

contract Coin {
    struct Bank {
        address owner;
        uint balance;
    }
}

NG:

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

contract Coin
{
    struct Bank {
        address owner;
        uint balance;
    }
}

制御構造 ifelsewhilefor にも同じ推奨事項が適用されます。

また、制御構造 ifwhilefor と条件を表す親ブロックの間には半角スペースを入れ、条件を表す親ブロックと開始ブレースの間にも半角スペースを入れる必要があります。

OK:

if (...) {
    ...
}

for (...) {
    ...
}

NG:

if (...)
{
    ...
}

while(...){
}

for (...) {
    ...;}

本体が1つの文を含む制御構造の場合、文が1行に収まっていれば、中括弧を省略しても問題ありません。

OK:

if (x < 10)
    x += 1;

NG:

if (x < 10)
    someArray.push(Coin({
        name: 'spam',
        value: 42
    }));

else または else if 句を持つ if ブロックでは、 elseif の閉じ括弧と同じ行に配置します。 これは、他のブロックのような構造のルールに比べて例外的なものです。

OK:

if (x < 3) {
    x += 1;
} else if (x > 7) {
    x -= 1;
} else {
    x = 5;
}

if (x < 3)
    x += 1;
else
    x -= 1;

NG:

if (x < 3) {
    x += 1;
}
else {
    x -= 1;
}

関数宣言

短い関数宣言の場合は、関数本体の開始波括弧を関数宣言と同じ行に置くことをお勧めします。

閉じ括弧は、関数宣言と同じインデントレベルでなければなりません。

冒頭のブレースの前には半角スペースを入れてください。

OK:

function increment(uint x) public pure returns (uint) {
    return x + 1;
}

function increment(uint x) public pure onlyOwner returns (uint) {
    return x + 1;
}

NG:

function increment(uint x) public pure returns (uint)
{
    return x + 1;
}

function increment(uint x) public pure returns (uint){
    return x + 1;
}

function increment(uint x) public pure returns (uint) {
    return x + 1;
    }

function increment(uint x) public pure returns (uint) {
    return x + 1;}

関数の修飾順序は次のようになります。

  1. ビジビリティ

  2. ミュータビリティ

  3. バーチャル

  4. オーバーライド

  5. カスタムモディファイア

OK:

function balance(uint from) public view override returns (uint)  {
    return balanceOf[from];
}

function shutdown() public onlyOwner {
    selfdestruct(owner);
}

NG:

function balance(uint from) public override view returns (uint)  {
    return balanceOf[from];
}

function shutdown() onlyOwner public {
    selfdestruct(owner);
}

長い関数宣言の場合は、各引数を関数本体と同じインデントレベルで一行にまとめることをお勧めします。 閉じ括弧と開き括弧も同様に、関数宣言と同じインデントレベルで一行に置く必要があります。

OK:

function thisFunctionHasLotsOfArguments(
    address a,
    address b,
    address c,
    address d,
    address e,
    address f
)
    public
{
    doSomething();
}

NG:

function thisFunctionHasLotsOfArguments(address a, address b, address c,
    address d, address e, address f) public {
    doSomething();
}

function thisFunctionHasLotsOfArguments(address a,
                                        address b,
                                        address c,
                                        address d,
                                        address e,
                                        address f) public {
    doSomething();
}

function thisFunctionHasLotsOfArguments(
    address a,
    address b,
    address c,
    address d,
    address e,
    address f) public {
    doSomething();
}

長い関数宣言にモディファイアがある場合は、各モディファイアをそれぞれの行に落とす必要があります。

OK:

function thisFunctionNameIsReallyLong(address x, address y, address z)
    public
    onlyOwner
    priced
    returns (address)
{
    doSomething();
}

function thisFunctionNameIsReallyLong(
    address x,
    address y,
    address z
)
    public
    onlyOwner
    priced
    returns (address)
{
    doSomething();
}

NG:

function thisFunctionNameIsReallyLong(address x, address y, address z)
                                      public
                                      onlyOwner
                                      priced
                                      returns (address) {
    doSomething();
}

function thisFunctionNameIsReallyLong(address x, address y, address z)
    public onlyOwner priced returns (address)
{
    doSomething();
}

function thisFunctionNameIsReallyLong(address x, address y, address z)
    public
    onlyOwner
    priced
    returns (address) {
    doSomething();
}

複数行の出力パラメータやreturn文は、 最大の行の長さ セクションで推奨されている長い行の折り返しと同じスタイルにしてください。

OK:

function thisFunctionNameIsReallyLong(
    address a,
    address b,
    address c
)
    public
    returns (
        address someAddressName,
        uint256 LongArgument,
        uint256 Argument
    )
{
    doSomething()

    return (
        veryLongReturnArg1,
        veryLongReturnArg2,
        veryLongReturnArg3
    );
}

NG:

function thisFunctionNameIsReallyLong(
    address a,
    address b,
    address c
)
    public
    returns (address someAddressName,
             uint256 LongArgument,
             uint256 Argument)
{
    doSomething()

    return (veryLongReturnArg1,
            veryLongReturnArg1,
            veryLongReturnArg1);
}

ベースが引数を必要とする継承されたコントラクトのコンストラクタ関数については、関数宣言が長い場合や読みにくい場合には、モディファイアと同じ方法でベースのコンストラクタを新しい行に落とすことをお勧めします。

OK:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
// Base contracts just to make this compile
contract B {
    constructor(uint) {
    }
}


contract C {
    constructor(uint, uint) {
    }
}


contract D {
    constructor(uint) {
    }
}


contract A is B, C, D {
    uint x;

    constructor(uint param1, uint param2, uint param3, uint param4, uint param5)
        B(param1)
        C(param2, param3)
        D(param4)
    {
        // do something with param5
        x = param5;
    }
}

NG:

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

// Base contracts just to make this compile
contract B {
    constructor(uint) {
    }
}

contract C {
    constructor(uint, uint) {
    }
}

contract D {
    constructor(uint) {
    }
}

contract A is B, C, D {
    uint x;

    constructor(uint param1, uint param2, uint param3, uint param4, uint param5)
    B(param1)
    C(param2, param3)
    D(param4) {
        x = param5;
    }
}

contract X is B, C, D {
    uint x;

    constructor(uint param1, uint param2, uint param3, uint param4, uint param5)
        B(param1)
        C(param2, param3)
        D(param4) {
            x = param5;
        }
}

短い関数を1つの文で宣言する場合、1行で宣言しても構いません。

許可されています。

function shortFunction() public { doSomething(); }

この関数宣言のガイドラインは、読みやすさを向上させることを目的としています。 このガイドラインは、関数宣言のすべての可能性を網羅するものではありませんので、執筆者は最善の判断を下す必要があります。

マッピング

変数宣言では、キーワード mapping とその型は空白で区切りません。 また、ネストした mapping キーワードとその型は空白で区切りません。

OK:

mapping(uint => uint) map;
mapping(address => bool) registeredAddresses;
mapping(uint => mapping(bool => Data[])) public data;
mapping(uint => mapping(uint => s)) data;

NG:

mapping (uint => uint) map;
mapping( address => bool ) registeredAddresses;
mapping (uint => mapping (bool => Data[])) public data;
mapping(uint => mapping (uint => s)) data;

変数宣言

配列変数の宣言では、型と括弧の間にスペースを入れてはいけません。

OK:

uint[] x;

NG:

uint [] x;

その他の推奨事項

  • 文字列は、シングルクォートではなくダブルクォートで引用してください。

OK:

str = "foo";
str = "Hamlet says, 'To be or not to be...'";

NG:

str = 'bar';
str = '"Be yourself; everyone else is already taken." -Oscar Wilde';
  • 演算子を左右の半角スペースで囲みます。

OK:

x = 3;
x = 100 / 10;
x += 3 + 4;
x |= y && z;

NG:

x=3;
x = 100/10;
x += 3+4;
x |= y&&z;
  • 優先順位の高い演算子は、優先順位を示すために周囲の空白を除外できます。 これは、複雑な文の可読性を高めるためのものです。 演算子の両側には、常に同じ量の空白を使用する必要があります。

OK:

x = 2**3 + 5;
x = 2*y + 3*z;
x = (a+b) * (a-b);

NG:

x = 2** 3 + 5;
x = y+z;
x +=1;

レイアウトの順序

コントラクトの要素を以下の順序でレイアウトします。

  1. プラグマ文

  2. インポート文

  3. インターフェース

  4. ライブラリ

  5. コントラクト

各コントラクト、ライブラリ、インターフェースの内部では、以下の順序を使用します。

  1. 型の宣言

  2. 状態変数

  3. イベント

  4. エラー

  5. モディファイア

  6. 関数

注釈

イベントや状態変数での使用に近い形で型を宣言した方がわかりやすいかもしれません。

OK:

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

abstract contract Math {
    error DivideByZero();
    function divide(int256 numerator, int256 denominator) public virtual returns (uint256);
}

NG:

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

abstract contract Math {
    function divide(int256 numerator, int256 denominator) public virtual returns (uint256);
    error DivideByZero();
}

命名規則

命名規則は、広く採用され使用されることで力を発揮します。 異なる規約を使用することで、他の方法ではすぐには得られない重要なメタ情報を伝えることができます。

ここで述べられているネーミングの推奨事項は、読みやすさを向上させることを目的としているため、ルールではなく、物事の名前を通して最も多くの情報を伝えるためのガイドラインとなっています。

最後に、コードベース内の一貫性は、常にこのドキュメントで説明されている規約よりも優先されるべきです。

命名スタイル

混乱を避けるために、以下の名称は異なるネーミングスタイルを参照するために使用されます。

  • b (半角英小文字)

  • B (半角英大文字)

  • lowercase

  • UPPERCASE

  • UPPER_CASE_WITH_UNDERSCORES

  • CapitalizedWords (またはCapWords)

  • mixedCase (CapitalizedWordsとの違いは、頭文字が小文字であること!)

注釈

CapWordsで頭文字を使用する場合は、頭文字のすべての文字を大文字にします。 したがって、HttpServerErrorよりもHTTPServerErrorの方がよいです。 頭文字をmixedCaseで使用する場合は、頭文字の文字をすべて大文字にします。 ただし、名前の先頭の文字は小文字にします。 したがって、xmlHTTPRequestの方がXMLHTTPRequestよりも優れています。

避けるべき名前

  • l - 小文字のエル

  • O - 大文字のオー

  • I - 大文字のアイ

これらは一文字の変数名には絶対に使用しないでください。 これらは、数字のoneやzeroと区別がつかないことがあります。

コントラクトとライブラリの名前

  • コントラクトやライブラリの名前は、CapWordsスタイルを使用してください。 例: SimpleToken, SmartBank, CertificateHashRepository, Player, Congress, Owned

  • コントラクト名とライブラリ名は、ファイル名と一致している必要があります。

  • コントラクトファイルに複数のコントラクトやライブラリが含まれている場合、ファイル名は コアコントラクト と一致させる必要があります。しかし、これは避けることができるならば、推奨されません。

以下の例のように、コントラクト名が Congress 、ライブラリ名が Owned の場合、関連するファイル名は Congress.solOwned.sol になります。

OK:

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

// Owned.sol
contract Owned {
    address public owner;

    modifier onlyOwner {
        require(msg.sender == owner);
        _;
    }

    constructor() {
        owner = msg.sender;
    }

    function transferOwnership(address newOwner) public onlyOwner {
        owner = newOwner;
    }
}

そして、 Congress.sol で次のコードになっています。

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

import "./Owned.sol";

contract Congress is Owned, TokenRecipient {
    //...
}

NG:

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

// owned.sol
contract owned {
    address public owner;

    modifier onlyOwner {
        require(msg.sender == owner);
        _;
    }

    constructor() {
        owner = msg.sender;
    }

    function transferOwnership(address newOwner) public onlyOwner {
        owner = newOwner;
    }
}

そして、 Congress.sol で次のコードになっています。

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

import "./owned.sol";

contract Congress is owned, tokenRecipient {
    //...
}

構造体名

構造体の名前は、CapWordsスタイルを使用してください。 例: MyCoinPositionPositionXY

イベント名

イベント名は、CapWordsスタイルを使用してください。 例: Deposit, Transfer, Approval, BeforeTransfer, AfterTransfer

関数名

関数はmixedCaseを使用してください。 例: getBalance, transfer, verifyOwner, addMember, changeOwner

関数の引数名

関数の引数には、mixedCaseを使用してください。 例: initialSupply, account, recipientAddress, senderAddress, newOwner

カスタム構造体を操作するライブラリ関数を書くときは、構造体を第1引数にして、常に self という名前にしてください。

ローカル変数名と状態変数名

mixedCaseを使用してください。 例: totalSupply, remainingSupply, balancesOf, creatorAddress, isPreSale, tokenExchangeRate

定数

定数の名前は、すべて大文字で、アンダースコアで単語を区切ってください。 例: MAX_BLOCKS, TOKEN_NAME, TOKEN_TICKER, CONTRACT_VERSION

モディファイア名

mixedCaseを使用してください。 例: onlyByonlyAfteronlyDuringThePreSale

列挙

列挙(enum)は、単純な型宣言のスタイルで、CapWordsスタイルを使用してください。 例: TokenGroup, Frame, HashStyle, CharacterLocation

名前の衝突の回避

  • singleTrailingUnderscore_

この規約は、希望する名前が、既存の状態変数、関数、組み込み、またはその他の予約名と衝突する場合に提案されます。

非外部関数と非外部変数のためのアンダースコア接頭辞

  • _singleLeadingUnderscore

この規約は、非外部関数と非外部状態変数(つまり private または internal )で推奨されています。 ビジビリティの指定がない状態変数は、デフォルトで internal となります。

スマートコントラクトを設計する際、public-facing API(どのアカウントからも呼び出せる関数)は重要な検討事項です。 アンダースコアを付けると、そのような関数の意図をすぐに認識できますが、より重要なのは、関数を非外部から外部( public を含む)に変更し、それに応じて名前を変更すると、名前を変更する際にすべての呼び出しサイトを確認しなければならない点です。 これは、意図しない外部関数に対する重要な手動チェックであり、セキュリティ脆弱性の一般的な原因でもあります(この変更のためのfind-replace-allツールは避けてください)。

NatSpec

Solidityのコントラクトには、NatSpecコメントを含めることができます。 コメントはトリプルスラッシュ( /// )またはダブルアスタリスクブロック( /** ... */ )で記述し、関数宣言や文の直上で使用する必要があります。

例えば、 シンプルなスマートコントラクト のコントラクトにコメントを加えたものは、次のようになります。

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

/// @author The Solidity Team
/// @title A simple storage example
contract SimpleStorage {
    uint storedData;

    /// Store `x`.
    /// @param x the new value to store
    /// @dev stores the number in the state variable `storedData`
    function set(uint x) public {
        storedData = x;
    }

    /// Return the stored value.
    /// @dev retrieves the value of the state variable `storedData`
    /// @return the stored value
    function get() public view returns (uint) {
        return storedData;
    }
}

Solidityのコントラクトは、すべてのパブリックインターフェース(ABIのすべて)に対して NatSpec を使って完全にアノテーションすることを推奨します。

詳しい説明は NatSpec の項を参照してください。

共通パターン

コントラクトからの出金

エフェクト後の送金方法としては、出金パターンの使用が推奨されます。 エフェクトの結果としてEtherを送信する最も直感的な方法はダイレクトな transfer コールですが、これは潜在的なセキュリティリスクがあるため推奨されません。 これについては、 セキュリティへの配慮 のページで詳しく説明しています。

King of the Ether をヒントに、「一番のお金持ち」になるために何らかの対価、例えば、Etherを一番多く送ることを目的としたコントラクトにおいて、実際に行われている出金パターンの例を以下に示します。

次のコントラクトでは、自分が一番お金持ちでなくなった場合、新しく一番お金持ちになった人の資金を受け取ります。

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

contract WithdrawalContract {
    address public richest;
    uint public mostSent;

    mapping(address => uint) pendingWithdrawals;

    /// Etherの送信量が現在最も多い量より多くなかった
    error NotEnoughEther();

    constructor() payable {
        richest = msg.sender;
        mostSent = msg.value;
    }

    function becomeRichest() public payable {
        if (msg.value <= mostSent) revert NotEnoughEther();
        pendingWithdrawals[richest] += msg.value;
        richest = msg.sender;
        mostSent = msg.value;
    }

    function withdraw() public {
        uint amount = pendingWithdrawals[msg.sender];
        // Reentrancy攻撃を防ぐため、送金前にpendingしている返金の額をゼロにすることを忘れないでください
        pendingWithdrawals[msg.sender] = 0;
        payable(msg.sender).transfer(amount);
    }
}

これは、より直感的な送信パターンとは対照的です。

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

contract SendContract {
    address payable public richest;
    uint public mostSent;

    /// Etherの送信量が現在最も多い量より多くなかった
    error NotEnoughEther();

    constructor() payable {
        richest = payable(msg.sender);
        mostSent = msg.value;
    }

    function becomeRichest() public payable {
        if (msg.value <= mostSent) revert NotEnoughEther();
        // この行は問題を引き起こす可能性があります(以下で説明します)。
        richest.transfer(msg.value);
        richest = payable(msg.sender);
        mostSent = msg.value;
    }
}

この例では、攻撃者は、失敗する受信関数やフォールバック関数を持つコントラクトのアドレスを richest にすることで、コントラクトを使用不能な状態に陥れることができることに注意してください(例えば、 revert() を使用したり、送金された2300ガス制限を超えて消費したりすることなど)。 そうすれば、「毒された」コントラクトに資金を届けるために transfer が呼び出されるたびに、それは失敗し、したがって becomeRichest も失敗して、コントラクトは永遠に動けなくなります。

一方、最初の例の出金パターンを使用した場合、攻撃者は自分の出金が失敗するだけで、コントラクトの残りの部分の働きを引き起こすことはできません。

アクセス制限

アクセスを制限することはコントラクトの一般的なパターンです。 トランザクションの内容やコントラクトの状態を人間やコンピュータに読まれないように制限できないことに注意してください。 暗号化することで多少難しくできますが、あなたのコントラクトがデータを読めることになっていれば、他の人も読めてしまいます。

コントラクトの状態を 他のコントラクト が読み取るアクセスを制限できます。 状態変数を public で宣言しない限り、これはデフォルトの動作です。

さらに、コントラクトの状態を変更したり、コントラクトの関数を呼び出すことができる人を制限できます。 これがこのセクションの目的です。

関数モディファイア を使用することで、これらの制限が非常に読みやすくなります。

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

contract AccessRestriction {
    // これらはコンストラクション段階で代入され、`msg.sender`はこのコントラクトを作成するアカウントです
    address public owner = msg.sender;
    uint public creationTime = block.timestamp;

    // 次に、このコントラクトで発生しうるエラーの一覧とテキストによる説明を特殊なコメントで示します

    /// この操作を実行する権限が送信者にありません
    error Unauthorized();

    /// 関数呼び出しが早すぎます
    error TooEarly();

    /// 関数呼び出しで送信されるEtherが不足しています
    error NotEnoughEther();

    // モディファイアは、関数のボディを変更するために使用できます
    // このモディファイアを使用すると、特定のアドレスから関数が呼び出された場合にのみ実行されるチェックが前置されます
    modifier onlyBy(address account)
    {
        if (msg.sender != account)
            revert Unauthorized();
        // "_;" を忘れないでください!
        // モディファイアが使用されると、実際の関数ボディに置き換えられます
        _;
    }

    /// `newOwner` をこのコントラクトの新しいオーナーにします
    function changeOwner(address newOwner)
        public
        onlyBy(owner)
    {
        owner = newOwner;
    }

    modifier onlyAfter(uint time) {
        if (block.timestamp < time)
            revert TooEarly();
        _;
    }

    /// 所有者情報を消去します
    /// コントラクトが作成されてから6週間後にのみ呼び出すことができます
    function disown()
        public
        onlyBy(owner)
        onlyAfter(creationTime + 6 weeks)
    {
        delete owner;
    }

    // このモディファイアは、関数呼び出しに関連する一定の料金を要求します
    // 呼び出し側が過剰に送金した場合、払い戻されますが、関数ボディの後にのみ払い戻されます
    // これは Solidity バージョン 0.4.0 以前では危険で、`_;` の後の部分をスキップすることが可能でした
    modifier costs(uint amount) {
        if (msg.value < amount)
            revert NotEnoughEther();

        _;
        if (msg.value > amount)
            payable(msg.sender).transfer(msg.value - amount);
    }

    function forceOwnerChange(address newOwner)
        public
        payable
        costs(200 ether)
    {
        owner = newOwner;
        // これは条件の一例です
        if (uint160(owner) & 0 == 1)
            // バージョン0.4.0以前のSolidityでは、返金されませんでした
            return;
        // 過払い金を返還します
    }
}

関数呼び出しへのアクセスを制限する、より特殊な方法については、次の例で説明します。

ステートマシン

コントラクトはしばしばステートマシンとして動作します。 つまり、異なる動作をする特定の ステージ を持っていたり、異なる関数を呼び出すことができるということです。 関数呼び出しはしばしばステージを終了し、コントラクトを次のステージに移行させます(特にコントラクトが インタラクション をモデルとしている場合)。 また、 ある時点 で自動的に到達するステージもあるのが一般的です。

例えば、ブラインドオークションのコントラクトでは、「ブラインド入札を受け付ける」というステージから始まり、「入札を公開する」に移行し、「オークションの結果を決定する」で終了します。

このような場合、関数モディファイアを使って状態をモデル化し、コントラクトの間違った使い方を防ぐことができます。

次の例では、モディファイア atStage によって、あるステージでしかその関数を呼び出すことができないようにしています。

時限式の自動トランジションはモディファイア timedTransitions で処理されます。

注釈

モディファイアの順序に関して: atStageがtimedTransitionsと組み合わされている場合は、新しいステージが考慮されるように、後者の後に言及するようにしてください。

最後に、モディファイア transitionNext を使うと、関数が終了したときに自動的に次のステージに進むことができます。

注釈

モディファイアは省略可能: これは、バージョン0.4.0以前のSolidityにのみ適用されます。 モディファイアは、関数呼び出しを使用せず、単にコードを置き換えることで適用されるため、関数自体がreturnを使用している場合、transitionNextモディファイアのコードをスキップできます。 その場合は、それらの関数から手動でnextStageを呼び出すようにしてください。 バージョン0.4.0からは、モディファイアのコードは、関数が明示的にreturnしても実行されます。

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

contract StateMachine {
    enum Stages {
        AcceptingBlindedBids,
        RevealBids,
        AnotherStage,
        AreWeDoneYet,
        Finished
    }
    /// 現時点では関数を呼び出せません
    error FunctionInvalidAtThisStage();

    // これが現在のステージです
    Stages public stage = Stages.AcceptingBlindedBids;

    uint public creationTime = block.timestamp;

    modifier atStage(Stages stage_) {
        if (stage != stage_)
            revert FunctionInvalidAtThisStage();
        _;
    }

    function nextStage() internal {
        stage = Stages(uint(stage) + 1);
    }

    // 時間指定でトランジションを行います
    // 必ずこのモディファイアを最初に指定してください、そうしないとガードは新しいステージを考慮しません
    modifier timedTransitions() {
        if (stage == Stages.AcceptingBlindedBids &&
                    block.timestamp >= creationTime + 10 days)
            nextStage();
        if (stage == Stages.RevealBids &&
                block.timestamp >= creationTime + 12 days)
            nextStage();
        // トランザクションによる他のステージへの推移
        _;
    }

    // モディファイアの順序が重要です!
    function bid()
        public
        payable
        timedTransitions
        atStage(Stages.AcceptingBlindedBids)
    {
        // 実装は省略します
    }

    function reveal()
        public
        timedTransitions
        atStage(Stages.RevealBids)
    {
    }

    // このモディファイアは、関数が終わった後、次のステージに移行します
    modifier transitionNext()
    {
        _;
        nextStage();
    }

    function g()
        public
        timedTransitions
        atStage(Stages.AnotherStage)
        transitionNext
    {
    }

    function h()
        public
        timedTransitions
        atStage(Stages.AreWeDoneYet)
        transitionNext
    {
    }

    function i()
        public
        timedTransitions
        atStage(Stages.Finished)
    {
    }
}

資料

一般的な資料

統合(Ethereum)開発環境

  • Brownie

    Ethereum Virtual Machineをターゲットとしたスマートコントラクトのための、Pythonベースの開発およびテストフレームワーク。

  • Dapp

    コマンドラインからスマートコントラクトを構築、テスト、デプロイするためのツール。

  • Embark

    非中央集権的なアプリケーションを構築及びデプロイするための開発者プラットフォーム。

  • Foundry

    Rustで書かれたEthereumアプリケーション開発のための高速、ポータブル、モジュラーなツールキット。

  • Hardhat

    ローカルEthereumネットワーク、デバッグ機能、プラグインエコシステムを備えたEthereum開発環境。

  • Remix

    サーバーサイドのコンポーネントを使用せず、コンパイラとSolidity実行環境を統合したブラウザベースのIDE。

  • Truffle

    Ethereum開発フレームワーク。

エディターとの統合

Solidityのツール

  • abi-to-sol

    与えられたABI JSONからSolidityインターフェースソースを生成するツール。

  • Doxity

    Solidityのためのドキュメントジェネレーター。

  • Ethlint

    Solidityのスタイルとセキュリティの問題を特定し、修正するためのリンター。

  • evmdis

    バイトコードに対して静的解析を行い、生のEVM操作よりも高い抽象度を提供するEVM逆アセンブラ。

  • EVM Lab

    EVMと対話するためのリッチなツールパッケージ。 VM、Etherchain API、ガスコストを表示するトレースビューアが含まれています。

  • hevm

    EVMデバッガとシンボリック実行エンジン。

  • leafleth

    Solidityスマートコントラクトのためのドキュメント生成ツール。

  • PIET

    シンプルなグラフィカルインターフェースを介してSolidityスマートコントラクトを開発、監査、使用するためのツール。

  • Scaffold-ETH

    迅速なプロダクトイテレーションに焦点を当てたフォーク可能なEthereum開発スタック。

  • sol2uml

    Solidityコントラクト用のUnified Modeling Language (UML)クラスのダイアグラムジェネレーター。

  • solc-select

    Solidityのコンパイラバージョンを素早く切り替えるスクリプト。

  • Solidity REPL

    コマンドラインのSolidityコンソールですぐにSolidityを試すことができます。

  • solgraph

    Solidityのコントロールフローを可視化し、潜在的なセキュリティの脆弱性を明らかにします。

  • Solhint

    スマートコントラクトの検証のためのセキュリティ、スタイルガイド、ベストプラクティスルールを提供するSolidityリンター。

  • Sourcify

    非中央集権型の自動コントラクト検証サービスとコントラクトメタデータのパブリックリポジトリ。

  • Sūrya

    スマートコントラクトシステムのためのユーティリティーツールで、多数のビジュアル出力とコントラクトの構造に関する情報を提供します。 また、関数呼び出しグラフのクエリもサポートしています。

  • Universal Mutator

    設定可能なルールを持ち、SolidityとVyperをサポートする、突然変異生成のためのツール。

サードパーティのSolidityパーサーとグラマー

コントリビューティング

貢献はいつでも歓迎です。 Solidityに貢献するための選択肢はたくさんあります。

特に、以下の領域でのサポートに感謝します。

まずは ソースからのビルド を使って、Solidityのコンポーネントやビルドプロセスに慣れてみてください。 また、Solidityでのスマートコントラクトの書き方を熟知することも有効でしょう。

このプロジェクトは Contributor Code of Conduct 付きで公開されていることにご注意ください。 イシュー、プルリクエスト、Gitterチャンネルなど、このプロジェクトに参加することで、その条件を守ることに同意したことになります。

チームコール

議論したいイシューやプルリクエストがある場合や、チームやコントリビューターが取り組んでいることを聞きたい場合は、パブリックなチームコールに参加できます。

  • 毎週水曜日の午後3時(CET/CEST)から。

コールは Jitsi で行われます。

イシューの報告方法

問題を報告するには、 GitHubイシュートラッカー を利用してください。 報告の際には、以下の内容をお知らせください。

  • Solidityのバージョン。

  • ソースコード(必要に応じて)。

  • オペレーティングシステム。

  • イシューを再現するための手順。

  • 実際の挙動と期待される挙動の比較。

イシューの原因となったソースコードを最小限に減らすことは、常に非常に役に立ち、時には誤解を解くことにもなります。

言語設計に関する技術的な議論については、 Solidity forum への投稿が正しい場所です( Solidityの言語設計 を参照してください)。

プルリクエストのワークフロー

貢献するためには、 develop ブランチをフォークして、そこで変更を加えてください。 コミットメッセージには、変更した内容に加えて、変更した理由を詳しく書いてください(小さな変更の場合を除く)。

フォーク後に develop からの変更を取り込む必要がある場合(たとえば、潜在的なマージコンフリクトを解決するため)、 git merge の使用を避け、代わりに git rebase でブランチを作成してください。 そうすることで、あなたの変更をより簡単に確認できます。

また、新機能を書いている場合は、 test/ の下に適切なテストケースを追加してください(下記を参照してください)。

ただし、より大きな変更を行う場合は、まず Solidity Development Gitter チャンネル (前述のものとは異なり、こちらは言語の使い方ではなく、コンパイラや言語の開発に重点を置いています)に相談してください。

新機能やバグフィックスは、 Changelog.md ファイルに追加してください。 該当する場合は、過去のエントリーのスタイルに従ってください。

最後に、このプロジェクトの コーディングスタイル を尊重するようにしてください。 また、CIテストを行っているとはいえ、プルリクエストを提出する前にコードをテストし、ローカルにビルドされることを確認してください。

プルリクエストを提出する前に、私たちの レビューチェックリスト に目を通すことを強くお勧めします。 私たちはすべてのPRを徹底的にレビューし、あなたが正しい結果を得られるようサポートしますが、簡単に回避できる多くの一般的な問題があり、レビューがよりスムーズに行えるようになります。

ご協力ありがとうございます!

コンパイラーテストの実行

事前準備

すべてのコンパイラテストを実行するために、いくつかの依存関係( evmonelibz3 )をオプションでインストールできます。

macOSでは、一部のテストスクリプトでGNU coreutilsがインストールされていることが前提となっています。 インストールするにはHomebrewを使うのが最も簡単です: brew install coreutils

Windowsシステムでは、シンボリックリンクを作成する権限を持っていることを確認してください、さもなければいくつかのテストが失敗するかもしれません。 管理者がその権限を持つべきですが、 他のユーザーにその権限を与えること や、 開発者モードを有効にする ことも可能です。

テストの実行

Solidityには様々なタイプのテストがあり、そのほとんどが Boost C++ Test Framework アプリケーション soltest にバンドルされています。 ほとんどの変更には、 build/test/soltest またはそのラッパー scripts/soltest.sh を実行すれば十分です。

./scripts/tests.sh スクリプトは、 Boost C++ Test Framework アプリケーション soltest (またはそのラッパー scripts/soltest.sh )にバンドルされているものや、コマンドラインテスト、コンパイルテストなど、ほとんどのSolidityテストを自動的に実行します。

テストシステムは、セマンティックテストを実行するための evmone の場所を自動的に発見しようとします。

evmone ライブラリは、現在の作業ディレクトリ、その親、またはその親の親に対する deps または deps/lib ディレクトリに配置されている必要があります。 また、環境変数 ETH_EVMONE を使って evmone 共有オブジェクトの場所を明示的に指定することもできます。

evmone は主にセマンティックテストとガステストを実行するために必要です。 インストールされていない場合は、 scripts/soltest.sh--no-semantic-tests フラグを渡すことで、これらのテストをスキップできます。

evmone ライブラリと hera ライブラリは、どちらもファイル名の拡張子が、Linuxでは .so 、Windowsシステムでは .dll 、macOSでは .dylib になるようにしてください。

SMTテストを実行するためには、 libz3 ライブラリがインストールされており、コンパイラのconfigure段階で cmake が位置を特定できる必要があります。

libz3 ライブラリがシステムにインストールされていない場合は、 ./scripts/tests.sh を実行する前に SMT_FLAGS=--no-smt をエクスポートしてSMTテストを無効にするか、 ./scripts/soltest.sh --no-smt を実行する必要があります。 これらのテストは libsolidity/smtCheckerTestslibsolidity/smtCheckerTestsJSON です。

注釈

Soltestで実行されたすべてのユニットテストのリストを取得するには、 ./build/test/soltest --list_content=HRF を実行してください。

より迅速な結果を得るために、一部のテストや特定のテストを実行できます。

テストのサブセットを実行するには、 ./scripts/soltest.sh -t TestSuite/TestName``のようにフィルターを使うことができます。 ``TestName にはワイルドカード * を指定できます。

あるいは、例えば、yul disambiguatorのすべてのテストを実行するには、次のようにします。 ./scripts/soltest.sh -t "yulOptimizerTests/disambiguator/*" --no-smt です。

./build/test/soltest --help には、利用可能なすべてのオプションに関する広範なヘルプがあります。

特に、以下のオプションを参考にしてください。

注釈

Windows環境で、上記の基本セットをlibz3なしで実行したい方は、次のようにしてください。 Git Bashを使っている場合、 ./build/test/Release/soltest.exe -- --no-smt を実行してください。 プレーンなコマンドプロンプトで実行する場合、 .\build\test\Release\soltest.exe -- --no-smt を実行してください。

GDBを使ってデバッグしたい場合は、「通常」とは異なる方法でビルドするようにしてください。 例えば、 build フォルダで以下のコマンドを実行します。

cmake -DCMAKE_BUILD_TYPE=Debug ..
make

これにより、 --debug フラグを使ってテストをデバッグする際に、ブレークやプリントが可能な関数や変数にアクセスできるようにシンボルが作成されます。

CIは、Emscriptenターゲットのコンパイルを必要とする追加のテスト( solc-js やサードパーティのSolidityフレームワークのテストなど)を実行します。

構文テストの作成と実行

構文テストは、コンパイラが無効なコードに対して正しいエラーメッセージを生成し、有効なコードを適切に受け入れるかどうかをチェックします。 これらのテストは tests/libsolidity/syntaxTests フォルダー内の個々のファイルに格納されます。 これらのファイルには、それぞれのテストで期待される結果を記載した注釈を含める必要があります。 テストスイートは、これらのファイルをコンパイルし、期待される結果に対してチェックします。

例えば、次のようなものです。 ./test/libsolidity/syntaxTests/double_stateVariable_declaration.sol

contract test {
    uint256 variable;
    uint128 variable;
}
// ----
// DeclarationError: (36-52): Identifier already declared.

構文テストは、少なくともテスト対象のコントラクトそのものと、それに続くセパレータ // ---- を含んでいなければなりません。 セパレータに続くコメントは、予想されるコンパイラのエラーや警告を説明するために使用されます。 数字の範囲は、エラーが発生したソースの場所を示します。 もし、エラーや警告を出さずにコントラクトをコンパイルしたい場合は、セパレータとそれに続くコメントを省くことができます。

上の例では、状態変数 variable が2回宣言されていますが、これは許されません。 この結果、識別子がすでに宣言されているという DeclarationError が表示されます。

これらのテストには isoltest ツールが使用されており、 ./build/test/tools/ で見つけることができます。 これは対話型のツールで、好みのテキストエディタを使って失敗したコントラクトを編集できます。 variable の2番目の宣言を削除することで、このテストを破ってみましょう。

contract test {
    uint256 variable;
}
// ----
// DeclarationError: (36-52): Identifier already declared.

./build/test/tools/isoltest を再度実行すると、テストが失敗します。

syntaxTests/double_stateVariable_declaration.sol: FAIL
    Contract:
        contract test {
            uint256 variable;
        }

    Expected result:
        DeclarationError: (36-52): Identifier already declared.
    Obtained result:
        Success

isoltest は、期待される結果を得られた結果の横に表示し、また、現在のコントラクトファイルを編集、更新、スキップしたり、アプリケーションを終了する方法を提供します。

テストを失敗させるためのいくつかのオプションがあります。

  • edit: isoltest は、コントラクト内容を調整できるように、エディタでコントラクト内容を開こうとします。 isoltest --editor /path/to/editor のようにコマンドラインで指定されたエディタを使用するか、 EDITOR のように環境変数で指定されたエディタを使用するか、 /usr/bin/editor だけを使用するか(順不同)。

  • update: テスト中のコントラクトに対する期待値を更新。 これは、満たされていない期待値を削除し、満たされていない期待値を追加することで、アノテーションを更新します。 その後、テストが再度実行されます。

  • skip: この特定のテストの実行をスキップします。

  • quit: isoltest を終了します。

これらのオプションは、テストプロセス全体を停止する quit を除いて、すべて現在のコントラクトに適用されます。

上のテストを自動的に更新すると、次のように変更されます。

contract test {
    uint256 variable;
}
// ----

そして、テストを再実行します。 これで合格です。

Re-running test case...
syntaxTests/double_stateVariable_declaration.sol: OK

注釈

コントラクトファイルの名前には、 double_variable_declaration.sol など、テストする内容を説明するものを選んでください。 継承やクロスコントラクトコールをテストする場合を除き、1つのファイルに複数のコントラクトを入れないでください。 各ファイルは、新機能の1つの側面をテストする必要があります。

コマンドラインテスト

エンドツーエンドのコマンドラインテストスイートは、様々なシナリオにおけるコンパイラバイナリ全体の動作をチェックします。 これらのテストは test/cmdlineTests/ にサブディレクトリごとに1つずつあり、 cmdlineTests.sh スクリプトを使って実行できます。

デフォルトでは、スクリプトは利用可能なすべてのテストを実行します。 また、1つ以上の ファイル名パターン を指定することもでき、その場合は少なくとも1つのパターンにマッチするテストのみが実行されます。 また、特定のパターンの前に --exclude をつけることで、そのパターンにマッチするファイルを除外することもできます。

デフォルトでは、スクリプトは solc バイナリが作業コピーの build/ サブディレクトリにあると仮定します。 コンパイラをソースツリーの外でビルドする場合は、 SOLIDITY_BUILD_DIR 環境変数を使ってビルドディレクトリを別の場所に指定できます。

例:

export SOLIDITY_BUILD_DIR=~/solidity/build/
test/cmdlineTests.sh "standard_*" "*_yul_*" --exclude "standard_yul_*"

上記のコマンドは test/cmdlineTests/standard_ で始まるディレクトリと test/cmdlineTests/ のサブディレクトリで、名前のどこかに _yul_ が含まれるテストを実行しますが、名前が standard_yul_ で始まるテストは実行されません。 また、ホームディレクトリにある solidity/build/solc/solc ファイルがコンパイラのバイナリであると仮定されます(Windows を使用している場合は、 solidity/build/solc/Release/solc.exe を使用します)。

コマンドラインテストにはいくつかの種類があります。

  • 標準JSONテスト: 少なくとも input.json ファイルが含まれます。 一般的に含まれているものは以下の通りです。

    • input.json: コマンドラインで --standard-json オプションに渡す入力ファイル。

    • output.json: 標準JSON出力ファイル。

    • args: solc に渡す追加のコマンドライン引数。

  • CLIテスト: 少なくとも input.* ファイルが含まれます( input.json 以外). 一般的に含まれているものは以下の通りです。

    • input.*: コマンドラインで solc に与えられる単一の入力ファイル。 通常は input.sol または input.yul

    • args: solc に渡される追加のコマンドライン引数。

    • stdin: 標準入力から solc に渡す内容。

    • output: 期待される標準出力の内容。

    • err: 期待される標準エラー出力の内容。

    • exit: 期待される終了コード。省略された場合は0。

  • スクリプトテスト: test.* ファイルが含まれます。 一般的に含まれているものは以下の通りです。

    • test.*: 単一のスクリプトで、通常は test.sh または test.py 。 スクリプトは実行可能でなければなりません。

AFLによるファザーの実行

ファジングとは、多かれ少なかれランダムな入力に対してプログラムを実行し、例外的な実行状態(セグメンテーションフォールトや例外など)を見つける技術です。 最近のFuzzerは賢く、入力の内部で有向検索を行います。 私たちは solfuzzer と呼ばれる特殊なバイナリを持っています。 solfuzzer はソースコードを入力として受け取り、内部のコンパイラエラーやセグメンテーションフォールトなどに遭遇するたびに失敗しますが、例えばコードにエラーが含まれている場合は失敗しません。 このようにして、ファジングツールはコンパイラの内部問題を見つけることができます。

ファジングには主に AFL を使用しています。 AFLパッケージをリポジトリ(afl, afl-clang)からダウンロードしてインストールするか、手動でビルドする必要があります。 次に、AFLをコンパイラとしてSolidity(または solfuzzer バイナリのみ)をビルドします。

cd build
# if needed
make clean
cmake .. -DCMAKE_C_COMPILER=path/to/afl-gcc -DCMAKE_CXX_COMPILER=path/to/afl-g++
make solfuzzer

この段階では、以下のようなメッセージが表示されます。

Scanning dependencies of target solfuzzer
[ 98%] Building CXX object test/tools/CMakeFiles/solfuzzer.dir/fuzzer.cpp.o
afl-cc 2.52b by <lcamtuf@google.com>
afl-as 2.52b by <lcamtuf@google.com>
[+] Instrumented 1949 locations (64-bit, non-hardened mode, ratio 100%).
[100%] Linking CXX executable solfuzzer

インストルメンテーションメッセージが表示されない場合は、AFLのclangバイナリを指すcmakeフラグを切り替えてみてください。

# if previously failed
make clean
cmake .. -DCMAKE_C_COMPILER=path/to/afl-clang -DCMAKE_CXX_COMPILER=path/to/afl-clang++
make solfuzzer

そうでない場合は、実行時に「binary is not instrumented」というエラーでファザーが停止します。

afl-fuzz 2.52b by <lcamtuf@google.com>
... (truncated messages)
[*] Validating target binary...

[-] Looks like the target binary is not instrumented! The fuzzer depends on
    compile-time instrumentation to isolate interesting test cases while
    mutating the input data. For more information, and for tips on how to
    instrument binaries, please see /usr/share/doc/afl-doc/docs/README.

    When source code is not available, you may be able to leverage QEMU
    mode support. Consult the README for tips on how to enable this.
    (It is also possible to use afl-fuzz as a traditional, "dumb" fuzzer.
    For that, you can use the -n option - but expect much worse results.)

[-] PROGRAM ABORT : No instrumentation detected
         Location : check_binary(), afl-fuzz.c:6920

次に、いくつかのサンプルソースファイルが必要です。 これにより、ファザーがエラーを見つけるのが非常に簡単になります。 構文テストからいくつかのファイルをコピーするか、ドキュメントや他のテストからテストファイルを抽出できます。

mkdir /tmp/test_cases
cd /tmp/test_cases
# extract from tests:
path/to/solidity/scripts/isolate_tests.py path/to/solidity/test/libsolidity/SolidityEndToEndTest.cpp
# extract from documentation:
path/to/solidity/scripts/isolate_tests.py path/to/solidity/docs

AFLのドキュメントでは、コーパス(最初の入力ファイル)はあまり大きくしない方が良いとされています。 ファイル自体の大きさは1kB以下で、1つの機能に対して入力ファイルは多くても1つなので、少ない数から始めた方が良いでしょう。 また、 afl-cmin というツールがあり、バイナリの挙動が似ている入力ファイルをトリミングできます。

ここで、ファザーを実行します( -m ではメモリサイズを60MBに拡張しています)。

afl-fuzz -m 60 -i /tmp/test_cases -o /tmp/fuzzer_reports -- /path/to/solfuzzer

ファザーは、 /tmp/fuzzer_reports の失敗につながるソースファイルを作成します。 多くの場合、同じエラーを発生させる多くの類似したソースファイルを見つけます。 ツール scripts/uniqueErrors.sh を使って、固有のエラーをフィルタリングできます。

Whiskers

Whiskers は、 Mustache に似た文字列テンプレートシステムです。 コンパイラは、コードの可読性、ひいては保守性や検証性を高めるために、さまざまな場所でこのシステムを使用しています。

この構文は、Mustacheとは大幅に異なります。 テンプレートマーカー {{}} は、解析を助け、 Yul との衝突を避けるために、 <> に置き換えられています(シンボル <> はインラインアセンブリでは無効であり、 {} はブロックの区切りに使用されます)。 もう1つの制限は、リストは1つの深さまでしか解決されず、再帰的にはならないことです。 これは将来変更される可能性があります。

大まかな仕様は以下の通りです。

<name> が出現すると、与えられた変数 name の文字列値で置き換えられます。 このとき、エスケープや繰り返しの置き換えは行われません。 ある領域は <#name>...</name> で区切ることができます。 領域は、テンプレートシステムに供給された変数セットの数だけ、その内容を連結したものに置き換えられ、その都度、 <inner> 項目をそれぞれの値で置き換えます。 トップレベルの変数は、このような領域内で使用することもできます。

<?name>...<!name>...</name> 形式の条件式もあります。 ここでは、ブーリアンパラメータ name の値に応じて、テンプレートの置換が最初のセグメントまたは2番目のセグメントで再帰的に続けられます。 <?+name>...<!+name>...</+name> を使用する場合は、文字列パラメータ name が空でないかどうかをチェックします。

ドキュメンテーションのスタイルガイド

次のセクションでは、Solidityへのドキュメント提供に特化したスタイルの推奨事項を紹介します。

英語

プロジェクト名やブランド名を使用する場合を除き、国際英語を使用してください。 ローカルのスラングや参考文献の使用を極力控え、誰が読んでも分かりやすい言葉遣いを心がけてください。 以下は参考資料です。

注釈

公式のSolidityドキュメントは英語で書かれていますが、コミュニティの貢献によって他の言語の 翻訳 も利用できます。 コミュニティの翻訳に貢献する方法については、 翻訳ガイド を参照してください。

見出しのタイトルケース

見出しには タイトルケース を使用します。 つまり、タイトルの主要な単語はすべて大文字にしますが、冠詞、接続詞、前置詞はタイトルの最初でない限り、大文字にしません。

例えば、次のようなものはすべて正しいです。

  • Title Case for Headings

  • For Headings Use Title Case

  • Local and State Variable Names

  • Order of Layout

短縮形の展開

単語では短縮形を利用しないでください。 例えば、

  • 「Don't」ではなく「Do not」。

  • 「Can't」ではなく「Can not」。

能動態と受動態

チュートリアル形式のドキュメントでは、誰が、何がタスクを実行しているのかを読者が理解しやすいように、能動態(アクティブボイス)を推奨します。 しかし、Solidityのドキュメントは、チュートリアルとリファレンスコンテンツが混在しているため、受動態(パッシブボイス)の方が適している場合もあります。

要約すると

  • 例えば、Ethereum VMの言語定義や内部構造などの技術的な参照には、受動態を使用します。

  • Solidityのある側面を適用するための推奨事項を説明する際には、能動態を使用します。

例えば、以下はSolidityの側面を指定しているため、受動態になっています。

Functions can be declared pure in which case they promise not to read from or modify the state.

例えば、以下はSolidityのアプリケーションについて説明しているので、能動態になっています。

When invoking the compiler, you can specify how to discover the first element of a path, and also path prefix remappings.

一般的用語

  • 「function parameters」と「return variables」であり、input parametersとoutput parametersではありません。

コードの例

CIプロセスでは、PRを作成する際に ./test/cmdlineTests.sh スクリプトを使用して pragma soliditycontractlibraryinterface で始まるコードブロック形式のコード例をすべてテストします。 新しいコード例を追加する場合は、PRを作成する前にそのコード例が動作し、テストに合格することを確認してください。

すべてのコード例は、コントラクトコードが有効な最大の範囲をカバーする pragma バージョンで始まるようにします。 例えば、 pragma solidity >=0.4.0 <0.9.0; などとしてください。

ドキュメントのテストの実行

ドキュメントに必要な依存関係をインストールし、リンク切れや構文の問題などの問題をチェックする ./docs/docs.sh を実行することで、あなたの貢献が私たちのドキュメントテストに合格することを確認してください。

Solidityの言語設計

言語設計のプロセスに積極的に参加し、Solidityの将来に関するアイデアを共有するには、 Solidityフォーラム に参加してください。

Solidityフォーラムは、新しい言語機能やその実装のアイデアの初期段階や、既存の機能の修正を提案し、議論する場として機能しています。

提案が具体的になれば、その実現に向けて SolidityのGitHubリポジトリ でもイシューという形で議論されます。

フォーラムやイシューの議論に加えて、定期的に言語設計ディスカッションコールを開催し、特定のトピックや課題、機能の実装について詳細に議論しています。 これらのコールへの招待状は、フォーラムを通じて共有されます。

また、フィードバックアンケートなど、言語設計に関連したコンテンツをフォーラムで共有しています。

新機能の実装についてチームの状況を知りたい場合は、 SolidityのGithubプロジェクト で実装状況を確認できます。 デザインバックログに登録されている問題は、さらに詳細な仕様が必要なため、言語デザインコールまたは通常のチームコールで議論されます。 デフォルトのブランチ( develop )から breakingブランチ に変更することで、次のブレーキングリリースに向けた変更点を確認できます。

その場限りのケースや質問については、Solidityコンパイラや言語開発に関する会話のための専用チャットルームである Solidity-dev Gitter チャンネル を通じて連絡を取ることができます。

言語設計のプロセスをより協力的で透明性の高いものに改善するために、みなさんの意見をお聞かせください。

言語の影響

Solidityは、いくつかの有名なプログラミング言語に影響やインスピレーションを受けた カーリーブラケット言語 です。

Solidityは、C++から最も大きな影響を受けていますが、PythonやJavaScriptなどの言語からもコンセプトを借りています。

C++からの影響は、変数宣言やforループの構文、関数のオーバーロードの概念、暗黙的な型変換と明示的な型変換、その他多くの細部に見られます。

言語開発の初期において、SolidityはJavaScriptの影響を部分的に受けていました。 これは、関数レベルでの変数のスコープや、キーワード var の使用によるものでした。 JavaScriptの影響はバージョン0.4.0から少なくなりました。 現在、JavaScriptとの主な類似点は、関数がキーワード function を使って定義されていることです。 また、Solidityは、JavaScriptと同様のインポート構文とセマンティクスをサポートしています。 これらの点を除けば、Solidityは他の多くのカーリーブラケット言語と同じように見えますし、もはやJavaScriptの影響は大きくありません。

もう一つSolidityに影響を与えたのがPythonです。 Solidityのモディファイアは、Pythonのデコレータをモデルにして追加されたもので、機能はより制限されています。 さらに、多重継承、C3線形化、 super キーワードは、値や参照型の一般的な代入とコピーのセマンティクスと同様に、Pythonから採用されています。

Solidityブランドガイド

このブランドガイドでは、Solidityのブランドポリシーやロゴ使用のガイドラインなどを紹介しています。

Solidityのブランド

プログラミング言語「Solidity」は、コアチームが運営するオープンソースのコミュニティプロジェクトです。 コアチームのスポンサーは Ethereum Foundation です。

このドキュメントは、Solidityのブランド名とロゴの最適な使用方法についての情報を提供することを目的としています。

ブランド名やロゴを使用する前に、この文書をよくお読みになることをお勧めします。 みなさんのご協力に深く感謝します!

Solidityのブランド名

「Solidity」は、Solidityプログラミング言語のみを指す言葉です。

以下の場合に「Solidity」は使用しないでください。

  • 他のプログラミング言語を指し示す行為。

  • 誤解を招くような方法や、無関係なモジュール、ツール、ドキュメント、その他のリソースとSolidityの関連性を示唆するような方法での使用。

  • プログラミング言語Solidityがオープンソースであり、無料で使用できるかどうかについて、コミュニティを混乱させるような方法。

Solidityロゴのライセンス

Creative Commons License

Solidityのロゴは、 Creative Commons Attribution 4.0 International License の下で配布され、ライセンスされています。

これは、最も寛容なクリエイティブ・コモンズ・ライセンスであり、いかなる目的のためにも再利用や修正を認めています。

あなたは次のことが後述する条件で自由です。

  • Share — 媒体や形式を問わず、コピーして再配布できます。

  • Adapt — 営利目的であっても、いかなる目的であっても、素材をリミックス、変換、構築すること。

条件:

  • Attribution — 適切なクレジットを表示し、ライセンスへのリンクを提供し、変更があった場合はそれを示さなければなりません。妥当な方法でそうできますが、Solidityコアチームがあなたやあなたの使用を支持していることを示唆するような方法ではありません。

Solidityロゴを使用する際は、Solidityロゴのガイドラインを尊重してください。

Solidityロゴのガイドライン

_images/logo.svg

(ロゴの上で右クリックするとダウンロードできます。)

次のことをしないでください。

  • ロゴの比率の変更(伸ばしたり切ったりしない)。

  • ロゴの色の変更。どうしても必要な場合を除く。

クレジット

このドキュメントは、 Python Software Foundation Trademark Usage PolicyRust Media Guide から派生した部分もあります。