Skip to main content

forge_lint/sol/high/
arbitrary_send_eth.rs

1use super::ArbitrarySendEth;
2use crate::{
3    linter::{LateLintPass, LintContext},
4    sol::{
5        Severity, SolLint,
6        analysis::{
7            DEFAULT_HELPER_ANALYSIS_CACHE_LIMIT, HelperAnalysisCache, arg_for_param,
8            branch_always_exits, callee_no_arg_returns, function_no_arg_returns,
9            is_address_like_cast, is_address_self, is_builtin, is_contract_cast, is_literal_zero,
10            is_msg_sender, is_require_or_assert, is_sender_member, loop_stmts, loop_update,
11            modifier_prefix, referenced_item, tuple_elems, underlying_var, var_is_address_like,
12        },
13    },
14};
15use solar::{
16    ast::{BinOpKind, LitKind, StateMutability, UnOpKind},
17    interface::{Span, data_structures::Never, sym},
18    sema::{
19        Gcx,
20        builtins::Builtin,
21        hir::{
22            self, CallArgs, ContractId, ContractKind, ElementaryType, Expr, ExprKind, FunctionId,
23            Hir, ItemId, LoopSource, Modifier, Res, Stmt, StmtKind, TypeKind, Variable, VariableId,
24            Visit,
25        },
26        ty::TyKind,
27    },
28};
29use std::{collections::HashSet, ops::ControlFlow};
30
31declare_forge_lint!(
32    ARBITRARY_SEND_ETH,
33    Severity::High,
34    "arbitrary-send-eth",
35    "ETH is sent to a user-controlled destination; restrict the destination or the caller"
36);
37
38/// Recursion budget for `_msgSender()`-style helper chains.
39const HELPER_DEPTH: u8 = 3;
40
41/// Recursion budget for self-alias chains.
42const SELF_ALIAS_DEPTH: u8 = 8;
43
44/// Cap on inlined helper calls (covers `ctor → _init → _initInner → _initLeaf`).
45const HELPER_CALL_DEPTH: usize = 4;
46
47impl<'gcx> LateLintPass<'gcx> for ArbitrarySendEth {
48    fn check_function(
49        &mut self,
50        ctx: &LintContext,
51        gcx: Gcx<'gcx>,
52        func: &'gcx hir::Function<'gcx>,
53    ) {
54        if matches!(func.state_mutability, StateMutability::Pure | StateMutability::View)
55            || func.is_constructor()
56            || func.contract.is_some_and(|cid| gcx.hir.contract(cid).kind == ContractKind::Library)
57        {
58            return;
59        }
60        let Some(body) = func.body else { return };
61
62        // Modifier arguments are evaluated by the caller before any modifier guard runs.
63        let mut args = Analyzer::new(gcx);
64        for arg in func.modifiers.iter().flat_map(|m| m.args.exprs()) {
65            let _ = args.visit_expr(arg);
66        }
67        for span in args.hits {
68            ctx.emit(&ARBITRARY_SEND_ETH, span);
69        }
70
71        let mut a = Analyzer::new(gcx);
72        for m in func.modifiers {
73            a.hoist_modifier_facts(m);
74        }
75        a.visit_stmts(body.stmts);
76        if !a.hits.is_empty() && !func.modifiers.iter().any(|m| a.guards.modifier_restricts(m)) {
77            for span in a.hits {
78                ctx.emit(&ARBITRARY_SEND_ETH, span);
79            }
80        }
81    }
82}
83
84/// Path-sensitive facts.
85#[derive(Clone, Default)]
86struct State {
87    /// Locals (and function pointers) proven to denote a safe destination on this path.
88    safe_vars: HashSet<VariableId>,
89    /// True once a caller-restricting guard has fired on this path.
90    caller_restricted: bool,
91}
92
93impl State {
94    fn meet(&self, other: &Self) -> Self {
95        Self {
96            safe_vars: self.safe_vars.intersection(&other.safe_vars).copied().collect(),
97            caller_restricted: self.caller_restricted && other.caller_restricted,
98        }
99    }
100}
101
102struct Analyzer<'gcx> {
103    gcx: Gcx<'gcx>,
104    guards: CallerGuards<'gcx>,
105    state: State,
106    /// States at `break`/`continue` of each enclosing loop, innermost last.
107    loop_exits: Vec<Vec<State>>,
108    /// Every variable written on any path.
109    written: HashSet<VariableId>,
110    hits: Vec<Span>,
111}
112
113impl<'gcx> Analyzer<'gcx> {
114    fn new(gcx: Gcx<'gcx>) -> Self {
115        Self {
116            gcx,
117            guards: CallerGuards::new(gcx),
118            state: State::default(),
119            loop_exits: Vec::new(),
120            written: HashSet::new(),
121            hits: Vec::new(),
122        }
123    }
124
125    /// Hoists `require(param == msg.sender)`-style guards from the prefix of modifier `m` onto
126    /// the caller's argument variables.
127    fn hoist_modifier_facts(&mut self, m: &'gcx Modifier<'gcx>) {
128        let ItemId::Function(fid) = m.id else { return };
129        let Some(prefix) = modifier_prefix(&self.gcx.hir, fid) else { return };
130        let modifier = self.gcx.hir.function(fid);
131        let mut a = Self::new(self.gcx);
132        for stmt in prefix {
133            a.stmt(stmt);
134        }
135        for &param in modifier.parameters {
136            if a.state.safe_vars.contains(&param)
137                && !a.written.contains(&param)
138                && let Some(caller) = arg_for_param(self.gcx, fid, param, &m.args)
139                    .and_then(|expr| underlying_var(self.gcx, expr))
140                && self.is_safe_target(caller)
141            {
142                self.state.safe_vars.insert(caller);
143            }
144        }
145    }
146
147    /// True when `expr` denotes a destination that is fixed at deploy time or is the caller
148    /// itself: `msg.sender`, `tx.origin`, `address(this)`, address or zero literals,
149    /// `immutable`/`constant` state, tracked locals and `this.f` function pointers.
150    fn is_safe(&self, expr: &'gcx Expr<'gcx>) -> bool {
151        self.is_safe_inner(expr, HELPER_DEPTH)
152    }
153
154    fn is_safe_inner(&self, expr: &'gcx Expr<'gcx>, depth: u8) -> bool {
155        let expr = peel_casts(self.gcx, expr);
156        match &expr.kind {
157            ExprKind::Member(base, _) => {
158                is_sender_member(self.gcx, expr) || is_address_self(self.gcx, base)
159            }
160            ExprKind::Lit(_) => is_trusted_literal(expr),
161            ExprKind::Ident(_) => {
162                is_builtin(self.gcx, expr, sym::this)
163                    || self.gcx.resolved_variable(expr).is_some_and(|v| self.is_safe_var(v))
164            }
165            ExprKind::Ternary(_, t, f) => {
166                self.is_safe_inner(t, depth) && self.is_safe_inner(f, depth)
167            }
168            ExprKind::Call(callee, args, _) => {
169                depth > 0
170                    && args.exprs().next().is_none()
171                    && callee_no_arg_returns(self.gcx, callee, |e| self.is_safe_inner(e, depth - 1))
172            }
173            _ => false,
174        }
175    }
176
177    fn is_safe_var(&self, v: VariableId) -> bool {
178        let var = self.gcx.hir.variable(v);
179        self.state.safe_vars.contains(&v)
180            || (var.kind.is_state() && (var.is_immutable() || var.is_constant()))
181    }
182
183    /// Only locals and `immutable`/`constant` state can carry a safe-fact: mutable storage may be
184    /// rewritten between the check and the sink.
185    fn is_safe_target(&self, v: VariableId) -> bool {
186        let var = self.gcx.hir.variable(v);
187        !var.kind.is_state() || var.is_immutable() || var.is_constant()
188    }
189
190    /// `target = rhs`; `rhs == None` is an unknown value.
191    fn assign_var(&mut self, target: VariableId, rhs: Option<&'gcx Expr<'gcx>>) {
192        self.written.insert(target);
193        self.state.safe_vars.remove(&target);
194        if !self.gcx.hir.variable(target).kind.is_state() && rhs.is_some_and(|r| self.is_safe(r)) {
195            self.state.safe_vars.insert(target);
196        }
197    }
198
199    /// Handles single and tuple LHS; tuple slots align with a tuple-literal RHS.
200    fn assign_lhs(&mut self, lhs: &'gcx Expr<'gcx>, rhs: Option<&'gcx Expr<'gcx>>) {
201        if let Some(elems) = tuple_elems(lhs) {
202            let rhs = rhs.and_then(tuple_elems);
203            for (i, lhs) in elems.iter().enumerate() {
204                if let Some(lhs) = lhs {
205                    self.assign_lhs(lhs, rhs.and_then(|r| r.get(i).copied().flatten()));
206                }
207            }
208        } else if let Some(v) = underlying_var(self.gcx, lhs) {
209            self.assign_var(v, rhs);
210        }
211    }
212
213    /// Records variables proven equal to a safe destination by `pred` (`!pred` when `negate`).
214    fn add_facts(&mut self, pred: &'gcx Expr<'gcx>, negate: bool) {
215        match &pred.peel_parens().kind {
216            ExprKind::Binary(lhs, op, rhs) => {
217                let (eq, and, or) = if negate {
218                    (BinOpKind::Ne, BinOpKind::Or, BinOpKind::And)
219                } else {
220                    (BinOpKind::Eq, BinOpKind::And, BinOpKind::Or)
221                };
222                if op.kind == and {
223                    self.add_facts(lhs, negate);
224                    self.add_facts(rhs, negate);
225                } else if op.kind == or {
226                    // Only facts established by both disjuncts hold.
227                    let before = self.state.clone();
228                    self.add_facts(lhs, negate);
229                    let after_lhs = std::mem::replace(&mut self.state, before);
230                    self.add_facts(rhs, negate);
231                    self.state = after_lhs.meet(&self.state);
232                } else if op.kind == eq {
233                    for (a, b) in [(lhs, rhs), (rhs, lhs)] {
234                        if self.is_safe(a)
235                            && let Some(v) = underlying_var(self.gcx, b)
236                            && self.is_safe_target(v)
237                        {
238                            self.state.safe_vars.insert(v);
239                        }
240                    }
241                }
242            }
243            ExprKind::Unary(op, inner) if op.kind == UnOpKind::Not => {
244                self.add_facts(inner, !negate);
245            }
246            _ => {}
247        }
248    }
249
250    /// Applies a guard known to hold (`holds`) or fail on the current path.
251    fn note_guard(&mut self, cond: &'gcx Expr<'gcx>, holds: bool) {
252        self.add_facts(cond, !holds);
253        if self.guards.cond_restricts(cond, holds) {
254            self.state.caller_restricted = true;
255        }
256    }
257
258    /// Visits `stmts` up to the first that cannot fall through; returns whether the end is
259    /// reachable.
260    fn visit_stmts(&mut self, stmts: &'gcx [Stmt<'gcx>]) -> bool {
261        stmts.iter().all(|s| self.stmt(s))
262    }
263
264    /// Visits `stmt`, returning whether control can fall through it.
265    fn stmt(&mut self, stmt: &'gcx Stmt<'gcx>) -> bool {
266        match &stmt.kind {
267            StmtKind::Block(b) | StmtKind::UncheckedBlock(b) => return self.visit_stmts(b.stmts),
268            StmtKind::Break | StmtKind::Continue => {
269                let state = self.state.clone();
270                if let Some(exits) = self.loop_exits.last_mut() {
271                    exits.push(state);
272                }
273                return false;
274            }
275            StmtKind::If(cond, then, else_) => {
276                let _ = self.visit_expr(cond);
277                let before = self.state.clone();
278                self.note_guard(cond, true);
279                let then_falls = self.stmt(then);
280                let after_then = std::mem::replace(&mut self.state, before);
281                self.note_guard(cond, false);
282                let else_falls = else_.is_none_or(|e| self.stmt(e));
283                match (then_falls, else_falls) {
284                    (true, true) => self.state = after_then.meet(&self.state),
285                    (true, false) => self.state = after_then,
286                    _ => {}
287                }
288                return then_falls || else_falls;
289            }
290            StmtKind::Loop(block, source) => {
291                // Only facts holding on every exit survive. `for`/`while` bodies may not run at
292                // all; `do-while` bodies run at least once.
293                let baseline = (!matches!(source, LoopSource::DoWhile)).then(|| self.state.clone());
294                self.loop_exits.push(baseline.into_iter().collect());
295                if self.visit_stmts(block.stmts)
296                    && loop_update(*source).is_none_or(|update| self.stmt(update))
297                {
298                    let state = self.state.clone();
299                    self.loop_exits.last_mut().expect("pushed above").push(state);
300                }
301                let exits = self.loop_exits.pop().expect("pushed above");
302                let falls = !exits.is_empty();
303                if let Some(joined) = exits.into_iter().reduce(|a, b| a.meet(&b)) {
304                    self.state = joined;
305                }
306                return falls;
307            }
308            StmtKind::Try(t) => {
309                let _ = self.visit_expr(&t.expr);
310                let outer = self.state.clone();
311                let mut joined = None::<State>;
312                for clause in t.clauses {
313                    self.state = outer.clone();
314                    if self.visit_stmts(clause.block.stmts) {
315                        joined = Some(
316                            joined.map_or_else(|| self.state.clone(), |j| j.meet(&self.state)),
317                        );
318                    }
319                }
320                let falls = joined.is_some();
321                self.state = joined.unwrap_or(outer);
322                return falls;
323            }
324            StmtKind::DeclSingle(vid) => {
325                if let Some(init) = self.gcx.hir.variable(*vid).initializer {
326                    self.assign_var(*vid, Some(init));
327                }
328            }
329            StmtKind::DeclMulti(vars, init) => {
330                for (vid, rhs) in vars.iter().zip(tuple_elems(init).into_iter().flatten()) {
331                    if let (Some(vid), Some(rhs)) = (vid, rhs) {
332                        self.assign_var(*vid, Some(rhs));
333                    }
334                }
335            }
336            _ => {}
337        }
338        let _ = self.walk_stmt(stmt);
339        !branch_always_exits(self.gcx, stmt)
340    }
341}
342
343impl<'gcx> Visit<'gcx> for Analyzer<'gcx> {
344    type BreakValue = Never;
345
346    fn hir(&self) -> &'gcx Hir<'gcx> {
347        &self.gcx.hir
348    }
349
350    fn visit_stmt(&mut self, stmt: &'gcx Stmt<'gcx>) -> ControlFlow<Never> {
351        self.stmt(stmt);
352        ControlFlow::Continue(())
353    }
354
355    fn visit_expr(&mut self, expr: &'gcx Expr<'gcx>) -> ControlFlow<Never> {
356        match &expr.kind {
357            // `rhs` may not execute: its facts and writes survive only if they also hold without
358            // it, while `lhs` facts flow into `rhs`.
359            ExprKind::Binary(lhs, op, rhs) if matches!(op.kind, BinOpKind::And | BinOpKind::Or) => {
360                let _ = self.visit_expr(lhs);
361                let skipped = self.state.clone();
362                self.add_facts(lhs, op.kind == BinOpKind::Or);
363                let _ = self.visit_expr(rhs);
364                self.state = skipped.meet(&self.state);
365            }
366            ExprKind::Ternary(cond, t, f) => {
367                let _ = self.visit_expr(cond);
368                let before = self.state.clone();
369                self.add_facts(cond, false);
370                let _ = self.visit_expr(t);
371                let after_t = std::mem::replace(&mut self.state, before);
372                self.add_facts(cond, true);
373                let _ = self.visit_expr(f);
374                self.state = after_t.meet(&self.state);
375            }
376            ExprKind::Call(callee, args, _) if is_require_or_assert(self.gcx, callee) => {
377                // Sinks inside the predicate run before the guard takes effect.
378                let _ = self.walk_expr(expr);
379                if let Some(cond) = args.exprs().next() {
380                    self.note_guard(cond, true);
381                }
382            }
383            ExprKind::Call(..) => {
384                if !self.state.caller_restricted
385                    && let Some(dest) = match_sink(self.gcx, expr)
386                    && !self.is_safe(dest)
387                {
388                    self.hits.push(expr.span);
389                }
390                let _ = self.walk_expr(expr);
391            }
392            ExprKind::Assign(lhs, _, rhs) => {
393                self.assign_lhs(lhs, Some(rhs));
394                let _ = self.walk_expr(expr);
395            }
396            ExprKind::Delete(target) => {
397                self.assign_lhs(target, None);
398                let _ = self.walk_expr(expr);
399            }
400            _ => {
401                let _ = self.walk_expr(expr);
402            }
403        }
404        ControlFlow::Continue(())
405    }
406}
407
408/// Destination of an ETH-sending call: `selfdestruct(x)`, `x.{call,send,transfer}`,
409/// `f{value: v}()`, `IFoo(x).f{value: v}()` and common OpenZeppelin/Solady helpers. Sends to
410/// `address(this)` or of a literal-zero amount are not sinks.
411fn match_sink<'gcx>(gcx: Gcx<'gcx>, expr: &'gcx Expr<'gcx>) -> Option<&'gcx Expr<'gcx>> {
412    let ExprKind::Call(callee, args, opts) = &expr.kind else { return None };
413    let callee = callee.peel_parens();
414    if gcx.resolved_builtin(callee) == Some(Builtin::Selfdestruct) {
415        return args.exprs().next().filter(|dest| !is_address_self(gcx, dest));
416    }
417    if opts.is_some_and(|o| {
418        o.args.iter().any(|a| a.name.name == sym::value && !is_literal_zero(&a.value))
419    }) {
420        return match &callee.kind {
421            ExprKind::Member(recv, _) => (!is_address_self(gcx, recv)).then_some(recv),
422            _ => expr_is_function(gcx, callee).then_some(callee),
423        };
424    }
425    let ExprKind::Member(recv, member) = &callee.kind else { return None };
426    if matches!(
427        gcx.resolved_builtin(callee),
428        Some(Builtin::AddressPayableTransfer | Builtin::AddressPayableSend)
429    ) {
430        return (!is_address_self(gcx, recv) && !is_literal_zero(args.exprs().next()?))
431            .then_some(recv);
432    }
433    match_eth_library_call(gcx, expr, recv, member.name.as_str(), args)
434}
435
436/// Destination of an OpenZeppelin `Address` / Solady `SafeTransferLib` ETH helper, called either
437/// statically (`Lib.f(to, ...)`) or via `using ... for address` (`to.f(...)`).
438fn match_eth_library_call<'gcx>(
439    gcx: Gcx<'gcx>,
440    call: &Expr<'gcx>,
441    recv: &'gcx Expr<'gcx>,
442    name: &str,
443    args: &'gcx CallArgs<'gcx>,
444) -> Option<&'gcx Expr<'gcx>> {
445    // Amount position and accepted arities, in the static form.
446    let (amount, arities): (Option<usize>, &[usize]) = match name {
447        "sendValue" | "safeTransferETH" | "safeMoveETH" => (Some(1), &[2]),
448        "forceSafeTransferETH" => (Some(1), &[2, 3]),
449        "trySafeTransferETH" => (Some(1), &[3]),
450        "functionCallWithValue" => (Some(2), &[3, 4]),
451        "safeTransferAllETH" => (None, &[1]),
452        "forceSafeTransferAllETH" => (None, &[1, 2]),
453        "trySafeTransferAllETH" => (None, &[2]),
454        _ => return None,
455    };
456    let using = gcx.resolved_call(call).is_some_and(|resolved| resolved.attached);
457    let is_lib = matches!(referenced_item(gcx, recv), Some(ItemId::Contract(cid))
458        if gcx.hir.contract(cid).kind == ContractKind::Library);
459    if (!using && !is_lib) || !arities.contains(&(args.len() + usize::from(using))) {
460        return None;
461    }
462    let dest = if using { recv } else { gcx.call_arg(call, 0)? };
463    if let Some(i) = amount
464        && is_literal_zero(gcx.call_arg(call, i - usize::from(using))?)
465    {
466        return None;
467    }
468    (!is_address_self(gcx, dest)).then_some(dest)
469}
470
471/// Recognises guards that restrict `msg.sender` to a deploy-time-fixed principal, backed by a
472/// memoised analysis of which state variables may alias `address(this)`.
473struct CallerGuards<'gcx> {
474    gcx: Gcx<'gcx>,
475    alias_cache: HelperAnalysisCache<(VariableId, u8), bool>,
476    /// Functions currently being inlined, to stop recursion.
477    stack: Vec<FunctionId>,
478}
479
480impl<'gcx> CallerGuards<'gcx> {
481    fn new(gcx: Gcx<'gcx>) -> Self {
482        Self {
483            gcx,
484            alias_cache: HelperAnalysisCache::new(DEFAULT_HELPER_ANALYSIS_CACHE_LIMIT),
485            stack: Vec::new(),
486        }
487    }
488
489    /// True when modifier `m` reverts unless `msg.sender` is a trusted principal.
490    fn modifier_restricts(&mut self, m: &Modifier<'_>) -> bool {
491        let ItemId::Function(fid) = m.id else { return false };
492        modifier_prefix(&self.gcx.hir, fid)
493            .is_some_and(|prefix| prefix.into_iter().any(|s| self.stmt_restricts(s)))
494    }
495
496    fn stmt_restricts(&mut self, stmt: &'gcx Stmt<'gcx>) -> bool {
497        match &stmt.kind {
498            StmtKind::Expr(e) => self.expr_restricts(e),
499            StmtKind::Block(b) | StmtKind::UncheckedBlock(b) => {
500                b.stmts.iter().any(|s| self.stmt_restricts(s))
501            }
502            StmtKind::If(cond, then, else_) => {
503                let then_exits = branch_always_exits(self.gcx, then);
504                let else_exits = else_.is_some_and(|e| branch_always_exits(self.gcx, e));
505                // `if (!guard) revert;` restricts by itself; otherwise every non-exiting branch
506                // must restrict.
507                (then_exits != else_exits && self.cond_restricts(cond, else_exits))
508                    || ((then_exits || self.stmt_restricts(then))
509                        && (else_exits || else_.is_some_and(|e| self.stmt_restricts(e))))
510            }
511            _ => false,
512        }
513    }
514
515    /// `require(guard)` / `assert(guard)`, or a call to an internal helper whose body restricts
516    /// the caller and cannot `return` early.
517    fn expr_restricts(&mut self, expr: &'gcx Expr<'gcx>) -> bool {
518        let ExprKind::Call(callee, args, _) = &expr.peel_parens().kind else { return false };
519        if is_require_or_assert(self.gcx, callee) {
520            return args.exprs().next().is_some_and(|c| self.cond_restricts(c, true));
521        }
522        self.gcx
523            .resolved_function(callee)
524            .filter(|_| matches!(callee.peel_parens().kind, ExprKind::Ident(_)))
525            .is_some_and(|fid| {
526                if self.stack.contains(&fid) {
527                    return false;
528                }
529                let Some(body) = self.gcx.hir.function(fid).body else { return false };
530                // A trailing bare `return;` is a normal exit and cannot bypass an earlier guard.
531                let mut stmts = body.stmts;
532                while let [rest @ .., last] = stmts
533                    && matches!(last.kind, StmtKind::Return(None))
534                {
535                    stmts = rest;
536                }
537                if stmts.iter().any(stmt_contains_return) {
538                    return false;
539                }
540                self.stack.push(fid);
541                let restricts = stmts.iter().any(|s| self.stmt_restricts(s));
542                self.stack.pop();
543                restricts
544            })
545    }
546
547    /// True when `cond` (holding iff `holds`) entails `msg.sender == <trusted>` on every path.
548    fn cond_restricts(&mut self, cond: &'gcx Expr<'gcx>, holds: bool) -> bool {
549        match &cond.peel_parens().kind {
550            ExprKind::Binary(lhs, op, rhs) => {
551                let (eq, any, all) = if holds {
552                    (BinOpKind::Eq, BinOpKind::And, BinOpKind::Or)
553                } else {
554                    (BinOpKind::Ne, BinOpKind::Or, BinOpKind::And)
555                };
556                if op.kind == any {
557                    self.cond_restricts(lhs, holds) || self.cond_restricts(rhs, holds)
558                } else if op.kind == all {
559                    self.cond_restricts(lhs, holds) && self.cond_restricts(rhs, holds)
560                } else if op.kind == eq {
561                    [(lhs, rhs), (rhs, lhs)].into_iter().any(|(a, b)| {
562                        is_msg_sender_like(self.gcx, a, HELPER_DEPTH)
563                            && self.is_trusted_principal(b, HELPER_DEPTH)
564                    })
565                } else {
566                    false
567                }
568            }
569            ExprKind::Unary(op, inner) if op.kind == UnOpKind::Not => {
570                self.cond_restricts(inner, !holds)
571            }
572            _ => false,
573        }
574    }
575
576    /// Conservatively recognises deploy-time-fixed caller principals: address/zero literals and
577    /// state (or statically indexed state) that cannot alias `address(this)`, possibly behind a
578    /// no-arg getter. Parameters, locals, `msg.sender`, `tx.origin` and `this` are rejected.
579    fn is_trusted_principal(&mut self, expr: &'gcx Expr<'gcx>, depth: u8) -> bool {
580        let expr = peel_casts(self.gcx, expr);
581        match &expr.kind {
582            ExprKind::Lit(_) => is_trusted_literal(expr),
583            ExprKind::Ident(_) => self.gcx.resolved_variable(expr).is_some_and(|v| {
584                self.gcx.hir.variable(v).kind.is_state()
585                    && !self.state_var_aliases_self(v, SELF_ALIAS_DEPTH)
586            }),
587            ExprKind::Member(base, _) => self.is_trusted_principal(base, depth),
588            ExprKind::Index(base, idx) => {
589                self.is_trusted_principal(base, depth)
590                    && idx.is_none_or(|i| index_is_static(self.gcx, i))
591            }
592            ExprKind::Call(callee, args, _) => {
593                depth > 0
594                    && args.exprs().next().is_none()
595                    && callee_no_arg_returns(self.gcx, callee, |e| {
596                        self.is_trusted_principal(e, depth - 1)
597                    })
598            }
599            _ => false,
600        }
601    }
602
603    /// True when state variable `v` may hold `address(this)`: through its initializer or an
604    /// assignment in any function of its contract or a derived contract.
605    fn state_var_aliases_self(&mut self, v: VariableId, depth: u8) -> bool {
606        let var = self.gcx.hir.variable(v);
607        if depth == 0 || !var.kind.is_state() {
608            return false;
609        }
610        let key = (v, depth);
611        if let Some(cached) = self.alias_cache.get(&key) {
612            return *cached;
613        }
614        if self.alias_cache.is_in_progress(&key) {
615            return false;
616        }
617        self.alias_cache.start(key);
618        let aliases = var
619            .initializer
620            .is_some_and(|init| self.rhs_carries_self(var, init, depth - 1, &HashSet::new()))
621            || var.contract.is_some_and(|cid| {
622                self.gcx.hir.contracts_enumerated().any(|(c, contract)| {
623                    (c == cid || contract.linearized_bases.contains(&cid))
624                        && self.contract_assigns_self(c, v, depth - 1)
625                })
626            });
627        self.alias_cache.finish(key, aliases);
628        aliases
629    }
630
631    /// Whether assigning `rhs` to `target` may plant `address(this)` in it: an address-typed
632    /// target must receive `address(this)` itself, an aggregate may embed it anywhere.
633    fn rhs_carries_self(
634        &mut self,
635        target: &Variable<'_>,
636        rhs: &'gcx Expr<'gcx>,
637        depth: u8,
638        locals: &HashSet<VariableId>,
639    ) -> bool {
640        if var_is_address_like(target) {
641            self.expr_resolves_to_self(rhs, depth)
642                || lhs_root_var(self.gcx, rhs).is_some_and(|v| locals.contains(&v))
643        } else {
644            self.expr_may_contain_self(rhs, depth, locals)
645        }
646    }
647
648    /// True when `expr` may embed `address(this)` (or a local carrying it) anywhere.
649    fn expr_may_contain_self(
650        &mut self,
651        expr: &'gcx Expr<'gcx>,
652        depth: u8,
653        locals: &HashSet<VariableId>,
654    ) -> bool {
655        if self.expr_resolves_to_self(expr, depth)
656            || lhs_root_var(self.gcx, expr).is_some_and(|v| locals.contains(&v))
657        {
658            return true;
659        }
660        if depth == 0 {
661            return false;
662        }
663        let children: Vec<&'gcx Expr<'gcx>> = match &peel_casts(self.gcx, expr).kind {
664            ExprKind::Call(_, args, _) => args.exprs().collect(),
665            ExprKind::Ternary(_, t, f) => vec![t, f],
666            ExprKind::Tuple(elems) => elems.iter().copied().flatten().collect(),
667            ExprKind::Array(elems) => elems.iter().collect(),
668            _ => Vec::new(),
669        };
670        children.into_iter().any(|e| self.expr_may_contain_self(e, depth - 1, locals))
671    }
672
673    /// True when `expr` may evaluate to `address(this)`.
674    fn expr_resolves_to_self(&mut self, expr: &'gcx Expr<'gcx>, depth: u8) -> bool {
675        let expr = peel_casts(self.gcx, expr);
676        if is_address_self(self.gcx, expr) {
677            return true;
678        }
679        if depth == 0 {
680            return false;
681        }
682        match &expr.kind {
683            ExprKind::Ident(_) | ExprKind::Member(..) | ExprKind::Index(..) => {
684                lhs_root_var(self.gcx, expr).is_some_and(|v| self.state_var_aliases_self(v, depth))
685            }
686            ExprKind::Call(callee, args, _) if args.exprs().next().is_none() => {
687                self.gcx.resolved_function(callee).is_some_and(|fid| {
688                    function_no_arg_returns(self.gcx, fid, &mut |e| {
689                        self.expr_resolves_to_self(e, depth - 1)
690                    })
691                })
692            }
693            ExprKind::Call(callee, args, _) => identity_helper_arg(self.gcx, callee, args)
694                .is_some_and(|a| self.expr_resolves_to_self(a, depth - 1)),
695            ExprKind::Ternary(_, t, f) => {
696                self.expr_resolves_to_self(t, depth - 1) || self.expr_resolves_to_self(f, depth - 1)
697            }
698            ExprKind::Assign(_, _, rhs) => self.expr_resolves_to_self(rhs, depth - 1),
699            _ => false,
700        }
701    }
702
703    /// Scans every function of `cid` for an assignment that may plant `address(this)` in `v`.
704    fn contract_assigns_self(&mut self, cid: ContractId, v: VariableId, depth: u8) -> bool {
705        self.gcx.hir.contract(cid).all_functions().any(|fid| {
706            let mut scan = SelfAssignScan {
707                guards: &mut *self,
708                target: v,
709                depth,
710                found: false,
711                stack: Vec::new(),
712                locals: HashSet::new(),
713            };
714            scan.scan_function(fid, None);
715            scan.found
716        })
717    }
718}
719
720/// Scans one function, its modifiers / base constructors and inlined internal helpers for an
721/// assignment that may plant `address(this)` into `target`.
722struct SelfAssignScan<'a, 'gcx> {
723    guards: &'a mut CallerGuards<'gcx>,
724    target: VariableId,
725    depth: u8,
726    found: bool,
727    stack: Vec<FunctionId>,
728    /// Locals that may (path-insensitively) carry `address(this)`.
729    locals: HashSet<VariableId>,
730}
731
732impl<'gcx> SelfAssignScan<'_, 'gcx> {
733    fn may_contain_self(&mut self, expr: &'gcx Expr<'gcx>) -> bool {
734        self.guards.expr_may_contain_self(expr, self.depth, &self.locals)
735    }
736
737    fn note_local(&mut self, v: VariableId, rhs: &'gcx Expr<'gcx>) {
738        if !self.guards.gcx.hir.variable(v).kind.is_state() && self.may_contain_self(rhs) {
739            self.locals.insert(v);
740        }
741    }
742
743    fn assign(&mut self, lhs: &'gcx Expr<'gcx>, rhs: &'gcx Expr<'gcx>) {
744        if let Some(elems) = tuple_elems(lhs) {
745            let rhs = tuple_elems(rhs);
746            for (i, lhs) in elems.iter().enumerate() {
747                if let Some(lhs) = lhs
748                    && let Some(rhs) = rhs.and_then(|r| r.get(i).copied().flatten())
749                {
750                    self.assign(lhs, rhs);
751                }
752            }
753        } else if let Some(v) = lhs_root_var(self.guards.gcx, lhs) {
754            if v == self.target {
755                let var = self.guards.gcx.hir.variable(v);
756                self.found |= self.guards.rhs_carries_self(var, rhs, self.depth, &self.locals);
757            } else {
758                self.note_local(v, rhs);
759            }
760        }
761    }
762
763    /// Scans `fid`, seeding its parameters from `args` when given.
764    fn scan_function(&mut self, fid: FunctionId, args: Option<&'gcx CallArgs<'gcx>>) {
765        if self.found || self.stack.len() >= HELPER_CALL_DEPTH || self.stack.contains(&fid) {
766            return;
767        }
768        let f = self.guards.gcx.hir.function(fid);
769        let Some(body) = f.body else { return };
770        let saved = self.locals.clone();
771        for &param in f.parameters {
772            if let Some(arg) =
773                args.and_then(|args| arg_for_param(self.guards.gcx, fid, param, args))
774                && self.may_contain_self(arg)
775            {
776                self.locals.insert(param);
777            }
778        }
779        self.stack.push(fid);
780        for m in f.modifiers {
781            if let Some(invoked) = invoked_function(&self.guards.gcx.hir, m) {
782                self.scan_function(invoked, Some(&m.args));
783            }
784        }
785        for stmt in body.stmts {
786            let _ = self.visit_stmt(stmt);
787        }
788        self.stack.pop();
789        self.locals = saved;
790    }
791}
792
793impl<'gcx> Visit<'gcx> for SelfAssignScan<'_, 'gcx> {
794    type BreakValue = Never;
795
796    fn hir(&self) -> &'gcx Hir<'gcx> {
797        &self.guards.gcx.hir
798    }
799
800    fn visit_stmt(&mut self, stmt: &'gcx Stmt<'gcx>) -> ControlFlow<Never> {
801        if self.found {
802            return ControlFlow::Continue(());
803        }
804        match &stmt.kind {
805            StmtKind::DeclSingle(vid) => {
806                if let Some(init) = self.guards.gcx.hir.variable(*vid).initializer {
807                    self.note_local(*vid, init);
808                }
809            }
810            StmtKind::DeclMulti(vars, init) => {
811                for (vid, rhs) in vars.iter().zip(tuple_elems(init).into_iter().flatten()) {
812                    if let (Some(vid), Some(rhs)) = (vid, rhs) {
813                        self.note_local(*vid, rhs);
814                    }
815                }
816            }
817            _ => {}
818        }
819        self.walk_stmt(stmt)
820    }
821
822    fn visit_expr(&mut self, expr: &'gcx Expr<'gcx>) -> ControlFlow<Never> {
823        if self.found {
824            return ControlFlow::Continue(());
825        }
826        match &expr.peel_parens().kind {
827            ExprKind::Assign(lhs, _, rhs) => self.assign(lhs, rhs),
828            ExprKind::Call(callee, args, _) => match &callee.peel_parens().kind {
829                // `target.push(<self>)` on an array / bytes state variable.
830                ExprKind::Member(recv, member) => {
831                    if member.name.as_str() == "push"
832                        && lhs_root_var(self.guards.gcx, recv) == Some(self.target)
833                        && expr_is_array_or_bytes(self.guards.gcx, recv)
834                        && args.exprs().any(|a| self.may_contain_self(a))
835                    {
836                        self.found = true;
837                    }
838                }
839                _ => {
840                    if let Some(fid) = self.guards.gcx.resolved_function(callee) {
841                        self.scan_function(fid, Some(args));
842                    }
843                }
844            },
845            _ => {}
846        }
847        self.walk_expr(expr)
848    }
849}
850
851/// The function invoked by a modifier or base-constructor invocation.
852fn invoked_function(hir: &Hir<'_>, m: &Modifier<'_>) -> Option<FunctionId> {
853    match m.id {
854        ItemId::Function(fid) => Some(fid),
855        ItemId::Contract(cid) => hir.contract(cid).ctor,
856        _ => None,
857    }
858}
859
860/// Argument returned verbatim (modulo casts) by an identity helper call `id(x)` / `Lib.id(x)`.
861fn identity_helper_arg<'gcx>(
862    gcx: Gcx<'gcx>,
863    callee: &'gcx Expr<'gcx>,
864    args: &'gcx CallArgs<'gcx>,
865) -> Option<&'gcx Expr<'gcx>> {
866    gcx.resolved_function(callee).and_then(|fid| {
867        let f = gcx.hir.function(fid);
868        let [stmt] = f.body?.stmts else { return None };
869        let StmtKind::Return(Some(ret)) = &stmt.kind else { return None };
870        let param = underlying_var(gcx, peel_casts(gcx, ret))?;
871        (f.parameters.len() == args.len() && f.returns.len() == 1 && f.parameters.contains(&param))
872            .then(|| arg_for_param(gcx, fid, param, args))
873            .flatten()
874    })
875}
876
877/// Variable at the root of an lvalue, through member / index accesses and address casts.
878fn lhs_root_var(gcx: Gcx<'_>, lhs: &Expr<'_>) -> Option<VariableId> {
879    match &lhs.peel_parens().kind {
880        ExprKind::Member(base, _) | ExprKind::Index(base, _) | ExprKind::Payable(base) => {
881            lhs_root_var(gcx, base)
882        }
883        ExprKind::Call(callee, args, _) if is_address_like_cast(gcx, callee) => {
884            args.exprs().next().and_then(|expr| lhs_root_var(gcx, expr))
885        }
886        _ => underlying_var(gcx, lhs),
887    }
888}
889
890/// True when an index expression only depends on literals and state: no locals, parameters,
891/// builtins (`msg.sender`) or non-cast calls.
892fn index_is_static(gcx: Gcx<'_>, expr: &Expr<'_>) -> bool {
893    expr.visit(&mut |e| match &e.kind {
894        ExprKind::Lit(_)
895        | ExprKind::Type(_)
896        | ExprKind::Payable(_)
897        | ExprKind::Unary(..)
898        | ExprKind::Binary(..)
899        | ExprKind::Member(..)
900        | ExprKind::Index(..)
901        | ExprKind::Ternary(..)
902        | ExprKind::Tuple([Some(_)]) => ControlFlow::Continue(()),
903        ExprKind::Ident(_)
904            if gcx.resolved_expr(e).is_some_and(|res| match res {
905                Res::Item(ItemId::Variable(v)) => gcx.hir.variable(v).kind.is_state(),
906                Res::Builtin(_) => false,
907                _ => true,
908            }) =>
909        {
910            ControlFlow::Continue(())
911        }
912        ExprKind::Call(callee, ..)
913            if matches!(callee.peel_parens().kind, ExprKind::Type(_))
914                || is_contract_cast(gcx, callee) =>
915        {
916            ControlFlow::Continue(())
917        }
918        _ => ControlFlow::Break(()),
919    })
920    .is_continue()
921}
922
923/// True when any statement of a helper body is a `return` (bare or valued).
924fn stmt_contains_return(stmt: &Stmt<'_>) -> bool {
925    match &stmt.kind {
926        StmtKind::Return(_) => true,
927        StmtKind::Block(b) | StmtKind::UncheckedBlock(b) => {
928            b.stmts.iter().any(stmt_contains_return)
929        }
930        StmtKind::Loop(b, source) => loop_stmts(*b, *source).any(stmt_contains_return),
931        StmtKind::If(_, t, e) => {
932            stmt_contains_return(t) || e.is_some_and(|e| stmt_contains_return(e))
933        }
934        StmtKind::Try(t) => {
935            t.clauses.iter().any(|c| c.block.stmts.iter().any(stmt_contains_return))
936        }
937        _ => false,
938    }
939}
940
941/// `msg.sender` modulo parens, casts, `payable(..)` and no-arg helpers such as `_msgSender()`.
942fn is_msg_sender_like<'gcx>(gcx: Gcx<'gcx>, expr: &'gcx Expr<'gcx>, depth: u8) -> bool {
943    let expr = peel_casts(gcx, expr);
944    is_msg_sender(gcx, expr)
945        || matches!(&expr.kind, ExprKind::Call(callee, args, _)
946            if depth > 0
947                && args.exprs().next().is_none()
948                && callee_no_arg_returns(gcx, callee, |e| is_msg_sender_like(gcx, e, depth - 1)))
949}
950
951/// Looks through parens, `payable(..)`, address-like casts and integer casts.
952fn peel_casts<'a>(gcx: Gcx<'_>, expr: &'a Expr<'a>) -> &'a Expr<'a> {
953    let expr = expr.peel_parens();
954    match &expr.kind {
955        ExprKind::Payable(inner) => peel_casts(gcx, inner),
956        ExprKind::Call(callee, args, _)
957            if is_address_like_cast(gcx, callee) || is_numeric_cast(callee) =>
958        {
959            args.exprs().next().map_or(expr, |expr| peel_casts(gcx, expr))
960        }
961        _ => expr,
962    }
963}
964
965/// `uint<N>(..)` / `int<N>(..)` cast head.
966fn is_numeric_cast(callee: &Expr<'_>) -> bool {
967    matches!(
968        &callee.peel_parens().kind,
969        ExprKind::Type(hir::Type {
970            kind: TypeKind::Elementary(ElementaryType::UInt(_) | ElementaryType::Int(_)),
971            ..
972        })
973    )
974}
975
976/// An address literal or the integer literal `0`.
977fn is_trusted_literal(expr: &Expr<'_>) -> bool {
978    matches!(&expr.kind, ExprKind::Lit(lit) if matches!(lit.kind, LitKind::Address(_)))
979        || is_literal_zero(expr)
980}
981
982fn expr_is_function<'gcx>(gcx: Gcx<'gcx>, expr: &'gcx Expr<'gcx>) -> bool {
983    gcx.type_of_expr(expr.peel_parens().id)
984        .is_some_and(|ty| matches!(ty.peel_refs().kind, TyKind::Fn(_)))
985}
986
987fn expr_is_array_or_bytes<'gcx>(gcx: Gcx<'gcx>, expr: &'gcx Expr<'gcx>) -> bool {
988    gcx.type_of_expr(expr.peel_parens().id).is_some_and(|ty| {
989        matches!(
990            ty.peel_refs().kind,
991            TyKind::Array(..) | TyKind::DynArray(_) | TyKind::Elementary(ElementaryType::Bytes)
992        )
993    })
994}