Skip to main content

scope/contract/
defi.rs

1//! # DeFi Protocol Analysis
2//!
3//! Detects DeFi-specific patterns and risks in smart contract source code,
4//! including oracle dependencies, flash loan risks, lending protocol patterns,
5//! and DEX integration concerns.
6//!
7//! ## Detected Patterns
8//!
9//! - **Oracle usage** - Chainlink, Uniswap TWAP, Band Protocol
10//! - **Flash loan vectors** - AAVE, dYdX flash loan callbacks
11//! - **Lending patterns** - Borrow/lend, collateral, liquidation
12//! - **DEX integration** - Uniswap, Sushiswap router calls, slippage checks
13//! - **Staking/yield** - Staking, rewards, vesting patterns
14//! - **Token standards** - ERC-20, ERC-721, ERC-1155 detection
15
16use crate::contract::source::ContractSource;
17use serde::{Deserialize, Serialize};
18
19/// Complete DeFi analysis result.
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct DefiAnalysis {
22    /// Protocol type classification.
23    pub protocol_type: ProtocolType,
24    /// Whether the contract depends on price oracles.
25    pub has_oracle_dependency: bool,
26    /// Oracle details.
27    pub oracle_info: Vec<OracleInfo>,
28    /// Whether flash loan attack vectors exist.
29    pub has_flash_loan_risk: bool,
30    /// Flash loan details.
31    pub flash_loan_info: Vec<String>,
32    /// DEX integration patterns found.
33    pub dex_integrations: Vec<DexIntegration>,
34    /// Lending/borrowing patterns.
35    pub lending_patterns: Vec<String>,
36    /// Token standard(s) implemented.
37    pub token_standards: Vec<TokenStandard>,
38    /// Staking/yield patterns.
39    pub staking_patterns: Vec<String>,
40    /// DeFi-specific risk factors.
41    pub risk_factors: Vec<DefiRiskFactor>,
42}
43
44/// Classification of the DeFi protocol type.
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub enum ProtocolType {
47    /// Token (ERC-20, ERC-721, etc.)
48    Token,
49    /// Decentralized exchange / AMM
50    DEX,
51    /// Lending protocol
52    Lending,
53    /// Yield farming / staking
54    Yield,
55    /// Governance
56    Governance,
57    /// Bridge
58    Bridge,
59    /// NFT marketplace
60    NFTMarketplace,
61    /// Generic / unclassified
62    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/// Oracle integration details.
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct OracleInfo {
83    /// Oracle provider.
84    pub provider: String,
85    /// How the oracle is used.
86    pub usage: String,
87    /// Potential risks.
88    pub risks: Vec<String>,
89}
90
91/// DEX integration details.
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct DexIntegration {
94    /// DEX name/protocol.
95    pub dex: String,
96    /// Type of integration.
97    pub integration_type: String,
98    /// Whether slippage protection is present.
99    pub has_slippage_protection: bool,
100    /// Whether deadline protection is present.
101    pub has_deadline_protection: bool,
102}
103
104/// Token standard detected.
105#[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/// A DeFi-specific risk factor.
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct DefiRiskFactor {
129    /// Risk name.
130    pub name: String,
131    /// Description.
132    pub description: String,
133    /// Severity (1-10).
134    pub severity: u8,
135}
136
137/// Analyze DeFi patterns in contract source code.
138pub 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    // Chainlink
177    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        // Check for proper staleness check
185        if !code.contains("updatedAt") && !code.contains("answeredInRound") {
186            risks.push("Missing staleness check on Chainlink price feed".to_string());
187        }
188
189        // Check for sequencer uptime feed (L2s)
190        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    // Uniswap TWAP
202    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    // Band Protocol
218    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    // Generic getPrice / fetchPrice patterns
227    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    // Check if the contract is flash-loan SAFE (validates balance changes)
263    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    // Uniswap V2/V3 Router
276    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    // SushiSwap
291    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    // Curve
301    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    // Balancer
315    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    // ERC-20 detection
364    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    // ERC-721 detection
373    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    // ERC-1155 detection
381    if code_lower.contains("erc1155") || code_lower.contains("ierc1155") {
382        standards.push(TokenStandard::ERC1155);
383    }
384
385    // ERC-4626 vault
386    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    // Oracle risks
466    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    // Flash loan risks
479    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    // DEX integration risks
490    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    // Lending risks
512    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}