forge_lint/sol/low/
solmate_safe_transfer_lib.rs1use 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
40struct 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 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 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 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 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 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}