1#![allow(missing_docs)]
2use crate::errors::{AnalysisError, AnalysisResult};
11use serde::{Deserialize, Serialize};
12use tracing::{debug, info};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
16pub enum Opcode {
17 Add,
19 Sub,
20 Mul,
21 Div,
22 Sdiv,
23 Mod,
24 Smod,
25 Addmod,
26 Mulmod,
27 Exp,
28 SignExtend,
29
30 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 Keccak256,
48
49 Pop,
51 Mload,
52 Mstore,
53 Mstore8,
54 Sload,
55 Sstore,
56 Msize,
57
58 Pc,
60 Gas,
61 Jump,
62 Jumpi,
63 Jumpdest,
64 Return,
65 Revert,
66 Selfdestruct,
67
68 Call,
70 Callcode,
71 Delegatecall,
72 Staticcall,
73 Create,
74 Create2,
75
76 Calldataload,
78 Calldatasize,
79 Calldatacopy,
80 Codesize,
81 Codecopy,
82 Extcodesize,
83 Extcodecopy,
84 Extcodehash,
85 Returndatasize,
86 Returndatacopy,
87
88 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(u8),
106 Dup(u8),
107 Swap(u8),
108 Log(u8),
109
110 Nop,
112 Invalid,
113 Unknown(u8),
114}
115
116impl Opcode {
117 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 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 pub fn is_jump(&self) -> bool {
263 matches!(self, Opcode::Jump | Opcode::Jumpi)
264 }
265
266 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 pub fn mutates_state(&self) -> bool {
281 matches!(self, Opcode::Sstore | Opcode::Create | Opcode::Create2)
282 }
283}
284
285#[derive(Debug, Clone, Serialize, Deserialize)]
287pub struct Instruction {
288 pub pc: usize,
290 pub opcode: Opcode,
292 pub immediates: Vec<u8>,
294}
295
296impl Instruction {
297 pub fn new(pc: usize, opcode: Opcode, immediates: Vec<u8>) -> Self {
299 Self {
300 pc,
301 opcode,
302 immediates,
303 }
304 }
305
306 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
324pub struct BytecodeAnalyzer;
326
327impl BytecodeAnalyzer {
328 pub fn disassemble(bytecode: &str) -> AnalysisResult<Vec<Instruction>> {
330 debug!("Disassembling bytecode ({} chars)", bytecode.len());
331
332 let bytecode = bytecode.strip_prefix("0x").unwrap_or(bytecode);
334
335 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 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 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 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 if instr.opcode.is_call() {
390 analysis.calls.push(instr.pc);
391 }
392
393 if instr.opcode.is_jump() {
395 analysis.jumps.push(instr.pc);
396 }
397
398 if instr.opcode.mutates_state() {
400 analysis.state_mutations.push(instr.pc);
401 }
402
403 if i > 0 && instr.opcode.is_call() {
405 let prev = &instructions[i - 1];
406 if !matches!(prev.opcode, Opcode::Gas) {
407 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 if instr.opcode.is_call() && i > 0 {
419 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#[derive(Debug, Clone, Serialize, Deserialize)]
441pub struct BytecodeAnalysis {
442 pub instruction_count: usize,
444 pub calls: Vec<usize>,
446 pub jumps: Vec<usize>,
448 pub state_mutations: Vec<usize>,
450 pub issues: Vec<BytecodeIssue>,
452}
453
454impl BytecodeAnalysis {
455 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
482pub struct BytecodeIssue {
483 pub severity: Severity,
485 pub issue_type: IssueType,
487 pub pc: usize,
489 pub description: String,
491}
492
493#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
495pub enum Severity {
496 Critical,
498 High,
500 Medium,
502 Low,
504 Info,
506}
507
508#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
510pub enum IssueType {
511 PotentialReentrancy,
513 UnsafeCall,
515 IntegerOverflow,
517 DangerousDelegatecall,
519 MissingAccessControl,
521 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}