1use core::fmt;
2
3use alloy_primitives::{Address, Bytes, TxKind, U256};
4use alloy_sol_types::SolCall;
5use tempo_contracts::precompiles::{
6 ACCOUNT_KEYCHAIN_ADDRESS,
7 IAccountKeychain::{
8 KeyRestrictions as AbiKeyRestrictions, LegacyTokenLimit as AbiLegacyTokenLimit,
9 TokenLimit as AbiTokenLimit, removeAllowedCallsCall, revokeKeyCall, setAllowedCallsCall,
10 updateSpendingLimitCall,
11 },
12 ITIP20, authorizeKeyCall, legacyAuthorizeKeyCall,
13};
14use tempo_primitives::{
15 SignatureType,
16 transaction::{Call, CallScope, SelectorRule, TokenLimit},
17};
18
19#[derive(Clone, Debug, Default, PartialEq, Eq)]
21pub struct KeyRestrictions {
22 expiry: Option<u64>,
24 limits: Option<Vec<TokenLimit>>,
26 allowed_calls: Option<Vec<CallScope>>,
28}
29
30impl KeyRestrictions {
31 pub fn with_expiry(mut self, expiry: u64) -> Self {
33 self.expiry = Some(expiry);
34 self
35 }
36
37 pub fn with_limits(mut self, limits: Vec<TokenLimit>) -> Self {
39 self.limits = Some(limits);
40 self
41 }
42
43 pub fn with_allowed_calls(mut self, allowed_calls: Vec<CallScope>) -> Self {
45 self.allowed_calls = Some(allowed_calls);
46 self
47 }
48
49 pub fn with_no_spending(mut self) -> Self {
51 self.limits = Some(Vec::new());
52 self
53 }
54
55 pub fn with_no_calls(mut self) -> Self {
57 self.allowed_calls = Some(Vec::new());
58 self
59 }
60
61 pub fn is_unrestricted(&self) -> bool {
63 self.allowed_calls.is_none()
64 }
65
66 pub fn is_call_allowed(&self, target: &Address, input: &[u8]) -> bool {
76 (|| {
77 let Some(scopes) = &self.allowed_calls else {
78 return Some(true);
79 };
80 let scope = scopes.iter().find(|s| s.target == *target)?;
81
82 if scope.selector_rules.is_empty() {
83 return Some(true);
84 }
85
86 let selector: [u8; 4] = input.get(..4)?.try_into().ok()?;
87 let rule = scope
88 .selector_rules
89 .iter()
90 .find(|r| r.selector == selector)?;
91
92 if rule.recipients.is_empty() {
93 return Some(true);
94 }
95
96 let word: [u8; 32] = input.get(4..36)?.try_into().ok()?;
97 Some(rule.recipients.contains(&Address::from_word(word.into())))
98 })()
99 .unwrap_or(false)
100 }
101
102 pub fn expiry(&self) -> Option<u64> {
104 self.expiry
105 }
106
107 pub fn limits(&self) -> Option<&[TokenLimit]> {
109 self.limits.as_deref()
110 }
111
112 pub fn allowed_calls(&self) -> Option<&[CallScope]> {
114 self.allowed_calls.as_deref()
115 }
116
117 fn has_periodic_limits(&self) -> bool {
118 self.limits
119 .as_ref()
120 .is_some_and(|limits| limits.iter().any(|limit| limit.period != 0))
121 }
122
123 fn has_call_scopes(&self) -> bool {
124 self.allowed_calls.is_some()
125 }
126}
127
128impl From<KeyRestrictions> for AbiKeyRestrictions {
129 fn from(restrictions: KeyRestrictions) -> Self {
130 let KeyRestrictions {
131 expiry,
132 limits,
133 allowed_calls,
134 } = restrictions;
135
136 Self {
137 expiry: expiry.unwrap_or(u64::MAX),
138 enforceLimits: limits.is_some(),
139 limits: limits
140 .unwrap_or_default()
141 .into_iter()
142 .map(|limit| AbiTokenLimit {
143 token: limit.token,
144 amount: limit.limit,
145 period: limit.period,
146 })
147 .collect(),
148 allowAnyCalls: allowed_calls.is_none(),
149 allowedCalls: allowed_calls
150 .unwrap_or_default()
151 .into_iter()
152 .map(Into::into)
153 .collect(),
154 }
155 }
156}
157
158#[derive(Clone, Debug)]
183pub struct CallScopeBuilder {
184 target: Address,
185 selector_rules: Vec<SelectorRule>,
186}
187
188impl CallScopeBuilder {
189 pub fn new(target: Address) -> Self {
191 Self {
192 target,
193 selector_rules: Vec::new(),
194 }
195 }
196
197 pub fn with_selector(mut self, selector: [u8; 4]) -> Self {
199 self.selector_rules.push(SelectorRule {
200 selector,
201 recipients: vec![],
202 });
203 self
204 }
205
206 pub fn transfer(mut self, recipients: Vec<Address>) -> Self {
208 self.selector_rules.push(SelectorRule {
209 selector: ITIP20::transferCall::SELECTOR,
210 recipients,
211 });
212 self
213 }
214
215 pub fn transfer_with_memo(mut self, recipients: Vec<Address>) -> Self {
217 self.selector_rules.push(SelectorRule {
218 selector: ITIP20::transferWithMemoCall::SELECTOR,
219 recipients,
220 });
221 self
222 }
223
224 pub fn approve(mut self, recipients: Vec<Address>) -> Self {
226 self.selector_rules.push(SelectorRule {
227 selector: ITIP20::approveCall::SELECTOR,
228 recipients,
229 });
230 self
231 }
232
233 pub fn build(self) -> CallScope {
235 CallScope {
236 target: self.target,
237 selector_rules: self.selector_rules,
238 }
239 }
240}
241
242#[derive(Clone, Copy, Debug, PartialEq, Eq)]
244pub enum KeychainBuildError {
245 LegacyPeriodicLimits,
247 LegacyCallScopes,
249}
250
251impl std::error::Error for KeychainBuildError {}
252impl fmt::Display for KeychainBuildError {
253 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
254 let msg = match self {
255 Self::LegacyPeriodicLimits => {
256 "legacy authorizeKey does not support periodic token limits"
257 }
258 Self::LegacyCallScopes => {
259 "legacy authorizeKey does not support call-scope restrictions"
260 }
261 };
262 write!(f, "{msg}")
263 }
264}
265
266pub fn authorize_key_legacy(
268 key_id: Address,
269 signature_type: SignatureType,
270 restrictions: KeyRestrictions,
271) -> Result<Call, KeychainBuildError> {
272 if restrictions.has_call_scopes() {
273 return Err(KeychainBuildError::LegacyCallScopes);
274 }
275 if restrictions.has_periodic_limits() {
276 return Err(KeychainBuildError::LegacyPeriodicLimits);
277 }
278
279 let KeyRestrictions {
280 expiry,
281 limits,
282 allowed_calls: _,
283 } = restrictions;
284 let enforce_limits = limits.is_some();
285 let limits = limits
286 .unwrap_or_default()
287 .into_iter()
288 .map(|limit| AbiLegacyTokenLimit {
289 token: limit.token,
290 amount: limit.limit,
291 })
292 .collect();
293
294 Ok(account_keychain_call(legacyAuthorizeKeyCall {
295 keyId: key_id,
296 signatureType: signature_type.into(),
297 expiry: expiry.unwrap_or(u64::MAX),
298 enforceLimits: enforce_limits,
299 limits,
300 }))
301}
302
303pub fn authorize_key(
305 key_id: Address,
306 signature_type: SignatureType,
307 restrictions: KeyRestrictions,
308) -> Call {
309 account_keychain_call(authorizeKeyCall {
310 keyId: key_id,
311 signatureType: signature_type.into(),
312 config: restrictions.into(),
313 })
314}
315
316pub fn revoke_key(key_id: Address) -> Call {
318 account_keychain_call(revokeKeyCall { keyId: key_id })
319}
320
321pub fn update_spending_limit(key_id: Address, token: Address, new_limit: U256) -> Call {
323 account_keychain_call(updateSpendingLimitCall {
324 keyId: key_id,
325 token,
326 newLimit: new_limit,
327 })
328}
329
330pub fn set_allowed_calls(key_id: Address, scopes: Vec<CallScope>) -> Call {
349 account_keychain_call(setAllowedCallsCall {
350 keyId: key_id,
351 scopes: scopes.into_iter().map(Into::into).collect(),
352 })
353}
354
355pub fn remove_allowed_calls(key_id: Address, target: Address) -> Call {
370 account_keychain_call(removeAllowedCallsCall {
371 keyId: key_id,
372 target,
373 })
374}
375
376fn account_keychain_call(call: impl SolCall) -> Call {
377 Call {
378 to: TxKind::Call(ACCOUNT_KEYCHAIN_ADDRESS),
379 value: U256::ZERO,
380 input: Bytes::from(call.abi_encode()),
381 }
382}
383
384#[cfg(test)]
385mod tests {
386 use super::*;
387 use alloy_primitives::{address, uint};
388 use tempo_contracts::precompiles::IAccountKeychain::{
389 CallScope as AbiCallScope, SelectorRule as AbiSelectorRule,
390 SignatureType as AbiSignatureType, removeAllowedCallsCall, revokeKeyCall,
391 setAllowedCallsCall, updateSpendingLimitCall,
392 };
393
394 #[test]
395 fn test_authorize_key_t3_defaults_to_unrestricted_never_expiring() {
396 let call = authorize_key(
397 address!("0x1111111111111111111111111111111111111111"),
398 SignatureType::Secp256k1,
399 KeyRestrictions::default(),
400 );
401
402 let decoded = authorizeKeyCall::abi_decode(&call.input).expect("decode authorizeKey");
403 assert_eq!(
404 decoded.keyId,
405 address!("0x1111111111111111111111111111111111111111")
406 );
407 assert_eq!(decoded.signatureType, AbiSignatureType::Secp256k1);
408 assert_eq!(decoded.config.expiry, u64::MAX);
409 assert!(!decoded.config.enforceLimits);
410 assert!(decoded.config.limits.is_empty());
411 assert!(decoded.config.allowAnyCalls);
412 assert!(decoded.config.allowedCalls.is_empty());
413 }
414
415 #[test]
416 fn test_authorize_key_t3_preserves_call_scopes() {
417 let restrictions = KeyRestrictions::default()
418 .with_expiry(123)
419 .with_limits(vec![TokenLimit {
420 token: address!("0x20c0000000000000000000000000000000000001"),
421 limit: uint!(42_U256),
422 period: 60,
423 }])
424 .with_allowed_calls(vec![CallScope {
425 target: address!("0x20c0000000000000000000000000000000000002"),
426 selector_rules: vec![SelectorRule {
427 selector: [0xaa, 0xbb, 0xcc, 0xdd],
428 recipients: vec![address!("0x3333333333333333333333333333333333333333")],
429 }],
430 }]);
431
432 let call = authorize_key(
433 address!("0x1111111111111111111111111111111111111111"),
434 SignatureType::P256,
435 restrictions,
436 );
437
438 let decoded = authorizeKeyCall::abi_decode(&call.input).expect("decode authorizeKey");
439 assert_eq!(decoded.signatureType, AbiSignatureType::P256);
440 assert_eq!(decoded.config.expiry, 123);
441 assert!(decoded.config.enforceLimits);
442 assert_eq!(decoded.config.limits.len(), 1);
443 assert!(!decoded.config.allowAnyCalls);
444 assert_eq!(decoded.config.allowedCalls.len(), 1);
445 assert_eq!(decoded.config.allowedCalls[0].selectorRules.len(), 1);
446 assert_eq!(
447 decoded.config.allowedCalls[0].selectorRules[0].selector,
448 [0xaa_u8, 0xbb, 0xcc, 0xdd]
449 );
450 }
451
452 #[test]
453 fn test_authorize_key_legacy_rejects_t3_only_restrictions() {
454 let scoped = authorize_key_legacy(
455 address!("0x1111111111111111111111111111111111111111"),
456 SignatureType::Secp256k1,
457 KeyRestrictions::default().with_no_calls(),
458 )
459 .expect_err("legacy ABI should reject call scopes");
460 assert_eq!(scoped, KeychainBuildError::LegacyCallScopes);
461
462 let periodic = authorize_key_legacy(
463 address!("0x1111111111111111111111111111111111111111"),
464 SignatureType::Secp256k1,
465 KeyRestrictions::default().with_limits(vec![TokenLimit {
466 token: address!("0x20c0000000000000000000000000000000000001"),
467 limit: U256::from(1),
468 period: 1,
469 }]),
470 )
471 .expect_err("legacy ABI should reject periodic limits");
472 assert_eq!(periodic, KeychainBuildError::LegacyPeriodicLimits);
473 }
474
475 #[test]
476 fn test_authorize_key_legacy_flattens_limits() {
477 let call = authorize_key_legacy(
478 address!("0x1111111111111111111111111111111111111111"),
479 SignatureType::WebAuthn,
480 KeyRestrictions::default()
481 .with_expiry(999)
482 .with_limits(vec![TokenLimit {
483 token: address!("0x20c0000000000000000000000000000000000001"),
484 limit: U256::from(7),
485 period: 0,
486 }]),
487 )
488 .expect("legacy restrictions are compatible");
489
490 let decoded =
491 legacyAuthorizeKeyCall::abi_decode(&call.input).expect("decode legacy authorizeKey");
492 assert_eq!(decoded.signatureType, AbiSignatureType::WebAuthn);
493 assert_eq!(decoded.expiry, 999);
494 assert!(decoded.enforceLimits);
495 assert_eq!(decoded.limits.len(), 1);
496 assert_eq!(decoded.limits[0].amount, U256::from(7));
497 }
498
499 #[test]
500 fn test_call_scope_builder_tip20_selectors() {
501 let token = address!("0x20c0000000000000000000000000000000000001");
502 let recipient = address!("0x3333333333333333333333333333333333333333");
503
504 let scope = CallScopeBuilder::new(token)
505 .transfer(vec![recipient])
506 .approve(vec![])
507 .build();
508
509 assert_eq!(scope.target, token);
510 assert_eq!(scope.selector_rules.len(), 2);
511 assert_eq!(
512 scope.selector_rules[0].selector,
513 ITIP20::transferCall::SELECTOR
514 );
515 assert_eq!(scope.selector_rules[0].recipients, vec![recipient]);
516
517 assert_eq!(
518 scope.selector_rules[1].selector,
519 ITIP20::approveCall::SELECTOR
520 );
521 assert!(scope.selector_rules[1].recipients.is_empty());
522 }
523
524 #[test]
525 fn test_roundtrip_abi_call_scope_conversion() {
526 let scopes = vec![AbiCallScope {
527 target: address!("0x20c0000000000000000000000000000000000002"),
528 selectorRules: vec![AbiSelectorRule {
529 selector: [0x12, 0x34, 0x56, 0x78].into(),
530 recipients: vec![address!("0x3333333333333333333333333333333333333333")],
531 }],
532 }];
533
534 let primitive: Vec<CallScope> = scopes.clone().into_iter().map(Into::into).collect();
535 let roundtrip: Vec<AbiCallScope> = primitive.into_iter().map(Into::into).collect();
536 assert_eq!(roundtrip, scopes);
537 }
538
539 #[test]
540 fn test_revoke_key_encodes_correctly() {
541 let key_id = address!("0x1111111111111111111111111111111111111111");
542 let call = revoke_key(key_id);
543
544 assert_eq!(call.to, TxKind::Call(ACCOUNT_KEYCHAIN_ADDRESS));
545 assert_eq!(call.value, U256::ZERO);
546
547 let decoded = revokeKeyCall::abi_decode(&call.input).expect("decode revokeKey");
548 assert_eq!(decoded.keyId, key_id);
549 }
550
551 #[test]
552 fn test_update_spending_limit_encodes_correctly() {
553 let key_id = address!("0x1111111111111111111111111111111111111111");
554 let token = address!("0x2222222222222222222222222222222222222222");
555 let limit = uint!(1000_U256);
556 let call = update_spending_limit(key_id, token, limit);
557
558 assert_eq!(call.to, TxKind::Call(ACCOUNT_KEYCHAIN_ADDRESS));
559 assert_eq!(call.value, U256::ZERO);
560
561 let decoded =
562 updateSpendingLimitCall::abi_decode(&call.input).expect("decode updateSpendingLimit");
563 assert_eq!(decoded.keyId, key_id);
564 assert_eq!(decoded.token, token);
565 assert_eq!(decoded.newLimit, limit);
566 }
567
568 #[test]
569 fn test_set_allowed_calls_encodes_correctly() {
570 let key_id = address!("0x1111111111111111111111111111111111111111");
571 let scopes = vec![CallScope {
572 target: address!("0x2222222222222222222222222222222222222222"),
573 selector_rules: vec![SelectorRule {
574 selector: [0xaa, 0xbb, 0xcc, 0xdd],
575 recipients: vec![address!("0x3333333333333333333333333333333333333333")],
576 }],
577 }];
578 let call = set_allowed_calls(key_id, scopes);
579
580 assert_eq!(call.to, TxKind::Call(ACCOUNT_KEYCHAIN_ADDRESS));
581 assert_eq!(call.value, U256::ZERO);
582
583 let decoded = setAllowedCallsCall::abi_decode(&call.input).expect("decode setAllowedCalls");
584 assert_eq!(decoded.keyId, key_id);
585 assert_eq!(decoded.scopes.len(), 1);
586 assert_eq!(decoded.scopes[0].selectorRules.len(), 1);
587 }
588
589 #[test]
590 fn test_is_call_allowed_unrestricted() {
591 let r = KeyRestrictions::default();
592 let target = address!("0x2222222222222222222222222222222222222222");
593 assert!(r.is_call_allowed(&target, &[]));
594 assert!(r.is_call_allowed(&target, &[0xaa, 0xbb, 0xcc, 0xdd]));
595 }
596
597 #[test]
598 fn test_is_call_allowed_empty_scopes_denies_all() {
599 let r = KeyRestrictions::default().with_no_calls();
600 let target = address!("0x2222222222222222222222222222222222222222");
601 assert!(!r.is_call_allowed(&target, &[0xaa, 0xbb, 0xcc, 0xdd]));
602 }
603
604 #[test]
605 fn test_is_call_allowed_target_not_in_scope() {
606 let token = address!("0x20c0000000000000000000000000000000000001");
607 let other = address!("0x3333333333333333333333333333333333333333");
608 let r = KeyRestrictions::default()
609 .with_allowed_calls(vec![CallScopeBuilder::new(token).build()]);
610 assert!(!r.is_call_allowed(&other, &[0xaa, 0xbb, 0xcc, 0xdd]));
611 }
612
613 #[test]
614 fn test_is_call_allowed_no_selector_rules_allows_any_call() {
615 let token = address!("0x20c0000000000000000000000000000000000001");
616 let r = KeyRestrictions::default()
617 .with_allowed_calls(vec![CallScopeBuilder::new(token).build()]);
618 assert!(r.is_call_allowed(&token, &[0xaa, 0xbb, 0xcc, 0xdd]));
619 assert!(r.is_call_allowed(&token, &[]));
620 }
621
622 #[test]
623 fn test_is_call_allowed_selector_match() {
624 let token = address!("0x20c0000000000000000000000000000000000001");
625 let r = KeyRestrictions::default().with_allowed_calls(vec![
626 CallScopeBuilder::new(token)
627 .with_selector([0xaa, 0xbb, 0xcc, 0xdd])
628 .build(),
629 ]);
630
631 assert!(r.is_call_allowed(&token, &[0xaa, 0xbb, 0xcc, 0xdd]));
632 assert!(!r.is_call_allowed(&token, &[0x11, 0x22, 0x33, 0x44]));
633 assert!(!r.is_call_allowed(&token, &[0xaa, 0xbb]));
634 }
635
636 #[test]
637 fn test_is_call_allowed_tip20_transfer_with_recipients() {
638 let token = address!("0x20c0000000000000000000000000000000000001");
639 let allowed = address!("0x4444444444444444444444444444444444444444");
640 let denied = address!("0x5555555555555555555555555555555555555555");
641
642 let r = KeyRestrictions::default().with_allowed_calls(vec![
643 CallScopeBuilder::new(token).transfer(vec![allowed]).build(),
644 ]);
645
646 let mut input = Vec::new();
648 input.extend_from_slice(&ITIP20::transferCall::SELECTOR);
649 input.extend_from_slice(&[0u8; 12]);
651 input.extend_from_slice(allowed.as_slice());
652 input.extend_from_slice(&[0u8; 32]);
654
655 assert!(r.is_call_allowed(&token, &input));
656
657 let mut bad_input = Vec::new();
659 bad_input.extend_from_slice(&ITIP20::transferCall::SELECTOR);
660 bad_input.extend_from_slice(&[0u8; 12]);
661 bad_input.extend_from_slice(denied.as_slice());
662 bad_input.extend_from_slice(&[0u8; 32]);
663
664 assert!(!r.is_call_allowed(&token, &bad_input));
665 }
666
667 #[test]
668 fn test_is_call_allowed_recipient_word_too_short() {
669 let token = address!("0x20c0000000000000000000000000000000000001");
670 let allowed = address!("0x4444444444444444444444444444444444444444");
671 let r = KeyRestrictions::default().with_allowed_calls(vec![
672 CallScopeBuilder::new(token).transfer(vec![allowed]).build(),
673 ]);
674
675 let input = ITIP20::transferCall::SELECTOR.to_vec();
677 assert!(!r.is_call_allowed(&token, &input));
678 }
679
680 #[test]
681 fn test_is_call_allowed_no_recipients_allows_any() {
682 let token = address!("0x20c0000000000000000000000000000000000001");
683 let anyone = address!("0x9999999999999999999999999999999999999999");
684
685 let r = KeyRestrictions::default()
686 .with_allowed_calls(vec![CallScopeBuilder::new(token).transfer(vec![]).build()]);
687
688 let mut input = Vec::new();
689 input.extend_from_slice(&ITIP20::transferCall::SELECTOR);
690 input.extend_from_slice(&[0u8; 12]);
691 input.extend_from_slice(anyone.as_slice());
692 input.extend_from_slice(&[0u8; 32]);
693
694 assert!(r.is_call_allowed(&token, &input));
695 }
696
697 #[test]
698 fn test_remove_allowed_calls_encodes_correctly() {
699 let key_id = address!("0x1111111111111111111111111111111111111111");
700 let target = address!("0x2222222222222222222222222222222222222222");
701 let call = remove_allowed_calls(key_id, target);
702
703 assert_eq!(call.to, TxKind::Call(ACCOUNT_KEYCHAIN_ADDRESS));
704 assert_eq!(call.value, U256::ZERO);
705
706 let decoded =
707 removeAllowedCallsCall::abi_decode(&call.input).expect("decode removeAllowedCalls");
708 assert_eq!(decoded.keyId, key_id);
709 assert_eq!(decoded.target, target);
710 }
711}