Skip to main content

Try it Live

Run Opcode examples in the interactive playground

Usage Patterns

Real-world examples for EVM bytecode analysis, disassembly, and optimization detection.

Bytecode Disassembly

Basic Disassembler

import * as Opcode from '@tevm/primitives/Opcode'

function disassembleContract(bytecode: Uint8Array): void {
  const assembly = Opcode.disassemble(bytecode)

  console.log("Disassembly:")
  console.log("============")
  for (const line of assembly) {
    console.log(line)
  }
}

// Example output:
// 0000: PUSH1 0x80
// 0002: PUSH1 0x40
// 0004: MSTORE
// 0005: CALLVALUE
// 0006: DUP1
// 0007: ISZERO

Annotated Disassembly

function annotatedDisassembly(bytecode: Uint8Array): string[] {
  const instructions = Opcode.parse(bytecode)
  const lines: string[] = []

  for (const inst of instructions) {
    const name = Opcode.name(inst.opcode) ?? `INVALID_${inst.opcode.toString(16)}`
    const info = Opcode.info(inst.opcode)

    let line = `${inst.offset.toString(16).padStart(4, '0')}: ${name}`

    if (inst.immediate) {
      const hex = Array(inst.immediate)
        .map(b => b.toString(16).padStart(2, '0'))
        .join('')
      line += ` 0x${hex}`
    }

    if (info) {
      line += ` // gas: ${info.gasCost}, stack: ${info.stackInputs}${info.stackOutputs}`
    }

    lines.push(line)
  }

  return lines
}

// Example output:
// 0000: PUSH1 0x80 // gas: 3, stack: 0 → 1
// 0002: PUSH1 0x40 // gas: 3, stack: 0 → 1
// 0004: MSTORE // gas: 3, stack: 2 → 0

Jump Analysis

Build Jump Table

function buildJumpTable(bytecode: Uint8Array): Map<number, number[]> {
  const instructions = Opcode.parse(bytecode)
  const jumpTable = new Map<number, number[]>()
  const validDests = Opcode.jumpDests(bytecode)

  // Mark all valid jump destinations
  for (const dest of validDests) {
    jumpTable.set(dest, [])
  }

  // Find all JUMP/JUMPI instructions
  // Note: This is simplified - real analysis needs symbolic execution
  for (const inst of instructions) {
    if (Opcode.isJump(inst.opcode)) {
      // In real implementation, would trace stack to find jump target
      console.log(`Jump instruction at ${inst.offset}`)
    }
  }

  return jumpTable
}

Validate Jump Destinations

function validateJumpDestinations(bytecode: Uint8Array): {
  valid: boolean
  errors: string[]
} {
  const validDests = Opcode.jumpDests(bytecode)
  const errors: string[] = []

  // Check if JUMPDEST instructions are actually reachable
  // (simplified - real analysis needs control flow graph)

  if (validDests.size === 0) {
    errors.push("No JUMPDEST instructions found")
  }

  return {
    valid: errors.length === 0,
    errors
  }
}

Gas Analysis

Estimate Static Gas Cost

function estimateStaticGas(bytecode: Uint8Array): bigint {
  const instructions = Opcode.parse(bytecode)
  let total = 0n

  for (const inst of instructions) {
    const cost = Opcode.getGasCost(inst.opcode)
    if (cost !== undefined) {
      total += BigInt(cost)
    }
  }

  return total
}

Find Gas-Intensive Operations

interface GasHotspot {
  offset: number
  opcode: BrandedOpcode
  name: string
  gasCost: number
}

function findGasHotspots(bytecode: Uint8Array, threshold: number = 100): GasHotspot[] {
  const instructions = Opcode.parse(bytecode)
  const hotspots: GasHotspot[] = []

  for (const inst of instructions) {
    const cost = Opcode.getGasCost(inst.opcode)
    const name = Opcode.name(inst.opcode)

    if (cost !== undefined && cost >= threshold && name) {
      hotspots.push({
        offset: inst.offset,
        opcode: inst.opcode,
        name,
        gasCost: cost
      })
    }
  }

  return hotspots.sort((a, b) => b.gasCost - a.gasCost)
}

Stack Analysis

Track Stack Depth

interface StackTrace {
  offset: number
  opcode: string
  depth: number
  effect: number
}

function traceStackDepth(bytecode: Uint8Array): StackTrace[] {
  const instructions = Opcode.parse(bytecode)
  const trace: StackTrace[] = []
  let depth = 0

  for (const inst of instructions) {
    const effect = Opcode.getStackEffect(inst.opcode) ?? 0
    const name = Opcode.name(inst.opcode) ?? "UNKNOWN"

    depth += effect

    trace.push({
      offset: inst.offset,
      opcode: name,
      depth,
      effect
    })

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

  return trace
}

Find Maximum Stack Depth

function findMaxStackDepth(bytecode: Uint8Array): number {
  const trace = traceStackDepth(bytecode)
  return Math.max(...trace.map(t => t.depth))
}

Pattern Detection

Detect Compiler Patterns

function detectSolidityConstructor(bytecode: Uint8Array): boolean {
  const instructions = Opcode.parse(bytecode)

  // Solidity constructors typically start with:
  // PUSH1 0x80
  // PUSH1 0x40
  // MSTORE
  if (instructions.length < 3) return false

  const [inst1, inst2, inst3] = instructions

  return (
    inst1.opcode === Opcode.PUSH1 &&
    inst1.immediate?.[0] === 0x80 &&
    inst2.opcode === Opcode.PUSH1 &&
    inst2.immediate?.[0] === 0x40 &&
    inst3.opcode === Opcode.MSTORE
  )
}

Find Function Selectors

function findFunctionSelectors(bytecode: Uint8Array): Set<string> {
  const instructions = Opcode.parse(bytecode)
  const selectors = new Set<string>()

  // Look for PUSH4 instructions (4-byte selectors)
  for (const inst of instructions) {
    if (inst.opcode === Opcode.PUSH4 && inst.immediate) {
      const selector = '0x' + Array(inst.immediate)
        .map(b => b.toString(16).padStart(2, '0'))
        .join('')
      selectors.add(selector)
    }
  }

  return selectors
}

Detect Optimization Patterns

interface OptimizationPattern {
  name: string
  pattern: BrandedOpcode[]
  description: string
}

const OPTIMIZATION_PATTERNS: OptimizationPattern[] = [
  {
    name: "DUP + SWAP elimination",
    pattern: [Opcode.DUP1, Opcode.SWAP1],
    description: "Redundant DUP1 followed by SWAP1"
  },
  {
    name: "Double PUSH",
    pattern: [Opcode.PUSH1, Opcode.POP],
    description: "PUSH immediately followed by POP"
  }
]

function findOptimizationOpportunities(bytecode: Uint8Array): string[] {
  const instructions = Opcode.parse(bytecode)
  const opportunities: string[] = []

  for (let i = 0; i < instructions.length - 1; i++) {
    for (const pattern of OPTIMIZATION_PATTERNS) {
      let matches = true
      for (let j = 0; j < pattern.pattern.length && i + j < instructions.length; j++) {
        if (instructions[i + j].opcode !== pattern.pattern[j]) {
          matches = false
          break
        }
      }

      if (matches) {
        opportunities.push(
          `${pattern.name} at offset ${instructions[i].offset}: ${pattern.description}`
        )
      }
    }
  }

  return opportunities
}

Code Coverage

Track Executed Instructions

class BytecodeCoverage {
  private executed = new Set<number>()
  private instructions: Instruction[]

  constructor(bytecode: Uint8Array) {
    this.instructions = Opcode.parse(bytecode)
  }

  markExecuted(offset: number): void {
    this.executed.add(offset)
  }

  getCoveragePercent(): number {
    return (this.executed.size / this.instructions.length) * 100
  }

  getUncoveredInstructions(): Instruction[] {
    return this.instructions.filter(inst => !this.executed.has(inst.offset))
  }

  report(): string {
    const covered = this.executed.size
    const total = this.instructions.length
    const percent = this.getCoveragePercent().toFixed(2)

    return `Coverage: ${covered}/${total} instructions (${percent}%)`
  }
}

Bytecode Comparison

Compare Bytecode

function compareBytecode(
  bytecode1: Uint8Array,
  bytecode2: Uint8Array
): {
  identical: boolean
  differences: Array<{
    offset: number
    opcode1: string
    opcode2: string
  }>
} {
  const inst1 = Opcode.parse(bytecode1)
  const inst2 = Opcode.parse(bytecode2)

  if (inst1.length !== inst2.length) {
    return {
      identical: false,
      differences: [{ offset: -1, opcode1: `${inst1.length} instructions`, opcode2: `${inst2.length} instructions` }]
    }
  }

  const differences: Array<{ offset: number; opcode1: string; opcode2: string }> = []

  for (let i = 0; i < inst1.length; i++) {
    if (inst1[i].opcode !== inst2[i].opcode) {
      differences.push({
        offset: inst1[i].offset,
        opcode1: Opcode.name(inst1[i].opcode) ?? `0x${inst1[i].opcode.toString(16)}`,
        opcode2: Opcode.name(inst2[i].opcode) ?? `0x${inst2[i].opcode.toString(16)}`
      })
    }
  }

  return {
    identical: differences.length === 0,
    differences
  }
}

Metadata Extraction

Extract Contract Metadata

function extractMetadata(bytecode: Uint8Array): {
  hasMetadata: boolean
  metadataHash?: string
  codeSize: number
} {
  // Solidity adds metadata at end: 0xa1 0x65 'b' 'z' 'z' 'r' '0' 0x58 0x20 &lt;32 bytes> 0x00 0x29
  const codeSize = bytecode.length

  // Check for metadata marker
  if (bytecode.length < 43) {
    return { hasMetadata: false, codeSize }
  }

  const metadataStart = bytecode.length - 43
  if (
    bytecode[metadataStart] === 0xa1 &&
    bytecode[metadataStart + 1] === 0x65 &&
    bytecode[metadataStart + 2] === 0x62 && // 'b'
    bytecode[metadataStart + 3] === 0x7a && // 'z'
    bytecode[metadataStart + 4] === 0x7a && // 'z'
    bytecode[metadataStart + 5] === 0x72 && // 'r'
    bytecode[metadataStart + 6] === 0x30    // '0'
  ) {
    const hash = bytecode.slice(metadataStart + 9, metadataStart + 41)
    const metadataHash = '0x' + Array(hash)
      .map(b => b.toString(16).padStart(2, '0'))
      .join('')

    return {
      hasMetadata: true,
      metadataHash,
      codeSize: metadataStart
    }
  }

  return { hasMetadata: false, codeSize }
}
Bytecode Analysis Performance: For large contracts (>10KB), consider parsing once and caching the instruction array. Reparsing on every analysis is wasteful.

Control Flow Analysis

Build Basic Blocks

interface BasicBlock {
  start: number
  end: number
  instructions: Instruction[]
  endsWithJump: boolean
  endsWithTerminator: boolean
}

function buildBasicBlocks(bytecode: Uint8Array): BasicBlock[] {
  const instructions = Opcode.parse(bytecode)
  const jumpDests = Opcode.jumpDests(bytecode)
  const blocks: BasicBlock[] = []

  let blockStart = 0
  let blockInstructions: Instruction[] = []

  for (let i = 0; i < instructions.length; i++) {
    const inst = instructions[i]
    blockInstructions.push(inst)

    // Block ends at: JUMP, JUMPI, terminator, or before JUMPDEST
    const isJump = Opcode.isJump(inst.opcode)
    const isTerminator = Opcode.isTerminating(inst.opcode)
    const nextIsJumpDest = i < instructions.length - 1 && jumpDests.has(instructions[i + 1].offset)

    if (isJump || isTerminator || nextIsJumpDest) {
      blocks.push({
        start: blockStart,
        end: inst.offset,
        instructions: blockInstructions,
        endsWithJump: isJump,
        endsWithTerminator: isTerminator
      })

      blockInstructions = []
      if (i < instructions.length - 1) {
        blockStart = instructions[i + 1].offset
      }
    }
  }

  // Add remaining instructions as final block
  if (blockInstructions.length > 0) {
    const lastInst = blockInstructions[blockInstructions.length - 1]
    blocks.push({
      start: blockStart,
      end: lastInst.offset,
      instructions: blockInstructions,
      endsWithJump: false,
      endsWithTerminator: false
    })
  }

  return blocks
}

See Also