Three drawers, three lifespans
Picture a smart contract as a clerk who sits at a desk and only wakes up when a transaction calls it. To get its job done, the clerk has three very different places to put data. Behind the desk sits a locked filing cabinet — whatever the clerk files there is still there next time, even years later, but opening it and writing in it is slow and costly. On the desk lies a scratch notepad — handy for working things out, but swept into the bin the instant the clerk goes back to sleep. And someone has slid a sealed envelope under the door — the clerk can read the letter inside but is not allowed to write on it. These are the EVM's three data locations: storage, memory, and calldata.
The dimension that really separates them is lifespan. Storage is the only place whose contents survive from one transaction to the next: it is part of the account's permanent state, recorded for all time in the world state. Memory and the stack are born fresh — all zeros — at the start of every call and vanish when it returns. Calldata exists only for the duration of the call and can never be modified. The previous guide covered the stack, the fourth region, where the arithmetic actually happens; this guide is about where the data itself lives.
Storage: the contract's permanent filing cabinet
Every contract account owns its own storage: a giant key–value map with 2²⁵⁶ possible keys, called slots, each holding one 32-byte value, and every slot starts at zero. There are exactly two opcodes to touch it — `SLOAD(key)` reads the value at a slot, and `SSTORE(key, value)` writes one. This is the only EVM data that outlives the transaction; writing it literally changes the blockchain's state.
How does a Solidity variable become a slot? The compiler simply hands out slots in order: the first state variable gets slot 0, the next slot 1, and so on. To save space it packs small variables that fit together into a single 32-byte slot — two `uint128`s share one slot, and an `address` (20 bytes) plus a `bool` can sit side by side.
contract Layout {
uint256 a; // slot 0 fills the whole 32-byte slot
uint128 b; // slot 1, bytes 0..15 packed together
uint128 c; // slot 1, bytes 16..31 into one slot with b
address owner; // slot 2, bytes 0..19
bool paused; // slot 2, byte 20 packs in beside owner
}Packing is not just tidiness — it is money. Because the unit of cost is the slot, reading or writing two variables that share one slot can touch storage a single time instead of twice. Lay your variables out carelessly and you can silently double your gas bill.
Why writing storage is the most expensive thing a contract does
Here are the real numbers. Writing a fresh (previously-zero) slot with `SSTORE` costs 20,000 gas — 22,100 once you count the 2,100 charged the first time a slot is touched in a transaction (a cold access). Changing an existing non-zero value costs 5,000. Reading with `SLOAD` costs 2,100 cold, then 100 each time after (it is now warm). For comparison, an `ADD` costs 3 gas and an `MSTORE` to memory costs 3. The base cost of an entire transaction is 21,000 — so writing one new storage slot costs about as much as sending a whole transaction from scratch.
Why so brutal? Because the write is permanent and global. Every full node on Earth must store that 32-byte value forever and fold it into the chain's state. Concretely, `SSTORE` mutates the world state — a Merkle-Patricia trie — which changes the state root that every node recomputes and that each block header commits to. You are not writing to one computer; you are writing to thousands of them, permanently, and the gas price reflects that externality.
Memory: the scratch notepad
Memory is a volatile, byte-addressable array that is entirely zero at the start of every call frame and thrown away the moment that call returns. Its opcodes are `MLOAD`/`MSTORE` (which read and write 32 bytes at a byte offset) and `MSTORE8` (one byte). It is where the EVM builds anything that does not fit in a single stack word: an in-memory array or struct, the bytes you feed to `keccak256`, the data a function returns. Think of it as RAM that is wiped between calls.
Each access is cheap — 3 gas — but you pay to grow memory. The cost of having touched `a` 32-byte words of memory is `3·a + ⌊a²/512⌋` gas. The linear term keeps small buffers nearly free; the quadratic term makes huge allocations punishing. That curve is deliberate: because memory is never persisted, the only thing stopping a contract from asking for gigabytes (and exhausting nodes' RAM) is the gas meter itself. The next guide dissects gas in full.
memory_cost(words) = 3 * words + floor(words^2 / 512) 32 words (1 KB): 3*32 + 32^2/512 = 96 + 2 = 98 gas 320 words (10 KB): 3*320 + 320^2/512 = 960 + 200 = 1,160 gas 3200 words (100 KB): 3*3200 + 3200^2/512 = 9,600 + 20,000 = 29,600 gas
Calldata: the read-only envelope
Calldata is the input of the call — `msg.data` — a read-only byte array the caller handed in. For a contract call it is a 4-byte function selector followed by the ABI-encoded arguments (how that encoding works is the rung's final guide, on bytecode and the ABI). You read it with `CALLDATALOAD`, `CALLDATASIZE`, and `CALLDATACOPY`; there is no opcode to write it, because it belongs to the caller, not to you.
Because calldata is never copied or allocated, reading a parameter straight out of it is the cheapest option of all. In Solidity, declaring a function argument as `calldata` rather than `memory` skips the copy — a real saving for large arrays and strings. The catch is the flip side of its safety: calldata is immutable, so the moment you need to change the data, you must first copy it into memory and pay for that copy.
// `calldata`: read the array in place, no copy — cheapest.
function total(uint256[] calldata xs) external pure returns (uint256 s) {
for (uint256 i; i < xs.length; ++i) s += xs[i];
}
// `memory`: the array is copied in first — only needed if you mutate it.
function zeroFirst(uint256[] memory xs) internal pure returns (uint256[] memory) {
xs[0] = 0; // legal: memory is writable
return xs;
}Where mappings and dynamic arrays really live
Slot-by-counter works for fixed-size variables, but a mapping or a dynamic array has no fixed size — it cannot live in one slot. So the EVM scatters their entries across the 2²⁵⁶ slot space using Keccak-256. For a mapping declared at slot `p`, the value for key `k` is stored at slot `keccak256(k ++ p)`, where the key and the slot number are each padded to 32 bytes and concatenated.
// mapping(address => uint256) balances; declared at slot 1 // where is balances[0x00..AbC] stored? slot = keccak256( pad32(0x00..AbC) ++ pad32(1) ) // 64 bytes in, 32 bytes out // dynamic array uint256[] xs; declared at slot p: // slot p holds xs.length // keccak256(p) + i holds xs[i]
Two consequences fall straight out of this. First, you cannot enumerate a mapping's keys on-chain: the trie only stores slots and their values, never which key produced a slot, so the original keys are unrecoverable from storage alone. Second, two different mappings essentially never collide — Keccak makes the odds of two keys landing on the same slot astronomically small. A struct stored in a mapping simply occupies consecutive slots beginning at that hash.
Putting it together — and a fourth region
Here is a small vault that uses all three locations in one function. Trace where each piece of data lives:
contract Vault {
uint256 total; // storage, slot 0
mapping(address => uint256) balances; // storage, slot 1
function deposit(uint256[] calldata amounts) external {
uint256 sum; // a stack local (one 256-bit value)
for (uint256 i; i < amounts.length; ++i)
sum += amounts[i]; // read each item straight from calldata
balances[msg.sender] += sum; // SLOAD then SSTORE at keccak256(sender ++ 1)
total += sum; // SLOAD then SSTORE at slot 0
}
}Walk it line by line. `amounts` arrives in calldata and is read in place — no copy. `sum` is a value-type local, so it lives on the stack (last guide's territory), not in memory; memory would only appear if we built a new array or hashed some bytes. The two final lines are the only expensive ones: each does an `SLOAD` then an `SSTORE` against storage — `balances[msg.sender]` at `keccak256(sender ++ 1)` and `total` at slot 0 — and each is what actually mutates the world state.
- Storage — survives across transactions; a map of 32-byte slots in the world state; `SLOAD`/`SSTORE`; by far the costliest (~20,000 gas to write a fresh slot).
- Memory — fresh and zeroed each call, gone on return; a byte array; `MLOAD`/`MSTORE`; cheap, but its cost grows quadratically as it expands.
- Calldata — the read-only call input (`msg.data`); `CALLDATALOAD`/`CALLDATACOPY`; cheapest of all, but immutable.
- Transient storage — key–value like storage but cleared at the transaction's end; `TSTORE`/`TLOAD`; cheap, for cross-call state within one transaction.