Skip to main content

Try it Live

Run Opcode examples in the interactive playground
Conceptual Guide - For API reference and method documentation, see Opcode API.
EVM opcodes are the low-level machine instructions that smart contracts execute on the Ethereum Virtual Machine. This guide teaches opcode fundamentals using Tevm.

What Are Opcodes?

Opcodes are single-byte instructions (0x00-0xFF) that tell the EVM what to do. The EVM has ~140 unique opcodes organized by category:
  • Arithmetic - ADD, MUL, SUB, DIV, MOD
  • Logic - AND, OR, XOR, NOT, LT, GT, EQ
  • Storage - SLOAD, SSTORE (persistent contract storage)
  • Memory - MLOAD, MSTORE (temporary execution data)
  • Control Flow - JUMP, JUMPI, JUMPDEST (loops, conditionals)
  • Calls - CALL, DELEGATECALL, STATICCALL, CREATE
  • System - ADDRESS, CALLER, CALLVALUE, RETURN, REVERT

Stack Machine Architecture

The EVM is a stack machine - all operations push/pop values to/from a runtime stack:
Stack (max 1024 items, 32 bytes each):
┌──────────┐
│  Item n  │ ← Top of stack
├──────────┤
│  Item 2  │
├──────────┤
│  Item 1  │
└──────────┘
import * as Opcode from 'tevm/Opcode'

// Bytecode: PUSH1 5, PUSH1 3, ADD
const bytecode = new Uint8Array([0x60, 0x05, 0x60, 0x03, 0x01])

const instructions = Opcode.parse(bytecode)
// [
//   { offset: 0, opcode: 0x60, immediate: Uint8Array([0x05]) }, // PUSH1 5
//   { offset: 2, opcode: 0x60, immediate: Uint8Array([0x03]) }, // PUSH1 3
//   { offset: 4, opcode: 0x01 }                                 // ADD
// ]

// Execution flow:
// Stack: []
// PUSH1 5 → Stack: [5]
// PUSH1 3 → Stack: [3, 5]
// ADD     → Stack: [8]  // Pops 3 and 5, pushes result
Every opcode has stack inputs (items consumed) and stack outputs (items produced):
import * as Opcode from 'tevm/Opcode'

const info = Opcode.info(0x01) // ADD
console.log(info)
// {
//   gasCost: 3,
//   stackInputs: 2,   // Pops 2 items
//   stackOutputs: 1,  // Pushes 1 result
//   name: "ADD"
// }

Opcode Categories

Arithmetic Operations

Binary operations pop 2 values, push 1 result:
import * as Opcode from 'tevm/Opcode'

// ADD (0x01): a + b
const addCode = new Uint8Array([0x60, 0x05, 0x60, 0x03, 0x01])
// Stack: [] → [5] → [3,5] → [8]

// MUL (0x02): a * b
const mulCode = new Uint8Array([0x60, 0x05, 0x60, 0x03, 0x02])
// Stack: [] → [5] → [3,5] → [15]

// SUB (0x03): a - b
const subCode = new Uint8Array([0x60, 0x05, 0x60, 0x03, 0x03])
// Stack: [] → [5] → [3,5] → [2]  // Note: 5 - 3, not 3 - 5

console.log(Opcode.disassemble(addCode))
// ["0000: PUSH1 0x05", "0002: PUSH1 0x03", "0004: ADD"]

Storage Operations

SLOAD reads from persistent contract storage, SSTORE writes:
import * as Opcode from 'tevm/Opcode'

// SLOAD (0x54): Load from storage[slot]
const sloadCode = new Uint8Array([
  0x60, 0x00,  // PUSH1 0 (storage slot)
  0x54         // SLOAD
])
// Stack: [] → [0] → [value]

// SSTORE (0x55): Store value to storage[slot]
const sstoreCode = new Uint8Array([
  0x60, 0x42,  // PUSH1 0x42 (value)
  0x60, 0x00,  // PUSH1 0 (storage slot)
  0x55         // SSTORE
])
// Stack: [] → [0x42] → [0,0x42] → []

console.log(Opcode.info(0x54))
// { gasCost: 100, stackInputs: 1, stackOutputs: 1, name: "SLOAD" }

console.log(Opcode.info(0x55))
// { gasCost: 100, stackInputs: 2, stackOutputs: 0, name: "SSTORE" }

Memory Operations

MLOAD reads from temporary memory, MSTORE writes:
import * as Opcode from 'tevm/Opcode'

// MSTORE (0x52): Store value at memory[offset:offset+32]
const mstoreCode = new Uint8Array([
  0x60, 0x42,  // PUSH1 0x42 (value)
  0x60, 0x00,  // PUSH1 0 (memory offset)
  0x52         // MSTORE
])
// Stack: [] → [0x42] → [0,0x42] → []
// Memory[0:32] = 0x42

// MLOAD (0x51): Load value from memory[offset:offset+32]
const mloadCode = new Uint8Array([
  0x60, 0x00,  // PUSH1 0 (memory offset)
  0x51         // MLOAD
])
// Stack: [] → [0] → [value]

console.log(Opcode.disassemble(mstoreCode))
// ["0000: PUSH1 0x42", "0002: PUSH1 0x00", "0004: MSTORE"]

Control Flow

JUMP and JUMPI control program flow. Jump targets must be JUMPDEST (0x5B):
import * as Opcode from 'tevm/Opcode'

const code = new Uint8Array([
  0x60, 0x05,  // PUSH1 5 (jump target)
  0x56,        // JUMP
  0x00,        // STOP (skipped)
  0x00,        // STOP (skipped)
  0x5B,        // JUMPDEST (position 5)
  0x60, 0x01,  // PUSH1 1
])

// Find valid jump destinations
const jumpDests = Opcode.jumpDests(code)
console.log(jumpDests) // Set([5])

// Validate jump target
console.log(Opcode.isValidJumpDest(code, 5))  // true (JUMPDEST at offset 5)
console.log(Opcode.isValidJumpDest(code, 3))  // false (STOP, not JUMPDEST)

// JUMPI (0x57): Conditional jump
const jumpiCode = new Uint8Array([
  0x60, 0x01,  // PUSH1 1 (condition: true)
  0x60, 0x06,  // PUSH1 6 (jump target)
  0x57,        // JUMPI (jumps if condition != 0)
  0x00,        // STOP (skipped)
  0x5B,        // JUMPDEST (position 6)
])
// Stack: [] → [1] → [6,1] → []
// Jumps to position 6 because condition is 1 (true)

External Calls

CALL invokes other contracts:
import * as Opcode from 'tevm/Opcode'

// CALL (0xF1): Call another contract
// Stack inputs: [gas, address, value, argsOffset, argsSize, retOffset, retSize]
const callCode = new Uint8Array([
  0x60, 0x00,  // PUSH1 0 (retSize)
  0x60, 0x00,  // PUSH1 0 (retOffset)
  0x60, 0x00,  // PUSH1 0 (argsSize)
  0x60, 0x00,  // PUSH1 0 (argsOffset)
  0x60, 0x00,  // PUSH1 0 (value in wei)
  0x73,        // PUSH20 (address follows)
  0x12, 0x34, 0x56, 0x78, 0x90,  // 20-byte address
  0xAB, 0xCD, 0xEF, 0x12, 0x34,
  0x56, 0x78, 0x90, 0xAB, 0xCD,
  0xEF, 0x12, 0x34, 0x56, 0x78,
  0x5B, 0x8D, 0x80,  // PUSH2 0x5B8D (gas limit)
  0xF1         // CALL
])

console.log(Opcode.info(0xF1))
// { gasCost: 100, stackInputs: 7, stackOutputs: 1, name: "CALL" }
// Returns 1 on success, 0 on failure

Gas Costs

Every opcode has a base gas cost. Some have dynamic costs:
import * as Opcode from 'tevm/Opcode'

// Fixed gas costs
console.log(Opcode.info(0x01)) // ADD: 3 gas
console.log(Opcode.info(0x02)) // MUL: 5 gas
console.log(Opcode.info(0x10)) // LT: 3 gas

// Dynamic gas costs (base shown, runtime varies)
console.log(Opcode.info(0x54)) // SLOAD: 100 gas (warm) or 2100 gas (cold)
console.log(Opcode.info(0x55)) // SSTORE: 100-22000 gas (depends on storage state)
console.log(Opcode.info(0xF1)) // CALL: 100+ gas (warm/cold + value transfer)
console.log(Opcode.info(0x20)) // KECCAK256: 30 + 6*words gas
Gas costs changed across hardforks. EIP-2929 (Berlin) introduced warm/cold access costs for storage and contract calls.

PUSH Instructions

PUSH1-PUSH32 (0x60-0x7F) embed immediate data in bytecode:
import * as Opcode from 'tevm/Opcode'

// PUSH1 (0x60): Push 1 byte
console.log(Opcode.pushBytes(0x60)) // 1
console.log(Opcode.pushOpcode(1))   // 0x60

// PUSH32 (0x7F): Push 32 bytes
console.log(Opcode.pushBytes(0x7F)) // 32
console.log(Opcode.pushOpcode(32))  // 0x7F

// Check if opcode is PUSH
console.log(Opcode.isPush(0x60))    // true (PUSH1)
console.log(Opcode.isPush(0x7F))    // true (PUSH32)
console.log(Opcode.isPush(0x01))    // false (ADD)

// Parse PUSH data
const code = new Uint8Array([
  0x60, 0x42,              // PUSH1 0x42
  0x7F,                    // PUSH32 (32 bytes follow)
  ...new Array(32).fill(0xFF)
])

const instructions = Opcode.parse(code)
console.log(instructions[0].immediate)  // Uint8Array([0x42])
console.log(instructions[1].immediate?.length)  // 32
PUSH instruction data is NOT executable. Parsing must skip immediate bytes or you’ll misinterpret data as opcodes.

Stack Depth Limit

The EVM stack has a 1024-item maximum. Stack overflow causes execution to fail:
import * as Opcode from 'tevm/Opcode'

// Track stack depth through bytecode
function analyzeStackDepth(bytecode: Uint8Array): number {
  let depth = 0
  let maxDepth = 0

  for (const inst of Opcode.parse(bytecode)) {
    const info = Opcode.info(inst.opcode)
    depth -= info.stackInputs
    depth += info.stackOutputs

    if (depth < 0) {
      throw new Error(`Stack underflow at offset ${inst.offset}`)
    }
    if (depth > 1024) {
      throw new Error(`Stack overflow at offset ${inst.offset}`)
    }

    maxDepth = Math.max(maxDepth, depth)
  }

  return maxDepth
}

// Example: Safe code
const safeCode = new Uint8Array([0x60, 0x01, 0x60, 0x02, 0x01])
console.log(analyzeStackDepth(safeCode)) // 1

Common Patterns

Function Dispatch

Contracts check function selectors (first 4 bytes of calldata):
import * as Opcode from 'tevm/Opcode'

const dispatchCode = new Uint8Array([
  0x60, 0x00,  // PUSH1 0 (calldata offset)
  0x35,        // CALLDATALOAD (load 32 bytes from calldata)
  0x60, 0xE0,  // PUSH1 0xE0 (224 bits)
  0x1C,        // SHR (shift right to get first 4 bytes)
  // Stack now has function selector
  0x80,        // DUP1 (duplicate selector)
  0x63, 0xa9, 0x05, 0x9c, 0xbb,  // PUSH4 0xa9059cbb (transfer selector)
  0x14,        // EQ (check if equal)
  0x60, 0x20,  // PUSH1 0x20 (jump target for transfer)
  0x57,        // JUMPI (jump if selector matches)
])

console.log(Opcode.disassemble(dispatchCode))
// [
//   "0000: PUSH1 0x00",
//   "0002: CALLDATALOAD",
//   "0003: PUSH1 0xe0",
//   "0005: SHR",
//   "0006: DUP1",
//   "0007: PUSH4 0xa9059cbb",
//   "000c: EQ",
//   "000d: PUSH1 0x20",
//   "000f: JUMPI"
// ]

Storage Access

Reading and modifying storage:
import * as Opcode from 'tevm/Opcode'

// Increment storage slot 0
const incrementCode = new Uint8Array([
  0x60, 0x00,  // PUSH1 0 (slot)
  0x54,        // SLOAD (load current value)
  0x60, 0x01,  // PUSH1 1
  0x01,        // ADD (increment)
  0x60, 0x00,  // PUSH1 0 (slot)
  0x55,        // SSTORE (store new value)
])

console.log(Opcode.disassemble(incrementCode))
// [
//   "0000: PUSH1 0x00",
//   "0002: SLOAD",
//   "0003: PUSH1 0x01",
//   "0005: ADD",
//   "0006: PUSH1 0x00",
//   "0008: SSTORE"
// ]

Memory Layout

Common memory usage pattern:
import * as Opcode from 'tevm/Opcode'

// Standard memory setup at contract start
const memorySetupCode = new Uint8Array([
  0x60, 0x80,  // PUSH1 0x80 (128 - free memory pointer start)
  0x60, 0x40,  // PUSH1 0x40 (64 - free memory pointer location)
  0x52,        // MSTORE (store 0x80 at memory[0x40])
])

console.log(Opcode.disassemble(memorySetupCode))
// [
//   "0000: PUSH1 0x80",
//   "0002: PUSH1 0x40",
//   "0004: MSTORE"
// ]

// Memory layout:
// 0x00-0x1f: scratch space
// 0x20-0x3f: scratch space
// 0x40-0x5f: free memory pointer
// 0x60-0x7f: zero slot
// 0x80+: allocated memory

Complete Example: Return Value

Contract that returns the value 42:
import * as Opcode from 'tevm/Opcode'

const code = new Uint8Array([
  0x60, 0x2a,  // PUSH1 0x2a (42 in hex)
  0x60, 0x00,  // PUSH1 0 (memory offset)
  0x52,        // MSTORE (store 42 at memory[0])
  0x60, 0x20,  // PUSH1 0x20 (32 bytes to return)
  0x60, 0x00,  // PUSH1 0 (memory offset to return from)
  0xf3,        // RETURN
])

console.log(Opcode.disassemble(code))
// [
//   "0000: PUSH1 0x2a",
//   "0002: PUSH1 0x00",
//   "0004: MSTORE",
//   "0005: PUSH1 0x20",
//   "0007: PUSH1 0x00",
//   "0009: RETURN"
// ]

// Execution trace:
// Stack: []
// PUSH1 0x2a → Stack: [0x2a]
// PUSH1 0x00 → Stack: [0x00, 0x2a]
// MSTORE     → Stack: [], Memory[0:32] = 0x2a
// PUSH1 0x20 → Stack: [0x20]
// PUSH1 0x00 → Stack: [0x00, 0x20]
// RETURN     → Returns memory[0:32] containing 42

Resources

Next Steps