Skip to main content

forge_lint/sol/high/
controlled_delegatecall.rs

1use super::ControlledDelegatecall;
2use crate::{
3    linter::{LateLintPass, LintContext},
4    sol::{
5        Severity, SolLint,
6        analysis::{
7            arg_for_param, branch_always_exits, count_placeholders, do_while_user_stmts,
8            has_side_effect, is_address_like_cast, is_loop_termination_if, is_require_or_assert,
9            loop_update, stmts_before_placeholder, stmts_break_or_continue, tuple_elems,
10            var_is_address_like,
11        },
12    },
13};
14use solar::{
15    ast::{BinOpKind, LitKind, UnOpKind},
16    interface::{Span, sym},
17    sema::{
18        Gcx,
19        builtins::Builtin,
20        hir::{
21            self, ElementaryType, Expr, ExprKind, FunctionKind, ItemId, LoopSource, Res, Stmt,
22            StmtKind, TypeKind, VariableId, Visit,
23        },
24    },
25};
26use std::{collections::HashSet, ops::ControlFlow};
27
28declare_forge_lint!(
29    CONTROLLED_DELEGATECALL,
30    Severity::High,
31    "controlled-delegatecall",
32    "`delegatecall` target is not provably trusted"
33);
34
35/// How many levels of no-argument helper functions are inlined when checking a target.
36const HELPER_DEPTH: u8 = 3;
37
38impl<'gcx> LateLintPass<'gcx> for ControlledDelegatecall {
39    fn check_function(
40        &mut self,
41        ctx: &LintContext,
42        gcx: Gcx<'gcx>,
43        func: &'gcx hir::Function<'gcx>,
44    ) {
45        let Some(body) = func.body else { return };
46        let mut analyzer = Analyzer::new(gcx);
47        for modifier in func.modifiers {
48            analyzer.safe_vars.extend(modifier_safe_vars(gcx, modifier));
49        }
50        let _ = analyzer.visit_stmts(body.stmts);
51        for span in analyzer.hits {
52            ctx.emit(&CONTROLLED_DELEGATECALL, span);
53        }
54    }
55}
56
57/// Flow-sensitive walk tracking which local address variables provably hold a trusted target.
58///
59/// `visit_stmt` breaks when control cannot fall through to the next statement.
60struct Analyzer<'gcx> {
61    gcx: Gcx<'gcx>,
62    safe_vars: HashSet<VariableId>,
63    /// Every variable written during the walk.
64    assigned: HashSet<VariableId>,
65    /// Per enclosing loop, the states at each `break`/`continue`.
66    loop_exits: Vec<Vec<HashSet<VariableId>>>,
67    hits: Vec<Span>,
68}
69
70fn intersect(a: &HashSet<VariableId>, b: &HashSet<VariableId>) -> HashSet<VariableId> {
71    a.intersection(b).copied().collect()
72}
73
74impl<'gcx> Analyzer<'gcx> {
75    fn new(gcx: Gcx<'gcx>) -> Self {
76        Self {
77            gcx,
78            safe_vars: HashSet::new(),
79            assigned: HashSet::new(),
80            loop_exits: Vec::new(),
81            hits: Vec::new(),
82        }
83    }
84
85    fn visit_stmts(&mut self, stmts: &'gcx [Stmt<'gcx>]) -> ControlFlow<()> {
86        stmts.iter().try_for_each(|stmt| self.visit_stmt(stmt))
87    }
88
89    fn is_trusted_target(&self, expr: &'gcx Expr<'gcx>) -> bool {
90        self.is_trusted_target_inner(expr, HELPER_DEPTH)
91    }
92
93    fn is_trusted_target_inner(&self, expr: &'gcx Expr<'gcx>, depth: u8) -> bool {
94        match &expr.peel_parens().kind {
95            ExprKind::Lit(lit) => match &lit.kind {
96                LitKind::Address(_) => true,
97                LitKind::Number(n) => n.is_zero(),
98                _ => false,
99            },
100            ExprKind::Ident(_) => self.gcx.resolved_expr(expr).is_some_and(|res| match res {
101                Res::Builtin(builtin) => builtin.name() == sym::this,
102                Res::Item(ItemId::Variable(vid)) => {
103                    let var = self.gcx.hir.variable(vid);
104                    (var.is_constant() && var_is_address_like(var)) || self.safe_vars.contains(&vid)
105                }
106                _ => false,
107            }),
108            ExprKind::Call(callee, args, _) if is_cast(self.gcx, callee) => {
109                args.exprs().next().is_some_and(|arg| self.is_trusted_target_inner(arg, depth))
110            }
111            ExprKind::Payable(inner) => self.is_trusted_target_inner(inner, depth),
112            ExprKind::Ternary(_, if_true, if_false) => {
113                self.is_trusted_target_inner(if_true, depth)
114                    && self.is_trusted_target_inner(if_false, depth)
115            }
116            ExprKind::Assign(_, _, rhs) => self.is_trusted_target_inner(rhs, depth),
117            ExprKind::Call(callee, args, _) => {
118                depth > 0
119                    && args.exprs().next().is_none()
120                    && no_arg_helper_return(self.gcx, callee)
121                        .is_some_and(|ret| self.is_trusted_target_inner(ret, depth - 1))
122            }
123            _ => false,
124        }
125    }
126
127    /// Local or constant address-like variable: the only kind a comparison can vouch for.
128    fn is_trusted_fact_target(&self, var: VariableId) -> bool {
129        let variable = self.gcx.hir.variable(var);
130        (!variable.kind.is_state() || variable.is_constant()) && var_is_address_like(variable)
131    }
132
133    /// Records a write to `var`; it is safe afterwards only if local, address-like and `trusted`.
134    fn assign(&mut self, var: VariableId, trusted: bool) {
135        self.assigned.insert(var);
136        self.safe_vars.remove(&var);
137        let variable = self.gcx.hir.variable(var);
138        if trusted && !variable.kind.is_state() && var_is_address_like(variable) {
139            self.safe_vars.insert(var);
140        }
141    }
142
143    fn assign_expr(&mut self, lhs: &'gcx Expr<'gcx>, rhs: Option<&'gcx Expr<'gcx>>) {
144        if let Some(var) = underlying_var(self.gcx, lhs) {
145            self.assign(var, rhs.is_some_and(|rhs| self.is_trusted_target(rhs)));
146        }
147    }
148
149    fn handle_assign(
150        &mut self,
151        lhs: &'gcx Expr<'gcx>,
152        op: Option<hir::BinOp>,
153        rhs: &'gcx Expr<'gcx>,
154    ) {
155        let rhs = op.is_none().then_some(rhs);
156        let Some(lhs_elems) = tuple_elems(lhs) else { return self.assign_expr(lhs, rhs) };
157        let rhs_elems = rhs.and_then(tuple_elems);
158        for (i, lhs_elem) in lhs_elems.iter().enumerate() {
159            if let Some(lhs_elem) = lhs_elem {
160                let rhs_elem = rhs_elems.and_then(|elems| elems.get(i).copied().flatten());
161                self.assign_expr(lhs_elem, rhs_elem);
162            }
163        }
164    }
165
166    fn is_controlled_delegatecall(&self, expr: &'gcx Expr<'gcx>) -> bool {
167        let ExprKind::Call(callee, ..) = &expr.peel_parens().kind else { return false };
168        let ExprKind::Member(receiver, _) = &callee.peel_parens().kind else { return false };
169        self.gcx.resolved_builtin(callee) == Some(Builtin::AddressDelegatecall)
170            && !self.is_trusted_target(receiver)
171    }
172
173    /// Learns which variables are trusted when `pred` evaluates to `!negate`.
174    fn add_facts(&mut self, pred: &'gcx Expr<'gcx>, negate: bool) {
175        if !has_side_effect(pred) {
176            self.add_facts_unchecked(pred, negate);
177        }
178    }
179
180    fn add_facts_unchecked(&mut self, pred: &'gcx Expr<'gcx>, negate: bool) {
181        match &pred.peel_parens().kind {
182            ExprKind::Binary(lhs, op, rhs) => {
183                let (eq, and_op, or_op) = if negate {
184                    (BinOpKind::Ne, BinOpKind::Or, BinOpKind::And)
185                } else {
186                    (BinOpKind::Eq, BinOpKind::And, BinOpKind::Or)
187                };
188                if op.kind == and_op {
189                    self.add_facts_unchecked(lhs, negate);
190                    self.add_facts_unchecked(rhs, negate);
191                } else if op.kind == or_op {
192                    // Only facts established by both disjuncts hold.
193                    let baseline = self.safe_vars.clone();
194                    self.add_facts_unchecked(lhs, negate);
195                    let lhs_added = std::mem::replace(&mut self.safe_vars, baseline.clone());
196                    self.add_facts_unchecked(rhs, negate);
197                    let rhs_added = std::mem::replace(&mut self.safe_vars, baseline);
198                    self.safe_vars.extend(intersect(&lhs_added, &rhs_added));
199                } else if op.kind == eq {
200                    for (safe, candidate) in [(lhs, rhs), (rhs, lhs)] {
201                        if self.is_trusted_target(safe)
202                            && let Some(var) = underlying_var(self.gcx, candidate)
203                            && self.is_trusted_fact_target(var)
204                        {
205                            self.safe_vars.insert(var);
206                        }
207                    }
208                }
209            }
210            ExprKind::Unary(op, inner) if op.kind == UnOpKind::Not => {
211                self.add_facts_unchecked(inner, !negate);
212            }
213            _ => {}
214        }
215    }
216
217    /// Visits `arm` under the assumption that `cond == !negate`, returning the resulting state when
218    /// the arm falls through.
219    fn visit_arm(
220        &mut self,
221        cond: &'gcx Expr<'gcx>,
222        negate: bool,
223        arm: impl FnOnce(&mut Self) -> ControlFlow<()>,
224    ) -> Option<HashSet<VariableId>> {
225        self.add_facts(cond, negate);
226        arm(self).is_continue().then(|| self.safe_vars.clone())
227    }
228
229    /// Joins the states of two arms; `None` marks an arm that does not fall through.
230    fn join(
231        &mut self,
232        a: Option<HashSet<VariableId>>,
233        b: Option<HashSet<VariableId>>,
234    ) -> ControlFlow<()> {
235        match (a, b) {
236            (Some(a), Some(b)) => self.safe_vars = intersect(&a, &b),
237            (Some(state), None) | (None, Some(state)) => self.safe_vars = state,
238            (None, None) => return ControlFlow::Break(()),
239        }
240        ControlFlow::Continue(())
241    }
242}
243
244impl<'gcx> Visit<'gcx> for Analyzer<'gcx> {
245    type BreakValue = ();
246
247    fn hir(&self) -> &'gcx hir::Hir<'gcx> {
248        &self.gcx.hir
249    }
250
251    fn visit_stmt(&mut self, stmt: &'gcx Stmt<'gcx>) -> ControlFlow<()> {
252        match &stmt.kind {
253            StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => {
254                self.visit_stmts(block.stmts)
255            }
256            StmtKind::If(cond, then, else_) => {
257                let _ = self.visit_expr(cond);
258                let baseline = self.safe_vars.clone();
259                let then_state = self.visit_arm(cond, false, |this| this.visit_stmt(then));
260                self.safe_vars = baseline;
261                let else_state = self.visit_arm(cond, true, |this| match else_ {
262                    Some(else_) => this.visit_stmt(else_),
263                    None => ControlFlow::Continue(()),
264                });
265                self.join(then_state, else_state)
266            }
267            StmtKind::Loop(block, LoopSource::DoWhile)
268                if !stmts_break_or_continue(do_while_user_stmts(block.stmts)) =>
269            {
270                // Without `break`/`continue` the body runs straight through once, then the
271                // lowered `if (!cond) break;` condition is evaluated.
272                self.visit_stmts(do_while_user_stmts(block.stmts))?;
273                if let Some(last) = block.stmts.last()
274                    && is_loop_termination_if(last)
275                    && let StmtKind::If(cond, ..) = &last.kind
276                {
277                    let _ = self.visit_expr(cond);
278                }
279                ControlFlow::Continue(())
280            }
281            StmtKind::Loop(block, source) => {
282                // The state after the loop is what holds on every way out: never entering the
283                // body, each `break`/`continue`, and falling off the end of an iteration.
284                self.loop_exits.push(vec![self.safe_vars.clone()]);
285                let falls_through = self.visit_stmts(block.stmts).is_continue()
286                    && loop_update(*source)
287                        .is_none_or(|update| self.visit_stmt(update).is_continue());
288                let mut exits = self.loop_exits.pop().expect("loop frame");
289                if falls_through {
290                    exits.push(self.safe_vars.clone());
291                }
292                self.safe_vars =
293                    exits.iter().skip(1).fold(exits[0].clone(), |a, b| intersect(&a, b));
294                ControlFlow::Continue(())
295            }
296            StmtKind::Break | StmtKind::Continue => {
297                if let Some(exits) = self.loop_exits.last_mut() {
298                    exits.push(self.safe_vars.clone());
299                }
300                ControlFlow::Break(())
301            }
302            StmtKind::Try(stmt_try) => {
303                let _ = self.visit_expr(&stmt_try.expr);
304                let outer = self.safe_vars.clone();
305                let mut joined = None;
306                for clause in stmt_try.clauses {
307                    self.safe_vars = outer.clone();
308                    if self.visit_stmts(clause.block.stmts).is_continue() {
309                        joined = Some(match joined {
310                            Some(state) => intersect(&state, &self.safe_vars),
311                            None => self.safe_vars.clone(),
312                        });
313                    }
314                }
315                self.safe_vars = joined.unwrap_or(outer);
316                ControlFlow::Continue(())
317            }
318            StmtKind::Err(_) => {
319                self.safe_vars.clear();
320                ControlFlow::Continue(())
321            }
322            StmtKind::DeclSingle(var) => {
323                let init = self.gcx.hir.variable(*var).initializer;
324                self.assign(*var, init.is_some_and(|init| self.is_trusted_target(init)));
325                self.walk_stmt(stmt)
326            }
327            StmtKind::DeclMulti(vars, init) => {
328                let inits = tuple_elems(init);
329                for (i, var) in vars.iter().enumerate() {
330                    if let Some(var) = var {
331                        let init = inits.and_then(|elems| elems.get(i).copied().flatten());
332                        self.assign(*var, init.is_some_and(|init| self.is_trusted_target(init)));
333                    }
334                }
335                self.walk_stmt(stmt)
336            }
337            _ => {
338                let _ = self.walk_stmt(stmt);
339                if branch_always_exits(self.gcx, stmt) {
340                    ControlFlow::Break(())
341                } else {
342                    ControlFlow::Continue(())
343                }
344            }
345        }
346    }
347
348    fn visit_expr(&mut self, expr: &'gcx Expr<'gcx>) -> ControlFlow<()> {
349        if self.is_controlled_delegatecall(expr) {
350            self.hits.push(expr.span);
351        }
352        match &expr.kind {
353            ExprKind::Binary(lhs, op, rhs) if matches!(op.kind, BinOpKind::And | BinOpKind::Or) => {
354                let _ = self.visit_expr(lhs);
355                let skipped_rhs = self.safe_vars.clone();
356                let ran_rhs =
357                    self.visit_arm(lhs, op.kind == BinOpKind::Or, |this| this.visit_expr(rhs));
358                self.join(Some(skipped_rhs), ran_rhs)
359            }
360            ExprKind::Ternary(cond, if_true, if_false) => {
361                let _ = self.visit_expr(cond);
362                let baseline = self.safe_vars.clone();
363                let true_state = self.visit_arm(cond, false, |this| this.visit_expr(if_true));
364                self.safe_vars = baseline;
365                let false_state = self.visit_arm(cond, true, |this| this.visit_expr(if_false));
366                self.join(true_state, false_state)
367            }
368            ExprKind::Call(callee, args, _) if is_require_or_assert(self.gcx, callee) => {
369                let _ = self.walk_expr(expr);
370                let mut args = args.exprs();
371                if let Some(cond) = args.next()
372                    && !args.any(has_side_effect)
373                {
374                    self.add_facts(cond, false);
375                }
376                ControlFlow::Continue(())
377            }
378            ExprKind::Assign(lhs, op, rhs) => {
379                let _ = self.walk_expr(expr);
380                self.handle_assign(lhs, *op, rhs);
381                ControlFlow::Continue(())
382            }
383            ExprKind::Delete(target) => {
384                // `delete` zeroes the target, and the zero address is trusted.
385                if let Some(var) = underlying_var(self.gcx, target) {
386                    self.assign(var, true);
387                }
388                self.walk_expr(expr)
389            }
390            _ => self.walk_expr(expr),
391        }
392    }
393}
394
395/// The variable a bare identifier refers to, looking through parens, `payable(...)` and
396/// address-like or numeric casts.
397fn underlying_var(gcx: Gcx<'_>, expr: &Expr<'_>) -> Option<VariableId> {
398    match &expr.peel_parens().kind {
399        ExprKind::Ident(_) => gcx.resolved_variable(expr),
400        ExprKind::Call(callee, args, _) if is_cast(gcx, callee) => {
401            args.exprs().next().and_then(|expr| underlying_var(gcx, expr))
402        }
403        ExprKind::Payable(inner) => underlying_var(gcx, inner),
404        _ => None,
405    }
406}
407
408/// `address(..)`, `IFoo(..)`, `uintN(..)`, `intN(..)` or `bytes(..)` cast head.
409fn is_cast(gcx: Gcx<'_>, callee: &Expr<'_>) -> bool {
410    is_address_like_cast(gcx, callee)
411        || matches!(
412            &callee.peel_parens().kind,
413            ExprKind::Type(hir::Type {
414                kind: TypeKind::Elementary(
415                    ElementaryType::Int(_) | ElementaryType::UInt(_) | ElementaryType::Bytes
416                ),
417                ..
418            })
419        )
420}
421
422/// The expression returned by a non-virtual, non-overriding, parameterless helper whose body is a
423/// single `return <expr>;` or `<ret> = <expr>;` (optionally followed by a bare `return;`).
424fn no_arg_helper_return<'gcx>(
425    gcx: Gcx<'gcx>,
426    callee: &'gcx Expr<'gcx>,
427) -> Option<&'gcx Expr<'gcx>> {
428    let fid = gcx
429        .resolved_function(callee)
430        .filter(|_| matches!(callee.peel_parens().kind, ExprKind::Ident(_)))?;
431    let func = gcx.hir.function(fid);
432    if func.virtual_ || func.override_ || !func.parameters.is_empty() {
433        return None;
434    }
435    let body = func.body?;
436    let stmts = match body.stmts.split_last() {
437        Some((last, rest)) if matches!(last.kind, StmtKind::Return(None)) => rest,
438        _ => body.stmts,
439    };
440    let [stmt] = stmts else { return None };
441    match &stmt.kind {
442        StmtKind::Return(Some(expr)) => Some(expr),
443        StmtKind::Expr(expr) => match &expr.peel_parens().kind {
444            ExprKind::Assign(lhs, None, rhs)
445                if func.returns.len() == 1 && underlying_var(gcx, lhs) == Some(func.returns[0]) =>
446            {
447                Some(rhs)
448            }
449            _ => None,
450        },
451        _ => None,
452    }
453}
454
455/// Caller variables proven trusted by the statements a modifier runs before `_`: a parameter that
456/// is bound to the variable, never reassigned in the prefix, and safe when `_` is reached.
457fn modifier_safe_vars<'gcx>(
458    gcx: Gcx<'gcx>,
459    invocation: &'gcx hir::Modifier<'gcx>,
460) -> Vec<VariableId> {
461    let Some(fid) = invocation.id.as_function() else { return Vec::new() };
462    let modifier = gcx.hir.function(fid);
463    let Some(body) = modifier.body else { return Vec::new() };
464    let mut prefix = Vec::new();
465    if modifier.kind != FunctionKind::Modifier
466        || count_placeholders(body.stmts) != 1
467        || stmts_before_placeholder(body.stmts, &mut prefix).is_none()
468    {
469        return Vec::new();
470    }
471    let bindings: Vec<_> = modifier
472        .parameters
473        .iter()
474        .filter_map(|&param| {
475            let arg = arg_for_param(gcx, fid, param, &invocation.args)?;
476            Some((param, underlying_var(gcx, arg)?))
477        })
478        .collect();
479    if bindings.is_empty() {
480        return Vec::new();
481    }
482
483    let mut analyzer = Analyzer::new(gcx);
484    let _ = prefix.iter().try_for_each(|stmt| analyzer.visit_stmt(stmt));
485    bindings
486        .into_iter()
487        .filter(|&(param, caller_var)| {
488            !analyzer.assigned.contains(&param)
489                && analyzer.safe_vars.contains(&param)
490                && analyzer.is_trusted_fact_target(caller_var)
491        })
492        .map(|(_, caller_var)| caller_var)
493        .collect()
494}