Skip to main content

forge_lint/sol/low/
return_bomb.rs

1use super::ReturnBomb;
2use crate::{
3    linter::{LateLintPass, LintContext},
4    sol::{Severity, SolLint, analysis::is_call_with_gas_limit},
5};
6use solar::sema::{
7    Gcx, Ty,
8    builtins::Builtin,
9    hir::{self, ExprKind},
10    ty::TyKind,
11};
12
13declare_forge_lint!(
14    RETURN_BOMB,
15    Severity::Low,
16    "return-bomb",
17    "external call with a gas limit may copy unbounded return data"
18);
19
20impl<'gcx> LateLintPass<'gcx> for ReturnBomb {
21    fn check_expr(&mut self, ctx: &LintContext, gcx: Gcx<'gcx>, expr: &'gcx hir::Expr<'gcx>) {
22        // Flag gas-limited calls that can force the caller to copy unbounded returndata: a
23        // low-level call on an address, or a call returning dynamic data.
24        let expr = expr.peel_parens();
25        if !is_call_with_gas_limit(expr) {
26            return;
27        }
28        let ExprKind::Call(callee, ..) = &expr.kind else { return };
29        let low_level = matches!(
30            gcx.resolved_builtin(callee),
31            Some(Builtin::AddressCall | Builtin::AddressDelegatecall | Builtin::AddressStaticcall)
32        );
33        if low_level || gcx.type_of_expr(expr.id).is_some_and(|ty| is_dynamic_ty(gcx, ty)) {
34            ctx.emit(&RETURN_BOMB, expr.span);
35        }
36    }
37}
38
39fn is_dynamic_ty<'gcx>(gcx: Gcx<'gcx>, ty: Ty<'gcx>) -> bool {
40    let ty = ty.peel_refs();
41    match ty.kind {
42        TyKind::Tuple(elements) => elements.iter().any(|ty| is_dynamic_ty(gcx, *ty)),
43        _ => ty.is_dynamically_encoded(gcx),
44    }
45}