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