Skip to main content

tempo_precompiles/account_keychain/
mod.rs

1//! [Account keychain] precompile for managing session keys and spending limits.
2//!
3//! Each account can authorize secondary keys (session keys) with per-token spending caps,
4//! signature type constraints, and expiry. The main key (address zero) retains full control;
5//! T6 admin keys can also manage other access keys.
6//!
7//! [Account keychain]: <https://docs.tempo.xyz/protocol/transactions/AccountKeychain>
8
9pub 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
37/// Allowed TIP-20 selectors for recipient-constrained rules.
38const 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/// Key information stored in the precompile
51///
52/// Storage layout (packed into single slot, right-aligned):
53/// - byte 0: signature_type (u8)
54/// - bytes 1-8: expiry (u64, little-endian)
55/// - byte 9: enforce_limits (bool)
56/// - byte 10: is_revoked (bool)
57/// - byte 11: is_admin (bool)
58#[derive(Debug, Clone, Default, PartialEq, Eq, Storable)]
59pub struct AuthorizedKey {
60    /// Signature type used by this key.
61    pub signature_type: StoredSignatureType,
62    /// Block timestamp when key expires
63    pub expiry: u64,
64    /// Whether to enforce spending limits for this key
65    pub enforce_limits: bool,
66    /// Whether this key has been revoked. Once revoked, a key cannot be re-authorized
67    /// with the same key_id. This prevents replay attacks.
68    pub is_revoked: bool,
69    /// Whether this key has admin privileges for keychain management.
70    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/// Account Keychain contract for managing authorized keys (session keys, spending limits).
106///
107/// The struct fields define the on-chain storage layout; the `#[contract]` macro generates the
108/// storage handlers which provide an ergonomic way to interact with the EVM state.
109#[contract(addr = ACCOUNT_KEYCHAIN_ADDRESS)]
110pub struct AccountKeychain {
111    // keys[account][keyId] -> AuthorizedKey
112    keys: Mapping<Address, Mapping<Address, AuthorizedKey>>,
113    // spendingLimits[(account, keyId)][token] -> { remaining, max, period, period_end }
114    // Using a hash of account and keyId as the key to avoid triple nesting
115    spending_limits: Mapping<B256, Mapping<Address, SpendingLimitState>>,
116
117    // key_scopes[(account, keyId)] -> call scoping configuration.
118    key_scopes: Mapping<B256, KeyScope>,
119
120    // key_authorization_witnesses[account][witness] -> true once manually burned.
121    key_authorization_witnesses: Mapping<Address, Mapping<B256, bool>>,
122
123    // WARNING(rusowsky): transient storage slots must always be placed at the very end until the `contract`
124    // macro is refactored and has 2 independent layouts (persistent and transient).
125    // If new (persistent) storage fields need to be added to the precompile, they must go above this one.
126    transaction_key: Address,
127    // The transaction origin (tx.origin) - the EOA that signed the transaction.
128    // Used to ensure spending limits only apply when msg_sender == tx_origin.
129    tx_origin: Address,
130}
131
132/// Key-level call scope.
133///
134/// This is the only level that needs an explicit mode bit: an empty `targets` set is ambiguous
135/// between "unrestricted" and "scoped deny-all". `is_scoped = false` means ignore the tree and
136/// allow any call, while `is_scoped = true && targets.is_empty()` means the key currently allows
137/// no targets.
138#[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/// Target-level scope for one target under one account key.
146///
147/// Only persisted for targets present in the parent `targets` set. An empty `selectors` set means
148/// any selector on the target is allowed; deleting the target from `targets` removes the scope.
149/// This asymmetry is intentional: once the parent target is explicitly allowed, an empty child set
150/// means "no further restriction", not "deny all selectors".
151#[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/// Selector-level scope for one selector under one target.
158///
159/// Only persisted for selectors present in the parent `selectors` set. An empty `recipients` set
160/// means any recipient is allowed; deleting the selector from `selectors` removes the scope.
161/// Future incremental remove APIs must delete the selector entry when the last recipient is
162/// removed; leaving an existing selector with `recipients = []` would widen permissions to
163/// allow-all recipients.
164#[derive(Debug, Clone, Storable, Default)]
165pub struct SelectorScope {
166    pub recipients: Set<Address>,
167}
168
169/// Per-token spending limit state.
170///
171/// `remaining` stays in the first slot so the legacy `spending_limits` layout remains intact.
172/// It remains `U256` for the same reason, even though T3 caps `max` to TIP-20's `u128` supply
173/// range and runtime logic maintains `remaining <= max` for periodic limits.
174/// T3+ extends the same row with period metadata in later slots.
175#[derive(Debug, Clone, Default, PartialEq, Eq, Storable)]
176pub struct SpendingLimitState {
177    /// Remaining amount currently available to spend.
178    pub remaining: U256,
179    /// Maximum amount allowed per period, capped to TIP-20's `u128` supply range.
180    pub max: u128,
181    /// Duration of each period in seconds. `0` means non-periodic.
182    pub period: u64,
183    /// End timestamp of the current period window.
184    pub period_end: u64,
185}
186
187impl SpendingLimitState {
188    /// Computes the period end for the current rollover window, saturating on
189    /// all intermediate operations to avoid overflow in extreme timestamps.
190    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    /// Create a hash key for account+key scoped storage rows.
204    ///
205    /// This is used to access account-key rows like `spending_limits[key][token]` and
206    /// `key_scopes[key]`. The hash combines account and key_id to avoid triple nesting.
207    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    /// Initializes the account keychain precompile.
224    pub fn initialize(&mut self) -> Result<()> {
225        self.__initialize()
226    }
227
228    /// Registers a new access key with signature type, expiry, and optional per-token spending
229    /// limits. Only callable with the account's root key or, on T6+, an admin access key.
230    ///
231    /// # Errors
232    /// - `UnauthorizedCaller` — only the root key or, on T6+, an admin access key can
233    ///   authorize keys, and for contract callers on T2+, `msg.sender` must match `tx.origin`
234    /// - `ZeroPublicKey` — `keyId` cannot be the zero address
235    /// - `ExpiryInPast` — expiry must be in the future (enforced since T0)
236    /// - `KeyAlreadyExists` — a key with this ID is already registered
237    /// - `KeyAlreadyRevoked` — revoked keys cannot be re-authorized
238    /// - `InvalidSignatureType` — must be Secp256k1, P256, or WebAuthn
239    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        // Validate inputs
264        if key_id == Address::ZERO {
265            return Err(AccountKeychainError::zero_public_key().into());
266        }
267        // Admin keys are explicit access-key rows; the root key remains implicit.
268        if is_admin && key_id == msg_sender {
269            return Err(AccountKeychainError::invalid_key_id().into());
270        }
271
272        // T0+: Expiry must be in the future (also catches expiry == 0 which means "key doesn't exist")
273        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        // Check if key already exists (key exists if expiry > 0)
281        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        // Check if this key was previously revoked - prevents replay attacks
287        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        // TIP-1011 fields are hardfork-gated at T3, so reject them before mutating state.
294        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                // T5+: prevent footguns where callers accidentally pass scopes with allow-all.
306                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        // Create and store the new key
331        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        // Emit event
366        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    /// Registers a new unrestricted admin access key. Only newly authorized key IDs can become
377    /// admin keys; existing or previously revoked keys must not be upgraded in place.
378    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    /// Burns a TIP-1053 witness without authorizing a key.
405    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    /// Permanently revokes an access key. Once revoked, a key ID can never be re-authorized for
415    /// this account, preventing replay of old `KeyAuthorization` signatures.
416    ///
417    /// # Errors
418    /// - `UnauthorizedCaller` — only the root key or, on T6+, an admin access key can revoke;
419    ///   for contract callers on T2+, `msg.sender` must match `tx.origin`
420    /// - `KeyNotFound` — no key registered with this ID
421    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        // Key exists if expiry > 0
427        if key.expiry == 0 {
428            return Err(AccountKeychainError::key_not_found().into());
429        }
430
431        // Mark the key as revoked - this prevents replay attacks by ensuring
432        // the same key_id can never be re-authorized for this account.
433        // We keep is_revoked=true but clear other fields.
434        let revoked_key = AuthorizedKey {
435            is_revoked: true,
436            ..Default::default()
437        };
438        self.keys[msg_sender][call.keyId].write(revoked_key)?;
439
440        // Note: We don't clear spending limits here - they become inaccessible
441
442        // Emit event
443        self.emit_event(AccountKeychainEvent::key_revoked(msg_sender, call.keyId))
444    }
445
446    /// Updates the spending limit for a key-token pair. Can also convert an unlimited key into a
447    /// limited one. Delegates to `load_active_key` for existence/revocation/expiry checks.
448    ///
449    /// # Errors
450    /// - `UnauthorizedCaller` — the transaction wasn't signed by the root key or, on T6+, an
451    ///   admin access key, or on T2+ contract callers where `msg.sender != tx.origin`
452    /// - `InvalidKeyId` — on T6+, `keyId` cannot be an admin access key
453    /// - `KeyAlreadyRevoked` — the target key has been permanently revoked
454    /// - `KeyNotFound` — no key is registered under the given `keyId`
455    /// - `KeyExpired` — the key's expiry is at or before the current block timestamp
456    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 this key had unlimited spending (enforce_limits=false), enable limits now
470        if !key.enforce_limits {
471            key.enforce_limits = true;
472            self.keys[msg_sender][call.keyId].write(key)?;
473        }
474
475        // Update the spending limit
476        let limit_key = Self::spending_limit_key(msg_sender, call.keyId);
477        if self.storage.spec().is_t3() {
478            // T3: newLimit updates both the configured cap and current remaining amount,
479            // while preserving period + period_end.
480            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        // Emit event
491        self.emit_event(AccountKeychainEvent::spending_limit_updated(
492            msg_sender,
493            call.keyId,
494            call.token,
495            call.newLimit,
496        ))
497    }
498
499    /// Returns key info for the given account-key pair, or a blank entry if inexistent or revoked.
500    pub fn get_key(&self, call: getKeyCall) -> Result<KeyInfo> {
501        let key = self.keys[call.account][call.keyId].read()?;
502
503        // Key doesn't exist if expiry == 0, or key has been revoked
504        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    /// Returns the remaining spending limit for a key-token pair.
524    ///
525    /// T2+ returns zero for missing, revoked, or expired keys. Pre-T2 preserves the historical
526    /// behavior of reading the raw stored remaining amount so old blocks reexecute identically.
527    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    /// Returns the remaining spending limit together with the active period end timestamp.
542    ///
543    /// Missing, revoked, or expired keys report zeroed values instead of erroring.
544    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    /// Root/admin-only create-or-replace updates for one or more target call scopes.
562    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    /// Root/admin-only removal of one target call scope.
596    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    /// Returns whether an account key is call-scoped together with its configured call scopes.
621    ///
622    /// `isScoped = false` means unrestricted. `isScoped = true` with an empty `scopes` vec means
623    /// the key is scoped but currently allows no targets. Missing, revoked, or expired access
624    /// keys also report scoped deny-all so this getter never exposes stale persisted scope state.
625    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    /// Returns whether a TIP-1053 key-authorization witness has been manually burned.
696    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    /// Returns true for the root key or for an active admin access key.
704    /// Returns the access key used to authorize the current transaction (`Address::ZERO` = root key).
705    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    /// Internal: Set the transaction key (called during transaction validation)
714    ///
715    /// SECURITY CRITICAL: This must be called by the transaction validation logic
716    /// BEFORE the transaction is executed, to store which key authorized the transaction.
717    /// - If key_id is Address::ZERO (main key), this should store Address::ZERO
718    /// - If key_id is a specific key address, this should store that key
719    ///
720    /// This creates a secure channel between validation and the precompile to ensure
721    /// root/admin checks can distinguish root-signed transactions from access-key-signed
722    /// transactions.
723    /// Uses transient storage, so the key is automatically cleared after the transaction.
724    pub fn set_transaction_key(&mut self, key_id: Address) -> Result<()> {
725        self.transaction_key.t_write(key_id)
726    }
727
728    /// Sets the transaction origin (tx.origin) for the current transaction.
729    ///
730    /// Called by the handler before transaction execution.
731    /// Uses transient storage, so it's automatically cleared after the transaction.
732    pub fn set_tx_origin(&mut self, origin: Address) -> Result<()> {
733        self.tx_origin.t_write(origin)
734    }
735
736    /// Persists the authorization-time restrictions for a freshly created key.
737    ///
738    /// T0-T2 only store raw spending limits. T3 additionally seeds periodic metadata and replaces
739    /// the key's call-scope tree in one pass.
740    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    /// Validates a top-level call against scoped permissions for this key.
782    ///
783    /// Validation walks the scope tree from coarse to fine:
784    /// - `is_scoped = false` => unrestricted key
785    /// - target missing from `targets` => target denied
786    /// - target present with `selectors = []` => allow any selector on that target
787    /// - selector missing from `selectors` => selector denied
788    /// - selector present with `recipients = []` => allow any recipient for that selector
789    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        // Key-level scoped flag decides whether this CALL must match the stored scope tree.
808        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        // Empty child sets mean "no further restriction" once the parent target was explicitly
817        // allowed, so a present target with `selectors = []` allows any selector.
818        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        // Scoped targets next match on the 4-byte selector.
830        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        // Likewise, a present selector with `recipients = []` means any recipient is allowed.
841        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        // Recipient-constrained selectors only permit ABI-encoded address arguments.
854        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    /// Replaces the full call-scope tree for an account key.
871    ///
872    /// `None` switches the key back to unrestricted mode, while `Some([])` preserves scoped mode
873    /// with no targets so reads can distinguish scoped deny-all from unrestricted mode. This is
874    /// the only place where an empty top-level list means deny-all; below the key level, empty
875    /// child sets mean "no further restriction".
876    fn replace_allowed_calls(
877        &mut self,
878        account_key: B256,
879        allowed_calls: Option<&[CallScope]>,
880    ) -> Result<()> {
881        // Fresh authorizations should not have any pre-existing call-scope rows because
882        // `authorize_key` rejects both existing and previously revoked keys before reaching this
883        // path. We still clear the scope tree first as a defense-in-depth measure against stale or
884        // out-of-band state, and keep it because the valid-path cost is low (empty target set).
885        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    /// Deletes every persisted target scope under an account key.
911    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    /// Deletes one target scope and all nested selector/recipient rows beneath it.
921    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    /// Clears every selector scope stored under one target.
930    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    /// Creates or replaces one target scope, including all nested selector rules.
946    fn upsert_target_scope(&mut self, account_key: B256, scope: &CallScope) -> Result<()> {
947        let target = scope.target;
948
949        // Pre-T4: validate call scopes inline
950        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            // Keeping the target while clearing nested selector rows intentionally widens this
959            // target to allow-all selectors. Future incremental remove APIs must delete the target
960            // instead of leaving `selectors = []` behind accidentally.
961            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                    // Keep the pre-T4 empty-set delete to preserve the original storage-touch
973                    // pattern. Removing it earlier changes same-tx call-scope warmness without
974                    // changing persisted state.
975                    self.key_scopes[account_key].target_scopes[target].selector_scopes[selector]
976                        .recipients
977                        .delete()?;
978                }
979            } else {
980                // `validate_selector_rules` already rejected duplicates.
981                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    /// Validates a list of [`CallScope`]s.
991    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            // Post-T4: validate call scopes before inserting
999            if self.storage.spec().is_t4() {
1000                self.validate_call_scope(scope)?;
1001            }
1002        }
1003        Ok(())
1004    }
1005
1006    /// Validates a single [`CallScope`].
1007    fn validate_call_scope(&self, scope: &CallScope) -> Result<()> {
1008        // The public API uses the absence of a target to block it, so persisting address(0) as a
1009        // real target is always confusing and serves no useful purpose.
1010        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    /// Validates per-selector scope rules for one target before they are persisted.
1022    ///
1023    /// `recipients = []` is an explicit allow-all sentinel at the selector level. To deny a
1024    /// selector entirely, omit it from `selectorRules` or remove the target scope instead of
1025    /// leaving behind an empty child set via incremental mutation.
1026    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                        // Pre-T4: validate that TIP-20 is initialized
1034                        TIP20Factory::new().is_tip20(target)?
1035                    } else {
1036                        // Post-T4: only validate the address
1037                        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    /// Ensures admin operations are authorized for this caller.
1069    ///
1070    /// Rules:
1071    /// - transaction must be signed by the root key (`transaction_key == Address::ZERO`) or,
1072    ///   on T6+, an active admin access key
1073    /// - T2+: caller must match tx.origin
1074    ///
1075    /// # Errors
1076    /// - `UnauthorizedCaller` when the transaction is signed by a non-admin access key
1077    /// - `UnauthorizedCaller` on T2+ when `msg.sender != tx.origin`
1078    /// - storage read errors from transient key/origin or account metadata lookups
1079    ///
1080    /// The T2 check prevents transaction-global root-key status from being reused by
1081    /// intermediate contracts (confused-deputy self-administration).
1082    ///
1083    /// `tx_origin` is seeded by the handler before validation/execution.
1084    /// If origin is not seeded (zero), admin ops are rejected.
1085    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    /// Internal predicate for root/admin status.
1104    ///
1105    /// Warning: this returns true when `key_id == account`, because the root key
1106    /// is implicitly admin even when it is not stored as an access key.
1107    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    /// Internal predicate for active key status.
1123    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    /// Load and validate a key exists, is not revoked, and is not expired.
1158    ///
1159    /// Returns the key if valid, or an error if:
1160    /// - Key doesn't exist (expiry == 0)
1161    /// - Key has been revoked
1162    /// - Key has expired at or before `current_timestamp`
1163    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    /// Validate keychain authorization (existence, revocation, expiry, and optionally signature type).
1187    ///
1188    /// # Arguments
1189    /// * `account` - The account that owns the key
1190    /// * `key_id` - The key identifier to validate
1191    /// * `current_timestamp` - Current block timestamp for expiry check
1192    /// * `expected_sig_type` - The signature type from the actual signature (0=Secp256k1, 1=P256,
1193    ///   2=WebAuthn). Pass `None` to skip validation (for backward compatibility pre-T1).
1194    ///
1195    /// # Errors
1196    /// - `KeyAlreadyRevoked` — the key has been permanently revoked
1197    /// - `KeyNotFound` — no key is registered under the given `key_id`
1198    /// - `KeyExpired` — `current_timestamp` is at or past the key's expiry
1199    /// - `SignatureTypeMismatch` — the key's stored type differs from `expected_sig_type`
1200    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        // Validate that the signature type matches the key type stored in the keychain
1210        // Only check if expected_sig_type is provided (T1+ hardfork)
1211        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    /// Computes the effective remaining limit at `current_timestamp` without mutating storage.
1225    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    /// Computes the effective remaining limit and period end at `current_timestamp`
1237    /// without mutating storage.
1238    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        // T2+: return zero if key doesn't exist or has been revoked
1252        if key.is_revoked || key.expiry == 0 {
1253            return Ok((U256::ZERO, 0));
1254        }
1255
1256        // T3+: return zero if key has expired
1257        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    /// Deducts `amount` from the key's remaining spending limit for `token`, failing if exceeded.
1289    ///
1290    /// # Errors
1291    /// - `KeyAlreadyRevoked` — the key has been permanently revoked
1292    /// - `KeyNotFound` — no key is registered under the given `key_id`
1293    /// - `SpendingLimitExceeded` — `amount` exceeds the key's remaining limit for `token`
1294    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 using main key (zero address), no spending limits apply
1302        if key_id == Address::ZERO {
1303            return Ok(());
1304        }
1305
1306        // Check key is valid (exists and not revoked)
1307        let current_timestamp = self.storage.timestamp().saturating_to::<u64>();
1308        let key = self.load_active_key(account, key_id, current_timestamp)?;
1309
1310        // If enforce_limits is false, this key has unlimited spending
1311        if !key.enforce_limits {
1312            return Ok(());
1313        }
1314
1315        // Check and update spending limit
1316        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        // Update remaining limit
1347        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    /// Refund spending limit after a fee refund.
1369    ///
1370    /// Restores the spending limit by the refunded amount.
1371    /// Should be called after a fee refund to avoid permanently reducing the spending limit.
1372    /// On T3, this should never restore more than the configured max in the current fee flow,
1373    /// but we still clamp as defense in depth in case a future caller violates that invariant.
1374    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        // Silently skip refund if the key was revoked or expired — the fee was already
1392        // collected and the key is no longer active, so there is nothing to restore.
1393        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        // Legacy pre-T3 rows only persisted `remaining`, so migrated keys deserialize with
1416        // `max = 0`. Preserve that legacy behavior and only clamp rows that were configured
1417        // with a real T3 max.
1418        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    /// Authorize a token transfer with access key spending limits.
1428    ///
1429    /// This method checks if the transaction is using an access key, and if so,
1430    /// verifies and updates the spending limits for that key.
1431    /// Should be called before executing a transfer.
1432    ///
1433    /// # Errors
1434    /// - `KeyAlreadyRevoked` — the session key has been permanently revoked
1435    /// - `KeyNotFound` — no key is registered for the current transaction key
1436    /// - `SpendingLimitExceeded` — `amount` exceeds the key's remaining limit for `token`
1437    pub fn authorize_transfer(
1438        &mut self,
1439        account: Address,
1440        token: Address,
1441        amount: U256,
1442    ) -> Result<()> {
1443        // Get the transaction key for this account
1444        let transaction_key = self.transaction_key.t_read()?;
1445
1446        // If using main key (Address::ZERO), no spending limits apply
1447        if transaction_key == Address::ZERO {
1448            return Ok(());
1449        }
1450
1451        // Only apply spending limits if the caller is the tx origin.
1452        let tx_origin = self.tx_origin.t_read()?;
1453        if account != tx_origin {
1454            return Ok(());
1455        }
1456
1457        // Verify and update spending limits for this access key
1458        self.verify_and_update_spending(account, transaction_key, token, amount)
1459    }
1460
1461    /// Authorize a token approval with access key spending limits.
1462    ///
1463    /// This method checks if the transaction is using an access key, and if so,
1464    /// verifies and updates the spending limits for that key.
1465    /// Should be called before executing an approval.
1466    ///
1467    /// # Errors
1468    /// - `KeyAlreadyRevoked` — the session key has been permanently revoked
1469    /// - `KeyNotFound` — no key is registered for the current transaction key
1470    /// - `SpendingLimitExceeded` — the approval increase exceeds the remaining limit for `token`
1471    pub fn authorize_approve(
1472        &mut self,
1473        account: Address,
1474        token: Address,
1475        old_approval: U256,
1476        new_approval: U256,
1477    ) -> Result<()> {
1478        // Get the transaction key for this account
1479        let transaction_key = self.transaction_key.t_read()?;
1480
1481        // If using main key (Address::ZERO), no spending limits apply
1482        if transaction_key == Address::ZERO {
1483            return Ok(());
1484        }
1485
1486        // Only apply spending limits if the caller is the tx origin.
1487        let tx_origin = self.tx_origin.t_read()?;
1488        if account != tx_origin {
1489            return Ok(());
1490        }
1491
1492        // Calculate the increase in approval (only deduct if increasing)
1493        // If old approval is 100 and new approval is 120, deduct 20 from spending limit
1494        // If old approval is 100 and new approval is 80, deduct 0 (decreasing approval is free)
1495        let approval_increase = new_approval.saturating_sub(old_approval);
1496
1497        // Only check spending limits if there's an increase in approval
1498        if approval_increase.is_zero() {
1499            return Ok(());
1500        }
1501
1502        // Verify and update spending limits for this access key
1503        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    // Helper function to assert unauthorized error
1551    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            // Test 1: Initially transaction key should be zero
2128            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            // Test 2: Set transaction key to an access key address
2136            keychain.set_transaction_key(access_key_addr)?;
2137
2138            // Test 3: Verify it was stored
2139            let loaded_key = keychain.transaction_key.t_read()?;
2140            assert_eq!(loaded_key, access_key_addr, "Transaction key should be set");
2141
2142            // Test 4: Verify getTransactionKey works
2143            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            // Test 5: Clear transaction key
2151            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            // Initialize the keychain
2173            let mut keychain = AccountKeychain::new();
2174            keychain.initialize()?;
2175
2176            // First, authorize a key with main key (transaction_key = 0) to set up the test
2177            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            // Now set transaction key to non-zero (simulating access key usage)
2192            keychain.set_transaction_key(access_key)?;
2193
2194            // Test 1: authorize_key should fail with access key
2195            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            // Test 2: revoke_key should fail with access key
2214            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            // Test 3: update_spending_limit should fail with access key
2225            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            // Mark delegated sender as a contract account to model the confused-deputy path.
2255            keychain
2256                .storage
2257                .set_code(delegated_sender, Bytecode::new_raw(vec![0x60, 0x00].into()))?;
2258
2259            // Setup a key for delegated_sender under a direct-root call.
2260            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            // Simulate a contract-mediated call where tx.origin != msg.sender.
2279            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            // On T2, contract callers are allowed for admin operations only when
2339            // `msg.sender == tx.origin`.
2340            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            // Pre-T2, admin operations do not enforce msg.sender == tx.origin.
2406            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            // Setup under matching tx.origin first.
2463            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            // On T2+, admin ops require `msg.sender == tx.origin`.
2486            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    /// Admin ops on T2 must reject when `tx_origin` is never seeded (zero).
2503    ///
2504    /// This catches any execution path that forgets to call `seed_tx_origin`.
2505    #[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            // Bootstrap: seed origin so we can authorize a key for later revoke/update tests.
2518            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            // Clear tx_origin back to zero — simulates an execution path that
2541            // never called seed_tx_origin.
2542            keychain.set_tx_origin(Address::ZERO)?;
2543
2544            // authorize_key must reject
2545            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            // revoke_key must reject
2567            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            // update_spending_limit must reject
2575            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            // Use main key for all operations
2604            keychain.set_transaction_key(Address::ZERO)?;
2605            keychain.set_tx_origin(account)?;
2606
2607            // Step 1: Authorize a key with a spending limit
2608            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            // Verify key exists and limit is set
2626            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            // Step 2: Revoke the key
2642            let revoke_call = revokeKeyCall { keyId: key_id };
2643            keychain.revoke_key(account, revoke_call)?;
2644
2645            // Verify key is revoked and remaining limit returns 0
2646            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            // Step 3: Try to re-authorize the same key (replay attack)
2662            // This should fail because the key was revoked
2663            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            // Verify it's the correct error
2670            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        // Must use T0 hardfork for expiry validation to be enforced
2686        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            // Use main key for the operation
2694            keychain.set_transaction_key(Address::ZERO)?;
2695
2696            // Try to authorize with expiry = 0 (in the past)
2697            let auth_call = authorizeKeyCall {
2698                keyId: key_id,
2699                signatureType: SignatureType::Secp256k1,
2700                config: KeyRestrictions {
2701                    expiry: 0, // Zero expiry is in the past - should fail
2702                    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            // Verify it's the correct error
2715            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            // Also test with a non-zero but past expiry
2726            let auth_call_past = authorizeKeyCall {
2727                keyId: key_id,
2728                signatureType: SignatureType::Secp256k1,
2729                config: KeyRestrictions {
2730                    expiry: 1, // Very old timestamp - should fail
2731                    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            // Use main key for all operations
2822            keychain.set_transaction_key(Address::ZERO)?;
2823
2824            // Authorize key 1
2825            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            // Revoke key 1
2839            keychain.revoke_key(account, revokeKeyCall { keyId: key_id_1 })?;
2840
2841            // Authorizing a different key (key 2) should still work
2842            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            // Verify key 2 is authorized
2856            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            // authorize access key with 100 token spending limit
2881            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            // Switch to access key for remaining tests
2909            keychain.set_transaction_key(access_key)?;
2910
2911            // Increase approval by 30, which deducts from the limit
2912            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            // Decrease approval to 20, does not affect limit
2922            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            // Increase from 20 to 50, reducing the limit by 30
2932            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            // Assert that spending limits only applied when account is tx origin
2942            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)); // unchanged
2950
2951            // Assert that exceeding remaining limit fails
2952            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            // Assert that the main key bypasses spending limits, does not affect existing limits
2961            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 that spending limits are only enforced when msg_sender == tx_origin.
2976    ///
2977    /// This test verifies the fix for the bug where spending limits were incorrectly
2978    /// applied to contract-initiated transfers. The scenario:
2979    ///
2980    /// 1. EOA Alice uses an access key with spending limits
2981    /// 2. Alice calls a contract that transfers tokens
2982    /// 3. The contract's transfer should NOT be subject to Alice's spending limits
2983    ///    (the contract is transferring its own tokens, not Alice's)
2984    #[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(); // The EOA that signs the transaction
2989        let access_key = Address::random(); // Alice's access key with spending limits
2990        let contract_address = Address::random(); // A contract that Alice calls
2991        let token = Address::random();
2992
2993        StorageCtx::enter(&mut storage, || {
2994            let mut keychain = AccountKeychain::new();
2995            keychain.initialize()?;
2996
2997            // Setup: Alice authorizes an access key with a spending limit of 100 tokens
2998            keychain.set_transaction_key(Address::ZERO)?; // Use main key for setup
2999            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            // Verify spending limit is set
3019            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            // Now simulate a transaction where Alice uses her access key
3031            keychain.set_transaction_key(access_key)?;
3032            keychain.set_tx_origin(eoa_alice)?;
3033
3034            // Test 1: When msg_sender == tx_origin (Alice directly transfers)
3035            // Spending limit SHOULD be enforced
3036            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            // Test 2: When msg_sender != tx_origin (contract transfers its own tokens)
3050            // Spending limit should NOT be enforced - the contract isn't spending Alice's tokens
3051            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            // Test 3: Alice can still spend her remaining limit
3065            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            // Test 4: Alice cannot exceed her spending limit
3079            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            // Test 5: But contracts can still transfer (they're not subject to Alice's limits)
3086            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        // Use pre-T0 to avoid expiry validation (focus on existence check)
3100        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            // Authorize a key with expiry = 1 (minimal positive value)
3109            let auth_call = authorizeKeyCall {
3110                keyId: key_id,
3111                signatureType: SignatureType::Secp256k1,
3112                config: KeyRestrictions {
3113                    expiry: 1, // Minimal positive expiry
3114                    enforceLimits: false,
3115                    limits: vec![],
3116                    allowAnyCalls: true,
3117                    allowedCalls: vec![],
3118                },
3119            };
3120            authorize_key(&mut keychain, account, auth_call.clone())?;
3121
3122            // Verify key exists with expiry = 1
3123            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            // Try to re-authorize - should fail because expiry > 0
3130            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        // Same inputs should produce same output
3154        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        // Different accounts should produce different hashes
3159        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        // Different key_ids should produce different hashes
3166        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        // Order matters: (account1, key_id2) != (key_id2, account1) if we swap
3173        // But since the types are the same, let's verify swapping produces different result
3174        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        // Verify hash is not default/zero
3181        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            // Before initialize: operations should work after init
3191            keychain.initialize()?;
3192
3193            // Verify we can perform operations after initialize
3194            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            // This would fail if initialize didn't set up storage properly
3210            authorize_key(&mut keychain, account, auth_call)?;
3211
3212            // Verify key was stored
3213            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            // Authorize with WebAuthn signature type
3234            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            // Verify key was stored with WebAuthn type (value = 2)
3248            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            // Verify via validation that signature type 2 is accepted
3259            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            // Verify signature type mismatch is rejected
3266            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            // Authorize a key with expiry far in the future
3285            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            // Update should work when key is not expired
3303            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            // Verify the limit was updated
3315            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            // Case 1: Key with enforce_limits = false
3338            let auth_call = authorizeKeyCall {
3339                keyId: key_id,
3340                signatureType: SignatureType::Secp256k1,
3341                config: KeyRestrictions {
3342                    expiry: u64::MAX,
3343                    enforceLimits: false, // Initially no limits
3344                    limits: vec![],
3345                    allowAnyCalls: true,
3346                    allowedCalls: vec![],
3347                },
3348            };
3349            authorize_key(&mut keychain, account, auth_call)?;
3350
3351            // Verify key has enforce_limits = false
3352            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            // Update spending limit - this should toggle enforce_limits to true
3362            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            // Verify enforce_limits is now true
3370            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            // Verify the spending limit was set
3380            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            // Setup: Create and revoke a key
3404            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            // Setup: Create a valid key
3424            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            // Test 1: Revoked key (expiry=0, is_revoked=true) - should return empty with isRevoked=true
3438            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            // Test 2: Never existed key (expiry=0, is_revoked=false) - should return empty
3453            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            // Test 3: Valid key (expiry>0, is_revoked=false) - should return actual key info
3468            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            // Create keys with each signature type
3500            authorize_key(
3501                &mut keychain,
3502                account,
3503                authorizeKeyCall {
3504                    keyId: key_secp,
3505                    signatureType: SignatureType::Secp256k1, // type 0
3506                    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, // type 1
3522                    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, // type 2
3538                    config: KeyRestrictions {
3539                        expiry: u64::MAX,
3540                        enforceLimits: false,
3541                        limits: vec![],
3542                        allowAnyCalls: true,
3543                        allowedCalls: vec![],
3544                    },
3545                },
3546            )?;
3547
3548            // Verify each key returns the correct signature type
3549            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            // Verify they are all distinct
3580            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            // Use main key for authorization
3598            keychain.set_transaction_key(Address::ZERO)?;
3599
3600            // Authorize a P256 key
3601            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            // Test 1: Validation should succeed with matching signature type (P256 = 1)
3615            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            // Test 2: Validation should fail with mismatched signature type (Secp256k1 = 0)
3622            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            // Test 3: Validation should fail with WebAuthn (2) when key is P256 (1)
3639            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            // Test 4: Validation should succeed with None (backward compatibility, pre-T1)
3647            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            // warp block time so that key auth expires
4699            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                    // T3: expired keys are zeroed out
4707                    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                    // T3+: expired key returns zero directly
4718                    assert_eq!(StorageCtx.counter_sload() - sload_before, 1);
4719                } else {
4720                    // pre-T3: expired keys are NOT zeroed; the raw stored limit is returned
4721                    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                    // pre-T2: direct storage read without reading the key
4729                    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                // revoke key auth
4775                keychain.revoke_key(account, revokeKeyCall { keyId: key_id })?;
4776
4777                let sload_before = StorageCtx.counter_sload();
4778                if hardfork.is_t2() {
4779                    // T2+: revoked keys are zeroed out
4780                    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                    // T2+: revoked key returns zero directly
4791                    assert_eq!(StorageCtx.counter_sload() - sload_before, 1);
4792                } else {
4793                    // pre-T2: revoked keys are NOT zeroed; the raw stored limit is returned
4794                    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                    // pre-T2: direct storage read without reading the key
4802                    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}