1pub mod dispatch;
10
11use std::collections::HashSet;
12
13use alloy::sol_types::SolCall;
14use tempo_contracts::precompiles::{AccountKeychainError, AccountKeychainEvent, ITIP20};
15pub use tempo_contracts::precompiles::{
16 IAccountKeychain,
17 IAccountKeychain::{
18 CallScope, KeyInfo, KeyRestrictions, SelectorRule, SignatureType, TokenLimit,
19 getAllowedCallsCall, getKeyCall, getRemainingLimitCall, getRemainingLimitWithPeriodCall,
20 getTransactionKeyCall, removeAllowedCallsCall, revokeKeyCall, setAllowedCallsCall,
21 updateSpendingLimitCall,
22 },
23 authorizeKeyCall, getAllowedCallsReturn, getRemainingLimitReturn,
24};
25use tempo_primitives::TempoAddressExt;
26
27use crate::{
28 ACCOUNT_KEYCHAIN_ADDRESS,
29 error::Result,
30 storage::{Handler, Mapping, Set},
31 tip20_factory::TIP20Factory,
32};
33use alloy::primitives::{Address, B256, FixedBytes, TxKind, U256, keccak256};
34use tempo_precompiles_macros::{Storable, contract};
35
36const TIP20_TRANSFER_SELECTOR: [u8; 4] = ITIP20::transferCall::SELECTOR;
38const TIP20_APPROVE_SELECTOR: [u8; 4] = ITIP20::approveCall::SELECTOR;
39const TIP20_TRANSFER_WITH_MEMO_SELECTOR: [u8; 4] = ITIP20::transferWithMemoCall::SELECTOR;
40
41#[inline]
42pub fn is_constrained_tip20_selector(selector: [u8; 4]) -> bool {
43 matches!(
44 selector,
45 TIP20_TRANSFER_SELECTOR | TIP20_APPROVE_SELECTOR | TIP20_TRANSFER_WITH_MEMO_SELECTOR
46 )
47}
48
49#[derive(Debug, Clone, Default, PartialEq, Eq, Storable)]
57pub struct AuthorizedKey {
58 pub signature_type: u8,
60 pub expiry: u64,
62 pub enforce_limits: bool,
64 pub is_revoked: bool,
67}
68
69#[contract(addr = ACCOUNT_KEYCHAIN_ADDRESS)]
74pub struct AccountKeychain {
75 keys: Mapping<Address, Mapping<Address, AuthorizedKey>>,
77 spending_limits: Mapping<B256, Mapping<Address, SpendingLimitState>>,
80
81 key_scopes: Mapping<B256, KeyScope>,
83
84 transaction_key: Address,
88 tx_origin: Address,
91}
92
93#[derive(Debug, Clone, Storable, Default)]
100pub struct KeyScope {
101 pub is_scoped: bool,
102 pub targets: Set<Address>,
103 pub target_scopes: Mapping<Address, TargetScope>,
104}
105
106#[derive(Debug, Clone, Storable, Default)]
113pub struct TargetScope {
114 pub selectors: Set<FixedBytes<4>>,
115 pub selector_scopes: Mapping<FixedBytes<4>, SelectorScope>,
116}
117
118#[derive(Debug, Clone, Storable, Default)]
126pub struct SelectorScope {
127 pub recipients: Set<Address>,
128}
129
130#[derive(Debug, Clone, Default, PartialEq, Eq, Storable)]
137pub struct SpendingLimitState {
138 pub remaining: U256,
140 pub max: u128,
142 pub period: u64,
144 pub period_end: u64,
146}
147
148impl SpendingLimitState {
149 fn compute_next_period_end(&self, current_timestamp: u64) -> u64 {
152 debug_assert!(
153 self.period != 0,
154 "period rollovers require a non-zero period"
155 );
156 let elapsed = current_timestamp.saturating_sub(self.period_end);
157 let periods_elapsed = (elapsed / self.period).saturating_add(1);
158 let advance = self.period.saturating_mul(periods_elapsed);
159 self.period_end.saturating_add(advance)
160 }
161}
162
163impl AccountKeychain {
164 pub fn spending_limit_key(account: Address, key_id: Address) -> B256 {
169 let mut data = [0u8; 40];
170 data[..20].copy_from_slice(account.as_slice());
171 data[20..].copy_from_slice(key_id.as_slice());
172 keccak256(data)
173 }
174
175 #[inline]
176 fn t3_spending_limit_cap(limit: U256) -> Result<u128> {
177 if limit > U256::from(u128::MAX) {
178 return Err(AccountKeychainError::invalid_spending_limit().into());
179 }
180
181 Ok(limit.to::<u128>())
182 }
183
184 pub fn initialize(&mut self) -> Result<()> {
186 self.__initialize()
187 }
188
189 pub fn authorize_key(&mut self, msg_sender: Address, call: authorizeKeyCall) -> Result<()> {
201 let config = &call.config;
202
203 self.ensure_admin_caller(msg_sender)?;
204 let is_t3 = self.storage.spec().is_t3();
205
206 if call.keyId == Address::ZERO {
208 return Err(AccountKeychainError::zero_public_key().into());
209 }
210
211 if self.storage.spec().is_t0() {
213 let current_timestamp = self.storage.timestamp().saturating_to::<u64>();
214 if config.expiry <= current_timestamp {
215 return Err(AccountKeychainError::expiry_in_past().into());
216 }
217 }
218
219 let existing_key = self.keys[msg_sender][call.keyId].read()?;
221 if existing_key.expiry > 0 {
222 return Err(AccountKeychainError::key_already_exists().into());
223 }
224
225 if existing_key.is_revoked {
227 return Err(AccountKeychainError::key_already_revoked().into());
228 }
229
230 let signature_type = match call.signatureType {
232 SignatureType::Secp256k1 => 0,
233 SignatureType::P256 => 1,
234 SignatureType::WebAuthn => 2,
235 _ => return Err(AccountKeychainError::invalid_signature_type().into()),
236 };
237
238 let allowed_call_configs = if is_t3 {
240 if config.enforceLimits {
241 let mut seen_tokens = HashSet::with_capacity(config.limits.len());
242 for limit in &config.limits {
243 if !seen_tokens.insert(limit.token) {
244 return Err(AccountKeychainError::invalid_spending_limit().into());
245 }
246 }
247 }
248
249 if config.allowAnyCalls {
250 None
251 } else {
252 Some(config.allowedCalls.as_slice())
253 }
254 } else {
255 if config.limits.iter().any(|limit| limit.period != 0) {
256 return Err(AccountKeychainError::invalid_spending_limit().into());
257 }
258
259 if !config.allowAnyCalls || !config.allowedCalls.is_empty() {
260 return Err(AccountKeychainError::invalid_call_scope().into());
261 }
262
263 None
264 };
265
266 let new_key = AuthorizedKey {
268 signature_type,
269 expiry: config.expiry,
270 enforce_limits: config.enforceLimits,
271 is_revoked: false,
272 };
273
274 self.keys[msg_sender][call.keyId].write(new_key)?;
275
276 let limits = config
277 .enforceLimits
278 .then_some(config.limits.iter())
279 .into_iter()
280 .flatten();
281
282 self.apply_key_authorization_restrictions(
283 msg_sender,
284 call.keyId,
285 limits,
286 allowed_call_configs,
287 )?;
288
289 self.emit_event(AccountKeychainEvent::KeyAuthorized(
291 IAccountKeychain::KeyAuthorized {
292 account: msg_sender,
293 publicKey: call.keyId,
294 signatureType: signature_type,
295 expiry: config.expiry,
296 },
297 ))
298 }
299
300 pub fn revoke_key(&mut self, msg_sender: Address, call: revokeKeyCall) -> Result<()> {
308 self.ensure_admin_caller(msg_sender)?;
309
310 let key = self.keys[msg_sender][call.keyId].read()?;
311
312 if key.expiry == 0 {
314 return Err(AccountKeychainError::key_not_found().into());
315 }
316
317 let revoked_key = AuthorizedKey {
321 is_revoked: true,
322 ..Default::default()
323 };
324 self.keys[msg_sender][call.keyId].write(revoked_key)?;
325
326 self.emit_event(AccountKeychainEvent::KeyRevoked(
330 IAccountKeychain::KeyRevoked {
331 account: msg_sender,
332 publicKey: call.keyId,
333 },
334 ))
335 }
336
337 pub fn update_spending_limit(
347 &mut self,
348 msg_sender: Address,
349 call: updateSpendingLimitCall,
350 ) -> Result<()> {
351 self.ensure_admin_caller(msg_sender)?;
352
353 let current_timestamp = self.storage.timestamp().saturating_to::<u64>();
354 let mut key = self.load_active_key(msg_sender, call.keyId, current_timestamp)?;
355
356 if !key.enforce_limits {
358 key.enforce_limits = true;
359 self.keys[msg_sender][call.keyId].write(key)?;
360 }
361
362 let limit_key = Self::spending_limit_key(msg_sender, call.keyId);
364 if self.storage.spec().is_t3() {
365 let mut limit_state = self.spending_limits[limit_key][call.token].read()?;
368 limit_state.remaining = call.newLimit;
369 limit_state.max = Self::t3_spending_limit_cap(call.newLimit)?;
370 self.spending_limits[limit_key][call.token].write(limit_state)?;
371 } else {
372 self.spending_limits[limit_key][call.token]
373 .remaining
374 .write(call.newLimit)?;
375 }
376
377 self.emit_event(AccountKeychainEvent::SpendingLimitUpdated(
379 IAccountKeychain::SpendingLimitUpdated {
380 account: msg_sender,
381 publicKey: call.keyId,
382 token: call.token,
383 newLimit: call.newLimit,
384 },
385 ))
386 }
387
388 pub fn get_key(&self, call: getKeyCall) -> Result<KeyInfo> {
390 let key = self.keys[call.account][call.keyId].read()?;
391
392 if key.expiry == 0 || key.is_revoked {
394 return Ok(KeyInfo {
395 signatureType: SignatureType::Secp256k1,
396 keyId: Address::ZERO,
397 expiry: 0,
398 enforceLimits: false,
399 isRevoked: key.is_revoked,
400 });
401 }
402
403 let signature_type = match key.signature_type {
405 0 => SignatureType::Secp256k1,
406 1 => SignatureType::P256,
407 2 => SignatureType::WebAuthn,
408 _ => SignatureType::Secp256k1, };
410
411 Ok(KeyInfo {
412 signatureType: signature_type,
413 keyId: call.keyId,
414 expiry: key.expiry,
415 enforceLimits: key.enforce_limits,
416 isRevoked: key.is_revoked,
417 })
418 }
419
420 pub fn get_remaining_limit(&self, call: getRemainingLimitCall) -> Result<U256> {
425 if !self.storage.spec().is_t2() {
426 let limit_key = Self::spending_limit_key(call.account, call.keyId);
427 return self.spending_limits[limit_key][call.token].remaining.read();
428 }
429
430 self.get_remaining_limit_with_period(getRemainingLimitWithPeriodCall {
431 account: call.account,
432 keyId: call.keyId,
433 token: call.token,
434 })
435 .map(|ret| ret.remaining)
436 }
437
438 pub fn get_remaining_limit_with_period(
442 &self,
443 call: getRemainingLimitWithPeriodCall,
444 ) -> Result<getRemainingLimitReturn> {
445 let (remaining, period_end) = self.effective_limit_state(
446 call.account,
447 call.keyId,
448 call.token,
449 self.storage.timestamp().saturating_to::<u64>(),
450 )?;
451
452 Ok(getRemainingLimitReturn {
453 remaining,
454 periodEnd: period_end,
455 })
456 }
457
458 pub fn set_allowed_calls(
460 &mut self,
461 msg_sender: Address,
462 call: setAllowedCallsCall,
463 ) -> Result<()> {
464 if !self.storage.spec().is_t3() {
465 return Err(AccountKeychainError::invalid_call_scope().into());
466 }
467
468 self.ensure_admin_caller(msg_sender)?;
469
470 let current_timestamp = self.storage.timestamp().saturating_to::<u64>();
471 self.load_active_key(msg_sender, call.keyId, current_timestamp)?;
472
473 let key_hash = Self::spending_limit_key(msg_sender, call.keyId);
474 let scopes = call.scopes;
475
476 if scopes.is_empty() {
477 return Err(AccountKeychainError::invalid_call_scope().into());
478 }
479
480 self.validate_call_scopes(&scopes)?;
481
482 for scope in &scopes {
483 self.upsert_target_scope(key_hash, scope)?;
484 }
485
486 self.key_scopes[key_hash].is_scoped.write(true)
487 }
488
489 pub fn remove_allowed_calls(
491 &mut self,
492 msg_sender: Address,
493 call: removeAllowedCallsCall,
494 ) -> Result<()> {
495 self.ensure_admin_caller(msg_sender)?;
496
497 let current_timestamp = self.storage.timestamp().saturating_to::<u64>();
498 self.load_active_key(msg_sender, call.keyId, current_timestamp)?;
499
500 let key_hash = Self::spending_limit_key(msg_sender, call.keyId);
501 let current_mode = self.key_scopes[key_hash].is_scoped.read()?;
502 if !current_mode {
503 return Ok(());
504 }
505
506 self.remove_target_scope(key_hash, call.target)?;
507
508 Ok(())
509 }
510
511 pub fn get_allowed_calls(&self, call: getAllowedCallsCall) -> Result<getAllowedCallsReturn> {
517 if call.keyId.is_zero() {
518 return Ok(getAllowedCallsReturn {
519 isScoped: false,
520 scopes: Vec::new(),
521 });
522 }
523
524 let current_timestamp = self.storage.timestamp().saturating_to::<u64>();
525 let key = self.keys[call.account][call.keyId].read()?;
526 if key.expiry == 0 || key.is_revoked || current_timestamp >= key.expiry {
527 return Ok(getAllowedCallsReturn {
528 isScoped: true,
529 scopes: Vec::new(),
530 });
531 }
532
533 let key_hash = Self::spending_limit_key(call.account, call.keyId);
534 let is_scoped = self.key_scopes[key_hash].is_scoped.read()?;
535
536 if !is_scoped {
537 return Ok(getAllowedCallsReturn {
538 isScoped: false,
539 scopes: Vec::new(),
540 });
541 }
542
543 let targets = self.key_scopes[key_hash].targets.read()?;
544 let mut scopes = Vec::new();
545 for target in targets {
546 let selectors = self.key_scopes[key_hash].target_scopes[target]
547 .selectors
548 .read()?;
549
550 let scope = if selectors.is_empty() {
551 CallScope {
552 target,
553 selectorRules: Vec::new(),
554 }
555 } else {
556 let mut rules = Vec::new();
557
558 for selector in selectors {
559 let recipients: Vec<Address> = self.key_scopes[key_hash].target_scopes[target]
560 .selector_scopes[selector]
561 .recipients
562 .read()?
563 .into();
564
565 rules.push(SelectorRule {
566 selector,
567 recipients,
568 });
569 }
570
571 CallScope {
572 target,
573 selectorRules: rules,
574 }
575 };
576
577 scopes.push(scope);
578 }
579
580 Ok(getAllowedCallsReturn {
581 isScoped: true,
582 scopes,
583 })
584 }
585
586 pub fn get_transaction_key(
588 &self,
589 _call: getTransactionKeyCall,
590 _msg_sender: Address,
591 ) -> Result<Address> {
592 self.transaction_key.t_read()
593 }
594
595 pub fn set_transaction_key(&mut self, key_id: Address) -> Result<()> {
606 self.transaction_key.t_write(key_id)
607 }
608
609 pub fn set_tx_origin(&mut self, origin: Address) -> Result<()> {
614 self.tx_origin.t_write(origin)
615 }
616
617 fn apply_key_authorization_restrictions<'a>(
622 &mut self,
623 account: Address,
624 key_id: Address,
625 limits: impl IntoIterator<Item = &'a TokenLimit>,
626 allowed_calls: Option<&[CallScope]>,
627 ) -> Result<()> {
628 let limit_key = Self::spending_limit_key(account, key_id);
629
630 let is_t3 = self.storage.spec().is_t3();
631 debug_assert!(is_t3 || allowed_calls.is_none());
632
633 let now = self.storage.timestamp().saturating_to::<u64>();
634 for limit in limits {
635 if is_t3 {
636 let period_end = if limit.period == 0 {
637 0
638 } else {
639 now.saturating_add(limit.period)
640 };
641
642 self.spending_limits[limit_key][limit.token].write(SpendingLimitState {
643 remaining: limit.amount,
644 max: Self::t3_spending_limit_cap(limit.amount)?,
645 period: limit.period,
646 period_end,
647 })?;
648 } else {
649 self.spending_limits[limit_key][limit.token]
650 .remaining
651 .write(limit.amount)?;
652 }
653 }
654
655 if !is_t3 {
656 return Ok(());
657 }
658
659 self.replace_allowed_calls(limit_key, allowed_calls)
660 }
661
662 pub fn validate_call_scope_for_transaction(
671 &self,
672 account: Address,
673 key_id: Address,
674 to: &TxKind,
675 input: &[u8],
676 ) -> Result<()> {
677 if key_id == Address::ZERO || !self.storage.spec().is_t3() {
678 return Ok(());
679 }
680
681 let target = match to {
682 TxKind::Call(target) => *target,
683 TxKind::Create => return Err(AccountKeychainError::call_not_allowed().into()),
684 };
685
686 let key_hash = Self::spending_limit_key(account, key_id);
687
688 if !self.key_scopes[key_hash].is_scoped.read()? {
690 return Ok(());
691 }
692
693 if !self.key_scopes[key_hash].targets.contains(&target)? {
694 return Err(AccountKeychainError::call_not_allowed().into());
695 }
696
697 let target_is_unconstrained = self.key_scopes[key_hash].target_scopes[target]
700 .selectors
701 .is_empty()?;
702 if target_is_unconstrained {
703 return Ok(());
704 }
705
706 if input.len() < 4 {
707 return Err(AccountKeychainError::call_not_allowed().into());
708 }
709
710 let selector = FixedBytes::<4>::from(
712 <[u8; 4]>::try_from(&input[..4]).expect("input len checked above"),
713 );
714 if !self.key_scopes[key_hash].target_scopes[target]
715 .selectors
716 .contains(&selector)?
717 {
718 return Err(AccountKeychainError::call_not_allowed().into());
719 }
720
721 let selector_is_unconstrained = self.key_scopes[key_hash].target_scopes[target]
723 .selector_scopes[selector]
724 .recipients
725 .is_empty()?;
726 if selector_is_unconstrained {
727 return Ok(());
728 }
729
730 if input.len() < 36 {
731 return Err(AccountKeychainError::call_not_allowed().into());
732 }
733
734 let recipient_word = &input[4..36];
736 if recipient_word[..12].iter().any(|byte| *byte != 0) {
737 return Err(AccountKeychainError::call_not_allowed().into());
738 }
739
740 let recipient = Address::from_slice(&recipient_word[12..]);
741 if self.key_scopes[key_hash].target_scopes[target].selector_scopes[selector]
742 .recipients
743 .contains(&recipient)?
744 {
745 Ok(())
746 } else {
747 Err(AccountKeychainError::call_not_allowed().into())
748 }
749 }
750
751 fn replace_allowed_calls(
758 &mut self,
759 account_key: B256,
760 allowed_calls: Option<&[CallScope]>,
761 ) -> Result<()> {
762 self.clear_all_target_scopes(account_key)?;
767
768 match allowed_calls {
769 None => {
770 self.key_scopes[account_key].is_scoped.write(false)?;
771 Ok(())
772 }
773 Some(scopes) => {
774 self.key_scopes[account_key].is_scoped.write(true)?;
775
776 if scopes.is_empty() {
777 return Ok(());
778 }
779
780 self.validate_call_scopes(scopes)?;
781
782 for scope in scopes {
783 self.upsert_target_scope(account_key, scope)?;
784 }
785
786 Ok(())
787 }
788 }
789 }
790
791 fn clear_all_target_scopes(&mut self, account_key: B256) -> Result<()> {
793 let targets = self.key_scopes[account_key].targets.read()?;
794 for target in targets {
795 self.clear_target_selectors(account_key, target)?;
796 }
797
798 self.key_scopes[account_key].targets.delete()
799 }
800
801 fn remove_target_scope(&mut self, account_key: B256, target: Address) -> Result<()> {
803 if !self.key_scopes[account_key].targets.remove(&target)? {
804 return Ok(());
805 }
806
807 self.clear_target_selectors(account_key, target)
808 }
809
810 fn clear_target_selectors(&mut self, account_key: B256, target: Address) -> Result<()> {
812 let selectors = self.key_scopes[account_key].target_scopes[target]
813 .selectors
814 .read()?;
815 for selector in selectors {
816 self.key_scopes[account_key].target_scopes[target].selector_scopes[selector]
817 .recipients
818 .delete()?;
819 }
820
821 self.key_scopes[account_key].target_scopes[target]
822 .selectors
823 .delete()
824 }
825
826 fn upsert_target_scope(&mut self, account_key: B256, scope: &CallScope) -> Result<()> {
828 let target = scope.target;
829
830 if !self.storage.spec().is_t4() {
832 self.validate_call_scope(scope)?;
833 }
834
835 self.key_scopes[account_key].targets.insert(target)?;
836 self.clear_target_selectors(account_key, target)?;
837
838 if scope.selectorRules.is_empty() {
839 return Ok(());
843 }
844
845 for rule in &scope.selectorRules {
846 let selector = rule.selector;
847 self.key_scopes[account_key].target_scopes[target]
848 .selectors
849 .insert(selector)?;
850
851 if rule.recipients.is_empty() {
852 if !self.storage.spec().is_t4() {
853 self.key_scopes[account_key].target_scopes[target].selector_scopes[selector]
857 .recipients
858 .delete()?;
859 }
860 } else {
861 self.key_scopes[account_key].target_scopes[target].selector_scopes[selector]
863 .recipients
864 .write(Set::new_unchecked(rule.recipients.clone()))?;
865 }
866 }
867
868 Ok(())
869 }
870
871 fn validate_call_scopes(&self, scopes: &[CallScope]) -> Result<()> {
873 let mut seen_targets = HashSet::new();
874 for scope in scopes {
875 if !seen_targets.insert(scope.target) {
876 return Err(AccountKeychainError::invalid_call_scope().into());
877 }
878
879 if self.storage.spec().is_t4() {
881 self.validate_call_scope(scope)?;
882 }
883 }
884 Ok(())
885 }
886
887 fn validate_call_scope(&self, scope: &CallScope) -> Result<()> {
889 if scope.target.is_zero() {
892 return Err(AccountKeychainError::invalid_call_scope().into());
893 }
894
895 if !scope.selectorRules.is_empty() {
896 self.validate_selector_rules(scope.target, &scope.selectorRules)?;
897 }
898
899 Ok(())
900 }
901
902 fn validate_selector_rules(&self, target: Address, rules: &[SelectorRule]) -> Result<()> {
908 let mut cached_is_tip20: Option<bool> = None;
909 let mut is_tip20 = || -> Result<bool> {
910 match cached_is_tip20 {
911 Some(v) => Ok(v),
912 None => Ok(*cached_is_tip20.insert({
913 if !self.storage.spec().is_t4() {
914 TIP20Factory::new().is_tip20(target)?
916 } else {
917 target.is_tip20()
919 }
920 })),
921 }
922 };
923
924 let mut selectors = HashSet::new();
925 for rule in rules {
926 if !selectors.insert(rule.selector) {
927 return Err(AccountKeychainError::invalid_call_scope().into());
928 }
929
930 if rule.recipients.is_empty() {
931 continue;
932 }
933
934 if !is_constrained_tip20_selector(*rule.selector) || !is_tip20()? {
935 return Err(AccountKeychainError::invalid_call_scope().into());
936 }
937
938 let mut unique_recipients = HashSet::new();
939 for recipient in &rule.recipients {
940 if recipient.is_zero() || !unique_recipients.insert(*recipient) {
941 return Err(AccountKeychainError::invalid_call_scope().into());
942 }
943 }
944 }
945
946 Ok(())
947 }
948
949 fn ensure_admin_caller(&self, msg_sender: Address) -> Result<()> {
966 if !self.transaction_key.t_read()?.is_zero() {
967 return Err(AccountKeychainError::unauthorized_caller().into());
968 }
969
970 if self.storage.spec().is_t2() {
971 let tx_origin = self.tx_origin.t_read()?;
972 if tx_origin.is_zero() || tx_origin != msg_sender {
973 return Err(AccountKeychainError::unauthorized_caller().into());
974 }
975 }
976
977 Ok(())
978 }
979
980 fn load_active_key(
987 &self,
988 account: Address,
989 key_id: Address,
990 current_timestamp: u64,
991 ) -> Result<AuthorizedKey> {
992 let key = self.keys[account][key_id].read()?;
993
994 if key.is_revoked {
995 return Err(AccountKeychainError::key_already_revoked().into());
996 }
997
998 if key.expiry == 0 {
999 return Err(AccountKeychainError::key_not_found().into());
1000 }
1001
1002 if current_timestamp >= key.expiry {
1003 return Err(AccountKeychainError::key_expired().into());
1004 }
1005
1006 Ok(key)
1007 }
1008
1009 pub fn validate_keychain_authorization(
1024 &self,
1025 account: Address,
1026 key_id: Address,
1027 current_timestamp: u64,
1028 expected_sig_type: Option<u8>,
1029 ) -> Result<AuthorizedKey> {
1030 let key = self.load_active_key(account, key_id, current_timestamp)?;
1031
1032 if let Some(sig_type) = expected_sig_type
1035 && key.signature_type != sig_type
1036 {
1037 return Err(AccountKeychainError::signature_type_mismatch(
1038 key.signature_type,
1039 sig_type,
1040 )
1041 .into());
1042 }
1043
1044 Ok(key)
1045 }
1046
1047 pub fn effective_remaining_limit(
1049 &self,
1050 account: Address,
1051 key_id: Address,
1052 token: Address,
1053 current_timestamp: u64,
1054 ) -> Result<U256> {
1055 self.effective_limit_state(account, key_id, token, current_timestamp)
1056 .map(|(remaining, _)| remaining)
1057 }
1058
1059 fn effective_limit_state(
1062 &self,
1063 account: Address,
1064 key_id: Address,
1065 token: Address,
1066 current_timestamp: u64,
1067 ) -> Result<(U256, u64)> {
1068 if key_id.is_zero() && self.storage.spec().is_t3() {
1069 return Ok((U256::ZERO, 0));
1070 }
1071
1072 let key = self.keys[account][key_id].read()?;
1073
1074 if key.is_revoked || key.expiry == 0 {
1076 return Ok((U256::ZERO, 0));
1077 }
1078
1079 if current_timestamp >= key.expiry && self.storage.spec().is_t3() {
1081 return Ok((U256::ZERO, 0));
1082 }
1083
1084 let limit_key = Self::spending_limit_key(account, key_id);
1085 let remaining = self.spending_limits[limit_key][token].remaining.read()?;
1086
1087 if !self.storage.spec().is_t3() {
1088 return Ok((remaining, 0));
1089 }
1090
1091 let period = self.spending_limits[limit_key][token].period.read()?;
1092 if period == 0 {
1093 return Ok((remaining, 0));
1094 }
1095
1096 let period_end = self.spending_limits[limit_key][token].period_end.read()?;
1097 if current_timestamp < period_end {
1098 return Ok((remaining, period_end));
1099 }
1100
1101 let elapsed = current_timestamp.saturating_sub(period_end);
1102 let periods_elapsed = (elapsed / period).saturating_add(1);
1103 let advance = period.saturating_mul(periods_elapsed);
1104 let next_end = period_end.saturating_add(advance);
1105
1106 let max = self.spending_limits[limit_key][token].max.read()?;
1107
1108 Ok((U256::from(max), next_end))
1109 }
1110
1111 pub fn verify_and_update_spending(
1118 &mut self,
1119 account: Address,
1120 key_id: Address,
1121 token: Address,
1122 amount: U256,
1123 ) -> Result<()> {
1124 if key_id == Address::ZERO {
1126 return Ok(());
1127 }
1128
1129 let current_timestamp = self.storage.timestamp().saturating_to::<u64>();
1131 let key = self.load_active_key(account, key_id, current_timestamp)?;
1132
1133 if !key.enforce_limits {
1135 return Ok(());
1136 }
1137
1138 let limit_key = Self::spending_limit_key(account, key_id);
1140 if !self.storage.spec().is_t3() {
1141 let remaining = self.spending_limits[limit_key][token].remaining.read()?;
1142 if amount > remaining {
1143 return Err(AccountKeychainError::spending_limit_exceeded().into());
1144 }
1145
1146 let new_remaining = remaining - amount;
1147 self.spending_limits[limit_key][token]
1148 .remaining
1149 .write(new_remaining)?;
1150 return Ok(());
1151 }
1152
1153 let mut limit_state = self.spending_limits[limit_key][token].read()?;
1154 let mut remaining = limit_state.remaining;
1155 let is_periodic = limit_state.period != 0;
1156
1157 if is_periodic && current_timestamp >= limit_state.period_end {
1158 let next_end = limit_state.compute_next_period_end(current_timestamp);
1159
1160 remaining = U256::from(limit_state.max);
1161 limit_state.remaining = remaining;
1162 limit_state.period_end = next_end;
1163 }
1164
1165 if amount > remaining {
1166 return Err(AccountKeychainError::spending_limit_exceeded().into());
1167 }
1168
1169 let new_remaining = remaining - amount;
1171 if is_periodic {
1172 limit_state.remaining = new_remaining;
1173 self.spending_limits[limit_key][token].write(limit_state)?;
1174 } else {
1175 self.spending_limits[limit_key][token]
1176 .remaining
1177 .write(new_remaining)?;
1178 }
1179
1180 self.emit_event(AccountKeychainEvent::AccessKeySpend(
1181 IAccountKeychain::AccessKeySpend {
1182 account,
1183 publicKey: key_id,
1184 token,
1185 amount,
1186 remainingLimit: new_remaining,
1187 },
1188 ))?;
1189
1190 Ok(())
1191 }
1192
1193 pub fn refund_spending_limit(
1200 &mut self,
1201 account: Address,
1202 token: Address,
1203 amount: U256,
1204 ) -> Result<()> {
1205 let transaction_key = self.transaction_key.t_read()?;
1206
1207 if transaction_key == Address::ZERO {
1208 return Ok(());
1209 }
1210
1211 let tx_origin = self.tx_origin.t_read()?;
1212 if account != tx_origin {
1213 return Ok(());
1214 }
1215
1216 let current_timestamp = self.storage.timestamp().saturating_to::<u64>();
1219 let key = match self.load_active_key(account, transaction_key, current_timestamp) {
1220 Ok(key) => key,
1221 Err(err) if err.is_system_error() => return Err(err),
1222 Err(_) => return Ok(()),
1223 };
1224
1225 if !key.enforce_limits {
1226 return Ok(());
1227 }
1228
1229 let limit_key = Self::spending_limit_key(account, transaction_key);
1230 if !self.storage.spec().is_t3() {
1231 let remaining = self.spending_limits[limit_key][token].remaining.read()?;
1232 let refunded = remaining.saturating_add(amount);
1233 return self.spending_limits[limit_key][token]
1234 .remaining
1235 .write(refunded);
1236 }
1237
1238 let mut limit_state = self.spending_limits[limit_key][token].read()?;
1239 let refunded = limit_state.remaining.saturating_add(amount);
1240 limit_state.remaining = if limit_state.max == 0 {
1244 refunded
1245 } else {
1246 refunded.min(U256::from(limit_state.max))
1247 };
1248
1249 self.spending_limits[limit_key][token].write(limit_state)
1250 }
1251
1252 pub fn authorize_transfer(
1263 &mut self,
1264 account: Address,
1265 token: Address,
1266 amount: U256,
1267 ) -> Result<()> {
1268 let transaction_key = self.transaction_key.t_read()?;
1270
1271 if transaction_key == Address::ZERO {
1273 return Ok(());
1274 }
1275
1276 let tx_origin = self.tx_origin.t_read()?;
1278 if account != tx_origin {
1279 return Ok(());
1280 }
1281
1282 self.verify_and_update_spending(account, transaction_key, token, amount)
1284 }
1285
1286 pub fn authorize_approve(
1297 &mut self,
1298 account: Address,
1299 token: Address,
1300 old_approval: U256,
1301 new_approval: U256,
1302 ) -> Result<()> {
1303 let transaction_key = self.transaction_key.t_read()?;
1305
1306 if transaction_key == Address::ZERO {
1308 return Ok(());
1309 }
1310
1311 let tx_origin = self.tx_origin.t_read()?;
1313 if account != tx_origin {
1314 return Ok(());
1315 }
1316
1317 let approval_increase = new_approval.saturating_sub(old_approval);
1321
1322 if approval_increase.is_zero() {
1324 return Ok(());
1325 }
1326
1327 self.verify_and_update_spending(account, transaction_key, token, approval_increase)
1329 }
1330}
1331
1332#[cfg(test)]
1333mod tests {
1334 use super::*;
1335 use crate::{
1336 error::TempoPrecompileError,
1337 storage::{StorageCtx, hashmap::HashMapStorageProvider},
1338 test_util::TIP20Setup,
1339 };
1340 use alloy::primitives::{Address, B256, TxKind, U256};
1341 use revm::state::Bytecode;
1342 use tempo_chainspec::hardfork::TempoHardfork;
1343 use tempo_contracts::precompiles::{DEFAULT_FEE_TOKEN, IAccountKeychain::SignatureType};
1344
1345 fn assert_unauthorized_error(error: TempoPrecompileError) {
1347 match error {
1348 TempoPrecompileError::AccountKeychainError(e) => {
1349 assert!(
1350 matches!(e, AccountKeychainError::UnauthorizedCaller(_)),
1351 "Expected UnauthorizedCaller error, got: {e:?}"
1352 );
1353 }
1354 _ => panic!("Expected AccountKeychainError, got: {error:?}"),
1355 }
1356 }
1357
1358 fn assert_call_not_allowed(error: TempoPrecompileError) {
1359 match error {
1360 TempoPrecompileError::AccountKeychainError(e) => {
1361 assert!(
1362 matches!(e, AccountKeychainError::CallNotAllowed(_)),
1363 "Expected CallNotAllowed error, got: {e:?}"
1364 );
1365 }
1366 _ => panic!("Expected AccountKeychainError, got: {error:?}"),
1367 }
1368 }
1369
1370 fn assert_invalid_call_scope(error: TempoPrecompileError) {
1371 match error {
1372 TempoPrecompileError::AccountKeychainError(e) => {
1373 assert!(
1374 matches!(e, AccountKeychainError::InvalidCallScope(_)),
1375 "Expected InvalidCallScope error, got: {e:?}"
1376 );
1377 }
1378 _ => panic!("Expected AccountKeychainError, got: {error:?}"),
1379 }
1380 }
1381
1382 #[test]
1383 fn test_transaction_key_transient_storage() -> eyre::Result<()> {
1384 let mut storage = HashMapStorageProvider::new(1);
1385 let access_key_addr = Address::random();
1386 StorageCtx::enter(&mut storage, || {
1387 let mut keychain = AccountKeychain::new();
1388
1389 let initial_key = keychain.transaction_key.t_read()?;
1391 assert_eq!(
1392 initial_key,
1393 Address::ZERO,
1394 "Initial transaction key should be zero"
1395 );
1396
1397 keychain.set_transaction_key(access_key_addr)?;
1399
1400 let loaded_key = keychain.transaction_key.t_read()?;
1402 assert_eq!(loaded_key, access_key_addr, "Transaction key should be set");
1403
1404 let get_tx_key_call = getTransactionKeyCall {};
1406 let result = keychain.get_transaction_key(get_tx_key_call, Address::ZERO)?;
1407 assert_eq!(
1408 result, access_key_addr,
1409 "getTransactionKey should return the set key"
1410 );
1411
1412 keychain.set_transaction_key(Address::ZERO)?;
1414 let cleared_key = keychain.transaction_key.t_read()?;
1415 assert_eq!(
1416 cleared_key,
1417 Address::ZERO,
1418 "Transaction key should be cleared"
1419 );
1420
1421 Ok(())
1422 })
1423 }
1424
1425 #[test]
1426 fn test_admin_operations_blocked_with_access_key() -> eyre::Result<()> {
1427 let mut storage = HashMapStorageProvider::new(1);
1428 let msg_sender = Address::random();
1429 let existing_key = Address::random();
1430 let access_key = Address::random();
1431 let token = Address::random();
1432 let other = Address::random();
1433 StorageCtx::enter(&mut storage, || {
1434 let mut keychain = AccountKeychain::new();
1436 keychain.initialize()?;
1437
1438 keychain.set_transaction_key(Address::ZERO)?;
1440 let setup_call = authorizeKeyCall {
1441 keyId: existing_key,
1442 signatureType: SignatureType::Secp256k1,
1443 config: KeyRestrictions {
1444 expiry: u64::MAX,
1445 enforceLimits: true,
1446 limits: vec![],
1447 allowAnyCalls: true,
1448 allowedCalls: vec![],
1449 },
1450 };
1451 keychain.authorize_key(msg_sender, setup_call)?;
1452
1453 keychain.set_transaction_key(access_key)?;
1455
1456 let auth_call = authorizeKeyCall {
1458 keyId: other,
1459 signatureType: SignatureType::P256,
1460 config: KeyRestrictions {
1461 expiry: u64::MAX,
1462 enforceLimits: true,
1463 limits: vec![],
1464 allowAnyCalls: true,
1465 allowedCalls: vec![],
1466 },
1467 };
1468 let auth_result = keychain.authorize_key(msg_sender, auth_call);
1469 assert!(
1470 auth_result.is_err(),
1471 "authorize_key should fail when using access key"
1472 );
1473 assert_unauthorized_error(auth_result.unwrap_err());
1474
1475 let revoke_call = revokeKeyCall {
1477 keyId: existing_key,
1478 };
1479 let revoke_result = keychain.revoke_key(msg_sender, revoke_call);
1480 assert!(
1481 revoke_result.is_err(),
1482 "revoke_key should fail when using access key"
1483 );
1484 assert_unauthorized_error(revoke_result.unwrap_err());
1485
1486 let update_call = updateSpendingLimitCall {
1488 keyId: existing_key,
1489 token,
1490 newLimit: U256::from(1000),
1491 };
1492 let update_result = keychain.update_spending_limit(msg_sender, update_call);
1493 assert!(
1494 update_result.is_err(),
1495 "update_spending_limit should fail when using access key"
1496 );
1497 assert_unauthorized_error(update_result.unwrap_err());
1498
1499 Ok(())
1500 })
1501 }
1502
1503 #[test]
1504 fn test_admin_operations_require_tx_origin_on_t2() -> eyre::Result<()> {
1505 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
1506 let tx_origin = Address::random();
1507 let delegated_sender = Address::random();
1508 let existing_key = Address::random();
1509 let token = Address::random();
1510 let other = Address::random();
1511
1512 StorageCtx::enter(&mut storage, || {
1513 let mut keychain = AccountKeychain::new();
1514 keychain.initialize()?;
1515
1516 keychain
1518 .storage
1519 .set_code(delegated_sender, Bytecode::new_raw(vec![0x60, 0x00].into()))?;
1520
1521 keychain.set_transaction_key(Address::ZERO)?;
1523 keychain.set_tx_origin(delegated_sender)?;
1524 keychain.authorize_key(
1525 delegated_sender,
1526 authorizeKeyCall {
1527 keyId: existing_key,
1528 signatureType: SignatureType::Secp256k1,
1529 config: KeyRestrictions {
1530 expiry: u64::MAX,
1531 enforceLimits: true,
1532 limits: vec![],
1533 allowAnyCalls: true,
1534 allowedCalls: vec![],
1535 },
1536 },
1537 )?;
1538
1539 keychain.set_tx_origin(tx_origin)?;
1541
1542 let auth_result = keychain.authorize_key(
1543 delegated_sender,
1544 authorizeKeyCall {
1545 keyId: other,
1546 signatureType: SignatureType::P256,
1547 config: KeyRestrictions {
1548 expiry: u64::MAX,
1549 enforceLimits: true,
1550 limits: vec![],
1551 allowAnyCalls: true,
1552 allowedCalls: vec![],
1553 },
1554 },
1555 );
1556 assert!(auth_result.is_err());
1557 assert_unauthorized_error(auth_result.unwrap_err());
1558
1559 let revoke_result = keychain.revoke_key(
1560 delegated_sender,
1561 revokeKeyCall {
1562 keyId: existing_key,
1563 },
1564 );
1565 assert!(revoke_result.is_err());
1566 assert_unauthorized_error(revoke_result.unwrap_err());
1567
1568 let update_result = keychain.update_spending_limit(
1569 delegated_sender,
1570 updateSpendingLimitCall {
1571 keyId: existing_key,
1572 token,
1573 newLimit: U256::from(1000),
1574 },
1575 );
1576 assert!(update_result.is_err());
1577 assert_unauthorized_error(update_result.unwrap_err());
1578
1579 Ok(())
1580 })
1581 }
1582
1583 #[test]
1584 fn test_admin_operations_allow_contract_origin_on_t2() -> eyre::Result<()> {
1585 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
1586 let contract_sender = Address::random();
1587 let key_id = Address::random();
1588 let token = Address::random();
1589
1590 StorageCtx::enter(&mut storage, || {
1591 let mut keychain = AccountKeychain::new();
1592 keychain.initialize()?;
1593
1594 keychain
1595 .storage
1596 .set_code(contract_sender, Bytecode::new_raw(vec![0x60, 0x00].into()))?;
1597
1598 keychain.set_transaction_key(Address::ZERO)?;
1601 keychain.set_tx_origin(contract_sender)?;
1602
1603 keychain.authorize_key(
1604 contract_sender,
1605 authorizeKeyCall {
1606 keyId: key_id,
1607 signatureType: SignatureType::Secp256k1,
1608 config: KeyRestrictions {
1609 expiry: u64::MAX,
1610 enforceLimits: true,
1611 limits: vec![TokenLimit {
1612 token,
1613 amount: U256::from(100),
1614 period: 0,
1615 }],
1616 allowAnyCalls: true,
1617 allowedCalls: vec![],
1618 },
1619 },
1620 )?;
1621
1622 keychain.update_spending_limit(
1623 contract_sender,
1624 updateSpendingLimitCall {
1625 keyId: key_id,
1626 token,
1627 newLimit: U256::from(200),
1628 },
1629 )?;
1630
1631 assert_eq!(
1632 keychain.get_remaining_limit(getRemainingLimitCall {
1633 account: contract_sender,
1634 keyId: key_id,
1635 token,
1636 })?,
1637 U256::from(200)
1638 );
1639
1640 keychain.revoke_key(contract_sender, revokeKeyCall { keyId: key_id })?;
1641
1642 let key_info = keychain.get_key(getKeyCall {
1643 account: contract_sender,
1644 keyId: key_id,
1645 })?;
1646 assert!(key_info.isRevoked);
1647
1648 Ok(())
1649 })
1650 }
1651
1652 #[test]
1653 fn test_admin_operations_allow_origin_mismatch_pre_t2() -> eyre::Result<()> {
1654 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T0);
1655 let msg_sender = Address::random();
1656 let other_origin = Address::random();
1657 let key_id = Address::random();
1658 let token = Address::random();
1659
1660 StorageCtx::enter(&mut storage, || {
1661 let mut keychain = AccountKeychain::new();
1662 keychain.initialize()?;
1663
1664 keychain.set_transaction_key(Address::ZERO)?;
1666 keychain.set_tx_origin(other_origin)?;
1667
1668 keychain.authorize_key(
1669 msg_sender,
1670 authorizeKeyCall {
1671 keyId: key_id,
1672 signatureType: SignatureType::Secp256k1,
1673 config: KeyRestrictions {
1674 expiry: u64::MAX,
1675 enforceLimits: true,
1676 limits: vec![TokenLimit {
1677 token,
1678 amount: U256::from(100),
1679 period: 0,
1680 }],
1681 allowAnyCalls: true,
1682 allowedCalls: vec![],
1683 },
1684 },
1685 )?;
1686
1687 keychain.update_spending_limit(
1688 msg_sender,
1689 updateSpendingLimitCall {
1690 keyId: key_id,
1691 token,
1692 newLimit: U256::from(200),
1693 },
1694 )?;
1695
1696 keychain.revoke_key(msg_sender, revokeKeyCall { keyId: key_id })?;
1697
1698 let key_info = keychain.get_key(getKeyCall {
1699 account: msg_sender,
1700 keyId: key_id,
1701 })?;
1702 assert!(key_info.isRevoked);
1703
1704 Ok(())
1705 })
1706 }
1707
1708 #[test]
1709 fn test_admin_operations_reject_eoa_mismatch_on_t2() -> eyre::Result<()> {
1710 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
1711 let account = Address::random();
1712 let other_origin = Address::random();
1713 let key_id = Address::random();
1714 let token = Address::random();
1715
1716 StorageCtx::enter(&mut storage, || {
1717 let mut keychain = AccountKeychain::new();
1718 keychain.initialize()?;
1719
1720 keychain.set_transaction_key(Address::ZERO)?;
1722 keychain.set_tx_origin(account)?;
1723 keychain.authorize_key(
1724 account,
1725 authorizeKeyCall {
1726 keyId: key_id,
1727 signatureType: SignatureType::Secp256k1,
1728 config: KeyRestrictions {
1729 expiry: u64::MAX,
1730 enforceLimits: true,
1731 limits: vec![TokenLimit {
1732 token,
1733 amount: U256::from(100),
1734 period: 0,
1735 }],
1736 allowAnyCalls: true,
1737 allowedCalls: vec![],
1738 },
1739 },
1740 )?;
1741
1742 keychain.set_tx_origin(other_origin)?;
1744 let result = keychain.update_spending_limit(
1745 account,
1746 updateSpendingLimitCall {
1747 keyId: key_id,
1748 token,
1749 newLimit: U256::from(200),
1750 },
1751 );
1752 assert!(result.is_err());
1753 assert_unauthorized_error(result.unwrap_err());
1754
1755 Ok(())
1756 })
1757 }
1758
1759 #[test]
1763 fn test_admin_operations_reject_unseeded_origin_on_t2() -> eyre::Result<()> {
1764 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
1765 let account = Address::random();
1766 let key_id = Address::random();
1767 let other_key = Address::random();
1768 let token = Address::random();
1769
1770 StorageCtx::enter(&mut storage, || {
1771 let mut keychain = AccountKeychain::new();
1772 keychain.initialize()?;
1773
1774 keychain.set_transaction_key(Address::ZERO)?;
1776 keychain.set_tx_origin(account)?;
1777 keychain.authorize_key(
1778 account,
1779 authorizeKeyCall {
1780 keyId: key_id,
1781 signatureType: SignatureType::Secp256k1,
1782 config: KeyRestrictions {
1783 expiry: u64::MAX,
1784 enforceLimits: true,
1785 limits: vec![TokenLimit {
1786 token,
1787 amount: U256::from(100),
1788 period: 0,
1789 }],
1790 allowAnyCalls: true,
1791 allowedCalls: vec![],
1792 },
1793 },
1794 )?;
1795
1796 keychain.set_tx_origin(Address::ZERO)?;
1799
1800 let auth_result = keychain.authorize_key(
1802 account,
1803 authorizeKeyCall {
1804 keyId: other_key,
1805 signatureType: SignatureType::P256,
1806 config: KeyRestrictions {
1807 expiry: u64::MAX,
1808 enforceLimits: false,
1809 limits: vec![],
1810 allowAnyCalls: true,
1811 allowedCalls: vec![],
1812 },
1813 },
1814 );
1815 assert!(
1816 auth_result.is_err(),
1817 "authorize_key must reject when tx_origin is not seeded on T2"
1818 );
1819 assert_unauthorized_error(auth_result.unwrap_err());
1820
1821 let revoke_result = keychain.revoke_key(account, revokeKeyCall { keyId: key_id });
1823 assert!(
1824 revoke_result.is_err(),
1825 "revoke_key must reject when tx_origin is not seeded on T2"
1826 );
1827 assert_unauthorized_error(revoke_result.unwrap_err());
1828
1829 let update_result = keychain.update_spending_limit(
1831 account,
1832 updateSpendingLimitCall {
1833 keyId: key_id,
1834 token,
1835 newLimit: U256::from(200),
1836 },
1837 );
1838 assert!(
1839 update_result.is_err(),
1840 "update_spending_limit must reject when tx_origin is not seeded on T2"
1841 );
1842 assert_unauthorized_error(update_result.unwrap_err());
1843
1844 Ok(())
1845 })
1846 }
1847
1848 #[test]
1849 fn test_replay_protection_revoked_key_cannot_be_reauthorized() -> eyre::Result<()> {
1850 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
1851 let account = Address::random();
1852 let key_id = Address::random();
1853 let token = Address::random();
1854 StorageCtx::enter(&mut storage, || {
1855 let mut keychain = AccountKeychain::new();
1856 keychain.initialize()?;
1857
1858 keychain.set_transaction_key(Address::ZERO)?;
1860 keychain.set_tx_origin(account)?;
1861
1862 let auth_call = authorizeKeyCall {
1864 keyId: key_id,
1865 signatureType: SignatureType::Secp256k1,
1866 config: KeyRestrictions {
1867 expiry: u64::MAX,
1868 enforceLimits: true,
1869 limits: vec![TokenLimit {
1870 token,
1871 amount: U256::from(100),
1872 period: 0,
1873 }],
1874 allowAnyCalls: true,
1875 allowedCalls: vec![],
1876 },
1877 };
1878 keychain.authorize_key(account, auth_call.clone())?;
1879
1880 let key_info = keychain.get_key(getKeyCall {
1882 account,
1883 keyId: key_id,
1884 })?;
1885 assert_eq!(key_info.expiry, u64::MAX);
1886 assert!(!key_info.isRevoked);
1887 assert_eq!(
1888 keychain.get_remaining_limit(getRemainingLimitCall {
1889 account,
1890 keyId: key_id,
1891 token,
1892 })?,
1893 U256::from(100)
1894 );
1895
1896 let revoke_call = revokeKeyCall { keyId: key_id };
1898 keychain.revoke_key(account, revoke_call)?;
1899
1900 let key_info = keychain.get_key(getKeyCall {
1902 account,
1903 keyId: key_id,
1904 })?;
1905 assert_eq!(key_info.expiry, 0);
1906 assert!(key_info.isRevoked);
1907 assert_eq!(
1908 keychain.get_remaining_limit(getRemainingLimitCall {
1909 account,
1910 keyId: key_id,
1911 token,
1912 })?,
1913 U256::ZERO
1914 );
1915
1916 let replay_result = keychain.authorize_key(account, auth_call);
1919 assert!(
1920 replay_result.is_err(),
1921 "Re-authorizing a revoked key should fail"
1922 );
1923
1924 match replay_result.unwrap_err() {
1926 TempoPrecompileError::AccountKeychainError(e) => {
1927 assert!(
1928 matches!(e, AccountKeychainError::KeyAlreadyRevoked(_)),
1929 "Expected KeyAlreadyRevoked error, got: {e:?}"
1930 );
1931 }
1932 e => panic!("Expected AccountKeychainError, got: {e:?}"),
1933 }
1934 Ok(())
1935 })
1936 }
1937
1938 #[test]
1939 fn test_authorize_key_rejects_expiry_in_past() -> eyre::Result<()> {
1940 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T0);
1942 let account = Address::random();
1943 let key_id = Address::random();
1944 StorageCtx::enter(&mut storage, || {
1945 let mut keychain = AccountKeychain::new();
1946 keychain.initialize()?;
1947
1948 keychain.set_transaction_key(Address::ZERO)?;
1950
1951 let auth_call = authorizeKeyCall {
1953 keyId: key_id,
1954 signatureType: SignatureType::Secp256k1,
1955 config: KeyRestrictions {
1956 expiry: 0, enforceLimits: false,
1958 limits: vec![],
1959 allowAnyCalls: true,
1960 allowedCalls: vec![],
1961 },
1962 };
1963 let result = keychain.authorize_key(account, auth_call);
1964 assert!(
1965 result.is_err(),
1966 "Authorizing with expiry in past should fail"
1967 );
1968
1969 match result.unwrap_err() {
1971 TempoPrecompileError::AccountKeychainError(e) => {
1972 assert!(
1973 matches!(e, AccountKeychainError::ExpiryInPast(_)),
1974 "Expected ExpiryInPast error, got: {e:?}"
1975 );
1976 }
1977 e => panic!("Expected AccountKeychainError, got: {e:?}"),
1978 }
1979
1980 let auth_call_past = authorizeKeyCall {
1982 keyId: key_id,
1983 signatureType: SignatureType::Secp256k1,
1984 config: KeyRestrictions {
1985 expiry: 1, enforceLimits: false,
1987 limits: vec![],
1988 allowAnyCalls: true,
1989 allowedCalls: vec![],
1990 },
1991 };
1992 let result_past = keychain.authorize_key(account, auth_call_past);
1993 assert!(
1994 matches!(
1995 result_past,
1996 Err(TempoPrecompileError::AccountKeychainError(
1997 AccountKeychainError::ExpiryInPast(_)
1998 ))
1999 ),
2000 "Expected ExpiryInPast error for past expiry, got: {result_past:?}"
2001 );
2002
2003 Ok(())
2004 })
2005 }
2006
2007 #[test]
2008 fn test_pre_t3_authorize_key_rejects_tip_1011_fields_without_writing_key() -> eyre::Result<()> {
2009 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T1C);
2010 let account = Address::random();
2011 let key_id = Address::random();
2012 let token = Address::random();
2013
2014 StorageCtx::enter(&mut storage, || {
2015 let mut keychain = AccountKeychain::new();
2016 keychain.initialize()?;
2017 keychain.set_transaction_key(Address::ZERO)?;
2018
2019 let result = keychain.authorize_key(
2020 account,
2021 authorizeKeyCall {
2022 keyId: key_id,
2023 signatureType: SignatureType::Secp256k1,
2024 config: KeyRestrictions {
2025 expiry: u64::MAX,
2026 enforceLimits: true,
2027 limits: vec![TokenLimit {
2028 token,
2029 amount: U256::from(100u64),
2030 period: 60,
2031 }],
2032 allowAnyCalls: true,
2033 allowedCalls: vec![],
2034 },
2035 },
2036 );
2037
2038 assert!(
2039 matches!(
2040 result,
2041 Err(TempoPrecompileError::AccountKeychainError(
2042 AccountKeychainError::InvalidSpendingLimit(_)
2043 ))
2044 ),
2045 "expected InvalidSpendingLimit, got {result:?}"
2046 );
2047
2048 assert_eq!(
2049 keychain.keys[account][key_id].read()?,
2050 AuthorizedKey::default(),
2051 "pre-T3 invalid TIP-1011 fields must not leave behind a key"
2052 );
2053
2054 let limit_key = AccountKeychain::spending_limit_key(account, key_id);
2055 assert_eq!(
2056 keychain.spending_limits[limit_key][token].read()?,
2057 SpendingLimitState::default(),
2058 "pre-T3 invalid TIP-1011 fields must not initialize limits"
2059 );
2060
2061 Ok(())
2062 })
2063 }
2064
2065 #[test]
2066 fn test_different_key_id_can_be_authorized_after_revocation() -> eyre::Result<()> {
2067 let mut storage = HashMapStorageProvider::new(1);
2068 let account = Address::random();
2069 let key_id_1 = Address::random();
2070 let key_id_2 = Address::random();
2071 StorageCtx::enter(&mut storage, || {
2072 let mut keychain = AccountKeychain::new();
2073 keychain.initialize()?;
2074
2075 keychain.set_transaction_key(Address::ZERO)?;
2077
2078 let auth_call_1 = authorizeKeyCall {
2080 keyId: key_id_1,
2081 signatureType: SignatureType::Secp256k1,
2082 config: KeyRestrictions {
2083 expiry: u64::MAX,
2084 enforceLimits: false,
2085 limits: vec![],
2086 allowAnyCalls: true,
2087 allowedCalls: vec![],
2088 },
2089 };
2090 keychain.authorize_key(account, auth_call_1)?;
2091
2092 keychain.revoke_key(account, revokeKeyCall { keyId: key_id_1 })?;
2094
2095 let auth_call_2 = authorizeKeyCall {
2097 keyId: key_id_2,
2098 signatureType: SignatureType::P256,
2099 config: KeyRestrictions {
2100 expiry: u64::MAX,
2101 enforceLimits: true,
2102 limits: vec![],
2103 allowAnyCalls: true,
2104 allowedCalls: vec![],
2105 },
2106 };
2107 keychain.authorize_key(account, auth_call_2)?;
2108
2109 let key_info = keychain.get_key(getKeyCall {
2111 account,
2112 keyId: key_id_2,
2113 })?;
2114 assert_eq!(key_info.expiry, u64::MAX);
2115 assert!(!key_info.isRevoked);
2116
2117 Ok(())
2118 })
2119 }
2120
2121 #[test]
2122 fn test_authorize_approve() -> eyre::Result<()> {
2123 let mut storage = HashMapStorageProvider::new(1);
2124
2125 let eoa = Address::random();
2126 let access_key = Address::random();
2127 let token = Address::random();
2128 let contract = Address::random();
2129
2130 StorageCtx::enter(&mut storage, || {
2131 let mut keychain = AccountKeychain::new();
2132 keychain.initialize()?;
2133
2134 keychain.set_transaction_key(Address::ZERO)?;
2136 keychain.set_tx_origin(eoa)?;
2137
2138 let auth_call = authorizeKeyCall {
2139 keyId: access_key,
2140 signatureType: SignatureType::Secp256k1,
2141 config: KeyRestrictions {
2142 expiry: u64::MAX,
2143 enforceLimits: true,
2144 limits: vec![TokenLimit {
2145 token,
2146 amount: U256::from(100),
2147 period: 0,
2148 }],
2149 allowAnyCalls: true,
2150 allowedCalls: vec![],
2151 },
2152 };
2153 keychain.authorize_key(eoa, auth_call)?;
2154
2155 let initial_limit = keychain.get_remaining_limit(getRemainingLimitCall {
2156 account: eoa,
2157 keyId: access_key,
2158 token,
2159 })?;
2160 assert_eq!(initial_limit, U256::from(100));
2161
2162 keychain.set_transaction_key(access_key)?;
2164
2165 keychain.authorize_approve(eoa, token, U256::ZERO, U256::from(30))?;
2167
2168 let limit_after = keychain.get_remaining_limit(getRemainingLimitCall {
2169 account: eoa,
2170 keyId: access_key,
2171 token,
2172 })?;
2173 assert_eq!(limit_after, U256::from(70));
2174
2175 keychain.authorize_approve(eoa, token, U256::from(30), U256::from(20))?;
2177
2178 let limit_unchanged = keychain.get_remaining_limit(getRemainingLimitCall {
2179 account: eoa,
2180 keyId: access_key,
2181 token,
2182 })?;
2183 assert_eq!(limit_unchanged, U256::from(70));
2184
2185 keychain.authorize_approve(eoa, token, U256::from(20), U256::from(50))?;
2187
2188 let limit_after_increase = keychain.get_remaining_limit(getRemainingLimitCall {
2189 account: eoa,
2190 keyId: access_key,
2191 token,
2192 })?;
2193 assert_eq!(limit_after_increase, U256::from(40));
2194
2195 keychain.authorize_approve(contract, token, U256::ZERO, U256::from(1000))?;
2197
2198 let limit_after_contract = keychain.get_remaining_limit(getRemainingLimitCall {
2199 account: eoa,
2200 keyId: access_key,
2201 token,
2202 })?;
2203 assert_eq!(limit_after_contract, U256::from(40)); let exceed_result = keychain.authorize_approve(eoa, token, U256::ZERO, U256::from(50));
2207 assert!(matches!(
2208 exceed_result,
2209 Err(TempoPrecompileError::AccountKeychainError(
2210 AccountKeychainError::SpendingLimitExceeded(_)
2211 ))
2212 ));
2213
2214 keychain.set_transaction_key(Address::ZERO)?;
2216 keychain.authorize_approve(eoa, token, U256::ZERO, U256::from(1000))?;
2217
2218 let limit_main_key = keychain.get_remaining_limit(getRemainingLimitCall {
2219 account: eoa,
2220 keyId: access_key,
2221 token,
2222 })?;
2223 assert_eq!(limit_main_key, U256::from(40));
2224
2225 Ok(())
2226 })
2227 }
2228
2229 #[test]
2239 fn test_spending_limits_only_apply_to_tx_origin() -> eyre::Result<()> {
2240 let mut storage = HashMapStorageProvider::new(1);
2241
2242 let eoa_alice = Address::random(); let access_key = Address::random(); let contract_address = Address::random(); let token = Address::random();
2246
2247 StorageCtx::enter(&mut storage, || {
2248 let mut keychain = AccountKeychain::new();
2249 keychain.initialize()?;
2250
2251 keychain.set_transaction_key(Address::ZERO)?; keychain.set_tx_origin(eoa_alice)?;
2254
2255 let auth_call = authorizeKeyCall {
2256 keyId: access_key,
2257 signatureType: SignatureType::Secp256k1,
2258 config: KeyRestrictions {
2259 expiry: u64::MAX,
2260 enforceLimits: true,
2261 limits: vec![TokenLimit {
2262 token,
2263 amount: U256::from(100),
2264 period: 0,
2265 }],
2266 allowAnyCalls: true,
2267 allowedCalls: vec![],
2268 },
2269 };
2270 keychain.authorize_key(eoa_alice, auth_call)?;
2271
2272 let limit = keychain.get_remaining_limit(getRemainingLimitCall {
2274 account: eoa_alice,
2275 keyId: access_key,
2276 token,
2277 })?;
2278 assert_eq!(
2279 limit,
2280 U256::from(100),
2281 "Initial spending limit should be 100"
2282 );
2283
2284 keychain.set_transaction_key(access_key)?;
2286 keychain.set_tx_origin(eoa_alice)?;
2287
2288 keychain.authorize_transfer(eoa_alice, token, U256::from(30))?;
2291
2292 let limit_after = keychain.get_remaining_limit(getRemainingLimitCall {
2293 account: eoa_alice,
2294 keyId: access_key,
2295 token,
2296 })?;
2297 assert_eq!(
2298 limit_after,
2299 U256::from(70),
2300 "Spending limit should be reduced to 70 after Alice's direct transfer"
2301 );
2302
2303 keychain.authorize_transfer(contract_address, token, U256::from(1000))?;
2306
2307 let limit_unchanged = keychain.get_remaining_limit(getRemainingLimitCall {
2308 account: eoa_alice,
2309 keyId: access_key,
2310 token,
2311 })?;
2312 assert_eq!(
2313 limit_unchanged,
2314 U256::from(70),
2315 "Spending limit should remain 70 - contract transfer doesn't affect Alice's limit"
2316 );
2317
2318 keychain.authorize_transfer(eoa_alice, token, U256::from(70))?;
2320
2321 let limit_depleted = keychain.get_remaining_limit(getRemainingLimitCall {
2322 account: eoa_alice,
2323 keyId: access_key,
2324 token,
2325 })?;
2326 assert_eq!(
2327 limit_depleted,
2328 U256::ZERO,
2329 "Spending limit should be depleted after Alice spends remaining 70"
2330 );
2331
2332 let exceed_result = keychain.authorize_transfer(eoa_alice, token, U256::from(1));
2334 assert!(
2335 exceed_result.is_err(),
2336 "Should fail when Alice tries to exceed spending limit"
2337 );
2338
2339 let contract_result =
2341 keychain.authorize_transfer(contract_address, token, U256::from(999999));
2342 assert!(
2343 contract_result.is_ok(),
2344 "Contract should still be able to transfer even though Alice's limit is depleted"
2345 );
2346
2347 Ok(())
2348 })
2349 }
2350
2351 #[test]
2352 fn test_authorize_key_rejects_existing_key_boundary() -> eyre::Result<()> {
2353 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::Genesis);
2355 let account = Address::random();
2356 let key_id = Address::random();
2357 StorageCtx::enter(&mut storage, || {
2358 let mut keychain = AccountKeychain::new();
2359 keychain.initialize()?;
2360 keychain.set_transaction_key(Address::ZERO)?;
2361
2362 let auth_call = authorizeKeyCall {
2364 keyId: key_id,
2365 signatureType: SignatureType::Secp256k1,
2366 config: KeyRestrictions {
2367 expiry: 1, enforceLimits: false,
2369 limits: vec![],
2370 allowAnyCalls: true,
2371 allowedCalls: vec![],
2372 },
2373 };
2374 keychain.authorize_key(account, auth_call.clone())?;
2375
2376 let key_info = keychain.get_key(getKeyCall {
2378 account,
2379 keyId: key_id,
2380 })?;
2381 assert_eq!(key_info.expiry, 1, "Key should have expiry = 1");
2382
2383 let result = keychain.authorize_key(account, auth_call);
2385 assert!(result.is_err(), "Should reject when key.expiry > 0");
2386 match result.unwrap_err() {
2387 TempoPrecompileError::AccountKeychainError(e) => {
2388 assert!(
2389 matches!(e, AccountKeychainError::KeyAlreadyExists(_)),
2390 "Expected KeyAlreadyExists, got: {e:?}"
2391 );
2392 }
2393 e => panic!("Expected AccountKeychainError, got: {e:?}"),
2394 }
2395
2396 Ok(())
2397 })
2398 }
2399
2400 #[test]
2401 fn test_spending_limit_key_derivation() {
2402 let account1 = Address::repeat_byte(0x01);
2403 let account2 = Address::repeat_byte(0x02);
2404 let key_id1 = Address::repeat_byte(0xAA);
2405 let key_id2 = Address::repeat_byte(0xBB);
2406
2407 let hash1a = AccountKeychain::spending_limit_key(account1, key_id1);
2409 let hash1b = AccountKeychain::spending_limit_key(account1, key_id1);
2410 assert_eq!(hash1a, hash1b, "Same inputs must produce same hash");
2411
2412 let hash2 = AccountKeychain::spending_limit_key(account2, key_id1);
2414 assert_ne!(
2415 hash1a, hash2,
2416 "Different accounts must produce different hashes"
2417 );
2418
2419 let hash3 = AccountKeychain::spending_limit_key(account1, key_id2);
2421 assert_ne!(
2422 hash1a, hash3,
2423 "Different key_ids must produce different hashes"
2424 );
2425
2426 let hash_swapped = AccountKeychain::spending_limit_key(key_id1, account1);
2429 assert_ne!(
2430 hash1a, hash_swapped,
2431 "Swapped order must produce different hash"
2432 );
2433
2434 assert_ne!(hash1a, B256::ZERO, "Hash should not be zero");
2436 }
2437
2438 #[test]
2439 fn test_initialize_sets_up_storage_state() -> eyre::Result<()> {
2440 let mut storage = HashMapStorageProvider::new(1);
2441 StorageCtx::enter(&mut storage, || {
2442 let mut keychain = AccountKeychain::new();
2443
2444 keychain.initialize()?;
2446
2447 keychain.set_transaction_key(Address::ZERO)?;
2449
2450 let account = Address::random();
2451 let key_id = Address::random();
2452 let auth_call = authorizeKeyCall {
2453 keyId: key_id,
2454 signatureType: SignatureType::Secp256k1,
2455 config: KeyRestrictions {
2456 expiry: u64::MAX,
2457 enforceLimits: false,
2458 limits: vec![],
2459 allowAnyCalls: true,
2460 allowedCalls: vec![],
2461 },
2462 };
2463 keychain.authorize_key(account, auth_call)?;
2465
2466 let key_info = keychain.get_key(getKeyCall {
2468 account,
2469 keyId: key_id,
2470 })?;
2471 assert_eq!(key_info.expiry, u64::MAX, "Key should be stored after init");
2472
2473 Ok(())
2474 })
2475 }
2476
2477 #[test]
2478 fn test_authorize_key_webauthn_signature_type() -> eyre::Result<()> {
2479 let mut storage = HashMapStorageProvider::new(1);
2480 let account = Address::random();
2481 let key_id = Address::random();
2482 StorageCtx::enter(&mut storage, || {
2483 let mut keychain = AccountKeychain::new();
2484 keychain.initialize()?;
2485 keychain.set_transaction_key(Address::ZERO)?;
2486
2487 let auth_call = authorizeKeyCall {
2489 keyId: key_id,
2490 signatureType: SignatureType::WebAuthn,
2491 config: KeyRestrictions {
2492 expiry: u64::MAX,
2493 enforceLimits: false,
2494 limits: vec![],
2495 allowAnyCalls: true,
2496 allowedCalls: vec![],
2497 },
2498 };
2499 keychain.authorize_key(account, auth_call)?;
2500
2501 let key_info = keychain.get_key(getKeyCall {
2503 account,
2504 keyId: key_id,
2505 })?;
2506 assert_eq!(
2507 key_info.signatureType,
2508 SignatureType::WebAuthn,
2509 "Signature type should be WebAuthn"
2510 );
2511
2512 let result = keychain.validate_keychain_authorization(account, key_id, 0, Some(2));
2514 assert!(
2515 result.is_ok(),
2516 "WebAuthn (type 2) validation should succeed"
2517 );
2518
2519 let mismatch = keychain.validate_keychain_authorization(account, key_id, 0, Some(0));
2521 assert!(mismatch.is_err(), "Secp256k1 should not match WebAuthn key");
2522
2523 Ok(())
2524 })
2525 }
2526
2527 #[test]
2528 fn test_update_spending_limit_expiry_boundary() -> eyre::Result<()> {
2529 let mut storage = HashMapStorageProvider::new(1);
2530 let account = Address::random();
2531 let key_id = Address::random();
2532 let token = Address::random();
2533 StorageCtx::enter(&mut storage, || {
2534 let mut keychain = AccountKeychain::new();
2535 keychain.initialize()?;
2536 keychain.set_transaction_key(Address::ZERO)?;
2537
2538 let auth_call = authorizeKeyCall {
2540 keyId: key_id,
2541 signatureType: SignatureType::Secp256k1,
2542 config: KeyRestrictions {
2543 expiry: u64::MAX,
2544 enforceLimits: true,
2545 limits: vec![TokenLimit {
2546 token,
2547 amount: U256::from(100),
2548 period: 0,
2549 }],
2550 allowAnyCalls: true,
2551 allowedCalls: vec![],
2552 },
2553 };
2554 keychain.authorize_key(account, auth_call)?;
2555
2556 let update_call = updateSpendingLimitCall {
2558 keyId: key_id,
2559 token,
2560 newLimit: U256::from(200),
2561 };
2562 let result = keychain.update_spending_limit(account, update_call);
2563 assert!(
2564 result.is_ok(),
2565 "Update should succeed when key not expired: {result:?}"
2566 );
2567
2568 let limit = keychain.get_remaining_limit(getRemainingLimitCall {
2570 account,
2571 keyId: key_id,
2572 token,
2573 })?;
2574 assert_eq!(limit, U256::from(200), "Limit should be updated to 200");
2575
2576 Ok(())
2577 })
2578 }
2579
2580 #[test]
2581 fn test_update_spending_limit_enforce_limits_toggle() -> eyre::Result<()> {
2582 let mut storage = HashMapStorageProvider::new(1);
2583 let account = Address::random();
2584 let key_id = Address::random();
2585 let token = Address::random();
2586 StorageCtx::enter(&mut storage, || {
2587 let mut keychain = AccountKeychain::new();
2588 keychain.initialize()?;
2589 keychain.set_transaction_key(Address::ZERO)?;
2590
2591 let auth_call = authorizeKeyCall {
2593 keyId: key_id,
2594 signatureType: SignatureType::Secp256k1,
2595 config: KeyRestrictions {
2596 expiry: u64::MAX,
2597 enforceLimits: false, limits: vec![],
2599 allowAnyCalls: true,
2600 allowedCalls: vec![],
2601 },
2602 };
2603 keychain.authorize_key(account, auth_call)?;
2604
2605 let key_before = keychain.get_key(getKeyCall {
2607 account,
2608 keyId: key_id,
2609 })?;
2610 assert!(
2611 !key_before.enforceLimits,
2612 "Key should start with enforce_limits=false"
2613 );
2614
2615 let update_call = updateSpendingLimitCall {
2617 keyId: key_id,
2618 token,
2619 newLimit: U256::from(500),
2620 };
2621 keychain.update_spending_limit(account, update_call)?;
2622
2623 let key_after = keychain.get_key(getKeyCall {
2625 account,
2626 keyId: key_id,
2627 })?;
2628 assert!(
2629 key_after.enforceLimits,
2630 "enforce_limits should be true after update"
2631 );
2632
2633 let limit = keychain.get_remaining_limit(getRemainingLimitCall {
2635 account,
2636 keyId: key_id,
2637 token,
2638 })?;
2639 assert_eq!(limit, U256::from(500), "Spending limit should be 500");
2640
2641 Ok(())
2642 })
2643 }
2644
2645 #[test]
2646 fn test_get_key_or_logic_existence_check() -> eyre::Result<()> {
2647 let mut storage = HashMapStorageProvider::new(1);
2648 let account = Address::random();
2649 let key_id_revoked = Address::random();
2650 let key_id_valid = Address::random();
2651 let key_id_never_existed = Address::random();
2652 StorageCtx::enter(&mut storage, || {
2653 let mut keychain = AccountKeychain::new();
2654 keychain.initialize()?;
2655 keychain.set_transaction_key(Address::ZERO)?;
2656
2657 let auth_call = authorizeKeyCall {
2659 keyId: key_id_revoked,
2660 signatureType: SignatureType::P256,
2661 config: KeyRestrictions {
2662 expiry: u64::MAX,
2663 enforceLimits: false,
2664 limits: vec![],
2665 allowAnyCalls: true,
2666 allowedCalls: vec![],
2667 },
2668 };
2669 keychain.authorize_key(account, auth_call)?;
2670 keychain.revoke_key(
2671 account,
2672 revokeKeyCall {
2673 keyId: key_id_revoked,
2674 },
2675 )?;
2676
2677 let auth_valid = authorizeKeyCall {
2679 keyId: key_id_valid,
2680 signatureType: SignatureType::Secp256k1,
2681 config: KeyRestrictions {
2682 expiry: u64::MAX,
2683 enforceLimits: false,
2684 limits: vec![],
2685 allowAnyCalls: true,
2686 allowedCalls: vec![],
2687 },
2688 };
2689 keychain.authorize_key(account, auth_valid)?;
2690
2691 let revoked_info = keychain.get_key(getKeyCall {
2693 account,
2694 keyId: key_id_revoked,
2695 })?;
2696 assert_eq!(
2697 revoked_info.keyId,
2698 Address::ZERO,
2699 "Revoked key should return zero keyId"
2700 );
2701 assert!(
2702 revoked_info.isRevoked,
2703 "Revoked key should have isRevoked=true"
2704 );
2705
2706 let never_info = keychain.get_key(getKeyCall {
2708 account,
2709 keyId: key_id_never_existed,
2710 })?;
2711 assert_eq!(
2712 never_info.keyId,
2713 Address::ZERO,
2714 "Non-existent key should return zero keyId"
2715 );
2716 assert_eq!(
2717 never_info.expiry, 0,
2718 "Non-existent key should have expiry=0"
2719 );
2720
2721 let valid_info = keychain.get_key(getKeyCall {
2723 account,
2724 keyId: key_id_valid,
2725 })?;
2726 assert_eq!(
2727 valid_info.keyId, key_id_valid,
2728 "Valid key should return actual keyId"
2729 );
2730 assert_eq!(
2731 valid_info.expiry,
2732 u64::MAX,
2733 "Valid key should have correct expiry"
2734 );
2735 assert!(!valid_info.isRevoked, "Valid key should not be revoked");
2736
2737 Ok(())
2738 })
2739 }
2740
2741 #[test]
2742 fn test_get_key_signature_type_match_arms() -> eyre::Result<()> {
2743 let mut storage = HashMapStorageProvider::new(1);
2744 let account = Address::random();
2745 let key_secp = Address::random();
2746 let key_p256 = Address::random();
2747 let key_webauthn = Address::random();
2748 StorageCtx::enter(&mut storage, || {
2749 let mut keychain = AccountKeychain::new();
2750 keychain.initialize()?;
2751 keychain.set_transaction_key(Address::ZERO)?;
2752
2753 keychain.authorize_key(
2755 account,
2756 authorizeKeyCall {
2757 keyId: key_secp,
2758 signatureType: SignatureType::Secp256k1, config: KeyRestrictions {
2760 expiry: u64::MAX,
2761 enforceLimits: false,
2762 limits: vec![],
2763 allowAnyCalls: true,
2764 allowedCalls: vec![],
2765 },
2766 },
2767 )?;
2768
2769 keychain.authorize_key(
2770 account,
2771 authorizeKeyCall {
2772 keyId: key_p256,
2773 signatureType: SignatureType::P256, config: KeyRestrictions {
2775 expiry: u64::MAX,
2776 enforceLimits: false,
2777 limits: vec![],
2778 allowAnyCalls: true,
2779 allowedCalls: vec![],
2780 },
2781 },
2782 )?;
2783
2784 keychain.authorize_key(
2785 account,
2786 authorizeKeyCall {
2787 keyId: key_webauthn,
2788 signatureType: SignatureType::WebAuthn, config: KeyRestrictions {
2790 expiry: u64::MAX,
2791 enforceLimits: false,
2792 limits: vec![],
2793 allowAnyCalls: true,
2794 allowedCalls: vec![],
2795 },
2796 },
2797 )?;
2798
2799 let secp_info = keychain.get_key(getKeyCall {
2801 account,
2802 keyId: key_secp,
2803 })?;
2804 assert_eq!(
2805 secp_info.signatureType,
2806 SignatureType::Secp256k1,
2807 "Secp256k1 key should return Secp256k1"
2808 );
2809
2810 let p256_info = keychain.get_key(getKeyCall {
2811 account,
2812 keyId: key_p256,
2813 })?;
2814 assert_eq!(
2815 p256_info.signatureType,
2816 SignatureType::P256,
2817 "P256 key should return P256"
2818 );
2819
2820 let webauthn_info = keychain.get_key(getKeyCall {
2821 account,
2822 keyId: key_webauthn,
2823 })?;
2824 assert_eq!(
2825 webauthn_info.signatureType,
2826 SignatureType::WebAuthn,
2827 "WebAuthn key should return WebAuthn"
2828 );
2829
2830 assert_ne!(secp_info.signatureType, p256_info.signatureType);
2832 assert_ne!(secp_info.signatureType, webauthn_info.signatureType);
2833 assert_ne!(p256_info.signatureType, webauthn_info.signatureType);
2834
2835 Ok(())
2836 })
2837 }
2838
2839 #[test]
2840 fn test_validate_keychain_authorization_checks_signature_type() -> eyre::Result<()> {
2841 let mut storage = HashMapStorageProvider::new(1);
2842 let account = Address::random();
2843 let key_id = Address::random();
2844 StorageCtx::enter(&mut storage, || {
2845 let mut keychain = AccountKeychain::new();
2846 keychain.initialize()?;
2847
2848 keychain.set_transaction_key(Address::ZERO)?;
2850
2851 let auth_call = authorizeKeyCall {
2853 keyId: key_id,
2854 signatureType: SignatureType::P256,
2855 config: KeyRestrictions {
2856 expiry: u64::MAX,
2857 enforceLimits: false,
2858 limits: vec![],
2859 allowAnyCalls: true,
2860 allowedCalls: vec![],
2861 },
2862 };
2863 keychain.authorize_key(account, auth_call)?;
2864
2865 let result = keychain.validate_keychain_authorization(account, key_id, 0, Some(1));
2867 assert!(
2868 result.is_ok(),
2869 "Validation should succeed with matching signature type"
2870 );
2871
2872 let mismatch_result =
2874 keychain.validate_keychain_authorization(account, key_id, 0, Some(0));
2875 assert!(
2876 mismatch_result.is_err(),
2877 "Validation should fail with mismatched signature type"
2878 );
2879 match mismatch_result.unwrap_err() {
2880 TempoPrecompileError::AccountKeychainError(e) => {
2881 assert!(
2882 matches!(e, AccountKeychainError::SignatureTypeMismatch(_)),
2883 "Expected SignatureTypeMismatch error, got: {e:?}"
2884 );
2885 }
2886 e => panic!("Expected AccountKeychainError, got: {e:?}"),
2887 }
2888
2889 let webauthn_mismatch =
2891 keychain.validate_keychain_authorization(account, key_id, 0, Some(2));
2892 assert!(
2893 webauthn_mismatch.is_err(),
2894 "Validation should fail with WebAuthn when key is P256"
2895 );
2896
2897 let none_result = keychain.validate_keychain_authorization(account, key_id, 0, None);
2899 assert!(
2900 none_result.is_ok(),
2901 "Validation should succeed when signature type check is skipped (pre-T1)"
2902 );
2903
2904 Ok(())
2905 })
2906 }
2907
2908 #[test]
2909 fn test_refund_spending_limit_restores_limit() -> eyre::Result<()> {
2910 let mut storage = HashMapStorageProvider::new(1);
2911 let eoa = Address::random();
2912 let access_key = Address::random();
2913 let token = Address::random();
2914
2915 StorageCtx::enter(&mut storage, || {
2916 let mut keychain = AccountKeychain::new();
2917 keychain.initialize()?;
2918
2919 keychain.set_transaction_key(Address::ZERO)?;
2920
2921 let auth_call = authorizeKeyCall {
2922 keyId: access_key,
2923 signatureType: SignatureType::Secp256k1,
2924 config: KeyRestrictions {
2925 expiry: u64::MAX,
2926 enforceLimits: true,
2927 limits: vec![TokenLimit {
2928 token,
2929 amount: U256::from(100),
2930 period: 0,
2931 }],
2932 allowAnyCalls: true,
2933 allowedCalls: vec![],
2934 },
2935 };
2936 keychain.authorize_key(eoa, auth_call)?;
2937
2938 keychain.set_transaction_key(access_key)?;
2939 keychain.set_tx_origin(eoa)?;
2940
2941 keychain.authorize_transfer(eoa, token, U256::from(60))?;
2942
2943 let remaining = keychain.get_remaining_limit(getRemainingLimitCall {
2944 account: eoa,
2945 keyId: access_key,
2946 token,
2947 })?;
2948 assert_eq!(remaining, U256::from(40));
2949
2950 keychain.refund_spending_limit(eoa, token, U256::from(25))?;
2951
2952 let after_refund = keychain.get_remaining_limit(getRemainingLimitCall {
2953 account: eoa,
2954 keyId: access_key,
2955 token,
2956 })?;
2957 assert_eq!(after_refund, U256::from(65));
2958
2959 Ok(())
2960 })
2961 }
2962
2963 #[test]
2964 fn test_refund_spending_limit_noop_for_main_key() -> eyre::Result<()> {
2965 let mut storage = HashMapStorageProvider::new(1);
2966 let eoa = Address::random();
2967 let token = Address::random();
2968
2969 StorageCtx::enter(&mut storage, || {
2970 let mut keychain = AccountKeychain::new();
2971 keychain.initialize()?;
2972
2973 keychain.set_transaction_key(Address::ZERO)?;
2974 keychain.set_tx_origin(eoa)?;
2975
2976 let result = keychain.refund_spending_limit(eoa, token, U256::from(50));
2977 assert!(result.is_ok());
2978
2979 Ok(())
2980 })
2981 }
2982
2983 #[test]
2984 fn test_refund_spending_limit_noop_after_key_revocation() -> eyre::Result<()> {
2985 let mut storage = HashMapStorageProvider::new(1);
2986 let eoa = Address::random();
2987 let access_key = Address::random();
2988 let token = Address::random();
2989
2990 StorageCtx::enter(&mut storage, || {
2991 let mut keychain = AccountKeychain::new();
2992 keychain.initialize()?;
2993
2994 keychain.set_transaction_key(Address::ZERO)?;
2995
2996 let auth_call = authorizeKeyCall {
2997 keyId: access_key,
2998 signatureType: SignatureType::Secp256k1,
2999 config: KeyRestrictions {
3000 expiry: u64::MAX,
3001 enforceLimits: true,
3002 limits: vec![TokenLimit {
3003 token,
3004 amount: U256::from(100),
3005 period: 0,
3006 }],
3007 allowAnyCalls: true,
3008 allowedCalls: vec![],
3009 },
3010 };
3011 keychain.authorize_key(eoa, auth_call)?;
3012
3013 keychain.set_transaction_key(access_key)?;
3014 keychain.set_tx_origin(eoa)?;
3015
3016 keychain.authorize_transfer(eoa, token, U256::from(60))?;
3017
3018 let remaining = keychain.get_remaining_limit(getRemainingLimitCall {
3019 account: eoa,
3020 keyId: access_key,
3021 token,
3022 })?;
3023 assert_eq!(remaining, U256::from(40));
3024
3025 keychain.set_transaction_key(Address::ZERO)?;
3026 keychain.revoke_key(eoa, revokeKeyCall { keyId: access_key })?;
3027
3028 keychain.set_transaction_key(access_key)?;
3029
3030 let result = keychain.refund_spending_limit(eoa, token, U256::from(25));
3031 assert!(result.is_ok());
3032
3033 let after_refund = keychain.get_remaining_limit(getRemainingLimitCall {
3034 account: eoa,
3035 keyId: access_key,
3036 token,
3037 })?;
3038 assert_eq!(
3039 after_refund,
3040 U256::from(40),
3041 "limit should be unchanged after revoked key refund"
3042 );
3043
3044 Ok(())
3045 })
3046 }
3047
3048 #[test]
3049 fn test_refund_spending_limit_noop_after_key_expiry() -> eyre::Result<()> {
3050 let mut storage = HashMapStorageProvider::new(1);
3051 let eoa = Address::random();
3052 let access_key = Address::random();
3053 let token = Address::random();
3054
3055 storage.set_timestamp(U256::from(100u64));
3056 StorageCtx::enter(&mut storage, || {
3057 let mut keychain = AccountKeychain::new();
3058 keychain.initialize()?;
3059
3060 keychain.set_transaction_key(Address::ZERO)?;
3061
3062 let auth_call = authorizeKeyCall {
3063 keyId: access_key,
3064 signatureType: SignatureType::Secp256k1,
3065 config: KeyRestrictions {
3066 expiry: 200,
3067 enforceLimits: true,
3068 limits: vec![TokenLimit {
3069 token,
3070 amount: U256::from(100),
3071 period: 0,
3072 }],
3073 allowAnyCalls: true,
3074 allowedCalls: vec![],
3075 },
3076 };
3077 keychain.authorize_key(eoa, auth_call)?;
3078
3079 keychain.set_transaction_key(access_key)?;
3080 keychain.set_tx_origin(eoa)?;
3081 keychain.authorize_transfer(eoa, token, U256::from(60))?;
3082
3083 Ok::<_, eyre::Report>(())
3084 })?;
3085
3086 storage.set_timestamp(U256::from(200u64));
3087 StorageCtx::enter(&mut storage, || {
3088 let mut keychain = AccountKeychain::new();
3089 keychain.set_transaction_key(access_key)?;
3090 keychain.set_tx_origin(eoa)?;
3091
3092 let result = keychain.refund_spending_limit(eoa, token, U256::from(25));
3093 assert!(result.is_ok());
3094
3095 let after_refund = keychain.get_remaining_limit(getRemainingLimitCall {
3096 account: eoa,
3097 keyId: access_key,
3098 token,
3099 })?;
3100 assert_eq!(
3101 after_refund,
3102 U256::from(40),
3103 "limit should be unchanged after expired key refund"
3104 );
3105
3106 Ok(())
3107 })
3108 }
3109
3110 #[test]
3111 fn test_refund_spending_limit_propagates_system_errors() -> eyre::Result<()> {
3112 let mut storage = HashMapStorageProvider::new(1);
3113 let eoa = Address::random();
3114 let access_key = Address::random();
3115 let token = Address::random();
3116
3117 let key_slot = StorageCtx::enter(&mut storage, || {
3118 let mut keychain = AccountKeychain::new();
3119 keychain.initialize()?;
3120
3121 keychain.set_transaction_key(Address::ZERO)?;
3122
3123 let auth_call = authorizeKeyCall {
3124 keyId: access_key,
3125 signatureType: SignatureType::Secp256k1,
3126 config: KeyRestrictions {
3127 expiry: u64::MAX,
3128 enforceLimits: true,
3129 limits: vec![TokenLimit {
3130 token,
3131 amount: U256::from(100),
3132 period: 0,
3133 }],
3134 allowAnyCalls: true,
3135 allowedCalls: vec![],
3136 },
3137 };
3138 keychain.authorize_key(eoa, auth_call)?;
3139
3140 keychain.set_transaction_key(access_key)?;
3141 keychain.set_tx_origin(eoa)?;
3142 keychain.authorize_transfer(eoa, token, U256::from(60))?;
3143
3144 Ok::<_, TempoPrecompileError>(keychain.keys[eoa][access_key].as_slot().slot())
3145 })?;
3146
3147 storage.fail_next_sload_at(ACCOUNT_KEYCHAIN_ADDRESS, key_slot);
3148
3149 StorageCtx::enter(&mut storage, || {
3150 let mut keychain = AccountKeychain::new();
3151 keychain.set_transaction_key(access_key)?;
3152 keychain.set_tx_origin(eoa)?;
3153
3154 let err = keychain
3155 .refund_spending_limit(eoa, token, U256::from(25))
3156 .unwrap_err();
3157
3158 assert!(matches!(err, TempoPrecompileError::Fatal(_)));
3159
3160 Ok(())
3161 })
3162 }
3163
3164 #[test]
3165 fn test_refund_spending_limit_clamped_by_saturating_add() -> eyre::Result<()> {
3166 let mut storage = HashMapStorageProvider::new(1);
3167 let eoa = Address::random();
3168 let access_key = Address::random();
3169 let token = Address::random();
3170 let original_limit = U256::from(100);
3171
3172 StorageCtx::enter(&mut storage, || {
3173 let mut keychain = AccountKeychain::new();
3174 keychain.initialize()?;
3175
3176 keychain.set_transaction_key(Address::ZERO)?;
3177
3178 let auth_call = authorizeKeyCall {
3179 keyId: access_key,
3180 signatureType: SignatureType::Secp256k1,
3181 config: KeyRestrictions {
3182 expiry: u64::MAX,
3183 enforceLimits: true,
3184 limits: vec![TokenLimit {
3185 token,
3186 amount: original_limit,
3187 period: 0,
3188 }],
3189 allowAnyCalls: true,
3190 allowedCalls: vec![],
3191 },
3192 };
3193 keychain.authorize_key(eoa, auth_call)?;
3194
3195 keychain.set_transaction_key(access_key)?;
3196 keychain.set_tx_origin(eoa)?;
3197
3198 keychain.authorize_transfer(eoa, token, U256::from(10))?;
3199
3200 let remaining = keychain.get_remaining_limit(getRemainingLimitCall {
3201 account: eoa,
3202 keyId: access_key,
3203 token,
3204 })?;
3205 assert_eq!(remaining, U256::from(90));
3206
3207 keychain.refund_spending_limit(eoa, token, U256::from(50))?;
3208
3209 let after_refund = keychain.get_remaining_limit(getRemainingLimitCall {
3210 account: eoa,
3211 keyId: access_key,
3212 token,
3213 })?;
3214 assert_eq!(
3215 after_refund,
3216 U256::from(140),
3217 "saturating_add should allow refund beyond original limit without overflow"
3218 );
3219
3220 Ok(())
3221 })
3222 }
3223
3224 #[test]
3225 fn test_t3_refund_spending_limit_clamps_to_max() -> eyre::Result<()> {
3226 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
3227 let eoa = Address::random();
3228 let access_key = Address::random();
3229 let token = Address::random();
3230 let original_limit = U256::from(100);
3231
3232 StorageCtx::enter(&mut storage, || {
3233 let mut keychain = AccountKeychain::new();
3234 keychain.initialize()?;
3235
3236 keychain.set_transaction_key(Address::ZERO)?;
3237 keychain.set_tx_origin(eoa)?;
3238
3239 let auth_call = authorizeKeyCall {
3240 keyId: access_key,
3241 signatureType: SignatureType::Secp256k1,
3242 config: KeyRestrictions {
3243 expiry: u64::MAX,
3244 enforceLimits: true,
3245 limits: vec![TokenLimit {
3246 token,
3247 amount: original_limit,
3248 period: 0,
3249 }],
3250 allowAnyCalls: true,
3251 allowedCalls: vec![],
3252 },
3253 };
3254 keychain.authorize_key(eoa, auth_call)?;
3255
3256 keychain.set_transaction_key(access_key)?;
3257 keychain.set_tx_origin(eoa)?;
3258
3259 keychain.authorize_transfer(eoa, token, U256::from(60))?;
3260 keychain.refund_spending_limit(eoa, token, U256::from(30))?;
3261
3262 let after_partial_refund = keychain.get_remaining_limit(getRemainingLimitCall {
3263 account: eoa,
3264 keyId: access_key,
3265 token,
3266 })?;
3267 assert_eq!(
3268 after_partial_refund,
3269 U256::from(70),
3270 "refund should restore the spent amount without forcing the max"
3271 );
3272
3273 keychain.refund_spending_limit(eoa, token, U256::from(50))?;
3274
3275 let after_refund = keychain.get_remaining_limit(getRemainingLimitCall {
3276 account: eoa,
3277 keyId: access_key,
3278 token,
3279 })?;
3280 assert_eq!(
3281 after_refund, original_limit,
3282 "refund should not restore more than the configured max"
3283 );
3284
3285 Ok(())
3286 })
3287 }
3288
3289 #[test]
3290 fn test_t3_refund_spending_limit_preserves_legacy_rows_without_max() -> eyre::Result<()> {
3291 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
3292 let eoa = Address::random();
3293 let access_key = Address::random();
3294 let token = Address::random();
3295
3296 StorageCtx::enter(&mut storage, || {
3297 let mut keychain = AccountKeychain::new();
3298 keychain.initialize()?;
3299
3300 let limit_key = AccountKeychain::spending_limit_key(eoa, access_key);
3301 keychain.keys[eoa][access_key].write(AuthorizedKey {
3302 signature_type: SignatureType::Secp256k1 as u8,
3303 expiry: u64::MAX,
3304 enforce_limits: true,
3305 is_revoked: false,
3306 })?;
3307 keychain.spending_limits[limit_key][token].write(SpendingLimitState {
3308 remaining: U256::from(90),
3309 max: 0,
3310 period: 0,
3311 period_end: 0,
3312 })?;
3313
3314 keychain.set_transaction_key(access_key)?;
3315 keychain.set_tx_origin(eoa)?;
3316 keychain.refund_spending_limit(eoa, token, U256::from(10))?;
3317
3318 let after_refund = keychain.get_remaining_limit(getRemainingLimitCall {
3319 account: eoa,
3320 keyId: access_key,
3321 token,
3322 })?;
3323 assert_eq!(
3324 after_refund,
3325 U256::from(100),
3326 "migrated pre-T3 rows should keep legacy saturating-add refund semantics"
3327 );
3328
3329 Ok(())
3330 })
3331 }
3332
3333 #[test]
3334 fn test_t3_authorize_key_ignores_limits_when_enforce_limits_false() -> eyre::Result<()> {
3335 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
3336 let account = Address::random();
3337 let key_id = Address::random();
3338 let token = Address::random();
3339
3340 StorageCtx::enter(&mut storage, || {
3341 let mut keychain = AccountKeychain::new();
3342 keychain.initialize()?;
3343 keychain.set_transaction_key(Address::ZERO)?;
3344 keychain.set_tx_origin(account)?;
3345
3346 keychain.authorize_key(
3347 account,
3348 authorizeKeyCall {
3349 keyId: key_id,
3350 signatureType: SignatureType::Secp256k1,
3351 config: KeyRestrictions {
3352 expiry: u64::MAX,
3353 enforceLimits: false,
3354 limits: vec![TokenLimit {
3355 token,
3356 amount: U256::from(100),
3357 period: 60,
3358 }],
3359 allowAnyCalls: true,
3360 allowedCalls: vec![],
3361 },
3362 },
3363 )?;
3364
3365 let limit_key = AccountKeychain::spending_limit_key(account, key_id);
3366 assert_eq!(
3367 keychain.spending_limits[limit_key][token].read()?,
3368 SpendingLimitState::default()
3369 );
3370
3371 let remaining =
3372 keychain.get_remaining_limit_with_period(getRemainingLimitWithPeriodCall {
3373 account,
3374 keyId: key_id,
3375 token,
3376 })?;
3377 assert_eq!(remaining.remaining, U256::ZERO);
3378 assert_eq!(remaining.periodEnd, 0);
3379
3380 Ok(())
3381 })
3382 }
3383
3384 #[test]
3385 fn test_t3_rejects_spending_limits_above_u128() -> eyre::Result<()> {
3386 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
3387 let account = Address::random();
3388 let invalid_key_id = Address::random();
3389 let valid_key_id = Address::random();
3390 let token = Address::random();
3391 let oversized_limit = U256::from(u128::MAX) + U256::from(1u8);
3392
3393 StorageCtx::enter(&mut storage, || {
3394 let mut keychain = AccountKeychain::new();
3395 keychain.initialize()?;
3396 keychain.set_transaction_key(Address::ZERO)?;
3397 keychain.set_tx_origin(account)?;
3398
3399 let authorize_result = keychain.authorize_key(
3400 account,
3401 authorizeKeyCall {
3402 keyId: invalid_key_id,
3403 signatureType: SignatureType::Secp256k1,
3404 config: KeyRestrictions {
3405 expiry: u64::MAX,
3406 enforceLimits: true,
3407 limits: vec![TokenLimit {
3408 token,
3409 amount: oversized_limit,
3410 period: 60,
3411 }],
3412 allowAnyCalls: true,
3413 allowedCalls: vec![],
3414 },
3415 },
3416 );
3417
3418 assert!(
3419 matches!(
3420 authorize_result,
3421 Err(TempoPrecompileError::AccountKeychainError(
3422 AccountKeychainError::InvalidSpendingLimit(_)
3423 ))
3424 ),
3425 "expected InvalidSpendingLimit, got {authorize_result:?}"
3426 );
3427
3428 keychain.authorize_key(
3429 account,
3430 authorizeKeyCall {
3431 keyId: valid_key_id,
3432 signatureType: SignatureType::Secp256k1,
3433 config: KeyRestrictions {
3434 expiry: u64::MAX,
3435 enforceLimits: true,
3436 limits: vec![TokenLimit {
3437 token,
3438 amount: U256::from(100u64),
3439 period: 60,
3440 }],
3441 allowAnyCalls: true,
3442 allowedCalls: vec![],
3443 },
3444 },
3445 )?;
3446
3447 let update_result = keychain.update_spending_limit(
3448 account,
3449 updateSpendingLimitCall {
3450 keyId: valid_key_id,
3451 token,
3452 newLimit: oversized_limit,
3453 },
3454 );
3455
3456 assert!(
3457 matches!(
3458 update_result,
3459 Err(TempoPrecompileError::AccountKeychainError(
3460 AccountKeychainError::InvalidSpendingLimit(_)
3461 ))
3462 ),
3463 "expected InvalidSpendingLimit, got {update_result:?}"
3464 );
3465
3466 Ok(())
3467 })
3468 }
3469
3470 #[test]
3471 fn test_t3_rejects_duplicate_token_limits() -> eyre::Result<()> {
3472 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
3473 let account = Address::random();
3474 let key_id = Address::random();
3475 let token = Address::random();
3476
3477 StorageCtx::enter(&mut storage, || {
3478 let mut keychain = AccountKeychain::new();
3479 keychain.initialize()?;
3480 keychain.set_transaction_key(Address::ZERO)?;
3481 keychain.set_tx_origin(account)?;
3482
3483 let result = keychain.authorize_key(
3484 account,
3485 authorizeKeyCall {
3486 keyId: key_id,
3487 signatureType: SignatureType::Secp256k1,
3488 config: KeyRestrictions {
3489 expiry: u64::MAX,
3490 enforceLimits: true,
3491 limits: vec![
3492 TokenLimit {
3493 token,
3494 amount: U256::from(100_u64),
3495 period: 0,
3496 },
3497 TokenLimit {
3498 token,
3499 amount: U256::from(200_u64),
3500 period: 60,
3501 },
3502 ],
3503 allowAnyCalls: true,
3504 allowedCalls: vec![],
3505 },
3506 },
3507 );
3508
3509 assert!(
3510 matches!(
3511 result,
3512 Err(TempoPrecompileError::AccountKeychainError(
3513 AccountKeychainError::InvalidSpendingLimit(_)
3514 ))
3515 ),
3516 "expected duplicate token limits to be rejected, got: {result:?}"
3517 );
3518
3519 let stored_key = keychain.keys[account][key_id].read()?;
3520 assert_eq!(
3521 stored_key.expiry, 0,
3522 "duplicate rejection must not persist the key"
3523 );
3524
3525 Ok(())
3526 })
3527 }
3528
3529 #[test]
3530 fn test_spending_limit_state_preserves_legacy_remaining_slot() -> eyre::Result<()> {
3531 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
3532 let account = Address::random();
3533 let key_id = Address::random();
3534 let token = Address::random();
3535
3536 StorageCtx::enter(&mut storage, || {
3537 let mut keychain = AccountKeychain::new();
3538 keychain.initialize()?;
3539
3540 let limit_key = AccountKeychain::spending_limit_key(account, key_id);
3541 let handler = &mut keychain.spending_limits[limit_key][token];
3542 let remaining = U256::from(123u64);
3543 handler.write(SpendingLimitState {
3544 remaining,
3545 max: 456,
3546 period: 60,
3547 period_end: 120,
3548 })?;
3549
3550 assert_eq!(
3551 StorageCtx.sload(ACCOUNT_KEYCHAIN_ADDRESS, handler.as_slot().slot())?,
3552 remaining
3553 );
3554
3555 Ok(())
3556 })
3557 }
3558
3559 #[test]
3560 fn test_t3_rejects_recipient_constrained_scope_for_undeployed_tip20() -> eyre::Result<()> {
3561 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
3562 let account = Address::random();
3563 let key_id = Address::random();
3564 let recipient = Address::repeat_byte(0x44);
3565 let mut target_bytes = [0u8; 20];
3566 target_bytes[0] = 0x20;
3567 target_bytes[1] = 0xc0;
3568 target_bytes[19] = 0x42;
3569 let undeployed_tip20 = Address::from(target_bytes);
3570
3571 StorageCtx::enter(&mut storage, || {
3572 let mut keychain = AccountKeychain::new();
3573 keychain.initialize()?;
3574 keychain.set_transaction_key(Address::ZERO)?;
3575 keychain.set_tx_origin(account)?;
3576
3577 keychain.authorize_key(
3578 account,
3579 authorizeKeyCall {
3580 keyId: key_id,
3581 signatureType: SignatureType::Secp256k1,
3582 config: KeyRestrictions {
3583 expiry: u64::MAX,
3584 enforceLimits: false,
3585 limits: vec![],
3586 allowAnyCalls: true,
3587 allowedCalls: vec![],
3588 },
3589 },
3590 )?;
3591
3592 let err = keychain
3593 .apply_key_authorization_restrictions(
3594 account,
3595 key_id,
3596 &[],
3597 Some(&[CallScope {
3598 target: undeployed_tip20,
3599 selectorRules: vec![SelectorRule {
3600 selector: TIP20_TRANSFER_SELECTOR.into(),
3601 recipients: vec![recipient],
3602 }],
3603 }]),
3604 )
3605 .expect_err("unexpected success for undeployed TIP-20 target");
3606
3607 match err {
3608 TempoPrecompileError::AccountKeychainError(
3609 AccountKeychainError::InvalidCallScope(_),
3610 ) => {}
3611 other => panic!("expected InvalidCallScope, got {other:?}"),
3612 }
3613
3614 Ok(())
3615 })
3616 }
3617
3618 #[test]
3619 fn test_t3_periodic_limit_rollover() -> eyre::Result<()> {
3620 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
3621 storage.set_timestamp(U256::from(1_000u64));
3622
3623 let account = Address::random();
3624 let key_id = Address::random();
3625 let token = Address::random();
3626
3627 StorageCtx::enter(&mut storage, || {
3628 let mut keychain = AccountKeychain::new();
3629 keychain.initialize()?;
3630 keychain.set_transaction_key(Address::ZERO)?;
3631 keychain.set_tx_origin(account)?;
3632 TIP20Setup::path_usd(account).apply()?;
3633
3634 keychain.authorize_key(
3635 account,
3636 authorizeKeyCall {
3637 keyId: key_id,
3638 signatureType: SignatureType::Secp256k1,
3639 config: KeyRestrictions {
3640 expiry: u64::MAX,
3641 enforceLimits: true,
3642 limits: vec![TokenLimit {
3643 token,
3644 amount: U256::from(100),
3645 period: 0,
3646 }],
3647 allowAnyCalls: true,
3648 allowedCalls: vec![],
3649 },
3650 },
3651 )?;
3652
3653 keychain.apply_key_authorization_restrictions(
3654 account,
3655 key_id,
3656 &[TokenLimit {
3657 token,
3658 amount: U256::from(100),
3659 period: 60,
3660 }],
3661 None,
3662 )?;
3663
3664 keychain.set_transaction_key(key_id)?;
3665 keychain.authorize_transfer(account, token, U256::from(80))?;
3666
3667 let remaining = keychain.get_remaining_limit(getRemainingLimitCall {
3668 account,
3669 keyId: key_id,
3670 token,
3671 })?;
3672 assert_eq!(remaining, U256::from(20));
3673
3674 Ok::<_, eyre::Report>(())
3675 })?;
3676
3677 storage.set_timestamp(U256::from(1_070u64));
3678 StorageCtx::enter(&mut storage, || {
3679 let mut keychain = AccountKeychain::new();
3680 keychain.set_transaction_key(key_id)?;
3681 keychain.set_tx_origin(account)?;
3682
3683 keychain.authorize_transfer(account, token, U256::from(10))?;
3684
3685 let remaining = keychain.get_remaining_limit(getRemainingLimitCall {
3686 account,
3687 keyId: key_id,
3688 token,
3689 })?;
3690 assert_eq!(remaining, U256::from(90));
3691 Ok(())
3692 })
3693 }
3694
3695 #[test]
3696 fn test_t3_get_allowed_calls_distinguishes_unrestricted_and_deny_all() -> eyre::Result<()> {
3697 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
3698 let account = Address::random();
3699 let key_id = Address::random();
3700
3701 StorageCtx::enter(&mut storage, || {
3702 let mut keychain = AccountKeychain::new();
3703 keychain.initialize()?;
3704 keychain.set_transaction_key(Address::ZERO)?;
3705 keychain.set_tx_origin(account)?;
3706
3707 keychain.authorize_key(
3708 account,
3709 authorizeKeyCall {
3710 keyId: key_id,
3711 signatureType: SignatureType::Secp256k1,
3712 config: KeyRestrictions {
3713 expiry: u64::MAX,
3714 enforceLimits: false,
3715 limits: vec![],
3716 allowAnyCalls: true,
3717 allowedCalls: vec![],
3718 },
3719 },
3720 )?;
3721
3722 let scopes = keychain.get_allowed_calls(getAllowedCallsCall {
3723 account,
3724 keyId: key_id,
3725 })?;
3726 assert!(!scopes.isScoped);
3727 assert!(scopes.scopes.is_empty());
3728
3729 keychain.apply_key_authorization_restrictions(account, key_id, &[], Some(&[]))?;
3730
3731 let deny_all = keychain.get_allowed_calls(getAllowedCallsCall {
3732 account,
3733 keyId: key_id,
3734 })?;
3735 assert!(deny_all.isScoped);
3736 assert!(deny_all.scopes.is_empty());
3737
3738 Ok(())
3739 })
3740 }
3741
3742 #[test]
3743 fn test_t3_get_allowed_calls_returns_deny_all_for_inactive_keys() -> eyre::Result<()> {
3744 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
3745 let account = Address::random();
3746 let revoked_key = Address::random();
3747 let expiring_key = Address::random();
3748 let target = DEFAULT_FEE_TOKEN;
3749
3750 storage.set_timestamp(U256::from(1_000u64));
3751 StorageCtx::enter(&mut storage, || {
3752 let mut keychain = AccountKeychain::new();
3753 keychain.initialize()?;
3754 keychain.set_transaction_key(Address::ZERO)?;
3755 keychain.set_tx_origin(account)?;
3756
3757 for (key_id, expiry) in [(revoked_key, u64::MAX), (expiring_key, 1_005)] {
3758 keychain.authorize_key(
3759 account,
3760 authorizeKeyCall {
3761 keyId: key_id,
3762 signatureType: SignatureType::Secp256k1,
3763 config: KeyRestrictions {
3764 expiry,
3765 enforceLimits: false,
3766 limits: vec![],
3767 allowAnyCalls: false,
3768 allowedCalls: vec![CallScope {
3769 target,
3770 selectorRules: vec![],
3771 }],
3772 },
3773 },
3774 )?;
3775 }
3776
3777 keychain.revoke_key(account, revokeKeyCall { keyId: revoked_key })?;
3778
3779 let revoked = keychain.get_allowed_calls(getAllowedCallsCall {
3780 account,
3781 keyId: revoked_key,
3782 })?;
3783 assert!(revoked.isScoped);
3784 assert!(revoked.scopes.is_empty());
3785
3786 let root = keychain.get_allowed_calls(getAllowedCallsCall {
3787 account,
3788 keyId: Address::ZERO,
3789 })?;
3790 assert!(!root.isScoped);
3791 assert!(root.scopes.is_empty());
3792
3793 Ok::<_, eyre::Report>(())
3794 })?;
3795
3796 storage.set_timestamp(U256::from(1_010u64));
3797 StorageCtx::enter(&mut storage, || {
3798 let keychain = AccountKeychain::new();
3799
3800 let expired = keychain.get_allowed_calls(getAllowedCallsCall {
3801 account,
3802 keyId: expiring_key,
3803 })?;
3804 assert!(expired.isScoped);
3805 assert!(expired.scopes.is_empty());
3806
3807 Ok(())
3808 })
3809 }
3810
3811 #[test]
3812 fn test_expired_key_has_zero_remaining_limit() -> eyre::Result<()> {
3813 for hardfork in [TempoHardfork::T0, TempoHardfork::T2, TempoHardfork::T3] {
3814 let mut storage = HashMapStorageProvider::new_with_spec(1, hardfork);
3815 let account = Address::random();
3816 let key_id = Address::random();
3817 let token = Address::random();
3818
3819 storage.set_timestamp(U256::from(1_000u64));
3820 StorageCtx::enter(&mut storage, || {
3821 let mut keychain = AccountKeychain::new();
3822 keychain.initialize()?;
3823 keychain.set_transaction_key(Address::ZERO)?;
3824 keychain.set_tx_origin(account)?;
3825
3826 keychain.authorize_key(
3827 account,
3828 authorizeKeyCall {
3829 keyId: key_id,
3830 signatureType: SignatureType::Secp256k1,
3831 config: KeyRestrictions {
3832 expiry: 1_005,
3833 enforceLimits: true,
3834 limits: vec![TokenLimit {
3835 token,
3836 amount: U256::from(100u64),
3837 period: 0,
3838 }],
3839 allowAnyCalls: true,
3840 allowedCalls: vec![],
3841 },
3842 },
3843 )?;
3844
3845 Ok::<_, eyre::Report>(())
3846 })?;
3847
3848 storage.set_timestamp(U256::from(1_010u64));
3850
3851 StorageCtx::enter(&mut storage, || {
3852 let keychain = AccountKeychain::new();
3853
3854 let sload_before = StorageCtx.counter_sload();
3855 if hardfork.is_t3() {
3856 let remaining = keychain.get_remaining_limit_with_period(
3858 getRemainingLimitWithPeriodCall {
3859 account,
3860 keyId: key_id,
3861 token,
3862 },
3863 )?;
3864 assert_eq!(remaining.remaining, U256::ZERO);
3865 assert_eq!(remaining.periodEnd, 0);
3866
3867 assert_eq!(StorageCtx.counter_sload() - sload_before, 1);
3869 } else {
3870 let remaining = keychain.get_remaining_limit(getRemainingLimitCall {
3872 account,
3873 keyId: key_id,
3874 token,
3875 })?;
3876 assert_eq!(remaining, U256::from(100u64));
3877
3878 let expected_delta = if hardfork.is_t2() { 2 } else { 1 };
3880 assert_eq!(StorageCtx.counter_sload() - sload_before, expected_delta);
3881 }
3882
3883 Ok::<_, eyre::Report>(())
3884 })?;
3885 }
3886
3887 Ok(())
3888 }
3889
3890 #[test]
3891 fn test_revoked_key_has_zero_remaining_limit() -> eyre::Result<()> {
3892 for hardfork in [TempoHardfork::T0, TempoHardfork::T2, TempoHardfork::T3] {
3893 let mut storage = HashMapStorageProvider::new_with_spec(1, hardfork);
3894 let account = Address::random();
3895 let key_id = Address::random();
3896 let token = Address::random();
3897
3898 StorageCtx::enter(&mut storage, || {
3899 let mut keychain = AccountKeychain::new();
3900 keychain.initialize()?;
3901 keychain.set_transaction_key(Address::ZERO)?;
3902 keychain.set_tx_origin(account)?;
3903
3904 keychain.authorize_key(
3905 account,
3906 authorizeKeyCall {
3907 keyId: key_id,
3908 signatureType: SignatureType::Secp256k1,
3909 config: KeyRestrictions {
3910 expiry: u64::MAX,
3911 enforceLimits: true,
3912 limits: vec![TokenLimit {
3913 token,
3914 amount: U256::from(100u64),
3915 period: 0,
3916 }],
3917 allowAnyCalls: true,
3918 allowedCalls: vec![],
3919 },
3920 },
3921 )?;
3922
3923 keychain.revoke_key(account, revokeKeyCall { keyId: key_id })?;
3925
3926 let sload_before = StorageCtx.counter_sload();
3927 if hardfork.is_t2() {
3928 let remaining = keychain.get_remaining_limit_with_period(
3930 getRemainingLimitWithPeriodCall {
3931 account,
3932 keyId: key_id,
3933 token,
3934 },
3935 )?;
3936 assert_eq!(remaining.remaining, U256::ZERO);
3937 assert_eq!(remaining.periodEnd, 0);
3938
3939 assert_eq!(StorageCtx.counter_sload() - sload_before, 1);
3941 } else {
3942 let remaining = keychain.get_remaining_limit(getRemainingLimitCall {
3944 account,
3945 keyId: key_id,
3946 token,
3947 })?;
3948 assert_eq!(remaining, U256::from(100u64));
3949
3950 assert_eq!(StorageCtx.counter_sload() - sload_before, 1);
3952 }
3953
3954 Ok::<_, eyre::Report>(())
3955 })?;
3956 }
3957
3958 Ok(())
3959 }
3960
3961 #[test]
3962 fn test_zero_key_remaining_limit_reads_storage_on_t2_but_not_t3() -> eyre::Result<()> {
3963 let (account, token) = (Address::random(), Address::random());
3964
3965 for (hardfork, expected_sloads) in [(TempoHardfork::T2, 1_u64), (TempoHardfork::T3, 0)] {
3966 let mut storage = HashMapStorageProvider::new_with_spec(1, hardfork);
3967 StorageCtx::enter(&mut storage, || {
3968 let mut keychain = AccountKeychain::new();
3969 let _ = keychain.initialize();
3970
3971 let sloads_before = StorageCtx.counter_sload();
3972 assert_eq!(
3973 keychain.get_remaining_limit(getRemainingLimitCall {
3974 account,
3975 keyId: Address::ZERO,
3976 token,
3977 })?,
3978 U256::ZERO
3979 );
3980
3981 assert_eq!(
3982 StorageCtx.counter_sload() - sloads_before,
3983 expected_sloads,
3984 "{hardfork:?} should perform the expected number of storage reads for zero key_id"
3985 );
3986
3987 Ok::<_, eyre::Report>(())
3988 })?;
3989 }
3990
3991 Ok(())
3992 }
3993
3994 #[test]
3995 fn test_t3_set_allowed_calls_rejects_zero_target() -> eyre::Result<()> {
3996 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
3997 let account = Address::random();
3998 let key_id = Address::random();
3999
4000 StorageCtx::enter(&mut storage, || {
4001 let mut keychain = AccountKeychain::new();
4002 keychain.initialize()?;
4003 keychain.set_transaction_key(Address::ZERO)?;
4004 keychain.set_tx_origin(account)?;
4005
4006 keychain.authorize_key(
4007 account,
4008 authorizeKeyCall {
4009 keyId: key_id,
4010 signatureType: SignatureType::Secp256k1,
4011 config: KeyRestrictions {
4012 expiry: u64::MAX,
4013 enforceLimits: false,
4014 limits: vec![],
4015 allowAnyCalls: true,
4016 allowedCalls: vec![],
4017 },
4018 },
4019 )?;
4020
4021 let err = keychain
4022 .set_allowed_calls(
4023 account,
4024 setAllowedCallsCall {
4025 keyId: key_id,
4026 scopes: vec![CallScope {
4027 target: Address::ZERO,
4028 selectorRules: vec![],
4029 }],
4030 },
4031 )
4032 .expect_err("unexpected success for zero target scope");
4033 assert_invalid_call_scope(err);
4034
4035 Ok(())
4036 })
4037 }
4038
4039 #[test]
4040 fn test_t3_set_allowed_calls_rejects_empty_scope_batch() -> eyre::Result<()> {
4041 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
4042 let account = Address::random();
4043 let key_id = Address::random();
4044
4045 StorageCtx::enter(&mut storage, || {
4046 let mut keychain = AccountKeychain::new();
4047 keychain.initialize()?;
4048 keychain.set_transaction_key(Address::ZERO)?;
4049 keychain.set_tx_origin(account)?;
4050
4051 keychain.authorize_key(
4052 account,
4053 authorizeKeyCall {
4054 keyId: key_id,
4055 signatureType: SignatureType::Secp256k1,
4056 config: KeyRestrictions {
4057 expiry: u64::MAX,
4058 enforceLimits: false,
4059 limits: vec![],
4060 allowAnyCalls: true,
4061 allowedCalls: vec![],
4062 },
4063 },
4064 )?;
4065
4066 let err = keychain
4067 .set_allowed_calls(
4068 account,
4069 setAllowedCallsCall {
4070 keyId: key_id,
4071 scopes: vec![],
4072 },
4073 )
4074 .expect_err("unexpected success for empty scope batch");
4075 assert_invalid_call_scope(err);
4076
4077 let scopes = keychain.get_allowed_calls(getAllowedCallsCall {
4078 account,
4079 keyId: key_id,
4080 })?;
4081 assert!(!scopes.isScoped);
4082 assert!(scopes.scopes.is_empty());
4083
4084 Ok(())
4085 })
4086 }
4087
4088 #[test]
4089 fn test_t3_set_allowed_calls_roundtrip_and_remove_target_scope() -> eyre::Result<()> {
4090 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
4091 let account = Address::random();
4092 let key_id = Address::random();
4093 let target = Address::random();
4094
4095 StorageCtx::enter(&mut storage, || {
4096 let mut keychain = AccountKeychain::new();
4097 keychain.initialize()?;
4098 keychain.set_transaction_key(Address::ZERO)?;
4099 keychain.set_tx_origin(account)?;
4100
4101 keychain.authorize_key(
4102 account,
4103 authorizeKeyCall {
4104 keyId: key_id,
4105 signatureType: SignatureType::Secp256k1,
4106 config: KeyRestrictions {
4107 expiry: u64::MAX,
4108 enforceLimits: false,
4109 limits: vec![],
4110 allowAnyCalls: true,
4111 allowedCalls: vec![],
4112 },
4113 },
4114 )?;
4115
4116 keychain.set_allowed_calls(
4117 account,
4118 setAllowedCallsCall {
4119 keyId: key_id,
4120 scopes: vec![CallScope {
4121 target,
4122 selectorRules: vec![SelectorRule {
4123 selector: TIP20_TRANSFER_SELECTOR.into(),
4124 recipients: vec![],
4125 }],
4126 }],
4127 },
4128 )?;
4129
4130 let scopes = keychain.get_allowed_calls(getAllowedCallsCall {
4131 account,
4132 keyId: key_id,
4133 })?;
4134 assert!(scopes.isScoped);
4135 assert_eq!(scopes.scopes.len(), 1);
4136 assert_eq!(scopes.scopes[0].target, target);
4137 assert_eq!(scopes.scopes[0].selectorRules.len(), 1);
4138 assert_eq!(
4139 *scopes.scopes[0].selectorRules[0].selector,
4140 TIP20_TRANSFER_SELECTOR
4141 );
4142 assert!(scopes.scopes[0].selectorRules[0].recipients.is_empty());
4143
4144 let allow = keychain.validate_call_scope_for_transaction(
4145 account,
4146 key_id,
4147 &TxKind::Call(target),
4148 &TIP20_TRANSFER_SELECTOR,
4149 );
4150 assert!(allow.is_ok());
4151
4152 keychain.remove_allowed_calls(
4153 account,
4154 removeAllowedCallsCall {
4155 keyId: key_id,
4156 target,
4157 },
4158 )?;
4159
4160 let removed = keychain.get_allowed_calls(getAllowedCallsCall {
4161 account,
4162 keyId: key_id,
4163 })?;
4164 assert!(removed.isScoped);
4165 assert!(removed.scopes.is_empty());
4166
4167 let denied = keychain
4168 .validate_call_scope_for_transaction(
4169 account,
4170 key_id,
4171 &TxKind::Call(target),
4172 &TIP20_TRANSFER_SELECTOR,
4173 )
4174 .expect_err("unexpected success for removed target scope");
4175 assert_call_not_allowed(denied);
4176
4177 Ok(())
4178 })
4179 }
4180
4181 #[test]
4182 fn test_t3_set_allowed_calls_empty_selector_rules_allow_all_selectors() -> eyre::Result<()> {
4183 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
4184 let account = Address::random();
4185 let key_id = Address::random();
4186 let target = DEFAULT_FEE_TOKEN;
4187
4188 StorageCtx::enter(&mut storage, || {
4189 let mut keychain = AccountKeychain::new();
4190 keychain.initialize()?;
4191 keychain.set_transaction_key(Address::ZERO)?;
4192 keychain.set_tx_origin(account)?;
4193
4194 keychain.authorize_key(
4195 account,
4196 authorizeKeyCall {
4197 keyId: key_id,
4198 signatureType: SignatureType::Secp256k1,
4199 config: KeyRestrictions {
4200 expiry: u64::MAX,
4201 enforceLimits: false,
4202 limits: vec![],
4203 allowAnyCalls: true,
4204 allowedCalls: vec![],
4205 },
4206 },
4207 )?;
4208
4209 keychain.set_allowed_calls(
4210 account,
4211 setAllowedCallsCall {
4212 keyId: key_id,
4213 scopes: vec![CallScope {
4214 target,
4215 selectorRules: vec![],
4216 }],
4217 },
4218 )?;
4219
4220 let scopes = keychain.get_allowed_calls(getAllowedCallsCall {
4221 account,
4222 keyId: key_id,
4223 })?;
4224 assert!(scopes.isScoped);
4225 assert_eq!(scopes.scopes.len(), 1);
4226 assert_eq!(scopes.scopes[0].target, target);
4227 assert!(scopes.scopes[0].selectorRules.is_empty());
4228
4229 let allow = keychain.validate_call_scope_for_transaction(
4230 account,
4231 key_id,
4232 &TxKind::Call(target),
4233 &[],
4234 );
4235 assert!(allow.is_ok());
4236
4237 Ok(())
4238 })
4239 }
4240
4241 #[test]
4242 fn test_empty_recipient_selector_delete_is_gated_at_t4() -> eyre::Result<()> {
4243 let account = Address::random();
4244 let key_id = Address::random();
4245 let target = Address::random();
4246
4247 let mut t3_sstores = 0;
4248 let mut t4_sstores = 0;
4249
4250 for (hardfork, writes) in [
4251 (TempoHardfork::T3, &mut t3_sstores),
4252 (TempoHardfork::T4, &mut t4_sstores),
4253 ] {
4254 let mut storage = HashMapStorageProvider::new_with_spec(1, hardfork);
4255 StorageCtx::enter(&mut storage, || {
4256 let mut keychain = AccountKeychain::new();
4257 keychain.initialize()?;
4258 keychain.set_transaction_key(Address::ZERO)?;
4259 keychain.set_tx_origin(account)?;
4260
4261 keychain.authorize_key(
4262 account,
4263 authorizeKeyCall {
4264 keyId: key_id,
4265 signatureType: SignatureType::Secp256k1,
4266 config: KeyRestrictions {
4267 expiry: u64::MAX,
4268 enforceLimits: false,
4269 limits: vec![],
4270 allowAnyCalls: true,
4271 allowedCalls: vec![],
4272 },
4273 },
4274 )?;
4275
4276 let before = StorageCtx.counter_sstore();
4277 keychain.set_allowed_calls(
4278 account,
4279 setAllowedCallsCall {
4280 keyId: key_id,
4281 scopes: vec![CallScope {
4282 target,
4283 selectorRules: vec![SelectorRule {
4284 selector: TIP20_TRANSFER_SELECTOR.into(),
4285 recipients: vec![],
4286 }],
4287 }],
4288 },
4289 )?;
4290 *writes = StorageCtx.counter_sstore() - before;
4291
4292 let scopes = keychain.get_allowed_calls(getAllowedCallsCall {
4293 account,
4294 keyId: key_id,
4295 })?;
4296 assert!(scopes.isScoped);
4297 assert_eq!(scopes.scopes.len(), 1);
4298 assert!(scopes.scopes[0].selectorRules[0].recipients.is_empty());
4299
4300 Ok::<_, eyre::Report>(())
4301 })?;
4302 }
4303
4304 assert_eq!(
4305 t3_sstores,
4306 t4_sstores + 1,
4307 "pre-T4 should retain the redundant empty-recipient delete"
4308 );
4309
4310 Ok(())
4311 }
4312
4313 #[test]
4314 fn test_t3_call_scope_selector_and_recipient_checks() -> eyre::Result<()> {
4315 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
4316 let account = Address::random();
4317 let key_id = Address::random();
4318 let target = DEFAULT_FEE_TOKEN;
4319 let allowed_recipient = Address::repeat_byte(0x22);
4320 let denied_recipient = Address::repeat_byte(0x33);
4321
4322 StorageCtx::enter(&mut storage, || {
4323 let mut keychain = AccountKeychain::new();
4324 keychain.initialize()?;
4325 keychain.set_transaction_key(Address::ZERO)?;
4326 keychain.set_tx_origin(account)?;
4327 TIP20Setup::path_usd(account).apply()?;
4328
4329 keychain.authorize_key(
4330 account,
4331 authorizeKeyCall {
4332 keyId: key_id,
4333 signatureType: SignatureType::Secp256k1,
4334 config: KeyRestrictions {
4335 expiry: u64::MAX,
4336 enforceLimits: false,
4337 limits: vec![],
4338 allowAnyCalls: true,
4339 allowedCalls: vec![],
4340 },
4341 },
4342 )?;
4343
4344 keychain.apply_key_authorization_restrictions(
4345 account,
4346 key_id,
4347 &[],
4348 Some(&[CallScope {
4349 target,
4350 selectorRules: vec![SelectorRule {
4351 selector: TIP20_TRANSFER_SELECTOR.into(),
4352 recipients: vec![allowed_recipient],
4353 }],
4354 }]),
4355 )?;
4356
4357 let make_calldata = |selector: [u8; 4], recipient: Address| {
4358 let mut data = selector.to_vec();
4359 let mut recipient_word = [0u8; 32];
4360 recipient_word[12..].copy_from_slice(recipient.as_slice());
4361 data.extend_from_slice(&recipient_word);
4362 data.extend_from_slice(&[0u8; 32]);
4363 data
4364 };
4365
4366 let allow = keychain.validate_call_scope_for_transaction(
4367 account,
4368 key_id,
4369 &TxKind::Call(target),
4370 &make_calldata(TIP20_TRANSFER_SELECTOR, allowed_recipient),
4371 );
4372 assert!(allow.is_ok());
4373
4374 let denied = keychain
4375 .validate_call_scope_for_transaction(
4376 account,
4377 key_id,
4378 &TxKind::Call(target),
4379 &make_calldata(TIP20_TRANSFER_SELECTOR, denied_recipient),
4380 )
4381 .expect_err("unexpected success for denied recipient");
4382 assert_call_not_allowed(denied);
4383
4384 let wrong_selector = keychain
4385 .validate_call_scope_for_transaction(
4386 account,
4387 key_id,
4388 &TxKind::Call(target),
4389 &make_calldata([0xde, 0xad, 0xbe, 0xef], allowed_recipient),
4390 )
4391 .expect_err("unexpected success for wrong selector");
4392 assert_call_not_allowed(wrong_selector);
4393
4394 Ok(())
4395 })
4396 }
4397
4398 #[test]
4399 fn test_t3_contract_creation_rejected_for_access_key() -> eyre::Result<()> {
4400 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
4401 let account = Address::random();
4402 let key_id = Address::random();
4403
4404 StorageCtx::enter(&mut storage, || {
4405 let mut keychain = AccountKeychain::new();
4406 keychain.initialize()?;
4407 keychain.set_transaction_key(Address::ZERO)?;
4408 keychain.set_tx_origin(account)?;
4409
4410 keychain.authorize_key(
4411 account,
4412 authorizeKeyCall {
4413 keyId: key_id,
4414 signatureType: SignatureType::Secp256k1,
4415 config: KeyRestrictions {
4416 expiry: u64::MAX,
4417 enforceLimits: false,
4418 limits: vec![],
4419 allowAnyCalls: true,
4420 allowedCalls: vec![],
4421 },
4422 },
4423 )?;
4424
4425 let err = keychain
4426 .validate_call_scope_for_transaction(account, key_id, &TxKind::Create, &[])
4427 .expect_err("unexpected success for CREATE");
4428 assert_call_not_allowed(err);
4429
4430 Ok(())
4431 })
4432 }
4433}