forge_lint/sol/high/
unchecked_calls.rs1use super::{UncheckedCall, UncheckedTransferERC20};
2use crate::{
3 linter::{EarlyLintPass, LateLintPass, LintContext},
4 sol::{
5 Severity, SolLint,
6 analysis::interface::{is_elementary, receiver_contract_id},
7 calls::is_low_level_call,
8 },
9};
10use solar::{
11 ast::{Expr, ExprKind, ItemFunction, Stmt, StmtKind, visit::Visit},
12 sema::{
13 Gcx,
14 hir::{self},
15 },
16};
17use std::ops::ControlFlow;
18
19declare_forge_lint!(
20 UNCHECKED_CALL,
21 Severity::High,
22 "unchecked-call",
23 "Low-level calls should check the success return value"
24);
25
26declare_forge_lint!(
27 ERC20_UNCHECKED_TRANSFER,
28 Severity::High,
29 "erc20-unchecked-transfer",
30 "ERC20 'transfer' and 'transferFrom' calls should check the return value"
31);
32
33impl<'hir> LateLintPass<'hir> for UncheckedTransferERC20 {
41 fn check_stmt(
42 &mut self,
43 ctx: &LintContext,
44 _gcx: Gcx<'hir>,
45 hir: &'hir hir::Hir<'hir>,
46 stmt: &'hir hir::Stmt<'hir>,
47 ) {
48 if let hir::StmtKind::Expr(expr) = &stmt.kind
50 && is_erc20_transfer_call(hir, expr)
51 {
52 ctx.emit(&ERC20_UNCHECKED_TRANSFER, expr.span);
53 }
54 }
55}
56
57fn is_erc20_transfer_call(hir: &hir::Hir<'_>, expr: &hir::Expr<'_>) -> bool {
63 let hir::ExprKind::Call(
65 hir::Expr { kind: hir::ExprKind::Member(contract_expr, func_ident), .. },
66 call_args,
67 ..,
68 ) = &expr.kind
69 else {
70 return false;
71 };
72
73 let arity = call_args.len();
75 let (expected_params, expected_returns): (&[&str], &[&str]) = match func_ident.as_str() {
76 "transferFrom" if arity == 3 => (&["address", "address", "uint256"], &["bool"]),
77 "transfer" if arity == 2 => (&["address", "uint256"], &["bool"]),
78 _ => return false,
79 };
80
81 let Some(cid) = receiver_contract_id(hir, contract_expr) else { return false };
82
83 hir.contract_item_ids(cid).any(|item| {
85 let Some(fid) = item.as_function() else { return false };
86 let func = hir.function(fid);
87 func.name.is_some_and(|name| name.as_str() == func_ident.as_str())
88 && func.kind.is_function()
89 && func.mutates_state()
90 && func.parameters.len() == expected_params.len()
91 && func.returns.len() == expected_returns.len()
92 && func
93 .parameters
94 .iter()
95 .zip(expected_params)
96 .all(|(id, &ty)| is_elementary(hir, *id, ty))
97 && func
98 .returns
99 .iter()
100 .zip(expected_returns)
101 .all(|(id, &ty)| is_elementary(hir, *id, ty))
102 })
103}
104
105impl<'ast> EarlyLintPass<'ast> for UncheckedCall {
108 fn check_item_function(&mut self, ctx: &LintContext, func: &'ast ItemFunction<'ast>) {
109 if let Some(body) = &func.body {
110 let mut checker = UncheckedCallChecker { ctx };
111 let _ = checker.visit_block(body);
112 }
113 }
114}
115
116struct UncheckedCallChecker<'a, 's> {
122 ctx: &'a LintContext<'s, 'a>,
123}
124
125impl<'ast> Visit<'ast> for UncheckedCallChecker<'_, '_> {
126 type BreakValue = ();
127
128 fn visit_stmt(&mut self, stmt: &'ast Stmt<'ast>) -> ControlFlow<Self::BreakValue> {
129 match &stmt.kind {
130 StmtKind::Expr(expr) => {
132 if is_low_level_call(expr) {
133 self.ctx.emit(&UNCHECKED_CALL, expr.span);
134 } else if let ExprKind::Assign(lhs, _, rhs) = &expr.kind {
135 if is_low_level_call(rhs) && is_unchecked_tuple_assignment(lhs) {
137 self.ctx.emit(&UNCHECKED_CALL, expr.span);
138 }
139 }
140 }
141 StmtKind::DeclMulti(vars, expr)
143 if is_low_level_call(expr) && vars.first().is_none_or(|v| v.is_none()) =>
144 {
145 self.ctx.emit(&UNCHECKED_CALL, stmt.span);
146 }
147 _ => {}
148 }
149 self.walk_stmt(stmt)
150 }
151}
152
153fn is_unchecked_tuple_assignment(expr: &Expr<'_>) -> bool {
158 if let ExprKind::Tuple(elements) = &expr.kind {
159 elements.first().is_none_or(|e| e.is_none())
160 } else {
161 false
162 }
163}