Skip to main content

forge_lint/sol/high/
function_selector_collision.rs

1use super::FunctionSelectorCollision;
2use crate::{
3    linter::{LateLintPass, LintContext},
4    sol::{
5        Severity, SolLint,
6        analysis::{
7            arg_for_param, branch_always_exits, expr_is_address, is_address_cast,
8            is_require_or_assert, ty_contract_id,
9        },
10    },
11};
12use alloy_primitives::Selector;
13use solar::{
14    ast::{LitKind, UnOpKind},
15    interface::{data_structures::Never, kw, sym},
16    sema::{
17        Gcx,
18        builtins::Builtin,
19        hir::{
20            self, BinOpKind, ContractId, ContractKind, Expr, ExprKind, LoopSource, Stmt, StmtKind,
21            VariableId, Visit,
22        },
23    },
24};
25use std::{
26    collections::{HashMap, HashSet},
27    ops::ControlFlow,
28};
29
30/// Path-state cap above which selector constraints are widened to "any selector".
31const MAX_LOOP_PATH_STATES: usize = 128;
32
33declare_forge_lint!(
34    FUNCTION_SELECTOR_COLLISION,
35    Severity::High,
36    "function-selector-collision",
37    "proxy and implementation functions have colliding selectors"
38);
39
40impl<'gcx> LateLintPass<'gcx> for FunctionSelectorCollision {
41    fn check_nested_contract(&mut self, ctx: &LintContext, gcx: Gcx<'gcx>, proxy_id: ContractId) {
42        let proxy = gcx.hir.contract(proxy_id);
43        if proxy.kind != ContractKind::Contract || proxy.linearization_failed() {
44            return;
45        }
46        let Some(fallback_id) = proxy.fallback else { return };
47        let fallback = gcx.hir.function(fallback_id);
48        let Some(body) = fallback.body else { return };
49
50        let mut collector = DelegateTargetCollector {
51            gcx,
52            contract_id: proxy_id,
53            current_inputs: Vec::new(),
54            paths: vec![PathState::default()],
55            placeholder: None,
56            return_controls: vec![Vec::new()],
57            continuation_cache: HashMap::new(),
58            loop_controls: Vec::new(),
59            targets: Vec::new(),
60        };
61        collector.visit_modifier_chain(Continuation {
62            modifiers: fallback.modifiers,
63            index: 0,
64            body,
65            body_input: fallback
66                .parameters
67                .first()
68                .map(|&var| CalldataInput { var, modifier: None }),
69        });
70
71        let proxy_functions = gcx.interface_functions(proxy_id);
72        for target in collector.targets {
73            let implementation = gcx.hir.contract(target.contract);
74            if target.contract == proxy_id
75                || implementation.kind == ContractKind::Library
76                || implementation.linearization_failed()
77            {
78                continue;
79            }
80
81            for proxy_function in proxy_functions.all() {
82                for implementation_function in gcx.interface_functions(target.contract).all() {
83                    let selector = proxy_function.selector;
84                    if selector != implementation_function.selector
85                        || !target.filters.iter().any(|filter| filter.allows(selector))
86                    {
87                        continue;
88                    }
89                    let proxy_signature = gcx.item_signature(proxy_function.id.into());
90                    let implementation_signature =
91                        gcx.item_signature(implementation_function.id.into());
92                    if proxy_signature == implementation_signature {
93                        continue;
94                    }
95                    let msg = format!(
96                        "proxy function `{}.{proxy_signature}` collides with implementation function `{}.{implementation_signature}` at selector `{selector}`",
97                        proxy.name.as_str(),
98                        implementation.name.as_str(),
99                    );
100                    ctx.emit_with_msg(&FUNCTION_SELECTOR_COLLISION, proxy.name.span, msg);
101                }
102            }
103        }
104    }
105}
106
107/// The `msg.sig` constraints known to hold on a path.
108#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
109struct SelectorFilter {
110    required: Option<Selector>,
111    excluded: Vec<Selector>,
112}
113
114impl SelectorFilter {
115    fn allows(&self, selector: Selector) -> bool {
116        self.required.is_none_or(|required| required == selector)
117            && !self.excluded.contains(&selector)
118    }
119
120    /// Narrows the filter by `msg.sig == selector` being `matches`; `None` if contradictory.
121    fn with_guard(mut self, selector: Selector, matches: bool) -> Option<Self> {
122        if matches {
123            if self.excluded.contains(&selector)
124                || self.required.is_some_and(|required| required != selector)
125            {
126                return None;
127            }
128            self.required = Some(selector);
129        } else {
130            if self.required == Some(selector) {
131                return None;
132            }
133            if self.required.is_none() && !self.excluded.contains(&selector) {
134                self.excluded.push(selector);
135                self.excluded.sort_unstable();
136            }
137        }
138        Some(self)
139    }
140}
141
142struct DelegateTarget {
143    contract: ContractId,
144    filters: Vec<SelectorFilter>,
145}
146
147/// A parameter that initially holds the full `msg.data`: the fallback's own, or one of the
148/// `modifier`-th applied modifier (the same modifier may be applied several times).
149#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
150struct CalldataInput {
151    var: VariableId,
152    modifier: Option<usize>,
153}
154
155#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
156struct PathState {
157    selector_filter: SelectorFilter,
158    modified_inputs: Vec<CalldataInput>,
159}
160
161impl PathState {
162    fn input_unmodified(&self, input: CalldataInput) -> bool {
163        !self.modified_inputs.contains(&input)
164    }
165
166    fn mark_input_modified(&mut self, input: CalldataInput) {
167        if !self.modified_inputs.contains(&input) {
168            self.modified_inputs.push(input);
169            self.modified_inputs.sort_unstable();
170        }
171    }
172
173    fn clear_inputs(&mut self, inputs: &[CalldataInput]) {
174        self.modified_inputs.retain(|input| !inputs.contains(input));
175    }
176}
177
178#[derive(Default)]
179struct LoopControl {
180    breaks: Vec<PathState>,
181    continues: Vec<PathState>,
182}
183
184/// What `_` resumes: the rest of the modifier chain and the function body.
185#[derive(Clone, Copy)]
186struct Continuation<'gcx> {
187    modifiers: &'gcx [hir::Modifier<'gcx>],
188    index: usize,
189    body: hir::Block<'gcx>,
190    body_input: Option<CalldataInput>,
191}
192
193fn lvalue_contains_var(gcx: Gcx<'_>, expr: &Expr<'_>, target: VariableId) -> bool {
194    match &expr.peel_parens().kind {
195        ExprKind::Ident(_) => gcx.resolved_variable(expr) == Some(target),
196        ExprKind::Tuple(exprs) => {
197            exprs.iter().flatten().any(|expr| lvalue_contains_var(gcx, expr, target))
198        }
199        _ => false,
200    }
201}
202
203fn extend_unique<T: PartialEq>(items: &mut Vec<T>, new_items: impl IntoIterator<Item = T>) {
204    for item in new_items {
205        if !items.contains(&item) {
206            items.push(item);
207        }
208    }
209}
210
211fn dedup(paths: &mut Vec<PathState>) {
212    let mut seen = HashSet::with_capacity(paths.len());
213    paths.retain(|path| seen.insert(path.clone()));
214}
215
216struct DelegateTargetCollector<'gcx> {
217    gcx: Gcx<'gcx>,
218    contract_id: ContractId,
219    /// Full-calldata inputs visible in the block being visited.
220    current_inputs: Vec<CalldataInput>,
221    /// Live path states; empty means the current point is unreachable.
222    paths: Vec<PathState>,
223    placeholder: Option<Continuation<'gcx>>,
224    /// Per function/modifier frame, the states at each `return`.
225    return_controls: Vec<Vec<PathState>>,
226    continuation_cache: HashMap<(usize, PathState), Vec<PathState>>,
227    loop_controls: Vec<LoopControl>,
228    targets: Vec<DelegateTarget>,
229}
230
231impl<'gcx> DelegateTargetCollector<'gcx> {
232    fn visit_modifier_chain(&mut self, cont: Continuation<'gcx>) {
233        let previous_inputs =
234            std::mem::replace(&mut self.current_inputs, cont.body_input.into_iter().collect());
235        if let Some(invocation) = cont.modifiers.get(cont.index) {
236            for arg in invocation.args.exprs() {
237                let _ = self.visit_expr(arg);
238            }
239            if let Some(modifier_id) =
240                self.gcx.resolve_modifier_target(self.contract_id, invocation)
241                && let Some(modifier_body) = self.gcx.hir.function(modifier_id).body
242            {
243                let modifier = self.gcx.hir.function(modifier_id);
244                let params: Vec<_> = modifier
245                    .parameters
246                    .iter()
247                    .map(|&var| CalldataInput { var, modifier: Some(cont.index) })
248                    .collect();
249                // Parameters bound to a full-calldata argument inherit its provenance.
250                let bindings: Vec<_> = params
251                    .iter()
252                    .filter_map(|&param| {
253                        let arg =
254                            arg_for_param(self.gcx, modifier_id, param.var, &invocation.args)?;
255                        Some((param, full_calldata_source(self.gcx, arg, &self.current_inputs)?))
256                    })
257                    .collect();
258                for path in &mut self.paths {
259                    path.clear_inputs(&params);
260                    for &(param, source) in &bindings {
261                        if source.is_some_and(|source| !path.input_unmodified(source)) {
262                            path.mark_input_modified(param);
263                        }
264                    }
265                }
266                let inputs = bindings.iter().map(|&(input, _)| input).collect();
267                let next = Continuation { index: cont.index + 1, ..cont };
268                self.visit_block(modifier_body, Some(next), inputs);
269                for path in &mut self.paths {
270                    path.clear_inputs(&params);
271                }
272                if let Some(returns) = self.return_controls.last_mut() {
273                    for path in returns {
274                        path.clear_inputs(&params);
275                    }
276                }
277            } else {
278                self.visit_modifier_chain(Continuation { index: cont.index + 1, ..cont });
279            }
280        } else {
281            self.visit_block(cont.body, None, self.current_inputs.clone());
282        }
283        self.current_inputs = previous_inputs;
284    }
285
286    fn visit_block(
287        &mut self,
288        block: hir::Block<'gcx>,
289        placeholder: Option<Continuation<'gcx>>,
290        inputs: Vec<CalldataInput>,
291    ) {
292        let previous = std::mem::replace(&mut self.placeholder, placeholder);
293        let previous_inputs = std::mem::replace(&mut self.current_inputs, inputs);
294        for stmt in block.stmts {
295            let _ = self.visit_stmt(stmt);
296        }
297        self.placeholder = previous;
298        self.current_inputs = previous_inputs;
299    }
300
301    /// Runs the continuation once per distinct incoming path state, memoizing the outcome.
302    fn visit_continuation(&mut self, cont: Continuation<'gcx>) {
303        let mut output_paths = Vec::new();
304        for input in std::mem::take(&mut self.paths) {
305            let key = (cont.index, input);
306            if let Some(cached) = self.continuation_cache.get(&key) {
307                extend_unique(&mut output_paths, cached.iter().cloned());
308                continue;
309            }
310            self.paths.push(key.1.clone());
311            self.return_controls.push(Vec::new());
312            self.visit_modifier_chain(cont);
313            let mut result = std::mem::take(&mut self.paths);
314            let returns = self.return_controls.pop().expect("return control stack is not empty");
315            extend_unique(&mut result, returns);
316            self.continuation_cache.insert(key, result.clone());
317            extend_unique(&mut output_paths, result);
318        }
319        self.paths = output_paths;
320    }
321
322    /// Records `contract` as a delegatecall target reachable under the current paths' selector
323    /// filters (only paths on which `required_input` still holds the full calldata count).
324    fn record_target(&mut self, contract: ContractId, required_input: Option<CalldataInput>) {
325        let mut filters = Vec::new();
326        extend_unique(
327            &mut filters,
328            self.paths
329                .iter()
330                .filter(|path| required_input.is_none_or(|input| path.input_unmodified(input)))
331                .map(|path| path.selector_filter.clone()),
332        );
333        if filters.is_empty() {
334            return;
335        }
336        let target = match self.targets.iter().position(|target| target.contract == contract) {
337            Some(index) => &mut self.targets[index],
338            None => {
339                self.targets.push(DelegateTarget { contract, filters: Vec::new() });
340                self.targets.last_mut().expect("target was just pushed")
341            }
342        };
343        if target.filters.contains(&SelectorFilter::default()) {
344            return;
345        }
346        extend_unique(&mut target.filters, filters);
347        if target.filters.len() > MAX_LOOP_PATH_STATES {
348            target.filters = vec![SelectorFilter::default()];
349        }
350    }
351
352    /// Splits the live paths into those where `expr` is true and those where it is false.
353    fn visit_condition(&mut self, expr: &'gcx Expr<'gcx>) -> (Vec<PathState>, Vec<PathState>) {
354        match &expr.peel_parens().kind {
355            ExprKind::Lit(lit) => {
356                let paths = std::mem::take(&mut self.paths);
357                match lit.kind {
358                    LitKind::Bool(true) => (paths, Vec::new()),
359                    LitKind::Bool(false) => (Vec::new(), paths),
360                    _ => (paths.clone(), paths),
361                }
362            }
363            ExprKind::Unary(op, inner) if op.kind == UnOpKind::Not => {
364                let (true_paths, false_paths) = self.visit_condition(inner);
365                (false_paths, true_paths)
366            }
367            ExprKind::Binary(lhs, op, rhs) if matches!(op.kind, BinOpKind::And | BinOpKind::Or) => {
368                let (lhs_true, lhs_false) = self.visit_condition(lhs);
369                if op.kind == BinOpKind::And {
370                    self.paths = lhs_true;
371                    let (rhs_true, mut rhs_false) = self.visit_condition(rhs);
372                    extend_unique(&mut rhs_false, lhs_false);
373                    (rhs_true, rhs_false)
374                } else {
375                    self.paths = lhs_false;
376                    let (mut rhs_true, rhs_false) = self.visit_condition(rhs);
377                    extend_unique(&mut rhs_true, lhs_true);
378                    (rhs_true, rhs_false)
379                }
380            }
381            ExprKind::Ternary(condition, true_expr, false_expr) => {
382                let (condition_true, condition_false) = self.visit_condition(condition);
383                self.paths = condition_true;
384                let (mut true_paths, mut false_paths) = self.visit_condition(true_expr);
385                self.paths = condition_false;
386                let (false_arm_true, false_arm_false) = self.visit_condition(false_expr);
387                extend_unique(&mut true_paths, false_arm_true);
388                extend_unique(&mut false_paths, false_arm_false);
389                (true_paths, false_paths)
390            }
391            _ => {
392                let _ = self.visit_expr(expr);
393                let paths = std::mem::take(&mut self.paths);
394                let guard = selector_guard(self.gcx, expr);
395                let branch = |condition_is_true: bool| {
396                    paths
397                        .iter()
398                        .filter_map(|path| {
399                            let mut path = path.clone();
400                            if let Some((selector, matches)) = guard {
401                                path.selector_filter = path
402                                    .selector_filter
403                                    .with_guard(selector, matches == condition_is_true)?;
404                            }
405                            Some(path)
406                        })
407                        .collect()
408                };
409                (branch(true), branch(false))
410            }
411        }
412    }
413
414    /// Visits loop-body statements: `break` paths are added to `exits`, and the paths reaching
415    /// the next iteration (fall-through and `continue`) are returned.
416    fn visit_loop_stmts(
417        &mut self,
418        stmts: &'gcx [Stmt<'gcx>],
419        exits: &mut Vec<PathState>,
420    ) -> Vec<PathState> {
421        self.loop_controls.push(LoopControl::default());
422        for stmt in stmts {
423            let _ = self.visit_stmt(stmt);
424        }
425        let mut next = std::mem::take(&mut self.paths);
426        let control = self.loop_controls.pop().expect("loop control stack is not empty");
427        extend_unique(exits, control.breaks);
428        extend_unique(&mut next, control.continues);
429        next
430    }
431
432    /// Collapses `paths` to one unconstrained state keeping only the inputs modified on all of
433    /// them.
434    fn widen_loop_paths(paths: &mut Vec<PathState>) {
435        let Some(first) = paths.first() else { return };
436        let mut modified_inputs = first.modified_inputs.clone();
437        modified_inputs
438            .retain(|input| paths.iter().skip(1).all(|path| path.modified_inputs.contains(input)));
439        *paths = vec![PathState { selector_filter: SelectorFilter::default(), modified_inputs }];
440    }
441
442    /// One iteration of a `for` loop with an update statement: `if (cond) { body } else break`
443    /// followed by the update, which `continue` also reaches. Returns the back-edge paths and the
444    /// loop-exit paths.
445    fn visit_for_iteration(
446        &mut self,
447        block: &hir::Block<'gcx>,
448        update: &'gcx Stmt<'gcx>,
449    ) -> Option<(Vec<PathState>, Vec<PathState>)> {
450        let [stmt] = block.stmts else { return None };
451        let (condition, body, else_stmt) = match &stmt.kind {
452            StmtKind::If(condition, then_stmt, else_stmt) => {
453                (Some(*condition), *then_stmt, *else_stmt)
454            }
455            _ => (None, stmt, None),
456        };
457
458        let mut exits = Vec::new();
459        if let Some(condition) = condition {
460            let (true_paths, false_paths) = self.visit_condition(condition);
461            self.paths = false_paths;
462            if let Some(else_stmt) = else_stmt {
463                let fallthrough =
464                    self.visit_loop_stmts(std::slice::from_ref(else_stmt), &mut exits);
465                extend_unique(&mut exits, fallthrough);
466            } else {
467                extend_unique(&mut exits, std::mem::take(&mut self.paths));
468            }
469            self.paths = true_paths;
470        }
471
472        self.paths = self.visit_loop_stmts(std::slice::from_ref(body), &mut exits);
473        let _ = self.visit_stmt(update);
474        Some((std::mem::take(&mut self.paths), exits))
475    }
476
477    /// Iterates the loop body to a fixpoint over the set of distinct entry states.
478    fn visit_loop(&mut self, block: &hir::Block<'gcx>, source: LoopSource<'gcx>) {
479        let mut pending = std::mem::take(&mut self.paths);
480        let mut seen = HashSet::new();
481        let mut exits = Vec::new();
482
483        loop {
484            pending.retain(|path| seen.insert(path.clone()));
485            if pending.is_empty() {
486                break;
487            }
488
489            self.paths = std::mem::take(&mut pending);
490            let next = if let LoopSource::For { update: Some(update) } = source
491                && let Some((next, for_exits)) = self.visit_for_iteration(block, update)
492            {
493                extend_unique(&mut exits, for_exits);
494                next
495            } else if matches!(source, LoopSource::DoWhile)
496                && let Some((condition, body)) = block.stmts.split_last()
497            {
498                // `continue` in a do-while body still evaluates the condition.
499                self.paths = self.visit_loop_stmts(body, &mut exits);
500                self.visit_loop_stmts(std::slice::from_ref(condition), &mut exits)
501            } else {
502                self.visit_loop_stmts(block.stmts, &mut exits)
503            };
504            extend_unique(&mut pending, next);
505            if seen.len() + pending.len() > MAX_LOOP_PATH_STATES {
506                Self::widen_loop_paths(&mut pending);
507            }
508            if exits.len() > MAX_LOOP_PATH_STATES {
509                Self::widen_loop_paths(&mut exits);
510            }
511        }
512
513        self.paths = exits;
514    }
515
516    /// Visits two alternative branches from the given entry paths and joins their outcomes.
517    fn visit_branches(
518        &mut self,
519        true_paths: Vec<PathState>,
520        true_branch: impl FnOnce(&mut Self),
521        false_paths: Vec<PathState>,
522        false_branch: impl FnOnce(&mut Self),
523    ) {
524        self.paths = true_paths;
525        true_branch(self);
526        let mut joined = std::mem::take(&mut self.paths);
527        self.paths = false_paths;
528        false_branch(self);
529        joined.append(&mut self.paths);
530        self.paths = joined;
531        dedup(&mut self.paths);
532    }
533}
534
535impl<'gcx> Visit<'gcx> for DelegateTargetCollector<'gcx> {
536    type BreakValue = Never;
537
538    fn hir(&self) -> &'gcx hir::Hir<'gcx> {
539        &self.gcx.hir
540    }
541
542    fn visit_expr(&mut self, expr: &'gcx Expr<'gcx>) -> ControlFlow<Never> {
543        if self.paths.is_empty() {
544            return ControlFlow::Continue(());
545        }
546        match &expr.kind {
547            ExprKind::Ternary(condition, true_expr, false_expr) => {
548                let (true_paths, false_paths) = self.visit_condition(condition);
549                self.visit_branches(
550                    true_paths,
551                    |this| _ = this.visit_expr(true_expr),
552                    false_paths,
553                    |this| _ = this.visit_expr(false_expr),
554                );
555            }
556            ExprKind::Binary(_, op, _) if matches!(op.kind, BinOpKind::And | BinOpKind::Or) => {
557                let (mut true_paths, false_paths) = self.visit_condition(expr);
558                extend_unique(&mut true_paths, false_paths);
559                self.paths = true_paths;
560                dedup(&mut self.paths);
561            }
562            ExprKind::Call(callee, args, opts) => {
563                let _ = self.visit_expr(callee);
564                for arg in opts.iter().flat_map(|opts| opts.args) {
565                    let _ = self.visit_expr(&arg.value);
566                }
567                let mut args = args.exprs();
568                if is_require_or_assert(self.gcx, callee) {
569                    let Some(condition) = args.next() else { return ControlFlow::Continue(()) };
570                    let args: Vec<_> = args.collect();
571                    let (true_paths, false_paths) = self.visit_condition(condition);
572                    // Remaining arguments are evaluated before `require`/`assert` decides
573                    // whether to revert, so their targets and side effects apply on both paths;
574                    // only the passing paths continue.
575                    self.paths = true_paths;
576                    for &arg in &args {
577                        let _ = self.visit_expr(arg);
578                    }
579                    let continuing_paths = std::mem::take(&mut self.paths);
580                    self.paths = false_paths;
581                    for arg in args {
582                        let _ = self.visit_expr(arg);
583                    }
584                    self.paths = continuing_paths;
585                } else {
586                    for arg in args {
587                        let _ = self.visit_expr(arg);
588                    }
589                    if let Some((target, required_input)) =
590                        delegated_contract(self.gcx, &self.current_inputs, expr)
591                    {
592                        self.record_target(target, required_input);
593                    }
594                }
595            }
596            _ => {
597                let mutated_inputs: Vec<_> = match &expr.peel_parens().kind {
598                    ExprKind::Assign(lhs, _, _) => self
599                        .current_inputs
600                        .iter()
601                        .copied()
602                        .filter(|input| lvalue_contains_var(self.gcx, lhs, input.var))
603                        .collect(),
604                    _ => Vec::new(),
605                };
606                let _ = self.walk_expr(expr);
607                for path in &mut self.paths {
608                    for &input in &mutated_inputs {
609                        path.mark_input_modified(input);
610                    }
611                }
612            }
613        }
614        ControlFlow::Continue(())
615    }
616
617    fn visit_stmt(&mut self, stmt: &'gcx Stmt<'gcx>) -> ControlFlow<Never> {
618        if self.paths.is_empty() {
619            return ControlFlow::Continue(());
620        }
621        match &stmt.kind {
622            StmtKind::If(condition, then_stmt, else_stmt) => {
623                let (true_paths, false_paths) = self.visit_condition(condition);
624                self.visit_branches(
625                    true_paths,
626                    |this| _ = this.visit_stmt(then_stmt),
627                    false_paths,
628                    |this| {
629                        if let Some(else_stmt) = else_stmt {
630                            let _ = this.visit_stmt(else_stmt);
631                        }
632                    },
633                );
634            }
635            StmtKind::Try(try_) => {
636                let _ = self.visit_expr(&try_.expr);
637                let paths = std::mem::take(&mut self.paths);
638                let mut joined = Vec::new();
639                for clause in try_.clauses {
640                    self.paths = paths.clone();
641                    for &var in clause.args {
642                        let _ = self.visit_nested_var(var);
643                    }
644                    for stmt in clause.block.stmts {
645                        let _ = self.visit_stmt(stmt);
646                    }
647                    joined.append(&mut self.paths);
648                }
649                self.paths = joined;
650                dedup(&mut self.paths);
651            }
652            StmtKind::Loop(block, source) => self.visit_loop(block, *source),
653            StmtKind::Break | StmtKind::Continue => {
654                let paths = std::mem::take(&mut self.paths);
655                if let Some(control) = self.loop_controls.last_mut() {
656                    let destination = if matches!(stmt.kind, StmtKind::Break) {
657                        &mut control.breaks
658                    } else {
659                        &mut control.continues
660                    };
661                    extend_unique(destination, paths);
662                }
663            }
664            StmtKind::Placeholder => {
665                if let Some(cont) = self.placeholder {
666                    self.visit_continuation(cont);
667                }
668            }
669            StmtKind::Return(expr) => {
670                if let Some(expr) = expr {
671                    let _ = self.visit_expr(expr);
672                }
673                let paths = std::mem::take(&mut self.paths);
674                let returns =
675                    self.return_controls.last_mut().expect("return control stack is not empty");
676                extend_unique(returns, paths);
677            }
678            StmtKind::AssemblyBlock(_) => {
679                // Inline assembly may rewrite any calldata parameter in scope.
680                for path in &mut self.paths {
681                    for &input in &self.current_inputs {
682                        path.mark_input_modified(input);
683                    }
684                }
685            }
686            _ => {
687                let _ = self.walk_stmt(stmt);
688                if branch_always_exits(self.gcx, stmt) {
689                    self.paths.clear();
690                }
691            }
692        }
693        ControlFlow::Continue(())
694    }
695}
696
697/// `msg.sig == F.selector` / `msg.sig != F.selector` as `(selector, matches)`.
698fn selector_guard(gcx: Gcx<'_>, expr: &Expr<'_>) -> Option<(Selector, bool)> {
699    let ExprKind::Binary(lhs, op, rhs) = &expr.peel_parens().kind else { return None };
700    let matches = match op.kind {
701        BinOpKind::Eq => true,
702        BinOpKind::Ne => false,
703        _ => return None,
704    };
705    let selector = if gcx.resolved_builtin(lhs) == Some(Builtin::MsgSig) {
706        rhs
707    } else if gcx.resolved_builtin(rhs) == Some(Builtin::MsgSig) {
708        lhs
709    } else {
710        return None;
711    };
712    let selector = selector.peel_parens();
713    let ExprKind::Member(function, member) = &selector.kind else { return None };
714    if member.name != sym::selector
715        || gcx.resolved_builtin(selector) != Some(Builtin::FunctionSelector)
716    {
717        return None;
718    }
719    let function = gcx.resolved_function(function)?;
720    Some((gcx.function_selector(function), matches))
721}
722
723/// The statically typed implementation contract of a proxy-style `<addr>.delegatecall(<full
724/// calldata>)`, with the calldata input that must be unmodified for the forwarding to be complete.
725fn delegated_contract<'gcx>(
726    gcx: Gcx<'gcx>,
727    full_calldata_inputs: &[CalldataInput],
728    expr: &'gcx Expr<'gcx>,
729) -> Option<(ContractId, Option<CalldataInput>)> {
730    let ExprKind::Call(callee, args, _) = &expr.peel_parens().kind else { return None };
731    let ExprKind::Member(receiver, member) = &callee.peel_parens().kind else { return None };
732    let required_input = full_calldata_source(gcx, args.exprs().next()?, full_calldata_inputs)?;
733    if member.name != kw::Delegatecall
734        || gcx.resolved_builtin(callee) != Some(Builtin::AddressDelegatecall)
735        || !expr_is_address(gcx, receiver)
736    {
737        return None;
738    }
739    typed_contract_behind_address_cast(gcx, receiver).map(|contract| (contract, required_input))
740}
741
742fn typed_contract_behind_address_cast<'gcx>(
743    gcx: Gcx<'gcx>,
744    expr: &'gcx Expr<'gcx>,
745) -> Option<ContractId> {
746    let expr = expr.peel_parens();
747    if let Some(id) = gcx.type_of_expr(expr.id).and_then(ty_contract_id) {
748        return Some(id);
749    }
750    match &expr.kind {
751        ExprKind::Call(callee, args, _) if is_address_cast(callee) => {
752            args.exprs().next().and_then(|arg| typed_contract_behind_address_cast(gcx, arg))
753        }
754        ExprKind::Payable(inner) => typed_contract_behind_address_cast(gcx, inner),
755        _ => None,
756    }
757}
758
759/// `Some(None)` for `msg.data`, `Some(Some(input))` for a known full-calldata input, `None`
760/// otherwise.
761fn full_calldata_source(
762    gcx: Gcx<'_>,
763    expr: &Expr<'_>,
764    full_calldata_inputs: &[CalldataInput],
765) -> Option<Option<CalldataInput>> {
766    if gcx.resolved_builtin(expr) == Some(Builtin::MsgData) {
767        return Some(None);
768    }
769    let ExprKind::Ident(_) = &expr.peel_parens().kind else { return None };
770    let id = gcx.resolved_variable(expr)?;
771    full_calldata_inputs.iter().copied().find(|input| input.var == id).map(Some)
772}