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, analysis::source_in_package},
5};
6use solar::sema::{
7    Gcx,
8    hir::{Expr, ExprKind, FunctionId},
9};
10
11declare_forge_lint!(
12    SOLMATE_SAFE_TRANSFER_LIB,
13    Severity::Low,
14    "solmate-safe-transfer-lib",
15    "the `SafeTransferLib` from Solmate does not check that the token has code, so a transfer to a token-less address succeeds silently"
16);
17
18impl<'gcx> LateLintPass<'gcx> for SolmateSafeTransferLib {
19    fn check_expr(&mut self, ctx: &LintContext, gcx: Gcx<'gcx>, expr: &'gcx Expr<'gcx>) {
20        // A name or member expression typed as a function is a resolved reference, called or
21        // used as a value: judge the single declaration the type checker selected (overloads,
22        // overrides, `using for` and import aliases already accounted for).
23        if matches!(expr.kind, ExprKind::Ident(..) | ExprKind::Member(..))
24            && let Some(function_id) = gcx.resolved_function(expr)
25            && is_unchecked_token_op(gcx, function_id)
26        {
27            ctx.emit(&SOLMATE_SAFE_TRANSFER_LIB, expr.span);
28        }
29    }
30}
31
32/// Whether `function_id` is one of the token operations of solmate's `SafeTransferLib`.
33/// `safeTransferETH` stays out: sending ETH involves no token code. A same-name function of
34/// another library (Uniswap's `TransferHelper` style) stays out through the resolution, and so
35/// does a same-name library from another package (Solady's `SafeTransferLib` checks token code
36/// on the empty-return path), which fails the provenance check: the declaring source must come
37/// from a solmate package path (`lib/solmate`, `solmate/...`). Matching a whole path component
38/// rather than a substring keeps a vendored or patched copy under a misleading path such as
39/// `vendor/solmate-fixed/` from being recognized.
40fn is_unchecked_token_op(gcx: Gcx<'_>, function_id: FunctionId) -> bool {
41    let function = gcx.hir.function(function_id);
42    let (Some(name), Some(contract_id)) = (function.name, function.contract) else { return false };
43    let contract = gcx.hir.contract(contract_id);
44    matches!(name.as_str(), "safeTransfer" | "safeTransferFrom" | "safeApprove")
45        && contract.kind.is_library()
46        && contract.name.as_str() == "SafeTransferLib"
47        && source_in_package(&gcx.hir, function.source, &["solmate"])
48}