Skip to main content

forge_lint/sol/low/
solmate_safe_transfer_lib.rs

1use super::SolmateSafeTransferLib;
2use crate::{
3    linter::{LateLintPass, LintContext},
4    sol::{Severity, SolLint},
5};
6use solar::{
7    interface::source_map::FileName,
8    sema::{
9        Gcx,
10        hir::{self, Expr, ExprKind, FunctionId, Hir, Visit},
11        ty::TyKind,
12    },
13};
14use std::{convert::Infallible, ops::ControlFlow};
15
16declare_forge_lint!(
17    SOLMATE_SAFE_TRANSFER_LIB,
18    Severity::Low,
19    "solmate-safe-transfer-lib",
20    "Solmate's `SafeTransferLib` does not check that the token has code, so a transfer to a token-less address succeeds silently"
21);
22
23impl<'hir> LateLintPass<'hir> for SolmateSafeTransferLib {
24    fn check_function(
25        &mut self,
26        ctx: &LintContext,
27        gcx: Gcx<'hir>,
28        hir: &'hir Hir<'hir>,
29        func: &'hir hir::Function<'hir>,
30    ) {
31        if let Some(body) = &func.body {
32            let mut finder = TokenOpFinder { gcx, hir, ctx };
33            for stmt in body.stmts {
34                let _ = finder.visit_stmt(stmt);
35            }
36        }
37    }
38}
39
40/// Looks for references to `SafeTransferLib`'s token operations, called or used as values.
41struct TokenOpFinder<'ctx, 's, 'c, 'hir> {
42    gcx: Gcx<'hir>,
43    hir: &'hir Hir<'hir>,
44    ctx: &'ctx LintContext<'s, 'c>,
45}
46
47impl<'hir> Visit<'hir> for TokenOpFinder<'_, '_, '_, 'hir> {
48    type BreakValue = Infallible;
49
50    fn hir(&self) -> &'hir Hir<'hir> {
51        self.hir
52    }
53
54    fn visit_expr(&mut self, expr: &'hir Expr<'hir>) -> ControlFlow<Self::BreakValue> {
55        // A name or member expression typed as a function is a resolved reference, called or
56        // used as a value: judge the single declaration the type checker selected. The callee
57        // of a call is visited by the default walk, so calls need no dedicated arm.
58        if matches!(expr.kind, ExprKind::Ident(..) | ExprKind::Member(..))
59            && let Some(function_id) = self.resolved_function(expr)
60            && self.is_unchecked_token_op(function_id)
61        {
62            self.ctx.emit(&SOLMATE_SAFE_TRANSFER_LIB, expr.span);
63        }
64        self.walk_expr(expr)
65    }
66}
67
68impl TokenOpFinder<'_, '_, '_, '_> {
69    /// The single function an expression resolves to, for a callee or a reference used as a
70    /// value. `type_of_expr` is the function the type checker resolved, so overload selection,
71    /// override shadowing, the qualified and `using for` forms and import aliases are already
72    /// accounted for.
73    fn resolved_function(&self, expr: &Expr<'_>) -> Option<FunctionId> {
74        let ty = self.gcx.type_of_expr(expr.peel_parens().id)?;
75        match ty.kind {
76            TyKind::Fn(function_ty) => function_ty.function_id,
77            _ => None,
78        }
79    }
80
81    /// Whether `function_id` is one of the token operations of solmate's `SafeTransferLib`.
82    /// `safeTransferETH` stays out: sending ETH involves no token code, so the missing-code
83    /// concern does not apply to it. A same-name function of another library (Uniswap's
84    /// `TransferHelper` style) stays out through the resolution, and so does a same-name
85    /// library from another package (Solady's `SafeTransferLib` checks token code on the
86    /// empty-return path), which fails the provenance check.
87    fn is_unchecked_token_op(&self, function_id: FunctionId) -> bool {
88        let function = self.hir.function(function_id);
89        let Some(name) = function.name else { return false };
90        let Some(contract_id) = function.contract else { return false };
91        // The name alone does not prove the declaration is solmate's: the declaring source
92        // must come from a solmate package path (`lib/solmate`, `solmate/...`).
93        if !self.is_solmate_source(function.source) {
94            return false;
95        }
96        let contract = self.hir.contract(contract_id);
97        matches!(name.as_str(), "safeTransfer" | "safeTransferFrom" | "safeApprove")
98            && contract.kind.is_library()
99            && contract.name.as_str() == "SafeTransferLib"
100    }
101
102    /// Whether a source file belongs to the solmate package, judged by a full path component.
103    /// Matching a whole component rather than a substring keeps a vendored or patched copy under
104    /// a misleading path such as `vendor/solmate-fixed/` from being recognized.
105    fn is_solmate_source(&self, source_id: hir::SourceId) -> bool {
106        match &self.hir.source(source_id).file.name {
107            FileName::Real(path) => path.components().any(|component| {
108                matches!(component, std::path::Component::Normal(name)
109                    if name.eq_ignore_ascii_case("solmate"))
110            }),
111            _ => false,
112        }
113    }
114}