Skip to main content

scope/contract/
mod.rs

1//! # Smart Contract Analysis Module
2//!
3//! Provides comprehensive contract analysis capabilities for EVM-compatible
4//! blockchain contracts, including:
5//!
6//! - **Source code retrieval** from block explorers (Etherscan, Sourcify)
7//! - **ABI parsing** and function signature decoding (4byte.directory)
8//! - **Proxy pattern detection** (EIP-1967, EIP-1822, UUPS, Transparent)
9//! - **Access control mapping** (Ownable, AccessControl, Roles)
10//! - **Vulnerability heuristics** (reentrancy, unchecked calls, selfdestruct, etc.)
11//! - **DeFi protocol checks** (oracle manipulation, flash loans, lending, swaps)
12//! - **External intelligence** (GitHub linking, audit report discovery)
13//!
14//! ## Architecture
15//!
16//! The module is organized into submodules that build on each other:
17//!
18//! ```text
19//! contract::source       → Raw data retrieval (source, ABI, bytecode metadata)
20//! contract::abi          → Function signature lookup and calldata decoding
21//! contract::proxy        → Proxy pattern identification and implementation resolution
22//! contract::access       → Access control and privilege mapping
23//! contract::vulnerability → Security heuristic scanning
24//! contract::defi         → DeFi-specific protocol analysis
25//! contract::external     → GitHub repo linking and audit report discovery
26//! ```
27
28pub mod abi;
29pub mod access;
30pub mod defi;
31pub mod external;
32pub mod proxy;
33pub mod source;
34pub mod vulnerability;
35
36use serde::{Deserialize, Serialize};
37
38/// Complete contract analysis result aggregating all submodule outputs.
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct ContractAnalysis {
41    /// Contract address analyzed.
42    pub address: String,
43    /// Chain the contract is deployed on.
44    pub chain: String,
45    /// Whether the contract source is verified on the block explorer.
46    pub is_verified: bool,
47    /// Contract source information (if verified).
48    pub source_info: Option<source::ContractSource>,
49    /// Proxy detection results.
50    pub proxy_info: Option<proxy::ProxyInfo>,
51    /// Access control mapping.
52    pub access_control: Option<access::AccessControlMap>,
53    /// Vulnerability scan findings.
54    pub vulnerabilities: Vec<vulnerability::VulnerabilityFinding>,
55    /// DeFi protocol analysis.
56    pub defi_analysis: Option<defi::DefiAnalysis>,
57    /// External intelligence (GitHub, audits).
58    pub external_info: Option<external::ExternalInfo>,
59    /// Overall security score (0-100, higher = safer).
60    pub security_score: u32,
61    /// Human-readable security summary.
62    pub security_summary: String,
63}
64
65/// Run a full contract analysis pipeline on an address.
66///
67/// Orchestrates all submodules: source retrieval, proxy detection,
68/// access control, vulnerability scanning, DeFi checks, and external intel.
69pub async fn analyze_contract(
70    address: &str,
71    chain: &str,
72    client: &dyn crate::chains::ChainClient,
73    http_client: &reqwest::Client,
74) -> crate::error::Result<ContractAnalysis> {
75    // Step 1: Verify it's actually a contract
76    let code = client.get_code(address).await?;
77    if code.is_empty() || code == "0x" {
78        return Err(crate::error::ScopeError::Chain(format!(
79            "{} is an EOA (externally owned account), not a contract",
80            address
81        )));
82    }
83
84    // Step 2: Fetch source code and ABI
85    let source_result = source::fetch_contract_source(address, chain, http_client).await;
86    let source_info = source_result.ok();
87    let is_verified = source_info.is_some();
88
89    // Step 3: Proxy detection (uses bytecode + source metadata)
90    let proxy_info = proxy::detect_proxy(
91        address,
92        chain,
93        &code,
94        source_info.as_ref(),
95        client,
96        http_client,
97    )
98    .await
99    .ok();
100
101    // Step 4: Access control analysis (requires source)
102    let access_control = source_info.as_ref().map(access::analyze_access_control);
103
104    // Step 5: Vulnerability scan (requires source + ABI context)
105    let vulnerabilities = if let Some(src) = source_info.as_ref() {
106        vulnerability::scan_vulnerabilities(src)
107    } else {
108        vulnerability::scan_bytecode_only(&code)
109    };
110
111    // Step 6: DeFi protocol checks
112    let defi_analysis = source_info.as_ref().map(defi::analyze_defi_patterns);
113
114    // Step 7: External intelligence
115    let external_info =
116        external::gather_external_info(address, chain, source_info.as_ref(), http_client)
117            .await
118            .ok();
119
120    // Compute security score
121    let security_score = compute_security_score(
122        is_verified,
123        &proxy_info,
124        &access_control,
125        &vulnerabilities,
126        &defi_analysis,
127        &external_info,
128    );
129
130    let security_summary = generate_security_summary(
131        is_verified,
132        &proxy_info,
133        &access_control,
134        &vulnerabilities,
135        security_score,
136    );
137
138    Ok(ContractAnalysis {
139        address: address.to_string(),
140        chain: chain.to_string(),
141        is_verified,
142        source_info,
143        proxy_info,
144        access_control,
145        vulnerabilities,
146        defi_analysis,
147        external_info,
148        security_score,
149        security_summary,
150    })
151}
152
153/// Compute an overall security score (0-100) from analysis components.
154fn compute_security_score(
155    is_verified: bool,
156    proxy_info: &Option<proxy::ProxyInfo>,
157    access_control: &Option<access::AccessControlMap>,
158    vulnerabilities: &[vulnerability::VulnerabilityFinding],
159    defi_analysis: &Option<defi::DefiAnalysis>,
160    external_info: &Option<external::ExternalInfo>,
161) -> u32 {
162    let mut score: i32 = 50; // Base score
163
164    // Verification bonus
165    if is_verified {
166        score += 15;
167    } else {
168        score -= 20;
169    }
170
171    // Proxy considerations
172    if let Some(proxy) = proxy_info
173        && proxy.is_proxy
174    {
175        score -= 5; // Proxies add complexity
176        if proxy.admin_address.is_some() {
177            score += 3; // At least admin is identifiable
178        }
179    }
180
181    // Access control
182    if let Some(ac) = access_control {
183        if ac.has_renounced_ownership {
184            score += 10;
185        }
186        if ac.has_role_based_access {
187            score += 5;
188        }
189        if ac.uses_tx_origin {
190            score -= 15;
191        }
192    }
193
194    // Vulnerabilities
195    for vuln in vulnerabilities {
196        match vuln.severity {
197            vulnerability::Severity::Critical => score -= 20,
198            vulnerability::Severity::High => score -= 12,
199            vulnerability::Severity::Medium => score -= 6,
200            vulnerability::Severity::Low => score -= 2,
201            vulnerability::Severity::Informational => score -= 1,
202        }
203    }
204
205    // DeFi risk factors
206    if let Some(defi) = defi_analysis {
207        if defi.has_oracle_dependency {
208            score -= 5;
209        }
210        if defi.has_flash_loan_risk {
211            score -= 8;
212        }
213    }
214
215    // External validation
216    if let Some(ext) = external_info {
217        if !ext.audit_reports.is_empty() {
218            score += 15;
219        }
220        if ext.github_repo.is_some() {
221            score += 5;
222        }
223    }
224
225    score.clamp(0, 100) as u32
226}
227
228/// Generate a human-readable security summary.
229fn generate_security_summary(
230    is_verified: bool,
231    proxy_info: &Option<proxy::ProxyInfo>,
232    access_control: &Option<access::AccessControlMap>,
233    vulnerabilities: &[vulnerability::VulnerabilityFinding],
234    score: u32,
235) -> String {
236    let mut parts = Vec::new();
237
238    // Verification status
239    if is_verified {
240        parts.push("Source code is verified on the block explorer.".to_string());
241    } else {
242        parts.push(
243            "WARNING: Source code is NOT verified — unable to perform source-level analysis."
244                .to_string(),
245        );
246    }
247
248    // Proxy status
249    if let Some(proxy) = proxy_info
250        && proxy.is_proxy
251    {
252        parts.push(format!(
253            "Contract is a {} proxy{}.",
254            proxy.proxy_type,
255            proxy
256                .implementation_address
257                .as_ref()
258                .map(|a| format!(" pointing to {}", a))
259                .unwrap_or_default()
260        ));
261    }
262
263    // Access control
264    if let Some(ac) = access_control {
265        if ac.has_renounced_ownership {
266            parts.push("Ownership has been renounced.".to_string());
267        } else if !ac.privileged_functions.is_empty() {
268            parts.push(format!(
269                "{} privileged function(s) found with owner/admin restrictions.",
270                ac.privileged_functions.len()
271            ));
272        }
273        if ac.uses_tx_origin {
274            parts.push("DANGER: Uses tx.origin for authorization.".to_string());
275        }
276    }
277
278    // Vulnerability summary
279    let critical = vulnerabilities
280        .iter()
281        .filter(|v| v.severity == vulnerability::Severity::Critical)
282        .count();
283    let high = vulnerabilities
284        .iter()
285        .filter(|v| v.severity == vulnerability::Severity::High)
286        .count();
287    if critical > 0 || high > 0 {
288        parts.push(format!(
289            "Found {} critical and {} high severity issue(s).",
290            critical, high
291        ));
292    } else if vulnerabilities.is_empty() {
293        parts.push("No vulnerability heuristics triggered.".to_string());
294    } else {
295        parts.push(format!(
296            "{} lower-severity finding(s) detected.",
297            vulnerabilities.len()
298        ));
299    }
300
301    // Score assessment
302    let rating = match score {
303        80..=100 => "GOOD",
304        60..=79 => "MODERATE",
305        40..=59 => "CAUTION",
306        20..=39 => "HIGH RISK",
307        _ => "CRITICAL RISK",
308    };
309    parts.push(format!("Security Score: {}/100 ({})", score, rating));
310
311    parts.join(" ")
312}
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317
318    #[test]
319    fn test_compute_security_score_verified() {
320        let score = compute_security_score(true, &None, &None, &[], &None, &None);
321        assert_eq!(score, 65); // 50 base + 15 verified
322    }
323
324    #[test]
325    fn test_compute_security_score_unverified() {
326        let score = compute_security_score(false, &None, &None, &[], &None, &None);
327        assert_eq!(score, 30); // 50 base - 20 unverified
328    }
329
330    #[test]
331    fn test_compute_security_score_with_critical_vuln() {
332        let vulns = vec![vulnerability::VulnerabilityFinding {
333            id: "TEST-001".to_string(),
334            title: "Test".to_string(),
335            severity: vulnerability::Severity::Critical,
336            category: vulnerability::VulnCategory::Reentrancy,
337            description: "Test finding".to_string(),
338            source_location: None,
339            recommendation: "Fix it".to_string(),
340        }];
341        let score = compute_security_score(true, &None, &None, &vulns, &None, &None);
342        assert_eq!(score, 45); // 65 - 20 critical
343    }
344
345    #[test]
346    fn test_security_summary_unverified() {
347        let summary = generate_security_summary(false, &None, &None, &[], 30);
348        assert!(summary.contains("NOT verified"));
349        assert!(summary.contains("30/100"));
350    }
351
352    #[test]
353    fn test_score_clamped_to_valid_range() {
354        let many_vulns: Vec<_> = (0..10)
355            .map(|i| vulnerability::VulnerabilityFinding {
356                id: format!("V-{}", i),
357                title: "Critical".to_string(),
358                severity: vulnerability::Severity::Critical,
359                category: vulnerability::VulnCategory::Reentrancy,
360                description: "Bad".to_string(),
361                source_location: None,
362                recommendation: "Fix".to_string(),
363            })
364            .collect();
365        let score = compute_security_score(false, &None, &None, &many_vulns, &None, &None);
366        assert_eq!(score, 0); // Clamped to 0
367    }
368
369    #[test]
370    fn test_compute_security_score_proxy_with_admin() {
371        use proxy::ProxyInfo;
372
373        let proxy = Some(ProxyInfo {
374            is_proxy: true,
375            admin_address: Some("0xadmin".to_string()),
376            proxy_type: "EIP-1967".to_string(),
377            implementation_address: None,
378            beacon_address: None,
379            details: vec![],
380        });
381        let score = compute_security_score(true, &proxy, &None, &[], &None, &None);
382        // 50 base + 15 verified - 5 proxy + 3 admin = 63
383        assert_eq!(score, 63);
384    }
385
386    #[test]
387    fn test_compute_security_score_access_control_renounced_and_role_based() {
388        use access::{AccessControlMap, AuthAnalysis};
389
390        let ac = Some(AccessControlMap {
391            ownership_pattern: None,
392            has_renounced_ownership: true,
393            has_role_based_access: true,
394            uses_tx_origin: false,
395            tx_origin_locations: vec![],
396            modifiers: vec![],
397            privileged_functions: vec![],
398            roles: vec![],
399            auth_analysis: AuthAnalysis {
400                msg_sender_checks: 0,
401                tx_origin_checks: 0,
402                has_origin_sender_comparison: false,
403                summary: String::new(),
404            },
405        });
406        let score = compute_security_score(true, &None, &ac, &[], &None, &None);
407        // 50 base + 15 verified + 10 renounced + 5 role-based = 80
408        assert_eq!(score, 80);
409    }
410
411    #[test]
412    fn test_compute_security_score_tx_origin() {
413        use access::{AccessControlMap, AuthAnalysis};
414
415        let ac = Some(AccessControlMap {
416            ownership_pattern: None,
417            has_renounced_ownership: false,
418            has_role_based_access: false,
419            uses_tx_origin: true,
420            tx_origin_locations: vec![],
421            modifiers: vec![],
422            privileged_functions: vec![],
423            roles: vec![],
424            auth_analysis: AuthAnalysis {
425                msg_sender_checks: 0,
426                tx_origin_checks: 1,
427                has_origin_sender_comparison: false,
428                summary: String::new(),
429            },
430        });
431        let score = compute_security_score(true, &None, &ac, &[], &None, &None);
432        // 50 base + 15 verified - 15 tx.origin = 50
433        assert_eq!(score, 50);
434    }
435
436    #[test]
437    fn test_compute_security_score_defi_oracle_and_flash_loan() {
438        use defi::{DefiAnalysis, ProtocolType};
439
440        let defi = Some(DefiAnalysis {
441            protocol_type: ProtocolType::Lending,
442            has_oracle_dependency: true,
443            oracle_info: vec![],
444            has_flash_loan_risk: true,
445            flash_loan_info: vec![],
446            dex_integrations: vec![],
447            lending_patterns: vec![],
448            token_standards: vec![],
449            staking_patterns: vec![],
450            risk_factors: vec![],
451        });
452        let score = compute_security_score(true, &None, &None, &[], &defi, &None);
453        // 50 base + 15 verified - 5 oracle - 8 flash loan = 52
454        assert_eq!(score, 52);
455    }
456
457    #[test]
458    fn test_compute_security_score_external_audit_and_github() {
459        use external::{AuditReport, ExternalInfo};
460
461        let ext = Some(ExternalInfo {
462            github_repo: Some("https://github.com/org/repo".to_string()),
463            audit_reports: vec![AuditReport {
464                auditor: "Trail of Bits".to_string(),
465                url: "https://example.com/report".to_string(),
466                date: Some("2024-01-01".to_string()),
467                scope: "Full".to_string(),
468            }],
469            sourcify_verified: None,
470            deployer: None,
471            explorer_url: String::new(),
472            metadata: vec![],
473        });
474        let score = compute_security_score(true, &None, &None, &[], &None, &ext);
475        // 50 base + 15 verified + 15 audits + 5 github = 85
476        assert_eq!(score, 85);
477    }
478
479    #[test]
480    fn test_compute_security_score_clamped_to_100() {
481        use access::{AccessControlMap, AuthAnalysis};
482        use external::{AuditReport, ExternalInfo};
483
484        let ac = Some(AccessControlMap {
485            ownership_pattern: None,
486            has_renounced_ownership: true,
487            has_role_based_access: true,
488            uses_tx_origin: false,
489            tx_origin_locations: vec![],
490            modifiers: vec![],
491            privileged_functions: vec![],
492            roles: vec![],
493            auth_analysis: AuthAnalysis {
494                msg_sender_checks: 0,
495                tx_origin_checks: 0,
496                has_origin_sender_comparison: false,
497                summary: String::new(),
498            },
499        });
500        let ext = Some(ExternalInfo {
501            github_repo: Some("https://github.com/org/repo".to_string()),
502            audit_reports: vec![AuditReport {
503                auditor: "ToB".to_string(),
504                url: "https://example.com".to_string(),
505                date: None,
506                scope: "Full".to_string(),
507            }],
508            sourcify_verified: None,
509            deployer: None,
510            explorer_url: String::new(),
511            metadata: vec![],
512        });
513        let score = compute_security_score(true, &None, &ac, &[], &None, &ext);
514        // Would be 50+15+10+5+15+5 = 100, clamped to 100
515        assert_eq!(score, 100);
516    }
517
518    #[test]
519    fn test_generate_security_summary_proxy_implementation() {
520        use proxy::ProxyInfo;
521
522        let proxy = Some(ProxyInfo {
523            is_proxy: true,
524            proxy_type: "EIP-1967".to_string(),
525            implementation_address: Some("0x1234567890123456789012345678901234567890".to_string()),
526            admin_address: None,
527            beacon_address: None,
528            details: vec![],
529        });
530        let summary = generate_security_summary(true, &proxy, &None, &[], 70);
531        assert!(summary.contains("pointing to 0x1234567890123456789012345678901234567890"));
532    }
533
534    #[test]
535    fn test_generate_security_summary_verified_no_vulns() {
536        let summary = generate_security_summary(true, &None, &None, &[], 80);
537        assert!(summary.contains("No vulnerability"));
538    }
539
540    #[test]
541    fn test_generate_security_summary_low_severity_vulns() {
542        let vulns = vec![vulnerability::VulnerabilityFinding {
543            id: "L-001".to_string(),
544            title: "Low".to_string(),
545            severity: vulnerability::Severity::Low,
546            category: vulnerability::VulnCategory::LogicError,
547            description: "Minor".to_string(),
548            source_location: None,
549            recommendation: "Consider".to_string(),
550        }];
551        let summary = generate_security_summary(true, &None, &None, &vulns, 75);
552        assert!(summary.contains("lower-severity"));
553    }
554
555    #[test]
556    fn test_generate_security_summary_access_control_privileged_and_tx_origin() {
557        use access::{
558            AccessControlMap, AuthAnalysis, PrivilegeRisk, PrivilegedFunction, SourceLocation,
559        };
560
561        let ac = Some(AccessControlMap {
562            ownership_pattern: Some("Ownable".to_string()),
563            has_renounced_ownership: false,
564            has_role_based_access: false,
565            uses_tx_origin: true,
566            tx_origin_locations: vec![SourceLocation {
567                file: "Contract.sol".to_string(),
568                line: 10,
569                snippet: "require(tx.origin == owner)".to_string(),
570            }],
571            modifiers: vec![],
572            privileged_functions: vec![PrivilegedFunction {
573                name: "withdraw".to_string(),
574                modifiers: vec!["onlyOwner".to_string()],
575                capability: "drain funds".to_string(),
576                risk: PrivilegeRisk::Critical,
577            }],
578            roles: vec![],
579            auth_analysis: AuthAnalysis {
580                msg_sender_checks: 0,
581                tx_origin_checks: 1,
582                has_origin_sender_comparison: false,
583                summary: String::new(),
584            },
585        });
586        let summary = generate_security_summary(true, &None, &ac, &[], 50);
587        assert!(summary.contains("privileged function"));
588        assert!(summary.contains("tx.origin"));
589    }
590
591    #[test]
592    fn test_generate_security_summary_score_ratings() {
593        assert!(generate_security_summary(true, &None, &None, &[], 85).contains("GOOD"));
594        assert!(generate_security_summary(true, &None, &None, &[], 70).contains("MODERATE"));
595        assert!(generate_security_summary(true, &None, &None, &[], 50).contains("CAUTION"));
596        assert!(generate_security_summary(true, &None, &None, &[], 30).contains("HIGH RISK"));
597        assert!(generate_security_summary(true, &None, &None, &[], 15).contains("CRITICAL RISK"));
598    }
599
600    #[test]
601    fn test_contract_analysis_struct_construction() {
602        let analysis = ContractAnalysis {
603            address: "0x123".to_string(),
604            chain: "ethereum".to_string(),
605            is_verified: true,
606            source_info: None,
607            proxy_info: None,
608            access_control: None,
609            vulnerabilities: vec![],
610            defi_analysis: None,
611            external_info: None,
612            security_score: 75,
613            security_summary: "Test summary".to_string(),
614        };
615        assert_eq!(analysis.address, "0x123");
616        assert_eq!(analysis.chain, "ethereum");
617        assert!(analysis.is_verified);
618        assert_eq!(analysis.security_score, 75);
619        assert!(analysis.security_summary.contains("Test"));
620    }
621
622    #[test]
623    fn test_contract_analysis_serialization_roundtrip() {
624        let analysis = ContractAnalysis {
625            address: "0xabc".to_string(),
626            chain: "polygon".to_string(),
627            is_verified: false,
628            source_info: None,
629            proxy_info: None,
630            access_control: None,
631            vulnerabilities: vec![],
632            defi_analysis: None,
633            external_info: None,
634            security_score: 42,
635            security_summary: "Summary".to_string(),
636        };
637        let json = serde_json::to_string(&analysis).unwrap();
638        let restored: ContractAnalysis = serde_json::from_str(&json).unwrap();
639        assert_eq!(restored.address, analysis.address);
640        assert_eq!(restored.security_score, analysis.security_score);
641    }
642
643    #[test]
644    fn test_compute_security_score_proxy_without_admin() {
645        use proxy::ProxyInfo;
646
647        let proxy = Some(ProxyInfo {
648            is_proxy: true,
649            admin_address: None,
650            proxy_type: "EIP-1967".to_string(),
651            implementation_address: Some("0ximpl".to_string()),
652            beacon_address: None,
653            details: vec![],
654        });
655        let score = compute_security_score(true, &proxy, &None, &[], &None, &None);
656        // 50 base + 15 verified - 5 proxy (no admin bonus) = 60
657        assert_eq!(score, 60);
658    }
659
660    #[test]
661    fn test_compute_security_score_proxy_info_not_proxy() {
662        use proxy::ProxyInfo;
663
664        let proxy = Some(ProxyInfo {
665            is_proxy: false,
666            admin_address: None,
667            proxy_type: "None".to_string(),
668            implementation_address: None,
669            beacon_address: None,
670            details: vec![],
671        });
672        let score = compute_security_score(true, &proxy, &None, &[], &None, &None);
673        assert_eq!(score, 65); // No proxy penalty when is_proxy is false
674    }
675
676    #[test]
677    fn test_compute_security_score_severity_high() {
678        let vulns = vec![vulnerability::VulnerabilityFinding {
679            id: "H-001".to_string(),
680            title: "High".to_string(),
681            severity: vulnerability::Severity::High,
682            category: vulnerability::VulnCategory::AccessControl,
683            description: "High finding".to_string(),
684            source_location: None,
685            recommendation: "Fix".to_string(),
686        }];
687        let score = compute_security_score(true, &None, &None, &vulns, &None, &None);
688        assert_eq!(score, 53); // 65 - 12
689    }
690
691    #[test]
692    fn test_compute_security_score_severity_medium() {
693        let vulns = vec![vulnerability::VulnerabilityFinding {
694            id: "M-001".to_string(),
695            title: "Medium".to_string(),
696            severity: vulnerability::Severity::Medium,
697            category: vulnerability::VulnCategory::LogicError,
698            description: "Medium finding".to_string(),
699            source_location: None,
700            recommendation: "Fix".to_string(),
701        }];
702        let score = compute_security_score(true, &None, &None, &vulns, &None, &None);
703        assert_eq!(score, 59); // 65 - 6
704    }
705
706    #[test]
707    fn test_compute_security_score_severity_low() {
708        let vulns = vec![vulnerability::VulnerabilityFinding {
709            id: "L-001".to_string(),
710            title: "Low".to_string(),
711            severity: vulnerability::Severity::Low,
712            category: vulnerability::VulnCategory::UncheckedCall,
713            description: "Low finding".to_string(),
714            source_location: None,
715            recommendation: "Fix".to_string(),
716        }];
717        let score = compute_security_score(true, &None, &None, &vulns, &None, &None);
718        assert_eq!(score, 63); // 65 - 2
719    }
720
721    #[test]
722    fn test_compute_security_score_severity_informational() {
723        let vulns = vec![vulnerability::VulnerabilityFinding {
724            id: "I-001".to_string(),
725            title: "Info".to_string(),
726            severity: vulnerability::Severity::Informational,
727            category: vulnerability::VulnCategory::Informational,
728            description: "Info finding".to_string(),
729            source_location: None,
730            recommendation: "Fix".to_string(),
731        }];
732        let score = compute_security_score(true, &None, &None, &vulns, &None, &None);
733        assert_eq!(score, 64); // 65 - 1
734    }
735
736    #[test]
737    fn test_compute_security_score_defi_oracle_only() {
738        use defi::{DefiAnalysis, ProtocolType};
739
740        let defi = Some(DefiAnalysis {
741            protocol_type: ProtocolType::DEX,
742            has_oracle_dependency: true,
743            oracle_info: vec![],
744            has_flash_loan_risk: false,
745            flash_loan_info: vec![],
746            dex_integrations: vec![],
747            lending_patterns: vec![],
748            token_standards: vec![],
749            staking_patterns: vec![],
750            risk_factors: vec![],
751        });
752        let score = compute_security_score(true, &None, &None, &[], &defi, &None);
753        assert_eq!(score, 60); // 65 - 5
754    }
755
756    #[test]
757    fn test_compute_security_score_defi_flash_loan_only() {
758        use defi::{DefiAnalysis, ProtocolType};
759
760        let defi = Some(DefiAnalysis {
761            protocol_type: ProtocolType::Lending,
762            has_oracle_dependency: false,
763            oracle_info: vec![],
764            has_flash_loan_risk: true,
765            flash_loan_info: vec![],
766            dex_integrations: vec![],
767            lending_patterns: vec![],
768            token_standards: vec![],
769            staking_patterns: vec![],
770            risk_factors: vec![],
771        });
772        let score = compute_security_score(true, &None, &None, &[], &defi, &None);
773        assert_eq!(score, 57); // 65 - 8
774    }
775
776    #[test]
777    fn test_compute_security_score_external_github_only() {
778        use external::ExternalInfo;
779
780        let ext = Some(ExternalInfo {
781            github_repo: Some("https://github.com/org/repo".to_string()),
782            audit_reports: vec![],
783            sourcify_verified: None,
784            deployer: None,
785            explorer_url: String::new(),
786            metadata: vec![],
787        });
788        let score = compute_security_score(true, &None, &None, &[], &None, &ext);
789        assert_eq!(score, 70); // 65 + 5
790    }
791
792    #[test]
793    fn test_compute_security_score_external_audit_only() {
794        use external::{AuditReport, ExternalInfo};
795
796        let ext = Some(ExternalInfo {
797            github_repo: None,
798            audit_reports: vec![AuditReport {
799                auditor: "Auditor".to_string(),
800                url: "https://audit.com".to_string(),
801                date: None,
802                scope: "Full".to_string(),
803            }],
804            sourcify_verified: None,
805            deployer: None,
806            explorer_url: String::new(),
807            metadata: vec![],
808        });
809        let score = compute_security_score(true, &None, &None, &[], &None, &ext);
810        assert_eq!(score, 80); // 65 + 15
811    }
812
813    #[test]
814    fn test_generate_security_summary_proxy_no_implementation() {
815        use proxy::ProxyInfo;
816
817        let proxy = Some(ProxyInfo {
818            is_proxy: true,
819            proxy_type: "EIP-1967".to_string(),
820            implementation_address: None,
821            admin_address: None,
822            beacon_address: None,
823            details: vec![],
824        });
825        let summary = generate_security_summary(true, &proxy, &None, &[], 65);
826        assert!(summary.contains("EIP-1967"));
827        assert!(summary.contains("proxy"));
828        assert!(!summary.contains("pointing to"));
829    }
830
831    #[test]
832    fn test_generate_security_summary_renounced_only() {
833        use access::{AccessControlMap, AuthAnalysis};
834
835        let ac = Some(AccessControlMap {
836            ownership_pattern: None,
837            has_renounced_ownership: true,
838            has_role_based_access: false,
839            uses_tx_origin: false,
840            tx_origin_locations: vec![],
841            modifiers: vec![],
842            privileged_functions: vec![],
843            roles: vec![],
844            auth_analysis: AuthAnalysis {
845                msg_sender_checks: 0,
846                tx_origin_checks: 0,
847                has_origin_sender_comparison: false,
848                summary: String::new(),
849            },
850        });
851        let summary = generate_security_summary(true, &None, &ac, &[], 80);
852        assert!(summary.contains("Ownership has been renounced"));
853    }
854
855    #[test]
856    fn test_generate_security_summary_critical_and_high_vulns() {
857        let vulns = vec![
858            vulnerability::VulnerabilityFinding {
859                id: "C-001".to_string(),
860                title: "Critical".to_string(),
861                severity: vulnerability::Severity::Critical,
862                category: vulnerability::VulnCategory::Reentrancy,
863                description: "Critical".to_string(),
864                source_location: None,
865                recommendation: "Fix".to_string(),
866            },
867            vulnerability::VulnerabilityFinding {
868                id: "H-001".to_string(),
869                title: "High".to_string(),
870                severity: vulnerability::Severity::High,
871                category: vulnerability::VulnCategory::AccessControl,
872                description: "High".to_string(),
873                source_location: None,
874                recommendation: "Fix".to_string(),
875            },
876        ];
877        let summary = generate_security_summary(true, &None, &None, &vulns, 40);
878        assert!(summary.contains("critical"));
879        assert!(summary.contains("high"));
880        assert!(summary.contains("1 critical"));
881        assert!(summary.contains("1 high"));
882    }
883}