Solidity ガス最適化の基本とテクニック

この記事は [DENSOアドベントカレンダー2024](https://qiita.com/advent-calendar/2024/denso) の17日目の記事です。

## はじめに
SolidityやEVM上でのガス最適化は、まず正確性・安全性を最優先した上で、どの箇所でどれだけ最適化するかを判断する必要があります。過剰な最適化は保守性や可読性を損ないうるため、計測とバランスが重要です。

## 最適化プロセス
1. **パフォーマンス測定**：`forge test --gas-report`などでガスコストを計測し、ボトルネックを特定します。
2. **最大効果の変更を選ぶ**：影響が大きい箇所（例：ループ内でのストレージアクセス）を優先します。
3. **コードリファクタリング**：冗長なコードや不要な機能を削除、データ型や処理手順を見直します。
4. **ユニットテストで正確さ確認**：最適化後も機能性や安全性が損なわれていないかチェックします。
5. **再測定と反復**：最適化が有効か再度計測し、必要に応じて微調整します。

## 基本的なガス削減技術

### 1. 不要なコード・機能を削除
使わない関数や変数、ライブラリはコードサイズとデプロイコストを増やします。まずは不要な要素を削るだけでもコスト削減が可能です。

### 2. ストレージアクセスの最小化
- **SSTORE**（ストレージ書き込み）は非常に高価です。  
  EIP-2200により、0→非0への変更は20,000ガス、非0→0は2,900ガス+リファンドの可能性など、複雑なコスト体系となっています。また、EIP-2929により初回アクセスはコールド（2100ガス相当）、2回目以降はウォーム（100ガス）とアクセス回数でコストが変化します。  
- **SLOAD**（ストレージ読み込み）はEIP-2929以前は200ガスでしたが、現在はコールドアクセス2100ガス、ウォームアクセス100ガスと定義されています。  
ループ内で頻繁にSLOAD/SSTOREを行わず、必要な値は一旦メモリかスタックに読み込んで使い回すことでコストを削減します。

### 3. メモリ対ストレージの使い分け
メモリはストレージより安価ですが、ストレージ→メモリの大量コピーはコスト増大を招くことがあります。ストレージポインタを使う、必要最小限だけ読み出すなどの工夫が有効です。ただしストレージポインタ使用時はバグに注意が必要です。

### 4. 冗長なチェックを避ける
テストでロジックが保証されているなら、不要な`require`等を削減できます。

### 5. リファンドの有効活用
SSTOREで値を0に戻すと（EIP-3529以前は15,000ガスまでのリファンドがあったが現在は減少）、トランザクション終端時にリファンドが発生します。  
ただしリファンドはトランザクション全体の1/5に制限（EIP-3529）されており、過度な依存は禁物です。

### 6. データ型とパッキング
`bytes32`は`string`よりコストが安い場合があります。  
小さな型を一つのストレージスロットに詰め込む（パッキング）ことでスロット数を減らし、SSTORE回数を削減します。

### 7. サイズが小さい型が必ずしも安価とは限らない
`uint8`などの小型型は内部でパディングされるため、単純に`uint256`より安いとは限りません。ただしパッキングで複数の小型型をまとめる場合は有効です。

### 8. 継承とストレージ
子コントラクトの変数は親コントラクトの後に続いて配置されます。ストレージレイアウトを理解し、継承を利用してパッキングを行うことでガス削減が可能な場合があります。

### 9. 関数可視性とパラメータ取り扱い
`external`関数でcalldataから直接読み込むことで、メモリコピーを削減できます。  
修飾子(modifier)はインライン展開されコードサイズ増大を引き起こす可能性があるため、必要なら関数呼び出しに変更すると良いでしょう。

### 10. イベント使用やストレージ回避
ストレージに保持する代わりにイベントログで記録できるデータは、ストレージコストを節約できます。パブリック変数をやたら定義せず、必要に応じて関数で返すなども考慮します。

### 11. ループの最適化
ループ内でSLOAD/SSTOREを繰り返さないように、事前にメモリやローカル変数に値を読み出して計算後に一度だけ更新します。

### 12. カスタムエラーの利用
`revert("message")`よりも`error CustomError()`を使うことでコードサイズとガスを削減可能（EIP-838, v0.8.4以降）。

### 13. ハッシュ関数の適材適所
`keccak256`、`sha256`、`ripemd160`は各々ガスコストが異なります。必要最小限の使用に留めます。

### 14. ライブラリの賢い利用
ライブラリはコードサイズ削減に有効ですが、呼び出しコストとのトレードオフを検討してください。

### 15. requireとassertの使い分け
`require`は実行時条件チェックに、`assert`は内部不変条件に使います。エラーメッセージを短くするとガス節約にもなります。

### 16. calldata有効活用
変更不要な引数をcalldataから直接参照することでメモリコピーコストを削減します。

### 17. コンパイラオプションとYulの活用
`solc --optimize --optimize-runs=...`や`-yul`, `-ir`オプションを使用してYul IRを調べ、さらなる低レベル最適化を検討します。

### 18. ビットマップ・バニティアドレス活用
ビット単位でフラグを圧縮する、先頭ゼロが多いアドレスを利用してストレージを節約するなどの工夫があります。

### 19. uncheckedブロック
オーバーフローチェック不要な箇所では`unchecked {}`を活用してガス削減可能です（Solidity 0.8以降、標準でオーバーフローチェックが有効）。

### 20. テスト戦略
ランダムな値や様々な条件下でテストを行い、予期せぬガス増やバグを早期発見します。

## ガス計測と可視化ツールの活用

### ガス計測ツール：forge
[foundry](https://book.getfoundry.sh/)の`forge test --gas-report`でテスト同時にガス使用量を確認できます。変更が効果的だったか即時にフィードバック可能です。

```bash
forge test --gas-report -vvvvv --watch
```

### ストレージ可視化ツール：sol2uml
[sol2uml](https://github.com/naddison36/sol2uml)を使ってストレージレイアウトを可視化し、変数の並び替えやパッキング改善点を特定します。

```bash
sol2uml storage -c GasContract src
```

## 参考記事・資料
- [shinonome氏によるgas最適化TIPS](https://blog.shinonome.io/gas-optimization-tips/)
- [関数名最適化ツール](https://emn178.github.io/solidity-optimize-name/)
- [関数の順序とガス最適化に関する記事](https://blog.emn178.cc/en/post/solidity-gas-optimization-function-name/)
- [wolflo/evm-opcodes/gas.md](https://github.com/wolflo/evm-opcodes/blob/main/gas.md)  
- 各種EIP（EIP-150, 161, 2200, 2929, 2930, 3529, 3541など）

## まとめ
ガス最適化はあくまで手段であり、正確性・安全性・可読性を損なわずに行う必要があります。  
計測（`forge test --gas-report`）と可視化（`sol2uml`）、そしてテストを重ねながら段階的な改善を行うことで、効率的で持続可能なガスコスト削減が実現できます。

---

本記事で示した手法や数値は、特定のハードフォーク後のEthereum仕様に基づくものであり、将来のEIPによる変更の可能性もあります。最新情報は公式EIPやEthereum公式ドキュメントを参照してください。