---
id: 23
title: "三種類のストレージを混同しない：Memory、Storage、Transient Storage"
slug: 0-5-three-kinds-of-storage
date: 2026/09/22
summary: 同じ 32 バイトのデータでも、メモリ、ストレージ、一時ストレージのどこに置くかでガスコストは二桁変わることがあり、生存期間もまったく異なります。本稿は帰属、寿命、価格設定という三つの軸で三種類のストレージを分解し、再入ロック、一時配列、状態変数をそれぞれどこに置くべきかを説明します。
keywords: EVM Memory,EVM Storage,Transient Storage,EIP-1153,SSTORE
heroImage: /images/articles/photos/0-5-three-kinds-of-storage.jpg
---

再入ロックをストレージに書くと、コールドスロットを初めて 0 から 1 へ書き込む時点で、コールドアクセスの追加料金 2100 と Gsset の 20000 を支払い、同じトランザクション内で 0 に書き戻すとさらに 100 を支払います。グロス消費は約 22200 ガスです。この書き込みは同時に 19900 ガスの返金を生みますが、返金の上限はトランザクション全体の消費の 1/5 です。これを 1 つの transient 変数に置き換えると、2 回の書き込みがそれぞれ 100 ガスで済み、返金カウンタにも依存しません。機能はまったく同じで、グロスのコストは二桁程度違います。

EVM には三種類の書き込み可能なデータ領域があります。メモリ（memory）、永続ストレージ（storage）、そして一時ストレージ（transient storage、EIP-1153）です。いずれも 32 バイト語で読み書きし、コントラクト内で代入できますが、帰属する単位、生存期間、価格設定の規則はそれぞれ異なります。置き場所を間違えても通常はエラーにならず、状態が想定外に消えるか、ガス請求が 10 倍になるだけです。答えるべき問いは、三種類のストレージがそれぞれ誰に帰属し、どれだけ生き、どの規則で価格設定され、何を担うのに適しているかです。

## ライフサイクル：フレーム、アカウント、トランザクションは三つの異なる尺度

メモリは実行フレーム（execution frame）に帰属します。CALL、DELEGATECALL、STATICCALL、CREATE のいずれかで入るコンテキストが 1 つのフレームで、フレームに入った時点でメモリはゼロから始まり、フレームが戻るかロールバックすると丸ごと破棄されます。親フレームのメモリは子フレームからは見えず、子フレームのメモリが親フレームへ返されることもありません。2 つのフレーム間でデータを渡すには、calldata と returndata を経由するしかありません。

ストレージはアカウントに帰属します。256 ビットのスロットから 256 ビットの値へのマッピングとしてアカウントの下にぶら下がり、書き込まれた内容はアカウントの storage trie に入ってワールドステートの一部となり、トランザクションをまたぎブロックをまたいで長期に存在します。ゼロ値は trie に書き込まれないため、スロットをゼロにすると対応するノードが取り除かれます。この点が返金規則の設計を直接決めています。

一時ストレージもアカウントに帰属しますが、スコープは 1 つのトランザクションです。同じトランザクション内では、そのアカウントのすべてのフレームが同じ一時ストレージを共有します。内側の呼び出しが書き込んだ値を、外側の呼び出しが読み取れます。フレームがロールバックすると、そのフレームの範囲内の書き込みも一緒にロールバックされ、挙動はストレージと一致します。一方、フレームが正常に戻った場合はロールバックされず、この点はメモリと逆です。トランザクションが終わると、すべての一時ストレージは無条件にゼロに戻されます。帰属規則にはもう 1 つ例外があります。DELEGATECALL と CALLCODE の一時ストレージは呼び出し側（命令を発行したコントラクト）に帰属し、CALL と STATICCALL は呼び出される側に帰属します。

三つの尺度はこう覚えられます。メモリはフレーム単位、ストレージはアカウント＋永続単位、一時ストレージはアカウント＋トランザクション単位です。

| 観点 | メモリ | ストレージ | 一時ストレージ |
|------|--------|---------|-------------------|
| 帰属 | 実行フレーム | アカウント | アカウント |
| 生存期間 | フレーム進入時に作成、フレーム終了で破棄 | 永続、ワールドステートに書き込まれる | トランザクション終了でゼロに戻る |
| アドレス指定 | バイトアドレス、32 バイト語単位で拡張 | 256 ビットスロット | 256 ビットスロット |
| 内部呼び出し間の共有 | 共有しない | 共有する | 同一アカウントのすべてのフレームで共有 |
| フレームのロールバック | フレームとともに破棄 | そのフレームの書き込みをロールバック | そのフレームの書き込みをロールバック |
| 1 回の書き込みコスト | 3 ガス＋拡張費 | 100〜20000＋コールドアクセス費 | 固定 100 ガス |

## メモリ：語単位で拡張し、拡張コストは二次的

メモリはバイト単位でアドレス指定しますが、割り当ては 32 バイト語の粒度で行います。まだ触れていない語にアクセスすると拡張が発生し、拡張費は総占有量に応じて計算されます。式は C_mem(a) = 3a + ⌊a² / 512⌋ で、a は語数です。実際に差し引かれる額は、拡張後の C_mem から拡張前の C_mem を引いた値になります。MSIZE は増える一方で、フレーム内で割り当て済みのメモリを解放することはできません。

この式の前半は線形項、後半は二次項です。a < 23（つまり 704 バイト）では二次項が切り捨てられて 0 になり、コストは穏やかに見えます。この点を越えると増加が速くなります。以下の 2 つの絶対値はいずれもイエローペーパーの式 (328) で推算したもので、基準は 1 つのフレームが空のメモリから一度に拡張する場合で、MLOAD、MSTORE の基礎コストは含みません。32 KB のメモリは a = 1024 に対応し、拡張費は 3 × 1024 + 1024² / 512 = 5120 ガスです。1 MB のメモリは a = 32768 に対応し、拡張費は 98304 + 2097152 ≈ 219 万ガスです。1 つのフレームがメモリの拡張だけで数百万ガスを消費しうることは、通常、フレーム内で大きなバッファを抱えることの硬い制約になります。

MLOAD と MSTORE の基礎コストは 3 ガス（Gverylow）で、拡張費は別途かかります。一度も書いていないメモリを読むと 0 が返りますが、新たに触れた語については拡張費を支払う必要があります。割り当てという動作自体はすでに起きているからです。これが、高いアドレスへまばらに書き込むと特に高くつく理由でもあります。遠いオフセットに値を 1 つ書くだけで、その間のすべての語が割り当て済みとして課金されます。

一時配列、ABI のエンコード／デコード用バッファ、ハッシュ関数の入力はすべてメモリに置かれます。Solidity の memory 変数、memory 配列、memory 構造体もこの領域に落ちます。次のコードは長さを一度に確保し、ループの中で新たな語に繰り返し触れるのを避けます。

```solidity
// 一度に n 語まで拡張し、以降の書き込みでは拡張費が発生しない
uint256[] memory buf = new uint256[](n);
for (uint256 i = 0; i < n; ++i) {
    buf[i] = i;
}
```

境界は明確です。メモリが CALL を通じて子フレームへ渡すのは内容のコピーで、ポインタ自体は現在のフレーム内でのみ有効です。フレームをまたいで共有する中間値をメモリに置くのは、子フレームが親フレームのメモリを見られるという前提に立つことですが、その前提は成り立ちません。

実際のエンジニアリングでは、メモリを迂回する選択のほうがよく見られます。大きなデータは calldata で受け取り、returndata で返せば、コストはバイト単位で課金され、このフレームのメモリも占有しません。フレーム内で繰り返し読み書きする必要がある場合や、ハッシュ入力を組み立てる必要がある場合にだけ、メモリへ展開する価値があります。このトレードオフは、多くのコントラクトが計算を外部スクリプトや子呼び出しに置き、戻り値で集約することで、単一フレーム内のバッファを小さく保つ理由を説明します。

## ストレージ：アカウントの storage trie に書き込み、書き込みは読み取りより一桁高い

ワールドステートでは各アカウントに 1 本の storage trie がぶら下がり、スロットは 256 ビットのキー、値は 256 ビットの語で、ゼロ値はツリーに入りません。読み取りは SLOAD を使います。EIP-2929（ベルリン・アップグレード、ブロック 12,244,000、2021 年 4 月 15 日）が導入したコールド／ホットアクセスの仕組みでは、ある (アドレス, スロット) の組への初回アクセスはコールドアクセスに分類され、2100 ガス（Gcoldsload）を取られます。今回のトランザクション内ですでにアクセスしたものはホットアクセスで、100 ガス（Gwarmaccess）です。コールド／ホットの集合はトランザクション・スコープで、スコープがロールバックすると集合もロールバックします。

書き込みは SSTORE を使い、コストは EIP-2200 の純計量規則で決まります。判断には三つの値を見ます。トランザクション開始時点でのスロットの元の値、現在の値、これから書き込む新しい値です。ベルリン以降の定数は次のとおりです。コールドスロットは追加で 2100 を取られます。元の値が現在の値と等しい場合（このトランザクションでまだそのスロットを変更していない場合）、0 から非ゼロへ書くと Gsset = 20000、非ゼロから別の値へ、または 0 へ書くと Gsreset = 2900 です。スロットがこのトランザクションですでに変更されている場合（元の値が現在の値と等しくない場合）は、ホットアクセスの 100 ガスを 1 回だけ取られます。新しい値が現在の値と等しい空の書き込みも 100 です。

返金規則は EIP-3529（ロンドン・アップグレード、ブロック 12,965,000、2021 年 8 月 5 日）で厳しくなりました。非ゼロをゼロへ書き換えたときの返金は 15000 から 4800 へ下がり（SSTORE_RESET_GAS と ACCESS_LIST_STORAGE_KEY_COST の合計）、SELFDESTRUCT の返金は廃止され、1 トランザクションの返金総額の上限は gas_used // 5 に抑えられました。元の値が 0 で、このトランザクション内でいったん非ゼロを書いてから 0 に書き戻すパターンは依然として 19900 ガスの返金を生みます（20000 から 100 を引いた値）が、同じく 1/5 の上限に縛られます。

| 操作（ベルリン / ロンドン口径） | Gas |
|--------------------------|-----|
| SLOAD、コールド / ホットアクセス | 2100 / 100 |
| SSTORE、元の値が現在の値と等しく、0 から非ゼロへ書き込み | 20000、コールドアクセス 2100 を追加 |
| SSTORE、元の値が現在の値と等しく、非ゼロから非ゼロまたはゼロへ変更 | 2900、コールドアクセス 2100 を追加；ゼロに変えると返金 4800 |
| SSTORE、スロットがこのトランザクションですでに変更済み | 100 |
| 再入ロック 0 → 1 → 0（同一トランザクション） | グロス 22200、返金 19900 |

トランザクションをまたいで保持したいものはストレージに置きます。残高、所有権、設定、累積カウンタなどです。代価は二層あり、一つはガス、もう一つは状態の膨張です。すべてのフルノードがこれらのスロットを長期に保存しなければなりません。返金は「0 に書き戻せば無料」という誤解を招きやすいですが、実際の口径はこうです。返金はトランザクション終了後にのみ精算され、総消費の最大 20% しか相殺できません。30000 ガスしか消費しないトランザクションは、理論上 6000 ガスの返金しか受け取れません。

## 一時ストレージ：内部呼び出しをまたいで有効、トランザクション終了で消去

EIP-1153 はカンクン・アップグレード（ブロック 19,426,587、2024 年 3 月 13 日）で TLOAD（0x5c）と TSTORE（0x5d）を導入しました。アドレス指定の方法は SLOAD、SSTORE と同じで、32 バイトのアドレスが 32 バイトの値を指します。どちらも 1 回の操作の固定コストは 100 ガスで、コールド／ホットの区別も返金もなく、将来の消去のためにコストを確保しておく必要もありません。仕様上、決してディスクに書かれないからです。

挙動のうちストレージと異なる点は三か所に集中します。時間の尺度では、トランザクションが終わるとゼロに戻り、値が永続構造へシリアライズされることはありません。ロールバックのセマンティクスでは、フレームのロールバックがそのフレームの書き込みをロールバックし、これはストレージと一致し、メモリとは異なります（メモリはフレームが戻るかロールバックすると丸ごと破棄されます）。コンテキストの制限では、TSTORE は STATICCALL の中で例外を投げ、TLOAD は許されます。また EIP-1153 は、EIP-2200 が SSTORE に課す制限を明示的に免除しています。TSTORE は gasleft が 2300 を超えることを要求しません。

この設計は「フレーム間通信」を直接狙ったものです。EIP-1153 以前、コントラクト間で一時的な状態を渡すには、CALL の引数と戻り値を使うか（途中の信頼できないコントラクトが改ざんしうる）、ストレージへの書き込みを使うか（高価で、返金に依存する）のどちらかでした。返金が EIP-3529 で gas_used の 1/5 に抑えられて以降、小さなトランザクションではほぼコストを回収できません。0 → 1 → 0 のロック書き込みは返金カウンタに 19900 ガスを積み上げるので、gas_used // 5 の上限から逆算すると、返金を満額受け取るにはトランザクション全体で約 99500 ガスが必要です。この 99500 は EIP-3529 の上限式から推算したトランザクション全体の gas_used であり、EIP 本文が示した数値ではありません。EIP-1153 の本文は別の角度から著者の見積もりを示しており、原文では、再入ロックの返金を満額受け取るには取引が「その他の操作」に約 80k ガスを使う必要があると述べています。二つの数字は口径が異なりますが互いによく合います。99500 からこのロック自身のグロス消費である約 22200 ガスを引くと約 77300 になり、著者が「その他の操作」に置いた 80k の見積もりと同じ桁です。一時ストレージは返金カウンタに関与しないため、このしきい値を回避します。

再入ロック、単一トランザクションの承認、コールバック終了時の残高バランス検査、プロキシ・コントラクトが下流へメタデータを渡す処理などは、ここに置くのに適しています。Solidity は 0.8.28 以降で transient 値型の状態変数をサポートし、EVM バージョンは cancun に設定する必要があります。参照型（配列、マッピング、構造体）ならびにローカル変数と引数はサポートされず、インライン・アセンブリを手で書く必要があります。以下は最小形態の再入ロックです。

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28; // EVM バージョンは cancun が必要

contract TransientLock {
    uint256 transient entered;

    modifier nonReentrant() {
        require(entered == 0, "reentrant");
        entered = 1; // TSTORE、100 ガス
        _;
        entered = 0; // 同一トランザクション内の後続の呼び出しは 0 を読む
    }

    function withdraw() external nonReentrant {}
}
```

コンパイラのバージョンや対象チェーンが条件を満たさない場合は、アセンブリを直接使えます。セマンティクスはまったく同じです。

```solidity
assembly {
    tstore(0, 1)       // スロット 0 に 1 を書き込む、100 ガス
    let v := tload(0)  // 読み戻す、100 ガス
}
```

## 誤用の四つの典型的な帰結

トランザクションをまたいで読みたい状態をメモリや一時ストレージに置くと、症状は「状態が消えた」という形で現れます。トランザクション終了後に値がゼロになり、次の呼び出しでは既定値の 0 が読まれます。コードはエラーを出しませんが、業務ロジックはすでに誤っています。この種のバグはローカルテストでは見つかりにくいことがよくあります。テストが同一トランザクションや同一のシミュレーション環境で完結することが多いからです。

1 つのトランザクション内だけで有効な中間値をストレージに置くと、症状はコストの暴走で、しかもコストはトランザクションの規模に依存します。再入ロックと一時的な承認は機能的にはストレージでも実装できますが、経済的には、返金を取り戻せるだけの規模になるまで待たなければなりません。トランザクションが小さいほど、実際の正味コストはグロスコストに近づきます。

呼び出しをまたいで共有する中間値をメモリに置くと、症状は子フレームから読めないことです。CALL は実行フレームを切り替え、子フレームが受け取るのは独立した、ゼロから始まるメモリです。親フレームが書き込んだ内容は子フレームには現れません。唯一合法な受け渡し経路は calldata と returndata です。

一時ストレージをメモリのマッピングの代わりに使うと、症状は再入時に予期しない挙動が現れることです。EIP-1153 はセキュリティ上の考慮でこの点を特に注意喚起しています。一時ストレージは呼び出しが戻っても破棄されないため、メモリのマッピングとして使うと、同一トランザクション内の再入呼び出しが前の回から残った値を読んでしまいます。セマンティクスの問題に加えて、1 回 100 ガスというコストもメモリ書き込みよりはるかに高いです。

見落としやすいもう一つの誤用は、ゼロに戻すのを忘れることです。再入ロックに 1 を書いた後、一部の分岐で早期に戻って 0 を書き戻さないと、同一トランザクション内の後続の呼び出しはこのロックによって永久に遮られてしまいます。EIP-1153 の仕様はきわめて率直に述べています。これらのスロットがトランザクション内の後続の呼び出しで実際に使われる場合にだけ、非ゼロの値を残すべきです。

## 境界と不確実なところ

本文中のすべてのガス数値は口径を示しています。ベルリン・アップグレード（2021 年 4 月 15 日、ブロック 12,244,000）以降のコールド／ホットアクセスの価格、ロンドン・アップグレード（2021 年 8 月 5 日、ブロック 12,965,000）以降の返金規則、カンクン・アップグレード（2024 年 3 月 13 日、ブロック 19,426,587）が導入した一時ストレージです。EVM は凍結された仕様ではないため、チェーンをまたいでデプロイする前に、対象チェーンがどの EIP を実装しているかを個別に確認する必要があります。ロンドン以前で止まっている EVM 互換チェーンでは、TLOAD と TSTORE は不正なオペコードとして実行を中止します。

Solidity の一時ストレージ対応には、修正済みのコンパイラ欠陥があります。0.8.28 から 0.8.33 で IR パイプライン（--via-ir）を有効にしたとき、同一のコンパイル単位で、一時変数を delete でクリアしつつ、同じ値型の永続ストレージのクリアも存在すると、生成される Yul のクリア用ヘルパー関数が同名のために再利用され、誤ったオペコードを発行します（TSTORE を使うべきところで SSTORE を使う、またはその逆）。0.8.34 で修正されました。欠陥は IR パイプラインにのみ影響し、legacy パイプラインは影響を受けません。一時変数を使い via-ir を通すプロジェクトでは、コンパイルのバージョンを修正済みの範囲の外に置く必要があります。

公開されたスケジュールがないものが 2 つあり、不確実としてマークするしかありません。一時ストレージの 100 ガスは EIP-1153 が定めた現在の値で、将来のハードフォークで調整されるかどうかについて、公開された予定も、すでにプロセスに入っている提案もありません。状態ツリーの実装（たとえば Verkle ツリーの推進）はストレージ読み取りの実際のコストの基礎を変えますし、EIP-2929 の動機にも、データベースのレイアウトを再設計しクライアントがストレージを直接読むようにすれば最悪処理時間をさらに下げられると書かれています。しかし価格設定の定数は依然として EIP-2929 と EIP-3529 が与えるもので、両者の間には長期的にずれが残る可能性があります。この 2 点はいずれも引用できる定量的な結論が出せず、方向性の判断として残すしかありません。

## 出典

- EIP-1153、Transient storage opcodes：https://eips.ethereum.org/EIPS/eip-1153
- EIP-2929、Gas cost increases for state access opcodes：https://eips.ethereum.org/EIPS/eip-2929
- EIP-3529、Reduction in refunds：https://eips.ethereum.org/EIPS/eip-3529
- EIP-2200、Structured Definitions for Net Gas Metering：https://eips.ethereum.org/EIPS/eip-2200
- Ethereum Yellow Paper、付録 G の料金表、式 (328) メモリ価格関数：https://ethereum.github.io/yellowpaper/paper.pdf
- ethereum.org オペコード・リファレンス（TLOAD、TSTORE は各 100 ガス）：https://ethereum.org/en/developers/docs/evm/opcodes/
- Solidity 0.8.28 リリースノート（transient 値型の状態変数対応）：https://www.soliditylang.org/blog/2024/10/09/solidity-0.8.28-release-announcement/
- Solidity ドキュメント：Transient Storage（EVM バージョンは cancun が必要、参照型とローカル変数は未対応）：https://docs.soliditylang.org/en/latest/contracts.html#transient-storage
- Solidity の一時ストレージ・クリア用ヘルパー衝突欠陥（0.8.28 から 0.8.33、0.8.34 で修正）：https://www.soliditylang.org/blog/2026/02/18/transient-storage-clearing-helper-collision-bug/
- ethereum.org ネットワーク・アップグレード履歴（各フォークのブロック高と日付）：https://ethereum.org/en/history/

## 関連記事

- [「楽観的並行制御（OCC）入門：データベースからオンチェーン実行へ」](/ja/blog/optimistic-concurrency-control-intro)
- [「性能指標の用語集：TPS、BPS、確認レイテンシ、最終性、コンフリクト率」](/ja/blog/performance-metrics-glossary)
- [「なぜ単一スレッド EVM は TPS の上限を生むのか：輻輳の歴史と実行モデル」](/ja/blog/evm-single-thread-bottleneck)

本稿は EVM 基礎シリーズに属し、同シリーズにはほかに 0.15「ABI 精読：セレクタ、静的パラメータ、動的型」があり、呼び出しデータがどのようにエンコード・デコードされるかを論じています。
