Skip to main content

forge_lint/sol/high/
unchecked_calls.rs

1use 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
28/// Checks that calls to functions with the same signature as the ERC20 transfer methods, and which
29/// return a boolean, are not ignored.
30///
31/// WARN: can issue false positives, as it doesn't check that the contract being called sticks to
32/// the full ERC20 specification.
33impl<'gcx> LateLintPass<'gcx> for UncheckedTransferERC20 {
34    fn check_stmt(&mut self, ctx: &LintContext, gcx: Gcx<'gcx>, stmt: &'gcx hir::Stmt<'gcx>) {
35        // Only expression statements can contain unchecked transfers.
36        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
44/// Checks if an expression is a call to a contract member matching the ERC20 signature of
45/// * `function transfer(address to, uint256 amount) external returns (bool);`
46/// * `function transferFrom(address from, address to, uint256 amount) external returns (bool);`
47fn 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
69/// Unchecked low-level calls appear as standalone expression statements, or with the success
70/// value discarded in a tuple. When the success value is checked (in require, if, etc.), the
71/// call is part of a larger expression and is not flagged.
72impl<'ast> EarlyLintPass<'ast> for UncheckedCall {
73    fn check_stmt(&mut self, ctx: &LintContext, stmt: &'ast Stmt<'ast>) {
74        let span = match &stmt.kind {
75            // `target.call(data);` and `(, existingVar) = target.call(data);`
76            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            // `(, bytes memory data) = target.call(data);`
86            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}