Coverage for src/alprina_cli/agents/web3_auditor/symbolic_executor.py: 0%
338 statements
« prev ^ index » next coverage.py v7.11.3, created at 2025-11-13 11:15 +0100
« prev ^ index » next coverage.py v7.11.3, created at 2025-11-13 11:15 +0100
1"""
2Symbolic Execution Engine for Solidity Smart Contracts
4WEEK 3 DAY 1: Symbolic Execution
5=================================
7Implements symbolic execution to detect vulnerabilities through path analysis
8and constraint solving. Uses Z3 theorem prover for constraint satisfaction.
10Author: Alprina Development Team
11Date: 2025-11-12
13References:
14- OYENTE: Symbolic execution for Ethereum (2016)
15- Mythril: Constraint-based vulnerability detection
16- Manticore: Dynamic symbolic execution
17"""
19import re
20from typing import List, Dict, Any, Optional, Set, Tuple
21from dataclasses import dataclass, field
22from enum import Enum
23import z3
25try:
26 from .solidity_analyzer import SolidityVulnerability, VulnerabilityType
27except ImportError:
28 # For standalone testing
29 import sys
30 from pathlib import Path
31 sys.path.insert(0, str(Path(__file__).parent))
32 from solidity_analyzer import SolidityVulnerability, VulnerabilityType
35class SymbolicType(Enum):
36 """Types in symbolic execution"""
37 UINT256 = "uint256"
38 INT256 = "int256"
39 ADDRESS = "address"
40 BOOL = "bool"
41 BYTES32 = "bytes32"
42 UNKNOWN = "unknown"
45@dataclass
46class SymbolicVariable:
47 """Represents a symbolic variable during execution"""
48 name: str
49 sym_type: SymbolicType
50 z3_var: z3.ExprRef
51 is_tainted: bool = False # From user input
52 source_line: Optional[int] = None
55@dataclass
56class PathConstraint:
57 """Represents a constraint along an execution path"""
58 condition: str
59 z3_constraint: z3.BoolRef
60 line_number: int
61 is_true_branch: bool # True if we took the "true" branch
64@dataclass
65class SymbolicState:
66 """State during symbolic execution"""
67 variables: Dict[str, SymbolicVariable] = field(default_factory=dict)
68 constraints: List[PathConstraint] = field(default_factory=list)
69 storage: Dict[str, SymbolicVariable] = field(default_factory=dict)
70 memory: Dict[str, SymbolicVariable] = field(default_factory=dict)
71 execution_path: List[int] = field(default_factory=list) # Line numbers
73 def copy(self) -> 'SymbolicState':
74 """Create a copy of this state for branch exploration"""
75 import copy
76 return copy.deepcopy(self)
79@dataclass
80class SymbolicVulnerability:
81 """Vulnerability found via symbolic execution"""
82 vulnerability_type: str
83 severity: str
84 title: str
85 description: str
86 line_number: int
87 function_name: str
88 proof: Optional[str] = None # Z3 model showing exploit
89 path_constraints: List[str] = field(default_factory=list)
90 confidence: int = 95
93class SymbolicExecutor:
94 """
95 Symbolic execution engine for Solidity contracts
97 Capabilities:
98 - Integer overflow/underflow detection via constraint solving
99 - Unreachable code detection
100 - Taint analysis for user inputs
101 - Path feasibility analysis
102 - Division by zero detection
104 Week 3 Day 1 Implementation:
105 - Basic symbolic execution framework
106 - Integer overflow detection with Z3
107 - Simple path exploration
108 """
110 def __init__(self):
111 self.solver = z3.Solver()
112 self.vulnerabilities: List[SymbolicVulnerability] = []
114 # Z3 constants for Solidity types
115 self.MAX_UINT256 = 2**256 - 1
116 self.MIN_INT256 = -(2**255)
117 self.MAX_INT256 = 2**255 - 1
119 def analyze_contract(self, contract_code: str, file_path: str) -> List[SolidityVulnerability]:
120 """
121 Analyze contract using symbolic execution
123 Returns standard SolidityVulnerability objects for integration
124 with existing Week 2 economic impact calculator
125 """
126 self.vulnerabilities = []
128 # Extract functions from contract
129 functions = self._extract_functions(contract_code)
131 for func in functions:
132 # Symbolically execute each function
133 self._execute_function(func, file_path)
135 # Convert to standard format
136 return self._convert_to_standard_format(file_path)
138 def _extract_functions(self, contract_code: str) -> List[Dict[str, Any]]:
139 """Extract function definitions from contract"""
140 functions = []
141 lines = contract_code.split('\n')
143 i = 0
144 while i < len(lines):
145 line = lines[i].strip()
147 # Match function definition
148 func_match = re.match(
149 r'function\s+(\w+)\s*\([^)]*\)\s*(public|external|internal|private)?',
150 line
151 )
153 if func_match:
154 func_name = func_match.group(1)
155 visibility = func_match.group(2) or 'internal'
157 # Extract function body
158 start_line = i
159 brace_count = 0
160 body_lines = []
162 # Find opening brace
163 while i < len(lines) and '{' not in lines[i]:
164 i += 1
166 if i < len(lines):
167 brace_count = lines[i].count('{') - lines[i].count('}')
168 body_lines.append(lines[i])
169 i += 1
171 # Extract until closing brace
172 while i < len(lines) and brace_count > 0:
173 line = lines[i]
174 brace_count += line.count('{') - line.count('}')
175 body_lines.append(line)
176 i += 1
178 functions.append({
179 'name': func_name,
180 'visibility': visibility,
181 'start_line': start_line + 1,
182 'body': '\n'.join(body_lines),
183 'body_lines': body_lines
184 })
186 i += 1
188 return functions
190 def _execute_function(self, func: Dict[str, Any], file_path: str):
191 """
192 Symbolically execute a function
194 Explores all execution paths and detects vulnerabilities
195 """
196 func_name = func['name']
197 start_line = func['start_line']
198 body_lines = func['body_lines']
200 # Initialize symbolic state
201 initial_state = SymbolicState()
203 # Create symbolic parameters (tainted input)
204 params = self._extract_parameters(func['body'])
205 for param_name, param_type in params.items():
206 sym_var = self._create_symbolic_variable(param_name, param_type, is_tainted=True)
207 initial_state.variables[param_name] = sym_var
209 # Execute symbolically
210 self._explore_paths(body_lines, initial_state, func_name, start_line, file_path)
212 def _extract_parameters(self, func_def: str) -> Dict[str, SymbolicType]:
213 """Extract function parameters and their types"""
214 params = {}
216 # Match parameter list
217 param_match = re.search(r'\(([^)]*)\)', func_def)
218 if not param_match:
219 return params
221 param_list = param_match.group(1)
222 if not param_list.strip():
223 return params
225 # Parse each parameter
226 for param in param_list.split(','):
227 param = param.strip()
228 if not param:
229 continue
231 parts = param.split()
232 if len(parts) >= 2:
233 param_type_str = parts[0]
234 param_name = parts[1]
236 # Map to symbolic type
237 param_type = self._map_to_symbolic_type(param_type_str)
238 params[param_name] = param_type
240 return params
242 def _map_to_symbolic_type(self, type_str: str) -> SymbolicType:
243 """Map Solidity type string to SymbolicType"""
244 if 'uint' in type_str:
245 return SymbolicType.UINT256
246 elif type_str.startswith('int'):
247 return SymbolicType.INT256
248 elif type_str == 'address':
249 return SymbolicType.ADDRESS
250 elif type_str == 'bool':
251 return SymbolicType.BOOL
252 elif 'bytes' in type_str:
253 return SymbolicType.BYTES32
254 else:
255 return SymbolicType.UNKNOWN
257 def _create_symbolic_variable(
258 self,
259 name: str,
260 sym_type: SymbolicType,
261 is_tainted: bool = False
262 ) -> SymbolicVariable:
263 """Create a symbolic variable with Z3 representation"""
265 if sym_type == SymbolicType.UINT256:
266 z3_var = z3.BitVec(name, 256)
267 elif sym_type == SymbolicType.INT256:
268 z3_var = z3.BitVec(name, 256)
269 elif sym_type == SymbolicType.BOOL:
270 z3_var = z3.Bool(name)
271 elif sym_type == SymbolicType.ADDRESS:
272 z3_var = z3.BitVec(name, 160) # Addresses are 160 bits
273 else:
274 z3_var = z3.BitVec(name, 256) # Default to 256-bit
276 return SymbolicVariable(
277 name=name,
278 sym_type=sym_type,
279 z3_var=z3_var,
280 is_tainted=is_tainted
281 )
283 def _explore_paths(
284 self,
285 body_lines: List[str],
286 state: SymbolicState,
287 func_name: str,
288 start_line: int,
289 file_path: str
290 ):
291 """
292 Explore execution paths in function body
294 Day 2 Enhancement: Path condition extraction and branch analysis
295 - Extract conditions from if/require/assert statements
296 - Track path constraints for each branch
297 - Detect unreachable code using constraint solving
298 """
300 i = 0
301 while i < len(body_lines):
302 line = body_lines[i]
303 line_number = start_line + i
304 line_stripped = line.strip()
306 if not line_stripped or line_stripped.startswith('//'):
307 i += 1
308 continue
310 # Track execution path
311 state.execution_path.append(line_number)
313 # DAY 2: Extract and analyze conditional branches
314 if self._is_conditional(line_stripped):
315 self._analyze_conditional_branch(
316 body_lines, i, state, func_name, start_line, file_path
317 )
319 # DAY 2: Detect require/assert statements
320 if 'require(' in line_stripped or 'assert(' in line_stripped:
321 self._analyze_requirement(line_stripped, line_number, state, func_name)
323 # Day 1: Arithmetic operations
324 self._analyze_arithmetic(line_stripped, line_number, state, func_name, file_path)
326 # Day 1: Divisions
327 self._analyze_division(line_stripped, line_number, state, func_name, file_path)
329 # Day 1: Tainted data flow
330 self._analyze_taint_flow(line_stripped, line_number, state, func_name, file_path)
332 # DAY 2: Check for unreachable code
333 if len(state.constraints) > 0:
334 self._check_path_feasibility(state, line_number, func_name, file_path)
336 i += 1
338 def _is_conditional(self, line: str) -> bool:
339 """Check if line contains a conditional statement"""
340 return (
341 line.strip().startswith('if ') or
342 line.strip().startswith('if(') or
343 'else if' in line or
344 'else {' in line
345 )
347 def _analyze_conditional_branch(
348 self,
349 body_lines: List[str],
350 line_index: int,
351 state: SymbolicState,
352 func_name: str,
353 start_line: int,
354 file_path: str
355 ):
356 """
357 Analyze conditional branches (if/else)
359 DAY 2: Path Condition Extraction
360 - Extract condition from if statement
361 - Create Z3 constraint for condition
362 - Explore both true and false branches
363 - Detect unreachable branches
364 """
365 line = body_lines[line_index]
366 line_number = start_line + line_index
368 # Extract condition from if statement
369 if_match = re.search(r'if\s*\(([^)]+)\)', line)
370 if not if_match:
371 return
373 condition_str = if_match.group(1).strip()
375 # Parse condition into Z3 constraint
376 z3_constraint = self._parse_condition_to_z3(condition_str, state)
378 if z3_constraint is None:
379 # Can't parse condition, skip advanced analysis
380 return
382 # Create path constraint for true branch
383 true_constraint = PathConstraint(
384 condition=condition_str,
385 z3_constraint=z3_constraint,
386 line_number=line_number,
387 is_true_branch=True
388 )
390 # Create path constraint for false branch
391 false_constraint = PathConstraint(
392 condition=f"!({condition_str})",
393 z3_constraint=z3.Not(z3_constraint),
394 line_number=line_number,
395 is_true_branch=False
396 )
398 # Check if true branch is feasible
399 true_state = state.copy()
400 true_state.constraints.append(true_constraint)
402 if self._is_path_feasible(true_state):
403 # True branch is reachable
404 pass
405 else:
406 # True branch is unreachable!
407 self.vulnerabilities.append(SymbolicVulnerability(
408 vulnerability_type="unreachable_code",
409 severity="low",
410 title=f"Unreachable Code in {func_name}",
411 description=(
412 f"The condition `{condition_str}` at line {line_number} "
413 f"can never be true given the current path constraints. "
414 f"The code inside this if-block is unreachable."
415 ),
416 line_number=line_number,
417 function_name=func_name,
418 proof=f"Z3 proved condition is always false: {condition_str}",
419 confidence=95
420 ))
422 # Check if false branch is feasible (for else clauses)
423 false_state = state.copy()
424 false_state.constraints.append(false_constraint)
426 if not self._is_path_feasible(false_state):
427 # Condition is always true (else is unreachable)
428 self.vulnerabilities.append(SymbolicVulnerability(
429 vulnerability_type="unreachable_code",
430 severity="info",
431 title=f"Condition Always True in {func_name}",
432 description=(
433 f"The condition `{condition_str}` at line {line_number} "
434 f"is always true. Consider simplifying the code."
435 ),
436 line_number=line_number,
437 function_name=func_name,
438 proof=f"Z3 proved condition is always true: {condition_str}",
439 confidence=90
440 ))
442 def _parse_condition_to_z3(self, condition: str, state: SymbolicState) -> Optional[z3.BoolRef]:
443 """
444 Parse a Solidity condition into a Z3 constraint
446 DAY 2: Enhanced condition parsing
447 Supports:
448 - Comparisons: ==, !=, <, >, <=, >=
449 - Boolean operators: &&, ||, !
450 - Basic arithmetic
451 """
453 # Simple comparison operators
454 for op, z3_op in [
455 ('==', lambda a, b: a == b),
456 ('!=', lambda a, b: a != b),
457 ('>=', lambda a, b: z3.UGE(a, b)), # Unsigned greater or equal
458 ('<=', lambda a, b: z3.ULE(a, b)),
459 ('>', lambda a, b: z3.UGT(a, b)),
460 ('<', lambda a, b: z3.ULT(a, b)),
461 ]:
462 if op in condition:
463 parts = condition.split(op)
464 if len(parts) == 2:
465 left = parts[0].strip()
466 right = parts[1].strip()
468 # Try to create Z3 expressions
469 left_z3 = self._parse_expression_to_z3(left, state)
470 right_z3 = self._parse_expression_to_z3(right, state)
472 if left_z3 is not None and right_z3 is not None:
473 return z3_op(left_z3, right_z3)
475 # Boolean conditions
476 if condition in state.variables:
477 var = state.variables[condition]
478 if var.sym_type == SymbolicType.BOOL:
479 return var.z3_var
481 # Negation
482 if condition.startswith('!'):
483 inner = condition[1:].strip()
484 inner_z3 = self._parse_condition_to_z3(inner, state)
485 if inner_z3 is not None:
486 return z3.Not(inner_z3)
488 # Can't parse
489 return None
491 def _parse_expression_to_z3(self, expr: str, state: SymbolicState) -> Optional[z3.ExprRef]:
492 """Parse a Solidity expression into Z3"""
494 # Integer literal
495 if expr.isdigit():
496 return z3.BitVecVal(int(expr), 256)
498 # Variable reference
499 if expr in state.variables:
500 return state.variables[expr].z3_var
502 # msg.sender, msg.value, etc.
503 if expr.startswith('msg.'):
504 # Create symbolic variable for msg properties
505 var_name = expr.replace('.', '_')
506 if var_name not in state.variables:
507 z3_var = z3.BitVec(var_name, 256)
508 state.variables[var_name] = SymbolicVariable(
509 name=var_name,
510 sym_type=SymbolicType.UINT256,
511 z3_var=z3_var,
512 is_tainted=True
513 )
514 return state.variables[var_name].z3_var
516 # Can't parse
517 return None
519 def _is_path_feasible(self, state: SymbolicState) -> bool:
520 """
521 Check if execution path is feasible using Z3
523 DAY 2: Constraint solving for path feasibility
524 Returns True if there exists a satisfying assignment
525 """
526 if len(state.constraints) == 0:
527 return True
529 solver = z3.Solver()
531 # Add all path constraints
532 for constraint in state.constraints:
533 solver.add(constraint.z3_constraint)
535 # Check satisfiability
536 result = solver.check()
537 return result == z3.sat
539 def _check_path_feasibility(
540 self,
541 state: SymbolicState,
542 line_number: int,
543 func_name: str,
544 file_path: str
545 ):
546 """
547 Check if current path is feasible
549 If not, report unreachable code
550 """
551 if not self._is_path_feasible(state):
552 # This path is unreachable!
553 constraint_desc = ", ".join([c.condition for c in state.constraints[-3:]])
555 self.vulnerabilities.append(SymbolicVulnerability(
556 vulnerability_type="unreachable_code",
557 severity="info",
558 title=f"Unreachable Code Detected in {func_name}",
559 description=(
560 f"Code at line {line_number} is unreachable due to "
561 f"contradicting path constraints: {constraint_desc}"
562 ),
563 line_number=line_number,
564 function_name=func_name,
565 proof=f"Path constraints are unsatisfiable",
566 confidence=90
567 ))
569 def _analyze_requirement(
570 self,
571 line: str,
572 line_number: int,
573 state: SymbolicState,
574 func_name: str
575 ):
576 """
577 Analyze require/assert statements
579 DAY 2: Extract constraints from require/assert
580 These become path constraints for subsequent code
581 """
583 # Extract condition from require/assert
584 req_match = re.search(r'(require|assert)\s*\(([^)]+)\)', line)
585 if not req_match:
586 return
588 condition_str = req_match.group(2).strip()
590 # Remove error message if present
591 if ',' in condition_str:
592 condition_str = condition_str.split(',')[0].strip()
594 # Parse to Z3
595 z3_constraint = self._parse_condition_to_z3(condition_str, state)
597 if z3_constraint is not None:
598 # Add as path constraint
599 constraint = PathConstraint(
600 condition=condition_str,
601 z3_constraint=z3_constraint,
602 line_number=line_number,
603 is_true_branch=True
604 )
605 state.constraints.append(constraint)
607 # Check if this requirement is always true
608 solver = z3.Solver()
609 solver.add(z3.Not(z3_constraint)) # Can it be false?
611 if solver.check() == z3.unsat:
612 # Requirement is always satisfied (redundant)
613 self.vulnerabilities.append(SymbolicVulnerability(
614 vulnerability_type="redundant_check",
615 severity="info",
616 title=f"Redundant Check in {func_name}",
617 description=(
618 f"The requirement `{condition_str}` at line {line_number} "
619 f"is always true and can be removed."
620 ),
621 line_number=line_number,
622 function_name=func_name,
623 proof=f"Z3 proved condition is always satisfied",
624 confidence=85
625 ))
627 def _analyze_arithmetic(
628 self,
629 line: str,
630 line_number: int,
631 state: SymbolicState,
632 func_name: str,
633 file_path: str
634 ):
635 """
636 Analyze arithmetic operations for overflow/underflow
638 Detects patterns like:
639 - balance += amount
640 - total = a + b
641 - result = value - 1
642 """
644 # Pattern: variable += expr
645 add_assign_match = re.search(r'(\w+)\s*\+=\s*(.+?)[;\s]', line)
646 if add_assign_match:
647 var_name = add_assign_match.group(1)
648 expr = add_assign_match.group(2).strip()
650 # Check if unchecked block
651 is_unchecked = 'unchecked' in line or any('unchecked' in bl for bl in state.execution_path[-5:] if isinstance(bl, str))
653 if not is_unchecked:
654 # Check for potential overflow
655 self._check_overflow_addition(var_name, expr, line_number, state, func_name, file_path)
657 # Pattern: variable = a + b
658 add_match = re.search(r'(\w+)\s*=\s*(.+?)\s*\+\s*(.+?)[;\s]', line)
659 if add_match:
660 result_var = add_match.group(1)
661 left_operand = add_match.group(2).strip()
662 right_operand = add_match.group(3).strip()
664 is_unchecked = 'unchecked' in line
665 if not is_unchecked:
666 self._check_overflow_addition_expr(
667 result_var, left_operand, right_operand,
668 line_number, state, func_name, file_path
669 )
671 # Pattern: variable -= expr (underflow)
672 sub_assign_match = re.search(r'(\w+)\s*-=\s*(.+?)[;\s]', line)
673 if sub_assign_match:
674 var_name = sub_assign_match.group(1)
675 expr = sub_assign_match.group(2).strip()
677 is_unchecked = 'unchecked' in line
678 if not is_unchecked:
679 self._check_underflow_subtraction(var_name, expr, line_number, state, func_name, file_path)
681 def _check_overflow_addition(
682 self,
683 var_name: str,
684 expr: str,
685 line_number: int,
686 state: SymbolicState,
687 func_name: str,
688 file_path: str
689 ):
690 """
691 Check if addition can overflow using Z3
693 Creates constraint: var + expr > MAX_UINT256
694 If satisfiable, overflow is possible
695 """
697 # Create Z3 variables
698 var_z3 = z3.BitVec(f"{var_name}_before", 256)
699 expr_z3 = z3.BitVec(f"{expr}_value", 256)
700 result_z3 = var_z3 + expr_z3
702 # Check if overflow possible: result < var (unsigned overflow wraps)
703 overflow_constraint = z3.ULT(result_z3, var_z3)
705 # Try to find a model where overflow occurs
706 solver = z3.Solver()
707 solver.add(overflow_constraint)
709 if solver.check() == z3.sat:
710 model = solver.model()
712 # Extract example values
713 var_value = model.eval(var_z3, model_completion=True)
714 expr_value = model.eval(expr_z3, model_completion=True)
716 proof = f"Overflow possible: {var_name} = {var_value}, {expr} = {expr_value}"
718 self.vulnerabilities.append(SymbolicVulnerability(
719 vulnerability_type="integer_overflow",
720 severity="high",
721 title=f"Integer Overflow in {func_name}",
722 description=(
723 f"Arithmetic operation `{var_name} += {expr}` at line {line_number} "
724 f"can overflow. This can lead to incorrect balances or unauthorized access."
725 ),
726 line_number=line_number,
727 function_name=func_name,
728 proof=proof,
729 confidence=90
730 ))
732 def _check_overflow_addition_expr(
733 self,
734 result_var: str,
735 left: str,
736 right: str,
737 line_number: int,
738 state: SymbolicState,
739 func_name: str,
740 file_path: str
741 ):
742 """Check if a + b can overflow"""
744 left_z3 = z3.BitVec(f"{left}_value", 256)
745 right_z3 = z3.BitVec(f"{right}_value", 256)
746 result_z3 = left_z3 + right_z3
748 # Overflow check: result < left OR result < right
749 overflow_constraint = z3.Or(
750 z3.ULT(result_z3, left_z3),
751 z3.ULT(result_z3, right_z3)
752 )
754 solver = z3.Solver()
755 solver.add(overflow_constraint)
757 if solver.check() == z3.sat:
758 model = solver.model()
759 left_value = model.eval(left_z3, model_completion=True)
760 right_value = model.eval(right_z3, model_completion=True)
762 proof = f"Overflow: {left} = {left_value}, {right} = {right_value}"
764 self.vulnerabilities.append(SymbolicVulnerability(
765 vulnerability_type="integer_overflow",
766 severity="medium",
767 title=f"Potential Integer Overflow in {func_name}",
768 description=(
769 f"Addition `{result_var} = {left} + {right}` at line {line_number} "
770 f"can overflow without `unchecked` block or overflow protection."
771 ),
772 line_number=line_number,
773 function_name=func_name,
774 proof=proof,
775 confidence=85
776 ))
778 def _check_underflow_subtraction(
779 self,
780 var_name: str,
781 expr: str,
782 line_number: int,
783 state: SymbolicState,
784 func_name: str,
785 file_path: str
786 ):
787 """Check if subtraction can underflow"""
789 var_z3 = z3.BitVec(f"{var_name}_before", 256)
790 expr_z3 = z3.BitVec(f"{expr}_value", 256)
792 # Underflow: var < expr (for unsigned)
793 underflow_constraint = z3.ULT(var_z3, expr_z3)
795 solver = z3.Solver()
796 solver.add(underflow_constraint)
798 if solver.check() == z3.sat:
799 model = solver.model()
800 var_value = model.eval(var_z3, model_completion=True)
801 expr_value = model.eval(expr_z3, model_completion=True)
803 proof = f"Underflow: {var_name} = {var_value}, {expr} = {expr_value}"
805 self.vulnerabilities.append(SymbolicVulnerability(
806 vulnerability_type="integer_underflow",
807 severity="high",
808 title=f"Integer Underflow in {func_name}",
809 description=(
810 f"Subtraction `{var_name} -= {expr}` at line {line_number} "
811 f"can underflow, wrapping to MAX_UINT256."
812 ),
813 line_number=line_number,
814 function_name=func_name,
815 proof=proof,
816 confidence=90
817 ))
819 def _analyze_division(
820 self,
821 line: str,
822 line_number: int,
823 state: SymbolicState,
824 func_name: str,
825 file_path: str
826 ):
827 """Detect potential division by zero"""
829 # Pattern: variable = a / b
830 div_match = re.search(r'(\w+)\s*=\s*(.+?)\s*/\s*(.+?)[;\s]', line)
831 if not div_match:
832 return
834 result_var = div_match.group(1)
835 numerator = div_match.group(2).strip()
836 denominator = div_match.group(3).strip()
838 # Check if denominator can be zero
839 if denominator.isdigit() and int(denominator) != 0:
840 # Constant non-zero, safe
841 return
843 # Check if there's a require statement protecting against zero
844 # This is a simplified check
845 if f"require({denominator} > 0" in line or f"require({denominator} != 0" in line:
846 return
848 # Potential division by zero
849 self.vulnerabilities.append(SymbolicVulnerability(
850 vulnerability_type="division_by_zero",
851 severity="medium",
852 title=f"Potential Division by Zero in {func_name}",
853 description=(
854 f"Division operation `{result_var} = {numerator} / {denominator}` at line {line_number} "
855 f"does not check if denominator is zero. This will cause transaction revert."
856 ),
857 line_number=line_number,
858 function_name=func_name,
859 proof=f"Denominator '{denominator}' not validated before division",
860 confidence=75
861 ))
863 def _analyze_taint_flow(
864 self,
865 line: str,
866 line_number: int,
867 state: SymbolicState,
868 func_name: str,
869 file_path: str
870 ):
871 """
872 Track tainted data flow from user inputs
874 Simplified implementation for Day 1
875 """
877 # Check for external calls with tainted data
878 call_match = re.search(r'\.call\s*\{', line)
879 if call_match:
880 # Check if any parameters are tainted
881 # This is a simplified check
882 for var_name, var in state.variables.items():
883 if var.is_tainted and var_name in line:
884 self.vulnerabilities.append(SymbolicVulnerability(
885 vulnerability_type="tainted_call",
886 severity="high",
887 title=f"Tainted Data in External Call in {func_name}",
888 description=(
889 f"External call at line {line_number} uses tainted user input '{var_name}'. "
890 f"This can lead to arbitrary external calls or reentrancy."
891 ),
892 line_number=line_number,
893 function_name=func_name,
894 proof=f"Tainted variable '{var_name}' flows to external call",
895 confidence=80
896 ))
897 break
899 def _convert_to_standard_format(self, file_path: str) -> List[SolidityVulnerability]:
900 """
901 Convert SymbolicVulnerability to standard SolidityVulnerability format
902 for integration with Week 2 economic impact calculator
903 """
904 standard_vulns = []
906 for vuln in self.vulnerabilities:
907 # Map vulnerability types
908 vuln_type_map = {
909 "integer_overflow": VulnerabilityType.INTEGER_OVERFLOW_UNDERFLOW,
910 "integer_underflow": VulnerabilityType.INTEGER_OVERFLOW_UNDERFLOW,
911 "division_by_zero": VulnerabilityType.LOGIC_ERROR,
912 "tainted_call": VulnerabilityType.UNCHECKED_LOW_LEVEL_CALL,
913 }
915 vuln_type = vuln_type_map.get(vuln.vulnerability_type, VulnerabilityType.LOGIC_ERROR)
917 # Create remediation advice
918 remediation = self._get_remediation(vuln.vulnerability_type)
920 # Create code snippet with proof
921 code_snippet = vuln.proof if vuln.proof else "See line number for details"
923 standard_vuln = SolidityVulnerability(
924 vulnerability_type=vuln_type,
925 severity=vuln.severity,
926 title=f"[Symbolic Execution] {vuln.title}",
927 description=vuln.description,
928 file_path=file_path,
929 line_number=vuln.line_number,
930 function_name=vuln.function_name,
931 contract_name="unknown",
932 code_snippet=code_snippet,
933 remediation=remediation,
934 confidence=vuln.confidence
935 )
937 standard_vulns.append(standard_vuln)
939 return standard_vulns
941 def _get_remediation(self, vuln_type: str) -> str:
942 """Get remediation advice for vulnerability type"""
943 remediation_map = {
944 "integer_overflow": (
945 "Use Solidity 0.8+ which has built-in overflow protection, "
946 "or wrap arithmetic in `unchecked {}` only when overflow is intended. "
947 "Consider using SafeMath library for Solidity <0.8."
948 ),
949 "integer_underflow": (
950 "Use Solidity 0.8+ with built-in underflow protection. "
951 "Add `require()` checks before subtraction to ensure sufficient balance."
952 ),
953 "division_by_zero": (
954 "Add `require(denominator > 0, \"Division by zero\")` before division operations."
955 ),
956 "tainted_call": (
957 "Validate and sanitize all user inputs before use in external calls. "
958 "Consider using a whitelist of allowed call targets."
959 ),
960 }
962 return remediation_map.get(vuln_type, "Review and validate the operation carefully.")
965# Example usage and testing
966if __name__ == "__main__":
967 executor = SymbolicExecutor()
969 # Test case: Simple overflow
970 test_contract = """
971 contract TestContract {
972 uint256 public totalSupply;
974 function mint(uint256 amount) external {
975 totalSupply += amount; // Can overflow!
976 }
978 function burn(uint256 amount) external {
979 totalSupply -= amount; // Can underflow!
980 }
982 function divide(uint256 a, uint256 b) external returns (uint256) {
983 return a / b; // Division by zero!
984 }
985 }
986 """
988 vulns = executor.analyze_contract(test_contract, "test.sol")
990 print(f"Found {len(vulns)} vulnerabilities:")
991 for vuln in vulns:
992 print(f"\n{vuln.severity.upper()}: {vuln.title}")
993 print(f"Line {vuln.line_number}: {vuln.description}")
994 if vuln.code_snippet:
995 print(f"Proof: {vuln.code_snippet}")