Skip to main content

scope_cli/cli/
contract.rs

1//! # Contract Analysis Command
2//!
3//! Performs comprehensive smart contract analysis including source code
4//! retrieval, proxy detection, access control mapping, vulnerability scanning,
5//! DeFi protocol checks, and external intelligence gathering.
6
7use crate::cli::output::Output;
8use crate::outln;
9use clap::Args;
10use scope::chains::ChainClientFactory;
11use scope::config::Config;
12use scope::contract;
13use scope::error::Result;
14
15/// Arguments for the contract analysis command.
16#[derive(Debug, Args)]
17#[command(
18    after_help = "\x1b[1mExamples:\x1b[0m
19  scope contract 0xdAC17F958D2ee523a2206206994597C13D831ec7
20  scope ct @usdt-contract                                 \x1b[2m# address book shortcut\x1b[0m
21  scope ct 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --chain polygon
22  scope contract 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D --json",
23    after_long_help = "\x1b[1mExamples:\x1b[0m
24
25  \x1b[1m$ scope contract 0xdAC17F958D2ee523a2206206994597C13D831ec7\x1b[0m
26
27  ┌─ Contract Analysis: 0xdAC17F958D2ee523a2206206994597C13D831ec7 ─
28  │  Chain             ethereum
29  │  Verified          Yes
30  │
31  │  Security Score    [████████████████────] 80/100
32  │
33  ├── Source Code
34  │  Contract Name    TetherToken
35  │  Compiler         v0.4.18+commit.9cf6e910
36  │  Optimization     No
37  │
38  ├── Proxy Detection
39  │  ✓ Not a proxy contract
40  │
41  ├── Access Control
42  │  Ownership        Ownable
43  │  Renounced        No
44  │    • pause (High): Can pause transfers
45  │    • addBlacklist (High): Can blacklist addresses
46  │
47  ├── Vulnerability Findings
48  │  ℹ SC-TX-ORIGIN — tx.origin authorization (Low)
49  │
50  ├── DeFi Analysis
51  │  Protocol Type    Token
52  │  Token Standards  ERC-20
53  │
54  ├── External Intelligence
55  │  Explorer         https://etherscan.io/address/0xdAC17...
56  │  ✓ Sourcify verified
57  │    • Trail of Bits (TetherToken)
58  └──────────────────────────────────────────────────
59
60  \x1b[1m$ scope ct 0xA0b86991... --json\x1b[0m
61
62  {
63    \"address\": \"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48\",
64    \"chain\": \"ethereum\",
65    \"is_verified\": true,
66    \"security_score\": 85,
67    \"security_summary\": \"Verified contract with ...\",
68    \"source_info\": { ... },
69    \"proxy_info\": { ... },
70    \"vulnerabilities\": [ ... ],
71    ...
72  }"
73)]
74pub struct ContractArgs {
75    /// Contract address to analyze.
76    ///
77    /// Must be a valid address on the target chain. The address must be
78    /// a deployed smart contract (not an externally owned account).
79    /// Use @label to resolve from the address book (e.g., @usdt-contract).
80    #[arg(value_name = "ADDRESS")]
81    pub address: String,
82
83    #[arg(
84        long,
85        short,
86        default_value = scope::chains::DEFAULT_CHAIN,
87        help = format!("Target blockchain network (Etherscan-compatible API): {}", scope::chains::registry::explorer_api_chain_list())
88    )]
89    pub chain: String,
90
91    /// Output raw JSON instead of formatted report.
92    ///
93    /// Useful for piping to `jq` or feeding to other tools.
94    #[arg(long)]
95    pub json: bool,
96}
97
98/// Run the contract analysis command.
99pub async fn run(
100    args: &ContractArgs,
101    _config: &Config,
102    clients: &dyn ChainClientFactory,
103    out: &Output,
104) -> Result<()> {
105    let spinner = crate::cli::progress::Spinner::new("Analyzing contract...", out);
106
107    let client = clients.create_chain_client(&args.chain)?;
108    let http_client = clients.http();
109
110    let analysis = contract::analyze_contract(
111        &args.address,
112        &args.chain,
113        client.as_ref(),
114        http_client.as_ref(),
115    )
116    .await?;
117
118    spinner.finish("Contract analysis complete");
119
120    if args.json {
121        outln!(
122            out,
123            "{}",
124            serde_json::to_string_pretty(&analysis)
125                .unwrap_or_else(|_| "Failed to serialize".to_string())
126        )?;
127    } else {
128        print_contract_report(&analysis, out)?;
129    }
130
131    Ok(())
132}
133
134/// Print a formatted contract analysis report to the terminal.
135///
136/// Uses `display::terminal` helpers for consistent box-drawing, color,
137/// and TTY-awareness matching the rest of the CLI.
138fn print_contract_report(
139    analysis: &contract::ContractAnalysis,
140    out: &Output,
141) -> std::io::Result<()> {
142    use scope::display::terminal as t;
143
144    let title = format!("Contract Analysis: {}", analysis.address);
145    outln!(out, "{}", t::section_header(&title))?;
146    outln!(out, "{}", t::kv_row("Chain", &analysis.chain))?;
147    outln!(
148        out,
149        "{}",
150        t::kv_row("Verified", if analysis.is_verified { "Yes" } else { "No" })
151    )?;
152    outln!(out, "{}", t::blank_row())?;
153    outln!(
154        out,
155        "{}",
156        t::score_bar("Security Score", analysis.security_score, 100)
157    )?;
158    outln!(out, "{}", t::detail_row(&analysis.security_summary))?;
159
160    if !analysis.is_verified {
161        outln!(out, "{}", t::blank_row())?;
162        outln!(
163            out,
164            "{}",
165            t::warning_row("Source code is NOT verified — analysis is limited")
166        )?;
167    }
168
169    // Source Info
170    if let Some(src) = &analysis.source_info {
171        outln!(out, "{}", t::subsection_header("Source Code"))?;
172        outln!(out, "{}", t::kv_row("Contract Name", &src.contract_name))?;
173        outln!(out, "{}", t::kv_row("Compiler", &src.compiler_version))?;
174        outln!(out, "{}", t::kv_row("EVM Version", &src.evm_version))?;
175        outln!(out, "{}", t::kv_row("License", &src.license_type))?;
176        outln!(
177            out,
178            "{}",
179            t::kv_row(
180                "Optimization",
181                &if src.optimization_used {
182                    format!("Yes ({} runs)", src.optimization_runs)
183                } else {
184                    "No".to_string()
185                }
186            )
187        )?;
188        outln!(
189            out,
190            "{}",
191            t::kv_row("ABI Functions", &src.parsed_abi.len().to_string())
192        )?;
193    }
194
195    // Proxy Info
196    if let Some(proxy) = &analysis.proxy_info {
197        outln!(out, "{}", t::subsection_header("Proxy Detection"))?;
198        if proxy.is_proxy {
199            outln!(out, "{}", t::kv_row("Type", &proxy.proxy_type))?;
200            if let Some(impl_addr) = &proxy.implementation_address {
201                outln!(out, "{}", t::kv_row("Implementation", impl_addr))?;
202            }
203            if let Some(admin) = &proxy.admin_address {
204                outln!(out, "{}", t::kv_row("Admin", admin))?;
205            }
206        } else {
207            outln!(out, "{}", t::check_pass("Not a proxy contract"))?;
208        }
209        for detail in &proxy.details {
210            outln!(out, "{}", t::bullet_row(detail))?;
211        }
212    }
213
214    // Access Control
215    if let Some(ac) = &analysis.access_control {
216        outln!(out, "{}", t::subsection_header("Access Control"))?;
217        if let Some(pattern) = &ac.ownership_pattern {
218            outln!(out, "{}", t::kv_row("Ownership", pattern))?;
219        }
220        outln!(
221            out,
222            "{}",
223            t::kv_row(
224                "Renounced",
225                if ac.has_renounced_ownership {
226                    "Yes"
227                } else {
228                    "No"
229                }
230            )
231        )?;
232        outln!(
233            out,
234            "{}",
235            t::kv_row(
236                "Role-based",
237                if ac.has_role_based_access {
238                    "Yes"
239                } else {
240                    "No"
241                }
242            )
243        )?;
244        if ac.uses_tx_origin {
245            outln!(
246                out,
247                "{}",
248                t::warning_row("Uses tx.origin for authorization")
249            )?;
250        }
251        if !ac.roles.is_empty() {
252            outln!(out, "{}", t::kv_row("Roles", &ac.roles.join(", ")))?;
253        }
254        if !ac.privileged_functions.is_empty() {
255            outln!(out, "{}", t::blank_row())?;
256            for pf in &ac.privileged_functions {
257                let sev = t::severity_label(&format!("{:?}", pf.risk));
258                outln!(
259                    out,
260                    "{}",
261                    t::bullet_row(&format!("{} ({}): {}", pf.name, sev, pf.capability))
262                )?;
263            }
264        }
265        outln!(out, "{}", t::blank_row())?;
266        outln!(out, "{}", t::kv_row("Auth", &ac.auth_analysis.summary))?;
267    }
268
269    // Vulnerabilities
270    outln!(out, "{}", t::subsection_header("Vulnerability Findings"))?;
271    if !analysis.vulnerabilities.is_empty() {
272        for vuln in &analysis.vulnerabilities {
273            let sev_str = format!("{}", vuln.severity);
274            let sev = t::severity_label(&sev_str);
275            match vuln.severity {
276                contract::vulnerability::Severity::Critical
277                | contract::vulnerability::Severity::High => {
278                    outln!(
279                        out,
280                        "{}",
281                        t::check_fail(&format!("{} — {} ({})", vuln.id, vuln.title, sev))
282                    )?;
283                }
284                _ => {
285                    outln!(
286                        out,
287                        "{}",
288                        t::info_row(&format!("{} — {} ({})", vuln.id, vuln.title, sev))
289                    )?;
290                }
291            }
292            outln!(out, "{}", t::detail_row(&vuln.description))?;
293            outln!(
294                out,
295                "{}",
296                t::detail_row(&format!("Fix: {}", vuln.recommendation))
297            )?;
298        }
299    } else {
300        outln!(out, "{}", t::check_pass("No heuristic findings triggered"))?;
301    }
302
303    // DeFi Analysis
304    if let Some(defi) = &analysis.defi_analysis {
305        outln!(out, "{}", t::subsection_header("DeFi Analysis"))?;
306        outln!(
307            out,
308            "{}",
309            t::kv_row("Protocol Type", &defi.protocol_type.to_string())
310        )?;
311        if !defi.token_standards.is_empty() {
312            let standards: Vec<String> =
313                defi.token_standards.iter().map(|s| s.to_string()).collect();
314            outln!(
315                out,
316                "{}",
317                t::kv_row("Token Standards", &standards.join(", "))
318            )?;
319        }
320        if defi.has_oracle_dependency {
321            for oracle in &defi.oracle_info {
322                outln!(
323                    out,
324                    "{}",
325                    t::kv_row("Oracle", &format!("{} ({})", oracle.provider, oracle.usage))
326                )?;
327            }
328        }
329        if defi.has_flash_loan_risk {
330            outln!(out, "{}", t::warning_row("Flash loan risk detected"))?;
331        }
332        for dex in &defi.dex_integrations {
333            let slippage = if dex.has_slippage_protection {
334                "✓"
335            } else {
336                "✗"
337            };
338            let deadline = if dex.has_deadline_protection {
339                "✓"
340            } else {
341                "✗"
342            };
343            outln!(
344                out,
345                "{}",
346                t::bullet_row(&format!(
347                    "{} — slippage: {} deadline: {}",
348                    dex.dex, slippage, deadline
349                ))
350            )?;
351        }
352        if !defi.risk_factors.is_empty() {
353            outln!(out, "{}", t::blank_row())?;
354            for rf in &defi.risk_factors {
355                outln!(
356                    out,
357                    "{}",
358                    t::bullet_row(&format!(
359                        "{} ({}/10): {}",
360                        rf.name, rf.severity, rf.description
361                    ))
362                )?;
363            }
364        }
365    }
366
367    // External Info
368    if let Some(ext) = &analysis.external_info {
369        outln!(out, "{}", t::subsection_header("External Intelligence"))?;
370        outln!(out, "{}", t::link_row("Explorer", &ext.explorer_url))?;
371        if let Some(repo) = &ext.github_repo {
372            outln!(out, "{}", t::link_row("GitHub", repo))?;
373        }
374        if let Some(verified) = &ext.sourcify_verified {
375            if *verified {
376                outln!(out, "{}", t::check_pass("Sourcify verified"))?;
377            } else {
378                outln!(out, "{}", t::check_fail("Sourcify not verified"))?;
379            }
380        }
381        if !ext.audit_reports.is_empty() {
382            outln!(out, "{}", t::blank_row())?;
383            for report in &ext.audit_reports {
384                outln!(
385                    out,
386                    "{}",
387                    t::bullet_row(&format!("{} ({})", report.auditor, report.scope))
388                )?;
389                if !report.url.is_empty() {
390                    outln!(out, "{}", t::detail_row(&report.url))?;
391                }
392            }
393        } else {
394            outln!(out, "{}", t::blank_row())?;
395            outln!(
396                out,
397                "{}",
398                t::info_row("No audit reports found — check block explorer manually")
399            )?;
400        }
401    }
402
403    outln!(out, "{}", t::section_footer())?;
404    Ok(())
405}
406
407#[cfg(test)]
408mod tests {
409    use super::*;
410    use scope::contract::ContractAnalysis;
411
412    fn quiet() -> crate::cli::output::Output {
413        crate::cli::output::Output::capture().0
414    }
415
416    fn minimal_analysis() -> ContractAnalysis {
417        ContractAnalysis {
418            address: "0xtest".to_string(),
419            chain: "ethereum".to_string(),
420            is_verified: false,
421            source_info: None,
422            proxy_info: None,
423            access_control: None,
424            vulnerabilities: vec![],
425            defi_analysis: None,
426            external_info: None,
427            security_score: 30,
428            security_summary: "Unverified contract".to_string(),
429        }
430    }
431
432    #[test]
433    fn test_print_report_minimal() {
434        print_contract_report(&minimal_analysis(), &quiet()).unwrap();
435    }
436
437    #[test]
438    fn test_print_report_verified_with_source() {
439        let mut a = minimal_analysis();
440        a.is_verified = true;
441        a.security_score = 75;
442        a.source_info = Some(scope::contract::source::ContractSource {
443            contract_name: "TestToken".to_string(),
444            source_code: "contract T {}".to_string(),
445            abi: "[]".to_string(),
446            compiler_version: "v0.8.19".to_string(),
447            optimization_used: true,
448            optimization_runs: 200,
449            evm_version: "paris".to_string(),
450            license_type: "MIT".to_string(),
451            is_proxy: false,
452            implementation_address: None,
453            constructor_arguments: String::new(),
454            library: String::new(),
455            swarm_source: String::new(),
456            parsed_abi: vec![],
457        });
458        print_contract_report(&a, &quiet()).unwrap();
459    }
460
461    #[test]
462    fn test_print_report_source_no_optimization() {
463        let mut a = minimal_analysis();
464        a.is_verified = true;
465        a.source_info = Some(scope::contract::source::ContractSource {
466            contract_name: "T".to_string(),
467            source_code: String::new(),
468            abi: "[]".to_string(),
469            compiler_version: "v0.8.19".to_string(),
470            optimization_used: false,
471            optimization_runs: 0,
472            evm_version: "paris".to_string(),
473            license_type: "MIT".to_string(),
474            is_proxy: false,
475            implementation_address: None,
476            constructor_arguments: String::new(),
477            library: String::new(),
478            swarm_source: String::new(),
479            parsed_abi: vec![],
480        });
481        print_contract_report(&a, &quiet()).unwrap();
482    }
483
484    #[test]
485    fn test_print_report_with_proxy() {
486        let mut a = minimal_analysis();
487        a.proxy_info = Some(scope::contract::proxy::ProxyInfo {
488            is_proxy: true,
489            proxy_type: "EIP-1967".to_string(),
490            implementation_address: Some("0ximpl".to_string()),
491            admin_address: Some("0xadmin".to_string()),
492            beacon_address: None,
493            details: vec!["Proxy detected".to_string()],
494        });
495        print_contract_report(&a, &quiet()).unwrap();
496    }
497
498    #[test]
499    fn test_print_report_not_proxy() {
500        let mut a = minimal_analysis();
501        a.proxy_info = Some(scope::contract::proxy::ProxyInfo {
502            is_proxy: false,
503            proxy_type: "None".to_string(),
504            implementation_address: None,
505            admin_address: None,
506            beacon_address: None,
507            details: vec![],
508        });
509        print_contract_report(&a, &quiet()).unwrap();
510    }
511
512    #[test]
513    fn test_print_report_access_control() {
514        let mut a = minimal_analysis();
515        a.access_control = Some(scope::contract::access::AccessControlMap {
516            ownership_pattern: Some("Ownable".to_string()),
517            has_renounced_ownership: true,
518            has_role_based_access: true,
519            uses_tx_origin: true,
520            tx_origin_locations: vec![],
521            modifiers: vec![],
522            privileged_functions: vec![scope::contract::access::PrivilegedFunction {
523                name: "mint".to_string(),
524                modifiers: vec!["onlyOwner".to_string()],
525                capability: "Mint tokens".to_string(),
526                risk: scope::contract::access::PrivilegeRisk::Critical,
527            }],
528            roles: vec!["MINTER_ROLE".to_string()],
529            auth_analysis: scope::contract::access::AuthAnalysis {
530                msg_sender_checks: 1,
531                tx_origin_checks: 1,
532                has_origin_sender_comparison: false,
533                summary: "Mixed auth".to_string(),
534            },
535        });
536        print_contract_report(&a, &quiet()).unwrap();
537    }
538
539    #[test]
540    fn test_print_report_vulns() {
541        let mut a = minimal_analysis();
542        a.vulnerabilities = vec![
543            contract::vulnerability::VulnerabilityFinding {
544                id: "V-1".to_string(),
545                title: "Critical issue".to_string(),
546                severity: contract::vulnerability::Severity::Critical,
547                category: contract::vulnerability::VulnCategory::Reentrancy,
548                description: "desc".to_string(),
549                source_location: None,
550                recommendation: "fix".to_string(),
551            },
552            contract::vulnerability::VulnerabilityFinding {
553                id: "V-2".to_string(),
554                title: "High issue".to_string(),
555                severity: contract::vulnerability::Severity::High,
556                category: contract::vulnerability::VulnCategory::UncheckedCall,
557                description: "desc".to_string(),
558                source_location: None,
559                recommendation: "fix".to_string(),
560            },
561            contract::vulnerability::VulnerabilityFinding {
562                id: "V-3".to_string(),
563                title: "Medium".to_string(),
564                severity: contract::vulnerability::Severity::Medium,
565                category: contract::vulnerability::VulnCategory::Delegatecall,
566                description: "desc".to_string(),
567                source_location: None,
568                recommendation: "fix".to_string(),
569            },
570            contract::vulnerability::VulnerabilityFinding {
571                id: "V-4".to_string(),
572                title: "Low".to_string(),
573                severity: contract::vulnerability::Severity::Low,
574                category: contract::vulnerability::VulnCategory::TxOrigin,
575                description: "desc".to_string(),
576                source_location: None,
577                recommendation: "fix".to_string(),
578            },
579            contract::vulnerability::VulnerabilityFinding {
580                id: "V-5".to_string(),
581                title: "Info".to_string(),
582                severity: contract::vulnerability::Severity::Informational,
583                category: contract::vulnerability::VulnCategory::Informational,
584                description: "desc".to_string(),
585                source_location: None,
586                recommendation: "fix".to_string(),
587            },
588        ];
589        print_contract_report(&a, &quiet()).unwrap();
590    }
591
592    #[test]
593    fn test_print_report_defi() {
594        let mut a = minimal_analysis();
595        a.defi_analysis = Some(scope::contract::defi::DefiAnalysis {
596            protocol_type: scope::contract::defi::ProtocolType::DEX,
597            has_oracle_dependency: true,
598            oracle_info: vec![scope::contract::defi::OracleInfo {
599                provider: "Chainlink".to_string(),
600                usage: "Price feed".to_string(),
601                risks: vec![],
602            }],
603            has_flash_loan_risk: true,
604            flash_loan_info: vec!["Flash loan detected".to_string()],
605            dex_integrations: vec![scope::contract::defi::DexIntegration {
606                dex: "Uniswap".to_string(),
607                integration_type: "Swap".to_string(),
608                has_slippage_protection: false,
609                has_deadline_protection: true,
610            }],
611            lending_patterns: vec![],
612            token_standards: vec![scope::contract::defi::TokenStandard::ERC20],
613            staking_patterns: vec![],
614            risk_factors: vec![scope::contract::defi::DefiRiskFactor {
615                name: "Test risk".to_string(),
616                description: "A risk".to_string(),
617                severity: 7,
618            }],
619        });
620        print_contract_report(&a, &quiet()).unwrap();
621    }
622
623    #[test]
624    fn test_print_report_external() {
625        let mut a = minimal_analysis();
626        a.external_info = Some(scope::contract::external::ExternalInfo {
627            explorer_url: "https://etherscan.io/address/0xtest".to_string(),
628            github_repo: Some("https://github.com/test/repo".to_string()),
629            sourcify_verified: Some(true),
630            deployer: None,
631            audit_reports: vec![scope::contract::external::AuditReport {
632                auditor: "Trail of Bits".to_string(),
633                scope: "Token".to_string(),
634                url: "https://audit.com".to_string(),
635                date: None,
636            }],
637            metadata: vec![],
638        });
639        print_contract_report(&a, &quiet()).unwrap();
640    }
641
642    #[test]
643    fn test_print_report_external_sourcify_false() {
644        let mut a = minimal_analysis();
645        a.external_info = Some(scope::contract::external::ExternalInfo {
646            explorer_url: "https://etherscan.io/address/0xtest".to_string(),
647            github_repo: None,
648            sourcify_verified: Some(false),
649            deployer: None,
650            audit_reports: vec![],
651            metadata: vec![],
652        });
653        print_contract_report(&a, &quiet()).unwrap();
654    }
655
656    #[test]
657    fn test_print_report_access_control_empty_roles() {
658        let mut a = minimal_analysis();
659        a.access_control = Some(scope::contract::access::AccessControlMap {
660            ownership_pattern: Some("Ownable".to_string()),
661            has_renounced_ownership: false,
662            has_role_based_access: false,
663            uses_tx_origin: false,
664            tx_origin_locations: vec![],
665            modifiers: vec![],
666            privileged_functions: vec![],
667            roles: vec![],
668            auth_analysis: scope::contract::access::AuthAnalysis {
669                msg_sender_checks: 0,
670                tx_origin_checks: 0,
671                has_origin_sender_comparison: false,
672                summary: "No auth checks".to_string(),
673            },
674        });
675        print_contract_report(&a, &quiet()).unwrap();
676    }
677
678    #[test]
679    fn test_print_report_external_audit_with_url() {
680        let mut a = minimal_analysis();
681        a.external_info = Some(scope::contract::external::ExternalInfo {
682            explorer_url: "https://etherscan.io/address/0xtest".to_string(),
683            github_repo: None,
684            sourcify_verified: None,
685            deployer: None,
686            audit_reports: vec![scope::contract::external::AuditReport {
687                auditor: "CertiK".to_string(),
688                scope: "Full".to_string(),
689                url: "https://certik.com/audit.pdf".to_string(),
690                date: None,
691            }],
692            metadata: vec![],
693        });
694        print_contract_report(&a, &quiet()).unwrap();
695    }
696
697    #[test]
698    fn test_print_report_access_control_with_roles() {
699        let mut a = minimal_analysis();
700        a.access_control = Some(scope::contract::access::AccessControlMap {
701            ownership_pattern: None,
702            has_renounced_ownership: false,
703            has_role_based_access: true,
704            uses_tx_origin: false,
705            tx_origin_locations: vec![],
706            modifiers: vec![],
707            privileged_functions: vec![],
708            roles: vec!["ADMIN_ROLE".to_string(), "MINTER_ROLE".to_string()],
709            auth_analysis: scope::contract::access::AuthAnalysis {
710                msg_sender_checks: 2,
711                tx_origin_checks: 0,
712                has_origin_sender_comparison: false,
713                summary: "Role-based".to_string(),
714            },
715        });
716        print_contract_report(&a, &quiet()).unwrap();
717    }
718
719    #[test]
720    fn test_print_report_defi_empty_token_standards() {
721        let mut a = minimal_analysis();
722        a.defi_analysis = Some(scope::contract::defi::DefiAnalysis {
723            protocol_type: scope::contract::defi::ProtocolType::Other,
724            has_oracle_dependency: false,
725            oracle_info: vec![],
726            has_flash_loan_risk: false,
727            flash_loan_info: vec![],
728            dex_integrations: vec![],
729            lending_patterns: vec![],
730            token_standards: vec![],
731            staking_patterns: vec![],
732            risk_factors: vec![],
733        });
734        print_contract_report(&a, &quiet()).unwrap();
735    }
736
737    #[test]
738    fn test_print_report_proxy_no_impl_or_admin() {
739        let mut a = minimal_analysis();
740        a.proxy_info = Some(scope::contract::proxy::ProxyInfo {
741            is_proxy: true,
742            proxy_type: "Minimal Proxy".to_string(),
743            implementation_address: None,
744            admin_address: None,
745            beacon_address: None,
746            details: vec!["Minimal proxy".to_string()],
747        });
748        print_contract_report(&a, &quiet()).unwrap();
749    }
750
751    #[test]
752    fn test_print_report_defi_slippage_protected_no_deadline() {
753        let mut a = minimal_analysis();
754        a.defi_analysis = Some(scope::contract::defi::DefiAnalysis {
755            protocol_type: scope::contract::defi::ProtocolType::DEX,
756            has_oracle_dependency: false,
757            oracle_info: vec![],
758            has_flash_loan_risk: false,
759            flash_loan_info: vec![],
760            dex_integrations: vec![scope::contract::defi::DexIntegration {
761                dex: "SushiSwap".to_string(),
762                integration_type: "Swap".to_string(),
763                has_slippage_protection: true,
764                has_deadline_protection: false,
765            }],
766            lending_patterns: vec![],
767            token_standards: vec![],
768            staking_patterns: vec![],
769            risk_factors: vec![],
770        });
771        print_contract_report(&a, &quiet()).unwrap();
772    }
773}