Lesson 1 — The EVM in plain English: stack, memory, storage, gas
Every smart-contract bug ultimately bottoms out in EVM semantics. Today: the mental model — what the EVM is, what its memory model looks like, and how gas turns 'infinite loops are bad' into a hard physical constraint.
Before you can read smart contracts critically you need a working mental model of the runtime they execute on. The Ethereum Virtual Machine (EVM) is a deceptively simple state machine — fewer than 150 opcodes, no syscalls, no threads, no I/O outside the chain — but the constraints of that simplicity drive almost every bug class you'll meet in the rest of this course. This lesson is the architecture in plain English.
**The EVM as a deterministic state machine.** Every Ethereum node runs an identical implementation of the EVM. Given the same starting state and the same transaction, every node arrives at the same ending state, byte-for-byte. This determinism is the foundation that lets the network reach consensus without trusting individual nodes. It also rules out anything non-deterministic from inside a contract — no random number generation, no current wall-clock time (only block timestamps with miner discretion), no filesystem, no network. If you can't reproduce it from the chain state plus the transaction, the EVM can't do it.
**Four data locations: stack, memory, storage, calldata.** The EVM stores data in four distinct places, and confusing them is the source of substantial gas overhead and a non-trivial fraction of subtle bugs. (1) **Stack** — a 1024-deep LIFO of 256-bit words used for arithmetic operands and most local computation; cheap (~3 gas per push/pop) but limited in depth. (2) **Memory** — a linear byte-array scoped to one transaction's execution; medium-cost (~3 gas per byte, plus a quadratic expansion penalty), wiped between calls. (3) **Storage** — the persistent key-value store of 256-bit slot to 256-bit value scoped to a contract address; expensive (20,000 gas for a cold write, 5,000 for warm, 2,100 for a cold read); the only place data survives between transactions. (4) **Calldata** — the immutable byte-array of the current call's input; cheap to read, can't be written to. The cost differential between stack/memory and storage drives a lot of contract design decisions.
**The 256-bit native word.** Every EVM operation is sized to 256 bits (32 bytes). This is why Solidity's `uint256` is the cheap default and why `uint8` arrays often *cost more* gas than `uint256` arrays — packing is helpful only within a single storage slot, never across operations on the stack. The 256-bit-everywhere choice also means integer overflow isn't a hardware-level signal in the EVM the way it is on x86; pre-Solidity-0.8, an overflowing addition just silently wrapped around modulo 2²⁵⁶. Whole exploit classes (BeautyChain, MICR, others) depended on this.
**Opcodes you'll see referenced constantly.** A short vocabulary will get you through most audit reports: `SLOAD` / `SSTORE` (storage read/write), `MLOAD` / `MSTORE` (memory read/write), `CALL` / `STATICCALL` / `DELEGATECALL` (external call variants), `CALLDATALOAD` (read calldata), `RETURN` / `REVERT` (terminate execution), `SELFDESTRUCT` (deprecated; removes a contract), `CREATE` / `CREATE2` (deploy new contracts), `KECCAK256` (the EVM's hash function). The full opcode list is in the [Ethereum Yellow Paper](https://ethereum.github.io/yellowpaper/paper.pdf) and the interactive reference at [evm.codes](https://www.evm.codes/) — the latter is the resource you'll come back to most often.
**Gas as a physical constraint.** Every opcode costs gas. Every transaction has a gas limit. When the limit is hit, the entire transaction reverts (all state changes roll back, but the gas is still consumed and paid to the validator). This converts 'don't write infinite loops' from a soft style rule into a hard physical limit — your code literally cannot loop forever; it will run out of gas. It also creates the gas-griefing attack surface (lesson 6) and the DoS-via-loop pattern: if a contract iterates over a user-controlled array, an attacker can stuff the array until iteration costs more than the block gas limit and the contract becomes permanently unusable.
**The single most useful piece of mental imagery.** Think of every contract function call as: (a) the EVM allocates a fresh memory area and copies calldata in, (b) it runs the bytecode using the stack for working space and memory for larger intermediate data, (c) any `SSTORE` modifies the persistent storage of *this contract*, (d) any `CALL` to another contract recursively runs the same model with that contract's bytecode + storage but the same overall transaction state, (e) when the outer call ends, memory is discarded but storage changes persist if the call didn't revert. This stack-of-frames model is the substrate on which reentrancy, cross-contract bugs, and access-control failures all play out.
Example
A concrete trace. Consider a function `transfer(address to, uint256 amount)` on a simple ERC-20. Execution proceeds: (1) calldata contains the 4-byte function selector + the encoded `to` and `amount`; the dispatcher matches the selector and jumps to the function body. (2) The contract `SLOAD`s the caller's balance from storage slot `keccak256(msg.sender, BALANCES_SLOT)` — a cold read at 2,100 gas — and compares it to `amount` (a stack operation, ~3 gas). (3) If sufficient, it `SSTORE`s a decremented sender balance (5,000 gas for the warm write that follows the cold read) and `SSTORE`s an incremented recipient balance (20,000 gas for the cold write if the recipient is new). (4) It emits a `Transfer` event by writing to a special log buffer (cheap). (5) It returns `true` (a stack push). Total gas: roughly 27,000-50,000 depending on whether the recipient had a prior balance. Every gas number in that trace is fixed by the EVM specification, not by Solidity or the contract author. Knowing where the gas goes is the first step to reading optimised contracts.
Common mistakes
- Assuming Solidity is the EVM. Solidity is one of several languages (Vyper, Huff, raw Yul) that compile to EVM bytecode. The EVM doesn't know about Solidity types, modifiers, or inheritance — those are compile-time constructs.
- Treating memory and storage as the same thing. They cost orders of magnitude different gas; misuse is the primary cause of unintentionally-expensive contracts.
- Forgetting that storage is per-contract. A `delegatecall` from contract A into contract B's code runs B's logic against A's storage — confusing this assumption is the root cause of multiple high-impact bugs (Parity multisig freeze).
- Believing gas refunds make any operation 'free.' Refunds were capped at 20% of gas used post-EIP-3529; clearing storage no longer makes the surrounding transaction cheap.
- Reasoning about the EVM as if it had wall-clock time. `block.timestamp` is set by the validator and is manipulable within ~12 seconds; never use it as a randomness source or short-window deadline.
Check your understanding
A contract has a function that iterates over an array of user-submitted entries to compute a total. Which of the following is the most defensible architectural choice?
Key terms covered
Sources & further reading
- PrimaryEthereum Yellow Paper
The formal specification of the EVM; reference for opcode semantics and gas costs.
- Primary
- Primary
- Primary
We prioritise primary sources. Where a topic moves quickly (regulation, security incidents), we re-check sources on the cadence shown by the page's "Next review" date.