forge_lint/sol/high/
unchecked_calls.rs
1use super::{UncheckedCall, UncheckedTransferERC20};
2use crate::{
3 linter::{EarlyLintPass, LintContext},
4 sol::{Severity, SolLint},
5};
6use solar_ast::{Expr, ExprKind, ItemFunction, Stmt, StmtKind, visit::Visit};
7use solar_interface::kw;
8use std::ops::ControlFlow;
9
10declare_forge_lint!(
11 UNCHECKED_CALL,
12 Severity::High,
13 "unchecked-call",
14 "Low-level calls should check the success return value"
15);
16
17declare_forge_lint!(
18 ERC20_UNCHECKED_TRANSFER,
19 Severity::High,
20 "erc20-unchecked-transfer",
21 "ERC20 'transfer' and 'transferFrom' calls should check the return value"
22);
23
24impl<'ast> EarlyLintPass<'ast> for UncheckedTransferERC20 {
29 fn check_item_function(&mut self, ctx: &LintContext<'_>, func: &'ast ItemFunction<'ast>) {
30 if let Some(body) = &func.body {
31 let mut checker = UncheckedTransferERC20Checker { ctx };
32 let _ = checker.visit_block(body);
33 }
34 }
35}
36
37struct UncheckedTransferERC20Checker<'a, 's> {
43 ctx: &'a LintContext<'s>,
44}
45
46impl<'ast> Visit<'ast> for UncheckedTransferERC20Checker<'_, '_> {
47 type BreakValue = ();
48
49 fn visit_stmt(&mut self, stmt: &'ast Stmt<'ast>) -> ControlFlow<Self::BreakValue> {
50 if let StmtKind::Expr(expr) = &stmt.kind
52 && is_erc20_transfer_call(expr)
53 {
54 self.ctx.emit(&ERC20_UNCHECKED_TRANSFER, expr.span);
55 }
56 self.walk_stmt(stmt)
57 }
58}
59
60fn is_erc20_transfer_call(expr: &Expr<'_>) -> bool {
67 if let ExprKind::Call(call_expr, args) = &expr.kind {
68 if let ExprKind::Member(_, member) = &call_expr.kind {
70 return (args.len() == 2 && member.as_str() == "transfer")
71 || (args.len() == 3 && member.as_str() == "transferFrom");
72 }
73 }
74 false
75}
76
77impl<'ast> EarlyLintPass<'ast> for UncheckedCall {
80 fn check_item_function(&mut self, ctx: &LintContext<'_>, func: &'ast ItemFunction<'ast>) {
81 if let Some(body) = &func.body {
82 let mut checker = UncheckedCallChecker { ctx };
83 let _ = checker.visit_block(body);
84 }
85 }
86}
87
88struct UncheckedCallChecker<'a, 's> {
94 ctx: &'a LintContext<'s>,
95}
96
97impl<'ast> Visit<'ast> for UncheckedCallChecker<'_, '_> {
98 type BreakValue = ();
99
100 fn visit_stmt(&mut self, stmt: &'ast Stmt<'ast>) -> ControlFlow<Self::BreakValue> {
101 match &stmt.kind {
102 StmtKind::Expr(expr) => {
104 if is_low_level_call(expr) {
105 self.ctx.emit(&UNCHECKED_CALL, expr.span);
106 } else if let ExprKind::Assign(lhs, _, rhs) = &expr.kind {
107 if is_low_level_call(rhs) && is_unchecked_tuple_assignment(lhs) {
109 self.ctx.emit(&UNCHECKED_CALL, expr.span);
110 }
111 }
112 }
113 StmtKind::DeclMulti(vars, expr) => {
115 if is_low_level_call(expr) && vars.first().is_none_or(|v| v.is_none()) {
116 self.ctx.emit(&UNCHECKED_CALL, stmt.span);
117 }
118 }
119 _ => {}
120 }
121 self.walk_stmt(stmt)
122 }
123}
124
125fn is_low_level_call(expr: &Expr<'_>) -> bool {
133 if let ExprKind::Call(call_expr, _args) = &expr.kind {
134 let callee = match &call_expr.kind {
136 ExprKind::CallOptions(inner_expr, _) => inner_expr,
138 _ => call_expr,
140 };
141
142 if let ExprKind::Member(_, member) = &callee.kind {
143 return matches!(member.name, kw::Call | kw::Delegatecall | kw::Staticcall);
145 }
146 }
147 false
148}
149
150fn is_unchecked_tuple_assignment(expr: &Expr<'_>) -> bool {
155 if let ExprKind::Tuple(elements) = &expr.kind {
156 elements.first().is_none_or(|e| e.is_none())
157 } else {
158 false
159 }
160}