Skip to main content

forge_lint/sol/high/
reentrancy.rs

1use super::ReentrancyEth;
2use crate::{
3    linter::{LateLintPass, LintContext},
4    sol::{
5        Severity, SolLint,
6        analysis::{
7            DEFAULT_HELPER_ANALYSIS_CACHE_LIMIT, HelperAnalysisCache, arg_for_param,
8            branch_always_exits, cast_type, count_placeholders, expr_is_address, for_each_child,
9            for_each_lhs_var, is_address_cast, is_builtin, is_require_or_assert, lhs_local_var,
10            loop_update, state_lhs_vars, stmts_before_placeholder, tuple_elems,
11        },
12    },
13};
14use alloy_primitives::U256;
15use solar::{
16    ast::{BinOpKind, FunctionKind, LitKind, StateMutability, UnOpKind, Visibility},
17    interface::{Span, Symbol, data_structures::Never, kw, sym},
18    sema::{
19        Gcx,
20        builtins::Builtin,
21        hir::{
22            self, CallArgs, CallOptions, Expr, ExprKind, FunctionId, ItemId, LoopSource, Stmt,
23            StmtKind, VariableId, Visit,
24        },
25        ty::{TyFnKind, TyKind},
26    },
27};
28use std::{
29    collections::{BTreeMap, BTreeSet, HashMap, HashSet},
30    ops::ControlFlow,
31};
32
33/// Gas stipend forwarded by `transfer`/`send`; a call capped at or below it cannot reenter.
34const REENTRANCY_GAS_STIPEND: u64 = 2_300;
35
36declare_forge_lint!(
37    REENTRANCY_BALANCE,
38    Severity::High,
39    "reentrancy-balance",
40    "external call can be reentered before a stale contract balance is checked"
41);
42
43declare_forge_lint!(
44    REENTRANCY_ETH,
45    Severity::High,
46    "reentrancy-eth",
47    "state read before ETH transfer is written after the transfer"
48);
49
50declare_forge_lint!(
51    REENTRANCY_NO_ETH,
52    Severity::Med,
53    "reentrancy-no-eth",
54    "state read before external call is written after the call"
55);
56
57impl<'gcx> LateLintPass<'gcx> for ReentrancyEth {
58    fn check_function(
59        &mut self,
60        ctx: &LintContext,
61        gcx: Gcx<'gcx>,
62        func: &'gcx hir::Function<'gcx>,
63    ) {
64        let Some(body) = func.body.filter(|_| is_entry_point(func)) else { return };
65        let mut analyzer = Analyzer::new(ctx, gcx, func);
66        if analyzer.has_enabled_lints() {
67            analyzer.analyze_callable(func, body, &mut FlowState::default());
68        }
69    }
70}
71
72/// Non-view functions an external caller can invoke: public/external functions, `fallback` and
73/// `receive`.
74fn is_entry_point(func: &hir::Function<'_>) -> bool {
75    !is_view_or_pure(func.state_mutability)
76        && !func.is_constructor()
77        && (func.is_special()
78            || (func.kind.is_function()
79                && matches!(func.visibility, Visibility::Public | Visibility::External)))
80}
81
82const fn is_view_or_pure(mutability: StateMutability) -> bool {
83    matches!(mutability, StateMutability::Pure | StateMutability::View)
84}
85
86type PathPredicates = BTreeMap<PathPredicate, bool>;
87type PathAlternatives = BTreeSet<PathPredicates>;
88
89/// Facts that hold along one execution path.
90#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
91struct FlowState {
92    /// State variables read so far.
93    state_reads: BTreeSet<VariableId>,
94    /// Reentrant calls made after a state read, with the state read before each, awaiting a
95    /// later write to that state.
96    pending_calls: BTreeMap<(Span, ReentrantCallKind), BTreeSet<VariableId>>,
97    /// Internal functions a function-typed local may point to.
98    internal_function_targets: BTreeMap<VariableId, BTreeSet<FunctionId>>,
99    /// Locals holding `address(this)`, with the path predicates under which they do.
100    self_address_local_paths: BTreeMap<VariableId, PathAlternatives>,
101    /// Locals derived from `address(this).balance`, with the predicates under which they are.
102    balance_local_paths: BTreeMap<VariableId, PathAlternatives>,
103    /// Supported arithmetic forms of local values; an empty set means unknown.
104    balance_local_forms: BTreeMap<VariableId, BTreeSet<BalanceForm>>,
105    /// Balance occurrences retained through arbitrary arithmetic on local values.
106    balance_local_dependencies: BTreeMap<VariableId, BTreeSet<BalanceForm>>,
107    /// Locals holding a comparison against a balance made stale by the given calls.
108    balance_comparison_locals: BTreeMap<VariableId, BTreeSet<Span>>,
109    /// External calls after which cached balance locals are stale.
110    pending_balance_calls: BTreeMap<Span, PathAlternatives>,
111    /// Reentrancy-guard locks written or bypassed while active.
112    invalidated_balance_guards: BTreeSet<VariableId>,
113    /// Boolean facts known to hold on this path.
114    path_predicates: PathPredicates,
115}
116
117#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
118enum PathPredicate {
119    Boolean(VariableId),
120    Equality(Operand, Operand),
121}
122
123impl PathPredicate {
124    fn mentions(self, f: impl Fn(VariableId) -> bool) -> bool {
125        match self {
126            Self::Boolean(var_id) => f(var_id),
127            Self::Equality(lhs, rhs) => {
128                [lhs, rhs].into_iter().any(|op| matches!(op, Operand::Variable(v) if f(v)))
129            }
130        }
131    }
132}
133
134/// A local variable or constant appearing in a predicate or lock expression.
135#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
136enum Operand {
137    Variable(VariableId),
138    Number(U256),
139    Boolean(bool),
140}
141
142#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
143enum ReentrantCallKind {
144    Eth,
145    NoEth,
146}
147
148/// One signed balance read, retaining the calls across which it was cached.
149#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
150struct BalanceTerm {
151    negative: bool,
152    stale_calls: BTreeSet<Span>,
153}
154
155/// A supported additive source pattern on one path. An empty term list is balance-independent.
156/// At most two balance terms are retained: a stale/current comparison needs one of each. More
157/// terms are rejected rather than cancelled, since Solidity arithmetic may wrap or revert.
158#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
159struct BalanceForm {
160    terms: Vec<BalanceTerm>,
161    path: PathPredicates,
162}
163
164impl BalanceForm {
165    fn combine(&self, rhs: &Self, subtract: bool) -> Option<Self> {
166        if self.terms.len() + rhs.terms.len() > 2 || !paths_compatible(&self.path, &rhs.path) {
167            return None;
168        }
169        let mut terms = self.terms.clone();
170        terms.extend(rhs.terms.iter().cloned().map(|mut term| {
171            term.negative ^= subtract;
172            term
173        }));
174        Some(Self {
175            terms,
176            path: self.path.iter().chain(&rhs.path).map(|(p, v)| (*p, *v)).collect(),
177        })
178    }
179
180    fn constrained(&self, active: &PathPredicates) -> Option<Self> {
181        paths_compatible(&self.path, active).then(|| Self {
182            terms: self.terms.clone(),
183            path: self.path.iter().chain(active).map(|(p, v)| (*p, *v)).collect(),
184        })
185    }
186}
187
188/// Balance-related facts about a value flowing into a local, parameter or return slot.
189#[derive(Clone, Debug, Default)]
190struct BalanceValue {
191    /// Supported arithmetic forms, separate from occurrence-based dependencies.
192    forms: BTreeSet<BalanceForm>,
193    /// Individual balance occurrences, without arithmetic signs or identities.
194    dependencies: BTreeSet<BalanceForm>,
195    /// Predicates under which the value derives from `address(this).balance`.
196    balance_paths: PathAlternatives,
197    /// Predicates under which the value is `address(this)`.
198    self_address_paths: PathAlternatives,
199    /// Calls whose stale balance the value compares against.
200    stale_comparisons: BTreeSet<Span>,
201}
202
203impl BalanceValue {
204    fn merge(&mut self, other: Self) {
205        self.forms.extend(other.forms);
206        self.dependencies.extend(other.dependencies);
207        self.balance_paths.extend(other.balance_paths);
208        self.self_address_paths.extend(other.self_address_paths);
209        self.stale_comparisons.extend(other.stale_comparisons);
210    }
211
212    /// The same value seen from a path on which `predicates` hold.
213    fn constrained(&self, predicates: &PathPredicates) -> Self {
214        Self {
215            forms: self.forms.iter().filter_map(|form| form.constrained(predicates)).collect(),
216            dependencies: self
217                .dependencies
218                .iter()
219                .filter_map(|form| form.constrained(predicates))
220                .collect(),
221            balance_paths: constrain_paths(&self.balance_paths, predicates),
222            self_address_paths: constrain_paths(&self.self_address_paths, predicates),
223            ..self.clone()
224        }
225    }
226}
227
228impl FlowState {
229    fn push_call(&mut self, span: Span, kind: ReentrantCallKind) {
230        if !self.state_reads.is_empty() {
231            self.pending_calls.entry((span, kind)).or_default().extend(&self.state_reads);
232        }
233    }
234
235    fn push_balance_call(&mut self, span: Span) {
236        if self
237            .balance_local_paths
238            .values()
239            .any(|paths| paths.iter().any(|path| paths_compatible(path, &self.path_predicates)))
240        {
241            self.pending_balance_calls
242                .entry(span)
243                .or_default()
244                .insert(self.path_predicates.clone());
245            for forms in self
246                .balance_local_forms
247                .values_mut()
248                .chain(self.balance_local_dependencies.values_mut())
249            {
250                *forms = std::mem::take(forms)
251                    .into_iter()
252                    .map(|mut form| {
253                        if paths_compatible(&form.path, &self.path_predicates) {
254                            for term in &mut form.terms {
255                                term.stale_calls.insert(span);
256                            }
257                        }
258                        form
259                    })
260                    .collect();
261            }
262        }
263    }
264
265    fn merge(&mut self, other: &Self) {
266        self.state_reads.extend(&other.state_reads);
267        merge_maps(&mut self.pending_calls, &other.pending_calls);
268        self.merge_balance(other);
269    }
270
271    fn merge_balance(&mut self, other: &Self) {
272        merge_maps(&mut self.internal_function_targets, &other.internal_function_targets);
273        merge_maps(&mut self.self_address_local_paths, &other.self_address_local_paths);
274        merge_maps(&mut self.balance_local_paths, &other.balance_local_paths);
275        merge_maps(&mut self.balance_local_forms, &other.balance_local_forms);
276        merge_maps(&mut self.balance_local_dependencies, &other.balance_local_dependencies);
277        merge_maps(&mut self.balance_comparison_locals, &other.balance_comparison_locals);
278        self.invalidated_balance_guards.extend(&other.invalidated_balance_guards);
279        merge_maps(&mut self.pending_balance_calls, &other.pending_balance_calls);
280    }
281
282    fn balance_only(&self) -> Self {
283        Self { state_reads: BTreeSet::new(), pending_calls: BTreeMap::new(), ..self.clone() }
284    }
285
286    /// Records that `predicate` holds; returns `false` if that contradicts a known fact.
287    fn constrain_path(&mut self, (predicate, value): (PathPredicate, bool)) -> bool {
288        match self.path_predicates.get(&predicate) {
289            Some(existing) => *existing == value,
290            None => {
291                self.path_predicates.insert(predicate, value);
292                for paths in self.self_address_local_paths.values_mut() {
293                    *paths = constrain_paths(paths, &self.path_predicates);
294                }
295                true
296            }
297        }
298    }
299}
300
301fn merge_maps<K: Copy + Ord, V: Clone + Ord>(
302    into: &mut BTreeMap<K, BTreeSet<V>>,
303    from: &BTreeMap<K, BTreeSet<V>>,
304) {
305    for (key, values) in from {
306        into.entry(*key).or_default().extend(values.iter().cloned());
307    }
308}
309
310/// Replaces `state` with the union of the reachable `branches`, keeping only the path predicates
311/// all of them agree on. Returns whether any branch was reachable.
312fn join_branches(
313    state: &mut FlowState,
314    branches: impl IntoIterator<Item = Option<FlowState>>,
315) -> bool {
316    *state = FlowState::default();
317    let mut predicates = None;
318    for branch in branches.into_iter().flatten() {
319        state.merge(&branch);
320        predicates = Some(match predicates {
321            Some(common) => common_path_predicates(&common, &branch.path_predicates),
322            None => branch.path_predicates,
323        });
324    }
325    let reachable = predicates.is_some();
326    state.path_predicates = predicates.unwrap_or_default();
327    reachable
328}
329
330struct Analyzer<'ctx, 's, 'c, 'gcx> {
331    ctx: &'ctx LintContext<'s, 'c>,
332    gcx: Gcx<'gcx>,
333    emitted: HashSet<Span>,
334    emitted_balance: HashSet<Span>,
335    call_stack: Vec<FunctionId>,
336    inline_cache: HelperAnalysisCache<InlineCallKey, (FlowState, Vec<BalanceValue>)>,
337    /// First function on the given call stack reachable again from a callee, per callee.
338    recursive_cuts: HashMap<(FunctionId, BTreeSet<FunctionId>), Option<FunctionId>>,
339    direct_internal_calls: HashMap<FunctionId, Vec<FunctionId>>,
340    reentrancy_eth_enabled: bool,
341    reentrancy_no_eth_enabled: bool,
342    reentrancy_balance_enabled: bool,
343    /// Set while re-running code only to refine the balance analysis.
344    balance_only_analysis: bool,
345    /// Balance facts about the return values of each analysed internal call site.
346    call_balance_values: HashMap<Span, Vec<BalanceValue>>,
347    /// Return-value accumulators of the internal calls currently being inlined.
348    return_collectors: Vec<(FunctionId, Vec<BalanceValue>)>,
349    /// Reentrancy-guard locks held by the modifiers wrapping the code being analysed.
350    active_balance_guards: Vec<VariableId>,
351    /// The lock guarding every entry point of each deployable contract, if any.
352    balance_reentry_lock: Option<VariableId>,
353}
354
355#[derive(Clone, Debug, PartialEq, Eq, Hash)]
356struct InlineCallKey {
357    func_id: FunctionId,
358    /// First active function that can cut recursion from this callee.
359    recursive_cut: Option<FunctionId>,
360    balance_only: bool,
361    active_balance_guards: Vec<VariableId>,
362    parameter_predicates: Vec<Option<(PathPredicate, bool)>>,
363    state: FlowState,
364}
365
366/// What to analyse when a modifier's `_` is reached: the remaining modifiers, the function body
367/// and the reentrancy-guard lock the current modifier holds.
368type ModifierContinuation<'gcx> =
369    (&'gcx [hir::Modifier<'gcx>], usize, hir::Block<'gcx>, Option<VariableId>);
370
371impl<'ctx, 's, 'c, 'gcx> Analyzer<'ctx, 's, 'c, 'gcx> {
372    fn new(
373        ctx: &'ctx LintContext<'s, 'c>,
374        gcx: Gcx<'gcx>,
375        entry: &'gcx hir::Function<'gcx>,
376    ) -> Self {
377        let reentrancy_balance_enabled = ctx.is_lint_enabled(REENTRANCY_BALANCE.id);
378        Self {
379            ctx,
380            gcx,
381            emitted: HashSet::new(),
382            emitted_balance: HashSet::new(),
383            call_stack: Vec::new(),
384            inline_cache: HelperAnalysisCache::new(DEFAULT_HELPER_ANALYSIS_CACHE_LIMIT),
385            recursive_cuts: HashMap::new(),
386            direct_internal_calls: HashMap::new(),
387            reentrancy_eth_enabled: ctx.is_lint_enabled(REENTRANCY_ETH.id),
388            reentrancy_no_eth_enabled: ctx.is_lint_enabled(REENTRANCY_NO_ETH.id),
389            reentrancy_balance_enabled,
390            balance_only_analysis: false,
391            call_balance_values: HashMap::new(),
392            return_collectors: Vec::new(),
393            active_balance_guards: Vec::new(),
394            balance_reentry_lock: reentrancy_balance_enabled
395                .then(|| balance_reentry_lock(gcx, entry))
396                .flatten(),
397        }
398    }
399
400    const fn has_enabled_lints(&self) -> bool {
401        self.reentrancy_eth_enabled
402            || self.reentrancy_no_eth_enabled
403            || self.reentrancy_balance_enabled
404    }
405
406    /// Analyses `func` (modifiers first, then `body`); returns whether it can fall through.
407    fn analyze_callable(
408        &mut self,
409        func: &'gcx hir::Function<'gcx>,
410        body: hir::Block<'gcx>,
411        state: &mut FlowState,
412    ) -> bool {
413        self.analyze_modifier_chain(func.modifiers, 0, body, state)
414    }
415
416    fn analyze_modifier_chain(
417        &mut self,
418        modifiers: &'gcx [hir::Modifier<'gcx>],
419        index: usize,
420        body: hir::Block<'gcx>,
421        state: &mut FlowState,
422    ) -> bool {
423        let Some(modifier) = modifiers.get(index) else {
424            return self.analyze_block(body, None, state);
425        };
426        for arg in modifier.args.exprs() {
427            self.analyze_expr(arg, state);
428        }
429        let Some((modifier_id, modifier_func, modifier_body)) = modifier
430            .id
431            .as_function()
432            .filter(|id| !self.call_stack.contains(id))
433            .map(|id| (id, self.gcx.hir.function(id)))
434            .and_then(|(id, func)| Some((id, func, func.body?)))
435        else {
436            return self.analyze_modifier_chain(modifiers, index + 1, body, state);
437        };
438
439        self.seed_balance_parameters(modifier_id, &modifier.args, state);
440        self.call_stack.push(modifier_id);
441        let balance_guard = self
442            .reentrancy_balance_enabled
443            .then(|| standard_reentrancy_guard_lock(self.gcx, modifier_func))
444            .flatten();
445        let continuation = Some((modifiers, index + 1, body, balance_guard));
446        let falls_through = self.analyze_block(modifier_body, continuation, state);
447        self.call_stack.pop();
448        self.clear_function_locals(modifier_id, state);
449        falls_through
450    }
451
452    /// Analyzes one loop iteration: the body, then the `for` update if the body completes.
453    fn analyze_iteration(
454        &mut self,
455        block: hir::Block<'gcx>,
456        source: LoopSource<'gcx>,
457        placeholder: Option<ModifierContinuation<'gcx>>,
458        state: &mut FlowState,
459    ) -> bool {
460        self.analyze_block(block, placeholder, state)
461            && loop_update(source)
462                .is_none_or(|update| self.analyze_stmt(update, placeholder, state))
463    }
464
465    fn analyze_block(
466        &mut self,
467        block: hir::Block<'gcx>,
468        placeholder: Option<ModifierContinuation<'gcx>>,
469        state: &mut FlowState,
470    ) -> bool {
471        block.stmts.iter().all(|stmt| self.analyze_stmt(stmt, placeholder, state))
472    }
473
474    /// Analyses `stmt`; returns whether control can continue past it.
475    fn analyze_stmt(
476        &mut self,
477        stmt: &'gcx Stmt<'gcx>,
478        placeholder: Option<ModifierContinuation<'gcx>>,
479        state: &mut FlowState,
480    ) -> bool {
481        match stmt.kind {
482            StmtKind::DeclSingle(var_id) => {
483                if let Some(init) = self.gcx.hir.variable(var_id).initializer {
484                    self.analyze_expr(init, state);
485                    self.update_internal_function_target(state, var_id, init);
486                    if self.reentrancy_balance_enabled {
487                        self.bind_locals(state, &[Some(var_id)], init, None);
488                    }
489                } else if self.reentrancy_balance_enabled {
490                    self.set_self_address_paths(state, var_id, PathAlternatives::new());
491                }
492                true
493            }
494            StmtKind::DeclMulti(vars, expr) => {
495                self.analyze_expr(expr, state);
496                if self.reentrancy_balance_enabled {
497                    self.bind_locals(state, vars, expr, None);
498                }
499                true
500            }
501            StmtKind::Expr(expr) | StmtKind::Emit(expr) => {
502                self.analyze_expr(expr, state);
503                true
504            }
505            StmtKind::Revert(expr) => {
506                self.analyze_expr(expr, state);
507                false
508            }
509            StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => {
510                self.analyze_block(block, placeholder, state)
511            }
512            StmtKind::Return(expr) => {
513                if let Some(expr) = expr {
514                    self.analyze_expr(expr, state);
515                }
516                if self.reentrancy_balance_enabled {
517                    self.record_return(expr, state);
518                }
519                false
520            }
521            StmtKind::Break | StmtKind::Continue => false,
522            StmtKind::Loop(block, source) => {
523                let before_loop = state.clone();
524                let mut body_state = state.clone();
525                self.analyze_iteration(block, source, placeholder, &mut body_state);
526                // One bounded second iteration exposes loop-carried balance checks while leaving
527                // the established ETH and no-ETH analysis unchanged.
528                let second_iteration = self.reentrancy_balance_enabled.then(|| {
529                    let mut second = body_state.balance_only();
530                    self.analyze_with_only_balance(|this| {
531                        this.analyze_iteration(block, source, placeholder, &mut second)
532                    });
533                    second
534                });
535                join_branches(state, [Some(before_loop), Some(body_state)]);
536                if let Some(second) = second_iteration {
537                    state.path_predicates =
538                        common_path_predicates(&state.path_predicates, &second.path_predicates);
539                    state.merge_balance(&second);
540                }
541                true
542            }
543            StmtKind::If(cond, then_stmt, else_stmt) => {
544                self.analyze_expr(cond, state);
545                if self.reentrancy_balance_enabled
546                    && (branch_stops_current_path(self.gcx, then_stmt)
547                        || else_stmt.is_some_and(|expr| branch_stops_current_path(self.gcx, expr)))
548                {
549                    self.emit_balance_calls(cond, state);
550                }
551                let (mut then_state, mut else_state) = (state.clone(), state.clone());
552                let (then_reachable, else_reachable) =
553                    self.split_on(cond, &mut then_state, &mut else_state);
554                let then_falls_through =
555                    then_reachable && self.analyze_stmt(then_stmt, placeholder, &mut then_state);
556                let else_falls_through = else_reachable
557                    && else_stmt.is_none_or(|e| self.analyze_stmt(e, placeholder, &mut else_state));
558                join_branches(
559                    state,
560                    [
561                        then_falls_through.then_some(then_state),
562                        else_falls_through.then_some(else_state),
563                    ],
564                )
565            }
566            StmtKind::Try(try_stmt) => {
567                self.analyze_expr(&try_stmt.expr, state);
568                let clauses = try_stmt
569                    .clauses
570                    .iter()
571                    .map(|clause| {
572                        let mut clause_state = state.clone();
573                        self.analyze_block(clause.block, placeholder, &mut clause_state)
574                            .then_some(clause_state)
575                    })
576                    .collect::<Vec<_>>();
577                join_branches(state, clauses)
578            }
579            StmtKind::Placeholder => {
580                let Some((modifiers, index, body, balance_guard)) = placeholder else {
581                    return true;
582                };
583                if let Some(lock_var) = balance_guard {
584                    state.invalidated_balance_guards.remove(&lock_var);
585                    self.active_balance_guards.push(lock_var);
586                }
587                let falls_through = self.analyze_modifier_chain(modifiers, index, body, state);
588                if balance_guard.is_some() {
589                    self.active_balance_guards.pop();
590                }
591                falls_through
592            }
593            StmtKind::AssemblyBlock(_) | StmtKind::Switch(_) => {
594                state.invalidated_balance_guards.extend(&self.active_balance_guards);
595                state.internal_function_targets.clear();
596                state.self_address_local_paths.clear();
597                true
598            }
599            StmtKind::Err(_) => true,
600        }
601    }
602
603    /// Constrains the branch states of a conditional on `cond`; returns whether each is reachable.
604    fn split_on(
605        &self,
606        cond: &'gcx Expr<'gcx>,
607        then_state: &mut FlowState,
608        else_state: &mut FlowState,
609    ) -> (bool, bool) {
610        let predicate =
611            self.reentrancy_balance_enabled.then(|| path_predicate(self.gcx, cond)).flatten();
612        let Some((predicate, value)) = predicate else { return (true, true) };
613        (
614            then_state.constrain_path((predicate, value)),
615            else_state.constrain_path((predicate, !value)),
616        )
617    }
618
619    fn analyze_expr(&mut self, expr: &'gcx Expr<'gcx>, state: &mut FlowState) {
620        match &expr.kind {
621            ExprKind::Assign(lhs, op, rhs) => {
622                if op.is_some() {
623                    self.analyze_expr(lhs, state);
624                }
625                self.analyze_expr(rhs, state);
626                self.analyze_lhs_indices(lhs, state);
627                self.record_write(lhs, state);
628                if let Some(var_id) = lhs_local_var(self.gcx, lhs) {
629                    if op.is_none() {
630                        self.update_internal_function_target(state, var_id, rhs);
631                    } else {
632                        state.internal_function_targets.remove(&var_id);
633                    }
634                }
635                if self.reentrancy_balance_enabled {
636                    let targets = match tuple_elems(lhs) {
637                        Some(elems) => elems
638                            .iter()
639                            .map(|e| e.and_then(|e| lhs_local_var(self.gcx, e)))
640                            .collect(),
641                        None => vec![lhs_local_var(self.gcx, lhs)],
642                    };
643                    self.bind_locals(state, &targets, rhs, op.map(|op| op.kind));
644                }
645            }
646            ExprKind::Delete(inner) => {
647                self.analyze_lhs_indices(inner, state);
648                self.record_write(inner, state);
649                if let Some(var_id) = lhs_local_var(self.gcx, inner) {
650                    state.internal_function_targets.remove(&var_id);
651                    if self.reentrancy_balance_enabled {
652                        self.clear_local(state, var_id);
653                    }
654                }
655            }
656            ExprKind::Unary(op, inner) => {
657                self.analyze_expr(inner, state);
658                if op.kind.has_side_effects() {
659                    self.record_write(inner, state);
660                    if self.reentrancy_balance_enabled
661                        && let Some(var_id) = lhs_local_var(self.gcx, inner)
662                    {
663                        self.set_self_address_paths(state, var_id, PathAlternatives::new());
664                    }
665                }
666            }
667            ExprKind::Call(callee, args, opts) => {
668                let mut operands = vec![*callee];
669                operands.extend(opts.iter().flat_map(|opts| opts.args).map(|opt| &opt.value));
670                operands.extend(args.exprs());
671
672                let before_operands = state.clone();
673                for operand in &operands {
674                    self.analyze_expr(operand, state);
675                }
676                // Solidity does not specify operand evaluation order. Reversing the operands
677                // covers both relative orders for each pair without changing the shared
678                // reentrancy analysis.
679                if self.reentrancy_balance_enabled && operands.len() > 1 {
680                    let mut reverse_state = before_operands.balance_only();
681                    self.analyze_with_only_balance(|this| {
682                        for operand in operands.iter().rev() {
683                            this.analyze_expr(operand, &mut reverse_state);
684                        }
685                    });
686                    state.merge_balance(&reverse_state);
687                }
688
689                if self.reentrancy_balance_enabled
690                    && is_require_or_assert(self.gcx, callee)
691                    && let Some(cond) = args.exprs().next()
692                {
693                    self.emit_balance_calls(cond, state);
694                }
695
696                for func_id in self.internal_callees(callee, state) {
697                    let returns = self.analyze_internal_call(func_id, args, state);
698                    self.merge_call_balance_values(expr.span, returns);
699                }
700                if !state.state_reads.is_empty()
701                    && let Some(kind) = self.reentrant_call_kind(callee, *opts)
702                {
703                    state.push_call(expr.span, kind);
704                }
705                if self.reentrancy_balance_enabled
706                    && call_options_allow_reentrancy(self.gcx, *opts)
707                    && callee_can_reenter(self.gcx, callee)
708                    && !self.balance_guard_blocks_call(state, callee)
709                {
710                    state.push_balance_call(expr.span);
711                }
712                if call_uses_delegate_context(self.gcx, callee) {
713                    state.invalidated_balance_guards.extend(&self.active_balance_guards);
714                }
715            }
716            ExprKind::Binary(lhs, op, rhs)
717                if self.reentrancy_balance_enabled
718                    && matches!(op.kind, BinOpKind::And | BinOpKind::Or) =>
719            {
720                self.analyze_expr(lhs, state);
721                let rhs_outcome = op.kind == BinOpKind::And;
722                let (mut short_state, mut rhs_state) = (state.clone(), state.clone());
723                let short_reachable =
724                    constrain_boolean_outcome(self.gcx, lhs, !rhs_outcome, &mut short_state);
725                let rhs_reachable =
726                    constrain_boolean_outcome(self.gcx, lhs, rhs_outcome, &mut rhs_state);
727                if rhs_reachable {
728                    self.analyze_expr(rhs, &mut rhs_state);
729                }
730                join_branches(
731                    state,
732                    [short_reachable.then_some(short_state), rhs_reachable.then_some(rhs_state)],
733                );
734            }
735            ExprKind::Ternary(cond, true_expr, false_expr) => {
736                self.analyze_expr(cond, state);
737                let (mut true_state, mut false_state) = (state.clone(), state.clone());
738                let (true_reachable, false_reachable) =
739                    self.split_on(cond, &mut true_state, &mut false_state);
740                if true_reachable {
741                    self.analyze_expr(true_expr, &mut true_state);
742                }
743                if false_reachable {
744                    self.analyze_expr(false_expr, &mut false_state);
745                }
746                join_branches(
747                    state,
748                    [true_reachable.then_some(true_state), false_reachable.then_some(false_state)],
749                );
750            }
751            ExprKind::Ident(_) => state.state_reads.extend(
752                self.gcx
753                    .resolved_variable(expr)
754                    .into_iter()
755                    .filter(|v| self.gcx.hir.variable(*v).kind.is_state()),
756            ),
757            _ => for_each_child(expr, &mut |child| self.analyze_expr(child, state)),
758        }
759    }
760
761    /// Evaluates the index and slice operands of an lvalue; the written root itself is no read.
762    fn analyze_lhs_indices(&mut self, expr: &'gcx Expr<'gcx>, state: &mut FlowState) {
763        match &expr.kind {
764            ExprKind::Index(base, index) => {
765                self.analyze_lhs_indices(base, state);
766                if let Some(index) = index {
767                    self.analyze_expr(index, state);
768                }
769            }
770            ExprKind::Slice(base, start, end) => {
771                self.analyze_lhs_indices(base, state);
772                for bound in [start, end].into_iter().flatten() {
773                    self.analyze_expr(bound, state);
774                }
775            }
776            ExprKind::Member(base, _) | ExprKind::Payable(base) => {
777                self.analyze_lhs_indices(base, state);
778            }
779            ExprKind::Tuple(exprs) => {
780                for expr in exprs.iter().flatten() {
781                    self.analyze_lhs_indices(expr, state);
782                }
783            }
784            _ => {}
785        }
786    }
787
788    /// Handles a write to `lhs`: reports pending reentrant calls that read the written state,
789    /// invalidates written guard locks and forgets path facts about written locals.
790    fn record_write(&mut self, lhs: &'gcx Expr<'gcx>, state: &mut FlowState) {
791        let written = state_lhs_vars(self.gcx, lhs);
792        self.emit_pending_calls(state, &written);
793        state
794            .invalidated_balance_guards
795            .extend(written.iter().filter(|v| self.active_balance_guards.contains(v)));
796        for_each_lhs_var(self.gcx, lhs, &mut |var_id| {
797            if !self.gcx.hir.variable(var_id).kind.is_state() {
798                forget_path_predicates(state, var_id);
799            }
800        });
801    }
802
803    fn analyze_internal_call(
804        &mut self,
805        func_id: FunctionId,
806        args: &CallArgs<'gcx>,
807        state: &mut FlowState,
808    ) -> Vec<BalanceValue> {
809        let func = self.gcx.hir.function(func_id);
810        let Some(body) = func.body.filter(|_| !self.call_stack.contains(&func_id)) else {
811            return Vec::new();
812        };
813
814        self.seed_balance_parameters(func_id, args, state);
815        let parameter_predicates = if self.reentrancy_balance_enabled {
816            func.parameters
817                .iter()
818                .map(|&param| {
819                    arg_for_param(self.gcx, func_id, param, args)
820                        .and_then(|arg| path_predicate(self.gcx, arg))
821                })
822                .collect()
823        } else {
824            Vec::new()
825        };
826
827        let key = InlineCallKey {
828            func_id,
829            recursive_cut: self.first_recursive_cut(func_id),
830            balance_only: self.balance_only_analysis,
831            active_balance_guards: self.active_balance_guards.clone(),
832            parameter_predicates: parameter_predicates.clone(),
833            state: state.clone(),
834        };
835        if self.inline_cache.is_in_progress(&key) {
836            self.clear_function_locals(func_id, state);
837            return Vec::new();
838        }
839        if let Some((cached, returns)) = self.inline_cache.get(&key).cloned() {
840            *state = if self.balance_only_analysis { cached.balance_only() } else { cached };
841            return returns;
842        }
843
844        self.inline_cache.start(key.clone());
845        if self.reentrancy_balance_enabled {
846            let slots = vec![BalanceValue::default(); func.returns.len()];
847            self.return_collectors.push((func_id, slots));
848        }
849        // Call-site results belong to this invocation; only its evaluation alternatives merge.
850        let caller_balance_values = std::mem::take(&mut self.call_balance_values);
851        self.call_stack.push(func_id);
852        let mut after = state.clone();
853        let falls_through = self.analyze_callable(func, body, &mut after);
854        self.call_stack.pop();
855
856        let mut returns = Vec::new();
857        if self.reentrancy_balance_enabled {
858            if falls_through {
859                self.record_return(None, &after);
860            }
861            returns = self.return_collectors.pop().expect("return collector is active").1;
862            remap_return_paths(
863                &self.gcx.hir,
864                func_id,
865                func.parameters,
866                &parameter_predicates,
867                &mut returns,
868            );
869        }
870        self.call_balance_values = caller_balance_values;
871        self.clear_function_locals(func_id, &mut after);
872        if self.balance_only_analysis {
873            after = after.balance_only();
874        }
875
876        self.inline_cache.finish(key, (after.clone(), returns.clone()));
877        *state = after;
878        returns
879    }
880
881    /// Internal functions a call through `callee` may reach, following function-typed locals.
882    fn internal_callees(
883        &self,
884        callee: &'gcx Expr<'gcx>,
885        state: &FlowState,
886    ) -> BTreeSet<FunctionId> {
887        if let Some(targets) =
888            lhs_local_var(self.gcx, callee).and_then(|v| state.internal_function_targets.get(&v))
889        {
890            return targets.clone();
891        }
892        static_internal_callee(self.gcx, callee).into_iter().collect()
893    }
894
895    fn update_internal_function_target(
896        &self,
897        state: &mut FlowState,
898        var_id: VariableId,
899        value: &'gcx Expr<'gcx>,
900    ) {
901        let targets = self.internal_callees(value, state);
902        state.internal_function_targets.remove(&var_id);
903        if !targets.is_empty() {
904            state.internal_function_targets.insert(var_id, targets);
905        }
906    }
907
908    fn merge_call_balance_values(&mut self, span: Span, values: Vec<BalanceValue>) {
909        let stored = self.call_balance_values.entry(span).or_default();
910        if stored.len() < values.len() {
911            stored.resize_with(values.len(), BalanceValue::default);
912        }
913        for (stored, value) in stored.iter_mut().zip(values) {
914            stored.merge(value);
915        }
916    }
917
918    fn analyze_with_only_balance<T>(&mut self, f: impl FnOnce(&mut Self) -> T) -> T {
919        let saved = (
920            self.reentrancy_eth_enabled,
921            self.reentrancy_no_eth_enabled,
922            self.balance_only_analysis,
923        );
924        (self.reentrancy_eth_enabled, self.reentrancy_no_eth_enabled, self.balance_only_analysis) =
925            (false, false, true);
926        let result = f(self);
927        (self.reentrancy_eth_enabled, self.reentrancy_no_eth_enabled, self.balance_only_analysis) =
928            saved;
929        result
930    }
931
932    fn first_recursive_cut(&mut self, func_id: FunctionId) -> Option<FunctionId> {
933        if self.call_stack.is_empty() {
934            return None;
935        }
936        let key = (func_id, self.call_stack.iter().copied().collect::<BTreeSet<_>>());
937        if let Some(cut) = self.recursive_cuts.get(&key) {
938            return *cut;
939        }
940        let cut = self.first_recursive_cut_from(func_id, &key.1, &mut HashSet::new());
941        self.recursive_cuts.insert(key, cut);
942        cut
943    }
944
945    /// Depth-first search from `func_id` for the first callee that is currently `active`.
946    fn first_recursive_cut_from(
947        &mut self,
948        func_id: FunctionId,
949        active: &BTreeSet<FunctionId>,
950        seen: &mut HashSet<FunctionId>,
951    ) -> Option<FunctionId> {
952        if !seen.insert(func_id) {
953            return None;
954        }
955        self.direct_internal_calls(func_id).into_iter().find_map(|callee| {
956            if active.contains(&callee) {
957                Some(callee)
958            } else {
959                self.first_recursive_cut_from(callee, active, seen)
960            }
961        })
962    }
963
964    /// Internal functions and modifiers `func_id` invokes directly.
965    fn direct_internal_calls(&mut self, func_id: FunctionId) -> Vec<FunctionId> {
966        if let Some(calls) = self.direct_internal_calls.get(&func_id) {
967            return calls.clone();
968        }
969        let mut collector = CallCollector { gcx: self.gcx, calls: BTreeSet::new() };
970        let _ = collector.visit_function(self.gcx.hir.function(func_id));
971        let calls = collector.calls.into_iter().collect::<Vec<_>>();
972        self.direct_internal_calls.insert(func_id, calls.clone());
973        calls
974    }
975
976    fn emit_pending_calls(&mut self, state: &FlowState, written_vars: &[VariableId]) {
977        for (&(span, kind), reads) in &state.pending_calls {
978            let Some(var_id) = written_vars.iter().find(|v| reads.contains(v)) else { continue };
979            if !self.emitted.insert(span) {
980                continue;
981            }
982            let (lint, what) = match kind {
983                ReentrantCallKind::Eth => (&REENTRANCY_ETH, "uncapped ETH transfer"),
984                ReentrantCallKind::NoEth => (&REENTRANCY_NO_ETH, "external call"),
985            };
986            let name = self
987                .gcx
988                .hir
989                .variable(*var_id)
990                .name
991                .map_or_else(|| "state".to_string(), |name| name.to_string());
992            let msg = format!("{what} can be reentered before `{name}` is updated");
993            self.ctx.emit_with_msg(lint, span, msg);
994        }
995    }
996
997    /// Reports pending balance calls whose stale balance `guard` compares against.
998    fn emit_balance_calls(&mut self, guard: &'gcx Expr<'gcx>, state: &FlowState) {
999        for (&span, call) in &state.pending_balance_calls {
1000            if !self.emitted_balance.contains(&span)
1001                && self.guard_has_stale_balance_comparison(guard, span, call, state)
1002            {
1003                self.ctx.emit(&REENTRANCY_BALANCE, span);
1004                self.emitted_balance.insert(span);
1005            }
1006        }
1007    }
1008
1009    /// True if `expr` compares a balance read before the pending `call` against one read after.
1010    fn guard_has_stale_balance_comparison(
1011        &self,
1012        expr: &'gcx Expr<'gcx>,
1013        span: Span,
1014        call: &PathAlternatives,
1015        state: &FlowState,
1016    ) -> bool {
1017        let expr = expr.peel_parens();
1018        let recurse = |e, s| self.guard_has_stale_balance_comparison(e, span, call, s);
1019        match &expr.kind {
1020            ExprKind::Binary(lhs, op, rhs) if matches!(op.kind, BinOpKind::And | BinOpKind::Or) => {
1021                if recurse(lhs, state) {
1022                    return true;
1023                }
1024                let mut rhs_state = state.clone();
1025                constrain_boolean_outcome(self.gcx, lhs, op.kind == BinOpKind::And, &mut rhs_state)
1026                    && recurse(rhs, &rhs_state)
1027            }
1028            ExprKind::Binary(lhs, op, rhs) => {
1029                let is_comparison = matches!(
1030                    op.kind,
1031                    BinOpKind::Lt
1032                        | BinOpKind::Le
1033                        | BinOpKind::Gt
1034                        | BinOpKind::Ge
1035                        | BinOpKind::Eq
1036                        | BinOpKind::Ne
1037                );
1038                (is_comparison && {
1039                    let lhs_dependencies = self.balance_operand_dependencies(lhs, state);
1040                    let rhs_dependencies = self.balance_operand_dependencies(rhs, state);
1041                    let direct = lhs_dependencies.iter().any(|lhs| {
1042                        rhs_dependencies.iter().any(|rhs| {
1043                            paths_compatible(&lhs.path, &rhs.path)
1044                                && call.iter().any(|path| {
1045                                    paths_compatible(path, &lhs.path)
1046                                        && paths_compatible(path, &rhs.path)
1047                                })
1048                                && lhs.terms.iter().any(|a| {
1049                                    rhs.terms.iter().any(|b| {
1050                                        a.stale_calls.contains(&span)
1051                                            != b.stale_calls.contains(&span)
1052                                    })
1053                                })
1054                        })
1055                    });
1056                    let lhs = self.balance_forms(lhs, state);
1057                    let rhs = self.balance_forms(rhs, state);
1058                    direct
1059                        || lhs.iter().any(|lhs| {
1060                            rhs.iter().filter_map(|rhs| lhs.combine(rhs, true)).any(|form| {
1061                                let [a, b] = form.terms.as_slice() else { return false };
1062                                a.negative != b.negative
1063                                    && a.stale_calls.contains(&span)
1064                                        != b.stale_calls.contains(&span)
1065                                    && call.iter().any(|path| paths_compatible(path, &form.path))
1066                            })
1067                        })
1068                }) || recurse(lhs, state)
1069                    || recurse(rhs, state)
1070            }
1071            ExprKind::Unary(_, inner) | ExprKind::Payable(inner) => recurse(inner, state),
1072            ExprKind::Ternary(cond, true_expr, false_expr) => {
1073                [*cond, *true_expr, *false_expr].into_iter().any(|e| recurse(e, state))
1074            }
1075            ExprKind::Call(..) => match cast_args(expr) {
1076                Some(args) => args.exprs().any(|arg| recurse(arg, state)),
1077                None => self.call_balance_values.get(&expr.span).is_some_and(|values| {
1078                    values.iter().any(|value| value.stale_comparisons.contains(&span))
1079                }),
1080            },
1081            ExprKind::Ident(_) => self.gcx.resolved_variable(expr).is_some_and(|var_id| {
1082                state
1083                    .balance_comparison_locals
1084                    .get(&var_id)
1085                    .is_some_and(|calls| calls.contains(&span))
1086            }),
1087            _ => false,
1088        }
1089    }
1090
1091    /// Binds the components of `rhs` to the local `targets` they are assigned to.
1092    fn bind_locals(
1093        &mut self,
1094        state: &mut FlowState,
1095        targets: &[Option<VariableId>],
1096        rhs: &'gcx Expr<'gcx>,
1097        op: Option<BinOpKind>,
1098    ) {
1099        if targets.iter().all(Option::is_none) {
1100            return;
1101        }
1102        let values = self.balance_values(rhs, state);
1103        for (var_id, value) in targets.iter().zip(values) {
1104            let Some(var_id) = *var_id else { continue };
1105            self.set_balance_local(state, var_id, &value, op);
1106            let paths =
1107                if op.is_some() { PathAlternatives::new() } else { value.self_address_paths };
1108            self.set_self_address_paths(state, var_id, paths);
1109        }
1110    }
1111
1112    fn set_self_address_paths(
1113        &self,
1114        state: &mut FlowState,
1115        var_id: VariableId,
1116        paths: PathAlternatives,
1117    ) {
1118        state.self_address_local_paths.remove(&var_id);
1119        if !paths.is_empty() {
1120            state.self_address_local_paths.insert(var_id, paths);
1121        }
1122    }
1123
1124    /// Makes `var_id` hold `value`; a compound assignment also keeps what the local held before.
1125    fn set_balance_local(
1126        &self,
1127        state: &mut FlowState,
1128        var_id: VariableId,
1129        value: &BalanceValue,
1130        op: Option<BinOpKind>,
1131    ) {
1132        let forms = match op {
1133            None => value.forms.clone(),
1134            Some(op @ (BinOpKind::Add | BinOpKind::Sub)) => state
1135                .balance_local_forms
1136                .get(&var_id)
1137                .into_iter()
1138                .flatten()
1139                .flat_map(|lhs| {
1140                    value.forms.iter().filter_map(move |rhs| lhs.combine(rhs, op == BinOpKind::Sub))
1141                })
1142                .collect(),
1143            Some(_) => BTreeSet::new(),
1144        };
1145        state.balance_local_forms.insert(var_id, forms);
1146        let mut dependencies = value.dependencies.clone();
1147        let mut balance_paths = value.balance_paths.clone();
1148        let mut stale_comparisons = value.stale_comparisons.clone();
1149        if op.is_some() {
1150            dependencies.extend(
1151                state
1152                    .balance_local_dependencies
1153                    .get(&var_id)
1154                    .into_iter()
1155                    .flatten()
1156                    .filter_map(|form| form.constrained(&state.path_predicates)),
1157            );
1158            balance_paths
1159                .extend(state.balance_local_paths.get(&var_id).into_iter().flatten().cloned());
1160            stale_comparisons
1161                .extend(state.balance_comparison_locals.get(&var_id).into_iter().flatten());
1162        }
1163        state.balance_local_dependencies.insert(var_id, dependencies);
1164        state.balance_local_paths.remove(&var_id);
1165        state.balance_comparison_locals.remove(&var_id);
1166        if !balance_paths.is_empty() {
1167            state.balance_local_paths.insert(var_id, balance_paths);
1168        }
1169        if !stale_comparisons.is_empty() {
1170            state.balance_comparison_locals.insert(var_id, stale_comparisons);
1171        }
1172    }
1173
1174    /// Balance facts of each component of `rhs`, one per destructuring target.
1175    fn balance_values(&self, rhs: &'gcx Expr<'gcx>, state: &FlowState) -> Vec<BalanceValue> {
1176        if let Some(elems) = tuple_elems(rhs) {
1177            return elems
1178                .iter()
1179                .map(|e| e.map(|e| self.balance_dependency(e, state)).unwrap_or_default())
1180                .collect();
1181        }
1182        match self.call_balance_values.get(&rhs.peel_parens().span) {
1183            Some(values) if !values.is_empty() => {
1184                values.iter().map(|v| v.constrained(&state.path_predicates)).collect()
1185            }
1186            _ => vec![self.balance_dependency(rhs, state)],
1187        }
1188    }
1189
1190    fn balance_dependency(&self, expr: &'gcx Expr<'gcx>, state: &FlowState) -> BalanceValue {
1191        let pending = &state.pending_balance_calls;
1192        BalanceValue {
1193            forms: self.balance_forms(expr, state),
1194            dependencies: self.balance_operand_dependencies(expr, state),
1195            balance_paths: self.expr_balance_paths(expr, state),
1196            self_address_paths: self.self_address_path(expr, state),
1197            stale_comparisons: pending
1198                .iter()
1199                .filter(|(span, call)| {
1200                    self.guard_has_stale_balance_comparison(expr, **span, call, state)
1201                })
1202                .map(|(span, _)| *span)
1203                .collect(),
1204        }
1205    }
1206
1207    /// Predicates under which `expr` evaluates to `address(this)`.
1208    fn self_address_path(&self, expr: &Expr<'_>, state: &FlowState) -> PathAlternatives {
1209        let expr = expr.peel_parens();
1210        match &expr.kind {
1211            ExprKind::Payable(inner) => self.self_address_path(inner, state),
1212            ExprKind::Call(callee, args, None) if is_address_cast(callee) && args.len() == 1 => {
1213                self.self_address_path(args.exprs().next().expect("one argument"), state)
1214            }
1215            ExprKind::Call(..) => self
1216                .call_balance_values
1217                .get(&expr.span)
1218                .and_then(|values| values.first())
1219                .map(|value| constrain_paths(&value.self_address_paths, &state.path_predicates))
1220                .unwrap_or_default(),
1221            ExprKind::Ident(_) if is_builtin(self.gcx, expr, sym::this) => {
1222                BTreeSet::from([state.path_predicates.clone()])
1223            }
1224            ExprKind::Ident(_) => self
1225                .gcx
1226                .resolved_variable(expr)
1227                .into_iter()
1228                .filter_map(|v| state.self_address_local_paths.get(&v))
1229                .flat_map(|paths| constrain_paths(paths, &state.path_predicates))
1230                .collect(),
1231            _ => PathAlternatives::new(),
1232        }
1233    }
1234
1235    /// Predicates under which `expr` is `<self address>.balance`.
1236    fn self_balance_paths(&self, expr: &Expr<'_>, state: &FlowState) -> PathAlternatives {
1237        match &expr.peel_parens().kind {
1238            ExprKind::Member(base, member) if member.name == kw::Balance => {
1239                self.self_address_path(base, state)
1240            }
1241            _ => PathAlternatives::new(),
1242        }
1243    }
1244
1245    /// Predicates under which `expr` derives from the contract's own balance.
1246    fn expr_balance_paths(&self, expr: &'gcx Expr<'gcx>, state: &FlowState) -> PathAlternatives {
1247        let expr = expr.peel_parens();
1248        let self_balance = self.self_balance_paths(expr, state);
1249        if !self_balance.is_empty() {
1250            return self_balance;
1251        }
1252        let recurse = |e| self.expr_balance_paths(e, state);
1253        match &expr.kind {
1254            ExprKind::Ident(_) => self
1255                .gcx
1256                .resolved_variable(expr)
1257                .into_iter()
1258                .filter_map(|v| state.balance_local_paths.get(&v))
1259                .flat_map(|paths| constrain_paths(paths, &state.path_predicates))
1260                .collect(),
1261            ExprKind::Unary(_, inner) | ExprKind::Payable(inner) => recurse(inner),
1262            ExprKind::Binary(lhs, _, rhs) => [*lhs, *rhs].into_iter().flat_map(recurse).collect(),
1263            ExprKind::Ternary(cond, true_expr, false_expr) => {
1264                [*cond, *true_expr, *false_expr].into_iter().flat_map(recurse).collect()
1265            }
1266            ExprKind::Call(..) => match cast_args(expr) {
1267                Some(args) => args.exprs().flat_map(recurse).collect(),
1268                None => self
1269                    .call_balance_values
1270                    .get(&expr.span)
1271                    .into_iter()
1272                    .flatten()
1273                    .flat_map(|value| constrain_paths(&value.balance_paths, &state.path_predicates))
1274                    .collect(),
1275            },
1276            _ => PathAlternatives::new(),
1277        }
1278    }
1279
1280    /// Retains balance occurrences across comparison operands without assuming arithmetic
1281    /// identities.
1282    fn balance_operand_dependencies(
1283        &self,
1284        expr: &'gcx Expr<'gcx>,
1285        state: &FlowState,
1286    ) -> BTreeSet<BalanceForm> {
1287        let recurse = |expr| self.balance_operand_dependencies(expr, state);
1288        match &expr.peel_parens().kind {
1289            ExprKind::Binary(lhs, _, rhs) => [*lhs, *rhs].into_iter().flat_map(recurse).collect(),
1290            ExprKind::Unary(_, inner) | ExprKind::Payable(inner) => recurse(inner),
1291            ExprKind::Ternary(cond, true_expr, false_expr) => {
1292                let mut then_state = state.clone();
1293                let mut else_state = state.clone();
1294                let (then_reachable, else_reachable) =
1295                    self.split_on(cond, &mut then_state, &mut else_state);
1296                [(true_expr, then_state, then_reachable), (false_expr, else_state, else_reachable)]
1297                    .into_iter()
1298                    .filter(|(_, _, reachable)| *reachable)
1299                    .flat_map(|(expr, state, _)| self.balance_operand_dependencies(expr, &state))
1300                    .collect()
1301            }
1302            ExprKind::Call(..) if let Some(args) = cast_args(expr) => {
1303                args.exprs().flat_map(recurse).collect()
1304            }
1305            ExprKind::Ident(_) => self
1306                .gcx
1307                .resolved_variable(expr)
1308                .into_iter()
1309                .filter_map(|var| state.balance_local_dependencies.get(&var))
1310                .flatten()
1311                .filter_map(|form| form.constrained(&state.path_predicates))
1312                .collect(),
1313            ExprKind::Call(..) => self
1314                .call_balance_values
1315                .get(&expr.peel_parens().span)
1316                .into_iter()
1317                .flatten()
1318                .flat_map(|value| &value.dependencies)
1319                .filter_map(|form| form.constrained(&state.path_predicates))
1320                .collect(),
1321            _ => self.balance_forms(expr, state),
1322        }
1323    }
1324
1325    /// Preserves balance terms through addition/subtraction and value-preserving integer casts.
1326    /// Other operators may form independent offsets, but cannot transform balance terms.
1327    /// Unsupported expressions return no forms, rather than an independent offset.
1328    fn balance_forms(&self, expr: &'gcx Expr<'gcx>, state: &FlowState) -> BTreeSet<BalanceForm> {
1329        let expr = expr.peel_parens();
1330        let paths = self.self_balance_paths(expr, state);
1331        if !paths.is_empty() {
1332            return paths
1333                .into_iter()
1334                .map(|path| BalanceForm {
1335                    terms: vec![BalanceTerm { negative: false, stale_calls: BTreeSet::new() }],
1336                    path,
1337                })
1338                .collect();
1339        }
1340        let independent = || {
1341            BTreeSet::from([BalanceForm {
1342                path: state.path_predicates.clone(),
1343                ..BalanceForm::default()
1344            }])
1345        };
1346        match &expr.kind {
1347            ExprKind::Lit(_) => independent(),
1348            ExprKind::Ident(_) => {
1349                let Some(var) = self.gcx.resolved_variable(expr) else {
1350                    return BTreeSet::new();
1351                };
1352                match state.balance_local_forms.get(&var) {
1353                    Some(forms) => forms
1354                        .iter()
1355                        .filter_map(|form| form.constrained(&state.path_predicates))
1356                        .collect(),
1357                    None => independent(),
1358                }
1359            }
1360            ExprKind::Ternary(cond, true_expr, false_expr) => {
1361                let mut then_state = state.clone();
1362                let mut else_state = state.clone();
1363                let (then_reachable, else_reachable) =
1364                    self.split_on(cond, &mut then_state, &mut else_state);
1365                [(true_expr, then_state, then_reachable), (false_expr, else_state, else_reachable)]
1366                    .into_iter()
1367                    .filter(|(_, _, reachable)| *reachable)
1368                    .flat_map(|(expr, state, _)| self.balance_forms(expr, &state))
1369                    .collect()
1370            }
1371            ExprKind::Binary(lhs, op, rhs) => {
1372                let lhs = self.balance_forms(lhs, state);
1373                let rhs = self.balance_forms(rhs, state);
1374                lhs.iter()
1375                    .flat_map(|lhs| {
1376                        rhs.iter().filter_map(|rhs| {
1377                            if matches!(op.kind, BinOpKind::Add | BinOpKind::Sub)
1378                                || (lhs.terms.is_empty() && rhs.terms.is_empty())
1379                            {
1380                                lhs.combine(rhs, op.kind == BinOpKind::Sub)
1381                            } else {
1382                                None
1383                            }
1384                        })
1385                    })
1386                    .collect()
1387            }
1388            ExprKind::Call(callee, args, _) if cast_type(callee).is_some() && args.len() == 1 => {
1389                let inner = args.exprs().next().expect("one argument");
1390                let preserving = match (
1391                    self.gcx.type_of_expr(inner.peel_parens().id),
1392                    self.gcx.type_of_expr(expr.peel_parens().id),
1393                ) {
1394                    (Some(from), Some(to)) => {
1395                        from.is_integer()
1396                            && to.is_integer()
1397                            && from.is_signed() == to.is_signed()
1398                            && from.convert_implicit_to(to, self.gcx)
1399                    }
1400                    _ => false,
1401                };
1402                self.balance_forms(inner, state)
1403                    .into_iter()
1404                    .filter(|form| preserving || form.terms.is_empty())
1405                    .collect()
1406            }
1407            ExprKind::Call(..) => self
1408                .call_balance_values
1409                .get(&expr.span)
1410                .into_iter()
1411                .flatten()
1412                .flat_map(|value| {
1413                    value.forms.iter().filter_map(|form| form.constrained(&state.path_predicates))
1414                })
1415                .collect(),
1416            _ => BTreeSet::new(),
1417        }
1418    }
1419
1420    /// Binds the balance facts of the call arguments to the callee's parameters.
1421    fn seed_balance_parameters(
1422        &mut self,
1423        func_id: FunctionId,
1424        args: &CallArgs<'gcx>,
1425        state: &mut FlowState,
1426    ) {
1427        if !self.reentrancy_balance_enabled {
1428            return;
1429        }
1430        for &param in self.gcx.hir.function(func_id).parameters {
1431            match arg_for_param(self.gcx, func_id, param, args) {
1432                Some(arg) => self.bind_locals(state, &[Some(param)], arg, None),
1433                None => self.clear_local(state, param),
1434            }
1435        }
1436    }
1437
1438    fn clear_local(&self, state: &mut FlowState, var_id: VariableId) {
1439        self.set_balance_local(state, var_id, &BalanceValue::default(), None);
1440        self.set_self_address_paths(state, var_id, PathAlternatives::new());
1441    }
1442
1443    /// Accumulates the values returned by `return expr;` (or the named return variables when
1444    /// the function falls through) into the innermost return collector.
1445    fn record_return(&mut self, expr: Option<&'gcx Expr<'gcx>>, state: &FlowState) {
1446        let Some(&(func_id, _)) = self.return_collectors.last() else { return };
1447        let values = match expr {
1448            Some(expr) => self.balance_values(expr, state),
1449            None => self
1450                .gcx
1451                .hir
1452                .function(func_id)
1453                .returns
1454                .iter()
1455                .map(|&var_id| self.local_value(var_id, state))
1456                .collect(),
1457        };
1458        let (_, stored) = self.return_collectors.last_mut().expect("return collector is active");
1459        for (stored, value) in stored.iter_mut().zip(values) {
1460            stored.merge(value);
1461        }
1462    }
1463
1464    /// The balance facts currently recorded for the local `var_id`.
1465    fn local_value(&self, var_id: VariableId, state: &FlowState) -> BalanceValue {
1466        BalanceValue {
1467            forms: state.balance_local_forms.get(&var_id).cloned().unwrap_or_default(),
1468            dependencies: state
1469                .balance_local_dependencies
1470                .get(&var_id)
1471                .cloned()
1472                .unwrap_or_default(),
1473            balance_paths: state.balance_local_paths.get(&var_id).cloned().unwrap_or_default(),
1474            self_address_paths: state
1475                .self_address_local_paths
1476                .get(&var_id)
1477                .cloned()
1478                .unwrap_or_default(),
1479            stale_comparisons: state
1480                .balance_comparison_locals
1481                .get(&var_id)
1482                .cloned()
1483                .unwrap_or_default(),
1484        }
1485    }
1486
1487    /// Drops every fact about locals of `func_id` once its inlined body is left.
1488    fn clear_function_locals(&self, func_id: FunctionId, state: &mut FlowState) {
1489        let owned = owned_by(&self.gcx.hir, func_id);
1490        state.internal_function_targets.retain(|v, _| !owned(*v));
1491        state.self_address_local_paths.retain(|v, _| !owned(*v));
1492        state.balance_local_paths.retain(|v, _| !owned(*v));
1493        state.balance_local_forms.retain(|v, _| !owned(*v));
1494        state.balance_local_dependencies.retain(|v, _| !owned(*v));
1495        state.balance_comparison_locals.retain(|v, _| !owned(*v));
1496        state.path_predicates.retain(|predicate, _| !predicate.mentions(&owned));
1497    }
1498
1499    fn reentrant_call_kind(
1500        &self,
1501        callee: &'gcx Expr<'gcx>,
1502        opts: Option<&CallOptions<'gcx>>,
1503    ) -> Option<ReentrantCallKind> {
1504        if self.reentrancy_eth_enabled && is_uncapped_value_call(self.gcx, callee, opts) {
1505            Some(ReentrantCallKind::Eth)
1506        } else if self.reentrancy_no_eth_enabled
1507            && !call_sends_eth(self.gcx, opts)
1508            && callee_can_reenter(self.gcx, callee)
1509        {
1510            Some(ReentrantCallKind::NoEth)
1511        } else {
1512            None
1513        }
1514    }
1515
1516    /// True if the contract-wide reentrancy lock is held and intact around the call.
1517    fn balance_guard_blocks_call(&self, state: &FlowState, callee: &'gcx Expr<'gcx>) -> bool {
1518        !call_uses_delegate_context(self.gcx, callee)
1519            && self.balance_reentry_lock.is_some_and(|lock| {
1520                self.active_balance_guards.contains(&lock)
1521                    && !state.invalidated_balance_guards.contains(&lock)
1522            })
1523    }
1524}
1525
1526/// Collects the internal functions and modifiers a function invokes directly.
1527struct CallCollector<'gcx> {
1528    gcx: Gcx<'gcx>,
1529    calls: BTreeSet<FunctionId>,
1530}
1531
1532impl<'gcx> Visit<'gcx> for CallCollector<'gcx> {
1533    type BreakValue = Never;
1534
1535    fn hir(&self) -> &'gcx hir::Hir<'gcx> {
1536        &self.gcx.hir
1537    }
1538
1539    fn visit_modifier(
1540        &mut self,
1541        modifier: &'gcx hir::Modifier<'gcx>,
1542    ) -> ControlFlow<Self::BreakValue> {
1543        self.calls.extend(modifier.id.as_function());
1544        self.visit_call_args(&modifier.args)
1545    }
1546
1547    fn visit_expr(&mut self, expr: &'gcx Expr<'gcx>) -> ControlFlow<Self::BreakValue> {
1548        collect_internal_calls(self.gcx, expr, &mut self.calls);
1549        ControlFlow::Continue(())
1550    }
1551}
1552
1553fn collect_internal_calls(gcx: Gcx<'_>, expr: &Expr<'_>, calls: &mut BTreeSet<FunctionId>) {
1554    if let ExprKind::Call(callee, ..) = &expr.kind {
1555        calls.extend(static_internal_callee(gcx, callee));
1556    }
1557    for_each_child(expr, &mut |child| collect_internal_calls(gcx, child, calls));
1558}
1559
1560/// Internal function statically named by `callee`: a bare identifier or `super.f`.
1561fn static_internal_callee(gcx: Gcx<'_>, callee: &Expr<'_>) -> Option<FunctionId> {
1562    let callee = callee.peel_parens();
1563    let direct = match &callee.kind {
1564        ExprKind::Ident(_) => true,
1565        ExprKind::Member(base, _) => is_builtin(gcx, base, sym::super_),
1566        _ => false,
1567    };
1568    let TyKind::Fn(function) = gcx.type_of_expr(callee.id).filter(|_| direct)?.kind else {
1569        return None;
1570    };
1571    function.is_internal().then_some(function.function_id).flatten()
1572}
1573
1574/// The variables of `func_id`.
1575fn owned_by(hir: &hir::Hir<'_>, func_id: FunctionId) -> impl Fn(VariableId) -> bool {
1576    move |var_id| hir.variable(var_id).parent == Some(ItemId::Function(func_id))
1577}
1578
1579/// The boolean fact `expr` establishes when it evaluates to `true`: a local flag, its negation,
1580/// or an `==`/`!=` between locals and literals.
1581fn path_predicate(gcx: Gcx<'_>, expr: &Expr<'_>) -> Option<(PathPredicate, bool)> {
1582    match &expr.peel_parens().kind {
1583        ExprKind::Ident(_) => Some((PathPredicate::Boolean(lhs_local_var(gcx, expr)?), true)),
1584        ExprKind::Unary(op, inner) if op.kind == UnOpKind::Not => {
1585            path_predicate(gcx, inner).map(|(predicate, value)| (predicate, !value))
1586        }
1587        ExprKind::Binary(lhs, op, rhs) if matches!(op.kind, BinOpKind::Eq | BinOpKind::Ne) => {
1588            let (lhs, rhs) = (predicate_operand(gcx, lhs)?, predicate_operand(gcx, rhs)?);
1589            let predicate = PathPredicate::Equality(lhs.min(rhs), lhs.max(rhs));
1590            Some((predicate, op.kind == BinOpKind::Eq))
1591        }
1592        _ => None,
1593    }
1594}
1595
1596fn predicate_operand(gcx: Gcx<'_>, expr: &Expr<'_>) -> Option<Operand> {
1597    match &expr.peel_parens().kind {
1598        ExprKind::Ident(_) => Some(Operand::Variable(lhs_local_var(gcx, expr)?)),
1599        ExprKind::Lit(lit) => match lit.kind {
1600            LitKind::Number(value) => Some(Operand::Number(value)),
1601            LitKind::Bool(value) => Some(Operand::Boolean(value)),
1602            _ => None,
1603        },
1604        _ => None,
1605    }
1606}
1607
1608/// Records in `state` that `expr` evaluated to `outcome`; returns `false` if that is impossible.
1609fn constrain_boolean_outcome(
1610    gcx: Gcx<'_>,
1611    expr: &Expr<'_>,
1612    outcome: bool,
1613    state: &mut FlowState,
1614) -> bool {
1615    if let Some((predicate, value)) = path_predicate(gcx, expr) {
1616        return state.constrain_path((predicate, value == outcome));
1617    }
1618    match &expr.peel_parens().kind {
1619        // `a && b` being true (or `a || b` being false) fixes both operands.
1620        ExprKind::Binary(lhs, op, rhs)
1621            if op.kind == if outcome { BinOpKind::And } else { BinOpKind::Or } =>
1622        {
1623            constrain_boolean_outcome(gcx, lhs, outcome, state)
1624                && constrain_boolean_outcome(gcx, rhs, outcome, state)
1625        }
1626        _ => true,
1627    }
1628}
1629
1630fn common_path_predicates(lhs: &PathPredicates, rhs: &PathPredicates) -> PathPredicates {
1631    lhs.iter().filter(|(p, v)| rhs.get(p) == Some(v)).map(|(p, v)| (*p, *v)).collect()
1632}
1633
1634fn paths_compatible(lhs: &PathPredicates, rhs: &PathPredicates) -> bool {
1635    lhs.iter().all(|(p, v)| rhs.get(p).is_none_or(|other| other == v))
1636}
1637
1638/// Keeps the `paths` compatible with `active`, extended by the active predicates.
1639fn constrain_paths(paths: &PathAlternatives, active: &PathPredicates) -> PathAlternatives {
1640    paths
1641        .iter()
1642        .filter(|path| paths_compatible(path, active))
1643        .map(|path| path.iter().chain(active).map(|(p, v)| (*p, *v)).collect())
1644        .collect()
1645}
1646
1647/// Rewrites the callee-local predicates in the returned `values` into the caller's terms.
1648fn remap_return_paths(
1649    hir: &hir::Hir<'_>,
1650    func_id: FunctionId,
1651    parameters: &[VariableId],
1652    parameter_predicates: &[Option<(PathPredicate, bool)>],
1653    values: &mut [BalanceValue],
1654) {
1655    let owned = owned_by(hir, func_id);
1656    let remap = |mut path: PathPredicates| {
1657        for (&parameter, &argument) in parameters.iter().zip(parameter_predicates) {
1658            let Some(parameter_value) = path.remove(&PathPredicate::Boolean(parameter)) else {
1659                continue;
1660            };
1661            if let Some((predicate, argument_value)) = argument {
1662                let mapped = parameter_value == argument_value;
1663                if path.get(&predicate).is_some_and(|existing| *existing != mapped) {
1664                    return None;
1665                }
1666                path.insert(predicate, mapped);
1667            }
1668        }
1669        path.retain(|predicate, _| !predicate.mentions(&owned));
1670        Some(path)
1671    };
1672    for value in values {
1673        value.balance_paths =
1674            std::mem::take(&mut value.balance_paths).into_iter().filter_map(remap).collect();
1675        value.self_address_paths =
1676            std::mem::take(&mut value.self_address_paths).into_iter().filter_map(remap).collect();
1677        for forms in [&mut value.forms, &mut value.dependencies] {
1678            *forms = std::mem::take(forms)
1679                .into_iter()
1680                .filter_map(|form| Some(BalanceForm { path: remap(form.path)?, ..form }))
1681                .collect();
1682        }
1683    }
1684}
1685
1686/// Drops every path fact that mentions the (re)assigned local `var_id`.
1687fn forget_path_predicates(state: &mut FlowState, var_id: VariableId) {
1688    let mentions = |predicate: &PathPredicate| predicate.mentions(|v| v == var_id);
1689    state.path_predicates.retain(|p, _| !mentions(p));
1690    let strip = |paths: &mut PathAlternatives| {
1691        *paths = paths
1692            .iter()
1693            .map(|path| path.iter().filter(|(p, _)| !mentions(p)).map(|(p, v)| (*p, *v)).collect())
1694            .collect();
1695    };
1696    for forms in
1697        state.balance_local_forms.values_mut().chain(state.balance_local_dependencies.values_mut())
1698    {
1699        *forms = std::mem::take(forms)
1700            .into_iter()
1701            .map(|mut form| {
1702                form.path.retain(|p, _| !mentions(p));
1703                form
1704            })
1705            .collect();
1706    }
1707    state.balance_local_paths.values_mut().for_each(strip);
1708    state.self_address_local_paths.values_mut().for_each(strip);
1709    state.pending_balance_calls.values_mut().for_each(strip);
1710}
1711
1712/// Arguments of a plain type conversion such as `uint256(x)` or `address(x)`.
1713fn cast_args<'a>(expr: &'a Expr<'a>) -> Option<&'a CallArgs<'a>> {
1714    match &expr.peel_parens().kind {
1715        ExprKind::Call(callee, args, None)
1716            if matches!(callee.peel_parens().kind, ExprKind::Type(_) | ExprKind::TypeCall(_)) =>
1717        {
1718            Some(args)
1719        }
1720        _ => None,
1721    }
1722}
1723
1724fn call_option<'a>(opts: Option<&'a CallOptions<'a>>, name: Symbol) -> Option<&'a Expr<'a>> {
1725    opts?.args.iter().find(|opt| opt.name.name == name).map(|opt| &opt.value)
1726}
1727
1728fn call_sends_eth(gcx: Gcx<'_>, opts: Option<&CallOptions<'_>>) -> bool {
1729    call_option(opts, sym::value).is_some_and(|value| !is_zero_value(gcx, value))
1730}
1731
1732/// `.call{value: v}(...)` with a non-zero value and no gas cap other than `gasleft()`.
1733fn is_uncapped_value_call(gcx: Gcx<'_>, callee: &Expr<'_>, opts: Option<&CallOptions<'_>>) -> bool {
1734    matches!(&callee.peel_parens().kind, ExprKind::Member(_, member) if member.name == kw::Call)
1735        && call_sends_eth(gcx, opts)
1736        && call_option(opts, kw::Gas).is_none_or(|gas| {
1737            matches!(&gas.peel_parens().kind, ExprKind::Call(callee, args, None)
1738                if args.is_empty() && is_builtin(gcx, callee, sym::gasleft))
1739        })
1740}
1741
1742/// True unless a `gas:` option provably leaves the callee too little gas to reenter.
1743fn call_options_allow_reentrancy(gcx: Gcx<'_>, opts: Option<&CallOptions<'_>>) -> bool {
1744    let Some(gas) = call_option(opts, kw::Gas) else { return true };
1745    let sends_eth = call_sends_eth(gcx, opts);
1746    match const_value(gcx, gas, None, &mut BTreeSet::new()) {
1747        Some(Operand::Number(gas)) => {
1748            gas > U256::from(REENTRANCY_GAS_STIPEND) || (sends_eth && !gas.is_zero())
1749        }
1750        _ => true,
1751    }
1752}
1753
1754fn is_zero_value(gcx: Gcx<'_>, expr: &Expr<'_>) -> bool {
1755    matches!(const_value(gcx, expr, None, &mut BTreeSet::new()), Some(Operand::Number(n)) if n.is_zero())
1756}
1757
1758/// `break`/`continue` or anything that exits the function, so the current path stops here.
1759fn branch_stops_current_path(gcx: Gcx<'_>, stmt: &Stmt<'_>) -> bool {
1760    match &stmt.kind {
1761        StmtKind::Break | StmtKind::Continue => true,
1762        StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => {
1763            block.stmts.iter().any(|expr| branch_stops_current_path(gcx, expr))
1764        }
1765        StmtKind::If(_, then_stmt, Some(else_stmt)) => {
1766            branch_stops_current_path(gcx, then_stmt) && branch_stops_current_path(gcx, else_stmt)
1767        }
1768        _ => branch_always_exits(gcx, stmt),
1769    }
1770}
1771
1772/// The lock state variable of a standard reentrancy guard modifier: it rejects re-entry, sets
1773/// the lock, runs `_` exactly once and restores the lock right after.
1774fn standard_reentrancy_guard_lock(
1775    gcx: Gcx<'_>,
1776    modifier: &hir::Function<'_>,
1777) -> Option<VariableId> {
1778    if !matches!(modifier.kind, FunctionKind::Modifier) || !modifier.modifiers.is_empty() {
1779        return None;
1780    }
1781    let stmts = modifier.body?.stmts;
1782    if count_placeholders(stmts) != 1 {
1783        return None;
1784    }
1785    let mut activation = Vec::new();
1786    stmts_before_placeholder(stmts, &mut activation)?;
1787    let (lock_var, entered) = guard_activation(gcx, &activation, &mut BTreeSet::new())?;
1788    let index = stmts.iter().position(|s| count_placeholders(std::slice::from_ref(s)) == 1)?;
1789    let (restored_var, restored) = guard_restoration(gcx, stmts.get(index + 1)?)?;
1790    (lock_var == restored_var && entered != restored).then_some(lock_var)
1791}
1792
1793/// The lock and value set by the last of `stmts` (directly or via an argument-less helper), if
1794/// an earlier statement rejects that value.
1795fn guard_activation(
1796    gcx: Gcx<'_>,
1797    stmts: &[&Stmt<'_>],
1798    seen: &mut BTreeSet<FunctionId>,
1799) -> Option<(VariableId, Operand)> {
1800    let (activation, prefix) = stmts.split_last()?;
1801    if let Some((lock_var, entered)) = state_lock_assignment(gcx, activation) {
1802        return prefix
1803            .iter()
1804            .any(|stmt| stmt_rejects_lock_value(gcx, stmt, lock_var, entered))
1805            .then_some((lock_var, entered));
1806    }
1807    let helper_id = simple_internal_call(gcx, activation)?;
1808    let helper = gcx.hir.function(helper_id);
1809    if !helper.modifiers.is_empty() || !seen.insert(helper_id) {
1810        return None;
1811    }
1812    let body = helper.body?.stmts.iter().collect::<Vec<_>>();
1813    let result = guard_activation(gcx, &body, seen);
1814    seen.remove(&helper_id);
1815    result
1816}
1817
1818/// The lock and value restored by `stmt` (directly or via a single-statement helper).
1819fn guard_restoration(gcx: Gcx<'_>, stmt: &Stmt<'_>) -> Option<(VariableId, Operand)> {
1820    state_lock_assignment(gcx, stmt).or_else(|| {
1821        let helper = gcx.hir.function(simple_internal_call(gcx, stmt)?);
1822        let [stmt] = helper.modifiers.is_empty().then_some(helper.body?.stmts)? else {
1823            return None;
1824        };
1825        state_lock_assignment(gcx, stmt)
1826    })
1827}
1828
1829/// `f();` naming exactly one function.
1830fn simple_internal_call(gcx: Gcx<'_>, stmt: &Stmt<'_>) -> Option<FunctionId> {
1831    let StmtKind::Expr(expr) = stmt.kind else { return None };
1832    let ExprKind::Call(callee, args, None) = &expr.peel_parens().kind else { return None };
1833    (args.is_empty() && matches!(callee.peel_parens().kind, ExprKind::Ident(_)))
1834        .then(|| gcx.resolved_function(callee))
1835        .flatten()
1836}
1837
1838/// `lock = <constant>;` on a state variable.
1839fn state_lock_assignment(gcx: Gcx<'_>, stmt: &Stmt<'_>) -> Option<(VariableId, Operand)> {
1840    let StmtKind::Expr(expr) = stmt.kind else { return None };
1841    let ExprKind::Assign(lhs, None, rhs) = &expr.peel_parens().kind else { return None };
1842    let ExprKind::Ident(_) = &lhs.peel_parens().kind else { return None };
1843    let lock_var = gcx.resolved_variable(lhs).filter(|&v| gcx.hir.variable(v).kind.is_state())?;
1844    Some((lock_var, const_value(gcx, rhs, None, &mut BTreeSet::new())?))
1845}
1846
1847/// True if `stmt` reverts whenever `lock_var` holds `entered`.
1848fn stmt_rejects_lock_value(
1849    gcx: Gcx<'_>,
1850    stmt: &Stmt<'_>,
1851    lock_var: VariableId,
1852    entered: Operand,
1853) -> bool {
1854    let eval = |cond| match const_value(gcx, cond, Some((lock_var, entered)), &mut BTreeSet::new())
1855    {
1856        Some(Operand::Boolean(value)) => Some(value),
1857        _ => None,
1858    };
1859    match stmt.kind {
1860        StmtKind::Expr(expr) => {
1861            let ExprKind::Call(callee, args, _) = &expr.peel_parens().kind else { return false };
1862            is_require_or_assert(gcx, callee)
1863                && args.exprs().next().is_some_and(|cond| eval(cond) == Some(false))
1864        }
1865        StmtKind::If(cond, then_stmt, else_stmt) => match eval(cond) {
1866            Some(true) => branch_always_exits(gcx, then_stmt),
1867            Some(false) => else_stmt.is_some_and(|expr| branch_always_exits(gcx, expr)),
1868            None => false,
1869        },
1870        _ => false,
1871    }
1872}
1873
1874/// Constant-folds `expr` over literals, `constant` variables, casts, `!` and `==`/`!=`; `lock`
1875/// supplies the value of the lock variable.
1876fn const_value(
1877    gcx: Gcx<'_>,
1878    expr: &Expr<'_>,
1879    lock: Option<(VariableId, Operand)>,
1880    seen: &mut BTreeSet<VariableId>,
1881) -> Option<Operand> {
1882    let expr = expr.peel_parens();
1883    match &expr.kind {
1884        ExprKind::Lit(lit) => match lit.kind {
1885            LitKind::Bool(value) => Some(Operand::Boolean(value)),
1886            LitKind::Number(value) => Some(Operand::Number(value)),
1887            _ => None,
1888        },
1889        ExprKind::Ident(_) => {
1890            let var_id = gcx.resolved_variable(expr)?;
1891            if let Some((lock_var, entered)) = lock
1892                && lock_var == var_id
1893            {
1894                return Some(entered);
1895            }
1896            let var = gcx.hir.variable(var_id);
1897            if !var.is_constant() || !seen.insert(var_id) {
1898                return None;
1899            }
1900            let value = const_value(gcx, var.initializer?, lock, seen);
1901            seen.remove(&var_id);
1902            value
1903        }
1904        ExprKind::Unary(op, inner) if op.kind == UnOpKind::Not => {
1905            match const_value(gcx, inner, lock, seen)? {
1906                Operand::Boolean(value) => Some(Operand::Boolean(!value)),
1907                _ => None,
1908            }
1909        }
1910        ExprKind::Binary(lhs, op, rhs) if matches!(op.kind, BinOpKind::Eq | BinOpKind::Ne) => {
1911            let lhs = const_value(gcx, lhs, lock, seen)?;
1912            let rhs = const_value(gcx, rhs, lock, seen)?;
1913            Some(Operand::Boolean((lhs == rhs) == (op.kind == BinOpKind::Eq)))
1914        }
1915        ExprKind::Call(..) => {
1916            let args = cast_args(expr).filter(|args| args.len() == 1)?;
1917            const_value(gcx, args.exprs().next()?, lock, seen)
1918        }
1919        _ => None,
1920    }
1921}
1922
1923/// The reentrancy-guard lock, if any, that protects every entry point of every deployable
1924/// contract exposing `entry`.
1925fn balance_reentry_lock<'gcx>(
1926    gcx: Gcx<'gcx>,
1927    entry: &'gcx hir::Function<'gcx>,
1928) -> Option<VariableId> {
1929    let entry_id = gcx.hir.function_ids().find(|&id| std::ptr::eq(gcx.hir.function(id), entry))?;
1930    let defining_contract = entry.contract?;
1931    guard_locks(gcx, entry).into_iter().find(|&lock_var| {
1932        let mut deployed = false;
1933        for contract_id in gcx.hir.contract_ids() {
1934            let contract = gcx.hir.contract(contract_id);
1935            if !contract.can_be_deployed()
1936                || contract.is_abstract()
1937                || !contract.linearized_bases.contains(&defining_contract)
1938            {
1939                continue;
1940            }
1941            let interface = gcx.interface_functions(contract_id);
1942            let special = || [contract.fallback, contract.receive].into_iter().flatten();
1943            if !interface.iter().any(|f| f.id == entry_id) && !special().any(|id| id == entry_id) {
1944                continue;
1945            }
1946            deployed = true;
1947            let guarded = interface
1948                .iter()
1949                .map(|f| gcx.hir.function(f.id))
1950                .filter(|f| !is_view_or_pure(f.state_mutability))
1951                .chain(special().map(|id| gcx.hir.function(id)))
1952                .all(|f| guard_locks(gcx, f).contains(&lock_var));
1953            if !guarded {
1954                return false;
1955            }
1956        }
1957        deployed
1958    })
1959}
1960
1961/// Locks of the standard reentrancy guards among `function`'s modifiers.
1962fn guard_locks(gcx: Gcx<'_>, function: &hir::Function<'_>) -> Vec<VariableId> {
1963    function
1964        .modifiers
1965        .iter()
1966        .filter(|modifier| modifier.args.is_empty())
1967        .filter_map(|modifier| modifier.id.as_function())
1968        .filter_map(|id| standard_reentrancy_guard_lock(gcx, gcx.hir.function(id)))
1969        .collect()
1970}
1971
1972/// `delegatecall`/`callcode`, which run the callee in the caller's storage context.
1973fn call_uses_delegate_context(gcx: Gcx<'_>, callee: &Expr<'_>) -> bool {
1974    let callee = callee.peel_parens();
1975    gcx.resolved_builtin(callee) == Some(Builtin::AddressDelegatecall)
1976        || matches!(&callee.kind, ExprKind::Member(_, member) if member.name == kw::Callcode)
1977        || gcx.type_of_expr(callee.id).is_some_and(
1978            |ty| matches!(ty.kind, TyKind::Fn(function) if function.is_delegate_call()),
1979        )
1980}
1981
1982/// An external call that hands control to another contract: a low-level `call`/`callcode`/
1983/// `delegatecall` on an address, or a state-changing external function call.
1984fn callee_can_reenter<'gcx>(gcx: Gcx<'gcx>, callee: &Expr<'gcx>) -> bool {
1985    let callee = callee.peel_parens();
1986    match gcx.resolved_builtin(callee) {
1987        Some(Builtin::AddressCall | Builtin::AddressDelegatecall) => return true,
1988        Some(Builtin::AddressStaticcall) => return false,
1989        _ => {}
1990    }
1991    match &callee.kind {
1992        ExprKind::Member(receiver, member)
1993            if expr_is_address(gcx, receiver) && member.name == kw::Callcode =>
1994        {
1995            true
1996        }
1997        ExprKind::Member(receiver, _) if is_builtin(gcx, receiver, sym::super_) => false,
1998        _ => {
1999            let Some(TyKind::Fn(function)) = gcx.type_of_expr(callee.id).map(|ty| ty.kind) else {
2000                return false;
2001            };
2002            matches!(
2003                function.kind,
2004                TyFnKind::External | TyFnKind::Declaration | TyFnKind::DelegateCall
2005            ) && !is_view_or_pure(function.state_mutability)
2006        }
2007    }
2008}