Skip to main content

forge_lint/sol/low/
return_bomb.rs

1use super::ReturnBomb;
2use crate::{
3    linter::{LateLintPass, LintContext},
4    sol::{Severity, SolLint, calls::is_call_with_gas_limit},
5};
6use solar::{
7    ast::{ElementaryType, LitKind, StrKind},
8    interface::{Symbol, kw, sym},
9    sema::hir::{self, CallArgs, CallArgsKind, ExprKind, ItemId, TypeKind},
10};
11
12declare_forge_lint!(
13    RETURN_BOMB,
14    Severity::Low,
15    "return-bomb",
16    "external calls with a gas limit should not consume unbounded return data"
17);
18
19impl<'hir> LateLintPass<'hir> for ReturnBomb {
20    fn check_expr(
21        &mut self,
22        ctx: &LintContext,
23        hir: &'hir hir::Hir<'hir>,
24        expr: &'hir hir::Expr<'hir>,
25    ) {
26        // Flag gas-limited calls that can force the caller to copy unbounded returndata.
27        if low_level_call_with_gas_consumes_unbounded_return_data(hir, expr)
28            || call_with_gas_returns_dynamic_data(hir, expr)
29        {
30            ctx.emit(&RETURN_BOMB, expr.span);
31        }
32    }
33}
34
35/// Returns true for gas-limited calls that return dynamic data.
36fn call_with_gas_returns_dynamic_data(hir: &hir::Hir<'_>, expr: &hir::Expr<'_>) -> bool {
37    is_call_with_gas_limit(expr) && call_returns_dynamic_data(hir, expr)
38}
39/// Returns true if a call resolves to functions that return dynamic data.
40fn call_returns_dynamic_data(hir: &hir::Hir<'_>, expr: &hir::Expr<'_>) -> bool {
41    let ExprKind::Call(callee, args, _) = &expr.peel_parens().kind else { return false };
42    matching_functions_for_callee(hir, callee, args)
43        .is_some_and(|functions| functions_return_dynamic_data(hir, &functions))
44}
45
46/// Returns true for gas-limited low-level calls that copy unbounded returndata.
47fn low_level_call_with_gas_consumes_unbounded_return_data(
48    hir: &hir::Hir<'_>,
49    expr: &hir::Expr<'_>,
50) -> bool {
51    if !is_call_with_gas_limit(expr) {
52        return false;
53    }
54
55    let ExprKind::Call(callee, _, _) = &expr.peel_parens().kind else { return false };
56    let ExprKind::Member(receiver, member) = &callee.peel_parens().kind else { return false };
57    matches!(member.name, kw::Call | kw::Delegatecall | kw::Staticcall)
58        && expr_is_address(hir, receiver)
59}
60/// Returns the function overloads that can match a callee and its arguments.
61fn matching_functions_for_callee<'hir>(
62    hir: &'hir hir::Hir<'hir>,
63    callee: &hir::Expr<'hir>,
64    args: &CallArgs<'hir>,
65) -> Option<FunctionCandidates<'hir>> {
66    let functions = match &callee.peel_parens().kind {
67        ExprKind::Ident(reses) => {
68            let candidates = reses.iter().filter_map(res_function_id);
69            Some(select_functions_for_args(hir, candidates, args))
70        }
71        ExprKind::Member(base, member) => expr_contract_id(hir, base).map(|contract| {
72            let candidates = function_candidates_for_member(hir, contract, member.name);
73            select_functions_for_args(hir, candidates, args)
74        }),
75        _ => None,
76    };
77
78    functions
79        .filter(|functions| !functions.is_empty())
80        .or_else(|| function_pointer_candidates_for_callee(hir, callee, args))
81}
82
83/// Returns an external function pointer callee if its arguments can match.
84fn function_pointer_candidates_for_callee<'hir>(
85    hir: &'hir hir::Hir<'hir>,
86    callee: &hir::Expr<'hir>,
87    args: &CallArgs<'_>,
88) -> Option<FunctionCandidates<'hir>> {
89    expr_type(hir, callee)
90        .and_then(|ty| function_type_returns_for_args(hir, ty, args))
91        .map(FunctionCandidates::Pointer)
92}
93
94/// Returns function candidates with the given member name on a contract.
95fn function_candidates_for_member<'hir>(
96    hir: &'hir hir::Hir<'hir>,
97    contract: hir::ContractId,
98    member: Symbol,
99) -> impl Iterator<Item = hir::FunctionId> + Clone + 'hir {
100    hir.contract_item_ids(contract).filter_map(move |item| {
101        let ItemId::Function(id) = item else { return None };
102        let function = hir.function(id);
103        (function.name?.name == member && is_externally_callable(function)).then_some(id)
104    })
105}
106
107/// Returns true if a function can be called through `this.foo()` or `contract.foo()`.
108const fn is_externally_callable(function: &hir::Function<'_>) -> bool {
109    matches!(function.visibility, hir::Visibility::Public | hir::Visibility::External)
110}
111
112/// Selects the candidate functions that best match the call arguments.
113fn select_functions_for_args<'hir>(
114    hir: &'hir hir::Hir<'hir>,
115    candidates: impl Iterator<Item = hir::FunctionId>,
116    args: &CallArgs<'hir>,
117) -> FunctionCandidates<'hir> {
118    let mut exact = Vec::new();
119    let mut maybe = Vec::new();
120
121    for id in candidates {
122        match function_arg_match(hir, id, args) {
123            ArgMatch::Exact => exact.push(id),
124            ArgMatch::Maybe => maybe.push(id),
125            ArgMatch::No => {}
126        }
127    }
128
129    if exact.is_empty() {
130        FunctionCandidates::Maybe(maybe)
131    } else {
132        FunctionCandidates::Exact(exact)
133    }
134}
135/// Returns true if a gas-limited call can return dynamic data.
136fn functions_return_dynamic_data(hir: &hir::Hir<'_>, functions: &FunctionCandidates<'_>) -> bool {
137    match functions {
138        FunctionCandidates::Exact(functions) => match functions.as_slice() {
139            [] => false,
140            [function] => function_returns_dynamic_data(hir, *function),
141            [first, rest @ ..] => {
142                let first = function_returns_dynamic_data(hir, *first);
143                rest.iter().all(|&function| function_returns_dynamic_data(hir, function) == first)
144                    && first
145            }
146        },
147        FunctionCandidates::Maybe(functions) => {
148            functions.iter().any(|&function| function_returns_dynamic_data(hir, function))
149        }
150        FunctionCandidates::Pointer(returns) => returns_dynamic_data(hir, returns),
151    }
152}
153
154enum FunctionCandidates<'hir> {
155    Exact(Vec<hir::FunctionId>),
156    Maybe(Vec<hir::FunctionId>),
157    Pointer(&'hir [hir::VariableId]),
158}
159
160impl FunctionCandidates<'_> {
161    const fn is_empty(&self) -> bool {
162        match self {
163            Self::Exact(functions) | Self::Maybe(functions) => functions.is_empty(),
164            Self::Pointer(returns) => returns.is_empty(),
165        }
166    }
167}
168
169/// Returns true if any return variable has a dynamically encoded return type.
170fn returns_dynamic_data(hir: &hir::Hir<'_>, returns: &[hir::VariableId]) -> bool {
171    returns.iter().any(|&var| is_dynamic_type(hir, &hir.variable(var).ty))
172}
173
174const fn res_function_id(res: &hir::Res) -> Option<hir::FunctionId> {
175    match res {
176        hir::Res::Item(ItemId::Function(id)) => Some(*id),
177        _ => None,
178    }
179}
180
181/// Returns true if a function has any dynamically encoded return value.
182fn function_returns_dynamic_data(hir: &hir::Hir<'_>, function: hir::FunctionId) -> bool {
183    returns_dynamic_data(hir, hir.function(function).returns)
184}
185
186enum ArgMatch {
187    Exact,
188    Maybe,
189    No,
190}
191/// Returns how well call arguments match a candidate function signature.
192fn function_arg_match(hir: &hir::Hir<'_>, id: hir::FunctionId, args: &CallArgs<'_>) -> ArgMatch {
193    let function = hir.function(id);
194    params_arg_match(hir, function.parameters, args)
195}
196
197/// Returns how well call arguments match the expected parameter types.
198fn params_arg_match(
199    hir: &hir::Hir<'_>,
200    parameters: &[hir::VariableId],
201    args: &CallArgs<'_>,
202) -> ArgMatch {
203    if args.len() != parameters.len() {
204        return ArgMatch::No;
205    }
206
207    match &args.kind {
208        CallArgsKind::Unnamed(exprs) => {
209            params_match_args(hir, parameters.iter().copied().zip(exprs.iter()))
210        }
211        CallArgsKind::Named(named_args) => {
212            let mut maybe = false;
213            for named_arg in *named_args {
214                let Some(param) = parameters.iter().copied().find(|&param| {
215                    hir.variable(param).name.is_some_and(|name| name.name == named_arg.name.name)
216                }) else {
217                    return ArgMatch::No;
218                };
219                match expr_matches_type(hir, &named_arg.value, &hir.variable(param).ty) {
220                    Some(true) => {}
221                    Some(false) => return ArgMatch::No,
222                    None => maybe = true,
223                }
224            }
225            if maybe { ArgMatch::Maybe } else { ArgMatch::Exact }
226        }
227    }
228}
229/// Returns how well positional arguments match their expected parameter types.
230fn params_match_args<'hir>(
231    hir: &hir::Hir<'hir>,
232    params_and_args: impl Iterator<Item = (hir::VariableId, &'hir hir::Expr<'hir>)>,
233) -> ArgMatch {
234    let mut maybe = false;
235    for (param, arg) in params_and_args {
236        match expr_matches_type(hir, arg, &hir.variable(param).ty) {
237            Some(true) => {}
238            Some(false) => return ArgMatch::No,
239            None => maybe = true,
240        }
241    }
242    if maybe { ArgMatch::Maybe } else { ArgMatch::Exact }
243}
244/// Returns the contract id for expressions known to be contract-typed.
245fn expr_contract_id(hir: &hir::Hir<'_>, expr: &hir::Expr<'_>) -> Option<hir::ContractId> {
246    match &expr.peel_parens().kind {
247        ExprKind::Ident(reses) if is_this(reses) => enclosing_contract(hir, expr),
248        ExprKind::New(hir::Type { kind: TypeKind::Custom(ItemId::Contract(id)), .. }) => Some(*id),
249        ExprKind::Call(callee, _, _) => {
250            expr_type(hir, expr).and_then(type_contract_id).or_else(|| {
251                match &callee.peel_parens().kind {
252                    ExprKind::Ident(reses) => reses.iter().find_map(|res| match res {
253                        hir::Res::Item(ItemId::Contract(id)) => Some(*id),
254                        _ => None,
255                    }),
256                    ExprKind::Type(hir::Type {
257                        kind: TypeKind::Custom(ItemId::Contract(id)),
258                        ..
259                    }) => Some(*id),
260                    ExprKind::New(hir::Type {
261                        kind: TypeKind::Custom(ItemId::Contract(id)),
262                        ..
263                    }) => Some(*id),
264                    _ => None,
265                }
266            })
267        }
268        _ => expr_type(hir, expr).and_then(type_contract_id),
269    }
270}
271
272/// Returns the contract containing an expression span.
273fn enclosing_contract(hir: &hir::Hir<'_>, expr: &hir::Expr<'_>) -> Option<hir::ContractId> {
274    hir.function_ids()
275        .find_map(|id| {
276            let function = hir.function(id);
277            (function.body.is_some() && function.body_span.contains(expr.span))
278                .then_some(function.contract?)
279        })
280        .or_else(|| hir.contract_ids().find(|&id| hir.contract(id).span.contains(expr.span)))
281}
282
283fn is_this(reses: &[hir::Res]) -> bool {
284    reses.iter().any(|res| matches!(res, hir::Res::Builtin(builtin) if builtin.name() == sym::this))
285}
286
287/// Returns the contract id for contract-typed values.
288const fn type_contract_id(ty: &hir::Type<'_>) -> Option<hir::ContractId> {
289    let TypeKind::Custom(ItemId::Contract(id)) = ty.kind else { return None };
290    Some(id)
291}
292/// Returns true if an expression is known to have an address type.
293fn expr_is_address(hir: &hir::Hir<'_>, expr: &hir::Expr<'_>) -> bool {
294    match &expr.peel_parens().kind {
295        ExprKind::Lit(lit) => matches!(lit.kind, LitKind::Address(_)),
296        ExprKind::Payable(_) => true,
297        ExprKind::Call(callee, _, _) => {
298            matches!(
299                &callee.peel_parens().kind,
300                ExprKind::Type(hir::Type {
301                    kind: TypeKind::Elementary(ElementaryType::Address(_)),
302                    ..
303                })
304            ) || callee_is_address_returning_builtin(callee)
305                || expr_type(hir, expr).is_some_and(is_address_type)
306        }
307        ExprKind::Member(base, member) if member_is_builtin_address(base, member.name) => true,
308        _ => expr_type(hir, expr).is_some_and(is_address_type),
309    }
310}
311
312fn callee_is_address_returning_builtin(callee: &hir::Expr<'_>) -> bool {
313    let ExprKind::Ident(reses) = &callee.peel_parens().kind else { return false };
314    reses
315        .iter()
316        .any(|res| matches!(res, hir::Res::Builtin(builtin) if builtin.name() == sym::ecrecover))
317}
318
319fn member_is_builtin_address(base: &hir::Expr<'_>, member: Symbol) -> bool {
320    let ExprKind::Ident(reses) = &base.peel_parens().kind else { return false };
321    reses.iter().any(|res| {
322        let hir::Res::Builtin(builtin) = res else { return false };
323        matches!(
324            (builtin.name(), member),
325            (sym::msg, sym::sender) | (sym::block, kw::Coinbase) | (sym::tx, kw::Origin)
326        )
327    })
328}
329
330/// Returns whether an expression can match the expected type, if known.
331fn expr_matches_type(hir: &hir::Hir<'_>, expr: &hir::Expr<'_>, ty: &hir::Type<'_>) -> Option<bool> {
332    if let Some(matches) = bool_expr_matches_type(expr, ty) {
333        return Some(matches);
334    }
335
336    if let Some(literal) = integer_literal(expr)
337        && let Some(matches) = integer_literal_matches_type(literal, ty)
338    {
339        return Some(matches);
340    }
341
342    match &expr.peel_parens().kind {
343        ExprKind::Lit(lit) => return Some(lit_matches_type(lit, ty)),
344        ExprKind::Payable(_) => return Some(is_address_type(ty)),
345        _ => {}
346    }
347
348    expr_type(hir, expr).map(|expr_ty| types_match(hir, expr_ty, ty))
349}
350
351/// Returns whether a known boolean expression can match the expected type.
352fn bool_expr_matches_type(expr: &hir::Expr<'_>, ty: &hir::Type<'_>) -> Option<bool> {
353    expr_is_bool(expr).then(|| is_bool_type(ty))
354}
355
356fn expr_is_bool(expr: &hir::Expr<'_>) -> bool {
357    match &expr.peel_parens().kind {
358        ExprKind::Binary(_, op, _) => matches!(
359            op.kind,
360            hir::BinOpKind::Lt
361                | hir::BinOpKind::Le
362                | hir::BinOpKind::Gt
363                | hir::BinOpKind::Ge
364                | hir::BinOpKind::Eq
365                | hir::BinOpKind::Ne
366                | hir::BinOpKind::Or
367                | hir::BinOpKind::And
368        ),
369        ExprKind::Unary(op, _) => op.kind == hir::UnOpKind::Not,
370        _ => false,
371    }
372}
373
374const fn is_bool_type(ty: &hir::Type<'_>) -> bool {
375    matches!(ty.kind, TypeKind::Elementary(ElementaryType::Bool))
376}
377/// Returns the type of simple expressions needed by this lint.
378fn expr_type<'hir>(
379    hir: &'hir hir::Hir<'hir>,
380    expr: &hir::Expr<'hir>,
381) -> Option<&'hir hir::Type<'hir>> {
382    match &expr.peel_parens().kind {
383        ExprKind::Ident(reses) => {
384            let var = reses.iter().filter_map(hir::Res::as_variable).next()?;
385            Some(&hir.variable(var).ty)
386        }
387        ExprKind::Index(base, _) => match &expr_type(hir, base)?.kind {
388            TypeKind::Array(array) => Some(&array.element),
389            TypeKind::Mapping(mapping) => Some(&mapping.value),
390            _ => None,
391        },
392        ExprKind::Member(base, member) => {
393            let TypeKind::Custom(ItemId::Struct(id)) = expr_type(hir, base)?.kind else {
394                return None;
395            };
396            hir.strukt(id).fields.iter().find_map(|&field| {
397                (hir.variable(field).name?.name == member.name).then_some(&hir.variable(field).ty)
398            })
399        }
400        ExprKind::Call(callee, args, _) => match &callee.peel_parens().kind {
401            ExprKind::Type(ty) | ExprKind::New(ty) => Some(ty),
402            _ => call_return_type(hir, callee, args),
403        },
404        ExprKind::Ternary(_, true_, false_) => common_expr_type(hir, true_, false_),
405        _ => None,
406    }
407}
408
409/// Returns the common type for a ternary expression when one branch type can accept the other.
410fn common_expr_type<'hir>(
411    hir: &'hir hir::Hir<'hir>,
412    true_: &hir::Expr<'hir>,
413    false_: &hir::Expr<'hir>,
414) -> Option<&'hir hir::Type<'hir>> {
415    let true_ty = expr_type(hir, true_)?;
416    let false_ty = expr_type(hir, false_)?;
417    if types_match(hir, true_ty, false_ty) {
418        Some(false_ty)
419    } else if types_match(hir, false_ty, true_ty) {
420        Some(true_ty)
421    } else {
422        None
423    }
424}
425
426/// Returns the single return type for a statically resolved call expression.
427fn call_return_type<'hir>(
428    hir: &'hir hir::Hir<'hir>,
429    callee: &hir::Expr<'hir>,
430    args: &CallArgs<'hir>,
431) -> Option<&'hir hir::Type<'hir>> {
432    let functions = match matching_functions_for_callee(hir, callee, args)? {
433        FunctionCandidates::Exact(functions) | FunctionCandidates::Maybe(functions) => functions,
434        FunctionCandidates::Pointer(returns) => return single_return_type(hir, returns),
435    };
436    let function = single_function(functions.into_iter())?;
437    single_return_type(hir, hir.function(function).returns)
438}
439
440/// Returns exactly one function id from an iterator.
441fn single_function(
442    mut functions: impl Iterator<Item = hir::FunctionId>,
443) -> Option<hir::FunctionId> {
444    let function = functions.next()?;
445    functions.next().is_none().then_some(function)
446}
447
448/// Returns exactly one return type.
449fn single_return_type<'hir>(
450    hir: &'hir hir::Hir<'hir>,
451    returns: &[hir::VariableId],
452) -> Option<&'hir hir::Type<'hir>> {
453    let &[ret] = returns else { return None };
454    Some(&hir.variable(ret).ty)
455}
456
457/// Returns external function-type return variables when the arguments can match.
458fn function_type_returns_for_args<'hir>(
459    hir: &hir::Hir<'_>,
460    ty: &'hir hir::Type<'hir>,
461    args: &CallArgs<'_>,
462) -> Option<&'hir [hir::VariableId]> {
463    let TypeKind::Function(function) = ty.kind else { return None };
464    if function.visibility != hir::Visibility::External {
465        return None;
466    }
467    if matches!(params_arg_match(hir, function.parameters, args), ArgMatch::No) {
468        return None;
469    }
470    Some(function.returns)
471}
472
473/// Returns true if the type is an address type
474const fn is_address_type(ty: &hir::Type<'_>) -> bool {
475    matches!(ty.kind, TypeKind::Elementary(ElementaryType::Address(_)))
476}
477
478#[derive(Clone, Copy)]
479struct IntegerLiteral {
480    negative: bool,
481    bits: usize,
482    is_zero: bool,
483    is_power_of_two: bool,
484}
485
486/// Returns an integer literal's sign and precision, including unary negation.
487fn integer_literal(expr: &hir::Expr<'_>) -> Option<IntegerLiteral> {
488    match &expr.peel_parens().kind {
489        ExprKind::Lit(lit) => match lit.kind {
490            LitKind::Number(value) => Some(IntegerLiteral {
491                negative: false,
492                bits: value.bit_len().max(1),
493                is_zero: value.is_zero(),
494                is_power_of_two: value.is_power_of_two(),
495            }),
496            _ => None,
497        },
498        ExprKind::Unary(op, expr) if op.kind == hir::UnOpKind::Neg => {
499            let mut literal = integer_literal(expr)?;
500            if !literal.is_zero {
501                literal.negative = !literal.negative;
502            }
503            Some(literal)
504        }
505        _ => None,
506    }
507}
508
509/// Returns whether an integer literal fits in an expected integer type.
510fn integer_literal_matches_type(literal: IntegerLiteral, ty: &hir::Type<'_>) -> Option<bool> {
511    match ty.kind {
512        TypeKind::Elementary(ElementaryType::UInt(size)) => {
513            Some(!literal.negative && literal.bits <= usize::from(size.bits()))
514        }
515        TypeKind::Elementary(ElementaryType::Int(size)) => {
516            let bits = usize::from(size.bits());
517            Some(if literal.negative {
518                literal.bits < bits || (literal.bits == bits && literal.is_power_of_two)
519            } else {
520                literal.bits < bits
521            })
522        }
523        _ => None,
524    }
525}
526
527/// Returns true if a literal can be used for a value of the given type.
528const fn lit_matches_type(lit: &solar::ast::Lit<'_>, ty: &hir::Type<'_>) -> bool {
529    matches!(
530        (&lit.kind, &ty.kind),
531        (LitKind::Address(_), TypeKind::Elementary(ElementaryType::Address(_)))
532            | (LitKind::Bool(_), TypeKind::Elementary(ElementaryType::Bool))
533            | (
534                LitKind::Number(_),
535                TypeKind::Elementary(ElementaryType::Fixed(_, _) | ElementaryType::UFixed(_, _)),
536            )
537            | (
538                LitKind::Rational(_),
539                TypeKind::Elementary(
540                    ElementaryType::Int(_)
541                        | ElementaryType::UInt(_)
542                        | ElementaryType::Fixed(_, _)
543                        | ElementaryType::UFixed(_, _),
544                ),
545            )
546            | (
547                LitKind::Str(StrKind::Str | StrKind::Unicode, ..),
548                TypeKind::Elementary(ElementaryType::String),
549            )
550            | (
551                LitKind::Str(StrKind::Hex, ..),
552                TypeKind::Elementary(ElementaryType::Bytes | ElementaryType::FixedBytes(_)),
553            )
554    )
555}
556
557/// Returns true if two types are equivalent for overload resolution.
558fn types_match(hir: &hir::Hir<'_>, a: &hir::Type<'_>, b: &hir::Type<'_>) -> bool {
559    match (&a.kind, &b.kind) {
560        (
561            TypeKind::Elementary(ElementaryType::Address(_)),
562            TypeKind::Elementary(ElementaryType::Address(_)),
563        ) => true,
564        (TypeKind::Elementary(a), TypeKind::Elementary(b)) => elementary_type_matches(*a, *b),
565        (TypeKind::Array(a), TypeKind::Array(b)) => {
566            array_sizes_match(hir, a.size, b.size) && types_match(hir, &a.element, &b.element)
567        }
568        (
569            TypeKind::Custom(ItemId::Contract(actual)),
570            TypeKind::Custom(ItemId::Contract(expected)),
571        ) => contract_type_matches(hir, *actual, *expected),
572        (TypeKind::Custom(a), TypeKind::Custom(b)) => a == b,
573        (TypeKind::Function(a), TypeKind::Function(b)) => {
574            a.parameters.len() == b.parameters.len()
575                && a.returns.len() == b.returns.len()
576                && a.parameters
577                    .iter()
578                    .zip(b.parameters)
579                    .all(|(&a, &b)| types_match(hir, &hir.variable(a).ty, &hir.variable(b).ty))
580                && a.returns
581                    .iter()
582                    .zip(b.returns)
583                    .all(|(&a, &b)| types_match(hir, &hir.variable(a).ty, &hir.variable(b).ty))
584        }
585        _ => false,
586    }
587}
588
589/// Returns true if an actual contract type can be used where the expected contract type is needed.
590fn contract_type_matches(
591    hir: &hir::Hir<'_>,
592    actual: hir::ContractId,
593    expected: hir::ContractId,
594) -> bool {
595    actual == expected || hir.contract(actual).linearized_bases.contains(&expected)
596}
597
598/// Returns true if two array sizes are equivalent for overload resolution.
599fn array_sizes_match(
600    hir: &hir::Hir<'_>,
601    a: Option<&hir::Expr<'_>>,
602    b: Option<&hir::Expr<'_>>,
603) -> bool {
604    match (a, b) {
605        (None, None) => true,
606        (Some(a), Some(b)) => fixed_array_sizes_match(hir, a, b),
607        _ => false,
608    }
609}
610
611fn fixed_array_sizes_match(hir: &hir::Hir<'_>, a: &hir::Expr<'_>, b: &hir::Expr<'_>) -> bool {
612    matches!(
613        (const_array_size(hir, a), const_array_size(hir, b)),
614        (Some(LitKind::Number(a)), Some(LitKind::Number(b))) if a == b
615    )
616}
617
618type ConstArraySize = LitKind<'static>;
619
620fn const_array_size(hir: &hir::Hir<'_>, expr: &hir::Expr<'_>) -> Option<ConstArraySize> {
621    const MAX_DEPTH: usize = 64;
622    const_array_size_inner(hir, expr, MAX_DEPTH)
623}
624
625fn const_array_size_inner(
626    hir: &hir::Hir<'_>,
627    expr: &hir::Expr<'_>,
628    depth: usize,
629) -> Option<ConstArraySize> {
630    if depth == 0 {
631        return None;
632    }
633
634    match &expr.peel_parens().kind {
635        ExprKind::Ident(reses) => {
636            let var = reses.iter().find_map(hir::Res::as_variable)?;
637            let var = hir.variable(var);
638            if !var.is_constant() {
639                return None;
640            }
641            const_array_size_inner(hir, var.initializer?, depth - 1)
642        }
643        ExprKind::Lit(lit) => match lit.kind {
644            LitKind::Number(value) => Some(LitKind::Number(value)),
645            _ => None,
646        },
647        ExprKind::Unary(op, expr) => {
648            let LitKind::Number(value) = const_array_size_inner(hir, expr, depth - 1)? else {
649                return None;
650            };
651            match op.kind {
652                hir::UnOpKind::BitNot => Some(LitKind::Number(!value)),
653                hir::UnOpKind::Neg => Some(LitKind::Number(value.wrapping_neg())),
654                _ => None,
655            }
656        }
657        ExprKind::Binary(left, op, right) => {
658            let left = const_array_size_inner(hir, left, depth - 1)?;
659            let right = const_array_size_inner(hir, right, depth - 1)?;
660            const_array_size_binary_op(left, op.kind, right)
661        }
662        _ => None,
663    }
664}
665
666fn const_array_size_binary_op(
667    left: ConstArraySize,
668    op: hir::BinOpKind,
669    right: ConstArraySize,
670) -> Option<ConstArraySize> {
671    let (LitKind::Number(left), LitKind::Number(right)) = (left, right) else {
672        return None;
673    };
674
675    let value = match op {
676        hir::BinOpKind::BitOr => left | right,
677        hir::BinOpKind::BitAnd => left & right,
678        hir::BinOpKind::BitXor => left ^ right,
679        hir::BinOpKind::Shr => left.wrapping_shr(right.try_into().unwrap_or(usize::MAX)),
680        hir::BinOpKind::Shl => left.wrapping_shl(right.try_into().unwrap_or(usize::MAX)),
681        hir::BinOpKind::Sar => left.arithmetic_shr(right.try_into().unwrap_or(usize::MAX)),
682        hir::BinOpKind::Add => left.checked_add(right)?,
683        hir::BinOpKind::Sub => left.checked_sub(right)?,
684        hir::BinOpKind::Mul => left.checked_mul(right)?,
685        hir::BinOpKind::Div => left.checked_div(right)?,
686        hir::BinOpKind::Rem => left.checked_rem(right)?,
687        hir::BinOpKind::Pow => left.checked_pow(right)?,
688        hir::BinOpKind::Lt
689        | hir::BinOpKind::Le
690        | hir::BinOpKind::Gt
691        | hir::BinOpKind::Ge
692        | hir::BinOpKind::Eq
693        | hir::BinOpKind::Ne
694        | hir::BinOpKind::Or
695        | hir::BinOpKind::And => return None,
696    };
697    Some(LitKind::Number(value))
698}
699
700/// Returns true if an elementary value can be used for an expected elementary type.
701fn elementary_type_matches(actual: ElementaryType, expected: ElementaryType) -> bool {
702    use ElementaryType::{Address, Bool, Bytes, Fixed, FixedBytes, Int, String, UFixed, UInt};
703
704    match (actual, expected) {
705        (Address(_), Address(false)) => true,
706        (Address(payable), Address(true)) => payable,
707        (UInt(actual), UInt(expected))
708        | (Int(actual), Int(expected))
709        | (FixedBytes(actual), FixedBytes(expected)) => actual.bits() <= expected.bits(),
710        (Bool, Bool) | (String, String) | (Bytes, Bytes) => true,
711        (Fixed(actual_size, actual_scale), Fixed(expected_size, expected_scale))
712        | (UFixed(actual_size, actual_scale), UFixed(expected_size, expected_scale)) => {
713            actual_size == expected_size && actual_scale == expected_scale
714        }
715        _ => false,
716    }
717}
718
719/// Returns true if the type contains dynamically encoded ABI data.
720fn is_dynamic_type(hir: &hir::Hir<'_>, ty: &hir::Type<'_>) -> bool {
721    match &ty.kind {
722        TypeKind::Elementary(ElementaryType::Bytes | ElementaryType::String) => true,
723        TypeKind::Array(array) => array.size.is_none() || is_dynamic_type(hir, &array.element),
724        TypeKind::Custom(ItemId::Struct(id)) => hir
725            .strukt(*id)
726            .fields
727            .iter()
728            .any(|&field| is_dynamic_type(hir, &hir.variable(field).ty)),
729        _ => false,
730    }
731}