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            helper_cache::{DEFAULT_HELPER_ANALYSIS_CACHE_LIMIT, HelperAnalysisCache},
8            primitives::{branch_always_exits, is_require_or_assert},
9        },
10    },
11};
12use alloy_primitives::U256;
13use solar::{
14    ast::{
15        BinOpKind, DataLocation, ElementaryType, LitKind, StateMutability, StrKind, TypeSize,
16        UnOpKind, Visibility,
17    },
18    interface::{Span, kw, sym},
19    sema::{
20        Gcx, Ty,
21        hir::{
22            self, CallArgs, CallArgsKind, ExprKind, FunctionId, ItemId, Res, StmtKind, VariableId,
23        },
24        ty::{TyFnKind, TyKind},
25    },
26};
27use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
28
29const REENTRANCY_GAS_STIPEND: u64 = 2_300;
30
31declare_forge_lint!(
32    REENTRANCY_BALANCE,
33    Severity::High,
34    "reentrancy-balance",
35    "external call can be reentered before a stale contract balance is checked"
36);
37
38declare_forge_lint!(
39    REENTRANCY_ETH,
40    Severity::High,
41    "reentrancy-eth",
42    "state read before ETH transfer is written after the transfer"
43);
44
45declare_forge_lint!(
46    REENTRANCY_NO_ETH,
47    Severity::Med,
48    "reentrancy-no-eth",
49    "state read before external call is written after the call"
50);
51
52impl<'hir> LateLintPass<'hir> for ReentrancyEth {
53    fn check_function(
54        &mut self,
55        ctx: &LintContext,
56        gcx: Gcx<'hir>,
57        hir: &'hir hir::Hir<'hir>,
58        func: &'hir hir::Function<'hir>,
59    ) {
60        if !is_entry_point(func) {
61            return;
62        }
63
64        let Some(body) = func.body else { return };
65
66        let mut analyzer = Analyzer::new(ctx, gcx, hir, func);
67        if !analyzer.has_enabled_lints() {
68            return;
69        }
70        let mut state = FlowState::default();
71        analyzer.analyze_callable(func, body, &mut state);
72    }
73}
74
75fn is_entry_point(func: &hir::Function<'_>) -> bool {
76    if matches!(func.state_mutability, StateMutability::Pure | StateMutability::View) {
77        return false;
78    }
79    if func.is_constructor() {
80        return false;
81    }
82    if func.is_special() {
83        return true;
84    }
85    func.kind.is_function() && matches!(func.visibility, Visibility::Public | Visibility::External)
86}
87
88#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
89struct FlowState {
90    state_reads: BTreeSet<VariableId>,
91    pending_calls: Vec<PendingCall>,
92    internal_function_targets: BTreeMap<VariableId, BTreeSet<FunctionId>>,
93    self_address_local_paths: BTreeMap<VariableId, PathAlternatives>,
94    balance_locals: BTreeSet<VariableId>,
95    balance_local_paths: BTreeMap<VariableId, PathAlternatives>,
96    balance_comparison_locals: BTreeMap<VariableId, Vec<Span>>,
97    pending_balance_calls: Vec<PendingBalanceCall>,
98    invalidated_balance_guards: BTreeSet<VariableId>,
99    path_predicates: PathPredicates,
100}
101
102#[derive(Clone, Debug, PartialEq, Eq, Hash)]
103struct PendingCall {
104    span: Span,
105    kind: ReentrantCallKind,
106    state_reads: BTreeSet<VariableId>,
107}
108
109#[derive(Clone, Debug, PartialEq, Eq, Hash)]
110struct PendingBalanceCall {
111    span: Span,
112    stale_locals: BTreeSet<VariableId>,
113    paths: PathAlternatives,
114}
115
116type PathPredicates = BTreeMap<PathPredicate, bool>;
117type PathAlternatives = BTreeSet<PathPredicates>;
118
119#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
120enum PathPredicate {
121    Boolean(VariableId),
122    Equality(PredicateOperand, PredicateOperand),
123}
124
125impl PathPredicate {
126    fn contains(self, var_id: VariableId) -> bool {
127        match self {
128            Self::Boolean(predicate_var) => predicate_var == var_id,
129            Self::Equality(lhs, rhs) => {
130                lhs.variable() == Some(var_id) || rhs.variable() == Some(var_id)
131            }
132        }
133    }
134}
135
136#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
137enum PredicateOperand {
138    Variable(VariableId),
139    Number(U256),
140    Boolean(bool),
141}
142
143impl PredicateOperand {
144    const fn variable(self) -> Option<VariableId> {
145        let Self::Variable(var_id) = self else { return None };
146        Some(var_id)
147    }
148}
149
150#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
151enum ReentrantCallKind {
152    Eth,
153    NoEth,
154}
155
156impl FlowState {
157    fn push_read(&mut self, var_id: VariableId) {
158        self.state_reads.insert(var_id);
159    }
160
161    fn push_call(&mut self, span: Span, kind: ReentrantCallKind) {
162        if self.state_reads.is_empty() {
163            return;
164        }
165
166        if let Some(existing) =
167            self.pending_calls.iter_mut().find(|call| call.span == span && call.kind == kind)
168        {
169            existing.state_reads.extend(self.state_reads.iter().copied());
170        } else {
171            self.pending_calls.push(PendingCall {
172                span,
173                kind,
174                state_reads: self.state_reads.clone(),
175            });
176        }
177    }
178
179    fn push_balance_call(&mut self, span: Span) {
180        let stale_locals = self
181            .balance_locals
182            .iter()
183            .filter(|var_id| {
184                self.balance_local_paths
185                    .get(var_id)
186                    .is_some_and(|paths| paths_compatible_with(paths, &self.path_predicates))
187            })
188            .copied()
189            .collect::<BTreeSet<_>>();
190        if stale_locals.is_empty() {
191            return;
192        }
193        let paths = [self.path_predicates.clone()].into_iter().collect::<PathAlternatives>();
194
195        if let Some(existing) = self.pending_balance_calls.iter_mut().find(|call| call.span == span)
196        {
197            existing.stale_locals.extend(stale_locals);
198            existing.paths.extend(paths);
199        } else {
200            self.pending_balance_calls.push(PendingBalanceCall { span, stale_locals, paths });
201        }
202    }
203}
204
205struct Analyzer<'ctx, 's, 'c, 'hir> {
206    ctx: &'ctx LintContext<'s, 'c>,
207    gcx: Gcx<'hir>,
208    hir: &'hir hir::Hir<'hir>,
209    emitted: HashSet<Span>,
210    emitted_balance: HashSet<Span>,
211    call_stack: Vec<FunctionId>,
212    inline_cache: HelperAnalysisCache<InlineCallKey, InlineCallResult>,
213    recursive_cut_frontiers: HashMap<RecursiveFrontierKey, Vec<FunctionId>>,
214    direct_internal_calls: HashMap<FunctionId, Vec<FunctionId>>,
215    reentrancy_eth_enabled: bool,
216    reentrancy_no_eth_enabled: bool,
217    reentrancy_balance_enabled: bool,
218    balance_only_analysis: bool,
219    call_balance_values: HashMap<Span, Vec<BalanceValue>>,
220    return_collectors: Vec<ReturnCollector>,
221    active_balance_guards: Vec<VariableId>,
222    balance_reentry_lock: Option<VariableId>,
223}
224
225#[derive(Clone, Debug, PartialEq, Eq, Hash)]
226struct InlineCallKey {
227    func_id: FunctionId,
228    /// First active function that can cut recursion from this callee.
229    recursive_cut: Option<FunctionId>,
230    balance_only: bool,
231    active_balance_guards: Vec<VariableId>,
232    parameter_predicates: Vec<Option<(PathPredicate, bool)>>,
233    state: FlowState,
234}
235
236type ModifierContinuation<'hir> =
237    (&'hir [hir::Modifier<'hir>], usize, hir::Block<'hir>, Option<VariableId>);
238
239#[derive(Clone, Debug)]
240struct InlineCallResult {
241    state: FlowState,
242    returns: Vec<BalanceValue>,
243}
244
245#[derive(Clone, Debug, PartialEq, Eq, Hash)]
246struct RecursiveFrontierKey {
247    func_id: FunctionId,
248    active_call_stack: Vec<FunctionId>,
249}
250
251#[derive(Clone, Debug, Default)]
252struct BalanceValue {
253    balance_dependent: bool,
254    balance_paths: PathAlternatives,
255    self_address_paths: PathAlternatives,
256    stale_calls: HashSet<Span>,
257    stale_comparisons: Vec<Span>,
258}
259
260#[derive(Clone, Copy, Debug)]
261enum BalanceQuery {
262    Current(Span),
263    Stale(Span),
264}
265
266#[derive(Clone, Copy, Debug, PartialEq, Eq)]
267enum LockValue {
268    Bool(bool),
269    Number(U256),
270}
271
272#[derive(Debug)]
273struct ReturnCollector {
274    func_id: FunctionId,
275    values: Vec<BalanceValue>,
276}
277
278impl<'ctx, 's, 'c, 'hir> Analyzer<'ctx, 's, 'c, 'hir> {
279    fn new(
280        ctx: &'ctx LintContext<'s, 'c>,
281        gcx: Gcx<'hir>,
282        hir: &'hir hir::Hir<'hir>,
283        entry: &'hir hir::Function<'hir>,
284    ) -> Self {
285        let reentrancy_balance_enabled = ctx.is_lint_enabled(REENTRANCY_BALANCE.id);
286        Self {
287            ctx,
288            gcx,
289            hir,
290            emitted: HashSet::new(),
291            emitted_balance: HashSet::new(),
292            call_stack: Vec::new(),
293            inline_cache: HelperAnalysisCache::new(DEFAULT_HELPER_ANALYSIS_CACHE_LIMIT),
294            recursive_cut_frontiers: HashMap::new(),
295            direct_internal_calls: HashMap::new(),
296            reentrancy_eth_enabled: ctx.is_lint_enabled(REENTRANCY_ETH.id),
297            reentrancy_no_eth_enabled: ctx.is_lint_enabled(REENTRANCY_NO_ETH.id),
298            reentrancy_balance_enabled,
299            balance_only_analysis: false,
300            call_balance_values: HashMap::new(),
301            return_collectors: Vec::new(),
302            active_balance_guards: Vec::new(),
303            balance_reentry_lock: reentrancy_balance_enabled
304                .then(|| balance_reentry_lock(gcx, hir, entry))
305                .flatten(),
306        }
307    }
308
309    const fn has_enabled_lints(&self) -> bool {
310        self.reentrancy_eth_enabled
311            || self.reentrancy_no_eth_enabled
312            || self.reentrancy_balance_enabled
313    }
314
315    fn analyze_callable(
316        &mut self,
317        func: &'hir hir::Function<'hir>,
318        body: hir::Block<'hir>,
319        state: &mut FlowState,
320    ) -> bool {
321        self.analyze_modifier_chain(func.modifiers, 0, body, state)
322    }
323
324    fn analyze_modifier_chain(
325        &mut self,
326        modifiers: &'hir [hir::Modifier<'hir>],
327        index: usize,
328        body: hir::Block<'hir>,
329        state: &mut FlowState,
330    ) -> bool {
331        let Some(modifier) = modifiers.get(index) else {
332            return self.analyze_block(body, None, state);
333        };
334
335        for arg in modifier.args.exprs() {
336            self.analyze_expr(arg, state);
337        }
338
339        let Some(modifier_id) = modifier.id.as_function() else {
340            return self.analyze_modifier_chain(modifiers, index + 1, body, state);
341        };
342
343        if self.call_stack.contains(&modifier_id) {
344            return self.analyze_modifier_chain(modifiers, index + 1, body, state);
345        }
346
347        let modifier_func = self.hir.function(modifier_id);
348        let Some(modifier_body) = modifier_func.body else {
349            return self.analyze_modifier_chain(modifiers, index + 1, body, state);
350        };
351
352        self.seed_balance_parameters(modifier_func, &modifier.args, state);
353        self.call_stack.push(modifier_id);
354        let balance_guard = self
355            .reentrancy_balance_enabled
356            .then(|| standard_reentrancy_guard_lock(self.hir, modifier_func))
357            .flatten();
358        let continuation = Some((modifiers, index + 1, body, balance_guard));
359        let falls_through = self.analyze_block(modifier_body, continuation, state);
360        self.call_stack.pop();
361        self.clear_function_balance_locals(modifier_id, state);
362        falls_through
363    }
364
365    fn analyze_block(
366        &mut self,
367        block: hir::Block<'hir>,
368        placeholder: Option<ModifierContinuation<'hir>>,
369        state: &mut FlowState,
370    ) -> bool {
371        for stmt in block.stmts {
372            if !self.analyze_stmt(stmt, placeholder, state) {
373                return false;
374            }
375        }
376        true
377    }
378
379    fn analyze_stmt(
380        &mut self,
381        stmt: &'hir hir::Stmt<'hir>,
382        placeholder: Option<ModifierContinuation<'hir>>,
383        state: &mut FlowState,
384    ) -> bool {
385        match stmt.kind {
386            StmtKind::DeclSingle(var_id) => {
387                let init = self.hir.variable(var_id).initializer;
388                if let Some(init) = init {
389                    self.analyze_expr(init, state);
390                    self.update_internal_function_target(state, var_id, init);
391                    if self.reentrancy_balance_enabled {
392                        self.update_balance_local(state, var_id, Some(init), false);
393                    }
394                }
395                if self.reentrancy_balance_enabled {
396                    self.update_self_address_local(state, var_id, init);
397                }
398                true
399            }
400            StmtKind::DeclMulti(vars, expr) => {
401                self.analyze_expr(expr, state);
402                if self.reentrancy_balance_enabled {
403                    self.update_balance_vars(state, vars.iter().copied(), expr);
404                    self.update_self_address_vars(state, vars.iter().copied(), expr);
405                }
406                true
407            }
408            StmtKind::Expr(expr) => {
409                self.analyze_expr(expr, state);
410                true
411            }
412            StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => {
413                self.analyze_block(block, placeholder, state)
414            }
415            StmtKind::Emit(expr) => {
416                self.analyze_expr(expr, state);
417                true
418            }
419            StmtKind::Revert(expr) => {
420                self.analyze_expr(expr, state);
421                false
422            }
423            StmtKind::Return(expr) => {
424                if let Some(expr) = expr {
425                    self.analyze_expr(expr, state);
426                }
427                if self.reentrancy_balance_enabled {
428                    self.record_return(expr, state);
429                }
430                false
431            }
432            StmtKind::Break | StmtKind::Continue => false,
433            StmtKind::Loop(block, _) => {
434                let before_loop = state.clone();
435                let mut body_state = state.clone();
436                self.analyze_block(block, placeholder, &mut body_state);
437                // One bounded second iteration exposes loop-carried balance checks while leaving
438                // the established ETH and no-ETH analysis unchanged.
439                let second_iteration = self.reentrancy_balance_enabled.then(|| {
440                    let mut second_iteration = body_state.balance_only();
441                    self.analyze_with_only_balance(|this| {
442                        this.analyze_block(block, placeholder, &mut second_iteration);
443                    });
444                    second_iteration
445                });
446                state.clear();
447                state.merge(&before_loop);
448                state.merge(&body_state);
449                let mut path_predicates = common_path_predicates(
450                    &before_loop.path_predicates,
451                    &body_state.path_predicates,
452                );
453                if let Some(second_iteration) = second_iteration {
454                    path_predicates =
455                        common_path_predicates(&path_predicates, &second_iteration.path_predicates);
456                    state.merge_balance(&second_iteration);
457                }
458                state.path_predicates = path_predicates;
459                true
460            }
461            StmtKind::If(cond, then_stmt, else_stmt) => {
462                self.analyze_expr(cond, state);
463                if self.reentrancy_balance_enabled
464                    && (branch_stops_current_path(then_stmt)
465                        || else_stmt.is_some_and(branch_stops_current_path))
466                {
467                    self.emit_balance_calls(cond, state);
468                }
469
470                let mut then_state = state.clone();
471                let mut else_state = state.clone();
472                let predicate = self
473                    .reentrancy_balance_enabled
474                    .then(|| path_predicate(self.hir, cond))
475                    .flatten();
476                let then_reachable =
477                    predicate.is_none_or(|predicate| then_state.constrain_path(predicate));
478                let else_reachable = predicate
479                    .is_none_or(|(var_id, value)| else_state.constrain_path((var_id, !value)));
480                let then_falls_through =
481                    then_reachable && self.analyze_stmt(then_stmt, placeholder, &mut then_state);
482
483                let else_falls_through = if !else_reachable {
484                    false
485                } else if let Some(else_stmt) = else_stmt {
486                    self.analyze_stmt(else_stmt, placeholder, &mut else_state)
487                } else {
488                    true
489                };
490
491                state.clear();
492                if then_falls_through {
493                    state.merge(&then_state);
494                }
495                if else_falls_through {
496                    state.merge(&else_state);
497                }
498                state.path_predicates = match (then_falls_through, else_falls_through) {
499                    (true, true) => common_path_predicates(
500                        &then_state.path_predicates,
501                        &else_state.path_predicates,
502                    ),
503                    (true, false) => then_state.path_predicates,
504                    (false, true) => else_state.path_predicates,
505                    (false, false) => PathPredicates::new(),
506                };
507
508                then_falls_through || else_falls_through
509            }
510            StmtKind::Try(try_stmt) => {
511                self.analyze_expr(&try_stmt.expr, state);
512
513                let mut merged = FlowState::default();
514                let mut path_predicates = None;
515                let mut any_falls_through = false;
516                for clause in try_stmt.clauses {
517                    let mut clause_state = state.clone();
518                    let falls_through =
519                        self.analyze_block(clause.block, placeholder, &mut clause_state);
520                    if falls_through {
521                        merged.merge(&clause_state);
522                        path_predicates = Some(match path_predicates {
523                            Some(path_predicates) => common_path_predicates(
524                                &path_predicates,
525                                &clause_state.path_predicates,
526                            ),
527                            None => clause_state.path_predicates.clone(),
528                        });
529                        any_falls_through = true;
530                    }
531                }
532
533                *state = merged;
534                state.path_predicates = path_predicates.unwrap_or_default();
535                any_falls_through
536            }
537            StmtKind::Placeholder => {
538                if let Some((modifiers, index, body, balance_guard)) = placeholder {
539                    if let Some(lock_var) = balance_guard {
540                        state.invalidated_balance_guards.remove(&lock_var);
541                        self.active_balance_guards.push(lock_var);
542                    }
543                    let falls_through = self.analyze_modifier_chain(modifiers, index, body, state);
544                    if balance_guard.is_some() {
545                        self.active_balance_guards.pop();
546                    }
547                    falls_through
548                } else {
549                    true
550                }
551            }
552            StmtKind::AssemblyBlock(_) | StmtKind::Switch(_) => {
553                state.invalidated_balance_guards.extend(self.active_balance_guards.iter().copied());
554                state.internal_function_targets.clear();
555                state.self_address_local_paths.clear();
556                true
557            }
558            StmtKind::Err(_) => true,
559        }
560    }
561
562    fn analyze_expr(&mut self, expr: &'hir hir::Expr<'hir>, state: &mut FlowState) {
563        match &expr.kind {
564            ExprKind::Assign(lhs, op, rhs) => {
565                if op.is_some() {
566                    self.analyze_expr(lhs, state);
567                }
568                self.analyze_expr(rhs, state);
569                self.analyze_lhs_indices(lhs, state);
570                let written_vars = state_write_lhs_vars(self.hir, lhs);
571                if !written_vars.is_empty() {
572                    self.emit_pending_calls(state, &written_vars);
573                    self.invalidate_balance_guards(state, &written_vars);
574                }
575                forget_path_predicates(state, local_write_lhs_vars(self.hir, lhs));
576                if let Some(var_id) = lhs_local_var(self.hir, lhs) {
577                    if op.is_none() {
578                        self.update_internal_function_target(state, var_id, rhs);
579                    } else {
580                        state.internal_function_targets.remove(&var_id);
581                    }
582                }
583                if self.reentrancy_balance_enabled {
584                    self.update_balance_assignment(state, lhs, rhs, op.is_some());
585                    self.update_self_address_assignment(state, lhs, rhs, op.is_some());
586                }
587            }
588            ExprKind::Delete(inner) => {
589                self.analyze_lhs_indices(inner, state);
590                let written_vars = state_write_lhs_vars(self.hir, inner);
591                if !written_vars.is_empty() {
592                    self.emit_pending_calls(state, &written_vars);
593                    self.invalidate_balance_guards(state, &written_vars);
594                }
595                forget_path_predicates(state, local_write_lhs_vars(self.hir, inner));
596                if let Some(var_id) = lhs_local_var(self.hir, inner) {
597                    state.internal_function_targets.remove(&var_id);
598                }
599                if self.reentrancy_balance_enabled
600                    && let Some(var_id) = lhs_local_var(self.hir, inner)
601                {
602                    self.update_balance_local(state, var_id, None, false);
603                    self.update_self_address_local(state, var_id, None);
604                }
605            }
606            ExprKind::Unary(op, inner)
607                if matches!(
608                    op.kind,
609                    UnOpKind::PreInc | UnOpKind::PreDec | UnOpKind::PostInc | UnOpKind::PostDec
610                ) =>
611            {
612                self.analyze_expr(inner, state);
613                let written_vars = state_write_lhs_vars(self.hir, inner);
614                if !written_vars.is_empty() {
615                    self.emit_pending_calls(state, &written_vars);
616                    self.invalidate_balance_guards(state, &written_vars);
617                }
618                forget_path_predicates(state, local_write_lhs_vars(self.hir, inner));
619                if self.reentrancy_balance_enabled
620                    && let Some(var_id) = lhs_local_var(self.hir, inner)
621                {
622                    self.update_self_address_local(state, var_id, None);
623                }
624            }
625            ExprKind::Unary(_, inner) => {
626                self.analyze_expr(inner, state);
627            }
628            ExprKind::Call(callee, args, opts) => {
629                let uses_delegate_context = call_uses_delegate_context(self.gcx, callee);
630                let mut operands = Vec::with_capacity(1 + args.len() + usize::from(opts.is_some()));
631                operands.push(*callee);
632                if let Some(opts) = opts {
633                    for opt in opts.args {
634                        operands.push(&opt.value);
635                    }
636                }
637                for arg in args.exprs() {
638                    operands.push(arg);
639                }
640
641                let before_operands = state.clone();
642                for operand in &operands {
643                    self.analyze_expr(operand, state);
644                }
645                // Solidity does not specify operand evaluation order. Reversing the operands
646                // covers both relative orders for each pair without changing the shared
647                // reentrancy analysis.
648                if self.reentrancy_balance_enabled && operands.len() > 1 {
649                    let mut reverse_state = before_operands.balance_only();
650                    self.analyze_with_only_balance(|this| {
651                        for operand in operands.iter().rev() {
652                            this.analyze_expr(operand, &mut reverse_state);
653                        }
654                    });
655                    state.merge_balance(&reverse_state);
656                }
657
658                if self.reentrancy_balance_enabled
659                    && is_require_or_assert(callee)
660                    && let Some(cond) = args.exprs().next()
661                {
662                    self.emit_balance_calls(cond, state);
663                }
664
665                for func_id in self.resolved_internal_function_ids(callee, state) {
666                    let returns = self.analyze_internal_call(func_id, args, state);
667                    self.merge_call_balance_values(expr.span, returns);
668                }
669                if !state.state_reads.is_empty()
670                    && let Some(kind) = self.reentrant_call_kind(callee, args, *opts)
671                {
672                    state.push_call(expr.span, kind);
673                }
674                if self.reentrancy_balance_enabled
675                    && is_balance_reentrant_call(self.gcx, self.hir, callee, args, *opts)
676                    && !self.balance_guard_blocks_call(state, callee)
677                {
678                    state.push_balance_call(expr.span);
679                }
680                if uses_delegate_context {
681                    state
682                        .invalidated_balance_guards
683                        .extend(self.active_balance_guards.iter().copied());
684                }
685            }
686            ExprKind::Binary(lhs, op, rhs)
687                if self.reentrancy_balance_enabled
688                    && matches!(op.kind, BinOpKind::And | BinOpKind::Or) =>
689            {
690                self.analyze_expr(lhs, state);
691
692                let rhs_outcome = op.kind == BinOpKind::And;
693                let mut short_state = state.clone();
694                let short_reachable =
695                    constrain_boolean_outcome(self.hir, lhs, !rhs_outcome, &mut short_state);
696                let mut rhs_state = state.clone();
697                let rhs_reachable =
698                    constrain_boolean_outcome(self.hir, lhs, rhs_outcome, &mut rhs_state);
699                if rhs_reachable {
700                    self.analyze_expr(rhs, &mut rhs_state);
701                }
702
703                state.clear();
704                if short_reachable {
705                    state.merge(&short_state);
706                }
707                if rhs_reachable {
708                    state.merge(&rhs_state);
709                }
710                state.path_predicates = match (short_reachable, rhs_reachable) {
711                    (true, true) => common_path_predicates(
712                        &short_state.path_predicates,
713                        &rhs_state.path_predicates,
714                    ),
715                    (true, false) => short_state.path_predicates,
716                    (false, true) => rhs_state.path_predicates,
717                    (false, false) => PathPredicates::new(),
718                };
719            }
720            ExprKind::Binary(lhs, _, rhs) => {
721                self.analyze_expr(lhs, state);
722                self.analyze_expr(rhs, state);
723            }
724            ExprKind::Index(base, index) => {
725                self.analyze_expr(base, state);
726                if let Some(index) = index {
727                    self.analyze_expr(index, state);
728                }
729            }
730            ExprKind::Slice(base, start, end) => {
731                self.analyze_expr(base, state);
732                if let Some(start) = start {
733                    self.analyze_expr(start, state);
734                }
735                if let Some(end) = end {
736                    self.analyze_expr(end, state);
737                }
738            }
739            ExprKind::Ternary(cond, true_expr, false_expr) => {
740                self.analyze_expr(cond, state);
741
742                let mut true_state = state.clone();
743                let mut false_state = state.clone();
744                let predicate = self
745                    .reentrancy_balance_enabled
746                    .then(|| path_predicate(self.hir, cond))
747                    .flatten();
748                let true_reachable =
749                    predicate.is_none_or(|predicate| true_state.constrain_path(predicate));
750                let false_reachable = predicate
751                    .is_none_or(|(var_id, value)| false_state.constrain_path((var_id, !value)));
752                if true_reachable {
753                    self.analyze_expr(true_expr, &mut true_state);
754                }
755                if false_reachable {
756                    self.analyze_expr(false_expr, &mut false_state);
757                }
758
759                state.clear();
760                if true_reachable {
761                    state.merge(&true_state);
762                }
763                if false_reachable {
764                    state.merge(&false_state);
765                }
766                state.path_predicates = match (true_reachable, false_reachable) {
767                    (true, true) => common_path_predicates(
768                        &true_state.path_predicates,
769                        &false_state.path_predicates,
770                    ),
771                    (true, false) => true_state.path_predicates,
772                    (false, true) => false_state.path_predicates,
773                    (false, false) => PathPredicates::new(),
774                };
775            }
776            ExprKind::Array(exprs) => {
777                for expr in *exprs {
778                    self.analyze_expr(expr, state);
779                }
780            }
781            ExprKind::Tuple(exprs) => {
782                for expr in exprs.iter().copied().flatten() {
783                    self.analyze_expr(expr, state);
784                }
785            }
786            ExprKind::Member(base, _) | ExprKind::Payable(base) => {
787                self.analyze_expr(base, state);
788            }
789            ExprKind::New(_) | ExprKind::TypeCall(_) | ExprKind::Type(_) => {}
790            ExprKind::Ident(reses) => {
791                for &res in *reses {
792                    if let Res::Item(ItemId::Variable(var_id)) = res
793                        && self.hir.variable(var_id).kind.is_state()
794                    {
795                        state.push_read(var_id);
796                    }
797                }
798            }
799            ExprKind::Lit(_) | ExprKind::YulMember(..) | ExprKind::Err(_) => {}
800        }
801    }
802
803    fn analyze_internal_call(
804        &mut self,
805        func_id: FunctionId,
806        args: &CallArgs<'hir>,
807        state: &mut FlowState,
808    ) -> Vec<BalanceValue> {
809        if self.call_stack.contains(&func_id) {
810            return Vec::new();
811        }
812
813        let func = self.hir.function(func_id);
814        let Some(body) = func.body else { return Vec::new() };
815
816        if self.reentrancy_balance_enabled {
817            self.seed_balance_parameters(func, args, state);
818        }
819        let parameter_predicates = if self.reentrancy_balance_enabled {
820            func.parameters
821                .iter()
822                .enumerate()
823                .map(|(index, _)| {
824                    argument_for_parameter(self.hir, args, func.parameters, index)
825                        .and_then(|arg| path_predicate(self.hir, arg))
826                })
827                .collect::<Vec<_>>()
828        } else {
829            Vec::new()
830        };
831
832        let key = InlineCallKey {
833            func_id,
834            recursive_cut: self.first_recursive_cut(func_id),
835            balance_only: self.balance_only_analysis,
836            active_balance_guards: self.active_balance_guards.clone(),
837            parameter_predicates: parameter_predicates.clone(),
838            state: state.clone(),
839        };
840        if self.inline_cache.is_in_progress(&key) {
841            self.clear_function_balance_locals(func_id, state);
842            return Vec::new();
843        }
844        if let Some(cached) = self.inline_cache.get(&key) {
845            let cached = cached.clone();
846            *state =
847                if self.balance_only_analysis { cached.state.balance_only() } else { cached.state };
848            return cached.returns;
849        }
850
851        let mut after = state.clone();
852        self.inline_cache.start(key.clone());
853        if self.reentrancy_balance_enabled {
854            self.return_collectors.push(ReturnCollector {
855                func_id,
856                values: vec![BalanceValue::default(); func.returns.len()],
857            });
858        }
859        self.call_stack.push(func_id);
860        let falls_through = self.analyze_callable(func, body, &mut after);
861        self.call_stack.pop();
862
863        let mut returns = if self.reentrancy_balance_enabled {
864            if falls_through {
865                self.record_return(None, &after);
866            }
867            self.return_collectors.pop().expect("return collector is active").values
868        } else {
869            Vec::new()
870        };
871        remap_return_paths(self.hir, func_id, func.parameters, &parameter_predicates, &mut returns);
872        self.clear_function_balance_locals(func_id, &mut after);
873        if self.balance_only_analysis {
874            after = after.balance_only();
875        }
876
877        self.inline_cache
878            .finish(key, InlineCallResult { state: after.clone(), returns: returns.clone() });
879        *state = after;
880        returns
881    }
882
883    fn resolved_internal_function_ids(
884        &self,
885        callee: &'hir hir::Expr<'hir>,
886        state: &FlowState,
887    ) -> BTreeSet<FunctionId> {
888        if let Some(var_id) = lhs_local_var(self.hir, callee)
889            && let Some(targets) = state.internal_function_targets.get(&var_id)
890        {
891            return targets.clone();
892        }
893        match &callee.peel_parens().kind {
894            ExprKind::Ident(_) => {}
895            ExprKind::Member(base, _) if is_super(base) => {}
896            _ => return BTreeSet::new(),
897        }
898        let Some(ty) = self.gcx.type_of_expr(callee.peel_parens().id) else {
899            return BTreeSet::new();
900        };
901        let TyKind::Fn(function) = ty.kind else { return BTreeSet::new() };
902        function.is_internal().then_some(function.function_id).flatten().into_iter().collect()
903    }
904
905    fn update_internal_function_target(
906        &self,
907        state: &mut FlowState,
908        var_id: VariableId,
909        value: &'hir hir::Expr<'hir>,
910    ) {
911        let targets = self.resolved_internal_function_ids(value, state);
912        state.internal_function_targets.remove(&var_id);
913        if !targets.is_empty() {
914            state.internal_function_targets.insert(var_id, targets);
915        }
916    }
917
918    fn merge_call_balance_values(&mut self, span: Span, values: Vec<BalanceValue>) {
919        let stored = self.call_balance_values.entry(span).or_default();
920        if stored.len() < values.len() {
921            stored.resize_with(values.len(), BalanceValue::default);
922        }
923        for (stored, value) in stored.iter_mut().zip(values) {
924            stored.balance_dependent |= value.balance_dependent;
925            stored.balance_paths.extend(value.balance_paths);
926            stored.self_address_paths.extend(value.self_address_paths);
927            stored.stale_calls.extend(value.stale_calls);
928            extend_unique(&mut stored.stale_comparisons, value.stale_comparisons);
929        }
930    }
931
932    fn analyze_with_only_balance<T>(&mut self, f: impl FnOnce(&mut Self) -> T) -> T {
933        let reentrancy_eth_enabled = self.reentrancy_eth_enabled;
934        let reentrancy_no_eth_enabled = self.reentrancy_no_eth_enabled;
935        let balance_only_analysis = self.balance_only_analysis;
936        self.reentrancy_eth_enabled = false;
937        self.reentrancy_no_eth_enabled = false;
938        self.balance_only_analysis = true;
939        let result = f(self);
940        self.reentrancy_eth_enabled = reentrancy_eth_enabled;
941        self.reentrancy_no_eth_enabled = reentrancy_no_eth_enabled;
942        self.balance_only_analysis = balance_only_analysis;
943        result
944    }
945
946    fn first_recursive_cut(&mut self, func_id: FunctionId) -> Option<FunctionId> {
947        let active_call_stack = self.call_stack.iter().copied().collect::<BTreeSet<_>>();
948        if active_call_stack.is_empty() {
949            return None;
950        }
951
952        let active_call_stack = active_call_stack.into_iter().collect::<Vec<_>>();
953        let key = RecursiveFrontierKey { func_id, active_call_stack };
954        if let Some(frontier) = self.recursive_cut_frontiers.get(&key) {
955            return frontier.first().copied();
956        }
957
958        let active_call_stack = key.active_call_stack.iter().copied().collect::<BTreeSet<_>>();
959        let mut seen = HashSet::new();
960        let cut = self.first_recursive_cut_function(func_id, &active_call_stack, &mut seen);
961        self.recursive_cut_frontiers.insert(key, cut.into_iter().collect::<Vec<_>>());
962        cut
963    }
964
965    fn first_recursive_cut_function(
966        &mut self,
967        func_id: FunctionId,
968        active_call_stack: &BTreeSet<FunctionId>,
969        seen: &mut HashSet<FunctionId>,
970    ) -> Option<FunctionId> {
971        if !seen.insert(func_id) {
972            return None;
973        }
974
975        for callee_id in self.direct_internal_calls(func_id) {
976            if active_call_stack.contains(&callee_id) {
977                return Some(callee_id);
978            }
979            if let Some(cut) = self.first_recursive_cut_function(callee_id, active_call_stack, seen)
980            {
981                return Some(cut);
982            }
983        }
984        None
985    }
986
987    fn direct_internal_calls(&mut self, func_id: FunctionId) -> Vec<FunctionId> {
988        if let Some(calls) = self.direct_internal_calls.get(&func_id) {
989            return calls.clone();
990        }
991
992        let mut calls = BTreeSet::new();
993        let func = self.hir.function(func_id);
994        for modifier in func.modifiers {
995            for arg in modifier.args.exprs() {
996                self.collect_direct_internal_calls_expr(arg, &mut calls);
997            }
998            if let Some(modifier_id) = modifier.id.as_function() {
999                calls.insert(modifier_id);
1000            }
1001        }
1002        if let Some(body) = func.body {
1003            self.collect_direct_internal_calls_block(body, &mut calls);
1004        }
1005
1006        let calls = calls.into_iter().collect::<Vec<_>>();
1007        self.direct_internal_calls.insert(func_id, calls.clone());
1008        calls
1009    }
1010
1011    fn collect_direct_internal_calls_block(
1012        &mut self,
1013        block: hir::Block<'hir>,
1014        calls: &mut BTreeSet<FunctionId>,
1015    ) {
1016        for stmt in block.stmts {
1017            self.collect_direct_internal_calls_stmt(stmt, calls);
1018        }
1019    }
1020
1021    fn collect_direct_internal_calls_stmt(
1022        &mut self,
1023        stmt: &'hir hir::Stmt<'hir>,
1024        calls: &mut BTreeSet<FunctionId>,
1025    ) {
1026        match stmt.kind {
1027            StmtKind::DeclSingle(var_id) => {
1028                if let Some(init) = self.hir.variable(var_id).initializer {
1029                    self.collect_direct_internal_calls_expr(init, calls);
1030                }
1031            }
1032            StmtKind::DeclMulti(_, expr)
1033            | StmtKind::Expr(expr)
1034            | StmtKind::Emit(expr)
1035            | StmtKind::Revert(expr) => {
1036                self.collect_direct_internal_calls_expr(expr, calls);
1037            }
1038            StmtKind::Return(expr) => {
1039                if let Some(expr) = expr {
1040                    self.collect_direct_internal_calls_expr(expr, calls);
1041                }
1042            }
1043            StmtKind::Block(block) | StmtKind::UncheckedBlock(block) | StmtKind::Loop(block, _) => {
1044                self.collect_direct_internal_calls_block(block, calls);
1045            }
1046            StmtKind::If(cond, then_stmt, else_stmt) => {
1047                self.collect_direct_internal_calls_expr(cond, calls);
1048                self.collect_direct_internal_calls_stmt(then_stmt, calls);
1049                if let Some(else_stmt) = else_stmt {
1050                    self.collect_direct_internal_calls_stmt(else_stmt, calls);
1051                }
1052            }
1053            StmtKind::Try(try_stmt) => {
1054                self.collect_direct_internal_calls_expr(&try_stmt.expr, calls);
1055                for clause in try_stmt.clauses {
1056                    self.collect_direct_internal_calls_block(clause.block, calls);
1057                }
1058            }
1059            StmtKind::Break
1060            | StmtKind::Continue
1061            | StmtKind::Placeholder
1062            | StmtKind::AssemblyBlock(_)
1063            | StmtKind::Switch(_)
1064            | StmtKind::Err(_) => {}
1065        }
1066    }
1067
1068    fn collect_direct_internal_calls_expr(
1069        &mut self,
1070        expr: &'hir hir::Expr<'hir>,
1071        calls: &mut BTreeSet<FunctionId>,
1072    ) {
1073        match &expr.kind {
1074            ExprKind::Assign(lhs, _, rhs) | ExprKind::Binary(lhs, _, rhs) => {
1075                self.collect_direct_internal_calls_expr(lhs, calls);
1076                self.collect_direct_internal_calls_expr(rhs, calls);
1077            }
1078            ExprKind::Unary(_, inner)
1079            | ExprKind::Delete(inner)
1080            | ExprKind::Member(inner, _)
1081            | ExprKind::Payable(inner) => {
1082                self.collect_direct_internal_calls_expr(inner, calls);
1083            }
1084            ExprKind::Call(callee, args, opts) => {
1085                self.collect_direct_internal_calls_expr(callee, calls);
1086                if let Some(opts) = opts {
1087                    for opt in opts.args {
1088                        self.collect_direct_internal_calls_expr(&opt.value, calls);
1089                    }
1090                }
1091                for arg in args.exprs() {
1092                    self.collect_direct_internal_calls_expr(arg, calls);
1093                }
1094                for func_id in self.resolved_internal_function_ids(callee, &FlowState::default()) {
1095                    calls.insert(func_id);
1096                }
1097            }
1098            ExprKind::Index(base, index) => {
1099                self.collect_direct_internal_calls_expr(base, calls);
1100                if let Some(index) = index {
1101                    self.collect_direct_internal_calls_expr(index, calls);
1102                }
1103            }
1104            ExprKind::Slice(base, start, end) => {
1105                self.collect_direct_internal_calls_expr(base, calls);
1106                if let Some(start) = start {
1107                    self.collect_direct_internal_calls_expr(start, calls);
1108                }
1109                if let Some(end) = end {
1110                    self.collect_direct_internal_calls_expr(end, calls);
1111                }
1112            }
1113            ExprKind::Ternary(cond, true_expr, false_expr) => {
1114                self.collect_direct_internal_calls_expr(cond, calls);
1115                self.collect_direct_internal_calls_expr(true_expr, calls);
1116                self.collect_direct_internal_calls_expr(false_expr, calls);
1117            }
1118            ExprKind::Array(exprs) => {
1119                for expr in *exprs {
1120                    self.collect_direct_internal_calls_expr(expr, calls);
1121                }
1122            }
1123            ExprKind::Tuple(exprs) => {
1124                for expr in exprs.iter().copied().flatten() {
1125                    self.collect_direct_internal_calls_expr(expr, calls);
1126                }
1127            }
1128            ExprKind::Ident(_)
1129            | ExprKind::Lit(_)
1130            | ExprKind::New(_)
1131            | ExprKind::TypeCall(_)
1132            | ExprKind::Type(_)
1133            | ExprKind::YulMember(..)
1134            | ExprKind::Err(_) => {}
1135        }
1136    }
1137
1138    fn analyze_lhs_indices(&mut self, expr: &'hir hir::Expr<'hir>, state: &mut FlowState) {
1139        match &expr.kind {
1140            ExprKind::Index(base, index) => {
1141                self.analyze_lhs_indices(base, state);
1142                if let Some(index) = index {
1143                    self.analyze_expr(index, state);
1144                }
1145            }
1146            ExprKind::Slice(base, start, end) => {
1147                self.analyze_lhs_indices(base, state);
1148                if let Some(start) = start {
1149                    self.analyze_expr(start, state);
1150                }
1151                if let Some(end) = end {
1152                    self.analyze_expr(end, state);
1153                }
1154            }
1155            ExprKind::Member(base, _) | ExprKind::Payable(base) => {
1156                self.analyze_lhs_indices(base, state);
1157            }
1158            ExprKind::Tuple(exprs) => {
1159                for expr in exprs.iter().copied().flatten() {
1160                    self.analyze_lhs_indices(expr, state);
1161                }
1162            }
1163            _ => {}
1164        }
1165    }
1166
1167    fn emit_pending_calls(&mut self, state: &FlowState, written_vars: &[VariableId]) {
1168        for call in &state.pending_calls {
1169            let (lint, msg_prefix) = match call.kind {
1170                ReentrantCallKind::Eth => {
1171                    (&REENTRANCY_ETH, "uncapped ETH transfer can be reentered before")
1172                }
1173                ReentrantCallKind::NoEth => {
1174                    (&REENTRANCY_NO_ETH, "external call can be reentered before")
1175                }
1176            };
1177            if !self.ctx.is_lint_enabled(lint.id) || self.emitted.contains(&call.span) {
1178                continue;
1179            }
1180
1181            if let Some(var_id) =
1182                written_vars.iter().find(|&&var_id| call.state_reads.contains(&var_id))
1183            {
1184                let name = self
1185                    .hir
1186                    .variable(*var_id)
1187                    .name
1188                    .map(|name| name.as_str().to_string())
1189                    .unwrap_or_else(|| "state".to_string());
1190                self.ctx.emit_with_msg(
1191                    lint,
1192                    call.span,
1193                    format!("{msg_prefix} `{name}` is updated"),
1194                );
1195                self.emitted.insert(call.span);
1196            }
1197        }
1198    }
1199
1200    fn emit_balance_calls(&mut self, guard: &'hir hir::Expr<'hir>, state: &FlowState) {
1201        for call in &state.pending_balance_calls {
1202            if self.emitted_balance.contains(&call.span)
1203                || !self.guard_has_stale_balance_comparison(guard, call, state)
1204            {
1205                continue;
1206            }
1207
1208            self.ctx.emit(&REENTRANCY_BALANCE, call.span);
1209            self.emitted_balance.insert(call.span);
1210        }
1211    }
1212
1213    fn guard_has_stale_balance_comparison(
1214        &self,
1215        expr: &'hir hir::Expr<'hir>,
1216        call: &PendingBalanceCall,
1217        state: &FlowState,
1218    ) -> bool {
1219        match &expr.peel_parens().kind {
1220            ExprKind::Binary(lhs, op, rhs) if matches!(op.kind, BinOpKind::And | BinOpKind::Or) => {
1221                if self.guard_has_stale_balance_comparison(lhs, call, state) {
1222                    return true;
1223                }
1224                let mut rhs_state = state.clone();
1225                constrain_boolean_outcome(self.hir, lhs, op.kind == BinOpKind::And, &mut rhs_state)
1226                    && self.guard_has_stale_balance_comparison(rhs, call, &rhs_state)
1227            }
1228            ExprKind::Binary(lhs, op, rhs) => {
1229                let is_comparison = matches!(
1230                    op.kind,
1231                    BinOpKind::Lt
1232                        | BinOpKind::Le
1233                        | BinOpKind::Gt
1234                        | BinOpKind::Ge
1235                        | BinOpKind::Eq
1236                        | BinOpKind::Ne
1237                );
1238                let current_locals = state
1239                    .balance_locals
1240                    .difference(&call.stale_locals)
1241                    .copied()
1242                    .collect::<BTreeSet<_>>();
1243                (is_comparison
1244                    && ((self.expr_depends_on_balance(
1245                        lhs,
1246                        &current_locals,
1247                        BalanceQuery::Current(call.span),
1248                        state,
1249                    ) && self.expr_depends_on_balance(
1250                        rhs,
1251                        &call.stale_locals,
1252                        BalanceQuery::Stale(call.span),
1253                        state,
1254                    )) || (self.expr_depends_on_balance(
1255                        rhs,
1256                        &current_locals,
1257                        BalanceQuery::Current(call.span),
1258                        state,
1259                    ) && self.expr_depends_on_balance(
1260                        lhs,
1261                        &call.stale_locals,
1262                        BalanceQuery::Stale(call.span),
1263                        state,
1264                    ))))
1265                    || self.guard_has_stale_balance_comparison(lhs, call, state)
1266                    || self.guard_has_stale_balance_comparison(rhs, call, state)
1267            }
1268            ExprKind::Unary(_, inner) | ExprKind::Payable(inner) => {
1269                self.guard_has_stale_balance_comparison(inner, call, state)
1270            }
1271            ExprKind::Ternary(cond, true_expr, false_expr) => {
1272                self.guard_has_stale_balance_comparison(cond, call, state)
1273                    || self.guard_has_stale_balance_comparison(true_expr, call, state)
1274                    || self.guard_has_stale_balance_comparison(false_expr, call, state)
1275            }
1276            ExprKind::Call(callee, args, opts)
1277                if opts.is_none()
1278                    && matches!(
1279                        callee.peel_parens().kind,
1280                        ExprKind::Type(_) | ExprKind::TypeCall(_)
1281                    ) =>
1282            {
1283                args.exprs().any(|arg| self.guard_has_stale_balance_comparison(arg, call, state))
1284            }
1285            ExprKind::Call(_, _, _) => {
1286                self.call_balance_values.get(&expr.peel_parens().span).is_some_and(|values| {
1287                    values.iter().any(|value| value.stale_comparisons.contains(&call.span))
1288                })
1289            }
1290            ExprKind::Ident(reses) => reses.iter().any(|res| {
1291                matches!(res, Res::Item(ItemId::Variable(var_id)) if state
1292                    .balance_comparison_locals
1293                    .get(var_id)
1294                    .is_some_and(|calls| calls.contains(&call.span)))
1295            }),
1296            _ => false,
1297        }
1298    }
1299
1300    fn update_balance_local(
1301        &mut self,
1302        state: &mut FlowState,
1303        var_id: VariableId,
1304        value: Option<&'hir hir::Expr<'hir>>,
1305        reads_old_value: bool,
1306    ) {
1307        let value = value.map(|value| self.balance_dependency(value, state)).unwrap_or_default();
1308        self.update_balance_local_with_value(state, var_id, &value, reads_old_value);
1309    }
1310
1311    fn update_self_address_local(
1312        &self,
1313        state: &mut FlowState,
1314        var_id: VariableId,
1315        value: Option<&hir::Expr<'_>>,
1316    ) {
1317        let paths = value
1318            .and_then(|value| self.self_address_dependencies(value, state).into_iter().next())
1319            .unwrap_or_default();
1320        self.update_self_address_local_with_paths(state, var_id, paths);
1321    }
1322
1323    fn update_self_address_local_with_paths(
1324        &self,
1325        state: &mut FlowState,
1326        var_id: VariableId,
1327        paths: PathAlternatives,
1328    ) {
1329        state.self_address_local_paths.remove(&var_id);
1330        if !paths.is_empty() {
1331            state.self_address_local_paths.insert(var_id, paths);
1332        }
1333    }
1334
1335    fn update_self_address_assignment(
1336        &self,
1337        state: &mut FlowState,
1338        lhs: &hir::Expr<'_>,
1339        rhs: &hir::Expr<'_>,
1340        reads_old_value: bool,
1341    ) {
1342        if let ExprKind::Tuple(lhs_values) = &lhs.peel_parens().kind {
1343            let values = self.self_address_dependencies(rhs, state);
1344            for (lhs, paths) in lhs_values.iter().zip(values) {
1345                if let Some(var_id) = lhs.and_then(|lhs| lhs_local_var(self.hir, lhs)) {
1346                    self.update_self_address_local_with_paths(state, var_id, paths);
1347                }
1348            }
1349        } else if let Some(var_id) = lhs_local_var(self.hir, lhs) {
1350            if reads_old_value {
1351                self.update_self_address_local_with_paths(state, var_id, PathAlternatives::new());
1352            } else {
1353                self.update_self_address_local(state, var_id, Some(rhs));
1354            }
1355        }
1356    }
1357
1358    fn update_self_address_vars(
1359        &self,
1360        state: &mut FlowState,
1361        vars: impl Iterator<Item = Option<VariableId>>,
1362        value: &hir::Expr<'_>,
1363    ) {
1364        for (var_id, paths) in vars.zip(self.self_address_dependencies(value, state)) {
1365            if let Some(var_id) = var_id {
1366                self.update_self_address_local_with_paths(state, var_id, paths);
1367            }
1368        }
1369    }
1370
1371    fn self_address_dependencies(
1372        &self,
1373        expr: &hir::Expr<'_>,
1374        state: &FlowState,
1375    ) -> Vec<PathAlternatives> {
1376        match &expr.peel_parens().kind {
1377            ExprKind::Tuple(exprs) => exprs
1378                .iter()
1379                .map(|expr| {
1380                    expr.map(|expr| self.self_address_path(expr, state)).unwrap_or_default()
1381                })
1382                .collect(),
1383            ExprKind::Call(_, _, _) => self
1384                .call_balance_values
1385                .get(&expr.span)
1386                .map(|values| values.iter().map(|value| value.self_address_paths.clone()).collect())
1387                .unwrap_or_else(|| vec![self.self_address_path(expr, state)]),
1388            _ => vec![self.self_address_path(expr, state)],
1389        }
1390    }
1391
1392    fn self_address_path(&self, expr: &hir::Expr<'_>, state: &FlowState) -> PathAlternatives {
1393        let expr = expr.peel_parens();
1394        if let ExprKind::Call(_, _, _) = expr.kind
1395            && let Some(value) =
1396                self.call_balance_values.get(&expr.span).and_then(|values| values.first())
1397        {
1398            return constrain_paths(&value.self_address_paths, &state.path_predicates);
1399        }
1400        match &expr.kind {
1401            ExprKind::Payable(inner) => self.self_address_path(inner, state),
1402            ExprKind::Call(callee, args, opts)
1403                if opts.is_none() && is_address_type_expr(callee) && args.exprs().count() == 1 =>
1404            {
1405                args.exprs()
1406                    .next()
1407                    .map(|arg| self.self_address_path(arg, state))
1408                    .unwrap_or_default()
1409            }
1410            _ => self_address_paths(expr, state),
1411        }
1412    }
1413
1414    fn self_balance_paths(&self, expr: &hir::Expr<'_>, state: &FlowState) -> PathAlternatives {
1415        let ExprKind::Member(base, member) = &expr.peel_parens().kind else {
1416            return PathAlternatives::new();
1417        };
1418        if member.as_str() != "balance" {
1419            return PathAlternatives::new();
1420        }
1421        self.self_address_path(base, state)
1422    }
1423
1424    fn update_balance_assignment(
1425        &mut self,
1426        state: &mut FlowState,
1427        lhs: &'hir hir::Expr<'hir>,
1428        rhs: &'hir hir::Expr<'hir>,
1429        reads_old_value: bool,
1430    ) {
1431        if let ExprKind::Tuple(lhs_values) = &lhs.peel_parens().kind {
1432            let values = self.balance_dependencies(rhs, state);
1433            for (lhs, value) in lhs_values.iter().zip(values.iter()) {
1434                if let Some(var_id) = lhs.and_then(|lhs| lhs_local_var(self.hir, lhs)) {
1435                    self.update_balance_local_with_value(state, var_id, value, false);
1436                }
1437            }
1438        } else if let Some(var_id) = lhs_local_var(self.hir, lhs) {
1439            self.update_balance_local(state, var_id, Some(rhs), reads_old_value);
1440        }
1441    }
1442
1443    fn update_balance_vars(
1444        &mut self,
1445        state: &mut FlowState,
1446        vars: impl Iterator<Item = Option<VariableId>>,
1447        value: &'hir hir::Expr<'hir>,
1448    ) {
1449        let values = self.balance_dependencies(value, state);
1450        for (var_id, value) in vars.zip(values.iter()) {
1451            if let Some(var_id) = var_id {
1452                self.update_balance_local_with_value(state, var_id, value, false);
1453            }
1454        }
1455    }
1456
1457    fn update_balance_local_with_value(
1458        &self,
1459        state: &mut FlowState,
1460        var_id: VariableId,
1461        value: &BalanceValue,
1462        reads_old_value: bool,
1463    ) {
1464        let balance_dependent =
1465            value.balance_dependent || (reads_old_value && state.balance_locals.contains(&var_id));
1466        let mut balance_paths = value.balance_paths.clone();
1467        let mut stale_comparisons = value.stale_comparisons.clone();
1468        if reads_old_value {
1469            if let Some(old_paths) = state.balance_local_paths.get(&var_id) {
1470                balance_paths.extend(old_paths.iter().cloned());
1471            }
1472            if let Some(old_comparisons) = state.balance_comparison_locals.get(&var_id) {
1473                extend_unique(&mut stale_comparisons, old_comparisons.iter().copied());
1474            }
1475        }
1476
1477        for call in &mut state.pending_balance_calls {
1478            let stale = value.stale_calls.contains(&call.span)
1479                || (reads_old_value && call.stale_locals.contains(&var_id));
1480            call.stale_locals.remove(&var_id);
1481            if stale {
1482                call.stale_locals.insert(var_id);
1483            }
1484        }
1485
1486        state.balance_locals.remove(&var_id);
1487        state.balance_local_paths.remove(&var_id);
1488        state.balance_comparison_locals.remove(&var_id);
1489        if balance_dependent {
1490            state.balance_locals.insert(var_id);
1491            state.balance_local_paths.insert(var_id, balance_paths);
1492        }
1493        if !stale_comparisons.is_empty() {
1494            state.balance_comparison_locals.insert(var_id, stale_comparisons);
1495        }
1496    }
1497
1498    fn balance_dependencies(
1499        &self,
1500        expr: &'hir hir::Expr<'hir>,
1501        state: &FlowState,
1502    ) -> Vec<BalanceValue> {
1503        match &expr.peel_parens().kind {
1504            ExprKind::Tuple(exprs) => exprs
1505                .iter()
1506                .map(|expr| {
1507                    expr.map(|expr| self.balance_dependency(expr, state)).unwrap_or_default()
1508                })
1509                .collect(),
1510            ExprKind::Call(_, _, _) => self
1511                .call_balance_values
1512                .get(&expr.span)
1513                .cloned()
1514                .unwrap_or_else(|| vec![self.balance_dependency(expr, state)]),
1515            _ => vec![self.balance_dependency(expr, state)],
1516        }
1517    }
1518
1519    fn balance_dependency(&self, expr: &'hir hir::Expr<'hir>, state: &FlowState) -> BalanceValue {
1520        let balance_paths = self.expr_balance_paths(expr, state);
1521        let balance_dependent = !balance_paths.is_empty();
1522        let self_address_paths = self.self_address_path(expr, state);
1523        let stale_calls = state
1524            .pending_balance_calls
1525            .iter()
1526            .filter(|call| {
1527                self.expr_depends_on_balance(
1528                    expr,
1529                    &call.stale_locals,
1530                    BalanceQuery::Stale(call.span),
1531                    state,
1532                )
1533            })
1534            .map(|call| call.span)
1535            .collect();
1536        let stale_comparisons = state
1537            .pending_balance_calls
1538            .iter()
1539            .filter(|call| self.guard_has_stale_balance_comparison(expr, call, state))
1540            .map(|call| call.span)
1541            .collect();
1542        BalanceValue {
1543            balance_dependent,
1544            balance_paths,
1545            self_address_paths,
1546            stale_calls,
1547            stale_comparisons,
1548        }
1549    }
1550
1551    fn expr_balance_paths(
1552        &self,
1553        expr: &'hir hir::Expr<'hir>,
1554        state: &FlowState,
1555    ) -> PathAlternatives {
1556        let expr = expr.peel_parens();
1557        let self_balance_paths = self.self_balance_paths(expr, state);
1558        if !self_balance_paths.is_empty() {
1559            return self_balance_paths;
1560        }
1561
1562        match &expr.kind {
1563            ExprKind::Ident(reses) => {
1564                let mut paths = PathAlternatives::new();
1565                for var_id in reses.iter().filter_map(|res| match res {
1566                    Res::Item(ItemId::Variable(var_id)) => Some(var_id),
1567                    _ => None,
1568                }) {
1569                    if let Some(local_paths) = state.balance_local_paths.get(var_id) {
1570                        paths.extend(constrain_paths(local_paths, &state.path_predicates));
1571                    }
1572                }
1573                paths
1574            }
1575            ExprKind::Unary(_, inner) | ExprKind::Payable(inner) => {
1576                self.expr_balance_paths(inner, state)
1577            }
1578            ExprKind::Binary(lhs, _, rhs) => {
1579                let mut paths = self.expr_balance_paths(lhs, state);
1580                paths.extend(self.expr_balance_paths(rhs, state));
1581                paths
1582            }
1583            ExprKind::Ternary(cond, true_expr, false_expr) => {
1584                let mut paths = self.expr_balance_paths(cond, state);
1585                paths.extend(self.expr_balance_paths(true_expr, state));
1586                paths.extend(self.expr_balance_paths(false_expr, state));
1587                paths
1588            }
1589            ExprKind::Call(callee, args, opts)
1590                if opts.is_none()
1591                    && matches!(
1592                        callee.peel_parens().kind,
1593                        ExprKind::Type(_) | ExprKind::TypeCall(_)
1594                    ) =>
1595            {
1596                let mut paths = PathAlternatives::new();
1597                for arg in args.exprs() {
1598                    paths.extend(self.expr_balance_paths(arg, state));
1599                }
1600                paths
1601            }
1602            ExprKind::Call(_, _, _) => {
1603                let mut paths = PathAlternatives::new();
1604                if let Some(values) = self.call_balance_values.get(&expr.span) {
1605                    for value in values {
1606                        paths.extend(constrain_paths(&value.balance_paths, &state.path_predicates));
1607                    }
1608                }
1609                paths
1610            }
1611            _ => PathAlternatives::new(),
1612        }
1613    }
1614
1615    fn expr_depends_on_balance(
1616        &self,
1617        expr: &'hir hir::Expr<'hir>,
1618        locals: &BTreeSet<VariableId>,
1619        query: BalanceQuery,
1620        state: &FlowState,
1621    ) -> bool {
1622        let expr = expr.peel_parens();
1623        let self_balance_paths = self.self_balance_paths(expr, state);
1624        if !self_balance_paths.is_empty() {
1625            return match query {
1626                BalanceQuery::Current(call) => state
1627                    .pending_balance_calls
1628                    .iter()
1629                    .find(|pending| pending.span == call)
1630                    .is_some_and(|pending| {
1631                        path_alternatives_compatible(&self_balance_paths, &pending.paths)
1632                    }),
1633                BalanceQuery::Stale(_) => false,
1634            };
1635        }
1636
1637        match &expr.kind {
1638            ExprKind::Ident(reses) => reses.iter().any(
1639                |res| matches!(res, Res::Item(ItemId::Variable(var_id)) if locals.contains(var_id)),
1640            ),
1641            ExprKind::Unary(_, inner) | ExprKind::Payable(inner) => {
1642                self.expr_depends_on_balance(inner, locals, query, state)
1643            }
1644            ExprKind::Binary(lhs, _, rhs) => {
1645                self.expr_depends_on_balance(lhs, locals, query, state)
1646                    || self.expr_depends_on_balance(rhs, locals, query, state)
1647            }
1648            ExprKind::Ternary(cond, true_expr, false_expr) => {
1649                self.expr_depends_on_balance(cond, locals, query, state)
1650                    || self.expr_depends_on_balance(true_expr, locals, query, state)
1651                    || self.expr_depends_on_balance(false_expr, locals, query, state)
1652            }
1653            ExprKind::Call(callee, args, opts)
1654                if opts.is_none()
1655                    && matches!(
1656                        callee.peel_parens().kind,
1657                        ExprKind::Type(_) | ExprKind::TypeCall(_)
1658                    ) =>
1659            {
1660                args.exprs().any(|arg| self.expr_depends_on_balance(arg, locals, query, state))
1661            }
1662            ExprKind::Call(_, _, _) => {
1663                self.call_balance_values.get(&expr.span).is_some_and(|values| {
1664                    values.iter().any(|value| match query {
1665                        BalanceQuery::Current(call) => {
1666                            value.balance_dependent && !value.stale_calls.contains(&call)
1667                        }
1668                        BalanceQuery::Stale(call) => value.stale_calls.contains(&call),
1669                    })
1670                })
1671            }
1672            _ => false,
1673        }
1674    }
1675
1676    fn seed_balance_parameters(
1677        &mut self,
1678        func: &'hir hir::Function<'hir>,
1679        args: &CallArgs<'hir>,
1680        state: &mut FlowState,
1681    ) {
1682        if !self.reentrancy_balance_enabled {
1683            return;
1684        }
1685        let values = func
1686            .parameters
1687            .iter()
1688            .enumerate()
1689            .map(|(index, &param)| {
1690                let argument = argument_for_parameter(self.hir, args, func.parameters, index);
1691                let value =
1692                    argument.map(|arg| self.balance_dependency(arg, state)).unwrap_or_default();
1693                let self_address_paths =
1694                    argument.map(|arg| self.self_address_path(arg, state)).unwrap_or_default();
1695                (param, value, self_address_paths)
1696            })
1697            .collect::<Vec<_>>();
1698        for (param, value, self_address_paths) in values {
1699            self.update_balance_local_with_value(state, param, &value, false);
1700            self.update_self_address_local_with_paths(state, param, self_address_paths);
1701        }
1702    }
1703
1704    fn record_return(&mut self, expr: Option<&'hir hir::Expr<'hir>>, state: &FlowState) {
1705        let Some(func_id) = self.return_collectors.last().map(|collector| collector.func_id) else {
1706            return;
1707        };
1708        let func = self.hir.function(func_id);
1709        let values = if let Some(expr) = expr {
1710            self.balance_dependencies(expr, state)
1711        } else {
1712            func.returns
1713                .iter()
1714                .map(|var_id| {
1715                    let balance_dependent = state.balance_locals.contains(var_id);
1716                    let balance_paths =
1717                        state.balance_local_paths.get(var_id).cloned().unwrap_or_default();
1718                    let self_address_paths =
1719                        state.self_address_local_paths.get(var_id).cloned().unwrap_or_default();
1720                    let stale_calls = state
1721                        .pending_balance_calls
1722                        .iter()
1723                        .filter(|call| call.stale_locals.contains(var_id))
1724                        .map(|call| call.span)
1725                        .collect();
1726                    let stale_comparisons =
1727                        state.balance_comparison_locals.get(var_id).cloned().unwrap_or_default();
1728                    BalanceValue {
1729                        balance_dependent,
1730                        balance_paths,
1731                        self_address_paths,
1732                        stale_calls,
1733                        stale_comparisons,
1734                    }
1735                })
1736                .collect()
1737        };
1738        let collector = self.return_collectors.last_mut().expect("return collector is active");
1739        for (stored, value) in collector.values.iter_mut().zip(values) {
1740            stored.balance_dependent |= value.balance_dependent;
1741            stored.balance_paths.extend(value.balance_paths);
1742            stored.self_address_paths.extend(value.self_address_paths);
1743            stored.stale_calls.extend(value.stale_calls);
1744            extend_unique(&mut stored.stale_comparisons, value.stale_comparisons);
1745        }
1746    }
1747
1748    fn clear_function_balance_locals(&self, func_id: FunctionId, state: &mut FlowState) {
1749        let belongs_to_function = |var_id: &VariableId| {
1750            self.hir.variable(*var_id).parent == Some(ItemId::Function(func_id))
1751        };
1752        state.internal_function_targets.retain(|var_id, _| !belongs_to_function(var_id));
1753        state.balance_locals.retain(|var_id| !belongs_to_function(var_id));
1754        state.self_address_local_paths.retain(|var_id, _| !belongs_to_function(var_id));
1755        state.balance_local_paths.retain(|var_id, _| !belongs_to_function(var_id));
1756        state.balance_comparison_locals.retain(|var_id, _| !belongs_to_function(var_id));
1757        state.path_predicates.retain(|predicate, _| {
1758            !matches!(predicate, PathPredicate::Boolean(var_id) if belongs_to_function(var_id))
1759                && !matches!(
1760                    predicate,
1761                    PathPredicate::Equality(lhs, rhs)
1762                        if lhs.variable().is_some_and(|var_id| belongs_to_function(&var_id))
1763                            || rhs.variable().is_some_and(|var_id| belongs_to_function(&var_id))
1764                )
1765        });
1766        for call in &mut state.pending_balance_calls {
1767            call.stale_locals.retain(|var_id| !belongs_to_function(var_id));
1768        }
1769    }
1770
1771    fn reentrant_call_kind(
1772        &self,
1773        callee: &'hir hir::Expr<'hir>,
1774        args: &CallArgs<'hir>,
1775        opts: Option<&hir::CallOptions<'hir>>,
1776    ) -> Option<ReentrantCallKind> {
1777        if self.reentrancy_eth_enabled && is_uncapped_value_call(self.hir, callee, opts) {
1778            return Some(ReentrantCallKind::Eth);
1779        }
1780        if self.reentrancy_no_eth_enabled
1781            && is_no_eth_reentrant_call(self.gcx, self.hir, callee, args, opts)
1782        {
1783            return Some(ReentrantCallKind::NoEth);
1784        }
1785        None
1786    }
1787
1788    fn invalidate_balance_guards(&self, state: &mut FlowState, written_vars: &[VariableId]) {
1789        state.invalidated_balance_guards.extend(
1790            written_vars
1791                .iter()
1792                .filter(|var_id| self.active_balance_guards.contains(var_id))
1793                .copied(),
1794        );
1795    }
1796
1797    fn balance_guard_blocks_call(&self, state: &FlowState, callee: &'hir hir::Expr<'hir>) -> bool {
1798        !call_uses_delegate_context(self.gcx, callee)
1799            && self.balance_reentry_lock.is_some_and(|reentry_lock| {
1800                self.active_balance_guards.iter().any(|lock_var| {
1801                    *lock_var == reentry_lock
1802                        && !state.invalidated_balance_guards.contains(lock_var)
1803                })
1804            })
1805    }
1806}
1807
1808impl FlowState {
1809    fn clear(&mut self) {
1810        self.state_reads.clear();
1811        self.pending_calls.clear();
1812        self.internal_function_targets.clear();
1813        self.self_address_local_paths.clear();
1814        self.balance_locals.clear();
1815        self.balance_local_paths.clear();
1816        self.balance_comparison_locals.clear();
1817        self.pending_balance_calls.clear();
1818        self.invalidated_balance_guards.clear();
1819        self.path_predicates.clear();
1820    }
1821
1822    fn merge(&mut self, other: &Self) {
1823        self.state_reads.extend(other.state_reads.iter().copied());
1824        for call in &other.pending_calls {
1825            if let Some(existing) = self
1826                .pending_calls
1827                .iter_mut()
1828                .find(|existing| existing.span == call.span && existing.kind == call.kind)
1829            {
1830                existing.state_reads.extend(call.state_reads.iter().copied());
1831            } else {
1832                self.pending_calls.push(call.clone());
1833            }
1834        }
1835        for (var_id, targets) in &other.internal_function_targets {
1836            self.internal_function_targets
1837                .entry(*var_id)
1838                .or_default()
1839                .extend(targets.iter().copied());
1840        }
1841        merge_balance_local_paths(
1842            &mut self.self_address_local_paths,
1843            &other.self_address_local_paths,
1844        );
1845        self.balance_locals.extend(other.balance_locals.iter().copied());
1846        merge_balance_local_paths(&mut self.balance_local_paths, &other.balance_local_paths);
1847        merge_comparison_locals(
1848            &mut self.balance_comparison_locals,
1849            &other.balance_comparison_locals,
1850        );
1851        self.invalidated_balance_guards.extend(other.invalidated_balance_guards.iter().copied());
1852        for call in &other.pending_balance_calls {
1853            if let Some(existing) =
1854                self.pending_balance_calls.iter_mut().find(|existing| existing.span == call.span)
1855            {
1856                existing.stale_locals.extend(call.stale_locals.iter().copied());
1857                existing.paths.extend(call.paths.iter().cloned());
1858            } else {
1859                self.pending_balance_calls.push(call.clone());
1860            }
1861        }
1862    }
1863
1864    fn balance_only(&self) -> Self {
1865        Self {
1866            internal_function_targets: self.internal_function_targets.clone(),
1867            self_address_local_paths: self.self_address_local_paths.clone(),
1868            balance_locals: self.balance_locals.clone(),
1869            balance_local_paths: self.balance_local_paths.clone(),
1870            balance_comparison_locals: self.balance_comparison_locals.clone(),
1871            pending_balance_calls: self.pending_balance_calls.clone(),
1872            invalidated_balance_guards: self.invalidated_balance_guards.clone(),
1873            path_predicates: self.path_predicates.clone(),
1874            ..Self::default()
1875        }
1876    }
1877
1878    fn merge_balance(&mut self, other: &Self) {
1879        merge_balance_local_paths(
1880            &mut self.self_address_local_paths,
1881            &other.self_address_local_paths,
1882        );
1883        for (var_id, targets) in &other.internal_function_targets {
1884            self.internal_function_targets
1885                .entry(*var_id)
1886                .or_default()
1887                .extend(targets.iter().copied());
1888        }
1889        self.balance_locals.extend(other.balance_locals.iter().copied());
1890        merge_balance_local_paths(&mut self.balance_local_paths, &other.balance_local_paths);
1891        merge_comparison_locals(
1892            &mut self.balance_comparison_locals,
1893            &other.balance_comparison_locals,
1894        );
1895        self.invalidated_balance_guards.extend(other.invalidated_balance_guards.iter().copied());
1896        for call in &other.pending_balance_calls {
1897            if let Some(existing) =
1898                self.pending_balance_calls.iter_mut().find(|existing| existing.span == call.span)
1899            {
1900                existing.stale_locals.extend(call.stale_locals.iter().copied());
1901                existing.paths.extend(call.paths.iter().cloned());
1902            } else {
1903                self.pending_balance_calls.push(call.clone());
1904            }
1905        }
1906    }
1907
1908    fn constrain_path(&mut self, (predicate, value): (PathPredicate, bool)) -> bool {
1909        match self.path_predicates.get(&predicate) {
1910            Some(existing) => *existing == value,
1911            None => {
1912                self.path_predicates.insert(predicate, value);
1913                for paths in self.self_address_local_paths.values_mut() {
1914                    *paths = constrain_paths(paths, &self.path_predicates);
1915                }
1916                true
1917            }
1918        }
1919    }
1920}
1921
1922fn path_predicate(hir: &hir::Hir<'_>, expr: &hir::Expr<'_>) -> Option<(PathPredicate, bool)> {
1923    match &expr.peel_parens().kind {
1924        ExprKind::Ident(reses) => {
1925            let var_id = unique(reses.iter().filter_map(|res| match res {
1926                Res::Item(ItemId::Variable(var_id)) if !hir.variable(*var_id).kind.is_state() => {
1927                    Some(*var_id)
1928                }
1929                _ => None,
1930            }))?;
1931            Some((PathPredicate::Boolean(var_id), true))
1932        }
1933        ExprKind::Unary(op, inner) if op.kind == UnOpKind::Not => {
1934            path_predicate(hir, inner).map(|(predicate, value)| (predicate, !value))
1935        }
1936        ExprKind::Binary(lhs, op, rhs) if matches!(op.kind, BinOpKind::Eq | BinOpKind::Ne) => {
1937            let mut lhs = predicate_operand(hir, lhs)?;
1938            let mut rhs = predicate_operand(hir, rhs)?;
1939            if lhs > rhs {
1940                std::mem::swap(&mut lhs, &mut rhs);
1941            }
1942            Some((PathPredicate::Equality(lhs, rhs), op.kind == BinOpKind::Eq))
1943        }
1944        _ => None,
1945    }
1946}
1947
1948fn predicate_operand(hir: &hir::Hir<'_>, expr: &hir::Expr<'_>) -> Option<PredicateOperand> {
1949    match &expr.peel_parens().kind {
1950        ExprKind::Ident(reses) => {
1951            let var_id = unique(reses.iter().filter_map(|res| match res {
1952                Res::Item(ItemId::Variable(var_id)) if !hir.variable(*var_id).kind.is_state() => {
1953                    Some(*var_id)
1954                }
1955                _ => None,
1956            }))?;
1957            Some(PredicateOperand::Variable(var_id))
1958        }
1959        ExprKind::Lit(lit) => match lit.kind {
1960            LitKind::Number(value) => Some(PredicateOperand::Number(value)),
1961            LitKind::Bool(value) => Some(PredicateOperand::Boolean(value)),
1962            _ => None,
1963        },
1964        _ => None,
1965    }
1966}
1967
1968fn constrain_boolean_outcome(
1969    hir: &hir::Hir<'_>,
1970    expr: &hir::Expr<'_>,
1971    outcome: bool,
1972    state: &mut FlowState,
1973) -> bool {
1974    if let Some((var_id, value)) = path_predicate(hir, expr) {
1975        return state.constrain_path((var_id, if outcome { value } else { !value }));
1976    }
1977    match &expr.peel_parens().kind {
1978        ExprKind::Binary(lhs, op, rhs)
1979            if (outcome && op.kind == BinOpKind::And) || (!outcome && op.kind == BinOpKind::Or) =>
1980        {
1981            constrain_boolean_outcome(hir, lhs, outcome, state)
1982                && constrain_boolean_outcome(hir, rhs, outcome, state)
1983        }
1984        _ => true,
1985    }
1986}
1987
1988fn common_path_predicates(lhs: &PathPredicates, rhs: &PathPredicates) -> PathPredicates {
1989    lhs.iter()
1990        .filter(|(var_id, value)| rhs.get(var_id) == Some(*value))
1991        .map(|(var_id, value)| (*var_id, *value))
1992        .collect()
1993}
1994
1995fn paths_compatible(lhs: &PathPredicates, rhs: &PathPredicates) -> bool {
1996    lhs.iter().all(|(var_id, value)| rhs.get(var_id).is_none_or(|other| other == value))
1997}
1998
1999fn path_alternatives_compatible(lhs: &PathAlternatives, rhs: &PathAlternatives) -> bool {
2000    lhs.iter().any(|lhs| rhs.iter().any(|rhs| paths_compatible(lhs, rhs)))
2001}
2002
2003fn paths_compatible_with(paths: &PathAlternatives, active: &PathPredicates) -> bool {
2004    paths.iter().any(|path| paths_compatible(path, active))
2005}
2006
2007fn constrain_paths(paths: &PathAlternatives, active: &PathPredicates) -> PathAlternatives {
2008    paths
2009        .iter()
2010        .filter(|path| paths_compatible(path, active))
2011        .map(|path| {
2012            let mut constrained = path.clone();
2013            constrained.extend(active.iter().map(|(predicate, value)| (*predicate, *value)));
2014            constrained
2015        })
2016        .collect()
2017}
2018
2019fn remap_return_paths(
2020    hir: &hir::Hir<'_>,
2021    func_id: FunctionId,
2022    parameters: &[VariableId],
2023    parameter_predicates: &[Option<(PathPredicate, bool)>],
2024    values: &mut [BalanceValue],
2025) {
2026    for value in values {
2027        value.balance_paths =
2028            remap_paths(hir, func_id, parameters, parameter_predicates, &value.balance_paths);
2029        value.self_address_paths =
2030            remap_paths(hir, func_id, parameters, parameter_predicates, &value.self_address_paths);
2031        value.balance_dependent = !value.balance_paths.is_empty();
2032    }
2033}
2034
2035fn remap_paths(
2036    hir: &hir::Hir<'_>,
2037    func_id: FunctionId,
2038    parameters: &[VariableId],
2039    parameter_predicates: &[Option<(PathPredicate, bool)>],
2040    paths: &PathAlternatives,
2041) -> PathAlternatives {
2042    paths
2043        .iter()
2044        .filter_map(|path| {
2045            let mut path = path.clone();
2046            for (&parameter, &argument) in parameters.iter().zip(parameter_predicates) {
2047                let Some(parameter_value) = path.remove(&PathPredicate::Boolean(parameter)) else {
2048                    continue
2049                };
2050                let Some((argument_predicate, argument_value)) = argument else { continue };
2051                let mapped_value = if parameter_value { argument_value } else { !argument_value };
2052                if path
2053                    .get(&argument_predicate)
2054                    .is_some_and(|existing| *existing != mapped_value)
2055                {
2056                    return None;
2057                }
2058                path.insert(argument_predicate, mapped_value);
2059            }
2060            path.retain(|predicate, _| {
2061                !matches!(predicate, PathPredicate::Boolean(var_id) if hir.variable(*var_id).parent == Some(ItemId::Function(func_id)))
2062                    && !matches!(
2063                        predicate,
2064                        PathPredicate::Equality(lhs, rhs)
2065                            if lhs.variable().is_some_and(|var_id| hir.variable(var_id).parent == Some(ItemId::Function(func_id)))
2066                                || rhs.variable().is_some_and(|var_id| hir.variable(var_id).parent == Some(ItemId::Function(func_id)))
2067                    )
2068            });
2069            Some(path)
2070        })
2071        .collect()
2072}
2073
2074fn merge_balance_local_paths(
2075    stored: &mut BTreeMap<VariableId, PathAlternatives>,
2076    other: &BTreeMap<VariableId, PathAlternatives>,
2077) {
2078    for (var_id, paths) in other {
2079        stored.entry(*var_id).or_default().extend(paths.iter().cloned());
2080    }
2081}
2082
2083fn merge_comparison_locals(
2084    stored: &mut BTreeMap<VariableId, Vec<Span>>,
2085    other: &BTreeMap<VariableId, Vec<Span>>,
2086) {
2087    for (var_id, comparisons) in other {
2088        extend_unique(stored.entry(*var_id).or_default(), comparisons.iter().copied());
2089    }
2090}
2091
2092fn is_balance_reentrant_call<'hir>(
2093    gcx: Gcx<'hir>,
2094    hir: &'hir hir::Hir<'hir>,
2095    callee: &'hir hir::Expr<'hir>,
2096    _args: &CallArgs<'hir>,
2097    opts: Option<&hir::CallOptions<'hir>>,
2098) -> bool {
2099    if !call_options_allow_reentrancy(hir, opts) {
2100        return false;
2101    }
2102
2103    match &callee.peel_parens().kind {
2104        ExprKind::Member(receiver, _) if is_contract_receiver(gcx, receiver) => {
2105            external_call_can_reenter(gcx, callee)
2106        }
2107        ExprKind::Member(receiver, member)
2108            if is_address_like(gcx, hir, receiver)
2109                && matches!(
2110                    member.name,
2111                    kw::Call | kw::Callcode | kw::Delegatecall | kw::Staticcall
2112                ) =>
2113        {
2114            member.name != kw::Staticcall
2115        }
2116        ExprKind::Member(receiver, _) if is_super(receiver) => false,
2117        _ => external_call_can_reenter(gcx, callee),
2118    }
2119}
2120
2121fn call_options_allow_reentrancy(hir: &hir::Hir<'_>, opts: Option<&hir::CallOptions<'_>>) -> bool {
2122    let Some(opts) = opts else { return true };
2123    let Some(gas) = opts.args.iter().find(|opt| opt.name.name == kw::Gas) else {
2124        return true;
2125    };
2126    let may_transfer_value = opts
2127        .args
2128        .iter()
2129        .find(|opt| opt.name.name == sym::value)
2130        .is_some_and(|value| !is_zero_value(hir, &value.value));
2131    let mut seen = BTreeSet::new();
2132    concrete_gas_cap(hir, &gas.value, &mut seen).is_none_or(|gas| {
2133        gas > U256::from(REENTRANCY_GAS_STIPEND) || (may_transfer_value && !gas.is_zero())
2134    })
2135}
2136
2137fn concrete_gas_cap(
2138    hir: &hir::Hir<'_>,
2139    expr: &hir::Expr<'_>,
2140    seen: &mut BTreeSet<VariableId>,
2141) -> Option<U256> {
2142    match &expr.peel_parens().kind {
2143        ExprKind::Lit(lit) => match lit.kind {
2144            LitKind::Number(value) => Some(value),
2145            _ => None,
2146        },
2147        ExprKind::Ident(reses) => {
2148            let var_id = unique(reses.iter().filter_map(|res| match res {
2149                Res::Item(ItemId::Variable(var_id)) => Some(*var_id),
2150                _ => None,
2151            }))?;
2152            let var = hir.variable(var_id);
2153            if !var.is_constant() || !seen.insert(var_id) {
2154                return None;
2155            }
2156            concrete_gas_cap(hir, var.initializer?, seen)
2157        }
2158        ExprKind::Call(callee, args, opts)
2159            if opts.is_none()
2160                && matches!(
2161                    callee.peel_parens().kind,
2162                    ExprKind::Type(_) | ExprKind::TypeCall(_)
2163                )
2164                && args.exprs().count() == 1 =>
2165        {
2166            concrete_gas_cap(hir, args.exprs().next()?, seen)
2167        }
2168        _ => None,
2169    }
2170}
2171
2172fn branch_stops_current_path(stmt: &hir::Stmt<'_>) -> bool {
2173    match &stmt.kind {
2174        StmtKind::Break | StmtKind::Continue => true,
2175        StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => {
2176            block.stmts.iter().any(branch_stops_current_path)
2177        }
2178        StmtKind::If(_, then_stmt, Some(else_stmt)) => {
2179            branch_stops_current_path(then_stmt) && branch_stops_current_path(else_stmt)
2180        }
2181        _ => branch_always_exits(stmt),
2182    }
2183}
2184
2185fn standard_reentrancy_guard_lock(
2186    hir: &hir::Hir<'_>,
2187    modifier: &hir::Function<'_>,
2188) -> Option<VariableId> {
2189    if !matches!(modifier.kind, hir::FunctionKind::Modifier) || !modifier.modifiers.is_empty() {
2190        return None;
2191    }
2192    let body = modifier.body?;
2193    if body.stmts.iter().map(count_modifier_placeholders).sum::<usize>() != 1 {
2194        return None;
2195    }
2196    let mut activation_stmts = Vec::new();
2197    collect_stmts_before_unconditional_placeholder(body.stmts, &mut activation_stmts)?;
2198    let mut seen = BTreeSet::new();
2199    let (lock_var, entered) = guard_activation_from_stmt_refs(hir, &activation_stmts, &mut seen)?;
2200    let placeholder_index = body.stmts.iter().position(contains_unconditional_placeholder)?;
2201    let mut seen = BTreeSet::new();
2202    let (restored_var, restored) =
2203        guard_restoration_from_stmt(hir, body.stmts.get(placeholder_index + 1)?, &mut seen)?;
2204    (lock_var == restored_var && entered != restored).then_some(lock_var)
2205}
2206
2207fn collect_stmts_before_unconditional_placeholder<'hir>(
2208    stmts: &'hir [hir::Stmt<'hir>],
2209    before: &mut Vec<&'hir hir::Stmt<'hir>>,
2210) -> Option<()> {
2211    for stmt in stmts {
2212        match stmt.kind {
2213            StmtKind::Placeholder => return Some(()),
2214            StmtKind::Block(block) | StmtKind::UncheckedBlock(block)
2215                if contains_unconditional_placeholder(stmt) =>
2216            {
2217                return collect_stmts_before_unconditional_placeholder(block.stmts, before);
2218            }
2219            _ => before.push(stmt),
2220        }
2221    }
2222    None
2223}
2224
2225fn contains_unconditional_placeholder(stmt: &hir::Stmt<'_>) -> bool {
2226    match stmt.kind {
2227        StmtKind::Placeholder => true,
2228        StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => {
2229            block.stmts.iter().any(contains_unconditional_placeholder)
2230        }
2231        _ => false,
2232    }
2233}
2234
2235fn count_modifier_placeholders(stmt: &hir::Stmt<'_>) -> usize {
2236    match stmt.kind {
2237        StmtKind::Placeholder => 1,
2238        StmtKind::Block(block) | StmtKind::UncheckedBlock(block) | StmtKind::Loop(block, _) => {
2239            block.stmts.iter().map(count_modifier_placeholders).sum()
2240        }
2241        StmtKind::If(_, then_stmt, else_stmt) => {
2242            count_modifier_placeholders(then_stmt)
2243                + else_stmt.map_or(0, count_modifier_placeholders)
2244        }
2245        StmtKind::Try(try_stmt) => try_stmt
2246            .clauses
2247            .iter()
2248            .flat_map(|clause| clause.block.stmts)
2249            .map(count_modifier_placeholders)
2250            .sum(),
2251        StmtKind::AssemblyBlock(_) | StmtKind::Switch(_) | StmtKind::Err(_) => 2,
2252        StmtKind::DeclSingle(_)
2253        | StmtKind::DeclMulti(_, _)
2254        | StmtKind::Emit(_)
2255        | StmtKind::Revert(_)
2256        | StmtKind::Return(_)
2257        | StmtKind::Break
2258        | StmtKind::Continue
2259        | StmtKind::Expr(_) => 0,
2260    }
2261}
2262
2263fn balance_reentry_lock<'hir>(
2264    gcx: Gcx<'hir>,
2265    hir: &'hir hir::Hir<'hir>,
2266    entry: &'hir hir::Function<'hir>,
2267) -> Option<VariableId> {
2268    let entry_id = hir.function_ids().find(|&id| std::ptr::eq(hir.function(id), entry))?;
2269    let defining_contract = entry.contract?;
2270    reentrancy_guard_locks(hir, entry).into_iter().find(|&lock_var| {
2271        let mut has_effective_deployment = false;
2272        for contract_id in hir.contract_ids() {
2273            let contract = hir.contract(contract_id);
2274            if !contract.can_be_deployed()
2275                || contract.is_abstract()
2276                || !contract.linearized_bases.contains(&defining_contract)
2277            {
2278                continue;
2279            }
2280
2281            let interface = gcx.interface_functions(contract_id);
2282            let entry_is_effective = interface.iter().any(|function| function.id == entry_id)
2283                || contract.fallback == Some(entry_id)
2284                || contract.receive == Some(entry_id);
2285            if !entry_is_effective {
2286                continue;
2287            }
2288            has_effective_deployment = true;
2289
2290            let ordinary_entries_are_guarded = interface.iter().all(|function| {
2291                let function = hir.function(function.id);
2292                matches!(function.state_mutability, StateMutability::Pure | StateMutability::View)
2293                    || function_has_reentrancy_guard(hir, function, lock_var)
2294            });
2295            let special_entries_are_guarded =
2296                [contract.fallback, contract.receive].into_iter().flatten().all(|function_id| {
2297                    function_has_reentrancy_guard(hir, hir.function(function_id), lock_var)
2298                });
2299            if !ordinary_entries_are_guarded || !special_entries_are_guarded {
2300                return false;
2301            }
2302        }
2303        has_effective_deployment
2304    })
2305}
2306
2307fn reentrancy_guard_locks(hir: &hir::Hir<'_>, function: &hir::Function<'_>) -> Vec<VariableId> {
2308    function
2309        .modifiers
2310        .iter()
2311        .filter(|modifier| modifier.args.exprs().next().is_none())
2312        .filter_map(|modifier| modifier.id.as_function())
2313        .filter_map(|modifier_id| standard_reentrancy_guard_lock(hir, hir.function(modifier_id)))
2314        .collect()
2315}
2316
2317fn function_has_reentrancy_guard(
2318    hir: &hir::Hir<'_>,
2319    function: &hir::Function<'_>,
2320    lock_var: VariableId,
2321) -> bool {
2322    reentrancy_guard_locks(hir, function).contains(&lock_var)
2323}
2324
2325fn guard_activation_from_stmts(
2326    hir: &hir::Hir<'_>,
2327    stmts: &[hir::Stmt<'_>],
2328    seen: &mut BTreeSet<FunctionId>,
2329) -> Option<(VariableId, LockValue)> {
2330    let stmts = stmts.iter().collect::<Vec<_>>();
2331    guard_activation_from_stmt_refs(hir, &stmts, seen)
2332}
2333
2334fn guard_activation_from_stmt_refs(
2335    hir: &hir::Hir<'_>,
2336    stmts: &[&hir::Stmt<'_>],
2337    seen: &mut BTreeSet<FunctionId>,
2338) -> Option<(VariableId, LockValue)> {
2339    let (activation, prefix) = stmts.split_last()?;
2340    if let Some((lock_var, entered)) = state_lock_assignment(hir, activation) {
2341        return prefix
2342            .iter()
2343            .any(|stmt| stmt_rejects_lock_value(hir, stmt, lock_var, entered))
2344            .then_some((lock_var, entered));
2345    }
2346
2347    let helper_id = simple_internal_call(activation)?;
2348    if !seen.insert(helper_id) {
2349        return None;
2350    }
2351    let helper = hir.function(helper_id);
2352    let result = if helper.modifiers.is_empty() {
2353        guard_activation_from_stmts(hir, helper.body?.stmts, seen)
2354    } else {
2355        None
2356    };
2357    seen.remove(&helper_id);
2358    result
2359}
2360
2361fn guard_restoration_from_stmt(
2362    hir: &hir::Hir<'_>,
2363    stmt: &hir::Stmt<'_>,
2364    seen: &mut BTreeSet<FunctionId>,
2365) -> Option<(VariableId, LockValue)> {
2366    if let Some(restoration) = state_lock_assignment(hir, stmt) {
2367        return Some(restoration);
2368    }
2369
2370    let helper_id = simple_internal_call(stmt)?;
2371    if !seen.insert(helper_id) {
2372        return None;
2373    }
2374    let helper = hir.function(helper_id);
2375    let body = helper.modifiers.is_empty().then_some(helper.body?)?;
2376    let result = match body.stmts {
2377        [stmt] => state_lock_assignment(hir, stmt),
2378        _ => None,
2379    };
2380    seen.remove(&helper_id);
2381    result
2382}
2383
2384fn simple_internal_call(stmt: &hir::Stmt<'_>) -> Option<FunctionId> {
2385    let StmtKind::Expr(expr) = stmt.kind else { return None };
2386    let ExprKind::Call(callee, args, opts) = &expr.peel_parens().kind else { return None };
2387    if opts.is_some() || args.exprs().next().is_some() {
2388        return None;
2389    }
2390    let ExprKind::Ident(reses) = &callee.peel_parens().kind else { return None };
2391    unique(reses.iter().filter_map(|res| match res {
2392        Res::Item(ItemId::Function(func_id)) => Some(*func_id),
2393        _ => None,
2394    }))
2395}
2396
2397fn state_lock_assignment(
2398    hir: &hir::Hir<'_>,
2399    stmt: &hir::Stmt<'_>,
2400) -> Option<(VariableId, LockValue)> {
2401    let StmtKind::Expr(expr) = stmt.kind else { return None };
2402    let ExprKind::Assign(lhs, None, rhs) = &expr.peel_parens().kind else { return None };
2403    let lock_var = direct_state_var(hir, lhs)?;
2404    Some((lock_var, constant_lock_value(hir, rhs)?))
2405}
2406
2407fn direct_state_var(hir: &hir::Hir<'_>, expr: &hir::Expr<'_>) -> Option<VariableId> {
2408    let ExprKind::Ident(reses) = &expr.peel_parens().kind else { return None };
2409    unique(reses.iter().filter_map(|res| match res {
2410        Res::Item(ItemId::Variable(var_id)) if hir.variable(*var_id).kind.is_state() => {
2411            Some(*var_id)
2412        }
2413        _ => None,
2414    }))
2415}
2416
2417fn stmt_rejects_lock_value(
2418    hir: &hir::Hir<'_>,
2419    stmt: &hir::Stmt<'_>,
2420    lock_var: VariableId,
2421    entered: LockValue,
2422) -> bool {
2423    match stmt.kind {
2424        StmtKind::Expr(expr) => {
2425            let ExprKind::Call(callee, args, _) = &expr.peel_parens().kind else { return false };
2426            is_require_or_assert(callee)
2427                && args.exprs().next().is_some_and(|condition| {
2428                    eval_lock_condition(hir, condition, lock_var, entered) == Some(false)
2429                })
2430        }
2431        StmtKind::If(condition, then_stmt, else_stmt) => {
2432            let condition = eval_lock_condition(hir, condition, lock_var, entered);
2433            (condition == Some(true) && branch_always_exits(then_stmt))
2434                || (condition == Some(false) && else_stmt.is_some_and(branch_always_exits))
2435        }
2436        _ => false,
2437    }
2438}
2439
2440fn constant_lock_value(hir: &hir::Hir<'_>, expr: &hir::Expr<'_>) -> Option<LockValue> {
2441    eval_lock_value_inner(hir, expr, None, None, &mut BTreeSet::new())
2442}
2443
2444fn eval_lock_condition(
2445    hir: &hir::Hir<'_>,
2446    expr: &hir::Expr<'_>,
2447    lock_var: VariableId,
2448    entered: LockValue,
2449) -> Option<bool> {
2450    match eval_lock_value_inner(hir, expr, Some(lock_var), Some(entered), &mut BTreeSet::new())? {
2451        LockValue::Bool(value) => Some(value),
2452        LockValue::Number(_) => None,
2453    }
2454}
2455
2456fn eval_lock_value_inner(
2457    hir: &hir::Hir<'_>,
2458    expr: &hir::Expr<'_>,
2459    lock_var: Option<VariableId>,
2460    entered: Option<LockValue>,
2461    seen: &mut BTreeSet<VariableId>,
2462) -> Option<LockValue> {
2463    match &expr.peel_parens().kind {
2464        ExprKind::Lit(lit) => match lit.kind {
2465            LitKind::Bool(value) => Some(LockValue::Bool(value)),
2466            LitKind::Number(value) => Some(LockValue::Number(value)),
2467            _ => None,
2468        },
2469        ExprKind::Ident(reses) => {
2470            let var_id = unique(reses.iter().filter_map(|res| match res {
2471                Res::Item(ItemId::Variable(var_id)) => Some(*var_id),
2472                _ => None,
2473            }))?;
2474            if Some(var_id) == lock_var {
2475                return entered;
2476            }
2477            let var = hir.variable(var_id);
2478            if !var.is_constant() || !seen.insert(var_id) {
2479                return None;
2480            }
2481            let value = eval_lock_value_inner(hir, var.initializer?, lock_var, entered, seen);
2482            seen.remove(&var_id);
2483            value
2484        }
2485        ExprKind::Unary(op, inner) if op.kind == UnOpKind::Not => {
2486            let LockValue::Bool(value) =
2487                eval_lock_value_inner(hir, inner, lock_var, entered, seen)?
2488            else {
2489                return None;
2490            };
2491            Some(LockValue::Bool(!value))
2492        }
2493        ExprKind::Binary(lhs, op, rhs) if matches!(op.kind, BinOpKind::Eq | BinOpKind::Ne) => {
2494            let lhs = eval_lock_value_inner(hir, lhs, lock_var, entered, seen)?;
2495            let rhs = eval_lock_value_inner(hir, rhs, lock_var, entered, seen)?;
2496            Some(LockValue::Bool(if op.kind == BinOpKind::Eq { lhs == rhs } else { lhs != rhs }))
2497        }
2498        _ => None,
2499    }
2500}
2501
2502fn call_uses_delegate_context(gcx: Gcx<'_>, callee: &hir::Expr<'_>) -> bool {
2503    if let ExprKind::Member(_, member) = &callee.peel_parens().kind
2504        && matches!(member.name, kw::Callcode | kw::Delegatecall)
2505    {
2506        return true;
2507    }
2508    gcx.type_of_expr(callee.peel_parens().id).is_some_and(
2509        |ty| matches!(ty.kind, TyKind::Fn(function) if function.kind == TyFnKind::DelegateCall),
2510    )
2511}
2512
2513fn self_address_paths(expr: &hir::Expr<'_>, state: &FlowState) -> PathAlternatives {
2514    match &expr.peel_parens().kind {
2515        ExprKind::Ident(reses) => {
2516            if is_this(expr) {
2517                return [state.path_predicates.clone()].into_iter().collect();
2518            }
2519            let mut paths = PathAlternatives::new();
2520            for var_id in reses.iter().filter_map(|res| match res {
2521                Res::Item(ItemId::Variable(var_id)) => Some(var_id),
2522                _ => None,
2523            }) {
2524                if let Some(local_paths) = state.self_address_local_paths.get(var_id) {
2525                    paths.extend(constrain_paths(local_paths, &state.path_predicates));
2526                }
2527            }
2528            paths
2529        }
2530        ExprKind::Payable(inner) => self_address_paths(inner, state),
2531        ExprKind::Call(callee, args, opts)
2532            if opts.is_none() && is_address_type_expr(callee) && args.exprs().count() == 1 =>
2533        {
2534            args.exprs().next().map(|arg| self_address_paths(arg, state)).unwrap_or_default()
2535        }
2536        _ if is_this(expr) => [state.path_predicates.clone()].into_iter().collect(),
2537        _ => PathAlternatives::new(),
2538    }
2539}
2540
2541fn is_uncapped_value_call(
2542    hir: &hir::Hir<'_>,
2543    callee: &hir::Expr<'_>,
2544    opts: Option<&hir::CallOptions<'_>>,
2545) -> bool {
2546    let Some(opts) = opts else { return false };
2547    let ExprKind::Member(_, member) = &callee.peel_parens().kind else { return false };
2548    if member.name != kw::Call {
2549        return false;
2550    }
2551
2552    let mut value = None;
2553    let mut gas = None;
2554    for opt in opts.args {
2555        if opt.name.name == sym::value {
2556            value = Some(&opt.value);
2557        } else if opt.name.name == kw::Gas {
2558            gas = Some(&opt.value);
2559        }
2560    }
2561
2562    value.is_some_and(|value| !is_zero_value(hir, value)) && gas.is_none_or(gas_option_forwards_all)
2563}
2564
2565fn is_no_eth_reentrant_call<'hir>(
2566    gcx: Gcx<'hir>,
2567    hir: &'hir hir::Hir<'hir>,
2568    callee: &'hir hir::Expr<'hir>,
2569    _args: &CallArgs<'hir>,
2570    opts: Option<&hir::CallOptions<'hir>>,
2571) -> bool {
2572    if call_sends_eth(hir, opts) {
2573        return false;
2574    }
2575
2576    match &callee.peel_parens().kind {
2577        ExprKind::Member(receiver, _) if is_contract_receiver(gcx, receiver) => {
2578            external_call_can_reenter(gcx, callee)
2579        }
2580        ExprKind::Member(receiver, member)
2581            if is_address_like(gcx, hir, receiver)
2582                && matches!(
2583                    member.name,
2584                    kw::Call | kw::Callcode | kw::Delegatecall | kw::Staticcall
2585                ) =>
2586        {
2587            member.name != kw::Staticcall
2588        }
2589        ExprKind::Member(receiver, _) if is_super(receiver) => false,
2590        _ => external_call_can_reenter(gcx, callee),
2591    }
2592}
2593
2594fn call_sends_eth(hir: &hir::Hir<'_>, opts: Option<&hir::CallOptions<'_>>) -> bool {
2595    opts.is_some_and(|opts| {
2596        opts.args.iter().any(|opt| opt.name.name == sym::value && !is_zero_value(hir, &opt.value))
2597    })
2598}
2599
2600fn external_call_can_reenter<'hir>(gcx: Gcx<'hir>, callee: &'hir hir::Expr<'hir>) -> bool {
2601    let Some(ty) = gcx.type_of_expr(callee.peel_parens().id) else { return false };
2602    let TyKind::Fn(function) = ty.kind else { return false };
2603    is_externally_callable_fn_kind(function.kind)
2604        && !matches!(function.state_mutability, StateMutability::Pure | StateMutability::View)
2605}
2606
2607const fn is_externally_callable_fn_kind(kind: TyFnKind) -> bool {
2608    matches!(kind, TyFnKind::External | TyFnKind::Declaration | TyFnKind::DelegateCall)
2609}
2610
2611fn argument_for_parameter<'hir>(
2612    hir: &hir::Hir<'hir>,
2613    args: &CallArgs<'hir>,
2614    params: &[VariableId],
2615    index: usize,
2616) -> Option<&'hir hir::Expr<'hir>> {
2617    match args.kind {
2618        CallArgsKind::Unnamed(exprs) => exprs.get(index),
2619        CallArgsKind::Named(named_args) => {
2620            let name = hir.variable(*params.get(index)?).name?;
2621            named_args.iter().find(|arg| arg.name.name == name.name).map(|arg| &arg.value)
2622        }
2623    }
2624}
2625
2626fn is_contract_receiver<'hir>(gcx: Gcx<'hir>, expr: &'hir hir::Expr<'hir>) -> bool {
2627    gcx.type_of_expr(expr.peel_parens().id)
2628        .is_some_and(|ty| matches!(ty.peel_refs().kind, TyKind::Contract(_)))
2629}
2630
2631fn is_address_like<'hir>(
2632    gcx: Gcx<'hir>,
2633    hir: &'hir hir::Hir<'hir>,
2634    expr: &'hir hir::Expr<'hir>,
2635) -> bool {
2636    match &expr.peel_parens().kind {
2637        ExprKind::Payable(_) => true,
2638        ExprKind::Call(callee, _, _) if is_address_type_expr(callee) => true,
2639        _ => expr_ty(gcx, hir, expr).is_some_and(type_is_address_like),
2640    }
2641}
2642
2643fn is_address_type_expr(expr: &hir::Expr<'_>) -> bool {
2644    matches!(
2645        &expr.peel_parens().kind,
2646        ExprKind::Type(hir::Type {
2647            kind: hir::TypeKind::Elementary(ElementaryType::Address(_)),
2648            ..
2649        })
2650    )
2651}
2652
2653fn type_is_address_like(ty: Ty<'_>) -> bool {
2654    matches!(ty.peel_refs().kind, TyKind::Elementary(ElementaryType::Address(_)))
2655}
2656
2657fn expr_ty<'hir>(
2658    gcx: Gcx<'hir>,
2659    hir: &'hir hir::Hir<'hir>,
2660    expr: &'hir hir::Expr<'hir>,
2661) -> Option<Ty<'hir>> {
2662    match &expr.peel_parens().kind {
2663        ExprKind::Array(_) | ExprKind::YulMember(..) => None,
2664        ExprKind::Call(callee, args, _) => {
2665            let callee_ty = expr_ty(gcx, hir, callee)?;
2666            match callee_ty.kind {
2667                TyKind::Fn(func) => fn_call_return_type(gcx, func.returns),
2668                TyKind::Type(to) => Some(explicit_cast_ty(gcx, to, args)),
2669                _ => None,
2670            }
2671        }
2672        ExprKind::Ident(reses) => {
2673            let res = unique(reses.iter().filter(|res| !matches!(res, Res::Err(_))).copied())?;
2674            match res {
2675                Res::Builtin(builtin) if matches!(builtin.name(), sym::this | sym::super_) => None,
2676                Res::Item(ItemId::Variable(var_id)) => Some(
2677                    gcx.type_of_res(res)
2678                        .with_loc_if_ref_opt(gcx, variable_data_location(hir, var_id)),
2679                ),
2680                _ => Some(gcx.type_of_res(res)),
2681            }
2682        }
2683        ExprKind::Index(lhs, index) => {
2684            let lhs_ty = expr_ty(gcx, hir, lhs)?;
2685            if let Some(index) = index
2686                && !expr_ty(gcx, hir, index)?.convert_implicit_to(gcx.types.uint(256), gcx)
2687            {
2688                return None;
2689            }
2690            index_ty(gcx, lhs_ty)
2691        }
2692        ExprKind::Lit(lit) => Some(match &lit.kind {
2693            LitKind::Str(StrKind::Hex, s, _) => {
2694                let size = TypeSize::try_new_fb_bytes(s.as_byte_str().len().min(32) as u8)?;
2695                gcx.types.fixed_bytes(size.bytes())
2696            }
2697            LitKind::Str(_, s, _) => gcx.mk_ty_string_literal(s.as_byte_str()),
2698            LitKind::Number(int) => gcx.mk_ty_int_literal(false, int.bit_len() as _)?,
2699            LitKind::Rational(_) | LitKind::Err(_) => return None,
2700            LitKind::Address(_) => gcx.types.address,
2701            LitKind::Bool(_) => gcx.types.bool,
2702        }),
2703        ExprKind::Member(base, member) => member_ty(gcx, hir, base, member.name),
2704        ExprKind::New(ty) => {
2705            let ty = gcx.type_of_hir_ty(ty);
2706            Some(gcx.mk_ty(TyKind::Type(ty)))
2707        }
2708        ExprKind::Payable(inner) => {
2709            let inner_ty = expr_ty(gcx, hir, inner)?;
2710            inner_ty
2711                .convert_explicit_to(gcx.types.address_payable, gcx)
2712                .then_some(gcx.types.address_payable)
2713        }
2714        ExprKind::Slice(lhs, ..) => {
2715            let lhs_ty = expr_ty(gcx, hir, lhs)?;
2716            lhs_ty.is_sliceable().then_some(gcx.mk_ty(TyKind::Slice(lhs_ty)))
2717        }
2718        ExprKind::Tuple(exprs) => {
2719            let tys = exprs
2720                .iter()
2721                .map(|expr| expr.and_then(|expr| expr_ty(gcx, hir, expr)))
2722                .collect::<Option<Vec<_>>>()?;
2723            Some(gcx.mk_ty_tuple(gcx.mk_tys(&tys)))
2724        }
2725        ExprKind::Ternary(_, true_expr, false_expr) => {
2726            let true_ty = expr_ty(gcx, hir, true_expr)?;
2727            let false_ty = expr_ty(gcx, hir, false_expr)?;
2728            common_ty(gcx, true_ty, false_ty)
2729        }
2730        ExprKind::Type(ty) | ExprKind::TypeCall(ty) => {
2731            let ty = gcx.type_of_hir_ty(ty);
2732            Some(gcx.mk_ty(TyKind::Type(ty)))
2733        }
2734        ExprKind::Unary(op, inner) => match op.kind {
2735            UnOpKind::Not => Some(gcx.types.bool),
2736            _ => expr_ty(gcx, hir, inner),
2737        },
2738        ExprKind::Binary(_, op, _) if binary_op_returns_bool(op.kind) => Some(gcx.types.bool),
2739        ExprKind::Assign(..) | ExprKind::Binary(..) | ExprKind::Delete(..) | ExprKind::Err(_) => {
2740            None
2741        }
2742    }
2743}
2744
2745const fn binary_op_returns_bool(op: BinOpKind) -> bool {
2746    matches!(
2747        op,
2748        BinOpKind::Lt
2749            | BinOpKind::Le
2750            | BinOpKind::Gt
2751            | BinOpKind::Ge
2752            | BinOpKind::Eq
2753            | BinOpKind::Ne
2754            | BinOpKind::And
2755            | BinOpKind::Or
2756    )
2757}
2758
2759fn member_ty<'hir>(
2760    gcx: Gcx<'hir>,
2761    hir: &'hir hir::Hir<'hir>,
2762    base: &'hir hir::Expr<'hir>,
2763    member_name: solar::interface::Symbol,
2764) -> Option<Ty<'hir>> {
2765    if is_this(base) || is_super(base) {
2766        return None;
2767    }
2768
2769    let base_ty = expr_ty(gcx, hir, base)?;
2770    unique(
2771        gcx.members_of(base_ty, base_item_source(hir, base), base_contract(hir, base))
2772            .filter(|member| member.name == member_name)
2773            .map(|member| member.ty),
2774    )
2775}
2776
2777fn common_ty<'hir>(gcx: Gcx<'hir>, lhs: Ty<'hir>, rhs: Ty<'hir>) -> Option<Ty<'hir>> {
2778    if lhs.convert_implicit_to(rhs, gcx) {
2779        Some(rhs)
2780    } else {
2781        rhs.convert_implicit_to(lhs, gcx).then_some(lhs)
2782    }
2783}
2784
2785fn fn_call_return_type<'hir>(gcx: Gcx<'hir>, returns: &'hir [Ty<'hir>]) -> Option<Ty<'hir>> {
2786    Some(match returns {
2787        [] => gcx.types.unit,
2788        [ret] => *ret,
2789        _ => gcx.mk_ty_tuple(returns),
2790    })
2791}
2792
2793fn explicit_cast_ty<'hir>(gcx: Gcx<'hir>, to: Ty<'hir>, args: &'hir CallArgs<'hir>) -> Ty<'hir> {
2794    match args.exprs().next().and_then(|arg| expr_ty(gcx, &gcx.hir, arg)) {
2795        Some(from) => from.try_convert_explicit_to(to, gcx).unwrap_or(to),
2796        None => to,
2797    }
2798}
2799
2800fn index_ty<'hir>(gcx: Gcx<'hir>, base_ty: Ty<'hir>) -> Option<Ty<'hir>> {
2801    let loc = indexed_base_data_location(base_ty);
2802    match base_ty.peel_refs().kind {
2803        TyKind::Mapping(_, value) => Some(value.with_loc_if_ref_opt(gcx, loc)),
2804        _ => base_ty.base_type(gcx),
2805    }
2806}
2807
2808fn indexed_base_data_location(ty: Ty<'_>) -> Option<DataLocation> {
2809    ty.loc().or_else(|| matches!(ty.kind, TyKind::Mapping(..)).then_some(DataLocation::Storage))
2810}
2811
2812fn base_item_source(hir: &hir::Hir<'_>, expr: &hir::Expr<'_>) -> hir::SourceId {
2813    referenced_item(expr)
2814        .map(|id| hir.item(id).source())
2815        .unwrap_or_else(|| hir.sources_enumerated().next().expect("HIR has a source").0)
2816}
2817
2818fn base_contract(hir: &hir::Hir<'_>, expr: &hir::Expr<'_>) -> Option<hir::ContractId> {
2819    referenced_item(expr).and_then(|id| hir.item(id).contract())
2820}
2821
2822fn referenced_item(expr: &hir::Expr<'_>) -> Option<ItemId> {
2823    match &expr.peel_parens().kind {
2824        ExprKind::Ident([Res::Item(id), ..]) => Some(*id),
2825        _ => None,
2826    }
2827}
2828
2829fn variable_data_location(hir: &hir::Hir<'_>, var_id: VariableId) -> Option<DataLocation> {
2830    let var = hir.variable(var_id);
2831    var.data_location.or_else(|| var.kind.is_state().then_some(DataLocation::Storage))
2832}
2833
2834fn is_this(expr: &hir::Expr<'_>) -> bool {
2835    matches!(
2836        &expr.peel_parens().kind,
2837        ExprKind::Ident(reses)
2838            if reses.iter().any(|res| {
2839                matches!(res, Res::Builtin(builtin) if builtin.name() == sym::this)
2840            })
2841    )
2842}
2843
2844fn is_super(expr: &hir::Expr<'_>) -> bool {
2845    matches!(
2846        &expr.peel_parens().kind,
2847        ExprKind::Ident(reses)
2848            if reses.iter().any(|res| {
2849                matches!(res, Res::Builtin(builtin) if builtin.name() == sym::super_)
2850            })
2851    )
2852}
2853
2854fn unique<T>(mut iter: impl Iterator<Item = T>) -> Option<T> {
2855    let first = iter.next()?;
2856    iter.next().is_none().then_some(first)
2857}
2858
2859fn is_zero_value(hir: &hir::Hir<'_>, expr: &hir::Expr<'_>) -> bool {
2860    let mut seen = BTreeSet::new();
2861    is_zero_value_inner(hir, expr, &mut seen)
2862}
2863
2864fn is_zero_value_inner(
2865    hir: &hir::Hir<'_>,
2866    expr: &hir::Expr<'_>,
2867    seen: &mut BTreeSet<VariableId>,
2868) -> bool {
2869    match &expr.peel_parens().kind {
2870        ExprKind::Lit(lit) => matches!(lit.kind, LitKind::Number(value) if value.is_zero()),
2871        ExprKind::Ident(reses) => {
2872            let mut saw_variable = false;
2873            reses.iter().all(|res| match res {
2874                Res::Item(ItemId::Variable(var_id)) => {
2875                    saw_variable = true;
2876                    constant_var_is_zero(hir, *var_id, seen)
2877                }
2878                _ => false,
2879            }) && saw_variable
2880        }
2881        ExprKind::Call(callee, args, opts)
2882            if opts.is_none()
2883                && matches!(callee.peel_parens().kind, ExprKind::Type(_))
2884                && args.exprs().count() == 1 =>
2885        {
2886            args.exprs().next().is_some_and(|arg| is_zero_value_inner(hir, arg, seen))
2887        }
2888        _ => false,
2889    }
2890}
2891
2892fn constant_var_is_zero(
2893    hir: &hir::Hir<'_>,
2894    var_id: VariableId,
2895    seen: &mut BTreeSet<VariableId>,
2896) -> bool {
2897    let var = hir.variable(var_id);
2898    if !var.is_constant() || !seen.insert(var_id) {
2899        return false;
2900    }
2901    var.initializer.is_some_and(|init| is_zero_value_inner(hir, init, seen))
2902}
2903
2904fn gas_option_forwards_all(expr: &hir::Expr<'_>) -> bool {
2905    let ExprKind::Call(callee, args, opts) = &expr.peel_parens().kind else {
2906        return false;
2907    };
2908    if opts.is_some() || args.exprs().next().is_some() {
2909        return false;
2910    }
2911    matches!(
2912        &callee.peel_parens().kind,
2913        ExprKind::Ident(reses)
2914            if reses.iter().any(|res| {
2915                matches!(res, Res::Builtin(builtin) if builtin.name() == sym::gasleft)
2916            })
2917    )
2918}
2919
2920fn lhs_local_var(hir: &hir::Hir<'_>, lhs: &hir::Expr<'_>) -> Option<VariableId> {
2921    if let ExprKind::Ident(reses) = &lhs.peel_parens().kind {
2922        for res in *reses {
2923            if let Res::Item(ItemId::Variable(var_id)) = res
2924                && !hir.variable(*var_id).kind.is_state()
2925            {
2926                return Some(*var_id);
2927            }
2928        }
2929    }
2930    None
2931}
2932
2933fn local_write_lhs_vars(hir: &hir::Hir<'_>, expr: &hir::Expr<'_>) -> Vec<VariableId> {
2934    let mut vars = Vec::new();
2935    collect_local_write_lhs_vars(hir, expr, &mut vars);
2936    vars
2937}
2938
2939fn collect_local_write_lhs_vars(
2940    hir: &hir::Hir<'_>,
2941    expr: &hir::Expr<'_>,
2942    vars: &mut Vec<VariableId>,
2943) {
2944    match &expr.kind {
2945        ExprKind::Ident(reses) => {
2946            for &res in *reses {
2947                if let Res::Item(ItemId::Variable(var_id)) = res
2948                    && !hir.variable(var_id).kind.is_state()
2949                {
2950                    push_unique(vars, var_id);
2951                }
2952            }
2953        }
2954        ExprKind::Tuple(exprs) => {
2955            for expr in exprs.iter().copied().flatten() {
2956                collect_local_write_lhs_vars(hir, expr, vars);
2957            }
2958        }
2959        _ => {}
2960    }
2961}
2962
2963fn forget_path_predicates(state: &mut FlowState, vars: impl IntoIterator<Item = VariableId>) {
2964    for var_id in vars {
2965        state.path_predicates.retain(|predicate, _| !predicate.contains(var_id));
2966        for paths in state.balance_local_paths.values_mut() {
2967            *paths = paths
2968                .iter()
2969                .map(|path| {
2970                    let mut path = path.clone();
2971                    path.retain(|predicate, _| !predicate.contains(var_id));
2972                    path
2973                })
2974                .collect();
2975        }
2976        for paths in state.self_address_local_paths.values_mut() {
2977            *paths = paths
2978                .iter()
2979                .map(|path| {
2980                    let mut path = path.clone();
2981                    path.retain(|predicate, _| !predicate.contains(var_id));
2982                    path
2983                })
2984                .collect();
2985        }
2986        for call in &mut state.pending_balance_calls {
2987            call.paths = call
2988                .paths
2989                .iter()
2990                .map(|path| {
2991                    let mut path = path.clone();
2992                    path.retain(|predicate, _| !predicate.contains(var_id));
2993                    path
2994                })
2995                .collect();
2996        }
2997    }
2998}
2999
3000fn state_write_lhs_vars(hir: &hir::Hir<'_>, expr: &hir::Expr<'_>) -> Vec<VariableId> {
3001    let mut vars = Vec::new();
3002    collect_state_write_lhs_vars(hir, expr, &mut vars);
3003    vars
3004}
3005
3006fn collect_state_write_lhs_vars(
3007    hir: &hir::Hir<'_>,
3008    expr: &hir::Expr<'_>,
3009    vars: &mut Vec<VariableId>,
3010) {
3011    match &expr.kind {
3012        ExprKind::Ident(reses) => {
3013            for &res in *reses {
3014                if let Res::Item(ItemId::Variable(var_id)) = res
3015                    && hir.variable(var_id).kind.is_state()
3016                {
3017                    push_unique(vars, var_id);
3018                }
3019            }
3020        }
3021        ExprKind::Index(base, _) | ExprKind::Slice(base, ..) => {
3022            collect_state_write_lhs_vars(hir, base, vars);
3023        }
3024        ExprKind::Member(base, _)
3025        | ExprKind::Payable(base)
3026        | ExprKind::Unary(_, base)
3027        | ExprKind::Delete(base) => collect_state_write_lhs_vars(hir, base, vars),
3028        ExprKind::Tuple(exprs) => {
3029            for expr in exprs.iter().copied().flatten() {
3030                collect_state_write_lhs_vars(hir, expr, vars);
3031            }
3032        }
3033        _ => {}
3034    }
3035}
3036
3037fn push_unique<T: Copy + Eq>(items: &mut Vec<T>, item: T) {
3038    if !items.contains(&item) {
3039        items.push(item);
3040    }
3041}
3042
3043fn extend_unique<T: Copy + Eq>(items: &mut Vec<T>, values: impl IntoIterator<Item = T>) {
3044    for value in values {
3045        push_unique(items, value);
3046    }
3047}