Coverage for src/alprina_cli/agents/web3_auditor/mev_detector.py: 20%
142 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"""
2MEV (Miner Extractable Value) Detection Engine
4WEEK 3 DAY 3: MEV Detection
5============================
7Detects vulnerabilities that allow miners/validators to extract value by:
8- Front-running transactions
9- Sandwich attacks on DEX swaps
10- Liquidation manipulation
11- Timestamp manipulation
13Background:
14- 2024 MEV Stats: $500M+ extracted, $100M+ from malicious MEV
15- Common attack vectors: DEX arbitrage, liquidations, oracle updates
17Author: Alprina Development Team
18Date: 2025-11-12
20References:
21- Flashbots: MEV research and data
22- MEV-Explore: Historical MEV extraction data
23- DAIAN et al.: "Flash Boys 2.0" (2019)
24"""
26import re
27from typing import List, Dict, Any, Optional, Tuple
28from dataclasses import dataclass
29from enum import Enum
31try:
32 from .solidity_analyzer import SolidityVulnerability, VulnerabilityType
33except ImportError:
34 import sys
35 from pathlib import Path
36 sys.path.insert(0, str(Path(__file__).parent))
37 from solidity_analyzer import SolidityVulnerability, VulnerabilityType
40class MEVType(Enum):
41 """Types of MEV vulnerabilities"""
42 FRONTRUNNING = "frontrunning"
43 SANDWICH_ATTACK = "sandwich_attack"
44 LIQUIDATION_MEV = "liquidation_mev"
45 TIMESTAMP_MANIPULATION = "timestamp_manipulation"
46 ORACLE_MEV = "oracle_mev"
49@dataclass
50class MEVVulnerability:
51 """MEV vulnerability with profit estimation"""
52 mev_type: MEVType
53 severity: str
54 title: str
55 description: str
56 line_number: int
57 function_name: str
58 estimated_mev_profit: Tuple[float, float] # Min, max profit in USD
59 user_loss_per_tx: Tuple[float, float] # Min, max loss per transaction
60 attack_complexity: str # "low", "medium", "high"
61 time_to_exploit: str # "immediate", "hours", "days"
62 historical_examples: List[str]
63 confidence: int = 90
66class MEVDetector:
67 """
68 Detect MEV vulnerabilities in smart contracts
70 Week 3 Day 3 Implementation:
71 1. Front-running detection (oracle updates, approvals, etc.)
72 2. Sandwich attack detection (DEX swaps without slippage)
73 3. Liquidation MEV detection (public liquidations)
74 4. Timestamp manipulation detection
76 MEV Categories:
77 - Front-running: $200M+ in 2024
78 - Sandwich attacks: $150M+ in 2024
79 - Liquidation MEV: $100M+ in 2024
80 """
82 def __init__(self):
83 self.vulnerabilities: List[MEVVulnerability] = []
85 # Historical MEV data for context
86 self.mev_historical = {
87 "frontrunning": {
88 "examples": [
89 "Bancor front-running (2020): $500K+",
90 "Uniswap V2 front-runs (2021): $2M+",
91 "NFT mint front-runs (2021-2022): $10M+"
92 ],
93 "avg_profit_per_tx": (100, 5000),
94 "total_2024": 200_000_000
95 },
96 "sandwich": {
97 "examples": [
98 "jaredfromsubway.eth: $40M+ (2023-2024)",
99 "MEV bot 0x000: $20M+ (2024)",
100 "Various sandwich bots: $100M+ (2024)"
101 ],
102 "avg_profit_per_tx": (50, 2000),
103 "total_2024": 150_000_000
104 },
105 "liquidation": {
106 "examples": [
107 "Aave liquidations: $50M+ MEV (2024)",
108 "Compound liquidations: $30M+ MEV (2024)",
109 "MakerDAO liquidations: $20M+ MEV (2024)"
110 ],
111 "avg_profit_per_tx": (500, 50000),
112 "total_2024": 100_000_000
113 }
114 }
116 def analyze_contract(self, contract_code: str, file_path: str) -> List[SolidityVulnerability]:
117 """
118 Analyze contract for MEV vulnerabilities
120 Returns standard SolidityVulnerability objects for integration
121 """
122 self.vulnerabilities = []
124 # Extract functions
125 functions = self._extract_functions(contract_code)
127 for func in functions:
128 # Detect front-running vulnerabilities
129 self._detect_frontrunning(func, contract_code)
131 # Detect sandwich attack vulnerabilities
132 self._detect_sandwich_attacks(func, contract_code)
134 # Detect liquidation MEV
135 self._detect_liquidation_mev(func, contract_code)
137 # Detect timestamp manipulation
138 self._detect_timestamp_manipulation(func, contract_code)
140 # Convert to standard format
141 return self._convert_to_standard_format(file_path)
143 def _extract_functions(self, contract_code: str) -> List[Dict[str, Any]]:
144 """Extract function definitions from contract"""
145 functions = []
146 lines = contract_code.split('\n')
148 i = 0
149 while i < len(lines):
150 line = lines[i].strip()
152 # Match function definition
153 func_match = re.match(
154 r'function\s+(\w+)\s*\([^)]*\)\s*(public|external|internal|private)?',
155 line
156 )
158 if func_match:
159 func_name = func_match.group(1)
160 visibility = func_match.group(2) or 'internal'
162 # Extract function body
163 start_line = i
164 brace_count = 0
165 body_lines = []
167 # Find opening brace
168 while i < len(lines) and '{' not in lines[i]:
169 i += 1
171 if i < len(lines):
172 brace_count = lines[i].count('{') - lines[i].count('}')
173 body_lines.append(lines[i])
174 i += 1
176 # Extract until closing brace
177 while i < len(lines) and brace_count > 0:
178 line = lines[i]
179 brace_count += line.count('{') - line.count('}')
180 body_lines.append(line)
181 i += 1
183 functions.append({
184 'name': func_name,
185 'visibility': visibility,
186 'start_line': start_line + 1,
187 'body': '\n'.join(body_lines),
188 'body_lines': body_lines
189 })
191 i += 1
193 return functions
195 def _detect_frontrunning(self, func: Dict[str, Any], contract_code: str):
196 """
197 Detect front-running vulnerabilities
199 Patterns:
200 1. Oracle price update + immediate use
201 2. Approval + transferFrom in same tx
202 3. Public state changes used in price calculations
203 4. Order placement without commitment scheme
204 """
205 func_name = func['name']
206 body = func['body']
207 start_line = func['start_line']
209 # Pattern 1: Oracle update + price usage
210 if self._has_oracle_update_and_use(body):
211 self.vulnerabilities.append(MEVVulnerability(
212 mev_type=MEVType.FRONTRUNNING,
213 severity="critical",
214 title=f"Front-Running: Oracle Price Update in {func_name}",
215 description=(
216 f"Function {func_name} updates oracle price and immediately uses it. "
217 f"Attackers can front-run this transaction to profit from the price change."
218 ),
219 line_number=start_line,
220 function_name=func_name,
221 estimated_mev_profit=(10_000, 100_000),
222 user_loss_per_tx=(1_000, 50_000),
223 attack_complexity="low",
224 time_to_exploit="immediate",
225 historical_examples=self.mev_historical["frontrunning"]["examples"],
226 confidence=95
227 ))
229 # Pattern 2: Approval pattern (approve + action)
230 if 'approve(' in body and ('transfer' in body or 'swap' in body):
231 self.vulnerabilities.append(MEVVulnerability(
232 mev_type=MEVType.FRONTRUNNING,
233 severity="high",
234 title=f"Front-Running: Approval Pattern in {func_name}",
235 description=(
236 f"Function {func_name} contains approval followed by action. "
237 f"Attackers can front-run the approval to gain advantage."
238 ),
239 line_number=start_line,
240 function_name=func_name,
241 estimated_mev_profit=(100, 5_000),
242 user_loss_per_tx=(50, 1_000),
243 attack_complexity="medium",
244 time_to_exploit="immediate",
245 historical_examples=["ERC20 approval races: $5M+ (2020-2024)"],
246 confidence=85
247 ))
249 # Pattern 3: Public state change affecting price
250 if self._has_price_affecting_public_change(body, func):
251 self.vulnerabilities.append(MEVVulnerability(
252 mev_type=MEVType.FRONTRUNNING,
253 severity="high",
254 title=f"Front-Running: Public Price-Affecting Change in {func_name}",
255 description=(
256 f"Function {func_name} makes public state changes that affect prices. "
257 f"MEV bots can front-run to profit from predictable price impact."
258 ),
259 line_number=start_line,
260 function_name=func_name,
261 estimated_mev_profit=(1_000, 50_000),
262 user_loss_per_tx=(500, 10_000),
263 attack_complexity="medium",
264 time_to_exploit="immediate",
265 historical_examples=["DEX arbitrage front-runs: $200M+ (2024)"],
266 confidence=80
267 ))
269 def _detect_sandwich_attacks(self, func: Dict[str, Any], contract_code: str):
270 """
271 Detect sandwich attack vulnerabilities
273 Patterns:
274 1. Swap without slippage protection
275 2. Missing deadline parameter
276 3. Predictable swap routing
277 4. Large swap without price impact protection
278 """
279 func_name = func['name']
280 body = func['body']
281 start_line = func['start_line']
283 # Pattern 1: Swap without slippage protection
284 has_swap = any(keyword in body.lower() for keyword in ['swap', 'exchange', 'trade'])
286 if has_swap:
287 has_slippage = any(keyword in body.lower() for keyword in [
288 'minamount', 'min_amount', 'slippage', 'amountoutmin'
289 ])
291 if not has_slippage:
292 self.vulnerabilities.append(MEVVulnerability(
293 mev_type=MEVType.SANDWICH_ATTACK,
294 severity="critical",
295 title=f"Sandwich Attack: No Slippage Protection in {func_name}",
296 description=(
297 f"Function {func_name} performs swap without slippage protection. "
298 f"MEV bots can sandwich attack: buy before (front-run), "
299 f"user swap at worse price, sell after (back-run) for profit."
300 ),
301 line_number=start_line,
302 function_name=func_name,
303 estimated_mev_profit=(50, 5_000),
304 user_loss_per_tx=(25, 2_000),
305 attack_complexity="low",
306 time_to_exploit="immediate",
307 historical_examples=self.mev_historical["sandwich"]["examples"],
308 confidence=95
309 ))
311 # Pattern 2: Missing deadline protection
312 if has_swap:
313 has_deadline = 'deadline' in body.lower()
315 if not has_deadline:
316 self.vulnerabilities.append(MEVVulnerability(
317 mev_type=MEVType.SANDWICH_ATTACK,
318 severity="high",
319 title=f"Sandwich Attack: Missing Deadline in {func_name}",
320 description=(
321 f"Function {func_name} performs swap without deadline parameter. "
322 f"Transaction can be held in mempool and executed at worst price."
323 ),
324 line_number=start_line,
325 function_name=func_name,
326 estimated_mev_profit=(100, 10_000),
327 user_loss_per_tx=(50, 5_000),
328 attack_complexity="low",
329 time_to_exploit="immediate",
330 historical_examples=["Delayed execution attacks: $10M+ (2023-2024)"],
331 confidence=90
332 ))
334 # Pattern 3: getAmountsOut without TWAP (spot price vulnerability)
335 if 'getamountsout' in body.lower() or 'getamountout' in body.lower():
336 has_twap = 'twap' in body.lower() or 'observe' in body.lower()
338 if not has_twap:
339 self.vulnerabilities.append(MEVVulnerability(
340 mev_type=MEVType.SANDWICH_ATTACK,
341 severity="critical",
342 title=f"Sandwich Attack: Spot Price Usage in {func_name}",
343 description=(
344 f"Function {func_name} uses spot price (getAmountsOut) without TWAP. "
345 f"Highly vulnerable to sandwich attacks and price manipulation."
346 ),
347 line_number=start_line,
348 function_name=func_name,
349 estimated_mev_profit=(1_000, 50_000),
350 user_loss_per_tx=(500, 20_000),
351 attack_complexity="low",
352 time_to_exploit="immediate",
353 historical_examples=["Spot price manipulation: $100M+ (2024)"],
354 confidence=95
355 ))
357 def _detect_liquidation_mev(self, func: Dict[str, Any], contract_code: str):
358 """
359 Detect liquidation MEV vulnerabilities
361 Patterns:
362 1. Public liquidation function without priority queue
363 2. Missing liquidation delay
364 3. First-come-first-serve liquidation rewards
365 4. Full liquidation without gradual approach
366 """
367 func_name = func['name']
368 body = func['body']
369 start_line = func['start_line']
370 visibility = func['visibility']
372 # Pattern 1: Public liquidation function
373 is_liquidation = 'liquidat' in func_name.lower() or 'liquidat' in body.lower()
375 if is_liquidation and visibility in ['public', 'external']:
376 has_priority = any(keyword in body.lower() for keyword in [
377 'priority', 'queue', 'delay', 'timelock'
378 ])
380 if not has_priority:
381 self.vulnerabilities.append(MEVVulnerability(
382 mev_type=MEVType.LIQUIDATION_MEV,
383 severity="high",
384 title=f"Liquidation MEV: No Priority Protection in {func_name}",
385 description=(
386 f"Function {func_name} allows public liquidation without priority mechanism. "
387 f"MEV bots with better infrastructure will always win liquidations, "
388 f"extracting maximum value."
389 ),
390 line_number=start_line,
391 function_name=func_name,
392 estimated_mev_profit=(500, 100_000),
393 user_loss_per_tx=(0, 10_000), # Borrower loss
394 attack_complexity="medium",
395 time_to_exploit="hours",
396 historical_examples=self.mev_historical["liquidation"]["examples"],
397 confidence=90
398 ))
400 # Pattern 2: Full liquidation (100% at once)
401 if is_liquidation:
402 has_partial = any(keyword in body.lower() for keyword in [
403 'partial', 'percentage', 'ratio', 'closefactor'
404 ])
406 if not has_partial:
407 self.vulnerabilities.append(MEVVulnerability(
408 mev_type=MEVType.LIQUIDATION_MEV,
409 severity="medium",
410 title=f"Liquidation MEV: Full Liquidation in {func_name}",
411 description=(
412 f"Function {func_name} allows full (100%) liquidation. "
413 f"This incentivizes MEV bots to race for maximum profit, "
414 f"potentially causing unfair borrower losses."
415 ),
416 line_number=start_line,
417 function_name=func_name,
418 estimated_mev_profit=(1_000, 50_000),
419 user_loss_per_tx=(500, 20_000),
420 attack_complexity="low",
421 time_to_exploit="hours",
422 historical_examples=["Full liquidation races: $50M+ (2024)"],
423 confidence=80
424 ))
426 def _detect_timestamp_manipulation(self, func: Dict[str, Any], contract_code: str):
427 """
428 Detect timestamp manipulation vulnerabilities
430 Patterns:
431 1. block.timestamp used in critical logic
432 2. Randomness based on timestamp
433 3. Time-based rewards without protection
434 """
435 func_name = func['name']
436 body = func['body']
437 start_line = func['start_line']
439 # Pattern 1: block.timestamp in conditional
440 if 'block.timestamp' in body:
441 # Check if used in critical operations
442 has_critical_use = any(keyword in body.lower() for keyword in [
443 'reward', 'mint', 'claim', 'unlock', 'vesting'
444 ])
446 if has_critical_use:
447 self.vulnerabilities.append(MEVVulnerability(
448 mev_type=MEVType.TIMESTAMP_MANIPULATION,
449 severity="medium",
450 title=f"Timestamp Manipulation: Critical Use in {func_name}",
451 description=(
452 f"Function {func_name} uses block.timestamp in critical logic. "
453 f"Miners can manipulate timestamp by ~15 seconds, "
454 f"potentially affecting rewards or access control."
455 ),
456 line_number=start_line,
457 function_name=func_name,
458 estimated_mev_profit=(100, 10_000),
459 user_loss_per_tx=(0, 5_000),
460 attack_complexity="high",
461 time_to_exploit="hours",
462 historical_examples=["Timestamp manipulation: $5M+ (2020-2024)"],
463 confidence=75
464 ))
466 # Pattern 2: Randomness from timestamp
467 if 'block.timestamp' in body and any(keyword in body.lower() for keyword in [
468 'random', 'seed', 'keccak', 'hash'
469 ]):
470 self.vulnerabilities.append(MEVVulnerability(
471 mev_type=MEVType.TIMESTAMP_MANIPULATION,
472 severity="high",
473 title=f"Timestamp Manipulation: Weak Randomness in {func_name}",
474 description=(
475 f"Function {func_name} uses block.timestamp for randomness. "
476 f"Miners can manipulate this to predict or influence outcomes."
477 ),
478 line_number=start_line,
479 function_name=func_name,
480 estimated_mev_profit=(1_000, 100_000),
481 user_loss_per_tx=(500, 50_000),
482 attack_complexity="medium",
483 time_to_exploit="immediate",
484 historical_examples=["Weak randomness exploits: $20M+ (2020-2024)"],
485 confidence=90
486 ))
488 def _has_oracle_update_and_use(self, body: str) -> bool:
489 """Check if function updates oracle and uses price"""
490 has_update = any(keyword in body.lower() for keyword in [
491 'updateprice', 'setprice', 'oracle.update', 'pricefeed.update'
492 ])
494 has_use = any(keyword in body.lower() for keyword in [
495 'getprice', 'price()', 'swap', 'trade', 'exchange'
496 ])
498 return has_update and has_use
500 def _has_price_affecting_public_change(self, body: str, func: Dict[str, Any]) -> bool:
501 """Check if public function affects prices"""
502 is_public = func['visibility'] in ['public', 'external']
504 affects_price = any(keyword in body.lower() for keyword in [
505 'reserves', 'liquidity', 'balance', 'supply', 'mint', 'burn'
506 ])
508 return is_public and affects_price
510 def _convert_to_standard_format(self, file_path: str) -> List[SolidityVulnerability]:
511 """Convert MEV vulnerabilities to standard format"""
512 standard_vulns = []
514 for vuln in self.vulnerabilities:
515 # Map MEV types to standard vulnerability types
516 vuln_type_map = {
517 MEVType.FRONTRUNNING: VulnerabilityType.LOGIC_ERROR,
518 MEVType.SANDWICH_ATTACK: VulnerabilityType.ORACLE_MANIPULATION,
519 MEVType.LIQUIDATION_MEV: VulnerabilityType.ACCESS_CONTROL,
520 MEVType.TIMESTAMP_MANIPULATION: VulnerabilityType.TIMESTAMP_DEPENDENCE,
521 MEVType.ORACLE_MEV: VulnerabilityType.ORACLE_MANIPULATION,
522 }
524 vuln_type = vuln_type_map.get(vuln.mev_type, VulnerabilityType.LOGIC_ERROR)
526 # Create code snippet with MEV details
527 mev_details = (
528 f"MEV Profit Potential: ${vuln.estimated_mev_profit[0]:,} - ${vuln.estimated_mev_profit[1]:,}\n"
529 f"User Loss Per TX: ${vuln.user_loss_per_tx[0]:,} - ${vuln.user_loss_per_tx[1]:,}\n"
530 f"Attack Complexity: {vuln.attack_complexity}\n"
531 f"Time to Exploit: {vuln.time_to_exploit}\n"
532 f"Historical Examples: {', '.join(vuln.historical_examples[:2])}"
533 )
535 # Create remediation advice
536 remediation = self._get_remediation(vuln.mev_type)
538 standard_vuln = SolidityVulnerability(
539 vulnerability_type=vuln_type,
540 severity=vuln.severity,
541 title=f"[MEV] {vuln.title}",
542 description=vuln.description,
543 file_path=file_path,
544 line_number=vuln.line_number,
545 function_name=vuln.function_name,
546 contract_name="unknown",
547 code_snippet=mev_details,
548 remediation=remediation,
549 confidence=vuln.confidence
550 )
552 standard_vulns.append(standard_vuln)
554 return standard_vulns
556 def _get_remediation(self, mev_type: MEVType) -> str:
557 """Get remediation advice for MEV vulnerability"""
558 remediation_map = {
559 MEVType.FRONTRUNNING: (
560 "Use commit-reveal schemes or time-locks for sensitive operations. "
561 "Consider using private mempools (e.g., Flashbots Protect) or "
562 "submarine sends to hide transactions from front-runners."
563 ),
564 MEVType.SANDWICH_ATTACK: (
565 "Add slippage protection with amountOutMin parameter. "
566 "Include deadline parameter to prevent delayed execution. "
567 "Use TWAP (Time-Weighted Average Price) instead of spot prices. "
568 "Consider using MEV-aware DEX designs (e.g., CoWSwap, 1inch Fusion)."
569 ),
570 MEVType.LIQUIDATION_MEV: (
571 "Implement liquidation priority queue or Dutch auction mechanism. "
572 "Use partial liquidations with close factor < 100%. "
573 "Add liquidation delay to allow borrowers to self-liquidate. "
574 "Consider keeper-based liquidation systems."
575 ),
576 MEVType.TIMESTAMP_MANIPULATION: (
577 "Avoid using block.timestamp for critical logic. "
578 "Use block.number with estimated block times for time-based logic. "
579 "Never use block.timestamp for randomness (use Chainlink VRF instead)."
580 ),
581 MEVType.ORACLE_MEV: (
582 "Separate oracle updates from price usage across transactions. "
583 "Use commit-reveal for oracle updates. "
584 "Implement TWAP or median prices to prevent manipulation."
585 )
586 }
588 return remediation_map.get(mev_type, "Review MEV implications carefully.")
591# Example usage and testing
592if __name__ == "__main__":
593 detector = MEVDetector()
595 # Test case: DEX with multiple MEV vulnerabilities
596 test_contract = """
597 contract VulnerableDEX {
598 IUniswapV2Router router;
599 IPriceOracle oracle;
601 // VULNERABLE: Oracle update + immediate use (front-running)
602 function updateAndSwap(uint256 amountIn) external {
603 oracle.updatePrice();
604 uint256 price = oracle.getPrice();
605 _swap(amountIn, price);
606 }
608 // VULNERABLE: Swap without slippage protection (sandwich attack)
609 function swapTokens(uint256 amountIn) external {
610 address[] memory path = new address[](2);
611 path[0] = address(tokenA);
612 path[1] = address(tokenB);
614 router.swapExactTokensForTokens(
615 amountIn,
616 0, // NO SLIPPAGE PROTECTION!
617 path,
618 msg.sender,
619 block.timestamp + 3600
620 );
621 }
623 // VULNERABLE: Public liquidation without priority (liquidation MEV)
624 function liquidate(address user) external {
625 require(isLiquidatable(user), "Not liquidatable");
627 uint256 debt = getDebt(user);
628 _liquidate(user, debt); // Full liquidation, MEV race!
629 }
631 // VULNERABLE: Timestamp used for rewards (timestamp manipulation)
632 function claimRewards() external {
633 uint256 timeElapsed = block.timestamp - lastClaim[msg.sender];
634 uint256 reward = timeElapsed * rewardRate;
636 _mint(msg.sender, reward);
637 }
638 }
639 """
641 vulns = detector.analyze_contract(test_contract, "test.sol")
643 print(f"Found {len(vulns)} MEV vulnerabilities:\n")
644 for vuln in vulns:
645 print(f"{vuln.severity.upper()}: {vuln.title}")
646 print(f" {vuln.description}")
647 print(f" {vuln.code_snippet}")
648 print()