Coverage for src/alprina_cli/agents/web3_auditor/symbolic_executor.py: 17%
350 statements
« prev ^ index » next coverage.py v7.11.3, created at 2025-11-14 11:27 +0100
« prev ^ index » next coverage.py v7.11.3, created at 2025-11-14 11:27 +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
24# Optional z3 dependency for symbolic execution
25try:
26 import z3
27 Z3_AVAILABLE = True
28except ImportError:
29 Z3_AVAILABLE = False
30 # Mock z3 types for when it's not available
31 class z3:
32 ExprRef = Any
33 Solver = Any
35try:
36 from .solidity_analyzer import SolidityVulnerability, VulnerabilityType
37except ImportError:
38 # For standalone testing
39 import sys
40 from pathlib import Path
41 sys.path.insert(0, str(Path(__file__).parent))
42 from solidity_analyzer import SolidityVulnerability, VulnerabilityType
45class SymbolicType(Enum):
46 """Types in symbolic execution"""
47 UINT256 = "uint256"
48 INT256 = "int256"
49 ADDRESS = "address"
50 BOOL = "bool"
51 BYTES32 = "bytes32"
52 UNKNOWN = "unknown"
55@dataclass
56class SymbolicVariable:
57 """Represents a symbolic variable during execution"""
58 name: str
59 sym_type: SymbolicType
60 z3_var: z3.ExprRef
61 is_tainted: bool = False # From user input
62 source_line: Optional[int] = None
65@dataclass
66class PathConstraint:
67 """Represents a constraint along an execution path"""
68 condition: str
69 z3_constraint: z3.BoolRef
70 line_number: int
71 is_true_branch: bool # True if we took the "true" branch
74@dataclass
75class SymbolicState:
76 """State during symbolic execution"""
77 variables: Dict[str, SymbolicVariable] = field(default_factory=dict)
78 constraints: List[PathConstraint] = field(default_factory=list)
79 storage: Dict[str, SymbolicVariable] = field(default_factory=dict)
80 memory: Dict[str, SymbolicVariable] = field(default_factory=dict)
81 execution_path: List[int] = field(default_factory=list) # Line numbers
83 def copy(self) -> 'SymbolicState':
84 """Create a copy of this state for branch exploration"""
85 import copy
86 return copy.deepcopy(self)
89@dataclass
90class SymbolicVulnerability:
91 """Vulnerability found via symbolic execution"""
92 vulnerability_type: str
93 severity: str
94 title: str
95 description: str
96 line_number: int
97 function_name: str
98 proof: Optional[str] = None # Z3 model showing exploit
99 path_constraints: List[str] = field(default_factory=list)
100 confidence: int = 95
103class SymbolicExecutor:
104 """
105 Symbolic execution engine for Solidity contracts
107 Capabilities:
108 - Integer overflow/underflow detection via constraint solving
109 - Unreachable code detection
110 - Taint analysis for user inputs
111 - Path feasibility analysis
112 - Division by zero detection
114 Week 3 Day 1 Implementation:
115 - Basic symbolic execution framework
116 - Integer overflow detection with Z3
117 - Simple path exploration
118 """
120 def __init__(self):
121 self.z3_available = Z3_AVAILABLE
122 if Z3_AVAILABLE:
123 self.solver = z3.Solver()
124 else:
125 self.solver = None
126 self.vulnerabilities: List[SymbolicVulnerability] = []
128 # Z3 constants for Solidity types
129 self.MAX_UINT256 = 2**256 - 1
130 self.MIN_INT256 = -(2**255)
131 self.MAX_INT256 = 2**255 - 1
133 def analyze_contract(self, contract_code: str, file_path: str) -> List[SolidityVulnerability]:
134 """
135 Analyze contract using symbolic execution
137 Returns standard SolidityVulnerability objects for integration
138 with existing Week 2 economic impact calculator
139 """
140 # If z3 is not available, return empty list with warning
141 if not self.z3_available:
142 return []
144 self.vulnerabilities = []
146 # Extract functions from contract
147 functions = self._extract_functions(contract_code)
149 for func in functions:
150 # Symbolically execute each function
151 self._execute_function(func, file_path)
153 # Convert to standard format
154 return self._convert_to_standard_format(file_path)
156 def _extract_functions(self, contract_code: str) -> List[Dict[str, Any]]:
157 """Extract function definitions from contract"""
158 functions = []
159 lines = contract_code.split('\n')
161 i = 0
162 while i < len(lines):
163 line = lines[i].strip()
165 # Match function definition
166 func_match = re.match(
167 r'function\s+(\w+)\s*\([^)]*\)\s*(public|external|internal|private)?',
168 line
169 )
171 if func_match:
172 func_name = func_match.group(1)
173 visibility = func_match.group(2) or 'internal'
175 # Extract function body
176 start_line = i
177 brace_count = 0
178 body_lines = []
180 # Find opening brace
181 while i < len(lines) and '{' not in lines[i]:
182 i += 1
184 if i < len(lines):
185 brace_count = lines[i].count('{') - lines[i].count('}')
186 body_lines.append(lines[i])
187 i += 1
189 # Extract until closing brace
190 while i < len(lines) and brace_count > 0:
191 line = lines[i]
192 brace_count += line.count('{') - line.count('}')
193 body_lines.append(line)
194 i += 1
196 functions.append({
197 'name': func_name,
198 'visibility': visibility,
199 'start_line': start_line + 1,
200 'body': '\n'.join(body_lines),
201 'body_lines': body_lines
202 })
204 i += 1
206 return functions
208 def _execute_function(self, func: Dict[str, Any], file_path: str):
209 """
210 Symbolically execute a function
212 Explores all execution paths and detects vulnerabilities
213 """
214 func_name = func['name']
215 start_line = func['start_line']
216 body_lines = func['body_lines']
218 # Initialize symbolic state
219 initial_state = SymbolicState()
221 # Create symbolic parameters (tainted input)
222 params = self._extract_parameters(func['body'])
223 for param_name, param_type in params.items():
224 sym_var = self._create_symbolic_variable(param_name, param_type, is_tainted=True)
225 initial_state.variables[param_name] = sym_var
227 # Execute symbolically
228 self._explore_paths(body_lines, initial_state, func_name, start_line, file_path)
230 def _extract_parameters(self, func_def: str) -> Dict[str, SymbolicType]:
231 """Extract function parameters and their types"""
232 params = {}
234 # Match parameter list
235 param_match = re.search(r'\(([^)]*)\)', func_def)
236 if not param_match:
237 return params
239 param_list = param_match.group(1)
240 if not param_list.strip():
241 return params
243 # Parse each parameter
244 for param in param_list.split(','):
245 param = param.strip()
246 if not param:
247 continue
249 parts = param.split()
250 if len(parts) >= 2:
251 param_type_str = parts[0]
252 param_name = parts[1]
254 # Map to symbolic type
255 param_type = self._map_to_symbolic_type(param_type_str)
256 params[param_name] = param_type
258 return params
260 def _map_to_symbolic_type(self, type_str: str) -> SymbolicType:
261 """Map Solidity type string to SymbolicType"""
262 if 'uint' in type_str:
263 return SymbolicType.UINT256
264 elif type_str.startswith('int'):
265 return SymbolicType.INT256
266 elif type_str == 'address':
267 return SymbolicType.ADDRESS
268 elif type_str == 'bool':
269 return SymbolicType.BOOL
270 elif 'bytes' in type_str:
271 return SymbolicType.BYTES32
272 else:
273 return SymbolicType.UNKNOWN
275 def _create_symbolic_variable(
276 self,
277 name: str,
278 sym_type: SymbolicType,
279 is_tainted: bool = False
280 ) -> SymbolicVariable:
281 """Create a symbolic variable with Z3 representation"""
283 if sym_type == SymbolicType.UINT256:
284 z3_var = z3.BitVec(name, 256)
285 elif sym_type == SymbolicType.INT256:
286 z3_var = z3.BitVec(name, 256)
287 elif sym_type == SymbolicType.BOOL:
288 z3_var = z3.Bool(name)
289 elif sym_type == SymbolicType.ADDRESS:
290 z3_var = z3.BitVec(name, 160) # Addresses are 160 bits
291 else:
292 z3_var = z3.BitVec(name, 256) # Default to 256-bit
294 return SymbolicVariable(
295 name=name,
296 sym_type=sym_type,
297 z3_var=z3_var,
298 is_tainted=is_tainted
299 )
301 def _explore_paths(
302 self,
303 body_lines: List[str],
304 state: SymbolicState,
305 func_name: str,
306 start_line: int,
307 file_path: str
308 ):
309 """
310 Explore execution paths in function body
312 Day 2 Enhancement: Path condition extraction and branch analysis
313 - Extract conditions from if/require/assert statements
314 - Track path constraints for each branch
315 - Detect unreachable code using constraint solving
316 """
318 i = 0
319 while i < len(body_lines):
320 line = body_lines[i]
321 line_number = start_line + i
322 line_stripped = line.strip()
324 if not line_stripped or line_stripped.startswith('//'):
325 i += 1
326 continue
328 # Track execution path
329 state.execution_path.append(line_number)
331 # DAY 2: Extract and analyze conditional branches
332 if self._is_conditional(line_stripped):
333 self._analyze_conditional_branch(
334 body_lines, i, state, func_name, start_line, file_path
335 )
337 # DAY 2: Detect require/assert statements
338 if 'require(' in line_stripped or 'assert(' in line_stripped:
339 self._analyze_requirement(line_stripped, line_number, state, func_name)
341 # Day 1: Arithmetic operations
342 self._analyze_arithmetic(line_stripped, line_number, state, func_name, file_path)
344 # Day 1: Divisions
345 self._analyze_division(line_stripped, line_number, state, func_name, file_path)
347 # Day 1: Tainted data flow
348 self._analyze_taint_flow(line_stripped, line_number, state, func_name, file_path)
350 # DAY 2: Check for unreachable code
351 if len(state.constraints) > 0:
352 self._check_path_feasibility(state, line_number, func_name, file_path)
354 i += 1
356 def _is_conditional(self, line: str) -> bool:
357 """Check if line contains a conditional statement"""
358 return (
359 line.strip().startswith('if ') or
360 line.strip().startswith('if(') or
361 'else if' in line or
362 'else {' in line
363 )
365 def _analyze_conditional_branch(
366 self,
367 body_lines: List[str],
368 line_index: int,
369 state: SymbolicState,
370 func_name: str,
371 start_line: int,
372 file_path: str
373 ):
374 """
375 Analyze conditional branches (if/else)
377 DAY 2: Path Condition Extraction
378 - Extract condition from if statement
379 - Create Z3 constraint for condition
380 - Explore both true and false branches
381 - Detect unreachable branches
382 """
383 line = body_lines[line_index]
384 line_number = start_line + line_index
386 # Extract condition from if statement
387 if_match = re.search(r'if\s*\(([^)]+)\)', line)
388 if not if_match:
389 return
391 condition_str = if_match.group(1).strip()
393 # Parse condition into Z3 constraint
394 z3_constraint = self._parse_condition_to_z3(condition_str, state)
396 if z3_constraint is None:
397 # Can't parse condition, skip advanced analysis
398 return
400 # Create path constraint for true branch
401 true_constraint = PathConstraint(
402 condition=condition_str,
403 z3_constraint=z3_constraint,
404 line_number=line_number,
405 is_true_branch=True
406 )
408 # Create path constraint for false branch
409 false_constraint = PathConstraint(
410 condition=f"!({condition_str})",
411 z3_constraint=z3.Not(z3_constraint),
412 line_number=line_number,
413 is_true_branch=False
414 )
416 # Check if true branch is feasible
417 true_state = state.copy()
418 true_state.constraints.append(true_constraint)
420 if self._is_path_feasible(true_state):
421 # True branch is reachable
422 pass
423 else:
424 # True branch is unreachable!
425 self.vulnerabilities.append(SymbolicVulnerability(
426 vulnerability_type="unreachable_code",
427 severity="low",
428 title=f"Unreachable Code in {func_name}",
429 description=(
430 f"The condition `{condition_str}` at line {line_number} "
431 f"can never be true given the current path constraints. "
432 f"The code inside this if-block is unreachable."
433 ),
434 line_number=line_number,
435 function_name=func_name,
436 proof=f"Z3 proved condition is always false: {condition_str}",
437 confidence=95
438 ))
440 # Check if false branch is feasible (for else clauses)
441 false_state = state.copy()
442 false_state.constraints.append(false_constraint)
444 if not self._is_path_feasible(false_state):
445 # Condition is always true (else is unreachable)
446 self.vulnerabilities.append(SymbolicVulnerability(
447 vulnerability_type="unreachable_code",
448 severity="info",
449 title=f"Condition Always True in {func_name}",
450 description=(
451 f"The condition `{condition_str}` at line {line_number} "
452 f"is always true. Consider simplifying the code."
453 ),
454 line_number=line_number,
455 function_name=func_name,
456 proof=f"Z3 proved condition is always true: {condition_str}",
457 confidence=90
458 ))
460 def _parse_condition_to_z3(self, condition: str, state: SymbolicState) -> Optional[z3.BoolRef]:
461 """
462 Parse a Solidity condition into a Z3 constraint
464 DAY 2: Enhanced condition parsing
465 Supports:
466 - Comparisons: ==, !=, <, >, <=, >=
467 - Boolean operators: &&, ||, !
468 - Basic arithmetic
469 """
471 # Simple comparison operators
472 for op, z3_op in [
473 ('==', lambda a, b: a == b),
474 ('!=', lambda a, b: a != b),
475 ('>=', lambda a, b: z3.UGE(a, b)), # Unsigned greater or equal
476 ('<=', lambda a, b: z3.ULE(a, b)),
477 ('>', lambda a, b: z3.UGT(a, b)),
478 ('<', lambda a, b: z3.ULT(a, b)),
479 ]:
480 if op in condition:
481 parts = condition.split(op)
482 if len(parts) == 2:
483 left = parts[0].strip()
484 right = parts[1].strip()
486 # Try to create Z3 expressions
487 left_z3 = self._parse_expression_to_z3(left, state)
488 right_z3 = self._parse_expression_to_z3(right, state)
490 if left_z3 is not None and right_z3 is not None:
491 return z3_op(left_z3, right_z3)
493 # Boolean conditions
494 if condition in state.variables:
495 var = state.variables[condition]
496 if var.sym_type == SymbolicType.BOOL:
497 return var.z3_var
499 # Negation
500 if condition.startswith('!'):
501 inner = condition[1:].strip()
502 inner_z3 = self._parse_condition_to_z3(inner, state)
503 if inner_z3 is not None:
504 return z3.Not(inner_z3)
506 # Can't parse
507 return None
509 def _parse_expression_to_z3(self, expr: str, state: SymbolicState) -> Optional[z3.ExprRef]:
510 """Parse a Solidity expression into Z3"""
512 # Integer literal
513 if expr.isdigit():
514 return z3.BitVecVal(int(expr), 256)
516 # Variable reference
517 if expr in state.variables:
518 return state.variables[expr].z3_var
520 # msg.sender, msg.value, etc.
521 if expr.startswith('msg.'):
522 # Create symbolic variable for msg properties
523 var_name = expr.replace('.', '_')
524 if var_name not in state.variables:
525 z3_var = z3.BitVec(var_name, 256)
526 state.variables[var_name] = SymbolicVariable(
527 name=var_name,
528 sym_type=SymbolicType.UINT256,
529 z3_var=z3_var,
530 is_tainted=True
531 )
532 return state.variables[var_name].z3_var
534 # Can't parse
535 return None
537 def _is_path_feasible(self, state: SymbolicState) -> bool:
538 """
539 Check if execution path is feasible using Z3
541 DAY 2: Constraint solving for path feasibility
542 Returns True if there exists a satisfying assignment
543 """
544 if len(state.constraints) == 0:
545 return True
547 solver = z3.Solver()
549 # Add all path constraints
550 for constraint in state.constraints:
551 solver.add(constraint.z3_constraint)
553 # Check satisfiability
554 result = solver.check()
555 return result == z3.sat
557 def _check_path_feasibility(
558 self,
559 state: SymbolicState,
560 line_number: int,
561 func_name: str,
562 file_path: str
563 ):
564 """
565 Check if current path is feasible
567 If not, report unreachable code
568 """
569 if not self._is_path_feasible(state):
570 # This path is unreachable!
571 constraint_desc = ", ".join([c.condition for c in state.constraints[-3:]])
573 self.vulnerabilities.append(SymbolicVulnerability(
574 vulnerability_type="unreachable_code",
575 severity="info",
576 title=f"Unreachable Code Detected in {func_name}",
577 description=(
578 f"Code at line {line_number} is unreachable due to "
579 f"contradicting path constraints: {constraint_desc}"
580 ),
581 line_number=line_number,
582 function_name=func_name,
583 proof=f"Path constraints are unsatisfiable",
584 confidence=90
585 ))
587 def _analyze_requirement(
588 self,
589 line: str,
590 line_number: int,
591 state: SymbolicState,
592 func_name: str
593 ):
594 """
595 Analyze require/assert statements
597 DAY 2: Extract constraints from require/assert
598 These become path constraints for subsequent code
599 """
601 # Extract condition from require/assert
602 req_match = re.search(r'(require|assert)\s*\(([^)]+)\)', line)
603 if not req_match:
604 return
606 condition_str = req_match.group(2).strip()
608 # Remove error message if present
609 if ',' in condition_str:
610 condition_str = condition_str.split(',')[0].strip()
612 # Parse to Z3
613 z3_constraint = self._parse_condition_to_z3(condition_str, state)
615 if z3_constraint is not None:
616 # Add as path constraint
617 constraint = PathConstraint(
618 condition=condition_str,
619 z3_constraint=z3_constraint,
620 line_number=line_number,
621 is_true_branch=True
622 )
623 state.constraints.append(constraint)
625 # Check if this requirement is always true
626 solver = z3.Solver()
627 solver.add(z3.Not(z3_constraint)) # Can it be false?
629 if solver.check() == z3.unsat:
630 # Requirement is always satisfied (redundant)
631 self.vulnerabilities.append(SymbolicVulnerability(
632 vulnerability_type="redundant_check",
633 severity="info",
634 title=f"Redundant Check in {func_name}",
635 description=(
636 f"The requirement `{condition_str}` at line {line_number} "
637 f"is always true and can be removed."
638 ),
639 line_number=line_number,
640 function_name=func_name,
641 proof=f"Z3 proved condition is always satisfied",
642 confidence=85
643 ))
645 def _analyze_arithmetic(
646 self,
647 line: str,
648 line_number: int,
649 state: SymbolicState,
650 func_name: str,
651 file_path: str
652 ):
653 """
654 Analyze arithmetic operations for overflow/underflow
656 Detects patterns like:
657 - balance += amount
658 - total = a + b
659 - result = value - 1
660 """
662 # Pattern: variable += expr
663 add_assign_match = re.search(r'(\w+)\s*\+=\s*(.+?)[;\s]', line)
664 if add_assign_match:
665 var_name = add_assign_match.group(1)
666 expr = add_assign_match.group(2).strip()
668 # Check if unchecked block
669 is_unchecked = 'unchecked' in line or any('unchecked' in bl for bl in state.execution_path[-5:] if isinstance(bl, str))
671 if not is_unchecked:
672 # Check for potential overflow
673 self._check_overflow_addition(var_name, expr, line_number, state, func_name, file_path)
675 # Pattern: variable = a + b
676 add_match = re.search(r'(\w+)\s*=\s*(.+?)\s*\+\s*(.+?)[;\s]', line)
677 if add_match:
678 result_var = add_match.group(1)
679 left_operand = add_match.group(2).strip()
680 right_operand = add_match.group(3).strip()
682 is_unchecked = 'unchecked' in line
683 if not is_unchecked:
684 self._check_overflow_addition_expr(
685 result_var, left_operand, right_operand,
686 line_number, state, func_name, file_path
687 )
689 # Pattern: variable -= expr (underflow)
690 sub_assign_match = re.search(r'(\w+)\s*-=\s*(.+?)[;\s]', line)
691 if sub_assign_match:
692 var_name = sub_assign_match.group(1)
693 expr = sub_assign_match.group(2).strip()
695 is_unchecked = 'unchecked' in line
696 if not is_unchecked:
697 self._check_underflow_subtraction(var_name, expr, line_number, state, func_name, file_path)
699 def _check_overflow_addition(
700 self,
701 var_name: str,
702 expr: str,
703 line_number: int,
704 state: SymbolicState,
705 func_name: str,
706 file_path: str
707 ):
708 """
709 Check if addition can overflow using Z3
711 Creates constraint: var + expr > MAX_UINT256
712 If satisfiable, overflow is possible
713 """
715 # Create Z3 variables
716 var_z3 = z3.BitVec(f"{var_name}_before", 256)
717 expr_z3 = z3.BitVec(f"{expr}_value", 256)
718 result_z3 = var_z3 + expr_z3
720 # Check if overflow possible: result < var (unsigned overflow wraps)
721 overflow_constraint = z3.ULT(result_z3, var_z3)
723 # Try to find a model where overflow occurs
724 solver = z3.Solver()
725 solver.add(overflow_constraint)
727 if solver.check() == z3.sat:
728 model = solver.model()
730 # Extract example values
731 var_value = model.eval(var_z3, model_completion=True)
732 expr_value = model.eval(expr_z3, model_completion=True)
734 proof = f"Overflow possible: {var_name} = {var_value}, {expr} = {expr_value}"
736 self.vulnerabilities.append(SymbolicVulnerability(
737 vulnerability_type="integer_overflow",
738 severity="high",
739 title=f"Integer Overflow in {func_name}",
740 description=(
741 f"Arithmetic operation `{var_name} += {expr}` at line {line_number} "
742 f"can overflow. This can lead to incorrect balances or unauthorized access."
743 ),
744 line_number=line_number,
745 function_name=func_name,
746 proof=proof,
747 confidence=90
748 ))
750 def _check_overflow_addition_expr(
751 self,
752 result_var: str,
753 left: str,
754 right: str,
755 line_number: int,
756 state: SymbolicState,
757 func_name: str,
758 file_path: str
759 ):
760 """Check if a + b can overflow"""
762 left_z3 = z3.BitVec(f"{left}_value", 256)
763 right_z3 = z3.BitVec(f"{right}_value", 256)
764 result_z3 = left_z3 + right_z3
766 # Overflow check: result < left OR result < right
767 overflow_constraint = z3.Or(
768 z3.ULT(result_z3, left_z3),
769 z3.ULT(result_z3, right_z3)
770 )
772 solver = z3.Solver()
773 solver.add(overflow_constraint)
775 if solver.check() == z3.sat:
776 model = solver.model()
777 left_value = model.eval(left_z3, model_completion=True)
778 right_value = model.eval(right_z3, model_completion=True)
780 proof = f"Overflow: {left} = {left_value}, {right} = {right_value}"
782 self.vulnerabilities.append(SymbolicVulnerability(
783 vulnerability_type="integer_overflow",
784 severity="medium",
785 title=f"Potential Integer Overflow in {func_name}",
786 description=(
787 f"Addition `{result_var} = {left} + {right}` at line {line_number} "
788 f"can overflow without `unchecked` block or overflow protection."
789 ),
790 line_number=line_number,
791 function_name=func_name,
792 proof=proof,
793 confidence=85
794 ))
796 def _check_underflow_subtraction(
797 self,
798 var_name: str,
799 expr: str,
800 line_number: int,
801 state: SymbolicState,
802 func_name: str,
803 file_path: str
804 ):
805 """Check if subtraction can underflow"""
807 var_z3 = z3.BitVec(f"{var_name}_before", 256)
808 expr_z3 = z3.BitVec(f"{expr}_value", 256)
810 # Underflow: var < expr (for unsigned)
811 underflow_constraint = z3.ULT(var_z3, expr_z3)
813 solver = z3.Solver()
814 solver.add(underflow_constraint)
816 if solver.check() == z3.sat:
817 model = solver.model()
818 var_value = model.eval(var_z3, model_completion=True)
819 expr_value = model.eval(expr_z3, model_completion=True)
821 proof = f"Underflow: {var_name} = {var_value}, {expr} = {expr_value}"
823 self.vulnerabilities.append(SymbolicVulnerability(
824 vulnerability_type="integer_underflow",
825 severity="high",
826 title=f"Integer Underflow in {func_name}",
827 description=(
828 f"Subtraction `{var_name} -= {expr}` at line {line_number} "
829 f"can underflow, wrapping to MAX_UINT256."
830 ),
831 line_number=line_number,
832 function_name=func_name,
833 proof=proof,
834 confidence=90
835 ))
837 def _analyze_division(
838 self,
839 line: str,
840 line_number: int,
841 state: SymbolicState,
842 func_name: str,
843 file_path: str
844 ):
845 """Detect potential division by zero"""
847 # Pattern: variable = a / b
848 div_match = re.search(r'(\w+)\s*=\s*(.+?)\s*/\s*(.+?)[;\s]', line)
849 if not div_match:
850 return
852 result_var = div_match.group(1)
853 numerator = div_match.group(2).strip()
854 denominator = div_match.group(3).strip()
856 # Check if denominator can be zero
857 if denominator.isdigit() and int(denominator) != 0:
858 # Constant non-zero, safe
859 return
861 # Check if there's a require statement protecting against zero
862 # This is a simplified check
863 if f"require({denominator} > 0" in line or f"require({denominator} != 0" in line:
864 return
866 # Potential division by zero
867 self.vulnerabilities.append(SymbolicVulnerability(
868 vulnerability_type="division_by_zero",
869 severity="medium",
870 title=f"Potential Division by Zero in {func_name}",
871 description=(
872 f"Division operation `{result_var} = {numerator} / {denominator}` at line {line_number} "
873 f"does not check if denominator is zero. This will cause transaction revert."
874 ),
875 line_number=line_number,
876 function_name=func_name,
877 proof=f"Denominator '{denominator}' not validated before division",
878 confidence=75
879 ))
881 def _analyze_taint_flow(
882 self,
883 line: str,
884 line_number: int,
885 state: SymbolicState,
886 func_name: str,
887 file_path: str
888 ):
889 """
890 Track tainted data flow from user inputs
892 Simplified implementation for Day 1
893 """
895 # Check for external calls with tainted data
896 call_match = re.search(r'\.call\s*\{', line)
897 if call_match:
898 # Check if any parameters are tainted
899 # This is a simplified check
900 for var_name, var in state.variables.items():
901 if var.is_tainted and var_name in line:
902 self.vulnerabilities.append(SymbolicVulnerability(
903 vulnerability_type="tainted_call",
904 severity="high",
905 title=f"Tainted Data in External Call in {func_name}",
906 description=(
907 f"External call at line {line_number} uses tainted user input '{var_name}'. "
908 f"This can lead to arbitrary external calls or reentrancy."
909 ),
910 line_number=line_number,
911 function_name=func_name,
912 proof=f"Tainted variable '{var_name}' flows to external call",
913 confidence=80
914 ))
915 break
917 def _convert_to_standard_format(self, file_path: str) -> List[SolidityVulnerability]:
918 """
919 Convert SymbolicVulnerability to standard SolidityVulnerability format
920 for integration with Week 2 economic impact calculator
921 """
922 standard_vulns = []
924 for vuln in self.vulnerabilities:
925 # Map vulnerability types
926 vuln_type_map = {
927 "integer_overflow": VulnerabilityType.INTEGER_OVERFLOW_UNDERFLOW,
928 "integer_underflow": VulnerabilityType.INTEGER_OVERFLOW_UNDERFLOW,
929 "division_by_zero": VulnerabilityType.LOGIC_ERROR,
930 "tainted_call": VulnerabilityType.UNCHECKED_LOW_LEVEL_CALL,
931 }
933 vuln_type = vuln_type_map.get(vuln.vulnerability_type, VulnerabilityType.LOGIC_ERROR)
935 # Create remediation advice
936 remediation = self._get_remediation(vuln.vulnerability_type)
938 # Create code snippet with proof
939 code_snippet = vuln.proof if vuln.proof else "See line number for details"
941 standard_vuln = SolidityVulnerability(
942 vulnerability_type=vuln_type,
943 severity=vuln.severity,
944 title=f"[Symbolic Execution] {vuln.title}",
945 description=vuln.description,
946 file_path=file_path,
947 line_number=vuln.line_number,
948 function_name=vuln.function_name,
949 contract_name="unknown",
950 code_snippet=code_snippet,
951 remediation=remediation,
952 confidence=vuln.confidence
953 )
955 standard_vulns.append(standard_vuln)
957 return standard_vulns
959 def _get_remediation(self, vuln_type: str) -> str:
960 """Get remediation advice for vulnerability type"""
961 remediation_map = {
962 "integer_overflow": (
963 "Use Solidity 0.8+ which has built-in overflow protection, "
964 "or wrap arithmetic in `unchecked {}` only when overflow is intended. "
965 "Consider using SafeMath library for Solidity <0.8."
966 ),
967 "integer_underflow": (
968 "Use Solidity 0.8+ with built-in underflow protection. "
969 "Add `require()` checks before subtraction to ensure sufficient balance."
970 ),
971 "division_by_zero": (
972 "Add `require(denominator > 0, \"Division by zero\")` before division operations."
973 ),
974 "tainted_call": (
975 "Validate and sanitize all user inputs before use in external calls. "
976 "Consider using a whitelist of allowed call targets."
977 ),
978 }
980 return remediation_map.get(vuln_type, "Review and validate the operation carefully.")
983# Example usage and testing
984if __name__ == "__main__":
985 executor = SymbolicExecutor()
987 # Test case: Simple overflow
988 test_contract = """
989 contract TestContract {
990 uint256 public totalSupply;
992 function mint(uint256 amount) external {
993 totalSupply += amount; // Can overflow!
994 }
996 function burn(uint256 amount) external {
997 totalSupply -= amount; // Can underflow!
998 }
1000 function divide(uint256 a, uint256 b) external returns (uint256) {
1001 return a / b; // Division by zero!
1002 }
1003 }
1004 """
1006 vulns = executor.analyze_contract(test_contract, "test.sol")
1008 print(f"Found {len(vulns)} vulnerabilities:")
1009 for vuln in vulns:
1010 print(f"\n{vuln.severity.upper()}: {vuln.title}")
1011 print(f"Line {vuln.line_number}: {vuln.description}")
1012 if vuln.code_snippet:
1013 print(f"Proof: {vuln.code_snippet}")