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:
Occur frequently in compiler output (Solidity, Vyper, etc.)
Can be optimized by combining into single operations
Enable static analysis (e.g., PUSH+JUMP = static jump target)
Reveal semantics (e.g., function dispatch, callvalue checks)
Example
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:
Pattern Bytecode Meaning PUSH+ADD PUSH value, ADDAdd immediate PUSH+SUB PUSH value, SUBSubtract immediate PUSH+MUL PUSH value, MULMultiply immediate PUSH+DIV PUSH 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:
Pattern Bytecode Meaning PUSH+AND PUSH value, ANDBitwise AND with mask PUSH+OR PUSH value, ORBitwise OR with mask PUSH+XOR PUSH value, XORBitwise XOR with mask
Common use : Masking (e.g., PUSH 0xFF, AND = mask to byte)
3. Memory Fusions
Immediate memory access:
Pattern Bytecode Meaning PUSH+MLOAD PUSH offset, MLOADLoad from fixed address PUSH+MSTORE PUSH offset, MSTOREStore to fixed address PUSH+MSTORE8 PUSH 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:
Pattern Bytecode Meaning PUSH+JUMP PUSH target, JUMPStatic jump (target known!)PUSH+JUMPI PUSH target, JUMPIConditional jump to known target ISZERO+PUSH+JUMPI ISZERO, 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:
Pattern Bytecode Meaning DUP2+MSTORE+PUSH DUP2, MSTORE, PUSH valueMemory write pattern DUP3+ADD+MSTORE DUP3, ADD, MSTOREOffset calculation + store SWAP1+DUP2+ADD SWAP1, DUP2, ADDStack rearrange + add PUSH+DUP3+ADD PUSH value, DUP3, ADDImmediate + dup + add PUSH+ADD+DUP1 PUSH value, ADD, DUP1Add immediate + duplicate MLOAD+SWAP1+DUP2 MLOAD, 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:
Pattern Bytecode Meaning MULTI_PUSH PUSH1, PUSH1, PUSH1 (2-3x)Batch push values MULTI_POP POP, 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:
Pattern Bytecode Meaning FUNCTION_DISPATCH PUSH4 selector, EQ, PUSH target, JUMPIFunction selector check CALLVALUE_CHECK CALLVALUE, DUP1, ISZERONon-payable check PUSH0+REVERT PUSH0, 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 ( ` \n Found ${ 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 ( ', ' ) } ]` );
});
// 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 ( ` \n Function 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 === 0x40 n &&
inst2 ?. opcode === 'MLOAD' &&
inst3 ?. type === 'push' && inst3 . value === 0x20 n &&
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 };
}
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 === 1 n ) {
optimizations . push ( `INC at PC ${ inst . pc } ` ); // ADD 1 → INC
}
if ( inst . type === 'push_mul_fusion' && inst . value === 2 n ) {
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