1. The EVM Execution Model
The Ethereum Virtual Machine is a deterministic, quasi-Turing-complete state machine. Every EVM instance has access to five data regions: the stack, memory, storage, calldata, and code. Understanding which region a value lives in — and what it costs to access it — is the foundation of EVM security.
The Stack
The EVM operates on a 256-bit-wide stack with a maximum depth of 1,024 items. All arithmetic, logic, and control-flow opcodes read from and write to the stack. Stack depth overflows cause a revert — this is the "stack too deep" error you see in the compiler, and it is also an attack surface: a malicious contract can craft calls that push the stack to the limit before re-entering.
Memory
EVM memory is a byte-addressable, linearly-expandable array. It exists only for the duration of a single transaction. Slots are zero-initialized. Memory expands in 32-byte chunks, and gas cost scales quadratically with the maximum word offset reached — this is relevant for any loop that writes to memory.
Solidity reserves the first 128 bytes of memory for its own use:
0x40 can corrupt the heap. Any subsequent abi.encode or dynamic array allocation will overwrite your data.2. Opcodes and Gas
Every EVM instruction has a fixed or context-dependent gas cost. Understanding opcode costs is essential for building gas-efficient contracts — and for identifying where cost manipulation can cause DoS vulnerabilities.
Critical gas costs (security-relevant)
| Opcode | Name | Gas | Security Relevance |
|---|---|---|---|
| 0x54 | SLOAD | 2,100 (cold) / 100 (warm) | Storage reads in loops → DoS via gas exhaustion |
| 0x55 | SSTORE | 20,000 (zero→nonzero) / 5,000 | Unbounded writes → gas griefing |
| 0xF1 | CALL | Variable + 2,300 stipend | Reentrancy vector; stipend limits re-entry |
| 0xF4 | DELEGATECALL | Variable | Proxy pattern — storage collisions |
| 0xFF | SELFDESTRUCT | 5,000 (+ 25,000 if creates) | Force-sends ETH; contract destruction |
| 0x3D | RETURNDATASIZE | 2 | Used in safeTransfer pattern |
| 0x40 | BLOCKHASH | 20 | Only last 256 blocks; randomness pitfall |
| 0x44 | DIFFICULTY/PREVRANDAO | 2 | Post-Merge: validator-influenceable |
3. Storage Layout
Contract storage is a mapping from uint256 key to uint256 value. It is persistent across calls and transactions. Solidity assigns storage slots sequentially, starting at slot 0 for the first declared state variable.
Slot packing
Variables smaller than 32 bytes are packed together into a single slot, right-aligned. The compiler packs consecutive variables if they fit. Order matters for gas efficiency and for security.
Mappings and dynamic arrays
Mappings use keccak256(abi.encode(key, slot)) to compute the actual storage slot. Dynamic arrays store their length at the declared slot, and elements starting at keccak256(slot). This is why you can never directly iterate over a mapping — the keys are hashed and there is no reverse index.
Attack surfaces from storage layout
- Storage collision in proxies: Proxy at slot 0 stores admin address. Implementation also declares a state variable at slot 0. DELEGATECALL overwrites the admin. Use EIP-1967 unstructured storage slots.
- Dirty upper bits: When a
uint128value is packed in the upper half of a slot and code reads the full 256-bit slot via assembly, the upper bits may contain stale data from a different variable. - Storage aliasing in upgradeable contracts: An upgrade that reorders state variables without a storage gap corrupts existing data. Always use storage gaps (
uint256[50] __gap;) in upgradeable base contracts.
4. Calldata and ABI Encoding
Calldata is the immutable input payload sent with a transaction. It begins with the 4-byte function selector (first 4 bytes of keccak256("functionName(types)")), followed by ABI-encoded arguments.
The ABI encoding format
ABI encoding distinguishes between head and tail sections. Static types (uint, address, bool, fixed-size arrays) are encoded directly in the head. Dynamic types (bytes, string, dynamic arrays) place a 32-byte offset in the head pointing to the tail where the actual data lives.
ABI encoding pitfalls
abi.encodePacked(string_a, string_b) produces the same bytes as abi.encodePacked(string_b, string_a) if the strings are transpositions of each other. Never use encodePacked with two dynamic types when the result is used as a signature message or hash key.This vulnerability (SWC-133) is detected by AuditHunt's EncodePackedCollisionDetector on every scan. It affects signature verification, Merkle proof construction, and any hash-based authorization check.
5. Memory Safety in Assembly
Inline assembly (assembly { ... }) bypasses the Solidity memory safety model. Common mistakes:
- Not updating the free memory pointer — subsequent allocations overwrite your data.
- Reading uninitialized memory — EVM memory is zero-initialized per call, but dirty reads are possible if you calculate an offset that points before your allocation.
- Return data buffer race — after
CALL,returndatacopyreads from a shared buffer. If you make another external call before reading, the buffer is overwritten.
6. Security Implications — Checklist
A summary of the attack surfaces introduced by EVM internals:
- DELEGATECALL storage collision — Use EIP-1967 slots; never declare variables at slot 0 in an implementation contract.
- abi.encodePacked with dynamic types — Always use
abi.encodefor hash inputs when two or more parameters are dynamic. - Stack depth manipulation — Check return values of all external calls; do not assume success.
- Dirty storage upper bits — When reading packed values in assembly, mask the bits you need; don't assume padding is clean.
- Memory corruption in assembly — Always save and restore the free memory pointer; never write below it.
- SLOAD in hot loops — Cache storage reads in memory variables inside loops; a single SLOAD costs 2,100 gas cold.
- Mapping enumeration — Mappings are not iterable. If you need iteration, maintain a parallel array of keys.
encodePacked collisions, weak randomness, and assembly memory safety automatically. Paste your contract and run a free T0 scan.