1use super::ControlledDelegatecall;
2use crate::{
3 linter::{LateLintPass, LintContext},
4 sol::{Severity, SolLint},
5};
6use solar::{
7 ast::{self, LitKind},
8 interface::{Span, Symbol, data_structures::Never, kw, sym},
9 sema::{
10 Gcx,
11 builtins::Builtin,
12 hir::{
13 self, CallArgs, ElementaryType, ExprKind, FunctionId, FunctionKind, ItemId, LoopSource,
14 Res, StmtKind, TypeKind, Visit,
15 },
16 ty::{Ty, TyKind},
17 },
18};
19use std::{collections::HashSet, ops::ControlFlow};
20
21declare_forge_lint!(
22 CONTROLLED_DELEGATECALL,
23 Severity::High,
24 "controlled-delegatecall",
25 "delegatecall target is not provably trusted"
26);
27
28impl<'hir> LateLintPass<'hir> for ControlledDelegatecall {
29 fn check_function(
30 &mut self,
31 ctx: &LintContext,
32 gcx: Gcx<'hir>,
33 hir: &'hir hir::Hir<'hir>,
34 func: &'hir hir::Function<'hir>,
35 ) {
36 let Some(body) = func.body else { return };
37 let mut analyzer = Analyzer::new(gcx, hir);
38 for modifier in func.modifiers {
39 collect_modifier_safety(gcx, hir, modifier, &mut analyzer.safe_vars);
40 }
41 for stmt in body.stmts {
42 let _ = analyzer.visit_stmt(stmt);
43 if branch_always_exits(stmt) {
44 break;
45 }
46 }
47 for span in analyzer.hits {
48 ctx.emit(&CONTROLLED_DELEGATECALL, span);
49 }
50 }
51}
52
53struct Analyzer<'hir> {
54 gcx: Gcx<'hir>,
55 hir: &'hir hir::Hir<'hir>,
56 safe_vars: HashSet<hir::VariableId>,
57 hits: Vec<Span>,
58}
59
60#[derive(Clone)]
61struct FlowState {
62 safe_vars: HashSet<hir::VariableId>,
63}
64
65impl FlowState {
66 fn intersection(a: &Self, b: &Self) -> Self {
67 Self { safe_vars: a.safe_vars.intersection(&b.safe_vars).copied().collect() }
68 }
69
70 fn intersection_all(mut states: impl Iterator<Item = Self>) -> Self {
71 let mut out = states.next().unwrap_or_else(|| Self { safe_vars: HashSet::new() });
72 for state in states {
73 out = Self::intersection(&out, &state);
74 }
75 out
76 }
77}
78
79const HELPER_DEPTH: u8 = 3;
80
81impl<'hir> Analyzer<'hir> {
82 fn new(gcx: Gcx<'hir>, hir: &'hir hir::Hir<'hir>) -> Self {
83 Self { gcx, hir, safe_vars: HashSet::new(), hits: Vec::new() }
84 }
85
86 fn snapshot(&self) -> FlowState {
87 FlowState { safe_vars: self.safe_vars.clone() }
88 }
89
90 fn restore(&mut self, state: FlowState) {
91 self.safe_vars = state.safe_vars;
92 }
93
94 fn is_trusted_target(&self, expr: &'hir hir::Expr<'hir>) -> bool {
95 self.is_trusted_target_inner(expr, HELPER_DEPTH)
96 }
97
98 fn is_trusted_target_inner(&self, expr: &'hir hir::Expr<'hir>, depth: u8) -> bool {
99 match &expr.peel_parens().kind {
100 ExprKind::Lit(lit) => match &lit.kind {
101 LitKind::Address(_) => true,
102 LitKind::Number(n) => n.is_zero(),
103 _ => false,
104 },
105 ExprKind::Ident(reses) => reses.iter().any(|res| match res {
106 Res::Builtin(builtin) => builtin.name() == sym::this,
107 Res::Item(ItemId::Variable(vid)) => self.is_trusted_var(*vid),
108 _ => false,
109 }),
110 ExprKind::Call(callee, args, _)
111 if is_address_like_cast_callee(callee) || is_numeric_cast_callee(callee) =>
112 {
113 args.exprs().next().is_some_and(|arg| self.is_trusted_target_inner(arg, depth))
114 }
115 ExprKind::Payable(inner) => self.is_trusted_target_inner(inner, depth),
116 ExprKind::Ternary(_, if_true, if_false) => {
117 self.is_trusted_target_inner(if_true, depth)
118 && self.is_trusted_target_inner(if_false, depth)
119 }
120 ExprKind::Assign(_, _, rhs) => self.is_trusted_target_inner(rhs, depth),
121 ExprKind::Call(callee, args, _)
122 if depth > 0
123 && args.exprs().next().is_none()
124 && callee_no_arg_returns(self.hir, callee, |e| {
125 self.is_trusted_target_inner(e, depth - 1)
126 }) =>
127 {
128 true
129 }
130 _ => false,
131 }
132 }
133
134 fn is_trusted_var(&self, vid: hir::VariableId) -> bool {
135 let var = self.hir.variable(vid);
136 (var.is_constant() && var_is_address_like(var)) || self.safe_vars.contains(&vid)
137 }
138
139 fn handle_assign(
140 &mut self,
141 lhs: &'hir hir::Expr<'hir>,
142 op: Option<hir::BinOp>,
143 rhs: &'hir hir::Expr<'hir>,
144 ) {
145 let lhs = lhs.peel_parens();
146 if let ExprKind::Tuple(lhs_elems) = &lhs.kind {
147 let rhs_elems = tuple_elems(rhs);
148 for (i, lhs_elem) in lhs_elems.iter().enumerate() {
149 if let Some(lhs_expr) = lhs_elem {
150 self.assign_one(
151 lhs_expr,
152 op.is_none().then(|| tuple_slot(rhs_elems, i)).flatten(),
153 );
154 }
155 }
156 } else {
157 self.assign_one(lhs, op.is_none().then_some(rhs));
158 }
159 }
160
161 fn assign_one(&mut self, lhs: &'hir hir::Expr<'hir>, rhs: Option<&'hir hir::Expr<'hir>>) {
162 let Some(var) = underlying_var(lhs) else { return };
163 self.safe_vars.remove(&var);
164 let target = self.hir.variable(var);
165 if target.kind.is_state() || !var_is_address_like(target) {
166 return;
167 }
168 if rhs.is_some_and(|expr| self.is_trusted_target(expr)) {
169 self.safe_vars.insert(var);
170 }
171 }
172
173 fn delete_one(&mut self, lhs: &'hir hir::Expr<'hir>) {
174 let Some(var) = underlying_var(lhs) else { return };
175 self.safe_vars.remove(&var);
176 let target = self.hir.variable(var);
177 if !target.kind.is_state() && var_is_address_like(target) {
178 self.safe_vars.insert(var);
179 }
180 }
181
182 fn handle_decl(&mut self, var: hir::VariableId) {
183 let variable = self.hir.variable(var);
184 if !var_is_address_like(variable) {
185 return;
186 }
187 if let Some(init) = variable.initializer
188 && self.is_trusted_target(init)
189 {
190 self.safe_vars.insert(var);
191 }
192 }
193
194 fn is_controlled_delegatecall(&self, expr: &'hir hir::Expr<'hir>) -> bool {
195 let ExprKind::Call(callee, _, _) = &expr.peel_parens().kind else {
196 return false;
197 };
198 let ExprKind::Member(receiver, member) = &callee.peel_parens().kind else {
199 return false;
200 };
201 member.name == kw::Delegatecall
202 && receiver_is_address(self.gcx, receiver)
203 && !self.is_trusted_target(receiver)
204 }
205
206 fn add_facts(&mut self, pred: &'hir hir::Expr<'hir>, negate: bool) {
207 if expr_has_fact_side_effect(pred) {
208 return;
209 }
210 match &pred.peel_parens().kind {
211 ExprKind::Binary(lhs, op, rhs) => {
212 let (eq, and_op, or_op) = if negate {
213 (ast::BinOpKind::Ne, ast::BinOpKind::Or, ast::BinOpKind::And)
214 } else {
215 (ast::BinOpKind::Eq, ast::BinOpKind::And, ast::BinOpKind::Or)
216 };
217 if op.kind == and_op {
218 self.add_facts(lhs, negate);
219 self.add_facts(rhs, negate);
220 } else if op.kind == or_op {
221 self.add_facts_disjunction(lhs, rhs, negate);
222 } else if op.kind == eq {
223 for (safe, candidate) in [(lhs, rhs), (rhs, lhs)] {
224 if self.is_trusted_target(safe)
225 && let Some(var) = underlying_var(candidate)
226 && self.is_trusted_fact_target(var)
227 {
228 self.safe_vars.insert(var);
229 }
230 }
231 }
232 }
233 ExprKind::Unary(op, inner) if matches!(op.kind, ast::UnOpKind::Not) => {
234 self.add_facts(inner, !negate);
235 }
236 _ => {}
237 }
238 }
239
240 fn add_facts_unchecked(&mut self, pred: &'hir hir::Expr<'hir>, negate: bool) {
241 match &pred.peel_parens().kind {
242 ExprKind::Binary(lhs, op, rhs) => {
243 let (eq, and_op, or_op) = if negate {
244 (ast::BinOpKind::Ne, ast::BinOpKind::Or, ast::BinOpKind::And)
245 } else {
246 (ast::BinOpKind::Eq, ast::BinOpKind::And, ast::BinOpKind::Or)
247 };
248 if op.kind == and_op {
249 self.add_facts_unchecked(lhs, negate);
250 self.add_facts_unchecked(rhs, negate);
251 } else if op.kind == or_op {
252 self.add_facts_disjunction(lhs, rhs, negate);
253 } else if op.kind == eq {
254 for (safe, candidate) in [(lhs, rhs), (rhs, lhs)] {
255 if self.is_trusted_target(safe)
256 && let Some(var) = underlying_var(candidate)
257 && self.is_trusted_fact_target(var)
258 {
259 self.safe_vars.insert(var);
260 }
261 }
262 }
263 }
264 ExprKind::Unary(op, inner) if matches!(op.kind, ast::UnOpKind::Not) => {
265 self.add_facts_unchecked(inner, !negate);
266 }
267 _ => {}
268 }
269 }
270
271 fn add_facts_disjunction(
272 &mut self,
273 lhs: &'hir hir::Expr<'hir>,
274 rhs: &'hir hir::Expr<'hir>,
275 negate: bool,
276 ) {
277 let baseline = self.safe_vars.clone();
278 self.add_facts_unchecked(lhs, negate);
279 let lhs_added: HashSet<_> = self.safe_vars.difference(&baseline).copied().collect();
280 self.safe_vars.clone_from(&baseline);
281 self.add_facts_unchecked(rhs, negate);
282 let rhs_added: HashSet<_> = self.safe_vars.difference(&baseline).copied().collect();
283 self.safe_vars = baseline;
284 for var in lhs_added.intersection(&rhs_added) {
285 self.safe_vars.insert(*var);
286 }
287 }
288
289 fn is_trusted_fact_target(&self, var: hir::VariableId) -> bool {
290 let variable = self.hir.variable(var);
291 (!variable.kind.is_state() || variable.is_constant()) && var_is_address_like(variable)
292 }
293
294 fn visit_isolated(&mut self, stmts: &'hir [hir::Stmt<'hir>]) {
295 let mut exits = vec![self.snapshot()];
296 if let Some(fallthrough) = self.visit_stmts_until_loop_exit(stmts, &mut exits) {
297 exits.push(fallthrough);
298 }
299 self.restore(FlowState::intersection_all(exits.into_iter()));
300 }
301
302 fn visit_stmts_until_loop_exit(
303 &mut self,
304 stmts: &'hir [hir::Stmt<'hir>],
305 exits: &mut Vec<FlowState>,
306 ) -> Option<FlowState> {
307 for stmt in stmts {
308 self.visit_stmt_until_loop_exit(stmt, exits)?;
309 }
310 Some(self.snapshot())
311 }
312
313 fn visit_stmt_until_loop_exit(
314 &mut self,
315 stmt: &'hir hir::Stmt<'hir>,
316 exits: &mut Vec<FlowState>,
317 ) -> Option<()> {
318 match &stmt.kind {
319 StmtKind::Break | StmtKind::Continue => {
320 exits.push(self.snapshot());
321 None
322 }
323 StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => {
324 let state = self.visit_stmts_until_loop_exit(block.stmts, exits)?;
325 self.restore(state);
326 Some(())
327 }
328 StmtKind::If(cond, then, else_) => {
329 let _ = self.visit_expr(cond);
330 let baseline = self.snapshot();
331 self.add_facts(cond, false);
332 let then_fallthrough = self
333 .visit_stmt_until_loop_exit(then, exits)
334 .and_then(|_| (!branch_always_exits(then)).then(|| self.snapshot()));
335 self.restore(baseline);
336 self.add_facts(cond, true);
337 let else_fallthrough = match else_ {
338 Some(else_stmt) => self
339 .visit_stmt_until_loop_exit(else_stmt, exits)
340 .and_then(|_| (!branch_always_exits(else_stmt)).then(|| self.snapshot())),
341 None => Some(self.snapshot()),
342 };
343 match (then_fallthrough, else_fallthrough) {
344 (Some(then_state), Some(else_state)) => {
345 self.restore(FlowState::intersection(&then_state, &else_state));
346 Some(())
347 }
348 (Some(state), None) | (None, Some(state)) => {
349 self.restore(state);
350 Some(())
351 }
352 (None, None) => None,
353 }
354 }
355 StmtKind::Loop(..) => {
356 let _ = self.visit_stmt(stmt);
357 Some(())
358 }
359 _ => {
360 let _ = self.visit_stmt(stmt);
361 (!branch_always_exits(stmt)).then_some(())
362 }
363 }
364 }
365}
366
367impl<'hir> Visit<'hir> for Analyzer<'hir> {
368 type BreakValue = Never;
369
370 fn hir(&self) -> &'hir hir::Hir<'hir> {
371 self.hir
372 }
373
374 fn visit_stmt(&mut self, stmt: &'hir hir::Stmt<'hir>) -> ControlFlow<Self::BreakValue> {
375 match &stmt.kind {
376 StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => {
377 for stmt in block.stmts {
378 let _ = self.visit_stmt(stmt);
379 if branch_always_exits(stmt) {
380 break;
381 }
382 }
383 return ControlFlow::Continue(());
384 }
385 StmtKind::If(cond, then, else_) => {
386 let _ = self.visit_expr(cond);
387 let baseline = self.snapshot();
388 self.add_facts(cond, false);
389 let _ = self.visit_stmt(then);
390 let then_exits = branch_always_exits(then);
391 let after_then = self.snapshot();
392 self.restore(baseline);
393 self.add_facts(cond, true);
394 let else_exits = match else_ {
395 Some(else_stmt) => {
396 let _ = self.visit_stmt(else_stmt);
397 branch_always_exits(else_stmt)
398 }
399 None => false,
400 };
401 let after_else = self.snapshot();
402 let joined = match (then_exits, else_exits) {
403 (true, false) => after_else,
404 (false, true) => after_then,
405 _ => FlowState::intersection(&after_then, &after_else),
406 };
407 self.restore(joined);
408 return ControlFlow::Continue(());
409 }
410 StmtKind::Loop(block, source) => {
411 if matches!(source, LoopSource::DoWhile)
412 && !do_while_user_stmts(block.stmts).iter().any(stmt_has_break_or_continue)
413 {
414 for stmt in do_while_user_stmts(block.stmts) {
415 let _ = self.visit_stmt(stmt);
416 if branch_always_exits(stmt) {
417 break;
418 }
419 }
420 if let Some(cond) = do_while_lowered_condition(block.stmts) {
421 let _ = self.visit_expr(cond);
422 }
423 } else {
424 self.visit_isolated(block.stmts);
425 }
426 return ControlFlow::Continue(());
427 }
428 StmtKind::Try(stmt_try) => {
429 let _ = self.visit_expr(&stmt_try.expr);
430 let outer = self.snapshot();
431 let mut clause_exits = Vec::new();
432 for clause in stmt_try.clauses {
433 self.restore(outer.clone());
434 let mut exited = false;
435 for stmt in clause.block.stmts {
436 let _ = self.visit_stmt(stmt);
437 if branch_always_exits(stmt) {
438 exited = true;
439 break;
440 }
441 }
442 if !exited {
443 clause_exits.push(self.snapshot());
444 }
445 }
446 self.restore(
447 clause_exits
448 .into_iter()
449 .reduce(|a, b| FlowState::intersection(&a, &b))
450 .unwrap_or(outer),
451 );
452 return ControlFlow::Continue(());
453 }
454 StmtKind::Err(_) => {
455 self.safe_vars.clear();
456 }
457 StmtKind::DeclSingle(var) => self.handle_decl(*var),
458 StmtKind::DeclMulti(vars, init) => {
459 if let ExprKind::Tuple(exprs) = &init.peel_parens().kind {
460 for (var, expr) in vars.iter().zip(exprs.iter()) {
461 let (Some(var), Some(expr)) = (var, expr) else { continue };
462 self.assign_one_var(*var, Some(expr));
463 }
464 } else {
465 for var in vars.iter().flatten() {
466 self.assign_one_var(*var, None);
467 }
468 }
469 }
470 _ => {}
471 }
472 self.walk_stmt(stmt)
473 }
474
475 fn visit_expr(&mut self, expr: &'hir hir::Expr<'hir>) -> ControlFlow<Self::BreakValue> {
476 if let ExprKind::Binary(lhs, op, rhs) = &expr.kind
477 && matches!(op.kind, ast::BinOpKind::And | ast::BinOpKind::Or)
478 {
479 let _ = self.visit_expr(lhs);
480 let negate = matches!(op.kind, ast::BinOpKind::Or);
481 let skipped_rhs = self.snapshot();
482 self.add_facts(lhs, negate);
483 let result = self.visit_expr(rhs);
484 let ran_rhs = self.snapshot();
485 self.restore(FlowState::intersection(&skipped_rhs, &ran_rhs));
486 return result;
487 }
488 if let ExprKind::Ternary(cond, then_expr, else_expr) = &expr.kind {
489 let _ = self.visit_expr(cond);
490 let pre_arm = self.snapshot();
491 self.add_facts(cond, false);
492 let _ = self.visit_expr(then_expr);
493 let post_then = self.snapshot();
494 self.restore(pre_arm);
495 self.add_facts(cond, true);
496 let _ = self.visit_expr(else_expr);
497 let post_else = self.snapshot();
498 self.restore(FlowState::intersection(&post_then, &post_else));
499 return ControlFlow::Continue(());
500 }
501 if self.is_controlled_delegatecall(expr) {
502 self.hits.push(expr.span);
503 }
504 match &expr.kind {
505 ExprKind::Call(callee, args, _) if is_require_or_assert(callee) => {
506 let result = self.walk_expr(expr);
507 if let Some(cond) = args.exprs().next() {
508 let mut args = args.exprs();
509 let _ = args.next();
510 if !args.any(expr_has_fact_side_effect) {
511 self.add_facts(cond, false);
512 }
513 }
514 return result;
515 }
516 ExprKind::Assign(lhs, op, rhs) => {
517 let result = self.walk_expr(expr);
518 self.handle_assign(lhs, *op, rhs);
519 return result;
520 }
521 ExprKind::Delete(target) => self.delete_one(target.peel_parens()),
522 _ => {}
523 }
524 self.walk_expr(expr)
525 }
526}
527
528impl<'hir> Analyzer<'hir> {
529 fn assign_one_var(&mut self, var: hir::VariableId, rhs: Option<&'hir hir::Expr<'hir>>) {
530 self.safe_vars.remove(&var);
531 let variable = self.hir.variable(var);
532 if variable.kind.is_state() || !var_is_address_like(variable) {
533 return;
534 }
535 if rhs.is_some_and(|expr| self.is_trusted_target(expr)) {
536 self.safe_vars.insert(var);
537 }
538 }
539}
540
541fn underlying_var(expr: &hir::Expr<'_>) -> Option<hir::VariableId> {
542 match &expr.peel_parens().kind {
543 ExprKind::Ident(reses) => reses.iter().find_map(|res| match res {
544 Res::Item(ItemId::Variable(vid)) => Some(*vid),
545 _ => None,
546 }),
547 ExprKind::Call(callee, args, _)
548 if is_address_like_cast_callee(callee) || is_numeric_cast_callee(callee) =>
549 {
550 args.exprs().next().and_then(underlying_var)
551 }
552 ExprKind::Payable(inner) => underlying_var(inner),
553 _ => None,
554 }
555}
556
557fn tuple_elems<'hir>(expr: &'hir hir::Expr<'hir>) -> Option<&'hir [Option<&'hir hir::Expr<'hir>>]> {
558 match &expr.peel_parens().kind {
559 ExprKind::Tuple(elems) => Some(*elems),
560 _ => None,
561 }
562}
563
564fn tuple_slot<'hir>(
565 elems: Option<&'hir [Option<&'hir hir::Expr<'hir>>]>,
566 idx: usize,
567) -> Option<&'hir hir::Expr<'hir>> {
568 elems.and_then(|elems| elems.get(idx).copied().flatten())
569}
570
571const fn var_is_address_like(var: &hir::Variable<'_>) -> bool {
572 matches!(
573 var.ty.kind,
574 TypeKind::Elementary(ElementaryType::Address(_)) | TypeKind::Custom(ItemId::Contract(_))
575 )
576}
577
578fn receiver_is_address<'hir>(gcx: Gcx<'hir>, expr: &'hir hir::Expr<'hir>) -> bool {
579 gcx.type_of_expr(expr.peel_parens().id).is_some_and(ty_is_address)
580}
581
582fn is_address_like_cast_callee(callee: &hir::Expr<'_>) -> bool {
583 match &callee.peel_parens().kind {
584 ExprKind::Type(hir::Type {
585 kind: TypeKind::Elementary(ElementaryType::Address(_)),
586 ..
587 }) => true,
588 ExprKind::Ident(reses) => {
589 !reses.is_empty()
590 && reses.iter().all(|res| matches!(res, Res::Item(ItemId::Contract(_))))
591 }
592 _ => false,
593 }
594}
595
596fn is_numeric_cast_callee(callee: &hir::Expr<'_>) -> bool {
597 match &callee.peel_parens().kind {
598 ExprKind::Type(hir::Type { kind: TypeKind::Elementary(ty), .. }) => {
599 matches!(ty, ElementaryType::Int(_) | ElementaryType::UInt(_) | ElementaryType::Bytes)
600 }
601 _ => false,
602 }
603}
604
605fn ty_is_address(ty: Ty<'_>) -> bool {
606 matches!(ty.peel_refs().kind, TyKind::Elementary(ElementaryType::Address(_)))
607}
608
609fn callee_no_arg_returns<'hir>(
610 hir: &'hir hir::Hir<'hir>,
611 callee: &'hir hir::Expr<'hir>,
612 mut pred: impl FnMut(&'hir hir::Expr<'hir>) -> bool,
613) -> bool {
614 let ExprKind::Ident(reses) = &callee.peel_parens().kind else { return false };
615 let fids: Vec<_> = reses
616 .iter()
617 .filter_map(|res| match res {
618 Res::Item(ItemId::Function(fid)) => Some(*fid),
619 _ => None,
620 })
621 .collect();
622 let [fid] = fids.as_slice() else { return false };
623 function_is_statically_trusted(hir.function(*fid))
624 && function_no_arg_returns(hir, *fid, &mut pred)
625}
626
627const fn function_is_statically_trusted(func: &hir::Function<'_>) -> bool {
628 !func.virtual_ && !func.override_
629}
630
631fn collect_modifier_safety<'hir>(
632 gcx: Gcx<'hir>,
633 hir: &'hir hir::Hir<'hir>,
634 invocation: &'hir hir::Modifier<'hir>,
635 out_safe: &mut HashSet<hir::VariableId>,
636) {
637 let ItemId::Function(fid) = invocation.id else { return };
638 let Some((modifier, prefix)) = modifier_prefix(hir, fid) else { return };
639 let arg_map: Vec<(hir::VariableId, hir::VariableId)> = modifier
640 .parameters
641 .iter()
642 .filter_map(|&modifier_param| {
643 let arg = arg_for_param(hir, modifier, modifier_param, &invocation.args)?;
644 Some((modifier_param, underlying_var(arg)?))
645 })
646 .collect();
647 if arg_map.is_empty() {
648 return;
649 }
650
651 let mut assigned_params = HashSet::new();
652 let mut collector = AssignedParamCollector { hir, out: &mut assigned_params };
653 for stmt in &prefix {
654 let _ = collector.visit_stmt(stmt);
655 }
656
657 let mut analyzer = Analyzer::new(gcx, hir);
658 for stmt in &prefix {
659 let _ = analyzer.visit_stmt(stmt);
660 if branch_always_exits(stmt) {
661 break;
662 }
663 }
664
665 for (modifier_param, caller_var) in arg_map {
666 if !assigned_params.contains(&modifier_param)
667 && analyzer.safe_vars.contains(&modifier_param)
668 && analyzer.is_trusted_fact_target(caller_var)
669 {
670 out_safe.insert(caller_var);
671 }
672 }
673}
674
675fn modifier_prefix<'hir>(
676 hir: &'hir hir::Hir<'hir>,
677 fid: FunctionId,
678) -> Option<(&'hir hir::Function<'hir>, Vec<&'hir hir::Stmt<'hir>>)> {
679 let modifier = hir.function(fid);
680 if !matches!(modifier.kind, FunctionKind::Modifier) {
681 return None;
682 }
683 let body = modifier.body?;
684 if count_placeholders(body.stmts) != 1 {
685 return None;
686 }
687 let mut prefix = Vec::new();
688 collect_stmts_before_placeholder(body.stmts, &mut prefix)?;
689 Some((modifier, prefix))
690}
691
692fn collect_stmts_before_placeholder<'hir>(
693 stmts: &'hir [hir::Stmt<'hir>],
694 out: &mut Vec<&'hir hir::Stmt<'hir>>,
695) -> Option<()> {
696 for (i, stmt) in stmts.iter().enumerate() {
697 match &stmt.kind {
698 StmtKind::Placeholder => {
699 out.extend(stmts[..i].iter());
700 return Some(());
701 }
702 StmtKind::Block(block) | StmtKind::UncheckedBlock(block)
703 if count_placeholders(block.stmts) >= 1 =>
704 {
705 out.extend(stmts[..i].iter());
706 return collect_stmts_before_placeholder(block.stmts, out);
707 }
708 _ => {
709 if count_placeholders_in_stmt(stmt) > 0 {
710 return None;
711 }
712 }
713 }
714 }
715 None
716}
717
718fn arg_for_param<'hir>(
719 hir: &'hir hir::Hir<'hir>,
720 function: &hir::Function<'hir>,
721 param: hir::VariableId,
722 args: &'hir CallArgs<'hir>,
723) -> Option<&'hir hir::Expr<'hir>> {
724 let param_idx = function.parameters.iter().position(|p| *p == param)?;
725 match args.kind {
726 hir::CallArgsKind::Unnamed(exprs) => exprs.get(param_idx),
727 hir::CallArgsKind::Named(named) => {
728 let param_name = hir.variable(param).name?;
729 named.iter().find(|arg| arg.name.name == param_name.name).map(|arg| &arg.value)
730 }
731 }
732}
733
734struct AssignedParamCollector<'a, 'hir> {
735 hir: &'hir hir::Hir<'hir>,
736 out: &'a mut HashSet<hir::VariableId>,
737}
738
739impl AssignedParamCollector<'_, '_> {
740 fn add_lhs(&mut self, lhs: &hir::Expr<'_>) {
741 match &lhs.peel_parens().kind {
742 ExprKind::Tuple(elems) => {
743 for expr in elems.iter().flatten() {
744 self.add_lhs(expr);
745 }
746 }
747 _ => {
748 if let Some(var) = underlying_var(lhs) {
749 self.out.insert(var);
750 }
751 }
752 }
753 }
754}
755
756impl<'hir> Visit<'hir> for AssignedParamCollector<'_, 'hir> {
757 type BreakValue = Never;
758
759 fn hir(&self) -> &'hir hir::Hir<'hir> {
760 self.hir
761 }
762
763 fn visit_expr(&mut self, expr: &'hir hir::Expr<'hir>) -> ControlFlow<Self::BreakValue> {
764 match &expr.peel_parens().kind {
765 ExprKind::Assign(lhs, _, _) => self.add_lhs(lhs),
766 ExprKind::Delete(target) => self.add_lhs(target),
767 _ => {}
768 }
769 self.walk_expr(expr)
770 }
771}
772
773fn function_no_arg_returns<'hir>(
774 hir: &'hir hir::Hir<'hir>,
775 fid: FunctionId,
776 pred: &mut impl FnMut(&'hir hir::Expr<'hir>) -> bool,
777) -> bool {
778 let func = hir.function(fid);
779 let Some(body) = func.body else { return false };
780 if !func.parameters.is_empty() {
781 return false;
782 }
783 let stmts = match body.stmts.split_last() {
784 Some((last, rest)) if matches!(last.kind, StmtKind::Return(None)) => rest,
785 _ => body.stmts,
786 };
787 if stmts.len() != 1 {
788 return false;
789 }
790 match &stmts[0].kind {
791 StmtKind::Return(Some(expr)) => pred(expr),
792 StmtKind::Expr(expr) => match &expr.peel_parens().kind {
793 ExprKind::Assign(lhs, None, rhs) => {
794 func.returns.len() == 1
795 && underlying_var(lhs).is_some_and(|var| var == func.returns[0])
796 && pred(rhs)
797 }
798 _ => false,
799 },
800 _ => false,
801 }
802}
803
804fn is_require_or_assert(callee: &hir::Expr<'_>) -> bool {
805 let ExprKind::Ident(reses) = &callee.kind else { return false };
806 reses.iter().any(
807 |res| matches!(res, Res::Builtin(builtin) if builtin.name() == sym::require || builtin.name() == sym::assert),
808 )
809}
810
811fn branch_always_exits(stmt: &hir::Stmt<'_>) -> bool {
812 match &stmt.kind {
813 StmtKind::Return(_) | StmtKind::Revert(_) => true,
814 StmtKind::Expr(expr) => is_exit_call(expr),
815 StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => {
816 block.stmts.iter().any(branch_always_exits)
817 }
818 StmtKind::If(_, then_stmt, Some(else_stmt)) => {
819 branch_always_exits(then_stmt) && branch_always_exits(else_stmt)
820 }
821 StmtKind::Try(stmt_try) => {
822 !stmt_try.clauses.is_empty()
823 && stmt_try
824 .clauses
825 .iter()
826 .all(|clause| clause.block.stmts.iter().any(branch_always_exits))
827 }
828 _ => false,
829 }
830}
831
832fn is_exit_call(expr: &hir::Expr<'_>) -> bool {
833 let ExprKind::Call(callee, args, _) = &expr.kind else { return false };
834 if is_builtin(callee, kw::Revert) {
835 return true;
836 }
837 if let ExprKind::Ident(reses) = &callee.peel_parens().kind
838 && reses.iter().any(|res| matches!(res, Res::Builtin(Builtin::Selfdestruct)))
839 {
840 return true;
841 }
842 if is_require_or_assert(callee)
843 && let hir::CallArgsKind::Unnamed(unnamed) = args.kind
844 && let Some(first) = unnamed.first()
845 && matches!(
846 &first.peel_parens().kind,
847 ExprKind::Lit(lit) if matches!(lit.kind, ast::LitKind::Bool(false))
848 )
849 {
850 return true;
851 }
852 false
853}
854
855fn expr_has_fact_side_effect(expr: &hir::Expr<'_>) -> bool {
856 match &expr.peel_parens().kind {
857 ExprKind::Assign(..) | ExprKind::Delete(_) => true,
858 ExprKind::Unary(op, inner) => {
859 matches!(
860 op.kind,
861 ast::UnOpKind::PreInc
862 | ast::UnOpKind::PreDec
863 | ast::UnOpKind::PostInc
864 | ast::UnOpKind::PostDec
865 ) || expr_has_fact_side_effect(inner)
866 }
867 ExprKind::Array(exprs) => exprs.iter().any(expr_has_fact_side_effect),
868 ExprKind::Binary(lhs, _, rhs) => {
869 expr_has_fact_side_effect(lhs) || expr_has_fact_side_effect(rhs)
870 }
871 ExprKind::Call(callee, args, options) => {
872 expr_has_fact_side_effect(callee)
873 || args.exprs().any(expr_has_fact_side_effect)
874 || options.is_some_and(|options| {
875 options.args.iter().any(|arg| expr_has_fact_side_effect(&arg.value))
876 })
877 }
878 ExprKind::Index(base, index) => {
879 expr_has_fact_side_effect(base) || index.is_some_and(expr_has_fact_side_effect)
880 }
881 ExprKind::Slice(base, start, end) => {
882 expr_has_fact_side_effect(base)
883 || start.is_some_and(expr_has_fact_side_effect)
884 || end.is_some_and(expr_has_fact_side_effect)
885 }
886 ExprKind::Member(base, _) | ExprKind::Payable(base) | ExprKind::YulMember(base, _) => {
887 expr_has_fact_side_effect(base)
888 }
889 ExprKind::Ternary(cond, then_expr, else_expr) => {
890 expr_has_fact_side_effect(cond)
891 || expr_has_fact_side_effect(then_expr)
892 || expr_has_fact_side_effect(else_expr)
893 }
894 ExprKind::Tuple(exprs) => {
895 exprs.iter().flatten().any(|expr| expr_has_fact_side_effect(expr))
896 }
897 ExprKind::Lit(_)
898 | ExprKind::Ident(_)
899 | ExprKind::New(_)
900 | ExprKind::TypeCall(_)
901 | ExprKind::Type(_)
902 | ExprKind::Err(_) => false,
903 }
904}
905
906fn do_while_user_stmts<'a, 'hir>(stmts: &'a [hir::Stmt<'hir>]) -> &'a [hir::Stmt<'hir>] {
908 if let Some((last, rest)) = stmts.split_last()
909 && let StmtKind::If(_, then_stmt, else_stmt) = &last.kind
910 && (is_break_stmt(then_stmt) || else_stmt.as_ref().is_some_and(|stmt| is_break_stmt(stmt)))
911 {
912 return rest;
913 }
914 stmts
915}
916
917fn do_while_lowered_condition<'hir>(
918 stmts: &'hir [hir::Stmt<'hir>],
919) -> Option<&'hir hir::Expr<'hir>> {
920 let last = stmts.last()?;
921 let StmtKind::If(cond, then_stmt, else_stmt) = &last.kind else { return None };
922 (is_break_stmt(then_stmt) || else_stmt.as_ref().is_some_and(|stmt| is_break_stmt(stmt)))
923 .then_some(*cond)
924}
925
926fn is_break_stmt(stmt: &hir::Stmt<'_>) -> bool {
927 match &stmt.kind {
928 StmtKind::Break => true,
929 StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => {
930 block.stmts.len() == 1 && is_break_stmt(&block.stmts[0])
931 }
932 _ => false,
933 }
934}
935
936fn stmt_has_break_or_continue(stmt: &hir::Stmt<'_>) -> bool {
937 match &stmt.kind {
938 StmtKind::Break | StmtKind::Continue => true,
939 StmtKind::Block(block) | StmtKind::UncheckedBlock(block) | StmtKind::Loop(block, _) => {
940 block.stmts.iter().any(stmt_has_break_or_continue)
941 }
942 StmtKind::If(_, then_stmt, else_stmt) => {
943 stmt_has_break_or_continue(then_stmt)
944 || else_stmt.as_ref().is_some_and(|stmt| stmt_has_break_or_continue(stmt))
945 }
946 StmtKind::Try(stmt_try) => stmt_try
947 .clauses
948 .iter()
949 .any(|clause| clause.block.stmts.iter().any(stmt_has_break_or_continue)),
950 _ => false,
951 }
952}
953
954fn count_placeholders(stmts: &[hir::Stmt<'_>]) -> usize {
955 stmts.iter().map(count_placeholders_in_stmt).sum()
956}
957
958fn count_placeholders_in_stmt(stmt: &hir::Stmt<'_>) -> usize {
959 match &stmt.kind {
960 StmtKind::Placeholder => 1,
961 StmtKind::Block(block) | StmtKind::UncheckedBlock(block) | StmtKind::Loop(block, _) => {
962 count_placeholders(block.stmts)
963 }
964 StmtKind::If(_, then_stmt, else_stmt) => {
965 count_placeholders_in_stmt(then_stmt)
966 + else_stmt.as_ref().map_or(0, |stmt| count_placeholders_in_stmt(stmt))
967 }
968 StmtKind::Try(stmt_try) => {
969 stmt_try.clauses.iter().map(|clause| count_placeholders(clause.block.stmts)).sum()
970 }
971 _ => 0,
972 }
973}
974
975fn is_builtin(expr: &hir::Expr<'_>, name: Symbol) -> bool {
976 matches!(
977 &expr.peel_parens().kind,
978 ExprKind::Ident(reses)
979 if reses.iter().any(|res| matches!(res, Res::Builtin(builtin) if builtin.name() == name))
980 )
981}