forge_lint/sol/high/
unchecked_calls.rs1use super::{UncheckedCall, UncheckedTransferERC20};
2use crate::{
3 linter::{EarlyLintPass, LateLintPass, LintContext},
4 sol::{Severity, SolLint},
5};
6use solar::{
7 ast::{Expr, ExprKind, ItemFunction, Stmt, StmtKind, visit::Visit},
8 interface::kw,
9 sema::hir::{self},
10};
11use std::ops::ControlFlow;
12
13declare_forge_lint!(
14 UNCHECKED_CALL,
15 Severity::High,
16 "unchecked-call",
17 "Low-level calls should check the success return value"
18);
19
20declare_forge_lint!(
21 ERC20_UNCHECKED_TRANSFER,
22 Severity::High,
23 "erc20-unchecked-transfer",
24 "ERC20 'transfer' and 'transferFrom' calls should check the return value"
25);
26
27impl<'hir> LateLintPass<'hir> for UncheckedTransferERC20 {
35 fn check_stmt(
36 &mut self,
37 ctx: &LintContext,
38 hir: &'hir hir::Hir<'hir>,
39 stmt: &'hir hir::Stmt<'hir>,
40 ) {
41 if let hir::StmtKind::Expr(expr) = &stmt.kind
43 && is_erc20_transfer_call(hir, expr)
44 {
45 ctx.emit(&ERC20_UNCHECKED_TRANSFER, expr.span);
46 }
47 }
48}
49
50fn is_erc20_transfer_call(hir: &hir::Hir<'_>, expr: &hir::Expr<'_>) -> bool {
56 let is_type = |var_id: hir::VariableId, type_str: &str| {
57 matches!(
58 &hir.variable(var_id).ty.kind,
59 hir::TypeKind::Elementary(ty) if ty.to_abi_str() == type_str
60 )
61 };
62
63 let hir::ExprKind::Call(
65 hir::Expr { kind: hir::ExprKind::Member(contract_expr, func_ident), .. },
66 hir::CallArgs { kind: hir::CallArgsKind::Unnamed(args), .. },
67 ..,
68 ) = &expr.kind
69 else {
70 return false;
71 };
72
73 let (expected_params, expected_returns): (&[&str], &[&str]) = match func_ident.as_str() {
75 "transferFrom" if args.len() == 3 => (&["address", "address", "uint256"], &["bool"]),
76 "transfer" if args.len() == 2 => (&["address", "uint256"], &["bool"]),
77 _ => return false,
78 };
79
80 let Some(cid) = (match &contract_expr.kind {
81 hir::ExprKind::Ident([hir::Res::Item(hir::ItemId::Variable(id)), ..]) => {
83 if let hir::TypeKind::Custom(hir::ItemId::Contract(cid)) = hir.variable(*id).ty.kind {
84 Some(cid)
85 } else {
86 None
87 }
88 }
89 hir::ExprKind::Call(
91 hir::Expr {
92 kind: hir::ExprKind::Ident([hir::Res::Item(hir::ItemId::Contract(cid))]),
93 ..
94 },
95 ..,
96 ) => Some(*cid),
97 _ => None,
98 }) else {
99 return false;
100 };
101
102 hir.contract_item_ids(cid).any(|item| {
104 let Some(fid) = item.as_function() else { return false };
105 let func = hir.function(fid);
106 func.name.is_some_and(|name| name.as_str() == func_ident.as_str())
107 && func.kind.is_function()
108 && func.mutates_state()
109 && func.parameters.len() == expected_params.len()
110 && func.returns.len() == expected_returns.len()
111 && func.parameters.iter().zip(expected_params).all(|(id, &ty)| is_type(*id, ty))
112 && func.returns.iter().zip(expected_returns).all(|(id, &ty)| is_type(*id, ty))
113 })
114}
115
116impl<'ast> EarlyLintPass<'ast> for UncheckedCall {
119 fn check_item_function(&mut self, ctx: &LintContext, func: &'ast ItemFunction<'ast>) {
120 if let Some(body) = &func.body {
121 let mut checker = UncheckedCallChecker { ctx };
122 let _ = checker.visit_block(body);
123 }
124 }
125}
126
127struct UncheckedCallChecker<'a, 's> {
133 ctx: &'a LintContext<'s, 'a>,
134}
135
136impl<'ast> Visit<'ast> for UncheckedCallChecker<'_, '_> {
137 type BreakValue = ();
138
139 fn visit_stmt(&mut self, stmt: &'ast Stmt<'ast>) -> ControlFlow<Self::BreakValue> {
140 match &stmt.kind {
141 StmtKind::Expr(expr) => {
143 if is_low_level_call(expr) {
144 self.ctx.emit(&UNCHECKED_CALL, expr.span);
145 } else if let ExprKind::Assign(lhs, _, rhs) = &expr.kind {
146 if is_low_level_call(rhs) && is_unchecked_tuple_assignment(lhs) {
148 self.ctx.emit(&UNCHECKED_CALL, expr.span);
149 }
150 }
151 }
152 StmtKind::DeclMulti(vars, expr) => {
154 if is_low_level_call(expr) && vars.first().is_none_or(|v| v.is_none()) {
155 self.ctx.emit(&UNCHECKED_CALL, stmt.span);
156 }
157 }
158 _ => {}
159 }
160 self.walk_stmt(stmt)
161 }
162}
163
164fn is_low_level_call(expr: &Expr<'_>) -> bool {
172 if let ExprKind::Call(call_expr, _args) = &expr.kind {
173 let callee = match &call_expr.kind {
175 ExprKind::CallOptions(inner_expr, _) => inner_expr,
177 _ => call_expr,
179 };
180
181 if let ExprKind::Member(_, member) = &callee.kind {
182 return matches!(member.name, kw::Call | kw::Delegatecall | kw::Staticcall);
184 }
185 }
186 false
187}
188
189fn is_unchecked_tuple_assignment(expr: &Expr<'_>) -> bool {
194 if let ExprKind::Tuple(elements) = &expr.kind {
195 elements.first().is_none_or(|e| e.is_none())
196 } else {
197 false
198 }
199}