# [EIP1283] SSTOREのガス最適化の仕組みを理解しよう！

> EIP1283では、SSTORE（ストレージ書き込み）命令に関するガスメータリングの仕組みを見直す提案が行われています。

- URL: https://cardene.dev/articles/eb4eee35e5a2716f632a
- 公開日: 2025-07-13
- 更新日: 2025-07-13
- トピック: Blockchain
- タグ: ethereum, solidity, smartcontract, ブロックチェーン

## はじめに

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

https://amzn.asia/d/gxvJ0Pw

今回は、`SSTORE` によるストレージ変更のガス代を、実際の変更内容に応じて柔軟に最適化することで無駄なガス消費を減らす仕組みを提案している**EIP1283**についてまとめていきます！

:::message
以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。
:::

https://eips.ethereum.org/EIPS/eip-1283

他にも様々なEIPについてまとめています。

https://cardene.notion.site/ERC-EIP-2a03fa3ea33d43baa9ed82288f98d4a9?pvs=4

## 概要

**EIP1283**では、`SSTORE`（ストレージ書き込み）命令に関するガスメータリングの仕組みを見直す提案が行われています。
**EIP1283**は、既存のガスコスト体系が実際の実装と乖離している場合があることを踏まえ、より実態に即したガス消費量を設定することで、効率的なスマートコントラクトの開発と運用を可能にしようとするものです。

具体的には、ストレージの操作に対して「**純ガス計測（net gas metering）**」という考え方を導入し、あるストレージスロット（データの保存場所）に対して複数回書き込みを行う場合のガスコストを最適化します。
これにより、無駄なガス消費を抑えながら、同一トランザクション内で柔軟にストレージを利用できるようになります。

この提案は、過去に提案された**EIP1087**とは異なるアプローチを採用しており、さまざまなEVMの実装にとって互換性が高く、実装コストも低く抑えられる設計となっています。

## 動機

**EIP1283**が提案された背景には、現行の `SSTORE` 命令に対するガス課金方法が、すべてのEVM実装にとって必ずしも最適とは言えないという課題があります。
例えば、ストレージの一時的な変更や、同一トランザクション内で複数回にわたって同じスロットにアクセスするような処理に対して過剰なガスが課されるケースがありました。

**EIP1283**は、以下の3つの情報に基づいてガス計測を行います。

* ストレージスロットの「元の値（original value）」
* 現在の値（current value）
* ガスリファウンドのためのカウンター（refund counter）

これにより、実際のストレージの変化や処理の内容に即した、より合理的なガス計測が可能になります。
例えば、同じ関数呼び出しの中で再度ストレージを書き換えるような処理（再入防止ロックや、同一コントラクト内での一括送信など）においては、1回目の書き込みに比べて2回目以降のコストが不要に高くなることがありましたが、**EIP1283**によりそれが改善されます。

また、サブコール（内部呼び出し）と親のコールフレーム間で一時的な値のやりとりを行うようなケース（例えばエラーメッセージの伝達など）でも、ガス効率の改善が見込まれます。
こうした一時的なデータは、トランザクションが終了した時点で不要になるため、永続的な保存を前提としたガス体系では適切に処理できていませんでした。

このように、**EIP1283**では現実のユースケースを考慮しながら、より実用的かつ互換性のある形でガスメータリングの改善を目指しています。
これにより、開発者は無駄なガスを意識することなく、柔軟で効率的なスマートコントラクト設計を行うことが可能になります。

## 仕様

Ethereumの `SSTORE` 命令は、ストレージスロットへの書き込みを行う際にガスを消費しますが、その計算方法が非効率であるという指摘がありました。
**EIP1283**では、より現実的で柔軟なガス課金を実現するために、`SSTORE` におけるガス計測の仕様を再定義しています。

**EIP1283**では、ストレージの状態に応じてガス消費とリファウンド（返金）を細かく調整します。
これにより、スマートコントラクトが同じトランザクション内で複数回ストレージを書き換えるような場合でも、過剰なガスが発生しにくくなります。

### 用語の定義

**EIP1283**の理解には、3つの状態が重要です。

* 「元の値（original value）」
    * トランザクションが取り消された場合に復元されるべきストレージの値。
* 「現在の値（current value）」
    * `SSTORE` 命令を実行する直前のストレージの値。
* 「新しい値（new value）」
    * `SSTORE` 命令を実行した後に設定されるストレージの値。

この3つの値の関係性に基づいて、`SSTORE` 命令によるガス消費とリファウンドの条件が定義されます。

### 新しいガス課金ロジック

まず、現在の値と新しい値が同じである場合（つまり書き込みが実質的に不要な場合）、200ガスが差し引かれるだけの処理となります。
これは「No-Op（何もしない）」とみなされるケースです。

次に、現在の値と新しい値が異なる場合、以下のように場合分けされます。

#### 書き込み対象のスロットが初回変更である場合（元の値と現在の値が同じ）

この場合は、まだ実行中のトランザクションによって書き換えられていないスロットです。

元の値が0（初期状態）であれば、新規ストレージの書き込みとして20000ガスが課されます。
元の値が0以外（上書き）であれば、5000ガスが課されます。
また、新しい値が0であれば、15000ガスがリファウンドとして加算されます。

#### 書き込み対象のスロットがすでに変更済みである場合（元の値と現在の値が異なる）

このようなスロットは「**dirty（汚れている）**」とみなされ、ベースコストとして200ガスが課されます。
その上で、以下の条件に応じてリファウンドが調整されます。

* 元の値が0でなく、現在の値が0かつ新しい値が0以外
    * すなわち0から0以外に戻す場合、リファウンドとして記録されていた15000ガスを差し引きます。
* 新しい値が0（かつ現在の値は0ではない）
    * すなわち0以外から0にする場合、15000ガスをリファウンドとして加算します。

さらに、元の値と新しい値が一致する場合、すなわちスロットの値が元の状態に戻った場合には、「リセット」として以下のリファウンドが適用されます。

* 元の値が0の場合
    * 19800ガスがリファウンドに加算されます。
* 元の値が0以外の場合
    * 4800ガスがリファウンドに加算されます。

### リファウンドカウンターの仕様と実装上の注意点

リファウンドの扱いは従来と同様で、トランザクション全体で消費されたガスの半分までしか返金できません。
また、返金カウンターが負の値になることはありません。

ただし、リファウンドの管理方式によっては実装上の調整が必要です。

* トランザクション全体で1つのリファウンドカウンターを使用する実装では、カウンターは符号なし（unsigned）のままで問題ありません。
* 各実行フレーム（call frame）ごとにリファウンドカウンターを管理し、終了時に親フレームに統合するような実装では、一時的に負の値が発生する可能性があるため、符号付き（signed）として扱う必要があります。

このように、新しい `SSTORE` のガス仕様は、実行状況に応じた細かい条件分岐を導入することで、より合理的かつ効率的なガス計測を実現しようとしています。
特にストレージ操作が頻繁なコントラクトにおいては、ガスコストの最適化に大きく寄与します。

### SSTOREの新しいガス課金方式

このEIPでは、SSTORE（ストレージ書き込み）に関するガス課金を3つの分類に分けて定義し、それぞれの状態に応じたガスの使われ方を明示しています。従来よりも明確に状態遷移を定義することで、最適なガス消費とリファウンド（返金）のロジックを実現します。

### 3つのSSTORE状態タイプ

まず、`SSTORE` の動作は以下の3つの状態タイプに分類されます。

- **No-op（ノーオペレーション）**
    これは、ストレージに対して書き込みが不要な場合です。
    すなわち、現在の値と新しい値が等しいときに該当します。
    ガスは200だけ差し引かれ、値の変更も内部的な処理も発生しません。

- **Fresh（フレッシュ状態）**
    これは、対象のストレージスロットが現在のトランザクション内で初めて変更されるか、もしくは元の値にリセットされた状態です。
    このとき、元の値と現在の値が等しく、かつ新しい値は異なる場合が該当します。

- **Dirty（ダーティ状態）**
    これは、対象のストレージスロットがすでに変更済みであり、元の値と現在の値が異なる場合に該当します。
    この状態では、すでに変更が反映されたあとに再度変更が行われることになります。

この3つの状態タイプにより、`original value`（元の値）、`current value`（現在の値）、`new value`（新しい値）のすべての組み合わせをカバーできます。

#### Fresh から Dirty への遷移とガス処理

ストレージスロットへの最初の変更は常にFreshから始まります。
その後、さらに値が変更されるとDirty状態になります。
Fresh状態では、従来通りのガス（20000または5000）が課されます。

一方、Dirty状態では一律200ガスが課されますが、このときリファウンドの扱いが重要になります。
例えば、現在の値が0であったにも関わらず、新しい値が0以外になる場合は、以前付与したリファウンドをキャンセルする必要があります。
逆に、新しい値が0になる場合は、リファウンドを追加します。

注意すべき点として、全てのストレージはFresh状態から始まるため、誤って「**返金をしていないのに返金を取り消す**」といった矛盾は発生しません。

#### ステート遷移とガスの一覧表

![state.png](/images/qiita-dc8e64ae-2773-400e-b1a7-d28098bc2648.webp)

ステート遷移をわかりやすくするために、元の値（original）、現在の値（current）、新しい値（new）を軸にしたガス課金表が用意されています。

##### 元の値が0の場合

| new value | current = original (0) | current ≠ original          |
| --------- | ---------------------- | --------------------------- |
| ≠0        | 20000 gas（新規書き込み）      | 200 gas（Dirty）              |
| 0         | 200 gas（No-op）         | 200 gas, 19800 refund（リセット） |

##### 元の値が0以外の場合

| new value = original | current = original     | current ≠ original    | current = 0            |
| -------------------- | ---------------------- | --------------------- | ---------------------- |
| same（original）       | 200 gas                | 200 gas, 4800 refund  | 200 gas, -10200 refund |
| ≠ original, ≠ 0      | 5000 gas               | 200 gas               | 200 gas, -15000 refund |
| 0                    | 5000 gas, 15000 refund | 200 gas, 15000 refund | 200 gas                |

このように、状態の組み合わせに応じてガスが課金・返金される仕組みになっています。
特に注目すべきは、「リセット」による返金や、非ゼロ→ゼロへの遷移による返金が高く設定されていることです。

## 補足

**EIP1283**では、`SSTORE` 命令におけるガスコストの計算を見直すことで、**EIP1087**や**EIP1153**が目指した「一時的なストレージの効率化」に近い効果を、よりシンプルな形で実現しようとしています。

従来のEIP-1087では、「ダーティマップ（dirty map）」と呼ばれるストレージの変更履歴を追跡する仕組みが必要でした。この仕組みは、すべてのストレージの変更を明示的に記録する必要があるため、一部のクライアント実装においてはメモリや処理時間のコストが大きく、実装が煩雑になるという問題がありました。

本EIPは、そうした複雑な仕組みを導入せずに、以下の3つの情報のみでガス計算を完結させることが可能です。

* 元の値（トランザクションが取り消された場合に戻る値）
* 現在の値（SSTOREを呼び出す前の値）
* 新しい値（SSTOREで書き込む値）

この3点があれば、すべての状態変化とガスの返金処理が追跡可能であり、追加の構造体やマッピングを導入せずに済みます。

特にEIP-658以降、多くのEVMクライアントでは、RLPエンコードなどの処理を排除して、メモリ上でストレージ変更をトラッキングするような効率的な手法が採用されています。そういった実装では、変更の内容をすべて逐一確認することはむしろコスト高になる可能性があり、本EIPのように最小限の情報で状態を把握できる方式がより適しています。

さらに、本提案による新しいガス計測方式は、**現在の仕様よりガスが多くかかるケースは一切存在しない**という特性を持ちます。つまり、すべてのケースにおいてガス消費は現状と同等か、むしろ効率化されます。

また、このEIPは、EIP-1087が想定していた一時ストレージの使用例すべてをカバーできます。EIP-1087を簡単に実装できるクライアントであれば、このEIPの仕様も同様に実装可能です。一部のクライアントにおいては軽微なリファクタリングが必要になる可能性がありますが、実行時の追加処理やメモリコストは発生しません。

### EIP-1087とのガス消費比較と例

EIP-1087で提示された使用例に対して、本EIPがどのようにガスコストを削減できるかについて、以下のような検証結果があります。

* 空のストレージを持つコントラクトが、スロット0に1を設定し、再び0に戻す場合のガス消費は、
  **20000（初回書き込み）+ 200（再書き込み）− 19800（リセットによる返金）＝ 400ガス**
  となり、極めて効率的です。

* スロット0を5回連続でインクリメントするような処理では、
  **20000（初回書き込み）+ 5 × 200（以降の書き換え）＝ 21000ガス**
  となります。これは現在の仕様とほぼ同等ですが、明確に状態が定義されているため、開発者の予測可能性が向上します。

* 口座A→B→Cへのバランス転送（すべての口座が初めから非ゼロ残高を持っている場合）においては、
  **5000 × 3（3回の更新）+ 200（追加の変更）− 4800（リセットによる返金）＝ 10400ガス**
  となります。これも現行方式と比較してガス効率の良さが証明される例です。

## 互換性

**EIP1283**の実装にはハードフォークが必要です。
ガスコストの増加は予想されず、多くのコントラクトでガスコストが削減されます。

## テスト

**EIP1283**では、`SSTORE` 命令の新しいガス計測方式が提案されていますが、それが現実にどのように作用するのかを確認するため、複数のテストケースが用意されています。
合計17のケースがあり、そのうち15件は連続する2回の `SSTORE` 操作によるもの、残り2件は3回の `SSTORE` 操作を通じて、状態の「**リセット → 再書き込み**」が正しく処理されるかを確認するためのものです。

### テストケース一覧表

各テストケースでは、バイトコード（SSTOREを行うEVM命令列）、消費されたガス量、リファウンドされたガス量、および各操作ごとのストレージ値（初期値、1回目、2回目、3回目）が記録されています。

| バイトコード                           | 使用ガス  | リファウンド | 初期値 | 1回目 | 2回目 | 3回目 |
| -------------------------------- | ----- | ------ | --- | --- | --- | --- |
| 0x60006000556000600055           | 412   | 0      | 0   | 0   | 0   |     |
| 0x60006000556001600055           | 20212 | 0      | 0   | 0   | 1   |     |
| 0x60016000556000600055           | 20212 | 19800  | 0   | 1   | 0   |     |
| 0x60016000556002600055           | 20212 | 0      | 0   | 1   | 2   |     |
| 0x60016000556001600055           | 20212 | 0      | 0   | 1   | 1   |     |
| 0x60006000556000600055           | 5212  | 15000  | 1   | 0   | 0   |     |
| 0x60006000556001600055           | 5212  | 4800   | 1   | 0   | 1   |     |
| 0x60006000556002600055           | 5212  | 0      | 1   | 0   | 2   |     |
| 0x60026000556000600055           | 5212  | 15000  | 1   | 2   | 0   |     |
| 0x60026000556003600055           | 5212  | 0      | 1   | 2   | 3   |     |
| 0x60026000556001600055           | 5212  | 4800   | 1   | 2   | 1   |     |
| 0x60026000556002600055           | 5212  | 0      | 1   | 2   | 2   |     |
| 0x60016000556000600055           | 5212  | 15000  | 1   | 1   | 0   |     |
| 0x60016000556002600055           | 5212  | 0      | 1   | 1   | 2   |     |
| 0x60016000556001600055           | 412   | 0      | 1   | 1   | 1   |     |
| 0x600160005560006000556001600055 | 40218 | 19800  | 0   | 1   | 0   | 1   |
| 0x600060005560016000556000600055 | 10218 | 19800  | 1   | 0   | 1   | 0   |

### テストケースの目的と観察結果

テストでは以下のようなケースを網羅しています。

* 値の変更がない場合（No-op）には、最低限のガスのみが使用されて返金も発生しません。
* 値が初めて設定される場合（Fresh）では、20000ガスが消費されます。
* 値が元に戻される場合（Reset）では、リファウンドが発生します（最大19800ガス）。
* 値が連続で変更された場合（Dirty）では、200ガスごとの追加消費で済みます。

例えば、以下のようなパターンが確認できます。

* `0 → 1 → 0` の変更（スロットを一度セットしてリセット）：19800ガスの返金が発生。
* `1 → 0 → 1` の変更（元の値に戻して再度設定）：リファウンドが維持され、再設定時のガスコストが最小限になる。
* `1 → 0 → 0`（値が元のゼロに戻り続ける）：リファウンドは1回分（15000ガス）しか発生せず、それ以上は追加されない。

### 数学的証明の概要

**EIP1283**では、これらのガス課金が仕様通り正当であることを数学的に証明しています。
証明では、元の値が0と0以外の2つの場合に分けて、それぞれ次の条件が成立することを示します。

#### 元の値がゼロの場合

* 最終値がゼロのままなら、全ての操作に対して200ガス × N（操作回数）が課金される。
* 最終値が0以外になった場合、最初の書き込みに20000ガス、その後の変更ごとに200ガスが追加される。

#### 元の値が0以外の場合

* 最終値が元と同じ場合
    * 200ガス × N
* 最終値が0の場合
    * 5000ガス − 15000ガス（返金）+ 200ガス × (N − 1)
* 最終値が異なる0以外の値になった場合
    * 5000ガス + 200ガス × (N − 1)

帰納法によって、すべてのステップでこれらの式が一貫して成り立つことが示されており、これは仕様として非常に整合性の高い設計であることを裏付けています。

## 引用

Wei Tang (@sorpaas), "EIP-1283: Net gas metering for SSTORE without dirty maps," Ethereum Improvement Proposals, no. 1283, August 2018. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-1283.

## 最後に

今回は「`SSTORE` によるストレージ変更のガス代を、実際の変更内容に応じて柔軟に最適化することで無駄なガス消費を減らす仕組みを提案している**EIP1283**」についてまとめてきました！
いかがだったでしょうか？

質問などがある方は以下のTwitterのDMなどからお気軽に質問してください！

[Twitter @cardene777](https://twitter.com/cardene777)

他の媒体でも情報発信しているのでぜひ他も見ていってください！

https://chaldene.net/

https://zenn.dev/heku

https://cardene.substack.com/

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58

https://cardene.notion.site/ERC-EIP-2a03fa3ea33d43baa9ed82288f98d4a9?pvs=4
