Skip to main content

forge_lint/sol/high/
controlled_delegatecall.rs

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
906/// Strips the trailing `if (...) break;` that lowers `do { ... } while (cond);`.
907fn 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}