スマートコントラクトを記述する際に、ガス消費量を最適化するにはどうすればよいですか?

Hans-Helmut Kraus
Hans-Helmut Kraus
Ethereum smart contract auditor and security expert; 以太坊智能合约审计师与安全专家。

はい、もちろんです!このトピックについてお話ししましょう。これはSolidity開発者なら誰もが避けて通れない課題です。ガス最適化をうまく行えば、あなたのDAppがユーザーからより人気になるだけでなく(誰も無駄な出費はしたくないですからね)、あなたの技術力の証でもあります。


スマートコントラクトを記述する際のガス消費量を最適化する方法:ベテランが教える節約術!

やあ、友よ!スマートコントラクトを書き始めたばかりですか?それなら、ガスという厄介な存在について話しましょう。

イーサリアム上でコードを実行するのは、車を運転するようなものです。各操作(例えばデータの保存、計算など)には「ガソリン」(Gas)を消費し、この「ガソリン」は実際のお金(ETH)を払って購入する必要があります。あなたのコントラクトコードが「重く」なればなるほど、ユーザーはその利用に「ガソリン」を多く消費し、手数料は高くなります。やがて、誰もあなたの製品を使いたがらなくなるでしょう。

だから、ガス最適化は何も難解な技術ではなく、それは節約の芸術なのです。以下に、私のとっておきの実践テクニックをいくつか共有します。分かりやすく解説しますのでご安心ください。

一、 あなたの「倉庫」(Storage)を賢く管理する — ここが最も費用がかかる場所です!

イーサリアムで最も高価な操作は、チェーン上にデータを保存すること(storageへの書き込み)です。これを非常に高価なクラウドハードディスクだと思ってください。1バイト保存するだけでも痛い出費です。

1. データパッキング (Struct Packing)

イーサリアム仮想マシン(EVM)は、データを「スロット」(Slot)単位で処理します。1つのスロットは32バイト(256ビット)です。

悪い例:

// これではuint256自体が32バイトを占有するため、2つのスロットを占有します。
struct User {
    uint128 id;      // 新しいスロットを占有
    uint256 balance; // 別のスロットを占有
    uint128 timestamp; // さらに新しいスロットを占有
}

節約のための書き方:

// これなら2つのスロットしか占有しません。
struct User {
    uint128 id;      // 下のtimestampと一緒に1つのスロットにパッキングされます
    uint128 timestamp; // (128+128 = 256ビット = 32バイト = 1つのスロット)
    uint256 balance; // 自身で1つのスロットを占有
}

まとめると: Solidityは、32バイトの「箱」に収まる変数群をまとめてくれます。そのため、小さなサイズの変数(例えばuint128booladdress)を隣接して宣言すると、それらは自動的にパッキングされ、ストレージスペースを節約できます。

2. memory または calldata を最大限に活用する
  • storage:オンチェーンのハードディスク、永続ストレージ、最も高価。
  • memory:メモリ、関数実行後に消滅、次に高価。
  • calldata:関数外部呼び出しの引数格納領域、読み取り専用、最も安価。

原則: データが関数の実行中に一時的に使用されるだけの場合、または引数として渡されて読み込まれるだけの場合、絶対にそれをstorageに保存しないでください。

節約のための書き方:

// external関数の配列引数は、calldataを使用するのが最も安価です
function sum(uint[] calldata numbers) external pure returns (uint) {
    uint total = 0;
    // ここで`numbers`はメモリにコピーされず、`calldata`から直接読み込まれるため、大幅な節約になります。
    for(uint i = 0; i < numbers.length; i++) {
        total += numbers[i];
    }
    return total;
}
3. constantimmutable を使用する

変数の値が最初から確定しており、二度と変更されない場合は、これら2つのキーワードを使用します。

  • constant:コンパイル時に確定する定数(例:固定料金 uint public constant RATE = 5;)。
  • immutable:デプロイ時(constructor内)にのみ確定する「定数」(例:プロジェクトオーナーのウォレットアドレス)。

利点: これらの変数の値はコントラクトのバイトコードに直接「ハードコード」され、それらを読み取る際にstorage操作を伴いません。Gas消費は極めて低いです。石に刻まれた情報のように、一目見ればわかるので、毎回倉庫を探し回る必要はありません。

二、 あなたの「実行ロジック」(Execution)を最適化する — コードをより軽快に動作させる

ストレージだけでなく、コードの実行ロジックもGasを消費します。

1. externalpublic よりも優れている

関数が外部アカウントからのみ呼び出される(例えばユーザーがウォレット経由で呼び出す場合など)場合で、コントラクト内部の他の関数から呼び出されないのであれば、externalとして宣言してください。

なぜでしょうか? public関数の引数は自動的にmemoryにコピーされますが、external関数の引数はcalldataから直接読み込まれるため、コピーのオーバーヘッドが省けます。この小さな変更が、塵も積もれば山となります。

2. ループ(Loops)には注意する

ループはGasキラーです。特にループの回数が不定または非常に大きくなる可能性がある場合です。もしあなたのループが配列に基づいており、その配列がユーザーによって無限に肥大化させられる場合、いずれGasがブロック上限を超え、実行不能になる可能性があります。

原則:

  • ループ内でストレージ操作(SSTORE)を実行することを避ける。
  • ユーザーにデータを分割して処理させるようにし、一つのループで全てを一度に処理しないように努める。
3. unchecked算術演算を使用する (Solidity 0.8+)

Solidity 0.8.0バージョン以降、全ての算術演算はデフォルトでオーバーフローとアンダーフローのチェックを行います。これはわずかなGasコストが増加します。

もし、あなたの演算がオーバーフローしないことを100%確信している場合(例えば、forループ内のi++で、iuintの最大値に達しないことが分かっている場合)、uncheckedコードブロックで囲むことができます。

節約のための書き方:

// 非常に確信がある場合に使用
unchecked {
    for (uint i = 0; i < length; i++) {
        // ... ここの`i++`はオーバーフローチェックが行われないため、よりGasを節約できます
    }
}

警告: これは車のABSをオフにして運転するようなものです。安全性については自己責任となります。間違った使い方をすれば、壊滅的な結果を招く可能性があります!

三、 アーキテクチャレベルでの「抜本的な最適化」

時には、どんなにコードの詳細を最適化しても、アーキテクチャの観点から考えることには及びません。

1. イベント(Events)を有効活用する

全てのデータをオンチェーンのstorageに保存する必要はありません。多くの場合、私たちは外部に何かが起こったことを「通知」したいだけです。

例: ユーザーが取引を完了しました。取引の全ての詳細をstorageに保存する必要があるでしょうか?必ずしもそうではありません。

節約のための書き方:

event Trade(address indexed user, uint amount, uint price);

function executeTrade(uint amount, uint price) external {
    // ... 取引ロジックを実行 ...

    // 取引の詳細を「ログ」として発行し、ストレージに保存しない
    emit Trade(msg.sender, amount, price);
}

オフチェーンのサービス(例えばウェブサイトのフロントエンド)はこれらのイベントをリッスンし、データを従来のデータベースに保存してクエリに利用できます。これにより、履歴を記録する目的を達成しつつ、膨大なGasを節約できます。

覚えておいてください: スマートコントラクトはあなたの「ステートマシン」であり、最も重要なコアの状態を維持することのみを担当します。イベントはあなたの「ブロードキャストシステム」であり、何が起こったかを世界に通知する役割を果たします。

まとめ:Gas節約の究極の奥義

  1. 保存を減らし、計算を増やす:ストレージは非常に高価であり、計算は比較的安価です。
  2. オンチェーンで検証、オフチェーンで計算:複雑な計算(ソート、データ処理など)はオフチェーンで実行し、結果と証明をオンチェーンに提出し、コントラクトには簡単な検証作業のみを行わせる。
  3. 最終奥義:オンチェーンに載せる必要がないものは、載せない!

これらの経験談が、より効率的で費用対効果の高いコントラクトを書くのに役立つことを願っています。マイニングは大変ですから、節約できるところは節約しましょう!快適なコーディングをお楽しみください!