Skip to main content

forge_lint/sol/high/
unprotected_initializer.rs

1use super::UnprotectedInitializer;
2use crate::{
3    linter::{LateLintPass, LintContext},
4    sol::{Severity, SolLint},
5};
6use solar::{
7    ast::{ContractKind, DataLocation, FunctionKind, StateMutability, Visibility},
8    interface::{Symbol, kw, sym},
9    sema::{
10        builtins::Builtin,
11        hir::{self, ContractId, ExprKind, FunctionId, ItemId, Res, StmtKind, VariableId},
12    },
13};
14use std::collections::HashSet;
15
16declare_forge_lint!(
17    UNPROTECTED_INITIALIZER,
18    Severity::High,
19    "unprotected-initializer",
20    "upgradeable initializer is not protected against direct implementation calls"
21);
22
23impl<'hir> LateLintPass<'hir> for UnprotectedInitializer {
24    fn check_nested_contract(
25        &mut self,
26        ctx: &LintContext,
27        _gcx: solar::sema::Gcx<'hir>,
28        hir: &'hir hir::Hir<'hir>,
29        contract_id: ContractId,
30    ) {
31        let contract = hir.contract(contract_id);
32        if !matches!(contract.kind, ContractKind::Contract) || contract.linearization_failed() {
33            return;
34        }
35
36        let upgradeable = contract
37            .linearized_bases
38            .iter()
39            .any(|&base_id| hir.contract(base_id).name.as_str() == "Initializable");
40        let runtime_entries = effective_runtime_dispatch_surface(hir, contract.linearized_bases);
41        if !upgradeable
42            && !runtime_entries.iter().any(|&fid| has_initializer_modifier(hir, hir.function(fid)))
43        {
44            return;
45        }
46
47        if initializers_disabled_in_constructor(hir, contract) {
48            return;
49        }
50
51        if !has_destructive_entrypoint(hir, contract, &runtime_entries) {
52            return;
53        }
54
55        for fid in runtime_entries {
56            let func = hir.function(fid);
57            if !is_public_initializer(hir, func) || has_modifier_named(hir, func, "onlyProxy") {
58                continue;
59            }
60
61            let Some(body) = func.body else { continue };
62            let mut analyzer =
63                StateWriteAnalyzer { hir, bases: contract.linearized_bases, stack: Vec::new() };
64            if analyzer.block_writes_state(body) {
65                ctx.emit(&UNPROTECTED_INITIALIZER, func.name.map_or(func.span, |name| name.span));
66            }
67        }
68    }
69}
70
71fn is_public_initializer(hir: &hir::Hir<'_>, func: &hir::Function<'_>) -> bool {
72    func.kind.is_function()
73        && matches!(func.visibility, Visibility::Public | Visibility::External)
74        && !matches!(func.state_mutability, StateMutability::Pure | StateMutability::View)
75        && has_initializer_modifier(hir, func)
76}
77
78fn initializers_disabled_in_constructor<'hir>(
79    hir: &'hir hir::Hir<'hir>,
80    contract: &hir::Contract<'hir>,
81) -> bool {
82    contract.linearized_bases.iter().filter_map(|&cid| hir.contract(cid).ctor).any(|ctor_id| {
83        let ctor = hir.function(ctor_id);
84        function_calls_named(hir, ctor, contract.linearized_bases, "_disableInitializers")
85    })
86}
87
88fn has_destructive_entrypoint<'hir>(
89    hir: &'hir hir::Hir<'hir>,
90    contract: &hir::Contract<'hir>,
91    runtime_entries: &[FunctionId],
92) -> bool {
93    runtime_entries.iter().copied().any(|fid| {
94        let func = hir.function(fid);
95        if has_modifier_named(hir, func, "onlyProxy") {
96            return false;
97        }
98
99        let Some(body) = func.body else { return false };
100        let mut finder =
101            DestructiveSinkFinder { hir, bases: contract.linearized_bases, stack: vec![fid] };
102        finder.block_has_destructive_sink(body)
103    })
104}
105
106fn effective_runtime_dispatch_surface(hir: &hir::Hir<'_>, bases: &[ContractId]) -> Vec<FunctionId> {
107    let mut seen_functions: HashSet<(Symbol, String)> = HashSet::new();
108    let mut seen_fallback = false;
109    let mut seen_receive = false;
110    let mut entries = Vec::new();
111
112    for &cid in bases {
113        for fid in hir.contract(cid).all_functions() {
114            let func = hir.function(fid);
115            match func.kind {
116                FunctionKind::Function => {
117                    if !matches!(func.visibility, Visibility::Public | Visibility::External) {
118                        continue;
119                    }
120                    let Some(name) = func.name else { continue };
121                    if seen_functions.insert((name.name, parameter_signature(hir, func.parameters)))
122                    {
123                        entries.push(fid);
124                    }
125                }
126                FunctionKind::Fallback => {
127                    if !seen_fallback {
128                        seen_fallback = true;
129                        entries.push(fid);
130                    }
131                }
132                FunctionKind::Receive => {
133                    if !seen_receive {
134                        seen_receive = true;
135                        entries.push(fid);
136                    }
137                }
138                FunctionKind::Constructor | FunctionKind::Modifier => {}
139            }
140        }
141    }
142
143    entries
144}
145
146fn parameter_signature(hir: &hir::Hir<'_>, params: &[VariableId]) -> String {
147    params
148        .iter()
149        .map(|&param| format!("{:?}", hir.variable(param).ty.kind))
150        .collect::<Vec<_>>()
151        .join(",")
152}
153
154struct DestructiveSinkFinder<'hir> {
155    hir: &'hir hir::Hir<'hir>,
156    bases: &'hir [ContractId],
157    stack: Vec<FunctionId>,
158}
159
160impl<'hir> DestructiveSinkFinder<'hir> {
161    fn block_has_destructive_sink(&mut self, block: hir::Block<'hir>) -> bool {
162        block.stmts.iter().any(|stmt| self.stmt_has_destructive_sink(stmt))
163    }
164
165    fn stmt_has_destructive_sink(&mut self, stmt: &'hir hir::Stmt<'hir>) -> bool {
166        match &stmt.kind {
167            StmtKind::DeclSingle(var_id) => self
168                .hir
169                .variable(*var_id)
170                .initializer
171                .is_some_and(|init| self.expr_has_destructive_sink(init)),
172            StmtKind::DeclMulti(_, expr)
173            | StmtKind::Emit(expr)
174            | StmtKind::Revert(expr)
175            | StmtKind::Return(Some(expr))
176            | StmtKind::Expr(expr) => self.expr_has_destructive_sink(expr),
177            StmtKind::Block(block) | StmtKind::UncheckedBlock(block) | StmtKind::Loop(block, _) => {
178                self.block_has_destructive_sink(*block)
179            }
180            StmtKind::If(condition, then_stmt, else_stmt) => {
181                self.expr_has_destructive_sink(condition)
182                    || self.stmt_has_destructive_sink(then_stmt)
183                    || else_stmt.is_some_and(|stmt| self.stmt_has_destructive_sink(stmt))
184            }
185            StmtKind::Try(stmt_try) => {
186                self.expr_has_destructive_sink(&stmt_try.expr)
187                    || stmt_try
188                        .clauses
189                        .iter()
190                        .any(|clause| self.block_has_destructive_sink(clause.block))
191            }
192            StmtKind::Return(None)
193            | StmtKind::Break
194            | StmtKind::Continue
195            | StmtKind::Placeholder
196            | StmtKind::AssemblyBlock(_)
197            | StmtKind::Switch(_)
198            | StmtKind::Err(_) => false,
199        }
200    }
201
202    fn expr_has_destructive_sink(&mut self, expr: &'hir hir::Expr<'hir>) -> bool {
203        match &expr.kind {
204            ExprKind::Call(callee, args, opts) => {
205                if is_destructive_call(callee) {
206                    return true;
207                }
208
209                if self.expr_has_destructive_sink(callee)
210                    || opts.is_some_and(|opts| {
211                        opts.args.iter().any(|opt| self.expr_has_destructive_sink(&opt.value))
212                    })
213                    || args.exprs().any(|arg| self.expr_has_destructive_sink(arg))
214                {
215                    return true;
216                }
217
218                resolved_internal_function_ids(self.hir, callee, self.bases)
219                    .into_iter()
220                    .any(|func_id| self.function_has_destructive_sink(func_id))
221            }
222            ExprKind::Assign(lhs, _, rhs) | ExprKind::Binary(lhs, _, rhs) => {
223                self.expr_has_destructive_sink(lhs) || self.expr_has_destructive_sink(rhs)
224            }
225            ExprKind::Unary(_, inner) | ExprKind::Delete(inner) | ExprKind::Payable(inner) => {
226                self.expr_has_destructive_sink(inner)
227            }
228            ExprKind::Index(base, index) => {
229                self.expr_has_destructive_sink(base)
230                    || index.is_some_and(|index| self.expr_has_destructive_sink(index))
231            }
232            ExprKind::Slice(base, start, end) => {
233                self.expr_has_destructive_sink(base)
234                    || start.is_some_and(|start| self.expr_has_destructive_sink(start))
235                    || end.is_some_and(|end| self.expr_has_destructive_sink(end))
236            }
237            ExprKind::Member(base, _) => self.expr_has_destructive_sink(base),
238            ExprKind::Ternary(condition, if_true, if_false) => {
239                self.expr_has_destructive_sink(condition)
240                    || self.expr_has_destructive_sink(if_true)
241                    || self.expr_has_destructive_sink(if_false)
242            }
243            ExprKind::Array(exprs) => exprs.iter().any(|expr| self.expr_has_destructive_sink(expr)),
244            ExprKind::Tuple(exprs) => {
245                exprs.iter().flatten().any(|expr| self.expr_has_destructive_sink(expr))
246            }
247            ExprKind::Lit(_)
248            | ExprKind::Ident(_)
249            | ExprKind::New(_)
250            | ExprKind::TypeCall(_)
251            | ExprKind::Type(_)
252            | ExprKind::YulMember(..)
253            | ExprKind::Err(_) => false,
254        }
255    }
256
257    fn function_has_destructive_sink(&mut self, func_id: FunctionId) -> bool {
258        if self.stack.contains(&func_id) {
259            return false;
260        }
261
262        let func = self.hir.function(func_id);
263        let Some(body) = func.body else { return false };
264        self.stack.push(func_id);
265        let found = self.block_has_destructive_sink(body);
266        self.stack.pop();
267        found
268    }
269}
270
271fn is_destructive_call(callee: &hir::Expr<'_>) -> bool {
272    match &callee.peel_parens().kind {
273        ExprKind::Member(_, member) => matches!(member.name, kw::Delegatecall | kw::Callcode),
274        ExprKind::Ident(resolutions) => {
275            resolutions.iter().any(|res| matches!(res, Res::Builtin(Builtin::Selfdestruct)))
276        }
277        _ => false,
278    }
279}
280
281fn has_initializer_modifier(hir: &hir::Hir<'_>, func: &hir::Function<'_>) -> bool {
282    has_modifier_named(hir, func, "initializer") || has_modifier_named(hir, func, "reinitializer")
283}
284
285fn has_modifier_named(hir: &hir::Hir<'_>, func: &hir::Function<'_>, name: &str) -> bool {
286    func.modifiers.iter().any(|modifier| modifier_name_is(hir, modifier, name))
287}
288
289fn modifier_name_is(hir: &hir::Hir<'_>, modifier: &hir::Modifier<'_>, name: &str) -> bool {
290    match modifier.id {
291        ItemId::Function(fid) => hir.function(fid).name.is_some_and(|ident| ident.as_str() == name),
292        ItemId::Contract(cid) => hir.contract(cid).name.as_str() == name,
293        _ => false,
294    }
295}
296
297fn function_calls_named<'hir>(
298    hir: &'hir hir::Hir<'hir>,
299    func: &hir::Function<'hir>,
300    bases: &'hir [ContractId],
301    name: &str,
302) -> bool {
303    let Some(body) = func.body else { return false };
304    let mut finder = CallNameFinder { hir, name, bases, stack: vec![] };
305    finder.block_calls_named(body)
306}
307
308struct CallNameFinder<'a, 'hir> {
309    hir: &'hir hir::Hir<'hir>,
310    name: &'a str,
311    bases: &'hir [ContractId],
312    stack: Vec<FunctionId>,
313}
314
315impl<'hir> CallNameFinder<'_, 'hir> {
316    fn block_calls_named(&mut self, block: hir::Block<'hir>) -> bool {
317        block.stmts.iter().any(|stmt| self.stmt_calls_named(stmt))
318    }
319
320    fn stmt_calls_named(&mut self, stmt: &'hir hir::Stmt<'hir>) -> bool {
321        match &stmt.kind {
322            StmtKind::DeclSingle(var_id) => self
323                .hir
324                .variable(*var_id)
325                .initializer
326                .is_some_and(|init| self.expr_calls_named(init)),
327            StmtKind::DeclMulti(_, expr)
328            | StmtKind::Emit(expr)
329            | StmtKind::Revert(expr)
330            | StmtKind::Return(Some(expr))
331            | StmtKind::Expr(expr) => self.expr_calls_named(expr),
332            StmtKind::Block(block) | StmtKind::UncheckedBlock(block) | StmtKind::Loop(block, _) => {
333                self.block_calls_named(*block)
334            }
335            StmtKind::If(condition, then_stmt, else_stmt) => {
336                self.expr_calls_named(condition)
337                    || self.stmt_calls_named(then_stmt)
338                    || else_stmt.is_some_and(|stmt| self.stmt_calls_named(stmt))
339            }
340            StmtKind::Try(stmt_try) => {
341                self.expr_calls_named(&stmt_try.expr)
342                    || stmt_try.clauses.iter().any(|clause| self.block_calls_named(clause.block))
343            }
344            StmtKind::Return(None)
345            | StmtKind::Break
346            | StmtKind::Continue
347            | StmtKind::Placeholder
348            | StmtKind::AssemblyBlock(_)
349            | StmtKind::Switch(_)
350            | StmtKind::Err(_) => false,
351        }
352    }
353
354    fn expr_calls_named(&mut self, expr: &'hir hir::Expr<'hir>) -> bool {
355        match &expr.kind {
356            ExprKind::Call(callee, args, opts) => {
357                let called_functions = resolved_internal_function_ids(self.hir, callee, self.bases);
358                if called_functions
359                    .iter()
360                    .copied()
361                    .any(|func_id| self.function_matches_name(func_id))
362                {
363                    return true;
364                }
365
366                if let Some(opts) = opts
367                    && opts.args.iter().any(|opt| self.expr_calls_named(&opt.value))
368                {
369                    return true;
370                }
371
372                if args.exprs().any(|arg| self.expr_calls_named(arg)) {
373                    return true;
374                }
375
376                for func_id in called_functions {
377                    if self.function_belongs_to_bases(func_id) && self.function_calls_named(func_id)
378                    {
379                        return true;
380                    }
381                }
382
383                false
384            }
385            ExprKind::Assign(lhs, _, rhs) | ExprKind::Binary(lhs, _, rhs) => {
386                self.expr_calls_named(lhs) || self.expr_calls_named(rhs)
387            }
388            ExprKind::Unary(_, inner) | ExprKind::Delete(inner) | ExprKind::Payable(inner) => {
389                self.expr_calls_named(inner)
390            }
391            ExprKind::Index(base, index) => {
392                self.expr_calls_named(base)
393                    || index.is_some_and(|index| self.expr_calls_named(index))
394            }
395            ExprKind::Slice(base, start, end) => {
396                self.expr_calls_named(base)
397                    || start.is_some_and(|start| self.expr_calls_named(start))
398                    || end.is_some_and(|end| self.expr_calls_named(end))
399            }
400            ExprKind::Member(base, _) => self.expr_calls_named(base),
401            ExprKind::Ternary(condition, if_true, if_false) => {
402                self.expr_calls_named(condition)
403                    || self.expr_calls_named(if_true)
404                    || self.expr_calls_named(if_false)
405            }
406            ExprKind::Array(exprs) => exprs.iter().any(|expr| self.expr_calls_named(expr)),
407            ExprKind::Tuple(exprs) => {
408                exprs.iter().flatten().any(|expr| self.expr_calls_named(expr))
409            }
410            ExprKind::Lit(_)
411            | ExprKind::Ident(_)
412            | ExprKind::New(_)
413            | ExprKind::TypeCall(_)
414            | ExprKind::Type(_)
415            | ExprKind::YulMember(..)
416            | ExprKind::Err(_) => false,
417        }
418    }
419
420    fn function_calls_named(&mut self, func_id: FunctionId) -> bool {
421        if self.stack.contains(&func_id) {
422            return false;
423        }
424
425        let func = self.hir.function(func_id);
426        let Some(body) = func.body else { return false };
427        self.stack.push(func_id);
428        let found = self.block_calls_named(body);
429        self.stack.pop();
430        found
431    }
432
433    fn function_matches_name(&self, func_id: FunctionId) -> bool {
434        self.function_belongs_to_bases(func_id)
435            && self.hir.function(func_id).name.is_some_and(|ident| ident.as_str() == self.name)
436    }
437
438    fn function_belongs_to_bases(&self, func_id: FunctionId) -> bool {
439        self.hir
440            .function(func_id)
441            .contract
442            .is_some_and(|contract_id| self.bases.contains(&contract_id))
443    }
444}
445
446struct StateWriteAnalyzer<'hir> {
447    hir: &'hir hir::Hir<'hir>,
448    bases: &'hir [ContractId],
449    stack: Vec<FunctionId>,
450}
451
452impl<'hir> StateWriteAnalyzer<'hir> {
453    fn block_writes_state(&mut self, block: hir::Block<'hir>) -> bool {
454        block.stmts.iter().any(|stmt| self.stmt_writes_state(stmt))
455    }
456
457    fn stmt_writes_state(&mut self, stmt: &'hir hir::Stmt<'hir>) -> bool {
458        match &stmt.kind {
459            StmtKind::DeclSingle(var_id) => self
460                .hir
461                .variable(*var_id)
462                .initializer
463                .is_some_and(|init| self.expr_writes_state(init)),
464            StmtKind::DeclMulti(_, expr)
465            | StmtKind::Emit(expr)
466            | StmtKind::Revert(expr)
467            | StmtKind::Return(Some(expr))
468            | StmtKind::Expr(expr) => self.expr_writes_state(expr),
469            StmtKind::Block(block) | StmtKind::UncheckedBlock(block) | StmtKind::Loop(block, _) => {
470                self.block_writes_state(*block)
471            }
472            StmtKind::If(condition, then_stmt, else_stmt) => {
473                self.expr_writes_state(condition)
474                    || self.stmt_writes_state(then_stmt)
475                    || else_stmt.is_some_and(|stmt| self.stmt_writes_state(stmt))
476            }
477            StmtKind::Try(stmt_try) => {
478                self.expr_writes_state(&stmt_try.expr)
479                    || stmt_try.clauses.iter().any(|clause| self.block_writes_state(clause.block))
480            }
481            StmtKind::Return(None)
482            | StmtKind::Break
483            | StmtKind::Continue
484            | StmtKind::Placeholder
485            | StmtKind::AssemblyBlock(_)
486            | StmtKind::Switch(_)
487            | StmtKind::Err(_) => false,
488        }
489    }
490
491    fn expr_writes_state(&mut self, expr: &'hir hir::Expr<'hir>) -> bool {
492        match &expr.kind {
493            ExprKind::Assign(lhs, _, rhs) => {
494                lhs_writes_state(self.hir, lhs)
495                    || self.expr_writes_state(lhs)
496                    || self.expr_writes_state(rhs)
497            }
498            ExprKind::Delete(inner) => {
499                lhs_writes_state(self.hir, inner) || self.expr_writes_state(inner)
500            }
501            ExprKind::Unary(op, inner) => {
502                (op.kind.has_side_effects() && lhs_writes_state(self.hir, inner))
503                    || self.expr_writes_state(inner)
504            }
505            ExprKind::Call(callee, args, opts) => {
506                if member_call_writes_state(self.hir, callee) {
507                    return true;
508                }
509
510                if self.expr_writes_state(callee)
511                    || opts.is_some_and(|opts| {
512                        opts.args.iter().any(|opt| self.expr_writes_state(&opt.value))
513                    })
514                    || args.exprs().any(|arg| self.expr_writes_state(arg))
515                {
516                    return true;
517                }
518
519                resolved_internal_function_ids(self.hir, callee, self.bases)
520                    .into_iter()
521                    .any(|func_id| self.function_writes_state(func_id))
522            }
523            ExprKind::Binary(lhs, _, rhs) => {
524                self.expr_writes_state(lhs) || self.expr_writes_state(rhs)
525            }
526            ExprKind::Index(base, index) => {
527                self.expr_writes_state(base)
528                    || index.is_some_and(|index| self.expr_writes_state(index))
529            }
530            ExprKind::Slice(base, start, end) => {
531                self.expr_writes_state(base)
532                    || start.is_some_and(|start| self.expr_writes_state(start))
533                    || end.is_some_and(|end| self.expr_writes_state(end))
534            }
535            ExprKind::Member(base, _) | ExprKind::Payable(base) => self.expr_writes_state(base),
536            ExprKind::Ternary(condition, if_true, if_false) => {
537                self.expr_writes_state(condition)
538                    || self.expr_writes_state(if_true)
539                    || self.expr_writes_state(if_false)
540            }
541            ExprKind::Array(exprs) => exprs.iter().any(|expr| self.expr_writes_state(expr)),
542            ExprKind::Tuple(exprs) => {
543                exprs.iter().flatten().any(|expr| self.expr_writes_state(expr))
544            }
545            ExprKind::Lit(_)
546            | ExprKind::Ident(_)
547            | ExprKind::New(_)
548            | ExprKind::TypeCall(_)
549            | ExprKind::Type(_)
550            | ExprKind::YulMember(..)
551            | ExprKind::Err(_) => false,
552        }
553    }
554
555    fn function_writes_state(&mut self, func_id: FunctionId) -> bool {
556        if self.stack.contains(&func_id) {
557            return false;
558        }
559
560        let func = self.hir.function(func_id);
561        let Some(body) = func.body else { return false };
562        self.stack.push(func_id);
563        let writes = self.block_writes_state(body);
564        self.stack.pop();
565        writes
566    }
567}
568
569fn member_call_writes_state(hir: &hir::Hir<'_>, callee: &hir::Expr<'_>) -> bool {
570    let ExprKind::Member(base, member) = &callee.peel_parens().kind else { return false };
571    matches!(member.as_str(), "push" | "pop") && lhs_writes_state(hir, base)
572}
573
574fn lhs_writes_state(hir: &hir::Hir<'_>, expr: &hir::Expr<'_>) -> bool {
575    match &expr.peel_parens().kind {
576        ExprKind::Ident(resolutions) => {
577            resolutions.iter().any(|res| matches!(res, Res::Item(ItemId::Variable(var_id)) if hir.variable(*var_id).kind.is_state()))
578        }
579        ExprKind::Index(base, _) | ExprKind::Slice(base, _, _) | ExprKind::Member(base, _) => {
580            expr_references_storage(hir, base)
581        }
582        ExprKind::Tuple(exprs) => {
583            exprs.iter().flatten().any(|expr| lhs_writes_state(hir, expr))
584        }
585        _ => false,
586    }
587}
588
589fn expr_references_storage(hir: &hir::Hir<'_>, expr: &hir::Expr<'_>) -> bool {
590    match &expr.peel_parens().kind {
591        ExprKind::Ident(resolutions) => resolutions.iter().any(|res| {
592            matches!(res, Res::Item(ItemId::Variable(var_id)) if variable_references_storage(hir.variable(*var_id)))
593        }),
594        ExprKind::Index(base, _) | ExprKind::Slice(base, _, _) | ExprKind::Member(base, _) => {
595            expr_references_storage(hir, base)
596        }
597        _ => false,
598    }
599}
600
601fn variable_references_storage(var: &hir::Variable<'_>) -> bool {
602    var.kind.is_state() || var.data_location == Some(DataLocation::Storage)
603}
604
605fn resolved_internal_function_ids(
606    hir: &hir::Hir<'_>,
607    callee: &hir::Expr<'_>,
608    bases: &[ContractId],
609) -> Vec<FunctionId> {
610    match &callee.peel_parens().kind {
611        ExprKind::Ident(resolutions) => resolutions
612            .iter()
613            .filter_map(|res| match res {
614                Res::Item(ItemId::Function(func_id)) => Some(*func_id),
615                _ => None,
616            })
617            .collect(),
618        ExprKind::Member(base, method) => {
619            let ExprKind::Ident(resolutions) = &base.peel_parens().kind else { return vec![] };
620            let is_super = resolutions
621                .iter()
622                .any(|res| matches!(res, Res::Builtin(builtin) if builtin.name() == sym::super_));
623
624            let contracts: Vec<_> = if is_super {
625                bases.get(1..).unwrap_or_default().to_vec()
626            } else {
627                resolutions
628                    .iter()
629                    .filter_map(|res| match res {
630                        Res::Item(ItemId::Contract(cid)) => Some(*cid),
631                        _ => None,
632                    })
633                    .collect()
634            };
635
636            contracts
637                .into_iter()
638                .flat_map(|cid| hir.contract(cid).all_functions())
639                .filter(|&fid| {
640                    hir.function(fid).name.is_some_and(|name| name.as_str() == method.as_str())
641                })
642                .collect()
643        }
644        _ => vec![],
645    }
646}