@bcoders.gr/evm-disassembler
Version:
A comprehensive EVM bytecode disassembler and analyzer with support for multiple EVM versions
286 lines (247 loc) • 9.14 kB
JavaScript
/**
* Stack depth analysis for EVM bytecode
* @module stack-analyzer
*/
const { StackAnalysisError } = require('./errors');
const { STACK_EFFECTS } = require('./constants');
/**
* Analyze stack depth changes throughout bytecode execution
* @param {Array} instructions - Array of decoded instructions
* @returns {Object} Stack analysis results
*/
function analyzeStackDepth(instructions) {
let currentDepth = 0;
let maxDepth = 0;
let minDepth = 0;
const depthMap = new Map();
const errors = [];
for (let i = 0; i < instructions.length; i++) {
const inst = instructions[i];
const pc = inst.pc;
// Record depth at this PC
depthMap.set(pc, currentDepth);
// Get stack effect for this opcode
const effect = getStackEffect(inst);
if (effect) {
const [inputs, outputs] = effect;
// Check for stack underflow
if (currentDepth < inputs) {
errors.push({
pc,
opcode: inst.opcode,
error: 'Stack underflow',
required: inputs,
available: currentDepth
});
}
// Update stack depth
currentDepth = currentDepth - inputs + outputs;
// Track max and min depths
maxDepth = Math.max(maxDepth, currentDepth);
minDepth = Math.min(minDepth, currentDepth);
// Check for stack overflow (EVM stack limit is 1024)
if (currentDepth > 1024) {
errors.push({
pc,
opcode: inst.opcode,
error: 'Stack overflow',
depth: currentDepth
});
}
}
// Add stack depth to instruction
inst.stackDepthBefore = depthMap.get(pc);
inst.stackDepthAfter = currentDepth;
}
return {
maxDepth,
minDepth,
finalDepth: currentDepth,
depthMap,
errors,
hasErrors: errors.length > 0
};
}
/**
* Get stack effect for an instruction
* @param {Object} instruction - Decoded instruction
* @returns {Array|null} [inputs, outputs] or null if unknown
*/
function getStackEffect(instruction) {
const opcode = instruction.opcode;
// Handle DUP operations
if (opcode.startsWith('DUP')) {
const n = parseInt(opcode.substring(3)) || 1;
return [n, n + 1]; // Duplicates the nth stack item
}
// Handle SWAP operations
if (opcode.startsWith('SWAP')) {
const n = parseInt(opcode.substring(4)) || 1;
return [n + 1, n + 1]; // Swaps 1st and (n+1)th stack items
}
// Handle PUSH operations
if (opcode.startsWith('PUSH')) {
return [0, 1]; // Pushes one item onto stack
}
// Look up in predefined effects
return STACK_EFFECTS[opcode] || null;
}
/**
* Perform control flow analysis to track stack depth across jumps
* @param {Array} instructions - Array of decoded instructions
* @returns {Object} Control flow analysis results
*/
function analyzeControlFlow(instructions) {
const jumpDests = new Set();
const jumps = [];
const conditionalJumps = [];
const unreachableCode = new Set();
// First pass: identify all jump destinations
for (const inst of instructions) {
if (inst.opcode === 'JUMPDEST') {
jumpDests.add(inst.pc);
}
}
// Second pass: identify jumps and analyze reachability
let reachable = true;
for (let i = 0; i < instructions.length; i++) {
const inst = instructions[i];
if (inst.opcode === 'JUMP') {
jumps.push({
from: inst.pc,
instruction: inst
});
reachable = false; // Code after unconditional jump is unreachable
} else if (inst.opcode === 'JUMPI') {
conditionalJumps.push({
from: inst.pc,
instruction: inst
});
// Code after conditional jump is still reachable
} else if (inst.opcode === 'STOP' || inst.opcode === 'RETURN' ||
inst.opcode === 'REVERT' || inst.opcode === 'SELFDESTRUCT') {
reachable = false; // Code after terminating instruction is unreachable
} else if (inst.opcode === 'JUMPDEST') {
reachable = true; // Jump destinations are reachable
}
if (!reachable && inst.opcode !== 'JUMPDEST') {
unreachableCode.add(inst.pc);
}
inst.reachable = reachable;
}
return {
jumpDests: Array.from(jumpDests),
jumps,
conditionalJumps,
unreachableCode: Array.from(unreachableCode),
totalJumpDests: jumpDests.size,
totalJumps: jumps.length,
totalConditionalJumps: conditionalJumps.length,
unreachableInstructions: unreachableCode.size
};
}
/**
* Analyze stack patterns to identify common operations
* @param {Array} instructions - Array of decoded instructions
* @returns {Object} Pattern analysis results
*/
function analyzeStackPatterns(instructions) {
const patterns = {
functionSelectors: [],
storageAccesses: [],
memoryOperations: [],
externalCalls: [],
events: []
};
for (let i = 0; i < instructions.length; i++) {
const inst = instructions[i];
const nextInst = instructions[i + 1];
const prevInst = instructions[i - 1];
// Detect function selector pattern (PUSH4, DUP1, PUSH4, EQ)
if (inst.opcode === 'PUSH4' && nextInst && nextInst.opcode === 'DUP1') {
const selector = inst.pushData;
if (selector) {
patterns.functionSelectors.push({
pc: inst.pc,
selector: selector.substring(0, 8),
instruction: inst
});
}
}
// Detect storage access patterns
if (inst.opcode === 'SLOAD' || inst.opcode === 'SSTORE') {
patterns.storageAccesses.push({
pc: inst.pc,
operation: inst.opcode,
instruction: inst
});
}
// Detect memory operations
if (inst.opcode === 'MLOAD' || inst.opcode === 'MSTORE' || inst.opcode === 'MSTORE8') {
patterns.memoryOperations.push({
pc: inst.pc,
operation: inst.opcode,
instruction: inst
});
}
// Detect external calls
if (inst.opcode === 'CALL' || inst.opcode === 'DELEGATECALL' ||
inst.opcode === 'STATICCALL' || inst.opcode === 'CALLCODE') {
patterns.externalCalls.push({
pc: inst.pc,
callType: inst.opcode,
instruction: inst
});
}
// Detect event emissions (LOG0-LOG4)
if (inst.opcode.startsWith('LOG')) {
patterns.events.push({
pc: inst.pc,
logType: inst.opcode,
topicCount: parseInt(inst.opcode.substring(3)),
instruction: inst
});
}
}
return patterns;
}
/**
* Calculate complexity metrics for the bytecode
* @param {Array} instructions - Array of decoded instructions
* @param {Object} stackAnalysis - Stack analysis results
* @param {Object} controlFlow - Control flow analysis results
* @returns {Object} Complexity metrics
*/
function calculateComplexity(instructions, stackAnalysis, controlFlow) {
const opcodeFrequency = {};
let cyclomaticComplexity = 1; // Base complexity
// Count opcode frequencies
for (const inst of instructions) {
opcodeFrequency[inst.opcode] = (opcodeFrequency[inst.opcode] || 0) + 1;
}
// Calculate cyclomatic complexity (number of decision points + 1)
cyclomaticComplexity += controlFlow.totalConditionalJumps;
cyclomaticComplexity += (opcodeFrequency['JUMPI'] || 0);
// Calculate other metrics
const metrics = {
totalInstructions: instructions.length,
uniqueOpcodes: Object.keys(opcodeFrequency).length,
cyclomaticComplexity,
maxStackDepth: stackAnalysis.maxDepth,
jumpDensity: (controlFlow.totalJumps + controlFlow.totalConditionalJumps) / instructions.length,
unreachableRatio: controlFlow.unreachableInstructions / instructions.length,
opcodeFrequency,
mostFrequentOpcodes: Object.entries(opcodeFrequency)
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
.map(([opcode, count]) => ({ opcode, count, percentage: (count / instructions.length * 100).toFixed(2) }))
};
return metrics;
}
module.exports = {
analyzeStackDepth,
getStackEffect,
analyzeControlFlow,
analyzeStackPatterns,
calculateComplexity
};