1use crate::contract::source::ContractSource;
17use serde::{Deserialize, Serialize};
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct DefiAnalysis {
22 pub protocol_type: ProtocolType,
24 pub has_oracle_dependency: bool,
26 pub oracle_info: Vec<OracleInfo>,
28 pub has_flash_loan_risk: bool,
30 pub flash_loan_info: Vec<String>,
32 pub dex_integrations: Vec<DexIntegration>,
34 pub lending_patterns: Vec<String>,
36 pub token_standards: Vec<TokenStandard>,
38 pub staking_patterns: Vec<String>,
40 pub risk_factors: Vec<DefiRiskFactor>,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
46pub enum ProtocolType {
47 Token,
49 DEX,
51 Lending,
53 Yield,
55 Governance,
57 Bridge,
59 NFTMarketplace,
61 Other,
63}
64
65impl std::fmt::Display for ProtocolType {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 match self {
68 ProtocolType::Token => write!(f, "Token"),
69 ProtocolType::DEX => write!(f, "DEX/AMM"),
70 ProtocolType::Lending => write!(f, "Lending"),
71 ProtocolType::Yield => write!(f, "Yield/Staking"),
72 ProtocolType::Governance => write!(f, "Governance"),
73 ProtocolType::Bridge => write!(f, "Bridge"),
74 ProtocolType::NFTMarketplace => write!(f, "NFT Marketplace"),
75 ProtocolType::Other => write!(f, "Other"),
76 }
77 }
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct OracleInfo {
83 pub provider: String,
85 pub usage: String,
87 pub risks: Vec<String>,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct DexIntegration {
94 pub dex: String,
96 pub integration_type: String,
98 pub has_slippage_protection: bool,
100 pub has_deadline_protection: bool,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
106pub enum TokenStandard {
107 ERC20,
108 ERC721,
109 ERC1155,
110 ERC4626,
111 Custom(String),
112}
113
114impl std::fmt::Display for TokenStandard {
115 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116 match self {
117 TokenStandard::ERC20 => write!(f, "ERC-20"),
118 TokenStandard::ERC721 => write!(f, "ERC-721"),
119 TokenStandard::ERC1155 => write!(f, "ERC-1155"),
120 TokenStandard::ERC4626 => write!(f, "ERC-4626"),
121 TokenStandard::Custom(name) => write!(f, "{}", name),
122 }
123 }
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct DefiRiskFactor {
129 pub name: String,
131 pub description: String,
133 pub severity: u8,
135}
136
137pub fn analyze_defi_patterns(source: &ContractSource) -> DefiAnalysis {
139 let code = &source.source_code;
140
141 let oracle_info = detect_oracles(code);
142 let has_oracle_dependency = !oracle_info.is_empty();
143 let flash_loan_info = detect_flash_loan_patterns(code);
144 let has_flash_loan_risk = !flash_loan_info.is_empty();
145 let dex_integrations = detect_dex_integrations(code);
146 let lending_patterns = detect_lending_patterns(code);
147 let token_standards = detect_token_standards(code, &source.parsed_abi);
148 let staking_patterns = detect_staking_patterns(code);
149 let protocol_type =
150 classify_protocol(code, &token_standards, &dex_integrations, &lending_patterns);
151 let risk_factors = assess_defi_risks(
152 &oracle_info,
153 &flash_loan_info,
154 &dex_integrations,
155 &lending_patterns,
156 );
157
158 DefiAnalysis {
159 protocol_type,
160 has_oracle_dependency,
161 oracle_info,
162 has_flash_loan_risk,
163 flash_loan_info,
164 dex_integrations,
165 lending_patterns,
166 token_standards,
167 staking_patterns,
168 risk_factors,
169 }
170}
171
172fn detect_oracles(code: &str) -> Vec<OracleInfo> {
173 let mut oracles = Vec::new();
174 let code_lower = code.to_lowercase();
175
176 if code_lower.contains("aggregatorv3interface")
178 || code_lower.contains("latestrounddata")
179 || code_lower.contains("chainlink")
180 || code_lower.contains("pricefeed")
181 {
182 let mut risks = vec!["Stale price data if heartbeat check is missing".to_string()];
183
184 if !code.contains("updatedAt") && !code.contains("answeredInRound") {
186 risks.push("Missing staleness check on Chainlink price feed".to_string());
187 }
188
189 if !code_lower.contains("sequenceruptimefeed") {
191 risks.push("Missing L2 sequencer uptime check (if deployed on L2)".to_string());
192 }
193
194 oracles.push(OracleInfo {
195 provider: "Chainlink".to_string(),
196 usage: "Price feed (AggregatorV3Interface)".to_string(),
197 risks,
198 });
199 }
200
201 if code_lower.contains("observe(")
203 || code_lower.contains("twap")
204 || code_lower.contains("oraclelibrary")
205 {
206 oracles.push(OracleInfo {
207 provider: "Uniswap V3 TWAP".to_string(),
208 usage: "Time-weighted average price oracle".to_string(),
209 risks: vec![
210 "TWAP can be manipulated with sustained capital over the observation window"
211 .to_string(),
212 "Short TWAP windows are more susceptible to manipulation".to_string(),
213 ],
214 });
215 }
216
217 if code_lower.contains("istdreference") || code_lower.contains("bandprotocol") {
219 oracles.push(OracleInfo {
220 provider: "Band Protocol".to_string(),
221 usage: "External data oracle".to_string(),
222 risks: vec!["Verify Band oracle reliability for the specific data feed".to_string()],
223 });
224 }
225
226 if (code_lower.contains("getprice") || code_lower.contains("fetchprice")) && oracles.is_empty()
228 {
229 oracles.push(OracleInfo {
230 provider: "Custom/Unknown".to_string(),
231 usage: "Price retrieval function".to_string(),
232 risks: vec!["Custom oracle — verify data source reliability".to_string()],
233 });
234 }
235
236 oracles
237}
238
239fn detect_flash_loan_patterns(code: &str) -> Vec<String> {
240 let mut patterns = Vec::new();
241 let code_lower = code.to_lowercase();
242
243 if code_lower.contains("flashloan") || code_lower.contains("flash_loan") {
244 patterns.push("Flash loan function detected".to_string());
245 }
246
247 if code_lower.contains("executeflashloan")
248 || code_lower.contains("onflashloan")
249 || code_lower.contains("flashloansimple")
250 {
251 patterns.push("AAVE-style flash loan callback".to_string());
252 }
253
254 if code_lower.contains("callfunctionwithvalue") || code_lower.contains("flashborrowerfn") {
255 patterns.push("dYdX-style flash loan integration".to_string());
256 }
257
258 if code_lower.contains("uniswapv2call") || code_lower.contains("uniswapv3flashcallback") {
259 patterns.push("Uniswap flash swap callback".to_string());
260 }
261
262 if !patterns.is_empty() && !code_lower.contains("balanceof") {
264 patterns
265 .push("WARNING: Flash loan callback without explicit balance validation".to_string());
266 }
267
268 patterns
269}
270
271fn detect_dex_integrations(code: &str) -> Vec<DexIntegration> {
272 let mut integrations = Vec::new();
273 let code_lower = code.to_lowercase();
274
275 if code_lower.contains("iuniswapv2router") || code_lower.contains("swaprouter") {
277 let has_slippage =
278 code_lower.contains("amountoutmin") || code_lower.contains("amountinmax");
279 let has_deadline =
280 code_lower.contains("deadline") || code_lower.contains("block.timestamp");
281
282 integrations.push(DexIntegration {
283 dex: "Uniswap".to_string(),
284 integration_type: "Swap router".to_string(),
285 has_slippage_protection: has_slippage,
286 has_deadline_protection: has_deadline,
287 });
288 }
289
290 if code_lower.contains("isushiswap") || code_lower.contains("sushirouter") {
292 integrations.push(DexIntegration {
293 dex: "SushiSwap".to_string(),
294 integration_type: "Swap router".to_string(),
295 has_slippage_protection: code_lower.contains("amountoutmin"),
296 has_deadline_protection: code_lower.contains("deadline"),
297 });
298 }
299
300 if code_lower.contains("icurve")
302 || code_lower.contains("curvepool")
303 || code_lower.contains("stableswap")
304 {
305 integrations.push(DexIntegration {
306 dex: "Curve".to_string(),
307 integration_type: "Stableswap pool".to_string(),
308 has_slippage_protection: code_lower.contains("min_amount")
309 || code_lower.contains("_min_dy"),
310 has_deadline_protection: false,
311 });
312 }
313
314 if code_lower.contains("ibalancer")
316 || code_lower.contains("ivault") && code_lower.contains("swap")
317 {
318 integrations.push(DexIntegration {
319 dex: "Balancer".to_string(),
320 integration_type: "Vault swap".to_string(),
321 has_slippage_protection: code_lower.contains("limit"),
322 has_deadline_protection: code_lower.contains("deadline"),
323 });
324 }
325
326 integrations
327}
328
329fn detect_lending_patterns(code: &str) -> Vec<String> {
330 let mut patterns = Vec::new();
331 let code_lower = code.to_lowercase();
332
333 if code_lower.contains("borrow") && code_lower.contains("repay") {
334 patterns.push("Borrow/repay lending pattern".to_string());
335 }
336
337 if code_lower.contains("collateral") && code_lower.contains("liquidat") {
338 patterns.push("Collateralized lending with liquidation".to_string());
339 }
340
341 if code_lower.contains("lendingpool") || code_lower.contains("comptroller") {
342 patterns.push("Aave/Compound-style lending pool integration".to_string());
343 }
344
345 if code_lower.contains("healthfactor") || code_lower.contains("collateralratio") {
346 patterns.push("Health factor / collateral ratio monitoring".to_string());
347 }
348
349 if code_lower.contains("interest") && code_lower.contains("rate") {
350 patterns.push("Interest rate model".to_string());
351 }
352
353 patterns
354}
355
356fn detect_token_standards(
357 code: &str,
358 abi: &[crate::contract::source::AbiEntry],
359) -> Vec<TokenStandard> {
360 let mut standards = Vec::new();
361 let code_lower = code.to_lowercase();
362
363 let has_erc20_functions = abi.iter().any(|e| {
365 e.entry_type == "function"
366 && (e.name == "transfer" || e.name == "approve" || e.name == "transferFrom")
367 });
368 if has_erc20_functions || code_lower.contains("erc20") || code_lower.contains("ierc20") {
369 standards.push(TokenStandard::ERC20);
370 }
371
372 let has_erc721 = abi.iter().any(|e| {
374 e.entry_type == "function" && (e.name == "ownerOf" || e.name == "safeTransferFrom")
375 });
376 if has_erc721 || code_lower.contains("erc721") || code_lower.contains("ierc721") {
377 standards.push(TokenStandard::ERC721);
378 }
379
380 if code_lower.contains("erc1155") || code_lower.contains("ierc1155") {
382 standards.push(TokenStandard::ERC1155);
383 }
384
385 if code_lower.contains("erc4626")
387 || (code_lower.contains("deposit") && code_lower.contains("shares"))
388 {
389 standards.push(TokenStandard::ERC4626);
390 }
391
392 standards
393}
394
395fn detect_staking_patterns(code: &str) -> Vec<String> {
396 let mut patterns = Vec::new();
397 let code_lower = code.to_lowercase();
398
399 if code_lower.contains("stake") && code_lower.contains("unstake") {
400 patterns.push("Stake/unstake mechanism".to_string());
401 }
402
403 if code_lower.contains("rewardpertoken") || code_lower.contains("earned") {
404 patterns.push("Reward distribution (Synthetix-style)".to_string());
405 }
406
407 if code_lower.contains("vesting") || code_lower.contains("vestingschedule") {
408 patterns.push("Token vesting schedule".to_string());
409 }
410
411 if code_lower.contains("timelock") || code_lower.contains("lockperiod") {
412 patterns.push("Time-locked staking".to_string());
413 }
414
415 patterns
416}
417
418fn classify_protocol(
419 code: &str,
420 token_standards: &[TokenStandard],
421 dex_integrations: &[DexIntegration],
422 lending_patterns: &[String],
423) -> ProtocolType {
424 let code_lower = code.to_lowercase();
425
426 if !lending_patterns.is_empty() {
427 ProtocolType::Lending
428 } else if !dex_integrations.is_empty()
429 || code_lower.contains("addliquidity")
430 || code_lower.contains("removeliquidity")
431 {
432 ProtocolType::DEX
433 } else if code_lower.contains("governance")
434 || code_lower.contains("propose") && code_lower.contains("vote")
435 {
436 ProtocolType::Governance
437 } else if code_lower.contains("bridge") || code_lower.contains("crosschain") {
438 ProtocolType::Bridge
439 } else if token_standards
440 .iter()
441 .any(|s| matches!(s, TokenStandard::ERC721 | TokenStandard::ERC1155))
442 && (code_lower.contains("marketplace") || code_lower.contains("auction"))
443 {
444 ProtocolType::NFTMarketplace
445 } else if code_lower.contains("stake")
446 || code_lower.contains("farm")
447 || code_lower.contains("yield")
448 {
449 ProtocolType::Yield
450 } else if !token_standards.is_empty() {
451 ProtocolType::Token
452 } else {
453 ProtocolType::Other
454 }
455}
456
457fn assess_defi_risks(
458 oracle_info: &[OracleInfo],
459 flash_loan_info: &[String],
460 dex_integrations: &[DexIntegration],
461 lending_patterns: &[String],
462) -> Vec<DefiRiskFactor> {
463 let mut risks = Vec::new();
464
465 for oracle in oracle_info {
467 for risk in &oracle.risks {
468 if risk.contains("Missing staleness") || risk.contains("Missing L2") {
469 risks.push(DefiRiskFactor {
470 name: format!("{} oracle risk", oracle.provider),
471 description: risk.clone(),
472 severity: 7,
473 });
474 }
475 }
476 }
477
478 if !flash_loan_info.is_empty() {
480 risks.push(DefiRiskFactor {
481 name: "Flash loan exposure".to_string(),
482 description: "Contract interacts with flash loans. Ensure all state \
483 changes are validated after flash loan execution."
484 .to_string(),
485 severity: 6,
486 });
487 }
488
489 for dex in dex_integrations {
491 if !dex.has_slippage_protection {
492 risks.push(DefiRiskFactor {
493 name: format!("{} missing slippage protection", dex.dex),
494 description: "DEX swap without minimum output amount. Transaction \
495 can be sandwiched by MEV bots."
496 .to_string(),
497 severity: 8,
498 });
499 }
500 if !dex.has_deadline_protection {
501 risks.push(DefiRiskFactor {
502 name: format!("{} missing deadline", dex.dex),
503 description: "DEX swap without deadline parameter. Transaction can be \
504 held in mempool indefinitely and executed at a bad price."
505 .to_string(),
506 severity: 5,
507 });
508 }
509 }
510
511 if !lending_patterns.is_empty() && !lending_patterns.iter().any(|p| p.contains("liquidation")) {
513 risks.push(DefiRiskFactor {
514 name: "Lending without liquidation mechanism".to_string(),
515 description: "Lending pattern detected without explicit liquidation handling. \
516 Bad debt may accumulate without a liquidation pathway."
517 .to_string(),
518 severity: 7,
519 });
520 }
521
522 risks
523}
524
525#[cfg(test)]
526mod tests {
527 use super::*;
528 use crate::contract::source::ContractSource;
529
530 fn make_source(code: &str) -> ContractSource {
531 ContractSource {
532 contract_name: "Test".to_string(),
533 source_code: code.to_string(),
534 abi: "[]".to_string(),
535 compiler_version: "v0.8.19".to_string(),
536 optimization_used: true,
537 optimization_runs: 200,
538 evm_version: "paris".to_string(),
539 license_type: "MIT".to_string(),
540 is_proxy: false,
541 implementation_address: None,
542 constructor_arguments: String::new(),
543 library: String::new(),
544 swarm_source: String::new(),
545 parsed_abi: vec![],
546 }
547 }
548
549 #[test]
550 fn test_detect_chainlink_oracle() {
551 let src = make_source(
552 "AggregatorV3Interface priceFeed; (,int price,,,) = priceFeed.latestRoundData();",
553 );
554 let analysis = analyze_defi_patterns(&src);
555 assert!(analysis.has_oracle_dependency);
556 assert!(
557 analysis
558 .oracle_info
559 .iter()
560 .any(|o| o.provider == "Chainlink")
561 );
562 }
563
564 #[test]
565 fn test_detect_uniswap_integration() {
566 let src = make_source(
567 "IUniswapV2Router router; router.swapExactTokensForTokens(amountIn, amountOutMin, path, to, deadline);",
568 );
569 let analysis = analyze_defi_patterns(&src);
570 assert!(!analysis.dex_integrations.is_empty());
571 assert!(analysis.dex_integrations[0].has_slippage_protection);
572 assert!(analysis.dex_integrations[0].has_deadline_protection);
573 }
574
575 #[test]
576 fn test_detect_flash_loan() {
577 let src = make_source(
578 "function onFlashLoan(address, address, uint256, uint256, bytes calldata) external returns (bytes32) {}",
579 );
580 let analysis = analyze_defi_patterns(&src);
581 assert!(analysis.has_flash_loan_risk);
582 }
583
584 #[test]
585 fn test_detect_lending() {
586 let src = make_source(
587 "function borrow(uint amount) {} function repay(uint amount) {} function liquidate() {}",
588 );
589 let analysis = analyze_defi_patterns(&src);
590 assert!(!analysis.lending_patterns.is_empty());
591 assert!(matches!(analysis.protocol_type, ProtocolType::Lending));
592 }
593
594 #[test]
595 fn test_detect_erc20_from_abi() {
596 let src = ContractSource {
597 contract_name: "Token".to_string(),
598 source_code: String::new(),
599 abi: "[]".to_string(),
600 compiler_version: "v0.8.19".to_string(),
601 optimization_used: true,
602 optimization_runs: 200,
603 evm_version: "paris".to_string(),
604 license_type: "MIT".to_string(),
605 is_proxy: false,
606 implementation_address: None,
607 constructor_arguments: String::new(),
608 library: String::new(),
609 swarm_source: String::new(),
610 parsed_abi: vec![crate::contract::source::AbiEntry {
611 entry_type: "function".to_string(),
612 name: "transfer".to_string(),
613 inputs: vec![],
614 outputs: vec![],
615 state_mutability: "nonpayable".to_string(),
616 }],
617 };
618 let analysis = analyze_defi_patterns(&src);
619 assert!(
620 analysis
621 .token_standards
622 .iter()
623 .any(|s| matches!(s, TokenStandard::ERC20))
624 );
625 }
626
627 #[test]
628 fn test_missing_slippage_risk() {
629 let src = make_source(
630 "IUniswapV2Router router; router.swapExactTokensForTokens(amountIn, 0, path, to, deadline);",
631 );
632 let analysis = analyze_defi_patterns(&src);
633 assert!(!analysis.dex_integrations.is_empty());
634 }
635
636 #[test]
637 fn test_uniswap_twap_oracle() {
638 let src = make_source("OracleLibrary.observe(pool, secondsAgos);");
639 let analysis = analyze_defi_patterns(&src);
640 assert!(
641 analysis
642 .oracle_info
643 .iter()
644 .any(|o| o.provider == "Uniswap V3 TWAP")
645 );
646 }
647
648 #[test]
649 fn test_band_protocol_oracle() {
650 let src = make_source("IStdReference ref; ref.getReferenceData('ETH', 'USD');");
651 let analysis = analyze_defi_patterns(&src);
652 assert!(
653 analysis
654 .oracle_info
655 .iter()
656 .any(|o| o.provider == "Band Protocol")
657 );
658 }
659
660 #[test]
661 fn test_custom_oracle() {
662 let src = make_source("function getPrice() external view returns (uint);");
663 let analysis = analyze_defi_patterns(&src);
664 assert!(
665 analysis
666 .oracle_info
667 .iter()
668 .any(|o| o.provider == "Custom/Unknown")
669 );
670 }
671
672 #[test]
673 fn test_chainlink_missing_staleness() {
674 let src = make_source("AggregatorV3Interface priceFeed; priceFeed.latestRoundData();");
675 let analysis = analyze_defi_patterns(&src);
676 let cl = analysis
677 .oracle_info
678 .iter()
679 .find(|o| o.provider == "Chainlink")
680 .unwrap();
681 assert!(cl.risks.iter().any(|r| r.contains("Missing staleness")));
682 }
683
684 #[test]
685 fn test_flash_loan_dydx() {
686 let src = make_source("function callFunctionWithValue() external {}");
687 let analysis = analyze_defi_patterns(&src);
688 assert!(analysis.flash_loan_info.iter().any(|s| s.contains("dYdX")));
689 }
690
691 #[test]
692 fn test_flash_loan_uniswap_swap() {
693 let src = make_source("function uniswapV2Call(address, uint, uint, bytes) external {}");
694 let analysis = analyze_defi_patterns(&src);
695 assert!(
696 analysis
697 .flash_loan_info
698 .iter()
699 .any(|s| s.contains("flash swap"))
700 );
701 }
702
703 #[test]
704 fn test_flash_loan_no_balanceof_warning() {
705 let src = make_source("function flashLoan() external { doStuff(); }");
706 let analysis = analyze_defi_patterns(&src);
707 assert!(
708 analysis
709 .flash_loan_info
710 .iter()
711 .any(|s| s.contains("WARNING"))
712 );
713 }
714
715 #[test]
716 fn test_sushiswap_integration() {
717 let src = make_source("ISushiSwap router; router.swap(amountOutMin, deadline);");
718 let analysis = analyze_defi_patterns(&src);
719 assert!(
720 analysis
721 .dex_integrations
722 .iter()
723 .any(|d| d.dex == "SushiSwap")
724 );
725 }
726
727 #[test]
728 fn test_curve_integration() {
729 let src = make_source("ICurve pool; pool.exchange(0, 1, amount, min_amount);");
730 let analysis = analyze_defi_patterns(&src);
731 assert!(analysis.dex_integrations.iter().any(|d| d.dex == "Curve"));
732 }
733
734 #[test]
735 fn test_balancer_integration() {
736 let src = make_source("IBalancer vault; vault.swap(singleSwap, limit, deadline);");
737 let analysis = analyze_defi_patterns(&src);
738 assert!(
739 analysis
740 .dex_integrations
741 .iter()
742 .any(|d| d.dex == "Balancer")
743 );
744 }
745
746 #[test]
747 fn test_lending_pool_pattern() {
748 let src = make_source("LendingPool pool; pool.deposit(); Comptroller comp;");
749 let analysis = analyze_defi_patterns(&src);
750 assert!(
751 analysis
752 .lending_patterns
753 .iter()
754 .any(|p| p.contains("Aave/Compound"))
755 );
756 }
757
758 #[test]
759 fn test_health_factor_pattern() {
760 let src = make_source("function borrow() {} function repay() {} uint healthFactor;");
761 let analysis = analyze_defi_patterns(&src);
762 assert!(
763 analysis
764 .lending_patterns
765 .iter()
766 .any(|p| p.contains("Health factor"))
767 );
768 }
769
770 #[test]
771 fn test_interest_rate_pattern() {
772 let src = make_source("function borrow() {} function repay() {} uint interest; uint rate;");
773 let analysis = analyze_defi_patterns(&src);
774 assert!(
775 analysis
776 .lending_patterns
777 .iter()
778 .any(|p| p.contains("Interest rate"))
779 );
780 }
781
782 #[test]
783 fn test_staking_reward_pattern() {
784 let src = make_source(
785 "function stake() {} function unstake() {} function rewardPerToken() view {}",
786 );
787 let analysis = analyze_defi_patterns(&src);
788 assert!(
789 analysis
790 .staking_patterns
791 .iter()
792 .any(|p| p.contains("Synthetix"))
793 );
794 }
795
796 #[test]
797 fn test_vesting_pattern() {
798 let src = make_source("VestingSchedule schedule; function vesting() {}");
799 let analysis = analyze_defi_patterns(&src);
800 assert!(
801 analysis
802 .staking_patterns
803 .iter()
804 .any(|p| p.contains("vesting"))
805 );
806 }
807
808 #[test]
809 fn test_timelock_staking() {
810 let src = make_source("function stake() {} function unstake() {} uint lockPeriod;");
811 let analysis = analyze_defi_patterns(&src);
812 assert!(
813 analysis
814 .staking_patterns
815 .iter()
816 .any(|p| p.contains("Time-locked"))
817 );
818 }
819
820 #[test]
821 fn test_erc721_detection() {
822 let src = make_source("import ERC721; contract NFT is ERC721 {}");
823 let analysis = analyze_defi_patterns(&src);
824 assert!(
825 analysis
826 .token_standards
827 .iter()
828 .any(|s| matches!(s, TokenStandard::ERC721))
829 );
830 }
831
832 #[test]
833 fn test_erc1155_detection() {
834 let src = make_source("import ERC1155; contract Multi is ERC1155 {}");
835 let analysis = analyze_defi_patterns(&src);
836 assert!(
837 analysis
838 .token_standards
839 .iter()
840 .any(|s| matches!(s, TokenStandard::ERC1155))
841 );
842 }
843
844 #[test]
845 fn test_erc4626_detection() {
846 let src = make_source("import ERC4626; contract Vault is ERC4626 {}");
847 let analysis = analyze_defi_patterns(&src);
848 assert!(
849 analysis
850 .token_standards
851 .iter()
852 .any(|s| matches!(s, TokenStandard::ERC4626))
853 );
854 }
855
856 #[test]
857 fn test_classify_governance() {
858 let src = make_source("contract Gov { function propose() {} function vote() {} }");
859 let analysis = analyze_defi_patterns(&src);
860 assert!(matches!(analysis.protocol_type, ProtocolType::Governance));
861 }
862
863 #[test]
864 fn test_classify_bridge() {
865 let src = make_source("contract Bridge { function bridge() {} function crossChain() {} }");
866 let analysis = analyze_defi_patterns(&src);
867 assert!(matches!(analysis.protocol_type, ProtocolType::Bridge));
868 }
869
870 #[test]
871 fn test_classify_yield() {
872 let src = make_source("contract Farm { function stake() {} function farm() {} }");
873 let analysis = analyze_defi_patterns(&src);
874 assert!(matches!(analysis.protocol_type, ProtocolType::Yield));
875 }
876
877 #[test]
878 fn test_classify_other() {
879 let src = make_source("contract Utils { function doSomething() {} }");
880 let analysis = analyze_defi_patterns(&src);
881 assert!(matches!(analysis.protocol_type, ProtocolType::Other));
882 }
883
884 #[test]
885 fn test_lending_without_liquidation_risk() {
886 let src = make_source("function borrow(uint a) {} function repay(uint a) {}");
887 let analysis = analyze_defi_patterns(&src);
888 assert!(
889 analysis
890 .risk_factors
891 .iter()
892 .any(|r| r.name.contains("Lending without liquidation"))
893 );
894 }
895
896 #[test]
897 fn test_dex_missing_slippage_risk_factor() {
898 let src = make_source(
899 "IUniswapV2Router router; router.swap(amount, 0, path, to, block.timestamp);",
900 );
901 let analysis = analyze_defi_patterns(&src);
902 for dex in &analysis.dex_integrations {
903 if !dex.has_slippage_protection {
904 assert!(
905 analysis
906 .risk_factors
907 .iter()
908 .any(|r| r.name.contains("slippage"))
909 );
910 }
911 }
912 }
913
914 #[test]
915 fn test_protocol_type_display() {
916 assert_eq!(format!("{}", ProtocolType::Token), "Token");
917 assert_eq!(format!("{}", ProtocolType::DEX), "DEX/AMM");
918 assert_eq!(format!("{}", ProtocolType::Lending), "Lending");
919 assert_eq!(format!("{}", ProtocolType::Yield), "Yield/Staking");
920 assert_eq!(format!("{}", ProtocolType::Governance), "Governance");
921 assert_eq!(format!("{}", ProtocolType::Bridge), "Bridge");
922 assert_eq!(
923 format!("{}", ProtocolType::NFTMarketplace),
924 "NFT Marketplace"
925 );
926 assert_eq!(format!("{}", ProtocolType::Other), "Other");
927 }
928
929 #[test]
930 fn test_token_standard_display() {
931 assert_eq!(format!("{}", TokenStandard::ERC20), "ERC-20");
932 assert_eq!(format!("{}", TokenStandard::ERC721), "ERC-721");
933 assert_eq!(format!("{}", TokenStandard::ERC1155), "ERC-1155");
934 assert_eq!(format!("{}", TokenStandard::ERC4626), "ERC-4626");
935 assert_eq!(
936 format!("{}", TokenStandard::Custom("Wrapped".to_string())),
937 "Wrapped"
938 );
939 }
940}