forge_lint/sol/low/
return_bomb.rs1use 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 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
40fn 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
46fn 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}
60fn 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}