use crate::contract::source::ContractSource;
use regex::Regex;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Severity {
Critical,
High,
Medium,
Low,
Informational,
}
impl std::fmt::Display for Severity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Severity::Critical => write!(f, "Critical"),
Severity::High => write!(f, "High"),
Severity::Medium => write!(f, "Medium"),
Severity::Low => write!(f, "Low"),
Severity::Informational => write!(f, "Informational"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum VulnCategory {
Reentrancy,
UncheckedCall,
Selfdestruct,
Delegatecall,
TxOrigin,
IntegerOverflow,
UninitializedStorage,
TimestampDependence,
FrontRunning,
AccessControl,
DoS,
LogicError,
Informational,
}
impl std::fmt::Display for VulnCategory {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
VulnCategory::Reentrancy => write!(f, "Reentrancy"),
VulnCategory::UncheckedCall => write!(f, "Unchecked External Call"),
VulnCategory::Selfdestruct => write!(f, "Selfdestruct"),
VulnCategory::Delegatecall => write!(f, "Delegatecall"),
VulnCategory::TxOrigin => write!(f, "tx.origin Usage"),
VulnCategory::IntegerOverflow => write!(f, "Integer Overflow"),
VulnCategory::UninitializedStorage => write!(f, "Uninitialized Storage"),
VulnCategory::TimestampDependence => write!(f, "Timestamp Dependence"),
VulnCategory::FrontRunning => write!(f, "Front-Running"),
VulnCategory::AccessControl => write!(f, "Access Control"),
VulnCategory::DoS => write!(f, "Denial of Service"),
VulnCategory::LogicError => write!(f, "Logic Error"),
VulnCategory::Informational => write!(f, "Informational"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VulnerabilityFinding {
pub id: String,
pub title: String,
pub severity: Severity,
pub category: VulnCategory,
pub description: String,
pub source_location: Option<String>,
pub recommendation: String,
}
pub fn scan_vulnerabilities(source: &ContractSource) -> Vec<VulnerabilityFinding> {
let mut findings = Vec::new();
let code = &source.source_code;
let compiler = &source.compiler_version;
check_reentrancy(code, &mut findings);
check_unchecked_calls(code, &mut findings);
check_selfdestruct(code, &mut findings);
check_delegatecall(code, &mut findings);
check_tx_origin(code, &mut findings);
check_integer_overflow(code, compiler, &mut findings);
check_timestamp_dependence(code, &mut findings);
check_uninitialized_storage(code, &mut findings);
check_front_running(code, &mut findings);
check_dos_patterns(code, &mut findings);
findings
}
pub fn scan_bytecode_only(bytecode: &str) -> Vec<VulnerabilityFinding> {
let mut findings = Vec::new();
let code = bytecode.trim_start_matches("0x").to_lowercase();
if code.contains("ff") {
if code.len() < 200 {
findings.push(VulnerabilityFinding {
id: "SCOPE-BYTE-001".to_string(),
title: "Potential SELFDESTRUCT in bytecode".to_string(),
severity: Severity::Informational,
category: VulnCategory::Selfdestruct,
description: "Bytecode may contain SELFDESTRUCT opcode. \
Verify source code to confirm."
.to_string(),
source_location: None,
recommendation: "Verify contract source code to confirm selfdestruct presence."
.to_string(),
});
}
}
if code.contains("f4") {
findings.push(VulnerabilityFinding {
id: "SCOPE-BYTE-002".to_string(),
title: "DELEGATECALL opcode detected".to_string(),
severity: Severity::Informational,
category: VulnCategory::Delegatecall,
description: "Bytecode contains DELEGATECALL. This may be a proxy contract \
or may delegate execution to another contract."
.to_string(),
source_location: None,
recommendation: "Verify source code to understand delegatecall usage.".to_string(),
});
}
if findings.is_empty() {
findings.push(VulnerabilityFinding {
id: "SCOPE-BYTE-000".to_string(),
title: "Unverified contract".to_string(),
severity: Severity::Medium,
category: VulnCategory::Informational,
description: "Contract source code is not verified. Full vulnerability analysis \
requires verified source code."
.to_string(),
source_location: None,
recommendation:
"Request contract verification on the block explorer before interacting."
.to_string(),
});
}
findings
}
fn check_reentrancy(code: &str, findings: &mut Vec<VulnerabilityFinding>) {
let re =
Regex::new(r"\.call\{[^}]*value[^}]*\}\s*\([^)]*\)[\s\S]{0,200}(?:\w+\s*=|\w+\[.*\]\s*=)");
if let Ok(re) = re
&& re.is_match(code)
{
findings.push(VulnerabilityFinding {
id: "SCOPE-REENT-001".to_string(),
title: "Potential reentrancy vulnerability".to_string(),
severity: Severity::High,
category: VulnCategory::Reentrancy,
description: "External call with value transfer found before state variable \
update. An attacker could re-enter the function before state is updated."
.to_string(),
source_location: None,
recommendation: "Use the checks-effects-interactions pattern: update state \
before making external calls. Consider using ReentrancyGuard."
.to_string(),
});
}
if code.contains(".call{")
&& !code.contains("nonReentrant")
&& !code.contains("ReentrancyGuard")
{
if code.contains("balances[") || code.contains("_balances[") {
findings.push(VulnerabilityFinding {
id: "SCOPE-REENT-002".to_string(),
title: "External call without reentrancy guard".to_string(),
severity: Severity::Medium,
category: VulnCategory::Reentrancy,
description: "Contract makes external calls with value but does not use \
a reentrancy guard (nonReentrant modifier)."
.to_string(),
source_location: None,
recommendation: "Add OpenZeppelin ReentrancyGuard to functions with external calls."
.to_string(),
});
}
}
}
fn check_unchecked_calls(code: &str, findings: &mut Vec<VulnerabilityFinding>) {
let re = Regex::new(r"(?:address\([^)]+\)|[\w.]+)\.call\{?[^}]*\}?\([^)]*\)\s*;");
if let Ok(re) = re {
for mat in re.find_iter(code) {
let context = mat.as_str();
if !context.contains("require") && !context.contains("(bool") {
findings.push(VulnerabilityFinding {
id: "SCOPE-UCALL-001".to_string(),
title: "Unchecked low-level call".to_string(),
severity: Severity::Medium,
category: VulnCategory::UncheckedCall,
description: format!(
"Low-level call without return value check: {}",
&context[..context.len().min(80)]
),
source_location: None,
recommendation: "Always check the return value of low-level calls: \
(bool success, ) = addr.call(...); require(success);"
.to_string(),
});
break; }
}
}
if code.contains(".send(") && !code.contains("require") {
let re = Regex::new(r"\.send\([^)]*\)\s*;");
if let Ok(re) = re
&& re.is_match(code)
{
findings.push(VulnerabilityFinding {
id: "SCOPE-UCALL-002".to_string(),
title: "Unchecked send()".to_string(),
severity: Severity::Medium,
category: VulnCategory::UncheckedCall,
description: "send() return value not checked. send() returns false on failure \
but does not revert."
.to_string(),
source_location: None,
recommendation: "Use transfer() instead of send(), or check the return value."
.to_string(),
});
}
}
}
fn check_selfdestruct(code: &str, findings: &mut Vec<VulnerabilityFinding>) {
if code.contains("selfdestruct") || code.contains("suicide") {
let severity = if code.contains("onlyOwner") || code.contains("onlyAdmin") {
Severity::Medium
} else {
Severity::Critical
};
findings.push(VulnerabilityFinding {
id: "SCOPE-DESTR-001".to_string(),
title: "Contract uses selfdestruct".to_string(),
severity,
category: VulnCategory::Selfdestruct,
description: "Contract can be destroyed via selfdestruct. After EIP-6780, \
selfdestruct only sends ETH (except in same-transaction creation), \
but the pattern indicates potential fund extraction."
.to_string(),
source_location: None,
recommendation: "Remove selfdestruct if possible. If needed, ensure it's \
protected by a timelock and multisig."
.to_string(),
});
}
}
fn check_delegatecall(code: &str, findings: &mut Vec<VulnerabilityFinding>) {
let re = Regex::new(r"\.delegatecall\(");
if let Ok(re) = re
&& re.is_match(code)
{
let dangerous_re = Regex::new(r"(?:address|_\w+|target|impl)\s*\.\s*delegatecall\(");
let is_likely_user_input = dangerous_re.map(|re| re.is_match(code)).unwrap_or(false);
if is_likely_user_input {
findings.push(VulnerabilityFinding {
id: "SCOPE-DELCALL-001".to_string(),
title: "Delegatecall to variable address".to_string(),
severity: Severity::High,
category: VulnCategory::Delegatecall,
description: "Contract uses delegatecall with a variable target address. \
If the target is user-controlled, an attacker can execute arbitrary code \
in this contract's context."
.to_string(),
source_location: None,
recommendation: "Ensure delegatecall target is immutable or access-controlled. \
Never delegatecall to user-supplied addresses."
.to_string(),
});
} else {
findings.push(VulnerabilityFinding {
id: "SCOPE-DELCALL-002".to_string(),
title: "Contract uses delegatecall".to_string(),
severity: Severity::Low,
category: VulnCategory::Delegatecall,
description: "Contract uses delegatecall (common in proxy patterns). \
Verify the target address is properly controlled."
.to_string(),
source_location: None,
recommendation:
"Verify delegatecall targets are immutable or behind access control."
.to_string(),
});
}
}
}
fn check_tx_origin(code: &str, findings: &mut Vec<VulnerabilityFinding>) {
if !code.contains("tx.origin") {
return;
}
let auth_re = Regex::new(r"(?:require|if|assert)\s*\(.*tx\.origin");
if let Ok(re) = auth_re
&& re.is_match(code)
{
if code.contains("tx.origin == msg.sender") || code.contains("msg.sender == tx.origin") {
findings.push(VulnerabilityFinding {
id: "SCOPE-TXORG-002".to_string(),
title: "tx.origin == msg.sender check".to_string(),
severity: Severity::Low,
category: VulnCategory::TxOrigin,
description: "Contract checks tx.origin == msg.sender as an anti-contract guard. \
This prevents contracts from calling these functions but may break \
legitimate integrations."
.to_string(),
source_location: None,
recommendation: "Consider if blocking contract callers is truly necessary. \
This pattern limits composability."
.to_string(),
});
} else {
findings.push(VulnerabilityFinding {
id: "SCOPE-TXORG-001".to_string(),
title: "tx.origin used for authorization".to_string(),
severity: Severity::Critical,
category: VulnCategory::TxOrigin,
description: "Contract uses tx.origin for authorization checks. \
A malicious contract can trick a user into calling it, and then \
call this contract on the user's behalf."
.to_string(),
source_location: None,
recommendation: "Replace tx.origin with msg.sender for all authorization checks."
.to_string(),
});
}
}
}
fn check_integer_overflow(code: &str, compiler: &str, findings: &mut Vec<VulnerabilityFinding>) {
let is_pre_08 = compiler.contains("v0.4.")
|| compiler.contains("v0.5.")
|| compiler.contains("v0.6.")
|| compiler.contains("v0.7.");
if !is_pre_08 {
if code.contains("unchecked {") {
findings.push(VulnerabilityFinding {
id: "SCOPE-OVFLOW-002".to_string(),
title: "Unchecked arithmetic block".to_string(),
severity: Severity::Low,
category: VulnCategory::IntegerOverflow,
description: "Contract uses unchecked {} blocks which disable overflow checks. \
Verify that arithmetic within these blocks cannot overflow."
.to_string(),
source_location: None,
recommendation: "Ensure values in unchecked blocks have been validated upstream."
.to_string(),
});
}
return;
}
if !code.contains("SafeMath") && !code.contains("safeAdd") && !code.contains("safeSub") {
findings.push(VulnerabilityFinding {
id: "SCOPE-OVFLOW-001".to_string(),
title: "Pre-0.8 contract without SafeMath".to_string(),
severity: Severity::High,
category: VulnCategory::IntegerOverflow,
description: format!(
"Contract compiled with {} (pre-0.8) without SafeMath library. \
Arithmetic operations may overflow/underflow silently.",
compiler
),
source_location: None,
recommendation: "Use OpenZeppelin SafeMath or upgrade to Solidity 0.8+.".to_string(),
});
}
}
fn check_timestamp_dependence(code: &str, findings: &mut Vec<VulnerabilityFinding>) {
if code.contains("block.timestamp") || code.contains("now") {
let critical_re =
Regex::new(r"(?:require|if|assert)\s*\(.*(?:block\.timestamp|now)\s*(?:[<>=!]+|<=|>=)");
if let Ok(re) = critical_re
&& re.is_match(code)
{
findings.push(VulnerabilityFinding {
id: "SCOPE-TIME-001".to_string(),
title: "Timestamp used in critical comparison".to_string(),
severity: Severity::Low,
category: VulnCategory::TimestampDependence,
description: "Block timestamp used in conditional logic. Miners can \
manipulate timestamps by ~15 seconds. Avoid using timestamps for \
randomness or precise timing."
.to_string(),
source_location: None,
recommendation: "Use block numbers for time-based logic where precision matters. \
Timestamp is acceptable for coarse-grained checks (hours/days)."
.to_string(),
});
}
}
}
fn check_uninitialized_storage(code: &str, findings: &mut Vec<VulnerabilityFinding>) {
let re = Regex::new(r"function\s+\w+[^{]*\{[^}]*\bstruct\s+\w+\s+\w+\s*;");
if let Ok(re) = re
&& re.is_match(code)
{
findings.push(VulnerabilityFinding {
id: "SCOPE-UNINIT-001".to_string(),
title: "Potential uninitialized storage pointer".to_string(),
severity: Severity::Medium,
category: VulnCategory::UninitializedStorage,
description: "Struct variable declared without explicit storage/memory keyword. \
In older Solidity versions, this defaults to storage and may point to \
unexpected storage slots."
.to_string(),
source_location: None,
recommendation:
"Always specify 'memory' or 'storage' for struct variables in functions."
.to_string(),
});
}
}
fn check_front_running(code: &str, findings: &mut Vec<VulnerabilityFinding>) {
if code.contains("function approve") && !code.contains("increaseAllowance") {
findings.push(VulnerabilityFinding {
id: "SCOPE-FRONT-001".to_string(),
title: "ERC20 approve front-running".to_string(),
severity: Severity::Informational,
category: VulnCategory::FrontRunning,
description: "Standard ERC20 approve() is susceptible to front-running. \
An attacker can front-run an allowance change to spend both old and new values."
.to_string(),
source_location: None,
recommendation: "Implement increaseAllowance/decreaseAllowance pattern, \
or require setting allowance to 0 before changing."
.to_string(),
});
}
if (code.contains("bid") || code.contains("vote"))
&& !code.contains("commit")
&& code.contains("function")
{
let fn_re = Regex::new(r"function\s+(?:bid|vote|placeBid|castVote)");
if let Ok(re) = fn_re
&& re.is_match(code)
{
findings.push(VulnerabilityFinding {
id: "SCOPE-FRONT-002".to_string(),
title: "Bid/vote without commit-reveal".to_string(),
severity: Severity::Medium,
category: VulnCategory::FrontRunning,
description: "Contract has bid/vote functions without commit-reveal scheme. \
On-chain bids/votes are visible in the mempool and can be front-run."
.to_string(),
source_location: None,
recommendation: "Implement a commit-reveal scheme for private bidding/voting."
.to_string(),
});
}
}
}
fn check_dos_patterns(code: &str, findings: &mut Vec<VulnerabilityFinding>) {
let re = Regex::new(r"for\s*\([^)]*;\s*\w+\s*<\s*\w+\.length\s*;");
if let Ok(re) = re
&& re.is_match(code)
{
if code.contains("push(") {
findings.push(VulnerabilityFinding {
id: "SCOPE-DOS-001".to_string(),
title: "Unbounded loop over dynamic array".to_string(),
severity: Severity::Medium,
category: VulnCategory::DoS,
description: "Contract loops over a dynamic array that can grow via push(). \
If the array grows large enough, the loop may exceed the block gas limit, \
making the function uncallable."
.to_string(),
source_location: None,
recommendation: "Set a maximum array size, use pagination, or restructure \
to avoid iterating over unbounded arrays."
.to_string(),
});
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::contract::source::ContractSource;
fn make_source(code: &str, compiler: &str) -> ContractSource {
ContractSource {
contract_name: "Test".to_string(),
source_code: code.to_string(),
abi: "[]".to_string(),
compiler_version: compiler.to_string(),
optimization_used: true,
optimization_runs: 200,
evm_version: "paris".to_string(),
license_type: "MIT".to_string(),
is_proxy: false,
implementation_address: None,
constructor_arguments: String::new(),
library: String::new(),
swarm_source: String::new(),
parsed_abi: vec![],
}
}
#[test]
fn test_selfdestruct_detection() {
let src = make_source("function kill() { selfdestruct(owner); }", "v0.8.19");
let findings = scan_vulnerabilities(&src);
assert!(
findings
.iter()
.any(|f| f.category == VulnCategory::Selfdestruct)
);
}
#[test]
fn test_tx_origin_detection() {
let src = make_source(
"function withdraw() { require(tx.origin == owner); }",
"v0.8.19",
);
let findings = scan_vulnerabilities(&src);
assert!(
findings
.iter()
.any(|f| f.category == VulnCategory::TxOrigin)
);
}
#[test]
fn test_pre08_no_safemath() {
let src = make_source("contract Token { uint256 x = a + b; }", "v0.7.6");
let findings = scan_vulnerabilities(&src);
assert!(
findings
.iter()
.any(|f| f.category == VulnCategory::IntegerOverflow)
);
}
#[test]
fn test_08_with_unchecked() {
let src = make_source("unchecked { x = a + b; }", "v0.8.19");
let findings = scan_vulnerabilities(&src);
assert!(findings.iter().any(|f| f.id == "SCOPE-OVFLOW-002"));
}
#[test]
fn test_bytecode_only_scan() {
let findings = scan_bytecode_only("0x6080604052");
assert!(findings.iter().any(|f| f.title.contains("Unverified")));
}
#[test]
fn test_erc20_approve_frontrunning() {
let src = make_source(
"function approve(address spender, uint256 amount) public returns (bool) {}",
"v0.8.19",
);
let findings = scan_vulnerabilities(&src);
assert!(
findings
.iter()
.any(|f| f.category == VulnCategory::FrontRunning)
);
}
#[test]
fn test_clean_contract() {
let src = make_source(
"pragma solidity ^0.8.19;\ncontract Safe { function foo() view returns (uint) { return 1; } }",
"v0.8.19",
);
let findings = scan_vulnerabilities(&src);
assert!(
!findings
.iter()
.any(|f| f.severity == Severity::Critical || f.severity == Severity::High)
);
}
#[test]
fn test_severity_display_all() {
assert_eq!(format!("{}", Severity::Critical), "Critical");
assert_eq!(format!("{}", Severity::High), "High");
assert_eq!(format!("{}", Severity::Medium), "Medium");
assert_eq!(format!("{}", Severity::Low), "Low");
assert_eq!(format!("{}", Severity::Informational), "Informational");
}
#[test]
fn test_vuln_category_display_all() {
assert_eq!(format!("{}", VulnCategory::Reentrancy), "Reentrancy");
assert_eq!(
format!("{}", VulnCategory::UncheckedCall),
"Unchecked External Call"
);
assert_eq!(format!("{}", VulnCategory::Selfdestruct), "Selfdestruct");
assert_eq!(format!("{}", VulnCategory::Delegatecall), "Delegatecall");
assert_eq!(format!("{}", VulnCategory::TxOrigin), "tx.origin Usage");
assert_eq!(
format!("{}", VulnCategory::IntegerOverflow),
"Integer Overflow"
);
assert_eq!(
format!("{}", VulnCategory::UninitializedStorage),
"Uninitialized Storage"
);
assert_eq!(
format!("{}", VulnCategory::TimestampDependence),
"Timestamp Dependence"
);
assert_eq!(format!("{}", VulnCategory::FrontRunning), "Front-Running");
assert_eq!(format!("{}", VulnCategory::AccessControl), "Access Control");
assert_eq!(format!("{}", VulnCategory::DoS), "Denial of Service");
assert_eq!(format!("{}", VulnCategory::LogicError), "Logic Error");
assert_eq!(format!("{}", VulnCategory::Informational), "Informational");
}
#[test]
fn test_reentrancy_call_value_state_update() {
let code = "function withdraw(uint amount) {\n (bool ok,) = msg.sender.call{value: amount}(\"\");\n balances[msg.sender] = 0;\n}";
let src = make_source(code, "v0.8.19");
let findings = scan_vulnerabilities(&src);
assert!(findings.iter().any(|f| f.id == "SCOPE-REENT-001"));
}
#[test]
fn test_reentrancy_no_guard_with_balances() {
let code = "function send() { (bool s,) = addr.call{value: 1}(\"\"); balances[addr] = 0; }";
let src = make_source(code, "v0.8.19");
let findings = scan_vulnerabilities(&src);
assert!(findings.iter().any(|f| f.id == "SCOPE-REENT-002"));
}
#[test]
fn test_unchecked_low_level_call() {
let code = "function pay() { address(target).call{value: 1}(\"\"); }";
let src = make_source(code, "v0.8.19");
let findings = scan_vulnerabilities(&src);
assert!(findings.iter().any(|f| f.id == "SCOPE-UCALL-001"));
}
#[test]
fn test_unchecked_send() {
let code = "function pay() { addr.send(1 ether); }";
let src = make_source(code, "v0.8.19");
let findings = scan_vulnerabilities(&src);
assert!(findings.iter().any(|f| f.id == "SCOPE-UCALL-002"));
}
#[test]
fn test_delegatecall_variable_addr() {
let code = "function f(address target) { target.delegatecall(data); }";
let src = make_source(code, "v0.8.19");
let findings = scan_vulnerabilities(&src);
assert!(findings.iter().any(|f| f.id == "SCOPE-DELCALL-001"));
}
#[test]
fn test_delegatecall_constant() {
let code = "function f() { IMPL.delegatecall(data); }";
let src = make_source(code, "v0.8.19");
let findings = scan_vulnerabilities(&src);
assert!(findings.iter().any(|f| f.id == "SCOPE-DELCALL-002"));
}
#[test]
fn test_tx_origin_msg_sender_comparison() {
let code = "function check() { require(tx.origin == msg.sender); }";
let src = make_source(code, "v0.8.19");
let findings = scan_vulnerabilities(&src);
assert!(findings.iter().any(|f| f.id == "SCOPE-TXORG-002"));
}
#[test]
fn test_tx_origin_dangerous_auth() {
let code = "function check() { require(tx.origin == owner); }";
let src = make_source(code, "v0.8.19");
let findings = scan_vulnerabilities(&src);
assert!(findings.iter().any(|f| f.id == "SCOPE-TXORG-001"));
}
#[test]
fn test_timestamp_dependence_require() {
let code = "function claim() { require(block.timestamp > deadline); }";
let src = make_source(code, "v0.8.19");
let findings = scan_vulnerabilities(&src);
assert!(findings.iter().any(|f| f.id == "SCOPE-TIME-001"));
}
#[test]
fn test_front_running_bid_no_commit() {
let code = "contract Auction { function bid() public payable {} function placeBid() {} }";
let src = make_source(code, "v0.8.19");
let findings = scan_vulnerabilities(&src);
assert!(findings.iter().any(|f| f.id == "SCOPE-FRONT-002"));
}
#[test]
fn test_dos_unbounded_loop() {
let code = "function distribute() { for (uint i = 0; i < recipients.length; i++) {} } function add() { recipients.push(addr); }";
let src = make_source(code, "v0.8.19");
let findings = scan_vulnerabilities(&src);
assert!(findings.iter().any(|f| f.id == "SCOPE-DOS-001"));
}
#[test]
fn test_pre08_with_safemath_no_finding() {
let src = make_source("using SafeMath for uint256; uint x = a.add(b);", "v0.7.6");
let findings = scan_vulnerabilities(&src);
assert!(!findings.iter().any(|f| f.id == "SCOPE-OVFLOW-001"));
}
#[test]
fn test_08_no_unchecked_no_overflow() {
let src = make_source("uint x = a + b;", "v0.8.19");
let findings = scan_vulnerabilities(&src);
assert!(
!findings
.iter()
.any(|f| f.category == VulnCategory::IntegerOverflow)
);
}
#[test]
fn test_bytecode_short_with_ff() {
let findings = scan_bytecode_only("0xff");
assert!(findings.iter().any(|f| f.id == "SCOPE-BYTE-001"));
}
#[test]
fn test_bytecode_with_delegatecall_opcode() {
let code = "6080604052f46080604052f46080604052f46080604052f46080604052f46080604052f46080604052f46080604052f46080604052f46080604052f46080604052";
let findings = scan_bytecode_only(code);
assert!(findings.iter().any(|f| f.id == "SCOPE-BYTE-002"));
}
#[test]
fn test_selfdestruct_with_owner_guard_medium() {
let src = make_source(
"function kill() onlyOwner { selfdestruct(owner); }",
"v0.8.19",
);
let findings = scan_vulnerabilities(&src);
let sd = findings
.iter()
.find(|f| f.category == VulnCategory::Selfdestruct)
.unwrap();
assert_eq!(sd.severity, Severity::Medium);
}
#[test]
fn test_selfdestruct_no_guard_critical() {
let src = make_source(
"function kill() public { selfdestruct(msg.sender); }",
"v0.8.19",
);
let findings = scan_vulnerabilities(&src);
let sd = findings
.iter()
.find(|f| f.category == VulnCategory::Selfdestruct)
.unwrap();
assert_eq!(sd.severity, Severity::Critical);
}
#[test]
fn test_suicide_alias_detected() {
let src = make_source("function kill() { suicide(owner); }", "v0.4.24");
let findings = scan_vulnerabilities(&src);
assert!(
findings
.iter()
.any(|f| f.category == VulnCategory::Selfdestruct)
);
}
#[test]
fn test_pre04_compiler_overflow() {
let src = make_source("contract X { }", "v0.4.24");
let findings = scan_vulnerabilities(&src);
assert!(findings.iter().any(|f| f.id == "SCOPE-OVFLOW-001"));
}
#[test]
fn test_pre05_compiler_overflow() {
let src = make_source("contract X { }", "v0.5.17");
let findings = scan_vulnerabilities(&src);
assert!(findings.iter().any(|f| f.id == "SCOPE-OVFLOW-001"));
}
#[test]
fn test_pre06_compiler_overflow() {
let src = make_source("contract X { }", "v0.6.12");
let findings = scan_vulnerabilities(&src);
assert!(findings.iter().any(|f| f.id == "SCOPE-OVFLOW-001"));
}
}