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

1""" 

2Symbolic Execution Engine for Solidity Smart Contracts 

3 

4WEEK 3 DAY 1: Symbolic Execution 

5================================= 

6 

7Implements symbolic execution to detect vulnerabilities through path analysis 

8and constraint solving. Uses Z3 theorem prover for constraint satisfaction. 

9 

10Author: Alprina Development Team 

11Date: 2025-11-12 

12 

13References: 

14- OYENTE: Symbolic execution for Ethereum (2016) 

15- Mythril: Constraint-based vulnerability detection 

16- Manticore: Dynamic symbolic execution 

17""" 

18 

19import re 

20from typing import List, Dict, Any, Optional, Set, Tuple 

21from dataclasses import dataclass, field 

22from enum import Enum 

23 

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 

34 

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 

43 

44 

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" 

53 

54 

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 

63 

64 

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 

72 

73 

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 

82 

83 def copy(self) -> 'SymbolicState': 

84 """Create a copy of this state for branch exploration""" 

85 import copy 

86 return copy.deepcopy(self) 

87 

88 

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 

101 

102 

103class SymbolicExecutor: 

104 """ 

105 Symbolic execution engine for Solidity contracts 

106 

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 

113 

114 Week 3 Day 1 Implementation: 

115 - Basic symbolic execution framework 

116 - Integer overflow detection with Z3 

117 - Simple path exploration 

118 """ 

119 

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] = [] 

127 

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 

132 

133 def analyze_contract(self, contract_code: str, file_path: str) -> List[SolidityVulnerability]: 

134 """ 

135 Analyze contract using symbolic execution 

136 

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 [] 

143 

144 self.vulnerabilities = [] 

145 

146 # Extract functions from contract 

147 functions = self._extract_functions(contract_code) 

148 

149 for func in functions: 

150 # Symbolically execute each function 

151 self._execute_function(func, file_path) 

152 

153 # Convert to standard format 

154 return self._convert_to_standard_format(file_path) 

155 

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') 

160 

161 i = 0 

162 while i < len(lines): 

163 line = lines[i].strip() 

164 

165 # Match function definition 

166 func_match = re.match( 

167 r'function\s+(\w+)\s*\([^)]*\)\s*(public|external|internal|private)?', 

168 line 

169 ) 

170 

171 if func_match: 

172 func_name = func_match.group(1) 

173 visibility = func_match.group(2) or 'internal' 

174 

175 # Extract function body 

176 start_line = i 

177 brace_count = 0 

178 body_lines = [] 

179 

180 # Find opening brace 

181 while i < len(lines) and '{' not in lines[i]: 

182 i += 1 

183 

184 if i < len(lines): 

185 brace_count = lines[i].count('{') - lines[i].count('}') 

186 body_lines.append(lines[i]) 

187 i += 1 

188 

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 

195 

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 }) 

203 

204 i += 1 

205 

206 return functions 

207 

208 def _execute_function(self, func: Dict[str, Any], file_path: str): 

209 """ 

210 Symbolically execute a function 

211 

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'] 

217 

218 # Initialize symbolic state 

219 initial_state = SymbolicState() 

220 

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 

226 

227 # Execute symbolically 

228 self._explore_paths(body_lines, initial_state, func_name, start_line, file_path) 

229 

230 def _extract_parameters(self, func_def: str) -> Dict[str, SymbolicType]: 

231 """Extract function parameters and their types""" 

232 params = {} 

233 

234 # Match parameter list 

235 param_match = re.search(r'\(([^)]*)\)', func_def) 

236 if not param_match: 

237 return params 

238 

239 param_list = param_match.group(1) 

240 if not param_list.strip(): 

241 return params 

242 

243 # Parse each parameter 

244 for param in param_list.split(','): 

245 param = param.strip() 

246 if not param: 

247 continue 

248 

249 parts = param.split() 

250 if len(parts) >= 2: 

251 param_type_str = parts[0] 

252 param_name = parts[1] 

253 

254 # Map to symbolic type 

255 param_type = self._map_to_symbolic_type(param_type_str) 

256 params[param_name] = param_type 

257 

258 return params 

259 

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 

274 

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""" 

282 

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 

293 

294 return SymbolicVariable( 

295 name=name, 

296 sym_type=sym_type, 

297 z3_var=z3_var, 

298 is_tainted=is_tainted 

299 ) 

300 

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 

311 

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 """ 

317 

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() 

323 

324 if not line_stripped or line_stripped.startswith('//'): 

325 i += 1 

326 continue 

327 

328 # Track execution path 

329 state.execution_path.append(line_number) 

330 

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 ) 

336 

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) 

340 

341 # Day 1: Arithmetic operations 

342 self._analyze_arithmetic(line_stripped, line_number, state, func_name, file_path) 

343 

344 # Day 1: Divisions 

345 self._analyze_division(line_stripped, line_number, state, func_name, file_path) 

346 

347 # Day 1: Tainted data flow 

348 self._analyze_taint_flow(line_stripped, line_number, state, func_name, file_path) 

349 

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) 

353 

354 i += 1 

355 

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 ) 

364 

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) 

376 

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 

385 

386 # Extract condition from if statement 

387 if_match = re.search(r'if\s*\(([^)]+)\)', line) 

388 if not if_match: 

389 return 

390 

391 condition_str = if_match.group(1).strip() 

392 

393 # Parse condition into Z3 constraint 

394 z3_constraint = self._parse_condition_to_z3(condition_str, state) 

395 

396 if z3_constraint is None: 

397 # Can't parse condition, skip advanced analysis 

398 return 

399 

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 ) 

407 

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 ) 

415 

416 # Check if true branch is feasible 

417 true_state = state.copy() 

418 true_state.constraints.append(true_constraint) 

419 

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 )) 

439 

440 # Check if false branch is feasible (for else clauses) 

441 false_state = state.copy() 

442 false_state.constraints.append(false_constraint) 

443 

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 )) 

459 

460 def _parse_condition_to_z3(self, condition: str, state: SymbolicState) -> Optional[z3.BoolRef]: 

461 """ 

462 Parse a Solidity condition into a Z3 constraint 

463 

464 DAY 2: Enhanced condition parsing 

465 Supports: 

466 - Comparisons: ==, !=, <, >, <=, >= 

467 - Boolean operators: &&, ||, ! 

468 - Basic arithmetic 

469 """ 

470 

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() 

485 

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) 

489 

490 if left_z3 is not None and right_z3 is not None: 

491 return z3_op(left_z3, right_z3) 

492 

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 

498 

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) 

505 

506 # Can't parse 

507 return None 

508 

509 def _parse_expression_to_z3(self, expr: str, state: SymbolicState) -> Optional[z3.ExprRef]: 

510 """Parse a Solidity expression into Z3""" 

511 

512 # Integer literal 

513 if expr.isdigit(): 

514 return z3.BitVecVal(int(expr), 256) 

515 

516 # Variable reference 

517 if expr in state.variables: 

518 return state.variables[expr].z3_var 

519 

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 

533 

534 # Can't parse 

535 return None 

536 

537 def _is_path_feasible(self, state: SymbolicState) -> bool: 

538 """ 

539 Check if execution path is feasible using Z3 

540 

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 

546 

547 solver = z3.Solver() 

548 

549 # Add all path constraints 

550 for constraint in state.constraints: 

551 solver.add(constraint.z3_constraint) 

552 

553 # Check satisfiability 

554 result = solver.check() 

555 return result == z3.sat 

556 

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 

566 

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:]]) 

572 

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 )) 

586 

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 

596 

597 DAY 2: Extract constraints from require/assert 

598 These become path constraints for subsequent code 

599 """ 

600 

601 # Extract condition from require/assert 

602 req_match = re.search(r'(require|assert)\s*\(([^)]+)\)', line) 

603 if not req_match: 

604 return 

605 

606 condition_str = req_match.group(2).strip() 

607 

608 # Remove error message if present 

609 if ',' in condition_str: 

610 condition_str = condition_str.split(',')[0].strip() 

611 

612 # Parse to Z3 

613 z3_constraint = self._parse_condition_to_z3(condition_str, state) 

614 

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) 

624 

625 # Check if this requirement is always true 

626 solver = z3.Solver() 

627 solver.add(z3.Not(z3_constraint)) # Can it be false? 

628 

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 )) 

644 

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 

655 

656 Detects patterns like: 

657 - balance += amount 

658 - total = a + b 

659 - result = value - 1 

660 """ 

661 

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() 

667 

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)) 

670 

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) 

674 

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() 

681 

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 ) 

688 

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() 

694 

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) 

698 

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 

710 

711 Creates constraint: var + expr > MAX_UINT256 

712 If satisfiable, overflow is possible 

713 """ 

714 

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 

719 

720 # Check if overflow possible: result < var (unsigned overflow wraps) 

721 overflow_constraint = z3.ULT(result_z3, var_z3) 

722 

723 # Try to find a model where overflow occurs 

724 solver = z3.Solver() 

725 solver.add(overflow_constraint) 

726 

727 if solver.check() == z3.sat: 

728 model = solver.model() 

729 

730 # Extract example values 

731 var_value = model.eval(var_z3, model_completion=True) 

732 expr_value = model.eval(expr_z3, model_completion=True) 

733 

734 proof = f"Overflow possible: {var_name} = {var_value}, {expr} = {expr_value}" 

735 

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 )) 

749 

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""" 

761 

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 

765 

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 ) 

771 

772 solver = z3.Solver() 

773 solver.add(overflow_constraint) 

774 

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) 

779 

780 proof = f"Overflow: {left} = {left_value}, {right} = {right_value}" 

781 

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 )) 

795 

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""" 

806 

807 var_z3 = z3.BitVec(f"{var_name}_before", 256) 

808 expr_z3 = z3.BitVec(f"{expr}_value", 256) 

809 

810 # Underflow: var < expr (for unsigned) 

811 underflow_constraint = z3.ULT(var_z3, expr_z3) 

812 

813 solver = z3.Solver() 

814 solver.add(underflow_constraint) 

815 

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) 

820 

821 proof = f"Underflow: {var_name} = {var_value}, {expr} = {expr_value}" 

822 

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 )) 

836 

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""" 

846 

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 

851 

852 result_var = div_match.group(1) 

853 numerator = div_match.group(2).strip() 

854 denominator = div_match.group(3).strip() 

855 

856 # Check if denominator can be zero 

857 if denominator.isdigit() and int(denominator) != 0: 

858 # Constant non-zero, safe 

859 return 

860 

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 

865 

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 )) 

880 

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 

891 

892 Simplified implementation for Day 1 

893 """ 

894 

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 

916 

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 = [] 

923 

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 } 

932 

933 vuln_type = vuln_type_map.get(vuln.vulnerability_type, VulnerabilityType.LOGIC_ERROR) 

934 

935 # Create remediation advice 

936 remediation = self._get_remediation(vuln.vulnerability_type) 

937 

938 # Create code snippet with proof 

939 code_snippet = vuln.proof if vuln.proof else "See line number for details" 

940 

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 ) 

954 

955 standard_vulns.append(standard_vuln) 

956 

957 return standard_vulns 

958 

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 } 

979 

980 return remediation_map.get(vuln_type, "Review and validate the operation carefully.") 

981 

982 

983# Example usage and testing 

984if __name__ == "__main__": 

985 executor = SymbolicExecutor() 

986 

987 # Test case: Simple overflow 

988 test_contract = """ 

989 contract TestContract { 

990 uint256 public totalSupply; 

991 

992 function mint(uint256 amount) external { 

993 totalSupply += amount; // Can overflow! 

994 } 

995 

996 function burn(uint256 amount) external { 

997 totalSupply -= amount; // Can underflow! 

998 } 

999 

1000 function divide(uint256 a, uint256 b) external returns (uint256) { 

1001 return a / b; // Division by zero! 

1002 } 

1003 } 

1004 """ 

1005 

1006 vulns = executor.analyze_contract(test_contract, "test.sol") 

1007 

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}")