Skip to content

Commit b3c44bb

Browse files
committed
fix(cast): parse tempo scope signatures
1 parent e27ccc5 commit b3c44bb

1 file changed

Lines changed: 157 additions & 25 deletions

File tree

crates/cast/src/cmd/tempo_policy_args.rs

Lines changed: 157 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
use alloy_primitives::{Address, hex, keccak256};
1+
use alloy_primitives::{Address, hex};
22
use foundry_cli::utils::parse_fee_token_address;
3+
use foundry_common::abi::get_func;
4+
use std::borrow::Cow;
35
use 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.
2628
pub(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

8292
fn 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.
119172
pub(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)]
159212
mod 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

Comments
 (0)