Skip to main content

Try it Live

Run Bytecode examples in the interactive playground
Fusion detection identifies multi-instruction patterns that can be optimized, statically analyzed, or replaced with synthetic opcodes. These patterns are common in compiler-generated bytecode and provide opportunities for performance improvements and deeper analysis.

What are Fusions?

Fusions are sequences of 2-4 EVM instructions that:
  1. Occur frequently in compiler output (Solidity, Vyper, etc.)
  2. Can be optimized by combining into single operations
  3. Enable static analysis (e.g., PUSH+JUMP = static jump target)
  4. Reveal semantics (e.g., function dispatch, callvalue checks)

Example

// Solidity: x = x + 5
Compiles to:
PUSH1 0x05    // Push immediate value 5
ADD           // Add to stack top
This PUSH+ADD fusion can be detected and:
  • Optimized to single “add immediate” operation
  • Recognized as constant addition pattern
  • Analyzed for gas savings

Fusion Categories

1. Arithmetic Fusions

Immediate arithmetic operations:
PatternBytecodeMeaning
PUSH+ADDPUSH value, ADDAdd immediate
PUSH+SUBPUSH value, SUBSubtract immediate
PUSH+MULPUSH value, MULMultiply immediate
PUSH+DIVPUSH value, DIVDivide immediate
Example:
// Detect PUSH+ADD
for (const inst of code.scan({ detectFusions: true })) {
  if (inst.type === 'push_add_fusion') {
    console.log(`Add immediate: +${inst.value} at PC ${inst.pc}`);
  }
}

2. Bitwise Fusions

Immediate bitwise operations:
PatternBytecodeMeaning
PUSH+ANDPUSH value, ANDBitwise AND with mask
PUSH+ORPUSH value, ORBitwise OR with mask
PUSH+XORPUSH value, XORBitwise XOR with mask
Common use: Masking (e.g., PUSH 0xFF, AND = mask to byte)

3. Memory Fusions

Immediate memory access:
PatternBytecodeMeaning
PUSH+MLOADPUSH offset, MLOADLoad from fixed address
PUSH+MSTOREPUSH offset, MSTOREStore to fixed address
PUSH+MSTORE8PUSH offset, MSTORE8Store byte to fixed address
Example:
for (const inst of code.scan({ detectFusions: true })) {
  if (inst.type === 'push_mstore_fusion') {
    console.log(`Store to memory[${inst.value}]`);
  }
}

4. Control Flow Fusions

Static control flow:
PatternBytecodeMeaning
PUSH+JUMPPUSH target, JUMPStatic jump (target known!)
PUSH+JUMPIPUSH target, JUMPIConditional jump to known target
ISZERO+PUSH+JUMPIISZERO, PUSH target, JUMPIInverted conditional (Solidity if (!cond))
PUSH+JUMP fusions reveal compile-time jump targets. This enables:
  • Control flow graph construction without execution
  • Dead code detection
  • Function boundary identification
  • Jump target validation at compile time
Example:
// Extract all static jump targets
const jumpTargets = new Set<bigint>();

for (const inst of code.scan({ detectFusions: true })) {
  if (inst.type === 'push_jump_fusion') {
    jumpTargets.add(inst.value);
    console.log(`Static jump to PC ${inst.value}`);
  }
}

// Validate targets are JUMPDEST
jumpTargets.forEach(target => {
  const instruction = code.getInstructionAt(Number(target));
  if (instruction?.opcode !== 'JUMPDEST') {
    console.error(`Invalid jump target: ${target} is not JUMPDEST`);
  }
});

5. Stack Manipulation Fusions

Complex stack patterns:
PatternBytecodeMeaning
DUP2+MSTORE+PUSHDUP2, MSTORE, PUSH valueMemory write pattern
DUP3+ADD+MSTOREDUP3, ADD, MSTOREOffset calculation + store
SWAP1+DUP2+ADDSWAP1, DUP2, ADDStack rearrange + add
PUSH+DUP3+ADDPUSH value, DUP3, ADDImmediate + dup + add
PUSH+ADD+DUP1PUSH value, ADD, DUP1Add immediate + duplicate
MLOAD+SWAP1+DUP2MLOAD, SWAP1, DUP2Load + rearrange
These patterns appear in:
  • ABI encoding/decoding
  • Struct field access
  • Array element computation
  • Memory copying

6. Multi-Instruction Fusions

Sequences of same instruction:
PatternBytecodeMeaning
MULTI_PUSHPUSH1, PUSH1, PUSH1 (2-3x)Batch push values
MULTI_POPPOP, POP, POP (2-3x)Batch pop values
Example:
for (const inst of code.scan({ detectFusions: true })) {
  if (inst.type === 'multi_push') {
    console.log(`Push ${inst.count} values: ${inst.values.join(', ')}`);
  }
}

7. Solidity-Specific Patterns

High-level language patterns:
PatternBytecodeMeaning
FUNCTION_DISPATCHPUSH4 selector, EQ, PUSH target, JUMPIFunction selector check
CALLVALUE_CHECKCALLVALUE, DUP1, ISZERONon-payable check
PUSH0+REVERTPUSH0, PUSH0, REVERTEmpty revert (no error)

FUNCTION_DISPATCH

Extracts function selectors from Solidity function dispatcher:
const functions = new Map<number, bigint>();

for (const inst of code.scan({ detectFusions: true })) {
  if (inst.type === 'function_dispatch') {
    const selector = inst.selector;
    const target = inst.target;

    functions.set(selector, target);
    console.log(`Function 0x${selector.toString(16)} at PC ${target}`);
  }
}

console.log(`\nFound ${functions.size} functions`);
This enables:
  • ABI reconstruction from bytecode alone
  • Function boundary detection for decompilation
  • Selector collision detection
  • Gas profiling per function

CALLVALUE_CHECK

Detects non-payable function checks:
for (const inst of code.scan({ detectFusions: true })) {
  if (inst.type === 'callvalue_check') {
    console.log(`Non-payable check at PC ${inst.pc}`);
  }
}
Indicates Solidity function without payable modifier.

Detection API

Fusion detection integrates with iteration:
// Enable fusion detection
for (const inst of code.scan({ detectFusions: true })) {
  // inst.type includes fusion types
  switch (inst.type) {
    case 'push_add_fusion':
      console.log(`Add ${inst.value}`);
      break;

    case 'push_jump_fusion':
      console.log(`Static jump to ${inst.value}`);
      break;

    case 'function_dispatch':
      console.log(`Function ${inst.selector.toString(16)}`);
      break;

    default:
      console.log(`Regular: ${inst.type}`);
  }
}

Options

interface ScanOptions {
  /** Enable fusion pattern detection (default: false) */
  detectFusions?: boolean

  /** Minimum fusion length to detect (default: 2) */
  minFusionLength?: number

  /** Maximum fusion length to detect (default: 4) */
  maxFusionLength?: number

  /** Specific fusion types to detect (default: all) */
  fusionTypes?: FusionType[]
}

Selective Detection

// Only detect control flow fusions
for (const inst of code.scan({
  detectFusions: true,
  fusionTypes: ['push_jump_fusion', 'push_jumpi_fusion', 'iszero_jumpi']
})) {
  if (inst.type.includes('jump')) {
    console.log(`Control flow: ${inst.type}`);
  }
}

Usage Patterns

Fusion Statistics

const fusionCounts = new Map<string, number>();

for (const inst of code.scan({ detectFusions: true })) {
  if (inst.type.includes('fusion') || inst.type.includes('dispatch') || inst.type.includes('check')) {
    const count = fusionCounts.get(inst.type) || 0;
    fusionCounts.set(inst.type, count + 1);
  }
}

console.log('Fusion patterns detected:');
Array(fusionCounts.entries())
  .sort((a, b) => b[1] - a[1])
  .forEach(([type, count]) => {
    console.log(`  ${type}: ${count}x`);
  });

Optimization Opportunities

let gasSavings = 0;

for (const inst of code.scan({ detectFusions: true })) {
  switch (inst.type) {
    case 'push_add_fusion':
    case 'push_mul_fusion':
    case 'push_sub_fusion':
      // Each fusion saves ~3 gas (one instruction)
      gasSavings += 3;
      break;

    case 'multi_push':
      // Batch operations save gas
      gasSavings += (inst.count - 1) * 2;
      break;

    case 'push_jump_fusion':
      // Static jumps can be optimized further
      gasSavings += 5;
      break;
  }
}

console.log(`Potential gas savings: ${gasSavings} gas`);

Control Flow Graph from Fusions

// Build CFG using static jump detection
const cfg = new Map<number, number[]>(); // PC → successor PCs

let currentBlock = 0;
const successors: number[] = [];

for (const inst of code.scan({ detectFusions: true })) {
  if (inst.type === 'push_jump_fusion') {
    // Static jump - add edge
    successors.push(Number(inst.value));
    cfg.set(currentBlock, [...successors]);
    currentBlock = inst.pc + inst.original_length;
    successors.length = 0;
  } else if (inst.type === 'push_jumpi_fusion') {
    // Conditional - add both edges
    successors.push(Number(inst.value)); // Jump target
    successors.push(inst.pc + inst.original_length); // Fallthrough
    cfg.set(currentBlock, [...successors]);
    currentBlock = inst.pc + inst.original_length;
    successors.length = 0;
  } else if (inst.type === 'jumpdest') {
    currentBlock = inst.pc;
  }
}

console.log('Control flow graph:');
cfg.forEach((succs, pc) => {
  console.log(`  PC ${pc} → [${succs.join(', ')}]`);
});

Function Extraction

// Extract function bodies using dispatch pattern
const functions = new Map<number, { start: number; end: number }>();

for (const inst of code.scan({ detectFusions: true })) {
  if (inst.type === 'function_dispatch') {
    const selector = inst.selector;
    const start = Number(inst.target);

    // Find end (next JUMPDEST or end of code)
    let end = code.size();
    for (const next of code.scan()) {
      if (next.pc > start && next.type === 'jumpdest') {
        end = next.pc;
        break;
      }
    }

    functions.set(selector, { start, end });
  }
}

// Extract each function's bytecode
functions.forEach(({ start, end }, selector) => {
  const functionCode = code.slice(start, end);
  console.log(`\nFunction 0x${selector.toString(16)}:`);
  console.log(functionCode.prettyPrint({ compact: true }));
});

Pattern Frequency Analysis

// Identify most common patterns for optimization priority
const patterns = new Map<string, { count: number; gasCost: number }>();

for (const inst of code.scan({ detectFusions: true })) {
  if (inst.type.includes('fusion')) {
    const entry = patterns.get(inst.type) || { count: 0, gasCost: 0 };

    entry.count++;

    // Estimate gas for pattern
    switch (inst.type) {
      case 'push_add_fusion':
      case 'push_sub_fusion':
      case 'push_mul_fusion':
        entry.gasCost += 6; // PUSH(3) + ADD/SUB/MUL(3)
        break;
      case 'push_jump_fusion':
        entry.gasCost += 11; // PUSH(3) + JUMP(8)
        break;
      // ... other patterns
    }

    patterns.set(inst.type, entry);
  }
}

// Sort by total gas impact
const sorted = Array(patterns.entries())
  .map(([type, { count, gasCost }]) => ({
    type,
    count,
    totalGas: gasCost,
    avgGas: gasCost / count
  }))
  .sort((a, b) => b.totalGas - a.totalGas);

console.log('Patterns by gas impact:');
sorted.forEach(({ type, count, totalGas }) => {
  console.log(`  ${type}: ${count}x (${totalGas} gas total)`);
});

Integration with Other APIs

With prettyPrint

Pretty print annotates fusions with ⚡ symbol:
const output = code.prettyPrint({
  showFusions: true,
  colors: true
});

console.log(output);

// Output:
//    1 |    0 | ⚡ 60 01  | PUSH1     | 0x1 ⚡ PUSH+ADD
//    2 |    2 |    01     | ADD       | [gas: 3]

With analyzeBlocks

Detect fusions within blocks:
const blocks = code.analyzeBlocks();

blocks.forEach(block => {
  const blockFusions: OpcodeData[] = [];

  for (const inst of code.scan({ detectFusions: true })) {
    if (inst.pc >= block.startPc && inst.pc < block.endPc) {
      if (inst.type.includes('fusion')) {
        blockFusions.push(inst);
      }
    }
  }

  if (blockFusions.length > 0) {
    console.log(`Block ${block.index}: ${blockFusions.length} fusions`);
    blockFusions.forEach(f => console.log(`  ${f.type} at PC ${f.pc}`));
  }
});

With analyzeGas

Estimate gas savings from fusion optimization:
const gasAnalysis = code.analyzeGas();
let fusionGasSavings = 0;

for (const inst of code.scan({ detectFusions: true })) {
  if (inst.type.includes('fusion')) {
    // Each fusion saves 1-2 instructions worth of gas
    fusionGasSavings += 3;
  }
}

const optimizedGas = Number(gasAnalysis.total) - fusionGasSavings;
console.log(`Current gas: ${gasAnalysis.total}`);
console.log(`Potential savings: ${fusionGasSavings}`);
console.log(`Optimized gas: ${optimizedGas}`);

Advanced Patterns

Custom Fusion Detection

Implement custom pattern matching:
function detectCustomPattern(code: BrandedBytecode): CustomPattern[] {
  const patterns: CustomPattern[] = [];

  // Collect instructions for lookahead
  const instructions = Array(code.scan({ detectFusions: true }));

  for (let i = 0; i < instructions.length; i++) {
    const inst1 = instructions[i];
    const inst2 = instructions[i + 1];
    const inst3 = instructions[i + 2];
    const inst4 = instructions[i + 3];

    // Custom pattern: PUSH1 0x40, MLOAD, PUSH1 0x20, ADD
    // (Common "get free memory pointer + 32" pattern)
    if (
      inst1.type === 'push' && inst1.value === 0x40n &&
      inst2?.opcode === 'MLOAD' &&
      inst3?.type === 'push' && inst3.value === 0x20n &&
      inst4?.opcode === 'ADD'
    ) {
      patterns.push({
        type: 'free_memory_alloc',
        pc: inst1.pc,
        size: 32
      });
    }
  }

  return patterns;
}

Fusion-Based Decompilation

Use fusions to identify high-level constructs:
interface HighLevelConstruct {
  type: 'function' | 'modifier' | 'constructor' | 'fallback';
  selector?: number;
  startPc: number;
  endPc: number;
  hasCallvalueCheck: boolean;
}

function identifyConstructs(code: BrandedBytecode): HighLevelConstruct[] {
  const constructs: HighLevelConstruct[] = [];
  let currentConstruct: Partial<HighLevelConstruct> | null = null;

  for (const inst of code.scan({ detectFusions: true })) {
    if (inst.type === 'function_dispatch') {
      if (currentConstruct) {
        constructs.push(currentConstruct as HighLevelConstruct);
      }

      currentConstruct = {
        type: 'function',
        selector: inst.selector,
        startPc: Number(inst.target),
        endPc: 0,
        hasCallvalueCheck: false
      };
    } else if (inst.type === 'callvalue_check' && currentConstruct) {
      currentConstruct.hasCallvalueCheck = true;
    }
  }

  return constructs;
}

Compiler Fingerprinting

Different compilers generate different fusion patterns:
interface CompilerFingerprint {
  name: string;
  version?: string;
  confidence: number;
}

function detectCompiler(code: BrandedBytecode): CompilerFingerprint {
  const patterns = {
    push0Revert: 0,
    functionDispatch: 0,
    callvalueCheck: 0,
    multiPush: 0
  };

  for (const inst of code.scan({ detectFusions: true })) {
    if (inst.type === 'push0_revert') patterns.push0Revert++;
    if (inst.type === 'function_dispatch') patterns.functionDispatch++;
    if (inst.type === 'callvalue_check') patterns.callvalueCheck++;
    if (inst.type === 'multi_push') patterns.multiPush++;
  }

  // Solidity 0.8.0+ uses PUSH0 (EIP-3855)
  if (patterns.push0Revert > 0 && patterns.functionDispatch > 0) {
    return {
      name: 'Solidity',
      version: '>=0.8.0',
      confidence: 0.95
    };
  }

  // Vyper tends to have fewer fusion opportunities
  if (patterns.functionDispatch === 0 && patterns.multiPush > 5) {
    return {
      name: 'Vyper',
      confidence: 0.7
    };
  }

  return { name: 'Unknown', confidence: 0.0 };
}

Performance

Detection Overhead

Fusion detection adds minimal overhead:
  • Disabled: ~0.5ms per 1KB bytecode
  • Enabled: ~0.8ms per 1KB bytecode
  • Overhead: ~60% (still sub-millisecond for most contracts)
Enable fusion detection only when needed. For simple iteration, leave disabled for maximum performance.

Caching

Fusion analysis is deterministic - results can be cached:
const fusionCache = new WeakMap<BrandedBytecode, OpcodeData[]>();

function getFusions(code: BrandedBytecode): OpcodeData[] {
  if (fusionCache.has(code)) {
    return fusionCache.get(code)!;
  }

  const fusions: OpcodeData[] = [];
  for (const inst of code.scan({ detectFusions: true })) {
    if (inst.type.includes('fusion') || inst.type.includes('dispatch')) {
      fusions.push(inst);
    }
  }

  fusionCache.set(code, fusions);
  return fusions;
}

Limitations

Fusion detection is based on sequential pattern matching and cannot:
  • Detect patterns across basic blocks - Limited to same block
  • Handle data dependencies - Only structural patterns
  • Account for runtime values - Only compile-time constants
  • Detect semantically equivalent variants - Only exact patterns
Results represent syntactic patterns, not semantic equivalence.

What’s Detected

✅ Sequential instruction patterns (2-4 instructions) ✅ Immediate values in PUSH instructions ✅ Static jump targets ✅ Function selectors (4-byte constants) ✅ Common compiler idioms

What’s Not Detected

❌ Semantically equivalent but structurally different patterns ❌ Patterns spanning multiple basic blocks ❌ Data-dependent patterns ❌ Runtime-computed patterns

Use Cases

1. Bytecode Optimization

Identify patterns for optimization passes:
const optimizations: string[] = [];

for (const inst of code.scan({ detectFusions: true })) {
  if (inst.type === 'push_add_fusion' && inst.value === 1n) {
    optimizations.push(`INC at PC ${inst.pc}`); // ADD 1 → INC
  }

  if (inst.type === 'push_mul_fusion' && inst.value === 2n) {
    optimizations.push(`SHL at PC ${inst.pc}`); // MUL 2 → SHL 1
  }
}

console.log(`Found ${optimizations.length} optimization opportunities`);

2. Security Analysis

Detect suspicious patterns:
const suspiciousPatterns: string[] = [];

for (const inst of code.scan({ detectFusions: true })) {
  // Multiple callvalue checks (unusual)
  if (inst.type === 'callvalue_check') {
    suspiciousPatterns.push('Multiple callvalue checks detected');
  }

  // Function dispatch with very high selector (collision attempt?)
  if (inst.type === 'function_dispatch' && inst.selector > 0xFFFFFF00) {
    suspiciousPatterns.push(`Suspicious selector: 0x${inst.selector.toString(16)}`);
  }
}

3. Reverse Engineering

Reconstruct contract structure:
// Build function map from fusions
const contractStructure = {
  functions: new Map<number, { pc: number; isPayable: boolean }>(),
  constructor: null as number | null,
  fallback: null as number | null
};

for (const inst of code.scan({ detectFusions: true })) {
  if (inst.type === 'function_dispatch') {
    contractStructure.functions.set(inst.selector, {
      pc: Number(inst.target),
      isPayable: false // Updated by callvalue_check
    });
  }

  if (inst.type === 'callvalue_check') {
    // Mark previous function as non-payable
    // (implementation detail - track context)
  }
}

console.log(`Contract has ${contractStructure.functions.size} functions`);

See Also