""" Tests for Flash Loan Attack Detector ===================================== Covers core detection heuristics: flash loan identification, attack type classification, severity scoring, sophistication analysis, and trace parsing - all without requiring network calls. """ import unittest from datetime import UTC, datetime from app.flash_loan_attack_detector import ( AAVE_V2_FLASHLOAN_SIG, BALANCER_FLASHLOAN_SIG, FLASHLOAN_PROVIDERS, FLASHLOAN_SIGNATURES, AttackSeverity, AttackType, DetectionMethod, FlashLoanAttack, FlashLoanAttackDetector, FlashLoanCall, FlashLoanProtocol, TransactionTrace, _calculate_severity, _detect_attack_type, _is_known_flashloan_provider, _resolve_shared_aave_sig, _score_sophistication, ) class TestEnumsAndConstants(unittest.TestCase): """Enum and type classification.""" def test_severity_ordering(self): """Severity levels have correct values.""" self.assertEqual(AttackSeverity.CRITICAL.value, "critical") self.assertEqual(AttackSeverity.HIGH.value, "high") self.assertEqual(AttackSeverity.MEDIUM.value, "medium") self.assertEqual(AttackSeverity.LOW.value, "low") self.assertEqual(AttackSeverity.INFO.value, "info") def test_attack_types(self): """All expected attack types are present.""" types = {t.value for t in AttackType} expected = { "price_oracle_manipulation", "lp_drain", "arbitrage", "governance_attack", "liquidation_manipulation", "cross_protocol_chain", "self_liquidation", "reentrancy_exploit", "borrow_manipulation", "synthetic_position", "unknown", } for e in expected: self.assertIn(e, types, f"Missing attack type: {e}") def test_flashloan_protocols(self): """All expected flash loan protocols are present.""" protocols = {p.value for p in FlashLoanProtocol} expected = { "aave_v2", "aave_v3", "dydx", "uniswap_v3", "balancer", "euler", "radiant", "spark", "maker", "morpho", "silo", "unknown", } for e in expected: self.assertIn(e, protocols, f"Missing protocol: {e}") def test_flashloan_signatures(self): """All major flash loan signatures are defined.""" self.assertIn("0xab9c4b5d", FLASHLOAN_SIGNATURES) self.assertIn("0x42b0b77c", FLASHLOAN_SIGNATURES) self.assertIn("0x490e6c32", FLASHLOAN_SIGNATURES) self.assertIn("0x52b0f4c1", FLASHLOAN_SIGNATURES) def test_ethereum_providers_defined(self): """Ethereum has flash loan providers configured.""" self.assertIn("ethereum", FLASHLOAN_PROVIDERS) self.assertGreater(len(FLASHLOAN_PROVIDERS["ethereum"]), 5) def test_detection_methods(self): """All detection methods have correct values.""" self.assertEqual(DetectionMethod.DIRECT_CALL.value, "direct_call") self.assertEqual(DetectionMethod.LIFECYCLE_PATTERN.value, "lifecycle_pattern") class TestFlashLoanCall(unittest.TestCase): """FlashLoanCall data model.""" def test_valid_call(self): """Creating a valid flash loan call.""" call = FlashLoanCall( protocol=FlashLoanProtocol.AAVE_V3, provider_address="0x87870Bca3F3fD6335C3F4cE8392D69350B4fA4E2", chain="ethereum", block_number=20000000, tx_index=5, amount_borrowed_usd=1_000_000.0, amount_repaid_usd=1_001_000.0, ) self.assertEqual(call.protocol, FlashLoanProtocol.AAVE_V3) self.assertEqual(call.amount_borrowed_usd, 1_000_000.0) self.assertAlmostEqual(call.profit_usd(), -1000.0) # repaid more than borrowed def test_negative_amount_raises(self): """Negative borrow amount should raise ValueError.""" with self.assertRaises(ValueError): FlashLoanCall( protocol=FlashLoanProtocol.UNISWAP_V3, provider_address="0xdead", chain="ethereum", block_number=1, tx_index=0, amount_borrowed_usd=-100.0, ) def test_summary_format(self): """Summary produces expected format.""" call = FlashLoanCall( protocol=FlashLoanProtocol.AAVE_V3, provider_address="0x87870Bca3F3fD6335C3F4cE8392D69350B4fA4E2", chain="ethereum", block_number=20000000, tx_index=5, amount_borrowed_usd=500_000.0, token_symbol="USDC", ) summary = call.summary() self.assertIn("AAVE_V3", summary) self.assertIn("500,000", summary) self.assertIn("USDC", summary) class TestTransactionTrace(unittest.TestCase): """TransactionTrace parsing and detection.""" def setUp(self): self.valid_trace = TransactionTrace( tx_hash="0x" + "ab" * 32, chain="ethereum", block_number=20000000, tx_index=1, from_address="0xattacker00000000000000000000000000000001", to_address="0x7d2768De32b0b80b7a3454c06BdAc94A69DDc7A9", value_eth=0.0, gas_price_gwei=50.0, gas_used=250000, input_data=AAVE_V2_FLASHLOAN_SIG + "0" * 200, ) def test_flashloan_sig_detection(self): """Trace with flash loan signature detected correctly.""" self.assertTrue(self.valid_trace.has_flashloan_sig()) def test_no_flashloan_sig(self): """Trace without flash loan signature.""" trace = TransactionTrace( tx_hash="0x" + "cd" * 32, chain="ethereum", block_number=20000000, tx_index=2, from_address="0xnormal0000000000000000000000000000000001", to_address="0xnormalcontract0000000000000000000000001", input_data="0xa9059cbb" + "0" * 100, # ERC20 transfer ) self.assertFalse(trace.has_flashloan_sig()) def test_flashloan_protocol_identification(self): """Correct protocol identified from signature.""" # Use a unique signature (BALANCER_V2) to avoid signature conflicts trace = TransactionTrace( tx_hash="0x" + "ab" * 32, chain="ethereum", block_number=20000000, tx_index=1, from_address="0xattacker00000000000000000000000000000001", to_address="0x7d2768De32b0b80b7a3454c06BdAc94A69DDc7A9", value_eth=0.0, gas_price_gwei=50.0, gas_used=250000, input_data=BALANCER_FLASHLOAN_SIG + "0" * 200, ) protocol = trace.flashloan_protocol() self.assertEqual(protocol, FlashLoanProtocol.BALANCER) def test_unknown_sig_returns_none(self): """Unknown signature returns None.""" trace = TransactionTrace( tx_hash="0x" + "ef" * 32, chain="ethereum", block_number=1, tx_index=0, from_address="0x00", to_address="0x01", input_data="0xdeadbeef" + "0" * 100, ) self.assertIsNone(trace.flashloan_protocol()) def test_short_input_data(self): """Short input data doesn't crash.""" trace = TransactionTrace( tx_hash="0x" + "01" * 32, chain="ethereum", block_number=1, tx_index=0, from_address="0x00", to_address="0x01", input_data="0x1234", ) self.assertFalse(trace.has_flashloan_sig()) self.assertIsNone(trace.flashloan_protocol()) class TestDetectionHeuristics(unittest.TestCase): """Core detection logic.""" def test_oracle_manipulation_detection(self): """Oracle manipulation is detected when oracle patterns present.""" flash_loans = [ FlashLoanCall( protocol=FlashLoanProtocol.AAVE_V3, provider_address="0xaave", chain="ethereum", block_number=1, tx_index=0, amount_borrowed_usd=1_000_000.0, ) ] trace = TransactionTrace( tx_hash="0x" + "ab" * 32, chain="ethereum", block_number=1, tx_index=0, from_address="0xattacker", to_address="0xpool", input_data="0x1234" + "oracle" * 10 + "price" * 10 + "twap" * 5, ) attack_type, confidence = _detect_attack_type(flash_loans, trace) self.assertEqual(attack_type, AttackType.PRICE_ORACLE_MANIPULATION) self.assertGreater(confidence, 0.5) def test_arbitrage_is_default(self): """Normal flash loan without suspicious patterns defaults to arbitrage.""" flash_loans = [ FlashLoanCall( protocol=FlashLoanProtocol.UNISWAP_V3, provider_address="0xuni", chain="ethereum", block_number=1, tx_index=0, amount_borrowed_usd=100_000.0, ) ] trace = TransactionTrace( tx_hash="0x" + "cd" * 32, chain="ethereum", block_number=1, tx_index=0, from_address="0xtrader", to_address="0xpool", input_data="0x1234" + "normal" * 5, ) attack_type, confidence = _detect_attack_type(flash_loans, trace) self.assertEqual(attack_type, AttackType.ARBITRAGE) self.assertAlmostEqual(confidence, 0.5) def test_governance_attack_detection(self): """Governance-related contracts trigger governance attack classification.""" flash_loans = [ FlashLoanCall( protocol=FlashLoanProtocol.AAVE_V2, provider_address="0xaave", chain="ethereum", block_number=1, tx_index=0, amount_borrowed_usd=10_000_000.0, ) ] trace = TransactionTrace( tx_hash="0x" + "ef" * 32, chain="ethereum", block_number=1, tx_index=0, from_address="0xgovattacker", to_address="0xgov", input_data="0x1234" + "propose" * 5 + "vote" * 3 + "execute" * 2, ) attack_type, confidence = _detect_attack_type(flash_loans, trace) self.assertEqual(attack_type, AttackType.GOVERNANCE_ATTACK) self.assertGreater(confidence, 0.5) def test_lp_drain_detection(self): """Multiple flash loans with LP interaction triggers LP drain.""" flash_loans = [ FlashLoanCall( protocol=FlashLoanProtocol.BALANCER, provider_address="0xbal", chain="ethereum", block_number=1, tx_index=0, amount_borrowed_usd=5_000_000.0, ), FlashLoanCall( protocol=FlashLoanProtocol.AAVE_V3, provider_address="0xaave", chain="ethereum", block_number=1, tx_index=0, amount_borrowed_usd=3_000_000.0, ), ] trace = TransactionTrace( tx_hash="0x" + "01" * 32, chain="ethereum", block_number=1, tx_index=0, from_address="0xattacker", to_address="0xpool", input_data="0x1234" + "removeLiquidity" * 3 + "withdraw" * 2 + "burn" * 2, ) attack_type, confidence = _detect_attack_type(flash_loans, trace) self.assertEqual(attack_type, AttackType.LP_DRAIN) self.assertGreater(confidence, 0.5) def test_cross_protocol_chain_detection(self): """Three or more flash loans trigger cross-protocol classification.""" flash_loans = [ FlashLoanCall( protocol=FlashLoanProtocol.AAVE_V3, provider_address="0x1", chain="ethereum", block_number=1, tx_index=0, ), FlashLoanCall( protocol=FlashLoanProtocol.UNISWAP_V3, provider_address="0x2", chain="ethereum", block_number=1, tx_index=0, ), FlashLoanCall( protocol=FlashLoanProtocol.BALANCER, provider_address="0x3", chain="ethereum", block_number=1, tx_index=0, ), FlashLoanCall( protocol=FlashLoanProtocol.DYDX, provider_address="0x4", chain="ethereum", block_number=1, tx_index=0, ), ] trace = TransactionTrace( tx_hash="0x" + "02" * 32, chain="ethereum", block_number=1, tx_index=0, from_address="0xattacker", to_address="0xpool", input_data="0x1234", ) attack_type, confidence = _detect_attack_type(flash_loans, trace) self.assertEqual(attack_type, AttackType.CROSS_PROTOCOL_CHAIN) self.assertGreater(confidence, 0.5) class TestSeverityScoring(unittest.TestCase): """Attack severity calculation.""" def test_critical_over_1m(self): """Losses over $1M are critical.""" severity = _calculate_severity( attacker_profit=1_500_000, victim_loss=500_000, sophistication=5.0, ) self.assertEqual(severity, AttackSeverity.CRITICAL) def test_critical_high_sophistication(self): """High sophistication alone can trigger critical.""" severity = _calculate_severity( attacker_profit=100_000, victim_loss=50_000, sophistication=9.0, ) self.assertEqual(severity, AttackSeverity.CRITICAL) def test_high_between_100k_and_1m(self): """Losses between $100K and $1M are high.""" severity = _calculate_severity( attacker_profit=500_000, victim_loss=0, sophistication=4.0, ) self.assertEqual(severity, AttackSeverity.HIGH) def test_medium_between_10k_and_100k(self): """Losses between $10K and $100K are medium.""" severity = _calculate_severity( attacker_profit=50_000, victim_loss=0, sophistication=3.0, ) self.assertEqual(severity, AttackSeverity.MEDIUM) def test_low_under_10k(self): """Losses under $10K are low.""" severity = _calculate_severity( attacker_profit=5_000, victim_loss=0, sophistication=2.0, ) self.assertEqual(severity, AttackSeverity.LOW) def test_info_no_loss(self): """No profit/loss gives info severity.""" severity = _calculate_severity( attacker_profit=0, victim_loss=0, sophistication=0.0, ) self.assertEqual(severity, AttackSeverity.INFO) class TestSophisticationScoring(unittest.TestCase): """Attack sophistication scoring.""" def test_single_flashloan_low_sophistication(self): """Single flash loan has low sophistication.""" flash_loans = [ FlashLoanCall( protocol=FlashLoanProtocol.UNISWAP_V3, provider_address="0x1", chain="ethereum", block_number=1, tx_index=0, ) ] score = _score_sophistication( flash_loans, [], AttackType.ARBITRAGE, ) self.assertLessEqual(score, 3.0) def test_multiple_flashloans_higher_sophistication(self): """Multiple flash loans increase sophistication.""" flash_loans = [ FlashLoanCall( protocol=FlashLoanProtocol.AAVE_V3, provider_address="0x1", chain="ethereum", block_number=1, tx_index=0, ), FlashLoanCall( protocol=FlashLoanProtocol.UNISWAP_V3, provider_address="0x2", chain="ethereum", block_number=1, tx_index=0, ), FlashLoanCall( protocol=FlashLoanProtocol.BALANCER, provider_address="0x3", chain="ethereum", block_number=1, tx_index=0, ), ] score = _score_sophistication( flash_loans, [DetectionMethod.LIFECYCLE_PATTERN, DetectionMethod.ORACLE_DEVIATION], AttackType.CROSS_PROTOCOL_CHAIN, ) self.assertGreater(score, 4.0) def test_max_sophistication_capped(self): """Sophistication score is capped at 10.0.""" flash_loans = [ FlashLoanCall( protocol=FlashLoanProtocol.AAVE_V3, provider_address="0x1", chain="ethereum", block_number=1, tx_index=0, ) for _ in range(10) ] score = _score_sophistication( flash_loans, [ DetectionMethod.MULTIPLE_CALLS, DetectionMethod.ORACLE_DEVIATION, DetectionMethod.LIFECYCLE_PATTERN, ], AttackType.GOVERNANCE_ATTACK, ) self.assertLessEqual(score, 10.0) class TestFlashLoanAttackModel(unittest.TestCase): """FlashLoanAttack data model.""" def test_valid_attack(self): """Creating a valid attack with all fields.""" now = datetime.now(UTC).isoformat() attack = FlashLoanAttack( chain="ethereum", block_number=20000000, tx_hash="0x" + "ab" * 32, attacker_address="0xattacker00000000000000000000000000000001", attack_type=AttackType.PRICE_ORACLE_MANIPULATION, protocol_used=FlashLoanProtocol.AAVE_V3, total_borrowed_usd=5_000_000.0, attacker_profit_usd=1_200_000.0, victim_loss_usd=4_000_000.0, severity=AttackSeverity.CRITICAL, confidence=0.85, tags=["price_oracle_manipulation", "flash_loan_used"], detected_at=now, ) self.assertEqual(attack.chain, "ethereum") self.assertEqual(attack.severity, AttackSeverity.CRITICAL) self.assertAlmostEqual(attack.total_borrowed_usd, 5_000_000.0) def test_negative_profit_raises(self): """Negative profit should raise ValueError.""" with self.assertRaises(ValueError): FlashLoanAttack( chain="ethereum", block_number=1, tx_hash="0x" + "01" * 32, attacker_address="0x00", attacker_profit_usd=-100.0, ) def test_summary_format(self): """Attack summary produces expected format.""" attack = FlashLoanAttack( chain="ethereum", block_number=20000000, tx_hash="0x" + "ab" * 32, attacker_address="0xattacker00000000000000000000000000000001", attack_type=AttackType.PRICE_ORACLE_MANIPULATION, protocol_used=FlashLoanProtocol.AAVE_V3, attacker_profit_usd=1_200_000.0, victim_loss_usd=4_000_000.0, severity=AttackSeverity.CRITICAL, ) summary = attack.summary() self.assertIn("CRITICAL", summary) self.assertIn("FLASH LOAN ATTACK", summary) self.assertIn("1,200,000", summary) self.assertIn("4,000,000", summary) def test_to_dict_serialization(self): """to_dict produces JSON-serializable output.""" attack = FlashLoanAttack( chain="ethereum", block_number=20000000, tx_hash="0x" + "ab" * 32, attacker_address="0xattacker00000000000000000000000000000001", attack_type=AttackType.LP_DRAIN, protocol_used=FlashLoanProtocol.BALANCER, attacker_profit_usd=500_000.0, victim_loss_usd=2_000_000.0, severity=AttackSeverity.HIGH, ) d = attack.to_dict() self.assertEqual(d["type"], "flash_loan_attack") self.assertEqual(d["chain"], "ethereum") self.assertEqual(d["attack_type"], "lp_drain") self.assertEqual(d["protocol"], "balancer") self.assertEqual(d["severity"], "high") self.assertAlmostEqual(d["attacker_profit_usd"], 500_000.0) class TestProviderIdentification(unittest.TestCase): """Known flash loan provider identification.""" def test_known_provider_matches(self): """Known Aave V2 address is identified on Ethereum.""" result = _is_known_flashloan_provider( "0x7d2768De32b0b80b7a3454c06BdAc94A69DDc7A9", "ethereum", ) self.assertTrue(result) def test_unknown_provider_returns_false(self): """Unknown address returns False.""" result = _is_known_flashloan_provider( "0x0000000000000000000000000000000000000001", "ethereum", ) self.assertFalse(result) def test_case_insensitive(self): """Address matching is case-insensitive.""" result = _is_known_flashloan_provider( "0x7D2768DE32B0B80B7A3454C06BDAC94A69DDC7A9", # uppercase "ethereum", ) self.assertTrue(result) def test_chain_specific_providers(self): """Chain specific providers (e.g., Base) are resolved correctly.""" self.assertIn("base", FLASHLOAN_PROVIDERS) self.assertGreater(len(FLASHLOAN_PROVIDERS["base"]), 0) class TestSharedSigResolution(unittest.TestCase): """Shared AAVE signature disambiguation.""" def test_aave_v2_by_address(self): """Known Aave V2 address resolves to AAVE_V2.""" result = _resolve_shared_aave_sig( "0x7d2768De32b0b80b7a3454c06BdAc94A69DDc7A9", "ethereum", ) self.assertEqual(result, FlashLoanProtocol.AAVE_V2) def test_aave_v3_by_address(self): """Known Aave V3 address resolves to AAVE_V3.""" result = _resolve_shared_aave_sig( "0x87870Bca3F3fD6335C3F4cE8392D69350B4fA4E2", "ethereum", ) self.assertEqual(result, FlashLoanProtocol.AAVE_V3) def test_unknown_address_returns_unknown(self): """Unrecognized provider address returns UNKNOWN.""" result = _resolve_shared_aave_sig( "0x0000000000000000000000000000000000000000", "ethereum", ) self.assertEqual(result, FlashLoanProtocol.UNKNOWN) def test_case_insensitive_resolution(self): """Address matching is case-insensitive.""" result = _resolve_shared_aave_sig( "0x7D2768DE32B0B80B7A3454C06BDAC94A69DDC7A9", "ethereum", ) self.assertEqual(result, FlashLoanProtocol.AAVE_V2) class TestDetectorInitialization(unittest.TestCase): """FlashLoanAttackDetector initialization.""" def test_default_chains(self): """Default initialization covers all supported chains.""" detector = FlashLoanAttackDetector() self.assertGreater(len(detector.chains), 5) self.assertIn("ethereum", detector.chains) self.assertIn("bsc", detector.chains) def test_custom_chains(self): """Custom chain list is used.""" detector = FlashLoanAttackDetector(chains=["ethereum", "base"]) self.assertEqual(detector.chains, ["ethereum", "base"]) def test_cache_starts_empty(self): """Cache is empty on initialization.""" detector = FlashLoanAttackDetector() self.assertEqual(len(detector._cache), 0) def test_clear_cache(self): """Clearing cache works correctly.""" detector = FlashLoanAttackDetector() detector._cache["test"] = [] count = detector.clear_cache() self.assertEqual(count, 1) self.assertEqual(len(detector._cache), 0) class TestInternalMethods(unittest.TestCase): """Internal helper methods.""" def setUp(self): self.detector = FlashLoanAttackDetector() def test_extract_amount_from_input(self): """Amount extraction from flash loan calldata.""" # Aave flashLoan sig + 32 bytes amount input_data = AAVE_V2_FLASHLOAN_SIG + "00" * 32 + "ff" * 32 amount = self.detector._extract_amount_from_input(input_data) self.assertEqual(amount, "0x" + "00" * 32) def test_extract_amount_short_input(self): """Short input returns '0'.""" amount = self.detector._extract_amount_from_input("0x1234") self.assertEqual(amount, "0") def test_identify_known_provider(self): """Known provider address is identified.""" protocol = self.detector._identify_provider( "0x7d2768De32b0b80b7a3454c06BdAc94A69DDc7A9", "ethereum", ) self.assertEqual(protocol, FlashLoanProtocol.AAVE_V2) def test_identify_unknown_provider(self): """Unknown address returns None.""" protocol = self.detector._identify_provider( "0x0000000000000000000000000000000000000001", "ethereum", ) self.assertIsNone(protocol) def test_generate_tags(self): """Tags generation includes all expected tags.""" tags = self.detector._generate_tags( AttackType.PRICE_ORACLE_MANIPULATION, [ FlashLoanCall( protocol=FlashLoanProtocol.AAVE_V3, provider_address="0x1", chain="ethereum", block_number=1, tx_index=0, ), FlashLoanCall( protocol=FlashLoanProtocol.UNISWAP_V3, provider_address="0x2", chain="ethereum", block_number=1, tx_index=0, ), ], [DetectionMethod.DIRECT_CALL, DetectionMethod.ORACLE_DEVIATION], ) self.assertIn("price_oracle_manipulation", tags) self.assertIn("flash_loan_used", tags) self.assertIn("cross_protocol", tags) self.assertIn("oracle_attack", tags) def test_generate_timeline(self): """Timeline generation includes relevant events.""" flash_loans = [ FlashLoanCall( protocol=FlashLoanProtocol.AAVE_V3, provider_address="0x1", chain="ethereum", block_number=100, tx_index=0, amount_borrowed_usd=500_000.0, ), ] trace = TransactionTrace( tx_hash="0x" + "01" * 32, chain="ethereum", block_number=100, tx_index=5, from_address="0xattacker", to_address="0xpool", internal_calls=[{"to": "0xswap"}], ) timeline = self.detector._generate_timeline(flash_loans, trace) self.assertGreater(len(timeline), 2) self.assertTrue(any("Step 1" in t for t in timeline)) self.assertTrue(any("Internal calls" in t for t in timeline)) class TestHighValueAttackScenarios(unittest.TestCase): """Real-world attack scenario simulations.""" def test_typical_flash_loan_arbitrage(self): """Normal arbitrage should NOT be flagged as an attack.""" detector = FlashLoanAttackDetector() trace = TransactionTrace( tx_hash="0x" + "aa" * 32, chain="ethereum", block_number=20000000, tx_index=3, from_address="0xmevbot0000000000000000000000000000000001", to_address="0x7d2768De32b0b80b7a3454c06BdAc94A69DDc7A9", input_data=AAVE_V2_FLASHLOAN_SIG + "00" * 100, internal_calls=[ {"to": "0xuniswap", "input": "0x"}, {"to": "0xcurve", "input": "0x"}, ], ) # Simulate analysis flash_loans = detector._find_flash_loans(trace) attack_type, _confidence = _detect_attack_type(flash_loans, trace) # Without oracle/LP patterns, should default to arbitrage self.assertEqual(attack_type, AttackType.ARBITRAGE) def test_sophisticated_oracle_attack_scenario(self): """Sophisticated multi-step oracle manipulation should score high.""" flash_loans = [ FlashLoanCall( protocol=FlashLoanProtocol.AAVE_V3, provider_address="0x1", chain="ethereum", block_number=1, tx_index=0, amount_borrowed_usd=10_000_000.0, ), FlashLoanCall( protocol=FlashLoanProtocol.UNISWAP_V3, provider_address="0x2", chain="ethereum", block_number=1, tx_index=0, amount_borrowed_usd=5_000_000.0, ), ] trace = TransactionTrace( tx_hash="0x" + "bb" * 32, chain="ethereum", block_number=1, tx_index=0, from_address="0xattacker", to_address="0xmanipulator", input_data="0x" + "oracle" * 20 + "price" * 20 + "twap" * 10 + "getRoundData" * 10, internal_calls=[{"to": "0xpricefeed"}, {"to": "0xlp"}, {"to": "0xdex"}] * 5, ) attack_type, confidence = _detect_attack_type(flash_loans, trace) self.assertEqual(attack_type, AttackType.PRICE_ORACLE_MANIPULATION) self.assertGreater(confidence, 0.7) # Profit > $1M should be critical severity = _calculate_severity( attacker_profit=2_000_000, victim_loss=8_000_000, sophistication=8.5, ) self.assertEqual(severity, AttackSeverity.CRITICAL) if __name__ == "__main__": unittest.main()