スマートコントラクトを実行するには「ガス」と呼ばれる手数料が必要です。ガス代はEthereumネットワークの混雑状況やコントラクトの処理の複雑さによって変動します。ガス代を意識せずにコントラクトを作成すると、ユーザーにとって利用しにくいものになってしまう可能性があります。
このセクションでは、Solidityでスマートコントラクトを書く際に、ガス代を節約するための基本的なテクニックを学びます。効率的なコードは、ガス代を節約し、より良いユーザー体験を提供します。
ガスとは何か?
Ethereumネットワーク上で行われる全ての操作(トランザクションの発行、コントラクトの実行など)には、計算コストがかかります。このコストを測る単位がガスです。
ユーザーはトランザクションを実行するために、ガス代(Gas Fee)をETHで支払う必要があります。ガス代は以下の計算式で決まります。
ガス代 = 使用ガス量 (Gas Used) × ガス価格 (Gas Price)
ガス価格はネットワークの需要と供給によって変動します。コントラクト開発者は、使用ガス量をできるだけ少なくする(=ガスを最適化する)ことで、ユーザーが支払うガス代を抑えることができます。
注意: ガス最適化は重要ですが、コードの可読性やセキュリティを犠牲にしてはいけません。バランスが大切です。
基本的なガス節約テクニック
ガスを節約するための基本的な方法をいくつか紹介します。
1. データ型の選択
Solidityでは様々なデータ型を使用できますが、型のサイズによってガス消費量が異なります。
- 適切なサイズの整数型を使う:
uint256
は256ビットの整数を格納できますが、もし値がそれほど大きくならないことが分かっているなら、uint128
,uint64
,uint32
,uint8
など、より小さい型を使う方がガス効率が良い場合があります。特に、構造体やストレージ内で隣接して宣言された場合、複数の小さな変数が1つのストレージスロットにパックされ、ガスを節約できることがあります。(ただし、常にパックされるとは限らず、計算時の型変換で逆にガスが増える場合もあるため注意が必要です。) bytes
vsstring
: 固定長のデータにはbytes1
からbytes32
を使うのが効率的です。可変長のデータの場合、bytes
はstring
よりも一般的にガス効率が良いとされています(特にオンチェーンでの操作)。- 定数とイミュータブル: コンパイル時またはデプロイ時に値が決まり、その後変更されない変数は
constant
またはimmutable
として宣言します。これにより、実行時のストレージアクセスが不要になり、ガスが大幅に節約できます。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract GasOptimizationDataTypes { // constant はコンパイル時に値が確定 uint256 public constant MAX_SUPPLY = 10000; // immutable はコンストラクタで一度だけ設定可能 address public immutable owner; // 小さい型を使うことでパッキングされる可能性がある struct Player { uint64 id; // 64ビット uint64 score; // 64ビット uint128 registrationTime; // 128ビット (合計256ビットで1スロットに収まる可能性) } mapping(uint256 => Player) public players; constructor() { owner = msg.sender; } // stringよりもbytesの方がガス効率が良い場合がある bytes public dataBytes; function setDataBytes(bytes memory _data) public { dataBytes = _data; }
}
2. ストレージ、メモリ、キャルデータの使い分け
変数をどこに格納するかは、ガス消費に大きく影響します。
種類 | 説明 | コスト | 主な用途 |
---|---|---|---|
ストレージ (Storage) | ブロックチェーン上に永続的に保存される状態変数。 | 非常に高コスト | コントラクトの状態(残高、所有者など) |
メモリ (Memory) | 関数実行中のみ存在する一時的な変数。 | 比較的安価 | 関数内の計算、一時的なデータの保持 |
キャルデータ (Calldata) | 外部関数呼び出し時の引数データ領域。読み取り専用。 | 最も安価 | 外部からの関数引数(変更しない場合) |
最適化のヒント:
- 関数内でストレージ変数を複数回読み書きする場合は、一度メモリ変数にコピーしてから操作し、最後にストレージに書き戻す方がガス効率が良い場合があります。
- 外部関数の引数で、関数内で変更する必要がない参照型(配列や構造体など)は、
memory
ではなくcalldata
を指定するとガスを節約できます。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract StorageMemoryCalldata { uint256[] public numbers; // ストレージへの書き込みは高コスト function addNumberStorage(uint256 _newNumber) public { numbers.push(_newNumber); // ストレージへの書き込み } // Calldataを使う例 (読み取り専用) function sumCalldata(uint256[] calldata _values) public pure returns (uint256) { uint256 total = 0; // _values は calldata なので変更不可、読み取りは低コスト for (uint i = 0; i < _values.length; i++) { total += _values[i]; } return total; } // メモリを使う例 uint256 public totalSum; function processNumbersMemory() public { // ストレージ配列をメモリにコピー uint256[] memory localNumbers = numbers; uint256 sum = 0; uint256 len = localNumbers.length; // lengthの読み取りもストレージアクセスなのでキャッシュ // メモリ上で計算 for (uint i = 0; i < len; i++) { sum += localNumbers[i] * 2; // メモリ上の操作は比較的安価 } // 最後にストレージに書き戻す (この例では計算結果を別の変数に保存) totalSum = sum; // ストレージへの書き込み }
}
3. ループの最適化
ループ処理、特にストレージ内の配列やマッピングに対するループは、ガスを大量に消費する可能性があります。
- ループ回数を最小限に: 可能であれば、ループを使わない設計を検討します。
- ストレージアクセスを減らす: ループ内で毎回ストレージにアクセスするのではなく、前述のようにメモリに必要なデータをコピーしてからループ処理を行います。ループの境界値(例: 配列の長さ)もループ前にメモリ変数にキャッシュします。
- 無制限ループの回避: ユーザー入力によってループ回数が決まるような処理は、意図せず大量のガスを消費する(あるいはガスリミットを超える)可能性があるため、慎重に設計するか、ループ回数に上限を設けるなどの対策が必要です。
4. コード構造と関数の可視性
external
vspublic
: 関数がコントラクト内部から呼び出される必要がない場合は、public
ではなくexternal
を使用します。external
関数の引数はcalldata
として扱われるため、ガス効率が向上します。view
とpure
関数の活用: 状態変数を変更しない関数はview
(読み取りのみ) またはpure
(状態を読み取りもしない) としてマークします。これらの関数は、外部から呼び出された場合(トランザクションを送信しない場合)、ガスを消費しません。- 短いエラーメッセージ:
require
やrevert
で使用するエラーメッセージの文字列が長いほど、デプロイ時や実行時のガス代が増加します。簡潔なメッセージにするか、Solidity 0.8.4以降で導入されたカスタムエラーを使用することを検討してください。 - 不要なコードの削除: 使われていない関数や変数(デッドコード)は削除します。これにより、デプロイ時のガス代が削減されます。
- ゼロアドレスチェック: アドレス型の引数を受け取る関数では、
address(0)
でないことをrequire
でチェックすることが一般的ですが、このチェックにもガスがかかります。本当に必要な場合のみ行います。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract FunctionVisibility { uint256 public value; // 外部からのみ呼び出される場合は external が効率的 function updateValueExternal(uint256 _newValue) external { require(_newValue != 0, "Value cannot be zero"); // 短いエラーメッセージ value = _newValue; } // 内部からも呼び出す可能性がある場合は public function getValuePublic() public view returns (uint256) { return value; } // 状態を読み取らない純粋な計算 function add(uint256 a, uint256 b) public pure returns (uint256) { return a + b; } // 内部処理用の private 関数 function _internalLogic() private pure returns (bool) { // ... 何らかの計算 ... return true; } // カスタムエラー (Solidity 0.8.4+) error InvalidAmount(uint256 amount); function checkAmount(uint256 _amount) public pure { if (_amount == 0) { revert InvalidAmount({amount: _amount}); // カスタムエラーはガス効率が良い } }
}
発展的なトピックとツール
基本的な最適化に加えて、さらにガス効率を高めるための高度なテクニックや役立つツールがあります。
- アセンブリ (Yul): Solidityコード内で低レベルな操作を直接記述できるインラインアセンブリ(Yul)を使うと、特定の処理でガスを大幅に削減できる場合がありますが、複雑でエラーが発生しやすいため、深い理解が必要です。
- ガス効率の良いライブラリ: OpenZeppelin Contracts などの標準ライブラリは、多くの場合、ガス効率が考慮されて実装されています。車輪の再発明を避け、信頼できるライブラリを活用しましょう。
- ガス見積もりツール:
- Remix IDE: コードを実行する前に、ガス消費量のおおよその見積もりを表示します。
- Hardhat Gas Reporter: Hardhat環境でテストを実行する際に、各関数のガス消費量をレポートするプラグインです (hardhat-gas-reporter)。
- Solidityコンパイラの最適化: Solidityコンパイラにはオプティマイザが組み込まれており、コンパイル時にコードのガス効率を高めようとします。Hardhatなどのツールでコンパイラ設定の
optimizer
を有効にすることができます。runs
パラメータで使用頻度に応じた最適化レベルを指定できます。
// hardhat.config.js の例 (一部抜粋)
module.exports = { solidity: { version: "0.8.20", settings: { optimizer: { enabled: true, runs: 200 // このコントラクトが約200回実行されると仮定した場合の最適化 } } }, gasReporter: { // hardhat-gas-reporter の設定例 enabled: true, currency: 'JPY', // ガス代を日本円で表示 gasPrice: 21 // ガス価格を仮定 (gwei) }
};
まとめ
ガス最適化は、Ethereum上で動作するスマートコントラクト開発において非常に重要な側面です。基本的なテクニックを適用するだけでも、ガス消費量を大幅に削減できることがあります。
- 適切なデータ型を選択する (
constant
,immutable
, サイズ) - ストレージアクセスを最小限にする (
memory
,calldata
の活用) - ループ処理を慎重に行う
- 関数の可視性 (
external
) や修飾子 (view
,pure
) を正しく使う - エラーメッセージや不要なコードを整理する
- ガス見積もりツールやコンパイラの最適化機能を活用する
ただし、最適化にこだわりすぎるあまり、コードが複雑になりすぎたり、セキュリティ上のリスクを生み出したりしないよう注意が必要です。常に可読性、安全性、効率性のバランスを考慮しながら開発を進めましょう。
次のステップでは、スマートコントラクトのセキュリティについて学んでいきます。