Skip to main content

forge_lint/sol/low/
reentrancy_events.rs

1use super::{
2    ReentrancyEvents,
3    calls_loop::{
4        is_state_mutating_external_call, resolved_internal_function_ids,
5        resolved_super_function_ids,
6    },
7};
8use crate::{
9    linter::{LateLintPass, LintContext},
10    sol::{
11        Severity, SolLint,
12        analysis::helper_cache::{DEFAULT_HELPER_ANALYSIS_CACHE_LIMIT, HelperAnalysisCache},
13    },
14};
15use solar::{
16    ast::LitKind,
17    interface::{Span, kw, sym},
18    sema::{
19        Gcx,
20        hir::{
21            self, Block, ContractId, Expr, ExprKind, Function, FunctionId, Hir, Res, Stmt, StmtKind,
22        },
23    },
24};
25use std::collections::{HashMap, HashSet};
26
27declare_forge_lint!(
28    REENTRANCY_EVENTS,
29    Severity::Low,
30    "reentrancy-events",
31    "event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on"
32);
33
34impl<'hir> LateLintPass<'hir> for ReentrancyEvents {
35    fn check_function(
36        &mut self,
37        ctx: &LintContext,
38        gcx: Gcx<'hir>,
39        hir: &'hir Hir<'hir>,
40        func: &'hir Function<'hir>,
41    ) {
42        let Some(body) = func.body else { return };
43
44        let mut analyzer = Analyzer::new(ctx, gcx, hir, func.contract);
45        let _ = analyzer.analyze_callable(func, body, FlowState::default());
46    }
47}
48
49type Placeholder<'hir> = Option<(&'hir [hir::Modifier<'hir>], usize, Block<'hir>)>;
50
51/// Per-path state tracked by the may-analysis.
52#[derive(Clone, Debug, Default, PartialEq, Eq)]
53struct FlowState {
54    /// True iff an external call has occurred on the path leading to the current program point.
55    external_call_seen: bool,
56}
57
58impl FlowState {
59    const fn merge(&mut self, other: &Self) {
60        self.external_call_seen |= other.external_call_seen;
61    }
62}
63
64/// Summarises how a piece of code can exit, with the [`FlowState`] reaching each exit kind.
65/// `None` means no path produces that exit; `Some(_)` means some path does.
66///
67/// Aborting paths (`revert`/`require(false)`/etc.) drop their state — they are simply absent
68/// from every bucket, so they cannot taint subsequent statements.
69#[derive(Clone, Debug, Default)]
70struct Exits {
71    /// Control falls through to the next statement of the enclosing block.
72    fallthrough: Option<FlowState>,
73    /// Control exits the enclosing function via `return`.
74    return_: Option<FlowState>,
75    /// Control exits the enclosing loop via `break`.
76    break_: Option<FlowState>,
77    /// Control goes back to the loop header via `continue`.
78    continue_: Option<FlowState>,
79}
80
81impl Exits {
82    fn fallthrough(state: FlowState) -> Self {
83        Self { fallthrough: Some(state), ..Default::default() }
84    }
85
86    fn return_(state: FlowState) -> Self {
87        Self { return_: Some(state), ..Default::default() }
88    }
89
90    fn break_(state: FlowState) -> Self {
91        Self { break_: Some(state), ..Default::default() }
92    }
93
94    fn continue_(state: FlowState) -> Self {
95        Self { continue_: Some(state), ..Default::default() }
96    }
97
98    /// Aborting exit (`revert`, infinite loop, panic): no paths flow out.
99    fn abort() -> Self {
100        Self::default()
101    }
102
103    const fn merge(&mut self, other: Self) {
104        merge_opt(&mut self.fallthrough, other.fallthrough);
105        merge_opt(&mut self.return_, other.return_);
106        merge_opt(&mut self.break_, other.break_);
107        merge_opt(&mut self.continue_, other.continue_);
108    }
109}
110
111const fn merge_opt(dst: &mut Option<FlowState>, src: Option<FlowState>) {
112    match (dst.as_mut(), src) {
113        (None, src) => *dst = src,
114        (Some(_), None) => {}
115        (Some(d), Some(s)) => d.merge(&s),
116    }
117}
118
119#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
120struct InlineCallKey {
121    func_id: FunctionId,
122    external_call_seen: bool,
123    suppress_inline_reports: bool,
124}
125
126struct Analyzer<'ctx, 's, 'c, 'hir> {
127    ctx: &'ctx LintContext<'s, 'c>,
128    gcx: Gcx<'hir>,
129    hir: &'hir Hir<'hir>,
130    /// Top-level analyzed contract; used to resolve `this.<method>` without consulting
131    /// Solar for the `this` builtin. Held fixed across inlined helpers (runtime `this`).
132    enclosing_contract: Option<ContractId>,
133    /// Call stack to break recursion when inlining internal helpers and modifiers.
134    call_stack: Vec<FunctionId>,
135    /// Cached summaries for transitive helper analysis. This keeps shared helper graphs from
136    /// being rescanned for every call edge in a function.
137    inline_cache: HelperAnalysisCache<InlineCallKey, Exits>,
138    /// Cached conservative summaries used only when a recursive edge is cut.
139    external_call_reachability: HashMap<FunctionId, bool>,
140    /// Spans already reported, to dedupe diagnostics across paths/iterations.
141    emitted: HashSet<Span>,
142    /// When `true`, suppress emit diagnostics: we are inside an inlined helper that was
143    /// entered with a clean state, so the helper's own self-pass will catch any taint.
144    suppress_inline_reports: bool,
145    /// Set by `analyze_internal_call` when the inlined callee has no normal exits, so the
146    /// enclosing statement can treat itself as aborting.
147    expr_aborted: bool,
148}
149
150impl<'ctx, 's, 'c, 'hir> Analyzer<'ctx, 's, 'c, 'hir> {
151    fn new(
152        ctx: &'ctx LintContext<'s, 'c>,
153        gcx: Gcx<'hir>,
154        hir: &'hir Hir<'hir>,
155        enclosing_contract: Option<ContractId>,
156    ) -> Self {
157        Self {
158            ctx,
159            gcx,
160            hir,
161            enclosing_contract,
162            call_stack: Vec::new(),
163            inline_cache: HelperAnalysisCache::new(DEFAULT_HELPER_ANALYSIS_CACHE_LIMIT),
164            external_call_reachability: HashMap::new(),
165            emitted: HashSet::new(),
166            suppress_inline_reports: false,
167            expr_aborted: false,
168        }
169    }
170
171    fn analyze_callable(
172        &mut self,
173        func: &'hir Function<'hir>,
174        body: Block<'hir>,
175        entry: FlowState,
176    ) -> Exits {
177        self.analyze_modifier_chain(func.modifiers, 0, body, entry)
178    }
179
180    fn analyze_modifier_chain(
181        &mut self,
182        modifiers: &'hir [hir::Modifier<'hir>],
183        index: usize,
184        body: Block<'hir>,
185        mut entry: FlowState,
186    ) -> Exits {
187        let Some(modifier) = modifiers.get(index) else {
188            return self.analyze_block(body, None, entry);
189        };
190
191        for arg in modifier.args.exprs() {
192            self.expr_aborted = false;
193            self.analyze_expr(arg, &mut entry);
194            // An aborting arg means the modifier (and therefore its body) is never entered.
195            if self.expr_aborted {
196                return Exits::abort();
197            }
198        }
199
200        let Some(modifier_id) = modifier.id.as_function() else {
201            return self.analyze_modifier_chain(modifiers, index + 1, body, entry);
202        };
203
204        // Note: we deliberately do NOT skip duplicate modifier IDs here. A modifier may
205        // legitimately appear at multiple indices in the chain (e.g. `f() m(false) m(true)`),
206        // and the chain itself cannot recurse infinitely because `index` strictly increases.
207        // True recursion through internal calls is still handled by `analyze_internal_call`.
208
209        let modifier_func = self.hir.function(modifier_id);
210        let Some(modifier_body) = modifier_func.body else {
211            return self.analyze_modifier_chain(modifiers, index + 1, body, entry);
212        };
213
214        self.call_stack.push(modifier_id);
215        let summary = self.analyze_block(modifier_body, Some((modifiers, index + 1, body)), entry);
216        self.call_stack.pop();
217        summary
218    }
219
220    fn analyze_block(
221        &mut self,
222        block: Block<'hir>,
223        placeholder: Placeholder<'hir>,
224        mut entry: FlowState,
225    ) -> Exits {
226        let mut summary = Exits::default();
227        for stmt in block.stmts {
228            let stmt_exits = self.analyze_stmt(stmt, placeholder, entry);
229            // Non-fallthrough exits propagate up out of the block.
230            merge_opt(&mut summary.return_, stmt_exits.return_);
231            merge_opt(&mut summary.break_, stmt_exits.break_);
232            merge_opt(&mut summary.continue_, stmt_exits.continue_);
233            // Only the fallthrough state reaches the next statement.
234            match stmt_exits.fallthrough {
235                Some(next) => entry = next,
236                None => return summary, // Subsequent statements are dead.
237            }
238        }
239        summary.fallthrough = Some(entry);
240        summary
241    }
242
243    fn analyze_stmt(
244        &mut self,
245        stmt: &'hir Stmt<'hir>,
246        placeholder: Placeholder<'hir>,
247        mut entry: FlowState,
248    ) -> Exits {
249        // Reset once per statement so each branch can read `expr_aborted` after analyzing
250        // its top-level expressions without leaking state from a previous statement.
251        self.expr_aborted = false;
252        match stmt.kind {
253            StmtKind::DeclSingle(var_id) => {
254                if let Some(init) = self.hir.variable(var_id).initializer {
255                    self.analyze_expr(init, &mut entry);
256                }
257                if self.expr_aborted {
258                    return Exits::abort();
259                }
260                Exits::fallthrough(entry)
261            }
262            StmtKind::DeclMulti(_, expr) | StmtKind::Expr(expr) => {
263                self.analyze_expr(expr, &mut entry);
264                // Aborts via builtins (`revert()`, `selfdestruct(...)`, `require(false, …)`,
265                // `assert(false)`) or via an inlined helper with no normal exit.
266                if is_aborting_call(expr) || self.expr_aborted {
267                    return Exits::abort();
268                }
269                Exits::fallthrough(entry)
270            }
271            StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => {
272                self.analyze_block(block, placeholder, entry)
273            }
274            StmtKind::Emit(expr) => {
275                // Solidity evaluates event arguments before emitting, so an external call inside
276                // the arguments also taints this emit. Analyze the args first, then check state.
277                self.analyze_expr(expr, &mut entry);
278                // If an argument aborts (e.g. `emit E(helperThatAlwaysReverts())`), the emit
279                // itself is unreachable, so it must not be reported and the path aborts.
280                if self.expr_aborted {
281                    return Exits::abort();
282                }
283                if entry.external_call_seen
284                    && !self.suppress_inline_reports
285                    && self.emitted.insert(stmt.span)
286                {
287                    self.ctx.emit(&REENTRANCY_EVENTS, stmt.span);
288                }
289                Exits::fallthrough(entry)
290            }
291            StmtKind::Revert(expr) => {
292                self.analyze_expr(expr, &mut entry);
293                Exits::abort()
294            }
295            StmtKind::Return(expr) => {
296                if let Some(expr) = expr {
297                    self.analyze_expr(expr, &mut entry);
298                }
299                // If the return value computation aborts, the `return` itself never runs.
300                if self.expr_aborted {
301                    return Exits::abort();
302                }
303                Exits::return_(entry)
304            }
305            StmtKind::Break => Exits::break_(entry),
306            StmtKind::Continue => Exits::continue_(entry),
307            StmtKind::Loop(block, _) => {
308                // Two-pass fixpoint: with a 1-bit state the back-edge can only strengthen
309                // `external_call_seen` from false to true, so a second pass with the merged
310                // entry suffices to catch emits tainted only on iterations 2..N. Duplicate
311                // diagnostics from the first pass are deduped via `self.emitted`.
312                let first = self.analyze_block(block, placeholder, entry.clone());
313
314                // Back-edge entry: pre-loop entry merged with anything that loops back
315                // (fallthrough at the end of the body, or an explicit `continue`).
316                let mut back_edge = entry.clone();
317                if let Some(ft) = &first.fallthrough {
318                    back_edge.merge(ft);
319                }
320                if let Some(c) = &first.continue_ {
321                    back_edge.merge(c);
322                }
323
324                let body = if back_edge == entry {
325                    first
326                } else {
327                    self.analyze_block(block, placeholder, back_edge)
328                };
329
330                // Post-loop state combines the entry (zero iterations), fallthrough at end of
331                // body, plus any `break` or `continue` exits. Aborting paths are absent and
332                // therefore drop out.
333                let mut post = entry;
334                if let Some(ft) = body.fallthrough {
335                    post.merge(&ft);
336                }
337                if let Some(b) = body.break_ {
338                    post.merge(&b);
339                }
340                if let Some(c) = body.continue_ {
341                    post.merge(&c);
342                }
343                Exits {
344                    fallthrough: Some(post),
345                    return_: body.return_,
346                    break_: None,
347                    continue_: None,
348                }
349            }
350            StmtKind::If(cond, then_stmt, else_stmt) => {
351                self.analyze_expr(cond, &mut entry);
352                // If the condition aborts (e.g. `if (helperThatAlwaysReverts())`), neither
353                // branch is reachable.
354                if self.expr_aborted {
355                    return Exits::abort();
356                }
357
358                let then_exits = self.analyze_stmt(then_stmt, placeholder, entry.clone());
359                let else_exits = if let Some(else_stmt) = else_stmt {
360                    self.analyze_stmt(else_stmt, placeholder, entry)
361                } else {
362                    Exits::fallthrough(entry)
363                };
364
365                let mut merged = then_exits;
366                merged.merge(else_exits);
367                merged
368            }
369            StmtKind::Try(try_stmt) => {
370                self.analyze_expr(&try_stmt.expr, &mut entry);
371                // If evaluating the try-call expression aborts before the call itself runs
372                // (e.g. an aborting arg), no clause can execute.
373                if self.expr_aborted {
374                    return Exits::abort();
375                }
376
377                let mut summary = Exits::default();
378                for clause in try_stmt.clauses {
379                    let clause_exits = self.analyze_block(clause.block, placeholder, entry.clone());
380                    summary.merge(clause_exits);
381                }
382                summary
383            }
384            StmtKind::Placeholder => {
385                if let Some((modifiers, index, body)) = placeholder {
386                    self.analyze_modifier_chain(modifiers, index, body, entry)
387                } else {
388                    Exits::fallthrough(entry)
389                }
390            }
391            StmtKind::AssemblyBlock(_) | StmtKind::Switch(_) | StmtKind::Err(_) => {
392                // Inline assembly can perform external interactions
393                // (call/delegatecall/create, logs). Conservatively taint.
394                entry.external_call_seen = true;
395                Exits::fallthrough(entry)
396            }
397        }
398    }
399
400    fn analyze_expr(&mut self, expr: &'hir Expr<'hir>, state: &mut FlowState) {
401        match &expr.kind {
402            ExprKind::Call(callee, args, opts) => {
403                self.analyze_expr(callee, state);
404                if let Some(opts) = opts {
405                    for opt in opts.args {
406                        self.analyze_expr(&opt.value, state);
407                    }
408                }
409                for arg in args.exprs() {
410                    self.analyze_expr(arg, state);
411                }
412
413                if is_state_mutating_external_call(
414                    self.gcx,
415                    self.hir,
416                    callee,
417                    args.len(),
418                    self.enclosing_contract,
419                ) {
420                    state.external_call_seen = true;
421                }
422
423                // Follow internal/private/public helpers transitively so external calls in
424                // helpers also taint the caller's flow state.
425                for func_id in resolved_internal_function_ids(self.hir, callee) {
426                    self.analyze_internal_call(func_id, state);
427                }
428                // Same for `super.<member>(...)` base-chain dispatch.
429                for func_id in resolved_super_function_ids(
430                    self.hir,
431                    self.enclosing_contract,
432                    callee,
433                    args.len(),
434                ) {
435                    self.analyze_internal_call(func_id, state);
436                }
437            }
438            ExprKind::Binary(lhs, op, rhs)
439                if matches!(op.kind, hir::BinOpKind::And | hir::BinOpKind::Or) =>
440            {
441                // Short-circuiting `&&`/`||`: LHS always runs, RHS is conditional. Model RHS
442                // on a forked state so its taint only reaches the merged result when the
443                // short-circuit path is also possible, and so an aborting RHS does not kill
444                // the whole expression (the short-circuit path still falls through).
445                self.analyze_expr(lhs, state);
446                let lhs_aborted = std::mem::replace(&mut self.expr_aborted, false);
447
448                let mut rhs_state = state.clone();
449                self.analyze_expr(rhs, &mut rhs_state);
450                let rhs_aborted = self.expr_aborted;
451
452                // The expression aborts iff LHS aborts (then no path survives); an
453                // RHS-only abort just drops the non-short-circuit path.
454                self.expr_aborted = lhs_aborted;
455
456                if !lhs_aborted && !rhs_aborted {
457                    state.merge(&rhs_state);
458                }
459            }
460            ExprKind::Assign(lhs, _, rhs) | ExprKind::Binary(lhs, _, rhs) => {
461                self.analyze_expr(lhs, state);
462                self.analyze_expr(rhs, state);
463            }
464            ExprKind::Unary(_, inner) | ExprKind::Delete(inner) | ExprKind::Payable(inner) => {
465                self.analyze_expr(inner, state);
466            }
467            ExprKind::Index(base, index) => {
468                self.analyze_expr(base, state);
469                if let Some(index) = index {
470                    self.analyze_expr(index, state);
471                }
472            }
473            ExprKind::Slice(base, start, end) => {
474                self.analyze_expr(base, state);
475                if let Some(start) = start {
476                    self.analyze_expr(start, state);
477                }
478                if let Some(end) = end {
479                    self.analyze_expr(end, state);
480                }
481            }
482            ExprKind::Ternary(cond, then_expr, else_expr) => {
483                self.analyze_expr(cond, state);
484                // Sample `expr_aborted` per branch so an aborting branch can't poison the
485                // sibling. The ternary aborts iff `cond` aborts OR both branches abort.
486                let outer_aborted = std::mem::replace(&mut self.expr_aborted, false);
487
488                let mut then_state = state.clone();
489                self.analyze_expr(then_expr, &mut then_state);
490                let then_aborted = std::mem::replace(&mut self.expr_aborted, false);
491
492                let mut else_state = state.clone();
493                self.analyze_expr(else_expr, &mut else_state);
494                let else_aborted = self.expr_aborted;
495
496                self.expr_aborted = outer_aborted || (then_aborted && else_aborted);
497
498                // Aborting branches drop their state; only surviving branches contribute.
499                match (then_aborted, else_aborted) {
500                    (true, true) => {}
501                    (true, false) => *state = else_state,
502                    (false, true) => *state = then_state,
503                    (false, false) => {
504                        *state = then_state;
505                        state.merge(&else_state);
506                    }
507                }
508            }
509            ExprKind::Array(exprs) => {
510                for expr in *exprs {
511                    self.analyze_expr(expr, state);
512                }
513            }
514            ExprKind::Tuple(exprs) => {
515                for expr in exprs.iter().copied().flatten() {
516                    self.analyze_expr(expr, state);
517                }
518            }
519            ExprKind::Member(base, _) => self.analyze_expr(base, state),
520            ExprKind::Ident(_)
521            | ExprKind::Lit(_)
522            | ExprKind::New(_)
523            | ExprKind::TypeCall(_)
524            | ExprKind::Type(_)
525            | ExprKind::YulMember(..)
526            | ExprKind::Err(_) => {}
527        }
528    }
529
530    fn analyze_internal_call(&mut self, func_id: FunctionId, state: &mut FlowState) {
531        if self.call_stack.contains(&func_id) {
532            // Keep inline summaries stack-insensitive by replacing cut recursive edges with a
533            // conservative cached "can this helper ever taint by external call?" summary.
534            if self.helper_may_reach_external_call(func_id) {
535                state.external_call_seen = true;
536            }
537            return;
538        }
539
540        let func = self.hir.function(func_id);
541        let Some(body) = func.body else { return };
542
543        let suppress_inline_reports = self.suppress_inline_reports || !state.external_call_seen;
544        let key = InlineCallKey {
545            func_id,
546            external_call_seen: state.external_call_seen,
547            suppress_inline_reports,
548        };
549
550        if self.inline_cache.is_in_progress(&key) {
551            return;
552        }
553
554        if let Some(summary) = self.inline_cache.get(&key).cloned() {
555            self.apply_inline_summary(summary, state);
556            return;
557        }
558
559        // Suppress diagnostics inside helpers entered with a clean state — the helper's
560        // own self-pass will independently catch any intra-helper taint, avoiding
561        // duplicate reports across callers.
562        let prev_suppress = self.suppress_inline_reports;
563        self.suppress_inline_reports = suppress_inline_reports;
564
565        self.inline_cache.start(key);
566        self.call_stack.push(func_id);
567        let summary = self.analyze_callable(func, body, state.clone());
568        self.call_stack.pop();
569
570        self.inline_cache.finish(key, summary.clone());
571
572        self.suppress_inline_reports = prev_suppress;
573
574        self.apply_inline_summary(summary, state);
575    }
576
577    fn apply_inline_summary(&mut self, summary: Exits, state: &mut FlowState) {
578        // Caller inherits the state of paths that return normally. If the callee has no
579        // normal exits (always aborts), signal abort to the enclosing statement.
580        let any_normal = summary.fallthrough.is_some() || summary.return_.is_some();
581        if any_normal {
582            let mut after = FlowState::default();
583            if let Some(ft) = summary.fallthrough {
584                after.merge(&ft);
585            }
586            if let Some(rt) = summary.return_ {
587                after.merge(&rt);
588            }
589            *state = after;
590        } else {
591            self.expr_aborted = true;
592        }
593    }
594
595    fn helper_may_reach_external_call(&mut self, func_id: FunctionId) -> bool {
596        self.helper_may_reach_external_call_inner(func_id, &mut HashSet::new()).0
597    }
598
599    fn helper_may_reach_external_call_inner(
600        &mut self,
601        func_id: FunctionId,
602        seen: &mut HashSet<FunctionId>,
603    ) -> (bool, bool) {
604        if let Some(cached) = self.external_call_reachability.get(&func_id) {
605            return (*cached, false);
606        }
607        if !seen.insert(func_id) {
608            return (false, true);
609        }
610
611        let func = self.hir.function(func_id);
612        let mut may_reach = false;
613        let mut cut_recursive_edge = false;
614        for modifier in func.modifiers {
615            for arg in modifier.args.exprs() {
616                let (arg_may_reach, arg_cut_recursive_edge) =
617                    self.expr_may_reach_external_call(arg, seen);
618                cut_recursive_edge |= arg_cut_recursive_edge;
619                if arg_may_reach {
620                    may_reach = true;
621                    break;
622                }
623            }
624            if may_reach {
625                break;
626            }
627            if let Some(modifier_id) = modifier.id.as_function() {
628                let (modifier_may_reach, modifier_cut_recursive_edge) =
629                    self.helper_may_reach_external_call_inner(modifier_id, seen);
630                cut_recursive_edge |= modifier_cut_recursive_edge;
631                if modifier_may_reach {
632                    may_reach = true;
633                    break;
634                }
635            }
636        }
637        if !may_reach && let Some(body) = func.body {
638            let (body_may_reach, body_cut_recursive_edge) =
639                self.block_may_reach_external_call(body, seen);
640            may_reach = body_may_reach;
641            cut_recursive_edge |= body_cut_recursive_edge;
642        }
643
644        seen.remove(&func_id);
645        if may_reach || !cut_recursive_edge {
646            self.external_call_reachability.insert(func_id, may_reach);
647        }
648        (may_reach, cut_recursive_edge)
649    }
650
651    fn block_may_reach_external_call(
652        &mut self,
653        block: Block<'hir>,
654        seen: &mut HashSet<FunctionId>,
655    ) -> (bool, bool) {
656        let mut cut_recursive_edge = false;
657        for stmt in block.stmts {
658            let (may_reach, stmt_cut_recursive_edge) =
659                self.stmt_may_reach_external_call(stmt, seen);
660            cut_recursive_edge |= stmt_cut_recursive_edge;
661            if may_reach {
662                return (true, cut_recursive_edge);
663            }
664        }
665        (false, cut_recursive_edge)
666    }
667
668    fn stmt_may_reach_external_call(
669        &mut self,
670        stmt: &'hir Stmt<'hir>,
671        seen: &mut HashSet<FunctionId>,
672    ) -> (bool, bool) {
673        match stmt.kind {
674            StmtKind::DeclSingle(var_id) => match self.hir.variable(var_id).initializer {
675                Some(expr) => self.expr_may_reach_external_call(expr, seen),
676                None => (false, false),
677            },
678            StmtKind::DeclMulti(_, expr)
679            | StmtKind::Expr(expr)
680            | StmtKind::Emit(expr)
681            | StmtKind::Revert(expr) => self.expr_may_reach_external_call(expr, seen),
682            StmtKind::Return(expr) => match expr {
683                Some(expr) => self.expr_may_reach_external_call(expr, seen),
684                None => (false, false),
685            },
686            StmtKind::Block(block) | StmtKind::UncheckedBlock(block) | StmtKind::Loop(block, _) => {
687                self.block_may_reach_external_call(block, seen)
688            }
689            StmtKind::If(cond, then_stmt, else_stmt) => Self::any_may_reach_external_call([
690                self.expr_may_reach_external_call(cond, seen),
691                self.stmt_may_reach_external_call(then_stmt, seen),
692                else_stmt
693                    .map(|else_stmt| self.stmt_may_reach_external_call(else_stmt, seen))
694                    .unwrap_or((false, false)),
695            ]),
696            StmtKind::Try(try_stmt) => {
697                let mut cut_recursive_edge = false;
698                let (expr_may_reach, expr_cut_recursive_edge) =
699                    self.expr_may_reach_external_call(&try_stmt.expr, seen);
700                cut_recursive_edge |= expr_cut_recursive_edge;
701                if expr_may_reach {
702                    return (true, cut_recursive_edge);
703                }
704                for clause in try_stmt.clauses {
705                    let (clause_may_reach, clause_cut_recursive_edge) =
706                        self.block_may_reach_external_call(clause.block, seen);
707                    cut_recursive_edge |= clause_cut_recursive_edge;
708                    if clause_may_reach {
709                        return (true, cut_recursive_edge);
710                    }
711                }
712                (false, cut_recursive_edge)
713            }
714            StmtKind::AssemblyBlock(_) | StmtKind::Switch(_) => (true, false),
715            StmtKind::Break | StmtKind::Continue | StmtKind::Placeholder | StmtKind::Err(_) => {
716                (false, false)
717            }
718        }
719    }
720
721    fn expr_may_reach_external_call(
722        &mut self,
723        expr: &'hir Expr<'hir>,
724        seen: &mut HashSet<FunctionId>,
725    ) -> (bool, bool) {
726        match &expr.kind {
727            ExprKind::Call(callee, args, opts) => {
728                let mut cut_recursive_edge = false;
729                let (callee_may_reach, callee_cut_recursive_edge) =
730                    self.expr_may_reach_external_call(callee, seen);
731                cut_recursive_edge |= callee_cut_recursive_edge;
732                if callee_may_reach {
733                    return (true, cut_recursive_edge);
734                }
735                if let Some(opts) = opts {
736                    for opt in opts.args {
737                        let (opt_may_reach, opt_cut_recursive_edge) =
738                            self.expr_may_reach_external_call(&opt.value, seen);
739                        cut_recursive_edge |= opt_cut_recursive_edge;
740                        if opt_may_reach {
741                            return (true, cut_recursive_edge);
742                        }
743                    }
744                }
745                for arg in args.exprs() {
746                    let (arg_may_reach, arg_cut_recursive_edge) =
747                        self.expr_may_reach_external_call(arg, seen);
748                    cut_recursive_edge |= arg_cut_recursive_edge;
749                    if arg_may_reach {
750                        return (true, cut_recursive_edge);
751                    }
752                }
753                if is_state_mutating_external_call(
754                    self.gcx,
755                    self.hir,
756                    callee,
757                    args.len(),
758                    self.enclosing_contract,
759                ) {
760                    return (true, cut_recursive_edge);
761                }
762
763                let internal: Vec<_> = resolved_internal_function_ids(self.hir, callee).collect();
764                for func_id in internal {
765                    let (func_may_reach, func_cut_recursive_edge) =
766                        self.helper_may_reach_external_call_inner(func_id, seen);
767                    cut_recursive_edge |= func_cut_recursive_edge;
768                    if func_may_reach {
769                        return (true, cut_recursive_edge);
770                    }
771                }
772
773                for func_id in resolved_super_function_ids(
774                    self.hir,
775                    self.enclosing_contract,
776                    callee,
777                    args.len(),
778                ) {
779                    let (func_may_reach, func_cut_recursive_edge) =
780                        self.helper_may_reach_external_call_inner(func_id, seen);
781                    cut_recursive_edge |= func_cut_recursive_edge;
782                    if func_may_reach {
783                        return (true, cut_recursive_edge);
784                    }
785                }
786
787                (false, cut_recursive_edge)
788            }
789            ExprKind::Assign(lhs, _, rhs) | ExprKind::Binary(lhs, _, rhs) => {
790                Self::any_may_reach_external_call([
791                    self.expr_may_reach_external_call(lhs, seen),
792                    self.expr_may_reach_external_call(rhs, seen),
793                ])
794            }
795            ExprKind::Unary(_, inner)
796            | ExprKind::Delete(inner)
797            | ExprKind::Payable(inner)
798            | ExprKind::Member(inner, _) => self.expr_may_reach_external_call(inner, seen),
799            ExprKind::Index(base, index) => Self::any_may_reach_external_call([
800                self.expr_may_reach_external_call(base, seen),
801                index
802                    .map(|index| self.expr_may_reach_external_call(index, seen))
803                    .unwrap_or((false, false)),
804            ]),
805            ExprKind::Slice(base, start, end) => Self::any_may_reach_external_call([
806                self.expr_may_reach_external_call(base, seen),
807                start
808                    .map(|start| self.expr_may_reach_external_call(start, seen))
809                    .unwrap_or((false, false)),
810                end.map(|end| self.expr_may_reach_external_call(end, seen))
811                    .unwrap_or((false, false)),
812            ]),
813            ExprKind::Ternary(cond, then_expr, else_expr) => Self::any_may_reach_external_call([
814                self.expr_may_reach_external_call(cond, seen),
815                self.expr_may_reach_external_call(then_expr, seen),
816                self.expr_may_reach_external_call(else_expr, seen),
817            ]),
818            ExprKind::Array(exprs) => self.exprs_may_reach_external_call(exprs, seen),
819            ExprKind::Tuple(exprs) => {
820                let mut cut_recursive_edge = false;
821                for expr in exprs.iter().copied().flatten() {
822                    let (expr_may_reach, expr_cut_recursive_edge) =
823                        self.expr_may_reach_external_call(expr, seen);
824                    cut_recursive_edge |= expr_cut_recursive_edge;
825                    if expr_may_reach {
826                        return (true, cut_recursive_edge);
827                    }
828                }
829                (false, cut_recursive_edge)
830            }
831            ExprKind::Ident(_)
832            | ExprKind::Lit(_)
833            | ExprKind::New(_)
834            | ExprKind::TypeCall(_)
835            | ExprKind::Type(_)
836            | ExprKind::YulMember(..)
837            | ExprKind::Err(_) => (false, false),
838        }
839    }
840
841    fn exprs_may_reach_external_call(
842        &mut self,
843        exprs: &'hir [Expr<'hir>],
844        seen: &mut HashSet<FunctionId>,
845    ) -> (bool, bool) {
846        let mut cut_recursive_edge = false;
847        for expr in exprs {
848            let (expr_may_reach, expr_cut_recursive_edge) =
849                self.expr_may_reach_external_call(expr, seen);
850            cut_recursive_edge |= expr_cut_recursive_edge;
851            if expr_may_reach {
852                return (true, cut_recursive_edge);
853            }
854        }
855        (false, cut_recursive_edge)
856    }
857
858    fn any_may_reach_external_call(
859        results: impl IntoIterator<Item = (bool, bool)>,
860    ) -> (bool, bool) {
861        let mut cut_recursive_edge = false;
862        for (may_reach, result_cut_recursive_edge) in results {
863            cut_recursive_edge |= result_cut_recursive_edge;
864            if may_reach {
865                return (true, cut_recursive_edge);
866            }
867        }
868        (false, cut_recursive_edge)
869    }
870}
871
872/// Returns `true` when the expression-statement is a builtin call that always terminates
873/// execution: `revert()` / `revert("msg")`, `selfdestruct(...)`, `require(false, ...)`, or
874/// `assert(false)`.
875fn is_aborting_call(expr: &Expr<'_>) -> bool {
876    let ExprKind::Call(callee, args, _) = &expr.peel_parens().kind else {
877        return false;
878    };
879    let ExprKind::Ident(reses) = &callee.peel_parens().kind else {
880        return false;
881    };
882    for res in *reses {
883        let Res::Builtin(b) = res else { continue };
884        let name = b.name();
885        if name == kw::Revert || name == kw::Selfdestruct {
886            return true;
887        }
888        if (name == sym::require || name == sym::assert)
889            && args.exprs().next().is_some_and(literal_false)
890        {
891            return true;
892        }
893    }
894    false
895}
896
897/// Returns `true` if `expr` is the boolean literal `false`.
898fn literal_false(expr: &Expr<'_>) -> bool {
899    matches!(
900        &expr.peel_parens().kind,
901        ExprKind::Lit(lit) if matches!(lit.kind, LitKind::Bool(false))
902    )
903}