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::hir::{self},
13};
14use std::ops::ControlFlow;
15
16declare_forge_lint!(
17 UNCHECKED_CALL,
18 Severity::High,
19 "unchecked-call",
20 "Low-level calls should check the success return value"
21);
22
23declare_forge_lint!(
24 ERC20_UNCHECKED_TRANSFER,
25 Severity::High,
26 "erc20-unchecked-transfer",
27 "ERC20 'transfer' and 'transferFrom' calls should check the return value"
28);
29
30impl<'hir> LateLintPass<'hir> for UncheckedTransferERC20 {
38 fn check_stmt(
39 &mut self,
40 ctx: &LintContext,
41 hir: &'hir hir::Hir<'hir>,
42 stmt: &'hir hir::Stmt<'hir>,
43 ) {
44 if let hir::StmtKind::Expr(expr) = &stmt.kind
46 && is_erc20_transfer_call(hir, expr)
47 {
48 ctx.emit(&ERC20_UNCHECKED_TRANSFER, expr.span);
49 }
50 }
51}
52
53fn is_erc20_transfer_call(hir: &hir::Hir<'_>, expr: &hir::Expr<'_>) -> bool {
59 let hir::ExprKind::Call(
61 hir::Expr { kind: hir::ExprKind::Member(contract_expr, func_ident), .. },
62 call_args,
63 ..,
64 ) = &expr.kind
65 else {
66 return false;
67 };
68
69 let arity = call_args.len();
71 let (expected_params, expected_returns): (&[&str], &[&str]) = match func_ident.as_str() {
72 "transferFrom" if arity == 3 => (&["address", "address", "uint256"], &["bool"]),
73 "transfer" if arity == 2 => (&["address", "uint256"], &["bool"]),
74 _ => return false,
75 };
76
77 let Some(cid) = receiver_contract_id(hir, contract_expr) else { return false };
78
79 hir.contract_item_ids(cid).any(|item| {
81 let Some(fid) = item.as_function() else { return false };
82 let func = hir.function(fid);
83 func.name.is_some_and(|name| name.as_str() == func_ident.as_str())
84 && func.kind.is_function()
85 && func.mutates_state()
86 && func.parameters.len() == expected_params.len()
87 && func.returns.len() == expected_returns.len()
88 && func
89 .parameters
90 .iter()
91 .zip(expected_params)
92 .all(|(id, &ty)| is_elementary(hir, *id, ty))
93 && func
94 .returns
95 .iter()
96 .zip(expected_returns)
97 .all(|(id, &ty)| is_elementary(hir, *id, ty))
98 })
99}
100
101impl<'ast> EarlyLintPass<'ast> for UncheckedCall {
104 fn check_item_function(&mut self, ctx: &LintContext, func: &'ast ItemFunction<'ast>) {
105 if let Some(body) = &func.body {
106 let mut checker = UncheckedCallChecker { ctx };
107 let _ = checker.visit_block(body);
108 }
109 }
110}
111
112struct UncheckedCallChecker<'a, 's> {
118 ctx: &'a LintContext<'s, 'a>,
119}
120
121impl<'ast> Visit<'ast> for UncheckedCallChecker<'_, '_> {
122 type BreakValue = ();
123
124 fn visit_stmt(&mut self, stmt: &'ast Stmt<'ast>) -> ControlFlow<Self::BreakValue> {
125 match &stmt.kind {
126 StmtKind::Expr(expr) => {
128 if is_low_level_call(expr) {
129 self.ctx.emit(&UNCHECKED_CALL, expr.span);
130 } else if let ExprKind::Assign(lhs, _, rhs) = &expr.kind {
131 if is_low_level_call(rhs) && is_unchecked_tuple_assignment(lhs) {
133 self.ctx.emit(&UNCHECKED_CALL, expr.span);
134 }
135 }
136 }
137 StmtKind::DeclMulti(vars, expr)
139 if is_low_level_call(expr) && vars.first().is_none_or(|v| v.is_none()) =>
140 {
141 self.ctx.emit(&UNCHECKED_CALL, stmt.span);
142 }
143 _ => {}
144 }
145 self.walk_stmt(stmt)
146 }
147}
148
149fn is_unchecked_tuple_assignment(expr: &Expr<'_>) -> bool {
154 if let ExprKind::Tuple(elements) = &expr.kind {
155 elements.first().is_none_or(|e| e.is_none())
156 } else {
157 false
158 }
159}