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::primitives::{branch_always_exits, is_require_or_assert},
7    },
8};
9use alloy_primitives::Selector;
10use solar::{
11    ast::{LitKind, UnOpKind},
12    interface::{data_structures::Never, kw, sym},
13    sema::{
14        Gcx,
15        builtins::Builtin,
16        hir::{
17            self, BinOpKind, CallArgs, ContractId, ContractKind, Expr, ExprKind, ItemId, Stmt,
18            StmtKind, TypeKind, Visit,
19        },
20        ty::{ResolvedMember, Ty, TyKind},
21    },
22};
23use std::{
24    collections::{HashMap, HashSet},
25    ops::ControlFlow,
26};
27
28const MAX_LOOP_PATH_STATES: usize = 128;
29
30declare_forge_lint!(
31    FUNCTION_SELECTOR_COLLISION,
32    Severity::High,
33    "function-selector-collision",
34    "proxy and implementation functions have colliding selectors"
35);
36
37impl<'hir> LateLintPass<'hir> for FunctionSelectorCollision {
38    fn check_nested_contract(
39        &mut self,
40        ctx: &LintContext,
41        gcx: Gcx<'hir>,
42        hir: &'hir hir::Hir<'hir>,
43        proxy_id: ContractId,
44    ) {
45        let proxy = hir.contract(proxy_id);
46        if proxy.kind != ContractKind::Contract || proxy.linearization_failed() {
47            return;
48        }
49        let Some(fallback_id) = proxy.fallback else { return };
50        let fallback = hir.function(fallback_id);
51        let Some(body) = fallback.body else { return };
52
53        let mut collector = DelegateTargetCollector {
54            gcx,
55            hir,
56            current_inputs: Vec::new(),
57            paths: vec![PathState::initial()],
58            placeholder: None,
59            return_controls: vec![Vec::new()],
60            continuation_cache: HashMap::new(),
61            loop_controls: Vec::new(),
62            targets: Vec::new(),
63        };
64        collector.visit_modifier_chain(
65            fallback.modifiers,
66            0,
67            body,
68            fallback.parameters.first().copied().map(CalldataInput::Fallback),
69        );
70
71        let proxy_functions = gcx.interface_functions(proxy_id);
72        for target in collector.targets {
73            let implementation_id = target.contract;
74            if implementation_id == proxy_id {
75                continue;
76            }
77            let implementation = hir.contract(implementation_id);
78            if implementation.kind == ContractKind::Library || implementation.linearization_failed()
79            {
80                continue;
81            }
82
83            for proxy_function in proxy_functions.all() {
84                for implementation_function in gcx.interface_functions(implementation_id).all() {
85                    if proxy_function.selector != implementation_function.selector
86                        || !target.allows(implementation_function.selector)
87                    {
88                        continue;
89                    }
90                    let proxy_signature = gcx.item_signature(proxy_function.id.into());
91                    let implementation_signature =
92                        gcx.item_signature(implementation_function.id.into());
93                    if proxy_signature == implementation_signature {
94                        continue;
95                    }
96
97                    let msg = format!(
98                        "proxy function `{}.{proxy_signature}` collides with implementation function `{}.{implementation_signature}` at selector `{}`",
99                        proxy.name.as_str(),
100                        implementation.name.as_str(),
101                        proxy_function.selector,
102                    );
103                    ctx.emit_with_msg(&FUNCTION_SELECTOR_COLLISION, proxy.name.span, msg);
104                }
105            }
106        }
107    }
108}
109
110#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
111struct SelectorFilter {
112    required: Option<Selector>,
113    excluded: Vec<Selector>,
114}
115
116impl SelectorFilter {
117    fn allows(&self, selector: Selector) -> bool {
118        self.required.is_none_or(|required| required == selector)
119            && !self.excluded.contains(&selector)
120    }
121
122    fn with_guard(mut self, selector: Selector, matches: bool) -> Option<Self> {
123        if matches {
124            if self.excluded.contains(&selector)
125                || self.required.is_some_and(|required| required != selector)
126            {
127                return None;
128            }
129            self.required = Some(selector);
130        } else {
131            if self.required == Some(selector) {
132                return None;
133            }
134            if self.required.is_none() && !self.excluded.contains(&selector) {
135                self.excluded.push(selector);
136                self.excluded.sort_unstable();
137            }
138        }
139        Some(self)
140    }
141}
142
143struct DelegateTarget {
144    contract: ContractId,
145    filters: Vec<SelectorFilter>,
146}
147
148impl DelegateTarget {
149    fn allows(&self, selector: Selector) -> bool {
150        self.filters.iter().any(|filter| filter.allows(selector))
151    }
152}
153
154#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
155enum CalldataInput {
156    Fallback(hir::VariableId),
157    Modifier { index: usize, param: hir::VariableId },
158}
159
160impl CalldataInput {
161    const fn variable(self) -> hir::VariableId {
162        match self {
163            Self::Fallback(variable) => variable,
164            Self::Modifier { param, .. } => param,
165        }
166    }
167}
168
169#[derive(Clone, Debug, PartialEq, Eq, Hash)]
170struct PathState {
171    selector_filter: SelectorFilter,
172    modified_inputs: Vec<CalldataInput>,
173}
174
175impl PathState {
176    fn initial() -> Self {
177        Self { selector_filter: SelectorFilter::default(), modified_inputs: Vec::new() }
178    }
179
180    fn input_unmodified(&self, input: CalldataInput) -> bool {
181        !self.modified_inputs.contains(&input)
182    }
183
184    fn mark_input_modified(&mut self, input: CalldataInput) {
185        if !self.modified_inputs.contains(&input) {
186            self.modified_inputs.push(input);
187            self.modified_inputs.sort_unstable();
188        }
189    }
190
191    fn clear_inputs(&mut self, inputs: &[CalldataInput]) {
192        self.modified_inputs.retain(|input| !inputs.contains(input));
193    }
194}
195
196#[derive(Default)]
197struct LoopControl {
198    breaks: Vec<PathState>,
199    continues: Vec<PathState>,
200}
201
202fn lvalue_contains_var(expr: &Expr<'_>, target: hir::VariableId) -> bool {
203    match &expr.peel_parens().kind {
204        ExprKind::Ident(reses) => reses
205            .iter()
206            .any(|res| matches!(res, hir::Res::Item(ItemId::Variable(id)) if *id == target)),
207        ExprKind::Tuple(exprs) => {
208            exprs.iter().flatten().any(|expr| lvalue_contains_var(expr, target))
209        }
210        _ => false,
211    }
212}
213
214struct DelegateTargetCollector<'hir> {
215    gcx: Gcx<'hir>,
216    hir: &'hir hir::Hir<'hir>,
217    current_inputs: Vec<CalldataInput>,
218    paths: Vec<PathState>,
219    placeholder:
220        Option<(&'hir [hir::Modifier<'hir>], usize, hir::Block<'hir>, Option<CalldataInput>)>,
221    return_controls: Vec<Vec<PathState>>,
222    continuation_cache: HashMap<(usize, PathState), Vec<PathState>>,
223    loop_controls: Vec<LoopControl>,
224    targets: Vec<DelegateTarget>,
225}
226
227impl<'hir> DelegateTargetCollector<'hir> {
228    fn visit_modifier_chain(
229        &mut self,
230        modifiers: &'hir [hir::Modifier<'hir>],
231        index: usize,
232        body: hir::Block<'hir>,
233        body_input: Option<CalldataInput>,
234    ) {
235        let previous_inputs =
236            std::mem::replace(&mut self.current_inputs, body_input.into_iter().collect());
237        let Some(invocation) = modifiers.get(index) else {
238            self.visit_block(body, None, self.current_inputs.clone());
239            self.current_inputs = previous_inputs;
240            return;
241        };
242
243        for arg in invocation.args.exprs() {
244            let _ = self.visit_expr(arg);
245        }
246
247        if let Some(modifier_id) = invocation.id.as_function() {
248            let modifier = self.hir.function(modifier_id);
249            if let Some(modifier_body) = modifier.body {
250                let bindings = modifier_input_bindings(
251                    self.hir,
252                    modifier,
253                    &invocation.args,
254                    &self.current_inputs,
255                    index,
256                );
257                let params = modifier
258                    .parameters
259                    .iter()
260                    .copied()
261                    .map(|param| CalldataInput::Modifier { index, param })
262                    .collect::<Vec<_>>();
263                for path in &mut self.paths {
264                    path.clear_inputs(&params);
265                    for &(param, source) in &bindings {
266                        if source.is_some_and(|source| !path.input_unmodified(source)) {
267                            path.mark_input_modified(param);
268                        }
269                    }
270                }
271
272                let modifier_inputs = bindings.iter().map(|&(input, _)| input).collect();
273                self.visit_block(
274                    modifier_body,
275                    Some((modifiers, index + 1, body, body_input)),
276                    modifier_inputs,
277                );
278                self.clear_local_inputs(&params);
279                self.current_inputs = previous_inputs;
280                return;
281            }
282        }
283
284        self.visit_modifier_chain(modifiers, index + 1, body, body_input);
285        self.current_inputs = previous_inputs;
286    }
287
288    fn visit_block(
289        &mut self,
290        block: hir::Block<'hir>,
291        placeholder: Option<(
292            &'hir [hir::Modifier<'hir>],
293            usize,
294            hir::Block<'hir>,
295            Option<CalldataInput>,
296        )>,
297        inputs: Vec<CalldataInput>,
298    ) {
299        let previous = self.placeholder;
300        let previous_inputs = std::mem::replace(&mut self.current_inputs, inputs);
301        self.placeholder = placeholder;
302        for stmt in block.stmts {
303            let _ = self.visit_stmt(stmt);
304        }
305        self.placeholder = previous;
306        self.current_inputs = previous_inputs;
307    }
308
309    fn visit_continuation(
310        &mut self,
311        modifiers: &'hir [hir::Modifier<'hir>],
312        index: usize,
313        body: hir::Block<'hir>,
314        body_input: Option<CalldataInput>,
315    ) {
316        let input_paths = std::mem::take(&mut self.paths);
317        let mut output_paths = Vec::new();
318        for input in input_paths {
319            let key = (index, input.clone());
320            if let Some(cached) = self.continuation_cache.get(&key) {
321                Self::extend_unique(&mut output_paths, cached.iter().cloned());
322                continue;
323            }
324
325            self.paths.push(input);
326            self.return_controls.push(Vec::new());
327            self.visit_modifier_chain(modifiers, index, body, body_input);
328            let mut result = std::mem::take(&mut self.paths);
329            let returns = self.return_controls.pop().expect("return control stack is not empty");
330            Self::extend_unique(&mut result, returns);
331            self.continuation_cache.insert(key, result.clone());
332            Self::extend_unique(&mut output_paths, result);
333        }
334        self.paths = output_paths;
335    }
336
337    fn clear_local_inputs(&mut self, inputs: &[CalldataInput]) {
338        for path in &mut self.paths {
339            path.clear_inputs(inputs);
340        }
341        if let Some(returns) = self.return_controls.last_mut() {
342            for path in returns {
343                path.clear_inputs(inputs);
344            }
345        }
346    }
347
348    fn record_target(&mut self, contract: ContractId, required_input: Option<CalldataInput>) {
349        let mut filters = Vec::new();
350        for path in &self.paths {
351            if required_input.is_none_or(|input| path.input_unmodified(input))
352                && !filters.contains(&path.selector_filter)
353            {
354                filters.push(path.selector_filter.clone());
355            }
356        }
357        if filters.is_empty() {
358            return;
359        }
360        if filters.len() > MAX_LOOP_PATH_STATES {
361            filters.clear();
362            filters.push(SelectorFilter::default());
363        }
364
365        if let Some(target) = self.targets.iter_mut().find(|target| target.contract == contract) {
366            if target.filters.contains(&SelectorFilter::default()) {
367                return;
368            }
369            for filter in filters {
370                if !target.filters.contains(&filter) {
371                    target.filters.push(filter);
372                }
373            }
374            if target.filters.len() > MAX_LOOP_PATH_STATES {
375                target.filters.clear();
376                target.filters.push(SelectorFilter::default());
377            }
378        } else {
379            self.targets.push(DelegateTarget { contract, filters });
380        }
381    }
382
383    fn branch_paths(
384        paths: &[PathState],
385        guard: Option<(Selector, bool)>,
386        condition_is_true: bool,
387    ) -> Vec<PathState> {
388        paths
389            .iter()
390            .filter_map(|path| {
391                let mut path = path.clone();
392                if let Some((selector, matches)) = guard {
393                    path.selector_filter =
394                        path.selector_filter.with_guard(selector, matches == condition_is_true)?;
395                }
396                Some(path)
397            })
398            .collect()
399    }
400
401    fn dedup_paths(&mut self) {
402        let mut seen = HashSet::with_capacity(self.paths.len());
403        self.paths.retain(|path| seen.insert(path.clone()));
404    }
405
406    fn extend_unique(paths: &mut Vec<PathState>, new_paths: impl IntoIterator<Item = PathState>) {
407        for path in new_paths {
408            if !paths.contains(&path) {
409                paths.push(path);
410            }
411        }
412    }
413
414    fn visit_condition(&mut self, expr: &'hir Expr<'hir>) -> (Vec<PathState>, Vec<PathState>) {
415        match &expr.peel_parens().kind {
416            ExprKind::Lit(lit) => {
417                let paths = std::mem::take(&mut self.paths);
418                match lit.kind {
419                    LitKind::Bool(true) => (paths, Vec::new()),
420                    LitKind::Bool(false) => (Vec::new(), paths),
421                    _ => (paths.clone(), paths),
422                }
423            }
424            ExprKind::Unary(op, inner) if op.kind == UnOpKind::Not => {
425                let (true_paths, false_paths) = self.visit_condition(inner);
426                (false_paths, true_paths)
427            }
428            ExprKind::Binary(lhs, op, rhs) if matches!(op.kind, BinOpKind::And | BinOpKind::Or) => {
429                let (lhs_true, lhs_false) = self.visit_condition(lhs);
430                if op.kind == BinOpKind::And {
431                    self.paths = lhs_true;
432                    let (rhs_true, mut rhs_false) = self.visit_condition(rhs);
433                    Self::extend_unique(&mut rhs_false, lhs_false);
434                    (rhs_true, rhs_false)
435                } else {
436                    self.paths = lhs_false;
437                    let (mut rhs_true, rhs_false) = self.visit_condition(rhs);
438                    Self::extend_unique(&mut rhs_true, lhs_true);
439                    (rhs_true, rhs_false)
440                }
441            }
442            ExprKind::Ternary(condition, true_expr, false_expr) => {
443                let (condition_true, condition_false) = self.visit_condition(condition);
444
445                self.paths = condition_true;
446                let (mut true_paths, mut false_paths) = self.visit_condition(true_expr);
447
448                self.paths = condition_false;
449                let (false_arm_true, false_arm_false) = self.visit_condition(false_expr);
450                Self::extend_unique(&mut true_paths, false_arm_true);
451                Self::extend_unique(&mut false_paths, false_arm_false);
452                (true_paths, false_paths)
453            }
454            _ => {
455                let _ = self.visit_expr(expr);
456                let paths = std::mem::take(&mut self.paths);
457                let guard = selector_guard(self.gcx, expr);
458                (Self::branch_paths(&paths, guard, true), Self::branch_paths(&paths, guard, false))
459            }
460        }
461    }
462
463    fn visit_loop_stmts(&mut self, stmts: &'hir [Stmt<'hir>]) -> (Vec<PathState>, LoopControl) {
464        self.loop_controls.push(LoopControl::default());
465        for stmt in stmts {
466            let _ = self.visit_stmt(stmt);
467        }
468        let paths = std::mem::take(&mut self.paths);
469        let control = self.loop_controls.pop().expect("loop control stack is not empty");
470        (paths, control)
471    }
472
473    fn widen_loop_paths(paths: &mut Vec<PathState>) {
474        let Some(first) = paths.first() else { return };
475        let mut modified_inputs = first.modified_inputs.clone();
476        modified_inputs
477            .retain(|input| paths.iter().skip(1).all(|path| path.modified_inputs.contains(input)));
478        paths.clear();
479        paths.push(PathState { selector_filter: SelectorFilter::default(), modified_inputs });
480    }
481
482    fn visit_for_iteration(
483        &mut self,
484        block: &hir::Block<'hir>,
485    ) -> Option<(Vec<PathState>, Vec<PathState>)> {
486        let [stmt] = block.stmts else { return None };
487        let (condition, body, else_stmt) = match &stmt.kind {
488            StmtKind::If(condition, then_stmt, else_stmt) => {
489                let StmtKind::Block(body) = &then_stmt.kind else { return None };
490                (Some(*condition), body, *else_stmt)
491            }
492            StmtKind::Block(body) => (None, body, None),
493            _ => return None,
494        };
495        if body.span != block.span {
496            return None;
497        }
498        let (update, body) = body.stmts.split_last()?;
499        if !matches!(update.kind, StmtKind::Expr(_)) {
500            return None;
501        }
502
503        let mut exits = Vec::new();
504        if let Some(condition) = condition {
505            let (true_paths, false_paths) = self.visit_condition(condition);
506
507            self.paths = false_paths;
508            if let Some(else_stmt) = else_stmt {
509                let (fallthrough, control) = self.visit_loop_stmts(std::slice::from_ref(else_stmt));
510                Self::extend_unique(&mut exits, control.breaks);
511                Self::extend_unique(&mut exits, fallthrough);
512            } else {
513                Self::extend_unique(&mut exits, std::mem::take(&mut self.paths));
514            }
515
516            self.paths = true_paths;
517        }
518
519        let (mut update_paths, control) = self.visit_loop_stmts(body);
520        Self::extend_unique(&mut exits, control.breaks);
521        Self::extend_unique(&mut update_paths, control.continues);
522        self.paths = update_paths;
523        let _ = self.visit_stmt(update);
524        Some((std::mem::take(&mut self.paths), exits))
525    }
526
527    fn visit_loop(&mut self, block: &hir::Block<'hir>, source: hir::LoopSource) {
528        let mut pending = std::mem::take(&mut self.paths);
529        let mut seen = HashSet::new();
530        let mut exits = Vec::new();
531
532        loop {
533            pending.retain(|path| seen.insert(path.clone()));
534            if pending.is_empty() {
535                break;
536            }
537
538            self.paths = std::mem::take(&mut pending);
539            let next = if source == hir::LoopSource::For
540                && let Some((next, for_exits)) = self.visit_for_iteration(block)
541            {
542                Self::extend_unique(&mut exits, for_exits);
543                next
544            } else if source == hir::LoopSource::DoWhile
545                && let Some((condition, body)) = block.stmts.split_last()
546            {
547                let (mut condition_paths, control) = self.visit_loop_stmts(body);
548                Self::extend_unique(&mut exits, control.breaks);
549                Self::extend_unique(&mut condition_paths, control.continues);
550
551                self.paths = condition_paths;
552                let (mut next, control) = self.visit_loop_stmts(std::slice::from_ref(condition));
553                Self::extend_unique(&mut exits, control.breaks);
554                Self::extend_unique(&mut next, control.continues);
555                next
556            } else {
557                let (mut next, control) = self.visit_loop_stmts(block.stmts);
558                Self::extend_unique(&mut exits, control.breaks);
559                Self::extend_unique(&mut next, control.continues);
560                next
561            };
562            Self::extend_unique(&mut pending, next);
563            if seen.len() + pending.len() > MAX_LOOP_PATH_STATES {
564                Self::widen_loop_paths(&mut pending);
565            }
566            if exits.len() > MAX_LOOP_PATH_STATES {
567                Self::widen_loop_paths(&mut exits);
568            }
569        }
570
571        self.paths = exits;
572    }
573}
574
575impl<'hir> Visit<'hir> for DelegateTargetCollector<'hir> {
576    type BreakValue = Never;
577
578    fn hir(&self) -> &'hir hir::Hir<'hir> {
579        self.hir
580    }
581
582    fn visit_expr(&mut self, expr: &'hir Expr<'hir>) -> ControlFlow<Self::BreakValue> {
583        if self.paths.is_empty() {
584            return ControlFlow::Continue(());
585        }
586
587        if let ExprKind::Ternary(condition, true_expr, false_expr) = &expr.kind {
588            let (true_paths, false_paths) = self.visit_condition(condition);
589
590            self.paths = true_paths;
591            let _ = self.visit_expr(true_expr);
592            let mut joined = std::mem::take(&mut self.paths);
593
594            self.paths = false_paths;
595            let _ = self.visit_expr(false_expr);
596            joined.append(&mut self.paths);
597            self.paths = joined;
598            self.dedup_paths();
599            return ControlFlow::Continue(());
600        }
601
602        if let ExprKind::Binary(_, op, _) = &expr.kind
603            && matches!(op.kind, BinOpKind::And | BinOpKind::Or)
604        {
605            let (mut true_paths, false_paths) = self.visit_condition(expr);
606            Self::extend_unique(&mut true_paths, false_paths);
607            self.paths = true_paths;
608            self.dedup_paths();
609            return ControlFlow::Continue(());
610        }
611
612        if let ExprKind::Call(callee, args, opts) = &expr.kind
613            && is_require_or_assert(callee)
614        {
615            let _ = self.visit_expr(callee);
616            if let Some(opts) = opts {
617                for arg in opts.args {
618                    let _ = self.visit_expr(&arg.value);
619                }
620            }
621
622            let mut args = args.exprs();
623            let Some(condition) = args.next() else { return ControlFlow::Continue(()) };
624            let args = args.collect::<Vec<_>>();
625            let (true_paths, false_paths) = self.visit_condition(condition);
626
627            self.paths = true_paths;
628            for &arg in &args {
629                let _ = self.visit_expr(arg);
630            }
631            let continuing_paths = std::mem::take(&mut self.paths);
632
633            // Remaining arguments are evaluated before `require`/`assert` decides whether to
634            // revert, so preserve their targets and side effects on the failing paths too.
635            self.paths = false_paths;
636            for arg in args {
637                let _ = self.visit_expr(arg);
638            }
639            self.paths = continuing_paths;
640            return ControlFlow::Continue(());
641        }
642
643        if let ExprKind::Call(callee, args, opts) = &expr.kind {
644            let _ = self.visit_expr(callee);
645            if let Some(opts) = opts {
646                for arg in opts.args {
647                    let _ = self.visit_expr(&arg.value);
648                }
649            }
650            for arg in args.exprs() {
651                let _ = self.visit_expr(arg);
652            }
653            if let Some((target, required_input)) =
654                delegated_contract(self.gcx, &self.current_inputs, expr)
655            {
656                self.record_target(target, required_input);
657            }
658            return ControlFlow::Continue(());
659        }
660
661        let mutated_inputs = match &expr.peel_parens().kind {
662            ExprKind::Assign(lhs, _, _) => self
663                .current_inputs
664                .iter()
665                .copied()
666                .filter(|&input| lvalue_contains_var(lhs, input.variable()))
667                .collect::<Vec<_>>(),
668            _ => Vec::new(),
669        };
670        let flow = self.walk_expr(expr);
671        if !mutated_inputs.is_empty() {
672            for path in &mut self.paths {
673                for &input in &mutated_inputs {
674                    path.mark_input_modified(input);
675                }
676            }
677        }
678        flow
679    }
680
681    fn visit_stmt(&mut self, stmt: &'hir Stmt<'hir>) -> ControlFlow<Self::BreakValue> {
682        if self.paths.is_empty() {
683            return ControlFlow::Continue(());
684        }
685
686        if let StmtKind::If(condition, then_stmt, else_stmt) = &stmt.kind {
687            let (true_paths, false_paths) = self.visit_condition(condition);
688
689            self.paths = true_paths;
690            let _ = self.visit_stmt(then_stmt);
691            let mut joined = std::mem::take(&mut self.paths);
692
693            self.paths = false_paths;
694            if let Some(else_stmt) = else_stmt {
695                let _ = self.visit_stmt(else_stmt);
696            }
697            joined.append(&mut self.paths);
698            self.paths = joined;
699            self.dedup_paths();
700            return ControlFlow::Continue(());
701        }
702
703        if let StmtKind::Try(try_) = &stmt.kind {
704            let _ = self.visit_expr(&try_.expr);
705            let paths = std::mem::take(&mut self.paths);
706            let mut joined = Vec::new();
707            for clause in try_.clauses {
708                self.paths = paths.clone();
709                for &var in clause.args {
710                    let _ = self.visit_nested_var(var);
711                }
712                for stmt in clause.block.stmts {
713                    let _ = self.visit_stmt(stmt);
714                }
715                joined.append(&mut self.paths);
716            }
717            self.paths = joined;
718            self.dedup_paths();
719            return ControlFlow::Continue(());
720        }
721
722        if let StmtKind::Loop(block, source) = &stmt.kind {
723            self.visit_loop(block, *source);
724            return ControlFlow::Continue(());
725        }
726
727        if matches!(stmt.kind, StmtKind::Break | StmtKind::Continue) {
728            let paths = std::mem::take(&mut self.paths);
729            if let Some(control) = self.loop_controls.last_mut() {
730                let destination = if matches!(stmt.kind, StmtKind::Break) {
731                    &mut control.breaks
732                } else {
733                    &mut control.continues
734                };
735                Self::extend_unique(destination, paths);
736            }
737            return ControlFlow::Continue(());
738        }
739
740        if matches!(stmt.kind, StmtKind::Placeholder) {
741            if let Some((modifiers, index, body, body_input)) = self.placeholder {
742                self.visit_continuation(modifiers, index, body, body_input);
743            }
744            return ControlFlow::Continue(());
745        }
746
747        if let StmtKind::Return(expr) = stmt.kind {
748            if let Some(expr) = expr {
749                let _ = self.visit_expr(expr);
750            }
751            let paths = std::mem::take(&mut self.paths);
752            let returns =
753                self.return_controls.last_mut().expect("return control stack is not empty");
754            Self::extend_unique(returns, paths);
755            return ControlFlow::Continue(());
756        }
757
758        if matches!(stmt.kind, StmtKind::AssemblyBlock(_)) {
759            for path in &mut self.paths {
760                for &input in &self.current_inputs {
761                    path.mark_input_modified(input);
762                }
763            }
764            return ControlFlow::Continue(());
765        }
766        let flow = self.walk_stmt(stmt);
767        if branch_always_exits(stmt) {
768            self.paths.clear();
769        }
770        flow
771    }
772}
773
774fn selector_guard(gcx: Gcx<'_>, expr: &Expr<'_>) -> Option<(Selector, bool)> {
775    let ExprKind::Binary(lhs, op, rhs) = &expr.peel_parens().kind else { return None };
776    let matches = match op.kind {
777        BinOpKind::Eq => true,
778        BinOpKind::Ne => false,
779        _ => return None,
780    };
781    if is_msg_sig(lhs) {
782        selected_function_selector(gcx, rhs).map(|selector| (selector, matches))
783    } else if is_msg_sig(rhs) {
784        selected_function_selector(gcx, lhs).map(|selector| (selector, matches))
785    } else {
786        None
787    }
788}
789
790fn is_msg_sig(expr: &Expr<'_>) -> bool {
791    matches!(
792        &expr.peel_parens().kind,
793        ExprKind::Member(base, member)
794            if member.name == sym::sig && is_builtin_named(base, sym::msg)
795    )
796}
797
798fn selected_function_selector(gcx: Gcx<'_>, expr: &Expr<'_>) -> Option<Selector> {
799    let expr = expr.peel_parens();
800    let ExprKind::Member(function, member) = &expr.kind else { return None };
801    if member.name != sym::selector
802        || gcx.builtin_member(expr.id) != Some(Builtin::FunctionSelector)
803    {
804        return None;
805    }
806    let ResolvedMember::Res(hir::Res::Item(ItemId::Function(function))) =
807        gcx.resolved_member(function.peel_parens().id)?
808    else {
809        return None;
810    };
811    Some(gcx.function_selector(function))
812}
813
814/// Returns the statically typed implementation contract for a proxy-style delegatecall.
815fn delegated_contract<'hir>(
816    gcx: Gcx<'hir>,
817    full_calldata_inputs: &[CalldataInput],
818    expr: &'hir Expr<'hir>,
819) -> Option<(ContractId, Option<CalldataInput>)> {
820    let ExprKind::Call(callee, args, _) = &expr.peel_parens().kind else { return None };
821    let ExprKind::Member(receiver, member) = &callee.peel_parens().kind else { return None };
822    let required_input = forwards_full_calldata(args, full_calldata_inputs)?;
823    if member.name != kw::Delegatecall
824        || gcx.builtin_callee(callee.id) != Some(Builtin::AddressDelegatecall)
825        || !gcx.type_of_expr(receiver.peel_parens().id).is_some_and(ty_is_address)
826    {
827        return None;
828    }
829    typed_contract_behind_address_cast(gcx, receiver).map(|contract| (contract, required_input))
830}
831
832fn typed_contract_behind_address_cast<'hir>(
833    gcx: Gcx<'hir>,
834    expr: &'hir Expr<'hir>,
835) -> Option<ContractId> {
836    let expr = expr.peel_parens();
837    if let Some(ty) = gcx.type_of_expr(expr.id)
838        && let TyKind::Contract(id) = ty.peel_refs().kind
839    {
840        return Some(id);
841    }
842    match &expr.kind {
843        ExprKind::Call(callee, args, _) if is_address_cast(callee) => {
844            args.exprs().next().and_then(|arg| typed_contract_behind_address_cast(gcx, arg))
845        }
846        ExprKind::Payable(inner) => typed_contract_behind_address_cast(gcx, inner),
847        _ => None,
848    }
849}
850
851fn forwards_full_calldata(
852    args: &CallArgs<'_>,
853    full_calldata_inputs: &[CalldataInput],
854) -> Option<Option<CalldataInput>> {
855    let arg = args.exprs().next()?;
856    full_calldata_source(arg, full_calldata_inputs)
857}
858
859fn full_calldata_source(
860    expr: &Expr<'_>,
861    full_calldata_inputs: &[CalldataInput],
862) -> Option<Option<CalldataInput>> {
863    if matches!(
864        &expr.peel_parens().kind,
865        ExprKind::Member(base, member)
866            if member.name == sym::data && is_builtin_named(base, sym::msg)
867    ) {
868        return Some(None);
869    }
870    let ExprKind::Ident(reses) = &expr.peel_parens().kind else { return None };
871    reses.iter().find_map(|res| {
872        let hir::Res::Item(ItemId::Variable(id)) = res else { return None };
873        full_calldata_inputs.iter().copied().find(|input| input.variable() == *id).map(Some)
874    })
875}
876
877fn modifier_input_bindings<'hir>(
878    hir: &'hir hir::Hir<'hir>,
879    modifier: &'hir hir::Function<'hir>,
880    args: &'hir CallArgs<'hir>,
881    full_calldata_inputs: &[CalldataInput],
882    modifier_index: usize,
883) -> Vec<(CalldataInput, Option<CalldataInput>)> {
884    modifier
885        .parameters
886        .iter()
887        .copied()
888        .filter_map(|param| {
889            let arg = arg_for_param(hir, modifier, param, args)?;
890            let input = CalldataInput::Modifier { index: modifier_index, param };
891            Some((input, full_calldata_source(arg, full_calldata_inputs)?))
892        })
893        .collect()
894}
895
896fn arg_for_param<'hir>(
897    hir: &'hir hir::Hir<'hir>,
898    function: &'hir hir::Function<'hir>,
899    param: hir::VariableId,
900    args: &'hir CallArgs<'hir>,
901) -> Option<&'hir Expr<'hir>> {
902    let param_idx = function.parameters.iter().position(|candidate| *candidate == param)?;
903    match args.kind {
904        hir::CallArgsKind::Unnamed(exprs) => exprs.get(param_idx),
905        hir::CallArgsKind::Named(named) => {
906            let param_name = hir.variable(param).name?;
907            named.iter().find(|arg| arg.name.name == param_name.name).map(|arg| &arg.value)
908        }
909    }
910}
911
912fn is_builtin_named(expr: &Expr<'_>, name: solar::interface::Symbol) -> bool {
913    matches!(
914        &expr.peel_parens().kind,
915        ExprKind::Ident(reses)
916            if reses.iter().any(|res| matches!(res, hir::Res::Builtin(b) if b.name() == name))
917    )
918}
919
920fn is_address_cast(callee: &Expr<'_>) -> bool {
921    matches!(
922        &callee.peel_parens().kind,
923        ExprKind::Type(hir::Type {
924            kind: TypeKind::Elementary(hir::ElementaryType::Address(_)),
925            ..
926        })
927    )
928}
929
930fn ty_is_address(ty: Ty<'_>) -> bool {
931    matches!(ty.peel_refs().kind, TyKind::Elementary(hir::ElementaryType::Address(_)))
932}