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