イーサリアムのメインネットワークのガス料金は、特にネットワークが混雑している場合に常に発生する問題です。ピーク時には、ユーザーは非常に高額な取引手数料を支払わなければならないことがよくあります。したがって、スマートコントラクトの開発段階でガス料金を最適化することが特に重要です。ガス消費を最適化すると、トランザクションコストが効果的に削減されるだけでなく、トランザクション効率も向上し、ユーザーにより経済的で効率的なブロックチェーンエクスペリエンスがもたらされます。

この記事では、イーサリアム仮想マシン (EVM) のガス料金メカニズム、ガス料金の最適化に関連する中心的な概念、スマート コントラクト開発時のガス料金最適化のベスト プラクティスについて概説します。これらのコンテンツを通じて、開発者にインスピレーションと実践的な支援を提供するとともに、一般ユーザーがEVMのガス料金がどのように運用されるかをより深く理解し、ブロックチェーンエコシステムの課題に共同で対処できるようにしたいと考えています。

EVMのガス料金メカニズムの紹介

EVM 準拠のネットワークでは、「ガス」は、特定の操作を実行するために必要な計算能力を測定するために使用される単位を指します。

次の図は、EVM の構造レイアウトを示しています。この図では、ガス消費量は、オペレーションの実行、外部メッセージの呼び出し、メモリとストレージの読み取りと書き込みの 3 つの部分に分割されています。

イーサリアム スマート コントラクトのガス最適化に関するベスト プラクティス トップ 10

出典: イーサリアム公式ウェブサイト[1]

各トランザクションの実行にはコンピューティング リソースが必要となるため、無限ループやサービス拒否 (DoS) 攻撃を防ぐために料金が請求されます。取引を完了するために必要な手数料は「ガス手数料」と呼ばれます。

EIP-1559 (ロンドンハードフォーク) が発効して以来、ガス料金は次の式で計算されます。

ガス料金 = 使用ガス単位 * (基本料金 + 優先料金)

基本料金は燃やされ、優先料金はバリデーターがブロックチェーンにトランザクションを追加するインセンティブとして機能します。トランザクション送信時により高い優先順位の料金を設定すると、トランザクションが次のブロックに含まれる可能性が高くなります。これは、ユーザーがバリデーターに支払う「チップ」に似ています。

1. EVM におけるガスの最適化を理解する

Solidity を使用してスマート コントラクトをコンパイルすると、コントラクトは一連の「オペコード」または複数のオペコードに変換されます。

オペレーション コード (コントラクトの作成、メッセージ コールの実行、アカウント ストレージへのアクセス、仮想マシンでの操作の実行など) にはガス消費コストが認識されており、これはイーサリアム イエロー ペーパー [2] に記録されています。

イーサリアム スマート コントラクトのガス最適化に関するベスト プラクティス トップ 10

EIP に多くの変更が加えられた後、一部のオペコードのガス コストが調整され、イエロー ブックのコストから逸脱する可能性があります。オペコードの最新のコストの詳細については、こちらを参照してください[3]。

2.ガス最適化の基本概念

Gas 最適化の中心的な概念は、EVM ブロックチェーン上でコスト効率の高い操作を優先し、高価な Gas コスト操作を回避することです。

EVM では、次の操作のコストが低くなります。

  • メモリ変数の読み取りと書き込み
  • 定数と不変変数の読み取り
  • ローカル変数の読み取りと書き込み
  • calldata 配列や構造体などの calldata 変数を読み取る
  • 内部関数呼び出し

コストのかかる操作には次のようなものがあります。

  • コントラクトストレージに保存された状態変数の読み取りおよび書き込み
  • 外部関数呼び出し
  • ループ動作

EVM ガスコスト最適化のベスト プラクティス

上記の基本概念に基づいて、開発者コミュニティ向けにガス料金最適化のベスト プラクティスのリストをまとめました。これらの実践に従うことで、開発者はスマート コントラクトのガス消費量を削減し、トランザクション コストを削減し、より効率的でユーザー フレンドリーなアプリケーションを作成できます。

1. ストレージの使用量を最小限に抑える

Solidity では、ストレージのリソースは限られており、そのガス消費量はメモリよりもはるかに多くなります。スマート コントラクトがストレージからデータを読み書きするたびに、高いガスコストが発生します。

イーサリアム イエロー ペーパーの定義によれば、ストレージ操作のコストはメモリ操作のコストの 100 倍以上です。たとえば、OPcodesmload および mstore 命令はわずか 3 Gas ユニットを消費しますが、sload や sstore などのストレージ操作には、最良の場合でも少なくとも 100 ユニットのコストがかかります。

イーサリアム スマート コントラクトのガス最適化に関するベスト プラクティス トップ 10

ストレージの使用量を制限するには、次のような方法があります。

  • 非永続データをメモリに保存する
  • 中間結果をメモリに保存し、すべての計算が完了した後に結果をストレージ変数に割り当てることで、ストレージの変更の回数を減らします。

2. 可変包装

スマート コントラクトで使用されるストレージ スロットの数と、開発者がデータを表現する方法は、ガス料金の消費に大きく影響します。

Solidity コンパイラーは、コンパイル プロセス中に連続ストレージ変数をパックし、変数ストレージの基本単位として 32 バイトのストレージ スロットを使用します。変数のパッケージ化とは、複数の変数が 1 つのストレージ スロットに収まるように変数を適切に配置することを指します。

左側は 3 つのストレージ スロットを消費するあまり効率の悪い実装であり、右側はより効率的な実装です。

イーサリアム スマート コントラクトのガス最適化に関するベスト プラクティス トップ 10

この詳細を調整することで、開発者は 20,000 ガス ユニットを節約できます (未使用のストレージ スロットを保管するには 20,000 ガスの費用がかかります)。ただし、必要なストレージ スロットは 2 つだけです。

各ストレージ スロットはガスを消費するため、可変パッキングにより必要なストレージ スロットの数が減り、ガスの使用が最適化されます。

3. データ型の最適化

変数は複数のデータ型で表すことができますが、データ型が異なれば運用コストも異なります。適切なデータ タイプを選択すると、Gas の使用量を最適化することができます。

たとえば、Solidity では、整数をさまざまなサイズ (uint8、uint16、uint32 など) に再分割できます。 EVM は 256 ビット単位で動作するため、uint8 を使用すると、EVM はまずそれを uint256 に変換する必要があり、この変換により追加の Gas が消費されます。

イーサリアム スマート コントラクトのガス最適化に関するベスト プラクティス トップ 10

図のコードを通じて、uint8 と uint256 のガスコストを比較できます。 UseUint() 関数は 120,382 Gas ユニットを消費し、UseUInt8() 関数は 166,111 Gas ユニットを消費します。

個別に考えると、ここで uint256 を使用すると、uint8 よりも安価になります。ただし、前に提案した変数パッキングの最適化を使用する場合は異なります。開発者が 4 つの uint8 変数を 1 つのストレージ スロットに詰め込むことができれば、それらを反復処理する総コストは 4 つの uint256 変数よりも低くなります。このようにして、スマート コントラクトはストレージ スロットの読み取りと書き込みを 1 回行うことができ、1 回の操作で 4 つの uint8 変数をメモリ/ストレージに配置できます。

4. 動的変数の代わりに固定サイズの変数を使用する

データが 32 バイト以内で制御できる場合は、バイトまたは文字列の代わりに bytes32 データ型を使用することをお勧めします。一般に、固定サイズ変数は可変サイズ変数よりもガス消費量が少なくなります。バイト長を制限できる場合は、bytes1 から bytes32 までの最小長を選択してください。

イーサリアム スマート コントラクトのガス最適化に関するベスト プラクティス トップ 10

イーサリアム スマート コントラクトのガス最適化に関するベスト プラクティス トップ 10

5. マッピングと配列

Solidity のデータ リストは、配列 (Array) とマッピング (Mapping) の 2 つのデータ型で表現できますが、それらの構文と構造はまったく異なります。

ほとんどの場合、マップはより効率的で安価ですが、配列は反復可能であり、データ型のパッキングをサポートします。したがって、反復が必要な場合やデータ型のパッケージ化によってガス消費量を最適化できる場合を除き、データ リストを管理するときは最初にマッピングを使用することをお勧めします。

6. メモリの代わりに calldata を使用する

関数パラメータで宣言された変数は、calldata またはメモリに保存できます。 2 つの主な違いは、メモリは関数によって変更できるのに対し、calldata は不変であることです。

この原則を覚えておいてください。関数パラメータが読み取り専用の場合、メモリよりも calldata を優先して使用する必要があります。これにより、関数呼び出しデータからメモリへの不必要なコピーが回避されます。

例 1: メモリの使用

イーサリアム スマート コントラクトのガス最適化に関するベスト プラクティス トップ 10

Memory キーワードを使用すると、ABI デコード中に配列値がエンコードされた呼び出しデータからメモリにコピーされます。このコード ブロックの実行コストは 3,694 Gas ユニットです。

例 2: calldata の使用

イーサリアム スマート コントラクトのガス最適化に関するベスト プラクティス トップ 10

calldata から値を直接読み取る場合、中間のメモリ操作はスキップされます。この最適化方法により、実行コストはわずか 2,413 ガス ユニットに削減され、ガス効率は 35% 向上します。

7. 可能な限り定数/不変キーワードを使用する

定数/不変変数はコントラクトのストレージに保存されません。これらの変数はコンパイル時に計算され、コントラクトのバイトコードに保存されます。したがって、ストレージよりもアクセスコストが大幅に安くなり、可能な限り Constant または Immutable キーワードを使用することをお勧めします。

8. オーバーフロー/アンダーフローが発生しないことを確認する場合は、Unchecked を使用します。

開発者は、算術演算によってオーバーフローやアンダーフローが発生しないと確信している場合、Solidity v0.8.0 で導入された unchecked キーワードを使用して、冗長なオーバーフローまたはアンダーフロー チェックを回避し、ガス コストを節約できます。

以下の図では、条件 i に従います。

イーサリアム スマート コントラクトのガス最適化に関するベスト プラクティス トップ 10

さらに、コンパイラ バージョン 0.8.0 以降では、コンパイラ自体にオーバーフローおよびアンダーフロー保護が組み込まれているため、SafeMath ライブラリを使用する必要がなくなりました。

9. モディファイアを最適化する

修飾子のコードは変更された関数に埋め込まれており、修飾子が使用されるたびにそのコードがコピーされます。これにより、バイトコードのサイズとガス消費量が増加します。モディファイアのガスコストを最適化する 1 つの方法は次のとおりです。

最適化前:

イーサリアム スマート コントラクトのガス最適化に関するベスト プラクティス トップ 10

最適化後:

イーサリアム スマート コントラクトのガス最適化に関するベスト プラクティス トップ 10

この場合、ロジックを内部関数 _checkOwner() にリファクタリングし、内部関数を修飾子で再利用できるようにすることで、バイトコード サイズが削減され、ガス コストが削減されます。

10. 短絡回路の最適化

|| 演算子と && 演算子の場合、論理演算は短絡評価を受けます。つまり、最初の条件がすでに論理式の結果を決定できる場合、2 番目の条件は評価されません。

ガス消費を最適化するには、コストのかかる計算をスキップできるように、計算コストの低い条件を最初に配置する必要があります。

追加の一般的なアドバイス

1. 無駄なコードを削除する

コントラクト内に使用されていない関数や変数がある場合は、削除することをお勧めします。これは、契約の展開コストを削減し、契約のサイズを小さく保つための最も直接的な方法です。

以下にいくつかの実践的な提案を示します。

計算には最も効率的なアルゴリズムを使用します。特定の計算結果が契約で直接使用される場合は、これらの冗長な計算プロセスを削除する必要があります。基本的に、未使用の計算は削除する必要があります。

イーサリアムでは、開発者はストレージスペースを解放することでGas報酬を得ることができます。変数が不要になった場合は、delete キーワードを使用して削除するか、デフォルト値に設定する必要があります。

ループの最適化: コストのかかるループ操作を回避し、可能な限りループをマージし、繰り返される計算をループ本体の外に移動します。

2. プリコンパイルされた契約を使用する

プリコンパイルされたコントラクトは、暗号化やハッシュ操作などの複雑なライブラリ関数を提供します。コードは EVM 上では実行されず、クライアント ノード上でローカルに実行されるため、必要なガスが少なくなります。プリコンパイルされたコントラクトを使用すると、スマート コントラクトの実行に必要な計算量が削減され、ガスを節約できます。

プリコンパイルされたコントラクトの例には、楕円曲線デジタル署名アルゴリズム (ECDSA) や SHA2-256 ハッシュ アルゴリズムなどがあります。これらのプリコンパイルされたコントラクトをスマート コントラクトで使用することで、開発者はガスコストを削減し、アプリケーションの動作効率を向上させることができます。

Ethereum ネットワークでサポートされるプリコンパイル済みコントラクトの完全なリストについては、ここを参照してください[4]。

3. インラインアセンブリコードを使用する

インライン アセンブリにより、開発者は、高価な Solidity オペコードを使用せずに、EVM によって直接実行できる低レベルでありながら効率的なコードを作成できます。インライン アセンブリでは、メモリとストレージの使用量をより正確に制御できるため、ガス料金がさらに削減されます。さらに、インライン アセンブリでは、Solidity だけを使用して実現するのが難しいいくつかの複雑な操作を実行できるため、ガス消費を最適化するための柔軟性が向上します。

インライン アセンブリを使用してガスを節約するコード例を次に示します。

イーサリアム スマート コントラクトのガス最適化に関するベスト プラクティス トップ 10

上の図からわかるように、インライン アセンブリ技術を使用した 2 番目のユース ケースは、標準のユース ケースと比較してガス効率が高くなります。

ただし、インライン アセンブリの使用にはリスクがあり、エラーが発生しやすい可能性もあります。したがって、経験豊富な開発者のみが使用するように注意してください。

4. レイヤ 2 ソリューションを使用する

レイヤ 2 ソリューションを使用すると、イーサリアム メインネット上で保存および計算する必要があるデータの量が削減されます。

ロールアップ、サイドチェーン、ステート チャネルなどのレイヤー 2 ソリューションは、メインのイーサリアム チェーンからトランザクション処理をオフロードすることで、より高速かつ安価なトランザクションを可能にします。

これらのソリューションは、多数のトランザクションをまとめることにより、オンチェーン トランザクションの数を減らし、ガス料金を削減します。レイヤ 2 ソリューションを使用すると、イーサリアムのスケーラビリティも向上し、ネットワークに過負荷や輻輳を引き起こすことなく、より多くのユーザーやアプリケーションがネットワークに参加できるようになります。

5. 最適化ツールとライブラリを使用する

solc オプティマイザー、Truffle のビルド オプティマイザー、Remix の Solidity コンパイラーなど、いくつかの最適化ツールが利用可能です。

イーサリアム スマート コントラクトのガス最適化に関するベスト プラクティス トップ 10

これらのツールは、バイトコード サイズを最小限に抑え、デッド コードを削除し、スマート コントラクトの実行に必要な操作の数を減らすのに役立ちます。 「solmate」などの他の Gas 最適化ライブラリと組み合わせることで、開発者は効果的に Gas コストを削減し、スマート コントラクトの効率を向上させることができます。

結論は

ガス消費の最適化は、トランザクションコストを最小限に抑え、EVM 互換ネットワーク上のスマートコントラクトの効率を向上させるために、開発者にとって重要なステップです。開発者は、コスト削減操作を優先し、ストレージ使用量を削減し、インライン アセンブリを活用し、この記事で説明するその他のベスト プラクティスに従うことで、契約のガス消費量を効果的に削減できます。

ただし、最適化プロセス中、開発者はセキュリティ上の脆弱性の導入を防ぐために注意して作業する必要があることに注意する必要があります。コードを最適化し、ガス消費量を削減するプロセスにおいて、スマート コントラクトに固有のセキュリティが決して犠牲にされるべきではありません。

[1] :https://ethereum.org/en/developers/docs/gas/

[2] :https://ethereum.github.io/ yellowpaper/paper.pdf

[3] :https://www.evm.codes/

[4] :https://www.evm.codes/precompiled