Skip to main content

forge_lint/sol/low/
missing_events_arithmetic.rs

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