UNPKG

@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
/** * 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 };