1use crate::contract::source::ContractSource;
20use regex::Regex;
21use serde::{Deserialize, Serialize};
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
25pub enum Severity {
26 Critical,
27 High,
28 Medium,
29 Low,
30 Informational,
31}
32
33impl std::fmt::Display for Severity {
34 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35 match self {
36 Severity::Critical => write!(f, "Critical"),
37 Severity::High => write!(f, "High"),
38 Severity::Medium => write!(f, "Medium"),
39 Severity::Low => write!(f, "Low"),
40 Severity::Informational => write!(f, "Informational"),
41 }
42 }
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
47pub enum VulnCategory {
48 Reentrancy,
49 UncheckedCall,
50 Selfdestruct,
51 Delegatecall,
52 TxOrigin,
53 IntegerOverflow,
54 UninitializedStorage,
55 TimestampDependence,
56 FrontRunning,
57 AccessControl,
58 DoS,
59 LogicError,
60 Informational,
61}
62
63impl std::fmt::Display for VulnCategory {
64 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65 match self {
66 VulnCategory::Reentrancy => write!(f, "Reentrancy"),
67 VulnCategory::UncheckedCall => write!(f, "Unchecked External Call"),
68 VulnCategory::Selfdestruct => write!(f, "Selfdestruct"),
69 VulnCategory::Delegatecall => write!(f, "Delegatecall"),
70 VulnCategory::TxOrigin => write!(f, "tx.origin Usage"),
71 VulnCategory::IntegerOverflow => write!(f, "Integer Overflow"),
72 VulnCategory::UninitializedStorage => write!(f, "Uninitialized Storage"),
73 VulnCategory::TimestampDependence => write!(f, "Timestamp Dependence"),
74 VulnCategory::FrontRunning => write!(f, "Front-Running"),
75 VulnCategory::AccessControl => write!(f, "Access Control"),
76 VulnCategory::DoS => write!(f, "Denial of Service"),
77 VulnCategory::LogicError => write!(f, "Logic Error"),
78 VulnCategory::Informational => write!(f, "Informational"),
79 }
80 }
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct VulnerabilityFinding {
86 pub id: String,
88 pub title: String,
90 pub severity: Severity,
92 pub category: VulnCategory,
94 pub description: String,
96 pub source_location: Option<String>,
98 pub recommendation: String,
100}
101
102pub fn scan_vulnerabilities(source: &ContractSource) -> Vec<VulnerabilityFinding> {
104 let mut findings = Vec::new();
105 let code = &source.source_code;
106 let compiler = &source.compiler_version;
107
108 check_reentrancy(code, &mut findings);
109 check_unchecked_calls(code, &mut findings);
110 check_selfdestruct(code, &mut findings);
111 check_delegatecall(code, &mut findings);
112 check_tx_origin(code, &mut findings);
113 check_integer_overflow(code, compiler, &mut findings);
114 check_timestamp_dependence(code, &mut findings);
115 check_uninitialized_storage(code, &mut findings);
116 check_front_running(code, &mut findings);
117 check_dos_patterns(code, &mut findings);
118
119 findings
120}
121
122pub fn scan_bytecode_only(bytecode: &str) -> Vec<VulnerabilityFinding> {
124 let mut findings = Vec::new();
125 let code = bytecode.trim_start_matches("0x").to_lowercase();
126
127 if code.contains("ff") {
129 if code.len() < 200 {
132 findings.push(VulnerabilityFinding {
133 id: "SCOPE-BYTE-001".to_string(),
134 title: "Potential SELFDESTRUCT in bytecode".to_string(),
135 severity: Severity::Informational,
136 category: VulnCategory::Selfdestruct,
137 description: "Bytecode may contain SELFDESTRUCT opcode. \
138 Verify source code to confirm."
139 .to_string(),
140 source_location: None,
141 recommendation: "Verify contract source code to confirm selfdestruct presence."
142 .to_string(),
143 });
144 }
145 }
146
147 if code.contains("f4") {
149 findings.push(VulnerabilityFinding {
150 id: "SCOPE-BYTE-002".to_string(),
151 title: "DELEGATECALL opcode detected".to_string(),
152 severity: Severity::Informational,
153 category: VulnCategory::Delegatecall,
154 description: "Bytecode contains DELEGATECALL. This may be a proxy contract \
155 or may delegate execution to another contract."
156 .to_string(),
157 source_location: None,
158 recommendation: "Verify source code to understand delegatecall usage.".to_string(),
159 });
160 }
161
162 if findings.is_empty() {
163 findings.push(VulnerabilityFinding {
164 id: "SCOPE-BYTE-000".to_string(),
165 title: "Unverified contract".to_string(),
166 severity: Severity::Medium,
167 category: VulnCategory::Informational,
168 description: "Contract source code is not verified. Full vulnerability analysis \
169 requires verified source code."
170 .to_string(),
171 source_location: None,
172 recommendation:
173 "Request contract verification on the block explorer before interacting."
174 .to_string(),
175 });
176 }
177
178 findings
179}
180
181fn check_reentrancy(code: &str, findings: &mut Vec<VulnerabilityFinding>) {
183 let re =
185 Regex::new(r"\.call\{[^}]*value[^}]*\}\s*\([^)]*\)[\s\S]{0,200}(?:\w+\s*=|\w+\[.*\]\s*=)");
186 if let Ok(re) = re
187 && re.is_match(code)
188 {
189 findings.push(VulnerabilityFinding {
190 id: "SCOPE-REENT-001".to_string(),
191 title: "Potential reentrancy vulnerability".to_string(),
192 severity: Severity::High,
193 category: VulnCategory::Reentrancy,
194 description: "External call with value transfer found before state variable \
195 update. An attacker could re-enter the function before state is updated."
196 .to_string(),
197 source_location: None,
198 recommendation: "Use the checks-effects-interactions pattern: update state \
199 before making external calls. Consider using ReentrancyGuard."
200 .to_string(),
201 });
202 }
203
204 if code.contains(".call{")
206 && !code.contains("nonReentrant")
207 && !code.contains("ReentrancyGuard")
208 {
209 if code.contains("balances[") || code.contains("_balances[") {
211 findings.push(VulnerabilityFinding {
212 id: "SCOPE-REENT-002".to_string(),
213 title: "External call without reentrancy guard".to_string(),
214 severity: Severity::Medium,
215 category: VulnCategory::Reentrancy,
216 description: "Contract makes external calls with value but does not use \
217 a reentrancy guard (nonReentrant modifier)."
218 .to_string(),
219 source_location: None,
220 recommendation: "Add OpenZeppelin ReentrancyGuard to functions with external calls."
221 .to_string(),
222 });
223 }
224 }
225}
226
227fn check_unchecked_calls(code: &str, findings: &mut Vec<VulnerabilityFinding>) {
229 let re = Regex::new(r"(?:address\([^)]+\)|[\w.]+)\.call\{?[^}]*\}?\([^)]*\)\s*;");
232 if let Ok(re) = re {
233 for mat in re.find_iter(code) {
234 let context = mat.as_str();
235 if !context.contains("require") && !context.contains("(bool") {
237 findings.push(VulnerabilityFinding {
238 id: "SCOPE-UCALL-001".to_string(),
239 title: "Unchecked low-level call".to_string(),
240 severity: Severity::Medium,
241 category: VulnCategory::UncheckedCall,
242 description: format!(
243 "Low-level call without return value check: {}",
244 &context[..context.len().min(80)]
245 ),
246 source_location: None,
247 recommendation: "Always check the return value of low-level calls: \
248 (bool success, ) = addr.call(...); require(success);"
249 .to_string(),
250 });
251 break; }
253 }
254 }
255
256 if code.contains(".send(") && !code.contains("require") {
258 let re = Regex::new(r"\.send\([^)]*\)\s*;");
259 if let Ok(re) = re
260 && re.is_match(code)
261 {
262 findings.push(VulnerabilityFinding {
263 id: "SCOPE-UCALL-002".to_string(),
264 title: "Unchecked send()".to_string(),
265 severity: Severity::Medium,
266 category: VulnCategory::UncheckedCall,
267 description: "send() return value not checked. send() returns false on failure \
268 but does not revert."
269 .to_string(),
270 source_location: None,
271 recommendation: "Use transfer() instead of send(), or check the return value."
272 .to_string(),
273 });
274 }
275 }
276}
277
278fn check_selfdestruct(code: &str, findings: &mut Vec<VulnerabilityFinding>) {
280 if code.contains("selfdestruct") || code.contains("suicide") {
281 let severity = if code.contains("onlyOwner") || code.contains("onlyAdmin") {
282 Severity::Medium
283 } else {
284 Severity::Critical
285 };
286
287 findings.push(VulnerabilityFinding {
288 id: "SCOPE-DESTR-001".to_string(),
289 title: "Contract uses selfdestruct".to_string(),
290 severity,
291 category: VulnCategory::Selfdestruct,
292 description: "Contract can be destroyed via selfdestruct. After EIP-6780, \
293 selfdestruct only sends ETH (except in same-transaction creation), \
294 but the pattern indicates potential fund extraction."
295 .to_string(),
296 source_location: None,
297 recommendation: "Remove selfdestruct if possible. If needed, ensure it's \
298 protected by a timelock and multisig."
299 .to_string(),
300 });
301 }
302}
303
304fn check_delegatecall(code: &str, findings: &mut Vec<VulnerabilityFinding>) {
306 let re = Regex::new(r"\.delegatecall\(");
308 if let Ok(re) = re
309 && re.is_match(code)
310 {
311 let dangerous_re = Regex::new(r"(?:address|_\w+|target|impl)\s*\.\s*delegatecall\(");
313 let is_likely_user_input = dangerous_re.map(|re| re.is_match(code)).unwrap_or(false);
314
315 if is_likely_user_input {
316 findings.push(VulnerabilityFinding {
317 id: "SCOPE-DELCALL-001".to_string(),
318 title: "Delegatecall to variable address".to_string(),
319 severity: Severity::High,
320 category: VulnCategory::Delegatecall,
321 description: "Contract uses delegatecall with a variable target address. \
322 If the target is user-controlled, an attacker can execute arbitrary code \
323 in this contract's context."
324 .to_string(),
325 source_location: None,
326 recommendation: "Ensure delegatecall target is immutable or access-controlled. \
327 Never delegatecall to user-supplied addresses."
328 .to_string(),
329 });
330 } else {
331 findings.push(VulnerabilityFinding {
332 id: "SCOPE-DELCALL-002".to_string(),
333 title: "Contract uses delegatecall".to_string(),
334 severity: Severity::Low,
335 category: VulnCategory::Delegatecall,
336 description: "Contract uses delegatecall (common in proxy patterns). \
337 Verify the target address is properly controlled."
338 .to_string(),
339 source_location: None,
340 recommendation:
341 "Verify delegatecall targets are immutable or behind access control."
342 .to_string(),
343 });
344 }
345 }
346}
347
348fn check_tx_origin(code: &str, findings: &mut Vec<VulnerabilityFinding>) {
350 if !code.contains("tx.origin") {
351 return;
352 }
353
354 let auth_re = Regex::new(r"(?:require|if|assert)\s*\(.*tx\.origin");
356 if let Ok(re) = auth_re
357 && re.is_match(code)
358 {
359 if code.contains("tx.origin == msg.sender") || code.contains("msg.sender == tx.origin") {
361 findings.push(VulnerabilityFinding {
362 id: "SCOPE-TXORG-002".to_string(),
363 title: "tx.origin == msg.sender check".to_string(),
364 severity: Severity::Low,
365 category: VulnCategory::TxOrigin,
366 description: "Contract checks tx.origin == msg.sender as an anti-contract guard. \
367 This prevents contracts from calling these functions but may break \
368 legitimate integrations."
369 .to_string(),
370 source_location: None,
371 recommendation: "Consider if blocking contract callers is truly necessary. \
372 This pattern limits composability."
373 .to_string(),
374 });
375 } else {
376 findings.push(VulnerabilityFinding {
377 id: "SCOPE-TXORG-001".to_string(),
378 title: "tx.origin used for authorization".to_string(),
379 severity: Severity::Critical,
380 category: VulnCategory::TxOrigin,
381 description: "Contract uses tx.origin for authorization checks. \
382 A malicious contract can trick a user into calling it, and then \
383 call this contract on the user's behalf."
384 .to_string(),
385 source_location: None,
386 recommendation: "Replace tx.origin with msg.sender for all authorization checks."
387 .to_string(),
388 });
389 }
390 }
391}
392
393fn check_integer_overflow(code: &str, compiler: &str, findings: &mut Vec<VulnerabilityFinding>) {
395 let is_pre_08 = compiler.contains("v0.4.")
397 || compiler.contains("v0.5.")
398 || compiler.contains("v0.6.")
399 || compiler.contains("v0.7.");
400
401 if !is_pre_08 {
402 if code.contains("unchecked {") {
404 findings.push(VulnerabilityFinding {
405 id: "SCOPE-OVFLOW-002".to_string(),
406 title: "Unchecked arithmetic block".to_string(),
407 severity: Severity::Low,
408 category: VulnCategory::IntegerOverflow,
409 description: "Contract uses unchecked {} blocks which disable overflow checks. \
410 Verify that arithmetic within these blocks cannot overflow."
411 .to_string(),
412 source_location: None,
413 recommendation: "Ensure values in unchecked blocks have been validated upstream."
414 .to_string(),
415 });
416 }
417 return;
418 }
419
420 if !code.contains("SafeMath") && !code.contains("safeAdd") && !code.contains("safeSub") {
422 findings.push(VulnerabilityFinding {
423 id: "SCOPE-OVFLOW-001".to_string(),
424 title: "Pre-0.8 contract without SafeMath".to_string(),
425 severity: Severity::High,
426 category: VulnCategory::IntegerOverflow,
427 description: format!(
428 "Contract compiled with {} (pre-0.8) without SafeMath library. \
429 Arithmetic operations may overflow/underflow silently.",
430 compiler
431 ),
432 source_location: None,
433 recommendation: "Use OpenZeppelin SafeMath or upgrade to Solidity 0.8+.".to_string(),
434 });
435 }
436}
437
438fn check_timestamp_dependence(code: &str, findings: &mut Vec<VulnerabilityFinding>) {
440 if code.contains("block.timestamp") || code.contains("now") {
441 let critical_re =
443 Regex::new(r"(?:require|if|assert)\s*\(.*(?:block\.timestamp|now)\s*(?:[<>=!]+|<=|>=)");
444 if let Ok(re) = critical_re
445 && re.is_match(code)
446 {
447 findings.push(VulnerabilityFinding {
448 id: "SCOPE-TIME-001".to_string(),
449 title: "Timestamp used in critical comparison".to_string(),
450 severity: Severity::Low,
451 category: VulnCategory::TimestampDependence,
452 description: "Block timestamp used in conditional logic. Miners can \
453 manipulate timestamps by ~15 seconds. Avoid using timestamps for \
454 randomness or precise timing."
455 .to_string(),
456 source_location: None,
457 recommendation: "Use block numbers for time-based logic where precision matters. \
458 Timestamp is acceptable for coarse-grained checks (hours/days)."
459 .to_string(),
460 });
461 }
462 }
463}
464
465fn check_uninitialized_storage(code: &str, findings: &mut Vec<VulnerabilityFinding>) {
467 let re = Regex::new(r"function\s+\w+[^{]*\{[^}]*\bstruct\s+\w+\s+\w+\s*;");
469 if let Ok(re) = re
470 && re.is_match(code)
471 {
472 findings.push(VulnerabilityFinding {
473 id: "SCOPE-UNINIT-001".to_string(),
474 title: "Potential uninitialized storage pointer".to_string(),
475 severity: Severity::Medium,
476 category: VulnCategory::UninitializedStorage,
477 description: "Struct variable declared without explicit storage/memory keyword. \
478 In older Solidity versions, this defaults to storage and may point to \
479 unexpected storage slots."
480 .to_string(),
481 source_location: None,
482 recommendation:
483 "Always specify 'memory' or 'storage' for struct variables in functions."
484 .to_string(),
485 });
486 }
487}
488
489fn check_front_running(code: &str, findings: &mut Vec<VulnerabilityFinding>) {
491 if code.contains("function approve") && !code.contains("increaseAllowance") {
493 findings.push(VulnerabilityFinding {
495 id: "SCOPE-FRONT-001".to_string(),
496 title: "ERC20 approve front-running".to_string(),
497 severity: Severity::Informational,
498 category: VulnCategory::FrontRunning,
499 description: "Standard ERC20 approve() is susceptible to front-running. \
500 An attacker can front-run an allowance change to spend both old and new values."
501 .to_string(),
502 source_location: None,
503 recommendation: "Implement increaseAllowance/decreaseAllowance pattern, \
504 or require setting allowance to 0 before changing."
505 .to_string(),
506 });
507 }
508
509 if (code.contains("bid") || code.contains("vote"))
511 && !code.contains("commit")
512 && code.contains("function")
513 {
514 let fn_re = Regex::new(r"function\s+(?:bid|vote|placeBid|castVote)");
516 if let Ok(re) = fn_re
517 && re.is_match(code)
518 {
519 findings.push(VulnerabilityFinding {
520 id: "SCOPE-FRONT-002".to_string(),
521 title: "Bid/vote without commit-reveal".to_string(),
522 severity: Severity::Medium,
523 category: VulnCategory::FrontRunning,
524 description: "Contract has bid/vote functions without commit-reveal scheme. \
525 On-chain bids/votes are visible in the mempool and can be front-run."
526 .to_string(),
527 source_location: None,
528 recommendation: "Implement a commit-reveal scheme for private bidding/voting."
529 .to_string(),
530 });
531 }
532 }
533}
534
535fn check_dos_patterns(code: &str, findings: &mut Vec<VulnerabilityFinding>) {
537 let re = Regex::new(r"for\s*\([^)]*;\s*\w+\s*<\s*\w+\.length\s*;");
539 if let Ok(re) = re
540 && re.is_match(code)
541 {
542 if code.contains("push(") {
544 findings.push(VulnerabilityFinding {
545 id: "SCOPE-DOS-001".to_string(),
546 title: "Unbounded loop over dynamic array".to_string(),
547 severity: Severity::Medium,
548 category: VulnCategory::DoS,
549 description: "Contract loops over a dynamic array that can grow via push(). \
550 If the array grows large enough, the loop may exceed the block gas limit, \
551 making the function uncallable."
552 .to_string(),
553 source_location: None,
554 recommendation: "Set a maximum array size, use pagination, or restructure \
555 to avoid iterating over unbounded arrays."
556 .to_string(),
557 });
558 }
559 }
560}
561
562#[cfg(test)]
563mod tests {
564 use super::*;
565 use crate::contract::source::ContractSource;
566
567 fn make_source(code: &str, compiler: &str) -> ContractSource {
568 ContractSource {
569 contract_name: "Test".to_string(),
570 source_code: code.to_string(),
571 abi: "[]".to_string(),
572 compiler_version: compiler.to_string(),
573 optimization_used: true,
574 optimization_runs: 200,
575 evm_version: "paris".to_string(),
576 license_type: "MIT".to_string(),
577 is_proxy: false,
578 implementation_address: None,
579 constructor_arguments: String::new(),
580 library: String::new(),
581 swarm_source: String::new(),
582 parsed_abi: vec![],
583 }
584 }
585
586 #[test]
587 fn test_selfdestruct_detection() {
588 let src = make_source("function kill() { selfdestruct(owner); }", "v0.8.19");
589 let findings = scan_vulnerabilities(&src);
590 assert!(
591 findings
592 .iter()
593 .any(|f| f.category == VulnCategory::Selfdestruct)
594 );
595 }
596
597 #[test]
598 fn test_tx_origin_detection() {
599 let src = make_source(
600 "function withdraw() { require(tx.origin == owner); }",
601 "v0.8.19",
602 );
603 let findings = scan_vulnerabilities(&src);
604 assert!(
605 findings
606 .iter()
607 .any(|f| f.category == VulnCategory::TxOrigin)
608 );
609 }
610
611 #[test]
612 fn test_pre08_no_safemath() {
613 let src = make_source("contract Token { uint256 x = a + b; }", "v0.7.6");
614 let findings = scan_vulnerabilities(&src);
615 assert!(
616 findings
617 .iter()
618 .any(|f| f.category == VulnCategory::IntegerOverflow)
619 );
620 }
621
622 #[test]
623 fn test_08_with_unchecked() {
624 let src = make_source("unchecked { x = a + b; }", "v0.8.19");
625 let findings = scan_vulnerabilities(&src);
626 assert!(findings.iter().any(|f| f.id == "SCOPE-OVFLOW-002"));
627 }
628
629 #[test]
630 fn test_bytecode_only_scan() {
631 let findings = scan_bytecode_only("0x6080604052");
632 assert!(findings.iter().any(|f| f.title.contains("Unverified")));
633 }
634
635 #[test]
636 fn test_erc20_approve_frontrunning() {
637 let src = make_source(
638 "function approve(address spender, uint256 amount) public returns (bool) {}",
639 "v0.8.19",
640 );
641 let findings = scan_vulnerabilities(&src);
642 assert!(
643 findings
644 .iter()
645 .any(|f| f.category == VulnCategory::FrontRunning)
646 );
647 }
648
649 #[test]
650 fn test_clean_contract() {
651 let src = make_source(
652 "pragma solidity ^0.8.19;\ncontract Safe { function foo() view returns (uint) { return 1; } }",
653 "v0.8.19",
654 );
655 let findings = scan_vulnerabilities(&src);
656 assert!(
657 !findings
658 .iter()
659 .any(|f| f.severity == Severity::Critical || f.severity == Severity::High)
660 );
661 }
662
663 #[test]
664 fn test_severity_display_all() {
665 assert_eq!(format!("{}", Severity::Critical), "Critical");
666 assert_eq!(format!("{}", Severity::High), "High");
667 assert_eq!(format!("{}", Severity::Medium), "Medium");
668 assert_eq!(format!("{}", Severity::Low), "Low");
669 assert_eq!(format!("{}", Severity::Informational), "Informational");
670 }
671
672 #[test]
673 fn test_vuln_category_display_all() {
674 assert_eq!(format!("{}", VulnCategory::Reentrancy), "Reentrancy");
675 assert_eq!(
676 format!("{}", VulnCategory::UncheckedCall),
677 "Unchecked External Call"
678 );
679 assert_eq!(format!("{}", VulnCategory::Selfdestruct), "Selfdestruct");
680 assert_eq!(format!("{}", VulnCategory::Delegatecall), "Delegatecall");
681 assert_eq!(format!("{}", VulnCategory::TxOrigin), "tx.origin Usage");
682 assert_eq!(
683 format!("{}", VulnCategory::IntegerOverflow),
684 "Integer Overflow"
685 );
686 assert_eq!(
687 format!("{}", VulnCategory::UninitializedStorage),
688 "Uninitialized Storage"
689 );
690 assert_eq!(
691 format!("{}", VulnCategory::TimestampDependence),
692 "Timestamp Dependence"
693 );
694 assert_eq!(format!("{}", VulnCategory::FrontRunning), "Front-Running");
695 assert_eq!(format!("{}", VulnCategory::AccessControl), "Access Control");
696 assert_eq!(format!("{}", VulnCategory::DoS), "Denial of Service");
697 assert_eq!(format!("{}", VulnCategory::LogicError), "Logic Error");
698 assert_eq!(format!("{}", VulnCategory::Informational), "Informational");
699 }
700
701 #[test]
702 fn test_reentrancy_call_value_state_update() {
703 let code = "function withdraw(uint amount) {\n (bool ok,) = msg.sender.call{value: amount}(\"\");\n balances[msg.sender] = 0;\n}";
704 let src = make_source(code, "v0.8.19");
705 let findings = scan_vulnerabilities(&src);
706 assert!(findings.iter().any(|f| f.id == "SCOPE-REENT-001"));
707 }
708
709 #[test]
710 fn test_reentrancy_no_guard_with_balances() {
711 let code = "function send() { (bool s,) = addr.call{value: 1}(\"\"); balances[addr] = 0; }";
712 let src = make_source(code, "v0.8.19");
713 let findings = scan_vulnerabilities(&src);
714 assert!(findings.iter().any(|f| f.id == "SCOPE-REENT-002"));
715 }
716
717 #[test]
718 fn test_unchecked_low_level_call() {
719 let code = "function pay() { address(target).call{value: 1}(\"\"); }";
720 let src = make_source(code, "v0.8.19");
721 let findings = scan_vulnerabilities(&src);
722 assert!(findings.iter().any(|f| f.id == "SCOPE-UCALL-001"));
723 }
724
725 #[test]
726 fn test_unchecked_send() {
727 let code = "function pay() { addr.send(1 ether); }";
728 let src = make_source(code, "v0.8.19");
729 let findings = scan_vulnerabilities(&src);
730 assert!(findings.iter().any(|f| f.id == "SCOPE-UCALL-002"));
731 }
732
733 #[test]
734 fn test_delegatecall_variable_addr() {
735 let code = "function f(address target) { target.delegatecall(data); }";
736 let src = make_source(code, "v0.8.19");
737 let findings = scan_vulnerabilities(&src);
738 assert!(findings.iter().any(|f| f.id == "SCOPE-DELCALL-001"));
739 }
740
741 #[test]
742 fn test_delegatecall_constant() {
743 let code = "function f() { IMPL.delegatecall(data); }";
744 let src = make_source(code, "v0.8.19");
745 let findings = scan_vulnerabilities(&src);
746 assert!(findings.iter().any(|f| f.id == "SCOPE-DELCALL-002"));
747 }
748
749 #[test]
750 fn test_tx_origin_msg_sender_comparison() {
751 let code = "function check() { require(tx.origin == msg.sender); }";
752 let src = make_source(code, "v0.8.19");
753 let findings = scan_vulnerabilities(&src);
754 assert!(findings.iter().any(|f| f.id == "SCOPE-TXORG-002"));
755 }
756
757 #[test]
758 fn test_tx_origin_dangerous_auth() {
759 let code = "function check() { require(tx.origin == owner); }";
760 let src = make_source(code, "v0.8.19");
761 let findings = scan_vulnerabilities(&src);
762 assert!(findings.iter().any(|f| f.id == "SCOPE-TXORG-001"));
763 }
764
765 #[test]
766 fn test_timestamp_dependence_require() {
767 let code = "function claim() { require(block.timestamp > deadline); }";
768 let src = make_source(code, "v0.8.19");
769 let findings = scan_vulnerabilities(&src);
770 assert!(findings.iter().any(|f| f.id == "SCOPE-TIME-001"));
771 }
772
773 #[test]
774 fn test_front_running_bid_no_commit() {
775 let code = "contract Auction { function bid() public payable {} function placeBid() {} }";
776 let src = make_source(code, "v0.8.19");
777 let findings = scan_vulnerabilities(&src);
778 assert!(findings.iter().any(|f| f.id == "SCOPE-FRONT-002"));
779 }
780
781 #[test]
782 fn test_dos_unbounded_loop() {
783 let code = "function distribute() { for (uint i = 0; i < recipients.length; i++) {} } function add() { recipients.push(addr); }";
784 let src = make_source(code, "v0.8.19");
785 let findings = scan_vulnerabilities(&src);
786 assert!(findings.iter().any(|f| f.id == "SCOPE-DOS-001"));
787 }
788
789 #[test]
790 fn test_pre08_with_safemath_no_finding() {
791 let src = make_source("using SafeMath for uint256; uint x = a.add(b);", "v0.7.6");
792 let findings = scan_vulnerabilities(&src);
793 assert!(!findings.iter().any(|f| f.id == "SCOPE-OVFLOW-001"));
794 }
795
796 #[test]
797 fn test_08_no_unchecked_no_overflow() {
798 let src = make_source("uint x = a + b;", "v0.8.19");
799 let findings = scan_vulnerabilities(&src);
800 assert!(
801 !findings
802 .iter()
803 .any(|f| f.category == VulnCategory::IntegerOverflow)
804 );
805 }
806
807 #[test]
808 fn test_bytecode_short_with_ff() {
809 let findings = scan_bytecode_only("0xff");
810 assert!(findings.iter().any(|f| f.id == "SCOPE-BYTE-001"));
811 }
812
813 #[test]
814 fn test_bytecode_with_delegatecall_opcode() {
815 let code = "6080604052f46080604052f46080604052f46080604052f46080604052f46080604052f46080604052f46080604052f46080604052f46080604052f46080604052";
816 let findings = scan_bytecode_only(code);
817 assert!(findings.iter().any(|f| f.id == "SCOPE-BYTE-002"));
818 }
819
820 #[test]
821 fn test_selfdestruct_with_owner_guard_medium() {
822 let src = make_source(
823 "function kill() onlyOwner { selfdestruct(owner); }",
824 "v0.8.19",
825 );
826 let findings = scan_vulnerabilities(&src);
827 let sd = findings
828 .iter()
829 .find(|f| f.category == VulnCategory::Selfdestruct)
830 .unwrap();
831 assert_eq!(sd.severity, Severity::Medium);
832 }
833
834 #[test]
835 fn test_selfdestruct_no_guard_critical() {
836 let src = make_source(
837 "function kill() public { selfdestruct(msg.sender); }",
838 "v0.8.19",
839 );
840 let findings = scan_vulnerabilities(&src);
841 let sd = findings
842 .iter()
843 .find(|f| f.category == VulnCategory::Selfdestruct)
844 .unwrap();
845 assert_eq!(sd.severity, Severity::Critical);
846 }
847
848 #[test]
849 fn test_suicide_alias_detected() {
850 let src = make_source("function kill() { suicide(owner); }", "v0.4.24");
851 let findings = scan_vulnerabilities(&src);
852 assert!(
853 findings
854 .iter()
855 .any(|f| f.category == VulnCategory::Selfdestruct)
856 );
857 }
858
859 #[test]
860 fn test_pre04_compiler_overflow() {
861 let src = make_source("contract X { }", "v0.4.24");
862 let findings = scan_vulnerabilities(&src);
863 assert!(findings.iter().any(|f| f.id == "SCOPE-OVFLOW-001"));
864 }
865
866 #[test]
867 fn test_pre05_compiler_overflow() {
868 let src = make_source("contract X { }", "v0.5.17");
869 let findings = scan_vulnerabilities(&src);
870 assert!(findings.iter().any(|f| f.id == "SCOPE-OVFLOW-001"));
871 }
872
873 #[test]
874 fn test_pre06_compiler_overflow() {
875 let src = make_source("contract X { }", "v0.6.12");
876 let findings = scan_vulnerabilities(&src);
877 assert!(findings.iter().any(|f| f.id == "SCOPE-OVFLOW-001"));
878 }
879}