はじめに

初めまして。 CryptoGamesというブロックチェーンゲーム企業でエンジニアをしている cardene(かるでね) です! スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。

代表的なゲームはクリプトスペルズというブロックチェーンゲームです。

以下でも情報発信しているので、興味ある記事があればぜひ読んでみてください!

今回はSolidityのバージョン0.8.22での変更点をまとめていきます。

以下の公式のリリース記事をもとにまとめていきます。

変更点

Solidityのバージョン0.8.22では以下の項目が変更されました。

  • チェックされないループインクリメント。
  • Yulオプティマイザの調整。
  • EVMアセンブリのJSONインポートサポート。
  • ファイルレベルでのイベント定義。

チェックされないループインクリメント

これまでは、ガスコスト効率化の観点から、uncheckedブロックでforループのカウントインクリメント部分を囲い、オーバーフローチェックを無視する方法が使用されていました。

solidity
for (uint i = 0; i < array.length;) {
    acc += array[i];
    unchecked { i++; } // i gets incremented without overflow checks -- less gas used
}

ただし、このコードは冗長だったため、Solidity0.8.22では特定の条件下でコードをシンプルに保ちながらガスコストを削減できるようになりました。 特定の条件とは以下になります。

  • ループ条件がi < ...の形式であること。
    • iはループ内で使われるカウンター変数です。
  • ループ条件の右側(比較される値)の型が、カウンター変数iの型に自動的に変換できること。
    • カウンターの型が比較の前に変わらないこと。
  • ループのカウンター変数iが、Solidityで用意されている整数型であること。
  • ループのカウンターを増やす式が、i++または++iであること。
  • ループ条件やループの中で、カウンター変数iが変更されないこと。

以下のコードだと最適化が適用されます。

solidity
for (uint i = 0; i < array.length; ++i) {
    acc += array[i]; // i はループ本体で変更されません
}

一方、以下の場合はiの比較前にuint16に変換されるため、最適化が適用されません。

solidity
for (uint8 i = 0; i < uint16(1000); ++i) {
    // ループ本体
}

また以下のような条件や設定もあります。

  • 最適化が有効になるのは<演算子だけです。
    • 他の演算子、例えば<=などは対象外です。
  • 開発者が独自に定義した<演算子は最適化の対象になりません。
    • Solidityで元々用意されている<演算子だけが対象です。
  • この最適化は、他の最適化設定が無効になっていても自動的に有効になります。
  • この最適化を無効にしたい場合は、settings.optimizer.details.simpleCounterForLoopUncheckedIncrementをfalseに設定することで設定できます。

Yulオプティマイザの調整

Solidityのバージョン0.8.20で導入されたPUSH0オペコードは、0をスタックにプッシュするための命令です。 PUSH0は他のオペコードに比べてガスコストが低いため、効率的にゼロを扱うために利用されています。

Solidityのバージョン0.8.24では、Yulオプティマイザ内で使用される最適化ステップの一つであるRematerialiserで、ゼロリテラル(値が0という定数)を変数参照として保持するのではなく常に再生成するように拡張されました。 これにより以下のメリットがあります。

  • ゼロリテラルをスタックにプッシュする時に、ガスコストの高いDUP命令を使わずにPUSH0を使えるようになる。
  • 全体としてガスコストが削減される。

これまでの方法では、ゼロリテラルを一度変数として保存してその変数を参照していましたが、DUP命令が使用されるためガスコストが高くなっていました。 新しく実装された方法では、ゼロリテラルが常に再生成されるためPUSH0オペコードが使用され、ガスコストが低く抑えられます。

また、最適化ステップのデフォルトに以下の2つのステップが追加されました。

  • Rematerialiser
    • ゼロリテラルを常に再生成するステップ。
  • UnusedPruner
    • 使われていない変数を削除するステップ。

EVMアセンブリのJSONインポートサポート

実験的な機能としてEVMアセンブリのインポートをサポートを追加しました。 これにより、外部ツールを使用してバイトコードが生成される直前に最適化を行うことが可能になります。 この新機能は、EVMアセンブリをインポートできるようにするものです。 これにより、バイトコードが生成される直前に外部ツールで最適化を行うことができます。 以下のような手順での使用が想定されています。

  1. コンパイラが生成したEVMアセンブリをエクスポート。
  2. 外部ツールを使用して、エクスポートしたアセンブリコードを修正。
  3. 修正されたアセンブリコードを再インポートし、通常のコンパイルプロセスを再開。

ただ、この機能はまだ実験段階にあり、現時点では本番環境での使用は推奨されていません。

ファイルレベルでのイベント定義

イベント定義をファイルレベルで行えるようになりました。 これにより、コードの整理がしやすくなり、イベントを無理にライブラリ内に定義する必要がなくなります。

今回のアップデートで、イベントをコントラクトの外やファイル全体のスコープで定義できるようになりました。 これにより、以下のようにコードの整理がしやすくなり、イベントの定義を共通の場所にまとめることができます。

solidity
event E(); // ファイルレベルでのイベント定義

contract D {
    function f() public {
        emit E(); // ファイルレベルで定義されたイベントの発行
    }
}

また、Solidityのバージョン0.8.21では、他のコントラクトやインターフェースで定義されたイベントを発行しようとすると、NatSpecドキュメント生成時にエラーが発生するバグがありました。 このバグが修正され、外部イベントを正しく発行できるようになりました。

NatSpecはSolidityの標準的なドキュメンテーション形式で、コントラクトや関数の説明をコメントとして記述し、それを元にドキュメントを生成します。 他のコントラクトやインターフェースで定義されたイベントを自身のコントラクト内で発行すると、コンパイルは成功するもののNatSpecドキュメント生成時に内部エラーが生じてしまっていました。

solidity
interface I {
    event ForeignEvent(); // インターフェースで定義されたイベント
}

contract C {
    event ForeignEvent(); // コントラクトで定義されたイベント
}

contract D {
    function f() public {
        emit I.ForeignEvent(); // インターフェースで定義されたイベントの発行
        emit C.ForeignEvent(); // コントラクトで定義されたイベントの発行
        emit E(); // ファイルレベルで定義されたイベントの発行
    }
}

その他の変更

その他の細かい修正については以下の記事を参考にしてください。

最後に

今回はSolidityのバージョン0.8.22の変更点をまとめました。

以下でも情報発信しているので、興味ある記事があればぜひ読んでみてください!