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