はじめに

初めまして。 『DApps開発入門』という本や色々記事を書いているかるでねです。

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

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

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

変更点

Solidity 0.8.31では、Ethereumの次期ネットワークアップグレード「Fusaka」への対応や、ストレージレイアウト機能の拡張、さらにリリースプロセスの改善が行われています。

主な変更点は以下の通りです。

  • Fusaka対応(osaka by Default、CLZ Opcode)
  • ストレージレイアウト指定子での定数サポート
  • 非推奨機能の導入(0.9.0での削除予定)
  • リリースプロセスの変更(ARM Linux対応、プレリリース導入、PPA廃止)

Fusaka対応

osaka by Default

Fusakaがメインネットに実装される予定となったため、コンパイラのデフォルトのEVMターゲットが「osaka」に変更されました。 これにより、--evm-version や settings.evmVersion を明示的に指定しない限り、自動的にFusakaの機能を使用したコンパイルが行われます。 もちろん、古いEVMバージョンを指定することも可能です。

CLZ Opcode

Solidity 0.8.31では、EIP7939で導入される「CLZ」オペコード(Count Leading Zeros)のサポートが追加されました。

このオペコードは、256ビットワードの先頭から連続するゼロビットの数を数えるものです。

例えば、以下のような値があるとします。

text
0x0000000000000000000000000000000000000000000000000000000000000010

この場合、先頭から連続する0ビットは 252個(10は0001 0000なので、最初の3ビットが0)となります。

CLZは、ビット操作、圧縮アルゴリズム、特定のデータ構造において便利な演算です。 現時点では、コンパイラ内部でこのオペコードを活用できる箇所は限定的ですが、将来的な最適化への活用が検討されています。

一方で、アプリケーション層では大きなメリットがあります。 例えば、以下のようなライブラリではすでに独自実装が用意されており、CLZオペコードによって効率的に置き換えられる可能性があります。

  • soladyなどのライブラリ
  • OpenZeppelinの Math.clz()

Yulでの使用方法

CLZオペコードは、Yulのビルトイン関数として以下のように使用できます。

solidity
assembly {
    let leadingZeros := clz(x)
}

clz(x) は256ビットワード x の先頭から連続するゼロビットの数を返します。

ストレージレイアウト指定子での定数サポート

Solidity 0.8.31では、ストレージレイアウト指定子において、定数(constant)変数をベーススロット式で使用できるようになりました。 これにより、より柔軟かつ可読性の高いストレージ配置が可能になります。

使用例

solidity
uint constant OFFSET = 0x10 + 1;

contract C layout at 0xAAAA + OFFSET {
    uint[3] x; // スロット 0xAABB..0xAABD を占有
}

このように、定数 OFFSET を使ってベーススロットを計算できます。

制限事項

Solidity の「コンパイル時に式を評価する機能」はまだ発展途上であり、扱える式には明確な制約があります。 そのため、ストレージレイアウトのbase slotを計算したり、constant の初期化式を書いたりする際には、この制限を理解しておく必要があります。

コンパイル時計算が扱える式は「有理数リテラルを使った算術式」のみ

Solidityのコンパイル時計算が評価できるのは、有理数リテラルを使った算術式だけです。

ここでいう 有理数リテラルとは、ソースコードに直接書かれた数値のことで、以下のような値のことを指します。

  • 10
  • 3.5
  • 1_000
  • 0x20

そして「有理数リテラルを使った算術式」とは、これらを +, -, *, / といった四則演算で組み合わせた単純な式のことです。

solidity
// OK: すべて有理数リテラルのみを使用
uint constant A = 10 + 5;
uint constant B = 3 * (2 + 7);
uint constant C = 1000 - 250;

このような式であれば、Solidityはコンパイル時に安全に計算できます。

コンパイル時計算で許可されない式の種類

有理数リテラルだけで構成される式は問題ありませんが、それ以外の要素が入るとコンパイル時評価の対象外となり、コンパイルエラーになります。

明示的な型変換を含む式

明示的な型変換(キャスト)とは、例えば uint256(x) のように特定の型へ値を変換する操作です。

solidity
uint8 x = 10;

// NG: 型変換が混ざるとコンパイル時計算は不可
uint constant BAD = uint256(x) + 1;

コンパイル時計算は無限精度で評価する一方、整数型は有限ビット幅で動作します。 そのため、型変換を含む式を安全に評価する方法が現状用意されておらず、エラーになります。

一部のビルトイン関数の使用

ビルトイン関数とは、Solidityやコンパイラが提供している特別な関数のことです。 型情報を返すものや、内部的なデータを取得する関数などが含まれます。

現段階のコンパイル時計算では、「関数呼び出しが含まれるような式は、その時点で評価対象外」となります。

solidity
// NG の例(説明用)
uint constant BAD = someBuiltin(10) + 3;

どれだけ単純に見える関数でも「*呼び出しがある」というだけでコンパイル時評価できません。

三項演算子の使用

三項演算子とは、以下のように条件によって返す値を分岐させるための演算子です。

text
条件 ? 値1 : 値2

条件式がコンパイル時点で分かっていても「分岐を含む式」と見なされるため、評価できません。

solidity
// NG: 条件が定数でも三項演算子が含まれている時点で評価不可
uint constant BAD = (true ? 1 : 2) + 5;

制限が constant の初期化式にも適用される

今回のアップデートでストレージレイアウトのbase slot式に constant が使えるようになりましたが、その constant に代入する式も同じ制限を受けます。

  • constant の初期化式で複雑な式を使う → コンパイルエラー
  • その constant を使ったストレージレイアウト指定も同じ制約を受ける

という構造になっています。

今後の改善予定

今後のリリースでは以下のような拡張が予定されています。

  • コンパイル時計算が扱える式の幅を広げる
  • よく使う表現を安全に扱うための専用ビルトイン関数を追加する

ただし、より柔軟なコンパイル時計算を実現するには、現行の簡易的な仕組みを超えた新しい評価システムが必要なため、段階的に拡張されていく見込みです。

非推奨機能(0.9.0での削除予定)

Solidity 0.9.0では、古い機能を削除してコンパイラをよりスリムにすることが計画されています。 そのため、0.8.31から削除予定の機能について非推奨警告(deprecation warning)が導入されました。

<address>.send() と <address>.transfer() の削除

これらの関数は、もともと「シンプルなEther送金」を行うために導入されました。 呼び出し先のコントラクトが複雑な処理を実行できないよう、ガスを 2300 に制限していました。

この2300ガスは、当時は以下の処理がギリギリ可能でした。

  • ストレージの読み取り
  • イベントの発行

しかし、書き込みや再帰的な呼び出しは不可能とされていました。

しかし、この設計には大きな問題がありました。

  • 「オペコードのガスコストは変わらない」という前提が崩れた
  • EIP150などでガスコストが変更され、2300ガスでは不十分なケースが発生

EIP150については以下の記事を参考にしてください。

そのため、現在では send() と transfer() の使用はアンチパターンと広く認識されており、代わりに <address>.call() を空のペイロードで使用することが推奨されています。

solidity
// 推奨される方法
(bool success, ) = recipient.call{value: amount}("");
require(success, "Transfer failed");

ただし、call() はデフォルトでガスの制限がないため、再入攻撃(reentrancy attack)の保護にはなりません。

そのため、以下のいずれかの対策が必要です。

  • トランジェントストレージでリエントランシーガードを実装
  • Checks-Effects-Interactions パターンを使用してコードを構造化

将来的には、EIP5920: PAY opcodeのようなEVMレベルの解決策が導入される可能性があり、その時には <address>.pay() として公開される可能性があります。

ABI Coder V1の削除

ABI Coder v2は、Solidity 0.4.19で実験的機能として導入されました。

当初は experimental pragma を使って明示的に有効化する必要がありましたが、0.7リリースサイクルで安定版と見なされ、0.8.0からデフォルトになりました。

ABI Coder v2の優位性

ABI Coder v2は、v1の完全な上位互換であり、以下の機能を追加しています。

  • 構造体のサポート
  • 任意にネストされた配列のサポート
  • より厳密な入力検証

v1の廃止理由

ABI Coder v1は、以下の理由から完全に時代遅れとなっています。

  • IR pipeline(中間表現パイプライン)ではv2が必須
  • v1はlegacy evmasm pipelineでのみ使用可能
  • --via-ir でコンパイルする場合、pragmaを無視して常にv2が使用される
  • v2は最初からYulで実装されており、両方のパイプラインで使用可能

そのため、ABI Coder v1は0.9.0で削除されます。

Virtual Modifiersの削除

Solidityでは、関数と同様に、修飾子(modifier)も virtual として宣言し、派生コントラクトでオーバーライドすることが可能です。

solidity
contract Base {
    modifier onlyOwner() virtual {
        require(msg.sender == owner, "Not owner");
        _;
    }
}

contract Derived is Base {
    modifier onlyOwner() override {
        require(msg.sender == admin, "Not admin");
        _;
    }
}

しかし、この機能は以下の理由から削除されることになりました。

  • ユーザーにとって予期しない挙動を引き起こしやすい
  • コンパイラ実装において複雑なコーナーケースを生む
  • 継承がすでに言語の複雑性の主要な原因であり、必須ではない機能は削除したい

なお、修飾子内で仮想関数を呼び出す機能は削除されません。

solidity
modifier onlyOwner() {
    require(isOwner(), "Not owner"); // isOwner()は仮想関数でOK
    _;
}

Contract Comparison Operatorsの削除

Solidityにおける Contract 型の変数は、内部的には単に address をラップしたものとして扱われています。 そのためこれまでは、アドレスに対して許されている操作がContract型にもほぼそのまま適用できていました。

実際、Solidity 0.5.0まではアドレス型が持つすべてのメンバーを Contract 型でも利用できるという時代があり、両者の扱いにはほとんど区別がありませんでした。

しかし言語仕様が進化する中で、次第に「アドレス型」と「Contract 型」は明確に区別すべきものとして整理されるようになりました。 この流れによって両者の機能や振る舞いは徐々に切り離されてきましたが、ひとつだけ残っていたのが比較演算子による比較でした。

Contract 型に対する比較演算が生む問題

現在のSolidityでは、以下のように Contract 型に対して < や > のような比較が書けてしまいます。

solidity
contract A {}
contract B {}

A a;
B b;

bool result = a < b; // これは何を比較している?

一見すると “コントラクトとしての何か” を比較しているようにも思えますが、実際には内部の address を比較しているだけです。

しかし、Contract 型は文法上「構造体に似た使い方をする型」でもあるため、読み手が「コントラクトの中身を比較しているのでは?」と誤解する可能性があります。 この不透明さが言語仕様として問題視されていました。

0.9.0 からは「明示的なアドレス変換」が必須に

こうした混乱を避けるため、Solidity 0.9.0 では Contract 型の比較演算子の利用は廃止され、比較を行う場合は address() による明示的な型変換が必須になります。

solidity
bool result = address(a) < address(b); // これなら「アドレスを比較している」と明確

これにより、以下のメリットがあります。

  • 何を比較しているのかが明確になる
  • Contract 型と address 型の区別がよりわかりやすくなる
  • 誤解を避け、コードの可読性と意図の明確性が向上する

memory-safe-assembly 特別コメントの削除

インラインアセンブリは、Solidityのメモリモデルと互換性のない方法でメモリを使用できます。 そのため、メモリ操作を含むアセンブリブロックや、メモリ内のSolidity変数への代入を行うアセンブリブロックがあると、特定のメモリ最適化がグローバルに無効化されます。 これを回避するため、プログラマーはアセンブリブロックが完全に安全であることを宣言できます。

solidity
assembly ("memory-safe") {
    ...
}

しかし、この機能が導入される前のバージョンでも使えるように、後方互換性のある一時的な仕組みとして特別なNatSpecコメントが用意されていました。

solidity
/// @solidity memory-safe-assembly
assembly {
    ...
}

Solidity 0.9.0は後方互換性がないため、この代替メカニズムは不要となり、削除されます。

リリースプロセスの変更

Solidity 0.8.31では、リリースプロセスにも複数の改善が加えられました。

Linux ARM対応

今回のリリースから、ARMアーキテクチャ向けの公式Linuxバイナリが提供されるようになりました。

これにより、以下のユーザーにとって利便性が向上します。

  • ARMベースのハードウェアでLinuxをメインOSとして使用している開発者
  • ARMハードウェア上でLinuxコンテナ(例:Docker)を使用してコンパイラを実行するユーザー

ARM サポート自体は新しいものではありません。

  • ARM ベースのMac向けにはすでにバイナリが提供されていた
  • Linux上でもソースからビルドすることは可能だった

今回の変更は、公式バイナリとしてCI・リリースインフラに統合された点が重要です。

これにより、以下のメリットがあります。

  • 他のプラットフォームと同じソースから提供される
  • 完全なテストプロセスを経ている
  • 生成されるバイトコードとメタデータがすべてのプラットフォームで同一であることが検証されている

プレリリースの導入

これまでSolidityは、以下の2つのビルドタイプのみを提供していました。

  • 正式リリース版
  • Nightly ビルド(開発版)

しかし、実験的機能を早期に共有するために、より軽量なプロセスとしてプレリリースが導入されました。 2024年10月に公開された v0.8.31-pre.1 では、今回のリリースに含まれる機能の一部(CLZオペコードのサポートなど)がすでに公開されていました。

PPA リリースの廃止

Solidityコンパイラは、複数のチャネルでバイナリを提供していますが、利用頻度の低いものについては廃止が計画されています。 今回のリリースから、Ubuntu PPA(Personal Package Archive)がバイナリ配布チャネルとして正式に廃止されました。 また、Dockerリリースについても、少なくとももう1リリース継続される予定ですが、利用が少ない場合は廃止される可能性があります。

Dockerリリースは、DockerHubからGitHubのコンテナレジストリに移行されており、新しいバイナリもそちらに公開されます。

その他の変更

Solidity 0.8.31では、上記以外にもいくつかの機能追加とバグ修正が行われています。

コンパイラ機能の追加

EVMバージョンのデフォルト設定を「osaka」に変更

前述の通り、デフォルトのEVMバージョンが「osaka」に変更されました。

ethdebug: EOFでの命令とソース位置の実験的サポート

EOF(EVM Object Format)における命令とソース位置の情報を ethdebug 形式で出力する実験的サポートが追加されました。 これにより、将来的なEOFベースのデバッグツールの開発が容易になります。

バグ修正

サブアセンブリのID割り当てにおける型幅の問題を修正

2階層以上ネストされたサブアセンブリに割り当てられるIDが、固定幅の型を使用していなかったため、ターゲットアーキテクチャ間で --asm-json 出力が不整合になる問題が修正されました。

ExpressionSimplifierによる無効なYulコード生成を修正

Yul Optimizerの ExpressionSimplifier が、スコープ外の変数を含む式を置換してしまい、無効なYulコードを生成するエッジケースが修正されました。

ビルドシステムの変更

Linux arm64バイナリのテストとリリース対応

前述の通り、ARM Linux向けのバイナリがテストおよびリリースプロセスに統合されました。

最小バージョン要件の更新

以下のツールチェーンの最小バージョンが更新されました。

  • Boost: 1.83.0(非Windows環境)
  • GCC: 13.3
  • Clang: 18.1.3

これにより、GCC 14未満とBoost 1.75未満の組み合わせで発生していた boost::rational 比較時の無限再帰問題が解消されます。

最後に

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

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