fhevm_forge/linter/rules/
missing_allow_addr.rs1use regex::Regex;
7use std::{path::Path, sync::OnceLock};
8use super::{LintError, LintRule, Severity, make_error};
9
10static EXTERNAL_CALL_RE: OnceLock<Regex> = OnceLock::new();
11static ALLOW_RE: OnceLock<Regex> = OnceLock::new();
12
13pub struct MissingAllowAddr;
14
15impl LintRule for MissingAllowAddr {
16 fn id(&self) -> &'static str { "FHEVM-002" }
17
18 fn check(&self, file_path: &Path, source: &str) -> Vec<LintError> {
19 let ext = file_path.extension().and_then(|e| e.to_str()).unwrap_or("");
20 if ext != "sol" { return vec![]; }
21
22 let external_re = EXTERNAL_CALL_RE.get_or_init(|| {
25 Regex::new(r"\b(\w+)\.(mint|transfer|deposit|withdraw|set|update|add|burn)\s*\([^)]*\b(euint|ebool)\b").unwrap()
26 });
27 let allow_re = ALLOW_RE.get_or_init(|| {
28 Regex::new(r"TFHE\.allow\(").unwrap()
29 });
30
31 let lines: Vec<&str> = source.lines().collect();
32 let mut errors = Vec::new();
33
34 for (i, line) in lines.iter().enumerate() {
35 if external_re.is_match(line) {
36 let window_start = i.saturating_sub(5);
38 let window = &lines[window_start..=i];
39 let has_allow = window.iter().any(|l| allow_re.is_match(l));
40
41 if !has_allow {
42 errors.push(make_error(
43 "FHEVM-002",
44 Severity::Error,
45 file_path,
46 i + 1,
47 "euint handle passed to external contract without TFHE.allow(). \
48 Call TFHE.allow(handle, address(externalContract)) before the external call.",
49 Some(line),
50 ));
51 }
52 }
53 }
54
55 errors
56 }
57}