forge_lint/sol/high/
unchecked_calls.rs1use super::{UncheckedCall, UncheckedTransferERC20};
2use crate::{
3 linter::{EarlyLintPass, LateLintPass, LintContext},
4 sol::{
5 Severity, SolLint,
6 analysis::{is_elementary, is_low_level_call, receiver_contract_id},
7 },
8};
9use solar::{
10 ast::{ExprKind, Stmt, StmtKind},
11 sema::{Gcx, hir},
12};
13
14declare_forge_lint!(
15 UNCHECKED_CALL,
16 Severity::High,
17 "unchecked-call",
18 "low-level call does not check the success return value"
19);
20
21declare_forge_lint!(
22 ERC20_UNCHECKED_TRANSFER,
23 Severity::High,
24 "erc20-unchecked-transfer",
25 "ERC20 `transfer` or `transferFrom` call does not check the return value"
26);
27
28impl<'gcx> LateLintPass<'gcx> for UncheckedTransferERC20 {
34 fn check_stmt(&mut self, ctx: &LintContext, gcx: Gcx<'gcx>, stmt: &'gcx hir::Stmt<'gcx>) {
35 if let hir::StmtKind::Expr(expr) = &stmt.kind
37 && is_erc20_transfer_call(gcx, expr)
38 {
39 ctx.emit(&ERC20_UNCHECKED_TRANSFER, expr.span);
40 }
41 }
42}
43
44fn is_erc20_transfer_call<'gcx>(gcx: Gcx<'gcx>, expr: &hir::Expr<'gcx>) -> bool {
48 let hir::ExprKind::Call(callee, call_args, ..) = &expr.kind else { return false };
49 let hir::ExprKind::Member(receiver, func_ident) = &callee.kind else { return false };
50 let params: &[&str] = match (func_ident.as_str(), call_args.len()) {
51 ("transfer", 2) => &["address", "uint256"],
52 ("transferFrom", 3) => &["address", "address", "uint256"],
53 _ => return false,
54 };
55 if receiver_contract_id(gcx, receiver).is_none() {
56 return false;
57 }
58 gcx.resolved_function(callee).is_some_and(|fid| {
59 let func = gcx.hir.function(fid);
60 func.name.is_some_and(|name| name.name == func_ident.name)
61 && func.kind.is_function()
62 && func.mutates_state()
63 && func.parameters.len() == params.len()
64 && func.parameters.iter().zip(params).all(|(id, ty)| is_elementary(&gcx.hir, *id, ty))
65 && matches!(func.returns, [ret] if is_elementary(&gcx.hir, *ret, "bool"))
66 })
67}
68
69impl<'ast> EarlyLintPass<'ast> for UncheckedCall {
73 fn check_stmt(&mut self, ctx: &LintContext, stmt: &'ast Stmt<'ast>) {
74 let span = match &stmt.kind {
75 StmtKind::Expr(expr)
77 if is_low_level_call(expr)
78 || matches!(&expr.kind, ExprKind::Assign(lhs, _, rhs)
79 if is_low_level_call(rhs)
80 && matches!(&lhs.kind, ExprKind::Tuple(elements)
81 if elements.first().is_none_or(|e| e.is_none()))) =>
82 {
83 expr.span
84 }
85 StmtKind::DeclMulti(vars, expr)
87 if is_low_level_call(expr) && vars.first().is_none_or(|v| v.is_none()) =>
88 {
89 stmt.span
90 }
91 _ => return,
92 };
93 ctx.emit(&UNCHECKED_CALL, span);
94 }
95}