Skip to main content

sentri_analyzer_evm/
bytecode.rs

1#![allow(missing_docs)]
2//! EVM Bytecode analysis and disassembly.
3//!
4//! Analyzes compiled EVM bytecode to detect:
5//! - Low-level vulnerabilities
6//! - Opcode patterns
7//! - Assembly-level issues
8//! - Gas optimization opportunities
9
10use crate::errors::{AnalysisError, AnalysisResult};
11use serde::{Deserialize, Serialize};
12use tracing::{debug, info};
13
14/// EVM opcode enumeration.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
16pub enum Opcode {
17    // Arithmetic
18    Add,
19    Sub,
20    Mul,
21    Div,
22    Sdiv,
23    Mod,
24    Smod,
25    Addmod,
26    Mulmod,
27    Exp,
28    SignExtend,
29
30    // Comparison
31    Lt,
32    Gt,
33    Slt,
34    Sgt,
35    Eq,
36    IsZero,
37    And,
38    Or,
39    Xor,
40    Not,
41    Byte,
42    Shl,
43    Shr,
44    Sar,
45
46    // Cryptographic
47    Keccak256,
48
49    // Stack
50    Pop,
51    Mload,
52    Mstore,
53    Mstore8,
54    Sload,
55    Sstore,
56    Msize,
57
58    // Flow
59    Pc,
60    Gas,
61    Jump,
62    Jumpi,
63    Jumpdest,
64    Return,
65    Revert,
66    Selfdestruct,
67
68    // Calls
69    Call,
70    Callcode,
71    Delegatecall,
72    Staticcall,
73    Create,
74    Create2,
75
76    // Data
77    Calldataload,
78    Calldatasize,
79    Calldatacopy,
80    Codesize,
81    Codecopy,
82    Extcodesize,
83    Extcodecopy,
84    Extcodehash,
85    Returndatasize,
86    Returndatacopy,
87
88    // Environment
89    Address,
90    Balance,
91    Origin,
92    Caller,
93    Callvalue,
94    Gasprice,
95    Blockcoinhash,
96    Coinbase,
97    Timestamp,
98    Number,
99    Difficulty,
100    Gaslimit,
101    Chainid,
102    Selfbalance,
103
104    // Push/Dup
105    Push(u8),
106    Dup(u8),
107    Swap(u8),
108    Log(u8),
109
110    // Special
111    Nop,
112    Invalid,
113    Unknown(u8),
114}
115
116impl Opcode {
117    /// Parse opcode from byte.
118    pub fn from_byte(byte: u8) -> Self {
119        match byte {
120            0x00 => Opcode::Nop,
121            0x01 => Opcode::Add,
122            0x02 => Opcode::Mul,
123            0x03 => Opcode::Sub,
124            0x04 => Opcode::Div,
125            0x05 => Opcode::Sdiv,
126            0x06 => Opcode::Mod,
127            0x07 => Opcode::Smod,
128            0x08 => Opcode::Addmod,
129            0x09 => Opcode::Mulmod,
130            0x0a => Opcode::Exp,
131            0x0b => Opcode::SignExtend,
132            0x10 => Opcode::Lt,
133            0x11 => Opcode::Gt,
134            0x12 => Opcode::Slt,
135            0x13 => Opcode::Sgt,
136            0x14 => Opcode::Eq,
137            0x15 => Opcode::IsZero,
138            0x16 => Opcode::And,
139            0x17 => Opcode::Or,
140            0x18 => Opcode::Xor,
141            0x19 => Opcode::Not,
142            0x1a => Opcode::Byte,
143            0x1b => Opcode::Shl,
144            0x1c => Opcode::Shr,
145            0x1d => Opcode::Sar,
146            0x20 => Opcode::Keccak256,
147            0x50 => Opcode::Pop,
148            0x51 => Opcode::Mload,
149            0x52 => Opcode::Mstore,
150            0x53 => Opcode::Mstore8,
151            0x54 => Opcode::Sload,
152            0x55 => Opcode::Sstore,
153            0x56 => Opcode::Jump,
154            0x57 => Opcode::Jumpi,
155            0x58 => Opcode::Pc,
156            0x59 => Opcode::Msize,
157            0x5a => Opcode::Gas,
158            0x5b => Opcode::Jumpdest,
159            0xf0 => Opcode::Create,
160            0xf1 => Opcode::Call,
161            0xf2 => Opcode::Callcode,
162            0xf3 => Opcode::Return,
163            0xf4 => Opcode::Delegatecall,
164            0xf5 => Opcode::Create2,
165            0xfa => Opcode::Staticcall,
166            0xfd => Opcode::Revert,
167            0xfe => Opcode::Invalid,
168            0xff => Opcode::Selfdestruct,
169            b if (0x60..=0x7f).contains(&b) => Opcode::Push(b - 0x60 + 1),
170            b if (0x80..=0x8f).contains(&b) => Opcode::Dup(b - 0x80 + 1),
171            b if (0x90..=0x9f).contains(&b) => Opcode::Swap(b - 0x90 + 1),
172            b if (0xa0..=0xa4).contains(&b) => Opcode::Log(b - 0xa0),
173            _ => Opcode::Unknown(byte),
174        }
175    }
176
177    /// Get opcode name.
178    pub fn name(&self) -> &'static str {
179        match self {
180            Opcode::Add => "ADD",
181            Opcode::Sub => "SUB",
182            Opcode::Mul => "MUL",
183            Opcode::Div => "DIV",
184            Opcode::Sdiv => "SDIV",
185            Opcode::Mod => "MOD",
186            Opcode::Smod => "SMOD",
187            Opcode::Addmod => "ADDMOD",
188            Opcode::Mulmod => "MULMOD",
189            Opcode::Exp => "EXP",
190            Opcode::SignExtend => "SIGNEXTEND",
191            Opcode::Lt => "LT",
192            Opcode::Gt => "GT",
193            Opcode::Slt => "SLT",
194            Opcode::Sgt => "SGT",
195            Opcode::Eq => "EQ",
196            Opcode::IsZero => "ISZERO",
197            Opcode::And => "AND",
198            Opcode::Or => "OR",
199            Opcode::Xor => "XOR",
200            Opcode::Not => "NOT",
201            Opcode::Byte => "BYTE",
202            Opcode::Shl => "SHL",
203            Opcode::Shr => "SHR",
204            Opcode::Sar => "SAR",
205            Opcode::Keccak256 => "KECCAK256",
206            Opcode::Pop => "POP",
207            Opcode::Mload => "MLOAD",
208            Opcode::Mstore => "MSTORE",
209            Opcode::Mstore8 => "MSTORE8",
210            Opcode::Sload => "SLOAD",
211            Opcode::Sstore => "SSTORE",
212            Opcode::Pc => "PC",
213            Opcode::Msize => "MSIZE",
214            Opcode::Gas => "GAS",
215            Opcode::Jump => "JUMP",
216            Opcode::Jumpi => "JUMPI",
217            Opcode::Jumpdest => "JUMPDEST",
218            Opcode::Return => "RETURN",
219            Opcode::Revert => "REVERT",
220            Opcode::Selfdestruct => "SELFDESTRUCT",
221            Opcode::Call => "CALL",
222            Opcode::Callcode => "CALLCODE",
223            Opcode::Delegatecall => "DELEGATECALL",
224            Opcode::Staticcall => "STATICCALL",
225            Opcode::Create => "CREATE",
226            Opcode::Create2 => "CREATE2",
227            Opcode::Calldataload => "CALLDATALOAD",
228            Opcode::Calldatasize => "CALLDATASIZE",
229            Opcode::Calldatacopy => "CALLDATACOPY",
230            Opcode::Codesize => "CODESIZE",
231            Opcode::Codecopy => "CODECOPY",
232            Opcode::Extcodesize => "EXTCODESIZE",
233            Opcode::Extcodecopy => "EXTCODECOPY",
234            Opcode::Extcodehash => "EXTCODEHASH",
235            Opcode::Returndatasize => "RETURNDATASIZE",
236            Opcode::Returndatacopy => "RETURNDATACOPY",
237            Opcode::Address => "ADDRESS",
238            Opcode::Balance => "BALANCE",
239            Opcode::Origin => "ORIGIN",
240            Opcode::Caller => "CALLER",
241            Opcode::Callvalue => "CALLVALUE",
242            Opcode::Gasprice => "GASPRICE",
243            Opcode::Blockcoinhash => "BLOCKHASH",
244            Opcode::Coinbase => "COINBASE",
245            Opcode::Timestamp => "TIMESTAMP",
246            Opcode::Number => "NUMBER",
247            Opcode::Difficulty => "DIFFICULTY",
248            Opcode::Gaslimit => "GASLIMIT",
249            Opcode::Chainid => "CHAINID",
250            Opcode::Selfbalance => "SELFBALANCE",
251            Opcode::Push(_) => "PUSH",
252            Opcode::Dup(_) => "DUP",
253            Opcode::Swap(_) => "SWAP",
254            Opcode::Log(_) => "LOG",
255            Opcode::Nop => "NOP",
256            Opcode::Invalid => "INVALID",
257            Opcode::Unknown(_) => "UNKNOWN",
258        }
259    }
260
261    /// Check if opcode is a jump.
262    pub fn is_jump(&self) -> bool {
263        matches!(self, Opcode::Jump | Opcode::Jumpi)
264    }
265
266    /// Check if opcode is a call.
267    pub fn is_call(&self) -> bool {
268        matches!(
269            self,
270            Opcode::Call
271                | Opcode::Callcode
272                | Opcode::Delegatecall
273                | Opcode::Staticcall
274                | Opcode::Create
275                | Opcode::Create2
276        )
277    }
278
279    /// Check if opcode mutates state.
280    pub fn mutates_state(&self) -> bool {
281        matches!(self, Opcode::Sstore | Opcode::Create | Opcode::Create2)
282    }
283}
284
285/// Bytecode instruction.
286#[derive(Debug, Clone, Serialize, Deserialize)]
287pub struct Instruction {
288    /// Program counter (offset in bytecode).
289    pub pc: usize,
290    /// Opcode.
291    pub opcode: Opcode,
292    /// Immediates (for PUSH instructions).
293    pub immediates: Vec<u8>,
294}
295
296impl Instruction {
297    /// Create instruction.
298    pub fn new(pc: usize, opcode: Opcode, immediates: Vec<u8>) -> Self {
299        Self {
300            pc,
301            opcode,
302            immediates,
303        }
304    }
305
306    /// Disassemble to string.
307    pub fn disassemble(&self) -> String {
308        match &self.opcode {
309            Opcode::Push(_) => {
310                let value = self
311                    .immediates
312                    .iter()
313                    .fold(String::new(), |acc, b| format!("{}{:02x}", acc, b));
314                format!("PUSH {} 0x{}", self.immediates.len(), value)
315            }
316            Opcode::Dup(n) => format!("DUP{}", n),
317            Opcode::Swap(n) => format!("SWAP{}", n),
318            Opcode::Log(n) => format!("LOG{}", n),
319            _ => self.opcode.name().to_string(),
320        }
321    }
322}
323
324/// Bytecode disassembler and analyzer.
325pub struct BytecodeAnalyzer;
326
327impl BytecodeAnalyzer {
328    /// Disassemble bytecode (hex string) to instructions.
329    pub fn disassemble(bytecode: &str) -> AnalysisResult<Vec<Instruction>> {
330        debug!("Disassembling bytecode ({} chars)", bytecode.len());
331
332        // Remove 0x prefix if present
333        let bytecode = bytecode.strip_prefix("0x").unwrap_or(bytecode);
334
335        // Parse hex string to bytes
336        let bytes = Self::hex_to_bytes(bytecode)
337            .map_err(|e| AnalysisError::bytecode(format!("Invalid bytecode hex: {}", e)))?;
338
339        let mut instructions = Vec::new();
340        let mut pc = 0;
341
342        while pc < bytes.len() {
343            let byte = bytes[pc];
344            let opcode = Opcode::from_byte(byte);
345
346            let mut immediates = Vec::new();
347            let mut next_pc = pc + 1;
348
349            // Handle PUSH instructions
350            if let Opcode::Push(n) = opcode {
351                let push_size = n as usize;
352                if pc + 1 + push_size <= bytes.len() {
353                    immediates = bytes[pc + 1..pc + 1 + push_size].to_vec();
354                    next_pc = pc + 1 + push_size;
355                }
356            }
357
358            instructions.push(Instruction::new(pc, opcode, immediates));
359            pc = next_pc;
360        }
361
362        info!("Disassembled {} instructions", instructions.len());
363        Ok(instructions)
364    }
365
366    /// Convert hex string to bytes.
367    fn hex_to_bytes(hex: &str) -> Result<Vec<u8>, String> {
368        if !hex.len().is_multiple_of(2) {
369            return Err("Odd-length hex string".to_string());
370        }
371
372        (0..hex.len())
373            .step_by(2)
374            .map(|i| {
375                u8::from_str_radix(&hex[i..i + 2], 16)
376                    .map_err(|_| format!("Invalid hex: {}", &hex[i..i + 2]))
377            })
378            .collect()
379    }
380
381    /// Analyze bytecode for vulnerabilities.
382    pub fn analyze(bytecode: &str) -> AnalysisResult<BytecodeAnalysis> {
383        let instructions = Self::disassemble(bytecode)?;
384
385        let mut analysis = BytecodeAnalysis::new();
386
387        for (i, instr) in instructions.iter().enumerate() {
388            // Track calls
389            if instr.opcode.is_call() {
390                analysis.calls.push(instr.pc);
391            }
392
393            // Track jumps
394            if instr.opcode.is_jump() {
395                analysis.jumps.push(instr.pc);
396            }
397
398            // Track state mutations
399            if instr.opcode.mutates_state() {
400                analysis.state_mutations.push(instr.pc);
401            }
402
403            // Detect unsafe call patterns
404            if i > 0 && instr.opcode.is_call() {
405                let prev = &instructions[i - 1];
406                if !matches!(prev.opcode, Opcode::Gas) {
407                    // Missing gas specification
408                    analysis.issues.push(BytecodeIssue {
409                        severity: Severity::Medium,
410                        issue_type: IssueType::UnsafeCall,
411                        pc: instr.pc,
412                        description: "Call without explicit gas limit".to_string(),
413                    });
414                }
415            }
416
417            // Detect reentrancy patterns
418            if instr.opcode.is_call() && i > 0 {
419                // Look for state mutations after call
420                for future in instructions.iter().skip(i + 1).take(10) {
421                    if future.opcode.mutates_state() {
422                        analysis.issues.push(BytecodeIssue {
423                            severity: Severity::High,
424                            issue_type: IssueType::PotentialReentrancy,
425                            pc: instr.pc,
426                            description: "State mutation after call (reentrancy risk)".to_string(),
427                        });
428                        break;
429                    }
430                }
431            }
432        }
433
434        analysis.instruction_count = instructions.len();
435        Ok(analysis)
436    }
437}
438
439/// Result of bytecode analysis.
440#[derive(Debug, Clone, Serialize, Deserialize)]
441pub struct BytecodeAnalysis {
442    /// Total instructions.
443    pub instruction_count: usize,
444    /// Detected call locations (PC offsets).
445    pub calls: Vec<usize>,
446    /// Detected jump locations.
447    pub jumps: Vec<usize>,
448    /// Detected state mutations.
449    pub state_mutations: Vec<usize>,
450    /// Identified issues.
451    pub issues: Vec<BytecodeIssue>,
452}
453
454impl BytecodeAnalysis {
455    /// Create empty analysis.
456    pub fn new() -> Self {
457        Self {
458            instruction_count: 0,
459            calls: Vec::new(),
460            jumps: Vec::new(),
461            state_mutations: Vec::new(),
462            issues: Vec::new(),
463        }
464    }
465
466    /// Check if analysis found critical issues.
467    pub fn has_critical(&self) -> bool {
468        self.issues
469            .iter()
470            .any(|issue| issue.severity == Severity::Critical)
471    }
472}
473
474impl Default for BytecodeAnalysis {
475    fn default() -> Self {
476        Self::new()
477    }
478}
479
480/// Issue found in bytecode.
481#[derive(Debug, Clone, Serialize, Deserialize)]
482pub struct BytecodeIssue {
483    /// Issue severity.
484    pub severity: Severity,
485    /// Issue type.
486    pub issue_type: IssueType,
487    /// Program counter where issue occurs.
488    pub pc: usize,
489    /// Human-readable description.
490    pub description: String,
491}
492
493/// Issue severity levels.
494#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
495pub enum Severity {
496    /// Critical issue.
497    Critical,
498    /// High severity.
499    High,
500    /// Medium severity.
501    Medium,
502    /// Low severity.
503    Low,
504    /// Informational.
505    Info,
506}
507
508/// Issue types.
509#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
510pub enum IssueType {
511    /// Reentrancy vulnerability.
512    PotentialReentrancy,
513    /// Unsafe external call.
514    UnsafeCall,
515    /// Integer overflow.
516    IntegerOverflow,
517    /// Delegatecall to untrusted code.
518    DangerousDelegatecall,
519    /// Unprotected function.
520    MissingAccessControl,
521    /// Other issue.
522    Other,
523}
524
525#[cfg(test)]
526mod tests {
527    use super::*;
528
529    #[test]
530    fn test_opcode_parsing() {
531        assert_eq!(Opcode::from_byte(0x01), Opcode::Add);
532        assert_eq!(Opcode::from_byte(0x02), Opcode::Mul);
533        assert_eq!(Opcode::from_byte(0xf1), Opcode::Call);
534        assert_eq!(Opcode::from_byte(0xff), Opcode::Selfdestruct);
535    }
536
537    #[test]
538    fn test_opcode_properties() {
539        assert!(Opcode::Call.is_call());
540        assert!(!Opcode::Add.is_call());
541        assert!(Opcode::Jump.is_jump());
542        assert!(Opcode::Sstore.mutates_state());
543    }
544
545    #[test]
546    fn test_hex_to_bytes() {
547        let bytes = BytecodeAnalyzer::hex_to_bytes("0102ff").unwrap();
548        assert_eq!(bytes, vec![0x01, 0x02, 0xff]);
549
550        let odd_hex = BytecodeAnalyzer::hex_to_bytes("010");
551        assert!(odd_hex.is_err());
552    }
553
554    #[test]
555    fn test_bytecode_analysis_creation() {
556        let analysis = BytecodeAnalysis::new();
557        assert_eq!(analysis.instruction_count, 0);
558        assert!(!analysis.has_critical());
559    }
560}