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) => {
668                let (callee, opts) = callee.split_call_options();
669                let mut operands = vec![callee];
670                operands.extend(opts.iter().flat_map(|opts| opts.args).map(|opt| &opt.value));
671                operands.extend(args.exprs());
672
673                let before_operands = state.clone();
674                for operand in &operands {
675                    self.analyze_expr(operand, state);
676                }
677                // Solidity does not specify operand evaluation order. Reversing the operands
678                // covers both relative orders for each pair without changing the shared
679                // reentrancy analysis.
680                if self.reentrancy_balance_enabled && operands.len() > 1 {
681                    let mut reverse_state = before_operands.balance_only();
682                    self.analyze_with_only_balance(|this| {
683                        for operand in operands.iter().rev() {
684                            this.analyze_expr(operand, &mut reverse_state);
685                        }
686                    });
687                    state.merge_balance(&reverse_state);
688                }
689
690                if self.reentrancy_balance_enabled
691                    && is_require_or_assert(self.gcx, callee)
692                    && let Some(cond) = args.exprs().next()
693                {
694                    self.emit_balance_calls(cond, state);
695                }
696
697                for func_id in self.internal_callees(callee, state) {
698                    let returns = self.analyze_internal_call(func_id, args, state);
699                    self.merge_call_balance_values(expr.span, returns);
700                }
701                if !state.state_reads.is_empty()
702                    && let Some(kind) = self.reentrant_call_kind(callee, opts)
703                {
704                    state.push_call(expr.span, kind);
705                }
706                if self.reentrancy_balance_enabled
707                    && call_options_allow_reentrancy(self.gcx, opts)
708                    && callee_can_reenter(self.gcx, callee)
709                    && !self.balance_guard_blocks_call(state, callee)
710                {
711                    state.push_balance_call(expr.span);
712                }
713                if call_uses_delegate_context(self.gcx, callee) {
714                    state.invalidated_balance_guards.extend(&self.active_balance_guards);
715                }
716            }
717            ExprKind::Binary(lhs, op, rhs)
718                if self.reentrancy_balance_enabled
719                    && matches!(op.kind, BinOpKind::And | BinOpKind::Or) =>
720            {
721                self.analyze_expr(lhs, state);
722                let rhs_outcome = op.kind == BinOpKind::And;
723                let (mut short_state, mut rhs_state) = (state.clone(), state.clone());
724                let short_reachable =
725                    constrain_boolean_outcome(self.gcx, lhs, !rhs_outcome, &mut short_state);
726                let rhs_reachable =
727                    constrain_boolean_outcome(self.gcx, lhs, rhs_outcome, &mut rhs_state);
728                if rhs_reachable {
729                    self.analyze_expr(rhs, &mut rhs_state);
730                }
731                join_branches(
732                    state,
733                    [short_reachable.then_some(short_state), rhs_reachable.then_some(rhs_state)],
734                );
735            }
736            ExprKind::Ternary(cond, true_expr, false_expr) => {
737                self.analyze_expr(cond, state);
738                let (mut true_state, mut false_state) = (state.clone(), state.clone());
739                let (true_reachable, false_reachable) =
740                    self.split_on(cond, &mut true_state, &mut false_state);
741                if true_reachable {
742                    self.analyze_expr(true_expr, &mut true_state);
743                }
744                if false_reachable {
745                    self.analyze_expr(false_expr, &mut false_state);
746                }
747                join_branches(
748                    state,
749                    [true_reachable.then_some(true_state), false_reachable.then_some(false_state)],
750                );
751            }
752            ExprKind::Ident(_) => state.state_reads.extend(
753                self.gcx
754                    .resolved_variable(expr)
755                    .into_iter()
756                    .filter(|v| self.gcx.hir.variable(*v).kind.is_state()),
757            ),
758            _ => for_each_child(expr, &mut |child| self.analyze_expr(child, state)),
759        }
760    }
761
762    /// Evaluates the index and slice operands of an lvalue; the written root itself is no read.
763    fn analyze_lhs_indices(&mut self, expr: &'gcx Expr<'gcx>, state: &mut FlowState) {
764        match &expr.kind {
765            ExprKind::Index(base, index) => {
766                self.analyze_lhs_indices(base, state);
767                if let Some(index) = index {
768                    self.analyze_expr(index, state);
769                }
770            }
771            ExprKind::Slice(base, start, end) => {
772                self.analyze_lhs_indices(base, state);
773                for bound in [start, end].into_iter().flatten() {
774                    self.analyze_expr(bound, state);
775                }
776            }
777            ExprKind::Member(base, _) | ExprKind::Payable(base) => {
778                self.analyze_lhs_indices(base, state);
779            }
780            ExprKind::Tuple(exprs) => {
781                for expr in exprs.iter().flatten() {
782                    self.analyze_lhs_indices(expr, state);
783                }
784            }
785            _ => {}
786        }
787    }
788
789    /// Handles a write to `lhs`: reports pending reentrant calls that read the written state,
790    /// invalidates written guard locks and forgets path facts about written locals.
791    fn record_write(&mut self, lhs: &'gcx Expr<'gcx>, state: &mut FlowState) {
792        let written = state_lhs_vars(self.gcx, lhs);
793        self.emit_pending_calls(state, &written);
794        state
795            .invalidated_balance_guards
796            .extend(written.iter().filter(|v| self.active_balance_guards.contains(v)));
797        for_each_lhs_var(self.gcx, lhs, &mut |var_id| {
798            if !self.gcx.hir.variable(var_id).kind.is_state() {
799                forget_path_predicates(state, var_id);
800            }
801        });
802    }
803
804    fn analyze_internal_call(
805        &mut self,
806        func_id: FunctionId,
807        args: &CallArgs<'gcx>,
808        state: &mut FlowState,
809    ) -> Vec<BalanceValue> {
810        let func = self.gcx.hir.function(func_id);
811        let Some(body) = func.body.filter(|_| !self.call_stack.contains(&func_id)) else {
812            return Vec::new();
813        };
814
815        self.seed_balance_parameters(func_id, args, state);
816        let parameter_predicates = if self.reentrancy_balance_enabled {
817            func.parameters
818                .iter()
819                .map(|&param| {
820                    arg_for_param(self.gcx, func_id, param, args)
821                        .and_then(|arg| path_predicate(self.gcx, arg))
822                })
823                .collect()
824        } else {
825            Vec::new()
826        };
827
828        let key = InlineCallKey {
829            func_id,
830            recursive_cut: self.first_recursive_cut(func_id),
831            balance_only: self.balance_only_analysis,
832            active_balance_guards: self.active_balance_guards.clone(),
833            parameter_predicates: parameter_predicates.clone(),
834            state: state.clone(),
835        };
836        if self.inline_cache.is_in_progress(&key) {
837            self.clear_function_locals(func_id, state);
838            return Vec::new();
839        }
840        if let Some((cached, returns)) = self.inline_cache.get(&key).cloned() {
841            *state = if self.balance_only_analysis { cached.balance_only() } else { cached };
842            return returns;
843        }
844
845        self.inline_cache.start(key.clone());
846        if self.reentrancy_balance_enabled {
847            let slots = vec![BalanceValue::default(); func.returns.len()];
848            self.return_collectors.push((func_id, slots));
849        }
850        // Call-site results belong to this invocation; only its evaluation alternatives merge.
851        let caller_balance_values = std::mem::take(&mut self.call_balance_values);
852        self.call_stack.push(func_id);
853        let mut after = state.clone();
854        let falls_through = self.analyze_callable(func, body, &mut after);
855        self.call_stack.pop();
856
857        let mut returns = Vec::new();
858        if self.reentrancy_balance_enabled {
859            if falls_through {
860                self.record_return(None, &after);
861            }
862            returns = self.return_collectors.pop().expect("return collector is active").1;
863            remap_return_paths(
864                &self.gcx.hir,
865                func_id,
866                func.parameters,
867                &parameter_predicates,
868                &mut returns,
869            );
870        }
871        self.call_balance_values = caller_balance_values;
872        self.clear_function_locals(func_id, &mut after);
873        if self.balance_only_analysis {
874            after = after.balance_only();
875        }
876
877        self.inline_cache.finish(key, (after.clone(), returns.clone()));
878        *state = after;
879        returns
880    }
881
882    /// Internal functions a call through `callee` may reach, following function-typed locals.
883    fn internal_callees(
884        &self,
885        callee: &'gcx Expr<'gcx>,
886        state: &FlowState,
887    ) -> BTreeSet<FunctionId> {
888        if let Some(targets) =
889            lhs_local_var(self.gcx, callee).and_then(|v| state.internal_function_targets.get(&v))
890        {
891            return targets.clone();
892        }
893        static_internal_callee(self.gcx, callee).into_iter().collect()
894    }
895
896    fn update_internal_function_target(
897        &self,
898        state: &mut FlowState,
899        var_id: VariableId,
900        value: &'gcx Expr<'gcx>,
901    ) {
902        let targets = self.internal_callees(value, state);
903        state.internal_function_targets.remove(&var_id);
904        if !targets.is_empty() {
905            state.internal_function_targets.insert(var_id, targets);
906        }
907    }
908
909    fn merge_call_balance_values(&mut self, span: Span, values: Vec<BalanceValue>) {
910        let stored = self.call_balance_values.entry(span).or_default();
911        if stored.len() < values.len() {
912            stored.resize_with(values.len(), BalanceValue::default);
913        }
914        for (stored, value) in stored.iter_mut().zip(values) {
915            stored.merge(value);
916        }
917    }
918
919    fn analyze_with_only_balance<T>(&mut self, f: impl FnOnce(&mut Self) -> T) -> T {
920        let saved = (
921            self.reentrancy_eth_enabled,
922            self.reentrancy_no_eth_enabled,
923            self.balance_only_analysis,
924        );
925        (self.reentrancy_eth_enabled, self.reentrancy_no_eth_enabled, self.balance_only_analysis) =
926            (false, false, true);
927        let result = f(self);
928        (self.reentrancy_eth_enabled, self.reentrancy_no_eth_enabled, self.balance_only_analysis) =
929            saved;
930        result
931    }
932
933    fn first_recursive_cut(&mut self, func_id: FunctionId) -> Option<FunctionId> {
934        if self.call_stack.is_empty() {
935            return None;
936        }
937        let key = (func_id, self.call_stack.iter().copied().collect::<BTreeSet<_>>());
938        if let Some(cut) = self.recursive_cuts.get(&key) {
939            return *cut;
940        }
941        let cut = self.first_recursive_cut_from(func_id, &key.1, &mut HashSet::new());
942        self.recursive_cuts.insert(key, cut);
943        cut
944    }
945
946    /// Depth-first search from `func_id` for the first callee that is currently `active`.
947    fn first_recursive_cut_from(
948        &mut self,
949        func_id: FunctionId,
950        active: &BTreeSet<FunctionId>,
951        seen: &mut HashSet<FunctionId>,
952    ) -> Option<FunctionId> {
953        if !seen.insert(func_id) {
954            return None;
955        }
956        self.direct_internal_calls(func_id).into_iter().find_map(|callee| {
957            if active.contains(&callee) {
958                Some(callee)
959            } else {
960                self.first_recursive_cut_from(callee, active, seen)
961            }
962        })
963    }
964
965    /// Internal functions and modifiers `func_id` invokes directly.
966    fn direct_internal_calls(&mut self, func_id: FunctionId) -> Vec<FunctionId> {
967        if let Some(calls) = self.direct_internal_calls.get(&func_id) {
968            return calls.clone();
969        }
970        let mut collector = CallCollector { gcx: self.gcx, calls: BTreeSet::new() };
971        let _ = collector.visit_function(self.gcx.hir.function(func_id));
972        let calls = collector.calls.into_iter().collect::<Vec<_>>();
973        self.direct_internal_calls.insert(func_id, calls.clone());
974        calls
975    }
976
977    fn emit_pending_calls(&mut self, state: &FlowState, written_vars: &[VariableId]) {
978        for (&(span, kind), reads) in &state.pending_calls {
979            let Some(var_id) = written_vars.iter().find(|v| reads.contains(v)) else { continue };
980            if !self.emitted.insert(span) {
981                continue;
982            }
983            let (lint, what) = match kind {
984                ReentrantCallKind::Eth => (&REENTRANCY_ETH, "uncapped ETH transfer"),
985                ReentrantCallKind::NoEth => (&REENTRANCY_NO_ETH, "external call"),
986            };
987            let name = self
988                .gcx
989                .hir
990                .variable(*var_id)
991                .name
992                .map_or_else(|| "state".to_string(), |name| name.to_string());
993            let msg = format!("{what} can be reentered before `{name}` is updated");
994            self.ctx.emit_with_msg(lint, span, msg);
995        }
996    }
997
998    /// Reports pending balance calls whose stale balance `guard` compares against.
999    fn emit_balance_calls(&mut self, guard: &'gcx Expr<'gcx>, state: &FlowState) {
1000        for (&span, call) in &state.pending_balance_calls {
1001            if !self.emitted_balance.contains(&span)
1002                && self.guard_has_stale_balance_comparison(guard, span, call, state)
1003            {
1004                self.ctx.emit(&REENTRANCY_BALANCE, span);
1005                self.emitted_balance.insert(span);
1006            }
1007        }
1008    }
1009
1010    /// True if `expr` compares a balance read before the pending `call` against one read after.
1011    fn guard_has_stale_balance_comparison(
1012        &self,
1013        expr: &'gcx Expr<'gcx>,
1014        span: Span,
1015        call: &PathAlternatives,
1016        state: &FlowState,
1017    ) -> bool {
1018        let expr = expr.peel_parens();
1019        let recurse = |e, s| self.guard_has_stale_balance_comparison(e, span, call, s);
1020        match &expr.kind {
1021            ExprKind::Binary(lhs, op, rhs) if matches!(op.kind, BinOpKind::And | BinOpKind::Or) => {
1022                if recurse(lhs, state) {
1023                    return true;
1024                }
1025                let mut rhs_state = state.clone();
1026                constrain_boolean_outcome(self.gcx, lhs, op.kind == BinOpKind::And, &mut rhs_state)
1027                    && recurse(rhs, &rhs_state)
1028            }
1029            ExprKind::Binary(lhs, op, rhs) => {
1030                let is_comparison = matches!(
1031                    op.kind,
1032                    BinOpKind::Lt
1033                        | BinOpKind::Le
1034                        | BinOpKind::Gt
1035                        | BinOpKind::Ge
1036                        | BinOpKind::Eq
1037                        | BinOpKind::Ne
1038                );
1039                (is_comparison && {
1040                    let lhs_dependencies = self.balance_operand_dependencies(lhs, state);
1041                    let rhs_dependencies = self.balance_operand_dependencies(rhs, state);
1042                    let direct = lhs_dependencies.iter().any(|lhs| {
1043                        rhs_dependencies.iter().any(|rhs| {
1044                            paths_compatible(&lhs.path, &rhs.path)
1045                                && call.iter().any(|path| {
1046                                    paths_compatible(path, &lhs.path)
1047                                        && paths_compatible(path, &rhs.path)
1048                                })
1049                                && lhs.terms.iter().any(|a| {
1050                                    rhs.terms.iter().any(|b| {
1051                                        a.stale_calls.contains(&span)
1052                                            != b.stale_calls.contains(&span)
1053                                    })
1054                                })
1055                        })
1056                    });
1057                    let lhs = self.balance_forms(lhs, state);
1058                    let rhs = self.balance_forms(rhs, state);
1059                    direct
1060                        || lhs.iter().any(|lhs| {
1061                            rhs.iter().filter_map(|rhs| lhs.combine(rhs, true)).any(|form| {
1062                                let [a, b] = form.terms.as_slice() else { return false };
1063                                a.negative != b.negative
1064                                    && a.stale_calls.contains(&span)
1065                                        != b.stale_calls.contains(&span)
1066                                    && call.iter().any(|path| paths_compatible(path, &form.path))
1067                            })
1068                        })
1069                }) || recurse(lhs, state)
1070                    || recurse(rhs, state)
1071            }
1072            ExprKind::Unary(_, inner) | ExprKind::Payable(inner) => recurse(inner, state),
1073            ExprKind::Ternary(cond, true_expr, false_expr) => {
1074                [*cond, *true_expr, *false_expr].into_iter().any(|e| recurse(e, state))
1075            }
1076            ExprKind::Call(..) => match cast_args(expr) {
1077                Some(args) => args.exprs().any(|arg| recurse(arg, state)),
1078                None => self.call_balance_values.get(&expr.span).is_some_and(|values| {
1079                    values.iter().any(|value| value.stale_comparisons.contains(&span))
1080                }),
1081            },
1082            ExprKind::Ident(_) => self.gcx.resolved_variable(expr).is_some_and(|var_id| {
1083                state
1084                    .balance_comparison_locals
1085                    .get(&var_id)
1086                    .is_some_and(|calls| calls.contains(&span))
1087            }),
1088            _ => false,
1089        }
1090    }
1091
1092    /// Binds the components of `rhs` to the local `targets` they are assigned to.
1093    fn bind_locals(
1094        &mut self,
1095        state: &mut FlowState,
1096        targets: &[Option<VariableId>],
1097        rhs: &'gcx Expr<'gcx>,
1098        op: Option<BinOpKind>,
1099    ) {
1100        if targets.iter().all(Option::is_none) {
1101            return;
1102        }
1103        let values = self.balance_values(rhs, state);
1104        for (var_id, value) in targets.iter().zip(values) {
1105            let Some(var_id) = *var_id else { continue };
1106            self.set_balance_local(state, var_id, &value, op);
1107            let paths =
1108                if op.is_some() { PathAlternatives::new() } else { value.self_address_paths };
1109            self.set_self_address_paths(state, var_id, paths);
1110        }
1111    }
1112
1113    fn set_self_address_paths(
1114        &self,
1115        state: &mut FlowState,
1116        var_id: VariableId,
1117        paths: PathAlternatives,
1118    ) {
1119        state.self_address_local_paths.remove(&var_id);
1120        if !paths.is_empty() {
1121            state.self_address_local_paths.insert(var_id, paths);
1122        }
1123    }
1124
1125    /// Makes `var_id` hold `value`; a compound assignment also keeps what the local held before.
1126    fn set_balance_local(
1127        &self,
1128        state: &mut FlowState,
1129        var_id: VariableId,
1130        value: &BalanceValue,
1131        op: Option<BinOpKind>,
1132    ) {
1133        let forms = match op {
1134            None => value.forms.clone(),
1135            Some(op @ (BinOpKind::Add | BinOpKind::Sub)) => state
1136                .balance_local_forms
1137                .get(&var_id)
1138                .into_iter()
1139                .flatten()
1140                .flat_map(|lhs| {
1141                    value.forms.iter().filter_map(move |rhs| lhs.combine(rhs, op == BinOpKind::Sub))
1142                })
1143                .collect(),
1144            Some(_) => BTreeSet::new(),
1145        };
1146        state.balance_local_forms.insert(var_id, forms);
1147        let mut dependencies = value.dependencies.clone();
1148        let mut balance_paths = value.balance_paths.clone();
1149        let mut stale_comparisons = value.stale_comparisons.clone();
1150        if op.is_some() {
1151            dependencies.extend(
1152                state
1153                    .balance_local_dependencies
1154                    .get(&var_id)
1155                    .into_iter()
1156                    .flatten()
1157                    .filter_map(|form| form.constrained(&state.path_predicates)),
1158            );
1159            balance_paths
1160                .extend(state.balance_local_paths.get(&var_id).into_iter().flatten().cloned());
1161            stale_comparisons
1162                .extend(state.balance_comparison_locals.get(&var_id).into_iter().flatten());
1163        }
1164        state.balance_local_dependencies.insert(var_id, dependencies);
1165        state.balance_local_paths.remove(&var_id);
1166        state.balance_comparison_locals.remove(&var_id);
1167        if !balance_paths.is_empty() {
1168            state.balance_local_paths.insert(var_id, balance_paths);
1169        }
1170        if !stale_comparisons.is_empty() {
1171            state.balance_comparison_locals.insert(var_id, stale_comparisons);
1172        }
1173    }
1174
1175    /// Balance facts of each component of `rhs`, one per destructuring target.
1176    fn balance_values(&self, rhs: &'gcx Expr<'gcx>, state: &FlowState) -> Vec<BalanceValue> {
1177        if let Some(elems) = tuple_elems(rhs) {
1178            return elems
1179                .iter()
1180                .map(|e| e.map(|e| self.balance_dependency(e, state)).unwrap_or_default())
1181                .collect();
1182        }
1183        match self.call_balance_values.get(&rhs.peel_parens().span) {
1184            Some(values) if !values.is_empty() => {
1185                values.iter().map(|v| v.constrained(&state.path_predicates)).collect()
1186            }
1187            _ => vec![self.balance_dependency(rhs, state)],
1188        }
1189    }
1190
1191    fn balance_dependency(&self, expr: &'gcx Expr<'gcx>, state: &FlowState) -> BalanceValue {
1192        let pending = &state.pending_balance_calls;
1193        BalanceValue {
1194            forms: self.balance_forms(expr, state),
1195            dependencies: self.balance_operand_dependencies(expr, state),
1196            balance_paths: self.expr_balance_paths(expr, state),
1197            self_address_paths: self.self_address_path(expr, state),
1198            stale_comparisons: pending
1199                .iter()
1200                .filter(|(span, call)| {
1201                    self.guard_has_stale_balance_comparison(expr, **span, call, state)
1202                })
1203                .map(|(span, _)| *span)
1204                .collect(),
1205        }
1206    }
1207
1208    /// Predicates under which `expr` evaluates to `address(this)`.
1209    fn self_address_path(&self, expr: &Expr<'_>, state: &FlowState) -> PathAlternatives {
1210        let expr = expr.peel_parens();
1211        match &expr.kind {
1212            ExprKind::Payable(inner) => self.self_address_path(inner, state),
1213            ExprKind::Call(callee, args)
1214                if callee.split_call_options().1.is_none()
1215                    && is_address_cast(callee)
1216                    && args.len() == 1 =>
1217            {
1218                self.self_address_path(args.exprs().next().expect("one argument"), state)
1219            }
1220            ExprKind::Call(..) => self
1221                .call_balance_values
1222                .get(&expr.span)
1223                .and_then(|values| values.first())
1224                .map(|value| constrain_paths(&value.self_address_paths, &state.path_predicates))
1225                .unwrap_or_default(),
1226            ExprKind::Ident(_) if is_builtin(self.gcx, expr, sym::this) => {
1227                BTreeSet::from([state.path_predicates.clone()])
1228            }
1229            ExprKind::Ident(_) => self
1230                .gcx
1231                .resolved_variable(expr)
1232                .into_iter()
1233                .filter_map(|v| state.self_address_local_paths.get(&v))
1234                .flat_map(|paths| constrain_paths(paths, &state.path_predicates))
1235                .collect(),
1236            _ => PathAlternatives::new(),
1237        }
1238    }
1239
1240    /// Predicates under which `expr` is `<self address>.balance`.
1241    fn self_balance_paths(&self, expr: &Expr<'_>, state: &FlowState) -> PathAlternatives {
1242        match &expr.peel_parens().kind {
1243            ExprKind::Member(base, member) if member.name == kw::Balance => {
1244                self.self_address_path(base, state)
1245            }
1246            _ => PathAlternatives::new(),
1247        }
1248    }
1249
1250    /// Predicates under which `expr` derives from the contract's own balance.
1251    fn expr_balance_paths(&self, expr: &'gcx Expr<'gcx>, state: &FlowState) -> PathAlternatives {
1252        let expr = expr.peel_parens();
1253        let self_balance = self.self_balance_paths(expr, state);
1254        if !self_balance.is_empty() {
1255            return self_balance;
1256        }
1257        let recurse = |e| self.expr_balance_paths(e, state);
1258        match &expr.kind {
1259            ExprKind::Ident(_) => self
1260                .gcx
1261                .resolved_variable(expr)
1262                .into_iter()
1263                .filter_map(|v| state.balance_local_paths.get(&v))
1264                .flat_map(|paths| constrain_paths(paths, &state.path_predicates))
1265                .collect(),
1266            ExprKind::Unary(_, inner) | ExprKind::Payable(inner) => recurse(inner),
1267            ExprKind::Binary(lhs, _, rhs) => [*lhs, *rhs].into_iter().flat_map(recurse).collect(),
1268            ExprKind::Ternary(cond, true_expr, false_expr) => {
1269                [*cond, *true_expr, *false_expr].into_iter().flat_map(recurse).collect()
1270            }
1271            ExprKind::Call(..) => match cast_args(expr) {
1272                Some(args) => args.exprs().flat_map(recurse).collect(),
1273                None => self
1274                    .call_balance_values
1275                    .get(&expr.span)
1276                    .into_iter()
1277                    .flatten()
1278                    .flat_map(|value| constrain_paths(&value.balance_paths, &state.path_predicates))
1279                    .collect(),
1280            },
1281            _ => PathAlternatives::new(),
1282        }
1283    }
1284
1285    /// Retains balance occurrences across comparison operands without assuming arithmetic
1286    /// identities.
1287    fn balance_operand_dependencies(
1288        &self,
1289        expr: &'gcx Expr<'gcx>,
1290        state: &FlowState,
1291    ) -> BTreeSet<BalanceForm> {
1292        let recurse = |expr| self.balance_operand_dependencies(expr, state);
1293        match &expr.peel_parens().kind {
1294            ExprKind::Binary(lhs, _, rhs) => [*lhs, *rhs].into_iter().flat_map(recurse).collect(),
1295            ExprKind::Unary(_, inner) | ExprKind::Payable(inner) => recurse(inner),
1296            ExprKind::Ternary(cond, true_expr, false_expr) => {
1297                let mut then_state = state.clone();
1298                let mut else_state = state.clone();
1299                let (then_reachable, else_reachable) =
1300                    self.split_on(cond, &mut then_state, &mut else_state);
1301                [(true_expr, then_state, then_reachable), (false_expr, else_state, else_reachable)]
1302                    .into_iter()
1303                    .filter(|(_, _, reachable)| *reachable)
1304                    .flat_map(|(expr, state, _)| self.balance_operand_dependencies(expr, &state))
1305                    .collect()
1306            }
1307            ExprKind::Call(..) if let Some(args) = cast_args(expr) => {
1308                args.exprs().flat_map(recurse).collect()
1309            }
1310            ExprKind::Ident(_) => self
1311                .gcx
1312                .resolved_variable(expr)
1313                .into_iter()
1314                .filter_map(|var| state.balance_local_dependencies.get(&var))
1315                .flatten()
1316                .filter_map(|form| form.constrained(&state.path_predicates))
1317                .collect(),
1318            ExprKind::Call(..) => self
1319                .call_balance_values
1320                .get(&expr.peel_parens().span)
1321                .into_iter()
1322                .flatten()
1323                .flat_map(|value| &value.dependencies)
1324                .filter_map(|form| form.constrained(&state.path_predicates))
1325                .collect(),
1326            _ => self.balance_forms(expr, state),
1327        }
1328    }
1329
1330    /// Preserves balance terms through addition/subtraction and value-preserving integer casts.
1331    /// Other operators may form independent offsets, but cannot transform balance terms.
1332    /// Unsupported expressions return no forms, rather than an independent offset.
1333    fn balance_forms(&self, expr: &'gcx Expr<'gcx>, state: &FlowState) -> BTreeSet<BalanceForm> {
1334        let expr = expr.peel_parens();
1335        let paths = self.self_balance_paths(expr, state);
1336        if !paths.is_empty() {
1337            return paths
1338                .into_iter()
1339                .map(|path| BalanceForm {
1340                    terms: vec![BalanceTerm { negative: false, stale_calls: BTreeSet::new() }],
1341                    path,
1342                })
1343                .collect();
1344        }
1345        let independent = || {
1346            BTreeSet::from([BalanceForm {
1347                path: state.path_predicates.clone(),
1348                ..BalanceForm::default()
1349            }])
1350        };
1351        match &expr.kind {
1352            ExprKind::Lit(_) => independent(),
1353            ExprKind::Ident(_) => {
1354                let Some(var) = self.gcx.resolved_variable(expr) else {
1355                    return BTreeSet::new();
1356                };
1357                match state.balance_local_forms.get(&var) {
1358                    Some(forms) => forms
1359                        .iter()
1360                        .filter_map(|form| form.constrained(&state.path_predicates))
1361                        .collect(),
1362                    None => independent(),
1363                }
1364            }
1365            ExprKind::Ternary(cond, true_expr, false_expr) => {
1366                let mut then_state = state.clone();
1367                let mut else_state = state.clone();
1368                let (then_reachable, else_reachable) =
1369                    self.split_on(cond, &mut then_state, &mut else_state);
1370                [(true_expr, then_state, then_reachable), (false_expr, else_state, else_reachable)]
1371                    .into_iter()
1372                    .filter(|(_, _, reachable)| *reachable)
1373                    .flat_map(|(expr, state, _)| self.balance_forms(expr, &state))
1374                    .collect()
1375            }
1376            ExprKind::Binary(lhs, op, rhs) => {
1377                let lhs = self.balance_forms(lhs, state);
1378                let rhs = self.balance_forms(rhs, state);
1379                lhs.iter()
1380                    .flat_map(|lhs| {
1381                        rhs.iter().filter_map(|rhs| {
1382                            if matches!(op.kind, BinOpKind::Add | BinOpKind::Sub)
1383                                || (lhs.terms.is_empty() && rhs.terms.is_empty())
1384                            {
1385                                lhs.combine(rhs, op.kind == BinOpKind::Sub)
1386                            } else {
1387                                None
1388                            }
1389                        })
1390                    })
1391                    .collect()
1392            }
1393            ExprKind::Call(callee, args) if cast_type(callee).is_some() && args.len() == 1 => {
1394                let inner = args.exprs().next().expect("one argument");
1395                let preserving = match (
1396                    self.gcx.type_of_expr(inner.peel_parens().id),
1397                    self.gcx.type_of_expr(expr.peel_parens().id),
1398                ) {
1399                    (Some(from), Some(to)) => {
1400                        from.is_integer()
1401                            && to.is_integer()
1402                            && from.is_signed() == to.is_signed()
1403                            && from.convert_implicit_to(to, self.gcx)
1404                    }
1405                    _ => false,
1406                };
1407                self.balance_forms(inner, state)
1408                    .into_iter()
1409                    .filter(|form| preserving || form.terms.is_empty())
1410                    .collect()
1411            }
1412            ExprKind::Call(..) => self
1413                .call_balance_values
1414                .get(&expr.span)
1415                .into_iter()
1416                .flatten()
1417                .flat_map(|value| {
1418                    value.forms.iter().filter_map(|form| form.constrained(&state.path_predicates))
1419                })
1420                .collect(),
1421            _ => BTreeSet::new(),
1422        }
1423    }
1424
1425    /// Binds the balance facts of the call arguments to the callee's parameters.
1426    fn seed_balance_parameters(
1427        &mut self,
1428        func_id: FunctionId,
1429        args: &CallArgs<'gcx>,
1430        state: &mut FlowState,
1431    ) {
1432        if !self.reentrancy_balance_enabled {
1433            return;
1434        }
1435        for &param in self.gcx.hir.function(func_id).parameters {
1436            match arg_for_param(self.gcx, func_id, param, args) {
1437                Some(arg) => self.bind_locals(state, &[Some(param)], arg, None),
1438                None => self.clear_local(state, param),
1439            }
1440        }
1441    }
1442
1443    fn clear_local(&self, state: &mut FlowState, var_id: VariableId) {
1444        self.set_balance_local(state, var_id, &BalanceValue::default(), None);
1445        self.set_self_address_paths(state, var_id, PathAlternatives::new());
1446    }
1447
1448    /// Accumulates the values returned by `return expr;` (or the named return variables when
1449    /// the function falls through) into the innermost return collector.
1450    fn record_return(&mut self, expr: Option<&'gcx Expr<'gcx>>, state: &FlowState) {
1451        let Some(&(func_id, _)) = self.return_collectors.last() else { return };
1452        let values = match expr {
1453            Some(expr) => self.balance_values(expr, state),
1454            None => self
1455                .gcx
1456                .hir
1457                .function(func_id)
1458                .returns
1459                .iter()
1460                .map(|&var_id| self.local_value(var_id, state))
1461                .collect(),
1462        };
1463        let (_, stored) = self.return_collectors.last_mut().expect("return collector is active");
1464        for (stored, value) in stored.iter_mut().zip(values) {
1465            stored.merge(value);
1466        }
1467    }
1468
1469    /// The balance facts currently recorded for the local `var_id`.
1470    fn local_value(&self, var_id: VariableId, state: &FlowState) -> BalanceValue {
1471        BalanceValue {
1472            forms: state.balance_local_forms.get(&var_id).cloned().unwrap_or_default(),
1473            dependencies: state
1474                .balance_local_dependencies
1475                .get(&var_id)
1476                .cloned()
1477                .unwrap_or_default(),
1478            balance_paths: state.balance_local_paths.get(&var_id).cloned().unwrap_or_default(),
1479            self_address_paths: state
1480                .self_address_local_paths
1481                .get(&var_id)
1482                .cloned()
1483                .unwrap_or_default(),
1484            stale_comparisons: state
1485                .balance_comparison_locals
1486                .get(&var_id)
1487                .cloned()
1488                .unwrap_or_default(),
1489        }
1490    }
1491
1492    /// Drops every fact about locals of `func_id` once its inlined body is left.
1493    fn clear_function_locals(&self, func_id: FunctionId, state: &mut FlowState) {
1494        let owned = owned_by(&self.gcx.hir, func_id);
1495        state.internal_function_targets.retain(|v, _| !owned(*v));
1496        state.self_address_local_paths.retain(|v, _| !owned(*v));
1497        state.balance_local_paths.retain(|v, _| !owned(*v));
1498        state.balance_local_forms.retain(|v, _| !owned(*v));
1499        state.balance_local_dependencies.retain(|v, _| !owned(*v));
1500        state.balance_comparison_locals.retain(|v, _| !owned(*v));
1501        state.path_predicates.retain(|predicate, _| !predicate.mentions(&owned));
1502    }
1503
1504    fn reentrant_call_kind(
1505        &self,
1506        callee: &'gcx Expr<'gcx>,
1507        opts: Option<&CallOptions<'gcx>>,
1508    ) -> Option<ReentrantCallKind> {
1509        if self.reentrancy_eth_enabled && is_uncapped_value_call(self.gcx, callee, opts) {
1510            Some(ReentrantCallKind::Eth)
1511        } else if self.reentrancy_no_eth_enabled
1512            && !call_sends_eth(self.gcx, opts)
1513            && callee_can_reenter(self.gcx, callee)
1514        {
1515            Some(ReentrantCallKind::NoEth)
1516        } else {
1517            None
1518        }
1519    }
1520
1521    /// True if the contract-wide reentrancy lock is held and intact around the call.
1522    fn balance_guard_blocks_call(&self, state: &FlowState, callee: &'gcx Expr<'gcx>) -> bool {
1523        !call_uses_delegate_context(self.gcx, callee)
1524            && self.balance_reentry_lock.is_some_and(|lock| {
1525                self.active_balance_guards.contains(&lock)
1526                    && !state.invalidated_balance_guards.contains(&lock)
1527            })
1528    }
1529}
1530
1531/// Collects the internal functions and modifiers a function invokes directly.
1532struct CallCollector<'gcx> {
1533    gcx: Gcx<'gcx>,
1534    calls: BTreeSet<FunctionId>,
1535}
1536
1537impl<'gcx> Visit<'gcx> for CallCollector<'gcx> {
1538    type BreakValue = Never;
1539
1540    fn hir(&self) -> &'gcx hir::Hir<'gcx> {
1541        &self.gcx.hir
1542    }
1543
1544    fn visit_modifier(
1545        &mut self,
1546        modifier: &'gcx hir::Modifier<'gcx>,
1547    ) -> ControlFlow<Self::BreakValue> {
1548        self.calls.extend(modifier.id.as_function());
1549        self.visit_call_args(&modifier.args)
1550    }
1551
1552    fn visit_expr(&mut self, expr: &'gcx Expr<'gcx>) -> ControlFlow<Self::BreakValue> {
1553        collect_internal_calls(self.gcx, expr, &mut self.calls);
1554        ControlFlow::Continue(())
1555    }
1556}
1557
1558fn collect_internal_calls(gcx: Gcx<'_>, expr: &Expr<'_>, calls: &mut BTreeSet<FunctionId>) {
1559    if let ExprKind::Call(callee, ..) = &expr.kind {
1560        calls.extend(static_internal_callee(gcx, callee));
1561    }
1562    for_each_child(expr, &mut |child| collect_internal_calls(gcx, child, calls));
1563}
1564
1565/// Internal function statically named by `callee`: a bare identifier or `super.f`.
1566fn static_internal_callee(gcx: Gcx<'_>, callee: &Expr<'_>) -> Option<FunctionId> {
1567    let callee = callee.peel_parens();
1568    let direct = match &callee.kind {
1569        ExprKind::Ident(_) => true,
1570        ExprKind::Member(base, _) => is_builtin(gcx, base, sym::super_),
1571        _ => false,
1572    };
1573    let TyKind::Fn(function) = gcx.type_of_expr(callee.id).filter(|_| direct)?.kind else {
1574        return None;
1575    };
1576    function.is_internal().then_some(function.function_id).flatten()
1577}
1578
1579/// The variables of `func_id`.
1580fn owned_by(hir: &hir::Hir<'_>, func_id: FunctionId) -> impl Fn(VariableId) -> bool {
1581    move |var_id| hir.variable(var_id).parent == Some(ItemId::Function(func_id))
1582}
1583
1584/// The boolean fact `expr` establishes when it evaluates to `true`: a local flag, its negation,
1585/// or an `==`/`!=` between locals and literals.
1586fn path_predicate(gcx: Gcx<'_>, expr: &Expr<'_>) -> Option<(PathPredicate, bool)> {
1587    match &expr.peel_parens().kind {
1588        ExprKind::Ident(_) => Some((PathPredicate::Boolean(lhs_local_var(gcx, expr)?), true)),
1589        ExprKind::Unary(op, inner) if op.kind == UnOpKind::Not => {
1590            path_predicate(gcx, inner).map(|(predicate, value)| (predicate, !value))
1591        }
1592        ExprKind::Binary(lhs, op, rhs) if matches!(op.kind, BinOpKind::Eq | BinOpKind::Ne) => {
1593            let (lhs, rhs) = (predicate_operand(gcx, lhs)?, predicate_operand(gcx, rhs)?);
1594            let predicate = PathPredicate::Equality(lhs.min(rhs), lhs.max(rhs));
1595            Some((predicate, op.kind == BinOpKind::Eq))
1596        }
1597        _ => None,
1598    }
1599}
1600
1601fn predicate_operand(gcx: Gcx<'_>, expr: &Expr<'_>) -> Option<Operand> {
1602    match &expr.peel_parens().kind {
1603        ExprKind::Ident(_) => Some(Operand::Variable(lhs_local_var(gcx, expr)?)),
1604        ExprKind::Lit(lit) => match lit.kind {
1605            LitKind::Number(value) => Some(Operand::Number(value)),
1606            LitKind::Bool(value) => Some(Operand::Boolean(value)),
1607            _ => None,
1608        },
1609        _ => None,
1610    }
1611}
1612
1613/// Records in `state` that `expr` evaluated to `outcome`; returns `false` if that is impossible.
1614fn constrain_boolean_outcome(
1615    gcx: Gcx<'_>,
1616    expr: &Expr<'_>,
1617    outcome: bool,
1618    state: &mut FlowState,
1619) -> bool {
1620    if let Some((predicate, value)) = path_predicate(gcx, expr) {
1621        return state.constrain_path((predicate, value == outcome));
1622    }
1623    match &expr.peel_parens().kind {
1624        // `a && b` being true (or `a || b` being false) fixes both operands.
1625        ExprKind::Binary(lhs, op, rhs)
1626            if op.kind == if outcome { BinOpKind::And } else { BinOpKind::Or } =>
1627        {
1628            constrain_boolean_outcome(gcx, lhs, outcome, state)
1629                && constrain_boolean_outcome(gcx, rhs, outcome, state)
1630        }
1631        _ => true,
1632    }
1633}
1634
1635fn common_path_predicates(lhs: &PathPredicates, rhs: &PathPredicates) -> PathPredicates {
1636    lhs.iter().filter(|(p, v)| rhs.get(p) == Some(v)).map(|(p, v)| (*p, *v)).collect()
1637}
1638
1639fn paths_compatible(lhs: &PathPredicates, rhs: &PathPredicates) -> bool {
1640    lhs.iter().all(|(p, v)| rhs.get(p).is_none_or(|other| other == v))
1641}
1642
1643/// Keeps the `paths` compatible with `active`, extended by the active predicates.
1644fn constrain_paths(paths: &PathAlternatives, active: &PathPredicates) -> PathAlternatives {
1645    paths
1646        .iter()
1647        .filter(|path| paths_compatible(path, active))
1648        .map(|path| path.iter().chain(active).map(|(p, v)| (*p, *v)).collect())
1649        .collect()
1650}
1651
1652/// Rewrites the callee-local predicates in the returned `values` into the caller's terms.
1653fn remap_return_paths(
1654    hir: &hir::Hir<'_>,
1655    func_id: FunctionId,
1656    parameters: &[VariableId],
1657    parameter_predicates: &[Option<(PathPredicate, bool)>],
1658    values: &mut [BalanceValue],
1659) {
1660    let owned = owned_by(hir, func_id);
1661    let remap = |mut path: PathPredicates| {
1662        for (&parameter, &argument) in parameters.iter().zip(parameter_predicates) {
1663            let Some(parameter_value) = path.remove(&PathPredicate::Boolean(parameter)) else {
1664                continue;
1665            };
1666            if let Some((predicate, argument_value)) = argument {
1667                let mapped = parameter_value == argument_value;
1668                if path.get(&predicate).is_some_and(|existing| *existing != mapped) {
1669                    return None;
1670                }
1671                path.insert(predicate, mapped);
1672            }
1673        }
1674        path.retain(|predicate, _| !predicate.mentions(&owned));
1675        Some(path)
1676    };
1677    for value in values {
1678        value.balance_paths =
1679            std::mem::take(&mut value.balance_paths).into_iter().filter_map(remap).collect();
1680        value.self_address_paths =
1681            std::mem::take(&mut value.self_address_paths).into_iter().filter_map(remap).collect();
1682        for forms in [&mut value.forms, &mut value.dependencies] {
1683            *forms = std::mem::take(forms)
1684                .into_iter()
1685                .filter_map(|form| Some(BalanceForm { path: remap(form.path)?, ..form }))
1686                .collect();
1687        }
1688    }
1689}
1690
1691/// Drops every path fact that mentions the (re)assigned local `var_id`.
1692fn forget_path_predicates(state: &mut FlowState, var_id: VariableId) {
1693    let mentions = |predicate: &PathPredicate| predicate.mentions(|v| v == var_id);
1694    state.path_predicates.retain(|p, _| !mentions(p));
1695    let strip = |paths: &mut PathAlternatives| {
1696        *paths = paths
1697            .iter()
1698            .map(|path| path.iter().filter(|(p, _)| !mentions(p)).map(|(p, v)| (*p, *v)).collect())
1699            .collect();
1700    };
1701    for forms in
1702        state.balance_local_forms.values_mut().chain(state.balance_local_dependencies.values_mut())
1703    {
1704        *forms = std::mem::take(forms)
1705            .into_iter()
1706            .map(|mut form| {
1707                form.path.retain(|p, _| !mentions(p));
1708                form
1709            })
1710            .collect();
1711    }
1712    state.balance_local_paths.values_mut().for_each(strip);
1713    state.self_address_local_paths.values_mut().for_each(strip);
1714    state.pending_balance_calls.values_mut().for_each(strip);
1715}
1716
1717/// Arguments of a plain type conversion such as `uint256(x)` or `address(x)`.
1718fn cast_args<'a>(expr: &'a Expr<'a>) -> Option<&'a CallArgs<'a>> {
1719    let (callee, args, opts) = expr.peel_parens().as_call()?;
1720    (opts.is_none() && matches!(callee.kind, ExprKind::Type(_) | ExprKind::TypeCall(_)))
1721        .then_some(args)
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)
1738                if callee.split_call_options().1.is_none()
1739                    && args.is_empty()
1740                    && is_builtin(gcx, callee, sym::gasleft))
1741        })
1742}
1743
1744/// True unless a `gas:` option provably leaves the callee too little gas to reenter.
1745fn call_options_allow_reentrancy(gcx: Gcx<'_>, opts: Option<&CallOptions<'_>>) -> bool {
1746    let Some(gas) = call_option(opts, kw::Gas) else { return true };
1747    let sends_eth = call_sends_eth(gcx, opts);
1748    match const_value(gcx, gas, None, &mut BTreeSet::new()) {
1749        Some(Operand::Number(gas)) => {
1750            gas > U256::from(REENTRANCY_GAS_STIPEND) || (sends_eth && !gas.is_zero())
1751        }
1752        _ => true,
1753    }
1754}
1755
1756fn is_zero_value(gcx: Gcx<'_>, expr: &Expr<'_>) -> bool {
1757    matches!(const_value(gcx, expr, None, &mut BTreeSet::new()), Some(Operand::Number(n)) if n.is_zero())
1758}
1759
1760/// `break`/`continue` or anything that exits the function, so the current path stops here.
1761fn branch_stops_current_path(gcx: Gcx<'_>, stmt: &Stmt<'_>) -> bool {
1762    match &stmt.kind {
1763        StmtKind::Break | StmtKind::Continue => true,
1764        StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => {
1765            block.stmts.iter().any(|expr| branch_stops_current_path(gcx, expr))
1766        }
1767        StmtKind::If(_, then_stmt, Some(else_stmt)) => {
1768            branch_stops_current_path(gcx, then_stmt) && branch_stops_current_path(gcx, else_stmt)
1769        }
1770        _ => branch_always_exits(gcx, stmt),
1771    }
1772}
1773
1774/// The lock state variable of a standard reentrancy guard modifier: it rejects re-entry, sets
1775/// the lock, runs `_` exactly once and restores the lock right after.
1776fn standard_reentrancy_guard_lock(
1777    gcx: Gcx<'_>,
1778    modifier: &hir::Function<'_>,
1779) -> Option<VariableId> {
1780    if !matches!(modifier.kind, FunctionKind::Modifier) || !modifier.modifiers.is_empty() {
1781        return None;
1782    }
1783    let stmts = modifier.body?.stmts;
1784    if count_placeholders(stmts) != 1 {
1785        return None;
1786    }
1787    let mut activation = Vec::new();
1788    stmts_before_placeholder(stmts, &mut activation)?;
1789    let (lock_var, entered) = guard_activation(gcx, &activation, &mut BTreeSet::new())?;
1790    let index = stmts.iter().position(|s| count_placeholders(std::slice::from_ref(s)) == 1)?;
1791    let (restored_var, restored) = guard_restoration(gcx, stmts.get(index + 1)?)?;
1792    (lock_var == restored_var && entered != restored).then_some(lock_var)
1793}
1794
1795/// The lock and value set by the last of `stmts` (directly or via an argument-less helper), if
1796/// an earlier statement rejects that value.
1797fn guard_activation(
1798    gcx: Gcx<'_>,
1799    stmts: &[&Stmt<'_>],
1800    seen: &mut BTreeSet<FunctionId>,
1801) -> Option<(VariableId, Operand)> {
1802    let (activation, prefix) = stmts.split_last()?;
1803    if let Some((lock_var, entered)) = state_lock_assignment(gcx, activation) {
1804        return prefix
1805            .iter()
1806            .any(|stmt| stmt_rejects_lock_value(gcx, stmt, lock_var, entered))
1807            .then_some((lock_var, entered));
1808    }
1809    let helper_id = simple_internal_call(gcx, activation)?;
1810    let helper = gcx.hir.function(helper_id);
1811    if !helper.modifiers.is_empty() || !seen.insert(helper_id) {
1812        return None;
1813    }
1814    let body = helper.body?.stmts.iter().collect::<Vec<_>>();
1815    let result = guard_activation(gcx, &body, seen);
1816    seen.remove(&helper_id);
1817    result
1818}
1819
1820/// The lock and value restored by `stmt` (directly or via a single-statement helper).
1821fn guard_restoration(gcx: Gcx<'_>, stmt: &Stmt<'_>) -> Option<(VariableId, Operand)> {
1822    state_lock_assignment(gcx, stmt).or_else(|| {
1823        let helper = gcx.hir.function(simple_internal_call(gcx, stmt)?);
1824        let [stmt] = helper.modifiers.is_empty().then_some(helper.body?.stmts)? else {
1825            return None;
1826        };
1827        state_lock_assignment(gcx, stmt)
1828    })
1829}
1830
1831/// `f();` naming exactly one function.
1832fn simple_internal_call(gcx: Gcx<'_>, stmt: &Stmt<'_>) -> Option<FunctionId> {
1833    let StmtKind::Expr(expr) = stmt.kind else { return None };
1834    let (callee, args, opts) = expr.peel_parens().as_call()?;
1835    (opts.is_none() && args.is_empty() && matches!(callee.kind, ExprKind::Ident(_)))
1836        .then(|| gcx.resolved_function(callee))
1837        .flatten()
1838}
1839
1840/// `lock = <constant>;` on a state variable.
1841fn state_lock_assignment(gcx: Gcx<'_>, stmt: &Stmt<'_>) -> Option<(VariableId, Operand)> {
1842    let StmtKind::Expr(expr) = stmt.kind else { return None };
1843    let ExprKind::Assign(lhs, None, rhs) = &expr.peel_parens().kind else { return None };
1844    let ExprKind::Ident(_) = &lhs.peel_parens().kind else { return None };
1845    let lock_var = gcx.resolved_variable(lhs).filter(|&v| gcx.hir.variable(v).kind.is_state())?;
1846    Some((lock_var, const_value(gcx, rhs, None, &mut BTreeSet::new())?))
1847}
1848
1849/// True if `stmt` reverts whenever `lock_var` holds `entered`.
1850fn stmt_rejects_lock_value(
1851    gcx: Gcx<'_>,
1852    stmt: &Stmt<'_>,
1853    lock_var: VariableId,
1854    entered: Operand,
1855) -> bool {
1856    let eval = |cond| match const_value(gcx, cond, Some((lock_var, entered)), &mut BTreeSet::new())
1857    {
1858        Some(Operand::Boolean(value)) => Some(value),
1859        _ => None,
1860    };
1861    match stmt.kind {
1862        StmtKind::Expr(expr) => {
1863            let ExprKind::Call(callee, args) = &expr.peel_parens().kind else { return false };
1864            is_require_or_assert(gcx, callee)
1865                && args.exprs().next().is_some_and(|cond| eval(cond) == Some(false))
1866        }
1867        StmtKind::If(cond, then_stmt, else_stmt) => match eval(cond) {
1868            Some(true) => branch_always_exits(gcx, then_stmt),
1869            Some(false) => else_stmt.is_some_and(|expr| branch_always_exits(gcx, expr)),
1870            None => false,
1871        },
1872        _ => false,
1873    }
1874}
1875
1876/// Constant-folds `expr` over literals, `constant` variables, casts, `!` and `==`/`!=`; `lock`
1877/// supplies the value of the lock variable.
1878fn const_value(
1879    gcx: Gcx<'_>,
1880    expr: &Expr<'_>,
1881    lock: Option<(VariableId, Operand)>,
1882    seen: &mut BTreeSet<VariableId>,
1883) -> Option<Operand> {
1884    let expr = expr.peel_parens();
1885    match &expr.kind {
1886        ExprKind::Lit(lit) => match lit.kind {
1887            LitKind::Bool(value) => Some(Operand::Boolean(value)),
1888            LitKind::Number(value) => Some(Operand::Number(value)),
1889            _ => None,
1890        },
1891        ExprKind::Ident(_) => {
1892            let var_id = gcx.resolved_variable(expr)?;
1893            if let Some((lock_var, entered)) = lock
1894                && lock_var == var_id
1895            {
1896                return Some(entered);
1897            }
1898            let var = gcx.hir.variable(var_id);
1899            if !var.is_constant() || !seen.insert(var_id) {
1900                return None;
1901            }
1902            let value = const_value(gcx, var.initializer?, lock, seen);
1903            seen.remove(&var_id);
1904            value
1905        }
1906        ExprKind::Unary(op, inner) if op.kind == UnOpKind::Not => {
1907            match const_value(gcx, inner, lock, seen)? {
1908                Operand::Boolean(value) => Some(Operand::Boolean(!value)),
1909                _ => None,
1910            }
1911        }
1912        ExprKind::Binary(lhs, op, rhs) if matches!(op.kind, BinOpKind::Eq | BinOpKind::Ne) => {
1913            let lhs = const_value(gcx, lhs, lock, seen)?;
1914            let rhs = const_value(gcx, rhs, lock, seen)?;
1915            Some(Operand::Boolean((lhs == rhs) == (op.kind == BinOpKind::Eq)))
1916        }
1917        ExprKind::Call(..) => {
1918            let args = cast_args(expr).filter(|args| args.len() == 1)?;
1919            const_value(gcx, args.exprs().next()?, lock, seen)
1920        }
1921        _ => None,
1922    }
1923}
1924
1925/// The reentrancy-guard lock, if any, that protects every entry point of every deployable
1926/// contract exposing `entry`.
1927fn balance_reentry_lock<'gcx>(
1928    gcx: Gcx<'gcx>,
1929    entry: &'gcx hir::Function<'gcx>,
1930) -> Option<VariableId> {
1931    let entry_id = gcx.hir.function_ids().find(|&id| std::ptr::eq(gcx.hir.function(id), entry))?;
1932    let defining_contract = entry.contract?;
1933    guard_locks(gcx, entry).into_iter().find(|&lock_var| {
1934        let mut deployed = false;
1935        for contract_id in gcx.hir.contract_ids() {
1936            let contract = gcx.hir.contract(contract_id);
1937            if !contract.can_be_deployed()
1938                || contract.is_abstract()
1939                || !contract.linearized_bases.contains(&defining_contract)
1940            {
1941                continue;
1942            }
1943            let interface = gcx.interface_functions(contract_id);
1944            let special = || [contract.fallback, contract.receive].into_iter().flatten();
1945            if !interface.iter().any(|f| f.id == entry_id) && !special().any(|id| id == entry_id) {
1946                continue;
1947            }
1948            deployed = true;
1949            let guarded = interface
1950                .iter()
1951                .map(|f| gcx.hir.function(f.id))
1952                .filter(|f| !is_view_or_pure(f.state_mutability))
1953                .chain(special().map(|id| gcx.hir.function(id)))
1954                .all(|f| guard_locks(gcx, f).contains(&lock_var));
1955            if !guarded {
1956                return false;
1957            }
1958        }
1959        deployed
1960    })
1961}
1962
1963/// Locks of the standard reentrancy guards among `function`'s modifiers.
1964fn guard_locks(gcx: Gcx<'_>, function: &hir::Function<'_>) -> Vec<VariableId> {
1965    function
1966        .modifiers
1967        .iter()
1968        .filter(|modifier| modifier.args.is_empty())
1969        .filter_map(|modifier| modifier.id.as_function())
1970        .filter_map(|id| standard_reentrancy_guard_lock(gcx, gcx.hir.function(id)))
1971        .collect()
1972}
1973
1974/// `delegatecall`/`callcode`, which run the callee in the caller's storage context.
1975fn call_uses_delegate_context(gcx: Gcx<'_>, callee: &Expr<'_>) -> bool {
1976    let callee = callee.peel_parens();
1977    gcx.resolved_builtin(callee) == Some(Builtin::AddressDelegatecall)
1978        || matches!(&callee.kind, ExprKind::Member(_, member) if member.name == kw::Callcode)
1979        || gcx.type_of_expr(callee.id).is_some_and(
1980            |ty| matches!(ty.kind, TyKind::Fn(function) if function.is_delegate_call()),
1981        )
1982}
1983
1984/// An external call that hands control to another contract: a low-level `call`/`callcode`/
1985/// `delegatecall` on an address, or a state-changing external function call.
1986fn callee_can_reenter<'gcx>(gcx: Gcx<'gcx>, callee: &Expr<'gcx>) -> bool {
1987    let callee = callee.peel_parens();
1988    match gcx.resolved_builtin(callee) {
1989        Some(Builtin::AddressCall | Builtin::AddressDelegatecall) => return true,
1990        Some(Builtin::AddressStaticcall) => return false,
1991        _ => {}
1992    }
1993    match &callee.kind {
1994        ExprKind::Member(receiver, member)
1995            if expr_is_address(gcx, receiver) && member.name == kw::Callcode =>
1996        {
1997            true
1998        }
1999        ExprKind::Member(receiver, _) if is_builtin(gcx, receiver, sym::super_) => false,
2000        _ => {
2001            let Some(TyKind::Fn(function)) = gcx.type_of_expr(callee.id).map(|ty| ty.kind) else {
2002                return false;
2003            };
2004            matches!(
2005                function.kind,
2006                TyFnKind::External | TyFnKind::Declaration | TyFnKind::DelegateCall
2007            ) && !is_view_or_pure(function.state_mutability)
2008        }
2009    }
2010}