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,
8    interface::{Symbol, kw, sym},
9    sema::{
10        Gcx, Ty,
11        hir::{self, ExprKind, TypeKind},
12        ty::TyKind,
13    },
14};
15
16declare_forge_lint!(
17    RETURN_BOMB,
18    Severity::Low,
19    "return-bomb",
20    "external calls with a gas limit should not consume unbounded return data"
21);
22
23impl<'hir> LateLintPass<'hir> for ReturnBomb {
24    fn check_expr(
25        &mut self,
26        ctx: &LintContext,
27        gcx: Gcx<'hir>,
28        _hir: &'hir hir::Hir<'hir>,
29        expr: &'hir hir::Expr<'hir>,
30    ) {
31        // Flag gas-limited calls that can force the caller to copy unbounded returndata.
32        if low_level_call_with_gas_consumes_unbounded_return_data(gcx, expr)
33            || call_with_gas_returns_dynamic_data(gcx, expr)
34        {
35            ctx.emit(&RETURN_BOMB, expr.span);
36        }
37    }
38}
39
40/// Returns true for gas-limited calls that return dynamic data.
41fn call_with_gas_returns_dynamic_data<'hir>(gcx: Gcx<'hir>, expr: &'hir hir::Expr<'hir>) -> bool {
42    is_call_with_gas_limit(expr)
43        && gcx.type_of_expr(expr.peel_parens().id).is_some_and(|ty| is_dynamic_ty(gcx, ty))
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<'hir>(
48    gcx: Gcx<'hir>,
49    expr: &'hir hir::Expr<'hir>,
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(gcx, receiver)
59}
60/// Returns true if an expression is known to have an address type.
61fn expr_is_address<'hir>(gcx: Gcx<'hir>, expr: &'hir hir::Expr<'hir>) -> bool {
62    match &expr.peel_parens().kind {
63        ExprKind::Payable(_) => true,
64        ExprKind::Call(callee, _, _) => {
65            matches!(
66                &callee.peel_parens().kind,
67                ExprKind::Type(hir::Type {
68                    kind: TypeKind::Elementary(ElementaryType::Address(_)),
69                    ..
70                })
71            ) || callee_is_address_returning_builtin(callee)
72                || gcx.type_of_expr(expr.peel_parens().id).is_some_and(ty_is_address)
73        }
74        ExprKind::Member(base, member) if member_is_builtin_address(base, member.name) => true,
75        _ => gcx.type_of_expr(expr.peel_parens().id).is_some_and(ty_is_address),
76    }
77}
78
79fn callee_is_address_returning_builtin(callee: &hir::Expr<'_>) -> bool {
80    let ExprKind::Ident(reses) = &callee.peel_parens().kind else { return false };
81    reses
82        .iter()
83        .any(|res| matches!(res, hir::Res::Builtin(builtin) if builtin.name() == sym::ecrecover))
84}
85
86fn member_is_builtin_address(base: &hir::Expr<'_>, member: Symbol) -> bool {
87    let ExprKind::Ident(reses) = &base.peel_parens().kind else { return false };
88    reses.iter().any(|res| {
89        let hir::Res::Builtin(builtin) = res else { return false };
90        matches!(
91            (builtin.name(), member),
92            (sym::msg, sym::sender) | (sym::block, kw::Coinbase) | (sym::tx, kw::Origin)
93        )
94    })
95}
96
97fn ty_is_address(ty: Ty<'_>) -> bool {
98    matches!(ty.peel_refs().kind, TyKind::Elementary(ElementaryType::Address(_)))
99}
100
101fn is_dynamic_ty<'hir>(gcx: Gcx<'hir>, ty: Ty<'hir>) -> bool {
102    let ty = ty.peel_refs();
103    match ty.kind {
104        TyKind::Struct(id) => {
105            ty.is_dynamically_encoded(gcx)
106                || gcx.struct_field_types(id).iter().any(|ty| is_dynamic_ty(gcx, *ty))
107        }
108        TyKind::Tuple(elements) => elements.iter().any(|ty| is_dynamic_ty(gcx, *ty)),
109        _ => ty.is_dynamically_encoded(gcx),
110    }
111}