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