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