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 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 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 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, ¶meter_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 ¤t_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 ¤t_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, ¶m)| {
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 (¶meter, &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}