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 burnKeyAuthorizationWitnessCall, getAllowedCallsCall, getKeyCall, getRemainingLimitCall,
20 getRemainingLimitWithPeriodCall, getTransactionKeyCall,
21 isKeyAuthorizationWitnessBurnedCall, removeAllowedCallsCall, revokeKeyCall,
22 setAllowedCallsCall, updateSpendingLimitCall,
23 },
24 authorizeKeyCall, authorizeKeyWithWitnessCall, getAllowedCallsReturn, getRemainingLimitReturn,
25};
26use tempo_primitives::TempoAddressExt;
27
28use crate::{
29 ACCOUNT_KEYCHAIN_ADDRESS,
30 error::Result,
31 storage::{Handler, Mapping, Set},
32 tip20_factory::TIP20Factory,
33};
34use alloy::primitives::{Address, B256, FixedBytes, TxKind, U256, keccak256};
35use tempo_precompiles_macros::{Storable, contract};
36
37const TIP20_TRANSFER_SELECTOR: [u8; 4] = ITIP20::transferCall::SELECTOR;
39const TIP20_APPROVE_SELECTOR: [u8; 4] = ITIP20::approveCall::SELECTOR;
40const TIP20_TRANSFER_WITH_MEMO_SELECTOR: [u8; 4] = ITIP20::transferWithMemoCall::SELECTOR;
41
42#[inline]
43pub fn is_constrained_tip20_selector(selector: [u8; 4]) -> bool {
44 matches!(
45 selector,
46 TIP20_TRANSFER_SELECTOR | TIP20_APPROVE_SELECTOR | TIP20_TRANSFER_WITH_MEMO_SELECTOR
47 )
48}
49
50#[derive(Debug, Clone, Default, PartialEq, Eq, Storable)]
59pub struct AuthorizedKey {
60 pub signature_type: StoredSignatureType,
62 pub expiry: u64,
64 pub enforce_limits: bool,
66 pub is_revoked: bool,
69 pub is_admin: bool,
71}
72
73#[repr(u8)]
74#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Storable)]
75pub enum StoredSignatureType {
76 #[default]
77 Secp256k1,
78 P256,
79 WebAuthn,
80}
81
82impl TryFrom<SignatureType> for StoredSignatureType {
83 type Error = crate::error::TempoPrecompileError;
84
85 fn try_from(value: SignatureType) -> std::result::Result<Self, Self::Error> {
86 match value {
87 SignatureType::Secp256k1 => Ok(Self::Secp256k1),
88 SignatureType::P256 => Ok(Self::P256),
89 SignatureType::WebAuthn => Ok(Self::WebAuthn),
90 _ => Err(AccountKeychainError::invalid_signature_type().into()),
91 }
92 }
93}
94
95impl From<StoredSignatureType> for SignatureType {
96 fn from(value: StoredSignatureType) -> Self {
97 match value {
98 StoredSignatureType::Secp256k1 => Self::Secp256k1,
99 StoredSignatureType::P256 => Self::P256,
100 StoredSignatureType::WebAuthn => Self::WebAuthn,
101 }
102 }
103}
104
105#[contract(addr = ACCOUNT_KEYCHAIN_ADDRESS)]
110pub struct AccountKeychain {
111 keys: Mapping<Address, Mapping<Address, AuthorizedKey>>,
113 spending_limits: Mapping<B256, Mapping<Address, SpendingLimitState>>,
116
117 key_scopes: Mapping<B256, KeyScope>,
119
120 key_authorization_witnesses: Mapping<Address, Mapping<B256, bool>>,
122
123 transaction_key: Address,
127 tx_origin: Address,
130}
131
132#[derive(Debug, Clone, Storable, Default)]
139pub struct KeyScope {
140 pub is_scoped: bool,
141 pub targets: Set<Address>,
142 pub target_scopes: Mapping<Address, TargetScope>,
143}
144
145#[derive(Debug, Clone, Storable, Default)]
152pub struct TargetScope {
153 pub selectors: Set<FixedBytes<4>>,
154 pub selector_scopes: Mapping<FixedBytes<4>, SelectorScope>,
155}
156
157#[derive(Debug, Clone, Storable, Default)]
165pub struct SelectorScope {
166 pub recipients: Set<Address>,
167}
168
169#[derive(Debug, Clone, Default, PartialEq, Eq, Storable)]
176pub struct SpendingLimitState {
177 pub remaining: U256,
179 pub max: u128,
181 pub period: u64,
183 pub period_end: u64,
185}
186
187impl SpendingLimitState {
188 fn compute_next_period_end(&self, current_timestamp: u64) -> u64 {
191 debug_assert!(
192 self.period != 0,
193 "period rollovers require a non-zero period"
194 );
195 let elapsed = current_timestamp.saturating_sub(self.period_end);
196 let periods_elapsed = (elapsed / self.period).saturating_add(1);
197 let advance = self.period.saturating_mul(periods_elapsed);
198 self.period_end.saturating_add(advance)
199 }
200}
201
202impl AccountKeychain {
203 pub fn spending_limit_key(account: Address, key_id: Address) -> B256 {
208 let mut data = [0u8; 40];
209 data[..20].copy_from_slice(account.as_slice());
210 data[20..].copy_from_slice(key_id.as_slice());
211 keccak256(data)
212 }
213
214 #[inline]
215 fn t3_spending_limit_cap(limit: U256) -> Result<u128> {
216 if limit > U256::from(u128::MAX) {
217 return Err(AccountKeychainError::invalid_spending_limit().into());
218 }
219
220 Ok(limit.to::<u128>())
221 }
222
223 pub fn initialize(&mut self) -> Result<()> {
225 self.__initialize()
226 }
227
228 pub fn authorize_key(
240 &mut self,
241 msg_sender: Address,
242 key_id: Address,
243 signature_type: SignatureType,
244 config: KeyRestrictions,
245 witness: Option<B256>,
246 ) -> Result<()> {
247 self.authorize_key_internal(msg_sender, key_id, signature_type, config, witness, false)
248 }
249
250 fn authorize_key_internal(
251 &mut self,
252 msg_sender: Address,
253 key_id: Address,
254 signature_type: SignatureType,
255 config: KeyRestrictions,
256 witness: Option<B256>,
257 is_admin: bool,
258 ) -> Result<()> {
259 let config = &config;
260 self.ensure_admin_caller(msg_sender)?;
261 let is_t3 = self.storage.spec().is_t3();
262
263 if key_id == Address::ZERO {
265 return Err(AccountKeychainError::zero_public_key().into());
266 }
267 if is_admin && key_id == msg_sender {
269 return Err(AccountKeychainError::invalid_key_id().into());
270 }
271
272 if self.storage.spec().is_t0() {
274 let current_timestamp = self.storage.timestamp().saturating_to::<u64>();
275 if config.expiry <= current_timestamp {
276 return Err(AccountKeychainError::expiry_in_past().into());
277 }
278 }
279
280 let existing_key = self.keys[msg_sender][key_id].read()?;
282 if existing_key.expiry > 0 {
283 return Err(AccountKeychainError::key_already_exists().into());
284 }
285
286 if existing_key.is_revoked {
288 return Err(AccountKeychainError::key_already_revoked().into());
289 }
290
291 let signature_type = StoredSignatureType::try_from(signature_type)?;
292
293 let allowed_call_configs = if is_t3 {
295 if config.enforceLimits {
296 let mut seen_tokens = HashSet::with_capacity(config.limits.len());
297 for limit in &config.limits {
298 if !seen_tokens.insert(limit.token) {
299 return Err(AccountKeychainError::invalid_spending_limit().into());
300 }
301 }
302 }
303
304 if config.allowAnyCalls {
305 if self.storage.spec().is_t5() && !config.allowedCalls.is_empty() {
307 return Err(AccountKeychainError::invalid_call_scope().into());
308 }
309
310 None
311 } else {
312 Some(config.allowedCalls.as_slice())
313 }
314 } else {
315 if config.limits.iter().any(|limit| limit.period != 0) {
316 return Err(AccountKeychainError::invalid_spending_limit().into());
317 }
318
319 if !config.allowAnyCalls || !config.allowedCalls.is_empty() {
320 return Err(AccountKeychainError::invalid_call_scope().into());
321 }
322
323 None
324 };
325
326 if let Some(witness) = witness {
327 self.ensure_key_authorization_witness_not_burned(msg_sender, witness)?;
328 }
329
330 let new_key = AuthorizedKey {
332 signature_type,
333 expiry: config.expiry,
334 enforce_limits: config.enforceLimits,
335 is_revoked: false,
336 is_admin,
337 };
338
339 self.keys[msg_sender][key_id].write(new_key)?;
340
341 if !is_admin {
342 let limits = config
343 .enforceLimits
344 .then_some(config.limits.iter())
345 .into_iter()
346 .flatten();
347
348 self.apply_key_authorization_restrictions(
349 msg_sender,
350 key_id,
351 limits,
352 allowed_call_configs,
353 )?;
354 }
355
356 if let Some(witness) = witness {
357 self.emit_event(AccountKeychainEvent::KeyAuthorizationWitness(
358 IAccountKeychain::KeyAuthorizationWitness {
359 account: msg_sender,
360 witness,
361 },
362 ))?;
363 }
364
365 self.emit_event(AccountKeychainEvent::key_authorized(
367 msg_sender,
368 key_id,
369 signature_type as u8,
370 config.expiry,
371 ))?;
372
373 Ok(())
374 }
375
376 pub fn authorize_admin_key(
379 &mut self,
380 msg_sender: Address,
381 key_id: Address,
382 signature_type: SignatureType,
383 witness: Option<B256>,
384 ) -> Result<()> {
385 self.authorize_key_internal(
386 msg_sender,
387 key_id,
388 signature_type,
389 KeyRestrictions {
390 expiry: u64::MAX,
391 enforceLimits: false,
392 limits: Vec::new(),
393 allowAnyCalls: true,
394 allowedCalls: Vec::new(),
395 },
396 witness,
397 true,
398 )?;
399 self.emit_event(AccountKeychainEvent::admin_key_authorized(
400 msg_sender, key_id,
401 ))
402 }
403
404 pub fn burn_key_authorization_witness(
406 &mut self,
407 msg_sender: Address,
408 call: burnKeyAuthorizationWitnessCall,
409 ) -> Result<()> {
410 self.ensure_admin_caller(msg_sender)?;
411 self.burn_key_authorization_witness_value(msg_sender, call.witness)
412 }
413
414 pub fn revoke_key(&mut self, msg_sender: Address, call: revokeKeyCall) -> Result<()> {
422 self.ensure_admin_caller(msg_sender)?;
423
424 let key = self.keys[msg_sender][call.keyId].read()?;
425
426 if key.expiry == 0 {
428 return Err(AccountKeychainError::key_not_found().into());
429 }
430
431 let revoked_key = AuthorizedKey {
435 is_revoked: true,
436 ..Default::default()
437 };
438 self.keys[msg_sender][call.keyId].write(revoked_key)?;
439
440 self.emit_event(AccountKeychainEvent::key_revoked(msg_sender, call.keyId))
444 }
445
446 pub fn update_spending_limit(
457 &mut self,
458 msg_sender: Address,
459 call: updateSpendingLimitCall,
460 ) -> Result<()> {
461 self.ensure_admin_caller(msg_sender)?;
462
463 let current_timestamp = self.storage.timestamp().saturating_to::<u64>();
464 let mut key = self.load_active_key(msg_sender, call.keyId, current_timestamp)?;
465 if key.is_admin {
466 return Err(AccountKeychainError::invalid_key_id().into());
467 }
468
469 if !key.enforce_limits {
471 key.enforce_limits = true;
472 self.keys[msg_sender][call.keyId].write(key)?;
473 }
474
475 let limit_key = Self::spending_limit_key(msg_sender, call.keyId);
477 if self.storage.spec().is_t3() {
478 let mut limit_state = self.spending_limits[limit_key][call.token].read()?;
481 limit_state.remaining = call.newLimit;
482 limit_state.max = Self::t3_spending_limit_cap(call.newLimit)?;
483 self.spending_limits[limit_key][call.token].write(limit_state)?;
484 } else {
485 self.spending_limits[limit_key][call.token]
486 .remaining
487 .write(call.newLimit)?;
488 }
489
490 self.emit_event(AccountKeychainEvent::spending_limit_updated(
492 msg_sender,
493 call.keyId,
494 call.token,
495 call.newLimit,
496 ))
497 }
498
499 pub fn get_key(&self, call: getKeyCall) -> Result<KeyInfo> {
501 let key = self.keys[call.account][call.keyId].read()?;
502
503 if key.expiry == 0 || key.is_revoked {
505 return Ok(KeyInfo {
506 signatureType: SignatureType::Secp256k1,
507 keyId: Address::ZERO,
508 expiry: 0,
509 enforceLimits: false,
510 isRevoked: key.is_revoked,
511 });
512 }
513
514 Ok(KeyInfo {
515 signatureType: key.signature_type.into(),
516 keyId: call.keyId,
517 expiry: key.expiry,
518 enforceLimits: key.enforce_limits,
519 isRevoked: key.is_revoked,
520 })
521 }
522
523 pub fn get_remaining_limit(&self, call: getRemainingLimitCall) -> Result<U256> {
528 if !self.storage.spec().is_t2() {
529 let limit_key = Self::spending_limit_key(call.account, call.keyId);
530 return self.spending_limits[limit_key][call.token].remaining.read();
531 }
532
533 self.get_remaining_limit_with_period(getRemainingLimitWithPeriodCall {
534 account: call.account,
535 keyId: call.keyId,
536 token: call.token,
537 })
538 .map(|ret| ret.remaining)
539 }
540
541 pub fn get_remaining_limit_with_period(
545 &self,
546 call: getRemainingLimitWithPeriodCall,
547 ) -> Result<getRemainingLimitReturn> {
548 let (remaining, period_end) = self.effective_limit_state(
549 call.account,
550 call.keyId,
551 call.token,
552 self.storage.timestamp().saturating_to::<u64>(),
553 )?;
554
555 Ok(getRemainingLimitReturn {
556 remaining,
557 periodEnd: period_end,
558 })
559 }
560
561 pub fn set_allowed_calls(
563 &mut self,
564 msg_sender: Address,
565 call: setAllowedCallsCall,
566 ) -> Result<()> {
567 if !self.storage.spec().is_t3() {
568 return Err(AccountKeychainError::invalid_call_scope().into());
569 }
570
571 self.ensure_admin_caller(msg_sender)?;
572
573 let current_timestamp = self.storage.timestamp().saturating_to::<u64>();
574 let key = self.load_active_key(msg_sender, call.keyId, current_timestamp)?;
575 if key.is_admin {
576 return Err(AccountKeychainError::invalid_key_id().into());
577 }
578
579 let key_hash = Self::spending_limit_key(msg_sender, call.keyId);
580 let scopes = call.scopes;
581
582 if scopes.is_empty() {
583 return Err(AccountKeychainError::invalid_call_scope().into());
584 }
585
586 self.validate_call_scopes(&scopes)?;
587
588 for scope in &scopes {
589 self.upsert_target_scope(key_hash, scope)?;
590 }
591
592 self.key_scopes[key_hash].is_scoped.write(true)
593 }
594
595 pub fn remove_allowed_calls(
597 &mut self,
598 msg_sender: Address,
599 call: removeAllowedCallsCall,
600 ) -> Result<()> {
601 self.ensure_admin_caller(msg_sender)?;
602
603 let current_timestamp = self.storage.timestamp().saturating_to::<u64>();
604 let key = self.load_active_key(msg_sender, call.keyId, current_timestamp)?;
605 if key.is_admin {
606 return Err(AccountKeychainError::invalid_key_id().into());
607 }
608
609 let key_hash = Self::spending_limit_key(msg_sender, call.keyId);
610 let current_mode = self.key_scopes[key_hash].is_scoped.read()?;
611 if !current_mode {
612 return Ok(());
613 }
614
615 self.remove_target_scope(key_hash, call.target)?;
616
617 Ok(())
618 }
619
620 pub fn get_allowed_calls(&self, call: getAllowedCallsCall) -> Result<getAllowedCallsReturn> {
626 if call.keyId.is_zero() {
627 return Ok(getAllowedCallsReturn {
628 isScoped: false,
629 scopes: Vec::new(),
630 });
631 }
632
633 let current_timestamp = self.storage.timestamp().saturating_to::<u64>();
634 let key = self.keys[call.account][call.keyId].read()?;
635 if key.expiry == 0 || key.is_revoked || current_timestamp >= key.expiry {
636 return Ok(getAllowedCallsReturn {
637 isScoped: true,
638 scopes: Vec::new(),
639 });
640 }
641
642 let key_hash = Self::spending_limit_key(call.account, call.keyId);
643 let is_scoped = self.key_scopes[key_hash].is_scoped.read()?;
644
645 if !is_scoped {
646 return Ok(getAllowedCallsReturn {
647 isScoped: false,
648 scopes: Vec::new(),
649 });
650 }
651
652 let targets = self.key_scopes[key_hash].targets.read()?;
653 let mut scopes = Vec::new();
654 for target in targets {
655 let selectors = self.key_scopes[key_hash].target_scopes[target]
656 .selectors
657 .read()?;
658
659 let scope = if selectors.is_empty() {
660 CallScope {
661 target,
662 selectorRules: Vec::new(),
663 }
664 } else {
665 let mut rules = Vec::new();
666
667 for selector in selectors {
668 let recipients: Vec<Address> = self.key_scopes[key_hash].target_scopes[target]
669 .selector_scopes[selector]
670 .recipients
671 .read()?
672 .into();
673
674 rules.push(SelectorRule {
675 selector,
676 recipients,
677 });
678 }
679
680 CallScope {
681 target,
682 selectorRules: rules,
683 }
684 };
685
686 scopes.push(scope);
687 }
688
689 Ok(getAllowedCallsReturn {
690 isScoped: true,
691 scopes,
692 })
693 }
694
695 pub fn is_key_authorization_witness_burned(
697 &self,
698 call: isKeyAuthorizationWitnessBurnedCall,
699 ) -> Result<bool> {
700 self.key_authorization_witnesses[call.account][call.witness].read()
701 }
702
703 pub fn get_transaction_key(
706 &self,
707 _call: getTransactionKeyCall,
708 _msg_sender: Address,
709 ) -> Result<Address> {
710 self.transaction_key.t_read()
711 }
712
713 pub fn set_transaction_key(&mut self, key_id: Address) -> Result<()> {
725 self.transaction_key.t_write(key_id)
726 }
727
728 pub fn set_tx_origin(&mut self, origin: Address) -> Result<()> {
733 self.tx_origin.t_write(origin)
734 }
735
736 fn apply_key_authorization_restrictions<'a>(
741 &mut self,
742 account: Address,
743 key_id: Address,
744 limits: impl IntoIterator<Item = &'a TokenLimit>,
745 allowed_calls: Option<&[CallScope]>,
746 ) -> Result<()> {
747 let limit_key = Self::spending_limit_key(account, key_id);
748
749 let is_t3 = self.storage.spec().is_t3();
750 debug_assert!(is_t3 || allowed_calls.is_none());
751
752 let now = self.storage.timestamp().saturating_to::<u64>();
753 for limit in limits {
754 if is_t3 {
755 let period_end = if limit.period == 0 {
756 0
757 } else {
758 now.saturating_add(limit.period)
759 };
760
761 self.spending_limits[limit_key][limit.token].write(SpendingLimitState {
762 remaining: limit.amount,
763 max: Self::t3_spending_limit_cap(limit.amount)?,
764 period: limit.period,
765 period_end,
766 })?;
767 } else {
768 self.spending_limits[limit_key][limit.token]
769 .remaining
770 .write(limit.amount)?;
771 }
772 }
773
774 if !is_t3 {
775 return Ok(());
776 }
777
778 self.replace_allowed_calls(limit_key, allowed_calls)
779 }
780
781 pub fn validate_call_scope_for_transaction(
790 &self,
791 account: Address,
792 key_id: Address,
793 to: &TxKind,
794 input: &[u8],
795 ) -> Result<()> {
796 if key_id == Address::ZERO || !self.storage.spec().is_t3() {
797 return Ok(());
798 }
799
800 let target = match to {
801 TxKind::Call(target) => *target,
802 TxKind::Create => return Err(AccountKeychainError::call_not_allowed().into()),
803 };
804
805 let key_hash = Self::spending_limit_key(account, key_id);
806
807 if !self.key_scopes[key_hash].is_scoped.read()? {
809 return Ok(());
810 }
811
812 if !self.key_scopes[key_hash].targets.contains(&target)? {
813 return Err(AccountKeychainError::call_not_allowed().into());
814 }
815
816 let target_is_unconstrained = self.key_scopes[key_hash].target_scopes[target]
819 .selectors
820 .is_empty()?;
821 if target_is_unconstrained {
822 return Ok(());
823 }
824
825 if input.len() < 4 {
826 return Err(AccountKeychainError::call_not_allowed().into());
827 }
828
829 let selector = FixedBytes::<4>::from(
831 <[u8; 4]>::try_from(&input[..4]).expect("input len checked above"),
832 );
833 if !self.key_scopes[key_hash].target_scopes[target]
834 .selectors
835 .contains(&selector)?
836 {
837 return Err(AccountKeychainError::call_not_allowed().into());
838 }
839
840 let selector_is_unconstrained = self.key_scopes[key_hash].target_scopes[target]
842 .selector_scopes[selector]
843 .recipients
844 .is_empty()?;
845 if selector_is_unconstrained {
846 return Ok(());
847 }
848
849 if input.len() < 36 {
850 return Err(AccountKeychainError::call_not_allowed().into());
851 }
852
853 let recipient_word = &input[4..36];
855 if recipient_word[..12].iter().any(|byte| *byte != 0) {
856 return Err(AccountKeychainError::call_not_allowed().into());
857 }
858
859 let recipient = Address::from_slice(&recipient_word[12..]);
860 if self.key_scopes[key_hash].target_scopes[target].selector_scopes[selector]
861 .recipients
862 .contains(&recipient)?
863 {
864 Ok(())
865 } else {
866 Err(AccountKeychainError::call_not_allowed().into())
867 }
868 }
869
870 fn replace_allowed_calls(
877 &mut self,
878 account_key: B256,
879 allowed_calls: Option<&[CallScope]>,
880 ) -> Result<()> {
881 self.clear_all_target_scopes(account_key)?;
886
887 match allowed_calls {
888 None => {
889 self.key_scopes[account_key].is_scoped.write(false)?;
890 Ok(())
891 }
892 Some(scopes) => {
893 self.key_scopes[account_key].is_scoped.write(true)?;
894
895 if scopes.is_empty() {
896 return Ok(());
897 }
898
899 self.validate_call_scopes(scopes)?;
900
901 for scope in scopes {
902 self.upsert_target_scope(account_key, scope)?;
903 }
904
905 Ok(())
906 }
907 }
908 }
909
910 fn clear_all_target_scopes(&mut self, account_key: B256) -> Result<()> {
912 let targets = self.key_scopes[account_key].targets.read()?;
913 for target in targets {
914 self.clear_target_selectors(account_key, target)?;
915 }
916
917 self.key_scopes[account_key].targets.delete()
918 }
919
920 fn remove_target_scope(&mut self, account_key: B256, target: Address) -> Result<()> {
922 if !self.key_scopes[account_key].targets.remove(&target)? {
923 return Ok(());
924 }
925
926 self.clear_target_selectors(account_key, target)
927 }
928
929 fn clear_target_selectors(&mut self, account_key: B256, target: Address) -> Result<()> {
931 let selectors = self.key_scopes[account_key].target_scopes[target]
932 .selectors
933 .read()?;
934 for selector in selectors {
935 self.key_scopes[account_key].target_scopes[target].selector_scopes[selector]
936 .recipients
937 .delete()?;
938 }
939
940 self.key_scopes[account_key].target_scopes[target]
941 .selectors
942 .delete()
943 }
944
945 fn upsert_target_scope(&mut self, account_key: B256, scope: &CallScope) -> Result<()> {
947 let target = scope.target;
948
949 if !self.storage.spec().is_t4() {
951 self.validate_call_scope(scope)?;
952 }
953
954 self.key_scopes[account_key].targets.insert(target)?;
955 self.clear_target_selectors(account_key, target)?;
956
957 if scope.selectorRules.is_empty() {
958 return Ok(());
962 }
963
964 for rule in &scope.selectorRules {
965 let selector = rule.selector;
966 self.key_scopes[account_key].target_scopes[target]
967 .selectors
968 .insert(selector)?;
969
970 if rule.recipients.is_empty() {
971 if !self.storage.spec().is_t4() {
972 self.key_scopes[account_key].target_scopes[target].selector_scopes[selector]
976 .recipients
977 .delete()?;
978 }
979 } else {
980 self.key_scopes[account_key].target_scopes[target].selector_scopes[selector]
982 .recipients
983 .write(Set::new_unchecked(rule.recipients.clone()))?;
984 }
985 }
986
987 Ok(())
988 }
989
990 fn validate_call_scopes(&self, scopes: &[CallScope]) -> Result<()> {
992 let mut seen_targets = HashSet::new();
993 for scope in scopes {
994 if !seen_targets.insert(scope.target) {
995 return Err(AccountKeychainError::invalid_call_scope().into());
996 }
997
998 if self.storage.spec().is_t4() {
1000 self.validate_call_scope(scope)?;
1001 }
1002 }
1003 Ok(())
1004 }
1005
1006 fn validate_call_scope(&self, scope: &CallScope) -> Result<()> {
1008 if scope.target.is_zero() {
1011 return Err(AccountKeychainError::invalid_call_scope().into());
1012 }
1013
1014 if !scope.selectorRules.is_empty() {
1015 self.validate_selector_rules(scope.target, &scope.selectorRules)?;
1016 }
1017
1018 Ok(())
1019 }
1020
1021 fn validate_selector_rules(&self, target: Address, rules: &[SelectorRule]) -> Result<()> {
1027 let mut cached_is_tip20: Option<bool> = None;
1028 let mut is_tip20 = || -> Result<bool> {
1029 match cached_is_tip20 {
1030 Some(v) => Ok(v),
1031 None => Ok(*cached_is_tip20.insert({
1032 if !self.storage.spec().is_t4() {
1033 TIP20Factory::new().is_tip20(target)?
1035 } else {
1036 target.is_tip20()
1038 }
1039 })),
1040 }
1041 };
1042
1043 let mut selectors = HashSet::new();
1044 for rule in rules {
1045 if !selectors.insert(rule.selector) {
1046 return Err(AccountKeychainError::invalid_call_scope().into());
1047 }
1048
1049 if rule.recipients.is_empty() {
1050 continue;
1051 }
1052
1053 if !is_constrained_tip20_selector(*rule.selector) || !is_tip20()? {
1054 return Err(AccountKeychainError::invalid_call_scope().into());
1055 }
1056
1057 let mut unique_recipients = HashSet::new();
1058 for recipient in &rule.recipients {
1059 if recipient.is_zero() || !unique_recipients.insert(*recipient) {
1060 return Err(AccountKeychainError::invalid_call_scope().into());
1061 }
1062 }
1063 }
1064
1065 Ok(())
1066 }
1067
1068 fn ensure_admin_caller(&self, msg_sender: Address) -> Result<()> {
1086 let transaction_key = self.transaction_key.t_read()?;
1087 if !transaction_key.is_zero()
1088 && (!self.storage.spec().is_t6() || !self.is_admin_key(msg_sender, transaction_key)?)
1089 {
1090 return Err(AccountKeychainError::unauthorized_caller().into());
1091 }
1092
1093 if self.storage.spec().is_t2() {
1094 let tx_origin = self.tx_origin.t_read()?;
1095 if tx_origin.is_zero() || tx_origin != msg_sender {
1096 return Err(AccountKeychainError::unauthorized_caller().into());
1097 }
1098 }
1099
1100 Ok(())
1101 }
1102
1103 pub fn is_admin_key(&self, account: Address, key_id: Address) -> Result<bool> {
1108 if key_id == account {
1109 return Ok(true);
1110 }
1111
1112 let current_timestamp = self.storage.timestamp().saturating_to::<u64>();
1113 let key = match self.load_active_key(account, key_id, current_timestamp) {
1114 Ok(key) => key,
1115 Err(err) if err.is_system_error() => return Err(err),
1116 Err(_) => return Ok(false),
1117 };
1118
1119 Ok(key.is_admin)
1120 }
1121
1122 pub fn is_active_key(&self, account: Address, key_id: Address) -> Result<bool> {
1124 let current_timestamp = self.storage.timestamp().saturating_to::<u64>();
1125 match self.load_active_key(account, key_id, current_timestamp) {
1126 Ok(_) => Ok(true),
1127 Err(err) if err.is_system_error() => Err(err),
1128 Err(_) => Ok(false),
1129 }
1130 }
1131
1132 fn ensure_key_authorization_witness_not_burned(
1133 &self,
1134 account: Address,
1135 witness: B256,
1136 ) -> Result<()> {
1137 if self.key_authorization_witnesses[account][witness].read()? {
1138 return Err(AccountKeychainError::key_authorization_witness_already_burned().into());
1139 }
1140
1141 Ok(())
1142 }
1143
1144 fn burn_key_authorization_witness_value(
1145 &mut self,
1146 account: Address,
1147 witness: B256,
1148 ) -> Result<()> {
1149 self.ensure_key_authorization_witness_not_burned(account, witness)?;
1150
1151 self.key_authorization_witnesses[account][witness].write(true)?;
1152 self.emit_event(AccountKeychainEvent::KeyAuthorizationWitnessBurned(
1153 IAccountKeychain::KeyAuthorizationWitnessBurned { account, witness },
1154 ))
1155 }
1156
1157 fn load_active_key(
1164 &self,
1165 account: Address,
1166 key_id: Address,
1167 current_timestamp: u64,
1168 ) -> Result<AuthorizedKey> {
1169 let key = self.keys[account][key_id].read()?;
1170
1171 if key.is_revoked {
1172 return Err(AccountKeychainError::key_already_revoked().into());
1173 }
1174
1175 if key.expiry == 0 {
1176 return Err(AccountKeychainError::key_not_found().into());
1177 }
1178
1179 if current_timestamp >= key.expiry {
1180 return Err(AccountKeychainError::key_expired().into());
1181 }
1182
1183 Ok(key)
1184 }
1185
1186 pub fn validate_keychain_authorization(
1201 &self,
1202 account: Address,
1203 key_id: Address,
1204 current_timestamp: u64,
1205 expected_sig_type: Option<u8>,
1206 ) -> Result<AuthorizedKey> {
1207 let key = self.load_active_key(account, key_id, current_timestamp)?;
1208
1209 if let Some(sig_type) = expected_sig_type
1212 && key.signature_type as u8 != sig_type
1213 {
1214 return Err(AccountKeychainError::signature_type_mismatch(
1215 key.signature_type as u8,
1216 sig_type,
1217 )
1218 .into());
1219 }
1220
1221 Ok(key)
1222 }
1223
1224 pub fn effective_remaining_limit(
1226 &self,
1227 account: Address,
1228 key_id: Address,
1229 token: Address,
1230 current_timestamp: u64,
1231 ) -> Result<U256> {
1232 self.effective_limit_state(account, key_id, token, current_timestamp)
1233 .map(|(remaining, _)| remaining)
1234 }
1235
1236 fn effective_limit_state(
1239 &self,
1240 account: Address,
1241 key_id: Address,
1242 token: Address,
1243 current_timestamp: u64,
1244 ) -> Result<(U256, u64)> {
1245 if key_id.is_zero() && self.storage.spec().is_t3() {
1246 return Ok((U256::ZERO, 0));
1247 }
1248
1249 let key = self.keys[account][key_id].read()?;
1250
1251 if key.is_revoked || key.expiry == 0 {
1253 return Ok((U256::ZERO, 0));
1254 }
1255
1256 if current_timestamp >= key.expiry && self.storage.spec().is_t3() {
1258 return Ok((U256::ZERO, 0));
1259 }
1260
1261 let limit_key = Self::spending_limit_key(account, key_id);
1262 let remaining = self.spending_limits[limit_key][token].remaining.read()?;
1263
1264 if !self.storage.spec().is_t3() {
1265 return Ok((remaining, 0));
1266 }
1267
1268 let period = self.spending_limits[limit_key][token].period.read()?;
1269 if period == 0 {
1270 return Ok((remaining, 0));
1271 }
1272
1273 let period_end = self.spending_limits[limit_key][token].period_end.read()?;
1274 if current_timestamp < period_end {
1275 return Ok((remaining, period_end));
1276 }
1277
1278 let elapsed = current_timestamp.saturating_sub(period_end);
1279 let periods_elapsed = (elapsed / period).saturating_add(1);
1280 let advance = period.saturating_mul(periods_elapsed);
1281 let next_end = period_end.saturating_add(advance);
1282
1283 let max = self.spending_limits[limit_key][token].max.read()?;
1284
1285 Ok((U256::from(max), next_end))
1286 }
1287
1288 pub fn verify_and_update_spending(
1295 &mut self,
1296 account: Address,
1297 key_id: Address,
1298 token: Address,
1299 amount: U256,
1300 ) -> Result<()> {
1301 if key_id == Address::ZERO {
1303 return Ok(());
1304 }
1305
1306 let current_timestamp = self.storage.timestamp().saturating_to::<u64>();
1308 let key = self.load_active_key(account, key_id, current_timestamp)?;
1309
1310 if !key.enforce_limits {
1312 return Ok(());
1313 }
1314
1315 let limit_key = Self::spending_limit_key(account, key_id);
1317 if !self.storage.spec().is_t3() {
1318 let remaining = self.spending_limits[limit_key][token].remaining.read()?;
1319 if amount > remaining {
1320 return Err(AccountKeychainError::spending_limit_exceeded().into());
1321 }
1322
1323 let new_remaining = remaining - amount;
1324 self.spending_limits[limit_key][token]
1325 .remaining
1326 .write(new_remaining)?;
1327 return Ok(());
1328 }
1329
1330 let mut limit_state = self.spending_limits[limit_key][token].read()?;
1331 let mut remaining = limit_state.remaining;
1332 let is_periodic = limit_state.period != 0;
1333
1334 if is_periodic && current_timestamp >= limit_state.period_end {
1335 let next_end = limit_state.compute_next_period_end(current_timestamp);
1336
1337 remaining = U256::from(limit_state.max);
1338 limit_state.remaining = remaining;
1339 limit_state.period_end = next_end;
1340 }
1341
1342 if amount > remaining {
1343 return Err(AccountKeychainError::spending_limit_exceeded().into());
1344 }
1345
1346 let new_remaining = remaining - amount;
1348 if is_periodic {
1349 limit_state.remaining = new_remaining;
1350 self.spending_limits[limit_key][token].write(limit_state)?;
1351 } else {
1352 self.spending_limits[limit_key][token]
1353 .remaining
1354 .write(new_remaining)?;
1355 }
1356
1357 self.emit_event(AccountKeychainEvent::access_key_spend(
1358 account,
1359 key_id,
1360 token,
1361 amount,
1362 new_remaining,
1363 ))?;
1364
1365 Ok(())
1366 }
1367
1368 pub fn refund_spending_limit(
1375 &mut self,
1376 account: Address,
1377 token: Address,
1378 amount: U256,
1379 ) -> Result<()> {
1380 let transaction_key = self.transaction_key.t_read()?;
1381
1382 if transaction_key == Address::ZERO {
1383 return Ok(());
1384 }
1385
1386 let tx_origin = self.tx_origin.t_read()?;
1387 if account != tx_origin {
1388 return Ok(());
1389 }
1390
1391 let current_timestamp = self.storage.timestamp().saturating_to::<u64>();
1394 let key = match self.load_active_key(account, transaction_key, current_timestamp) {
1395 Ok(key) => key,
1396 Err(err) if err.is_system_error() => return Err(err),
1397 Err(_) => return Ok(()),
1398 };
1399
1400 if !key.enforce_limits {
1401 return Ok(());
1402 }
1403
1404 let limit_key = Self::spending_limit_key(account, transaction_key);
1405 if !self.storage.spec().is_t3() {
1406 let remaining = self.spending_limits[limit_key][token].remaining.read()?;
1407 let refunded = remaining.saturating_add(amount);
1408 return self.spending_limits[limit_key][token]
1409 .remaining
1410 .write(refunded);
1411 }
1412
1413 let mut limit_state = self.spending_limits[limit_key][token].read()?;
1414 let refunded = limit_state.remaining.saturating_add(amount);
1415 limit_state.remaining = if limit_state.max == 0 {
1419 refunded
1420 } else {
1421 refunded.min(U256::from(limit_state.max))
1422 };
1423
1424 self.spending_limits[limit_key][token].write(limit_state)
1425 }
1426
1427 pub fn authorize_transfer(
1438 &mut self,
1439 account: Address,
1440 token: Address,
1441 amount: U256,
1442 ) -> Result<()> {
1443 let transaction_key = self.transaction_key.t_read()?;
1445
1446 if transaction_key == Address::ZERO {
1448 return Ok(());
1449 }
1450
1451 let tx_origin = self.tx_origin.t_read()?;
1453 if account != tx_origin {
1454 return Ok(());
1455 }
1456
1457 self.verify_and_update_spending(account, transaction_key, token, amount)
1459 }
1460
1461 pub fn authorize_approve(
1472 &mut self,
1473 account: Address,
1474 token: Address,
1475 old_approval: U256,
1476 new_approval: U256,
1477 ) -> Result<()> {
1478 let transaction_key = self.transaction_key.t_read()?;
1480
1481 if transaction_key == Address::ZERO {
1483 return Ok(());
1484 }
1485
1486 let tx_origin = self.tx_origin.t_read()?;
1488 if account != tx_origin {
1489 return Ok(());
1490 }
1491
1492 let approval_increase = new_approval.saturating_sub(old_approval);
1496
1497 if approval_increase.is_zero() {
1499 return Ok(());
1500 }
1501
1502 self.verify_and_update_spending(account, transaction_key, token, approval_increase)
1504 }
1505}
1506
1507#[cfg(test)]
1508mod tests {
1509 use super::*;
1510 use crate::{
1511 error::TempoPrecompileError,
1512 storage::{StorageCtx, hashmap::HashMapStorageProvider},
1513 test_util::TIP20Setup,
1514 };
1515 use alloy::primitives::{Address, B256, TxKind, U256};
1516 use revm::state::Bytecode;
1517 use tempo_chainspec::hardfork::TempoHardfork;
1518 use tempo_contracts::precompiles::{DEFAULT_FEE_TOKEN, IAccountKeychain::SignatureType};
1519
1520 fn authorize_key(
1521 keychain: &mut AccountKeychain,
1522 msg_sender: Address,
1523 call: authorizeKeyCall,
1524 ) -> Result<()> {
1525 AccountKeychain::authorize_key(
1526 keychain,
1527 msg_sender,
1528 call.keyId,
1529 call.signatureType,
1530 call.config,
1531 None,
1532 )
1533 }
1534
1535 fn authorize_key_with_witness(
1536 keychain: &mut AccountKeychain,
1537 msg_sender: Address,
1538 call: authorizeKeyWithWitnessCall,
1539 ) -> Result<()> {
1540 AccountKeychain::authorize_key(
1541 keychain,
1542 msg_sender,
1543 call.keyId,
1544 call.signatureType,
1545 call.config,
1546 Some(call.witness),
1547 )
1548 }
1549
1550 fn assert_unauthorized_error(error: TempoPrecompileError) {
1552 match error {
1553 TempoPrecompileError::AccountKeychainError(e) => {
1554 assert!(
1555 matches!(e, AccountKeychainError::UnauthorizedCaller(_)),
1556 "Expected UnauthorizedCaller error, got: {e:?}"
1557 );
1558 }
1559 _ => panic!("Expected AccountKeychainError, got: {error:?}"),
1560 }
1561 }
1562
1563 fn assert_call_not_allowed(error: TempoPrecompileError) {
1564 match error {
1565 TempoPrecompileError::AccountKeychainError(e) => {
1566 assert!(
1567 matches!(e, AccountKeychainError::CallNotAllowed(_)),
1568 "Expected CallNotAllowed error, got: {e:?}"
1569 );
1570 }
1571 _ => panic!("Expected AccountKeychainError, got: {error:?}"),
1572 }
1573 }
1574
1575 fn assert_invalid_call_scope(error: TempoPrecompileError) {
1576 match error {
1577 TempoPrecompileError::AccountKeychainError(e) => {
1578 assert!(
1579 matches!(e, AccountKeychainError::InvalidCallScope(_)),
1580 "Expected InvalidCallScope error, got: {e:?}"
1581 );
1582 }
1583 _ => panic!("Expected AccountKeychainError, got: {error:?}"),
1584 }
1585 }
1586
1587 fn assert_invalid_key_id(error: TempoPrecompileError) {
1588 match error {
1589 TempoPrecompileError::AccountKeychainError(e) => {
1590 assert!(
1591 matches!(e, AccountKeychainError::InvalidKeyId(_)),
1592 "Expected InvalidKeyId error, got: {e:?}"
1593 );
1594 }
1595 _ => panic!("Expected AccountKeychainError, got: {error:?}"),
1596 }
1597 }
1598
1599 fn assert_key_not_found(error: TempoPrecompileError) {
1600 match error {
1601 TempoPrecompileError::AccountKeychainError(e) => {
1602 assert!(
1603 matches!(e, AccountKeychainError::KeyNotFound(_)),
1604 "Expected KeyNotFound error, got: {e:?}"
1605 );
1606 }
1607 _ => panic!("Expected AccountKeychainError, got: {error:?}"),
1608 }
1609 }
1610
1611 fn unrestricted_restrictions() -> KeyRestrictions {
1612 tempo_alloy::provider::keychain::KeyRestrictions::default().into()
1613 }
1614
1615 #[test]
1616 fn test_t6_root_authorizes_admin_key() -> eyre::Result<()> {
1617 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T6);
1618 let account = Address::random();
1619 let admin_key = Address::random();
1620
1621 StorageCtx::enter(&mut storage, || {
1622 let mut keychain = AccountKeychain::new();
1623 keychain.initialize()?;
1624 keychain.set_tx_origin(account)?;
1625
1626 keychain.authorize_admin_key(account, admin_key, SignatureType::P256, None)?;
1627
1628 let key = keychain.keys[account][admin_key].read()?;
1629 assert_eq!(key.signature_type, StoredSignatureType::P256);
1630 assert_eq!(key.expiry, u64::MAX);
1631 assert!(!key.enforce_limits);
1632 assert!(!key.is_revoked);
1633 assert!(key.is_admin);
1634 assert!(keychain.is_admin_key(account, account)?);
1635 assert!(keychain.is_admin_key(account, admin_key)?);
1636
1637 Ok(())
1638 })
1639 }
1640
1641 #[test]
1642 fn test_t6_is_admin_key_uses_active_key_status() -> eyre::Result<()> {
1643 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T6);
1644 storage.set_timestamp(U256::from(100u64));
1645 let account = Address::random();
1646 let active_admin_key = Address::random();
1647 let non_admin_key = Address::random();
1648 let revoked_admin_key = Address::random();
1649 let expired_admin_key = Address::random();
1650 let missing_key = Address::random();
1651
1652 StorageCtx::enter(&mut storage, || {
1653 let mut keychain = AccountKeychain::new();
1654 keychain.initialize()?;
1655
1656 keychain.keys[account][active_admin_key].write(AuthorizedKey {
1657 signature_type: StoredSignatureType::Secp256k1,
1658 expiry: u64::MAX,
1659 enforce_limits: false,
1660 is_revoked: false,
1661 is_admin: true,
1662 })?;
1663 keychain.keys[account][non_admin_key].write(AuthorizedKey {
1664 signature_type: StoredSignatureType::Secp256k1,
1665 expiry: u64::MAX,
1666 enforce_limits: false,
1667 is_revoked: false,
1668 is_admin: false,
1669 })?;
1670 keychain.keys[account][revoked_admin_key].write(AuthorizedKey {
1671 signature_type: StoredSignatureType::Secp256k1,
1672 expiry: u64::MAX,
1673 enforce_limits: false,
1674 is_revoked: true,
1675 is_admin: true,
1676 })?;
1677 keychain.keys[account][expired_admin_key].write(AuthorizedKey {
1678 signature_type: StoredSignatureType::Secp256k1,
1679 expiry: 100,
1680 enforce_limits: false,
1681 is_revoked: false,
1682 is_admin: true,
1683 })?;
1684
1685 assert!(keychain.is_admin_key(account, account)?);
1686 assert!(keychain.is_admin_key(account, active_admin_key)?);
1687 assert!(!keychain.is_admin_key(account, non_admin_key)?);
1688 assert!(!keychain.is_admin_key(account, revoked_admin_key)?);
1689 assert!(!keychain.is_admin_key(account, expired_admin_key)?);
1690 assert!(!keychain.is_admin_key(account, missing_key)?);
1691
1692 Ok(())
1693 })
1694 }
1695
1696 #[test]
1697 fn test_t6_authorize_admin_key_with_witness_checks_burned_state() -> eyre::Result<()> {
1698 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T6);
1699 let account = Address::random();
1700 let first_admin_key = Address::random();
1701 let second_admin_key = Address::random();
1702 let witness = B256::repeat_byte(0x65);
1703
1704 StorageCtx::enter(&mut storage, || {
1705 let mut keychain = AccountKeychain::new();
1706 keychain.initialize()?;
1707 keychain.set_tx_origin(account)?;
1708
1709 keychain.authorize_admin_key(
1710 account,
1711 first_admin_key,
1712 SignatureType::Secp256k1,
1713 Some(witness),
1714 )?;
1715 assert!(!keychain.is_key_authorization_witness_burned(
1716 isKeyAuthorizationWitnessBurnedCall { account, witness }
1717 )?);
1718
1719 keychain.burn_key_authorization_witness(
1720 account,
1721 burnKeyAuthorizationWitnessCall { witness },
1722 )?;
1723 let result = keychain.authorize_admin_key(
1724 account,
1725 second_admin_key,
1726 SignatureType::Secp256k1,
1727 Some(witness),
1728 );
1729 assert_eq!(
1730 result.expect_err("burned witness must not authorize admin key"),
1731 AccountKeychainError::key_authorization_witness_already_burned().into()
1732 );
1733
1734 Ok(())
1735 })
1736 }
1737
1738 #[test]
1739 fn test_t6_admin_key_can_authorize_and_revoke_keys() -> eyre::Result<()> {
1740 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T6);
1741 let account = Address::random();
1742 let admin_key = Address::random();
1743 let child_key = Address::random();
1744
1745 StorageCtx::enter(&mut storage, || {
1746 let mut keychain = AccountKeychain::new();
1747 keychain.initialize()?;
1748 keychain.set_tx_origin(account)?;
1749 keychain.authorize_admin_key(account, admin_key, SignatureType::Secp256k1, None)?;
1750
1751 keychain.set_transaction_key(admin_key)?;
1752 authorize_key(
1753 &mut keychain,
1754 account,
1755 authorizeKeyCall {
1756 keyId: child_key,
1757 signatureType: SignatureType::WebAuthn,
1758 config: unrestricted_restrictions(),
1759 },
1760 )?;
1761 assert!(keychain.keys[account][child_key].read()?.expiry > 0);
1762
1763 keychain.revoke_key(account, revokeKeyCall { keyId: admin_key })?;
1764 assert!(!keychain.is_admin_key(account, admin_key)?);
1765
1766 Ok(())
1767 })
1768 }
1769
1770 #[test]
1771 fn test_t6_non_admin_key_cannot_authorize_keys() -> eyre::Result<()> {
1772 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T6);
1773 let account = Address::random();
1774 let access_key = Address::random();
1775 let child_key = Address::random();
1776
1777 StorageCtx::enter(&mut storage, || {
1778 let mut keychain = AccountKeychain::new();
1779 keychain.initialize()?;
1780 keychain.set_tx_origin(account)?;
1781 authorize_key(
1782 &mut keychain,
1783 account,
1784 authorizeKeyCall {
1785 keyId: access_key,
1786 signatureType: SignatureType::Secp256k1,
1787 config: unrestricted_restrictions(),
1788 },
1789 )?;
1790
1791 keychain.set_transaction_key(access_key)?;
1792 let result = authorize_key(
1793 &mut keychain,
1794 account,
1795 authorizeKeyCall {
1796 keyId: child_key,
1797 signatureType: SignatureType::Secp256k1,
1798 config: unrestricted_restrictions(),
1799 },
1800 );
1801
1802 assert_unauthorized_error(result.expect_err("non-admin key must not authorize keys"));
1803 Ok(())
1804 })
1805 }
1806
1807 #[test]
1808 fn test_t6_admin_key_restrictions_and_root_admin_authorization_rejected() -> eyre::Result<()> {
1809 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T6);
1810 let account = Address::random();
1811 let admin_key = Address::random();
1812 let token = Address::random();
1813
1814 StorageCtx::enter(&mut storage, || {
1815 let mut keychain = AccountKeychain::new();
1816 keychain.initialize()?;
1817 keychain.set_tx_origin(account)?;
1818 keychain.authorize_admin_key(account, admin_key, SignatureType::Secp256k1, None)?;
1819
1820 assert_invalid_key_id(
1821 keychain
1822 .update_spending_limit(
1823 account,
1824 updateSpendingLimitCall {
1825 keyId: admin_key,
1826 token,
1827 newLimit: U256::from(1),
1828 },
1829 )
1830 .expect_err("admin keys cannot receive spending limits"),
1831 );
1832
1833 assert_invalid_key_id(
1834 keychain
1835 .authorize_admin_key(account, account, SignatureType::Secp256k1, None)
1836 .expect_err("root key cannot be registered as an admin access key"),
1837 );
1838
1839 Ok(())
1840 })
1841 }
1842
1843 #[test]
1844 fn test_t6_root_slot_mutators_use_stored_key_row() -> eyre::Result<()> {
1845 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T6);
1846 let account = Address::random();
1847 let token = Address::random();
1848 let target = Address::random();
1849
1850 StorageCtx::enter(&mut storage, || {
1851 let mut keychain = AccountKeychain::new();
1852 keychain.initialize()?;
1853 keychain.set_tx_origin(account)?;
1854
1855 assert_key_not_found(
1856 keychain
1857 .revoke_key(account, revokeKeyCall { keyId: account })
1858 .expect_err("missing self-key row cannot be revoked"),
1859 );
1860
1861 assert_key_not_found(
1862 keychain
1863 .update_spending_limit(
1864 account,
1865 updateSpendingLimitCall {
1866 keyId: account,
1867 token,
1868 newLimit: U256::from(1),
1869 },
1870 )
1871 .expect_err("missing self-key row cannot receive spending limits"),
1872 );
1873
1874 assert_key_not_found(
1875 keychain
1876 .set_allowed_calls(
1877 account,
1878 setAllowedCallsCall {
1879 keyId: account,
1880 scopes: vec![CallScope {
1881 target,
1882 selectorRules: vec![],
1883 }],
1884 },
1885 )
1886 .expect_err("missing self-key row cannot receive call scopes"),
1887 );
1888
1889 assert_key_not_found(
1890 keychain
1891 .remove_allowed_calls(
1892 account,
1893 removeAllowedCallsCall {
1894 keyId: account,
1895 target,
1896 },
1897 )
1898 .expect_err("missing self-key row cannot remove call scopes"),
1899 );
1900
1901 keychain.keys[account][account].write(AuthorizedKey {
1902 signature_type: StoredSignatureType::Secp256k1,
1903 expiry: u64::MAX,
1904 enforce_limits: false,
1905 is_revoked: false,
1906 is_admin: false,
1907 })?;
1908
1909 keychain.update_spending_limit(
1910 account,
1911 updateSpendingLimitCall {
1912 keyId: account,
1913 token,
1914 newLimit: U256::from(1),
1915 },
1916 )?;
1917 assert_eq!(
1918 keychain.get_remaining_limit(getRemainingLimitCall {
1919 account,
1920 keyId: account,
1921 token,
1922 })?,
1923 U256::from(1)
1924 );
1925
1926 keychain.set_allowed_calls(
1927 account,
1928 setAllowedCallsCall {
1929 keyId: account,
1930 scopes: vec![CallScope {
1931 target,
1932 selectorRules: vec![],
1933 }],
1934 },
1935 )?;
1936 let allowed_calls = keychain.get_allowed_calls(getAllowedCallsCall {
1937 account,
1938 keyId: account,
1939 })?;
1940 assert!(allowed_calls.isScoped);
1941 assert_eq!(allowed_calls.scopes.len(), 1);
1942 assert_eq!(allowed_calls.scopes[0].target, target);
1943
1944 keychain.remove_allowed_calls(
1945 account,
1946 removeAllowedCallsCall {
1947 keyId: account,
1948 target,
1949 },
1950 )?;
1951 let allowed_calls = keychain.get_allowed_calls(getAllowedCallsCall {
1952 account,
1953 keyId: account,
1954 })?;
1955 assert!(allowed_calls.isScoped);
1956 assert!(allowed_calls.scopes.is_empty());
1957
1958 keychain.revoke_key(account, revokeKeyCall { keyId: account })?;
1959 assert!(keychain.keys[account][account].read()?.is_revoked);
1960 assert!(keychain.is_admin_key(account, account)?);
1961
1962 Ok(())
1963 })
1964 }
1965
1966 #[test]
1967 fn test_t6_existing_key_cannot_become_admin() -> eyre::Result<()> {
1968 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T6);
1969 let account = Address::random();
1970 let access_key = Address::random();
1971
1972 StorageCtx::enter(&mut storage, || {
1973 let mut keychain = AccountKeychain::new();
1974 keychain.initialize()?;
1975 keychain.set_tx_origin(account)?;
1976 authorize_key(
1977 &mut keychain,
1978 account,
1979 authorizeKeyCall {
1980 keyId: access_key,
1981 signatureType: SignatureType::Secp256k1,
1982 config: unrestricted_restrictions(),
1983 },
1984 )?;
1985
1986 let result =
1987 keychain.authorize_admin_key(account, access_key, SignatureType::Secp256k1, None);
1988 assert_eq!(
1989 result.expect_err("existing key must not become admin"),
1990 AccountKeychainError::key_already_exists().into()
1991 );
1992
1993 Ok(())
1994 })
1995 }
1996
1997 #[test]
1998 fn test_t5_authorize_key_with_witness_does_not_burn_and_allows_reuse() -> eyre::Result<()> {
1999 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T5);
2000 let account = Address::random();
2001 let first_key = Address::random();
2002 let second_key = Address::random();
2003 let witness = B256::ZERO;
2004
2005 StorageCtx::enter(&mut storage, || {
2006 let mut keychain = AccountKeychain::new();
2007 keychain.initialize()?;
2008 keychain.set_tx_origin(account)?;
2009
2010 authorize_key_with_witness(
2011 &mut keychain,
2012 account,
2013 authorizeKeyWithWitnessCall {
2014 keyId: first_key,
2015 signatureType: SignatureType::Secp256k1,
2016 config: unrestricted_restrictions(),
2017 witness,
2018 },
2019 )?;
2020
2021 assert!(!keychain.is_key_authorization_witness_burned(
2022 isKeyAuthorizationWitnessBurnedCall { account, witness }
2023 )?);
2024
2025 authorize_key_with_witness(
2026 &mut keychain,
2027 account,
2028 authorizeKeyWithWitnessCall {
2029 keyId: second_key,
2030 signatureType: SignatureType::Secp256k1,
2031 config: unrestricted_restrictions(),
2032 witness,
2033 },
2034 )?;
2035
2036 assert!(keychain.keys[account][second_key].read()?.expiry > 0);
2037 Ok(())
2038 })
2039 }
2040
2041 #[test]
2042 fn test_t5_burn_key_authorization_witness_blocks_later_auth() -> eyre::Result<()> {
2043 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T5);
2044 let account = Address::random();
2045 let witness = B256::repeat_byte(0x54);
2046
2047 StorageCtx::enter(&mut storage, || {
2048 let mut keychain = AccountKeychain::new();
2049 keychain.initialize()?;
2050 keychain.set_tx_origin(account)?;
2051
2052 keychain.burn_key_authorization_witness(
2053 account,
2054 burnKeyAuthorizationWitnessCall { witness },
2055 )?;
2056
2057 assert!(keychain.is_key_authorization_witness_burned(
2058 isKeyAuthorizationWitnessBurnedCall { account, witness }
2059 )?);
2060
2061 let result = authorize_key_with_witness(
2062 &mut keychain,
2063 account,
2064 authorizeKeyWithWitnessCall {
2065 keyId: Address::random(),
2066 signatureType: SignatureType::Secp256k1,
2067 config: unrestricted_restrictions(),
2068 witness,
2069 },
2070 );
2071 assert_eq!(
2072 result.expect_err("burned witness must not authorize"),
2073 AccountKeychainError::key_authorization_witness_already_burned().into()
2074 );
2075
2076 Ok(())
2077 })
2078 }
2079
2080 #[test]
2081 fn test_t5_access_key_cannot_burn_key_authorization_witness() -> eyre::Result<()> {
2082 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T5);
2083 let account = Address::random();
2084 let access_key = Address::random();
2085 let witness = B256::repeat_byte(0x55);
2086
2087 StorageCtx::enter(&mut storage, || {
2088 let mut keychain = AccountKeychain::new();
2089 keychain.initialize()?;
2090 keychain.set_tx_origin(account)?;
2091
2092 authorize_key(
2093 &mut keychain,
2094 account,
2095 authorizeKeyCall {
2096 keyId: access_key,
2097 signatureType: SignatureType::Secp256k1,
2098 config: unrestricted_restrictions(),
2099 },
2100 )?;
2101
2102 keychain.set_transaction_key(access_key)?;
2103 let result = keychain.burn_key_authorization_witness(
2104 account,
2105 burnKeyAuthorizationWitnessCall { witness },
2106 );
2107
2108 assert_unauthorized_error(
2109 result.expect_err("access key must not burn authorization witness"),
2110 );
2111
2112 assert!(!keychain.is_key_authorization_witness_burned(
2113 isKeyAuthorizationWitnessBurnedCall { account, witness }
2114 )?);
2115
2116 Ok(())
2117 })
2118 }
2119
2120 #[test]
2121 fn test_transaction_key_transient_storage() -> eyre::Result<()> {
2122 let mut storage = HashMapStorageProvider::new(1);
2123 let access_key_addr = Address::random();
2124 StorageCtx::enter(&mut storage, || {
2125 let mut keychain = AccountKeychain::new();
2126
2127 let initial_key = keychain.transaction_key.t_read()?;
2129 assert_eq!(
2130 initial_key,
2131 Address::ZERO,
2132 "Initial transaction key should be zero"
2133 );
2134
2135 keychain.set_transaction_key(access_key_addr)?;
2137
2138 let loaded_key = keychain.transaction_key.t_read()?;
2140 assert_eq!(loaded_key, access_key_addr, "Transaction key should be set");
2141
2142 let get_tx_key_call = getTransactionKeyCall {};
2144 let result = keychain.get_transaction_key(get_tx_key_call, Address::ZERO)?;
2145 assert_eq!(
2146 result, access_key_addr,
2147 "getTransactionKey should return the set key"
2148 );
2149
2150 keychain.set_transaction_key(Address::ZERO)?;
2152 let cleared_key = keychain.transaction_key.t_read()?;
2153 assert_eq!(
2154 cleared_key,
2155 Address::ZERO,
2156 "Transaction key should be cleared"
2157 );
2158
2159 Ok(())
2160 })
2161 }
2162
2163 #[test]
2164 fn test_admin_operations_blocked_with_access_key() -> eyre::Result<()> {
2165 let mut storage = HashMapStorageProvider::new(1);
2166 let msg_sender = Address::random();
2167 let existing_key = Address::random();
2168 let access_key = Address::random();
2169 let token = Address::random();
2170 let other = Address::random();
2171 StorageCtx::enter(&mut storage, || {
2172 let mut keychain = AccountKeychain::new();
2174 keychain.initialize()?;
2175
2176 keychain.set_transaction_key(Address::ZERO)?;
2178 let setup_call = authorizeKeyCall {
2179 keyId: existing_key,
2180 signatureType: SignatureType::Secp256k1,
2181 config: KeyRestrictions {
2182 expiry: u64::MAX,
2183 enforceLimits: true,
2184 limits: vec![],
2185 allowAnyCalls: true,
2186 allowedCalls: vec![],
2187 },
2188 };
2189 authorize_key(&mut keychain, msg_sender, setup_call)?;
2190
2191 keychain.set_transaction_key(access_key)?;
2193
2194 let auth_call = authorizeKeyCall {
2196 keyId: other,
2197 signatureType: SignatureType::P256,
2198 config: KeyRestrictions {
2199 expiry: u64::MAX,
2200 enforceLimits: true,
2201 limits: vec![],
2202 allowAnyCalls: true,
2203 allowedCalls: vec![],
2204 },
2205 };
2206 let auth_result = authorize_key(&mut keychain, msg_sender, auth_call);
2207 assert!(
2208 auth_result.is_err(),
2209 "authorize_key should fail when using access key"
2210 );
2211 assert_unauthorized_error(auth_result.unwrap_err());
2212
2213 let revoke_call = revokeKeyCall {
2215 keyId: existing_key,
2216 };
2217 let revoke_result = keychain.revoke_key(msg_sender, revoke_call);
2218 assert!(
2219 revoke_result.is_err(),
2220 "revoke_key should fail when using access key"
2221 );
2222 assert_unauthorized_error(revoke_result.unwrap_err());
2223
2224 let update_call = updateSpendingLimitCall {
2226 keyId: existing_key,
2227 token,
2228 newLimit: U256::from(1000),
2229 };
2230 let update_result = keychain.update_spending_limit(msg_sender, update_call);
2231 assert!(
2232 update_result.is_err(),
2233 "update_spending_limit should fail when using access key"
2234 );
2235 assert_unauthorized_error(update_result.unwrap_err());
2236
2237 Ok(())
2238 })
2239 }
2240
2241 #[test]
2242 fn test_admin_operations_require_tx_origin_on_t2() -> eyre::Result<()> {
2243 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
2244 let tx_origin = Address::random();
2245 let delegated_sender = Address::random();
2246 let existing_key = Address::random();
2247 let token = Address::random();
2248 let other = Address::random();
2249
2250 StorageCtx::enter(&mut storage, || {
2251 let mut keychain = AccountKeychain::new();
2252 keychain.initialize()?;
2253
2254 keychain
2256 .storage
2257 .set_code(delegated_sender, Bytecode::new_raw(vec![0x60, 0x00].into()))?;
2258
2259 keychain.set_transaction_key(Address::ZERO)?;
2261 keychain.set_tx_origin(delegated_sender)?;
2262 authorize_key(
2263 &mut keychain,
2264 delegated_sender,
2265 authorizeKeyCall {
2266 keyId: existing_key,
2267 signatureType: SignatureType::Secp256k1,
2268 config: KeyRestrictions {
2269 expiry: u64::MAX,
2270 enforceLimits: true,
2271 limits: vec![],
2272 allowAnyCalls: true,
2273 allowedCalls: vec![],
2274 },
2275 },
2276 )?;
2277
2278 keychain.set_tx_origin(tx_origin)?;
2280
2281 let auth_result = authorize_key(
2282 &mut keychain,
2283 delegated_sender,
2284 authorizeKeyCall {
2285 keyId: other,
2286 signatureType: SignatureType::P256,
2287 config: KeyRestrictions {
2288 expiry: u64::MAX,
2289 enforceLimits: true,
2290 limits: vec![],
2291 allowAnyCalls: true,
2292 allowedCalls: vec![],
2293 },
2294 },
2295 );
2296 assert!(auth_result.is_err());
2297 assert_unauthorized_error(auth_result.unwrap_err());
2298
2299 let revoke_result = keychain.revoke_key(
2300 delegated_sender,
2301 revokeKeyCall {
2302 keyId: existing_key,
2303 },
2304 );
2305 assert!(revoke_result.is_err());
2306 assert_unauthorized_error(revoke_result.unwrap_err());
2307
2308 let update_result = keychain.update_spending_limit(
2309 delegated_sender,
2310 updateSpendingLimitCall {
2311 keyId: existing_key,
2312 token,
2313 newLimit: U256::from(1000),
2314 },
2315 );
2316 assert!(update_result.is_err());
2317 assert_unauthorized_error(update_result.unwrap_err());
2318
2319 Ok(())
2320 })
2321 }
2322
2323 #[test]
2324 fn test_admin_operations_allow_contract_origin_on_t2() -> eyre::Result<()> {
2325 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
2326 let contract_sender = Address::random();
2327 let key_id = Address::random();
2328 let token = Address::random();
2329
2330 StorageCtx::enter(&mut storage, || {
2331 let mut keychain = AccountKeychain::new();
2332 keychain.initialize()?;
2333
2334 keychain
2335 .storage
2336 .set_code(contract_sender, Bytecode::new_raw(vec![0x60, 0x00].into()))?;
2337
2338 keychain.set_transaction_key(Address::ZERO)?;
2341 keychain.set_tx_origin(contract_sender)?;
2342
2343 authorize_key(
2344 &mut keychain,
2345 contract_sender,
2346 authorizeKeyCall {
2347 keyId: key_id,
2348 signatureType: SignatureType::Secp256k1,
2349 config: KeyRestrictions {
2350 expiry: u64::MAX,
2351 enforceLimits: true,
2352 limits: vec![TokenLimit {
2353 token,
2354 amount: U256::from(100),
2355 period: 0,
2356 }],
2357 allowAnyCalls: true,
2358 allowedCalls: vec![],
2359 },
2360 },
2361 )?;
2362
2363 keychain.update_spending_limit(
2364 contract_sender,
2365 updateSpendingLimitCall {
2366 keyId: key_id,
2367 token,
2368 newLimit: U256::from(200),
2369 },
2370 )?;
2371
2372 assert_eq!(
2373 keychain.get_remaining_limit(getRemainingLimitCall {
2374 account: contract_sender,
2375 keyId: key_id,
2376 token,
2377 })?,
2378 U256::from(200)
2379 );
2380
2381 keychain.revoke_key(contract_sender, revokeKeyCall { keyId: key_id })?;
2382
2383 let key_info = keychain.get_key(getKeyCall {
2384 account: contract_sender,
2385 keyId: key_id,
2386 })?;
2387 assert!(key_info.isRevoked);
2388
2389 Ok(())
2390 })
2391 }
2392
2393 #[test]
2394 fn test_admin_operations_allow_origin_mismatch_pre_t2() -> eyre::Result<()> {
2395 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T0);
2396 let msg_sender = Address::random();
2397 let other_origin = Address::random();
2398 let key_id = Address::random();
2399 let token = Address::random();
2400
2401 StorageCtx::enter(&mut storage, || {
2402 let mut keychain = AccountKeychain::new();
2403 keychain.initialize()?;
2404
2405 keychain.set_transaction_key(Address::ZERO)?;
2407 keychain.set_tx_origin(other_origin)?;
2408
2409 authorize_key(
2410 &mut keychain,
2411 msg_sender,
2412 authorizeKeyCall {
2413 keyId: key_id,
2414 signatureType: SignatureType::Secp256k1,
2415 config: KeyRestrictions {
2416 expiry: u64::MAX,
2417 enforceLimits: true,
2418 limits: vec![TokenLimit {
2419 token,
2420 amount: U256::from(100),
2421 period: 0,
2422 }],
2423 allowAnyCalls: true,
2424 allowedCalls: vec![],
2425 },
2426 },
2427 )?;
2428
2429 keychain.update_spending_limit(
2430 msg_sender,
2431 updateSpendingLimitCall {
2432 keyId: key_id,
2433 token,
2434 newLimit: U256::from(200),
2435 },
2436 )?;
2437
2438 keychain.revoke_key(msg_sender, revokeKeyCall { keyId: key_id })?;
2439
2440 let key_info = keychain.get_key(getKeyCall {
2441 account: msg_sender,
2442 keyId: key_id,
2443 })?;
2444 assert!(key_info.isRevoked);
2445
2446 Ok(())
2447 })
2448 }
2449
2450 #[test]
2451 fn test_admin_operations_reject_eoa_mismatch_on_t2() -> eyre::Result<()> {
2452 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
2453 let account = Address::random();
2454 let other_origin = Address::random();
2455 let key_id = Address::random();
2456 let token = Address::random();
2457
2458 StorageCtx::enter(&mut storage, || {
2459 let mut keychain = AccountKeychain::new();
2460 keychain.initialize()?;
2461
2462 keychain.set_transaction_key(Address::ZERO)?;
2464 keychain.set_tx_origin(account)?;
2465 authorize_key(
2466 &mut keychain,
2467 account,
2468 authorizeKeyCall {
2469 keyId: key_id,
2470 signatureType: SignatureType::Secp256k1,
2471 config: KeyRestrictions {
2472 expiry: u64::MAX,
2473 enforceLimits: true,
2474 limits: vec![TokenLimit {
2475 token,
2476 amount: U256::from(100),
2477 period: 0,
2478 }],
2479 allowAnyCalls: true,
2480 allowedCalls: vec![],
2481 },
2482 },
2483 )?;
2484
2485 keychain.set_tx_origin(other_origin)?;
2487 let result = keychain.update_spending_limit(
2488 account,
2489 updateSpendingLimitCall {
2490 keyId: key_id,
2491 token,
2492 newLimit: U256::from(200),
2493 },
2494 );
2495 assert!(result.is_err());
2496 assert_unauthorized_error(result.unwrap_err());
2497
2498 Ok(())
2499 })
2500 }
2501
2502 #[test]
2506 fn test_admin_operations_reject_unseeded_origin_on_t2() -> eyre::Result<()> {
2507 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
2508 let account = Address::random();
2509 let key_id = Address::random();
2510 let other_key = Address::random();
2511 let token = Address::random();
2512
2513 StorageCtx::enter(&mut storage, || {
2514 let mut keychain = AccountKeychain::new();
2515 keychain.initialize()?;
2516
2517 keychain.set_transaction_key(Address::ZERO)?;
2519 keychain.set_tx_origin(account)?;
2520 authorize_key(
2521 &mut keychain,
2522 account,
2523 authorizeKeyCall {
2524 keyId: key_id,
2525 signatureType: SignatureType::Secp256k1,
2526 config: KeyRestrictions {
2527 expiry: u64::MAX,
2528 enforceLimits: true,
2529 limits: vec![TokenLimit {
2530 token,
2531 amount: U256::from(100),
2532 period: 0,
2533 }],
2534 allowAnyCalls: true,
2535 allowedCalls: vec![],
2536 },
2537 },
2538 )?;
2539
2540 keychain.set_tx_origin(Address::ZERO)?;
2543
2544 let auth_result = authorize_key(
2546 &mut keychain,
2547 account,
2548 authorizeKeyCall {
2549 keyId: other_key,
2550 signatureType: SignatureType::P256,
2551 config: KeyRestrictions {
2552 expiry: u64::MAX,
2553 enforceLimits: false,
2554 limits: vec![],
2555 allowAnyCalls: true,
2556 allowedCalls: vec![],
2557 },
2558 },
2559 );
2560 assert!(
2561 auth_result.is_err(),
2562 "authorize_key must reject when tx_origin is not seeded on T2"
2563 );
2564 assert_unauthorized_error(auth_result.unwrap_err());
2565
2566 let revoke_result = keychain.revoke_key(account, revokeKeyCall { keyId: key_id });
2568 assert!(
2569 revoke_result.is_err(),
2570 "revoke_key must reject when tx_origin is not seeded on T2"
2571 );
2572 assert_unauthorized_error(revoke_result.unwrap_err());
2573
2574 let update_result = keychain.update_spending_limit(
2576 account,
2577 updateSpendingLimitCall {
2578 keyId: key_id,
2579 token,
2580 newLimit: U256::from(200),
2581 },
2582 );
2583 assert!(
2584 update_result.is_err(),
2585 "update_spending_limit must reject when tx_origin is not seeded on T2"
2586 );
2587 assert_unauthorized_error(update_result.unwrap_err());
2588
2589 Ok(())
2590 })
2591 }
2592
2593 #[test]
2594 fn test_replay_protection_revoked_key_cannot_be_reauthorized() -> eyre::Result<()> {
2595 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
2596 let account = Address::random();
2597 let key_id = Address::random();
2598 let token = Address::random();
2599 StorageCtx::enter(&mut storage, || {
2600 let mut keychain = AccountKeychain::new();
2601 keychain.initialize()?;
2602
2603 keychain.set_transaction_key(Address::ZERO)?;
2605 keychain.set_tx_origin(account)?;
2606
2607 let auth_call = authorizeKeyCall {
2609 keyId: key_id,
2610 signatureType: SignatureType::Secp256k1,
2611 config: KeyRestrictions {
2612 expiry: u64::MAX,
2613 enforceLimits: true,
2614 limits: vec![TokenLimit {
2615 token,
2616 amount: U256::from(100),
2617 period: 0,
2618 }],
2619 allowAnyCalls: true,
2620 allowedCalls: vec![],
2621 },
2622 };
2623 authorize_key(&mut keychain, account, auth_call.clone())?;
2624
2625 let key_info = keychain.get_key(getKeyCall {
2627 account,
2628 keyId: key_id,
2629 })?;
2630 assert_eq!(key_info.expiry, u64::MAX);
2631 assert!(!key_info.isRevoked);
2632 assert_eq!(
2633 keychain.get_remaining_limit(getRemainingLimitCall {
2634 account,
2635 keyId: key_id,
2636 token,
2637 })?,
2638 U256::from(100)
2639 );
2640
2641 let revoke_call = revokeKeyCall { keyId: key_id };
2643 keychain.revoke_key(account, revoke_call)?;
2644
2645 let key_info = keychain.get_key(getKeyCall {
2647 account,
2648 keyId: key_id,
2649 })?;
2650 assert_eq!(key_info.expiry, 0);
2651 assert!(key_info.isRevoked);
2652 assert_eq!(
2653 keychain.get_remaining_limit(getRemainingLimitCall {
2654 account,
2655 keyId: key_id,
2656 token,
2657 })?,
2658 U256::ZERO
2659 );
2660
2661 let replay_result = authorize_key(&mut keychain, account, auth_call);
2664 assert!(
2665 replay_result.is_err(),
2666 "Re-authorizing a revoked key should fail"
2667 );
2668
2669 match replay_result.unwrap_err() {
2671 TempoPrecompileError::AccountKeychainError(e) => {
2672 assert!(
2673 matches!(e, AccountKeychainError::KeyAlreadyRevoked(_)),
2674 "Expected KeyAlreadyRevoked error, got: {e:?}"
2675 );
2676 }
2677 e => panic!("Expected AccountKeychainError, got: {e:?}"),
2678 }
2679 Ok(())
2680 })
2681 }
2682
2683 #[test]
2684 fn test_authorize_key_rejects_expiry_in_past() -> eyre::Result<()> {
2685 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T0);
2687 let account = Address::random();
2688 let key_id = Address::random();
2689 StorageCtx::enter(&mut storage, || {
2690 let mut keychain = AccountKeychain::new();
2691 keychain.initialize()?;
2692
2693 keychain.set_transaction_key(Address::ZERO)?;
2695
2696 let auth_call = authorizeKeyCall {
2698 keyId: key_id,
2699 signatureType: SignatureType::Secp256k1,
2700 config: KeyRestrictions {
2701 expiry: 0, enforceLimits: false,
2703 limits: vec![],
2704 allowAnyCalls: true,
2705 allowedCalls: vec![],
2706 },
2707 };
2708 let result = authorize_key(&mut keychain, account, auth_call);
2709 assert!(
2710 result.is_err(),
2711 "Authorizing with expiry in past should fail"
2712 );
2713
2714 match result.unwrap_err() {
2716 TempoPrecompileError::AccountKeychainError(e) => {
2717 assert!(
2718 matches!(e, AccountKeychainError::ExpiryInPast(_)),
2719 "Expected ExpiryInPast error, got: {e:?}"
2720 );
2721 }
2722 e => panic!("Expected AccountKeychainError, got: {e:?}"),
2723 }
2724
2725 let auth_call_past = authorizeKeyCall {
2727 keyId: key_id,
2728 signatureType: SignatureType::Secp256k1,
2729 config: KeyRestrictions {
2730 expiry: 1, enforceLimits: false,
2732 limits: vec![],
2733 allowAnyCalls: true,
2734 allowedCalls: vec![],
2735 },
2736 };
2737 let result_past = authorize_key(&mut keychain, account, auth_call_past);
2738 assert!(
2739 matches!(
2740 result_past,
2741 Err(TempoPrecompileError::AccountKeychainError(
2742 AccountKeychainError::ExpiryInPast(_)
2743 ))
2744 ),
2745 "Expected ExpiryInPast error for past expiry, got: {result_past:?}"
2746 );
2747
2748 Ok(())
2749 })
2750 }
2751
2752 #[test]
2753 fn test_pre_t3_authorize_key_rejects_tip_1011_fields_without_writing_key() -> eyre::Result<()> {
2754 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T1C);
2755 let account = Address::random();
2756 let key_id = Address::random();
2757 let token = Address::random();
2758
2759 StorageCtx::enter(&mut storage, || {
2760 let mut keychain = AccountKeychain::new();
2761 keychain.initialize()?;
2762 keychain.set_transaction_key(Address::ZERO)?;
2763
2764 let result = authorize_key(
2765 &mut keychain,
2766 account,
2767 authorizeKeyCall {
2768 keyId: key_id,
2769 signatureType: SignatureType::Secp256k1,
2770 config: KeyRestrictions {
2771 expiry: u64::MAX,
2772 enforceLimits: true,
2773 limits: vec![TokenLimit {
2774 token,
2775 amount: U256::from(100u64),
2776 period: 60,
2777 }],
2778 allowAnyCalls: true,
2779 allowedCalls: vec![],
2780 },
2781 },
2782 );
2783
2784 assert!(
2785 matches!(
2786 result,
2787 Err(TempoPrecompileError::AccountKeychainError(
2788 AccountKeychainError::InvalidSpendingLimit(_)
2789 ))
2790 ),
2791 "expected InvalidSpendingLimit, got {result:?}"
2792 );
2793
2794 assert_eq!(
2795 keychain.keys[account][key_id].read()?,
2796 AuthorizedKey::default(),
2797 "pre-T3 invalid TIP-1011 fields must not leave behind a key"
2798 );
2799
2800 let limit_key = AccountKeychain::spending_limit_key(account, key_id);
2801 assert_eq!(
2802 keychain.spending_limits[limit_key][token].read()?,
2803 SpendingLimitState::default(),
2804 "pre-T3 invalid TIP-1011 fields must not initialize limits"
2805 );
2806
2807 Ok(())
2808 })
2809 }
2810
2811 #[test]
2812 fn test_different_key_id_can_be_authorized_after_revocation() -> eyre::Result<()> {
2813 let mut storage = HashMapStorageProvider::new(1);
2814 let account = Address::random();
2815 let key_id_1 = Address::random();
2816 let key_id_2 = Address::random();
2817 StorageCtx::enter(&mut storage, || {
2818 let mut keychain = AccountKeychain::new();
2819 keychain.initialize()?;
2820
2821 keychain.set_transaction_key(Address::ZERO)?;
2823
2824 let auth_call_1 = authorizeKeyCall {
2826 keyId: key_id_1,
2827 signatureType: SignatureType::Secp256k1,
2828 config: KeyRestrictions {
2829 expiry: u64::MAX,
2830 enforceLimits: false,
2831 limits: vec![],
2832 allowAnyCalls: true,
2833 allowedCalls: vec![],
2834 },
2835 };
2836 authorize_key(&mut keychain, account, auth_call_1)?;
2837
2838 keychain.revoke_key(account, revokeKeyCall { keyId: key_id_1 })?;
2840
2841 let auth_call_2 = authorizeKeyCall {
2843 keyId: key_id_2,
2844 signatureType: SignatureType::P256,
2845 config: KeyRestrictions {
2846 expiry: u64::MAX,
2847 enforceLimits: true,
2848 limits: vec![],
2849 allowAnyCalls: true,
2850 allowedCalls: vec![],
2851 },
2852 };
2853 authorize_key(&mut keychain, account, auth_call_2)?;
2854
2855 let key_info = keychain.get_key(getKeyCall {
2857 account,
2858 keyId: key_id_2,
2859 })?;
2860 assert_eq!(key_info.expiry, u64::MAX);
2861 assert!(!key_info.isRevoked);
2862
2863 Ok(())
2864 })
2865 }
2866
2867 #[test]
2868 fn test_authorize_approve() -> eyre::Result<()> {
2869 let mut storage = HashMapStorageProvider::new(1);
2870
2871 let eoa = Address::random();
2872 let access_key = Address::random();
2873 let token = Address::random();
2874 let contract = Address::random();
2875
2876 StorageCtx::enter(&mut storage, || {
2877 let mut keychain = AccountKeychain::new();
2878 keychain.initialize()?;
2879
2880 keychain.set_transaction_key(Address::ZERO)?;
2882 keychain.set_tx_origin(eoa)?;
2883
2884 let auth_call = authorizeKeyCall {
2885 keyId: access_key,
2886 signatureType: SignatureType::Secp256k1,
2887 config: KeyRestrictions {
2888 expiry: u64::MAX,
2889 enforceLimits: true,
2890 limits: vec![TokenLimit {
2891 token,
2892 amount: U256::from(100),
2893 period: 0,
2894 }],
2895 allowAnyCalls: true,
2896 allowedCalls: vec![],
2897 },
2898 };
2899 authorize_key(&mut keychain, eoa, auth_call)?;
2900
2901 let initial_limit = keychain.get_remaining_limit(getRemainingLimitCall {
2902 account: eoa,
2903 keyId: access_key,
2904 token,
2905 })?;
2906 assert_eq!(initial_limit, U256::from(100));
2907
2908 keychain.set_transaction_key(access_key)?;
2910
2911 keychain.authorize_approve(eoa, token, U256::ZERO, U256::from(30))?;
2913
2914 let limit_after = keychain.get_remaining_limit(getRemainingLimitCall {
2915 account: eoa,
2916 keyId: access_key,
2917 token,
2918 })?;
2919 assert_eq!(limit_after, U256::from(70));
2920
2921 keychain.authorize_approve(eoa, token, U256::from(30), U256::from(20))?;
2923
2924 let limit_unchanged = keychain.get_remaining_limit(getRemainingLimitCall {
2925 account: eoa,
2926 keyId: access_key,
2927 token,
2928 })?;
2929 assert_eq!(limit_unchanged, U256::from(70));
2930
2931 keychain.authorize_approve(eoa, token, U256::from(20), U256::from(50))?;
2933
2934 let limit_after_increase = keychain.get_remaining_limit(getRemainingLimitCall {
2935 account: eoa,
2936 keyId: access_key,
2937 token,
2938 })?;
2939 assert_eq!(limit_after_increase, U256::from(40));
2940
2941 keychain.authorize_approve(contract, token, U256::ZERO, U256::from(1000))?;
2943
2944 let limit_after_contract = keychain.get_remaining_limit(getRemainingLimitCall {
2945 account: eoa,
2946 keyId: access_key,
2947 token,
2948 })?;
2949 assert_eq!(limit_after_contract, U256::from(40)); let exceed_result = keychain.authorize_approve(eoa, token, U256::ZERO, U256::from(50));
2953 assert!(matches!(
2954 exceed_result,
2955 Err(TempoPrecompileError::AccountKeychainError(
2956 AccountKeychainError::SpendingLimitExceeded(_)
2957 ))
2958 ));
2959
2960 keychain.set_transaction_key(Address::ZERO)?;
2962 keychain.authorize_approve(eoa, token, U256::ZERO, U256::from(1000))?;
2963
2964 let limit_main_key = keychain.get_remaining_limit(getRemainingLimitCall {
2965 account: eoa,
2966 keyId: access_key,
2967 token,
2968 })?;
2969 assert_eq!(limit_main_key, U256::from(40));
2970
2971 Ok(())
2972 })
2973 }
2974
2975 #[test]
2985 fn test_spending_limits_only_apply_to_tx_origin() -> eyre::Result<()> {
2986 let mut storage = HashMapStorageProvider::new(1);
2987
2988 let eoa_alice = Address::random(); let access_key = Address::random(); let contract_address = Address::random(); let token = Address::random();
2992
2993 StorageCtx::enter(&mut storage, || {
2994 let mut keychain = AccountKeychain::new();
2995 keychain.initialize()?;
2996
2997 keychain.set_transaction_key(Address::ZERO)?; keychain.set_tx_origin(eoa_alice)?;
3000
3001 let auth_call = authorizeKeyCall {
3002 keyId: access_key,
3003 signatureType: SignatureType::Secp256k1,
3004 config: KeyRestrictions {
3005 expiry: u64::MAX,
3006 enforceLimits: true,
3007 limits: vec![TokenLimit {
3008 token,
3009 amount: U256::from(100),
3010 period: 0,
3011 }],
3012 allowAnyCalls: true,
3013 allowedCalls: vec![],
3014 },
3015 };
3016 authorize_key(&mut keychain, eoa_alice, auth_call)?;
3017
3018 let limit = keychain.get_remaining_limit(getRemainingLimitCall {
3020 account: eoa_alice,
3021 keyId: access_key,
3022 token,
3023 })?;
3024 assert_eq!(
3025 limit,
3026 U256::from(100),
3027 "Initial spending limit should be 100"
3028 );
3029
3030 keychain.set_transaction_key(access_key)?;
3032 keychain.set_tx_origin(eoa_alice)?;
3033
3034 keychain.authorize_transfer(eoa_alice, token, U256::from(30))?;
3037
3038 let limit_after = keychain.get_remaining_limit(getRemainingLimitCall {
3039 account: eoa_alice,
3040 keyId: access_key,
3041 token,
3042 })?;
3043 assert_eq!(
3044 limit_after,
3045 U256::from(70),
3046 "Spending limit should be reduced to 70 after Alice's direct transfer"
3047 );
3048
3049 keychain.authorize_transfer(contract_address, token, U256::from(1000))?;
3052
3053 let limit_unchanged = keychain.get_remaining_limit(getRemainingLimitCall {
3054 account: eoa_alice,
3055 keyId: access_key,
3056 token,
3057 })?;
3058 assert_eq!(
3059 limit_unchanged,
3060 U256::from(70),
3061 "Spending limit should remain 70 - contract transfer doesn't affect Alice's limit"
3062 );
3063
3064 keychain.authorize_transfer(eoa_alice, token, U256::from(70))?;
3066
3067 let limit_depleted = keychain.get_remaining_limit(getRemainingLimitCall {
3068 account: eoa_alice,
3069 keyId: access_key,
3070 token,
3071 })?;
3072 assert_eq!(
3073 limit_depleted,
3074 U256::ZERO,
3075 "Spending limit should be depleted after Alice spends remaining 70"
3076 );
3077
3078 let exceed_result = keychain.authorize_transfer(eoa_alice, token, U256::from(1));
3080 assert!(
3081 exceed_result.is_err(),
3082 "Should fail when Alice tries to exceed spending limit"
3083 );
3084
3085 let contract_result =
3087 keychain.authorize_transfer(contract_address, token, U256::from(999999));
3088 assert!(
3089 contract_result.is_ok(),
3090 "Contract should still be able to transfer even though Alice's limit is depleted"
3091 );
3092
3093 Ok(())
3094 })
3095 }
3096
3097 #[test]
3098 fn test_authorize_key_rejects_existing_key_boundary() -> eyre::Result<()> {
3099 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::Genesis);
3101 let account = Address::random();
3102 let key_id = Address::random();
3103 StorageCtx::enter(&mut storage, || {
3104 let mut keychain = AccountKeychain::new();
3105 keychain.initialize()?;
3106 keychain.set_transaction_key(Address::ZERO)?;
3107
3108 let auth_call = authorizeKeyCall {
3110 keyId: key_id,
3111 signatureType: SignatureType::Secp256k1,
3112 config: KeyRestrictions {
3113 expiry: 1, enforceLimits: false,
3115 limits: vec![],
3116 allowAnyCalls: true,
3117 allowedCalls: vec![],
3118 },
3119 };
3120 authorize_key(&mut keychain, account, auth_call.clone())?;
3121
3122 let key_info = keychain.get_key(getKeyCall {
3124 account,
3125 keyId: key_id,
3126 })?;
3127 assert_eq!(key_info.expiry, 1, "Key should have expiry = 1");
3128
3129 let result = authorize_key(&mut keychain, account, auth_call);
3131 assert!(result.is_err(), "Should reject when key.expiry > 0");
3132 match result.unwrap_err() {
3133 TempoPrecompileError::AccountKeychainError(e) => {
3134 assert!(
3135 matches!(e, AccountKeychainError::KeyAlreadyExists(_)),
3136 "Expected KeyAlreadyExists, got: {e:?}"
3137 );
3138 }
3139 e => panic!("Expected AccountKeychainError, got: {e:?}"),
3140 }
3141
3142 Ok(())
3143 })
3144 }
3145
3146 #[test]
3147 fn test_spending_limit_key_derivation() {
3148 let account1 = Address::repeat_byte(0x01);
3149 let account2 = Address::repeat_byte(0x02);
3150 let key_id1 = Address::repeat_byte(0xAA);
3151 let key_id2 = Address::repeat_byte(0xBB);
3152
3153 let hash1a = AccountKeychain::spending_limit_key(account1, key_id1);
3155 let hash1b = AccountKeychain::spending_limit_key(account1, key_id1);
3156 assert_eq!(hash1a, hash1b, "Same inputs must produce same hash");
3157
3158 let hash2 = AccountKeychain::spending_limit_key(account2, key_id1);
3160 assert_ne!(
3161 hash1a, hash2,
3162 "Different accounts must produce different hashes"
3163 );
3164
3165 let hash3 = AccountKeychain::spending_limit_key(account1, key_id2);
3167 assert_ne!(
3168 hash1a, hash3,
3169 "Different key_ids must produce different hashes"
3170 );
3171
3172 let hash_swapped = AccountKeychain::spending_limit_key(key_id1, account1);
3175 assert_ne!(
3176 hash1a, hash_swapped,
3177 "Swapped order must produce different hash"
3178 );
3179
3180 assert_ne!(hash1a, B256::ZERO, "Hash should not be zero");
3182 }
3183
3184 #[test]
3185 fn test_initialize_sets_up_storage_state() -> eyre::Result<()> {
3186 let mut storage = HashMapStorageProvider::new(1);
3187 StorageCtx::enter(&mut storage, || {
3188 let mut keychain = AccountKeychain::new();
3189
3190 keychain.initialize()?;
3192
3193 keychain.set_transaction_key(Address::ZERO)?;
3195
3196 let account = Address::random();
3197 let key_id = Address::random();
3198 let auth_call = authorizeKeyCall {
3199 keyId: key_id,
3200 signatureType: SignatureType::Secp256k1,
3201 config: KeyRestrictions {
3202 expiry: u64::MAX,
3203 enforceLimits: false,
3204 limits: vec![],
3205 allowAnyCalls: true,
3206 allowedCalls: vec![],
3207 },
3208 };
3209 authorize_key(&mut keychain, account, auth_call)?;
3211
3212 let key_info = keychain.get_key(getKeyCall {
3214 account,
3215 keyId: key_id,
3216 })?;
3217 assert_eq!(key_info.expiry, u64::MAX, "Key should be stored after init");
3218
3219 Ok(())
3220 })
3221 }
3222
3223 #[test]
3224 fn test_authorize_key_webauthn_signature_type() -> eyre::Result<()> {
3225 let mut storage = HashMapStorageProvider::new(1);
3226 let account = Address::random();
3227 let key_id = Address::random();
3228 StorageCtx::enter(&mut storage, || {
3229 let mut keychain = AccountKeychain::new();
3230 keychain.initialize()?;
3231 keychain.set_transaction_key(Address::ZERO)?;
3232
3233 let auth_call = authorizeKeyCall {
3235 keyId: key_id,
3236 signatureType: SignatureType::WebAuthn,
3237 config: KeyRestrictions {
3238 expiry: u64::MAX,
3239 enforceLimits: false,
3240 limits: vec![],
3241 allowAnyCalls: true,
3242 allowedCalls: vec![],
3243 },
3244 };
3245 authorize_key(&mut keychain, account, auth_call)?;
3246
3247 let key_info = keychain.get_key(getKeyCall {
3249 account,
3250 keyId: key_id,
3251 })?;
3252 assert_eq!(
3253 key_info.signatureType,
3254 SignatureType::WebAuthn,
3255 "Signature type should be WebAuthn"
3256 );
3257
3258 let result = keychain.validate_keychain_authorization(account, key_id, 0, Some(2));
3260 assert!(
3261 result.is_ok(),
3262 "WebAuthn (type 2) validation should succeed"
3263 );
3264
3265 let mismatch = keychain.validate_keychain_authorization(account, key_id, 0, Some(0));
3267 assert!(mismatch.is_err(), "Secp256k1 should not match WebAuthn key");
3268
3269 Ok(())
3270 })
3271 }
3272
3273 #[test]
3274 fn test_update_spending_limit_expiry_boundary() -> eyre::Result<()> {
3275 let mut storage = HashMapStorageProvider::new(1);
3276 let account = Address::random();
3277 let key_id = Address::random();
3278 let token = Address::random();
3279 StorageCtx::enter(&mut storage, || {
3280 let mut keychain = AccountKeychain::new();
3281 keychain.initialize()?;
3282 keychain.set_transaction_key(Address::ZERO)?;
3283
3284 let auth_call = authorizeKeyCall {
3286 keyId: key_id,
3287 signatureType: SignatureType::Secp256k1,
3288 config: KeyRestrictions {
3289 expiry: u64::MAX,
3290 enforceLimits: true,
3291 limits: vec![TokenLimit {
3292 token,
3293 amount: U256::from(100),
3294 period: 0,
3295 }],
3296 allowAnyCalls: true,
3297 allowedCalls: vec![],
3298 },
3299 };
3300 authorize_key(&mut keychain, account, auth_call)?;
3301
3302 let update_call = updateSpendingLimitCall {
3304 keyId: key_id,
3305 token,
3306 newLimit: U256::from(200),
3307 };
3308 let result = keychain.update_spending_limit(account, update_call);
3309 assert!(
3310 result.is_ok(),
3311 "Update should succeed when key not expired: {result:?}"
3312 );
3313
3314 let limit = keychain.get_remaining_limit(getRemainingLimitCall {
3316 account,
3317 keyId: key_id,
3318 token,
3319 })?;
3320 assert_eq!(limit, U256::from(200), "Limit should be updated to 200");
3321
3322 Ok(())
3323 })
3324 }
3325
3326 #[test]
3327 fn test_update_spending_limit_enforce_limits_toggle() -> eyre::Result<()> {
3328 let mut storage = HashMapStorageProvider::new(1);
3329 let account = Address::random();
3330 let key_id = Address::random();
3331 let token = Address::random();
3332 StorageCtx::enter(&mut storage, || {
3333 let mut keychain = AccountKeychain::new();
3334 keychain.initialize()?;
3335 keychain.set_transaction_key(Address::ZERO)?;
3336
3337 let auth_call = authorizeKeyCall {
3339 keyId: key_id,
3340 signatureType: SignatureType::Secp256k1,
3341 config: KeyRestrictions {
3342 expiry: u64::MAX,
3343 enforceLimits: false, limits: vec![],
3345 allowAnyCalls: true,
3346 allowedCalls: vec![],
3347 },
3348 };
3349 authorize_key(&mut keychain, account, auth_call)?;
3350
3351 let key_before = keychain.get_key(getKeyCall {
3353 account,
3354 keyId: key_id,
3355 })?;
3356 assert!(
3357 !key_before.enforceLimits,
3358 "Key should start with enforce_limits=false"
3359 );
3360
3361 let update_call = updateSpendingLimitCall {
3363 keyId: key_id,
3364 token,
3365 newLimit: U256::from(500),
3366 };
3367 keychain.update_spending_limit(account, update_call)?;
3368
3369 let key_after = keychain.get_key(getKeyCall {
3371 account,
3372 keyId: key_id,
3373 })?;
3374 assert!(
3375 key_after.enforceLimits,
3376 "enforce_limits should be true after update"
3377 );
3378
3379 let limit = keychain.get_remaining_limit(getRemainingLimitCall {
3381 account,
3382 keyId: key_id,
3383 token,
3384 })?;
3385 assert_eq!(limit, U256::from(500), "Spending limit should be 500");
3386
3387 Ok(())
3388 })
3389 }
3390
3391 #[test]
3392 fn test_get_key_or_logic_existence_check() -> eyre::Result<()> {
3393 let mut storage = HashMapStorageProvider::new(1);
3394 let account = Address::random();
3395 let key_id_revoked = Address::random();
3396 let key_id_valid = Address::random();
3397 let key_id_never_existed = Address::random();
3398 StorageCtx::enter(&mut storage, || {
3399 let mut keychain = AccountKeychain::new();
3400 keychain.initialize()?;
3401 keychain.set_transaction_key(Address::ZERO)?;
3402
3403 let auth_call = authorizeKeyCall {
3405 keyId: key_id_revoked,
3406 signatureType: SignatureType::P256,
3407 config: KeyRestrictions {
3408 expiry: u64::MAX,
3409 enforceLimits: false,
3410 limits: vec![],
3411 allowAnyCalls: true,
3412 allowedCalls: vec![],
3413 },
3414 };
3415 authorize_key(&mut keychain, account, auth_call)?;
3416 keychain.revoke_key(
3417 account,
3418 revokeKeyCall {
3419 keyId: key_id_revoked,
3420 },
3421 )?;
3422
3423 let auth_valid = authorizeKeyCall {
3425 keyId: key_id_valid,
3426 signatureType: SignatureType::Secp256k1,
3427 config: KeyRestrictions {
3428 expiry: u64::MAX,
3429 enforceLimits: false,
3430 limits: vec![],
3431 allowAnyCalls: true,
3432 allowedCalls: vec![],
3433 },
3434 };
3435 authorize_key(&mut keychain, account, auth_valid)?;
3436
3437 let revoked_info = keychain.get_key(getKeyCall {
3439 account,
3440 keyId: key_id_revoked,
3441 })?;
3442 assert_eq!(
3443 revoked_info.keyId,
3444 Address::ZERO,
3445 "Revoked key should return zero keyId"
3446 );
3447 assert!(
3448 revoked_info.isRevoked,
3449 "Revoked key should have isRevoked=true"
3450 );
3451
3452 let never_info = keychain.get_key(getKeyCall {
3454 account,
3455 keyId: key_id_never_existed,
3456 })?;
3457 assert_eq!(
3458 never_info.keyId,
3459 Address::ZERO,
3460 "Non-existent key should return zero keyId"
3461 );
3462 assert_eq!(
3463 never_info.expiry, 0,
3464 "Non-existent key should have expiry=0"
3465 );
3466
3467 let valid_info = keychain.get_key(getKeyCall {
3469 account,
3470 keyId: key_id_valid,
3471 })?;
3472 assert_eq!(
3473 valid_info.keyId, key_id_valid,
3474 "Valid key should return actual keyId"
3475 );
3476 assert_eq!(
3477 valid_info.expiry,
3478 u64::MAX,
3479 "Valid key should have correct expiry"
3480 );
3481 assert!(!valid_info.isRevoked, "Valid key should not be revoked");
3482
3483 Ok(())
3484 })
3485 }
3486
3487 #[test]
3488 fn test_get_key_signature_type_match_arms() -> eyre::Result<()> {
3489 let mut storage = HashMapStorageProvider::new(1);
3490 let account = Address::random();
3491 let key_secp = Address::random();
3492 let key_p256 = Address::random();
3493 let key_webauthn = Address::random();
3494 StorageCtx::enter(&mut storage, || {
3495 let mut keychain = AccountKeychain::new();
3496 keychain.initialize()?;
3497 keychain.set_transaction_key(Address::ZERO)?;
3498
3499 authorize_key(
3501 &mut keychain,
3502 account,
3503 authorizeKeyCall {
3504 keyId: key_secp,
3505 signatureType: SignatureType::Secp256k1, config: KeyRestrictions {
3507 expiry: u64::MAX,
3508 enforceLimits: false,
3509 limits: vec![],
3510 allowAnyCalls: true,
3511 allowedCalls: vec![],
3512 },
3513 },
3514 )?;
3515
3516 authorize_key(
3517 &mut keychain,
3518 account,
3519 authorizeKeyCall {
3520 keyId: key_p256,
3521 signatureType: SignatureType::P256, config: KeyRestrictions {
3523 expiry: u64::MAX,
3524 enforceLimits: false,
3525 limits: vec![],
3526 allowAnyCalls: true,
3527 allowedCalls: vec![],
3528 },
3529 },
3530 )?;
3531
3532 authorize_key(
3533 &mut keychain,
3534 account,
3535 authorizeKeyCall {
3536 keyId: key_webauthn,
3537 signatureType: SignatureType::WebAuthn, config: KeyRestrictions {
3539 expiry: u64::MAX,
3540 enforceLimits: false,
3541 limits: vec![],
3542 allowAnyCalls: true,
3543 allowedCalls: vec![],
3544 },
3545 },
3546 )?;
3547
3548 let secp_info = keychain.get_key(getKeyCall {
3550 account,
3551 keyId: key_secp,
3552 })?;
3553 assert_eq!(
3554 secp_info.signatureType,
3555 SignatureType::Secp256k1,
3556 "Secp256k1 key should return Secp256k1"
3557 );
3558
3559 let p256_info = keychain.get_key(getKeyCall {
3560 account,
3561 keyId: key_p256,
3562 })?;
3563 assert_eq!(
3564 p256_info.signatureType,
3565 SignatureType::P256,
3566 "P256 key should return P256"
3567 );
3568
3569 let webauthn_info = keychain.get_key(getKeyCall {
3570 account,
3571 keyId: key_webauthn,
3572 })?;
3573 assert_eq!(
3574 webauthn_info.signatureType,
3575 SignatureType::WebAuthn,
3576 "WebAuthn key should return WebAuthn"
3577 );
3578
3579 assert_ne!(secp_info.signatureType, p256_info.signatureType);
3581 assert_ne!(secp_info.signatureType, webauthn_info.signatureType);
3582 assert_ne!(p256_info.signatureType, webauthn_info.signatureType);
3583
3584 Ok(())
3585 })
3586 }
3587
3588 #[test]
3589 fn test_validate_keychain_authorization_checks_signature_type() -> eyre::Result<()> {
3590 let mut storage = HashMapStorageProvider::new(1);
3591 let account = Address::random();
3592 let key_id = Address::random();
3593 StorageCtx::enter(&mut storage, || {
3594 let mut keychain = AccountKeychain::new();
3595 keychain.initialize()?;
3596
3597 keychain.set_transaction_key(Address::ZERO)?;
3599
3600 let auth_call = authorizeKeyCall {
3602 keyId: key_id,
3603 signatureType: SignatureType::P256,
3604 config: KeyRestrictions {
3605 expiry: u64::MAX,
3606 enforceLimits: false,
3607 limits: vec![],
3608 allowAnyCalls: true,
3609 allowedCalls: vec![],
3610 },
3611 };
3612 authorize_key(&mut keychain, account, auth_call)?;
3613
3614 let result = keychain.validate_keychain_authorization(account, key_id, 0, Some(1));
3616 assert!(
3617 result.is_ok(),
3618 "Validation should succeed with matching signature type"
3619 );
3620
3621 let mismatch_result =
3623 keychain.validate_keychain_authorization(account, key_id, 0, Some(0));
3624 assert!(
3625 mismatch_result.is_err(),
3626 "Validation should fail with mismatched signature type"
3627 );
3628 match mismatch_result.unwrap_err() {
3629 TempoPrecompileError::AccountKeychainError(e) => {
3630 assert!(
3631 matches!(e, AccountKeychainError::SignatureTypeMismatch(_)),
3632 "Expected SignatureTypeMismatch error, got: {e:?}"
3633 );
3634 }
3635 e => panic!("Expected AccountKeychainError, got: {e:?}"),
3636 }
3637
3638 let webauthn_mismatch =
3640 keychain.validate_keychain_authorization(account, key_id, 0, Some(2));
3641 assert!(
3642 webauthn_mismatch.is_err(),
3643 "Validation should fail with WebAuthn when key is P256"
3644 );
3645
3646 let none_result = keychain.validate_keychain_authorization(account, key_id, 0, None);
3648 assert!(
3649 none_result.is_ok(),
3650 "Validation should succeed when signature type check is skipped (pre-T1)"
3651 );
3652
3653 Ok(())
3654 })
3655 }
3656
3657 #[test]
3658 fn test_refund_spending_limit_restores_limit() -> eyre::Result<()> {
3659 let mut storage = HashMapStorageProvider::new(1);
3660 let eoa = Address::random();
3661 let access_key = Address::random();
3662 let token = Address::random();
3663
3664 StorageCtx::enter(&mut storage, || {
3665 let mut keychain = AccountKeychain::new();
3666 keychain.initialize()?;
3667
3668 keychain.set_transaction_key(Address::ZERO)?;
3669
3670 let auth_call = authorizeKeyCall {
3671 keyId: access_key,
3672 signatureType: SignatureType::Secp256k1,
3673 config: KeyRestrictions {
3674 expiry: u64::MAX,
3675 enforceLimits: true,
3676 limits: vec![TokenLimit {
3677 token,
3678 amount: U256::from(100),
3679 period: 0,
3680 }],
3681 allowAnyCalls: true,
3682 allowedCalls: vec![],
3683 },
3684 };
3685 authorize_key(&mut keychain, eoa, auth_call)?;
3686
3687 keychain.set_transaction_key(access_key)?;
3688 keychain.set_tx_origin(eoa)?;
3689
3690 keychain.authorize_transfer(eoa, token, U256::from(60))?;
3691
3692 let remaining = keychain.get_remaining_limit(getRemainingLimitCall {
3693 account: eoa,
3694 keyId: access_key,
3695 token,
3696 })?;
3697 assert_eq!(remaining, U256::from(40));
3698
3699 keychain.refund_spending_limit(eoa, token, U256::from(25))?;
3700
3701 let after_refund = keychain.get_remaining_limit(getRemainingLimitCall {
3702 account: eoa,
3703 keyId: access_key,
3704 token,
3705 })?;
3706 assert_eq!(after_refund, U256::from(65));
3707
3708 Ok(())
3709 })
3710 }
3711
3712 #[test]
3713 fn test_refund_spending_limit_noop_for_main_key() -> eyre::Result<()> {
3714 let mut storage = HashMapStorageProvider::new(1);
3715 let eoa = Address::random();
3716 let token = Address::random();
3717
3718 StorageCtx::enter(&mut storage, || {
3719 let mut keychain = AccountKeychain::new();
3720 keychain.initialize()?;
3721
3722 keychain.set_transaction_key(Address::ZERO)?;
3723 keychain.set_tx_origin(eoa)?;
3724
3725 let result = keychain.refund_spending_limit(eoa, token, U256::from(50));
3726 assert!(result.is_ok());
3727
3728 Ok(())
3729 })
3730 }
3731
3732 #[test]
3733 fn test_refund_spending_limit_noop_after_key_revocation() -> eyre::Result<()> {
3734 let mut storage = HashMapStorageProvider::new(1);
3735 let eoa = Address::random();
3736 let access_key = Address::random();
3737 let token = Address::random();
3738
3739 StorageCtx::enter(&mut storage, || {
3740 let mut keychain = AccountKeychain::new();
3741 keychain.initialize()?;
3742
3743 keychain.set_transaction_key(Address::ZERO)?;
3744
3745 let auth_call = authorizeKeyCall {
3746 keyId: access_key,
3747 signatureType: SignatureType::Secp256k1,
3748 config: KeyRestrictions {
3749 expiry: u64::MAX,
3750 enforceLimits: true,
3751 limits: vec![TokenLimit {
3752 token,
3753 amount: U256::from(100),
3754 period: 0,
3755 }],
3756 allowAnyCalls: true,
3757 allowedCalls: vec![],
3758 },
3759 };
3760 authorize_key(&mut keychain, eoa, auth_call)?;
3761
3762 keychain.set_transaction_key(access_key)?;
3763 keychain.set_tx_origin(eoa)?;
3764
3765 keychain.authorize_transfer(eoa, token, U256::from(60))?;
3766
3767 let remaining = keychain.get_remaining_limit(getRemainingLimitCall {
3768 account: eoa,
3769 keyId: access_key,
3770 token,
3771 })?;
3772 assert_eq!(remaining, U256::from(40));
3773
3774 keychain.set_transaction_key(Address::ZERO)?;
3775 keychain.revoke_key(eoa, revokeKeyCall { keyId: access_key })?;
3776
3777 keychain.set_transaction_key(access_key)?;
3778
3779 let result = keychain.refund_spending_limit(eoa, token, U256::from(25));
3780 assert!(result.is_ok());
3781
3782 let after_refund = keychain.get_remaining_limit(getRemainingLimitCall {
3783 account: eoa,
3784 keyId: access_key,
3785 token,
3786 })?;
3787 assert_eq!(
3788 after_refund,
3789 U256::from(40),
3790 "limit should be unchanged after revoked key refund"
3791 );
3792
3793 Ok(())
3794 })
3795 }
3796
3797 #[test]
3798 fn test_refund_spending_limit_noop_after_key_expiry() -> eyre::Result<()> {
3799 let mut storage = HashMapStorageProvider::new(1);
3800 let eoa = Address::random();
3801 let access_key = Address::random();
3802 let token = Address::random();
3803
3804 storage.set_timestamp(U256::from(100u64));
3805 StorageCtx::enter(&mut storage, || {
3806 let mut keychain = AccountKeychain::new();
3807 keychain.initialize()?;
3808
3809 keychain.set_transaction_key(Address::ZERO)?;
3810
3811 let auth_call = authorizeKeyCall {
3812 keyId: access_key,
3813 signatureType: SignatureType::Secp256k1,
3814 config: KeyRestrictions {
3815 expiry: 200,
3816 enforceLimits: true,
3817 limits: vec![TokenLimit {
3818 token,
3819 amount: U256::from(100),
3820 period: 0,
3821 }],
3822 allowAnyCalls: true,
3823 allowedCalls: vec![],
3824 },
3825 };
3826 authorize_key(&mut keychain, eoa, auth_call)?;
3827
3828 keychain.set_transaction_key(access_key)?;
3829 keychain.set_tx_origin(eoa)?;
3830 keychain.authorize_transfer(eoa, token, U256::from(60))?;
3831
3832 Ok::<_, eyre::Report>(())
3833 })?;
3834
3835 storage.set_timestamp(U256::from(200u64));
3836 StorageCtx::enter(&mut storage, || {
3837 let mut keychain = AccountKeychain::new();
3838 keychain.set_transaction_key(access_key)?;
3839 keychain.set_tx_origin(eoa)?;
3840
3841 let result = keychain.refund_spending_limit(eoa, token, U256::from(25));
3842 assert!(result.is_ok());
3843
3844 let after_refund = keychain.get_remaining_limit(getRemainingLimitCall {
3845 account: eoa,
3846 keyId: access_key,
3847 token,
3848 })?;
3849 assert_eq!(
3850 after_refund,
3851 U256::from(40),
3852 "limit should be unchanged after expired key refund"
3853 );
3854
3855 Ok(())
3856 })
3857 }
3858
3859 #[test]
3860 fn test_refund_spending_limit_propagates_system_errors() -> eyre::Result<()> {
3861 let mut storage = HashMapStorageProvider::new(1);
3862 let eoa = Address::random();
3863 let access_key = Address::random();
3864 let token = Address::random();
3865
3866 let key_slot = StorageCtx::enter(&mut storage, || {
3867 let mut keychain = AccountKeychain::new();
3868 keychain.initialize()?;
3869
3870 keychain.set_transaction_key(Address::ZERO)?;
3871
3872 let auth_call = authorizeKeyCall {
3873 keyId: access_key,
3874 signatureType: SignatureType::Secp256k1,
3875 config: KeyRestrictions {
3876 expiry: u64::MAX,
3877 enforceLimits: true,
3878 limits: vec![TokenLimit {
3879 token,
3880 amount: U256::from(100),
3881 period: 0,
3882 }],
3883 allowAnyCalls: true,
3884 allowedCalls: vec![],
3885 },
3886 };
3887 authorize_key(&mut keychain, eoa, auth_call)?;
3888
3889 keychain.set_transaction_key(access_key)?;
3890 keychain.set_tx_origin(eoa)?;
3891 keychain.authorize_transfer(eoa, token, U256::from(60))?;
3892
3893 Ok::<_, TempoPrecompileError>(keychain.keys[eoa][access_key].as_slot().slot())
3894 })?;
3895
3896 storage.fail_next_sload_at(ACCOUNT_KEYCHAIN_ADDRESS, key_slot);
3897
3898 StorageCtx::enter(&mut storage, || {
3899 let mut keychain = AccountKeychain::new();
3900 keychain.set_transaction_key(access_key)?;
3901 keychain.set_tx_origin(eoa)?;
3902
3903 let err = keychain
3904 .refund_spending_limit(eoa, token, U256::from(25))
3905 .unwrap_err();
3906
3907 assert!(matches!(err, TempoPrecompileError::Fatal(_)));
3908
3909 Ok(())
3910 })
3911 }
3912
3913 #[test]
3914 fn test_refund_spending_limit_clamped_by_saturating_add() -> eyre::Result<()> {
3915 let mut storage = HashMapStorageProvider::new(1);
3916 let eoa = Address::random();
3917 let access_key = Address::random();
3918 let token = Address::random();
3919 let original_limit = U256::from(100);
3920
3921 StorageCtx::enter(&mut storage, || {
3922 let mut keychain = AccountKeychain::new();
3923 keychain.initialize()?;
3924
3925 keychain.set_transaction_key(Address::ZERO)?;
3926
3927 let auth_call = authorizeKeyCall {
3928 keyId: access_key,
3929 signatureType: SignatureType::Secp256k1,
3930 config: KeyRestrictions {
3931 expiry: u64::MAX,
3932 enforceLimits: true,
3933 limits: vec![TokenLimit {
3934 token,
3935 amount: original_limit,
3936 period: 0,
3937 }],
3938 allowAnyCalls: true,
3939 allowedCalls: vec![],
3940 },
3941 };
3942 authorize_key(&mut keychain, eoa, auth_call)?;
3943
3944 keychain.set_transaction_key(access_key)?;
3945 keychain.set_tx_origin(eoa)?;
3946
3947 keychain.authorize_transfer(eoa, token, U256::from(10))?;
3948
3949 let remaining = keychain.get_remaining_limit(getRemainingLimitCall {
3950 account: eoa,
3951 keyId: access_key,
3952 token,
3953 })?;
3954 assert_eq!(remaining, U256::from(90));
3955
3956 keychain.refund_spending_limit(eoa, token, U256::from(50))?;
3957
3958 let after_refund = keychain.get_remaining_limit(getRemainingLimitCall {
3959 account: eoa,
3960 keyId: access_key,
3961 token,
3962 })?;
3963 assert_eq!(
3964 after_refund,
3965 U256::from(140),
3966 "saturating_add should allow refund beyond original limit without overflow"
3967 );
3968
3969 Ok(())
3970 })
3971 }
3972
3973 #[test]
3974 fn test_t3_refund_spending_limit_clamps_to_max() -> eyre::Result<()> {
3975 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
3976 let eoa = Address::random();
3977 let access_key = Address::random();
3978 let token = Address::random();
3979 let original_limit = U256::from(100);
3980
3981 StorageCtx::enter(&mut storage, || {
3982 let mut keychain = AccountKeychain::new();
3983 keychain.initialize()?;
3984
3985 keychain.set_transaction_key(Address::ZERO)?;
3986 keychain.set_tx_origin(eoa)?;
3987
3988 let auth_call = authorizeKeyCall {
3989 keyId: access_key,
3990 signatureType: SignatureType::Secp256k1,
3991 config: KeyRestrictions {
3992 expiry: u64::MAX,
3993 enforceLimits: true,
3994 limits: vec![TokenLimit {
3995 token,
3996 amount: original_limit,
3997 period: 0,
3998 }],
3999 allowAnyCalls: true,
4000 allowedCalls: vec![],
4001 },
4002 };
4003 authorize_key(&mut keychain, eoa, auth_call)?;
4004
4005 keychain.set_transaction_key(access_key)?;
4006 keychain.set_tx_origin(eoa)?;
4007
4008 keychain.authorize_transfer(eoa, token, U256::from(60))?;
4009 keychain.refund_spending_limit(eoa, token, U256::from(30))?;
4010
4011 let after_partial_refund = keychain.get_remaining_limit(getRemainingLimitCall {
4012 account: eoa,
4013 keyId: access_key,
4014 token,
4015 })?;
4016 assert_eq!(
4017 after_partial_refund,
4018 U256::from(70),
4019 "refund should restore the spent amount without forcing the max"
4020 );
4021
4022 keychain.refund_spending_limit(eoa, token, U256::from(50))?;
4023
4024 let after_refund = keychain.get_remaining_limit(getRemainingLimitCall {
4025 account: eoa,
4026 keyId: access_key,
4027 token,
4028 })?;
4029 assert_eq!(
4030 after_refund, original_limit,
4031 "refund should not restore more than the configured max"
4032 );
4033
4034 Ok(())
4035 })
4036 }
4037
4038 #[test]
4039 fn test_t3_refund_spending_limit_preserves_legacy_rows_without_max() -> eyre::Result<()> {
4040 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
4041 let eoa = Address::random();
4042 let access_key = Address::random();
4043 let token = Address::random();
4044
4045 StorageCtx::enter(&mut storage, || {
4046 let mut keychain = AccountKeychain::new();
4047 keychain.initialize()?;
4048
4049 let limit_key = AccountKeychain::spending_limit_key(eoa, access_key);
4050 keychain.keys[eoa][access_key].write(AuthorizedKey {
4051 signature_type: StoredSignatureType::Secp256k1,
4052 expiry: u64::MAX,
4053 enforce_limits: true,
4054 is_revoked: false,
4055 is_admin: false,
4056 })?;
4057 keychain.spending_limits[limit_key][token].write(SpendingLimitState {
4058 remaining: U256::from(90),
4059 max: 0,
4060 period: 0,
4061 period_end: 0,
4062 })?;
4063
4064 keychain.set_transaction_key(access_key)?;
4065 keychain.set_tx_origin(eoa)?;
4066 keychain.refund_spending_limit(eoa, token, U256::from(10))?;
4067
4068 let after_refund = keychain.get_remaining_limit(getRemainingLimitCall {
4069 account: eoa,
4070 keyId: access_key,
4071 token,
4072 })?;
4073 assert_eq!(
4074 after_refund,
4075 U256::from(100),
4076 "migrated pre-T3 rows should keep legacy saturating-add refund semantics"
4077 );
4078
4079 Ok(())
4080 })
4081 }
4082
4083 #[test]
4084 fn test_t3_authorize_key_ignores_limits_when_enforce_limits_false() -> eyre::Result<()> {
4085 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
4086 let account = Address::random();
4087 let key_id = Address::random();
4088 let token = Address::random();
4089
4090 StorageCtx::enter(&mut storage, || {
4091 let mut keychain = AccountKeychain::new();
4092 keychain.initialize()?;
4093 keychain.set_transaction_key(Address::ZERO)?;
4094 keychain.set_tx_origin(account)?;
4095
4096 authorize_key(
4097 &mut keychain,
4098 account,
4099 authorizeKeyCall {
4100 keyId: key_id,
4101 signatureType: SignatureType::Secp256k1,
4102 config: KeyRestrictions {
4103 expiry: u64::MAX,
4104 enforceLimits: false,
4105 limits: vec![TokenLimit {
4106 token,
4107 amount: U256::from(100),
4108 period: 60,
4109 }],
4110 allowAnyCalls: true,
4111 allowedCalls: vec![],
4112 },
4113 },
4114 )?;
4115
4116 let limit_key = AccountKeychain::spending_limit_key(account, key_id);
4117 assert_eq!(
4118 keychain.spending_limits[limit_key][token].read()?,
4119 SpendingLimitState::default()
4120 );
4121
4122 let remaining =
4123 keychain.get_remaining_limit_with_period(getRemainingLimitWithPeriodCall {
4124 account,
4125 keyId: key_id,
4126 token,
4127 })?;
4128 assert_eq!(remaining.remaining, U256::ZERO);
4129 assert_eq!(remaining.periodEnd, 0);
4130
4131 Ok(())
4132 })
4133 }
4134
4135 #[test]
4136 fn test_t3_rejects_spending_limits_above_u128() -> eyre::Result<()> {
4137 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
4138 let account = Address::random();
4139 let invalid_key_id = Address::random();
4140 let valid_key_id = Address::random();
4141 let token = Address::random();
4142 let oversized_limit = U256::from(u128::MAX) + U256::from(1u8);
4143
4144 StorageCtx::enter(&mut storage, || {
4145 let mut keychain = AccountKeychain::new();
4146 keychain.initialize()?;
4147 keychain.set_transaction_key(Address::ZERO)?;
4148 keychain.set_tx_origin(account)?;
4149
4150 let authorize_result = authorize_key(
4151 &mut keychain,
4152 account,
4153 authorizeKeyCall {
4154 keyId: invalid_key_id,
4155 signatureType: SignatureType::Secp256k1,
4156 config: KeyRestrictions {
4157 expiry: u64::MAX,
4158 enforceLimits: true,
4159 limits: vec![TokenLimit {
4160 token,
4161 amount: oversized_limit,
4162 period: 60,
4163 }],
4164 allowAnyCalls: true,
4165 allowedCalls: vec![],
4166 },
4167 },
4168 );
4169
4170 assert!(
4171 matches!(
4172 authorize_result,
4173 Err(TempoPrecompileError::AccountKeychainError(
4174 AccountKeychainError::InvalidSpendingLimit(_)
4175 ))
4176 ),
4177 "expected InvalidSpendingLimit, got {authorize_result:?}"
4178 );
4179
4180 authorize_key(
4181 &mut keychain,
4182 account,
4183 authorizeKeyCall {
4184 keyId: valid_key_id,
4185 signatureType: SignatureType::Secp256k1,
4186 config: KeyRestrictions {
4187 expiry: u64::MAX,
4188 enforceLimits: true,
4189 limits: vec![TokenLimit {
4190 token,
4191 amount: U256::from(100u64),
4192 period: 60,
4193 }],
4194 allowAnyCalls: true,
4195 allowedCalls: vec![],
4196 },
4197 },
4198 )?;
4199
4200 let update_result = keychain.update_spending_limit(
4201 account,
4202 updateSpendingLimitCall {
4203 keyId: valid_key_id,
4204 token,
4205 newLimit: oversized_limit,
4206 },
4207 );
4208
4209 assert!(
4210 matches!(
4211 update_result,
4212 Err(TempoPrecompileError::AccountKeychainError(
4213 AccountKeychainError::InvalidSpendingLimit(_)
4214 ))
4215 ),
4216 "expected InvalidSpendingLimit, got {update_result:?}"
4217 );
4218
4219 Ok(())
4220 })
4221 }
4222
4223 #[test]
4224 fn test_t3_rejects_duplicate_token_limits() -> eyre::Result<()> {
4225 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
4226 let account = Address::random();
4227 let key_id = Address::random();
4228 let token = Address::random();
4229
4230 StorageCtx::enter(&mut storage, || {
4231 let mut keychain = AccountKeychain::new();
4232 keychain.initialize()?;
4233 keychain.set_transaction_key(Address::ZERO)?;
4234 keychain.set_tx_origin(account)?;
4235
4236 let result = authorize_key(
4237 &mut keychain,
4238 account,
4239 authorizeKeyCall {
4240 keyId: key_id,
4241 signatureType: SignatureType::Secp256k1,
4242 config: KeyRestrictions {
4243 expiry: u64::MAX,
4244 enforceLimits: true,
4245 limits: vec![
4246 TokenLimit {
4247 token,
4248 amount: U256::from(100_u64),
4249 period: 0,
4250 },
4251 TokenLimit {
4252 token,
4253 amount: U256::from(200_u64),
4254 period: 60,
4255 },
4256 ],
4257 allowAnyCalls: true,
4258 allowedCalls: vec![],
4259 },
4260 },
4261 );
4262
4263 assert!(
4264 matches!(
4265 result,
4266 Err(TempoPrecompileError::AccountKeychainError(
4267 AccountKeychainError::InvalidSpendingLimit(_)
4268 ))
4269 ),
4270 "expected duplicate token limits to be rejected, got: {result:?}"
4271 );
4272
4273 let stored_key = keychain.keys[account][key_id].read()?;
4274 assert_eq!(
4275 stored_key.expiry, 0,
4276 "duplicate rejection must not persist the key"
4277 );
4278
4279 Ok(())
4280 })
4281 }
4282
4283 #[test]
4284 fn test_pre_t5_authorize_key_ignores_scopes_when_allowing_any_call() -> eyre::Result<()> {
4285 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T4);
4286 let account = Address::random();
4287 let key_id = Address::random();
4288 let target = Address::random();
4289
4290 StorageCtx::enter(&mut storage, || {
4291 let mut keychain = AccountKeychain::new();
4292 keychain.initialize()?;
4293 keychain.set_transaction_key(Address::ZERO)?;
4294 keychain.set_tx_origin(account)?;
4295
4296 authorize_key(
4297 &mut keychain,
4298 account,
4299 authorizeKeyCall {
4300 keyId: key_id,
4301 signatureType: SignatureType::Secp256k1,
4302 config: KeyRestrictions {
4303 expiry: u64::MAX,
4304 enforceLimits: false,
4305 limits: vec![],
4306 allowAnyCalls: true,
4307 allowedCalls: vec![CallScope {
4308 target,
4309 selectorRules: vec![],
4310 }],
4311 },
4312 },
4313 )?;
4314
4315 let stored_key = keychain.keys[account][key_id].read()?;
4316 assert_eq!(stored_key.expiry, u64::MAX);
4317
4318 let scopes = keychain.get_allowed_calls(getAllowedCallsCall {
4319 account,
4320 keyId: key_id,
4321 })?;
4322 assert!(!scopes.isScoped);
4323 assert!(scopes.scopes.is_empty());
4324
4325 Ok(())
4326 })
4327 }
4328
4329 #[test]
4330 fn test_t5_authorize_key_rejects_scopes_when_allowing_any_call() -> eyre::Result<()> {
4331 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T5);
4332 let account = Address::random();
4333 let key_id = Address::random();
4334 let target = Address::random();
4335
4336 StorageCtx::enter(&mut storage, || {
4337 let mut keychain = AccountKeychain::new();
4338 keychain.initialize()?;
4339 keychain.set_transaction_key(Address::ZERO)?;
4340 keychain.set_tx_origin(account)?;
4341
4342 let err = authorize_key(
4343 &mut keychain,
4344 account,
4345 authorizeKeyCall {
4346 keyId: key_id,
4347 signatureType: SignatureType::Secp256k1,
4348 config: KeyRestrictions {
4349 expiry: u64::MAX,
4350 enforceLimits: false,
4351 limits: vec![],
4352 allowAnyCalls: true,
4353 allowedCalls: vec![CallScope {
4354 target,
4355 selectorRules: vec![],
4356 }],
4357 },
4358 },
4359 )
4360 .expect_err("allowAnyCalls=true must reject non-empty allowedCalls");
4361
4362 assert_invalid_call_scope(err);
4363
4364 let stored_key = keychain.keys[account][key_id].read()?;
4365 assert_eq!(
4366 stored_key.expiry, 0,
4367 "invalid call scope rejection must not persist the key"
4368 );
4369
4370 Ok(())
4371 })
4372 }
4373
4374 #[test]
4375 fn test_spending_limit_state_preserves_legacy_remaining_slot() -> eyre::Result<()> {
4376 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
4377 let account = Address::random();
4378 let key_id = Address::random();
4379 let token = Address::random();
4380
4381 StorageCtx::enter(&mut storage, || {
4382 let mut keychain = AccountKeychain::new();
4383 keychain.initialize()?;
4384
4385 let limit_key = AccountKeychain::spending_limit_key(account, key_id);
4386 let handler = &mut keychain.spending_limits[limit_key][token];
4387 let remaining = U256::from(123u64);
4388 handler.write(SpendingLimitState {
4389 remaining,
4390 max: 456,
4391 period: 60,
4392 period_end: 120,
4393 })?;
4394
4395 assert_eq!(
4396 StorageCtx.sload(ACCOUNT_KEYCHAIN_ADDRESS, handler.as_slot().slot())?,
4397 remaining
4398 );
4399
4400 Ok(())
4401 })
4402 }
4403
4404 #[test]
4405 fn test_t3_rejects_recipient_constrained_scope_for_undeployed_tip20() -> eyre::Result<()> {
4406 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
4407 let account = Address::random();
4408 let key_id = Address::random();
4409 let recipient = Address::repeat_byte(0x44);
4410 let mut target_bytes = [0u8; 20];
4411 target_bytes[0] = 0x20;
4412 target_bytes[1] = 0xc0;
4413 target_bytes[19] = 0x42;
4414 let undeployed_tip20 = Address::from(target_bytes);
4415
4416 StorageCtx::enter(&mut storage, || {
4417 let mut keychain = AccountKeychain::new();
4418 keychain.initialize()?;
4419 keychain.set_transaction_key(Address::ZERO)?;
4420 keychain.set_tx_origin(account)?;
4421
4422 authorize_key(
4423 &mut keychain,
4424 account,
4425 authorizeKeyCall {
4426 keyId: key_id,
4427 signatureType: SignatureType::Secp256k1,
4428 config: KeyRestrictions {
4429 expiry: u64::MAX,
4430 enforceLimits: false,
4431 limits: vec![],
4432 allowAnyCalls: true,
4433 allowedCalls: vec![],
4434 },
4435 },
4436 )?;
4437
4438 let err = keychain
4439 .apply_key_authorization_restrictions(
4440 account,
4441 key_id,
4442 &[],
4443 Some(&[CallScope {
4444 target: undeployed_tip20,
4445 selectorRules: vec![SelectorRule {
4446 selector: TIP20_TRANSFER_SELECTOR.into(),
4447 recipients: vec![recipient],
4448 }],
4449 }]),
4450 )
4451 .expect_err("unexpected success for undeployed TIP-20 target");
4452
4453 match err {
4454 TempoPrecompileError::AccountKeychainError(
4455 AccountKeychainError::InvalidCallScope(_),
4456 ) => {}
4457 other => panic!("expected InvalidCallScope, got {other:?}"),
4458 }
4459
4460 Ok(())
4461 })
4462 }
4463
4464 #[test]
4465 fn test_t3_periodic_limit_rollover() -> eyre::Result<()> {
4466 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
4467 storage.set_timestamp(U256::from(1_000u64));
4468
4469 let account = Address::random();
4470 let key_id = Address::random();
4471 let token = Address::random();
4472
4473 StorageCtx::enter(&mut storage, || {
4474 let mut keychain = AccountKeychain::new();
4475 keychain.initialize()?;
4476 keychain.set_transaction_key(Address::ZERO)?;
4477 keychain.set_tx_origin(account)?;
4478 TIP20Setup::path_usd(account).apply()?;
4479
4480 authorize_key(
4481 &mut keychain,
4482 account,
4483 authorizeKeyCall {
4484 keyId: key_id,
4485 signatureType: SignatureType::Secp256k1,
4486 config: KeyRestrictions {
4487 expiry: u64::MAX,
4488 enforceLimits: true,
4489 limits: vec![TokenLimit {
4490 token,
4491 amount: U256::from(100),
4492 period: 0,
4493 }],
4494 allowAnyCalls: true,
4495 allowedCalls: vec![],
4496 },
4497 },
4498 )?;
4499
4500 keychain.apply_key_authorization_restrictions(
4501 account,
4502 key_id,
4503 &[TokenLimit {
4504 token,
4505 amount: U256::from(100),
4506 period: 60,
4507 }],
4508 None,
4509 )?;
4510
4511 keychain.set_transaction_key(key_id)?;
4512 keychain.authorize_transfer(account, token, U256::from(80))?;
4513
4514 let remaining = keychain.get_remaining_limit(getRemainingLimitCall {
4515 account,
4516 keyId: key_id,
4517 token,
4518 })?;
4519 assert_eq!(remaining, U256::from(20));
4520
4521 Ok::<_, eyre::Report>(())
4522 })?;
4523
4524 storage.set_timestamp(U256::from(1_070u64));
4525 StorageCtx::enter(&mut storage, || {
4526 let mut keychain = AccountKeychain::new();
4527 keychain.set_transaction_key(key_id)?;
4528 keychain.set_tx_origin(account)?;
4529
4530 keychain.authorize_transfer(account, token, U256::from(10))?;
4531
4532 let remaining = keychain.get_remaining_limit(getRemainingLimitCall {
4533 account,
4534 keyId: key_id,
4535 token,
4536 })?;
4537 assert_eq!(remaining, U256::from(90));
4538 Ok(())
4539 })
4540 }
4541
4542 #[test]
4543 fn test_t3_get_allowed_calls_distinguishes_unrestricted_and_deny_all() -> eyre::Result<()> {
4544 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
4545 let account = Address::random();
4546 let key_id = Address::random();
4547
4548 StorageCtx::enter(&mut storage, || {
4549 let mut keychain = AccountKeychain::new();
4550 keychain.initialize()?;
4551 keychain.set_transaction_key(Address::ZERO)?;
4552 keychain.set_tx_origin(account)?;
4553
4554 authorize_key(
4555 &mut keychain,
4556 account,
4557 authorizeKeyCall {
4558 keyId: key_id,
4559 signatureType: SignatureType::Secp256k1,
4560 config: KeyRestrictions {
4561 expiry: u64::MAX,
4562 enforceLimits: false,
4563 limits: vec![],
4564 allowAnyCalls: true,
4565 allowedCalls: vec![],
4566 },
4567 },
4568 )?;
4569
4570 let scopes = keychain.get_allowed_calls(getAllowedCallsCall {
4571 account,
4572 keyId: key_id,
4573 })?;
4574 assert!(!scopes.isScoped);
4575 assert!(scopes.scopes.is_empty());
4576
4577 keychain.apply_key_authorization_restrictions(account, key_id, &[], Some(&[]))?;
4578
4579 let deny_all = keychain.get_allowed_calls(getAllowedCallsCall {
4580 account,
4581 keyId: key_id,
4582 })?;
4583 assert!(deny_all.isScoped);
4584 assert!(deny_all.scopes.is_empty());
4585
4586 Ok(())
4587 })
4588 }
4589
4590 #[test]
4591 fn test_t3_get_allowed_calls_returns_deny_all_for_inactive_keys() -> eyre::Result<()> {
4592 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
4593 let account = Address::random();
4594 let revoked_key = Address::random();
4595 let expiring_key = Address::random();
4596 let target = DEFAULT_FEE_TOKEN;
4597
4598 storage.set_timestamp(U256::from(1_000u64));
4599 StorageCtx::enter(&mut storage, || {
4600 let mut keychain = AccountKeychain::new();
4601 keychain.initialize()?;
4602 keychain.set_transaction_key(Address::ZERO)?;
4603 keychain.set_tx_origin(account)?;
4604
4605 for (key_id, expiry) in [(revoked_key, u64::MAX), (expiring_key, 1_005)] {
4606 authorize_key(
4607 &mut keychain,
4608 account,
4609 authorizeKeyCall {
4610 keyId: key_id,
4611 signatureType: SignatureType::Secp256k1,
4612 config: KeyRestrictions {
4613 expiry,
4614 enforceLimits: false,
4615 limits: vec![],
4616 allowAnyCalls: false,
4617 allowedCalls: vec![CallScope {
4618 target,
4619 selectorRules: vec![],
4620 }],
4621 },
4622 },
4623 )?;
4624 }
4625
4626 keychain.revoke_key(account, revokeKeyCall { keyId: revoked_key })?;
4627
4628 let revoked = keychain.get_allowed_calls(getAllowedCallsCall {
4629 account,
4630 keyId: revoked_key,
4631 })?;
4632 assert!(revoked.isScoped);
4633 assert!(revoked.scopes.is_empty());
4634
4635 let root = keychain.get_allowed_calls(getAllowedCallsCall {
4636 account,
4637 keyId: Address::ZERO,
4638 })?;
4639 assert!(!root.isScoped);
4640 assert!(root.scopes.is_empty());
4641
4642 Ok::<_, eyre::Report>(())
4643 })?;
4644
4645 storage.set_timestamp(U256::from(1_010u64));
4646 StorageCtx::enter(&mut storage, || {
4647 let keychain = AccountKeychain::new();
4648
4649 let expired = keychain.get_allowed_calls(getAllowedCallsCall {
4650 account,
4651 keyId: expiring_key,
4652 })?;
4653 assert!(expired.isScoped);
4654 assert!(expired.scopes.is_empty());
4655
4656 Ok(())
4657 })
4658 }
4659
4660 #[test]
4661 fn test_expired_key_has_zero_remaining_limit() -> eyre::Result<()> {
4662 for hardfork in [TempoHardfork::T0, TempoHardfork::T2, TempoHardfork::T3] {
4663 let mut storage = HashMapStorageProvider::new_with_spec(1, hardfork);
4664 let account = Address::random();
4665 let key_id = Address::random();
4666 let token = Address::random();
4667
4668 storage.set_timestamp(U256::from(1_000u64));
4669 StorageCtx::enter(&mut storage, || {
4670 let mut keychain = AccountKeychain::new();
4671 keychain.initialize()?;
4672 keychain.set_transaction_key(Address::ZERO)?;
4673 keychain.set_tx_origin(account)?;
4674
4675 authorize_key(
4676 &mut keychain,
4677 account,
4678 authorizeKeyCall {
4679 keyId: key_id,
4680 signatureType: SignatureType::Secp256k1,
4681 config: KeyRestrictions {
4682 expiry: 1_005,
4683 enforceLimits: true,
4684 limits: vec![TokenLimit {
4685 token,
4686 amount: U256::from(100u64),
4687 period: 0,
4688 }],
4689 allowAnyCalls: true,
4690 allowedCalls: vec![],
4691 },
4692 },
4693 )?;
4694
4695 Ok::<_, eyre::Report>(())
4696 })?;
4697
4698 storage.set_timestamp(U256::from(1_010u64));
4700
4701 StorageCtx::enter(&mut storage, || {
4702 let keychain = AccountKeychain::new();
4703
4704 let sload_before = StorageCtx.counter_sload();
4705 if hardfork.is_t3() {
4706 let remaining = keychain.get_remaining_limit_with_period(
4708 getRemainingLimitWithPeriodCall {
4709 account,
4710 keyId: key_id,
4711 token,
4712 },
4713 )?;
4714 assert_eq!(remaining.remaining, U256::ZERO);
4715 assert_eq!(remaining.periodEnd, 0);
4716
4717 assert_eq!(StorageCtx.counter_sload() - sload_before, 1);
4719 } else {
4720 let remaining = keychain.get_remaining_limit(getRemainingLimitCall {
4722 account,
4723 keyId: key_id,
4724 token,
4725 })?;
4726 assert_eq!(remaining, U256::from(100u64));
4727
4728 let expected_delta = if hardfork.is_t2() { 2 } else { 1 };
4730 assert_eq!(StorageCtx.counter_sload() - sload_before, expected_delta);
4731 }
4732
4733 Ok::<_, eyre::Report>(())
4734 })?;
4735 }
4736
4737 Ok(())
4738 }
4739
4740 #[test]
4741 fn test_revoked_key_has_zero_remaining_limit() -> eyre::Result<()> {
4742 for hardfork in [TempoHardfork::T0, TempoHardfork::T2, TempoHardfork::T3] {
4743 let mut storage = HashMapStorageProvider::new_with_spec(1, hardfork);
4744 let account = Address::random();
4745 let key_id = Address::random();
4746 let token = Address::random();
4747
4748 StorageCtx::enter(&mut storage, || {
4749 let mut keychain = AccountKeychain::new();
4750 keychain.initialize()?;
4751 keychain.set_transaction_key(Address::ZERO)?;
4752 keychain.set_tx_origin(account)?;
4753
4754 authorize_key(
4755 &mut keychain,
4756 account,
4757 authorizeKeyCall {
4758 keyId: key_id,
4759 signatureType: SignatureType::Secp256k1,
4760 config: KeyRestrictions {
4761 expiry: u64::MAX,
4762 enforceLimits: true,
4763 limits: vec![TokenLimit {
4764 token,
4765 amount: U256::from(100u64),
4766 period: 0,
4767 }],
4768 allowAnyCalls: true,
4769 allowedCalls: vec![],
4770 },
4771 },
4772 )?;
4773
4774 keychain.revoke_key(account, revokeKeyCall { keyId: key_id })?;
4776
4777 let sload_before = StorageCtx.counter_sload();
4778 if hardfork.is_t2() {
4779 let remaining = keychain.get_remaining_limit_with_period(
4781 getRemainingLimitWithPeriodCall {
4782 account,
4783 keyId: key_id,
4784 token,
4785 },
4786 )?;
4787 assert_eq!(remaining.remaining, U256::ZERO);
4788 assert_eq!(remaining.periodEnd, 0);
4789
4790 assert_eq!(StorageCtx.counter_sload() - sload_before, 1);
4792 } else {
4793 let remaining = keychain.get_remaining_limit(getRemainingLimitCall {
4795 account,
4796 keyId: key_id,
4797 token,
4798 })?;
4799 assert_eq!(remaining, U256::from(100u64));
4800
4801 assert_eq!(StorageCtx.counter_sload() - sload_before, 1);
4803 }
4804
4805 Ok::<_, eyre::Report>(())
4806 })?;
4807 }
4808
4809 Ok(())
4810 }
4811
4812 #[test]
4813 fn test_zero_key_remaining_limit_reads_storage_on_t2_but_not_t3() -> eyre::Result<()> {
4814 let (account, token) = (Address::random(), Address::random());
4815
4816 for (hardfork, expected_sloads) in [(TempoHardfork::T2, 1_u64), (TempoHardfork::T3, 0)] {
4817 let mut storage = HashMapStorageProvider::new_with_spec(1, hardfork);
4818 StorageCtx::enter(&mut storage, || {
4819 let mut keychain = AccountKeychain::new();
4820 let _ = keychain.initialize();
4821
4822 let sloads_before = StorageCtx.counter_sload();
4823 assert_eq!(
4824 keychain.get_remaining_limit(getRemainingLimitCall {
4825 account,
4826 keyId: Address::ZERO,
4827 token,
4828 })?,
4829 U256::ZERO
4830 );
4831
4832 assert_eq!(
4833 StorageCtx.counter_sload() - sloads_before,
4834 expected_sloads,
4835 "{hardfork:?} should perform the expected number of storage reads for zero key_id"
4836 );
4837
4838 Ok::<_, eyre::Report>(())
4839 })?;
4840 }
4841
4842 Ok(())
4843 }
4844
4845 #[test]
4846 fn test_t3_set_allowed_calls_rejects_zero_target() -> eyre::Result<()> {
4847 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
4848 let account = Address::random();
4849 let key_id = Address::random();
4850
4851 StorageCtx::enter(&mut storage, || {
4852 let mut keychain = AccountKeychain::new();
4853 keychain.initialize()?;
4854 keychain.set_transaction_key(Address::ZERO)?;
4855 keychain.set_tx_origin(account)?;
4856
4857 authorize_key(
4858 &mut keychain,
4859 account,
4860 authorizeKeyCall {
4861 keyId: key_id,
4862 signatureType: SignatureType::Secp256k1,
4863 config: KeyRestrictions {
4864 expiry: u64::MAX,
4865 enforceLimits: false,
4866 limits: vec![],
4867 allowAnyCalls: true,
4868 allowedCalls: vec![],
4869 },
4870 },
4871 )?;
4872
4873 let err = keychain
4874 .set_allowed_calls(
4875 account,
4876 setAllowedCallsCall {
4877 keyId: key_id,
4878 scopes: vec![CallScope {
4879 target: Address::ZERO,
4880 selectorRules: vec![],
4881 }],
4882 },
4883 )
4884 .expect_err("unexpected success for zero target scope");
4885 assert_invalid_call_scope(err);
4886
4887 Ok(())
4888 })
4889 }
4890
4891 #[test]
4892 fn test_t3_set_allowed_calls_rejects_empty_scope_batch() -> eyre::Result<()> {
4893 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
4894 let account = Address::random();
4895 let key_id = Address::random();
4896
4897 StorageCtx::enter(&mut storage, || {
4898 let mut keychain = AccountKeychain::new();
4899 keychain.initialize()?;
4900 keychain.set_transaction_key(Address::ZERO)?;
4901 keychain.set_tx_origin(account)?;
4902
4903 authorize_key(
4904 &mut keychain,
4905 account,
4906 authorizeKeyCall {
4907 keyId: key_id,
4908 signatureType: SignatureType::Secp256k1,
4909 config: KeyRestrictions {
4910 expiry: u64::MAX,
4911 enforceLimits: false,
4912 limits: vec![],
4913 allowAnyCalls: true,
4914 allowedCalls: vec![],
4915 },
4916 },
4917 )?;
4918
4919 let err = keychain
4920 .set_allowed_calls(
4921 account,
4922 setAllowedCallsCall {
4923 keyId: key_id,
4924 scopes: vec![],
4925 },
4926 )
4927 .expect_err("unexpected success for empty scope batch");
4928 assert_invalid_call_scope(err);
4929
4930 let scopes = keychain.get_allowed_calls(getAllowedCallsCall {
4931 account,
4932 keyId: key_id,
4933 })?;
4934 assert!(!scopes.isScoped);
4935 assert!(scopes.scopes.is_empty());
4936
4937 Ok(())
4938 })
4939 }
4940
4941 #[test]
4942 fn test_t3_set_allowed_calls_roundtrip_and_remove_target_scope() -> eyre::Result<()> {
4943 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
4944 let account = Address::random();
4945 let key_id = Address::random();
4946 let target = Address::random();
4947
4948 StorageCtx::enter(&mut storage, || {
4949 let mut keychain = AccountKeychain::new();
4950 keychain.initialize()?;
4951 keychain.set_transaction_key(Address::ZERO)?;
4952 keychain.set_tx_origin(account)?;
4953
4954 authorize_key(
4955 &mut keychain,
4956 account,
4957 authorizeKeyCall {
4958 keyId: key_id,
4959 signatureType: SignatureType::Secp256k1,
4960 config: KeyRestrictions {
4961 expiry: u64::MAX,
4962 enforceLimits: false,
4963 limits: vec![],
4964 allowAnyCalls: true,
4965 allowedCalls: vec![],
4966 },
4967 },
4968 )?;
4969
4970 keychain.set_allowed_calls(
4971 account,
4972 setAllowedCallsCall {
4973 keyId: key_id,
4974 scopes: vec![CallScope {
4975 target,
4976 selectorRules: vec![SelectorRule {
4977 selector: TIP20_TRANSFER_SELECTOR.into(),
4978 recipients: vec![],
4979 }],
4980 }],
4981 },
4982 )?;
4983
4984 let scopes = keychain.get_allowed_calls(getAllowedCallsCall {
4985 account,
4986 keyId: key_id,
4987 })?;
4988 assert!(scopes.isScoped);
4989 assert_eq!(scopes.scopes.len(), 1);
4990 assert_eq!(scopes.scopes[0].target, target);
4991 assert_eq!(scopes.scopes[0].selectorRules.len(), 1);
4992 assert_eq!(
4993 *scopes.scopes[0].selectorRules[0].selector,
4994 TIP20_TRANSFER_SELECTOR
4995 );
4996 assert!(scopes.scopes[0].selectorRules[0].recipients.is_empty());
4997
4998 let allow = keychain.validate_call_scope_for_transaction(
4999 account,
5000 key_id,
5001 &TxKind::Call(target),
5002 &TIP20_TRANSFER_SELECTOR,
5003 );
5004 assert!(allow.is_ok());
5005
5006 keychain.remove_allowed_calls(
5007 account,
5008 removeAllowedCallsCall {
5009 keyId: key_id,
5010 target,
5011 },
5012 )?;
5013
5014 let removed = keychain.get_allowed_calls(getAllowedCallsCall {
5015 account,
5016 keyId: key_id,
5017 })?;
5018 assert!(removed.isScoped);
5019 assert!(removed.scopes.is_empty());
5020
5021 let denied = keychain
5022 .validate_call_scope_for_transaction(
5023 account,
5024 key_id,
5025 &TxKind::Call(target),
5026 &TIP20_TRANSFER_SELECTOR,
5027 )
5028 .expect_err("unexpected success for removed target scope");
5029 assert_call_not_allowed(denied);
5030
5031 Ok(())
5032 })
5033 }
5034
5035 #[test]
5036 fn test_t3_set_allowed_calls_empty_selector_rules_allow_all_selectors() -> eyre::Result<()> {
5037 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
5038 let account = Address::random();
5039 let key_id = Address::random();
5040 let target = DEFAULT_FEE_TOKEN;
5041
5042 StorageCtx::enter(&mut storage, || {
5043 let mut keychain = AccountKeychain::new();
5044 keychain.initialize()?;
5045 keychain.set_transaction_key(Address::ZERO)?;
5046 keychain.set_tx_origin(account)?;
5047
5048 authorize_key(
5049 &mut keychain,
5050 account,
5051 authorizeKeyCall {
5052 keyId: key_id,
5053 signatureType: SignatureType::Secp256k1,
5054 config: KeyRestrictions {
5055 expiry: u64::MAX,
5056 enforceLimits: false,
5057 limits: vec![],
5058 allowAnyCalls: true,
5059 allowedCalls: vec![],
5060 },
5061 },
5062 )?;
5063
5064 keychain.set_allowed_calls(
5065 account,
5066 setAllowedCallsCall {
5067 keyId: key_id,
5068 scopes: vec![CallScope {
5069 target,
5070 selectorRules: vec![],
5071 }],
5072 },
5073 )?;
5074
5075 let scopes = keychain.get_allowed_calls(getAllowedCallsCall {
5076 account,
5077 keyId: key_id,
5078 })?;
5079 assert!(scopes.isScoped);
5080 assert_eq!(scopes.scopes.len(), 1);
5081 assert_eq!(scopes.scopes[0].target, target);
5082 assert!(scopes.scopes[0].selectorRules.is_empty());
5083
5084 let allow = keychain.validate_call_scope_for_transaction(
5085 account,
5086 key_id,
5087 &TxKind::Call(target),
5088 &[],
5089 );
5090 assert!(allow.is_ok());
5091
5092 Ok(())
5093 })
5094 }
5095
5096 #[test]
5097 fn test_empty_recipient_selector_delete_is_gated_at_t4() -> eyre::Result<()> {
5098 let account = Address::random();
5099 let key_id = Address::random();
5100 let target = Address::random();
5101
5102 let mut t3_sstores = 0;
5103 let mut t4_sstores = 0;
5104
5105 for (hardfork, writes) in [
5106 (TempoHardfork::T3, &mut t3_sstores),
5107 (TempoHardfork::T4, &mut t4_sstores),
5108 ] {
5109 let mut storage = HashMapStorageProvider::new_with_spec(1, hardfork);
5110 StorageCtx::enter(&mut storage, || {
5111 let mut keychain = AccountKeychain::new();
5112 keychain.initialize()?;
5113 keychain.set_transaction_key(Address::ZERO)?;
5114 keychain.set_tx_origin(account)?;
5115
5116 authorize_key(
5117 &mut keychain,
5118 account,
5119 authorizeKeyCall {
5120 keyId: key_id,
5121 signatureType: SignatureType::Secp256k1,
5122 config: KeyRestrictions {
5123 expiry: u64::MAX,
5124 enforceLimits: false,
5125 limits: vec![],
5126 allowAnyCalls: true,
5127 allowedCalls: vec![],
5128 },
5129 },
5130 )?;
5131
5132 let before = StorageCtx.counter_sstore();
5133 keychain.set_allowed_calls(
5134 account,
5135 setAllowedCallsCall {
5136 keyId: key_id,
5137 scopes: vec![CallScope {
5138 target,
5139 selectorRules: vec![SelectorRule {
5140 selector: TIP20_TRANSFER_SELECTOR.into(),
5141 recipients: vec![],
5142 }],
5143 }],
5144 },
5145 )?;
5146 *writes = StorageCtx.counter_sstore() - before;
5147
5148 let scopes = keychain.get_allowed_calls(getAllowedCallsCall {
5149 account,
5150 keyId: key_id,
5151 })?;
5152 assert!(scopes.isScoped);
5153 assert_eq!(scopes.scopes.len(), 1);
5154 assert!(scopes.scopes[0].selectorRules[0].recipients.is_empty());
5155
5156 Ok::<_, eyre::Report>(())
5157 })?;
5158 }
5159
5160 assert_eq!(
5161 t3_sstores,
5162 t4_sstores + 1,
5163 "pre-T4 should retain the redundant empty-recipient delete"
5164 );
5165
5166 Ok(())
5167 }
5168
5169 #[test]
5170 fn test_t3_call_scope_selector_and_recipient_checks() -> eyre::Result<()> {
5171 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
5172 let account = Address::random();
5173 let key_id = Address::random();
5174 let target = DEFAULT_FEE_TOKEN;
5175 let allowed_recipient = Address::repeat_byte(0x22);
5176 let denied_recipient = Address::repeat_byte(0x33);
5177
5178 StorageCtx::enter(&mut storage, || {
5179 let mut keychain = AccountKeychain::new();
5180 keychain.initialize()?;
5181 keychain.set_transaction_key(Address::ZERO)?;
5182 keychain.set_tx_origin(account)?;
5183 TIP20Setup::path_usd(account).apply()?;
5184
5185 authorize_key(
5186 &mut keychain,
5187 account,
5188 authorizeKeyCall {
5189 keyId: key_id,
5190 signatureType: SignatureType::Secp256k1,
5191 config: KeyRestrictions {
5192 expiry: u64::MAX,
5193 enforceLimits: false,
5194 limits: vec![],
5195 allowAnyCalls: true,
5196 allowedCalls: vec![],
5197 },
5198 },
5199 )?;
5200
5201 keychain.apply_key_authorization_restrictions(
5202 account,
5203 key_id,
5204 &[],
5205 Some(&[CallScope {
5206 target,
5207 selectorRules: vec![SelectorRule {
5208 selector: TIP20_TRANSFER_SELECTOR.into(),
5209 recipients: vec![allowed_recipient],
5210 }],
5211 }]),
5212 )?;
5213
5214 let make_calldata = |selector: [u8; 4], recipient: Address| {
5215 let mut data = selector.to_vec();
5216 let mut recipient_word = [0u8; 32];
5217 recipient_word[12..].copy_from_slice(recipient.as_slice());
5218 data.extend_from_slice(&recipient_word);
5219 data.extend_from_slice(&[0u8; 32]);
5220 data
5221 };
5222
5223 let allow = keychain.validate_call_scope_for_transaction(
5224 account,
5225 key_id,
5226 &TxKind::Call(target),
5227 &make_calldata(TIP20_TRANSFER_SELECTOR, allowed_recipient),
5228 );
5229 assert!(allow.is_ok());
5230
5231 let denied = keychain
5232 .validate_call_scope_for_transaction(
5233 account,
5234 key_id,
5235 &TxKind::Call(target),
5236 &make_calldata(TIP20_TRANSFER_SELECTOR, denied_recipient),
5237 )
5238 .expect_err("unexpected success for denied recipient");
5239 assert_call_not_allowed(denied);
5240
5241 let wrong_selector = keychain
5242 .validate_call_scope_for_transaction(
5243 account,
5244 key_id,
5245 &TxKind::Call(target),
5246 &make_calldata([0xde, 0xad, 0xbe, 0xef], allowed_recipient),
5247 )
5248 .expect_err("unexpected success for wrong selector");
5249 assert_call_not_allowed(wrong_selector);
5250
5251 Ok(())
5252 })
5253 }
5254
5255 #[test]
5256 fn test_t3_contract_creation_rejected_for_access_key() -> eyre::Result<()> {
5257 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
5258 let account = Address::random();
5259 let key_id = Address::random();
5260
5261 StorageCtx::enter(&mut storage, || {
5262 let mut keychain = AccountKeychain::new();
5263 keychain.initialize()?;
5264 keychain.set_transaction_key(Address::ZERO)?;
5265 keychain.set_tx_origin(account)?;
5266
5267 authorize_key(
5268 &mut keychain,
5269 account,
5270 authorizeKeyCall {
5271 keyId: key_id,
5272 signatureType: SignatureType::Secp256k1,
5273 config: KeyRestrictions {
5274 expiry: u64::MAX,
5275 enforceLimits: false,
5276 limits: vec![],
5277 allowAnyCalls: true,
5278 allowedCalls: vec![],
5279 },
5280 },
5281 )?;
5282
5283 let err = keychain
5284 .validate_call_scope_for_transaction(account, key_id, &TxKind::Create, &[])
5285 .expect_err("unexpected success for CREATE");
5286 assert_call_not_allowed(err);
5287
5288 Ok(())
5289 })
5290 }
5291}