← All posts
Ethereum

Stack Too Deep, Explained: How EIP-8024 Reaches Past the EVM's 16-Slot Limit

"Stack too deep" is a 16-slot reach limit, not a size limit. How EIP-8024's DUPN, SWAPN, and EXCHANGE opcodes fix it in legacy bytecode without EOF.

If you have written a non-trivial Solidity function, you have probably met the compiler error that stops everyone eventually: CompilerError: Stack too deep. It shows up when a function juggles too many local variables at once, and the usual advice — "split the function" or "wrap things in a struct" — treats the symptom without explaining the cause. The cause is not that the EVM stack is full. It is that the EVM can only reach the top 16 items of it. EIP-8024, currently in Review and running in Ethereum's Glamsterdam devnets, adds opcodes that let bytecode address items far deeper than slot 16 — and it does so without requiring EOF. Here is what the "stack too deep" limit actually is, and what changes when the reach extends.

The 16-Slot Ceiling Behind Every "Stack Too Deep"

The EVM stack holds up to 1,024 256-bit words. That is plenty of room; capacity is almost never the problem. The problem is the instruction set. To read or reorder items, the EVM has exactly two families of stack opcodes: DUP1DUP16, which copy one of the top 16 items to the top, and SWAP1SWAP16, which swap the top item with one of the next 16. There is no DUP20. Anything sitting deeper than the 16th slot from the top is unreachable until you shuffle the items above it out of the way.

That is what "stack too deep" means. When a function keeps more than roughly 16 values live at the same point — parameters, intermediate results, loop counters — the compiler runs out of addressable slots and gives up. The stack has space; the opcodes just cannot point at the value you need.

What Your Compiler Does When It Runs Out of Reach

Before the error, solc tries hard to avoid it. The legacy pipeline reorders operations and reuses slots so that whatever it needs next stays inside the top 16. When that scheduling fails, you get the error and the manual workarounds begin: grouping variables into a struct, breaking one function into two, or scoping blocks with { } so locals are freed early.

The other escape hatch is the IR pipeline. Compiling with --via-ir routes through Yul, whose optimizer can spill excess values to memory instead of failing outright. It resolves most stack-too-deep cases, but it is not free: memory reads and writes cost gas that pure stack manipulation does not, and the generated bytecode differs from the legacy pipeline's. That difference is exactly the kind of thing worth confirming end to end — a refactor or a pipeline switch should not change what your users actually sign, and a real-wallet test through @avalix/chroma is a cheap way to prove the on-chain behavior held.

DUPN, SWAPN, and EXCHANGE: Addressing the Stack by Index

EIP-8024 adds three instructions that take a one-byte immediate operand — an explicit index — instead of baking the depth into the opcode:

  • DUPN (0xe6) duplicates the n-th stack item to the top, where n comes from the byte that follows the opcode.
  • SWAPN (0xe7) swaps the top item with the (n+1)-th item.
  • EXCHANGE (0xe8) swaps two items further down, both selected from its immediate byte, so you can reorder deep values without disturbing the top.

Because the depth is an operand rather than part of the mnemonic, a single DUPN can reach depths up to roughly slot 235, and SWAPN up to slot 236 — far past the old 16. EXCHANGE packs two indices into one byte to address a pair within about 30 items. Crucially, the gas cost stays at 3, the same as DUP* and SWAP*, because these are still pointer swaps under the hood.

For compiler authors that is the whole point: with deep, direct addressing, the scheduler no longer has to perform the elaborate shuffle that "stack too deep" represents. Functions that fail today would compile without spilling to memory or forcing you to restructure your source.

The Immediate-Encoding Trick That Keeps Legacy Bytecode Safe

New opcodes with immediate bytes are risky in legacy EVM bytecode, and that risk is why EIP-663 — the original SWAPN/DUPN/EXCHANGE proposal — was held back and folded into EOF. The danger is jumpdest analysis: a client scanning bytecode for valid jump destinations must not mistake a data byte for a real JUMPDEST (0x5b), and an immediate could smuggle such a byte in.

EIP-8024's contribution is doing this without EOF's bytecode versioning. It carves out the dangerous immediate values: operands in the range 90 < x < 128 for DUPN/SWAPN (and 81 < x < 128 for EXCHANGE) are illegal and halt with exceptional failure. That range is chosen so a JUMPDEST byte or a PUSH opcode byte (0x600x7f) can never appear as one of these immediates, which keeps existing jumpdest analysis correct. The result is deep stack access that drops into today's legacy bytecode rather than waiting on a format migration.

Where This Leaves You

Nothing in your Solidity changes the day EIP-8024 ships — you will not write DUPN by hand. What changes is upstream: the compiler gains headroom, and a class of "stack too deep" errors becomes the compiler's problem instead of yours. The proposal is still in Review and only running in Glamsterdam devnets, so treat it as likely-but-not-final and keep your --via-ir and struct-grouping habits for now. But it is worth understanding why the error exists: not a full stack, just an arm too short to reach the bottom of it.