1use crate::chains::ChainRegistry;
4use crate::core::{Finding, ProjectConfig, SecurityScores, Severity};
5use crate::plugins::{PluginContext, PluginRegistry};
6use crate::security::checks;
7use std::path::Path;
8use std::sync::Arc;
9
10#[derive(Clone)]
12pub struct SecurityEngine {
13 config: Arc<crate::core::config::SecurityConfig>,
14 project_config: Arc<ProjectConfig>,
15 plugin_registry: Arc<PluginRegistry>,
16 check_registry: Vec<&'static checks::SecurityCheckMeta>,
17}
18
19impl SecurityEngine {
20 pub fn new(
22 config: &ProjectConfig,
23 plugin_registry: &PluginRegistry,
24 ) -> Result<Self, crate::core::ForgeGuardError> {
25 let mut engine = Self {
26 config: Arc::new(config.security.clone()),
27 project_config: Arc::new(config.clone()),
28 plugin_registry: Arc::new(plugin_registry.clone()),
29 check_registry: Vec::new(),
30 };
31
32 engine.register_builtin_checks();
34 engine.register_plugin_checks()?;
36
37 Ok(engine)
38 }
39
40 fn register_builtin_checks(&mut self) {
41 for check in checks::ALL_CHECKS {
42 let should_register = match check.severity {
43 "critical" | "high" => self.config.enable_high,
44 "medium" => self.config.enable_medium,
45 "low" => self.config.enable_low,
46 "informational" => self.config.enable_info,
47 _ => true,
48 };
49
50 let effective_severity = self
52 .config
53 .severity_overrides
54 .get(check.name)
55 .map(|s| s.as_str())
56 .unwrap_or(check.severity);
57
58 if should_register && self.filter_by_severity(effective_severity) {
59 self.check_registry.push(check);
60 }
61 }
62 }
63
64 fn filter_by_severity(&self, severity: &str) -> bool {
65 match severity {
66 "critical" | "high" => self.config.enable_high,
67 "medium" => self.config.enable_medium,
68 "low" => self.config.enable_low,
69 "informational" => self.config.enable_info,
70 _ => true,
71 }
72 }
73
74 fn register_plugin_checks(&mut self) -> Result<(), crate::core::ForgeGuardError> {
75 Ok(())
78 }
79
80 pub fn analyze_files(
83 &self,
84 files: &[std::path::PathBuf],
85 _chain_registry: &ChainRegistry,
86 ) -> Result<Vec<Finding>, crate::core::ForgeGuardError> {
87 use rayon::prelude::*;
88 let max_findings = self.config.max_findings_per_check;
89
90 let mut all_findings: Vec<Finding> = files
92 .par_iter()
93 .flat_map(|file| {
94 let content = match std::fs::read_to_string(file) {
95 Ok(c) => c,
96 Err(_) => return Vec::new(),
97 };
98
99 self.analyze_file(file, &content, max_findings)
100 })
101 .collect();
102
103 let plugin_ctx = PluginContext::new(&self.project_config, files.to_vec());
106 let plugin_results = self.plugin_registry.execute_all(&plugin_ctx);
107
108 for result in plugin_results {
109 if result.success {
110 all_findings.extend(result.findings);
111 } else if let Some(err) = &result.error {
112 eprintln!(" ⚠️ Plugin '{}' failed: {}", result.plugin_name, err);
113 }
114 }
115
116 Ok(all_findings)
117 }
118
119 pub fn analyze_files_quick(
124 &self,
125 files: &[std::path::PathBuf],
126 _chain_registry: &ChainRegistry,
127 ) -> Result<Vec<Finding>, crate::core::ForgeGuardError> {
128 use rayon::prelude::*;
129 let max_findings = self.config.max_findings_per_check.min(15);
131
132 let quick_checks: Vec<&&'static checks::SecurityCheckMeta> = self
134 .check_registry
135 .iter()
136 .filter(|check| {
137 let sev = check.severity;
139 (sev == "high" || sev == "critical")
140 && check.id != "FA-H-001"
142 && check.id != "FA-H-002"
143 })
144 .collect();
145
146 let all_findings: Vec<Finding> = files
147 .par_iter()
148 .flat_map(|file| {
149 let content = match std::fs::read_to_string(file) {
150 Ok(c) => c,
151 Err(_) => return Vec::new(),
152 };
153
154 let file_name = file.to_string_lossy().to_string();
155 let lines: Vec<&str> = content.lines().collect();
156 let mut findings = Vec::new();
157
158 for check in &quick_checks {
159 if findings.len() >= max_findings {
160 break;
161 }
162 let check_findings = self.execute_check(check, &file_name, &lines, &content);
163 findings.extend(check_findings);
164 }
165
166 findings
167 })
168 .collect();
169
170 Ok(all_findings)
172 }
173
174 fn analyze_file(&self, file: &Path, content: &str, max_findings: usize) -> Vec<Finding> {
176 let file_name = file.to_string_lossy().to_string();
177 let lines: Vec<&str> = content.lines().collect();
178 let mut findings = Vec::new();
179
180 for check in &self.check_registry {
182 if findings.len() >= max_findings {
183 break;
184 }
185
186 let check_findings = self.execute_check(check, &file_name, &lines, content);
187 findings.extend(check_findings);
188 }
189
190 findings
191 }
192
193 fn execute_check(
195 &self,
196 check: &&'static checks::SecurityCheckMeta,
197 file_name: &str,
198 lines: &[&str],
199 content: &str,
200 ) -> Vec<Finding> {
201 match check.id {
202 "FA-H-001" => self.check_reentrancy(file_name, lines, content),
204 "FA-H-002" => self.check_access_control(file_name, lines, content),
205 "FA-H-003" => self.check_delegatecall(file_name, lines, content),
206 "FA-H-004" => self.check_tx_origin(file_name, lines, content),
207 "FA-H-005" => self.check_create2(file_name, lines, content),
208 "FA-H-006" => self.check_dos(file_name, lines, content),
209 "FA-H-007" => self.check_storage_collision(file_name, lines, content),
210 "FA-H-008" => self.check_unsafe_assembly(file_name, lines, content),
211 "FA-H-009" => self.check_selfdestruct(file_name, lines, content),
212 "FA-H-010" => self.check_proxy_vulnerabilities(file_name, lines, content),
213 "FA-H-011" => self.check_oracle_manipulation(file_name, lines, content),
214 "FA-H-012" => self.check_signature_vulnerabilities(file_name, lines, content),
215 "FA-H-013" => self.check_replay_attacks(file_name, lines, content),
216 "FA-H-014" => self.check_erc20_issues(file_name, lines, content),
217 "FA-H-015" => self.check_bridge_vulnerabilities(file_name, lines, content),
218 "FA-H-016" => self.check_flash_loan_issues(file_name, lines, content),
219 "FA-H-017" => self.check_mev_issues(file_name, lines, content),
220 "FA-H-018" => self.check_cross_chain_issues(file_name, lines, content),
221 "FA-H-019" => self.check_dependency_vulnerabilities(file_name, lines, content),
222 "FA-H-020" => self.check_unsafe_imports(file_name, lines, content),
223 "FA-H-021" => self.check_unsafe_initializers(file_name, lines, content),
224 "FA-H-022" => self.check_unsafe_upgrade_paths(file_name, lines, content),
225 "FA-H-023" => self.check_clone_vulnerabilities(file_name, lines, content),
226
227 "FA-M-001" => self.check_gas_problems(file_name, lines, content),
229 "FA-M-002" => self.check_unsafe_casting(file_name, lines, content),
230 "FA-M-003" => self.check_timestamp_manipulation(file_name, lines, content),
231 "FA-M-004" => self.check_storage_inefficiencies(file_name, lines, content),
232 "FA-M-005" => self.check_unsafe_events(file_name, lines, content),
233 "FA-M-006" => self.check_poor_visibility(file_name, lines, content),
234 "FA-M-007" => self.check_bad_modifiers(file_name, lines, content),
235 "FA-M-008" => self.check_unsafe_math(file_name, lines, content),
236 "FA-M-009" => self.check_poor_access_patterns(file_name, lines, content),
237
238 "FA-L-001" => self.check_naming_issues(file_name, lines, content),
240 "FA-L-002" => self.check_code_duplication(file_name, lines, content),
241 "FA-L-003" => self.check_optimization(file_name, lines, content),
242
243 "FA-I-001" => self.check_style_issues(file_name, lines, content),
245 "FA-I-002" => self.check_documentation(file_name, lines, content),
246
247 _ => Vec::new(),
248 }
249 }
250
251 pub fn calculate_scores(&self, findings: &[Finding]) -> SecurityScores {
253 let mut scores = SecurityScores::perfect();
254
255 for finding in findings {
256 let deduction = match finding.severity {
257 Severity::Critical => 30,
258 Severity::High => 15,
259 Severity::Medium => 8,
260 Severity::Low => 3,
261 Severity::Informational => 1,
262 };
263
264 match finding.category.as_str() {
265 "Access Control" => {
266 scores.access_control = scores.access_control.saturating_sub(deduction)
267 }
268 "DeFi" => {
269 scores.security = scores.security.saturating_sub(deduction);
270 scores.exploit_resistance = scores.exploit_resistance.saturating_sub(deduction);
271 }
272 "Upgradeability" => {
273 scores.upgradeability = scores.upgradeability.saturating_sub(deduction);
274 scores.proxy_safety = scores.proxy_safety.saturating_sub(deduction);
275 }
276 "Gas" => scores.gas = scores.gas.saturating_sub(deduction),
277 "Deployment" => scores.deployment = scores.deployment.saturating_sub(deduction),
278 "Dependencies" => {
279 scores.dependencies = scores.dependencies.saturating_sub(deduction)
280 }
281 "Cross-Chain" => {
282 scores.chain_compatibility =
283 scores.chain_compatibility.saturating_sub(deduction)
284 }
285 "Logic" | "Security" => scores.security = scores.security.saturating_sub(deduction),
286 "Cryptography" => scores.security = scores.security.saturating_sub(deduction),
287 _ => scores.security = scores.security.saturating_sub(deduction),
288 }
289
290 scores.production_readiness = scores.production_readiness.saturating_sub(deduction / 2);
292 scores.architecture = scores.architecture.saturating_sub(deduction / 3);
293 }
294
295 scores
296 }
297
298 fn find_lines_containing<'a>(
303 &self,
304 lines: &[&'a str],
305 patterns: &[&str],
306 ) -> Vec<(usize, &'a str)> {
307 let mut results = Vec::new();
308 for (i, line) in lines.iter().enumerate() {
309 for pattern in patterns {
310 if line.contains(pattern)
311 && !line.trim_start().starts_with("//")
312 && !line.trim_start().starts_with("/*")
313 {
314 results.push((i + 1, line.trim()));
315 break;
316 }
317 }
318 }
319 results
320 }
321
322 #[allow(dead_code)]
323 fn find_lines_containing_excluding<'a>(
324 &self,
325 lines: &[&'a str],
326 pattern: &str,
327 exclude: &[&str],
328 ) -> Vec<(usize, &'a str)> {
329 let mut results = Vec::new();
330 for (i, line) in lines.iter().enumerate() {
331 if line.contains(pattern) && !line.trim_start().starts_with("//") {
332 let excluded = exclude.iter().any(|e| line.contains(e));
333 if !excluded {
334 results.push((i + 1, line.trim()));
335 }
336 }
337 }
338 results
339 }
340
341 fn make_finding(
342 &self,
343 check: &'static checks::SecurityCheckMeta,
344 file: &str,
345 line: usize,
346 snippet: &str,
347 ) -> Finding {
348 Finding::builder()
349 .id(&format!("{}-{}", check.id, line))
350 .title(check.name)
351 .description(check.description)
352 .severity(match check.severity {
353 "critical" => Severity::Critical,
354 "high" => Severity::High,
355 "medium" => Severity::Medium,
356 "low" => Severity::Low,
357 _ => Severity::Informational,
358 })
359 .file(file)
360 .location(line, 0)
361 .code(snippet)
362 .recommendation(check.remediation)
363 .category(check.category)
364 .blocks_deployment(check.blocks_deployment)
365 .build()
366 }
367
368 fn has_callback_reentrancy_indicators(
373 &self,
374 func: &crate::parser::FunctionDef,
375 contract: &crate::parser::Contract,
376 ) -> bool {
377 let has_mint = func.body.iter().any(|s| {
381 s.text.contains("_mint(")
382 || s.text.contains("_safeMint(")
383 || s.text.contains("_transfer(")
384 });
385 let is_safe_mint_contract = contract.is_erc721()
387 || contract.is_erc1155()
388 || contract.inheritance.iter().any(|i| {
389 let l = i.to_lowercase();
390 l.contains("erc777")
391 || l.contains("erc721")
392 || l.contains("erc1155")
393 || l.contains("erc721upgradeable")
394 || l.contains("erc1155upgradeable")
395 });
396 is_safe_mint_contract && has_mint
397 }
398
399 fn has_read_only_reentrancy(&self, func: &crate::parser::FunctionDef) -> bool {
403 let mut has_ext_call = false;
405 let mut state_read_after_call = false;
406 for stmt in &func.body {
407 match &stmt.kind {
408 crate::parser::StatementKind::ExternalCall => {
409 has_ext_call = true;
410 }
411 crate::parser::StatementKind::StateRead if has_ext_call => {
412 state_read_after_call = true;
413 }
414 _ => {}
415 }
416 }
417 if has_ext_call && state_read_after_call {
419 let reads_critical = func.body.iter().any(|s| {
420 matches!(s.kind, crate::parser::StatementKind::StateRead)
421 && (s.text.contains("balanceOf")
422 || s.text.contains("totalSupply")
423 || s.text.contains("ownerOf")
424 || s.text.contains("getBalance")
425 || s.text.contains(".balance"))
426 });
427 return reads_critical;
428 }
429 false
430 }
431
432 fn is_external_call_to_user_address(&self, stmts: &[crate::parser::Statement]) -> bool {
434 stmts.iter().any(|s| {
435 if matches!(s.kind, crate::parser::StatementKind::ExternalCall) {
436 let target = crate::parser::extract_call_target(&s.text);
437 matches!(target, crate::parser::CallTarget::LocalVariable(_))
438 || matches!(target, crate::parser::CallTarget::Address)
439 } else {
440 false
441 }
442 })
443 }
444
445 fn has_self_call_reentrancy(&self, func: &crate::parser::FunctionDef) -> bool {
447 func.body.iter().any(|s| {
448 if matches!(s.kind, crate::parser::StatementKind::ExternalCall) {
449 let target = crate::parser::extract_call_target(&s.text);
450 matches!(target, crate::parser::CallTarget::SelfCall)
451 } else {
452 false
453 }
454 })
455 }
456
457 fn check_reentrancy(&self, file: &str, _lines: &[&str], content: &str) -> Vec<Finding> {
459 let source_file = crate::parser::parse_source(content);
461 let mut findings = Vec::new();
462
463 for contract in &source_file.contracts {
464 let has_global_guard = source_file.inheritance_includes_reentrancy_guard(contract)
466 || contract.has_reentrancy_guard_modifier();
467
468 for func in &contract.functions {
469 if !func.modifies_state() {
471 continue;
472 }
473
474 if func.has_reentrancy_guard() {
476 continue;
477 }
478
479 if self.has_callback_reentrancy_indicators(func, contract) {
483 findings.push(
484 Finding::builder()
485 .id(&format!("FA-H-001-CB-{}", func.line))
486 .title(checks::REENTRANCY.name)
487 .description(&format!(
488 "{} — Callback reentrancy: function '{}' uses safe mint/transfer patterns that invoke receiver callbacks, enabling reentrancy via ERC-777/721/1155 hooks",
489 checks::REENTRANCY.description, func.name
490 ))
491 .severity(Severity::High)
492 .file(file)
493 .location(func.line, 0)
494 .code(&format!("Function {} uses safe mint/transfer with callback hooks", func.name))
495 .recommendation("Apply the Checks-Effects-Interactions pattern: update state before _safeMint/_mint. Consider using _mint instead of _safeMint when safe, or add a nonReentrant modifier. For ERC-1155, ensure balances are set before _safeTransferFrom.")
496 .category("Logic")
497 .blocks_deployment(true)
498 .build()
499 );
500 continue;
501 }
502
503 if self.has_read_only_reentrancy(func) {
506 findings.push(
507 Finding::builder()
508 .id(&format!("FA-H-001-RO-{}", func.line))
509 .title(checks::REENTRANCY.name)
510 .description(&format!(
511 "{} — Read-only reentrancy: function '{}' makes external calls then reads state (balanceOf, totalSupply, etc.) that the re-entering call could have modified",
512 checks::REENTRANCY.description, func.name
513 ))
514 .severity(Severity::High)
515 .file(file)
516 .location(func.line, 0)
517 .code(&format!("Function {} reads manipulated state after external call", func.name))
518 .recommendation("Use nonReentrant modifier for functions that read state after external calls. Consider using a snapshot pattern to isolate pre-call state from post-call reads.")
519 .category("Logic")
520 .blocks_deployment(true)
521 .build()
522 );
523 continue;
524 }
525
526 if self.has_self_call_reentrancy(func) {
529 findings.push(
530 Finding::builder()
531 .id(&format!("FA-H-001-SC-{}", func.line))
532 .title(checks::REENTRANCY.name)
533 .description(&format!(
534 "{} — Self-call reentrancy path: function '{}' calls this.function() which could re-enter",
535 checks::REENTRANCY.description, func.name
536 ))
537 .severity(Severity::Medium)
538 .file(file)
539 .location(func.line, 0)
540 .code(&format!("Function {} contains this.function() call — possible reentrancy path", func.name))
541 .recommendation("Use nonReentrant modifier or restructure to avoid self-calls that could create reentrancy paths.")
542 .category("Logic")
543 .blocks_deployment(false)
544 .build()
545 );
546 continue;
547 }
548
549 if has_global_guard
551 && func
552 .body
553 .iter()
554 .any(|s| matches!(s.kind, crate::parser::StatementKind::ExternalCall))
555 {
556 if let Some(ext_call_stmt) = func
557 .body
558 .iter()
559 .find(|s| matches!(s.kind, crate::parser::StatementKind::ExternalCall))
560 {
561 findings.push(
562 Finding::builder()
563 .id(&format!("FA-H-001-MG-{}", func.line))
564 .title(checks::REENTRANCY.name)
565 .description(&format!(
566 "Function '{}' makes external calls (line {}) but lacks nonReentrant modifier, despite contract having ReentrancyGuard",
567 func.name, ext_call_stmt.line
568 ))
569 .severity(Severity::Medium)
570 .file(file)
571 .location(func.line, 0)
572 .code(&format!("Function {} makes external call at line {} without nonReentrant", func.name, ext_call_stmt.line))
573 .recommendation(checks::REENTRANCY.remediation)
574 .category("Logic")
575 .blocks_deployment(false)
576 .build()
577 );
578 continue;
579 }
580 }
581
582 let mut findings_from_cei = self.analyze_cei_pattern(func, file);
585 findings.append(&mut findings_from_cei);
586
587 if !findings.iter().any(|f| f.id.starts_with("FA-H-001")) {
589 if self.is_external_call_to_user_address(&func.body) {
591 findings.push(
592 Finding::builder()
593 .id(&format!("FA-H-001-UC-{}", func.line))
594 .title(checks::REENTRANCY.name)
595 .description(&format!(
596 "{} — External call to user-controlled address in function '{}'. User-supplied addresses are high risk for reentrancy attacks",
597 checks::REENTRANCY.description, func.name
598 ))
599 .severity(Severity::High)
600 .file(file)
601 .location(func.line, 0)
602 .code(&format!("Function {} calls external address from user-supplied input", func.name))
603 .recommendation("Use nonReentrant modifier for functions that make external calls to user-supplied addresses. Validate call targets against a whitelist when possible.")
604 .category("Logic")
605 .blocks_deployment(true)
606 .build()
607 );
608 }
609 }
610 }
611 }
612
613 findings
614 }
615
616 fn analyze_cei_pattern(&self, func: &crate::parser::FunctionDef, file: &str) -> Vec<Finding> {
618 let mut seen_external_call = false;
619 let mut first_ext_call_line = 0;
620 let mut cei_violations = Vec::new();
621
622 for stmt in &func.body {
623 match &stmt.kind {
624 crate::parser::StatementKind::ExternalCall => {
625 seen_external_call = true;
626 if first_ext_call_line == 0 {
627 first_ext_call_line = stmt.line;
628 }
629 }
630 crate::parser::StatementKind::StateWrite if seen_external_call => {
631 let is_guard_assignment = stmt.text.contains("_status")
634 || stmt.text.contains("_ENTERED")
635 || stmt.text.contains("_NOT_ENTERED")
636 || stmt.text.contains("locked");
637 if !is_guard_assignment {
638 cei_violations.push((stmt.line, stmt.text.clone()));
639 }
640 }
641 _ => {}
642 }
643 }
644
645 if !cei_violations.is_empty() {
646 let snippet = cei_violations
647 .iter()
648 .map(|(line, text)| format!("line {}: {}", line, text))
649 .collect::<Vec<_>>()
650 .join("; ");
651 vec![
652 Finding::builder()
653 .id(&format!("FA-H-001-{}", func.line))
654 .title(checks::REENTRANCY.name)
655 .description(&format!(
656 "{} — CEI violation: state modification after external call in function '{}' (first external call at line {})",
657 checks::REENTRANCY.description, func.name, first_ext_call_line
658 ))
659 .severity(Severity::High)
660 .file(file)
661 .location(func.line, 0)
662 .code(&snippet)
663 .recommendation(checks::REENTRANCY.remediation)
664 .category("Logic")
665 .blocks_deployment(true)
666 .build()
667 ]
668 } else {
669 Vec::new()
670 }
671 }
672
673 fn check_access_control(&self, file: &str, _lines: &[&str], content: &str) -> Vec<Finding> {
675 let source_file = crate::parser::parse_source(content);
677 let mut findings = Vec::new();
678
679 let sensitive_prefixes = [
681 "withdraw",
682 "transfer",
683 "mint",
684 "burn",
685 "set",
686 "update",
687 "initialize",
688 "upgradeTo",
689 "upgrade",
690 "pause",
691 "unpause",
692 "freeze",
693 "unfreeze",
694 "destroy",
695 "kill",
696 "recover",
697 "drain",
698 "swap",
699 "add",
700 "remove",
701 "grant",
702 "revoke",
703 "change",
704 "configure",
705 "deposit",
706 "stake",
707 "unstake",
708 "claim",
709 "collect",
710 "distribute",
711 "allocate",
712 "delegate",
713 "execute",
714 "send",
715 "approve",
716 "reset",
717 "toggle",
718 "blacklist",
719 "whitelist",
720 "ban",
721 "suspend",
722 "close",
723 "open",
724 "lock",
725 "unlock",
726 "renounce",
727 "rescue",
728 "emergency",
729 "shutdown",
730 "finalize",
731 "override",
732 ];
733
734 for contract in &source_file.contracts {
735 let inherits_ac = source_file.inheritance_includes_access_control(contract)
737 || contract.inherits_access_control();
738
739 let has_known_ac_modifiers = self.contract_has_ac_modifiers(contract);
741
742 for func in &contract.functions {
743 if func.visibility != crate::parser::Visibility::Public
745 && func.visibility != crate::parser::Visibility::External
746 {
747 continue;
748 }
749
750 if func.is_constructor || func.is_fallback || func.is_receive {
752 continue;
753 }
754
755 if func.mutability == crate::parser::Mutability::Pure
757 || func.mutability == crate::parser::Mutability::View
758 {
759 continue;
760 }
761
762 let is_sensitive = sensitive_prefixes
764 .iter()
765 .any(|prefix| func.name.to_lowercase().starts_with(prefix));
766
767 if !is_sensitive {
768 let has_value_transfer = func.body.iter().any(|s| {
771 s.text.contains(".transfer(")
772 || s.text.contains(".send(")
773 || s.text.contains(".call{value")
774 });
775 let writes_critical = func
777 .body
778 .iter()
779 .filter(|s| matches!(s.kind, crate::parser::StatementKind::StateWrite))
780 .count()
781 > 1;
782 if !has_value_transfer && !writes_critical {
783 continue;
784 }
785 }
786
787 let has_ac_modifier = func.has_access_control(contract);
789
790 let has_inline_ac = self.function_has_inline_access_control(func);
792
793 let has_role_ac = self.function_has_role_based_access(func);
795
796 if func.name.to_lowercase().starts_with("initialize") {
798 self.check_initializer_access(
799 func,
800 contract,
801 &source_file,
802 file,
803 &mut findings,
804 );
805 continue;
806 }
807
808 let has_any_ac = has_ac_modifier || has_inline_ac || has_role_ac;
810 let has_any_modifier = !func.modifiers.is_empty();
811
812 if !has_any_ac {
813 if inherits_ac && !has_any_modifier {
814 findings.push(self.make_finding(
816 &checks::ACCESS_CONTROL, file, func.line, &format!(
817 "Function '{}' is public/external but lacks any access control modifier (contract inherits {})",
818 func.name,
819 contract.inheritance.join(", ")
820 )
821 ));
822 } else if !inherits_ac && !has_any_modifier {
823 findings.push(self.make_finding(
825 &checks::ACCESS_CONTROL, file, func.line, &format!(
826 "Function '{}' is public/external with no access control modifier — any caller can execute this sensitive function",
827 func.name
828 )
829 ));
830 } else if has_any_modifier && !has_ac_modifier && !has_inline_ac {
831 let mods = func.modifiers.join(", ");
833 findings.push(self.make_finding(
834 &checks::ACCESS_CONTROL, file, func.line, &format!(
835 "Function '{}' has modifiers ({}) but none appear to be access control",
836 func.name, mods
837 )
838 ));
839 }
840 }
841 }
842
843 if has_known_ac_modifiers || inherits_ac {
845 self.check_missing_disable_initializers(contract, file, &mut findings);
846 }
847 }
848
849 findings
850 }
851
852 fn contract_has_ac_modifiers(&self, contract: &crate::parser::Contract) -> bool {
854 contract.modifiers_defs.iter().any(|m| {
856 let lower = m.name.to_lowercase();
857 lower.contains("only") || lower.contains("auth") || lower == "whennotpaused"
858 }) || contract.functions.iter().any(|f| {
859 f.modifiers.iter().any(|mod_name| {
860 let lower = mod_name.to_lowercase();
861 lower.contains("only") || lower.contains("role") || lower == "whennotpaused"
862 })
863 })
864 }
865
866 fn function_has_inline_access_control(&self, func: &crate::parser::FunctionDef) -> bool {
868 func.body.iter().any(|s| {
869 if matches!(s.kind, crate::parser::StatementKind::Guard) {
870 let text_lower = s.text.to_lowercase();
871 text_lower.contains("msg.sender == owner")
873 || text_lower.contains("msg.sender == ") && text_lower.contains("owner")
874 || text_lower.contains("msg.sender == addresses")
875 || text_lower.contains("hasrole(")
876 || text_lower.contains("onlyowner")
877 || text_lower.contains("_isowner")
878 || text_lower.contains("_authorized")
879 || text_lower.contains("isowner")
880 || text_lower.contains("authorized")
881 || text_lower.contains("isadmin")
882 || text_lower.contains("_isadmin")
883 || text_lower.contains("onlyrole")
884 || text_lower.contains("hasrole")
885 || text_lower.contains("allowed to")
886 || text_lower.contains("not authorized")
887 || text_lower.contains("unauthorized")
888 || text_lower.contains("caller not")
889 || text_lower.contains("_checkrole")
890 } else {
891 false
892 }
893 })
894 }
895
896 fn function_has_role_based_access(&self, func: &crate::parser::FunctionDef) -> bool {
898 func.body.iter().any(|s| {
900 s.text.contains("_checkRole(") || s.text.contains("_grantRole(")
901 || s.text.contains("_revokeRole(") || s.text.contains("onlyRole")
902 })
903 || func.modifiers.iter().any(|m| {
905 let lower = m.to_lowercase();
906 lower.contains("role") || lower == "onlyrole"
907 })
908 }
909
910 fn check_initializer_access(
912 &self,
913 func: &crate::parser::FunctionDef,
914 _contract: &crate::parser::Contract,
915 _source_file: &crate::parser::SourceFile,
916 file: &str,
917 findings: &mut Vec<Finding>,
918 ) {
919 let body = &func.body;
920 let has_initializer_modifier = func.modifiers.iter().any(|m| {
921 let lower = m.to_lowercase();
922 lower == "initializer" || lower == "reinitializer" || lower.contains("initializer")
923 });
924
925 let calls_ownable_init = body.iter().any(|s| {
927 s.text.contains("__Ownable_init(") || s.text.contains("__Ownable_init_unchain(")
928 });
929 let calls_accesscontrol_init = body.iter().any(|s| {
930 s.text.contains("__AccessControl_init(")
931 || s.text.contains("__AccessControl_init_unchain(")
932 });
933 let calls_reentrancyguard_init = body
934 .iter()
935 .any(|s| s.text.contains("__ReentrancyGuard_init("));
936
937 if !has_initializer_modifier {
938 let ac_methods = [
939 (calls_ownable_init, "__Ownable_init"),
940 (calls_accesscontrol_init, "__AccessControl_init"),
941 (calls_reentrancyguard_init, "__ReentrancyGuard_init"),
942 ];
943 let init_calls: Vec<&str> = ac_methods
944 .iter()
945 .filter(|(called, _)| *called)
946 .map(|(_, name)| *name)
947 .collect();
948 if !init_calls.is_empty() {
949 findings.push(self.make_finding(
950 &checks::ACCESS_CONTROL,
951 file,
952 func.line,
953 &format!(
954 "Initialize function '{}' calls {} but lacks the 'initializer' modifier — can be front-run",
955 func.name,
956 init_calls.join(", ")
957 )
958 ));
959 }
960 }
961
962 let assigns_owner = body.iter().any(|s| {
964 let lower = s.text.to_lowercase();
965 (lower.contains("owner") || lower.contains("admin"))
966 && (s.text.contains('=') || s.text.contains(" := "))
967 && !matches!(s.kind, crate::parser::StatementKind::Guard)
968 });
969
970 if assigns_owner && !has_initializer_modifier {
971 findings.push(self.make_finding(
972 &checks::ACCESS_CONTROL,
973 file,
974 func.line,
975 &format!(
976 "Function '{}' assigns owner/admin directly but lacks 'initializer' modifier — unprotected",
977 func.name
978 )
979 ));
980 }
981 }
982
983 fn check_missing_disable_initializers(
985 &self,
986 contract: &crate::parser::Contract,
987 file: &str,
988 findings: &mut Vec<Finding>,
989 ) {
990 let has_constructor = contract.functions.iter().any(|f| f.is_constructor);
991 let calls_disable = contract.functions.iter().any(|f| {
992 f.body
993 .iter()
994 .any(|s| s.text.contains("disableInitializers"))
995 });
996 let is_upgradeable = contract.inheritance.iter().any(|i| {
997 let l = i.to_lowercase();
998 l.contains("initializable")
999 || l.contains("uups")
1000 || l.contains("transparentupgradeable")
1001 });
1002
1003 if is_upgradeable && has_constructor && !calls_disable {
1004 findings.push(self.make_finding(
1005 &checks::ACCESS_CONTROL,
1006 file,
1007 contract.line,
1008 &format!(
1009 "Implementation contract '{}' is upgradeable but its constructor does not call _disableInitializers(), leaving it vulnerable to selfdestruct",
1010 contract.name
1011 )
1012 ));
1013 }
1014 }
1015
1016 fn check_delegatecall(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1018 self.find_lines_containing(lines, &["delegatecall(", ".delegatecall("])
1019 .into_iter()
1020 .map(|(line, snippet)| self.make_finding(&checks::DELEGATECALL, file, line, snippet))
1021 .collect()
1022 }
1023
1024 fn check_tx_origin(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1026 self.find_lines_containing(lines, &["tx.origin"])
1027 .into_iter()
1028 .map(|(line, snippet)| self.make_finding(&checks::TX_ORIGIN, file, line, snippet))
1029 .collect()
1030 }
1031
1032 fn check_create2(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1034 self.find_lines_containing(lines, &["CREATE2", "create2(", ".create2("])
1035 .into_iter()
1036 .map(|(line, snippet)| self.make_finding(&checks::CREATE2, file, line, snippet))
1037 .collect()
1038 }
1039
1040 fn check_dos(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1042 self.find_lines_containing(lines, &["for (uint", "for (uint256", "while ("])
1043 .into_iter()
1044 .filter(|(_, snippet)| snippet.contains(".length"))
1045 .map(|(line, snippet)| self.make_finding(&checks::DOS, file, line, snippet))
1046 .collect()
1047 }
1048
1049 fn check_storage_collision(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1051 let has_storage_gap = lines
1053 .iter()
1054 .any(|l| l.contains("__gap") || l.contains("_gap"));
1055 let is_contract = lines.iter().any(|l| {
1056 l.contains("contract ")
1057 && l.contains("is ")
1058 && (l.contains("UUPS") || l.contains("Transparent") || l.contains("Beacon"))
1059 });
1060
1061 if is_contract && !has_storage_gap {
1062 vec![self.make_finding(
1063 &checks::STORAGE_COLLISION,
1064 file,
1065 1,
1066 "Upgradeable contract without storage gap",
1067 )]
1068 } else {
1069 Vec::new()
1070 }
1071 }
1072
1073 fn check_unsafe_assembly(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1075 self.find_lines_containing(lines, &["assembly {"])
1076 .into_iter()
1077 .map(|(line, snippet)| self.make_finding(&checks::UNSAFE_ASSEMBLY, file, line, snippet))
1078 .collect()
1079 }
1080
1081 fn check_selfdestruct(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1083 self.find_lines_containing(lines, &["selfdestruct(", "selfdestruct ("])
1084 .into_iter()
1085 .map(|(line, snippet)| self.make_finding(&checks::SELFDESTRUCT, file, line, snippet))
1086 .collect()
1087 }
1088
1089 fn check_proxy_vulnerabilities(
1091 &self,
1092 file: &str,
1093 lines: &[&str],
1094 _content: &str,
1095 ) -> Vec<Finding> {
1096 let mut findings = Vec::new();
1097 let has_initialize = lines.iter().any(|l| l.contains("function initialize"));
1099 let has_initializer_modifier = lines.iter().any(|l| l.contains("initializer"));
1100 let is_implementation = lines.iter().any(|l| {
1101 l.contains("is UUPSUpgradeable") || l.contains("is TransparentUpgradeableProxy")
1102 });
1103
1104 if is_implementation && has_initialize && !has_initializer_modifier {
1105 findings.push(self.make_finding(
1106 &checks::PROXY_VULNERABILITIES,
1107 file,
1108 1,
1109 "Implementation contract has initialize() without initializer modifier",
1110 ));
1111 }
1112
1113 if is_implementation {
1115 let has_disable = lines.iter().any(|l| l.contains("disableInitializers"));
1116 let has_constructor = lines.iter().any(|l| l.contains("constructor("));
1117 if has_constructor && !has_disable {
1118 findings.push(self.make_finding(
1119 &checks::PROXY_VULNERABILITIES,
1120 file,
1121 1,
1122 "Implementation contract constructor does not call disableInitializers()",
1123 ));
1124 }
1125 }
1126
1127 findings
1128 }
1129
1130 fn check_oracle_manipulation(
1132 &self,
1133 file: &str,
1134 lines: &[&str],
1135 _content: &str,
1136 ) -> Vec<Finding> {
1137 let has_chainlink = lines
1138 .iter()
1139 .any(|l| l.contains("Chainlink") || l.contains("AggregatorV3Interface"));
1140 let has_price_reference = lines.iter().any(|l| {
1141 l.contains("price") || l.contains("oracle") || l.contains("twap") || l.contains("TWAP")
1142 });
1143
1144 if has_price_reference
1145 && !has_chainlink
1146 && !lines
1147 .iter()
1148 .any(|l| l.contains("//") && l.contains("oracle"))
1149 {
1150 self.find_lines_containing(lines, &["price(", ".price(", "getPrice", "getLatestPrice"])
1151 .into_iter()
1152 .map(|(line, snippet)| {
1153 self.make_finding(&checks::ORACLE_MANIPULATION, file, line, snippet)
1154 })
1155 .collect()
1156 } else {
1157 Vec::new()
1158 }
1159 }
1160
1161 fn check_signature_vulnerabilities(
1163 &self,
1164 file: &str,
1165 lines: &[&str],
1166 _content: &str,
1167 ) -> Vec<Finding> {
1168 self.find_lines_containing(lines, &["ecrecover(", "ECDSA", "SignatureChecker"])
1169 .into_iter()
1170 .map(|(line, snippet)| {
1171 self.make_finding(&checks::SIGNATURE_VULNERABILITIES, file, line, snippet)
1172 })
1173 .collect()
1174 }
1175
1176 fn check_replay_attacks(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1178 let has_nonce = lines
1179 .iter()
1180 .any(|l| l.contains("nonce") || l.contains("Nonce"));
1181 let has_signing = lines
1182 .iter()
1183 .any(|l| l.contains("signature") || l.contains("ecrecover"));
1184
1185 if has_signing && !has_nonce {
1186 self.find_lines_containing(lines, &["ecrecover(", "ECDSA"])
1187 .into_iter()
1188 .map(|(line, snippet)| {
1189 self.make_finding(&checks::REPLAY_ATTACKS, file, line, snippet)
1190 })
1191 .collect()
1192 } else {
1193 Vec::new()
1194 }
1195 }
1196
1197 fn check_erc20_issues(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1199 let is_erc20 = lines
1200 .iter()
1201 .any(|l| l.contains("is IERC20") || l.contains("ERC20"));
1202 if !is_erc20 {
1203 return Vec::new();
1204 }
1205
1206 let mut findings = Vec::new();
1207
1208 let has_approve = lines.iter().any(|l| l.contains("approve("));
1210 let has_safe_approve = lines
1211 .iter()
1212 .any(|l| l.contains("increaseAllowance") || l.contains("decreaseAllowance"));
1213 if has_approve && !has_safe_approve {
1214 findings.push(self.make_finding(
1215 &checks::ERC20_ISSUES,
1216 file,
1217 1,
1218 "ERC20 approve() used without safe increaseAllowance/decreaseAllowance pattern",
1219 ));
1220 }
1221
1222 let has_init_mint = lines.iter().any(|l| l.contains("_mint("));
1224 if has_init_mint {
1225 findings.push(self.make_finding(
1226 &checks::ERC20_ISSUES,
1227 file,
1228 1,
1229 "ERC20 tokens minted — verify initialization and distribution",
1230 ));
1231 }
1232
1233 findings
1234 }
1235
1236 fn check_bridge_vulnerabilities(
1238 &self,
1239 file: &str,
1240 lines: &[&str],
1241 _content: &str,
1242 ) -> Vec<Finding> {
1243 let bridge_keywords = [
1244 "bridge",
1245 "relayer",
1246 "validator",
1247 "crossChain",
1248 "cross-chain",
1249 "message",
1250 "relay",
1251 ];
1252 let has_bridge = lines
1253 .iter()
1254 .any(|l| bridge_keywords.iter().any(|k| l.contains(k)));
1255
1256 if has_bridge {
1257 vec![self.make_finding(
1258 &checks::BRIDGE_VULNERABILITIES,
1259 file,
1260 1,
1261 "Bridge/relayer pattern detected — review validator set and message signing",
1262 )]
1263 } else {
1264 Vec::new()
1265 }
1266 }
1267
1268 fn check_flash_loan_issues(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1270 let flash_keywords = [
1271 "flashLoan",
1272 "flashloan",
1273 "flash_loan",
1274 "onFlashLoan",
1275 "IFlashLoan",
1276 ];
1277 let has_flash = lines
1278 .iter()
1279 .any(|l| flash_keywords.iter().any(|k| l.contains(k)));
1280
1281 if has_flash {
1282 vec![self.make_finding(
1283 &checks::FLASH_LOAN_ISSUES,
1284 file,
1285 1,
1286 "Flash loan pattern detected — verify price and balance checks",
1287 )]
1288 } else {
1289 Vec::new()
1290 }
1291 }
1292
1293 fn check_mev_issues(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1295 let has_swap = lines
1296 .iter()
1297 .any(|l| l.contains("swap") || l.contains("Swap"));
1298 let has_slippage = lines.iter().any(|l| {
1299 l.contains("slippage")
1300 || l.contains("minAmount")
1301 || l.contains("minReturn")
1302 || l.contains("amountOutMin")
1303 });
1304
1305 if has_swap && !has_slippage {
1306 self.find_lines_containing(lines, &[".swap", "swap("])
1307 .into_iter()
1308 .map(|(line, snippet)| self.make_finding(&checks::MEV_ISSUES, file, line, snippet))
1309 .collect()
1310 } else {
1311 Vec::new()
1312 }
1313 }
1314
1315 fn check_cross_chain_issues(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1317 let cross_chain_keywords = [
1318 "crossChain",
1319 "cross-chain",
1320 "LZ",
1321 "LayerZero",
1322 "CCIP",
1323 "Wormhole",
1324 "Hyperlane",
1325 ];
1326 let has_cross_chain = lines
1327 .iter()
1328 .any(|l| cross_chain_keywords.iter().any(|k| l.contains(k)));
1329
1330 if has_cross_chain {
1331 vec![self.make_finding(&checks::CROSS_CHAIN_ISSUES, file, 1,
1332 "Cross-chain interaction detected — verify chain ID handling and message verification")]
1333 } else {
1334 Vec::new()
1335 }
1336 }
1337
1338 fn check_dependency_vulnerabilities(
1340 &self,
1341 file: &str,
1342 lines: &[&str],
1343 _content: &str,
1344 ) -> Vec<Finding> {
1345 let mut findings = Vec::new();
1347 if let Some(_line) = lines
1348 .iter()
1349 .find(|l| l.contains("import") && l.contains("../"))
1350 {
1351 findings.push(self.make_finding(
1352 &checks::DEPENDENCY_VULNERABILITIES,
1353 file,
1354 1,
1355 "Relative import detected — verify package version",
1356 ));
1357 }
1358 findings
1359 }
1360
1361 fn check_unsafe_imports(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1363 self.find_lines_containing(lines, &["import "])
1364 .into_iter()
1365 .filter(|(_, snippet)| {
1366 snippet.contains("http")
1367 || snippet.contains("github.com/")
1368 && !snippet.contains("OpenZeppelin")
1369 && !snippet.contains("solmate")
1370 && !snippet.contains("forge-std")
1371 })
1372 .map(|(line, snippet)| self.make_finding(&checks::UNSAFE_IMPORTS, file, line, snippet))
1373 .collect()
1374 }
1375
1376 fn check_unsafe_initializers(
1378 &self,
1379 file: &str,
1380 lines: &[&str],
1381 _content: &str,
1382 ) -> Vec<Finding> {
1383 let has_initializer = lines
1384 .iter()
1385 .any(|l| l.contains("initializer") || l.contains("reinitializer"));
1386 let has_constructor = lines.iter().any(|l| l.contains("constructor("));
1387
1388 if has_initializer && !has_constructor {
1389 Vec::new()
1391 } else if has_initializer && has_constructor {
1392 let has_disable = lines.iter().any(|l| l.contains("disableInitializers"));
1393 if !has_disable {
1394 vec![self.make_finding(
1395 &checks::UNSAFE_INITIALIZERS,
1396 file,
1397 1,
1398 "Contract has initializer and constructor but no disableInitializers() call",
1399 )]
1400 } else {
1401 Vec::new()
1402 }
1403 } else {
1404 Vec::new()
1405 }
1406 }
1407
1408 fn check_unsafe_upgrade_paths(
1410 &self,
1411 file: &str,
1412 lines: &[&str],
1413 _content: &str,
1414 ) -> Vec<Finding> {
1415 let is_upgradeable = lines.iter().any(|l| {
1416 l.contains("is UUPSUpgradeable")
1417 || l.contains("is TransparentUpgradeableProxy")
1418 || l.contains("upgradeTo")
1419 });
1420 if is_upgradeable {
1421 vec![self.make_finding(
1422 &checks::UNSAFE_UPGRADE_PATHS,
1423 file,
1424 1,
1425 "Upgradeable contract detected — verify upgrade path security",
1426 )]
1427 } else {
1428 Vec::new()
1429 }
1430 }
1431
1432 fn check_clone_vulnerabilities(
1434 &self,
1435 file: &str,
1436 lines: &[&str],
1437 _content: &str,
1438 ) -> Vec<Finding> {
1439 self.find_lines_containing(lines, &["Clones.", "clone(", "ERC1167"])
1440 .into_iter()
1441 .map(|(line, snippet)| {
1442 self.make_finding(&checks::CLONE_VULNERABILITIES, file, line, snippet)
1443 })
1444 .collect()
1445 }
1446
1447 fn check_gas_problems(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1449 let mut findings = Vec::new();
1450 for (i, line) in lines.iter().enumerate() {
1452 let trimmed = line.trim();
1453 if trimmed.contains("for (uint")
1454 && trimmed.contains("i < ")
1455 && !trimmed.contains("memory ")
1456 && !trimmed.contains(".length")
1457 {
1458 findings.push(self.make_finding(&checks::GAS_PROBLEMS, file, i + 1, trimmed));
1460 }
1461 }
1462 findings
1463 }
1464
1465 fn check_unsafe_casting(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1467 self.find_lines_containing(lines, &["uint256(", "int256(", "address("])
1468 .into_iter()
1469 .filter(|(_, s)| {
1470 s.contains("uint8(")
1471 || s.contains("uint16(")
1472 || s.contains("uint32(")
1473 || s.contains("uint64(")
1474 || s.contains("uint128(")
1475 || s.contains("int8(")
1476 || s.contains("int16(")
1477 })
1478 .map(|(line, snippet)| self.make_finding(&checks::UNSAFE_CASTING, file, line, snippet))
1479 .collect()
1480 }
1481
1482 fn check_timestamp_manipulation(
1484 &self,
1485 file: &str,
1486 lines: &[&str],
1487 _content: &str,
1488 ) -> Vec<Finding> {
1489 let mut findings = Vec::new();
1490 for (i, line) in lines.iter().enumerate() {
1491 if !line.trim_start().starts_with("//")
1492 && (line.contains("block.timestamp") || line.contains("now "))
1493 {
1494 if line.contains("if ")
1496 || line.contains("require")
1497 || line.contains("==")
1498 || line.contains(">=")
1499 || line.contains("<=")
1500 {
1501 findings.push(self.make_finding(
1502 &checks::TIMESTAMP_MANIPULATION,
1503 file,
1504 i + 1,
1505 line.trim(),
1506 ));
1507 }
1508 }
1509 }
1510 findings
1511 }
1512
1513 fn check_storage_inefficiencies(
1515 &self,
1516 file: &str,
1517 lines: &[&str],
1518 _content: &str,
1519 ) -> Vec<Finding> {
1520 self.find_lines_containing(
1522 lines,
1523 &["uint256 public ", "uint256 internal ", "uint256 private "],
1524 )
1525 .into_iter()
1526 .filter(|(_, s)| !s.contains("address") && !s.contains("mapping") && s.len() < 100)
1527 .take(3)
1528 .map(|(line, snippet)| {
1529 self.make_finding(&checks::STORAGE_INEFFICIENCIES, file, line, snippet)
1530 })
1531 .collect()
1532 }
1533
1534 fn check_unsafe_events(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1536 self.find_lines_containing(lines, &["event ", "emit "])
1537 .into_iter()
1538 .filter(|(_, s)| {
1539 s.contains("password")
1540 || s.contains("secret")
1541 || s.contains("key")
1542 || s.contains("privateKey")
1543 || s.contains("mnemonic")
1544 })
1545 .map(|(line, snippet)| self.make_finding(&checks::UNSAFE_EVENTS, file, line, snippet))
1546 .collect()
1547 }
1548
1549 fn check_poor_visibility(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1551 self.find_lines_containing(lines, &["public "])
1553 .into_iter()
1554 .filter(|(_, s)| s.contains("mapping(") && s.contains("public"))
1555 .map(|(line, snippet)| self.make_finding(&checks::POOR_VISIBILITY, file, line, snippet))
1556 .collect()
1557 }
1558
1559 fn check_bad_modifiers(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1561 self.find_lines_containing(lines, &["modifier "])
1563 .into_iter()
1564 .filter(|(_, s)| {
1565 s.contains(".call")
1566 || s.contains(".transfer")
1567 || s.contains(".send")
1568 || s.contains("delegatecall")
1569 })
1570 .map(|(line, snippet)| self.make_finding(&checks::BAD_MODIFIERS, file, line, snippet))
1571 .collect()
1572 }
1573
1574 fn check_unsafe_math(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1576 let mut in_unchecked = false;
1578 let mut findings = Vec::new();
1579
1580 for (i, line) in lines.iter().enumerate() {
1581 let trimmed = line.trim();
1582 if trimmed.contains("unchecked {") || trimmed.contains("unchecked{") {
1583 in_unchecked = true;
1584 continue;
1585 }
1586 if trimmed.starts_with('}') && in_unchecked {
1587 in_unchecked = false;
1588 continue;
1589 }
1590 if in_unchecked
1591 && (trimmed.contains("++")
1592 || trimmed.contains("--")
1593 || trimmed.contains("+=")
1594 || trimmed.contains("-=")
1595 || trimmed.contains("*="))
1596 {
1597 findings.push(self.make_finding(&checks::UNSAFE_MATH, file, i + 1, trimmed));
1598 }
1599 }
1600 findings
1601 }
1602
1603 fn check_poor_access_patterns(
1605 &self,
1606 file: &str,
1607 lines: &[&str],
1608 _content: &str,
1609 ) -> Vec<Finding> {
1610 self.find_lines_containing(lines, &["storage ", "storage)"])
1611 .into_iter()
1612 .filter(|(_, s)| {
1613 !s.contains("memory") && (s.contains("memory") || s.contains("calldata"))
1614 })
1615 .take(3)
1616 .map(|(line, snippet)| {
1617 self.make_finding(&checks::POOR_ACCESS_PATTERNS, file, line, snippet)
1618 })
1619 .collect()
1620 }
1621
1622 fn check_naming_issues(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1624 let mut findings = Vec::new();
1625 for (i, line) in lines.iter().enumerate() {
1626 let trimmed = line.trim();
1627 if trimmed.starts_with("function ") && !trimmed.contains("(") {
1629 let name = trimmed.split_whitespace().nth(1).unwrap_or("");
1630 if name.contains('_') {
1631 findings.push(self.make_finding(&checks::NAMING_ISSUES, file, i + 1, trimmed));
1632 if findings.len() >= 3 {
1633 break;
1634 }
1635 }
1636 }
1637 }
1638 findings
1639 }
1640
1641 fn check_code_duplication(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1643 let mut findings = Vec::new();
1645 if lines.len() > 50 {
1646 let total_lines = lines.len();
1647 let unique_lines: std::collections::HashSet<&&str> = lines.iter().collect();
1648 let ratio = unique_lines.len() as f64 / total_lines as f64;
1649 if ratio < 0.5 {
1650 findings.push(self.make_finding(
1651 &checks::CODE_DUPLICATION,
1652 file,
1653 1,
1654 &format!(
1655 "Low code diversity ({:.0}% unique lines) — possible duplication",
1656 ratio * 100.0
1657 ),
1658 ));
1659 }
1660 }
1661 findings
1662 }
1663
1664 fn check_optimization(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1666 let mut findings = Vec::new();
1667 for (i, line) in lines.iter().enumerate() {
1668 let trimmed = line.trim();
1669 if trimmed.contains("memory ")
1671 && !trimmed.contains("internal")
1672 && !trimmed.starts_with("//")
1673 {
1674 findings.push(self.make_finding(
1675 &checks::OPTIMIZATION_ISSUES,
1676 file,
1677 i + 1,
1678 trimmed,
1679 ));
1680 if findings.len() >= 2 {
1681 break;
1682 }
1683 }
1684 }
1685 findings
1686 }
1687
1688 fn check_style_issues(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1690 let mut findings = Vec::new();
1691 for (i, line) in lines.iter().enumerate() {
1692 if line.len() > 120 {
1693 findings.push(self.make_finding(
1694 &checks::STYLE_ISSUES,
1695 file,
1696 i + 1,
1697 "Line exceeds 120 characters",
1698 ));
1699 if findings.len() >= 2 {
1700 break;
1701 }
1702 }
1703 }
1704 findings
1705 }
1706
1707 fn check_documentation(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1709 let mut findings = Vec::new();
1710 let mut has_natspec = false;
1711
1712 for (i, line) in lines.iter().enumerate() {
1713 let trimmed = line.trim();
1714
1715 if trimmed.starts_with("/**") {
1716 has_natspec = true;
1717 continue;
1718 }
1719
1720 if trimmed.starts_with("function ") {
1721 if !has_natspec && i > 0 && !lines[i - 1].trim().starts_with("//") {
1722 findings.push(self.make_finding(
1723 &checks::DOCUMENTATION_ISSUES,
1724 file,
1725 i + 1,
1726 trimmed,
1727 ));
1728 if findings.len() >= 3 {
1729 break;
1730 }
1731 }
1732 has_natspec = false;
1733 }
1734
1735 if trimmed.ends_with("*/") || trimmed.starts_with("///") || trimmed.starts_with("* ") {
1736 has_natspec = true;
1737 }
1738 }
1739 findings
1740 }
1741}