BitrootBlog
Back to website ↗
© 2026 Bitroot · Content is for general information only and is not financial, investment, legal, or tax advice.
Editorial StandardsBack to website
← All articles
EVM foundations·2026/09/22·About 14 min

Three Kinds of Storage, Don't Mix Them Up: Memory, Storage, and Transient Storage

The same 32 bytes of data can cost two orders of magnitude more or less in gas depending on whether it sits in memory, storage, or transient storage, and its lifetime is completely different too. This piece takes the three storage kinds apart along three lines — ownership, lifetime, and pricing — and explains where a reentrancy lock, a temporary array, and a state variable each belong.

A reentrancy lock written into storage pays a 2100 cold-access surcharge plus 20000 for Gsset the first time a cold slot goes from 0 to 1, pays another 100 to write it back to 0 later in the same transaction, and burns roughly 22200 gas gross; that write also generates a 19900 gas refund, while the refund cap is one fifth of the transaction's total gas used. Swap in a transient storage variable and each of the two writes costs 100 gas, with no reliance on the refund counter. Functionally identical, and the gross cost differs by around two orders of magnitude.

The EVM has three writable data areas: memory, storage (persistent storage), and transient storage (EIP-1153). All three read and write 32-byte words and can be assigned to inside a contract, yet their unit of ownership, their lifetime, and their pricing rules are all different. Putting something in the wrong place usually raises no error; it just makes state vanish where you did not expect it, or makes the gas bill ten times higher. The questions to answer are: who owns each of the three storage kinds, how long each lives, what rules price them, and what each is suited to hold.

Lifetimes: frames, accounts, and transactions are three different scales

Memory belongs to the execution frame. The context entered by a CALL, DELEGATECALL, STATICCALL, or CREATE is one frame; memory starts from zero when the frame is entered, and the whole block is discarded when the frame returns or reverts. A parent frame's memory is invisible to a child frame, and a child frame's memory is not handed back to the parent; the only way to pass data between two frames is calldata and returndata.

Storage belongs to the account. It hangs under the account's name as a mapping from 256-bit slots to 256-bit values, and what is written into it enters the account's storage trie as part of the world state, persisting across transactions and across blocks. Zero values are not written into the trie, so clearing a slot to zero removes the corresponding node — a point that directly determines how the refund rules are designed.

Transient storage also belongs to the account, but its scope is a single transaction. Within one transaction, every frame of that account shares the same transient storage: a value written by an inner call is readable by the outer call. When a frame reverts, writes made within that frame revert with it, matching storage's behavior; when a frame returns normally they do not revert, the opposite of memory. When the transaction ends, all transient storage is unconditionally cleared. There is one exception to the ownership rule: the transient storage of DELEGATECALL and CALLCODE belongs to the caller (the contract that issued the instruction), while that of CALL and STATICCALL belongs to the callee.

Remember the three scales this way: memory is scoped to a frame, storage to an account plus permanence, and transient storage to an account plus a transaction.

DimensionMemoryStorageTransient Storage
OwnershipExecution frameAccountAccount
LifetimeCreated on frame entry, discarded when the frame endsPersistent, written into the world stateCleared when the transaction ends
AddressingByte address, expanded in 32-byte words256-bit slots256-bit slots
Across internal callsNot sharedSharedShared across all frames of the same account
Frame revertDiscarded with the frameReverts that frame's writesReverts that frame's writes
Cost per write3 gas plus expansion100 to 20000 plus cold-access feeFlat 100 gas

Memory: expanded in words, with quadratic growth in cost

Memory is byte-addressed, but allocation happens at 32-byte-word granularity. Touching a word that has not yet been touched triggers expansion, and the expansion fee is computed over total usage with the formula C_mem(a) = 3a + ⌊a² / 512⌋, where a is the number of words; the actual charge is the post-expansion C_mem minus the pre-expansion C_mem. MSIZE only grows, and a frame cannot release memory it has already allocated.

The first half of that expression is linear and the second half is quadratic. For a < 23 (that is, 704 bytes) the quadratic term is floored to 0 and the cost looks mild; past that point it grows faster. Both absolute figures below are derived from the Yellow Paper's equation (328), under the baseline of a single frame expanding once from empty memory to the target size and excluding the base cost of MLOAD and MSTORE: 32 KB of memory gives a = 1024 and an expansion fee of 3 × 1024 + 1024² / 512 = 5120 gas; 1 MB of memory gives a = 32768 and an expansion fee of 98304 + 2097152 ≈ 2.19 million gas. A single frame can burn millions of gas on memory expansion alone, which usually becomes the hard constraint on building a large buffer inside a frame.

MLOAD and MSTORE have a base cost of 3 gas (Gverylow), with expansion charged separately. Reading memory that has never been written returns 0, but you still pay the expansion fee for the newly touched words, because the allocation has already happened. This is also why sparse writes to a high address are so expensive: writing a single value to a distant offset bills every word in between as allocated.

Temporary arrays, ABI encoding and decoding buffers, and hash function inputs all live in memory. Solidity's memory variables, memory arrays, and memory structs all land in this area. The code below allocates the length in one go, avoiding repeated touching of new words inside the loop:

// Expand to n words in one go; later writes incur no further expansion fee
uint256[] memory buf = new uint256[](n);
for (uint256 i = 0; i < n; ++i) {
    buf[i] = i;
}

The boundary is clear: what memory passes to a child frame through CALL is a copy of the contents, and the pointer itself is valid only inside the current frame. Putting an intermediate value meant to be shared across frames into memory assumes that the child frame can see the parent's memory, and that assumption does not hold.

In real engineering, the more common choice is to route around memory. Large blobs of data go in through calldata and come back out through returndata, priced by the byte and occupying no memory in the current frame; only when a value must be read and written repeatedly within a frame, or when a hash input has to be constructed, is it worth expanding into memory. That trade-off explains why many contracts push computation into external scripts or subcalls and aggregate the results through return values, keeping the buffer inside a single frame small.

Storage: written into the account's storage trie, an order of magnitude more expensive to write than to read

In the world state, every account carries a storage trie whose slots are 256-bit keys and whose values are 256-bit words, with zero values left out of the tree. Reads go through SLOAD. Under the cold/warm access mechanism introduced by EIP-2929 (Berlin, block 12,244,000, April 15, 2021), the first access to a given (address, slot) pair is a cold access costing 2100 gas (Gcoldsload), while a pair already accessed in this transaction is a warm access costing 100 gas (Gwarmaccess). The cold and warm sets are scoped to the transaction, and they revert along with the scope.

Writes go through SSTORE, with the cost determined by EIP-2200's net metering rules, which look at three values at once: the slot's original value at the start of the transaction, its current value, and the new value about to be written. After Berlin the constants are: a cold slot costs an extra 2100; when the original value equals the current value (the slot has not yet been changed in this transaction), writing from 0 to non-zero costs Gsset = 20000, and writing from non-zero to another value or to 0 costs Gsreset = 2900; when the slot has already been changed in this transaction (the original value differs from the current value), only the 100 gas warm access is charged; a no-op write where the new value equals the current value also costs 100.

Refund rules were tightened by EIP-3529 (London, block 12,965,000, August 5, 2021). The refund for rewriting non-zero to zero dropped from 15000 to 4800 (SSTORE_RESET_GAS plus ACCESS_LIST_STORAGE_KEY_COST), SELFDESTRUCT's refund was removed, and the total refund for a single transaction was capped at gas_used // 5. The pattern of an original value of 0, written non-zero and then back to 0 within the transaction, still produces a 19900 gas refund (20000 minus 100), but it is subject to the same one-fifth cap.

Operation (Berlin / London rules)Gas
SLOAD, cold / warm access2100 / 100
SSTORE, original value equals current, 0 written to non-zero20000, plus 2100 for cold access
SSTORE, original value equals current, non-zero to non-zero or to zero2900, plus 2100 for cold access; 4800 refund when clearing to zero
SSTORE, slot already changed in this transaction100
Reentrancy lock 0 → 1 → 0 (same transaction)22200 gross, 19900 refund

Things that must persist across transactions go into storage: balances, ownership, configuration, cumulative counters. The cost comes in two layers — one is gas, the other is state bloat, since every full node has to keep these slots for the long term. Refunds easily create the illusion that “writing back to 0 is free”; what actually happens is that refunds settle only after the transaction ends and offset at most 20% of total consumption. A transaction that consumes only 30000 gas can in theory get back at most 6000 gas of refund.

Transient Storage: survives internal calls, cleared when the transaction ends

EIP-1153 introduced TLOAD (0x5c) and TSTORE (0x5d) in the Cancun upgrade (block 19,426,587, March 13, 2024). Addressing works the same as SLOAD and SSTORE: a 32-byte address points at a 32-byte value. Both cost a flat 100 gas per operation, with no cold/warm distinction, no refund, and no need to reserve cost for future clearing, because in the specification they never touch disk.

Behaviorally, the differences from storage are concentrated in three places. On the time scale, values are cleared when the transaction ends and are never serialized into any persistent structure. On revert semantics, a frame revert rolls back that frame's writes, matching storage and differing from memory (which is discarded wholesale when a frame returns or reverts). On context restrictions, TSTORE raises an exception inside STATICCALL while TLOAD is allowed. In addition, EIP-1153 explicitly exempts TSTORE from EIP-2200's restriction on SSTORE: TSTORE does not require a gasleft above the 2300 call stipend.

This design aims directly at inter-frame communication. Before EIP-1153, contracts passed temporary state around either through CALL's arguments and return values (where an intermediate untrusted contract might tamper with them) or through storage writes (expensive, and reliant on refunds). After EIP-3529 compressed refunds to one fifth of gas_used, small transactions essentially cannot recover their cost. A 0 → 1 → 0 lock write accrues 19900 gas toward the refund counter; working back from the gas_used // 5 cap, the whole transaction needs roughly 99500 gas to recover the refund in full; that 99500 is a gas_used figure derived from EIP-3529's cap formula, not a number given in the EIP text. The body of EIP-1153 offers the authors' estimate from another direction, saying in so many words that a transaction must spend about 80k gas on “other operations” to recover the full refund on a reentrancy lock. The two numbers use different bases yet agree with each other: 99500 minus the lock's own gross consumption of roughly 22200 gas is about 77300, the same order of magnitude as the authors' 80k estimate for “other operations”. Transient storage does not feed the refund counter, and so sidesteps that threshold.

Reentrancy locks, single-transaction approvals, balance checks at the end of a callback, and proxy contracts passing metadata downstream all fit here. Solidity has supported transient value-type state variables since 0.8.28, with the EVM version set to cancun; reference types (arrays, mappings, structs), local variables, and parameters are not supported and require hand-written inline assembly. Below is a reentrancy lock in its minimal form:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28; // EVM version must be cancun

contract TransientLock {
    uint256 transient entered;

    modifier nonReentrant() {
        require(entered == 0, "reentrant");
        entered = 1; // TSTORE, 100 gas
        _;
        entered = 0; // Later calls in the same transaction read 0
    }

    function withdraw() external nonReentrant {}
}

If the compiler version or the target chain does not meet the conditions, assembly works directly with identical semantics:

assembly {
    tstore(0, 1)       // Write 1 to slot 0, 100 gas
    let v := tload(0)  // Read it back, 100 gas
}

Four typical consequences of misuse

Putting state that must be read across transactions into memory or transient storage shows up as “the state disappeared”. When the transaction ends the value is cleared, the next call reads the default 0, the code raises no error, and the business logic is already wrong. Bugs of this kind often stay invisible in local tests, because tests usually complete within a single transaction or the same simulated environment.

Putting an intermediate value that is valid for only one transaction into storage shows up as runaway cost, and the cost depends on the size of the transaction. A reentrancy lock and a temporary approval can be implemented in storage and still work; economically, though, the refund only pays off once the transaction is large enough. The smaller the transaction, the closer the actual net cost stays to the gross cost.

Putting an intermediate value shared across calls into memory shows up as the child frame being unable to read it. CALL switches the execution frame, and the child frame gets independent memory starting from zero; nothing the parent frame wrote appears in the child. The only legitimate paths for passing data are calldata and returndata.

Using transient storage in place of a mapping in memory shows up as unexpected behavior on reentrancy. EIP-1153 calls this out specifically in its security considerations: transient storage is not discarded when a call returns, so using it as an in-memory mapping means a reentrant call within the same transaction sees values left over from the previous round. Beyond the semantic problem, the 100 gas per operation is also far more than a memory write.

There is one more easy-to-miss misuse: forgetting to clear. If a reentrancy lock written to 1 returns early on some branch without writing back to 0, later calls in the same transaction are permanently blocked by that lock. EIP-1153's specification is blunt about it: a non-zero value should be left behind only when these slots really will be used by later calls within the transaction.

Boundaries and open questions

Every gas number in this piece carries a stated basis: cold/warm access prices after the Berlin upgrade (April 15, 2021, block 12,244,000), refund rules after the London upgrade (August 5, 2021, block 12,965,000), and transient storage as introduced by the Cancun upgrade (March 13, 2024, block 19,426,587). The EVM is not a frozen specification, so before deploying across chains you have to confirm which EIPs the target chain implements one by one: on an EVM-compatible chain that stopped at London or earlier, TLOAD and TSTORE abort execution outright as illegal opcodes.

Solidity's transient storage support had a compiler defect that has since been fixed: in 0.8.28 through 0.8.33, when the IR pipeline (--via-ir) is enabled and a compilation unit both clears a transient variable with delete and performs a persistent storage clear of the same value type, the generated Yul clearing helpers collide on the same name and get reused, emitting the wrong opcode (SSTORE where TSTORE belongs, or the reverse); 0.8.34 fixes it. The defect affects only the IR pipeline; the legacy pipeline is unaffected. Projects that use transient variables and go through via-ir need to keep their compiler version outside the affected range.

Two things have no public timetable and can only be marked uncertain. Transient storage's 100 gas is the current value set by EIP-1153, and whether a future hard fork adjusts it has no public schedule and no proposal in the pipeline. The implementation of the state tree (for example, progress on Verkle trees) will change the real cost basis of storage reads, and EIP-2929's motivation also states that redesigning the database layout so clients read storage directly would further cut worst-case processing time; but the pricing constants are still given by EIP-2929 and EIP-3529, so the two may diverge for a long time. Neither point supports a citable quantitative conclusion, so both are kept as directional judgments.

Sources

  • 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, appendix G fee schedule and equation (328) for the memory pricing function, https://ethereum.github.io/yellowpaper/paper.pdf
  • ethereum.org opcode reference (TLOAD and TSTORE at 100 gas each), https://ethereum.org/en/developers/docs/evm/opcodes/
  • Solidity 0.8.28 release announcement (support for transient value-type state variables), https://www.soliditylang.org/blog/2024/10/09/solidity-0.8.28-release-announcement/
  • Solidity docs: Transient Storage (EVM version must be cancun; reference types and local variables not yet supported), https://docs.soliditylang.org/en/latest/contracts.html#transient-storage
  • Solidity transient storage clearing helper collision bug (0.8.28 to 0.8.33, fixed in 0.8.34), https://www.soliditylang.org/blog/2026/02/18/transient-storage-clearing-helper-collision-bug/
  • ethereum.org network upgrade history (block heights and dates for each fork), https://ethereum.org/en/history/

Further reading

Optimistic Concurrency Control (OCC) Primer: From Databases to On-Chain ExecutionA Glossary of Performance Metrics: TPS, BPS, Confirmation Latency, Finality, and Conflict RateWhy a Single-Threaded EVM Caps TPS: Congestion History and the Execution Model

This piece belongs to the EVM fundamentals series; another piece in the same series, 0.15 “ABI in Detail: Selectors, Static Arguments, and Dynamic Types”, covers how calldata is encoded and decoded.

← PreviousStack Machine Anatomy: 256-bit Words, 1024-Item Stack Depth, and the Execution LoopNext →Gas Mechanics in Detail: Metering Units, the Fee Market, and Execution Halts
Contents
Lifetimes: frames, accounts, and transactions are three different scalesMemory: expanded in words, with quadratic growth in costStorage: written into the account's storage trie, an order of magnitude more expensive to write than to readTransient Storage: survives internal calls, cleared when the transaction endsFour typical consequences of misuseBoundaries and open questionsSourcesFurther reading
Reading settings