Skip to main content

forge_lint/sol/high/
arbitrary_send_eth.rs

1use super::ArbitrarySendEth;
2use crate::{
3    linter::{LateLintPass, LintContext},
4    sol::{Severity, SolLint},
5};
6use solar::{
7    ast::{self, LitKind},
8    interface::{Span, Symbol, data_structures::Never, kw, sym},
9    sema::{
10        Gcx, Ty,
11        builtins::Builtin,
12        hir::{
13            self, CallArgs, ContractKind, ElementaryType, ExprKind, FunctionId, FunctionKind,
14            ItemId, LoopSource, Res, StmtKind, TypeKind, Visit,
15        },
16        ty::TyKind,
17    },
18};
19use std::{
20    collections::{HashMap, HashSet},
21    ops::ControlFlow,
22};
23
24declare_forge_lint!(
25    ARBITRARY_SEND_ETH,
26    Severity::High,
27    "arbitrary-send-eth",
28    "ETH is sent to a user-controlled destination; restrict the destination or the caller"
29);
30
31impl<'hir> LateLintPass<'hir> for ArbitrarySendEth {
32    fn check_function(
33        &mut self,
34        ctx: &LintContext,
35        gcx: Gcx<'hir>,
36        hir: &'hir hir::Hir<'hir>,
37        func: &'hir hir::Function<'hir>,
38    ) {
39        if matches!(func.state_mutability, ast::StateMutability::Pure | ast::StateMutability::View)
40            || matches!(func.kind, FunctionKind::Constructor)
41            || func.contract.is_some_and(|cid| hir.contract(cid).kind == ContractKind::Library)
42        {
43            return;
44        }
45        let Some(body) = func.body else { return };
46        let mut entry = Analyzer::new(gcx, hir);
47        for m in func.modifiers {
48            for arg in m.args.exprs() {
49                let _ = entry.visit_expr(arg);
50            }
51        }
52        for span in &entry.hits {
53            ctx.emit(&ARBITRARY_SEND_ETH, *span);
54        }
55        let mut analyzer = Analyzer::new(gcx, hir);
56        for m in func.modifiers {
57            collect_modifier_safety(gcx, hir, m, &mut analyzer.safe_vars);
58        }
59        for stmt in body.stmts {
60            let _ = analyzer.visit_stmt(stmt);
61            if branch_always_exits(stmt) {
62                break;
63            }
64        }
65        if analyzer.hits.is_empty() {
66            return;
67        }
68        if func.modifiers.iter().any(|m| modifier_restricts_caller(gcx, hir, m)) {
69            return;
70        }
71        for span in analyzer.hits {
72            ctx.emit(&ARBITRARY_SEND_ETH, span);
73        }
74    }
75}
76
77struct Analyzer<'hir> {
78    gcx: Gcx<'hir>,
79    hir: &'hir hir::Hir<'hir>,
80    self_aliases: SelfAliasAnalysis<'hir>,
81    /// Locals/non-state vars proven equal to a safe origin on this path.
82    safe_vars: HashSet<hir::VariableId>,
83    /// Function-pointer locals proven to route to `this` on this path.
84    safe_fn_ptrs: HashSet<hir::VariableId>,
85    /// True once a caller-restricting guard has fired on this path.
86    caller_restricted: bool,
87    hits: Vec<Span>,
88}
89
90#[derive(Clone)]
91struct FlowState {
92    safe_vars: HashSet<hir::VariableId>,
93    safe_fn_ptrs: HashSet<hir::VariableId>,
94    caller_restricted: bool,
95}
96
97impl FlowState {
98    fn intersection(a: &Self, b: &Self) -> Self {
99        Self {
100            safe_vars: a.safe_vars.intersection(&b.safe_vars).copied().collect(),
101            safe_fn_ptrs: a.safe_fn_ptrs.intersection(&b.safe_fn_ptrs).copied().collect(),
102            caller_restricted: a.caller_restricted && b.caller_restricted,
103        }
104    }
105
106    fn intersection_all(mut states: impl Iterator<Item = Self>) -> Self {
107        let mut out = states.next().unwrap_or_else(|| Self {
108            safe_vars: HashSet::new(),
109            safe_fn_ptrs: HashSet::new(),
110            caller_restricted: false,
111        });
112        for state in states {
113            out = Self::intersection(&out, &state);
114        }
115        out
116    }
117}
118
119/// Recursion budget for `_msgSender()`-style helper chains.
120const HELPER_DEPTH: u8 = 3;
121
122/// Recursion budget for self-alias chains.
123const SELF_ALIAS_DEPTH: u8 = 8;
124
125impl<'hir> Analyzer<'hir> {
126    fn new(gcx: Gcx<'hir>, hir: &'hir hir::Hir<'hir>) -> Self {
127        Self {
128            gcx,
129            hir,
130            self_aliases: SelfAliasAnalysis::new(gcx, hir),
131            safe_vars: HashSet::new(),
132            safe_fn_ptrs: HashSet::new(),
133            caller_restricted: false,
134            hits: Vec::new(),
135        }
136    }
137
138    fn snapshot(&self) -> FlowState {
139        FlowState {
140            safe_vars: self.safe_vars.clone(),
141            safe_fn_ptrs: self.safe_fn_ptrs.clone(),
142            caller_restricted: self.caller_restricted,
143        }
144    }
145
146    fn restore(&mut self, state: FlowState) {
147        self.safe_vars = state.safe_vars;
148        self.safe_fn_ptrs = state.safe_fn_ptrs;
149        self.caller_restricted = state.caller_restricted;
150    }
151
152    fn is_safe(&self, expr: &'hir hir::Expr<'hir>) -> bool {
153        self.is_safe_inner(expr, HELPER_DEPTH)
154    }
155
156    fn is_safe_inner(&self, expr: &'hir hir::Expr<'hir>, depth: u8) -> bool {
157        match &expr.peel_parens().kind {
158            ExprKind::Member(base, ident) if ident.name == sym::sender => {
159                is_builtin(base, sym::msg)
160            }
161            ExprKind::Member(base, ident) if ident.name == kw::Origin => is_builtin(base, sym::tx),
162            ExprKind::Ident(_) if is_builtin(expr, sym::this) => true,
163            // Address literals are safe; only `0` is accepted among numeric literals.
164            ExprKind::Lit(lit) => match &lit.kind {
165                LitKind::Address(_) => true,
166                LitKind::Number(n) => n.is_zero(),
167                _ => false,
168            },
169            ExprKind::Ident(reses) => reses.iter().any(|r| match r {
170                Res::Item(ItemId::Variable(vid)) => self.is_safe_var(*vid),
171                _ => false,
172            }),
173            // Peel address and numeric casts so `payable(address(uint160(0)))` is safe.
174            ExprKind::Call(callee, args, _)
175                if is_address_like_cast_callee(callee) || is_numeric_cast_callee(callee) =>
176            {
177                args.exprs().next().is_some_and(|e| self.is_safe_inner(e, depth))
178            }
179            ExprKind::Payable(inner) => self.is_safe_inner(inner, depth),
180            ExprKind::Ternary(_, t, f) => {
181                self.is_safe_inner(t, depth) && self.is_safe_inner(f, depth)
182            }
183            ExprKind::Call(callee, args, _)
184                if depth > 0
185                    && args.exprs().next().is_none()
186                    && callee_no_arg_returns(self.hir, callee, |e| {
187                        self.is_safe_inner(e, depth - 1)
188                    }) =>
189            {
190                true
191            }
192            _ => false,
193        }
194    }
195
196    /// True when `vid` is currently in `safe_vars`, or is an `immutable`/`constant`
197    /// address-typed state variable.
198    fn is_safe_var(&self, vid: hir::VariableId) -> bool {
199        if self.safe_vars.contains(&vid) {
200            return true;
201        }
202        let var = self.hir.variable(vid);
203        var.kind.is_state() && (var.is_immutable() || var.is_constant()) && var_is_address_like(var)
204    }
205
206    /// `target = rhs`: update `safe_vars` for non-state targets.
207    fn assign(&mut self, target: hir::VariableId, rhs: &'hir hir::Expr<'hir>) {
208        if self.is_safe(rhs) {
209            self.safe_vars.insert(target);
210        } else {
211            self.safe_vars.remove(&target);
212        }
213    }
214
215    /// Handles single-var and tuple LHS; tuple slots align with a tuple-literal RHS.
216    fn handle_assign(&mut self, lhs: &hir::Expr<'_>, rhs: &'hir hir::Expr<'hir>) {
217        let lhs = lhs.peel_parens();
218        if let ExprKind::Tuple(lhs_elems) = &lhs.kind {
219            let rhs_elems = tuple_elems(rhs);
220            for (i, lhs_elem) in lhs_elems.iter().enumerate() {
221                if let Some(lhs_expr) = lhs_elem {
222                    self.assign_one(lhs_expr, tuple_slot(rhs_elems, i));
223                }
224            }
225        } else {
226            self.assign_one(lhs, Some(rhs));
227        }
228    }
229
230    /// `rhs == None` (unknown slot) drops the target's safe-fact.
231    fn assign_one(&mut self, lhs: &hir::Expr<'_>, rhs: Option<&'hir hir::Expr<'hir>>) {
232        let Some(target) = underlying_var(lhs) else { return };
233        self.safe_vars.remove(&target);
234        self.safe_fn_ptrs.remove(&target);
235        if self.hir.variable(target).kind.is_state() {
236            return;
237        }
238        if matches!(self.hir.variable(target).ty.kind, TypeKind::Function(_)) {
239            if rhs.is_some_and(|r| self.is_fn_ptr_safe_rhs(r)) {
240                self.safe_fn_ptrs.insert(target);
241            }
242            return;
243        }
244        if rhs.is_some_and(|r| self.is_safe(r)) {
245            self.safe_vars.insert(target);
246        }
247    }
248
249    /// True when `expr` is a function-pointer value whose destination is `this`.
250    fn is_fn_ptr_safe_rhs(&self, expr: &hir::Expr<'_>) -> bool {
251        match &expr.peel_parens().kind {
252            ExprKind::Member(base, _) => is_address_self(base),
253            ExprKind::Ident(reses) => reses.iter().any(|r| {
254                matches!(r, Res::Item(ItemId::Variable(vid)) if self.safe_fn_ptrs.contains(vid))
255            }),
256            ExprKind::Ternary(_, t, f) => self.is_fn_ptr_safe_rhs(t) && self.is_fn_ptr_safe_rhs(f),
257            _ => false,
258        }
259    }
260
261    /// True when `expr` is a fn-pointer call whose destination is provably `this`.
262    fn fn_ptr_call_routes_to_self(&self, expr: &'hir hir::Expr<'hir>) -> bool {
263        let ExprKind::Call(callee, _, _) = &expr.kind else { return false };
264        let callee_inner = callee.peel_parens();
265        let is_fn_ptr = match &callee_inner.kind {
266            ExprKind::Ident(reses) => reses.iter().any(|r| {
267                matches!(r, Res::Item(ItemId::Variable(vid))
268                    if matches!(self.hir.variable(*vid).ty.kind, TypeKind::Function(_)))
269            }),
270            _ => expr_is_function(self.gcx, callee_inner),
271        };
272        is_fn_ptr && self.is_fn_ptr_safe_rhs(callee_inner)
273    }
274
275    /// Records vars proven equal to a safe origin from `pred`. `negate = true` flips polarity.
276    fn add_facts(&mut self, pred: &'hir hir::Expr<'hir>, negate: bool) {
277        match &pred.peel_parens().kind {
278            ExprKind::Binary(lhs, op, rhs) => {
279                let (eq, and_op, or_op) = if negate {
280                    (ast::BinOpKind::Ne, ast::BinOpKind::Or, ast::BinOpKind::And)
281                } else {
282                    (ast::BinOpKind::Eq, ast::BinOpKind::And, ast::BinOpKind::Or)
283                };
284                if op.kind == and_op {
285                    self.add_facts(lhs, negate);
286                    self.add_facts(rhs, negate);
287                } else if op.kind == or_op {
288                    self.add_facts_disjunction(lhs, rhs, negate);
289                } else if op.kind == eq {
290                    for (a, b) in [(lhs, rhs), (rhs, lhs)] {
291                        if self.is_safe(a)
292                            && let Some(v) = underlying_var(b)
293                            && self.is_safe_target(v)
294                        {
295                            self.safe_vars.insert(v);
296                        }
297                    }
298                }
299            }
300            ExprKind::Unary(op, inner) if matches!(op.kind, ast::UnOpKind::Not) => {
301                self.add_facts(inner, !negate);
302            }
303            _ => {}
304        }
305    }
306
307    /// `lhs ∨ rhs`: a safety fact is added only if it holds under both arms.
308    fn add_facts_disjunction(
309        &mut self,
310        lhs: &'hir hir::Expr<'hir>,
311        rhs: &'hir hir::Expr<'hir>,
312        negate: bool,
313    ) {
314        let baseline = self.safe_vars.clone();
315        self.add_facts(lhs, negate);
316        let lhs_added: HashSet<_> = self.safe_vars.difference(&baseline).copied().collect();
317        self.safe_vars.clone_from(&baseline);
318        self.add_facts(rhs, negate);
319        let rhs_added: HashSet<_> = self.safe_vars.difference(&baseline).copied().collect();
320        self.safe_vars = baseline;
321        for v in lhs_added.intersection(&rhs_added) {
322            self.safe_vars.insert(*v);
323        }
324    }
325
326    /// A variable can carry a safe-fact iff it's a local/param or an `immutable`/`constant`
327    fn is_safe_target(&self, v: hir::VariableId) -> bool {
328        let var = self.hir.variable(v);
329        !var.kind.is_state() || var.is_immutable() || var.is_constant()
330    }
331
332    /// Visits a body that may execute zero times or out-of-line (loops, try clauses).
333    fn visit_isolated(&mut self, stmts: &'hir [hir::Stmt<'hir>]) {
334        let mut exits = vec![self.snapshot()];
335        if let Some(fallthrough) = self.visit_stmts_until_loop_exit(stmts, &mut exits) {
336            exits.push(fallthrough);
337        }
338        self.restore(FlowState::intersection_all(exits.into_iter()));
339    }
340
341    fn visit_stmts_until_loop_exit(
342        &mut self,
343        stmts: &'hir [hir::Stmt<'hir>],
344        exits: &mut Vec<FlowState>,
345    ) -> Option<FlowState> {
346        for stmt in stmts {
347            self.visit_stmt_until_loop_exit(stmt, exits)?;
348        }
349        Some(self.snapshot())
350    }
351
352    fn visit_stmt_until_loop_exit(
353        &mut self,
354        stmt: &'hir hir::Stmt<'hir>,
355        exits: &mut Vec<FlowState>,
356    ) -> Option<()> {
357        match &stmt.kind {
358            StmtKind::Break | StmtKind::Continue => {
359                exits.push(self.snapshot());
360                None
361            }
362            StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => {
363                let state = self.visit_stmts_until_loop_exit(block.stmts, exits)?;
364                self.restore(state);
365                Some(())
366            }
367            StmtKind::If(cond, then, else_) => {
368                let _ = self.visit_expr(cond);
369                let baseline = self.snapshot();
370                self.add_facts(cond, false);
371                let then_fallthrough = self
372                    .visit_stmt_until_loop_exit(then, exits)
373                    .and_then(|_| (!branch_always_exits(then)).then(|| self.snapshot()));
374                self.restore(baseline);
375                self.add_facts(cond, true);
376                let else_fallthrough = match else_ {
377                    Some(else_stmt) => self
378                        .visit_stmt_until_loop_exit(else_stmt, exits)
379                        .and_then(|_| (!branch_always_exits(else_stmt)).then(|| self.snapshot())),
380                    None => Some(self.snapshot()),
381                };
382                match (then_fallthrough, else_fallthrough) {
383                    (Some(then_state), Some(else_state)) => {
384                        self.restore(FlowState::intersection(&then_state, &else_state));
385                        Some(())
386                    }
387                    (Some(state), None) | (None, Some(state)) => {
388                        self.restore(state);
389                        Some(())
390                    }
391                    (None, None) => None,
392                }
393            }
394            StmtKind::Loop(..) => {
395                let _ = self.visit_stmt(stmt);
396                Some(())
397            }
398            _ => {
399                let _ = self.visit_stmt(stmt);
400                (!branch_always_exits(stmt)).then_some(())
401            }
402        }
403    }
404}
405
406impl<'hir> hir::Visit<'hir> for Analyzer<'hir> {
407    type BreakValue = Never;
408
409    fn hir(&self) -> &'hir hir::Hir<'hir> {
410        self.hir
411    }
412
413    fn visit_stmt(&mut self, stmt: &'hir hir::Stmt<'hir>) -> ControlFlow<Self::BreakValue> {
414        match &stmt.kind {
415            StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => {
416                for s in block.stmts {
417                    let _ = self.visit_stmt(s);
418                    if branch_always_exits(s) {
419                        break;
420                    }
421                }
422                return ControlFlow::Continue(());
423            }
424            StmtKind::If(cond, then, else_) => {
425                let _ = self.visit_expr(cond);
426                let baseline = self.snapshot();
427                self.add_facts(cond, false);
428                if cond_restricts_caller(self.hir, cond, true, &[], &mut self.self_aliases) {
429                    self.caller_restricted = true;
430                }
431                let _ = self.visit_stmt(then);
432                let then_exits = branch_always_exits(then);
433                let after_then = self.snapshot();
434                self.restore(baseline);
435                self.add_facts(cond, true);
436                if cond_restricts_caller(self.hir, cond, false, &[], &mut self.self_aliases) {
437                    self.caller_restricted = true;
438                }
439                let else_exits = match else_ {
440                    Some(e) => {
441                        let _ = self.visit_stmt(e);
442                        branch_always_exits(e)
443                    }
444                    None => false,
445                };
446                let after_else = self.snapshot();
447                // When both branches exit, the joined state is never read (the caller
448                // breaks on `branch_always_exits`), so intersection is a safe default.
449                let joined = match (then_exits, else_exits) {
450                    (true, false) => after_else,
451                    (false, true) => after_then,
452                    _ => FlowState::intersection(&after_then, &after_else),
453                };
454                self.restore(joined);
455                return ControlFlow::Continue(());
456            }
457            StmtKind::Loop(block, source) => {
458                if matches!(source, LoopSource::DoWhile)
459                    && !do_while_user_stmts(block.stmts).iter().any(stmt_has_break_or_continue)
460                {
461                    for s in block.stmts {
462                        let _ = self.visit_stmt(s);
463                    }
464                } else {
465                    self.visit_isolated(block.stmts);
466                }
467                return ControlFlow::Continue(());
468            }
469            StmtKind::Try(t) => {
470                let _ = self.visit_expr(&t.expr);
471                let outer = self.snapshot();
472                let mut clause_exits = Vec::new();
473                for clause in t.clauses {
474                    self.restore(outer.clone());
475                    let mut exited = false;
476                    for stmt in clause.block.stmts {
477                        let _ = self.visit_stmt(stmt);
478                        if branch_always_exits(stmt) {
479                            exited = true;
480                            break;
481                        }
482                    }
483                    if !exited {
484                        clause_exits.push(self.snapshot());
485                    }
486                }
487                self.restore(
488                    clause_exits
489                        .into_iter()
490                        .reduce(|a, b| FlowState::intersection(&a, &b))
491                        .unwrap_or(outer),
492                );
493                return ControlFlow::Continue(());
494            }
495            StmtKind::Err(_) => {
496                self.safe_vars.clear();
497            }
498            StmtKind::DeclSingle(vid) => {
499                let var = self.hir.variable(*vid);
500                if var_is_address_like(var)
501                    && let Some(init) = var.initializer
502                {
503                    self.assign(*vid, init);
504                } else if matches!(var.ty.kind, TypeKind::Function(_)) {
505                    if var.initializer.is_some_and(|init| self.is_fn_ptr_safe_rhs(init)) {
506                        self.safe_fn_ptrs.insert(*vid);
507                    } else {
508                        self.safe_fn_ptrs.remove(vid);
509                    }
510                }
511            }
512            StmtKind::DeclMulti(vars, init) => {
513                if let ExprKind::Tuple(rhs) = &init.peel_parens().kind {
514                    for (lhs, rhs) in vars.iter().zip(rhs.iter()) {
515                        let (Some(vid), Some(expr)) = (lhs, rhs) else { continue };
516                        let var = self.hir.variable(*vid);
517                        if var_is_address_like(var) {
518                            self.assign(*vid, expr);
519                        } else if matches!(var.ty.kind, TypeKind::Function(_)) {
520                            if self.is_fn_ptr_safe_rhs(expr) {
521                                self.safe_fn_ptrs.insert(*vid);
522                            } else {
523                                self.safe_fn_ptrs.remove(vid);
524                            }
525                        }
526                    }
527                }
528            }
529            _ => {}
530        }
531        self.walk_stmt(stmt)
532    }
533
534    fn visit_expr(&mut self, expr: &'hir hir::Expr<'hir>) -> ControlFlow<Self::BreakValue> {
535        if let ExprKind::Binary(lhs, op, rhs) = &expr.kind
536            && matches!(op.kind, ast::BinOpKind::And | ast::BinOpKind::Or)
537        {
538            let _ = self.visit_expr(lhs);
539            let negate = matches!(op.kind, ast::BinOpKind::Or);
540            let skipped_rhs = self.snapshot();
541            self.add_facts(lhs, negate);
542            let result = self.visit_expr(rhs);
543            let ran_rhs = self.snapshot();
544            self.restore(FlowState::intersection(&skipped_rhs, &ran_rhs));
545            return result;
546        }
547        if let ExprKind::Ternary(cond, then_e, else_e) = &expr.kind {
548            let _ = self.visit_expr(cond);
549            let pre_arm = self.snapshot();
550            self.add_facts(cond, false);
551            let _ = self.visit_expr(then_e);
552            let post_then = self.snapshot();
553            self.restore(pre_arm);
554            self.add_facts(cond, true);
555            let _ = self.visit_expr(else_e);
556            let post_else = self.snapshot();
557            self.restore(FlowState::intersection(&post_then, &post_else));
558            return ControlFlow::Continue(());
559        }
560        match &expr.kind {
561            ExprKind::Call(callee, args, _) if is_require_or_assert(callee) => {
562                let result = self.walk_expr(expr);
563                if let Some(cond) = args.exprs().next() {
564                    self.add_facts(cond, false);
565                    if cond_restricts_caller(self.hir, cond, true, &[], &mut self.self_aliases) {
566                        self.caller_restricted = true;
567                    }
568                }
569                return result;
570            }
571            ExprKind::Call(..) => {
572                if !self.caller_restricted
573                    && let Some(dest) = match_sink(self.gcx, self.hir, expr)
574                    && !self.is_safe(dest)
575                    && !self.fn_ptr_call_routes_to_self(expr)
576                {
577                    self.hits.push(expr.span);
578                }
579            }
580            ExprKind::Assign(lhs, _, rhs) => self.handle_assign(lhs, rhs),
581            ExprKind::Delete(target) => self.assign_one(target.peel_parens(), None),
582            _ => {}
583        }
584        self.walk_expr(expr)
585    }
586}
587
588/// Returns the destination expression when `expr` is an ETH-sending sink.
589fn match_sink<'hir>(
590    gcx: Gcx<'hir>,
591    hir: &'hir hir::Hir<'hir>,
592    expr: &'hir hir::Expr<'hir>,
593) -> Option<&'hir hir::Expr<'hir>> {
594    let ExprKind::Call(callee, args, opts) = &expr.kind else { return None };
595    if let ExprKind::Ident(reses) = &callee.peel_parens().kind
596        && reses.iter().any(|r| matches!(r, Res::Builtin(Builtin::Selfdestruct)))
597    {
598        let dest = args.exprs().next()?;
599        if is_address_self(dest) {
600            return None;
601        }
602        return Some(dest);
603    }
604
605    if let Some(opts) = opts
606        && opts.args.iter().any(|arg| arg.name.name == sym::value && !is_literal_zero(&arg.value))
607    {
608        let callee_inner = callee.peel_parens();
609        match &callee_inner.kind {
610            ExprKind::Member(recv, _) if !is_address_self(recv) => return Some(recv),
611            ExprKind::Ident(reses)
612                if reses.iter().any(|r| {
613                    matches!(
614                        r,
615                        Res::Item(ItemId::Variable(vid))
616                            if matches!(hir.variable(*vid).ty.kind, TypeKind::Function(_))
617                    )
618                }) =>
619            {
620                return Some(callee);
621            }
622            _ if expr_is_function(gcx, callee_inner) => {
623                return Some(callee);
624            }
625            _ => {}
626        }
627    }
628
629    let ExprKind::Member(recv, member) = &callee.peel_parens().kind else { return None };
630    if matches!(member.name, sym::transfer | sym::send)
631        && args.len() == 1
632        && receiver_is_address(gcx, recv)
633        && !is_address_self(recv)
634    {
635        let amt = args.exprs().next()?;
636        if !is_literal_zero(amt) {
637            return Some(recv);
638        }
639    }
640    match_eth_library_call(gcx, hir, recv, member.name, args)
641}
642
643/// Recognises common OZ/Solady ETH-sending helpers and returns the destination expression.
644fn match_eth_library_call<'hir>(
645    gcx: Gcx<'hir>,
646    hir: &'hir hir::Hir<'hir>,
647    recv: &'hir hir::Expr<'hir>,
648    member: Symbol,
649    args: &'hir CallArgs<'hir>,
650) -> Option<&'hir hir::Expr<'hir>> {
651    let n = args.len();
652    let using = receiver_is_address(gcx, recv);
653    let recv_is_lib = matches!(&recv.peel_parens().kind, ExprKind::Ident(reses)
654    if reses.iter().any(|r| matches!(
655        r,
656        Res::Item(ItemId::Contract(cid))
657            if hir.contract(*cid).kind == ContractKind::Library
658    )));
659    if !using && !recv_is_lib {
660        return None;
661    }
662    let name = member.as_str();
663    let valid = match name {
664        "sendValue" | "safeTransferETH" | "safeMoveETH" => (using && n == 1) || (!using && n == 2),
665        "forceSafeTransferETH" => (using && matches!(n, 1 | 2)) || (!using && matches!(n, 2 | 3)),
666        "trySafeTransferETH" => (using && n == 2) || (!using && n == 3),
667        "functionCallWithValue" => (using && matches!(n, 2 | 3)) || (!using && matches!(n, 3 | 4)),
668        "safeTransferAllETH" => (using && n == 0) || (!using && n == 1),
669        "forceSafeTransferAllETH" => {
670            (using && matches!(n, 0 | 1)) || (!using && matches!(n, 1 | 2))
671        }
672        "trySafeTransferAllETH" => (using && n == 1) || (!using && n == 2),
673        _ => false,
674    };
675
676    if !valid {
677        return None;
678    }
679
680    let dest = if using { recv } else { arg(args, 0, &["to", "target", "recipient"])? };
681    let amount = match name {
682        "safeTransferAllETH" | "forceSafeTransferAllETH" | "trySafeTransferAllETH" => None,
683        "functionCallWithValue" => {
684            Some(arg(args, if using { 1 } else { 2 }, &["value", "amount"])?)
685        }
686        _ => Some(arg(args, if using { 0 } else { 1 }, &["amount", "value"])?),
687    };
688    if amount.is_some_and(is_literal_zero) || is_address_self(dest) {
689        return None;
690    }
691    Some(dest)
692}
693
694/// Looks up call-site arg `pos` (positional) or any name in `names` (named-arg form).
695fn arg<'hir>(
696    args: &'hir CallArgs<'hir>,
697    pos: usize,
698    names: &[&str],
699) -> Option<&'hir hir::Expr<'hir>> {
700    match args.kind {
701        hir::CallArgsKind::Unnamed(exprs) => exprs.get(pos),
702        hir::CallArgsKind::Named(named) => {
703            named.iter().find(|a| names.iter().any(|n| a.name.as_str() == *n)).map(|a| &a.value)
704        }
705    }
706}
707
708/// True when a modifier reverts unless `msg.sender` equals a trusted principal.
709fn modifier_restricts_caller<'hir>(
710    gcx: Gcx<'hir>,
711    hir: &'hir hir::Hir<'hir>,
712    invocation: &hir::Modifier<'_>,
713) -> bool {
714    let ItemId::Function(fid) = invocation.id else { return false };
715    let mut self_aliases = SelfAliasAnalysis::new(gcx, hir);
716    modifier_function_restricts_caller(hir, fid, &mut Vec::new(), &mut self_aliases)
717}
718
719/// Resolves the `FunctionId` invoked by a modifier or base-constructor invocation.
720fn invoked_function(hir: &hir::Hir<'_>, invocation: &hir::Modifier<'_>) -> Option<FunctionId> {
721    match invocation.id {
722        ItemId::Function(fid) => Some(fid),
723        ItemId::Contract(cid) => hir.contract(cid).ctor,
724        _ => None,
725    }
726}
727
728fn modifier_function_restricts_caller<'hir>(
729    hir: &'hir hir::Hir<'hir>,
730    fid: FunctionId,
731    stack: &mut Vec<FunctionId>,
732    self_aliases: &mut SelfAliasAnalysis<'hir>,
733) -> bool {
734    if stack.contains(&fid) {
735        return false;
736    }
737    let Some((modifier, prefix)) = modifier_prefix(hir, fid) else { return false };
738    stack.push(fid);
739    let restricts = prefix
740        .iter()
741        .any(|s| stmt_restricts_caller(hir, s, modifier.parameters, stack, self_aliases));
742    stack.pop();
743    restricts
744}
745
746/// Returns the modifier function and the statements preceding its unique `_;` placeholder,
747/// or `None` when `fid` is not an eligible single-placeholder modifier.
748fn modifier_prefix<'hir>(
749    hir: &'hir hir::Hir<'hir>,
750    fid: FunctionId,
751) -> Option<(&'hir hir::Function<'hir>, Vec<&'hir hir::Stmt<'hir>>)> {
752    let modifier = hir.function(fid);
753    if !matches!(modifier.kind, FunctionKind::Modifier) {
754        return None;
755    }
756    let body = modifier.body?;
757    if count_placeholders(body.stmts) != 1 {
758        return None;
759    }
760    let mut prefix = Vec::new();
761    collect_stmts_before_placeholder(body.stmts, &mut prefix)?;
762    Some((modifier, prefix))
763}
764
765fn stmt_restricts_caller<'hir>(
766    hir: &'hir hir::Hir<'hir>,
767    stmt: &'hir hir::Stmt<'hir>,
768    params: &[hir::VariableId],
769    stack: &mut Vec<FunctionId>,
770    self_aliases: &mut SelfAliasAnalysis<'hir>,
771) -> bool {
772    match &stmt.kind {
773        StmtKind::Expr(e) => expr_restricts_caller(hir, e, params, stack, self_aliases),
774        StmtKind::If(cond, then, else_) => {
775            let then_exits = branch_always_exits(then);
776            let else_exits = else_.as_ref().is_some_and(|e| branch_always_exits(e));
777            let by_if_revert = match (then_exits, else_exits) {
778                (true, false) => cond_restricts_caller(hir, cond, false, params, self_aliases),
779                (false, true) => cond_restricts_caller(hir, cond, true, params, self_aliases),
780                _ => false,
781            };
782            if by_if_revert {
783                return true;
784            }
785            let then_restricts = stmt_restricts_caller(hir, then, params, stack, self_aliases);
786            let else_restricts = else_
787                .as_ref()
788                .is_some_and(|e| stmt_restricts_caller(hir, e, params, stack, self_aliases));
789            match (then_exits, else_exits) {
790                (true, true) => true,
791                (true, false) => else_.is_some() && else_restricts,
792                (false, true) => then_restricts,
793                (false, false) => then_restricts && else_.is_some() && else_restricts,
794            }
795        }
796        StmtKind::Block(b) | StmtKind::UncheckedBlock(b) => {
797            b.stmts.iter().any(|s| stmt_restricts_caller(hir, s, params, stack, self_aliases))
798        }
799        _ => false,
800    }
801}
802
803fn expr_restricts_caller<'hir>(
804    hir: &'hir hir::Hir<'hir>,
805    expr: &'hir hir::Expr<'hir>,
806    params: &[hir::VariableId],
807    stack: &mut Vec<FunctionId>,
808    self_aliases: &mut SelfAliasAnalysis<'hir>,
809) -> bool {
810    let ExprKind::Call(callee, args, _) = &expr.peel_parens().kind else { return false };
811    if is_require_or_assert(callee) {
812        return args
813            .exprs()
814            .next()
815            .is_some_and(|c| cond_restricts_caller(hir, c, true, params, self_aliases));
816    }
817    let ExprKind::Ident(reses) = &callee.peel_parens().kind else { return false };
818    reses.iter().any(|r| match r {
819        Res::Item(ItemId::Function(fid)) => {
820            if stack.contains(fid) {
821                return false;
822            }
823            let f = hir.function(*fid);
824            let Some(body) = f.body else { return false };
825            // Trailing bare `return;` is a normal exit and cannot bypass an earlier guard.
826            let mut stmts = body.stmts;
827            while let Some((last, init)) = stmts.split_last() {
828                if matches!(last.kind, StmtKind::Return(None)) {
829                    stmts = init;
830                } else {
831                    break;
832                }
833            }
834            if stmts.iter().any(stmt_contains_return) {
835                return false;
836            }
837            stack.push(*fid);
838            let r = stmts
839                .iter()
840                .any(|s| stmt_restricts_caller(hir, s, f.parameters, stack, self_aliases));
841            stack.pop();
842            r
843        }
844        _ => false,
845    })
846}
847
848/// True when any reachable statement is `return`. Used to disqualify caller-restricting
849/// helpers that might return without reverting.
850fn stmt_contains_return(stmt: &hir::Stmt<'_>) -> bool {
851    match &stmt.kind {
852        StmtKind::Return(_) => true,
853        StmtKind::Block(b) | StmtKind::UncheckedBlock(b) | StmtKind::Loop(b, _) => {
854            b.stmts.iter().any(stmt_contains_return)
855        }
856        StmtKind::If(_, t, e) => {
857            stmt_contains_return(t) || e.as_ref().is_some_and(|s| stmt_contains_return(s))
858        }
859        StmtKind::Try(t) => {
860            t.clauses.iter().any(|c| c.block.stmts.iter().any(stmt_contains_return))
861        }
862        _ => false,
863    }
864}
865
866/// True when `cond` entails `msg.sender == trusted` along every accepting path.
867fn cond_restricts_caller<'hir>(
868    hir: &'hir hir::Hir<'hir>,
869    cond: &'hir hir::Expr<'hir>,
870    polarity: bool,
871    params: &[hir::VariableId],
872    self_aliases: &mut SelfAliasAnalysis<'hir>,
873) -> bool {
874    match &cond.peel_parens().kind {
875        ExprKind::Binary(lhs, op, rhs) => {
876            let (eq, any_op, all_op) = if polarity {
877                (ast::BinOpKind::Eq, ast::BinOpKind::And, ast::BinOpKind::Or)
878            } else {
879                (ast::BinOpKind::Ne, ast::BinOpKind::Or, ast::BinOpKind::And)
880            };
881            if op.kind == any_op {
882                cond_restricts_caller(hir, lhs, polarity, params, self_aliases)
883                    || cond_restricts_caller(hir, rhs, polarity, params, self_aliases)
884            } else if op.kind == all_op {
885                cond_restricts_caller(hir, lhs, polarity, params, self_aliases)
886                    && cond_restricts_caller(hir, rhs, polarity, params, self_aliases)
887            } else if op.kind == eq {
888                let mut pair = |a: &'hir hir::Expr<'hir>, b: &'hir hir::Expr<'hir>| {
889                    is_msg_sender_like(hir, a, HELPER_DEPTH)
890                        && is_trusted_principal_inner(hir, b, params, HELPER_DEPTH, self_aliases)
891                };
892                pair(lhs, rhs) || pair(rhs, lhs)
893            } else {
894                false
895            }
896        }
897        ExprKind::Unary(op, inner) if matches!(op.kind, ast::UnOpKind::Not) => {
898            cond_restricts_caller(hir, inner, !polarity, params, self_aliases)
899        }
900        _ => false,
901    }
902}
903
904/// `msg.sender` modulo parens / casts / `payable(...)` / no-arg helpers.
905fn is_msg_sender_like<'hir>(
906    hir: &'hir hir::Hir<'hir>,
907    expr: &'hir hir::Expr<'hir>,
908    depth: u8,
909) -> bool {
910    is_caller_like(hir, expr, depth, sym::msg, sym::sender)
911}
912
913/// `tx.origin` modulo parens / casts / `payable(...)` / no-arg helpers.
914fn is_tx_origin_like<'hir>(
915    hir: &'hir hir::Hir<'hir>,
916    expr: &'hir hir::Expr<'hir>,
917    depth: u8,
918) -> bool {
919    is_caller_like(hir, expr, depth, sym::tx, kw::Origin)
920}
921
922/// True when `callee` is a zero-arg function whose body is `return <pred-matching>;`.
923fn callee_no_arg_returns<'hir>(
924    hir: &'hir hir::Hir<'hir>,
925    callee: &'hir hir::Expr<'hir>,
926    mut pred: impl FnMut(&'hir hir::Expr<'hir>) -> bool,
927) -> bool {
928    let ExprKind::Ident(reses) = &callee.peel_parens().kind else { return false };
929    reses.iter().any(|r| {
930        matches!(r, Res::Item(ItemId::Function(fid)) if function_no_arg_returns(hir, *fid, &mut pred))
931    })
932}
933
934/// True when `fid` is a zero-parameter function whose body is `return expr;`,
935/// or `namedRet = expr;` (with an optional trailing bare `return;`).
936fn function_no_arg_returns<'hir>(
937    hir: &'hir hir::Hir<'hir>,
938    fid: FunctionId,
939    pred: &mut impl FnMut(&'hir hir::Expr<'hir>) -> bool,
940) -> bool {
941    let f = hir.function(fid);
942    let Some(body) = f.body else { return false };
943    if !f.parameters.is_empty() {
944        return false;
945    }
946    // A trailing bare `return;` is a no-op; ignore it before matching the body shape.
947    let stmts: &[_] = match body.stmts.split_last() {
948        Some((last, rest)) if matches!(last.kind, StmtKind::Return(None)) => rest,
949        _ => body.stmts,
950    };
951    if stmts.len() != 1 {
952        return false;
953    }
954    match &stmts[0].kind {
955        StmtKind::Return(Some(e)) => pred(e),
956        // Named-return form: the sole named return is assigned the result.
957        StmtKind::Expr(e) => match &e.peel_parens().kind {
958            ExprKind::Assign(lhs, None, rhs) => {
959                f.returns.len() == 1
960                    && underlying_var(lhs).is_some_and(|v| v == f.returns[0])
961                    && pred(rhs)
962            }
963            _ => false,
964        },
965        _ => false,
966    }
967}
968
969/// Shared shape for `msg.sender` / `tx.origin` recognition.
970fn is_caller_like<'hir>(
971    hir: &'hir hir::Hir<'hir>,
972    expr: &'hir hir::Expr<'hir>,
973    depth: u8,
974    ns: Symbol,
975    member: Symbol,
976) -> bool {
977    match &expr.peel_parens().kind {
978        ExprKind::Member(base, ident) if ident.name == member => is_builtin(base, ns),
979        ExprKind::Payable(inner) => is_caller_like(hir, inner, depth, ns, member),
980        ExprKind::Call(callee, args, _) if is_address_like_cast_callee(callee) => {
981            args.exprs().next().is_some_and(|e| is_caller_like(hir, e, depth, ns, member))
982        }
983        ExprKind::Call(callee, args, _) if depth > 0 && args.exprs().next().is_none() => {
984            callee_no_arg_returns(hir, callee, |e| is_caller_like(hir, e, depth - 1, ns, member))
985        }
986        _ => false,
987    }
988}
989
990/// Conservatively recognises deploy-time-fixed caller principals.
991fn is_trusted_principal_inner<'hir>(
992    hir: &'hir hir::Hir<'hir>,
993    expr: &'hir hir::Expr<'hir>,
994    params: &[hir::VariableId],
995    depth: u8,
996    self_aliases: &mut SelfAliasAnalysis<'hir>,
997) -> bool {
998    if expr_touches_param(expr, params)
999        || is_msg_sender_like(hir, expr, HELPER_DEPTH)
1000        || is_tx_origin_like(hir, expr, HELPER_DEPTH)
1001        || is_address_self(expr)
1002    {
1003        return false;
1004    }
1005    let expr = expr.peel_parens();
1006    match &expr.kind {
1007        ExprKind::Lit(lit) => match &lit.kind {
1008            LitKind::Address(_) => true,
1009            LitKind::Number(n) => n.is_zero(),
1010            _ => false,
1011        },
1012        ExprKind::Call(callee, args, _) if is_address_like_cast_callee(callee) => {
1013            args.exprs().next().is_some_and(|inner| match &inner.peel_parens().kind {
1014                // Address literals trust; only the `0` numeric literal trusts.
1015                ExprKind::Lit(lit) => match &lit.kind {
1016                    LitKind::Address(_) => true,
1017                    LitKind::Number(n) => n.is_zero(),
1018                    _ => false,
1019                },
1020                _ => is_trusted_principal_inner(hir, inner, params, depth, self_aliases),
1021            })
1022        }
1023        ExprKind::Payable(inner) => {
1024            is_trusted_principal_inner(hir, inner, params, depth, self_aliases)
1025        }
1026        ExprKind::Ident(reses) => reses.iter().any(|r| match r {
1027            Res::Item(ItemId::Variable(vid)) => {
1028                let var = hir.variable(*vid);
1029                var.kind.is_state() && !self_aliases.state_var_aliases_self(*vid, SELF_ALIAS_DEPTH)
1030            }
1031            _ => false,
1032        }),
1033        ExprKind::Member(base, _) => {
1034            is_trusted_principal_inner(hir, base, params, depth, self_aliases)
1035        }
1036        ExprKind::Index(base, idx) => {
1037            is_trusted_principal_inner(hir, base, params, depth, self_aliases)
1038                && idx.is_none_or(|i| index_is_static(hir, i, params))
1039        }
1040        ExprKind::Call(callee, args, _) => {
1041            depth > 0
1042                && args.exprs().next().is_none()
1043                && callee_no_arg_returns(hir, callee, |e| {
1044                    is_trusted_principal_inner(hir, e, &[], depth - 1, self_aliases)
1045                })
1046        }
1047        _ => false,
1048    }
1049}
1050
1051/// Memoized state-var self-alias analysis used by caller-restriction checks.
1052struct SelfAliasAnalysis<'hir> {
1053    gcx: Gcx<'hir>,
1054    hir: &'hir hir::Hir<'hir>,
1055    cache: HashMap<(hir::VariableId, u8), bool>,
1056    active: HashSet<(hir::VariableId, u8)>,
1057}
1058
1059impl<'hir> SelfAliasAnalysis<'hir> {
1060    fn new(gcx: Gcx<'hir>, hir: &'hir hir::Hir<'hir>) -> Self {
1061        Self { gcx, hir, cache: HashMap::new(), active: HashSet::new() }
1062    }
1063
1064    /// True when `vid` is a state variable that may alias `address(this)`.
1065    fn state_var_aliases_self(&mut self, vid: hir::VariableId, depth: u8) -> bool {
1066        if depth == 0 {
1067            return false;
1068        }
1069        let var = self.hir.variable(vid);
1070        if !var.kind.is_state() {
1071            return false;
1072        }
1073
1074        let key = (vid, depth);
1075        if let Some(result) = self.cache.get(&key) {
1076            return *result;
1077        }
1078        if !self.active.insert(key) {
1079            return false;
1080        }
1081
1082        let result = self.state_var_aliases_self_uncached(vid, depth);
1083        self.active.remove(&key);
1084        self.cache.insert(key, result);
1085        result
1086    }
1087
1088    fn state_var_aliases_self_uncached(&mut self, vid: hir::VariableId, depth: u8) -> bool {
1089        let var = self.hir.variable(vid);
1090        if let Some(init) = var.initializer {
1091            let initializer_aliases = if var_is_address_like(var) {
1092                self.expr_resolves_to_self(init, depth - 1)
1093            } else {
1094                self.expr_may_contain_self_in(init, depth - 1, &HashSet::new())
1095            };
1096            if initializer_aliases {
1097                return true;
1098            }
1099        }
1100        let Some(cid) = var.contract else { return false };
1101        if self.contract_function_assigns_to_self(cid, vid, depth - 1) {
1102            return true;
1103        }
1104        let derived_contracts: Vec<_> = self
1105            .hir
1106            .contracts_enumerated()
1107            .filter_map(|(other_cid, other)| {
1108                (other_cid != cid && other.linearized_bases.contains(&cid)).then_some(other_cid)
1109            })
1110            .collect();
1111        derived_contracts
1112            .into_iter()
1113            .any(|other_cid| self.contract_function_assigns_to_self(other_cid, vid, depth - 1))
1114    }
1115
1116    /// Conservative free-standing "this expression *may* embed `address(this)` somewhere".
1117    fn expr_may_contain_self_in(
1118        &mut self,
1119        expr: &'hir hir::Expr<'hir>,
1120        depth: u8,
1121        local_aliases: &HashSet<hir::VariableId>,
1122    ) -> bool {
1123        if self.expr_resolves_to_self(expr, depth) {
1124            return true;
1125        }
1126        if let Some(vid) = lhs_root_var(expr)
1127            && local_aliases.contains(&vid)
1128        {
1129            return true;
1130        }
1131        if depth == 0 {
1132            return false;
1133        }
1134        match &expr.peel_parens().kind {
1135            ExprKind::Payable(inner) => {
1136                self.expr_may_contain_self_in(inner, depth - 1, local_aliases)
1137            }
1138            ExprKind::Call(callee, args, _)
1139                if is_address_like_cast_callee(callee) || is_numeric_cast_callee(callee) =>
1140            {
1141                args.exprs()
1142                    .next()
1143                    .is_some_and(|e| self.expr_may_contain_self_in(e, depth - 1, local_aliases))
1144            }
1145            ExprKind::Call(_, args, _) => {
1146                args.exprs().any(|e| self.expr_may_contain_self_in(e, depth - 1, local_aliases))
1147            }
1148            ExprKind::Ternary(_, t, f) => {
1149                self.expr_may_contain_self_in(t, depth - 1, local_aliases)
1150                    || self.expr_may_contain_self_in(f, depth - 1, local_aliases)
1151            }
1152            ExprKind::Tuple(elems) => elems
1153                .iter()
1154                .flatten()
1155                .any(|e| self.expr_may_contain_self_in(e, depth - 1, local_aliases)),
1156            ExprKind::Array(elems) => {
1157                elems.iter().any(|e| self.expr_may_contain_self_in(e, depth - 1, local_aliases))
1158            }
1159            _ => false,
1160        }
1161    }
1162
1163    /// True when `expr` may evaluate to `address(this)`.
1164    fn expr_resolves_to_self(&mut self, expr: &'hir hir::Expr<'hir>, depth: u8) -> bool {
1165        if is_address_self(expr) {
1166            return true;
1167        }
1168        if depth == 0 {
1169            return false;
1170        }
1171        match &expr.peel_parens().kind {
1172            ExprKind::Payable(inner) => self.expr_resolves_to_self(inner, depth - 1),
1173            ExprKind::Call(callee, args, _)
1174                if is_address_like_cast_callee(callee) || is_numeric_cast_callee(callee) =>
1175            {
1176                args.exprs().next().is_some_and(|e| self.expr_resolves_to_self(e, depth - 1))
1177            }
1178            ExprKind::Ident(reses) => reses.iter().any(|r| match r {
1179                Res::Item(ItemId::Variable(other_vid)) => {
1180                    self.state_var_aliases_self(*other_vid, depth)
1181                }
1182                _ => false,
1183            }),
1184            ExprKind::Member(_, _) | ExprKind::Index(_, _) => lhs_root_var(expr)
1185                .map(|vid| self.state_var_aliases_self(vid, depth))
1186                .unwrap_or(false),
1187            ExprKind::Call(callee, args, _) => {
1188                if args.exprs().count() == 0 {
1189                    self.callee_returns_self(callee, depth - 1)
1190                } else if let Some(arg) = identity_helper_arg(self.hir, callee, args) {
1191                    self.expr_resolves_to_self(arg, depth - 1)
1192                } else {
1193                    false
1194                }
1195            }
1196            ExprKind::Ternary(_, t, f) => {
1197                self.expr_resolves_to_self(t, depth - 1) || self.expr_resolves_to_self(f, depth - 1)
1198            }
1199            ExprKind::Assign(_, _, rhs) => self.expr_resolves_to_self(rhs, depth - 1),
1200            _ => false,
1201        }
1202    }
1203
1204    /// True when `callee` is a zero-arg helper whose body is `return <self-resolving>;`.
1205    fn callee_returns_self(&mut self, callee: &'hir hir::Expr<'hir>, depth: u8) -> bool {
1206        if callee_no_arg_returns(self.hir, callee, |e| self.expr_resolves_to_self(e, depth)) {
1207            return true;
1208        }
1209        let ExprKind::Member(base, member) = &callee.peel_parens().kind else { return false };
1210        let ExprKind::Ident(reses) = &base.peel_parens().kind else { return false };
1211        let Some(cid) = reses.iter().find_map(|r| match r {
1212            Res::Item(ItemId::Contract(cid))
1213                if self.hir.contract(*cid).kind == ContractKind::Library =>
1214            {
1215                Some(*cid)
1216            }
1217            _ => None,
1218        }) else {
1219            return false;
1220        };
1221        let contract_ids: Vec<_> = if self.hir.contract(cid).linearization_failed() {
1222            vec![cid]
1223        } else {
1224            self.hir.contract(cid).linearized_bases.to_vec()
1225        };
1226        for bid in contract_ids {
1227            let fids: Vec<_> = self.hir.contract(bid).all_functions().collect();
1228            for fid in fids {
1229                if self.hir.function(fid).name.is_none_or(|n| n.name != member.name) {
1230                    continue;
1231                }
1232                if function_no_arg_returns(self.hir, fid, &mut |e| {
1233                    self.expr_resolves_to_self(e, depth)
1234                }) {
1235                    return true;
1236                }
1237            }
1238        }
1239        false
1240    }
1241
1242    /// Scans every function of `cid` for an assignment that aliases `vid` to `address(this)`.
1243    fn contract_function_assigns_to_self(
1244        &mut self,
1245        cid: hir::ContractId,
1246        vid: hir::VariableId,
1247        depth: u8,
1248    ) -> bool {
1249        let fids: Vec<_> = self.hir.contract(cid).all_functions().collect();
1250        for fid in fids {
1251            let f = self.hir.function(fid);
1252            let Some(body) = f.body else { continue };
1253            let mut found = false;
1254            let mut scan = SelfAssignScan {
1255                hir: self.hir,
1256                aliases: self,
1257                target: vid,
1258                depth,
1259                found: &mut found,
1260                helper_stack: Vec::new(),
1261                local_self_aliases: HashSet::new(),
1262            };
1263            for inv in f.modifiers {
1264                if *scan.found {
1265                    break;
1266                }
1267                if let Some(invoked_fid) = invoked_function(scan.hir, inv) {
1268                    scan.scan_invoked(invoked_fid, &inv.args);
1269                }
1270            }
1271            if *scan.found {
1272                return true;
1273            }
1274            for stmt in body.stmts {
1275                if *scan.found {
1276                    break;
1277                }
1278                let _ = scan.visit_stmt(stmt);
1279            }
1280            if found {
1281                return true;
1282            }
1283        }
1284        false
1285    }
1286}
1287
1288/// Parameter returned verbatim by a single-statement function body.
1289fn function_returns_param(hir: &hir::Hir<'_>, fid: FunctionId) -> Option<hir::VariableId> {
1290    let f = hir.function(fid);
1291    let body = f.body?;
1292    if body.stmts.len() != 1 || f.returns.len() != 1 {
1293        return None;
1294    }
1295    let StmtKind::Return(Some(ret)) = &body.stmts[0].kind else { return None };
1296    fn unwrap<'a>(e: &'a hir::Expr<'a>) -> &'a hir::Expr<'a> {
1297        let e = e.peel_parens();
1298        match &e.kind {
1299            ExprKind::Payable(inner) => unwrap(inner),
1300            ExprKind::Call(callee, args, _)
1301                if is_address_like_cast_callee(callee) || is_numeric_cast_callee(callee) =>
1302            {
1303                args.exprs().next().map(unwrap).unwrap_or(e)
1304            }
1305            _ => e,
1306        }
1307    }
1308    let inner = unwrap(ret);
1309    let ExprKind::Ident(reses) = &inner.kind else { return None };
1310    for r in *reses {
1311        let Res::Item(ItemId::Variable(vid)) = r else { continue };
1312        if f.parameters.iter().any(|p| p == vid) {
1313            return Some(*vid);
1314        }
1315    }
1316    None
1317}
1318
1319/// Resolves a bare `id(addr)` or library-static `Lib.id(addr)` identity-helper call.
1320fn identity_helper_arg<'hir>(
1321    hir: &'hir hir::Hir<'hir>,
1322    callee: &'hir hir::Expr<'hir>,
1323    args: &'hir hir::CallArgs<'hir>,
1324) -> Option<&'hir hir::Expr<'hir>> {
1325    let callee = callee.peel_parens();
1326    let call_arity = args.exprs().count();
1327    let try_fid = |fid: FunctionId| -> Option<&'hir hir::Expr<'hir>> {
1328        let f = hir.function(fid);
1329        if f.parameters.len() != call_arity {
1330            return None;
1331        }
1332        let param = function_returns_param(hir, fid)?;
1333        arg_for_param(hir, f, param, args)
1334    };
1335    match &callee.kind {
1336        ExprKind::Ident(reses) => reses.iter().find_map(|r| match r {
1337            Res::Item(ItemId::Function(fid)) => try_fid(*fid),
1338            _ => None,
1339        }),
1340        ExprKind::Member(base, member) => {
1341            let ExprKind::Ident(reses) = &base.peel_parens().kind else { return None };
1342            let cid = reses.iter().find_map(|r| match r {
1343                Res::Item(ItemId::Contract(cid)) => Some(*cid),
1344                _ => None,
1345            })?;
1346            if hir.contract(cid).kind != ContractKind::Library {
1347                return None;
1348            }
1349            find_in_bases_or_self(hir, cid, |bid| {
1350                hir.contract(bid).all_functions().find_map(|fid| {
1351                    hir.function(fid)
1352                        .name
1353                        .is_some_and(|n| n.name == member.name)
1354                        .then(|| try_fid(fid))
1355                        .flatten()
1356                })
1357            })
1358        }
1359        _ => None,
1360    }
1361}
1362
1363/// Call-site argument expression bound to `param`, supporting positional and named args.
1364fn arg_for_param<'hir>(
1365    hir: &'hir hir::Hir<'hir>,
1366    f: &hir::Function<'hir>,
1367    param: hir::VariableId,
1368    args: &'hir hir::CallArgs<'hir>,
1369) -> Option<&'hir hir::Expr<'hir>> {
1370    let param_idx = f.parameters.iter().position(|p| *p == param)?;
1371    match args.kind {
1372        hir::CallArgsKind::Unnamed(exprs) => exprs.get(param_idx),
1373        hir::CallArgsKind::Named(named) => {
1374            let pname = hir.variable(param).name?;
1375            named.iter().find(|a| a.name.name == pname.name).map(|a| &a.value)
1376        }
1377    }
1378}
1379
1380/// `uint<N>(x)` / `int<N>(x)` cast callee, for unwrapping integer-round-trip launderings.
1381fn is_numeric_cast_callee(callee: &hir::Expr<'_>) -> bool {
1382    matches!(
1383        &callee.peel_parens().kind,
1384        ExprKind::Type(hir::Type {
1385            kind: TypeKind::Elementary(ElementaryType::UInt(_) | ElementaryType::Int(_)),
1386            ..
1387        })
1388    )
1389}
1390
1391/// Cap helper-call recursion (covers `ctor → _init → _initInner → _initLeaf`).
1392const HELPER_CALL_DEPTH: u8 = 4;
1393
1394/// Per-function scan state for [`SelfAliasAnalysis::contract_function_assigns_to_self`].
1395struct SelfAssignScan<'a, 'hir> {
1396    hir: &'hir hir::Hir<'hir>,
1397    aliases: &'a mut SelfAliasAnalysis<'hir>,
1398    target: hir::VariableId,
1399    depth: u8,
1400    found: &'a mut bool,
1401    helper_stack: Vec<FunctionId>,
1402    /// Locals path-insensitively known to *may* carry `address(this)`.
1403    local_self_aliases: HashSet<hir::VariableId>,
1404}
1405
1406impl<'hir> SelfAssignScan<'_, 'hir> {
1407    fn expr_may_contain_self(&mut self, expr: &'hir hir::Expr<'hir>) -> bool {
1408        self.aliases.expr_may_contain_self_in(expr, self.depth, &self.local_self_aliases)
1409    }
1410
1411    /// True when `lhs` (possibly inside a tuple) aliases the target.
1412    fn lhs_aliases_target(
1413        &mut self,
1414        lhs: &'hir hir::Expr<'hir>,
1415        rhs: &'hir hir::Expr<'hir>,
1416    ) -> bool {
1417        let lhs = lhs.peel_parens();
1418        let rhs = rhs.peel_parens();
1419        if let ExprKind::Tuple(lhs_elems) = &lhs.kind {
1420            let rhs_elems = tuple_elems(rhs);
1421            return lhs_elems.iter().enumerate().any(|(i, lhs_elem)| {
1422                lhs_elem.is_some_and(|le| {
1423                    tuple_slot(rhs_elems, i).is_some_and(|r| self.lhs_aliases_target(le, r))
1424                })
1425            });
1426        }
1427        if lhs_root_var(lhs) != Some(self.target) {
1428            return false;
1429        }
1430        let target = self.hir.variable(self.target);
1431        if var_is_address_like(target) {
1432            self.aliases.expr_resolves_to_self(rhs, self.depth)
1433                || lhs_root_var(rhs).is_some_and(|vid| self.local_self_aliases.contains(&vid))
1434        } else {
1435            self.expr_may_contain_self(rhs)
1436        }
1437    }
1438
1439    /// Records non-state locals proven (path-insensitively) to carry `address(this)`.
1440    fn record_local_self_alias(&mut self, lhs: &hir::Expr<'_>, rhs: &'hir hir::Expr<'hir>) {
1441        let lhs = lhs.peel_parens();
1442        let rhs = rhs.peel_parens();
1443        if let ExprKind::Tuple(lhs_elems) = &lhs.kind {
1444            let rhs_elems = tuple_elems(rhs);
1445            for (i, lhs_elem) in lhs_elems.iter().enumerate() {
1446                if let Some(le) = lhs_elem
1447                    && let Some(re) = tuple_slot(rhs_elems, i)
1448                {
1449                    self.record_local_self_alias(le, re);
1450                }
1451            }
1452            return;
1453        }
1454        if let Some(vid) = lhs_root_var(lhs)
1455            && !self.hir.variable(vid).kind.is_state()
1456            && self.expr_may_contain_self(rhs)
1457        {
1458            self.local_self_aliases.insert(vid);
1459        }
1460    }
1461
1462    /// Single internal-helper `FunctionId` for a bare-ident call; rejects overloads.
1463    fn helper_callee(&self, callee: &hir::Expr<'_>) -> Option<FunctionId> {
1464        let ExprKind::Ident(reses) = &callee.peel_parens().kind else { return None };
1465        let mut fid_iter = reses.iter().filter_map(|r| match r {
1466            Res::Item(ItemId::Function(fid)) => Some(*fid),
1467            _ => None,
1468        });
1469        let fid = fid_iter.next()?;
1470        fid_iter.next().is_none().then_some(fid)
1471    }
1472
1473    /// Marks each helper parameter as a self-carrier when its call-site arg may carry self.
1474    fn seed_helper_param_aliases(
1475        &mut self,
1476        f: &hir::Function<'hir>,
1477        call_args: &'hir hir::CallArgs<'hir>,
1478    ) {
1479        for &param in f.parameters {
1480            if let Some(arg) = arg_for_param(self.hir, f, param, call_args)
1481                && self.expr_may_contain_self(arg)
1482            {
1483                self.local_self_aliases.insert(param);
1484            }
1485        }
1486    }
1487
1488    /// Walks an invoked function (modifier or base constructor) and its own modifier chain.
1489    fn scan_invoked(&mut self, invoked_fid: FunctionId, inv_args: &'hir hir::CallArgs<'hir>) {
1490        if (self.helper_stack.len() as u8) >= HELPER_CALL_DEPTH
1491            || self.helper_stack.contains(&invoked_fid)
1492        {
1493            return;
1494        }
1495        let invoked = self.hir.function(invoked_fid);
1496        let Some(inv_body) = invoked.body else { return };
1497        let saved = self.local_self_aliases.clone();
1498        self.seed_helper_param_aliases(invoked, inv_args);
1499        self.helper_stack.push(invoked_fid);
1500        for inner in invoked.modifiers {
1501            if *self.found {
1502                break;
1503            }
1504            if let Some(inner_fid) = invoked_function(self.hir, inner) {
1505                self.scan_invoked(inner_fid, &inner.args);
1506            }
1507        }
1508        for stmt in inv_body.stmts {
1509            if *self.found {
1510                break;
1511            }
1512            let _ = self.visit_stmt(stmt);
1513        }
1514        self.helper_stack.pop();
1515        self.local_self_aliases = saved;
1516    }
1517}
1518
1519impl<'hir> hir::Visit<'hir> for SelfAssignScan<'_, 'hir> {
1520    type BreakValue = Never;
1521
1522    fn hir(&self) -> &'hir hir::Hir<'hir> {
1523        self.hir
1524    }
1525
1526    fn visit_stmt(&mut self, stmt: &'hir hir::Stmt<'hir>) -> ControlFlow<Self::BreakValue> {
1527        if *self.found {
1528            return ControlFlow::Continue(());
1529        }
1530        match &stmt.kind {
1531            StmtKind::DeclSingle(vid) => {
1532                let var = self.hir.variable(*vid);
1533                if !var.kind.is_state()
1534                    && let Some(init) = var.initializer
1535                    && self.expr_may_contain_self(init)
1536                {
1537                    self.local_self_aliases.insert(*vid);
1538                }
1539            }
1540            StmtKind::DeclMulti(vars, init) => {
1541                if let ExprKind::Tuple(rhs) = &init.peel_parens().kind {
1542                    for (lhs, rhs) in vars.iter().zip(rhs.iter()) {
1543                        if let (Some(vid), Some(expr)) = (lhs, rhs)
1544                            && !self.hir.variable(*vid).kind.is_state()
1545                            && self.expr_may_contain_self(expr)
1546                        {
1547                            self.local_self_aliases.insert(*vid);
1548                        }
1549                    }
1550                }
1551            }
1552            _ => {}
1553        }
1554        self.walk_stmt(stmt)
1555    }
1556
1557    fn visit_expr(&mut self, expr: &'hir hir::Expr<'hir>) -> ControlFlow<Self::BreakValue> {
1558        if *self.found {
1559            return ControlFlow::Continue(());
1560        }
1561        if let ExprKind::Assign(lhs, _, rhs) = &expr.peel_parens().kind {
1562            self.record_local_self_alias(lhs, rhs);
1563            if self.lhs_aliases_target(lhs, rhs) {
1564                *self.found = true;
1565                return ControlFlow::Continue(());
1566            }
1567        }
1568        if let ExprKind::Call(callee, call_args, _) = &expr.peel_parens().kind
1569            && let ExprKind::Member(recv, member) = &callee.peel_parens().kind
1570            && member.name.as_str() == "push"
1571            && lhs_root_var(recv) == Some(self.target)
1572            && expr_is_array_or_bytes(self.aliases.gcx, recv)
1573            && call_args.exprs().any(|a| self.expr_may_contain_self(a))
1574        {
1575            *self.found = true;
1576            return ControlFlow::Continue(());
1577        }
1578        if let ExprKind::Call(callee, call_args, _) = &expr.peel_parens().kind
1579            && (self.helper_stack.len() as u8) < HELPER_CALL_DEPTH
1580            && let Some(fid) = self.helper_callee(callee)
1581            && !self.helper_stack.contains(&fid)
1582        {
1583            let f = self.hir.function(fid);
1584            if let Some(body) = f.body {
1585                let saved = self.local_self_aliases.clone();
1586                self.seed_helper_param_aliases(f, call_args);
1587                self.helper_stack.push(fid);
1588                for stmt in body.stmts {
1589                    if *self.found {
1590                        break;
1591                    }
1592                    let _ = self.visit_stmt(stmt);
1593                }
1594                self.helper_stack.pop();
1595                self.local_self_aliases = saved;
1596            }
1597        }
1598        self.walk_expr(expr)
1599    }
1600}
1601
1602/// Returns the slot expressions of a tuple literal (after peeling parens), or `None` when
1603/// `expr` is not a tuple. Slots themselves may be `None` (gaps in a tuple LHS).
1604fn tuple_elems<'hir>(expr: &'hir hir::Expr<'hir>) -> Option<&'hir [Option<&'hir hir::Expr<'hir>>]> {
1605    match &expr.peel_parens().kind {
1606        ExprKind::Tuple(elems) => Some(*elems),
1607        _ => None,
1608    }
1609}
1610
1611/// Looks up a single slot from the result of [`tuple_elems`].
1612fn tuple_slot<'hir>(
1613    elems: Option<&'hir [Option<&'hir hir::Expr<'hir>>]>,
1614    i: usize,
1615) -> Option<&'hir hir::Expr<'hir>> {
1616    elems.and_then(|e| e.get(i).copied()).flatten()
1617}
1618
1619/// Applies `f` to each contract in `cid`'s linearization, or just `cid` itself when
1620/// linearization failed, returning the first `Some` result.
1621fn find_in_bases_or_self<T>(
1622    hir: &hir::Hir<'_>,
1623    cid: hir::ContractId,
1624    mut f: impl FnMut(hir::ContractId) -> Option<T>,
1625) -> Option<T> {
1626    let contract = hir.contract(cid);
1627    if contract.linearization_failed() {
1628        f(cid)
1629    } else {
1630        contract.linearized_bases.iter().find_map(|&bid| f(bid))
1631    }
1632}
1633
1634/// Variable at the root of an LHS expression.
1635fn lhs_root_var(lhs: &hir::Expr<'_>) -> Option<hir::VariableId> {
1636    match &lhs.peel_parens().kind {
1637        ExprKind::Ident(_) => underlying_var(lhs),
1638        ExprKind::Member(base, _) => lhs_root_var(base),
1639        ExprKind::Index(base, _) => lhs_root_var(base),
1640        ExprKind::Call(callee, args, _) if is_address_like_cast_callee(callee) => {
1641            args.exprs().next().and_then(lhs_root_var)
1642        }
1643        ExprKind::Payable(inner) => lhs_root_var(inner),
1644        _ => None,
1645    }
1646}
1647
1648/// True when every sub-expression of `expr` is independent of the call's parameters.
1649fn index_is_static<'hir>(
1650    hir: &'hir hir::Hir<'hir>,
1651    expr: &'hir hir::Expr<'hir>,
1652    params: &[hir::VariableId],
1653) -> bool {
1654    fn walk<'hir>(
1655        hir: &'hir hir::Hir<'hir>,
1656        e: &'hir hir::Expr<'hir>,
1657        params: &[hir::VariableId],
1658    ) -> bool {
1659        if expr_touches_param(e, params)
1660            || is_msg_sender_like(hir, e, HELPER_DEPTH)
1661            || is_tx_origin_like(hir, e, HELPER_DEPTH)
1662        {
1663            return false;
1664        }
1665        match &e.peel_parens().kind {
1666            ExprKind::Lit(_) => true,
1667            ExprKind::Ident(reses) => reses.iter().all(|r| match r {
1668                Res::Item(ItemId::Variable(vid)) => hir.variable(*vid).kind.is_state(),
1669                Res::Builtin(_) => false,
1670                _ => true,
1671            }),
1672            ExprKind::Payable(i) | ExprKind::Unary(_, i) => walk(hir, i, params),
1673            ExprKind::Binary(l, _, r) => walk(hir, l, params) && walk(hir, r, params),
1674            ExprKind::Member(base, _) => walk(hir, base, params),
1675            ExprKind::Index(base, idx) => {
1676                walk(hir, base, params) && idx.is_none_or(|i| walk(hir, i, params))
1677            }
1678            ExprKind::Ternary(c, t, f) => {
1679                walk(hir, c, params) && walk(hir, t, params) && walk(hir, f, params)
1680            }
1681            ExprKind::Call(callee, args, _) => {
1682                let callee_ok = match &callee.peel_parens().kind {
1683                    ExprKind::Type(_) => true,
1684                    ExprKind::Ident(reses) => {
1685                        reses.iter().any(|r| matches!(r, Res::Item(ItemId::Contract(_))))
1686                    }
1687                    _ => false,
1688                };
1689                callee_ok && args.exprs().all(|a| walk(hir, a, params))
1690            }
1691            _ => false,
1692        }
1693    }
1694    walk(hir, expr, params)
1695}
1696
1697/// True when any sub-expression references one of the supplied `VariableId`s.
1698fn expr_touches_param(expr: &hir::Expr<'_>, params: &[hir::VariableId]) -> bool {
1699    match &expr.peel_parens().kind {
1700        ExprKind::Ident(reses) => reses
1701            .iter()
1702            .any(|r| matches!(r, Res::Item(ItemId::Variable(vid)) if params.contains(vid))),
1703        ExprKind::Binary(l, _, r) | ExprKind::Assign(l, _, r) => {
1704            expr_touches_param(l, params) || expr_touches_param(r, params)
1705        }
1706        ExprKind::Unary(_, i)
1707        | ExprKind::Payable(i)
1708        | ExprKind::Delete(i)
1709        | ExprKind::Member(i, _) => expr_touches_param(i, params),
1710        ExprKind::Index(b, idx) => {
1711            expr_touches_param(b, params) || idx.is_some_and(|i| expr_touches_param(i, params))
1712        }
1713        ExprKind::Ternary(c, t, f) => {
1714            expr_touches_param(c, params)
1715                || expr_touches_param(t, params)
1716                || expr_touches_param(f, params)
1717        }
1718        ExprKind::Call(callee, args, _) => {
1719            expr_touches_param(callee, params)
1720                || args.exprs().any(|a| expr_touches_param(a, params))
1721        }
1722        _ => false,
1723    }
1724}
1725
1726/// Hoists `require(modParam == msg.sender)`-style guards from the modifier prefix.
1727fn collect_modifier_safety<'hir>(
1728    gcx: Gcx<'hir>,
1729    hir: &'hir hir::Hir<'hir>,
1730    invocation: &'hir hir::Modifier<'hir>,
1731    out_safe: &mut HashSet<hir::VariableId>,
1732) {
1733    let ItemId::Function(fid) = invocation.id else { return };
1734    let Some((modifier, prefix)) = modifier_prefix(hir, fid) else { return };
1735    let arg_map: Vec<(hir::VariableId, hir::VariableId)> = modifier
1736        .parameters
1737        .iter()
1738        .filter_map(|&mp| {
1739            let arg = arg_for_param(hir, modifier, mp, &invocation.args)?;
1740            Some((mp, underlying_var(arg)?))
1741        })
1742        .collect();
1743    if arg_map.is_empty() {
1744        return;
1745    }
1746    let mut assigned_params: HashSet<hir::VariableId> = HashSet::new();
1747    let mut collector = AssignedParamCollector { hir, out: &mut assigned_params };
1748    for stmt in &prefix {
1749        let _ = collector.visit_stmt(stmt);
1750    }
1751    let mut a = Analyzer::new(gcx, hir);
1752    for stmt in &prefix {
1753        let _ = a.visit_stmt(stmt);
1754    }
1755    for (mp, caller) in arg_map {
1756        if !assigned_params.contains(&mp) && a.safe_vars.contains(&mp) && a.is_safe_target(caller) {
1757            out_safe.insert(caller);
1758        }
1759    }
1760}
1761
1762/// Statements preceding the unique `_;` in a modifier body, in execution order.
1763fn collect_stmts_before_placeholder<'hir>(
1764    stmts: &'hir [hir::Stmt<'hir>],
1765    out: &mut Vec<&'hir hir::Stmt<'hir>>,
1766) -> Option<()> {
1767    for (i, stmt) in stmts.iter().enumerate() {
1768        match &stmt.kind {
1769            StmtKind::Placeholder => {
1770                out.extend(stmts[..i].iter());
1771                return Some(());
1772            }
1773            StmtKind::Block(b) | StmtKind::UncheckedBlock(b)
1774                if count_placeholders(b.stmts) >= 1 =>
1775            {
1776                out.extend(stmts[..i].iter());
1777                return collect_stmts_before_placeholder(b.stmts, out);
1778            }
1779            _ => {
1780                if count_placeholders_in_stmt(stmt) > 0 {
1781                    return None;
1782                }
1783            }
1784        }
1785    }
1786    None
1787}
1788
1789/// Collects every `VariableId` that appears as the target of an assignment or `delete`.
1790struct AssignedParamCollector<'a, 'hir> {
1791    hir: &'hir hir::Hir<'hir>,
1792    out: &'a mut HashSet<hir::VariableId>,
1793}
1794
1795impl AssignedParamCollector<'_, '_> {
1796    fn add_lhs(&mut self, lhs: &hir::Expr<'_>) {
1797        match &lhs.peel_parens().kind {
1798            ExprKind::Tuple(elems) => {
1799                for e in elems.iter().flatten() {
1800                    self.add_lhs(e);
1801                }
1802            }
1803            _ => {
1804                if let Some(vid) = underlying_var(lhs) {
1805                    self.out.insert(vid);
1806                }
1807            }
1808        }
1809    }
1810}
1811
1812impl<'hir> hir::Visit<'hir> for AssignedParamCollector<'_, 'hir> {
1813    type BreakValue = Never;
1814    fn hir(&self) -> &'hir hir::Hir<'hir> {
1815        self.hir
1816    }
1817    fn visit_expr(&mut self, expr: &'hir hir::Expr<'hir>) -> ControlFlow<Self::BreakValue> {
1818        match &expr.peel_parens().kind {
1819            ExprKind::Assign(lhs, _, _) => self.add_lhs(lhs),
1820            ExprKind::Delete(target) => self.add_lhs(target),
1821            _ => {}
1822        }
1823        self.walk_expr(expr)
1824    }
1825}
1826
1827/// Strips the trailing `if (...) break;` that lowers `do { ... } while (cond);`.
1828fn do_while_user_stmts<'a, 'hir>(stmts: &'a [hir::Stmt<'hir>]) -> &'a [hir::Stmt<'hir>] {
1829    if let Some((last, rest)) = stmts.split_last()
1830        && let StmtKind::If(_, t, e) = &last.kind
1831        && (is_break_stmt(t) || e.as_ref().is_some_and(|e| is_break_stmt(e)))
1832    {
1833        return rest;
1834    }
1835    stmts
1836}
1837
1838fn is_break_stmt(stmt: &hir::Stmt<'_>) -> bool {
1839    match &stmt.kind {
1840        StmtKind::Break => true,
1841        StmtKind::Block(b) | StmtKind::UncheckedBlock(b) => {
1842            b.stmts.len() == 1 && is_break_stmt(&b.stmts[0])
1843        }
1844        _ => false,
1845    }
1846}
1847
1848fn stmt_has_break_or_continue(stmt: &hir::Stmt<'_>) -> bool {
1849    match &stmt.kind {
1850        StmtKind::Break | StmtKind::Continue => true,
1851        StmtKind::Block(b) | StmtKind::UncheckedBlock(b) => {
1852            b.stmts.iter().any(stmt_has_break_or_continue)
1853        }
1854        StmtKind::If(_, t, e) => {
1855            stmt_has_break_or_continue(t)
1856                || e.as_ref().is_some_and(|s| stmt_has_break_or_continue(s))
1857        }
1858        StmtKind::Try(t) => {
1859            t.clauses.iter().any(|c| c.block.stmts.iter().any(stmt_has_break_or_continue))
1860        }
1861        StmtKind::Loop(..) => false,
1862        _ => false,
1863    }
1864}
1865
1866fn count_placeholders(stmts: &[hir::Stmt<'_>]) -> usize {
1867    stmts.iter().map(count_placeholders_in_stmt).sum()
1868}
1869
1870fn count_placeholders_in_stmt(stmt: &hir::Stmt<'_>) -> usize {
1871    match &stmt.kind {
1872        StmtKind::Placeholder => 1,
1873        StmtKind::Block(b) | StmtKind::UncheckedBlock(b) | StmtKind::Loop(b, _) => {
1874            count_placeholders(b.stmts)
1875        }
1876        StmtKind::If(_, t, e) => {
1877            count_placeholders_in_stmt(t) + e.as_ref().map_or(0, |s| count_placeholders_in_stmt(s))
1878        }
1879        StmtKind::Try(t) => t.clauses.iter().map(|c| count_placeholders(c.block.stmts)).sum(),
1880        _ => 0,
1881    }
1882}
1883
1884/// Resolves a `VariableId` for bare idents and address-like wrappers.
1885fn underlying_var(expr: &hir::Expr<'_>) -> Option<hir::VariableId> {
1886    match &expr.peel_parens().kind {
1887        ExprKind::Ident(reses) => reses.iter().find_map(|r| match r {
1888            Res::Item(ItemId::Variable(vid)) => Some(*vid),
1889            _ => None,
1890        }),
1891        ExprKind::Call(callee, args, _) if is_address_like_cast_callee(callee) => {
1892            args.exprs().next().and_then(underlying_var)
1893        }
1894        ExprKind::Payable(inner) => underlying_var(inner),
1895        _ => None,
1896    }
1897}
1898
1899/// `address` / `address payable` or a contract/interface type.
1900const fn var_is_address_like(var: &hir::Variable<'_>) -> bool {
1901    matches!(
1902        var.ty.kind,
1903        TypeKind::Elementary(ElementaryType::Address(_)) | TypeKind::Custom(ItemId::Contract(_))
1904    )
1905}
1906
1907/// True when `expr`'s static type is `address` / `address payable`.
1908fn receiver_is_address<'hir>(gcx: Gcx<'hir>, expr: &'hir hir::Expr<'hir>) -> bool {
1909    expr_ty(gcx, expr).is_some_and(ty_is_address)
1910}
1911
1912/// Callee of a single-argument cast that yields an address-shaped value.
1913fn is_address_like_cast_callee(callee: &hir::Expr<'_>) -> bool {
1914    match &callee.peel_parens().kind {
1915        ExprKind::Type(hir::Type {
1916            kind: TypeKind::Elementary(ElementaryType::Address(_)),
1917            ..
1918        }) => true,
1919        ExprKind::Ident(reses) => reses.iter().any(|r| matches!(r, Res::Item(ItemId::Contract(_)))),
1920        _ => false,
1921    }
1922}
1923
1924fn expr_ty<'hir>(gcx: Gcx<'hir>, expr: &'hir hir::Expr<'hir>) -> Option<Ty<'hir>> {
1925    gcx.type_of_expr(expr.peel_parens().id)
1926}
1927
1928fn expr_is_function<'hir>(gcx: Gcx<'hir>, expr: &'hir hir::Expr<'hir>) -> bool {
1929    expr_ty(gcx, expr).is_some_and(|ty| matches!(ty.peel_refs().kind, TyKind::Fn(_)))
1930}
1931
1932fn expr_is_array_or_bytes<'hir>(gcx: Gcx<'hir>, expr: &'hir hir::Expr<'hir>) -> bool {
1933    expr_ty(gcx, expr).is_some_and(|ty| {
1934        matches!(
1935            ty.peel_refs().kind,
1936            TyKind::Array(..) | TyKind::DynArray(_) | TyKind::Elementary(ElementaryType::Bytes)
1937        )
1938    })
1939}
1940
1941fn ty_is_address(ty: Ty<'_>) -> bool {
1942    matches!(ty.peel_refs().kind, TyKind::Elementary(ElementaryType::Address(_)))
1943}
1944
1945fn is_require_or_assert(callee: &hir::Expr<'_>) -> bool {
1946    let ExprKind::Ident(reses) = &callee.kind else { return false };
1947    reses.iter().any(
1948        |r| matches!(r, Res::Builtin(b) if b.name() == sym::require || b.name() == sym::assert),
1949    )
1950}
1951
1952/// `address(this)`, `payable(this)`, `IFoo(this)`, `IFoo(address(this))`, or bare `this`.
1953fn is_address_self(expr: &hir::Expr<'_>) -> bool {
1954    let expr = expr.peel_parens();
1955    if is_builtin(expr, sym::this) {
1956        return true;
1957    }
1958    if let ExprKind::Payable(inner) = &expr.kind {
1959        return is_address_self(inner);
1960    }
1961    matches!(&expr.kind, ExprKind::Call(callee, args, _) if is_address_like_cast_callee(callee)
1962        && args.exprs().next().is_some_and(is_address_self))
1963}
1964
1965fn is_builtin(expr: &hir::Expr<'_>, name: Symbol) -> bool {
1966    let ExprKind::Ident(reses) = &expr.peel_parens().kind else { return false };
1967    reses.iter().any(|r| matches!(r, Res::Builtin(b) if b.name() == name))
1968}
1969
1970fn is_literal_zero(expr: &hir::Expr<'_>) -> bool {
1971    if let ExprKind::Lit(lit) = &expr.peel_parens().kind
1972        && let LitKind::Number(n) = &lit.kind
1973    {
1974        return n.is_zero();
1975    }
1976    false
1977}
1978
1979/// `return`, custom-error `revert`, `revert(...)`, or `assert(false)` / `require(false, ...)`.
1980fn branch_always_exits(stmt: &hir::Stmt<'_>) -> bool {
1981    match &stmt.kind {
1982        StmtKind::Return(_) | StmtKind::Revert(_) => true,
1983        StmtKind::Expr(expr) => is_exit_call(expr),
1984        StmtKind::Block(b) | StmtKind::UncheckedBlock(b) => b.stmts.iter().any(branch_always_exits),
1985        StmtKind::If(_, t, Some(e)) => branch_always_exits(t) && branch_always_exits(e),
1986        StmtKind::Try(t) => {
1987            !t.clauses.is_empty()
1988                && t.clauses.iter().all(|c| c.block.stmts.iter().any(branch_always_exits))
1989        }
1990        _ => false,
1991    }
1992}
1993
1994fn is_exit_call(expr: &hir::Expr<'_>) -> bool {
1995    let ExprKind::Call(callee, args, _) = &expr.kind else { return false };
1996    if is_builtin(callee, kw::Revert) {
1997        return true;
1998    }
1999    if let ExprKind::Ident(reses) = &callee.peel_parens().kind
2000        && reses.iter().any(|r| matches!(r, Res::Builtin(Builtin::Selfdestruct)))
2001    {
2002        return true;
2003    }
2004    if is_require_or_assert(callee)
2005        && let hir::CallArgsKind::Unnamed(unnamed) = args.kind
2006        && let Some(first) = unnamed.first()
2007        && matches!(
2008            &first.peel_parens().kind,
2009            ExprKind::Lit(lit) if matches!(lit.kind, ast::LitKind::Bool(false))
2010        )
2011    {
2012        return true;
2013    }
2014    false
2015}