11use super :: { UncheckedCall , UncheckedTransferERC20 } ;
22use crate :: {
3- linter:: { EarlyLintPass , LintContext } ,
3+ linter:: { EarlyLintPass , LateLintPass , LintContext } ,
44 sol:: { Severity , SolLint } ,
55} ;
66use solar:: {
77 ast:: { Expr , ExprKind , ItemFunction , Stmt , StmtKind , visit:: Visit } ,
88 interface:: kw,
9+ sema:: hir:: { self } ,
910} ;
1011use std:: ops:: ControlFlow ;
1112
@@ -25,55 +26,91 @@ declare_forge_lint!(
2526
2627// -- ERC20 UNCKECKED TRANSFERS -------------------------------------------------------------------
2728
28- /// WARN: can issue false positives. It does not check that the contract being called is an ERC20.
29- /// TODO: re-implement using `LateLintPass` so that it can't issue false positives.
30- impl < ' ast > EarlyLintPass < ' ast > for UncheckedTransferERC20 {
31- fn check_item_function ( & mut self , ctx : & LintContext , func : & ' ast ItemFunction < ' ast > ) {
32- if let Some ( body) = & func. body {
33- let mut checker = UncheckedTransferERC20Checker { ctx } ;
34- let _ = checker. visit_block ( body) ;
35- }
36- }
37- }
38-
39- /// Visitor that detects unchecked ERC20 transfer calls within function bodies.
29+ /// Checks that calls to functions with the same signature as the ERC20 transfer methods, and which
30+ /// return a boolean are not ignored.
4031///
41- /// Unchecked transfers appear as standalone expression statements.
42- /// When a transfer's return value is used (in require, assignment, etc.), it's part
43- /// of a larger expression and won't be flagged.
44- struct UncheckedTransferERC20Checker < ' a , ' s > {
45- ctx : & ' a LintContext < ' s , ' a > ,
46- }
47-
48- impl < ' ast > Visit < ' ast > for UncheckedTransferERC20Checker < ' _ , ' _ > {
49- type BreakValue = ( ) ;
50-
51- fn visit_stmt ( & mut self , stmt : & ' ast Stmt < ' ast > ) -> ControlFlow < Self :: BreakValue > {
32+ /// WARN: can issue false positives, as it doesn't check that the contract being called sticks to
33+ /// the full ERC20 specification.
34+ impl < ' hir > LateLintPass < ' hir > for UncheckedTransferERC20 {
35+ fn check_stmt (
36+ & mut self ,
37+ ctx : & LintContext ,
38+ hir : & ' hir hir:: Hir < ' hir > ,
39+ stmt : & ' hir hir:: Stmt < ' hir > ,
40+ ) {
5241 // Only expression statements can contain unchecked transfers.
53- if let StmtKind :: Expr ( expr) = & stmt. kind
54- && is_erc20_transfer_call ( expr)
42+ if let hir :: StmtKind :: Expr ( expr) = & stmt. kind
43+ && is_erc20_transfer_call ( hir , expr)
5544 {
56- self . ctx . emit ( & ERC20_UNCHECKED_TRANSFER , expr. span ) ;
45+ ctx. emit ( & ERC20_UNCHECKED_TRANSFER , expr. span ) ;
5746 }
58- self . walk_stmt ( stmt)
5947 }
6048}
6149
6250/// Checks if an expression is an ERC20 `transfer` or `transferFrom` call.
63- /// `function ERC20. transfer(to, amount)`
64- /// `function ERC20. transferFrom(from, to, amount)`
51+ /// * `function transfer(address to, uint256 amount) external returns bool; `
52+ /// * `function transferFrom(address from, address to, uint256 amount) external returns bool; `
6553///
66- /// Validates both the method name and argument count to avoid false positives
67- /// from other functions that happen to be named "transfer".
68- fn is_erc20_transfer_call ( expr : & Expr < ' _ > ) -> bool {
69- if let ExprKind :: Call ( call_expr, args) = & expr. kind {
70- // Must be a member access pattern: `token.transfer(...)`
71- if let ExprKind :: Member ( _, member) = & call_expr. kind {
72- return ( args. len ( ) == 2 && member. as_str ( ) == "transfer" )
73- || ( args. len ( ) == 3 && member. as_str ( ) == "transferFrom" ) ;
54+ /// Validates the method name, the params (count + types), and the returns (count + types).
55+ fn is_erc20_transfer_call ( hir : & hir:: Hir < ' _ > , expr : & hir:: Expr < ' _ > ) -> bool {
56+ let is_type = |var_id : hir:: VariableId , type_str : & str | {
57+ matches ! (
58+ & hir. variable( var_id) . ty. kind,
59+ hir:: TypeKind :: Elementary ( ty) if ty. to_abi_str( ) == type_str
60+ )
61+ } ;
62+
63+ // Ensure the expression is a call to a contract member function.
64+ let hir:: ExprKind :: Call (
65+ hir:: Expr { kind : hir:: ExprKind :: Member ( contract_expr, func_ident) , .. } ,
66+ hir:: CallArgs { kind : hir:: CallArgsKind :: Unnamed ( args) , .. } ,
67+ ..,
68+ ) = & expr. kind
69+ else {
70+ return false ;
71+ } ;
72+
73+ // Determine the expected ERC20 signature from the call
74+ let ( expected_params, expected_returns) : ( & [ & str ] , & [ & str ] ) = match func_ident. as_str ( ) {
75+ "transferFrom" if args. len ( ) == 3 => ( & [ "address" , "address" , "uint256" ] , & [ "bool" ] ) ,
76+ "transfer" if args. len ( ) == 2 => ( & [ "address" , "uint256" ] , & [ "bool" ] ) ,
77+ _ => return false ,
78+ } ;
79+
80+ let Some ( cid) = ( match & contract_expr. kind {
81+ // Call to pre-instantiated contract variable
82+ hir:: ExprKind :: Ident ( [ hir:: Res :: Item ( hir:: ItemId :: Variable ( id) ) , ..] ) => {
83+ if let hir:: TypeKind :: Custom ( hir:: ItemId :: Contract ( cid) ) = hir. variable ( * id) . ty . kind {
84+ Some ( cid)
85+ } else {
86+ None
87+ }
7488 }
75- }
76- false
89+ // Call to address wrapped by the contract interface
90+ hir:: ExprKind :: Call (
91+ hir:: Expr {
92+ kind : hir:: ExprKind :: Ident ( [ hir:: Res :: Item ( hir:: ItemId :: Contract ( cid) ) ] ) ,
93+ ..
94+ } ,
95+ ..,
96+ ) => Some ( * cid) ,
97+ _ => None ,
98+ } ) else {
99+ return false ;
100+ } ;
101+
102+ // Try to find a function in the contract that matches the expected signature.
103+ hir. contract_item_ids ( cid) . any ( |item| {
104+ let Some ( fid) = item. as_function ( ) else { return false } ;
105+ let func = hir. function ( fid) ;
106+ func. name . is_some_and ( |name| name. as_str ( ) == func_ident. as_str ( ) )
107+ && func. kind . is_function ( )
108+ && func. mutates_state ( )
109+ && func. parameters . len ( ) == expected_params. len ( )
110+ && func. returns . len ( ) == expected_returns. len ( )
111+ && func. parameters . iter ( ) . zip ( expected_params) . all ( |( id, & ty) | is_type ( * id, ty) )
112+ && func. returns . iter ( ) . zip ( expected_returns) . all ( |( id, & ty) | is_type ( * id, ty) )
113+ } )
77114}
78115
79116// -- UNCKECKED LOW-LEVEL CALLS -------------------------------------------------------------------
0 commit comments