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