1- use alloy_primitives:: { Address , hex, keccak256 } ;
1+ use alloy_primitives:: { Address , hex} ;
22use foundry_cli:: utils:: parse_fee_token_address;
3+ use foundry_common:: abi:: get_func;
4+ use std:: borrow:: Cow ;
35use tempo_contracts:: precompiles:: {
46 IAccountKeychain :: { CallScope , SelectorRule } ,
57 PATH_USD_ADDRESS ,
@@ -25,6 +27,10 @@ impl SelectorArg {
2527/// (`transfer(address,uint256)`), or a well-known TIP-20 shorthand.
2628pub ( crate ) fn parse_selector_bytes ( s : & str ) -> Result < [ u8 ; 4 ] , String > {
2729 let s = s. trim ( ) ;
30+ if s. is_empty ( ) {
31+ return Err ( "selector cannot be empty" . to_string ( ) ) ;
32+ }
33+
2834 if s. starts_with ( "0x" ) || s. starts_with ( "0X" ) {
2935 let hex_str = & s[ 2 ..] ;
3036 if hex_str. len ( ) != 8 {
@@ -35,24 +41,23 @@ pub(crate) fn parse_selector_bytes(s: &str) -> Result<[u8; 4], String> {
3541 arr. copy_from_slice ( & bytes) ;
3642 Ok ( arr)
3743 } else {
38- let sig = if s. contains ( '(' ) {
39- s . to_string ( )
44+ let sig = if s. contains ( '(' ) || s . contains ( ')' ) {
45+ Cow :: Borrowed ( s )
4046 } else {
4147 match s {
42- "transfer" => "transfer(address,uint256)" . to_string ( ) ,
43- "approve" => "approve(address,uint256)" . to_string ( ) ,
44- "transferFrom" => "transferFrom(address,address,uint256)" . to_string ( ) ,
45- "transferWithMemo" => "transferWithMemo(address,uint256,bytes32)" . to_string ( ) ,
48+ "transfer" => Cow :: Borrowed ( "transfer(address,uint256)" ) ,
49+ "approve" => Cow :: Borrowed ( "approve(address,uint256)" ) ,
50+ "transferFrom" => Cow :: Borrowed ( "transferFrom(address,address,uint256)" ) ,
51+ "transferWithMemo" => Cow :: Borrowed ( "transferWithMemo(address,uint256,bytes32)" ) ,
4652 "transferFromWithMemo" => {
47- "transferFromWithMemo(address,address,uint256,bytes32)" . to_string ( )
53+ Cow :: Borrowed ( "transferFromWithMemo(address,address,uint256,bytes32)" )
4854 }
49- _ => format ! ( "{s}()" ) ,
55+ _ => Cow :: Owned ( format ! ( "{s}()" ) ) ,
5056 }
5157 } ;
52- let hash = keccak256 ( sig. as_bytes ( ) ) ;
53- let mut arr = [ 0u8 ; 4 ] ;
54- arr. copy_from_slice ( & hash[ ..4 ] ) ;
55- Ok ( arr)
58+ get_func ( & sig)
59+ . map ( |func| func. selector ( ) . into ( ) )
60+ . map_err ( |e| format ! ( "invalid function signature '{sig}': {e}" ) )
5661 }
5762}
5863
@@ -73,18 +78,32 @@ pub(crate) fn parse_scope(s: &str) -> Result<CallScope, String> {
7378
7479 let selector_rules = match selectors_str {
7580 None => vec ! [ ] ,
81+ Some ( sel_str) if sel_str. trim ( ) . is_empty ( ) => {
82+ return Err (
83+ "selector list cannot be empty; omit ':' to allow all selectors" . to_string ( )
84+ ) ;
85+ }
7686 Some ( sel_str) => parse_selector_rules ( sel_str) ?,
7787 } ;
7888
7989 Ok ( CallScope { target, selectorRules : selector_rules } )
8090}
8191
8292fn parse_selector_rules ( s : & str ) -> Result < Vec < SelectorRule > , String > {
83- let mut rules = Vec :: new ( ) ;
93+ let mut rules: Vec < SelectorRule > = Vec :: new ( ) ;
8494
85- for part in s . split ( ',' ) {
95+ for part in split_selector_rule_parts ( s ) ? {
8696 let part = part. trim ( ) ;
8797 if part. is_empty ( ) {
98+ return Err ( format ! ( "empty selector in scope '{s}'" ) ) ;
99+ }
100+
101+ if let Some ( rule) = rules. last_mut ( )
102+ && !rule. recipients . is_empty ( )
103+ && !part. contains ( '@' )
104+ && let Ok ( recipient) = part. parse :: < Address > ( )
105+ {
106+ rule. recipients . push ( recipient) ;
88107 continue ;
89108 }
90109
@@ -93,20 +112,25 @@ fn parse_selector_rules(s: &str) -> Result<Vec<SelectorRule>, String> {
93112 None => ( part, None ) ,
94113 } ;
95114
115+ let selector_str = selector_str. trim ( ) ;
116+ if selector_str. is_empty ( ) {
117+ return Err ( format ! ( "missing selector in scope '{part}'" ) ) ;
118+ }
119+
96120 let selector = parse_selector_bytes ( selector_str) ?;
97121
98122 let recipients = match recipients_str {
99123 None => vec ! [ ] ,
100- Some ( r) => r
101- . split ( ',' )
102- . filter ( |s| !s . trim ( ) . is_empty ( ) )
103- . map ( |addr_str| {
104- let addr_str = addr_str . trim ( ) ;
105- addr_str
106- . parse :: < Address > ( )
107- . map_err ( |e| format ! ( "invalid recipient address '{addr_str }': {e}" ) )
108- } )
109- . collect :: < Result < Vec < _ > , _ > > ( ) ? ,
124+ Some ( r) => {
125+ let r = r . trim ( ) ;
126+ if r . is_empty ( ) {
127+ return Err ( format ! ( "missing recipient after '@' in selector '{part}'" ) ) ;
128+ }
129+ vec ! [
130+ r . parse:: <Address >( )
131+ . map_err( |e| format!( "invalid recipient address '{r }': {e}" ) ) ? ,
132+ ]
133+ }
110134 } ;
111135
112136 rules. push ( SelectorRule { selector : selector. into ( ) , recipients } ) ;
@@ -115,6 +139,35 @@ fn parse_selector_rules(s: &str) -> Result<Vec<SelectorRule>, String> {
115139 Ok ( rules)
116140}
117141
142+ fn split_selector_rule_parts ( s : & str ) -> Result < Vec < & str > , String > {
143+ let mut parts = Vec :: new ( ) ;
144+ let mut depth = 0usize ;
145+ let mut start = 0usize ;
146+
147+ for ( idx, ch) in s. char_indices ( ) {
148+ match ch {
149+ '(' => depth += 1 ,
150+ ')' => {
151+ depth = depth
152+ . checked_sub ( 1 )
153+ . ok_or_else ( || format ! ( "unbalanced ')' in selector list '{s}'" ) ) ?;
154+ }
155+ ',' if depth == 0 => {
156+ parts. push ( & s[ start..idx] ) ;
157+ start = idx + ch. len_utf8 ( ) ;
158+ }
159+ _ => { }
160+ }
161+ }
162+
163+ if depth != 0 {
164+ return Err ( format ! ( "unbalanced '(' in selector list '{s}'" ) ) ;
165+ }
166+
167+ parts. push ( & s[ start..] ) ;
168+ Ok ( parts)
169+ }
170+
118171/// Parse a policy token label or address into an address.
119172pub ( crate ) fn parse_policy_token ( s : & str ) -> Result < Address , String > {
120173 match s. to_ascii_lowercase ( ) . as_str ( ) {
@@ -158,6 +211,7 @@ pub(crate) fn parse_period(s: &str) -> Result<u64, String> {
158211#[ cfg( test) ]
159212mod tests {
160213 use super :: * ;
214+ use alloy_primitives:: keccak256;
161215 use std:: str:: FromStr ;
162216
163217 #[ test]
@@ -192,6 +246,16 @@ mod tests {
192246 fn parse_selector_bytes_full_signature ( ) {
193247 let sel = parse_selector_bytes ( "increment()" ) . unwrap ( ) ;
194248 assert_eq ! ( sel, keccak256( b"increment()" ) [ ..4 ] ) ;
249+
250+ let sel = parse_selector_bytes ( "transfer(address,uint256)" ) . unwrap ( ) ;
251+ assert_eq ! ( sel, keccak256( b"transfer(address,uint256)" ) [ ..4 ] ) ;
252+ }
253+
254+ #[ test]
255+ fn parse_selector_bytes_rejects_invalid_signature ( ) {
256+ assert ! ( parse_selector_bytes( "" ) . is_err( ) ) ;
257+ assert ! ( parse_selector_bytes( "transfer(address,uint256" ) . is_err( ) ) ;
258+ assert ! ( parse_selector_bytes( "transfer)" ) . is_err( ) ) ;
195259 }
196260
197261 #[ test]
@@ -242,6 +306,74 @@ mod tests {
242306 assert_eq ! ( scope. selectorRules[ 0 ] . recipients. len( ) , 1 ) ;
243307 }
244308
309+ #[ test]
310+ fn parse_scope_full_signature_with_comma_argument_list ( ) {
311+ let scope =
312+ parse_scope ( "0x20c0000000000000000000000000000000000001:transfer(address,uint256)" )
313+ . unwrap ( ) ;
314+ assert_eq ! ( scope. selectorRules. len( ) , 1 ) ;
315+ assert_eq ! ( scope. selectorRules[ 0 ] . selector. 0 , keccak256( b"transfer(address,uint256)" ) [ ..4 ] ) ;
316+ }
317+
318+ #[ test]
319+ fn parse_scope_full_signatures_split_outside_parentheses ( ) {
320+ let scope = parse_scope (
321+ "0x20c0000000000000000000000000000000000001:transfer(address,uint256),approve(address,uint256)" ,
322+ )
323+ . unwrap ( ) ;
324+ assert_eq ! ( scope. selectorRules. len( ) , 2 ) ;
325+ assert_eq ! ( scope. selectorRules[ 0 ] . selector. 0 , keccak256( b"transfer(address,uint256)" ) [ ..4 ] ) ;
326+ assert_eq ! ( scope. selectorRules[ 1 ] . selector. 0 , keccak256( b"approve(address,uint256)" ) [ ..4 ] ) ;
327+ }
328+
329+ #[ test]
330+ fn parse_scope_selector_with_multiple_recipients ( ) {
331+ let scope = parse_scope (
332+ "0x20c0000000000000000000000000000000000001:transfer@0x1111111111111111111111111111111111111111,0x2222222222222222222222222222222222222222" ,
333+ )
334+ . unwrap ( ) ;
335+ assert_eq ! ( scope. selectorRules. len( ) , 1 ) ;
336+ assert_eq ! (
337+ scope. selectorRules[ 0 ] . recipients,
338+ vec![
339+ Address :: from_str( "0x1111111111111111111111111111111111111111" ) . unwrap( ) ,
340+ Address :: from_str( "0x2222222222222222222222222222222222222222" ) . unwrap( ) ,
341+ ]
342+ ) ;
343+ }
344+
345+ #[ test]
346+ fn parse_scope_selector_with_multiple_recipients_and_next_selector ( ) {
347+ let scope = parse_scope (
348+ "0x20c0000000000000000000000000000000000001:transfer@0x1111111111111111111111111111111111111111,0x2222222222222222222222222222222222222222,approve" ,
349+ )
350+ . unwrap ( ) ;
351+ assert_eq ! ( scope. selectorRules. len( ) , 2 ) ;
352+ assert_eq ! ( scope. selectorRules[ 0 ] . recipients. len( ) , 2 ) ;
353+ assert ! ( scope. selectorRules[ 1 ] . recipients. is_empty( ) ) ;
354+ }
355+
356+ #[ test]
357+ fn parse_scope_rejects_empty_selectors ( ) {
358+ assert ! ( parse_scope( "0x20c0000000000000000000000000000000000001:" ) . is_err( ) ) ;
359+ assert ! ( parse_scope( "0x20c0000000000000000000000000000000000001:transfer," ) . is_err( ) ) ;
360+ assert ! ( parse_scope( "0x20c0000000000000000000000000000000000001:,transfer" ) . is_err( ) ) ;
361+ assert ! (
362+ parse_scope(
363+ "0x20c0000000000000000000000000000000000001:@0x1111111111111111111111111111111111111111"
364+ )
365+ . is_err( )
366+ ) ;
367+ }
368+
369+ #[ test]
370+ fn parse_scope_rejects_empty_or_invalid_recipient ( ) {
371+ assert ! ( parse_scope( "0x20c0000000000000000000000000000000000001:transfer@" ) . is_err( ) ) ;
372+ assert ! (
373+ parse_scope( "0x20c0000000000000000000000000000000000001:transfer@approve" ) . is_err( )
374+ ) ;
375+ }
376+
245377 #[ test]
246378 fn parse_policy_token_path_usd ( ) {
247379 assert_eq ! ( parse_policy_token( "PathUSD" ) . unwrap( ) , PATH_USD_ADDRESS ) ;
0 commit comments