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