Coverage for src/alprina_cli/agents/web3_auditor/solidity_analyzer.py: 11%
428 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"""
2Solidity Smart Contract Static Analyzer
4Inspired by Slither but enhanced for startup Web3 security needs.
5Focuses on OWASP Smart Contract Top 10 detection with economic context.
6"""
8import re
9import ast
10from pathlib import Path
11from typing import List, Dict, Any, Optional, Tuple
12from dataclasses import dataclass
13from enum import Enum
15class VulnerabilityType(Enum):
16 REENTRANCY = "reentrancy"
17 ACCESS_CONTROL = "access_control"
18 INTEGER_OVERFLOW_UNDERFLOW = "integer_overflow"
19 UNCHECKED_LOW_LEVEL_CALL = "unchecked_call"
20 LOGIC_ERROR = "logic_error"
21 TIMESTAMP_DEPENDENCE = "timestamp_dependence"
22 UNINITIALIZED_STORAGE = "uninitialized_storage"
23 ORACLE_MANIPULATION = "oracle_manipulation"
24 GAS_LIMIT_ISSUE = "gas_limit"
25 DENIAL_OF_SERVICE = "denial_of_service"
27@dataclass
28class SolidityVulnerability:
29 """Represents a smart contract vulnerability"""
30 vulnerability_type: VulnerabilityType
31 severity: str # "critical", "high", "medium", "low"
32 title: str
33 description: str
34 file_path: str
35 line_number: Optional[int]
36 function_name: Optional[str]
37 contract_name: str
38 code_snippet: Optional[str] = None
39 remediation: Optional[str] = None
40 confidence: int = 100 # 0-100
42class SolidityStaticAnalyzer:
43 """
44 Enhanced Solidity analyzer focused on Web3 startup security needs
45 """
47 def __init__(self):
48 self.vulnerability_patterns = self._initialize_patterns()
49 self.contract_structure = None
50 self.functions = []
51 self.state_variables = []
53 def analyze_contract(self, contract_code: str, file_path: str) -> List[SolidityVulnerability]:
54 """
55 Comprehensive smart contract vulnerability analysis
57 Args:
58 contract_code: Solidity source code
59 file_path: Path to the contract file
61 Returns:
62 List of detected vulnerabilities
63 """
64 vulnerabilities = []
66 try:
67 # Parse contract structure
68 self._parse_contract_structure(contract_code)
70 # Run comprehensive vulnerability detection
71 reentrancy_vulns = self._detect_reentrancy_vulnerabilities(contract_code, file_path)
72 access_control_vulns = self._detect_access_control_vulnerabilities(contract_code, file_path)
73 overflow_vulns = self._detect_integer_vulnerabilities(contract_code, file_path)
74 call_vulns = self._detect_unchecked_calls(contract_code, file_path)
75 logic_vulns = self._detect_logic_errors(contract_code, file_path)
76 oracle_vulns = self._detect_oracle_manipulation(contract_code, file_path)
77 input_vulns = self._detect_input_validation_issues(contract_code, file_path)
79 vulnerabilities.extend(reentrancy_vulns)
80 vulnerabilities.extend(access_control_vulns)
81 vulnerabilities.extend(overflow_vulns)
82 vulnerabilities.extend(call_vulns)
83 vulnerabilities.extend(logic_vulns)
84 vulnerabilities.extend(oracle_vulns)
85 vulnerabilities.extend(input_vulns)
87 except Exception as e:
88 # Add parsing error as info-level vulnerability
89 vulnerabilities.append(SolidityVulnerability(
90 vulnerability_type=VulnerabilityType.LOGIC_ERROR,
91 severity="low",
92 title="Analysis Error",
93 description=f"Could not fully analyze contract: {str(e)}",
94 file_path=file_path,
95 line_number=None,
96 function_name=None,
97 contract_name="unknown",
98 confidence=20
99 ))
101 return vulnerabilities
103 def _parse_contract_structure(self, contract_code: str):
104 """Parse contract structure to identify functions and state variables"""
105 lines = contract_code.split('\n')
107 # Find contracts
108 self.contract_structure = {
109 'contracts': [],
110 'functions': [],
111 'state_variables': []
112 }
114 current_contract = None
116 for i, line in enumerate(lines):
117 line = line.strip()
119 # Skip comments and empty lines
120 if not line or line.startswith('//') or line.startswith('/*'):
121 continue
123 # Find contract definitions
124 if line.startswith('contract ') or line.startswith('abstract contract '):
125 contract_match = re.search(r'(abstract )?contract\s+(\w+)', line)
126 if contract_match:
127 current_contract = contract_match.group(2)
128 self.contract_structure['contracts'].append({
129 'name': current_contract,
130 'line': i + 1
131 })
132 continue
134 # Find function definitions
135 if current_contract and ('function ' in line or 'modifier ' in line):
136 func_match = re.search(r'(function|modifier)\s+(\w+)', line)
137 if func_match:
138 func_type = func_match.group(1)
139 func_name = func_match.group(2)
140 function_info = {
141 'name': func_name,
142 'type': func_type,
143 'contract': current_contract,
144 'line': i + 1,
145 'visibility': 'internal' # Default
146 }
148 # Check visibility modifiers
149 if 'public' in line:
150 function_info['visibility'] = 'public'
151 elif 'external' in line:
152 function_info['visibility'] = 'external'
153 elif 'internal' in line:
154 function_info['visibility'] = 'internal'
155 elif 'private' in line:
156 function_info['visibility'] = 'private'
158 # Check for payable
159 if 'payable' in line:
160 function_info['payable'] = True
162 self.contract_structure['functions'].append(function_info)
163 continue
165 # Find state variables
166 if current_contract and ('uint256 ' in line or 'address ' in line or 'mapping(' in line):
167 # Extract variable name
168 var_match = re.search(r'(uint256|address|mapping)\s+(?:\(.*?\))?\s*(\w+)', line)
169 if var_match:
170 var_type = var_match.group(1)
171 var_name = var_match.group(2)
172 self.contract_structure['state_variables'].append({
173 'name': var_name,
174 'type': var_type,
175 'contract': current_contract,
176 'line': i + 1
177 })
179 def _detect_reentrancy_vulnerabilities(self, contract_code: str, file_path: str) -> List[SolidityVulnerability]:
180 """Detect reentrancy attack vulnerabilities"""
181 vulnerabilities = []
182 lines = contract_code.split('\n')
184 for i, line in enumerate(lines):
185 line_content = line.strip()
186 if not line_content or line_content.startswith('//'):
187 continue
189 # Pattern 1: Call to external address before state change
190 if ('.call(' in line_content or '.transfer(' in line_content or '.send(' in line_content):
191 # Check if this happens before state update in same function
192 vuln = SolidityVulnerability(
193 vulnerability_type=VulnerabilityType.REENTRANCY,
194 severity="high",
195 title="Potential Reentrancy",
196 description=f"External call detected: {line_content[:80]}... Reentrancy vulnerability if state changes happen after this call.",
197 file_path=file_path,
198 line_number=i + 1,
199 function_name=None,
200 contract_name=self._get_current_function_contract(i, lines),
201 code_snippet=line_content.strip(),
202 remediation="Implement checks-effects-interactions pattern or use ReentrancyGuard modifier",
203 confidence=75
204 )
205 vulnerabilities.append(vuln)
207 # Pattern 2: Low-level calls without checks
208 if '.call.value(' in line_content and 'return' not in line_content:
209 vuln = SolidityVulnerability(
210 vulnerability_type=VulnerabilityType.UNCHECKED_LOW_LEVEL_CALL,
211 severity="medium",
212 title="Unchecked Low-Level Call",
213 description=f"Unverified low-level call: {line_content[:80]}...",
214 file_path=file_path,
215 line_number=i + 1,
216 function_name=None,
217 contract_name=self._get_current_function_contract(i, lines),
218 code_snippet=line_content.strip(),
219 remediation="Always check return values of low-level calls",
220 confidence=85
221 )
222 vulnerabilities.append(vuln)
224 return vulnerabilities
226 def _detect_access_control_vulnerabilities(self, contract_code: str, file_path: str) -> List[SolidityVulnerability]:
227 """Detect access control vulnerabilities"""
228 vulnerabilities = []
229 lines = contract_code.split('\n')
231 critical_functions = ['withdraw', 'transferOwnership', 'mint', 'burn', 'pause', 'unpause']
233 for i, line in enumerate(lines):
234 line_content = line.strip()
235 if not line_content or line_content.startswith('//'):
236 continue
238 # Check for critical functions without access controls
239 for func_name in critical_functions:
240 if f'function {func_name}' in line_content and 'public' in line_content:
241 # Look for modifiers in the same line
242 if not any(mod in line_content for mod in ['onlyOwner', 'require', 'if', 'modifier']):
243 vuln = SolidityVulnerability(
244 vulnerability_type=VulnerabilityType.ACCESS_CONTROL,
245 severity="critical",
246 title="Missing Access Control",
247 description=f"Critical function {func_name} lacks proper access control modifier",
248 file_path=file_path,
249 line_number=i + 1,
250 function_name=func_name,
251 contract_name=self._get_current_function_contract(i, lines),
252 code_snippet=line_content.strip(),
253 remediation=f"Add access control modifier (e.g., onlyOwner) to {func_name} function",
254 confidence=90
255 )
256 vulnerabilities.append(vuln)
258 # Pattern: owner() function that returns hardcoded address
259 for i, line in enumerate(lines):
260 line_content = line.strip()
261 if 'return' in line_content and '0x' in line_content:
262 if 'owner()' in ''.join(lines[max(0, i-2):i+2]):
263 vuln = SolidityVulnerability(
264 vulnerability_type=VulnerabilityType.ACCESS_CONTROL,
265 severity="medium",
266 title="Hardcoded Owner Address",
267 description="Owner function returns hardcoded address instead of dynamic storage",
268 file_path=file_path,
269 line_number=i + 1,
270 function_name=None,
271 contract_name=self._get_current_function_contract(i, lines),
272 code_snippet=line_content.strip(),
273 remediation="Use address storage variable for owner instead of hardcoded value",
274 confidence=70
275 )
276 vulnerabilities.append(vuln)
278 return vulnerabilities
280 def _detect_integer_vulnerabilities(self, contract_code: str, file_path: str) -> List[SolidityVulnerability]:
281 """Detect integer overflow/underflow vulnerabilities"""
282 vulnerabilities = []
283 lines = contract_code.split('\n')
285 arithmetic_operations = ['+', '-', '*', '/']
287 for i, line in enumerate(lines):
288 line_content = line.strip()
289 if not line_content or line_content.startswith('//'):
290 continue
292 # Look for arithmetic operations without SafeMath
293 for op in arithmetic_operations:
294 if op in line_content and 'SafeMath' not in ''.join(lines[max(0, i-5):i+5]):
295 # Check if this is a critical operation (balance, amount, etc.)
296 context_words = ['balance', 'amount', 'total', 'supply', 'price', 'value']
297 if any(word in line_content.lower() for word in context_words):
298 vuln = SolidityVulnerability(
299 vulnerability_type=VulnerabilityType.INTEGER_OVERFLOW_UNDERFLOW,
300 severity="medium",
301 title="Potential Integer Overflow/Underflow",
302 description=f"Arithmetic operation without overflow protection: {line_content[:80]}...",
303 file_path=file_path,
304 line_number=i + 1,
305 function_name=None,
306 contract_name=self._get_current_function_contract(i, lines),
307 code_snippet=line_content.strip(),
308 remediation="Use SafeMath library or Solidity 0.8+ which has built-in overflow protection",
309 confidence=65
310 )
311 vulnerabilities.append(vuln)
313 return vulnerabilities
315 def _detect_unchecked_calls(self, contract_code: str, file_path: str) -> List[SolidityVulnerability]:
316 """Detect unchecked external calls"""
317 vulnerabilities = []
318 lines = contract_code.split('\n')
320 for i, line in enumerate(lines):
321 line_content = line.strip()
322 if not line_content or line_content.startswith('//'):
323 continue
325 # External call patterns
326 external_calls = ['.call(', '.delegatecall(', '.transfer(', '.send(']
328 for pattern in external_calls:
329 if pattern in line_content:
330 # Check if return value is being used or verified
331 next_lines = lines[i+1:i+3] # Look at next 2-3 lines
332 has_check = any('require(' in next_line or 'if (' in next_line
333 for next_line in next_lines if next_line.strip())
335 if not has_check:
336 vuln = SolidityVulnerability(
337 vulnerability_type=VulnerabilityType.UNCHECKED_LOW_LEVEL_CALL,
338 severity="high",
339 title="Unchecked External Call",
340 description=f"External call {pattern} without return value verification",
341 file_path=file_path,
342 line_number=i + 1,
343 function_name=None,
344 contract_name=self._get_current_function_contract(i, lines),
345 code_snippet=line_content.strip(),
346 remediation="Always verify return values of external calls",
347 confidence=80
348 )
349 vulnerabilities.append(vuln)
351 return vulnerabilities
353 def _detect_logic_errors(self, contract_code: str, file_path: str) -> List[SolidityVulnerability]:
354 """Detect logic errors and bad practices"""
355 vulnerabilities = []
356 lines = contract_code.split('\n')
358 # Pattern 1: Using block.timestamp for critical operations
359 for i, line in enumerate(lines):
360 line_content = line.strip()
361 if 'block.timestamp' in line_content:
362 # Check if timestamp is used for something critical
363 critical_contexts = ['deadline', 'expiration', 'unlock', 'vest']
364 if any(context in ''.join(lines[max(0, i-3):i+3]).lower() for context in critical_contexts):
365 vuln = SolidityVulnerability(
366 vulnerability_type=VulnerabilityType.TIMESTAMP_DEPENDENCE,
367 severity="medium",
368 title="Block Timestamp Manipulation Risk",
369 description="Using block.timestamp for critical logic that miners can manipulate",
370 file_path=file_path,
371 line_number=i + 1,
372 function_name=None,
373 contract_name=self._get_current_function_contract(i, lines),
374 code_snippet=line_content.strip(),
375 remediation="Use block.number or external oracle for time-dependent logic",
376 confidence=75
377 )
378 vulnerabilities.append(vuln)
380 # Pattern 2: Uninitialized storage pointers
381 for i, line in enumerate(lines):
382 line_content = line.strip()
383 if 'Storage(' in line_content and 'new' in line_content:
384 vuln = SolidityVulnerability(
385 vulnerability_type=VulnerabilityType.UNINITIALIZED_STORAGE,
386 severity="medium",
387 title="Potential Uninitialized Storage Pointer",
388 description="Creating struct or array storage without proper initialization",
389 file_path=file_path,
390 line_number=i + 1,
391 function_name=None,
392 contract_name=self._get_current_function_contract(i, lines),
393 code_snippet=line_content.strip(),
394 remediation="Initialize storage variables properly before use",
395 confidence=60
396 )
397 vulnerabilities.append(vuln)
399 return vulnerabilities
401 def _detect_oracle_manipulation(self, contract_code: str, file_path: str) -> List[SolidityVulnerability]:
402 """
403 Detect price oracle manipulation vulnerabilities
405 WEEK 2 DAY 1: Enhanced Oracle Manipulation Detection
406 Based on OWASP SC02:2025 and 2024-2025 exploit research
408 Detection Patterns:
409 1. Chainlink oracle staleness (no updatedAt check)
410 2. Single oracle source (no aggregation)
411 3. Missing price bounds validation
412 4. UniswapV2 spot price usage (flash loan vulnerable)
413 5. Missing TWAP implementation
414 6. No oracle failure handling (try/catch)
415 7. Direct pool reserve usage
416 """
417 vulnerabilities = []
418 lines = contract_code.split('\n')
420 # Track oracle usage per function for contextual analysis
421 function_contexts = self._extract_function_contexts(lines)
423 # Pattern 1: Chainlink oracle without staleness checks
424 chainlink_vulns = self._detect_chainlink_staleness(lines, file_path, function_contexts)
425 vulnerabilities.extend(chainlink_vulns)
427 # Pattern 2: Single oracle source without aggregation
428 single_oracle_vulns = self._detect_single_oracle_usage(lines, file_path, function_contexts)
429 vulnerabilities.extend(single_oracle_vulns)
431 # Pattern 3: Missing price bounds validation
432 bounds_vulns = self._detect_missing_price_bounds(lines, file_path, function_contexts)
433 vulnerabilities.extend(bounds_vulns)
435 # Pattern 4: UniswapV2 spot price vulnerability
436 uniswap_vulns = self._detect_uniswap_spot_price(lines, file_path, function_contexts)
437 vulnerabilities.extend(uniswap_vulns)
439 # Pattern 5: Pool reserve manipulation
440 reserve_vulns = self._detect_pool_reserve_manipulation(lines, file_path, function_contexts)
441 vulnerabilities.extend(reserve_vulns)
443 # Pattern 6: Missing oracle failure handling
444 failure_vulns = self._detect_missing_oracle_failure_handling(lines, file_path, function_contexts)
445 vulnerabilities.extend(failure_vulns)
447 return vulnerabilities
449 def _extract_function_contexts(self, lines: List[str]) -> Dict[int, Dict[str, Any]]:
450 """Extract function contexts for contextual analysis"""
451 contexts = {}
452 current_function = None
453 current_contract = None
454 brace_count = 0
456 for i, line in enumerate(lines):
457 line_stripped = line.strip()
459 # Track contract
460 if line_stripped.startswith('contract ') or line_stripped.startswith('abstract contract '):
461 match = re.search(r'contract\s+(\w+)', line_stripped)
462 if match:
463 current_contract = match.group(1)
465 # Track function
466 if line_stripped.startswith('function '):
467 match = re.search(r'function\s+(\w+)', line_stripped)
468 if match:
469 current_function = match.group(1)
470 brace_count = 0
472 # Track braces for function scope
473 brace_count += line_stripped.count('{') - line_stripped.count('}')
475 # Store context
476 contexts[i] = {
477 'function': current_function,
478 'contract': current_contract,
479 'in_function': brace_count > 0 and current_function is not None
480 }
482 # Reset function when it ends
483 if brace_count == 0 and current_function is not None:
484 current_function = None
486 return contexts
488 def _detect_chainlink_staleness(
489 self,
490 lines: List[str],
491 file_path: str,
492 contexts: Dict[int, Dict[str, Any]]
493 ) -> List[SolidityVulnerability]:
494 """
495 Detect Chainlink oracle usage without staleness checks
497 CVE Pattern: Missing updatedAt validation
498 Real Exploits: Polter Finance, BonqDAO Protocol
499 """
500 vulnerabilities = []
502 # Pattern: latestRoundData() without updatedAt check
503 chainlink_patterns = [
504 r'latestRoundData\s*\(',
505 r'AggregatorV3Interface',
506 r'getRoundData\s*\(',
507 ]
509 for i, line in enumerate(lines):
510 line_content = line.strip()
512 # Check if line contains Chainlink oracle call
513 if any(re.search(pattern, line_content) for pattern in chainlink_patterns):
514 context = contexts.get(i, {})
515 function_name = context.get('function', 'unknown')
516 contract_name = context.get('contract', 'unknown')
518 # Check next 15 lines for staleness validation
519 check_window = lines[i:i+15]
520 has_staleness_check = any(
521 'updatedAt' in check_line and
522 ('block.timestamp' in check_line or 'now' in check_line)
523 for check_line in check_window
524 )
526 has_price_validation = any(
527 'price' in check_line and
528 ('require' in check_line or 'revert' in check_line) and
529 ('>' in check_line or '<' in check_line)
530 for check_line in check_window
531 )
533 if not has_staleness_check:
534 vulnerabilities.append(SolidityVulnerability(
535 vulnerability_type=VulnerabilityType.ORACLE_MANIPULATION,
536 severity="high",
537 title="Chainlink Oracle Staleness Not Checked",
538 description=(
539 f"Function '{function_name}' uses Chainlink oracle without validating "
540 f"data freshness. Stale price data can be exploited for profit. "
541 f"OWASP SC02:2025 - Price Oracle Manipulation. "
542 f"Similar to Polter Finance exploit (2024)."
543 ),
544 file_path=file_path,
545 line_number=i + 1,
546 function_name=function_name,
547 contract_name=contract_name,
548 code_snippet=line_content,
549 remediation=(
550 "Add staleness validation:\n"
551 "require(block.timestamp - updatedAt < 3600, 'Stale price');\n"
552 "Also validate: updatedAt > 0, answeredInRound >= roundId, price > 0"
553 ),
554 confidence=90
555 ))
557 if not has_price_validation:
558 vulnerabilities.append(SolidityVulnerability(
559 vulnerability_type=VulnerabilityType.ORACLE_MANIPULATION,
560 severity="medium",
561 title="Missing Chainlink Price Validation",
562 description=(
563 f"Function '{function_name}' doesn't validate price from Chainlink. "
564 f"Price should be checked for: price > 0, within bounds."
565 ),
566 file_path=file_path,
567 line_number=i + 1,
568 function_name=function_name,
569 contract_name=contract_name,
570 code_snippet=line_content,
571 remediation=(
572 "Add price validation:\n"
573 "require(price > 0, 'Invalid price');\n"
574 "require(price >= minPrice && price <= maxPrice, 'Price out of bounds');"
575 ),
576 confidence=85
577 ))
579 return vulnerabilities
581 def _detect_single_oracle_usage(
582 self,
583 lines: List[str],
584 file_path: str,
585 contexts: Dict[int, Dict[str, Any]]
586 ) -> List[SolidityVulnerability]:
587 """
588 Detect single oracle source without aggregation
590 OWASP Recommendation: Use multiple independent oracle sources
591 """
592 vulnerabilities = []
594 # Track oracle sources per function
595 function_oracle_counts = {}
597 oracle_source_patterns = [
598 r'latestRoundData\s*\(', # Chainlink
599 r'getAmountsOut\s*\(', # Uniswap
600 r'consult\s*\(', # TWAP
601 r'\.price\s*\(', # Generic price getter
602 ]
604 for i, line in enumerate(lines):
605 line_content = line.strip()
606 context = contexts.get(i, {})
607 function_name = context.get('function')
609 if not function_name or not context.get('in_function'):
610 continue
612 # Count oracle sources
613 for pattern in oracle_source_patterns:
614 if re.search(pattern, line_content):
615 if function_name not in function_oracle_counts:
616 function_oracle_counts[function_name] = {
617 'count': 0,
618 'line': i + 1,
619 'contract': context.get('contract', 'unknown')
620 }
621 function_oracle_counts[function_name]['count'] += 1
623 # Report functions with single oracle source
624 for func_name, data in function_oracle_counts.items():
625 if data['count'] == 1:
626 vulnerabilities.append(SolidityVulnerability(
627 vulnerability_type=VulnerabilityType.ORACLE_MANIPULATION,
628 severity="medium",
629 title="Single Oracle Source - No Aggregation",
630 description=(
631 f"Function '{func_name}' relies on single oracle source. "
632 f"OWASP SC02:2025 recommends multiple independent oracles "
633 f"to prevent single-point manipulation. "
634 f"$70M+ lost to oracle manipulation in 2024."
635 ),
636 file_path=file_path,
637 line_number=data['line'],
638 function_name=func_name,
639 contract_name=data['contract'],
640 code_snippet=None,
641 remediation=(
642 "Implement multi-oracle strategy:\n"
643 "1. Use Chainlink + UniswapV3 TWAP\n"
644 "2. Compare prices and revert if deviation > 10%\n"
645 "3. Take median of 3+ oracle sources"
646 ),
647 confidence=80
648 ))
650 return vulnerabilities
652 def _detect_missing_price_bounds(
653 self,
654 lines: List[str],
655 file_path: str,
656 contexts: Dict[int, Dict[str, Any]]
657 ) -> List[SolidityVulnerability]:
658 """
659 Detect missing MIN_PRICE and MAX_PRICE validation
661 OWASP Mitigation: Implement price boundaries
662 """
663 vulnerabilities = []
665 price_usage_pattern = r'(price|amount|value)\s*[=:]'
667 for i, line in enumerate(lines):
668 line_content = line.strip()
669 context = contexts.get(i, {})
671 if not context.get('in_function'):
672 continue
674 # Check if line assigns/uses price
675 if re.search(price_usage_pattern, line_content):
676 # Look for oracle calls in previous 5 lines
677 prev_lines = lines[max(0, i-5):i+1]
678 has_oracle_call = any(
679 'latestRoundData' in prev or
680 'getAmountOut' in prev or
681 'consult' in prev
682 for prev in prev_lines
683 )
685 if not has_oracle_call:
686 continue
688 # Check for bounds validation in next 10 lines
689 next_lines = lines[i+1:i+10]
690 has_min_check = any('MIN' in next_line.upper() or 'minPrice' in next_line for next_line in next_lines)
691 has_max_check = any('MAX' in next_line.upper() or 'maxPrice' in next_line for next_line in next_lines)
693 if not (has_min_check and has_max_check):
694 vulnerabilities.append(SolidityVulnerability(
695 vulnerability_type=VulnerabilityType.ORACLE_MANIPULATION,
696 severity="medium",
697 title="Missing Price Bounds Validation",
698 description=(
699 f"Price usage at line {i+1} lacks MIN/MAX bounds validation. "
700 f"OWASP SC02:2025 recommends price thresholds to detect anomalies. "
701 f"Without bounds, extreme price manipulation goes undetected."
702 ),
703 file_path=file_path,
704 line_number=i + 1,
705 function_name=context.get('function', 'unknown'),
706 contract_name=context.get('contract', 'unknown'),
707 code_snippet=line_content,
708 remediation=(
709 "Add price bounds:\n"
710 "uint256 constant MIN_PRICE = 1e6; // Adjust for token\n"
711 "uint256 constant MAX_PRICE = 1e12;\n"
712 "require(price >= MIN_PRICE && price <= MAX_PRICE, 'Price anomaly');"
713 ),
714 confidence=75
715 ))
717 return vulnerabilities
719 def _detect_uniswap_spot_price(
720 self,
721 lines: List[str],
722 file_path: str,
723 contexts: Dict[int, Dict[str, Any]]
724 ) -> List[SolidityVulnerability]:
725 """
726 Detect UniswapV2 spot price usage (flash loan vulnerable)
728 Critical: Spot prices can be manipulated within single transaction
729 Real Exploits: Moby (Jan 2025), The Vow (Aug 2024)
730 """
731 vulnerabilities = []
733 # Patterns indicating spot price usage
734 spot_price_patterns = [
735 r'getAmountsOut\s*\(',
736 r'getAmountOut\s*\(',
737 r'getReserves\s*\(',
738 r'\.reserves\(',
739 r'pair\.getReserves',
740 ]
742 twap_patterns = [
743 r'consult\s*\(',
744 r'TWAP',
745 r'timeWeighted',
746 r'observe\s*\(', # UniswapV3
747 ]
749 for i, line in enumerate(lines):
750 line_content = line.strip()
751 context = contexts.get(i, {})
753 # Check if using spot price
754 is_spot_price = any(re.search(pattern, line_content) for pattern in spot_price_patterns)
756 if not is_spot_price:
757 continue
759 # Check if TWAP is also used (good)
760 check_window = lines[max(0, i-10):i+10]
761 has_twap = any(
762 any(re.search(twap_pattern, check_line) for twap_pattern in twap_patterns)
763 for check_line in check_window
764 )
766 if not has_twap:
767 vulnerabilities.append(SolidityVulnerability(
768 vulnerability_type=VulnerabilityType.ORACLE_MANIPULATION,
769 severity="critical",
770 title="UniswapV2 Spot Price Flash Loan Vulnerability",
771 description=(
772 f"Line {i+1} uses Uniswap spot price without TWAP protection. "
773 f"CRITICAL: Spot prices can be manipulated within single transaction. "
774 f"Recent Exploits: Moby (Jan 2025), The Vow (Aug 2024). "
775 f"Attackers use flash loans to manipulate pool reserves. "
776 f"OWASP SC02:2025 - Most common DeFi exploit pattern (34.3%)."
777 ),
778 file_path=file_path,
779 line_number=i + 1,
780 function_name=context.get('function', 'unknown'),
781 contract_name=context.get('contract', 'unknown'),
782 code_snippet=line_content,
783 remediation=(
784 "CRITICAL FIX REQUIRED:\n"
785 "1. Use UniswapV3 TWAP with observe() for time-weighted prices\n"
786 "2. OR use Chainlink as primary oracle with Uniswap as backup\n"
787 "3. Never rely on spot prices for critical logic\n"
788 "4. Implement price deviation checks between oracles"
789 ),
790 confidence=95
791 ))
793 return vulnerabilities
795 def _detect_pool_reserve_manipulation(
796 self,
797 lines: List[str],
798 file_path: str,
799 contexts: Dict[int, Dict[str, Any]]
800 ) -> List[SolidityVulnerability]:
801 """
802 Detect direct pool reserve usage for pricing
804 Using pool reserves directly is extremely vulnerable to manipulation
805 """
806 vulnerabilities = []
808 reserve_patterns = [
809 r'reserve0',
810 r'reserve1',
811 r'\.reserves\s*\(',
812 r'balanceOf\(address\(this\)\)',
813 r'token\.balanceOf\(pool\)',
814 ]
816 for i, line in enumerate(lines):
817 line_content = line.strip()
818 context = contexts.get(i, {})
820 if not context.get('in_function'):
821 continue
823 # Check if using reserves for calculation
824 uses_reserves = any(re.search(pattern, line_content) for pattern in reserve_patterns)
826 if uses_reserves and ('*' in line_content or '/' in line_content or '=' in line_content):
827 # Check if it's in a price calculation context
828 next_lines = lines[i:i+5]
829 looks_like_price_calc = any(
830 'price' in next_line.lower() or
831 'value' in next_line.lower() or
832 'amount' in next_line.lower()
833 for next_line in next_lines
834 )
836 if looks_like_price_calc:
837 vulnerabilities.append(SolidityVulnerability(
838 vulnerability_type=VulnerabilityType.ORACLE_MANIPULATION,
839 severity="critical",
840 title="Pool Reserve Direct Usage - Flash Loan Vulnerability",
841 description=(
842 f"Line {i+1} uses pool reserves directly for pricing. "
843 f"CRITICAL: Reserves can be manipulated within single transaction. "
844 f"This is the #1 DeFi exploit pattern. "
845 f"Using pool balances as price oracle is NEVER safe."
846 ),
847 file_path=file_path,
848 line_number=i + 1,
849 function_name=context.get('function', 'unknown'),
850 contract_name=context.get('contract', 'unknown'),
851 code_snippet=line_content,
852 remediation=(
853 "CRITICAL FIX:\n"
854 "1. Never use pool reserves directly for pricing\n"
855 "2. Use Chainlink Price Feeds for external prices\n"
856 "3. Use UniswapV3 TWAP with sufficient time window (30+ min)\n"
857 "4. Implement multi-oracle aggregation"
858 ),
859 confidence=95
860 ))
862 return vulnerabilities
864 def _detect_missing_oracle_failure_handling(
865 self,
866 lines: List[str],
867 file_path: str,
868 contexts: Dict[int, Dict[str, Any]]
869 ) -> List[SolidityVulnerability]:
870 """
871 Detect oracle calls without try/catch blocks
873 Oracle failures can DOS contracts if not handled properly
874 """
875 vulnerabilities = []
877 oracle_call_patterns = [
878 r'latestRoundData\s*\(',
879 r'getRoundData\s*\(',
880 ]
882 for i, line in enumerate(lines):
883 line_content = line.strip()
884 context = contexts.get(i, {})
886 # Check if making oracle call
887 is_oracle_call = any(re.search(pattern, line_content) for pattern in oracle_call_patterns)
889 if not is_oracle_call:
890 continue
892 # Check if wrapped in try/catch
893 prev_lines = lines[max(0, i-3):i]
894 has_try = any('try' in prev_line for prev_line in prev_lines)
896 next_lines = lines[i+1:i+5]
897 has_catch = any('catch' in next_line for next_line in next_lines)
899 if not (has_try and has_catch):
900 vulnerabilities.append(SolidityVulnerability(
901 vulnerability_type=VulnerabilityType.ORACLE_MANIPULATION,
902 severity="medium",
903 title="Missing Oracle Failure Handling",
904 description=(
905 f"Oracle call at line {i+1} lacks try/catch error handling. "
906 f"Oracle failures can cause contract DOS. "
907 f"Best practice: wrap oracle calls in try/catch with fallback logic."
908 ),
909 file_path=file_path,
910 line_number=i + 1,
911 function_name=context.get('function', 'unknown'),
912 contract_name=context.get('contract', 'unknown'),
913 code_snippet=line_content,
914 remediation=(
915 "Add error handling:\n"
916 "try oracle.latestRoundData() returns (...) {\n"
917 " // use data\n"
918 "} catch {\n"
919 " // fallback logic or revert gracefully\n"
920 "}"
921 ),
922 confidence=70
923 ))
925 return vulnerabilities
927 def _detect_input_validation_issues(self, contract_code: str, file_path: str) -> List[SolidityVulnerability]:
928 """
929 Detect input validation vulnerabilities
931 WEEK 2 DAY 2: Enhanced Input Validation Detection
932 Based on OWASP Smart Contract Top 10 2025 - $14.6M in losses
934 Detection Patterns:
935 1. Missing address(0) checks for address parameters
936 2. Missing zero/negative amount checks
937 3. Missing array bounds validation
938 4. Unchecked low-level call return values (enhanced)
939 5. Missing parameter validation in critical functions
940 6. Unsafe type conversions
941 """
942 vulnerabilities = []
943 lines = contract_code.split('\n')
945 # Extract function contexts for analysis
946 function_contexts = self._extract_function_contexts(lines)
948 # Pattern 1: Missing address(0) validation
949 address_vulns = self._detect_missing_address_validation(lines, file_path, function_contexts)
950 vulnerabilities.extend(address_vulns)
952 # Pattern 2: Missing amount/value validation
953 amount_vulns = self._detect_missing_amount_validation(lines, file_path, function_contexts)
954 vulnerabilities.extend(amount_vulns)
956 # Pattern 3: Missing array bounds validation
957 array_vulns = self._detect_missing_array_bounds(lines, file_path, function_contexts)
958 vulnerabilities.extend(array_vulns)
960 # Pattern 4: Enhanced unchecked external calls
961 external_call_vulns = self._detect_unchecked_external_calls(lines, file_path, function_contexts)
962 vulnerabilities.extend(external_call_vulns)
964 # Pattern 5: Unsafe type conversions
965 conversion_vulns = self._detect_unsafe_type_conversions(lines, file_path, function_contexts)
966 vulnerabilities.extend(conversion_vulns)
968 return vulnerabilities
970 def _detect_missing_address_validation(
971 self,
972 lines: List[str],
973 file_path: str,
974 contexts: Dict[int, Dict[str, Any]]
975 ) -> List[SolidityVulnerability]:
976 """
977 Detect missing address(0) validation
979 OWASP: Lack of Input Validation ($14.6M in losses)
980 Critical Pattern: Sending funds to address(0) = permanent loss
981 """
982 vulnerabilities = []
984 # Track function parameters
985 for i, line in enumerate(lines):
986 line_content = line.strip()
987 context = contexts.get(i, {})
989 # Check if it's a function definition with address parameter
990 if line_content.startswith('function '):
991 # Extract parameters
992 if '(' in line_content and ')' in line_content:
993 params_match = re.search(r'\((.*?)\)', line_content)
994 if params_match:
995 params_str = params_match.group(1)
997 # Find address parameters
998 address_params = re.findall(r'address\s+(\w+)', params_str)
1000 if address_params:
1001 function_name = context.get('function', 'unknown')
1003 # Check next 20 lines for address(0) validation
1004 check_window = lines[i+1:i+20]
1006 for addr_param in address_params:
1007 has_validation = any(
1008 f'{addr_param}' in check_line and
1009 ('address(0)' in check_line or '0x0' in check_line) and
1010 ('require' in check_line or 'revert' in check_line or 'if' in check_line)
1011 for check_line in check_window
1012 )
1014 # Check if it's used in critical operations
1015 is_critical = any(
1016 f'{addr_param}' in check_line and
1017 any(op in check_line for op in ['transfer', 'send', 'call', 'delegatecall', '='])
1018 for check_line in check_window
1019 )
1021 if not has_validation and is_critical:
1022 vulnerabilities.append(SolidityVulnerability(
1023 vulnerability_type=VulnerabilityType.LOGIC_ERROR,
1024 severity="high",
1025 title="Missing Address Zero Validation",
1026 description=(
1027 f"Parameter '{addr_param}' in function '{function_name}' lacks address(0) check. "
1028 f"OWASP 2025: Lack of Input Validation ($14.6M in losses). "
1029 f"Funds sent to address(0) are permanently lost - no private key exists. "
1030 f"This is a common attack vector in 2024-2025."
1031 ),
1032 file_path=file_path,
1033 line_number=i + 1,
1034 function_name=function_name,
1035 contract_name=context.get('contract', 'unknown'),
1036 code_snippet=line_content,
1037 remediation=(
1038 f"Add validation:\n"
1039 f"require({addr_param} != address(0), 'Zero address not allowed');"
1040 ),
1041 confidence=85
1042 ))
1044 return vulnerabilities
1046 def _detect_missing_amount_validation(
1047 self,
1048 lines: List[str],
1049 file_path: str,
1050 contexts: Dict[int, Dict[str, Any]]
1051 ) -> List[SolidityVulnerability]:
1052 """
1053 Detect missing amount/value validation (zero or negative)
1055 Common Pattern: Functions accepting amounts without validation
1056 """
1057 vulnerabilities = []
1059 for i, line in enumerate(lines):
1060 line_content = line.strip()
1061 context = contexts.get(i, {})
1063 # Check if it's a function with amount/value parameter
1064 if line_content.startswith('function '):
1065 if '(' in line_content and ')' in line_content:
1066 params_match = re.search(r'\((.*?)\)', line_content)
1067 if params_match:
1068 params_str = params_match.group(1)
1070 # Find amount/value parameters (uint256, uint, int)
1071 amount_params = re.findall(
1072 r'(?:uint256|uint|int256|int)\s+(\w*(?:amount|value|quantity|balance|size)\w*)',
1073 params_str,
1074 re.IGNORECASE
1075 )
1077 if amount_params:
1078 function_name = context.get('function', 'unknown')
1080 # Check next 15 lines for validation
1081 check_window = lines[i+1:i+15]
1083 for amount_param in amount_params:
1084 has_validation = any(
1085 f'{amount_param}' in check_line and
1086 ('> 0' in check_line or '!= 0' in check_line or '>=' in check_line) and
1087 ('require' in check_line or 'revert' in check_line)
1088 for check_line in check_window
1089 )
1091 if not has_validation:
1092 vulnerabilities.append(SolidityVulnerability(
1093 vulnerability_type=VulnerabilityType.LOGIC_ERROR,
1094 severity="medium",
1095 title="Missing Amount Validation",
1096 description=(
1097 f"Parameter '{amount_param}' in function '{function_name}' lacks validation. "
1098 f"Should check: amount > 0 to prevent zero-value operations. "
1099 f"OWASP 2025: Input Validation ($14.6M in losses)."
1100 ),
1101 file_path=file_path,
1102 line_number=i + 1,
1103 function_name=function_name,
1104 contract_name=context.get('contract', 'unknown'),
1105 code_snippet=line_content,
1106 remediation=(
1107 f"Add validation:\n"
1108 f"require({amount_param} > 0, 'Amount must be greater than zero');"
1109 ),
1110 confidence=75
1111 ))
1113 return vulnerabilities
1115 def _detect_missing_array_bounds(
1116 self,
1117 lines: List[str],
1118 file_path: str,
1119 contexts: Dict[int, Dict[str, Any]]
1120 ) -> List[SolidityVulnerability]:
1121 """
1122 Detect missing array bounds validation
1124 Pattern: Array access without length check
1125 """
1126 vulnerabilities = []
1128 for i, line in enumerate(lines):
1129 line_content = line.strip()
1130 context = contexts.get(i, {})
1132 if not context.get('in_function'):
1133 continue
1135 # Check for array access patterns
1136 array_access_pattern = r'(\w+)\[(\w+|\d+)\]'
1137 matches = re.findall(array_access_pattern, line_content)
1139 for array_name, index in matches:
1140 # Skip if index is a number
1141 if index.isdigit():
1142 continue
1144 # Check if there's a bounds check before this line
1145 prev_lines = lines[max(0, i-5):i]
1146 has_bounds_check = any(
1147 f'{index}' in prev_line and
1148 (f'{array_name}.length' in prev_line or 'length' in prev_line) and
1149 ('require' in prev_line or 'if' in prev_line or '<' in prev_line)
1150 for prev_line in prev_lines
1151 )
1153 if not has_bounds_check:
1154 vulnerabilities.append(SolidityVulnerability(
1155 vulnerability_type=VulnerabilityType.LOGIC_ERROR,
1156 severity="medium",
1157 title="Missing Array Bounds Validation",
1158 description=(
1159 f"Array access '{array_name}[{index}]' lacks bounds checking. "
1160 f"Out-of-bounds access causes revert but wastes gas. "
1161 f"OWASP 2025: Input Validation."
1162 ),
1163 file_path=file_path,
1164 line_number=i + 1,
1165 function_name=context.get('function', 'unknown'),
1166 contract_name=context.get('contract', 'unknown'),
1167 code_snippet=line_content,
1168 remediation=(
1169 f"Add bounds check:\n"
1170 f"require({index} < {array_name}.length, 'Index out of bounds');"
1171 ),
1172 confidence=70
1173 ))
1175 return vulnerabilities
1177 def _detect_unchecked_external_calls(
1178 self,
1179 lines: List[str],
1180 file_path: str,
1181 contexts: Dict[int, Dict[str, Any]]
1182 ) -> List[SolidityVulnerability]:
1183 """
1184 Detect unchecked external calls (enhanced)
1186 OWASP 2025: Unchecked External Calls ($550.7K in losses)
1187 Climbed from #10 to #6 in 2025 rankings
1189 Pattern: Low-level calls (.call, .delegatecall) without return value check
1190 """
1191 vulnerabilities = []
1193 low_level_calls = [
1194 r'\.call\s*\(',
1195 r'\.delegatecall\s*\(',
1196 r'\.staticcall\s*\(',
1197 ]
1199 for i, line in enumerate(lines):
1200 line_content = line.strip()
1201 context = contexts.get(i, {})
1203 if not context.get('in_function'):
1204 continue
1206 # Check for low-level calls
1207 for pattern in low_level_calls:
1208 if re.search(pattern, line_content):
1209 # Check if return value is captured
1210 captures_return = re.search(r'(?:bool\s+\w+|[\(\w]+)\s*=.*\.(?:call|delegatecall|staticcall)', line_content)
1212 if captures_return:
1213 # Check if the captured value is validated
1214 # Extract the variable name
1215 var_match = re.search(r'(?:bool\s+(\w+)|[\(](\w+))', line_content)
1216 if var_match:
1217 var_name = var_match.group(1) or var_match.group(2)
1219 # Check next 5 lines for require/if with this variable
1220 check_window = lines[i+1:i+5]
1221 has_validation = any(
1222 var_name in check_line and
1223 ('require' in check_line or 'if' in check_line or 'assert' in check_line)
1224 for check_line in check_window
1225 )
1227 if not has_validation:
1228 vulnerabilities.append(SolidityVulnerability(
1229 vulnerability_type=VulnerabilityType.UNCHECKED_LOW_LEVEL_CALL,
1230 severity="high",
1231 title="Unchecked External Call Return Value",
1232 description=(
1233 f"Low-level call return value '{var_name}' captured but not validated. "
1234 f"OWASP 2025 #6: Unchecked External Calls ($550.7K in losses). "
1235 f"Climbed from #10 to #6 in 2025 rankings. "
1236 f"Failed external calls can cause unexpected behavior if not handled."
1237 ),
1238 file_path=file_path,
1239 line_number=i + 1,
1240 function_name=context.get('function', 'unknown'),
1241 contract_name=context.get('contract', 'unknown'),
1242 code_snippet=line_content,
1243 remediation=(
1244 f"Add validation:\n"
1245 f"require({var_name}, 'External call failed');\n"
1246 f"// OR use try/catch for better error handling"
1247 ),
1248 confidence=90
1249 ))
1250 else:
1251 # Return value not even captured!
1252 vulnerabilities.append(SolidityVulnerability(
1253 vulnerability_type=VulnerabilityType.UNCHECKED_LOW_LEVEL_CALL,
1254 severity="critical",
1255 title="External Call Return Value Ignored",
1256 description=(
1257 f"Low-level call return value completely ignored. "
1258 f"CRITICAL: OWASP 2025 #6 ($550.7K in losses). "
1259 f"The call may fail silently causing logic errors or fund loss. "
1260 f"Always capture and validate external call results."
1261 ),
1262 file_path=file_path,
1263 line_number=i + 1,
1264 function_name=context.get('function', 'unknown'),
1265 contract_name=context.get('contract', 'unknown'),
1266 code_snippet=line_content,
1267 remediation=(
1268 "Capture and validate return value:\n"
1269 "(bool success, ) = target.call(...);\n"
1270 "require(success, 'External call failed');"
1271 ),
1272 confidence=95
1273 ))
1275 return vulnerabilities
1277 def _detect_unsafe_type_conversions(
1278 self,
1279 lines: List[str],
1280 file_path: str,
1281 contexts: Dict[int, Dict[str, Any]]
1282 ) -> List[SolidityVulnerability]:
1283 """
1284 Detect unsafe type conversions
1286 Pattern: Converting between types without validation
1287 """
1288 vulnerabilities = []
1290 conversion_patterns = [
1291 r'uint256\s*\(\s*int', # int to uint
1292 r'uint\s*\(\s*int',
1293 r'int\s*\(\s*uint', # uint to int
1294 r'address\s*\(\s*uint', # uint to address
1295 ]
1297 for i, line in enumerate(lines):
1298 line_content = line.strip()
1299 context = contexts.get(i, {})
1301 if not context.get('in_function'):
1302 continue
1304 for pattern in conversion_patterns:
1305 if re.search(pattern, line_content):
1306 # Check if there's validation nearby
1307 check_window = lines[max(0, i-2):i+3]
1308 has_validation = any(
1309 'require' in check_line or 'assert' in check_line
1310 for check_line in check_window
1311 )
1313 if not has_validation:
1314 vulnerabilities.append(SolidityVulnerability(
1315 vulnerability_type=VulnerabilityType.LOGIC_ERROR,
1316 severity="medium",
1317 title="Unsafe Type Conversion",
1318 description=(
1319 f"Type conversion without validation at line {i+1}. "
1320 f"Converting between signed/unsigned or numeric/address types can cause unexpected behavior. "
1321 f"OWASP 2025: Input Validation."
1322 ),
1323 file_path=file_path,
1324 line_number=i + 1,
1325 function_name=context.get('function', 'unknown'),
1326 contract_name=context.get('contract', 'unknown'),
1327 code_snippet=line_content,
1328 remediation=(
1329 "Add validation before conversion:\n"
1330 "require(value >= 0, 'Invalid conversion');\n"
1331 "// OR use SafeCast library for safe conversions"
1332 ),
1333 confidence=70
1334 ))
1336 return vulnerabilities
1338 def _get_current_function_contract(self, line_index: int, lines: List[str]) -> str:
1339 """Helper to determine current contract context"""
1340 current_contract = "unknown"
1342 # Look backwards to find most recent contract
1343 for i in range(line_index, -1, -1):
1344 line = lines[i].strip()
1345 if line.startswith('contract ') or line.startswith('abstract contract '):
1346 match = re.search(r'contract\s+(\w+)', line)
1347 if match:
1348 current_contract = match.group(1)
1349 break
1350 # Stop looking if we hit another contract boundary
1351 if line.startswith('contract ') and i < line_index:
1352 break
1354 return current_contract
1356 def _initialize_patterns(self) -> Dict[str, List[str]]:
1357 """Initialize vulnerability pattern detectors"""
1358 return {
1359 'reentrancy': [
1360 r'\.call\(',
1361 r'\.transfer\(',
1362 r'\.send\('
1363 ],
1364 'access_control': [
1365 r'function\s+\w+\s*public',
1366 r'missing.*modifier',
1367 r'no.*access.*control'
1368 ],
1369 'integer_overflow': [
1370 r'[\+\-\*\/]',
1371 r'(balance|amount|total|supply).*[\+\-\*\/]'
1372 ],
1373 'unchecked_call': [
1374 r'\.call\(',
1375 r'\.delegatecall\('
1376 ],
1377 'oracle_manipulation': [
1378 r'uniswap.*router',
1379 r'price.*oracle',
1380 r'getAmountOut'
1381 ]
1382 }