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(¶ms);
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(¶ms);
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 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
814fn 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}