Skip to main content

scope/contract/
vulnerability.rs

1//! # Vulnerability Heuristic Scanner
2//!
3//! Scans smart contract source code and bytecode for common vulnerability
4//! patterns using heuristic analysis. This is NOT a formal verification
5//! tool — it identifies patterns that are *often* associated with vulnerabilities.
6//!
7//! ## Detected Vulnerability Categories
8//!
9//! - **Reentrancy** - State changes after external calls
10//! - **Unchecked external calls** - Missing return value checks on call/send
11//! - **Selfdestruct** - Contracts that can be destroyed
12//! - **Delegatecall** - Unprotected delegatecall to user-supplied address
13//! - **tx.origin** - Authorization via tx.origin
14//! - **Integer overflow** - Pre-Solidity 0.8 without SafeMath
15//! - **Uninitialized storage** - Storage variables without initialization
16//! - **Timestamp dependence** - Block timestamp manipulation
17//! - **Front-running** - Susceptible to MEV/front-running
18
19use crate::contract::source::ContractSource;
20use regex::Regex;
21use serde::{Deserialize, Serialize};
22
23/// Severity level for a vulnerability finding.
24#[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/// Vulnerability category.
46#[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/// A vulnerability finding from heuristic analysis.
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct VulnerabilityFinding {
86    /// Unique finding ID (e.g., "SCOPE-REENT-001").
87    pub id: String,
88    /// Short title.
89    pub title: String,
90    /// Severity classification.
91    pub severity: Severity,
92    /// Vulnerability category.
93    pub category: VulnCategory,
94    /// Detailed description of the finding.
95    pub description: String,
96    /// Source code location (if available).
97    pub source_location: Option<String>,
98    /// Recommended fix.
99    pub recommendation: String,
100}
101
102/// Scan verified source code for vulnerability heuristics.
103pub 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
122/// Scan bytecode only (unverified contracts) for basic patterns.
123pub 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    // Check for SELFDESTRUCT opcode (0xff)
128    if code.contains("ff") {
129        // This is a very rough heuristic — 0xff appears in many contexts.
130        // Only flag if the bytecode is short (likely a simple destructor)
131        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    // Check for DELEGATECALL opcode (0xf4)
148    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
181/// Check for reentrancy patterns: external calls before state updates.
182fn check_reentrancy(code: &str, findings: &mut Vec<VulnerabilityFinding>) {
183    // Pattern: .call{value:...}("") followed by state variable assignment
184    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    // Check for .call without reentrancy guard
205    if code.contains(".call{")
206        && !code.contains("nonReentrant")
207        && !code.contains("ReentrancyGuard")
208    {
209        // Only flag if there are state-changing operations
210        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
227/// Check for unchecked low-level calls.
228fn check_unchecked_calls(code: &str, findings: &mut Vec<VulnerabilityFinding>) {
229    // Pattern: .call(...) without checking return value
230    // Look for calls not assigned to (bool success, ) or not in require()
231    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            // Skip if return value is checked
236            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; // One finding is enough
252            }
253        }
254    }
255
256    // Check for send() without return check
257    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
278/// Check for selfdestruct usage.
279fn 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
304/// Check for dangerous delegatecall patterns.
305fn check_delegatecall(code: &str, findings: &mut Vec<VulnerabilityFinding>) {
306    // Dangerous: delegatecall to user-supplied address
307    let re = Regex::new(r"\.delegatecall\(");
308    if let Ok(re) = re
309        && re.is_match(code)
310    {
311        // Check if the target is hardcoded or user-supplied
312        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
348/// Check for tx.origin usage.
349fn check_tx_origin(code: &str, findings: &mut Vec<VulnerabilityFinding>) {
350    if !code.contains("tx.origin") {
351        return;
352    }
353
354    // Check if tx.origin is used for authorization (not just logging)
355    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        // Check if it's tx.origin == msg.sender (less dangerous anti-contract guard)
360        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
393/// Check for integer overflow (pre-Solidity 0.8).
394fn check_integer_overflow(code: &str, compiler: &str, findings: &mut Vec<VulnerabilityFinding>) {
395    // Solidity 0.8+ has built-in overflow checks
396    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        // Check for unchecked blocks in 0.8+ code
403        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    // Pre-0.8: check for SafeMath usage
421    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
438/// Check for timestamp dependence.
439fn check_timestamp_dependence(code: &str, findings: &mut Vec<VulnerabilityFinding>) {
440    if code.contains("block.timestamp") || code.contains("now") {
441        // Check if timestamp is used in critical logic
442        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
465/// Check for uninitialized storage patterns.
466fn check_uninitialized_storage(code: &str, findings: &mut Vec<VulnerabilityFinding>) {
467    // Pattern: struct variable declared in function without initialization
468    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
489/// Check for front-running susceptibility.
490fn check_front_running(code: &str, findings: &mut Vec<VulnerabilityFinding>) {
491    // Check for approve pattern without allowance check
492    if code.contains("function approve") && !code.contains("increaseAllowance") {
493        // ERC20 approve front-running is a known issue
494        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    // Check for commit-reveal missing in auction/governance
510    if (code.contains("bid") || code.contains("vote"))
511        && !code.contains("commit")
512        && code.contains("function")
513    {
514        // Only flag if there are actual bid/vote functions
515        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
535/// Check for denial-of-service patterns.
536fn check_dos_patterns(code: &str, findings: &mut Vec<VulnerabilityFinding>) {
537    // Check for loops over dynamic arrays
538    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        // Check if the array could be user-controlled
543        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}