Skip to main content

forge_lint/sol/low/
missing_events_access_control.rs

1use super::MissingEventsAccessControl;
2use crate::{
3    linter::{LateLintPass, LintContext},
4    sol::{
5        Severity, SolLint,
6        analysis::primitives::{branch_always_exits, is_require_or_assert},
7    },
8};
9use solar::{
10    ast::{ContractKind, DataLocation, LitKind, StateMutability, Visibility},
11    interface::{Span, kw, sym},
12    sema::hir::{self, EventId, ExprKind, FunctionId, ItemId, Res, StmtKind, VariableId},
13};
14use std::collections::{HashMap, HashSet};
15
16declare_forge_lint!(
17    MISSING_EVENTS_ACCESS_CONTROL,
18    Severity::Low,
19    "missing-events-access-control",
20    "access control changes should emit events"
21);
22
23impl<'hir> LateLintPass<'hir> for MissingEventsAccessControl {
24    fn check_contract(
25        &mut self,
26        ctx: &LintContext,
27        _gcx: solar::sema::Gcx<'hir>,
28        hir: &'hir hir::Hir<'hir>,
29        contract: &'hir hir::Contract<'hir>,
30    ) {
31        if !matches!(contract.kind, ContractKind::Contract | ContractKind::AbstractContract) {
32            return;
33        }
34
35        let access_control_vars = access_control_state_vars(hir, contract);
36        if access_control_vars.is_empty() {
37            return;
38        }
39
40        for func_id in contract.all_functions() {
41            let func = hir.function(func_id);
42            if !is_protected_entry_point(hir, func_id, func) {
43                continue;
44            }
45
46            let guard_vars = entry_point_access_guard_vars(hir, func_id, func);
47            let mut analyzer = WriteAnalyzer::new(hir, &access_control_vars, &guard_vars);
48            let writes = analyzer.analyze_entry_point(func_id);
49            let mut emitted = HashSet::new();
50
51            for write in writes {
52                if write.evented {
53                    continue;
54                }
55
56                if !emitted.insert(write.var_id) {
57                    continue;
58                }
59
60                let name = hir
61                    .variable(write.var_id)
62                    .name
63                    .map(|name| name.as_str().to_string())
64                    .unwrap_or_else(|| "state variable".to_string());
65                ctx.emit_with_msg(
66                    &MISSING_EVENTS_ACCESS_CONTROL,
67                    write.span,
68                    format!("`{name}` is changed without an event but is used for access control"),
69                );
70            }
71        }
72    }
73}
74
75fn is_external_function(func: &hir::Function<'_>) -> bool {
76    func.kind.is_function()
77        && matches!(func.visibility, Visibility::Public | Visibility::External)
78        && !func.is_constructor()
79        && !func.is_special()
80}
81
82fn is_state_mutating_entry_point(func: &hir::Function<'_>) -> bool {
83    is_external_function(func)
84        && !matches!(func.state_mutability, StateMutability::Pure | StateMutability::View)
85}
86
87fn is_protected_entry_point(
88    hir: &hir::Hir<'_>,
89    func_id: FunctionId,
90    func: &hir::Function<'_>,
91) -> bool {
92    is_state_mutating_entry_point(func) && is_protected(hir, func_id, func)
93}
94
95fn access_control_state_vars(
96    hir: &hir::Hir<'_>,
97    contract: &hir::Contract<'_>,
98) -> HashSet<VariableId> {
99    let mut out = HashSet::new();
100
101    for func_id in contract.all_functions() {
102        let func = hir.function(func_id);
103        for modifier in func.modifiers {
104            if let Some(modifier_id) = modifier.id.as_function() {
105                collect_access_control_state_vars_in_function(
106                    hir,
107                    modifier_id,
108                    &mut HashSet::new(),
109                    &mut HashSet::new(),
110                    &mut out,
111                    true,
112                );
113            }
114        }
115
116        collect_access_control_state_vars_in_function(
117            hir,
118            func_id,
119            &mut HashSet::new(),
120            &mut HashSet::new(),
121            &mut out,
122            false,
123        );
124    }
125
126    out.retain(|var_id| {
127        let var = hir.variable(*var_id);
128        var.kind.is_state() && !var.is_constant() && !var.is_immutable()
129    });
130    out
131}
132
133fn entry_point_access_guard_vars(
134    hir: &hir::Hir<'_>,
135    func_id: FunctionId,
136    func: &hir::Function<'_>,
137) -> HashSet<VariableId> {
138    let mut out = HashSet::new();
139
140    for modifier in func.modifiers {
141        if let Some(modifier_id) = modifier.id.as_function() {
142            collect_access_control_state_vars_in_function(
143                hir,
144                modifier_id,
145                &mut HashSet::new(),
146                &mut HashSet::new(),
147                &mut out,
148                true,
149            );
150        }
151    }
152
153    collect_access_control_state_vars_in_function(
154        hir,
155        func_id,
156        &mut HashSet::new(),
157        &mut HashSet::new(),
158        &mut out,
159        false,
160    );
161    out
162}
163
164fn collect_access_control_state_vars_in_function(
165    hir: &hir::Hir<'_>,
166    func_id: FunctionId,
167    seen: &mut HashSet<FunctionId>,
168    sender_aliases: &mut HashSet<VariableId>,
169    out: &mut HashSet<VariableId>,
170    stop_at_placeholder: bool,
171) {
172    if !seen.insert(func_id) {
173        return;
174    }
175
176    let func = hir.function(func_id);
177    let Some(body) = func.body else { return };
178
179    for stmt in body.stmts {
180        if stop_at_placeholder && matches!(stmt.kind, StmtKind::Placeholder) {
181            break;
182        }
183        collect_access_control_state_vars_in_stmt(hir, stmt, seen, sender_aliases, out);
184    }
185}
186
187fn collect_access_control_state_vars_in_stmt(
188    hir: &hir::Hir<'_>,
189    stmt: &hir::Stmt<'_>,
190    seen: &mut HashSet<FunctionId>,
191    sender_aliases: &mut HashSet<VariableId>,
192    out: &mut HashSet<VariableId>,
193) {
194    match stmt.kind {
195        StmtKind::If(cond, then_stmt, else_stmt) => {
196            if expr_looks_like_access_check(hir, cond, sender_aliases)
197                && (stmt_exits_or_reverts(then_stmt)
198                    || else_stmt.is_some_and(stmt_exits_or_reverts))
199            {
200                collect_access_check_state_vars(hir, cond, seen, out);
201            }
202            let mut then_aliases = sender_aliases.clone();
203            collect_access_control_state_vars_in_stmt(hir, then_stmt, seen, &mut then_aliases, out);
204            if let Some(else_stmt) = else_stmt {
205                let mut else_aliases = sender_aliases.clone();
206                collect_access_control_state_vars_in_stmt(
207                    hir,
208                    else_stmt,
209                    seen,
210                    &mut else_aliases,
211                    out,
212                );
213            }
214        }
215        StmtKind::Expr(expr) => {
216            collect_access_control_state_vars_in_expr(hir, expr, seen, sender_aliases, out);
217            update_sender_aliases_from_assignment(hir, expr, seen, sender_aliases);
218        }
219        StmtKind::Block(block) | StmtKind::UncheckedBlock(block) | StmtKind::Loop(block, _) => {
220            for stmt in block.stmts {
221                collect_access_control_state_vars_in_stmt(hir, stmt, seen, sender_aliases, out);
222            }
223        }
224        StmtKind::Try(try_stmt) => {
225            collect_access_control_state_vars_in_expr(
226                hir,
227                &try_stmt.expr,
228                seen,
229                sender_aliases,
230                out,
231            );
232            for clause in try_stmt.clauses {
233                for stmt in clause.block.stmts {
234                    collect_access_control_state_vars_in_stmt(hir, stmt, seen, sender_aliases, out);
235                }
236            }
237        }
238        StmtKind::Return(Some(expr)) | StmtKind::Emit(expr) | StmtKind::Revert(expr) => {
239            collect_access_control_state_vars_in_expr(hir, expr, seen, sender_aliases, out);
240        }
241        StmtKind::DeclSingle(var_id) => {
242            if let Some(init) = hir.variable(var_id).initializer {
243                collect_access_control_state_vars_in_expr(hir, init, seen, sender_aliases, out);
244            }
245            update_sender_alias_from_decl(hir, var_id, seen, sender_aliases);
246        }
247        StmtKind::DeclMulti(_, expr) => {
248            collect_access_control_state_vars_in_expr(hir, expr, seen, sender_aliases, out);
249        }
250        StmtKind::Return(None)
251        | StmtKind::Break
252        | StmtKind::Continue
253        | StmtKind::Placeholder
254        | StmtKind::AssemblyBlock(_)
255        | StmtKind::Switch(_)
256        | StmtKind::Err(_) => {}
257    }
258}
259
260fn collect_access_control_state_vars_in_expr(
261    hir: &hir::Hir<'_>,
262    expr: &hir::Expr<'_>,
263    seen: &mut HashSet<FunctionId>,
264    sender_aliases: &HashSet<VariableId>,
265    out: &mut HashSet<VariableId>,
266) {
267    match &expr.peel_parens().kind {
268        ExprKind::Call(callee, args, opts) if is_require_or_assert(callee) => {
269            if let Some(cond) = args.exprs().next()
270                && expr_looks_like_access_check(hir, cond, sender_aliases)
271            {
272                collect_access_check_state_vars(hir, cond, seen, out);
273            }
274            for arg in args.exprs() {
275                collect_access_control_state_vars_in_expr(hir, arg, seen, sender_aliases, out);
276            }
277            if let Some(opts) = opts {
278                for opt in opts.args {
279                    collect_access_control_state_vars_in_expr(
280                        hir,
281                        &opt.value,
282                        seen,
283                        sender_aliases,
284                        out,
285                    );
286                }
287            }
288        }
289        ExprKind::Call(callee, args, opts) => {
290            for callee_id in resolved_function_ids(callee) {
291                let callee_func = hir.function(callee_id);
292                if callee_func
293                    .name
294                    .is_some_and(|name| name_looks_like_access_control(name.as_str()))
295                    || function_has_access_guard(hir, callee_id, &mut HashSet::new())
296                {
297                    collect_state_vars_read_in_function(hir, callee_id, seen, out);
298                }
299            }
300
301            collect_access_control_state_vars_in_expr(hir, callee, seen, sender_aliases, out);
302            if let Some(opts) = opts {
303                for opt in opts.args {
304                    collect_access_control_state_vars_in_expr(
305                        hir,
306                        &opt.value,
307                        seen,
308                        sender_aliases,
309                        out,
310                    );
311                }
312            }
313            for arg in args.exprs() {
314                collect_access_control_state_vars_in_expr(hir, arg, seen, sender_aliases, out);
315            }
316        }
317        ExprKind::Assign(lhs, _, rhs) | ExprKind::Binary(lhs, _, rhs) => {
318            collect_access_control_state_vars_in_expr(hir, lhs, seen, sender_aliases, out);
319            collect_access_control_state_vars_in_expr(hir, rhs, seen, sender_aliases, out);
320        }
321        ExprKind::Unary(_, inner)
322        | ExprKind::Delete(inner)
323        | ExprKind::Member(inner, _)
324        | ExprKind::Payable(inner) => {
325            collect_access_control_state_vars_in_expr(hir, inner, seen, sender_aliases, out);
326        }
327        ExprKind::Index(base, index) => {
328            collect_access_control_state_vars_in_expr(hir, base, seen, sender_aliases, out);
329            if let Some(index) = index {
330                collect_access_control_state_vars_in_expr(hir, index, seen, sender_aliases, out);
331            }
332        }
333        ExprKind::Slice(base, start, end) => {
334            collect_access_control_state_vars_in_expr(hir, base, seen, sender_aliases, out);
335            if let Some(start) = start {
336                collect_access_control_state_vars_in_expr(hir, start, seen, sender_aliases, out);
337            }
338            if let Some(end) = end {
339                collect_access_control_state_vars_in_expr(hir, end, seen, sender_aliases, out);
340            }
341        }
342        ExprKind::Ternary(cond, true_expr, false_expr) => {
343            collect_access_control_state_vars_in_expr(hir, cond, seen, sender_aliases, out);
344            collect_access_control_state_vars_in_expr(hir, true_expr, seen, sender_aliases, out);
345            collect_access_control_state_vars_in_expr(hir, false_expr, seen, sender_aliases, out);
346        }
347        ExprKind::Array(exprs) => {
348            for expr in *exprs {
349                collect_access_control_state_vars_in_expr(hir, expr, seen, sender_aliases, out);
350            }
351        }
352        ExprKind::Tuple(exprs) => {
353            for expr in exprs.iter().copied().flatten() {
354                collect_access_control_state_vars_in_expr(hir, expr, seen, sender_aliases, out);
355            }
356        }
357        ExprKind::New(_) | ExprKind::TypeCall(_) | ExprKind::Type(_) => {}
358        ExprKind::Ident(_) | ExprKind::Lit(_) | ExprKind::YulMember(..) | ExprKind::Err(_) => {}
359    }
360}
361
362fn collect_access_check_state_vars(
363    hir: &hir::Hir<'_>,
364    expr: &hir::Expr<'_>,
365    seen: &mut HashSet<FunctionId>,
366    out: &mut HashSet<VariableId>,
367) {
368    collect_state_vars_read_in_expr(hir, expr, seen, out);
369
370    for callee_id in called_function_ids(expr) {
371        collect_state_vars_read_in_function(hir, callee_id, seen, out);
372    }
373}
374
375fn collect_state_vars_read_in_function(
376    hir: &hir::Hir<'_>,
377    func_id: FunctionId,
378    seen: &mut HashSet<FunctionId>,
379    out: &mut HashSet<VariableId>,
380) {
381    if !seen.insert(func_id) {
382        return;
383    }
384
385    let func = hir.function(func_id);
386    let Some(body) = func.body else { return };
387    for stmt in body.stmts {
388        collect_state_vars_read_in_stmt(hir, stmt, seen, out);
389    }
390}
391
392fn collect_state_vars_read_in_stmt(
393    hir: &hir::Hir<'_>,
394    stmt: &hir::Stmt<'_>,
395    seen: &mut HashSet<FunctionId>,
396    out: &mut HashSet<VariableId>,
397) {
398    match stmt.kind {
399        StmtKind::DeclSingle(var_id) => {
400            if let Some(init) = hir.variable(var_id).initializer {
401                collect_state_vars_read_in_expr(hir, init, seen, out);
402            }
403        }
404        StmtKind::DeclMulti(_, expr)
405        | StmtKind::Expr(expr)
406        | StmtKind::Emit(expr)
407        | StmtKind::Revert(expr)
408        | StmtKind::Return(Some(expr)) => collect_state_vars_read_in_expr(hir, expr, seen, out),
409        StmtKind::Block(block) | StmtKind::UncheckedBlock(block) | StmtKind::Loop(block, _) => {
410            for stmt in block.stmts {
411                collect_state_vars_read_in_stmt(hir, stmt, seen, out);
412            }
413        }
414        StmtKind::If(cond, then_stmt, else_stmt) => {
415            collect_state_vars_read_in_expr(hir, cond, seen, out);
416            collect_state_vars_read_in_stmt(hir, then_stmt, seen, out);
417            if let Some(else_stmt) = else_stmt {
418                collect_state_vars_read_in_stmt(hir, else_stmt, seen, out);
419            }
420        }
421        StmtKind::Try(try_stmt) => {
422            collect_state_vars_read_in_expr(hir, &try_stmt.expr, seen, out);
423            for clause in try_stmt.clauses {
424                for stmt in clause.block.stmts {
425                    collect_state_vars_read_in_stmt(hir, stmt, seen, out);
426                }
427            }
428        }
429        StmtKind::Return(None)
430        | StmtKind::Break
431        | StmtKind::Continue
432        | StmtKind::Placeholder
433        | StmtKind::AssemblyBlock(_)
434        | StmtKind::Switch(_)
435        | StmtKind::Err(_) => {}
436    }
437}
438
439fn collect_state_vars_read_in_expr(
440    hir: &hir::Hir<'_>,
441    expr: &hir::Expr<'_>,
442    seen: &mut HashSet<FunctionId>,
443    out: &mut HashSet<VariableId>,
444) {
445    match &expr.peel_parens().kind {
446        ExprKind::Ident(reses) => {
447            for res in *reses {
448                if let Res::Item(ItemId::Variable(var_id)) = res
449                    && hir.variable(*var_id).kind.is_state()
450                {
451                    out.insert(*var_id);
452                }
453            }
454        }
455        ExprKind::Assign(lhs, _, rhs) | ExprKind::Binary(lhs, _, rhs) => {
456            collect_state_vars_read_in_expr(hir, lhs, seen, out);
457            collect_state_vars_read_in_expr(hir, rhs, seen, out);
458        }
459        ExprKind::Call(callee, args, opts) => {
460            for callee_id in resolved_function_ids(callee) {
461                collect_state_vars_read_in_function(hir, callee_id, seen, out);
462            }
463
464            collect_state_vars_read_in_expr(hir, callee, seen, out);
465            if let Some(opts) = opts {
466                for opt in opts.args {
467                    collect_state_vars_read_in_expr(hir, &opt.value, seen, out);
468                }
469            }
470            for arg in args.exprs() {
471                collect_state_vars_read_in_expr(hir, arg, seen, out);
472            }
473        }
474        ExprKind::Unary(_, inner)
475        | ExprKind::Delete(inner)
476        | ExprKind::Member(inner, _)
477        | ExprKind::Payable(inner) => collect_state_vars_read_in_expr(hir, inner, seen, out),
478        ExprKind::Index(base, index) => {
479            collect_state_vars_read_in_expr(hir, base, seen, out);
480            if let Some(index) = index {
481                collect_state_vars_read_in_expr(hir, index, seen, out);
482            }
483        }
484        ExprKind::Slice(base, start, end) => {
485            collect_state_vars_read_in_expr(hir, base, seen, out);
486            if let Some(start) = start {
487                collect_state_vars_read_in_expr(hir, start, seen, out);
488            }
489            if let Some(end) = end {
490                collect_state_vars_read_in_expr(hir, end, seen, out);
491            }
492        }
493        ExprKind::Ternary(cond, true_expr, false_expr) => {
494            collect_state_vars_read_in_expr(hir, cond, seen, out);
495            collect_state_vars_read_in_expr(hir, true_expr, seen, out);
496            collect_state_vars_read_in_expr(hir, false_expr, seen, out);
497        }
498        ExprKind::Array(exprs) => {
499            for expr in *exprs {
500                collect_state_vars_read_in_expr(hir, expr, seen, out);
501            }
502        }
503        ExprKind::Tuple(exprs) => {
504            for expr in exprs.iter().copied().flatten() {
505                collect_state_vars_read_in_expr(hir, expr, seen, out);
506            }
507        }
508        ExprKind::New(_) | ExprKind::TypeCall(_) | ExprKind::Type(_) => {}
509        ExprKind::Lit(_) | ExprKind::YulMember(..) | ExprKind::Err(_) => {}
510    }
511}
512
513#[derive(Clone, Debug, Default)]
514struct WriteSources {
515    inputs: HashSet<VariableId>,
516    states: HashSet<VariableId>,
517    reads_sender: bool,
518}
519
520impl WriteSources {
521    fn input(var_id: VariableId) -> Self {
522        Self { inputs: HashSet::from([var_id]), states: HashSet::new(), reads_sender: false }
523    }
524
525    fn state(var_id: VariableId) -> Self {
526        Self { inputs: HashSet::new(), states: HashSet::from([var_id]), reads_sender: false }
527    }
528
529    fn sender() -> Self {
530        Self { inputs: HashSet::new(), states: HashSet::new(), reads_sender: true }
531    }
532
533    fn is_empty(&self) -> bool {
534        self.inputs.is_empty() && self.states.is_empty() && !self.reads_sender
535    }
536
537    fn extend(&mut self, other: Self) {
538        self.inputs.extend(other.inputs);
539        self.states.extend(other.states);
540        self.reads_sender |= other.reads_sender;
541    }
542
543    fn intersects(&self, other: &Self) -> bool {
544        self.reads_sender && other.reads_sender
545            || self.inputs.iter().any(|var_id| other.inputs.contains(var_id))
546            || self.states.iter().any(|var_id| other.states.contains(var_id))
547    }
548}
549
550#[derive(Clone, Debug)]
551struct StateWrite {
552    var_id: VariableId,
553    span: Span,
554    sources: WriteSources,
555    fixed_clear: bool,
556    evented: bool,
557}
558
559#[derive(Clone)]
560struct AnalyzerState {
561    taint: HashMap<VariableId, WriteSources>,
562    storage_aliases: HashMap<VariableId, VariableId>,
563    writes: Vec<StateWrite>,
564}
565
566struct WriteAnalyzer<'a, 'hir> {
567    hir: &'hir hir::Hir<'hir>,
568    targets: &'a HashSet<VariableId>,
569    guard_targets: &'a HashSet<VariableId>,
570    taint: HashMap<VariableId, WriteSources>,
571    storage_aliases: HashMap<VariableId, VariableId>,
572    writes: Vec<StateWrite>,
573    call_stack: Vec<FunctionId>,
574}
575
576impl<'a, 'hir> WriteAnalyzer<'a, 'hir> {
577    fn new(
578        hir: &'hir hir::Hir<'hir>,
579        targets: &'a HashSet<VariableId>,
580        guard_targets: &'a HashSet<VariableId>,
581    ) -> Self {
582        Self {
583            hir,
584            targets,
585            guard_targets,
586            taint: HashMap::new(),
587            storage_aliases: HashMap::new(),
588            writes: Vec::new(),
589            call_stack: Vec::new(),
590        }
591    }
592
593    fn analyze_entry_point(&mut self, func_id: FunctionId) -> Vec<StateWrite> {
594        let func = self.hir.function(func_id);
595        self.taint.clear();
596        self.storage_aliases.clear();
597        for &param in func.parameters {
598            self.taint.insert(param, WriteSources::input(param));
599        }
600
601        self.analyze_function(func_id);
602        std::mem::take(&mut self.writes)
603    }
604
605    fn analyze_function(&mut self, func_id: FunctionId) {
606        if self.call_stack.contains(&func_id) {
607            return;
608        }
609
610        let func = self.hir.function(func_id);
611        let Some(body) = func.body else { return };
612
613        self.call_stack.push(func_id);
614        self.analyze_modifiers(func);
615        for stmt in body.stmts {
616            self.analyze_stmt(stmt);
617        }
618        self.call_stack.pop();
619    }
620
621    fn analyze_modifiers(&mut self, func: &'hir hir::Function<'hir>) {
622        for modifier in func.modifiers {
623            let Some(modifier_id) = modifier.id.as_function() else { continue };
624            if self.call_stack.contains(&modifier_id) {
625                continue;
626            }
627
628            for arg in modifier.args.exprs() {
629                self.analyze_expr(arg);
630            }
631
632            let modifier_func = self.hir.function(modifier_id);
633            let Some(body) = modifier_func.body else { continue };
634            let saved_taint = self.taint.clone();
635            let saved_storage_aliases = self.storage_aliases.clone();
636
637            for (param, arg) in modifier_func.parameters.iter().copied().zip(modifier.args.exprs())
638            {
639                self.set_local_taint(param, self.value_sources(arg));
640            }
641
642            self.call_stack.push(modifier_id);
643            for stmt in body.stmts {
644                self.analyze_stmt(stmt);
645            }
646            self.call_stack.pop();
647
648            self.taint = saved_taint;
649            self.storage_aliases = saved_storage_aliases;
650        }
651    }
652
653    fn analyze_stmt(&mut self, stmt: &'hir hir::Stmt<'hir>) {
654        match stmt.kind {
655            StmtKind::DeclSingle(var_id) => {
656                let var = self.hir.variable(var_id);
657                if let Some(init) = var.initializer
658                    && !var.kind.is_state()
659                {
660                    self.analyze_expr(init);
661                    self.set_local_taint(var_id, self.value_sources(init));
662                    self.set_storage_alias_from_initializer(var_id, init);
663                }
664            }
665            StmtKind::DeclMulti(vars, expr) => {
666                self.analyze_expr(expr);
667                let sources = self.value_sources(expr);
668                for var_id in vars.iter().flatten().copied() {
669                    if !self.hir.variable(var_id).kind.is_state() {
670                        self.set_local_taint(var_id, sources.clone());
671                    }
672                }
673            }
674            StmtKind::Block(block) | StmtKind::UncheckedBlock(block) | StmtKind::Loop(block, _) => {
675                for stmt in block.stmts {
676                    self.analyze_stmt(stmt);
677                }
678            }
679            StmtKind::If(cond, then_stmt, else_stmt) => {
680                self.analyze_expr(cond);
681                let base = self.capture_state();
682
683                self.analyze_stmt(then_stmt);
684                let then_state = self.capture_state();
685
686                self.restore_state(base.clone());
687                if let Some(else_stmt) = else_stmt {
688                    self.analyze_stmt(else_stmt);
689                }
690                let else_state = self.capture_state();
691
692                let then_exits = stmt_exits_or_reverts(then_stmt);
693                let else_exits = else_stmt.is_some_and(stmt_exits_or_reverts);
694                self.restore_state(self.merge_branch_states(
695                    base,
696                    then_state,
697                    else_state,
698                    then_exits,
699                    else_exits,
700                    else_stmt.is_some(),
701                ));
702            }
703            StmtKind::Try(try_stmt) => {
704                self.analyze_expr(&try_stmt.expr);
705                for clause in try_stmt.clauses {
706                    for stmt in clause.block.stmts {
707                        self.analyze_stmt(stmt);
708                    }
709                }
710            }
711            StmtKind::Expr(expr) | StmtKind::Revert(expr) => {
712                self.analyze_expr(expr);
713            }
714            StmtKind::Emit(expr) => {
715                self.analyze_expr(expr);
716                self.mark_event(expr);
717            }
718            StmtKind::Return(expr) => {
719                if let Some(expr) = expr {
720                    self.analyze_expr(expr);
721                }
722            }
723            StmtKind::Break
724            | StmtKind::Continue
725            | StmtKind::Placeholder
726            | StmtKind::AssemblyBlock(_)
727            | StmtKind::Switch(_)
728            | StmtKind::Err(_) => {}
729        }
730    }
731
732    fn analyze_expr(&mut self, expr: &'hir hir::Expr<'hir>) {
733        match &expr.peel_parens().kind {
734            ExprKind::Assign(lhs, op, rhs) => {
735                self.analyze_expr(rhs);
736
737                let rhs_sources = self.value_sources(rhs);
738                let mut write_sources = rhs_sources.clone();
739                write_sources.extend(self.lhs_index_sources(lhs));
740                if op.is_some() {
741                    write_sources.extend(self.value_sources(lhs));
742                }
743
744                let fixed_clear = expr_is_zero_value(rhs);
745                for var_id in state_lhs_vars(self.hir, lhs, &self.storage_aliases) {
746                    if self.targets.contains(&var_id)
747                        && self.write_is_reportable(var_id, &write_sources, fixed_clear)
748                    {
749                        self.writes.push(StateWrite {
750                            var_id,
751                            span: lhs.span,
752                            sources: write_sources.clone(),
753                            fixed_clear,
754                            evented: false,
755                        });
756                    }
757                }
758
759                if let Some(local) = lhs_local_var(self.hir, lhs) {
760                    let mut local_sources = rhs_sources;
761                    if op.is_some() {
762                        local_sources.extend(self.value_sources(lhs));
763                    }
764                    self.set_local_taint(local, local_sources);
765                    self.set_storage_alias_from_initializer(local, rhs);
766                } else {
767                    self.analyze_lhs_indices(lhs);
768                }
769            }
770            ExprKind::Delete(inner) => {
771                let sources = self.lhs_index_sources(inner);
772                for var_id in state_lhs_vars(self.hir, inner, &self.storage_aliases) {
773                    if self.targets.contains(&var_id)
774                        && self.write_is_reportable(var_id, &sources, true)
775                    {
776                        self.writes.push(StateWrite {
777                            var_id,
778                            span: inner.span,
779                            sources: sources.clone(),
780                            fixed_clear: true,
781                            evented: false,
782                        });
783                    }
784                }
785                self.analyze_lhs_indices(inner);
786            }
787            ExprKind::Call(callee, args, opts) => {
788                self.analyze_expr(callee);
789                if let Some(opts) = opts {
790                    for opt in opts.args {
791                        self.analyze_expr(&opt.value);
792                    }
793                }
794                for arg in args.exprs() {
795                    self.analyze_expr(arg);
796                }
797
798                for callee_id in resolved_function_ids(callee) {
799                    self.analyze_internal_call(callee_id, args);
800                }
801            }
802            ExprKind::Binary(lhs, _, rhs) => {
803                self.analyze_expr(lhs);
804                self.analyze_expr(rhs);
805            }
806            ExprKind::Unary(_, inner) | ExprKind::Member(inner, _) | ExprKind::Payable(inner) => {
807                self.analyze_expr(inner);
808            }
809            ExprKind::Index(base, index) => {
810                self.analyze_expr(base);
811                if let Some(index) = index {
812                    self.analyze_expr(index);
813                }
814            }
815            ExprKind::Slice(base, start, end) => {
816                self.analyze_expr(base);
817                if let Some(start) = start {
818                    self.analyze_expr(start);
819                }
820                if let Some(end) = end {
821                    self.analyze_expr(end);
822                }
823            }
824            ExprKind::Ternary(cond, true_expr, false_expr) => {
825                self.analyze_expr(cond);
826                self.analyze_expr(true_expr);
827                self.analyze_expr(false_expr);
828            }
829            ExprKind::Array(exprs) => {
830                for expr in *exprs {
831                    self.analyze_expr(expr);
832                }
833            }
834            ExprKind::Tuple(exprs) => {
835                for expr in exprs.iter().copied().flatten() {
836                    self.analyze_expr(expr);
837                }
838            }
839            ExprKind::New(_) | ExprKind::TypeCall(_) | ExprKind::Type(_) => {}
840            ExprKind::Ident(_) | ExprKind::Lit(_) | ExprKind::YulMember(..) | ExprKind::Err(_) => {}
841        }
842    }
843
844    fn analyze_internal_call(&mut self, callee_id: FunctionId, args: &hir::CallArgs<'hir>) {
845        if self.call_stack.contains(&callee_id) {
846            return;
847        }
848
849        let callee = self.hir.function(callee_id);
850        let Some(_) = callee.body else { return };
851
852        let saved_taint = std::mem::take(&mut self.taint);
853        let saved_storage_aliases = std::mem::take(&mut self.storage_aliases);
854        for (param, arg) in callee.parameters.iter().copied().zip(args.exprs()) {
855            let sources = collect_value_sources(self.hir, &saved_taint, arg);
856            if !sources.is_empty() {
857                self.taint.insert(param, sources);
858            }
859        }
860
861        self.analyze_function(callee_id);
862        self.taint = saved_taint;
863        self.storage_aliases = saved_storage_aliases;
864    }
865
866    fn analyze_lhs_indices(&mut self, expr: &'hir hir::Expr<'hir>) {
867        match &expr.peel_parens().kind {
868            ExprKind::Index(base, index) => {
869                self.analyze_lhs_indices(base);
870                if let Some(index) = index {
871                    self.analyze_expr(index);
872                }
873            }
874            ExprKind::Slice(base, start, end) => {
875                self.analyze_lhs_indices(base);
876                if let Some(start) = start {
877                    self.analyze_expr(start);
878                }
879                if let Some(end) = end {
880                    self.analyze_expr(end);
881                }
882            }
883            ExprKind::Member(base, _) | ExprKind::Payable(base) => self.analyze_lhs_indices(base),
884            ExprKind::Tuple(exprs) => {
885                for expr in exprs.iter().copied().flatten() {
886                    self.analyze_lhs_indices(expr);
887                }
888            }
889            _ => {}
890        }
891    }
892
893    fn value_sources(&self, expr: &hir::Expr<'_>) -> WriteSources {
894        collect_value_sources(self.hir, &self.taint, expr)
895    }
896
897    fn lhs_index_sources(&self, expr: &hir::Expr<'_>) -> WriteSources {
898        collect_lhs_index_sources(self.hir, &self.taint, expr)
899    }
900
901    fn set_local_taint(&mut self, var_id: VariableId, sources: WriteSources) {
902        if sources.is_empty() {
903            self.taint.remove(&var_id);
904        } else {
905            self.taint.insert(var_id, sources);
906        }
907    }
908
909    fn set_storage_alias_from_initializer(
910        &mut self,
911        var_id: VariableId,
912        init: &'hir hir::Expr<'hir>,
913    ) {
914        let var = self.hir.variable(var_id);
915        if var.kind.is_state() || var.data_location != Some(DataLocation::Storage) {
916            self.storage_aliases.remove(&var_id);
917            return;
918        }
919
920        if let Some(root) = state_lhs_vars(self.hir, init, &self.storage_aliases).into_iter().next()
921        {
922            self.storage_aliases.insert(var_id, root);
923        } else {
924            self.storage_aliases.remove(&var_id);
925        }
926    }
927
928    fn mark_event(&mut self, expr: &hir::Expr<'_>) {
929        let Some(event_id) = emitted_event_id(expr) else { return };
930        let event_sources = self.value_sources(expr);
931
932        for write in &mut self.writes {
933            if !write.evented
934                && (write.fixed_clear || write.sources.intersects(&event_sources))
935                && event_mentions_state_var(self.hir, event_id, write.var_id)
936            {
937                write.evented = true;
938            }
939        }
940    }
941
942    fn capture_state(&self) -> AnalyzerState {
943        AnalyzerState {
944            taint: self.taint.clone(),
945            storage_aliases: self.storage_aliases.clone(),
946            writes: self.writes.clone(),
947        }
948    }
949
950    fn restore_state(&mut self, state: AnalyzerState) {
951        self.taint = state.taint;
952        self.storage_aliases = state.storage_aliases;
953        self.writes = state.writes;
954    }
955
956    fn merge_branch_states(
957        &self,
958        base: AnalyzerState,
959        then_state: AnalyzerState,
960        else_state: AnalyzerState,
961        then_exits: bool,
962        else_exits: bool,
963        has_else: bool,
964    ) -> AnalyzerState {
965        let mut writes = base.writes.clone();
966        let base_len = writes.len();
967        for (idx, write) in writes.iter_mut().enumerate().take(base_len) {
968            write.evented = then_state.writes[idx].evented && else_state.writes[idx].evented;
969        }
970        writes.extend_from_slice(&then_state.writes[base_len..]);
971        writes.extend_from_slice(&else_state.writes[base_len..]);
972
973        let (taint, storage_aliases) = match (then_exits, else_exits, has_else) {
974            (true, true, _) => (base.taint, base.storage_aliases),
975            (true, _, _) => (else_state.taint, else_state.storage_aliases),
976            (_, true, true) => (then_state.taint, then_state.storage_aliases),
977            _ => (
978                merge_source_maps(&then_state.taint, &else_state.taint),
979                merge_alias_maps(&then_state.storage_aliases, &else_state.storage_aliases),
980            ),
981        };
982
983        AnalyzerState { taint, storage_aliases, writes }
984    }
985
986    fn write_is_reportable(
987        &self,
988        var_id: VariableId,
989        sources: &WriteSources,
990        fixed_clear: bool,
991    ) -> bool {
992        self.targets.contains(&var_id)
993            && (!sources.is_empty() || (fixed_clear && self.guard_targets.contains(&var_id)))
994    }
995}
996
997fn merge_source_maps(
998    lhs: &HashMap<VariableId, WriteSources>,
999    rhs: &HashMap<VariableId, WriteSources>,
1000) -> HashMap<VariableId, WriteSources> {
1001    let mut out = lhs.clone();
1002    for (var_id, sources) in rhs {
1003        out.entry(*var_id).or_default().extend(sources.clone());
1004    }
1005    out.retain(|_, sources| !sources.is_empty());
1006    out
1007}
1008
1009fn merge_alias_maps(
1010    lhs: &HashMap<VariableId, VariableId>,
1011    rhs: &HashMap<VariableId, VariableId>,
1012) -> HashMap<VariableId, VariableId> {
1013    lhs.iter()
1014        .filter_map(|(alias, lhs_root)| {
1015            rhs.get(alias)
1016                .is_some_and(|rhs_root| rhs_root == lhs_root)
1017                .then_some((*alias, *lhs_root))
1018        })
1019        .collect()
1020}
1021
1022fn collect_value_sources(
1023    hir: &hir::Hir<'_>,
1024    taint: &HashMap<VariableId, WriteSources>,
1025    expr: &hir::Expr<'_>,
1026) -> WriteSources {
1027    let mut out = WriteSources::default();
1028    collect_value_sources_into(hir, taint, expr, &mut out);
1029    out
1030}
1031
1032fn collect_value_sources_into(
1033    hir: &hir::Hir<'_>,
1034    taint: &HashMap<VariableId, WriteSources>,
1035    expr: &hir::Expr<'_>,
1036    out: &mut WriteSources,
1037) {
1038    if is_sender_member(expr) {
1039        out.extend(WriteSources::sender());
1040        return;
1041    }
1042
1043    match &expr.peel_parens().kind {
1044        ExprKind::Ident(reses) => {
1045            for res in *reses {
1046                if let Res::Item(ItemId::Variable(var_id)) = res {
1047                    if hir.variable(*var_id).kind.is_state() {
1048                        out.extend(WriteSources::state(*var_id));
1049                    }
1050                    if let Some(sources) = taint.get(var_id) {
1051                        out.extend(sources.clone());
1052                    }
1053                }
1054            }
1055        }
1056        ExprKind::Assign(lhs, _, rhs) | ExprKind::Binary(lhs, _, rhs) => {
1057            collect_value_sources_into(hir, taint, lhs, out);
1058            collect_value_sources_into(hir, taint, rhs, out);
1059        }
1060        ExprKind::Call(callee, args, opts) => {
1061            collect_value_sources_into(hir, taint, callee, out);
1062            if let Some(opts) = opts {
1063                for opt in opts.args {
1064                    collect_value_sources_into(hir, taint, &opt.value, out);
1065                }
1066            }
1067            for arg in args.exprs() {
1068                collect_value_sources_into(hir, taint, arg, out);
1069            }
1070        }
1071        ExprKind::Unary(_, inner)
1072        | ExprKind::Delete(inner)
1073        | ExprKind::Member(inner, _)
1074        | ExprKind::Payable(inner) => collect_value_sources_into(hir, taint, inner, out),
1075        ExprKind::Index(base, index) => {
1076            collect_value_sources_into(hir, taint, base, out);
1077            if let Some(index) = index {
1078                collect_value_sources_into(hir, taint, index, out);
1079            }
1080        }
1081        ExprKind::Slice(base, start, end) => {
1082            collect_value_sources_into(hir, taint, base, out);
1083            if let Some(start) = start {
1084                collect_value_sources_into(hir, taint, start, out);
1085            }
1086            if let Some(end) = end {
1087                collect_value_sources_into(hir, taint, end, out);
1088            }
1089        }
1090        ExprKind::Ternary(cond, true_expr, false_expr) => {
1091            collect_value_sources_into(hir, taint, cond, out);
1092            collect_value_sources_into(hir, taint, true_expr, out);
1093            collect_value_sources_into(hir, taint, false_expr, out);
1094        }
1095        ExprKind::Array(exprs) => {
1096            for expr in *exprs {
1097                collect_value_sources_into(hir, taint, expr, out);
1098            }
1099        }
1100        ExprKind::Tuple(exprs) => {
1101            for expr in exprs.iter().copied().flatten() {
1102                collect_value_sources_into(hir, taint, expr, out);
1103            }
1104        }
1105        ExprKind::New(_) | ExprKind::TypeCall(_) | ExprKind::Type(_) => {}
1106        ExprKind::Lit(_) | ExprKind::YulMember(..) | ExprKind::Err(_) => {}
1107    }
1108}
1109
1110fn collect_lhs_index_sources(
1111    hir: &hir::Hir<'_>,
1112    taint: &HashMap<VariableId, WriteSources>,
1113    expr: &hir::Expr<'_>,
1114) -> WriteSources {
1115    let mut out = WriteSources::default();
1116    collect_lhs_index_sources_into(hir, taint, expr, &mut out);
1117    out
1118}
1119
1120fn collect_lhs_index_sources_into(
1121    hir: &hir::Hir<'_>,
1122    taint: &HashMap<VariableId, WriteSources>,
1123    expr: &hir::Expr<'_>,
1124    out: &mut WriteSources,
1125) {
1126    match &expr.peel_parens().kind {
1127        ExprKind::Index(base, index) => {
1128            collect_lhs_index_sources_into(hir, taint, base, out);
1129            if let Some(index) = index {
1130                out.extend(collect_value_sources(hir, taint, index));
1131            }
1132        }
1133        ExprKind::Slice(base, start, end) => {
1134            collect_lhs_index_sources_into(hir, taint, base, out);
1135            if let Some(start) = start {
1136                out.extend(collect_value_sources(hir, taint, start));
1137            }
1138            if let Some(end) = end {
1139                out.extend(collect_value_sources(hir, taint, end));
1140            }
1141        }
1142        ExprKind::Member(base, _) | ExprKind::Payable(base) | ExprKind::Unary(_, base) => {
1143            collect_lhs_index_sources_into(hir, taint, base, out);
1144        }
1145        ExprKind::Tuple(exprs) => {
1146            for expr in exprs.iter().copied().flatten() {
1147                collect_lhs_index_sources_into(hir, taint, expr, out);
1148            }
1149        }
1150        _ => {}
1151    }
1152}
1153
1154fn expr_is_zero_value(expr: &hir::Expr<'_>) -> bool {
1155    match &expr.peel_parens().kind {
1156        ExprKind::Lit(lit) => match lit.kind {
1157            LitKind::Number(value) => value.is_zero(),
1158            LitKind::Address(value) => value.is_zero(),
1159            LitKind::Bool(value) => !value,
1160            _ => false,
1161        },
1162        ExprKind::Call(_, args, _) => {
1163            let mut exprs = args.exprs();
1164            let Some(arg) = exprs.next() else { return false };
1165            exprs.next().is_none() && expr_is_zero_value(arg)
1166        }
1167        ExprKind::Unary(_, inner) | ExprKind::Payable(inner) => expr_is_zero_value(inner),
1168        _ => false,
1169    }
1170}
1171
1172fn lhs_local_var(hir: &hir::Hir<'_>, lhs: &hir::Expr<'_>) -> Option<VariableId> {
1173    if let ExprKind::Ident(reses) = &lhs.peel_parens().kind {
1174        for res in *reses {
1175            if let Res::Item(ItemId::Variable(var_id)) = res
1176                && !hir.variable(*var_id).kind.is_state()
1177            {
1178                return Some(*var_id);
1179            }
1180        }
1181    }
1182    None
1183}
1184
1185fn state_lhs_vars(
1186    hir: &hir::Hir<'_>,
1187    lhs: &hir::Expr<'_>,
1188    storage_aliases: &HashMap<VariableId, VariableId>,
1189) -> Vec<VariableId> {
1190    let mut vars = Vec::new();
1191    collect_state_lhs_vars(hir, lhs, storage_aliases, &mut vars);
1192    vars
1193}
1194
1195fn collect_state_lhs_vars(
1196    hir: &hir::Hir<'_>,
1197    expr: &hir::Expr<'_>,
1198    storage_aliases: &HashMap<VariableId, VariableId>,
1199    vars: &mut Vec<VariableId>,
1200) {
1201    match &expr.peel_parens().kind {
1202        ExprKind::Ident(reses) => {
1203            for res in *reses {
1204                if let Res::Item(ItemId::Variable(var_id)) = res {
1205                    let root = if hir.variable(*var_id).kind.is_state() {
1206                        Some(*var_id)
1207                    } else {
1208                        storage_aliases.get(var_id).copied()
1209                    };
1210                    if let Some(root) = root
1211                        && !vars.contains(&root)
1212                    {
1213                        vars.push(root);
1214                    }
1215                }
1216            }
1217        }
1218        ExprKind::Index(base, _) | ExprKind::Slice(base, ..) => {
1219            collect_state_lhs_vars(hir, base, storage_aliases, vars);
1220        }
1221        ExprKind::Member(base, _)
1222        | ExprKind::Payable(base)
1223        | ExprKind::Unary(_, base)
1224        | ExprKind::Delete(base) => collect_state_lhs_vars(hir, base, storage_aliases, vars),
1225        ExprKind::Tuple(exprs) => {
1226            for expr in exprs.iter().copied().flatten() {
1227                collect_state_lhs_vars(hir, expr, storage_aliases, vars);
1228            }
1229        }
1230        _ => {}
1231    }
1232}
1233
1234fn is_protected(hir: &hir::Hir<'_>, func_id: FunctionId, func: &hir::Function<'_>) -> bool {
1235    for modifier in func.modifiers {
1236        if let Some(modifier_id) = modifier.id.as_function()
1237            && modifier_has_access_control(hir, modifier_id)
1238        {
1239            return true;
1240        }
1241    }
1242
1243    function_has_access_guard(hir, func_id, &mut HashSet::new())
1244}
1245
1246fn update_sender_alias_from_decl(
1247    hir: &hir::Hir<'_>,
1248    var_id: VariableId,
1249    seen: &HashSet<FunctionId>,
1250    sender_aliases: &mut HashSet<VariableId>,
1251) {
1252    let var = hir.variable(var_id);
1253    if var.kind.is_state() {
1254        return;
1255    }
1256
1257    let mut sender_seen = seen.clone();
1258    if var
1259        .initializer
1260        .is_some_and(|init| expr_reads_sender(hir, init, &mut sender_seen, sender_aliases))
1261    {
1262        sender_aliases.insert(var_id);
1263    } else {
1264        sender_aliases.remove(&var_id);
1265    }
1266}
1267
1268fn update_sender_aliases_from_assignment(
1269    hir: &hir::Hir<'_>,
1270    expr: &hir::Expr<'_>,
1271    seen: &HashSet<FunctionId>,
1272    sender_aliases: &mut HashSet<VariableId>,
1273) {
1274    let ExprKind::Assign(lhs, _, rhs) = &expr.peel_parens().kind else { return };
1275    let Some(local) = lhs_local_var(hir, lhs) else { return };
1276
1277    let mut sender_seen = seen.clone();
1278    if expr_reads_sender(hir, rhs, &mut sender_seen, sender_aliases) {
1279        sender_aliases.insert(local);
1280    } else {
1281        sender_aliases.remove(&local);
1282    }
1283}
1284
1285fn modifier_has_access_control(hir: &hir::Hir<'_>, modifier_id: FunctionId) -> bool {
1286    let modifier = hir.function(modifier_id);
1287    if let Some(body) = modifier.body {
1288        let mut sender_aliases = HashSet::new();
1289        for stmt in body.stmts {
1290            if matches!(stmt.kind, StmtKind::Placeholder) {
1291                break;
1292            }
1293            if stmt_has_access_guard(hir, stmt, &mut HashSet::new(), &mut sender_aliases) {
1294                return true;
1295            }
1296        }
1297        return false;
1298    }
1299
1300    modifier.name.is_some_and(|name| name_looks_like_access_control(name.as_str()))
1301}
1302
1303fn function_has_access_guard(
1304    hir: &hir::Hir<'_>,
1305    func_id: FunctionId,
1306    seen: &mut HashSet<FunctionId>,
1307) -> bool {
1308    if !seen.insert(func_id) {
1309        return false;
1310    }
1311
1312    let func = hir.function(func_id);
1313    let Some(body) = func.body else {
1314        return func.name.is_some_and(|name| name_looks_like_access_control(name.as_str()));
1315    };
1316
1317    let mut sender_aliases = HashSet::new();
1318    for stmt in body.stmts {
1319        if stmt_has_access_guard(hir, stmt, seen, &mut sender_aliases) {
1320            return true;
1321        }
1322    }
1323    false
1324}
1325
1326fn stmt_has_access_guard(
1327    hir: &hir::Hir<'_>,
1328    stmt: &hir::Stmt<'_>,
1329    seen: &mut HashSet<FunctionId>,
1330    sender_aliases: &mut HashSet<VariableId>,
1331) -> bool {
1332    match stmt.kind {
1333        StmtKind::If(cond, then_stmt, else_stmt) => {
1334            (expr_looks_like_access_check(hir, cond, sender_aliases)
1335                && (stmt_exits_or_reverts(then_stmt)
1336                    || else_stmt.is_some_and(stmt_exits_or_reverts)))
1337                || {
1338                    let mut then_aliases = sender_aliases.clone();
1339                    stmt_has_access_guard(hir, then_stmt, seen, &mut then_aliases)
1340                }
1341                || else_stmt.is_some_and(|stmt| {
1342                    let mut else_aliases = sender_aliases.clone();
1343                    stmt_has_access_guard(hir, stmt, seen, &mut else_aliases)
1344                })
1345        }
1346        StmtKind::Expr(expr) => {
1347            let has_guard = expr_has_access_guard(hir, expr, seen, sender_aliases);
1348            update_sender_aliases_from_assignment(hir, expr, seen, sender_aliases);
1349            has_guard
1350        }
1351        StmtKind::Block(block) | StmtKind::UncheckedBlock(block) | StmtKind::Loop(block, _) => {
1352            for stmt in block.stmts {
1353                if stmt_has_access_guard(hir, stmt, seen, sender_aliases) {
1354                    return true;
1355                }
1356            }
1357            false
1358        }
1359        StmtKind::Try(try_stmt) => try_stmt.clauses.iter().any(|clause| {
1360            let mut clause_aliases = sender_aliases.clone();
1361            clause
1362                .block
1363                .stmts
1364                .iter()
1365                .any(|stmt| stmt_has_access_guard(hir, stmt, seen, &mut clause_aliases))
1366        }),
1367        StmtKind::Return(Some(expr)) | StmtKind::Emit(expr) | StmtKind::Revert(expr) => {
1368            expr_has_access_guard(hir, expr, seen, sender_aliases)
1369        }
1370        StmtKind::DeclSingle(var_id) => {
1371            let has_guard = hir
1372                .variable(var_id)
1373                .initializer
1374                .is_some_and(|init| expr_has_access_guard(hir, init, seen, sender_aliases));
1375            update_sender_alias_from_decl(hir, var_id, seen, sender_aliases);
1376            has_guard
1377        }
1378        StmtKind::DeclMulti(_, expr) => expr_has_access_guard(hir, expr, seen, sender_aliases),
1379        StmtKind::Return(None)
1380        | StmtKind::Break
1381        | StmtKind::Continue
1382        | StmtKind::Placeholder
1383        | StmtKind::AssemblyBlock(_)
1384        | StmtKind::Switch(_)
1385        | StmtKind::Err(_) => false,
1386    }
1387}
1388
1389fn stmt_exits_or_reverts(stmt: &hir::Stmt<'_>) -> bool {
1390    branch_always_exits(stmt)
1391}
1392
1393fn expr_has_access_guard(
1394    hir: &hir::Hir<'_>,
1395    expr: &hir::Expr<'_>,
1396    seen: &mut HashSet<FunctionId>,
1397    sender_aliases: &HashSet<VariableId>,
1398) -> bool {
1399    match &expr.peel_parens().kind {
1400        ExprKind::Call(callee, args, _) if is_require_or_assert(callee) => args
1401            .exprs()
1402            .next()
1403            .is_some_and(|cond| expr_looks_like_access_check(hir, cond, sender_aliases)),
1404        ExprKind::Call(callee, args, opts) => {
1405            for callee_id in resolved_function_ids(callee) {
1406                let func = hir.function(callee_id);
1407                if func.name.is_some_and(|name| name_looks_like_access_control(name.as_str()))
1408                    || function_has_access_guard(hir, callee_id, seen)
1409                {
1410                    return true;
1411                }
1412            }
1413
1414            expr_has_access_guard(hir, callee, seen, sender_aliases)
1415                || opts.is_some_and(|opts| {
1416                    opts.args
1417                        .iter()
1418                        .any(|opt| expr_has_access_guard(hir, &opt.value, seen, sender_aliases))
1419                })
1420                || args.exprs().any(|arg| expr_has_access_guard(hir, arg, seen, sender_aliases))
1421        }
1422        ExprKind::Binary(lhs, _, rhs) => {
1423            expr_has_access_guard(hir, lhs, seen, sender_aliases)
1424                || expr_has_access_guard(hir, rhs, seen, sender_aliases)
1425        }
1426        ExprKind::Unary(_, inner)
1427        | ExprKind::Delete(inner)
1428        | ExprKind::Member(inner, _)
1429        | ExprKind::Payable(inner) => expr_has_access_guard(hir, inner, seen, sender_aliases),
1430        ExprKind::Index(base, index) => {
1431            expr_has_access_guard(hir, base, seen, sender_aliases)
1432                || index
1433                    .is_some_and(|index| expr_has_access_guard(hir, index, seen, sender_aliases))
1434        }
1435        ExprKind::Slice(base, start, end) => {
1436            expr_has_access_guard(hir, base, seen, sender_aliases)
1437                || start
1438                    .is_some_and(|start| expr_has_access_guard(hir, start, seen, sender_aliases))
1439                || end.is_some_and(|end| expr_has_access_guard(hir, end, seen, sender_aliases))
1440        }
1441        ExprKind::Ternary(cond, true_expr, false_expr) => {
1442            expr_has_access_guard(hir, cond, seen, sender_aliases)
1443                || expr_has_access_guard(hir, true_expr, seen, sender_aliases)
1444                || expr_has_access_guard(hir, false_expr, seen, sender_aliases)
1445        }
1446        ExprKind::Array(exprs) => {
1447            exprs.iter().any(|expr| expr_has_access_guard(hir, expr, seen, sender_aliases))
1448        }
1449        ExprKind::Tuple(exprs) => exprs
1450            .iter()
1451            .copied()
1452            .flatten()
1453            .any(|expr| expr_has_access_guard(hir, expr, seen, sender_aliases)),
1454        ExprKind::Assign(_, _, rhs) => expr_has_access_guard(hir, rhs, seen, sender_aliases),
1455        _ => false,
1456    }
1457}
1458
1459fn expr_looks_like_access_check(
1460    hir: &hir::Hir<'_>,
1461    expr: &hir::Expr<'_>,
1462    sender_aliases: &HashSet<VariableId>,
1463) -> bool {
1464    expr_reads_sender(hir, expr, &mut HashSet::new(), sender_aliases)
1465        && expr_reads_state_variable_transitively(hir, expr)
1466}
1467
1468fn expr_reads_state_variable_transitively(hir: &hir::Hir<'_>, expr: &hir::Expr<'_>) -> bool {
1469    let mut vars = HashSet::new();
1470    collect_state_vars_read_in_expr(hir, expr, &mut HashSet::new(), &mut vars);
1471    !vars.is_empty()
1472}
1473
1474fn expr_reads_sender(
1475    hir: &hir::Hir<'_>,
1476    expr: &hir::Expr<'_>,
1477    seen: &mut HashSet<FunctionId>,
1478    sender_aliases: &HashSet<VariableId>,
1479) -> bool {
1480    if is_sender_member(expr) {
1481        return true;
1482    }
1483
1484    match &expr.peel_parens().kind {
1485        ExprKind::Ident(reses) => reses.iter().any(|res| {
1486            let Res::Item(ItemId::Variable(var_id)) = res else { return false };
1487            sender_aliases.contains(var_id)
1488        }),
1489        ExprKind::Call(callee, args, opts) => {
1490            for callee_id in resolved_function_ids(callee) {
1491                if function_reads_sender(hir, callee_id, seen) {
1492                    return true;
1493                }
1494            }
1495
1496            expr_reads_sender(hir, callee, seen, sender_aliases)
1497                || opts.is_some_and(|opts| {
1498                    opts.args
1499                        .iter()
1500                        .any(|opt| expr_reads_sender(hir, &opt.value, seen, sender_aliases))
1501                })
1502                || args.exprs().any(|arg| expr_reads_sender(hir, arg, seen, sender_aliases))
1503        }
1504        ExprKind::Binary(lhs, _, rhs) => {
1505            expr_reads_sender(hir, lhs, seen, sender_aliases)
1506                || expr_reads_sender(hir, rhs, seen, sender_aliases)
1507        }
1508        ExprKind::Unary(_, inner)
1509        | ExprKind::Delete(inner)
1510        | ExprKind::Member(inner, _)
1511        | ExprKind::Payable(inner) => expr_reads_sender(hir, inner, seen, sender_aliases),
1512        ExprKind::Index(base, index) => {
1513            expr_reads_sender(hir, base, seen, sender_aliases)
1514                || index.is_some_and(|index| expr_reads_sender(hir, index, seen, sender_aliases))
1515        }
1516        ExprKind::Slice(base, start, end) => {
1517            expr_reads_sender(hir, base, seen, sender_aliases)
1518                || start.is_some_and(|start| expr_reads_sender(hir, start, seen, sender_aliases))
1519                || end.is_some_and(|end| expr_reads_sender(hir, end, seen, sender_aliases))
1520        }
1521        ExprKind::Ternary(cond, true_expr, false_expr) => {
1522            expr_reads_sender(hir, cond, seen, sender_aliases)
1523                || expr_reads_sender(hir, true_expr, seen, sender_aliases)
1524                || expr_reads_sender(hir, false_expr, seen, sender_aliases)
1525        }
1526        ExprKind::Array(exprs) => {
1527            exprs.iter().any(|expr| expr_reads_sender(hir, expr, seen, sender_aliases))
1528        }
1529        ExprKind::Tuple(exprs) => exprs
1530            .iter()
1531            .copied()
1532            .flatten()
1533            .any(|expr| expr_reads_sender(hir, expr, seen, sender_aliases)),
1534        ExprKind::Assign(_, _, rhs) => expr_reads_sender(hir, rhs, seen, sender_aliases),
1535        _ => false,
1536    }
1537}
1538
1539fn function_reads_sender(
1540    hir: &hir::Hir<'_>,
1541    func_id: FunctionId,
1542    seen: &mut HashSet<FunctionId>,
1543) -> bool {
1544    if !seen.insert(func_id) {
1545        return false;
1546    }
1547
1548    let func = hir.function(func_id);
1549    let Some(body) = func.body else { return false };
1550    let mut sender_aliases = HashSet::new();
1551    body.stmts.iter().any(|stmt| stmt_reads_sender(hir, stmt, seen, &mut sender_aliases))
1552}
1553
1554fn stmt_reads_sender(
1555    hir: &hir::Hir<'_>,
1556    stmt: &hir::Stmt<'_>,
1557    seen: &mut HashSet<FunctionId>,
1558    sender_aliases: &mut HashSet<VariableId>,
1559) -> bool {
1560    match stmt.kind {
1561        StmtKind::DeclSingle(var_id) => {
1562            let reads = hir
1563                .variable(var_id)
1564                .initializer
1565                .is_some_and(|init| expr_reads_sender(hir, init, seen, sender_aliases));
1566            update_sender_alias_from_decl(hir, var_id, seen, sender_aliases);
1567            reads
1568        }
1569        StmtKind::Expr(expr) => {
1570            let reads = expr_reads_sender(hir, expr, seen, sender_aliases);
1571            update_sender_aliases_from_assignment(hir, expr, seen, sender_aliases);
1572            reads
1573        }
1574        StmtKind::DeclMulti(_, expr) | StmtKind::Emit(expr) | StmtKind::Revert(expr) => {
1575            expr_reads_sender(hir, expr, seen, sender_aliases)
1576        }
1577        StmtKind::Return(Some(expr)) => expr_reads_sender(hir, expr, seen, sender_aliases),
1578        StmtKind::Block(block) | StmtKind::UncheckedBlock(block) | StmtKind::Loop(block, _) => {
1579            block.stmts.iter().any(|stmt| stmt_reads_sender(hir, stmt, seen, sender_aliases))
1580        }
1581        StmtKind::If(cond, then_stmt, else_stmt) => {
1582            expr_reads_sender(hir, cond, seen, sender_aliases)
1583                || {
1584                    let mut then_aliases = sender_aliases.clone();
1585                    stmt_reads_sender(hir, then_stmt, seen, &mut then_aliases)
1586                }
1587                || else_stmt.is_some_and(|stmt| {
1588                    let mut else_aliases = sender_aliases.clone();
1589                    stmt_reads_sender(hir, stmt, seen, &mut else_aliases)
1590                })
1591        }
1592        StmtKind::Try(try_stmt) => {
1593            expr_reads_sender(hir, &try_stmt.expr, seen, sender_aliases)
1594                || try_stmt.clauses.iter().any(|clause| {
1595                    let mut clause_aliases = sender_aliases.clone();
1596                    clause
1597                        .block
1598                        .stmts
1599                        .iter()
1600                        .any(|stmt| stmt_reads_sender(hir, stmt, seen, &mut clause_aliases))
1601                })
1602        }
1603        StmtKind::Return(None)
1604        | StmtKind::Break
1605        | StmtKind::Continue
1606        | StmtKind::Placeholder
1607        | StmtKind::AssemblyBlock(_)
1608        | StmtKind::Switch(_)
1609        | StmtKind::Err(_) => false,
1610    }
1611}
1612
1613fn is_sender_member(expr: &hir::Expr<'_>) -> bool {
1614    let ExprKind::Member(base, member) = &expr.peel_parens().kind else { return false };
1615    let ExprKind::Ident(reses) = &base.peel_parens().kind else { return false };
1616
1617    reses.iter().any(|res| {
1618        let Res::Builtin(builtin) = res else { return false };
1619        matches!((builtin.name(), member.name), (sym::msg, sym::sender) | (sym::tx, kw::Origin))
1620    })
1621}
1622
1623fn name_looks_like_access_control(name: &str) -> bool {
1624    let lower = name.to_ascii_lowercase();
1625    lower == "auth"
1626        || lower == "requiresauth"
1627        || lower == "restricted"
1628        || lower.starts_with("onlyadmin")
1629        || lower.starts_with("onlyguardian")
1630        || lower.starts_with("onlymanager")
1631        || lower.starts_with("onlyowner")
1632        || lower.starts_with("onlyrole")
1633        || lower.starts_with("checkadmin")
1634        || lower.starts_with("_checkadmin")
1635        || lower.starts_with("checkguardian")
1636        || lower.starts_with("_checkguardian")
1637        || lower.starts_with("checkmanager")
1638        || lower.starts_with("_checkmanager")
1639        || lower.starts_with("checkowner")
1640        || lower.starts_with("_checkowner")
1641        || lower.starts_with("checkrole")
1642        || lower.starts_with("_checkrole")
1643}
1644
1645fn emitted_event_id(expr: &hir::Expr<'_>) -> Option<EventId> {
1646    let ExprKind::Call(callee, _, _) = &expr.peel_parens().kind else { return None };
1647    resolved_event_id(callee)
1648}
1649
1650fn resolved_event_id(expr: &hir::Expr<'_>) -> Option<EventId> {
1651    match &expr.peel_parens().kind {
1652        ExprKind::Ident(reses) => reses.iter().find_map(|res| {
1653            let Res::Item(ItemId::Event(event_id)) = res else { return None };
1654            Some(*event_id)
1655        }),
1656        ExprKind::Member(base, _) => resolved_event_id(base),
1657        _ => None,
1658    }
1659}
1660
1661fn event_mentions_state_var(hir: &hir::Hir<'_>, event_id: EventId, var_id: VariableId) -> bool {
1662    let Some(var_name) = hir.variable(var_id).name else { return false };
1663    let keywords = state_var_event_keywords(var_name.as_str());
1664    let event = hir.event(event_id);
1665
1666    name_contains_event_keyword(event.name.as_str(), &keywords)
1667        || event.parameters.iter().any(|param_id| {
1668            hir.variable(*param_id)
1669                .name
1670                .is_some_and(|name| name_contains_event_keyword(name.as_str(), &keywords))
1671        })
1672}
1673
1674fn state_var_event_keywords(name: &str) -> Vec<String> {
1675    let normalized = normalize_event_name_part(name);
1676    let mut out = Vec::from([normalized.clone()]);
1677
1678    if let Some(singular) = normalized.strip_suffix('s')
1679        && !singular.is_empty()
1680    {
1681        out.push(singular.to_string());
1682    }
1683
1684    for keyword in ["owner", "admin", "guardian", "manager", "role"] {
1685        if normalized.contains(keyword) {
1686            out.push(keyword.to_string());
1687        }
1688    }
1689
1690    out
1691}
1692
1693fn name_contains_event_keyword(name: &str, keywords: &[String]) -> bool {
1694    let normalized = normalize_event_name_part(name);
1695    keywords.iter().any(|keyword| !keyword.is_empty() && normalized.contains(keyword))
1696}
1697
1698fn normalize_event_name_part(name: &str) -> String {
1699    name.chars().filter(|ch| ch.is_ascii_alphanumeric()).map(|ch| ch.to_ascii_lowercase()).collect()
1700}
1701
1702fn called_function_ids(expr: &hir::Expr<'_>) -> HashSet<FunctionId> {
1703    let mut out = HashSet::new();
1704    collect_called_function_ids(expr, &mut out);
1705    out
1706}
1707
1708fn collect_called_function_ids(expr: &hir::Expr<'_>, out: &mut HashSet<FunctionId>) {
1709    match &expr.peel_parens().kind {
1710        ExprKind::Call(callee, args, opts) => {
1711            out.extend(resolved_function_ids(callee));
1712            collect_called_function_ids(callee, out);
1713            if let Some(opts) = opts {
1714                for opt in opts.args {
1715                    collect_called_function_ids(&opt.value, out);
1716                }
1717            }
1718            for arg in args.exprs() {
1719                collect_called_function_ids(arg, out);
1720            }
1721        }
1722        ExprKind::Assign(lhs, _, rhs) | ExprKind::Binary(lhs, _, rhs) => {
1723            collect_called_function_ids(lhs, out);
1724            collect_called_function_ids(rhs, out);
1725        }
1726        ExprKind::Unary(_, inner)
1727        | ExprKind::Delete(inner)
1728        | ExprKind::Member(inner, _)
1729        | ExprKind::Payable(inner) => collect_called_function_ids(inner, out),
1730        ExprKind::Index(base, index) => {
1731            collect_called_function_ids(base, out);
1732            if let Some(index) = index {
1733                collect_called_function_ids(index, out);
1734            }
1735        }
1736        ExprKind::Slice(base, start, end) => {
1737            collect_called_function_ids(base, out);
1738            if let Some(start) = start {
1739                collect_called_function_ids(start, out);
1740            }
1741            if let Some(end) = end {
1742                collect_called_function_ids(end, out);
1743            }
1744        }
1745        ExprKind::Ternary(cond, true_expr, false_expr) => {
1746            collect_called_function_ids(cond, out);
1747            collect_called_function_ids(true_expr, out);
1748            collect_called_function_ids(false_expr, out);
1749        }
1750        ExprKind::Array(exprs) => {
1751            for expr in *exprs {
1752                collect_called_function_ids(expr, out);
1753            }
1754        }
1755        ExprKind::Tuple(exprs) => {
1756            for expr in exprs.iter().copied().flatten() {
1757                collect_called_function_ids(expr, out);
1758            }
1759        }
1760        ExprKind::New(_)
1761        | ExprKind::TypeCall(_)
1762        | ExprKind::Type(_)
1763        | ExprKind::Ident(_)
1764        | ExprKind::Lit(_)
1765        | ExprKind::YulMember(..)
1766        | ExprKind::Err(_) => {}
1767    }
1768}
1769
1770fn resolved_function_ids<'hir>(
1771    callee: &'hir hir::Expr<'hir>,
1772) -> impl Iterator<Item = FunctionId> + 'hir {
1773    let reses = match &callee.peel_parens().kind {
1774        ExprKind::Ident(reses) => *reses,
1775        _ => &[],
1776    };
1777    reses.iter().filter_map(|res| match res {
1778        Res::Item(ItemId::Function(func_id)) => Some(*func_id),
1779        _ => None,
1780    })
1781}