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 __packing_authorized_key::{
12    ENFORCE_LIMITS_LOC, EXPIRY_LOC, IS_REVOKED_LOC, SIGNATURE_TYPE_LOC,
13};
14use tempo_contracts::precompiles::{AccountKeychainError, AccountKeychainEvent};
15pub use tempo_contracts::precompiles::{
16    IAccountKeychain,
17    IAccountKeychain::{
18        KeyInfo, SignatureType, TokenLimit, authorizeKeyCall, getKeyCall, getRemainingLimitCall,
19        getTransactionKeyCall, revokeKeyCall, updateSpendingLimitCall,
20    },
21};
22
23use crate::{
24    ACCOUNT_KEYCHAIN_ADDRESS,
25    error::Result,
26    storage::{Handler, Mapping, packing::insert_into_word},
27};
28use alloy::primitives::{Address, B256, U256};
29use tempo_precompiles_macros::{Storable, contract};
30
31/// Key information stored in the precompile
32///
33/// Storage layout (packed into single slot, right-aligned):
34/// - byte 0: signature_type (u8)
35/// - bytes 1-8: expiry (u64, little-endian)
36/// - byte 9: enforce_limits (bool)
37/// - byte 10: is_revoked (bool)
38#[derive(Debug, Clone, Default, PartialEq, Eq, Storable)]
39pub struct AuthorizedKey {
40    /// Signature type: 0 = secp256k1, 1 = P256, 2 = WebAuthn
41    pub signature_type: u8,
42    /// Block timestamp when key expires
43    pub expiry: u64,
44    /// Whether to enforce spending limits for this key
45    pub enforce_limits: bool,
46    /// Whether this key has been revoked. Once revoked, a key cannot be re-authorized
47    /// with the same key_id. This prevents replay attacks.
48    pub is_revoked: bool,
49}
50
51// TODO(rusowsky): remove this and create a read-only wrapper that is callable from read-only ctx with db access
52impl AuthorizedKey {
53    /// Decode AuthorizedKey from a storage slot value
54    ///
55    /// This is useful for read-only contexts (like pool validation) that don't have
56    /// access to PrecompileStorageProvider but need to decode the packed struct.
57    pub fn decode_from_slot(slot_value: U256) -> Self {
58        use crate::storage::{LayoutCtx, Storable, packing::PackedSlot};
59
60        // NOTE: fine to expect, as `StorageOps` on `PackedSlot` are infallible
61        Self::load(&PackedSlot(slot_value), U256::ZERO, LayoutCtx::FULL)
62            .expect("unable to decode AuthorizedKey from slot")
63    }
64
65    /// Encode AuthorizedKey to a storage slot value
66    ///
67    /// This is useful for tests that need to set up storage state directly.
68    pub fn encode_to_slot(&self) -> U256 {
69        let encoded = insert_into_word(
70            U256::ZERO,
71            &self.signature_type,
72            SIGNATURE_TYPE_LOC.offset_bytes,
73            SIGNATURE_TYPE_LOC.size,
74        )
75        .expect("unable to insert 'signature_type'");
76
77        let encoded = insert_into_word(
78            encoded,
79            &self.expiry,
80            EXPIRY_LOC.offset_bytes,
81            EXPIRY_LOC.size,
82        )
83        .expect("unable to insert 'expiry'");
84
85        let encoded = insert_into_word(
86            encoded,
87            &self.enforce_limits,
88            ENFORCE_LIMITS_LOC.offset_bytes,
89            ENFORCE_LIMITS_LOC.size,
90        )
91        .expect("unable to insert 'enforce_limits'");
92
93        insert_into_word(
94            encoded,
95            &self.is_revoked,
96            IS_REVOKED_LOC.offset_bytes,
97            IS_REVOKED_LOC.size,
98        )
99        .expect("unable to insert 'is_revoked'")
100    }
101}
102
103/// Account Keychain contract for managing authorized keys (session keys, spending limits).
104///
105/// The struct fields define the on-chain storage layout; the `#[contract]` macro generates the
106/// storage handlers which provide an ergonomic way to interact with the EVM state.
107#[contract(addr = ACCOUNT_KEYCHAIN_ADDRESS)]
108pub struct AccountKeychain {
109    // keys[account][keyId] -> AuthorizedKey
110    keys: Mapping<Address, Mapping<Address, AuthorizedKey>>,
111    // spendingLimits[(account, keyId)][token] -> amount
112    // Using a hash of account and keyId as the key to avoid triple nesting
113    spending_limits: Mapping<B256, Mapping<Address, U256>>,
114
115    // WARNING(rusowsky): transient storage slots must always be placed at the very end until the `contract`
116    // macro is refactored and has 2 independent layouts (persistent and transient).
117    // If new (persistent) storage fields need to be added to the precompile, they must go above this one.
118    transaction_key: Address,
119    // The transaction origin (tx.origin) - the EOA that signed the transaction.
120    // Used to ensure spending limits only apply when msg_sender == tx_origin.
121    tx_origin: Address,
122}
123
124impl AccountKeychain {
125    /// Create a hash key for spending limits mapping from account and keyId.
126    ///
127    /// This is used to access `spending_limits[key][token]` where `key` is the result
128    /// of this function. The hash combines account and key_id to avoid triple nesting.
129    pub fn spending_limit_key(account: Address, key_id: Address) -> B256 {
130        use alloy::primitives::keccak256;
131        let mut data = [0u8; 40];
132        data[..20].copy_from_slice(account.as_slice());
133        data[20..].copy_from_slice(key_id.as_slice());
134        keccak256(data)
135    }
136
137    /// Initializes the account keychain precompile.
138    pub fn initialize(&mut self) -> Result<()> {
139        self.__initialize()
140    }
141
142    /// Registers a new access key with signature type, expiry, and optional per-token spending
143    /// limits. Only callable with the account's main key (not a session key).
144    ///
145    /// # Errors
146    /// - `UnauthorizedCaller` — only the main key can authorize/revoke and, for contract
147    ///   callers on T2+, `msg.sender` must match `tx.origin`
148    /// - `ZeroPublicKey` — `keyId` cannot be the zero address
149    /// - `ExpiryInPast` — expiry must be in the future (enforced since T0)
150    /// - `KeyAlreadyExists` — a key with this ID is already registered
151    /// - `KeyAlreadyRevoked` — revoked keys cannot be re-authorized
152    /// - `InvalidSignatureType` — must be Secp256k1, P256, or WebAuthn
153    pub fn authorize_key(&mut self, msg_sender: Address, call: authorizeKeyCall) -> Result<()> {
154        self.ensure_admin_caller(msg_sender)?;
155
156        // Validate inputs
157        if call.keyId == Address::ZERO {
158            return Err(AccountKeychainError::zero_public_key().into());
159        }
160
161        // T0+: Expiry must be in the future (also catches expiry == 0 which means "key doesn't exist")
162        if self.storage.spec().is_t0() {
163            let current_timestamp = self.storage.timestamp().saturating_to::<u64>();
164            if call.expiry <= current_timestamp {
165                return Err(AccountKeychainError::expiry_in_past().into());
166            }
167        }
168
169        // Check if key already exists (key exists if expiry > 0)
170        let existing_key = self.keys[msg_sender][call.keyId].read()?;
171        if existing_key.expiry > 0 {
172            return Err(AccountKeychainError::key_already_exists().into());
173        }
174
175        // Check if this key was previously revoked - prevents replay attacks
176        if existing_key.is_revoked {
177            return Err(AccountKeychainError::key_already_revoked().into());
178        }
179
180        // Convert SignatureType enum to u8 for storage
181        let signature_type = match call.signatureType {
182            SignatureType::Secp256k1 => 0,
183            SignatureType::P256 => 1,
184            SignatureType::WebAuthn => 2,
185            _ => return Err(AccountKeychainError::invalid_signature_type().into()),
186        };
187
188        // Create and store the new key
189        let new_key = AuthorizedKey {
190            signature_type,
191            expiry: call.expiry,
192            enforce_limits: call.enforceLimits,
193            is_revoked: false,
194        };
195
196        self.keys[msg_sender][call.keyId].write(new_key)?;
197
198        // Set initial spending limits (only if enforce_limits is true)
199        if call.enforceLimits {
200            let limit_key = Self::spending_limit_key(msg_sender, call.keyId);
201            for limit in call.limits {
202                self.spending_limits[limit_key][limit.token].write(limit.amount)?;
203            }
204        }
205
206        // Emit event
207        self.emit_event(AccountKeychainEvent::KeyAuthorized(
208            IAccountKeychain::KeyAuthorized {
209                account: msg_sender,
210                publicKey: call.keyId,
211                signatureType: signature_type,
212                expiry: call.expiry,
213            },
214        ))
215    }
216
217    /// Permanently revokes an access key. Once revoked, a key ID can never be re-authorized for
218    /// this account, preventing replay of old `KeyAuthorization` signatures.
219    ///
220    /// # Errors
221    /// - `UnauthorizedCaller` — only the main key can authorize/revoke and, for contract
222    ///   callers on T2+, `msg.sender` must match `tx.origin`
223    /// - `KeyNotFound` — no key registered with this ID
224    pub fn revoke_key(&mut self, msg_sender: Address, call: revokeKeyCall) -> Result<()> {
225        self.ensure_admin_caller(msg_sender)?;
226
227        let key = self.keys[msg_sender][call.keyId].read()?;
228
229        // Key exists if expiry > 0
230        if key.expiry == 0 {
231            return Err(AccountKeychainError::key_not_found().into());
232        }
233
234        // Mark the key as revoked - this prevents replay attacks by ensuring
235        // the same key_id can never be re-authorized for this account.
236        // We keep is_revoked=true but clear other fields.
237        let revoked_key = AuthorizedKey {
238            is_revoked: true,
239            ..Default::default()
240        };
241        self.keys[msg_sender][call.keyId].write(revoked_key)?;
242
243        // Note: We don't clear spending limits here - they become inaccessible
244
245        // Emit event
246        self.emit_event(AccountKeychainEvent::KeyRevoked(
247            IAccountKeychain::KeyRevoked {
248                account: msg_sender,
249                publicKey: call.keyId,
250            },
251        ))
252    }
253
254    /// Updates the spending limit for a key-token pair. Can also convert an unlimited key into a
255    /// limited one. Delegates to `load_active_key` for existence/revocation checks.
256    ///
257    /// # Errors
258    /// - `UnauthorizedCaller` — the transaction wasn't signed by the main key, or on T2+
259    ///   contract callers where `msg.sender != tx.origin`
260    /// - `KeyAlreadyRevoked` — the target key has been permanently revoked
261    /// - `KeyNotFound` — no key is registered under the given `keyId`
262    /// - `KeyExpired` — the key's expiry is at or before the current block timestamp
263    pub fn update_spending_limit(
264        &mut self,
265        msg_sender: Address,
266        call: updateSpendingLimitCall,
267    ) -> Result<()> {
268        self.ensure_admin_caller(msg_sender)?;
269
270        // Verify key exists, hasn't been revoked, and hasn't expired
271        let mut key = self.load_active_key(msg_sender, call.keyId)?;
272
273        let current_timestamp = self.storage.timestamp().saturating_to::<u64>();
274        if current_timestamp >= key.expiry {
275            return Err(AccountKeychainError::key_expired().into());
276        }
277
278        // If this key had unlimited spending (enforce_limits=false), enable limits now
279        if !key.enforce_limits {
280            key.enforce_limits = true;
281            self.keys[msg_sender][call.keyId].write(key)?;
282        }
283
284        // Update the spending limit
285        let limit_key = Self::spending_limit_key(msg_sender, call.keyId);
286        self.spending_limits[limit_key][call.token].write(call.newLimit)?;
287
288        // Emit event
289        self.emit_event(AccountKeychainEvent::SpendingLimitUpdated(
290            IAccountKeychain::SpendingLimitUpdated {
291                account: msg_sender,
292                publicKey: call.keyId,
293                token: call.token,
294                newLimit: call.newLimit,
295            },
296        ))
297    }
298
299    /// Returns key info for the given account-key pair, or a blank entry if inexistent or revoked.
300    pub fn get_key(&self, call: getKeyCall) -> Result<KeyInfo> {
301        let key = self.keys[call.account][call.keyId].read()?;
302
303        // Key doesn't exist if expiry == 0, or key has been revoked
304        if key.expiry == 0 || key.is_revoked {
305            return Ok(KeyInfo {
306                signatureType: SignatureType::Secp256k1,
307                keyId: Address::ZERO,
308                expiry: 0,
309                enforceLimits: false,
310                isRevoked: key.is_revoked,
311            });
312        }
313
314        // Convert u8 signature_type to SignatureType enum
315        let signature_type = match key.signature_type {
316            0 => SignatureType::Secp256k1,
317            1 => SignatureType::P256,
318            2 => SignatureType::WebAuthn,
319            _ => SignatureType::Secp256k1, // Default fallback
320        };
321
322        Ok(KeyInfo {
323            signatureType: signature_type,
324            keyId: call.keyId,
325            expiry: key.expiry,
326            enforceLimits: key.enforce_limits,
327            isRevoked: key.is_revoked,
328        })
329    }
330
331    /// Returns the remaining spending limit for a key-token pair, or a blank entry if inexistent
332    /// or revoked (T2+).
333    pub fn get_remaining_limit(&self, call: getRemainingLimitCall) -> Result<U256> {
334        // T2+: return zero if key doesn't exist or has been revoked
335        if self.storage.spec().is_t2() {
336            let key = self.keys[call.account][call.keyId].read()?;
337            if key.expiry == 0 || key.is_revoked {
338                return Ok(U256::ZERO);
339            }
340        }
341
342        let limit_key = Self::spending_limit_key(call.account, call.keyId);
343        self.spending_limits[limit_key][call.token].read()
344    }
345
346    /// Returns the access key used to authorize the current transaction (`Address::ZERO` = root key).
347    pub fn get_transaction_key(
348        &self,
349        _call: getTransactionKeyCall,
350        _msg_sender: Address,
351    ) -> Result<Address> {
352        self.transaction_key.t_read()
353    }
354
355    /// Internal: Set the transaction key (called during transaction validation)
356    ///
357    /// SECURITY CRITICAL: This must be called by the transaction validation logic
358    /// BEFORE the transaction is executed, to store which key authorized the transaction.
359    /// - If key_id is Address::ZERO (main key), this should store Address::ZERO
360    /// - If key_id is a specific key address, this should store that key
361    ///
362    /// This creates a secure channel between validation and the precompile to ensure
363    /// only the main key can authorize/revoke other keys.
364    /// Uses transient storage, so the key is automatically cleared after the transaction.
365    pub fn set_transaction_key(&mut self, key_id: Address) -> Result<()> {
366        self.transaction_key.t_write(key_id)
367    }
368
369    /// Sets the transaction origin (tx.origin) for the current transaction.
370    ///
371    /// Called by the handler before transaction execution.
372    /// Uses transient storage, so it's automatically cleared after the transaction.
373    pub fn set_tx_origin(&mut self, origin: Address) -> Result<()> {
374        self.tx_origin.t_write(origin)
375    }
376
377    /// Ensures admin operations are authorized for this caller.
378    ///
379    /// Rules:
380    /// - transaction must be signed by the main key (`transaction_key == Address::ZERO`)
381    /// - T2+: caller must match tx.origin
382    ///
383    /// # Errors
384    /// - `UnauthorizedCaller` when called via an access key
385    /// - `UnauthorizedCaller` on T2+ when `msg.sender != tx.origin`
386    /// - storage read errors from transient key/origin or account metadata lookups
387    ///
388    /// The T2 check prevents transaction-global root-key status from being reused by
389    /// intermediate contracts (confused-deputy self-administration).
390    ///
391    /// `tx_origin` is seeded by the handler before validation/execution on in-repo flows.
392    /// The zero-origin fallback here exists for out-of-tree integrations that bypass
393    /// handler setup.
394    fn ensure_admin_caller(&self, msg_sender: Address) -> Result<()> {
395        if !self.transaction_key.t_read()?.is_zero() {
396            return Err(AccountKeychainError::unauthorized_caller().into());
397        }
398
399        if self.storage.spec().is_t2() {
400            let tx_origin = self.tx_origin.t_read()?;
401            if tx_origin != Address::ZERO && tx_origin != msg_sender {
402                return Err(AccountKeychainError::unauthorized_caller().into());
403            }
404        }
405
406        Ok(())
407    }
408
409    /// Load and validate a key exists and is not revoked.
410    ///
411    /// Returns the key if valid, or an error if:
412    /// - Key doesn't exist (expiry == 0)
413    /// - Key has been revoked
414    ///
415    /// Note: This does NOT check expiry against current timestamp.
416    /// Callers should check expiry separately if needed.
417    fn load_active_key(&self, account: Address, key_id: Address) -> Result<AuthorizedKey> {
418        let key = self.keys[account][key_id].read()?;
419
420        if key.is_revoked {
421            return Err(AccountKeychainError::key_already_revoked().into());
422        }
423
424        if key.expiry == 0 {
425            return Err(AccountKeychainError::key_not_found().into());
426        }
427
428        Ok(key)
429    }
430
431    /// Validate keychain authorization (existence, revocation, expiry, and optionally signature type).
432    ///
433    /// # Arguments
434    /// * `account` - The account that owns the key
435    /// * `key_id` - The key identifier to validate
436    /// * `current_timestamp` - Current block timestamp for expiry check
437    /// * `expected_sig_type` - The signature type from the actual signature (0=Secp256k1, 1=P256,
438    ///   2=WebAuthn). Pass `None` to skip validation (for backward compatibility pre-T1).
439    ///
440    /// # Errors
441    /// - `KeyAlreadyRevoked` — the key has been permanently revoked
442    /// - `KeyNotFound` — no key is registered under the given `key_id`
443    /// - `KeyExpired` — `current_timestamp` is at or past the key's expiry
444    /// - `SignatureTypeMismatch` — the key's stored type differs from `expected_sig_type`
445    pub fn validate_keychain_authorization(
446        &self,
447        account: Address,
448        key_id: Address,
449        current_timestamp: u64,
450        expected_sig_type: Option<u8>,
451    ) -> Result<()> {
452        let key = self.load_active_key(account, key_id)?;
453
454        if current_timestamp >= key.expiry {
455            return Err(AccountKeychainError::key_expired().into());
456        }
457
458        // Validate that the signature type matches the key type stored in the keychain
459        // Only check if expected_sig_type is provided (T1+ hardfork)
460        if let Some(sig_type) = expected_sig_type
461            && key.signature_type != sig_type
462        {
463            return Err(AccountKeychainError::signature_type_mismatch(
464                key.signature_type,
465                sig_type,
466            )
467            .into());
468        }
469
470        Ok(())
471    }
472
473    /// Deducts `amount` from the key's remaining spending limit for `token`, failing if exceeded.
474    ///
475    /// # Errors
476    /// - `KeyAlreadyRevoked` — the key has been permanently revoked
477    /// - `KeyNotFound` — no key is registered under the given `key_id`
478    /// - `SpendingLimitExceeded` — `amount` exceeds the key's remaining limit for `token`
479    pub fn verify_and_update_spending(
480        &mut self,
481        account: Address,
482        key_id: Address,
483        token: Address,
484        amount: U256,
485    ) -> Result<()> {
486        // If using main key (zero address), no spending limits apply
487        if key_id == Address::ZERO {
488            return Ok(());
489        }
490
491        // Check key is valid (exists and not revoked)
492        let key = self.load_active_key(account, key_id)?;
493
494        // If enforce_limits is false, this key has unlimited spending
495        if !key.enforce_limits {
496            return Ok(());
497        }
498
499        // Check and update spending limit
500        let limit_key = Self::spending_limit_key(account, key_id);
501        let remaining = self.spending_limits[limit_key][token].read()?;
502
503        if amount > remaining {
504            return Err(AccountKeychainError::spending_limit_exceeded().into());
505        }
506
507        // Update remaining limit
508        self.spending_limits[limit_key][token].write(remaining - amount)
509    }
510
511    /// Refund spending limit after a fee refund.
512    ///
513    /// Restores the spending limit by the refunded amount, clamped so it never
514    /// exceeds the limit that was set when the key was authorized.
515    /// Should be called after a fee refund to avoid permanently reducing the spending limit.
516    pub fn refund_spending_limit(
517        &mut self,
518        account: Address,
519        token: Address,
520        amount: U256,
521    ) -> Result<()> {
522        let transaction_key = self.transaction_key.t_read()?;
523
524        if transaction_key == Address::ZERO {
525            return Ok(());
526        }
527
528        let tx_origin = self.tx_origin.t_read()?;
529        if account != tx_origin {
530            return Ok(());
531        }
532
533        // Silently skip refund if the key was revoked or expired — the fee was already
534        // collected and the key is no longer active, so there is nothing to restore.
535        let key = match self.load_active_key(account, transaction_key) {
536            Ok(key) => key,
537            Err(_) => return Ok(()),
538        };
539
540        if !key.enforce_limits {
541            return Ok(());
542        }
543
544        let limit_key = Self::spending_limit_key(account, transaction_key);
545        let remaining = self.spending_limits[limit_key][token].read()?;
546
547        let new_remaining = remaining.saturating_add(amount);
548
549        self.spending_limits[limit_key][token].write(new_remaining)
550    }
551
552    /// Authorize a token transfer with access key spending limits.
553    ///
554    /// This method checks if the transaction is using an access key, and if so,
555    /// verifies and updates the spending limits for that key.
556    /// Should be called before executing a transfer.
557    ///
558    /// # Errors
559    /// - `KeyAlreadyRevoked` — the session key has been permanently revoked
560    /// - `KeyNotFound` — no key is registered for the current transaction key
561    /// - `SpendingLimitExceeded` — `amount` exceeds the key's remaining limit for `token`
562    pub fn authorize_transfer(
563        &mut self,
564        account: Address,
565        token: Address,
566        amount: U256,
567    ) -> Result<()> {
568        // Get the transaction key for this account
569        let transaction_key = self.transaction_key.t_read()?;
570
571        // If using main key (Address::ZERO), no spending limits apply
572        if transaction_key == Address::ZERO {
573            return Ok(());
574        }
575
576        // Only apply spending limits if the caller is the tx origin.
577        let tx_origin = self.tx_origin.t_read()?;
578        if account != tx_origin {
579            return Ok(());
580        }
581
582        // Verify and update spending limits for this access key
583        self.verify_and_update_spending(account, transaction_key, token, amount)
584    }
585
586    /// Authorize a token approval with access key spending limits.
587    ///
588    /// This method checks if the transaction is using an access key, and if so,
589    /// verifies and updates the spending limits for that key.
590    /// Should be called before executing an approval.
591    ///
592    /// # Errors
593    /// - `KeyAlreadyRevoked` — the session key has been permanently revoked
594    /// - `KeyNotFound` — no key is registered for the current transaction key
595    /// - `SpendingLimitExceeded` — the approval increase exceeds the remaining limit for `token`
596    pub fn authorize_approve(
597        &mut self,
598        account: Address,
599        token: Address,
600        old_approval: U256,
601        new_approval: U256,
602    ) -> Result<()> {
603        // Get the transaction key for this account
604        let transaction_key = self.transaction_key.t_read()?;
605
606        // If using main key (Address::ZERO), no spending limits apply
607        if transaction_key == Address::ZERO {
608            return Ok(());
609        }
610
611        // Only apply spending limits if the caller is the tx origin.
612        let tx_origin = self.tx_origin.t_read()?;
613        if account != tx_origin {
614            return Ok(());
615        }
616
617        // Calculate the increase in approval (only deduct if increasing)
618        // If old approval is 100 and new approval is 120, deduct 20 from spending limit
619        // If old approval is 100 and new approval is 80, deduct 0 (decreasing approval is free)
620        let approval_increase = new_approval.saturating_sub(old_approval);
621
622        // Only check spending limits if there's an increase in approval
623        if approval_increase.is_zero() {
624            return Ok(());
625        }
626
627        // Verify and update spending limits for this access key
628        self.verify_and_update_spending(account, transaction_key, token, approval_increase)
629    }
630}
631
632#[cfg(test)]
633mod tests {
634    use super::*;
635    use crate::{
636        error::TempoPrecompileError,
637        storage::{StorageCtx, hashmap::HashMapStorageProvider},
638    };
639    use alloy::primitives::{Address, U256};
640    use revm::state::Bytecode;
641    use tempo_chainspec::hardfork::TempoHardfork;
642    use tempo_contracts::precompiles::IAccountKeychain::SignatureType;
643
644    // Helper function to assert unauthorized error
645    fn assert_unauthorized_error(error: TempoPrecompileError) {
646        match error {
647            TempoPrecompileError::AccountKeychainError(e) => {
648                assert!(
649                    matches!(e, AccountKeychainError::UnauthorizedCaller(_)),
650                    "Expected UnauthorizedCaller error, got: {e:?}"
651                );
652            }
653            _ => panic!("Expected AccountKeychainError, got: {error:?}"),
654        }
655    }
656
657    #[test]
658    fn test_transaction_key_transient_storage() -> eyre::Result<()> {
659        let mut storage = HashMapStorageProvider::new(1);
660        let access_key_addr = Address::random();
661        StorageCtx::enter(&mut storage, || {
662            let mut keychain = AccountKeychain::new();
663
664            // Test 1: Initially transaction key should be zero
665            let initial_key = keychain.transaction_key.t_read()?;
666            assert_eq!(
667                initial_key,
668                Address::ZERO,
669                "Initial transaction key should be zero"
670            );
671
672            // Test 2: Set transaction key to an access key address
673            keychain.set_transaction_key(access_key_addr)?;
674
675            // Test 3: Verify it was stored
676            let loaded_key = keychain.transaction_key.t_read()?;
677            assert_eq!(loaded_key, access_key_addr, "Transaction key should be set");
678
679            // Test 4: Verify getTransactionKey works
680            let get_tx_key_call = getTransactionKeyCall {};
681            let result = keychain.get_transaction_key(get_tx_key_call, Address::ZERO)?;
682            assert_eq!(
683                result, access_key_addr,
684                "getTransactionKey should return the set key"
685            );
686
687            // Test 5: Clear transaction key
688            keychain.set_transaction_key(Address::ZERO)?;
689            let cleared_key = keychain.transaction_key.t_read()?;
690            assert_eq!(
691                cleared_key,
692                Address::ZERO,
693                "Transaction key should be cleared"
694            );
695
696            Ok(())
697        })
698    }
699
700    #[test]
701    fn test_admin_operations_blocked_with_access_key() -> eyre::Result<()> {
702        let mut storage = HashMapStorageProvider::new(1);
703        let msg_sender = Address::random();
704        let existing_key = Address::random();
705        let access_key = Address::random();
706        let token = Address::random();
707        let other = Address::random();
708        StorageCtx::enter(&mut storage, || {
709            // Initialize the keychain
710            let mut keychain = AccountKeychain::new();
711            keychain.initialize()?;
712
713            // First, authorize a key with main key (transaction_key = 0) to set up the test
714            keychain.set_transaction_key(Address::ZERO)?;
715            let setup_call = authorizeKeyCall {
716                keyId: existing_key,
717                signatureType: SignatureType::Secp256k1,
718                expiry: u64::MAX,
719                enforceLimits: true,
720                limits: vec![],
721            };
722            keychain.authorize_key(msg_sender, setup_call)?;
723
724            // Now set transaction key to non-zero (simulating access key usage)
725            keychain.set_transaction_key(access_key)?;
726
727            // Test 1: authorize_key should fail with access key
728            let auth_call = authorizeKeyCall {
729                keyId: other,
730                signatureType: SignatureType::P256,
731                expiry: u64::MAX,
732                enforceLimits: true,
733                limits: vec![],
734            };
735            let auth_result = keychain.authorize_key(msg_sender, auth_call);
736            assert!(
737                auth_result.is_err(),
738                "authorize_key should fail when using access key"
739            );
740            assert_unauthorized_error(auth_result.unwrap_err());
741
742            // Test 2: revoke_key should fail with access key
743            let revoke_call = revokeKeyCall {
744                keyId: existing_key,
745            };
746            let revoke_result = keychain.revoke_key(msg_sender, revoke_call);
747            assert!(
748                revoke_result.is_err(),
749                "revoke_key should fail when using access key"
750            );
751            assert_unauthorized_error(revoke_result.unwrap_err());
752
753            // Test 3: update_spending_limit should fail with access key
754            let update_call = updateSpendingLimitCall {
755                keyId: existing_key,
756                token,
757                newLimit: U256::from(1000),
758            };
759            let update_result = keychain.update_spending_limit(msg_sender, update_call);
760            assert!(
761                update_result.is_err(),
762                "update_spending_limit should fail when using access key"
763            );
764            assert_unauthorized_error(update_result.unwrap_err());
765
766            Ok(())
767        })
768    }
769
770    #[test]
771    fn test_admin_operations_require_tx_origin_on_t2() -> eyre::Result<()> {
772        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
773        let tx_origin = Address::random();
774        let delegated_sender = Address::random();
775        let existing_key = Address::random();
776        let token = Address::random();
777        let other = Address::random();
778
779        StorageCtx::enter(&mut storage, || {
780            let mut keychain = AccountKeychain::new();
781            keychain.initialize()?;
782
783            // Mark delegated sender as a contract account to model the confused-deputy path.
784            keychain
785                .storage
786                .set_code(delegated_sender, Bytecode::new_raw(vec![0x60, 0x00].into()))?;
787
788            // Setup a key for delegated_sender under a direct-root call.
789            keychain.set_transaction_key(Address::ZERO)?;
790            keychain.set_tx_origin(delegated_sender)?;
791            keychain.authorize_key(
792                delegated_sender,
793                authorizeKeyCall {
794                    keyId: existing_key,
795                    signatureType: SignatureType::Secp256k1,
796                    expiry: u64::MAX,
797                    enforceLimits: true,
798                    limits: vec![],
799                },
800            )?;
801
802            // Simulate a contract-mediated call where tx.origin != msg.sender.
803            keychain.set_tx_origin(tx_origin)?;
804
805            let auth_result = keychain.authorize_key(
806                delegated_sender,
807                authorizeKeyCall {
808                    keyId: other,
809                    signatureType: SignatureType::P256,
810                    expiry: u64::MAX,
811                    enforceLimits: true,
812                    limits: vec![],
813                },
814            );
815            assert!(auth_result.is_err());
816            assert_unauthorized_error(auth_result.unwrap_err());
817
818            let revoke_result = keychain.revoke_key(
819                delegated_sender,
820                revokeKeyCall {
821                    keyId: existing_key,
822                },
823            );
824            assert!(revoke_result.is_err());
825            assert_unauthorized_error(revoke_result.unwrap_err());
826
827            let update_result = keychain.update_spending_limit(
828                delegated_sender,
829                updateSpendingLimitCall {
830                    keyId: existing_key,
831                    token,
832                    newLimit: U256::from(1000),
833                },
834            );
835            assert!(update_result.is_err());
836            assert_unauthorized_error(update_result.unwrap_err());
837
838            Ok(())
839        })
840    }
841
842    #[test]
843    fn test_admin_operations_allow_contract_origin_on_t2() -> eyre::Result<()> {
844        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
845        let contract_sender = Address::random();
846        let key_id = Address::random();
847        let token = Address::random();
848
849        StorageCtx::enter(&mut storage, || {
850            let mut keychain = AccountKeychain::new();
851            keychain.initialize()?;
852
853            keychain
854                .storage
855                .set_code(contract_sender, Bytecode::new_raw(vec![0x60, 0x00].into()))?;
856
857            // On T2, contract callers are allowed for admin operations only when
858            // `msg.sender == tx.origin`.
859            keychain.set_transaction_key(Address::ZERO)?;
860            keychain.set_tx_origin(contract_sender)?;
861
862            keychain.authorize_key(
863                contract_sender,
864                authorizeKeyCall {
865                    keyId: key_id,
866                    signatureType: SignatureType::Secp256k1,
867                    expiry: u64::MAX,
868                    enforceLimits: true,
869                    limits: vec![TokenLimit {
870                        token,
871                        amount: U256::from(100),
872                    }],
873                },
874            )?;
875
876            keychain.update_spending_limit(
877                contract_sender,
878                updateSpendingLimitCall {
879                    keyId: key_id,
880                    token,
881                    newLimit: U256::from(200),
882                },
883            )?;
884
885            assert_eq!(
886                keychain.get_remaining_limit(getRemainingLimitCall {
887                    account: contract_sender,
888                    keyId: key_id,
889                    token,
890                })?,
891                U256::from(200)
892            );
893
894            keychain.revoke_key(contract_sender, revokeKeyCall { keyId: key_id })?;
895
896            let key_info = keychain.get_key(getKeyCall {
897                account: contract_sender,
898                keyId: key_id,
899            })?;
900            assert!(key_info.isRevoked);
901
902            Ok(())
903        })
904    }
905
906    #[test]
907    fn test_admin_operations_allow_origin_mismatch_pre_t2() -> eyre::Result<()> {
908        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T0);
909        let msg_sender = Address::random();
910        let other_origin = Address::random();
911        let key_id = Address::random();
912        let token = Address::random();
913
914        StorageCtx::enter(&mut storage, || {
915            let mut keychain = AccountKeychain::new();
916            keychain.initialize()?;
917
918            // Pre-T2, admin operations do not enforce msg.sender == tx.origin.
919            keychain.set_transaction_key(Address::ZERO)?;
920            keychain.set_tx_origin(other_origin)?;
921
922            keychain.authorize_key(
923                msg_sender,
924                authorizeKeyCall {
925                    keyId: key_id,
926                    signatureType: SignatureType::Secp256k1,
927                    expiry: u64::MAX,
928                    enforceLimits: true,
929                    limits: vec![TokenLimit {
930                        token,
931                        amount: U256::from(100),
932                    }],
933                },
934            )?;
935
936            keychain.update_spending_limit(
937                msg_sender,
938                updateSpendingLimitCall {
939                    keyId: key_id,
940                    token,
941                    newLimit: U256::from(200),
942                },
943            )?;
944
945            keychain.revoke_key(msg_sender, revokeKeyCall { keyId: key_id })?;
946
947            let key_info = keychain.get_key(getKeyCall {
948                account: msg_sender,
949                keyId: key_id,
950            })?;
951            assert!(key_info.isRevoked);
952
953            Ok(())
954        })
955    }
956
957    #[test]
958    fn test_admin_operations_reject_eoa_mismatch_on_t2() -> eyre::Result<()> {
959        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
960        let account = Address::random();
961        let other_origin = Address::random();
962        let key_id = Address::random();
963        let token = Address::random();
964
965        StorageCtx::enter(&mut storage, || {
966            let mut keychain = AccountKeychain::new();
967            keychain.initialize()?;
968
969            // Setup under matching tx.origin first.
970            keychain.set_transaction_key(Address::ZERO)?;
971            keychain.set_tx_origin(account)?;
972            keychain.authorize_key(
973                account,
974                authorizeKeyCall {
975                    keyId: key_id,
976                    signatureType: SignatureType::Secp256k1,
977                    expiry: u64::MAX,
978                    enforceLimits: true,
979                    limits: vec![TokenLimit {
980                        token,
981                        amount: U256::from(100),
982                    }],
983                },
984            )?;
985
986            // On T2+, admin ops require `msg.sender == tx.origin`.
987            keychain.set_tx_origin(other_origin)?;
988            let result = keychain.update_spending_limit(
989                account,
990                updateSpendingLimitCall {
991                    keyId: key_id,
992                    token,
993                    newLimit: U256::from(200),
994                },
995            );
996            assert!(result.is_err());
997            assert_unauthorized_error(result.unwrap_err());
998
999            Ok(())
1000        })
1001    }
1002
1003    #[test]
1004    fn test_replay_protection_revoked_key_cannot_be_reauthorized() -> eyre::Result<()> {
1005        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
1006        let account = Address::random();
1007        let key_id = Address::random();
1008        let token = Address::random();
1009        StorageCtx::enter(&mut storage, || {
1010            let mut keychain = AccountKeychain::new();
1011            keychain.initialize()?;
1012
1013            // Use main key for all operations
1014            keychain.set_transaction_key(Address::ZERO)?;
1015            keychain.set_tx_origin(account)?;
1016
1017            // Step 1: Authorize a key with a spending limit
1018            let auth_call = authorizeKeyCall {
1019                keyId: key_id,
1020                signatureType: SignatureType::Secp256k1,
1021                expiry: u64::MAX,
1022                enforceLimits: true,
1023                limits: vec![TokenLimit {
1024                    token,
1025                    amount: U256::from(100),
1026                }],
1027            };
1028            keychain.authorize_key(account, auth_call.clone())?;
1029
1030            // Verify key exists and limit is set
1031            let key_info = keychain.get_key(getKeyCall {
1032                account,
1033                keyId: key_id,
1034            })?;
1035            assert_eq!(key_info.expiry, u64::MAX);
1036            assert!(!key_info.isRevoked);
1037            assert_eq!(
1038                keychain.get_remaining_limit(getRemainingLimitCall {
1039                    account,
1040                    keyId: key_id,
1041                    token,
1042                })?,
1043                U256::from(100)
1044            );
1045
1046            // Step 2: Revoke the key
1047            let revoke_call = revokeKeyCall { keyId: key_id };
1048            keychain.revoke_key(account, revoke_call)?;
1049
1050            // Verify key is revoked and remaining limit returns 0
1051            let key_info = keychain.get_key(getKeyCall {
1052                account,
1053                keyId: key_id,
1054            })?;
1055            assert_eq!(key_info.expiry, 0);
1056            assert!(key_info.isRevoked);
1057            assert_eq!(
1058                keychain.get_remaining_limit(getRemainingLimitCall {
1059                    account,
1060                    keyId: key_id,
1061                    token,
1062                })?,
1063                U256::ZERO
1064            );
1065
1066            // Step 3: Try to re-authorize the same key (replay attack)
1067            // This should fail because the key was revoked
1068            let replay_result = keychain.authorize_key(account, auth_call);
1069            assert!(
1070                replay_result.is_err(),
1071                "Re-authorizing a revoked key should fail"
1072            );
1073
1074            // Verify it's the correct error
1075            match replay_result.unwrap_err() {
1076                TempoPrecompileError::AccountKeychainError(e) => {
1077                    assert!(
1078                        matches!(e, AccountKeychainError::KeyAlreadyRevoked(_)),
1079                        "Expected KeyAlreadyRevoked error, got: {e:?}"
1080                    );
1081                }
1082                e => panic!("Expected AccountKeychainError, got: {e:?}"),
1083            }
1084            Ok(())
1085        })
1086    }
1087
1088    #[test]
1089    fn test_authorize_key_rejects_expiry_in_past() -> eyre::Result<()> {
1090        // Must use T0 hardfork for expiry validation to be enforced
1091        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T0);
1092        let account = Address::random();
1093        let key_id = Address::random();
1094        StorageCtx::enter(&mut storage, || {
1095            let mut keychain = AccountKeychain::new();
1096            keychain.initialize()?;
1097
1098            // Use main key for the operation
1099            keychain.set_transaction_key(Address::ZERO)?;
1100
1101            // Try to authorize with expiry = 0 (in the past)
1102            let auth_call = authorizeKeyCall {
1103                keyId: key_id,
1104                signatureType: SignatureType::Secp256k1,
1105                expiry: 0, // Zero expiry is in the past - should fail
1106                enforceLimits: false,
1107                limits: vec![],
1108            };
1109            let result = keychain.authorize_key(account, auth_call);
1110            assert!(
1111                result.is_err(),
1112                "Authorizing with expiry in past should fail"
1113            );
1114
1115            // Verify it's the correct error
1116            match result.unwrap_err() {
1117                TempoPrecompileError::AccountKeychainError(e) => {
1118                    assert!(
1119                        matches!(e, AccountKeychainError::ExpiryInPast(_)),
1120                        "Expected ExpiryInPast error, got: {e:?}"
1121                    );
1122                }
1123                e => panic!("Expected AccountKeychainError, got: {e:?}"),
1124            }
1125
1126            // Also test with a non-zero but past expiry
1127            let auth_call_past = authorizeKeyCall {
1128                keyId: key_id,
1129                signatureType: SignatureType::Secp256k1,
1130                expiry: 1, // Very old timestamp - should fail
1131                enforceLimits: false,
1132                limits: vec![],
1133            };
1134            let result_past = keychain.authorize_key(account, auth_call_past);
1135            assert!(
1136                matches!(
1137                    result_past,
1138                    Err(TempoPrecompileError::AccountKeychainError(
1139                        AccountKeychainError::ExpiryInPast(_)
1140                    ))
1141                ),
1142                "Expected ExpiryInPast error for past expiry, got: {result_past:?}"
1143            );
1144
1145            Ok(())
1146        })
1147    }
1148
1149    #[test]
1150    fn test_different_key_id_can_be_authorized_after_revocation() -> eyre::Result<()> {
1151        let mut storage = HashMapStorageProvider::new(1);
1152        let account = Address::random();
1153        let key_id_1 = Address::random();
1154        let key_id_2 = Address::random();
1155        StorageCtx::enter(&mut storage, || {
1156            let mut keychain = AccountKeychain::new();
1157            keychain.initialize()?;
1158
1159            // Use main key for all operations
1160            keychain.set_transaction_key(Address::ZERO)?;
1161
1162            // Authorize key 1
1163            let auth_call_1 = authorizeKeyCall {
1164                keyId: key_id_1,
1165                signatureType: SignatureType::Secp256k1,
1166                expiry: u64::MAX,
1167                enforceLimits: false,
1168                limits: vec![],
1169            };
1170            keychain.authorize_key(account, auth_call_1)?;
1171
1172            // Revoke key 1
1173            keychain.revoke_key(account, revokeKeyCall { keyId: key_id_1 })?;
1174
1175            // Authorizing a different key (key 2) should still work
1176            let auth_call_2 = authorizeKeyCall {
1177                keyId: key_id_2,
1178                signatureType: SignatureType::P256,
1179                expiry: u64::MAX,
1180                enforceLimits: true,
1181                limits: vec![],
1182            };
1183            keychain.authorize_key(account, auth_call_2)?;
1184
1185            // Verify key 2 is authorized
1186            let key_info = keychain.get_key(getKeyCall {
1187                account,
1188                keyId: key_id_2,
1189            })?;
1190            assert_eq!(key_info.expiry, u64::MAX);
1191            assert!(!key_info.isRevoked);
1192
1193            Ok(())
1194        })
1195    }
1196
1197    #[test]
1198    fn test_authorize_approve() -> eyre::Result<()> {
1199        let mut storage = HashMapStorageProvider::new(1);
1200
1201        let eoa = Address::random();
1202        let access_key = Address::random();
1203        let token = Address::random();
1204        let contract = Address::random();
1205
1206        StorageCtx::enter(&mut storage, || {
1207            let mut keychain = AccountKeychain::new();
1208            keychain.initialize()?;
1209
1210            // authorize access key with 100 token spending limit
1211            keychain.set_transaction_key(Address::ZERO)?;
1212            keychain.set_tx_origin(eoa)?;
1213
1214            let auth_call = authorizeKeyCall {
1215                keyId: access_key,
1216                signatureType: SignatureType::Secp256k1,
1217                expiry: u64::MAX,
1218                enforceLimits: true,
1219                limits: vec![TokenLimit {
1220                    token,
1221                    amount: U256::from(100),
1222                }],
1223            };
1224            keychain.authorize_key(eoa, auth_call)?;
1225
1226            let initial_limit = keychain.get_remaining_limit(getRemainingLimitCall {
1227                account: eoa,
1228                keyId: access_key,
1229                token,
1230            })?;
1231            assert_eq!(initial_limit, U256::from(100));
1232
1233            // Switch to access key for remaining tests
1234            keychain.set_transaction_key(access_key)?;
1235
1236            // Increase approval by 30, which deducts from the limit
1237            keychain.authorize_approve(eoa, token, U256::ZERO, U256::from(30))?;
1238
1239            let limit_after = keychain.get_remaining_limit(getRemainingLimitCall {
1240                account: eoa,
1241                keyId: access_key,
1242                token,
1243            })?;
1244            assert_eq!(limit_after, U256::from(70));
1245
1246            // Decrease approval to 20, does not affect limit
1247            keychain.authorize_approve(eoa, token, U256::from(30), U256::from(20))?;
1248
1249            let limit_unchanged = keychain.get_remaining_limit(getRemainingLimitCall {
1250                account: eoa,
1251                keyId: access_key,
1252                token,
1253            })?;
1254            assert_eq!(limit_unchanged, U256::from(70));
1255
1256            // Increase from 20 to 50, reducing the limit by 30
1257            keychain.authorize_approve(eoa, token, U256::from(20), U256::from(50))?;
1258
1259            let limit_after_increase = keychain.get_remaining_limit(getRemainingLimitCall {
1260                account: eoa,
1261                keyId: access_key,
1262                token,
1263            })?;
1264            assert_eq!(limit_after_increase, U256::from(40));
1265
1266            // Assert that spending limits only applied when account is tx origin
1267            keychain.authorize_approve(contract, token, U256::ZERO, U256::from(1000))?;
1268
1269            let limit_after_contract = keychain.get_remaining_limit(getRemainingLimitCall {
1270                account: eoa,
1271                keyId: access_key,
1272                token,
1273            })?;
1274            assert_eq!(limit_after_contract, U256::from(40)); // unchanged
1275
1276            // Assert that exceeding remaining limit fails
1277            let exceed_result = keychain.authorize_approve(eoa, token, U256::ZERO, U256::from(50));
1278            assert!(matches!(
1279                exceed_result,
1280                Err(TempoPrecompileError::AccountKeychainError(
1281                    AccountKeychainError::SpendingLimitExceeded(_)
1282                ))
1283            ));
1284
1285            // Assert that the main key bypasses spending limits, does not affect existing limits
1286            keychain.set_transaction_key(Address::ZERO)?;
1287            keychain.authorize_approve(eoa, token, U256::ZERO, U256::from(1000))?;
1288
1289            let limit_main_key = keychain.get_remaining_limit(getRemainingLimitCall {
1290                account: eoa,
1291                keyId: access_key,
1292                token,
1293            })?;
1294            assert_eq!(limit_main_key, U256::from(40));
1295
1296            Ok(())
1297        })
1298    }
1299
1300    /// Test that spending limits are only enforced when msg_sender == tx_origin.
1301    ///
1302    /// This test verifies the fix for the bug where spending limits were incorrectly
1303    /// applied to contract-initiated transfers. The scenario:
1304    ///
1305    /// 1. EOA Alice uses an access key with spending limits
1306    /// 2. Alice calls a contract that transfers tokens
1307    /// 3. The contract's transfer should NOT be subject to Alice's spending limits
1308    ///    (the contract is transferring its own tokens, not Alice's)
1309    #[test]
1310    fn test_spending_limits_only_apply_to_tx_origin() -> eyre::Result<()> {
1311        let mut storage = HashMapStorageProvider::new(1);
1312
1313        let eoa_alice = Address::random(); // The EOA that signs the transaction
1314        let access_key = Address::random(); // Alice's access key with spending limits
1315        let contract_address = Address::random(); // A contract that Alice calls
1316        let token = Address::random();
1317
1318        StorageCtx::enter(&mut storage, || {
1319            let mut keychain = AccountKeychain::new();
1320            keychain.initialize()?;
1321
1322            // Setup: Alice authorizes an access key with a spending limit of 100 tokens
1323            keychain.set_transaction_key(Address::ZERO)?; // Use main key for setup
1324            keychain.set_tx_origin(eoa_alice)?;
1325
1326            let auth_call = authorizeKeyCall {
1327                keyId: access_key,
1328                signatureType: SignatureType::Secp256k1,
1329                expiry: u64::MAX,
1330                enforceLimits: true,
1331                limits: vec![TokenLimit {
1332                    token,
1333                    amount: U256::from(100),
1334                }],
1335            };
1336            keychain.authorize_key(eoa_alice, auth_call)?;
1337
1338            // Verify spending limit is set
1339            let limit = keychain.get_remaining_limit(getRemainingLimitCall {
1340                account: eoa_alice,
1341                keyId: access_key,
1342                token,
1343            })?;
1344            assert_eq!(
1345                limit,
1346                U256::from(100),
1347                "Initial spending limit should be 100"
1348            );
1349
1350            // Now simulate a transaction where Alice uses her access key
1351            keychain.set_transaction_key(access_key)?;
1352            keychain.set_tx_origin(eoa_alice)?;
1353
1354            // Test 1: When msg_sender == tx_origin (Alice directly transfers)
1355            // Spending limit SHOULD be enforced
1356            keychain.authorize_transfer(eoa_alice, token, U256::from(30))?;
1357
1358            let limit_after = keychain.get_remaining_limit(getRemainingLimitCall {
1359                account: eoa_alice,
1360                keyId: access_key,
1361                token,
1362            })?;
1363            assert_eq!(
1364                limit_after,
1365                U256::from(70),
1366                "Spending limit should be reduced to 70 after Alice's direct transfer"
1367            );
1368
1369            // Test 2: When msg_sender != tx_origin (contract transfers its own tokens)
1370            // Spending limit should NOT be enforced - the contract isn't spending Alice's tokens
1371            keychain.authorize_transfer(contract_address, token, U256::from(1000))?;
1372
1373            let limit_unchanged = keychain.get_remaining_limit(getRemainingLimitCall {
1374                account: eoa_alice,
1375                keyId: access_key,
1376                token,
1377            })?;
1378            assert_eq!(
1379                limit_unchanged,
1380                U256::from(70),
1381                "Spending limit should remain 70 - contract transfer doesn't affect Alice's limit"
1382            );
1383
1384            // Test 3: Alice can still spend her remaining limit
1385            keychain.authorize_transfer(eoa_alice, token, U256::from(70))?;
1386
1387            let limit_depleted = keychain.get_remaining_limit(getRemainingLimitCall {
1388                account: eoa_alice,
1389                keyId: access_key,
1390                token,
1391            })?;
1392            assert_eq!(
1393                limit_depleted,
1394                U256::ZERO,
1395                "Spending limit should be depleted after Alice spends remaining 70"
1396            );
1397
1398            // Test 4: Alice cannot exceed her spending limit
1399            let exceed_result = keychain.authorize_transfer(eoa_alice, token, U256::from(1));
1400            assert!(
1401                exceed_result.is_err(),
1402                "Should fail when Alice tries to exceed spending limit"
1403            );
1404
1405            // Test 5: But contracts can still transfer (they're not subject to Alice's limits)
1406            let contract_result =
1407                keychain.authorize_transfer(contract_address, token, U256::from(999999));
1408            assert!(
1409                contract_result.is_ok(),
1410                "Contract should still be able to transfer even though Alice's limit is depleted"
1411            );
1412
1413            Ok(())
1414        })
1415    }
1416
1417    #[test]
1418    fn test_authorized_key_encode_decode_roundtrip() {
1419        let original = AuthorizedKey {
1420            signature_type: 2,  // WebAuthn
1421            expiry: 1234567890, // some timestamp
1422            enforce_limits: true,
1423            is_revoked: false,
1424        };
1425
1426        let encoded = original.encode_to_slot();
1427        let decoded = AuthorizedKey::decode_from_slot(encoded);
1428
1429        assert_eq!(
1430            decoded, original,
1431            "encode/decode roundtrip should be lossless"
1432        );
1433
1434        // Test with revoked key
1435        let revoked = AuthorizedKey {
1436            signature_type: 0,
1437            expiry: 0,
1438            enforce_limits: false,
1439            is_revoked: true,
1440        };
1441        let encoded = revoked.encode_to_slot();
1442        let decoded = AuthorizedKey::decode_from_slot(encoded);
1443        assert_eq!(decoded, revoked);
1444    }
1445
1446    #[test]
1447    fn test_authorize_key_rejects_existing_key_boundary() -> eyre::Result<()> {
1448        // Use pre-T0 to avoid expiry validation (focus on existence check)
1449        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::Genesis);
1450        let account = Address::random();
1451        let key_id = Address::random();
1452        StorageCtx::enter(&mut storage, || {
1453            let mut keychain = AccountKeychain::new();
1454            keychain.initialize()?;
1455            keychain.set_transaction_key(Address::ZERO)?;
1456
1457            // Authorize a key with expiry = 1 (minimal positive value)
1458            let auth_call = authorizeKeyCall {
1459                keyId: key_id,
1460                signatureType: SignatureType::Secp256k1,
1461                expiry: 1, // Minimal positive expiry
1462                enforceLimits: false,
1463                limits: vec![],
1464            };
1465            keychain.authorize_key(account, auth_call.clone())?;
1466
1467            // Verify key exists with expiry = 1
1468            let key_info = keychain.get_key(getKeyCall {
1469                account,
1470                keyId: key_id,
1471            })?;
1472            assert_eq!(key_info.expiry, 1, "Key should have expiry = 1");
1473
1474            // Try to re-authorize - should fail because expiry > 0
1475            let result = keychain.authorize_key(account, auth_call);
1476            assert!(result.is_err(), "Should reject when key.expiry > 0");
1477            match result.unwrap_err() {
1478                TempoPrecompileError::AccountKeychainError(e) => {
1479                    assert!(
1480                        matches!(e, AccountKeychainError::KeyAlreadyExists(_)),
1481                        "Expected KeyAlreadyExists, got: {e:?}"
1482                    );
1483                }
1484                e => panic!("Expected AccountKeychainError, got: {e:?}"),
1485            }
1486
1487            Ok(())
1488        })
1489    }
1490
1491    #[test]
1492    fn test_spending_limit_key_derivation() {
1493        let account1 = Address::repeat_byte(0x01);
1494        let account2 = Address::repeat_byte(0x02);
1495        let key_id1 = Address::repeat_byte(0xAA);
1496        let key_id2 = Address::repeat_byte(0xBB);
1497
1498        // Same inputs should produce same output
1499        let hash1a = AccountKeychain::spending_limit_key(account1, key_id1);
1500        let hash1b = AccountKeychain::spending_limit_key(account1, key_id1);
1501        assert_eq!(hash1a, hash1b, "Same inputs must produce same hash");
1502
1503        // Different accounts should produce different hashes
1504        let hash2 = AccountKeychain::spending_limit_key(account2, key_id1);
1505        assert_ne!(
1506            hash1a, hash2,
1507            "Different accounts must produce different hashes"
1508        );
1509
1510        // Different key_ids should produce different hashes
1511        let hash3 = AccountKeychain::spending_limit_key(account1, key_id2);
1512        assert_ne!(
1513            hash1a, hash3,
1514            "Different key_ids must produce different hashes"
1515        );
1516
1517        // Order matters: (account1, key_id2) != (key_id2, account1) if we swap
1518        // But since the types are the same, let's verify swapping produces different result
1519        let hash_swapped = AccountKeychain::spending_limit_key(key_id1, account1);
1520        assert_ne!(
1521            hash1a, hash_swapped,
1522            "Swapped order must produce different hash"
1523        );
1524
1525        // Verify hash is not default/zero
1526        assert_ne!(hash1a, B256::ZERO, "Hash should not be zero");
1527    }
1528
1529    #[test]
1530    fn test_initialize_sets_up_storage_state() -> eyre::Result<()> {
1531        let mut storage = HashMapStorageProvider::new(1);
1532        StorageCtx::enter(&mut storage, || {
1533            let mut keychain = AccountKeychain::new();
1534
1535            // Before initialize: operations should work after init
1536            keychain.initialize()?;
1537
1538            // Verify we can perform operations after initialize
1539            keychain.set_transaction_key(Address::ZERO)?;
1540
1541            let account = Address::random();
1542            let key_id = Address::random();
1543            let auth_call = authorizeKeyCall {
1544                keyId: key_id,
1545                signatureType: SignatureType::Secp256k1,
1546                expiry: u64::MAX,
1547                enforceLimits: false,
1548                limits: vec![],
1549            };
1550
1551            // This would fail if initialize didn't set up storage properly
1552            keychain.authorize_key(account, auth_call)?;
1553
1554            // Verify key was stored
1555            let key_info = keychain.get_key(getKeyCall {
1556                account,
1557                keyId: key_id,
1558            })?;
1559            assert_eq!(key_info.expiry, u64::MAX, "Key should be stored after init");
1560
1561            Ok(())
1562        })
1563    }
1564
1565    #[test]
1566    fn test_authorize_key_webauthn_signature_type() -> eyre::Result<()> {
1567        let mut storage = HashMapStorageProvider::new(1);
1568        let account = Address::random();
1569        let key_id = Address::random();
1570        StorageCtx::enter(&mut storage, || {
1571            let mut keychain = AccountKeychain::new();
1572            keychain.initialize()?;
1573            keychain.set_transaction_key(Address::ZERO)?;
1574
1575            // Authorize with WebAuthn signature type
1576            let auth_call = authorizeKeyCall {
1577                keyId: key_id,
1578                signatureType: SignatureType::WebAuthn,
1579                expiry: u64::MAX,
1580                enforceLimits: false,
1581                limits: vec![],
1582            };
1583            keychain.authorize_key(account, auth_call)?;
1584
1585            // Verify key was stored with WebAuthn type (value = 2)
1586            let key_info = keychain.get_key(getKeyCall {
1587                account,
1588                keyId: key_id,
1589            })?;
1590            assert_eq!(
1591                key_info.signatureType,
1592                SignatureType::WebAuthn,
1593                "Signature type should be WebAuthn"
1594            );
1595
1596            // Verify via validation that signature type 2 is accepted
1597            let result = keychain.validate_keychain_authorization(account, key_id, 0, Some(2));
1598            assert!(
1599                result.is_ok(),
1600                "WebAuthn (type 2) validation should succeed"
1601            );
1602
1603            // Verify signature type mismatch is rejected
1604            let mismatch = keychain.validate_keychain_authorization(account, key_id, 0, Some(0));
1605            assert!(mismatch.is_err(), "Secp256k1 should not match WebAuthn key");
1606
1607            Ok(())
1608        })
1609    }
1610
1611    #[test]
1612    fn test_update_spending_limit_expiry_boundary() -> eyre::Result<()> {
1613        let mut storage = HashMapStorageProvider::new(1);
1614        let account = Address::random();
1615        let key_id = Address::random();
1616        let token = Address::random();
1617        StorageCtx::enter(&mut storage, || {
1618            let mut keychain = AccountKeychain::new();
1619            keychain.initialize()?;
1620            keychain.set_transaction_key(Address::ZERO)?;
1621
1622            // Authorize a key with expiry far in the future
1623            let auth_call = authorizeKeyCall {
1624                keyId: key_id,
1625                signatureType: SignatureType::Secp256k1,
1626                expiry: u64::MAX,
1627                enforceLimits: true,
1628                limits: vec![TokenLimit {
1629                    token,
1630                    amount: U256::from(100),
1631                }],
1632            };
1633            keychain.authorize_key(account, auth_call)?;
1634
1635            // Update should work when key is not expired
1636            let update_call = updateSpendingLimitCall {
1637                keyId: key_id,
1638                token,
1639                newLimit: U256::from(200),
1640            };
1641            let result = keychain.update_spending_limit(account, update_call);
1642            assert!(
1643                result.is_ok(),
1644                "Update should succeed when key not expired: {result:?}"
1645            );
1646
1647            // Verify the limit was updated
1648            let limit = keychain.get_remaining_limit(getRemainingLimitCall {
1649                account,
1650                keyId: key_id,
1651                token,
1652            })?;
1653            assert_eq!(limit, U256::from(200), "Limit should be updated to 200");
1654
1655            Ok(())
1656        })
1657    }
1658
1659    #[test]
1660    fn test_update_spending_limit_enforce_limits_toggle() -> eyre::Result<()> {
1661        let mut storage = HashMapStorageProvider::new(1);
1662        let account = Address::random();
1663        let key_id = Address::random();
1664        let token = Address::random();
1665        StorageCtx::enter(&mut storage, || {
1666            let mut keychain = AccountKeychain::new();
1667            keychain.initialize()?;
1668            keychain.set_transaction_key(Address::ZERO)?;
1669
1670            // Case 1: Key with enforce_limits = false
1671            let auth_call = authorizeKeyCall {
1672                keyId: key_id,
1673                signatureType: SignatureType::Secp256k1,
1674                expiry: u64::MAX,
1675                enforceLimits: false, // Initially no limits
1676                limits: vec![],
1677            };
1678            keychain.authorize_key(account, auth_call)?;
1679
1680            // Verify key has enforce_limits = false
1681            let key_before = keychain.get_key(getKeyCall {
1682                account,
1683                keyId: key_id,
1684            })?;
1685            assert!(
1686                !key_before.enforceLimits,
1687                "Key should start with enforce_limits=false"
1688            );
1689
1690            // Update spending limit - this should toggle enforce_limits to true
1691            let update_call = updateSpendingLimitCall {
1692                keyId: key_id,
1693                token,
1694                newLimit: U256::from(500),
1695            };
1696            keychain.update_spending_limit(account, update_call)?;
1697
1698            // Verify enforce_limits is now true
1699            let key_after = keychain.get_key(getKeyCall {
1700                account,
1701                keyId: key_id,
1702            })?;
1703            assert!(
1704                key_after.enforceLimits,
1705                "enforce_limits should be true after update"
1706            );
1707
1708            // Verify the spending limit was set
1709            let limit = keychain.get_remaining_limit(getRemainingLimitCall {
1710                account,
1711                keyId: key_id,
1712                token,
1713            })?;
1714            assert_eq!(limit, U256::from(500), "Spending limit should be 500");
1715
1716            Ok(())
1717        })
1718    }
1719
1720    #[test]
1721    fn test_get_key_or_logic_existence_check() -> eyre::Result<()> {
1722        let mut storage = HashMapStorageProvider::new(1);
1723        let account = Address::random();
1724        let key_id_revoked = Address::random();
1725        let key_id_valid = Address::random();
1726        let key_id_never_existed = Address::random();
1727        StorageCtx::enter(&mut storage, || {
1728            let mut keychain = AccountKeychain::new();
1729            keychain.initialize()?;
1730            keychain.set_transaction_key(Address::ZERO)?;
1731
1732            // Setup: Create and revoke a key
1733            let auth_call = authorizeKeyCall {
1734                keyId: key_id_revoked,
1735                signatureType: SignatureType::P256,
1736                expiry: u64::MAX,
1737                enforceLimits: false,
1738                limits: vec![],
1739            };
1740            keychain.authorize_key(account, auth_call)?;
1741            keychain.revoke_key(
1742                account,
1743                revokeKeyCall {
1744                    keyId: key_id_revoked,
1745                },
1746            )?;
1747
1748            // Setup: Create a valid key
1749            let auth_valid = authorizeKeyCall {
1750                keyId: key_id_valid,
1751                signatureType: SignatureType::Secp256k1,
1752                expiry: u64::MAX,
1753                enforceLimits: false,
1754                limits: vec![],
1755            };
1756            keychain.authorize_key(account, auth_valid)?;
1757
1758            // Test 1: Revoked key (expiry=0, is_revoked=true) - should return empty with isRevoked=true
1759            let revoked_info = keychain.get_key(getKeyCall {
1760                account,
1761                keyId: key_id_revoked,
1762            })?;
1763            assert_eq!(
1764                revoked_info.keyId,
1765                Address::ZERO,
1766                "Revoked key should return zero keyId"
1767            );
1768            assert!(
1769                revoked_info.isRevoked,
1770                "Revoked key should have isRevoked=true"
1771            );
1772
1773            // Test 2: Never existed key (expiry=0, is_revoked=false) - should return empty
1774            let never_info = keychain.get_key(getKeyCall {
1775                account,
1776                keyId: key_id_never_existed,
1777            })?;
1778            assert_eq!(
1779                never_info.keyId,
1780                Address::ZERO,
1781                "Non-existent key should return zero keyId"
1782            );
1783            assert_eq!(
1784                never_info.expiry, 0,
1785                "Non-existent key should have expiry=0"
1786            );
1787
1788            // Test 3: Valid key (expiry>0, is_revoked=false) - should return actual key info
1789            let valid_info = keychain.get_key(getKeyCall {
1790                account,
1791                keyId: key_id_valid,
1792            })?;
1793            assert_eq!(
1794                valid_info.keyId, key_id_valid,
1795                "Valid key should return actual keyId"
1796            );
1797            assert_eq!(
1798                valid_info.expiry,
1799                u64::MAX,
1800                "Valid key should have correct expiry"
1801            );
1802            assert!(!valid_info.isRevoked, "Valid key should not be revoked");
1803
1804            Ok(())
1805        })
1806    }
1807
1808    #[test]
1809    fn test_get_key_signature_type_match_arms() -> eyre::Result<()> {
1810        let mut storage = HashMapStorageProvider::new(1);
1811        let account = Address::random();
1812        let key_secp = Address::random();
1813        let key_p256 = Address::random();
1814        let key_webauthn = Address::random();
1815        StorageCtx::enter(&mut storage, || {
1816            let mut keychain = AccountKeychain::new();
1817            keychain.initialize()?;
1818            keychain.set_transaction_key(Address::ZERO)?;
1819
1820            // Create keys with each signature type
1821            keychain.authorize_key(
1822                account,
1823                authorizeKeyCall {
1824                    keyId: key_secp,
1825                    signatureType: SignatureType::Secp256k1, // type 0
1826                    expiry: u64::MAX,
1827                    enforceLimits: false,
1828                    limits: vec![],
1829                },
1830            )?;
1831
1832            keychain.authorize_key(
1833                account,
1834                authorizeKeyCall {
1835                    keyId: key_p256,
1836                    signatureType: SignatureType::P256, // type 1
1837                    expiry: u64::MAX,
1838                    enforceLimits: false,
1839                    limits: vec![],
1840                },
1841            )?;
1842
1843            keychain.authorize_key(
1844                account,
1845                authorizeKeyCall {
1846                    keyId: key_webauthn,
1847                    signatureType: SignatureType::WebAuthn, // type 2
1848                    expiry: u64::MAX,
1849                    enforceLimits: false,
1850                    limits: vec![],
1851                },
1852            )?;
1853
1854            // Verify each key returns the correct signature type
1855            let secp_info = keychain.get_key(getKeyCall {
1856                account,
1857                keyId: key_secp,
1858            })?;
1859            assert_eq!(
1860                secp_info.signatureType,
1861                SignatureType::Secp256k1,
1862                "Secp256k1 key should return Secp256k1"
1863            );
1864
1865            let p256_info = keychain.get_key(getKeyCall {
1866                account,
1867                keyId: key_p256,
1868            })?;
1869            assert_eq!(
1870                p256_info.signatureType,
1871                SignatureType::P256,
1872                "P256 key should return P256"
1873            );
1874
1875            let webauthn_info = keychain.get_key(getKeyCall {
1876                account,
1877                keyId: key_webauthn,
1878            })?;
1879            assert_eq!(
1880                webauthn_info.signatureType,
1881                SignatureType::WebAuthn,
1882                "WebAuthn key should return WebAuthn"
1883            );
1884
1885            // Verify they are all distinct
1886            assert_ne!(secp_info.signatureType, p256_info.signatureType);
1887            assert_ne!(secp_info.signatureType, webauthn_info.signatureType);
1888            assert_ne!(p256_info.signatureType, webauthn_info.signatureType);
1889
1890            Ok(())
1891        })
1892    }
1893
1894    #[test]
1895    fn test_validate_keychain_authorization_checks_signature_type() -> eyre::Result<()> {
1896        let mut storage = HashMapStorageProvider::new(1);
1897        let account = Address::random();
1898        let key_id = Address::random();
1899        StorageCtx::enter(&mut storage, || {
1900            let mut keychain = AccountKeychain::new();
1901            keychain.initialize()?;
1902
1903            // Use main key for authorization
1904            keychain.set_transaction_key(Address::ZERO)?;
1905
1906            // Authorize a P256 key
1907            let auth_call = authorizeKeyCall {
1908                keyId: key_id,
1909                signatureType: SignatureType::P256,
1910                expiry: u64::MAX,
1911                enforceLimits: false,
1912                limits: vec![],
1913            };
1914            keychain.authorize_key(account, auth_call)?;
1915
1916            // Test 1: Validation should succeed with matching signature type (P256 = 1)
1917            let result = keychain.validate_keychain_authorization(account, key_id, 0, Some(1));
1918            assert!(
1919                result.is_ok(),
1920                "Validation should succeed with matching signature type"
1921            );
1922
1923            // Test 2: Validation should fail with mismatched signature type (Secp256k1 = 0)
1924            let mismatch_result =
1925                keychain.validate_keychain_authorization(account, key_id, 0, Some(0));
1926            assert!(
1927                mismatch_result.is_err(),
1928                "Validation should fail with mismatched signature type"
1929            );
1930            match mismatch_result.unwrap_err() {
1931                TempoPrecompileError::AccountKeychainError(e) => {
1932                    assert!(
1933                        matches!(e, AccountKeychainError::SignatureTypeMismatch(_)),
1934                        "Expected SignatureTypeMismatch error, got: {e:?}"
1935                    );
1936                }
1937                e => panic!("Expected AccountKeychainError, got: {e:?}"),
1938            }
1939
1940            // Test 3: Validation should fail with WebAuthn (2) when key is P256 (1)
1941            let webauthn_mismatch =
1942                keychain.validate_keychain_authorization(account, key_id, 0, Some(2));
1943            assert!(
1944                webauthn_mismatch.is_err(),
1945                "Validation should fail with WebAuthn when key is P256"
1946            );
1947
1948            // Test 4: Validation should succeed with None (backward compatibility, pre-T1)
1949            let none_result = keychain.validate_keychain_authorization(account, key_id, 0, None);
1950            assert!(
1951                none_result.is_ok(),
1952                "Validation should succeed when signature type check is skipped (pre-T1)"
1953            );
1954
1955            Ok(())
1956        })
1957    }
1958
1959    #[test]
1960    fn test_refund_spending_limit_restores_limit() -> eyre::Result<()> {
1961        let mut storage = HashMapStorageProvider::new(1);
1962        let eoa = Address::random();
1963        let access_key = Address::random();
1964        let token = Address::random();
1965
1966        StorageCtx::enter(&mut storage, || {
1967            let mut keychain = AccountKeychain::new();
1968            keychain.initialize()?;
1969
1970            keychain.set_transaction_key(Address::ZERO)?;
1971
1972            let auth_call = authorizeKeyCall {
1973                keyId: access_key,
1974                signatureType: SignatureType::Secp256k1,
1975                expiry: u64::MAX,
1976                enforceLimits: true,
1977                limits: vec![TokenLimit {
1978                    token,
1979                    amount: U256::from(100),
1980                }],
1981            };
1982            keychain.authorize_key(eoa, auth_call)?;
1983
1984            keychain.set_transaction_key(access_key)?;
1985            keychain.set_tx_origin(eoa)?;
1986
1987            keychain.authorize_transfer(eoa, token, U256::from(60))?;
1988
1989            let remaining = keychain.get_remaining_limit(getRemainingLimitCall {
1990                account: eoa,
1991                keyId: access_key,
1992                token,
1993            })?;
1994            assert_eq!(remaining, U256::from(40));
1995
1996            keychain.refund_spending_limit(eoa, token, U256::from(25))?;
1997
1998            let after_refund = keychain.get_remaining_limit(getRemainingLimitCall {
1999                account: eoa,
2000                keyId: access_key,
2001                token,
2002            })?;
2003            assert_eq!(after_refund, U256::from(65));
2004
2005            Ok(())
2006        })
2007    }
2008
2009    #[test]
2010    fn test_refund_spending_limit_noop_for_main_key() -> eyre::Result<()> {
2011        let mut storage = HashMapStorageProvider::new(1);
2012        let eoa = Address::random();
2013        let token = Address::random();
2014
2015        StorageCtx::enter(&mut storage, || {
2016            let mut keychain = AccountKeychain::new();
2017            keychain.initialize()?;
2018
2019            keychain.set_transaction_key(Address::ZERO)?;
2020            keychain.set_tx_origin(eoa)?;
2021
2022            let result = keychain.refund_spending_limit(eoa, token, U256::from(50));
2023            assert!(result.is_ok());
2024
2025            Ok(())
2026        })
2027    }
2028
2029    #[test]
2030    fn test_refund_spending_limit_noop_after_key_revocation() -> eyre::Result<()> {
2031        let mut storage = HashMapStorageProvider::new(1);
2032        let eoa = Address::random();
2033        let access_key = Address::random();
2034        let token = Address::random();
2035
2036        StorageCtx::enter(&mut storage, || {
2037            let mut keychain = AccountKeychain::new();
2038            keychain.initialize()?;
2039
2040            keychain.set_transaction_key(Address::ZERO)?;
2041
2042            let auth_call = authorizeKeyCall {
2043                keyId: access_key,
2044                signatureType: SignatureType::Secp256k1,
2045                expiry: u64::MAX,
2046                enforceLimits: true,
2047                limits: vec![TokenLimit {
2048                    token,
2049                    amount: U256::from(100),
2050                }],
2051            };
2052            keychain.authorize_key(eoa, auth_call)?;
2053
2054            keychain.set_transaction_key(access_key)?;
2055            keychain.set_tx_origin(eoa)?;
2056
2057            keychain.authorize_transfer(eoa, token, U256::from(60))?;
2058
2059            let remaining = keychain.get_remaining_limit(getRemainingLimitCall {
2060                account: eoa,
2061                keyId: access_key,
2062                token,
2063            })?;
2064            assert_eq!(remaining, U256::from(40));
2065
2066            keychain.set_transaction_key(Address::ZERO)?;
2067            keychain.revoke_key(eoa, revokeKeyCall { keyId: access_key })?;
2068
2069            keychain.set_transaction_key(access_key)?;
2070
2071            let result = keychain.refund_spending_limit(eoa, token, U256::from(25));
2072            assert!(result.is_ok());
2073
2074            let after_refund = keychain.get_remaining_limit(getRemainingLimitCall {
2075                account: eoa,
2076                keyId: access_key,
2077                token,
2078            })?;
2079            assert_eq!(
2080                after_refund,
2081                U256::from(40),
2082                "limit should be unchanged after revoked key refund"
2083            );
2084
2085            Ok(())
2086        })
2087    }
2088
2089    #[test]
2090    fn test_refund_spending_limit_clamped_by_saturating_add() -> eyre::Result<()> {
2091        let mut storage = HashMapStorageProvider::new(1);
2092        let eoa = Address::random();
2093        let access_key = Address::random();
2094        let token = Address::random();
2095        let original_limit = U256::from(100);
2096
2097        StorageCtx::enter(&mut storage, || {
2098            let mut keychain = AccountKeychain::new();
2099            keychain.initialize()?;
2100
2101            keychain.set_transaction_key(Address::ZERO)?;
2102
2103            let auth_call = authorizeKeyCall {
2104                keyId: access_key,
2105                signatureType: SignatureType::Secp256k1,
2106                expiry: u64::MAX,
2107                enforceLimits: true,
2108                limits: vec![TokenLimit {
2109                    token,
2110                    amount: original_limit,
2111                }],
2112            };
2113            keychain.authorize_key(eoa, auth_call)?;
2114
2115            keychain.set_transaction_key(access_key)?;
2116            keychain.set_tx_origin(eoa)?;
2117
2118            keychain.authorize_transfer(eoa, token, U256::from(10))?;
2119
2120            let remaining = keychain.get_remaining_limit(getRemainingLimitCall {
2121                account: eoa,
2122                keyId: access_key,
2123                token,
2124            })?;
2125            assert_eq!(remaining, U256::from(90));
2126
2127            keychain.refund_spending_limit(eoa, token, U256::from(50))?;
2128
2129            let after_refund = keychain.get_remaining_limit(getRemainingLimitCall {
2130                account: eoa,
2131                keyId: access_key,
2132                token,
2133            })?;
2134            assert_eq!(
2135                after_refund,
2136                U256::from(140),
2137                "saturating_add should allow refund beyond original limit without overflow"
2138            );
2139
2140            Ok(())
2141        })
2142    }
2143}