EVM Opcodes Explained: A Complete Reference Guide

Learn how EVM opcodes work, from arithmetic and stack operations to memory, flow control, and system calls. Includes gas costs, bytecode examples, and categories.

By Zig EVM Team |

TL;DR: EVM opcodes are the atomic instructions that power every smart contract on Ethereum. There are roughly 140 opcodes organized into categories — arithmetic, stack manipulation, memory, storage, flow control, and system operations. Each opcode consumes a specific amount of gas, operates on a 256-bit stack, and together they form the bytecode that the Ethereum Virtual Machine executes. This guide covers every major category, explains how the stack machine model works, and provides concrete bytecode examples you can experiment with.

What Are EVM Opcodes?

Every smart contract on Ethereum, regardless of whether it was written in Solidity, Vyper, or Huff, compiles down to a sequence of opcodes — single-byte instructions that the Ethereum Virtual Machine (EVM) understands natively. An opcode is the smallest unit of execution on Ethereum.

The EVM is a stack-based virtual machine. Unlike register-based architectures (x86, ARM), it has no general-purpose registers. Instead, all computation happens by pushing values onto and popping values off a last-in-first-out stack. Each stack element is a 256-bit (32-byte) word, and the stack has a maximum depth of 1,024 items.

The EVM defines approximately 140 opcodes, each identified by a single byte from 0x00 to 0xFF. Any byte value that does not correspond to a valid opcode is treated as INVALID (0xFE) and causes execution to revert.

How the Stack Machine Model Works

Before diving into opcode categories, it is essential to understand the execution model:

  1. The EVM reads bytecode one byte at a time from left to right.
  2. Each byte is looked up in the opcode table.
  3. The opcode pops its inputs from the top of the stack.
  4. It performs its computation.
  5. It pushes any outputs back onto the stack.
  6. The program counter advances to the next instruction.

Here is a minimal example. The bytecode 6005600401 computes 5 + 4:

# Bytecode: 6005600401
# Step-by-step execution:
# 60 05  -> PUSH1 0x05    Stack: [5]
# 60 04  -> PUSH1 0x04    Stack: [4, 5]
# 01     -> ADD           Stack: [9]

The PUSH1 opcode reads the next byte from the bytecode stream and pushes it onto the stack. The ADD opcode pops two values, adds them, and pushes the result. You can try this yourself in the Zig EVM Playground.

Opcode Categories

Arithmetic Operations (0x01 - 0x0B)

Arithmetic opcodes perform math on 256-bit unsigned integers. All results are computed modulo 2^256.

OpcodeByteGasDescription
ADD0x013Addition
MUL0x025Multiplication
SUB0x033Subtraction
DIV0x045Integer division
SDIV0x055Signed integer division
MOD0x065Modulo
SMOD0x075Signed modulo
ADDMOD0x088Addition modulo
MULMOD0x098Multiplication modulo
EXP0x0A10+Exponentiation
SIGNEXTEND0x0B5Sign extension

In the Zig EVM implementation, each arithmetic opcode is implemented as a separate file. For example, the ADD opcode:

// src/opcodes/add.zig -- Zig EVM ADD implementation
fn execute(evm: *EVM) !void {
    // Pop two values from the stack
    const a = try evm.stack.pop();
    const b = try evm.stack.pop();
    // Push the result (modulo 2^256 by nature of u256)
    try evm.stack.push(a +% b);
}

The +% operator in Zig performs wrapping addition, which naturally gives us modulo 2^256 behavior — exactly what the EVM specification requires.

Comparison and Bitwise Operations (0x10 - 0x1D)

These opcodes compare stack values or perform bitwise logic.

OpcodeByteGasDescription
LT0x103Less than
GT0x113Greater than
SLT0x123Signed less than
SGT0x133Signed greater than
EQ0x143Equality
ISZERO0x153Is zero
AND0x163Bitwise AND
OR0x173Bitwise OR
XOR0x183Bitwise XOR
NOT0x193Bitwise NOT
BYTE0x1A3Extract byte
SHL0x1B3Shift left
SHR0x1C3Shift right
SAR0x1D3Arithmetic shift right

Comparison opcodes push 1 for true and 0 for false. This bytecode checks whether 10 > 5:

# Bytecode: 600A600511
# 60 0A  -> PUSH1 10     Stack: [10]
# 60 05  -> PUSH1 5      Stack: [5, 10]
# 11     -> GT           Stack: [1]  (true, because 10 > 5)

Stack Operations (0x50, 0x60-0x9F)

Stack opcodes manage the stack directly. They are among the most frequently executed instructions in any contract.

PUSH opcodes (0x60 - 0x7F): PUSH1 through PUSH32 push 1 to 32 bytes from the bytecode onto the stack. These are the only opcodes that consume additional bytes from the bytecode stream.

# PUSH1 pushes 1 byte
60 FF          # Pushes 0xFF (255)

# PUSH2 pushes 2 bytes
61 01 00       # Pushes 0x0100 (256)

# PUSH32 pushes 32 bytes (a full 256-bit word)
7F 00...01     # Pushes a full 256-bit value

DUP opcodes (0x80 - 0x8F): DUP1 through DUP16 duplicate the Nth stack item and place the copy on top.

SWAP opcodes (0x90 - 0x9F): SWAP1 through SWAP16 swap the top stack item with the (N+1)th item.

OpcodeByteGasDescription
POP0x502Remove top item
PUSH1-320x60-0x7F3Push N bytes
DUP1-160x80-0x8F3Duplicate Nth item
SWAP1-160x90-0x9F3Swap top with Nth

Memory Operations (0x51 - 0x59)

The EVM provides a linear, byte-addressable memory space that is volatile — it exists only during a single transaction execution. Memory is word-addressed in 32-byte chunks for load and store, but byte-level writes are also supported.

OpcodeByteGasDescription
MLOAD0x513+Load 32 bytes from memory
MSTORE0x523+Store 32 bytes to memory
MSTORE80x533+Store 1 byte to memory
MSIZE0x592Get current memory size

Memory costs increase quadratically as you use more of it. The gas cost for memory expansion is:

memory_cost = (memory_size_words ^ 2) / 512 + 3 * memory_size_words

This quadratic cost prevents contracts from allocating unlimited memory.

// Zig EVM memory expansion cost calculation
pub fn memoryCost(size_in_bytes: u64) u64 {
    const size_words = (size_in_bytes + 31) / 32;
    return (size_words * size_words) / 512 + 3 * size_words;
}

Storage Operations (0x54 - 0x55)

Storage is the persistent key-value store associated with each contract address. Unlike memory, storage persists between transactions.

OpcodeByteGasDescription
SLOAD0x542,100Load from storage
SSTORE0x555,000-20,000Store to storage

SSTORE is the most expensive common opcode because it permanently modifies the blockchain state. Writing a non-zero value to a previously zero storage slot costs 20,000 gas. Modifying an existing non-zero slot costs 5,000 gas.

Flow Control (0x56 - 0x5B)

Flow control opcodes change the program counter, enabling loops, conditionals, and function dispatch.

OpcodeByteGasDescription
JUMP0x568Unconditional jump
JUMPI0x5710Conditional jump
JUMPDEST0x5B1Mark valid jump destination
PC0x582Get program counter

Every JUMP or JUMPI destination must be a JUMPDEST instruction. This is a critical safety feature: it prevents jumps into the middle of PUSH data or arbitrary bytecode positions.

# Simple conditional: if top-of-stack != 0, jump to offset 0x0A
# 60 0A  -> PUSH1 0x0A   (jump destination)
# 60 01  -> PUSH1 0x01   (condition: true)
# 57     -> JUMPI         (conditional jump)
# ...
# 5B     -> JUMPDEST      (at offset 0x0A)

System Operations (0xF0 - 0xFF)

System opcodes handle contract creation, external calls, and self-destruction.

OpcodeByteGasDescription
CREATE0xF032,000Create a new contract
CALL0xF1VariableCall another contract
CALLCODE0xF2VariableCall with caller’s storage
RETURN0xF30Return output data
DELEGATECALL0xF4VariableDelegate call
CREATE20xF532,000Create with deterministic address
STATICCALL0xFAVariableRead-only call
REVERT0xFD0Revert with output data
SELFDESTRUCT0xFF5,000+Destroy contract

The CALL opcode is the primary mechanism for contract-to-contract interaction. It takes seven stack arguments: gas, address, value, input offset, input size, output offset, and output size.

Environmental and Block Information (0x30 - 0x48)

These opcodes provide access to transaction and block context.

OpcodeByteGasDescription
ADDRESS0x302Current contract address
BALANCE0x312,600Balance of an address
ORIGIN0x322Transaction origin
CALLER0x332Direct caller
CALLVALUE0x342ETH sent with call
CALLDATALOAD0x353Load call data
CALLDATASIZE0x362Size of call data
GASPRICE0x3A2Gas price of transaction
BLOCKHASH0x4020Hash of a recent block
COINBASE0x412Block miner/proposer
TIMESTAMP0x422Block timestamp
NUMBER0x432Block number
GASLIMIT0x452Block gas limit

A Complete Bytecode Example

Let us trace through a bytecode sequence that computes (3 + 7) * 2 and stores the result in memory:

# Bytecode: 600360070160020260005260206000F3
#
# 60 03     PUSH1 3         Stack: [3]
# 60 07     PUSH1 7         Stack: [7, 3]
# 01        ADD             Stack: [10]
# 60 02     PUSH1 2         Stack: [2, 10]
# 02        MUL             Stack: [20]
# 60 00     PUSH1 0         Stack: [0, 20]
# 52        MSTORE          Stack: []        Memory[0..31] = 20
# 60 20     PUSH1 32        Stack: [32]
# 60 00     PUSH1 0         Stack: [0, 32]
# F3        RETURN          Returns memory[0..31] = 20

You can paste this bytecode into the Zig EVM Playground to step through it instruction by instruction.

Gas Costs by Category

Understanding gas costs is critical for writing efficient smart contracts. Here is a summary by category:

CategoryTypical GasExamples
Zero-cost0STOP, RETURN, REVERT
Base2POP, PC, MSIZE
Very low3ADD, SUB, PUSH, DUP, SWAP
Low5MUL, DIV, MOD
Mid8ADDMOD, MULMOD, JUMP
High10JUMPI, EXP base
Memory3+ expansionMLOAD, MSTORE
Storage read2,100SLOAD
Storage write5,000-20,000SSTORE
Contract creation32,000CREATE, CREATE2

For a deeper exploration of gas costs and optimization strategies, see our guide on Understanding EVM Gas.

How Zig EVM Implements Opcodes

The Zig EVM project implements opcodes using a modular, one-file-per-opcode architecture. Each opcode lives in src/opcodes/ and exports a getImpl() function that returns the opcode byte and an execution function.

// Opcode registration in Zig EVM
// Each opcode is loaded into a HashMap for O(1) dispatch
pub fn loadOpcodes(self: *EVM) void {
    const add = @import("opcodes/add.zig").getImpl();
    self.opcodes.put(add.code, add.impl);
    // ... repeat for all opcodes
}

This design makes it straightforward to add new opcodes, test them in isolation, and understand the implementation of any single instruction without reading thousands of lines of code. The project currently implements 96 opcodes, covering arithmetic, stack manipulation, memory, flow control, and environmental queries.

Tools for Exploring EVM Opcodes

Several tools let you experiment with opcodes interactively:

ToolTypeBest For
Zig EVM PlaygroundWeb-based editor + debuggerWriting raw bytecode, stack visualization, execution tracing
evm.codesInteractive referenceLooking up individual opcodes, gas costs, and stack effects
ethervm.ioOpcode referenceQuick opcode lookup with decompiler
Remix IDEFull Solidity IDEDebugging compiled Solidity at the opcode level
Foundry (forge)CLI toolchainTracing opcode execution in tests and on-chain transactions

The Zig EVM Playground is unique in providing a Catppuccin-themed bytecode editor with real-time disassembly and stack visualization — ideal for learning how opcodes transform the stack step by step.

Key Takeaways

  • EVM opcodes are single-byte instructions that form the machine code of Ethereum smart contracts.
  • The EVM is a stack machine: all computation happens by pushing to and popping from a 256-bit, 1,024-deep stack.
  • Opcodes are grouped into categories: arithmetic, comparison/bitwise, stack, memory, storage, flow control, environment, and system.
  • Gas costs reflect computational expense: simple math costs 3-5 gas, while storage writes cost up to 20,000 gas.
  • Memory costs grow quadratically, preventing unbounded allocation.
  • Every valid jump destination must be marked with JUMPDEST for safety.
  • The Zig EVM project provides a modular, testable implementation of 96 opcodes that you can study and extend.
  • You can experiment with raw bytecode in the Zig EVM Playground or explore the full source on GitHub.

Frequently Asked Questions

What are EVM opcodes?
EVM opcodes are the low-level instructions that the Ethereum Virtual Machine executes. Every smart contract deployed on Ethereum is compiled down to a sequence of these single-byte opcodes, which manipulate a stack, memory, and persistent storage to carry out computations.
How many opcodes does the EVM have?
The EVM specification defines approximately 140 opcodes, with byte values ranging from 0x00 to 0xFF. Not all 256 possible byte values are assigned; unassigned values are treated as INVALID and revert execution.
What is the difference between PUSH and DUP opcodes?
PUSH opcodes (PUSH1 through PUSH32) place a new value onto the stack from the bytecode itself. DUP opcodes (DUP1 through DUP16) copy an existing stack item at a given depth and place the copy on top. PUSH increases the stack size by one, while DUP also increases it by one but reads from the existing stack rather than from bytecode.
Why do different opcodes cost different amounts of gas?
Gas costs reflect the computational resources an opcode consumes on the network. Simple operations like ADD cost 3 gas, while storage writes (SSTORE) cost 5,000-20,000 gas because they permanently alter blockchain state that every full node must store indefinitely.
Can I test EVM opcodes without deploying to Ethereum?
Yes. You can use local tools like the Zig EVM playground, Remix IDE, or Hardhat's local node to execute bytecode and inspect opcode-level behavior without spending real ETH.

Try Zig EVM

Explore the interactive playground or dive into the source code.