1use super::{ReentrancyEvents, calls_loop::is_state_mutating_external_call};
2use crate::{
3 linter::{LateLintPass, LintContext},
4 sol::{
5 Severity, SolLint,
6 analysis::{
7 DEFAULT_HELPER_ANALYSIS_CACHE_LIMIT, HelperAnalysisCache, dispatched_function,
8 for_each_child, is_exit_call, loop_stmts, loop_update,
9 },
10 },
11};
12use solar::{
13 interface::Span,
14 sema::{
15 Gcx,
16 hir::{
17 self, BinOpKind, Block, ContractId, Expr, ExprKind, Function, FunctionId, LoopSource,
18 Stmt, StmtKind,
19 },
20 },
21};
22use std::collections::{HashMap, HashSet};
23
24declare_forge_lint!(
25 REENTRANCY_EVENTS,
26 Severity::Low,
27 "reentrancy-events",
28 "event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on"
29);
30
31impl<'gcx> LateLintPass<'gcx> for ReentrancyEvents {
32 fn check_function(&mut self, ctx: &LintContext, gcx: Gcx<'gcx>, func: &'gcx Function<'gcx>) {
33 let Some(body) = func.body else { return };
34 Analyzer::new(ctx, gcx, func.contract).analyze_callable(func, body, false);
35 }
36}
37
38type Placeholder<'gcx> = Option<(&'gcx [hir::Modifier<'gcx>], usize, Block<'gcx>)>;
39
40#[derive(Clone, Copy, Debug, Default)]
44struct Exits {
45 fallthrough: Option<bool>,
46 return_: Option<bool>,
47 break_: Option<bool>,
48 continue_: Option<bool>,
49}
50
51impl Exits {
52 fn fallthrough(tainted: bool) -> Self {
53 Self { fallthrough: Some(tainted), ..Self::default() }
54 }
55
56 fn return_(tainted: bool) -> Self {
57 Self { return_: Some(tainted), ..Self::default() }
58 }
59
60 fn break_(tainted: bool) -> Self {
61 Self { break_: Some(tainted), ..Self::default() }
62 }
63
64 fn continue_(tainted: bool) -> Self {
65 Self { continue_: Some(tainted), ..Self::default() }
66 }
67
68 fn merge(&mut self, other: Self) {
69 self.fallthrough = join(self.fallthrough, other.fallthrough);
70 self.return_ = join(self.return_, other.return_);
71 self.break_ = join(self.break_, other.break_);
72 self.continue_ = join(self.continue_, other.continue_);
73 }
74
75 fn normal(self) -> Option<bool> {
77 join(self.fallthrough, self.return_)
78 }
79}
80
81fn join(lhs: Option<bool>, rhs: Option<bool>) -> Option<bool> {
83 match (lhs, rhs) {
84 (Some(lhs), Some(rhs)) => Some(lhs || rhs),
85 _ => lhs.or(rhs),
86 }
87}
88
89#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
90struct InlineCallKey {
91 func_id: FunctionId,
92 external_call_seen: bool,
93 suppress_inline_reports: bool,
94}
95
96struct Analyzer<'ctx, 's, 'c, 'gcx> {
97 ctx: &'ctx LintContext<'s, 'c>,
98 gcx: Gcx<'gcx>,
99 enclosing_contract: Option<ContractId>,
102 call_stack: Vec<FunctionId>,
103 inline_cache: HelperAnalysisCache<InlineCallKey, Exits>,
104 external_call_reachability: HashMap<FunctionId, bool>,
106 reachability_cut: bool,
108 emitted: HashSet<Span>,
109 suppress_inline_reports: bool,
111 expr_aborted: bool,
113}
114
115impl<'ctx, 's, 'c, 'gcx> Analyzer<'ctx, 's, 'c, 'gcx> {
116 fn new(
117 ctx: &'ctx LintContext<'s, 'c>,
118 gcx: Gcx<'gcx>,
119 enclosing_contract: Option<ContractId>,
120 ) -> Self {
121 Self {
122 ctx,
123 gcx,
124 enclosing_contract,
125 call_stack: Vec::new(),
126 inline_cache: HelperAnalysisCache::new(DEFAULT_HELPER_ANALYSIS_CACHE_LIMIT),
127 external_call_reachability: HashMap::new(),
128 reachability_cut: false,
129 emitted: HashSet::new(),
130 suppress_inline_reports: false,
131 expr_aborted: false,
132 }
133 }
134
135 fn analyze_callable(
136 &mut self,
137 func: &'gcx Function<'gcx>,
138 body: Block<'gcx>,
139 entry: bool,
140 ) -> Exits {
141 self.analyze_modifier_chain(func.modifiers, 0, body, entry)
142 }
143
144 fn analyze_modifier_chain(
145 &mut self,
146 modifiers: &'gcx [hir::Modifier<'gcx>],
147 index: usize,
148 body: Block<'gcx>,
149 mut entry: bool,
150 ) -> Exits {
151 let Some(modifier) = modifiers.get(index) else {
152 return self.analyze_block(body, None, entry);
153 };
154 for arg in modifier.args.exprs() {
155 self.expr_aborted = false;
156 self.analyze_expr(arg, &mut entry);
157 if self.expr_aborted {
159 return Exits::default();
160 }
161 }
162 let Some((modifier_id, modifier_body)) = self
166 .enclosing_contract
167 .map_or_else(
168 || modifier.id.as_function(),
169 |contract| self.gcx.resolve_modifier_target(contract, modifier),
170 )
171 .and_then(|id| Some((id, self.gcx.hir.function(id).body?)))
172 else {
173 return self.analyze_modifier_chain(modifiers, index + 1, body, entry);
174 };
175 self.call_stack.push(modifier_id);
176 let exits = self.analyze_block(modifier_body, Some((modifiers, index + 1, body)), entry);
177 self.call_stack.pop();
178 exits
179 }
180
181 fn analyze_iteration(
183 &mut self,
184 block: Block<'gcx>,
185 source: LoopSource<'gcx>,
186 placeholder: Placeholder<'gcx>,
187 entry: bool,
188 ) -> Exits {
189 let mut exits = self.analyze_block(block, placeholder, entry);
190 if let (Some(update), Some(state)) = (loop_update(source), exits.fallthrough.take()) {
191 exits.merge(self.analyze_stmt(update, placeholder, state));
192 }
193 exits
194 }
195
196 fn analyze_block(
197 &mut self,
198 block: Block<'gcx>,
199 placeholder: Placeholder<'gcx>,
200 mut entry: bool,
201 ) -> Exits {
202 let mut exits = Exits::default();
203 for stmt in block.stmts {
204 let stmt_exits = self.analyze_stmt(stmt, placeholder, entry);
205 exits.merge(Exits { fallthrough: None, ..stmt_exits });
206 let Some(next) = stmt_exits.fallthrough else { return exits };
208 entry = next;
209 }
210 exits.fallthrough = Some(entry);
211 exits
212 }
213
214 fn analyze_stmt(
215 &mut self,
216 stmt: &'gcx Stmt<'gcx>,
217 placeholder: Placeholder<'gcx>,
218 mut entry: bool,
219 ) -> Exits {
220 self.expr_aborted = false;
221 match stmt.kind {
222 StmtKind::DeclSingle(var_id) => {
223 if let Some(init) = self.gcx.hir.variable(var_id).initializer {
224 self.analyze_expr(init, &mut entry);
225 }
226 self.unless_aborted(Exits::fallthrough(entry))
227 }
228 StmtKind::DeclMulti(_, expr) | StmtKind::Expr(expr) => {
229 self.analyze_expr(expr, &mut entry);
230 if is_exit_call(self.gcx, expr) {
231 return Exits::default();
232 }
233 self.unless_aborted(Exits::fallthrough(entry))
234 }
235 StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => {
236 self.analyze_block(block, placeholder, entry)
237 }
238 StmtKind::Emit(expr) => {
239 self.analyze_expr(expr, &mut entry);
242 if self.expr_aborted {
243 return Exits::default();
244 }
245 if entry && !self.suppress_inline_reports && self.emitted.insert(stmt.span) {
246 self.ctx.emit(&REENTRANCY_EVENTS, stmt.span);
247 }
248 Exits::fallthrough(entry)
249 }
250 StmtKind::Revert(expr) => {
251 self.analyze_expr(expr, &mut entry);
252 Exits::default()
253 }
254 StmtKind::Return(expr) => {
255 if let Some(expr) = expr {
256 self.analyze_expr(expr, &mut entry);
257 }
258 self.unless_aborted(Exits::return_(entry))
259 }
260 StmtKind::Break => Exits::break_(entry),
261 StmtKind::Continue => Exits::continue_(entry),
262 StmtKind::Loop(block, source) => {
263 let first = self.analyze_iteration(block, source, placeholder, entry);
267 let back_edge =
268 entry || first.fallthrough.unwrap_or(false) || first.continue_.unwrap_or(false);
269 let body = if back_edge == entry {
270 first
271 } else {
272 self.analyze_iteration(block, source, placeholder, back_edge)
273 };
274 let post = entry
276 || body.fallthrough.unwrap_or(false)
277 || body.break_.unwrap_or(false)
278 || body.continue_.unwrap_or(false);
279 Exits { fallthrough: Some(post), return_: body.return_, ..Exits::default() }
280 }
281 StmtKind::If(cond, then_stmt, else_stmt) => {
282 self.analyze_expr(cond, &mut entry);
283 if self.expr_aborted {
284 return Exits::default();
285 }
286 let mut exits = self.analyze_stmt(then_stmt, placeholder, entry);
287 exits.merge(match else_stmt {
288 Some(else_stmt) => self.analyze_stmt(else_stmt, placeholder, entry),
289 None => Exits::fallthrough(entry),
290 });
291 exits
292 }
293 StmtKind::Try(try_stmt) => {
294 self.analyze_expr(&try_stmt.expr, &mut entry);
295 if self.expr_aborted {
296 return Exits::default();
297 }
298 let mut exits = Exits::default();
299 for clause in try_stmt.clauses {
300 exits.merge(self.analyze_block(clause.block, placeholder, entry));
301 }
302 exits
303 }
304 StmtKind::Placeholder => match placeholder {
305 Some((modifiers, index, body)) => {
306 self.analyze_modifier_chain(modifiers, index, body, entry)
307 }
308 None => Exits::fallthrough(entry),
309 },
310 StmtKind::AssemblyBlock(_) | StmtKind::Switch(_) | StmtKind::Err(_) => {
312 Exits::fallthrough(true)
313 }
314 }
315 }
316
317 fn unless_aborted(&self, exits: Exits) -> Exits {
318 if self.expr_aborted { Exits::default() } else { exits }
319 }
320
321 fn analyze_expr(&mut self, expr: &'gcx Expr<'gcx>, tainted: &mut bool) {
322 match &expr.kind {
323 ExprKind::Call(callee, ..) => {
324 for_each_child(expr, &mut |child| self.analyze_expr(child, tainted));
325 if is_state_mutating_external_call(self.gcx, callee) {
326 *tainted = true;
327 }
328 for func_id in self.callees(callee) {
331 self.analyze_internal_call(func_id, tainted);
332 }
333 }
334 ExprKind::Binary(lhs, op, rhs) if matches!(op.kind, BinOpKind::And | BinOpKind::Or) => {
335 self.analyze_expr(lhs, tainted);
339 let lhs_aborted = std::mem::replace(&mut self.expr_aborted, false);
340 let mut rhs_tainted = *tainted;
341 self.analyze_expr(rhs, &mut rhs_tainted);
342 let rhs_aborted = self.expr_aborted;
343 self.expr_aborted = lhs_aborted;
344 if !lhs_aborted && !rhs_aborted {
345 *tainted |= rhs_tainted;
346 }
347 }
348 ExprKind::Ternary(cond, then_expr, else_expr) => {
349 self.analyze_expr(cond, tainted);
350 let cond_aborted = std::mem::replace(&mut self.expr_aborted, false);
351 let mut then_tainted = *tainted;
352 self.analyze_expr(then_expr, &mut then_tainted);
353 let then_aborted = std::mem::replace(&mut self.expr_aborted, false);
354 let mut else_tainted = *tainted;
355 self.analyze_expr(else_expr, &mut else_tainted);
356 let else_aborted = self.expr_aborted;
357 self.expr_aborted = cond_aborted || (then_aborted && else_aborted);
360 if !(then_aborted && else_aborted) {
361 *tainted = (!then_aborted && then_tainted) || (!else_aborted && else_tainted);
362 }
363 }
364 _ => for_each_child(expr, &mut |child| self.analyze_expr(child, tainted)),
365 }
366 }
367
368 fn callees(&self, callee: &'gcx Expr<'gcx>) -> Vec<FunctionId> {
370 self.enclosing_contract
371 .map_or_else(
372 || self.gcx.resolved_function(callee),
373 |contract| dispatched_function(self.gcx, contract, callee),
374 )
375 .into_iter()
376 .collect()
377 }
378
379 fn analyze_internal_call(&mut self, func_id: FunctionId, tainted: &mut bool) {
380 if self.call_stack.contains(&func_id) {
381 *tainted |= self.helper_may_reach_external_call(func_id, &mut HashSet::new());
384 return;
385 }
386 let func = self.gcx.hir.function(func_id);
387 let Some(body) = func.body else { return };
388
389 let suppress = self.suppress_inline_reports || !*tainted;
392 let key = InlineCallKey {
393 func_id,
394 external_call_seen: *tainted,
395 suppress_inline_reports: suppress,
396 };
397 if self.inline_cache.is_in_progress(&key) {
398 return;
399 }
400 let summary = match self.inline_cache.get(&key) {
401 Some(summary) => *summary,
402 None => {
403 let prev_suppress = std::mem::replace(&mut self.suppress_inline_reports, suppress);
404 self.inline_cache.start(key);
405 self.call_stack.push(func_id);
406 let summary = self.analyze_callable(func, body, *tainted);
407 self.call_stack.pop();
408 self.inline_cache.finish(key, summary);
409 self.suppress_inline_reports = prev_suppress;
410 summary
411 }
412 };
413 match summary.normal() {
416 Some(after) => *tainted = after,
417 None => self.expr_aborted = true,
418 }
419 }
420
421 fn helper_may_reach_external_call(
423 &mut self,
424 func_id: FunctionId,
425 seen: &mut HashSet<FunctionId>,
426 ) -> bool {
427 if let Some(&cached) = self.external_call_reachability.get(&func_id) {
428 return cached;
429 }
430 if !seen.insert(func_id) {
431 self.reachability_cut = true;
432 return false;
433 }
434 let outer_cut = std::mem::replace(&mut self.reachability_cut, false);
435 let func = self.gcx.hir.function(func_id);
436 let may_reach = func.modifiers.iter().any(|modifier| {
437 modifier.args.exprs().any(|arg| self.expr_may_reach_external_call(arg, seen))
438 || modifier
439 .id
440 .as_function()
441 .is_some_and(|id| self.helper_may_reach_external_call(id, seen))
442 }) || func.body.is_some_and(|body| {
443 body.stmts.iter().any(|stmt| self.stmt_may_reach_external_call(stmt, seen))
444 });
445 seen.remove(&func_id);
446 if may_reach || !self.reachability_cut {
448 self.external_call_reachability.insert(func_id, may_reach);
449 }
450 self.reachability_cut |= outer_cut;
451 may_reach
452 }
453
454 fn stmt_may_reach_external_call(
455 &mut self,
456 stmt: &'gcx Stmt<'gcx>,
457 seen: &mut HashSet<FunctionId>,
458 ) -> bool {
459 match stmt.kind {
460 StmtKind::DeclSingle(var_id) => self
461 .gcx
462 .hir
463 .variable(var_id)
464 .initializer
465 .is_some_and(|init| self.expr_may_reach_external_call(init, seen)),
466 StmtKind::DeclMulti(_, expr)
467 | StmtKind::Expr(expr)
468 | StmtKind::Emit(expr)
469 | StmtKind::Revert(expr) => self.expr_may_reach_external_call(expr, seen),
470 StmtKind::Return(expr) => {
471 expr.is_some_and(|expr| self.expr_may_reach_external_call(expr, seen))
472 }
473 StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => {
474 block.stmts.iter().any(|stmt| self.stmt_may_reach_external_call(stmt, seen))
475 }
476 StmtKind::Loop(block, source) => {
477 loop_stmts(block, source).any(|stmt| self.stmt_may_reach_external_call(stmt, seen))
478 }
479 StmtKind::If(cond, then_stmt, else_stmt) => {
480 self.expr_may_reach_external_call(cond, seen)
481 || self.stmt_may_reach_external_call(then_stmt, seen)
482 || else_stmt.is_some_and(|stmt| self.stmt_may_reach_external_call(stmt, seen))
483 }
484 StmtKind::Try(try_stmt) => {
485 self.expr_may_reach_external_call(&try_stmt.expr, seen)
486 || try_stmt.clauses.iter().any(|clause| {
487 clause
488 .block
489 .stmts
490 .iter()
491 .any(|stmt| self.stmt_may_reach_external_call(stmt, seen))
492 })
493 }
494 StmtKind::AssemblyBlock(_) | StmtKind::Switch(_) => true,
495 StmtKind::Break | StmtKind::Continue | StmtKind::Placeholder | StmtKind::Err(_) => {
496 false
497 }
498 }
499 }
500
501 fn expr_may_reach_external_call(
502 &mut self,
503 expr: &'gcx Expr<'gcx>,
504 seen: &mut HashSet<FunctionId>,
505 ) -> bool {
506 let mut reached = false;
507 for_each_child(expr, &mut |child| {
508 reached = reached || self.expr_may_reach_external_call(child, seen);
509 });
510 if reached {
511 return true;
512 }
513 let ExprKind::Call(callee, ..) = &expr.kind else { return false };
514 is_state_mutating_external_call(self.gcx, callee)
515 || self
516 .callees(callee)
517 .into_iter()
518 .any(|id| self.helper_may_reach_external_call(id, seen))
519 }
520}