Skip to main content

tempo_revm/
evm.rs

1use crate::{TempoBlockEnv, TempoTxEnv, instructions};
2use alloy_evm::{Database, precompiles::PrecompilesMap};
3use alloy_primitives::{Address, U256};
4use revm::{
5    Context, Inspector,
6    context::{Cfg, CfgEnv, ContextError, Evm, FrameStack},
7    handler::{
8        EthFrame, EvmTr, FrameInitOrResult, FrameTr, ItemOrResult, instructions::EthInstructions,
9    },
10    inspector::InspectorEvmTr,
11    interpreter::{InitialAndFloorGas, interpreter::EthInterpreter},
12};
13use tempo_chainspec::hardfork::TempoHardfork;
14use tempo_precompiles::storage::StorageActions;
15
16/// The Tempo EVM context type.
17pub type TempoContext<DB> = Context<TempoBlockEnv, TempoTxEnv, CfgEnv<TempoHardfork>, DB>;
18
19/// TempoEvm extends the Evm with Tempo specific types and logic.
20#[derive(Debug, derive_more::Deref, derive_more::DerefMut)]
21#[expect(clippy::type_complexity)]
22pub struct TempoEvm<DB: Database, I> {
23    /// Inner EVM type.
24    #[deref]
25    #[deref_mut]
26    pub inner: Evm<
27        TempoContext<DB>,
28        I,
29        EthInstructions<EthInterpreter, TempoContext<DB>>,
30        PrecompilesMap,
31        EthFrame<EthInterpreter>,
32    >,
33    /// The fee collected in `collectFeePreTx` call.
34    pub(crate) collected_fee: U256,
35    /// The validator-credited amount (post-feeAMM haircut, in the validator's fee token) returned
36    /// by the most recent `collectFeePostTx` call.
37    ///
38    /// Reset to zero before each transaction so it reflects only the current tx.
39    pub validator_fee: U256,
40    /// The fee token used to pay fees for the current transaction.
41    pub(crate) fee_token: Option<Address>,
42    /// The expiry timestamp of the access key used by the current transaction.
43    /// Populated during validation for keychain-signed transactions or transactions carrying a KeyAuthorization.
44    pub(crate) key_expiry: Option<u64>,
45    /// When true, skips the `valid_after` time-window check during validation.
46    ///
47    /// The transaction pool sets this because it intentionally accepts transactions
48    /// with a future `valid_after` (queued until executable).
49    pub skip_valid_after_check: bool,
50    /// When true, skips the AMM liquidity check in `collect_fee_pre_tx`.
51    ///
52    /// The transaction pool sets this because it performs its own liquidity
53    /// validation against a cached view of the AMM state.
54    pub skip_liquidity_check: bool,
55    /// Recorded storage actions.
56    pub(crate) actions: StorageActions,
57}
58
59impl<DB: Database, I> TempoEvm<DB, I> {
60    /// Create a new Tempo EVM.
61    pub fn new(ctx: TempoContext<DB>, inspector: I) -> Self {
62        Self::new_with_actions(ctx, inspector, StorageActions::disabled())
63    }
64
65    /// Create a new Tempo EVM with a buffer for recording storage actions.
66    pub fn new_with_actions(ctx: TempoContext<DB>, inspector: I, actions: StorageActions) -> Self {
67        let precompiles =
68            tempo_precompiles::tempo_precompiles_with_actions(&ctx.cfg, actions.clone());
69
70        Self::new_inner(
71            Evm {
72                instruction: instructions::tempo_instructions(ctx.cfg.spec),
73                ctx,
74                inspector,
75                precompiles,
76                frame_stack: FrameStack::new(),
77            },
78            actions,
79        )
80    }
81
82    /// Inner helper function to create a new Tempo EVM with empty logs.
83    #[inline]
84    #[expect(clippy::type_complexity)]
85    fn new_inner(
86        inner: Evm<
87            TempoContext<DB>,
88            I,
89            EthInstructions<EthInterpreter, TempoContext<DB>>,
90            PrecompilesMap,
91            EthFrame<EthInterpreter>,
92        >,
93        actions: StorageActions,
94    ) -> Self {
95        Self {
96            inner,
97            collected_fee: U256::ZERO,
98            validator_fee: U256::ZERO,
99            fee_token: None,
100            key_expiry: None,
101            skip_valid_after_check: false,
102            skip_liquidity_check: false,
103            actions,
104        }
105    }
106
107    /// Computes initial gas limit and reservoir for a transaction given its initial gas spending.
108    pub(crate) fn initial_gas_and_reservoir(
109        &self,
110        init_and_floor_gas: &InitialAndFloorGas,
111    ) -> (u64, u64) {
112        // Pre-T0 it could happen that the initial gas spending is greater than the gas limit due to faulty validation.
113        //
114        // Before that it would overflow, so we are reproducing this behavior here by setting the gas limit to u64::MAX and the reservoir to 0.
115        if !self.cfg.spec.is_t0() && init_and_floor_gas.initial_total_gas() > self.tx.gas_limit {
116            (u64::MAX, 0)
117        } else {
118            init_and_floor_gas
119                .initial_gas_and_reservoir(self.tx.gas_limit, self.cfg.tx_gas_limit_cap())
120        }
121    }
122}
123
124impl<DB: Database, I> TempoEvm<DB, I> {
125    /// Consumed self and returns a new Evm type with given Inspector.
126    pub fn with_inspector<OINSP>(self, inspector: OINSP) -> TempoEvm<DB, OINSP> {
127        let Self { inner, actions, .. } = self;
128        TempoEvm::new_inner(inner.with_inspector(inspector), actions)
129    }
130
131    /// Consumes self and returns a new Evm type with given Precompiles.
132    pub fn with_precompiles(self, precompiles: PrecompilesMap) -> Self {
133        let Self { inner, actions, .. } = self;
134        Self::new_inner(inner.with_precompiles(precompiles), actions)
135    }
136
137    /// Consumes self and returns the inner Inspector.
138    pub fn into_inspector(self) -> I {
139        self.inner.into_inspector()
140    }
141
142    /// Clears all intermediate state from the EVM.
143    pub fn clear(&mut self) {
144        self.collected_fee = U256::ZERO;
145        self.fee_token = None;
146        self.key_expiry = None;
147    }
148}
149
150impl<DB, I> EvmTr for TempoEvm<DB, I>
151where
152    DB: Database,
153{
154    type Context = TempoContext<DB>;
155    type Instructions = EthInstructions<EthInterpreter, TempoContext<DB>>;
156    type Precompiles = PrecompilesMap;
157    type Frame = EthFrame<EthInterpreter>;
158
159    fn all(
160        &self,
161    ) -> (
162        &Self::Context,
163        &Self::Instructions,
164        &Self::Precompiles,
165        &FrameStack<Self::Frame>,
166    ) {
167        self.inner.all()
168    }
169
170    fn all_mut(
171        &mut self,
172    ) -> (
173        &mut Self::Context,
174        &mut Self::Instructions,
175        &mut Self::Precompiles,
176        &mut FrameStack<Self::Frame>,
177    ) {
178        self.inner.all_mut()
179    }
180
181    fn frame_stack(&mut self) -> &mut FrameStack<Self::Frame> {
182        &mut self.inner.frame_stack
183    }
184
185    fn frame_init(
186        &mut self,
187        frame_input: <Self::Frame as FrameTr>::FrameInit,
188    ) -> Result<
189        ItemOrResult<&mut Self::Frame, <Self::Frame as FrameTr>::FrameResult>,
190        ContextError<DB::Error>,
191    > {
192        self.inner.frame_init(frame_input)
193    }
194
195    fn frame_run(&mut self) -> Result<FrameInitOrResult<Self::Frame>, ContextError<DB::Error>> {
196        self.inner.frame_run()
197    }
198
199    fn frame_return_result(
200        &mut self,
201        result: <Self::Frame as FrameTr>::FrameResult,
202    ) -> Result<Option<<Self::Frame as FrameTr>::FrameResult>, ContextError<DB::Error>> {
203        self.inner.frame_return_result(result)
204    }
205}
206
207impl<DB, I> InspectorEvmTr for TempoEvm<DB, I>
208where
209    DB: Database,
210    I: Inspector<TempoContext<DB>>,
211{
212    type Inspector = I;
213
214    fn all_inspector(
215        &self,
216    ) -> (
217        &Self::Context,
218        &Self::Instructions,
219        &Self::Precompiles,
220        &FrameStack<Self::Frame>,
221        &Self::Inspector,
222    ) {
223        self.inner.all_inspector()
224    }
225
226    fn all_mut_inspector(
227        &mut self,
228    ) -> (
229        &mut Self::Context,
230        &mut Self::Instructions,
231        &mut Self::Precompiles,
232        &mut FrameStack<Self::Frame>,
233        &mut Self::Inspector,
234    ) {
235        self.inner.all_mut_inspector()
236    }
237}
238
239#[cfg(test)]
240mod tests {
241    use crate::gas_params::{tempo_gas_params, tempo_gas_params_with_amsterdam};
242    use alloy_eips::eip7702::Authorization;
243    use alloy_evm::FromRecoveredTx;
244    use alloy_primitives::{Address, Bytes, TxKind, U256, bytes, hex};
245    use alloy_sol_types::{SolCall, SolError};
246    use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
247    use p256::{
248        ecdsa::{SigningKey, signature::hazmat::PrehashSigner},
249        elliptic_curve::rand_core::OsRng,
250    };
251    use reth_evm::EvmInternals;
252    use revm::{
253        Context, DatabaseRef, ExecuteCommitEvm, ExecuteEvm, InspectEvm, MainContext,
254        bytecode::opcode,
255        context::{
256            CfgEnv, ContextTr, TxEnv,
257            result::{ExecutionResult, HaltReason},
258        },
259        database::{CacheDB, EmptyDB},
260        handler::system_call::SystemCallEvm,
261        inspector::{CountInspector, InspectSystemCallEvm},
262        state::{AccountInfo, Bytecode},
263    };
264    use sha2::{Digest, Sha256};
265    use tempo_chainspec::{constants::gas::STORAGE_CREDIT_VALUE, hardfork::TempoHardfork};
266    use tempo_contracts::precompiles::IStorageCredits::{self, Mode};
267    use tempo_precompiles::{
268        AuthorizedKey, DelegateCallNotAllowed, NONCE_PRECOMPILE_ADDRESS, PATH_USD_ADDRESS,
269        STORAGE_CREDITS_ADDRESS,
270        nonce::NonceManager,
271        storage::{FromWord, Handler, StorageCtx, evm::EvmPrecompileStorageProvider},
272        storage_credits::{CreditMode, StorageCredits},
273        test_util::TIP20Setup,
274        tip20::{ITIP20, TIP20Token},
275    };
276    use tempo_primitives::{
277        TempoTransaction,
278        transaction::{
279            KeyAuthorization, KeychainSignature, SignatureType, TempoSignedAuthorization,
280            tempo_transaction::Call,
281            tt_signature::{
282                PrimitiveSignature, TempoSignature, WebAuthnSignature, derive_p256_address,
283                normalize_p256_s,
284            },
285        },
286    };
287
288    use crate::{TempoBlockEnv, TempoEvm, TempoHaltReason, TempoInvalidTransaction, TempoTxEnv};
289    use revm::context::result::InvalidTransaction;
290
291    // ==================== Test Constants ====================
292
293    /// Default balance for funded accounts (1 ETH)
294    const DEFAULT_BALANCE: u128 = 1_000_000_000_000_000_000;
295
296    /// Identity precompile address (0x04)
297    const IDENTITY_PRECOMPILE: Address = Address::new([
298        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x04,
299    ]);
300
301    // ==================== Test Utility Functions ====================
302
303    /// Create an empty EVM instance with default settings and no inspector.
304    fn create_evm() -> TempoEvm<CacheDB<EmptyDB>, ()> {
305        let db = CacheDB::new(EmptyDB::new());
306        let ctx = Context::mainnet()
307            .with_db(db)
308            .with_block(Default::default())
309            .with_cfg(Default::default())
310            .with_tx(Default::default());
311        TempoEvm::new(ctx, ())
312    }
313
314    /// Create an EVM instance with a specific block timestamp.
315    fn create_evm_with_timestamp(timestamp: u64) -> TempoEvm<CacheDB<EmptyDB>, ()> {
316        let db = CacheDB::new(EmptyDB::new());
317        let mut block = TempoBlockEnv::default();
318        block.inner.timestamp = U256::from(timestamp);
319
320        let ctx = Context::mainnet()
321            .with_db(db)
322            .with_block(block)
323            .with_cfg(Default::default())
324            .with_tx(Default::default());
325
326        TempoEvm::new(ctx, ())
327    }
328
329    /// Fund an account with the default balance (1 ETH).
330    fn fund_account(evm: &mut TempoEvm<CacheDB<EmptyDB>, ()>, address: Address) {
331        fund_account_with_nonce(evm, address, 0);
332    }
333
334    /// Fund an account with the default balance and a specific nonce.
335    fn fund_account_with_nonce(
336        evm: &mut TempoEvm<CacheDB<EmptyDB>, ()>,
337        address: Address,
338        nonce: u64,
339    ) {
340        evm.ctx.db_mut().insert_account_info(
341            address,
342            AccountInfo {
343                balance: U256::from(DEFAULT_BALANCE),
344                nonce,
345                ..Default::default()
346            },
347        );
348    }
349
350    /// Create an EVM with a funded account at the given address.
351    fn create_funded_evm(address: Address) -> TempoEvm<CacheDB<EmptyDB>, ()> {
352        let mut evm = create_evm();
353        fund_account(&mut evm, address);
354        evm
355    }
356
357    /// Create an EVM with T1C hardfork enabled and a funded account.
358    /// This applies TIP-1000 gas params via `tempo_gas_params()`.
359    fn create_funded_evm_t1(address: Address) -> TempoEvm<CacheDB<EmptyDB>, ()> {
360        let db = CacheDB::new(EmptyDB::new());
361        let mut cfg = CfgEnv::<TempoHardfork>::default();
362        cfg.spec = TempoHardfork::T1C;
363        // Apply TIP-1000 gas params for T1C hardfork
364        cfg.gas_params = tempo_gas_params(TempoHardfork::T1C);
365
366        let ctx = Context::mainnet()
367            .with_db(db)
368            .with_block(Default::default())
369            .with_cfg(cfg)
370            .with_tx(Default::default());
371
372        let mut evm = TempoEvm::new(ctx, ());
373        fund_account(&mut evm, address);
374        evm
375    }
376
377    /// Create an EVM with T3 hardfork enabled and a funded account.
378    fn create_funded_evm_t3(address: Address) -> TempoEvm<CacheDB<EmptyDB>, ()> {
379        let db = CacheDB::new(EmptyDB::new());
380        let mut cfg = CfgEnv::<TempoHardfork>::default();
381        cfg.spec = TempoHardfork::T3;
382        cfg.gas_params = tempo_gas_params(TempoHardfork::T3);
383
384        let ctx = Context::mainnet()
385            .with_db(db)
386            .with_block(Default::default())
387            .with_cfg(cfg)
388            .with_tx(Default::default());
389
390        let mut evm = TempoEvm::new(ctx, ());
391        fund_account(&mut evm, address);
392        evm
393    }
394
395    /// Create an EVM with T4 hardfork enabled and a funded account.
396    fn create_funded_evm_t4(address: Address) -> TempoEvm<CacheDB<EmptyDB>, ()> {
397        let db = CacheDB::new(EmptyDB::new());
398        let mut cfg = CfgEnv::<TempoHardfork>::default();
399        cfg.spec = TempoHardfork::T4;
400        cfg.gas_params = tempo_gas_params(TempoHardfork::T4);
401        cfg.enable_amsterdam_eip8037 = true;
402
403        let ctx = Context::mainnet()
404            .with_db(db)
405            .with_block(Default::default())
406            .with_cfg(cfg)
407            .with_tx(Default::default());
408
409        let mut evm = TempoEvm::new(ctx, ());
410        fund_account(&mut evm, address);
411        evm
412    }
413
414    /// Creates a T7-enabled EVM with a funded account.
415    /// This activates the TIP-1060 SSTORE storage credits hook while keeping the
416    /// TIP-1016 state-gas split disabled to match production.
417    fn create_funded_evm_t7(address: Address) -> TempoEvm<CacheDB<EmptyDB>, ()> {
418        let db = CacheDB::new(EmptyDB::new());
419        let mut cfg = CfgEnv::<TempoHardfork>::default();
420        cfg.spec = TempoHardfork::T7;
421        cfg.gas_params = tempo_gas_params_with_amsterdam(TempoHardfork::T7, false);
422        cfg.enable_amsterdam_eip8037 = false;
423
424        let ctx = Context::mainnet()
425            .with_db(db)
426            .with_block(Default::default())
427            .with_cfg(cfg)
428            .with_tx(Default::default());
429
430        let mut evm = TempoEvm::new(ctx, ());
431        fund_account(&mut evm, address);
432        evm
433    }
434
435    /// Create an EVM with T7 hardfork, a specific timestamp, and a funded account.
436    fn create_funded_evm_t7_with_timestamp(
437        address: Address,
438        timestamp: u64,
439    ) -> TempoEvm<CacheDB<EmptyDB>, ()> {
440        let db = CacheDB::new(EmptyDB::new());
441        let mut cfg = CfgEnv::<TempoHardfork>::default();
442        cfg.spec = TempoHardfork::T7;
443        cfg.gas_params = tempo_gas_params_with_amsterdam(TempoHardfork::T7, false);
444        cfg.enable_amsterdam_eip8037 = false;
445
446        let mut block = TempoBlockEnv::default();
447        block.inner.timestamp = U256::from(timestamp);
448
449        let ctx = Context::mainnet()
450            .with_db(db)
451            .with_block(block)
452            .with_cfg(cfg)
453            .with_tx(Default::default());
454
455        let mut evm = TempoEvm::new(ctx, ());
456        fund_account(&mut evm, address);
457        evm
458    }
459
460    /// Create an EVM with a specific timestamp and a funded account.
461    fn create_funded_evm_with_timestamp(
462        address: Address,
463        timestamp: u64,
464    ) -> TempoEvm<CacheDB<EmptyDB>, ()> {
465        let mut evm = create_evm_with_timestamp(timestamp);
466        fund_account(&mut evm, address);
467        evm
468    }
469
470    /// Create an EVM with T1 hardfork, a specific timestamp, and a funded account.
471    fn create_funded_evm_t1_with_timestamp(
472        address: Address,
473        timestamp: u64,
474    ) -> TempoEvm<CacheDB<EmptyDB>, ()> {
475        let db = CacheDB::new(EmptyDB::new());
476        let mut cfg = CfgEnv::<TempoHardfork>::default();
477        cfg.spec = TempoHardfork::T1;
478        cfg.gas_params = tempo_gas_params(TempoHardfork::T1);
479
480        let mut block = TempoBlockEnv::default();
481        block.inner.timestamp = U256::from(timestamp);
482
483        let ctx = Context::mainnet()
484            .with_db(db)
485            .with_block(block)
486            .with_cfg(cfg)
487            .with_tx(Default::default());
488
489        let mut evm = TempoEvm::new(ctx, ());
490        fund_account(&mut evm, address);
491        evm
492    }
493
494    /// Create an EVM instance with a custom inspector.
495    fn create_evm_with_inspector<I>(inspector: I) -> TempoEvm<CacheDB<EmptyDB>, I> {
496        let db = CacheDB::new(EmptyDB::new());
497        let ctx = Context::mainnet()
498            .with_db(db)
499            .with_block(Default::default())
500            .with_cfg(Default::default())
501            .with_tx(Default::default());
502        TempoEvm::new(ctx, inspector)
503    }
504
505    /// Helper struct for managing P256 key pairs in tests.
506    struct P256KeyPair {
507        signing_key: SigningKey,
508        pub_key_x: alloy_primitives::B256,
509        pub_key_y: alloy_primitives::B256,
510        address: Address,
511    }
512
513    impl P256KeyPair {
514        /// Generate a new random P256 key pair.
515        fn random() -> Self {
516            let signing_key = SigningKey::random(&mut OsRng);
517            let verifying_key = signing_key.verifying_key();
518            let encoded_point = verifying_key.to_encoded_point(false);
519            let pub_key_x = alloy_primitives::B256::from_slice(encoded_point.x().unwrap().as_ref());
520            let pub_key_y = alloy_primitives::B256::from_slice(encoded_point.y().unwrap().as_ref());
521            let address = derive_p256_address(&pub_key_x, &pub_key_y);
522
523            Self {
524                signing_key,
525                pub_key_x,
526                pub_key_y,
527                address,
528            }
529        }
530
531        /// Create a WebAuthn signature for the given challenge.
532        fn sign_webauthn(&self, challenge: &[u8]) -> eyre::Result<WebAuthnSignature> {
533            // Create authenticator data
534            let mut authenticator_data = vec![0u8; 37];
535            authenticator_data[0..32].copy_from_slice(&[0xAA; 32]); // rpIdHash
536            authenticator_data[32] = 0x01; // UP flag set
537            authenticator_data[33..37].copy_from_slice(&[0, 0, 0, 0]); // signCount
538
539            // Create client data JSON
540            let challenge_b64url = URL_SAFE_NO_PAD.encode(challenge);
541            let client_data_json = format!(
542                r#"{{"type":"webauthn.get","challenge":"{challenge_b64url}","origin":"https://example.com","crossOrigin":false}}"#
543            );
544
545            // Compute message hash
546            let client_data_hash = Sha256::digest(client_data_json.as_bytes());
547            let mut final_hasher = Sha256::new();
548            final_hasher.update(&authenticator_data);
549            final_hasher.update(client_data_hash);
550            let message_hash = final_hasher.finalize();
551
552            // Sign
553            let signature: p256::ecdsa::Signature = self.signing_key.sign_prehash(&message_hash)?;
554            let sig_bytes = signature.to_bytes();
555
556            // Construct WebAuthn data
557            let mut webauthn_data = Vec::new();
558            webauthn_data.extend_from_slice(&authenticator_data);
559            webauthn_data.extend_from_slice(client_data_json.as_bytes());
560
561            Ok(WebAuthnSignature {
562                webauthn_data: Bytes::from(webauthn_data),
563                r: alloy_primitives::B256::from_slice(&sig_bytes[0..32]),
564                s: normalize_p256_s(&sig_bytes[32..64]).map_err(|e| eyre::eyre!(e))?,
565                pub_key_x: self.pub_key_x,
566                pub_key_y: self.pub_key_y,
567            })
568        }
569
570        /// Create a signed EIP-7702 authorization for the given delegate address.
571        fn create_signed_authorization(
572            &self,
573            delegate_address: Address,
574        ) -> eyre::Result<TempoSignedAuthorization> {
575            let auth = Authorization {
576                chain_id: U256::from(1),
577                address: delegate_address,
578                nonce: 0,
579            };
580
581            let mut sig_buf = Vec::new();
582            sig_buf.push(tempo_primitives::transaction::tt_authorization::MAGIC);
583            alloy_rlp::Encodable::encode(&auth, &mut sig_buf);
584            let auth_sig_hash = alloy_primitives::keccak256(&sig_buf);
585
586            let webauthn_sig = self.sign_webauthn(auth_sig_hash.as_slice())?;
587            let aa_sig = TempoSignature::Primitive(PrimitiveSignature::WebAuthn(webauthn_sig));
588
589            Ok(TempoSignedAuthorization::new_unchecked(auth, aa_sig))
590        }
591
592        /// Sign a transaction and return it ready for execution.
593        fn sign_tx(&self, tx: TempoTransaction) -> eyre::Result<tempo_primitives::AASigned> {
594            let webauthn_sig = self.sign_webauthn(tx.signature_hash().as_slice())?;
595            Ok(
596                tx.into_signed(TempoSignature::Primitive(PrimitiveSignature::WebAuthn(
597                    webauthn_sig,
598                ))),
599            )
600        }
601
602        fn sign_tx_keychain(
603            &self,
604            tx: TempoTransaction,
605        ) -> eyre::Result<tempo_primitives::AASigned> {
606            // V2: sign keccak256(0x04 || sig_hash || user_address)
607            let sig_hash = tx.signature_hash();
608            let effective_hash = alloy_primitives::keccak256(
609                [&[0x04], sig_hash.as_slice(), self.address.as_slice()].concat(),
610            );
611            let webauthn_sig = self.sign_webauthn(effective_hash.as_slice())?;
612            let keychain_sig =
613                KeychainSignature::new(self.address, PrimitiveSignature::WebAuthn(webauthn_sig));
614            Ok(tx.into_signed(TempoSignature::Keychain(keychain_sig)))
615        }
616    }
617
618    /// Builder for creating test transactions with sensible defaults.
619    struct TxBuilder {
620        calls: Vec<Call>,
621        nonce: u64,
622        nonce_key: U256,
623        gas_limit: u64,
624        max_fee_per_gas: u128,
625        max_priority_fee_per_gas: u128,
626        valid_before: Option<u64>,
627        valid_after: Option<u64>,
628        authorization_list: Vec<TempoSignedAuthorization>,
629        key_authorization: Option<tempo_primitives::transaction::SignedKeyAuthorization>,
630    }
631
632    impl Default for TxBuilder {
633        fn default() -> Self {
634            Self {
635                calls: vec![],
636                nonce: 0,
637                nonce_key: U256::ZERO,
638                gas_limit: 1_000_000,
639                max_fee_per_gas: 0,
640                max_priority_fee_per_gas: 0,
641                valid_before: Some(u64::MAX),
642                valid_after: None,
643                authorization_list: vec![],
644                key_authorization: None,
645            }
646        }
647    }
648
649    impl TxBuilder {
650        fn new() -> Self {
651            Self::default()
652        }
653
654        /// Add a call to the identity precompile with the given input.
655        fn call_identity(mut self, input: &[u8]) -> Self {
656            self.calls.push(Call {
657                to: TxKind::Call(IDENTITY_PRECOMPILE),
658                value: U256::ZERO,
659                input: Bytes::from(input.to_vec()),
660            });
661            self
662        }
663
664        /// Add a call to a specific address.
665        fn call(mut self, to: Address, input: &[u8]) -> Self {
666            self.calls.push(Call {
667                to: TxKind::Call(to),
668                value: U256::ZERO,
669                input: Bytes::from(input.to_vec()),
670            });
671            self
672        }
673
674        /// Add a create call with the given initcode.
675        fn create(mut self, initcode: &[u8]) -> Self {
676            self.calls.push(Call {
677                to: TxKind::Create,
678                value: U256::ZERO,
679                input: Bytes::from(initcode.to_vec()),
680            });
681            self
682        }
683
684        /// Add a call with a specific value transfer.
685        fn call_with_value(mut self, to: Address, input: &[u8], value: U256) -> Self {
686            self.calls.push(Call {
687                to: TxKind::Call(to),
688                value,
689                input: Bytes::from(input.to_vec()),
690            });
691            self
692        }
693
694        fn nonce(mut self, nonce: u64) -> Self {
695            self.nonce = nonce;
696            self
697        }
698
699        fn nonce_key(mut self, nonce_key: U256) -> Self {
700            self.nonce_key = nonce_key;
701            self
702        }
703
704        fn gas_limit(mut self, gas_limit: u64) -> Self {
705            self.gas_limit = gas_limit;
706            self
707        }
708
709        fn with_max_fee_per_gas(mut self, max_fee_per_gas: u128) -> Self {
710            self.max_fee_per_gas = max_fee_per_gas;
711            self
712        }
713
714        fn with_max_priority_fee_per_gas(mut self, max_priority_fee_per_gas: u128) -> Self {
715            self.max_priority_fee_per_gas = max_priority_fee_per_gas;
716            self
717        }
718
719        fn valid_before(mut self, valid_before: Option<u64>) -> Self {
720            self.valid_before = valid_before;
721            self
722        }
723
724        fn valid_after(mut self, valid_after: Option<u64>) -> Self {
725            self.valid_after = valid_after;
726            self
727        }
728
729        fn authorization(mut self, auth: TempoSignedAuthorization) -> Self {
730            self.authorization_list.push(auth);
731            self
732        }
733
734        fn key_authorization(
735            mut self,
736            key_auth: tempo_primitives::transaction::SignedKeyAuthorization,
737        ) -> Self {
738            self.key_authorization = Some(key_auth);
739            self
740        }
741
742        fn build(self) -> TempoTransaction {
743            TempoTransaction {
744                chain_id: 1,
745                fee_token: None,
746                max_priority_fee_per_gas: self.max_priority_fee_per_gas,
747                max_fee_per_gas: self.max_fee_per_gas,
748                gas_limit: self.gas_limit,
749                calls: self.calls,
750                access_list: Default::default(),
751                nonce_key: self.nonce_key,
752                nonce: self.nonce,
753                fee_payer_signature: None,
754                valid_before: self.valid_before.and_then(core::num::NonZeroU64::new),
755                valid_after: self.valid_after.and_then(core::num::NonZeroU64::new),
756                key_authorization: self.key_authorization,
757                tempo_authorization_list: self.authorization_list,
758            }
759        }
760    }
761
762    // ==================== End Test Utility Functions ====================
763
764    #[test_case::test_case(TempoHardfork::T1)]
765    #[test_case::test_case(TempoHardfork::T1C)]
766    fn test_access_millis_timestamp(spec: TempoHardfork) -> eyre::Result<()> {
767        let db = CacheDB::new(EmptyDB::new());
768
769        let mut ctx = Context::mainnet()
770            .with_db(db)
771            .with_block(TempoBlockEnv::default())
772            .with_cfg(CfgEnv::<TempoHardfork>::default())
773            .with_tx(Default::default());
774
775        ctx.cfg.spec = spec;
776        ctx.block.timestamp = U256::from(1000);
777        ctx.block.timestamp_millis_part = 100;
778
779        let mut tempo_evm = TempoEvm::new(ctx, ());
780        let ctx = &mut tempo_evm.ctx;
781
782        let internals = EvmInternals::new(&mut ctx.journaled_state, &ctx.block, &ctx.cfg, &ctx.tx);
783        let mut storage = EvmPrecompileStorageProvider::new_max_gas(internals, &ctx.cfg);
784
785        _ = StorageCtx::enter(&mut storage, || TIP20Setup::path_usd(Address::ZERO).apply())?;
786        drop(storage);
787
788        let contract = Address::random();
789
790        // Create a simple contract that returns output of the opcode.
791        ctx.db_mut().insert_account_info(
792            contract,
793            AccountInfo {
794                // MILLISTIMESTAMP PUSH0 MSTORE PUSH1 0x20 PUSH0 RETURN
795                code: Some(Bytecode::new_raw(bytes!("0x4F5F5260205FF3"))),
796                ..Default::default()
797            },
798        );
799
800        let tx_env = TxEnv {
801            kind: contract.into(),
802            ..Default::default()
803        };
804        let result = tempo_evm.transact_one(tx_env.into())?;
805
806        if !spec.is_t1c() {
807            assert!(result.is_success());
808            assert_eq!(
809                U256::from_be_slice(result.output().unwrap()),
810                U256::from(1000100)
811            );
812        } else {
813            assert!(matches!(
814                result,
815                ExecutionResult::Halt {
816                    reason: TempoHaltReason::Ethereum(HaltReason::OpcodeNotFound),
817                    ..
818                }
819            ));
820        }
821
822        Ok(())
823    }
824
825    #[test]
826    fn test_inspector_calls() -> eyre::Result<()> {
827        // This test calls TIP20 setSupplyCap which emits a SupplyCapUpdate log event
828        let caller = Address::repeat_byte(0x01);
829        let contract = Address::repeat_byte(0x42);
830
831        let input_bytes = ITIP20::setSupplyCapCall {
832            newSupplyCap: U256::from(100),
833        }
834        .abi_encode();
835
836        // Create bytecode that calls setSupplyCap(uint256 newSupplyCap) on PATH_USD
837        // it is 36 bytes long
838        let mut bytecode_bytes = vec![];
839
840        for (i, &byte) in input_bytes.iter().enumerate() {
841            bytecode_bytes.extend_from_slice(&[
842                opcode::PUSH1,
843                byte,
844                opcode::PUSH1,
845                i as u8,
846                opcode::MSTORE8,
847            ]);
848        }
849
850        // CALL to PATH_USD precompile
851        // CALL(gas, addr, value, argsOffset, argsSize, retOffset, retSize)
852        bytecode_bytes.extend_from_slice(&[
853            opcode::PUSH1,
854            0x00, // retSize
855            opcode::PUSH1,
856            0x00, // retOffset
857            opcode::PUSH1,
858            0x24, // argsSize (4 + 32 = 36 = 0x24)
859            opcode::PUSH1,
860            0x00, // argsOffset
861            opcode::PUSH1,
862            0x00, // value = 0
863        ]);
864
865        // PUSH20 PATH_USD_ADDRESS
866        bytecode_bytes.push(opcode::PUSH20);
867        bytecode_bytes.extend_from_slice(PATH_USD_ADDRESS.as_slice());
868
869        bytecode_bytes.extend_from_slice(&[
870            opcode::PUSH2,
871            0xFF,
872            0xFF, // gas
873            opcode::CALL,
874            opcode::POP, // pop success/failure
875            opcode::STOP,
876        ]);
877
878        let bytecode = Bytecode::new_raw(bytecode_bytes.into());
879
880        // Set up EVM with TIP20 infrastructure
881        let mut evm = create_evm_with_inspector(CountInspector::new());
882        // Set up TIP20 using the storage context pattern
883        {
884            let ctx = &mut evm.ctx;
885            let internals =
886                EvmInternals::new(&mut ctx.journaled_state, &ctx.block, &ctx.cfg, &ctx.tx);
887
888            let mut storage = EvmPrecompileStorageProvider::new_max_gas(internals, &ctx.cfg);
889            StorageCtx::enter(&mut storage, || {
890                TIP20Setup::path_usd(caller)
891                    .with_issuer(caller)
892                    .with_admin(contract) // Grant admin role to contract so it can call setSupplyCap
893                    .apply()
894            })?;
895        }
896
897        // Deploy the contract bytecode
898        evm.ctx.db_mut().insert_account_info(
899            contract,
900            AccountInfo {
901                code: Some(bytecode),
902                ..Default::default()
903            },
904        );
905
906        // Execute a call to the contract
907        let tx_env = TxEnv {
908            caller,
909            kind: TxKind::Call(contract),
910            gas_limit: 1_000_000,
911            ..Default::default()
912        };
913        let result = evm
914            .inspect_tx(tx_env.into())
915            .expect("execution should succeed");
916
917        assert!(result.result.is_success());
918
919        // Verify that a SupplyCapUpdate log was emitted by the TIP20 precompile
920        assert_eq!(result.result.logs().len(), 3);
921        // Log should be from TIP20_FACTORY
922        assert_eq!(result.result.logs()[0].address, PATH_USD_ADDRESS);
923
924        // Get the inspector and verify counts
925        let inspector = &evm.inspector;
926
927        // Verify CALL opcode was executed (the call to PATH_USD)
928        assert_eq!(inspector.get_count(opcode::CALL), 1);
929
930        assert_eq!(inspector.get_count(opcode::STOP), 1);
931
932        // Verify log count
933        assert_eq!(inspector.log_count(), 1);
934
935        // Verify call count (initial tx + CALL to PATH_USD)
936        assert_eq!(inspector.call_count(), 2);
937
938        // Should have 2 call ends
939        assert_eq!(inspector.call_end_count(), 2);
940
941        // ==================== Multi-call Tempo transaction test ====================
942        // Test inspector with a Tempo transaction that has multiple calls
943
944        let key_pair = P256KeyPair::random();
945        let tempo_caller = key_pair.address;
946
947        // Create signed authorization for Tempo tx
948        let signed_auth = key_pair.create_signed_authorization(Address::repeat_byte(0x42))?;
949
950        // Create a transaction with 3 calls to identity precompile
951        let tx = TxBuilder::new()
952            .call_identity(&[0x01, 0x02])
953            .call_identity(&[0x03, 0x04])
954            .call_identity(&[0x05, 0x06])
955            .authorization(signed_auth)
956            .build();
957
958        let signed_tx = key_pair.sign_tx(tx)?;
959        let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, tempo_caller);
960
961        // Create a new EVM with fresh inspector for multi-call test
962        let mut multi_evm = create_evm_with_inspector(CountInspector::new());
963        multi_evm.ctx.db_mut().insert_account_info(
964            tempo_caller,
965            AccountInfo {
966                balance: U256::from(DEFAULT_BALANCE),
967                ..Default::default()
968            },
969        );
970
971        // Execute the multi-call transaction with inspector
972        let multi_result = multi_evm.inspect_tx(tx_env)?;
973        assert!(multi_result.result.is_success(),);
974
975        // Verify inspector tracked all 3 calls
976        let multi_inspector = &multi_evm.inspector;
977
978        // Multi-call Tempo transactions execute each call as a separate frame
979        // call_count = 3 (one for each identity precompile call)
980        assert_eq!(multi_inspector.call_count(), 3,);
981        assert_eq!(multi_inspector.call_end_count(), 3,);
982
983        Ok(())
984    }
985
986    #[test]
987    fn test_tempo_tx_initial_gas() -> eyre::Result<()> {
988        let key_pair = P256KeyPair::random();
989        let caller = key_pair.address;
990
991        // Create EVM
992        let mut evm = create_funded_evm(caller);
993        evm.block.basefee = 100_000_000_000;
994
995        // Set up TIP20 first (required for fee token validation)
996        let block = TempoBlockEnv::default();
997        let ctx = &mut evm.ctx;
998        let internals = EvmInternals::new(&mut ctx.journaled_state, &block, &ctx.cfg, &ctx.tx);
999        let mut provider =
1000            EvmPrecompileStorageProvider::new_max_gas(internals, &Default::default());
1001
1002        StorageCtx::enter(&mut provider, || {
1003            TIP20Setup::path_usd(caller)
1004                .with_issuer(caller)
1005                .with_mint(caller, U256::from(100_000))
1006                .apply()
1007        })?;
1008
1009        drop(provider);
1010
1011        // First tx: single call
1012        let tx1 = TxBuilder::new()
1013            .call_identity(&[])
1014            .gas_limit(300_000)
1015            .with_max_fee_per_gas(200_000_000_000)
1016            .with_max_priority_fee_per_gas(0)
1017            .build();
1018
1019        let signed_tx1 = key_pair.sign_tx(tx1)?;
1020        let tx_env1 = TempoTxEnv::from_recovered_tx(&signed_tx1, caller);
1021
1022        let ctx = &mut evm.ctx;
1023        let internals = EvmInternals::new(&mut ctx.journaled_state, &block, &ctx.cfg, &ctx.tx);
1024        let mut provider =
1025            EvmPrecompileStorageProvider::new_max_gas(internals, &Default::default());
1026
1027        let slot = StorageCtx::enter(&mut provider, || {
1028            TIP20Token::from_address(PATH_USD_ADDRESS)?.balances[caller].read()
1029        })?;
1030        drop(provider);
1031
1032        assert_eq!(slot, U256::from(100_000));
1033
1034        let result1 = evm.transact_commit(tx_env1)?;
1035        assert!(result1.is_success());
1036        assert_eq!(result1.tx_gas_used(), 28_671);
1037
1038        let ctx = &mut evm.ctx;
1039        let internals = EvmInternals::new(&mut ctx.journaled_state, &block, &ctx.cfg, &ctx.tx);
1040        let mut provider =
1041            EvmPrecompileStorageProvider::new_max_gas(internals, &Default::default());
1042
1043        let slot = StorageCtx::enter(&mut provider, || {
1044            TIP20Token::from_address(PATH_USD_ADDRESS)?.balances[caller].read()
1045        })?;
1046        drop(provider);
1047
1048        assert_eq!(slot, U256::from(97_132));
1049
1050        // Second tx: two calls
1051        let tx2 = TxBuilder::new()
1052            .call_identity(&[])
1053            .call_identity(&[])
1054            .nonce(1)
1055            .gas_limit(35_000)
1056            .with_max_fee_per_gas(200_000_000_000)
1057            .with_max_priority_fee_per_gas(0)
1058            .build();
1059
1060        let signed_tx2 = key_pair.sign_tx(tx2)?;
1061        let tx_env2 = TempoTxEnv::from_recovered_tx(&signed_tx2, caller);
1062
1063        let result2 = evm.transact_commit(tx_env2)?;
1064        assert!(result2.is_success());
1065        assert_eq!(result2.tx_gas_used(), 31_286);
1066
1067        let ctx = &mut evm.ctx;
1068        let internals = EvmInternals::new(&mut ctx.journaled_state, &block, &ctx.cfg, &ctx.tx);
1069        let mut provider =
1070            EvmPrecompileStorageProvider::new_max_gas(internals, &Default::default());
1071
1072        let slot = StorageCtx::enter(&mut provider, || {
1073            TIP20Token::from_address(PATH_USD_ADDRESS)?.balances[caller].read()
1074        })?;
1075        drop(provider);
1076
1077        assert_eq!(slot, U256::from(94_003));
1078
1079        Ok(())
1080    }
1081
1082    /// Test creating and executing a Tempo transaction with:
1083    /// - WebAuthn signature
1084    /// - Authorization list (aa_auth_list)
1085    /// - Two calls to the identity precompile (0x04)
1086    #[test]
1087    fn test_tempo_tx() -> eyre::Result<()> {
1088        let key_pair = P256KeyPair::random();
1089        let caller = key_pair.address;
1090
1091        // Create signed authorization
1092        let signed_auth = key_pair.create_signed_authorization(Address::repeat_byte(0x42))?;
1093
1094        // Create and sign transaction with two calls to identity precompile
1095        let tx = TxBuilder::new()
1096            .call_identity(&[0x01, 0x02, 0x03, 0x04])
1097            .call_identity(&[0xAA, 0xBB, 0xCC, 0xDD])
1098            .authorization(signed_auth.clone())
1099            .build();
1100
1101        let signed_tx = key_pair.sign_tx(tx)?;
1102        let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1103
1104        // Verify transaction has AA auth list
1105        assert!(tx_env.tempo_tx_env.is_some(),);
1106        let tempo_env = tx_env.tempo_tx_env.as_ref().unwrap();
1107        assert_eq!(tempo_env.tempo_authorization_list.len(), 1);
1108        assert_eq!(tempo_env.aa_calls.len(), 2);
1109
1110        // Create EVM with T1C (required for V2 keychain signatures) and execute transaction
1111        let mut evm = create_funded_evm_t1(caller);
1112
1113        // Execute the transaction and commit state changes
1114        let result = evm.transact_commit(tx_env)?;
1115        assert!(result.is_success());
1116
1117        // Test with KeychainSignature using key_authorization to provision the access key
1118        let key_auth = KeyAuthorization::unrestricted(1, SignatureType::WebAuthn, caller);
1119        let key_auth_webauthn_sig = key_pair.sign_webauthn(key_auth.signature_hash().as_slice())?;
1120        let signed_key_auth =
1121            key_auth.into_signed(PrimitiveSignature::WebAuthn(key_auth_webauthn_sig));
1122
1123        // Create transaction with incremented nonce and key_authorization
1124        let tx2 = TxBuilder::new()
1125            .call_identity(&[0x01, 0x02, 0x03, 0x04])
1126            .call_identity(&[0xAA, 0xBB, 0xCC, 0xDD])
1127            .authorization(signed_auth)
1128            .nonce(1)
1129            .gas_limit(1_000_000)
1130            .key_authorization(signed_key_auth)
1131            .build();
1132
1133        let signed_tx = key_pair.sign_tx_keychain(tx2)?;
1134        let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1135
1136        // Explicitly test tempo_tx_env.signature.as_keychain()
1137        let tempo_env_keychain = tx_env
1138            .tempo_tx_env
1139            .as_ref()
1140            .expect("Transaction should have tempo_tx_env");
1141        let keychain_sig = tempo_env_keychain
1142            .signature
1143            .as_keychain()
1144            .expect("Signature should be a KeychainSignature");
1145
1146        // Validate KeychainSignature properties
1147        // KeychainSignature user_address should match the caller
1148        assert_eq!(keychain_sig.user_address, caller,);
1149
1150        // Verify the inner signature is WebAuthn
1151        assert!(matches!(
1152            keychain_sig.signature,
1153            PrimitiveSignature::WebAuthn(_)
1154        ));
1155
1156        // Verify key_id recovery works correctly using the transaction signature hash
1157        let recovered_key_id = keychain_sig
1158            .key_id(&tempo_env_keychain.signature_hash)
1159            .expect("Key ID recovery should succeed");
1160        assert_eq!(recovered_key_id, caller,);
1161
1162        // Execute the transaction with keychain signature and commit state changes
1163        let result = evm.transact_commit(tx_env)?;
1164        assert!(result.is_success());
1165
1166        // Test a transaction with a failing call to TIP20 contract with wrong input
1167        let tx_fail = TxBuilder::new()
1168            .call(PATH_USD_ADDRESS, &[0x01, 0x02]) // Too short for TIP20
1169            .nonce(2)
1170            .build();
1171
1172        let signed_tx_fail = key_pair.sign_tx_keychain(tx_fail)?;
1173        let tx_env_fail = TempoTxEnv::from_recovered_tx(&signed_tx_fail, caller);
1174
1175        let result_fail = evm.transact(tx_env_fail)?;
1176        assert!(!result_fail.result.is_success());
1177
1178        // Test 2D nonce transaction (nonce_key > 0)
1179        let nonce_key_2d = U256::from(42);
1180
1181        let tx_2d = TxBuilder::new()
1182            .call_identity(&[0x2D, 0x2D, 0x2D, 0x2D])
1183            .nonce_key(nonce_key_2d)
1184            .build();
1185
1186        let signed_tx_2d = key_pair.sign_tx_keychain(tx_2d)?;
1187        let tx_env_2d = TempoTxEnv::from_recovered_tx(&signed_tx_2d, caller);
1188
1189        assert!(tx_env_2d.tempo_tx_env.is_some());
1190        assert_eq!(
1191            tx_env_2d.tempo_tx_env.as_ref().unwrap().nonce_key,
1192            nonce_key_2d
1193        );
1194
1195        let result_2d = evm.transact_commit(tx_env_2d)?;
1196        assert!(result_2d.is_success());
1197
1198        // Verify 2D nonce was incremented
1199        let nonce_slot = NonceManager::new().nonces[caller][nonce_key_2d].slot();
1200        let stored_nonce = evm
1201            .ctx
1202            .db()
1203            .storage_ref(NONCE_PRECOMPILE_ADDRESS, nonce_slot)
1204            .unwrap_or_default();
1205        assert_eq!(stored_nonce, U256::from(1));
1206
1207        // Test second 2D nonce transaction
1208        let tx_2d_2 = TxBuilder::new()
1209            .call_identity(&[0x2E, 0x2E, 0x2E, 0x2E])
1210            .nonce_key(nonce_key_2d)
1211            .nonce(1)
1212            .build();
1213
1214        let signed_tx_2d_2 = key_pair.sign_tx_keychain(tx_2d_2)?;
1215        let tx_env_2d_2 = TempoTxEnv::from_recovered_tx(&signed_tx_2d_2, caller);
1216
1217        let result_2d_2 = evm.transact_commit(tx_env_2d_2)?;
1218        assert!(result_2d_2.is_success());
1219
1220        // Verify nonce incremented again
1221        let stored_nonce_2 = evm
1222            .ctx
1223            .db()
1224            .storage_ref(NONCE_PRECOMPILE_ADDRESS, nonce_slot)
1225            .unwrap_or_default();
1226        assert_eq!(stored_nonce_2, U256::from(2));
1227
1228        Ok(())
1229    }
1230
1231    #[test]
1232    fn test_t3_key_authorization_deny_all_scopes_blocks_same_tx_call() -> eyre::Result<()> {
1233        let key_pair = P256KeyPair::random();
1234        let caller = key_pair.address;
1235
1236        let mut evm = create_funded_evm_t3(caller);
1237
1238        // Set up TIP20 for fee payment.
1239        let block = TempoBlockEnv::default();
1240        {
1241            let ctx = &mut evm.ctx;
1242            let internals = EvmInternals::new(&mut ctx.journaled_state, &block, &ctx.cfg, &ctx.tx);
1243            let mut provider = EvmPrecompileStorageProvider::new_max_gas(internals, &ctx.cfg);
1244
1245            StorageCtx::enter(&mut provider, || {
1246                TIP20Setup::path_usd(caller)
1247                    .with_issuer(caller)
1248                    .with_mint(caller, U256::from(10_000_000))
1249                    .apply()
1250            })?;
1251        }
1252
1253        // Explicit deny-all marker in protocol payload: Some([]).
1254        let key_auth =
1255            KeyAuthorization::unrestricted(1, SignatureType::WebAuthn, caller).with_no_calls();
1256        let key_auth_sig = key_pair.sign_webauthn(key_auth.signature_hash().as_slice())?;
1257        let signed_key_auth = key_auth.into_signed(PrimitiveSignature::WebAuthn(key_auth_sig));
1258
1259        let tx = TxBuilder::new()
1260            .call_identity(&[0x01])
1261            .key_authorization(signed_key_auth)
1262            .gas_limit(5_000_000)
1263            .build();
1264
1265        // Use keychain signature so call-scope validation runs in the same tx.
1266        let signed_tx = key_pair.sign_tx_keychain(tx)?;
1267        let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1268
1269        let result = evm.transact_commit(tx_env)?;
1270        assert!(
1271            !result.is_success(),
1272            "deny-all scope should now fail during paid execution"
1273        );
1274        assert!(
1275            result.tx_gas_used() > 0,
1276            "failed execution should still consume gas"
1277        );
1278
1279        Ok(())
1280    }
1281
1282    #[test]
1283    fn test_t3_key_authorization_accepts_empty_recipient_allowlist_as_unconstrained()
1284    -> eyre::Result<()> {
1285        let key_pair = P256KeyPair::random();
1286        let caller = key_pair.address;
1287
1288        let mut evm = create_funded_evm_t3(caller);
1289
1290        let block = TempoBlockEnv::default();
1291        {
1292            let ctx = &mut evm.ctx;
1293            let internals = EvmInternals::new(&mut ctx.journaled_state, &block, &ctx.cfg, &ctx.tx);
1294            let mut provider = EvmPrecompileStorageProvider::new_max_gas(internals, &ctx.cfg);
1295
1296            StorageCtx::enter(&mut provider, || {
1297                TIP20Setup::path_usd(caller)
1298                    .with_issuer(caller)
1299                    .with_mint(caller, U256::from(10_000_000))
1300                    .apply()
1301            })?;
1302        }
1303
1304        let transfer_to = Address::repeat_byte(0xaa);
1305        let transfer_input = ITIP20::transferCall {
1306            to: transfer_to,
1307            amount: U256::from(1_u64),
1308        }
1309        .abi_encode();
1310
1311        let key_auth = KeyAuthorization::unrestricted(1, SignatureType::WebAuthn, caller)
1312            .with_allowed_calls(vec![tempo_primitives::transaction::CallScope {
1313                target: PATH_USD_ADDRESS,
1314                selector_rules: vec![tempo_primitives::transaction::SelectorRule {
1315                    selector: ITIP20::transferCall::SELECTOR,
1316                    recipients: Vec::new(),
1317                }],
1318            }]);
1319        let key_auth_sig = key_pair.sign_webauthn(key_auth.signature_hash().as_slice())?;
1320        let signed_key_auth = key_auth.into_signed(PrimitiveSignature::WebAuthn(key_auth_sig));
1321
1322        let tx = TxBuilder::new()
1323            .call(PATH_USD_ADDRESS, &transfer_input)
1324            .key_authorization(signed_key_auth)
1325            .gas_limit(5_000_000)
1326            .build();
1327
1328        let signed_tx = key_pair.sign_tx_keychain(tx)?;
1329        let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1330
1331        evm.transact_commit(tx_env)
1332            .expect("empty recipient allowlist should allow the call");
1333
1334        Ok(())
1335    }
1336
1337    #[test]
1338    fn test_same_tx_key_authorization_rejects_key_type_mismatch() -> eyre::Result<()> {
1339        let key_pair = P256KeyPair::random();
1340        let caller = key_pair.address;
1341
1342        let mut evm = create_funded_evm_t3(caller);
1343
1344        let key_auth = KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, caller);
1345        let key_auth_sig = key_pair.sign_webauthn(key_auth.signature_hash().as_slice())?;
1346        let signed_key_auth = key_auth.into_signed(PrimitiveSignature::WebAuthn(key_auth_sig));
1347
1348        let tx = TxBuilder::new()
1349            .call_identity(&[0x01])
1350            .key_authorization(signed_key_auth)
1351            .gas_limit(5_000_000)
1352            .build();
1353
1354        let signed_tx = key_pair.sign_tx_keychain(tx)?;
1355        let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1356
1357        let err = evm
1358            .transact_commit(tx_env)
1359            .expect_err("mismatched key_type should reject same-tx auth+use");
1360
1361        assert!(
1362            matches!(
1363                err,
1364                revm::context::result::EVMError::Transaction(
1365                    TempoInvalidTransaction::KeychainValidationFailed { .. }
1366                )
1367            ),
1368            "expected KeychainValidationFailed, got: {err:?}"
1369        );
1370
1371        Ok(())
1372    }
1373
1374    /// Test that Tempo transaction time window validation works correctly.
1375    /// Tests `valid_after` and `valid_before` fields against block timestamp.
1376    #[test]
1377    fn test_tempo_tx_time_window() -> eyre::Result<()> {
1378        let key_pair = P256KeyPair::random();
1379        let caller = key_pair.address;
1380
1381        // Create signed authorization
1382        let signed_auth = key_pair.create_signed_authorization(Address::repeat_byte(0x42))?;
1383
1384        // Helper to create and sign a transaction with time window parameters
1385        let create_signed_tx = |valid_after: Option<u64>, valid_before: Option<u64>| {
1386            let tx = TxBuilder::new()
1387                .call_identity(&[0x01, 0x02, 0x03, 0x04])
1388                .authorization(signed_auth.clone())
1389                .valid_after(valid_after)
1390                .valid_before(valid_before)
1391                .build();
1392            key_pair.sign_tx(tx)
1393        };
1394
1395        // Test case 1: Transaction fails when block_timestamp < valid_after
1396        {
1397            let mut evm = create_funded_evm_with_timestamp(caller, 100);
1398            let signed_tx = create_signed_tx(Some(200), None)?;
1399            let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1400
1401            let result = evm.transact(tx_env);
1402            assert!(result.is_err());
1403            let err = result.unwrap_err();
1404            assert!(
1405                matches!(
1406                    err,
1407                    revm::context::result::EVMError::Transaction(
1408                        TempoInvalidTransaction::ValidAfter {
1409                            current: 100,
1410                            valid_after: 200
1411                        }
1412                    )
1413                ),
1414                "Expected ValidAfter error, got: {err:?}"
1415            );
1416        }
1417
1418        // Test case 2: Transaction fails when block_timestamp >= valid_before
1419        {
1420            let mut evm = create_funded_evm_with_timestamp(caller, 200);
1421            let signed_tx = create_signed_tx(None, Some(200))?;
1422            let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1423
1424            let result = evm.transact(tx_env);
1425            assert!(result.is_err());
1426            let err = result.unwrap_err();
1427            assert!(
1428                matches!(
1429                    err,
1430                    revm::context::result::EVMError::Transaction(
1431                        TempoInvalidTransaction::ValidBefore {
1432                            current: 200,
1433                            valid_before: 200
1434                        }
1435                    )
1436                ),
1437                "Expected ValidBefore error, got: {err:?}"
1438            );
1439        }
1440
1441        // Test case 3: Transaction fails when block_timestamp > valid_before
1442        {
1443            let mut evm = create_funded_evm_with_timestamp(caller, 300);
1444            let signed_tx = create_signed_tx(None, Some(200))?;
1445            let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1446
1447            let result = evm.transact(tx_env);
1448            assert!(result.is_err());
1449            let err = result.unwrap_err();
1450            assert!(
1451                matches!(
1452                    err,
1453                    revm::context::result::EVMError::Transaction(
1454                        TempoInvalidTransaction::ValidBefore {
1455                            current: 300,
1456                            valid_before: 200
1457                        }
1458                    )
1459                ),
1460                "Expected ValidBefore error, got: {err:?}"
1461            );
1462        }
1463
1464        // Test case 4: Transaction succeeds when exactly at valid_after boundary
1465        {
1466            let mut evm = create_funded_evm_with_timestamp(caller, 200);
1467            let signed_tx = create_signed_tx(Some(200), None)?;
1468            let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1469
1470            let result = evm.transact(tx_env)?;
1471            assert!(result.result.is_success());
1472        }
1473
1474        // Test case 5: Transaction succeeds when within time window
1475        {
1476            let mut evm = create_funded_evm_with_timestamp(caller, 150);
1477            let signed_tx = create_signed_tx(Some(100), Some(200))?;
1478            let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1479
1480            let result = evm.transact(tx_env)?;
1481            assert!(result.result.is_success());
1482        }
1483
1484        // Test case 6: Transaction fails when block_timestamp < valid_after in a window
1485        {
1486            let mut evm = create_funded_evm_with_timestamp(caller, 50);
1487            let signed_tx = create_signed_tx(Some(100), Some(200))?;
1488            let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1489
1490            let result = evm.transact(tx_env);
1491            assert!(result.is_err());
1492            let err = result.unwrap_err();
1493            assert!(
1494                matches!(
1495                    err,
1496                    revm::context::result::EVMError::Transaction(
1497                        TempoInvalidTransaction::ValidAfter {
1498                            current: 50,
1499                            valid_after: 100
1500                        }
1501                    )
1502                ),
1503                "Expected ValidAfter error, got: {err:?}"
1504            );
1505        }
1506
1507        // Test case 7: Transaction fails when block_timestamp >= valid_before in a window
1508        {
1509            let mut evm = create_funded_evm_with_timestamp(caller, 200);
1510            let signed_tx = create_signed_tx(Some(100), Some(200))?;
1511            let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1512
1513            let result = evm.transact(tx_env);
1514            assert!(result.is_err());
1515            let err = result.unwrap_err();
1516            assert!(
1517                matches!(
1518                    err,
1519                    revm::context::result::EVMError::Transaction(
1520                        TempoInvalidTransaction::ValidBefore {
1521                            current: 200,
1522                            valid_before: 200
1523                        }
1524                    )
1525                ),
1526                "Expected ValidBefore error, got: {err:?}"
1527            );
1528        }
1529
1530        Ok(())
1531    }
1532
1533    /// Test executing a Tempo transaction where the first call is a Create kind.
1534    /// This should succeed as CREATE is allowed as the first call.
1535    #[test]
1536    fn test_tempo_tx_create_first_call() -> eyre::Result<()> {
1537        let key_pair = P256KeyPair::random();
1538        let caller = key_pair.address;
1539
1540        // Simple contract that just returns: PUSH1 0x00 PUSH1 0x00 RETURN
1541        let initcode = vec![0x60, 0x00, 0x60, 0x00, 0xF3];
1542
1543        // Create transaction with CREATE as first call (no authorization list)
1544        let tx = TxBuilder::new()
1545            .create(&initcode)
1546            .call_identity(&[0x01, 0x02])
1547            .gas_limit(200_000)
1548            .build();
1549
1550        let signed_tx = key_pair.sign_tx(tx)?;
1551        let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1552
1553        // Create EVM and execute
1554        let mut evm = create_funded_evm(caller);
1555        let result = evm.transact_commit(tx_env)?;
1556
1557        assert!(result.is_success(), "CREATE as first call should succeed");
1558
1559        Ok(())
1560    }
1561
1562    /// Test that a Tempo transaction fails when CREATE is the second call.
1563    /// CREATE must be the first call if used.
1564    #[test]
1565    fn test_tempo_tx_create_second_call_fails() -> eyre::Result<()> {
1566        let key_pair = P256KeyPair::random();
1567        let caller = key_pair.address;
1568
1569        // Simple initcode
1570        let initcode = vec![0x60, 0x00, 0x60, 0x00, 0xF3];
1571
1572        // Create transaction with a regular call first, then CREATE second
1573        let tx = TxBuilder::new()
1574            .call_identity(&[0x01, 0x02])
1575            .create(&initcode)
1576            .gas_limit(200_000)
1577            .build();
1578
1579        let signed_tx = key_pair.sign_tx(tx)?;
1580        let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1581
1582        // Create EVM and execute - should fail validation
1583        let mut evm = create_funded_evm(caller);
1584        let result = evm.transact(tx_env);
1585
1586        assert!(result.is_err(), "CREATE as second call should fail");
1587        let err = result.unwrap_err();
1588        assert!(
1589            matches!(
1590                err,
1591                revm::context::result::EVMError::Transaction(
1592                    TempoInvalidTransaction::CallsValidation(msg)
1593                ) if msg.contains("first call")
1594            ),
1595            "Expected CallsValidation error about 'first call', got: {err:?}"
1596        );
1597
1598        Ok(())
1599    }
1600
1601    /// Test validate_aa_initial_tx_gas error cases.
1602    /// Tests all error paths in the AA initial transaction gas validation:
1603    /// - CreateInitCodeSizeLimit: when initcode exceeds max size
1604    /// - ValueTransferNotAllowedInAATx: when a call has non-zero value
1605    /// - CallGasCostMoreThanGasLimit: when gas_limit < intrinsic_gas
1606    #[test]
1607    fn test_validate_aa_initial_tx_gas_errors() -> eyre::Result<()> {
1608        use revm::{context::result::EVMError, handler::Handler};
1609
1610        use crate::handler::TempoEvmHandler;
1611
1612        let key_pair = P256KeyPair::random();
1613        let caller = key_pair.address;
1614
1615        // Helper to create EVM with signed transaction
1616        let create_evm_with_tx =
1617            |tx: TempoTransaction| -> eyre::Result<TempoEvm<CacheDB<EmptyDB>, ()>> {
1618                let signed_tx = key_pair.sign_tx(tx)?;
1619                let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1620                let mut evm = create_funded_evm(caller);
1621                evm.ctx.tx = tx_env;
1622                Ok(evm)
1623            };
1624
1625        let handler = TempoEvmHandler::default();
1626
1627        // Test 1: CreateInitCodeSizeLimit - initcode exceeds max size
1628        {
1629            // Default max initcode size is 49152 bytes (2 * MAX_CODE_SIZE)
1630            let oversized_initcode = vec![0x60; 50_000];
1631
1632            let mut evm = create_evm_with_tx(
1633                TxBuilder::new()
1634                    .create(&oversized_initcode)
1635                    .gas_limit(10_000_000)
1636                    .build(),
1637            )?;
1638
1639            let result = handler.validate_initial_tx_gas(&mut evm);
1640            assert!(
1641                matches!(
1642                    result,
1643                    Err(EVMError::Transaction(
1644                        TempoInvalidTransaction::EthInvalidTransaction(
1645                            revm::context::result::InvalidTransaction::CreateInitCodeSizeLimit
1646                        )
1647                    ))
1648                ),
1649                "Expected CreateInitCodeSizeLimit error, got: {result:?}"
1650            );
1651        }
1652
1653        // Test 2: ValueTransferNotAllowedInAATx - call has non-zero value
1654        {
1655            let mut evm = create_evm_with_tx(
1656                TxBuilder::new()
1657                    .call_with_value(IDENTITY_PRECOMPILE, &[0x01, 0x02], U256::from(1000))
1658                    .build(),
1659            )?;
1660
1661            let result = handler.validate_initial_tx_gas(&mut evm);
1662            assert!(
1663                matches!(
1664                    result,
1665                    Err(EVMError::Transaction(
1666                        TempoInvalidTransaction::ValueTransferNotAllowedInAATx
1667                    ))
1668                ),
1669                "Expected ValueTransferNotAllowedInAATx error, got: {result:?}"
1670            );
1671        }
1672
1673        // Test 3: CallGasCostMoreThanGasLimit - gas_limit < intrinsic_gas
1674        {
1675            let mut evm = create_evm_with_tx(
1676                TxBuilder::new()
1677                    .call_identity(&[0x01, 0x02, 0x03, 0x04])
1678                    .gas_limit(1000) // Way too low, intrinsic cost is at least 21000
1679                    .build(),
1680            )?;
1681
1682            let result = handler.validate_initial_tx_gas(&mut evm);
1683            assert!(
1684                matches!(
1685                    result,
1686                    Err(EVMError::Transaction(
1687                        TempoInvalidTransaction::EthInvalidTransaction(
1688                            InvalidTransaction::CallGasCostMoreThanGasLimit {
1689                                gas_limit: 1000,
1690                                initial_gas
1691                            }
1692                        )
1693                    )) if initial_gas > 1000
1694                ),
1695                "Expected CallGasCostMoreThanGasLimit error, got: {result:?}"
1696            );
1697        }
1698
1699        // Test 4: gas_limit < floor_gas (EIP-7623)
1700        // For AA transactions, intrinsic gas is higher than for standard txs, so with
1701        // gas_limit=31000 the intrinsic gas check fires first (CallGasCostMoreThanGasLimit).
1702        // The floor gas error (GasFloorMoreThanGasLimit) would only appear if gas_limit
1703        // were between intrinsic_gas and floor_gas, but AA intrinsic gas already exceeds
1704        // both values here.
1705        {
1706            let large_calldata = vec![0x42; 1000]; // 1000 non-zero bytes = 1000 tokens
1707
1708            let mut evm = create_evm_with_tx(
1709                TxBuilder::new()
1710                    .call_identity(&large_calldata)
1711                    .gas_limit(31_000)
1712                    .build(),
1713            )?;
1714
1715            let result = handler.validate_initial_tx_gas(&mut evm);
1716
1717            assert!(
1718                matches!(
1719                    result,
1720                    Err(EVMError::Transaction(
1721                        TempoInvalidTransaction::EthInvalidTransaction(
1722                            InvalidTransaction::CallGasCostMoreThanGasLimit {
1723                                gas_limit: 31_000,
1724                                initial_gas
1725                            }
1726                        )
1727                    )) if initial_gas > 31_000
1728                ),
1729                "Expected CallGasCostMoreThanGasLimit, got: {result:?}"
1730            );
1731        }
1732
1733        // Test 5: Success when gas_limit >= both initial_gas and floor_gas
1734        // Verifies floor_gas > initial_gas for large calldata (EIP-7623 scenario)
1735        {
1736            let large_calldata = vec![0x42; 1000];
1737
1738            let mut evm = create_evm_with_tx(
1739                TxBuilder::new()
1740                    .call_identity(&large_calldata)
1741                    .gas_limit(1_000_000) // Plenty of gas for both initial and floor
1742                    .build(),
1743            )?;
1744
1745            let result = handler.validate_initial_tx_gas(&mut evm);
1746            assert!(
1747                result.is_ok(),
1748                "Expected success with sufficient gas, got: {result:?}"
1749            );
1750
1751            let gas = result.unwrap();
1752            // Verify floor_gas > initial_total_gas for this calldata (EIP-7623 scenario)
1753            assert!(
1754                gas.floor_gas > gas.initial_total_gas(),
1755                "Expected floor_gas ({}) > initial_total_gas ({}) for large calldata",
1756                gas.floor_gas,
1757                gas.initial_total_gas()
1758            );
1759        }
1760
1761        // Test 6: Success case - sufficient gas provided (small calldata)
1762        {
1763            let mut evm = create_evm_with_tx(
1764                TxBuilder::new()
1765                    .call_identity(&[0x01, 0x02, 0x03, 0x04])
1766                    .gas_limit(1_000_000)
1767                    .build(),
1768            )?;
1769
1770            let result = handler.validate_initial_tx_gas(&mut evm);
1771            assert!(result.is_ok(), "Expected success, got: {result:?}");
1772
1773            let gas = result.unwrap();
1774            assert!(
1775                gas.initial_total_gas() >= 21_000,
1776                "Initial gas should be at least 21k base"
1777            );
1778        }
1779
1780        Ok(())
1781    }
1782
1783    // ==================== TIP-1000 EVM Configuration Tests ====================
1784
1785    /// Test AA transaction gas usage for simple identity precompile call.
1786    /// This establishes a baseline for gas comparison.
1787    /// Uses T1 hardfork for TIP-1000 gas costs.
1788    #[test]
1789    fn test_aa_tx_gas_baseline_identity_call() -> eyre::Result<()> {
1790        let key_pair = P256KeyPair::random();
1791        let caller = key_pair.address;
1792
1793        let mut evm = create_funded_evm_t1(caller);
1794
1795        // Simple call to identity precompile
1796        // T1 adds 250k for new account creation (nonce == 0)
1797        let tx = TxBuilder::new()
1798            .call_identity(&[0x01, 0x02, 0x03, 0x04])
1799            .gas_limit(500_000)
1800            .build();
1801
1802        let signed_tx = key_pair.sign_tx(tx)?;
1803        let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1804
1805        let result = evm.transact_commit(tx_env)?;
1806        assert!(result.is_success());
1807
1808        // With T1 TIP-1000: new account cost (250k) + base intrinsic (21k) + WebAuthn (~3.4k) + calldata
1809        let gas_used = result.tx_gas_used();
1810        assert_eq!(
1811            gas_used, 278738,
1812            "T1 baseline identity call gas should be exact"
1813        );
1814
1815        Ok(())
1816    }
1817
1818    /// Test AA transaction gas usage with SSTORE to a new storage slot.
1819    /// This tests TIP-1000's increased SSTORE cost (250,000 gas for new slot).
1820    /// Uses T1 hardfork for TIP-1000 gas costs.
1821    #[test]
1822    fn test_aa_tx_gas_sstore_new_slot() -> eyre::Result<()> {
1823        let key_pair = P256KeyPair::random();
1824        let caller = key_pair.address;
1825        let contract = Address::repeat_byte(0x55);
1826
1827        let mut evm = create_funded_evm_t1(caller);
1828
1829        // Deploy contract that does SSTORE to slot 0:
1830        // PUSH1 0x42 PUSH1 0x00 SSTORE STOP
1831        // This stores value 0x42 at slot 0
1832        let sstore_bytecode = Bytecode::new_raw(bytes!("60426000555B00"));
1833        evm.ctx.db_mut().insert_account_info(
1834            contract,
1835            AccountInfo {
1836                code: Some(sstore_bytecode),
1837                ..Default::default()
1838            },
1839        );
1840
1841        // T1 costs: new account (250k) + SSTORE new slot (250k) + base costs
1842        let tx = TxBuilder::new()
1843            .call(contract, &[])
1844            .gas_limit(600_000)
1845            .build();
1846
1847        let signed_tx = key_pair.sign_tx(tx)?;
1848        let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1849
1850        let result = evm.transact_commit(tx_env)?;
1851        assert!(result.is_success(), "SSTORE transaction should succeed");
1852
1853        // With TIP-1000: new account (250k) + SSTORE to new slot (250k) + base costs
1854        let gas_used = result.tx_gas_used();
1855        assert_eq!(
1856            gas_used, 530863,
1857            "T1 SSTORE to new slot gas should be exact"
1858        );
1859
1860        Ok(())
1861    }
1862
1863    /// Test AA transaction gas usage with SSTORE to an existing storage slot (warm).
1864    /// Warm SSTORE should be much cheaper than cold SSTORE to a new slot.
1865    /// Uses T1 hardfork for TIP-1000 gas costs.
1866    #[test]
1867    fn test_aa_tx_gas_sstore_warm_slot() -> eyre::Result<()> {
1868        let key_pair = P256KeyPair::random();
1869        let caller = key_pair.address;
1870        let contract = Address::repeat_byte(0x56);
1871
1872        let mut evm = create_funded_evm_t1(caller);
1873
1874        // Deploy contract that does SSTORE to slot 0:
1875        // PUSH1 0x42 PUSH1 0x00 SSTORE STOP
1876        let sstore_bytecode = Bytecode::new_raw(bytes!("60426000555B00"));
1877        evm.ctx.db_mut().insert_account_info(
1878            contract,
1879            AccountInfo {
1880                code: Some(sstore_bytecode),
1881                ..Default::default()
1882            },
1883        );
1884
1885        // Pre-populate storage slot 0 with a non-zero value
1886        evm.ctx
1887            .db_mut()
1888            .insert_account_storage(contract, U256::ZERO, U256::from(1))
1889            .unwrap();
1890
1891        // T1 costs: new account (250k) + SSTORE reset (not new slot) + base costs
1892        let tx = TxBuilder::new()
1893            .call(contract, &[])
1894            .gas_limit(500_000)
1895            .build();
1896
1897        let signed_tx = key_pair.sign_tx(tx)?;
1898        let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1899
1900        let result = evm.transact_commit(tx_env)?;
1901        assert!(
1902            result.is_success(),
1903            "SSTORE to existing slot should succeed"
1904        );
1905
1906        // SSTORE to existing non-zero slot (reset) doesn't trigger the 250k new slot cost
1907        // But still has new account cost (250k) + cold SLOAD (2100) + warm SSTORE reset (~2900)
1908        let gas_used = result.tx_gas_used();
1909        assert_eq!(
1910            gas_used, 283663,
1911            "T1 SSTORE to existing slot gas should be exact"
1912        );
1913
1914        Ok(())
1915    }
1916
1917    /// Test AA transaction gas comparison: multiple SSTORE operations.
1918    /// Uses T1 hardfork for TIP-1000 gas costs.
1919    #[test]
1920    fn test_aa_tx_gas_multiple_sstores() -> eyre::Result<()> {
1921        let key_pair = P256KeyPair::random();
1922        let caller = key_pair.address;
1923        let contract = Address::repeat_byte(0x57);
1924
1925        let mut evm = create_funded_evm_t1(caller);
1926
1927        // Deploy contract that does 2 SSTOREs to different slots:
1928        // PUSH1 0x11 PUSH1 0x00 SSTORE  (store 0x11 at slot 0)
1929        // PUSH1 0x22 PUSH1 0x01 SSTORE  (store 0x22 at slot 1)
1930        // STOP
1931        let multi_sstore_bytecode = Bytecode::new_raw(bytes!("601160005560226001555B00"));
1932        evm.ctx.db_mut().insert_account_info(
1933            contract,
1934            AccountInfo {
1935                code: Some(multi_sstore_bytecode),
1936                ..Default::default()
1937            },
1938        );
1939
1940        // T1 costs: new account (250k) + 2 SSTORE new slots (2 * 250k) + base costs
1941        let tx = TxBuilder::new()
1942            .call(contract, &[])
1943            .gas_limit(1_000_000)
1944            .build();
1945
1946        let signed_tx = key_pair.sign_tx(tx)?;
1947        let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1948
1949        let result = evm.transact_commit(tx_env)?;
1950        assert!(
1951            result.is_success(),
1952            "Multiple SSTORE transaction should succeed"
1953        );
1954
1955        // With TIP-1000: new account (250k) + 2 SSTOREs to new slots (2 * 250k) = 750k + base
1956        let gas_used = result.tx_gas_used();
1957        assert_eq!(gas_used, 783069, "T1 multiple SSTOREs gas should be exact");
1958
1959        Ok(())
1960    }
1961
1962    /// Seed the TIP-1060 persistent storage credit balance for `owner` directly into the storage
1963    /// credits contract's storage. Storage creation mode is transient and must be selected inside
1964    /// each transaction with `setMode`.
1965    fn seed_storage_credit_balance(
1966        evm: &mut TempoEvm<CacheDB<EmptyDB>, ()>,
1967        owner: Address,
1968        balance: u64,
1969    ) {
1970        // The storage credits contract account must exist before we can write storage to it.
1971        evm.ctx
1972            .db_mut()
1973            .insert_account_info(STORAGE_CREDITS_ADDRESS, AccountInfo::default());
1974        let slot = StorageCredits::slot(owner);
1975        evm.ctx
1976            .db_mut()
1977            .insert_account_storage(STORAGE_CREDITS_ADDRESS, slot, U256::from(balance))
1978            .unwrap();
1979    }
1980
1981    fn storage_credit_word(evm: &TempoEvm<CacheDB<EmptyDB>, ()>, owner: Address) -> U256 {
1982        let slot = StorageCredits::slot(owner);
1983        evm.ctx
1984            .db()
1985            .storage_ref(STORAGE_CREDITS_ADDRESS, slot)
1986            .unwrap()
1987    }
1988
1989    /// Read back the TIP-1060 storage credit balance stored for `owner` from the storage credits contract.
1990    fn storage_credit_balance(evm: &TempoEvm<CacheDB<EmptyDB>, ()>, owner: Address) -> u64 {
1991        u64::from_word(storage_credit_word(evm, owner)).unwrap()
1992    }
1993
1994    fn tip1060_abi_mode(mode: CreditMode) -> Mode {
1995        match mode {
1996            CreditMode::Refund => Mode::Refund,
1997            CreditMode::Preserve => Mode::Preserve,
1998            CreditMode::Direct => Mode::Direct,
1999        }
2000    }
2001
2002    fn append_tip1060_precompile_call(bytecode_bytes: &mut Vec<u8>, input_bytes: &[u8]) {
2003        append_tip1060_precompile_call_store_return(bytecode_bytes, input_bytes, None);
2004    }
2005
2006    /// Appends bytecode that calls the TIP-1060 precompile and stores the returned word in `slot`.
2007    fn append_tip1060_precompile_call_store_return(
2008        bytecode_bytes: &mut Vec<u8>,
2009        input_bytes: &[u8],
2010        store_return_in_slot: Option<u8>,
2011    ) {
2012        for (i, &byte) in input_bytes.iter().enumerate() {
2013            assert!(i <= u8::MAX as usize);
2014            // PUSH1 <byte> PUSH1 <offset> MSTORE8  (write calldata byte at memory[offset])
2015            bytecode_bytes.extend_from_slice(&[
2016                opcode::PUSH1,
2017                byte,
2018                opcode::PUSH1,
2019                i as u8,
2020                opcode::MSTORE8,
2021            ]);
2022        }
2023
2024        let ret_size = if store_return_in_slot.is_some() {
2025            0x20
2026        } else {
2027            0
2028        };
2029
2030        // PUSH1 <retSize> PUSH1 0x00 PUSH1 <argsSize> PUSH1 0x00 PUSH1 0x00
2031        // (retOffset=0, argsOffset=0, value=0)
2032        bytecode_bytes.extend_from_slice(&[opcode::PUSH1, ret_size, opcode::PUSH1, 0x00]);
2033        bytecode_bytes.extend_from_slice(&[opcode::PUSH1, input_bytes.len() as u8]);
2034        bytecode_bytes.extend_from_slice(&bytes!("60006000"));
2035        // PUSH20 <STORAGE_CREDITS_ADDRESS>
2036        bytecode_bytes.push(opcode::PUSH20);
2037        bytecode_bytes.extend_from_slice(STORAGE_CREDITS_ADDRESS.as_slice());
2038        // PUSH3 0x0f4240 CALL POP  (call with 1_000_000 gas and discard success flag)
2039        bytecode_bytes.extend_from_slice(&bytes!("620f4240f150"));
2040
2041        if let Some(slot) = store_return_in_slot {
2042            // Store returned word: MLOAD(0) -> SSTORE(slot, value).
2043            bytecode_bytes.extend_from_slice(&[
2044                opcode::PUSH1,
2045                0x00,
2046                opcode::MLOAD,
2047                opcode::PUSH1,
2048                slot,
2049            ]);
2050            bytecode_bytes.push(opcode::SSTORE);
2051        }
2052    }
2053
2054    /// Appends bytecode that calls TIP-1060 precompile's `setMode(mode)` as the executing contract.
2055    fn append_tip1060_set_mode_call(bytecode_bytes: &mut Vec<u8>, mode: CreditMode) {
2056        let input_bytes = IStorageCredits::setModeCall {
2057            newMode: tip1060_abi_mode(mode),
2058        }
2059        .abi_encode();
2060
2061        append_tip1060_precompile_call(bytecode_bytes, &input_bytes);
2062    }
2063
2064    fn bytecode_with_tip1060_mode(mode: CreditMode, body: &[u8]) -> Bytecode {
2065        let mut bytecode = Vec::new();
2066        if mode != CreditMode::Refund {
2067            append_tip1060_set_mode_call(&mut bytecode, mode);
2068        }
2069        bytecode.extend_from_slice(body);
2070        Bytecode::new_raw(bytecode.into())
2071    }
2072
2073    fn run_tx_on_tip1060_contract(
2074        mode: CreditMode,
2075        contract: Address,
2076        bytecode: &[u8],
2077    ) -> eyre::Result<(u64, TempoEvm<CacheDB<EmptyDB>, ()>)> {
2078        run_tx_on_tip1060_contract_with_setup(mode, contract, bytecode, |_, _| Ok(()))
2079    }
2080
2081    fn run_tx_on_tip1060_contract_with_setup(
2082        mode: CreditMode,
2083        contract: Address,
2084        bytecode: &[u8],
2085        setup: impl FnOnce(&mut TempoEvm<CacheDB<EmptyDB>, ()>, Address) -> eyre::Result<()>,
2086    ) -> eyre::Result<(u64, TempoEvm<CacheDB<EmptyDB>, ()>)> {
2087        let key_pair = P256KeyPair::random();
2088        let caller = key_pair.address;
2089        let mut evm = create_funded_evm_t7(caller);
2090
2091        evm.ctx.db_mut().insert_account_info(
2092            contract,
2093            AccountInfo {
2094                code: Some(bytecode_with_tip1060_mode(mode, bytecode)),
2095                ..Default::default()
2096            },
2097        );
2098        setup(&mut evm, contract)?;
2099
2100        let tx = TxBuilder::new()
2101            .call(contract, &[])
2102            .gas_limit(2_000_000)
2103            .build();
2104        let signed_tx = key_pair.sign_tx(tx)?;
2105        let result = evm.transact_commit(TempoTxEnv::from_recovered_tx(&signed_tx, caller))?;
2106        assert!(result.is_success(), "test transaction should succeed");
2107
2108        Ok((result.tx_gas_used(), evm))
2109    }
2110
2111    fn mint_storage_credits_with_clears(
2112        evm: &mut TempoEvm<CacheDB<EmptyDB>, ()>,
2113        key_pair: &P256KeyPair,
2114        caller: Address,
2115        contract: Address,
2116        nonce: u64,
2117        credits: u64,
2118    ) -> eyre::Result<u64> {
2119        if credits == 0 {
2120            return Ok(nonce);
2121        }
2122
2123        let mut body = Vec::new();
2124        for credit in 0..credits {
2125            let slot = 0xf0 + credit;
2126            assert!(slot <= u64::from(u8::MAX));
2127            evm.ctx
2128                .db_mut()
2129                .insert_account_storage(contract, U256::from(slot), U256::ONE)?;
2130            body.extend_from_slice(&[opcode::PUSH1, 0, opcode::PUSH1, slot as u8, opcode::SSTORE]);
2131        }
2132        body.push(opcode::STOP);
2133
2134        evm.ctx.db_mut().insert_account_info(
2135            contract,
2136            AccountInfo {
2137                code: Some(Bytecode::new_raw(body.into())),
2138                ..Default::default()
2139            },
2140        );
2141
2142        let tx = TxBuilder::new()
2143            .call(contract, &[])
2144            .nonce(nonce)
2145            .gas_limit(2_000_000)
2146            .build();
2147        let signed_tx = key_pair.sign_tx(tx)?;
2148        let result = evm.transact_commit(TempoTxEnv::from_recovered_tx(&signed_tx, caller))?;
2149        assert!(result.is_success(), "credit-minting prelude should succeed");
2150        assert_eq!(
2151            storage_credit_balance(evm, contract),
2152            credits,
2153            "prelude should mint the requested storage credits"
2154        );
2155
2156        Ok(nonce + 1)
2157    }
2158
2159    fn branching_bytecode_with_tip1060_mode(mode: CreditMode) -> Bytecode {
2160        let mut bytecode = Vec::new();
2161        if mode != CreditMode::Refund {
2162            append_tip1060_set_mode_call(&mut bytecode, mode);
2163        }
2164
2165        let create_only_dest = bytecode.len() + 15;
2166        assert!(create_only_dest <= u8::MAX as usize);
2167
2168        bytecode.extend_from_slice(&[
2169            opcode::CALLDATASIZE,
2170            opcode::PUSH1,
2171            create_only_dest as u8,
2172            opcode::JUMPI,
2173        ]);
2174        bytecode.extend_from_slice(&bytes!("6001600055600060005500"));
2175        bytecode.push(opcode::JUMPDEST);
2176        bytecode.extend_from_slice(&bytes!("600160005500"));
2177        Bytecode::new_raw(bytecode.into())
2178    }
2179
2180    #[test]
2181    fn test_tip1060_storage_credits_delegatecall_rejected() -> eyre::Result<()> {
2182        let calldata = IStorageCredits::setModeCall {
2183            newMode: Mode::Direct,
2184        }
2185        .abi_encode();
2186
2187        for (call_opcode, contract) in [
2188            (opcode::DELEGATECALL, Address::repeat_byte(0x61)),
2189            (opcode::CALLCODE, Address::repeat_byte(0x62)),
2190        ] {
2191            let key_pair = P256KeyPair::random();
2192            let caller = key_pair.address;
2193            let mut bytecode = Vec::new();
2194            for (i, &byte) in calldata.iter().enumerate() {
2195                assert!(i <= u8::MAX as usize);
2196                bytecode.extend_from_slice(&[
2197                    opcode::PUSH1,
2198                    byte,
2199                    opcode::PUSH1,
2200                    i as u8,
2201                    opcode::MSTORE8,
2202                ]);
2203            }
2204
2205            // DELEGATECALL/CALLCODE into the storage credits precompile, then bubble the returned
2206            // custom error.
2207            // PUSH1 0x00 PUSH1 0x00 PUSH1 <argsSize> PUSH1 0x00
2208            bytecode.extend_from_slice(&bytes!("60006000"));
2209            bytecode.extend_from_slice(&[opcode::PUSH1, calldata.len() as u8]);
2210            bytecode.extend_from_slice(&bytes!("6000"));
2211            if call_opcode == opcode::CALLCODE {
2212                // CALLCODE also takes a value argument.
2213                bytecode.extend_from_slice(&bytes!("6000"));
2214            }
2215            bytecode.push(opcode::PUSH20);
2216            bytecode.extend_from_slice(STORAGE_CREDITS_ADDRESS.as_slice());
2217            // PUSH3 0x0f4240 <DELEGATECALL|CALLCODE> POP
2218            bytecode.extend_from_slice(&bytes!("620f4240"));
2219            bytecode.push(call_opcode);
2220            bytecode.push(opcode::POP);
2221            // RETURNDATASIZE PUSH1 0x00 PUSH1 0x00 RETURNDATACOPY RETURNDATASIZE PUSH1 0x00 REVERT
2222            bytecode.extend_from_slice(&bytes!("3d600060003e3d6000fd"));
2223
2224            let mut evm = create_funded_evm_t7(caller);
2225            evm.ctx.db_mut().insert_account_info(
2226                contract,
2227                AccountInfo {
2228                    code: Some(Bytecode::new_raw(bytecode.into())),
2229                    ..Default::default()
2230                },
2231            );
2232
2233            let tx = TxBuilder::new()
2234                .call(contract, &[])
2235                .gas_limit(1_000_000)
2236                .build();
2237            let signed_tx = key_pair.sign_tx(tx)?;
2238            let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
2239
2240            if let ExecutionResult::Revert { output, .. } = evm.transact_commit(tx_env)? {
2241                assert_eq!(
2242                    output.as_ref(),
2243                    DelegateCallNotAllowed {}.abi_encode().as_slice()
2244                );
2245            } else {
2246                panic!("expected DelegateCallNotAllowed revert");
2247            }
2248        }
2249
2250        Ok(())
2251    }
2252
2253    /// TIP-1060: mode changes must not overwrite the pending-refund counter or persist mode state.
2254    ///
2255    /// The tx first creates a slot in default Refund mode, then calls `setMode`. Settlement must
2256    /// read only `pending_refunds`, not confuse the transient mode bits with a credit balance.
2257    #[test]
2258    fn test_tip1060_refund_settlement_uses_pending_field_not_mode() -> eyre::Result<()> {
2259        for mode in [CreditMode::Refund, CreditMode::Preserve, CreditMode::Direct] {
2260            let key_pair = P256KeyPair::random();
2261            let caller = key_pair.address;
2262            let contract = Address::repeat_byte(0x62);
2263
2264            // Bytecode starts with a 0->1 create in Refund mode:
2265            // PUSH1 0x01 PUSH1 0x00 SSTORE  (store 0x01 at slot 0)
2266            let mut bytecode = bytes!("6001600055").to_vec();
2267            append_tip1060_set_mode_call(&mut bytecode, mode);
2268            bytecode.push(opcode::STOP);
2269
2270            let mut evm = create_funded_evm_t7(caller);
2271            evm.ctx.db_mut().insert_account_info(
2272                contract,
2273                AccountInfo {
2274                    code: Some(Bytecode::new_raw(bytecode.into())),
2275                    ..Default::default()
2276                },
2277            );
2278
2279            let tx = TxBuilder::new()
2280                .call(contract, &[])
2281                .gas_limit(2_000_000)
2282                .build();
2283            let signed_tx = key_pair.sign_tx(tx)?;
2284            let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
2285
2286            let result = evm.transact_commit(tx_env)?;
2287            assert!(result.is_success());
2288
2289            assert_eq!(
2290                storage_credit_balance(&evm, contract),
2291                0,
2292                "settlement must not consume the transient mode field as storage credit balance in {mode:?} mode"
2293            );
2294            assert_eq!(
2295                storage_credit_word(&evm, contract),
2296                U256::ZERO,
2297                "mode is transient and must not persist in the storage credit state word in {mode:?} mode"
2298            );
2299        }
2300
2301        Ok(())
2302    }
2303
2304    /// TIP-1060: `setMode` only updates transient state and must not touch persistent credits.
2305    ///
2306    /// A low gas limit proves `setMode` does not pay TIP-1000 storage creation gas. The pre-seeded
2307    /// self-credit sentinel proves TIP-1060 bookkeeping does not recursively consume itself.
2308    #[test]
2309    fn test_tip1060_set_mode_uses_transient_state_only() -> eyre::Result<()> {
2310        let key_pair = P256KeyPair::random();
2311        let caller = key_pair.address;
2312        let mut evm = create_funded_evm_t7(caller);
2313        evm.ctx.db_mut().insert_account_info(
2314            caller,
2315            AccountInfo {
2316                balance: U256::from(DEFAULT_BALANCE),
2317                nonce: 1,
2318                ..Default::default()
2319            },
2320        );
2321
2322        // Sentinel: recursive TIP-1060 accounting would consume this pre-seeded self credit.
2323        seed_storage_credit_balance(&mut evm, STORAGE_CREDITS_ADDRESS, 1);
2324
2325        let calldata = IStorageCredits::setModeCall {
2326            newMode: Mode::Preserve,
2327        }
2328        .abi_encode();
2329
2330        let tx = TxBuilder::new()
2331            .call(STORAGE_CREDITS_ADDRESS, &calldata)
2332            .nonce(1)
2333            // should succeed because setMode is a transient write with no TIP-1000 component.
2334            .gas_limit(50_000)
2335            .build();
2336        let signed_tx = key_pair.sign_tx(tx)?;
2337        let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
2338
2339        let result = evm.transact_commit(tx_env)?;
2340        assert!(
2341            result.is_success(),
2342            "setMode should not need the 250k TIP-1000 storage-creation charge"
2343        );
2344        assert!(
2345            result.tx_gas_used() < 50_000,
2346            "setMode should fit under the low gas limit as a transient write"
2347        );
2348
2349        assert_eq!(
2350            storage_credit_balance(&evm, caller),
2351            0,
2352            "setMode must not mint caller credits"
2353        );
2354        assert_eq!(
2355            storage_credit_word(&evm, caller),
2356            U256::ZERO,
2357            "setMode must not create or update persistent caller state"
2358        );
2359
2360        // Sentinel: setMode must not consume the precompile's own pre-seeded credit.
2361        assert_eq!(
2362            storage_credit_balance(&evm, STORAGE_CREDITS_ADDRESS),
2363            1,
2364            "storage-credits bookkeeping must not recursively consume its own storage credits"
2365        );
2366
2367        Ok(())
2368    }
2369
2370    /// TIP-1060: clearing a nonzero slot mints one credit and disables legacy SSTORE refunds.
2371    ///
2372    /// Slot 0 is seeded non-zero before the tx. The clear should mint a storage credit instead of
2373    /// using the legacy EVM refund counter.
2374    #[test]
2375    fn test_tip1060_sstore_clear_mints_storage_credit_without_legacy_refund() -> eyre::Result<()> {
2376        // PUSH1 0x00 PUSH1 0x00 SSTORE STOP: clear slot 0.
2377        let clear_bytecode = Bytecode::new_raw(bytes!("600060005500"));
2378
2379        let key_pair = P256KeyPair::random();
2380        let caller = key_pair.address;
2381        let contract = Address::repeat_byte(0x63);
2382
2383        let mut evm = create_funded_evm_t7(caller);
2384        evm.ctx.db_mut().insert_account_info(
2385            contract,
2386            AccountInfo {
2387                code: Some(clear_bytecode),
2388                ..Default::default()
2389            },
2390        );
2391        evm.ctx
2392            .db_mut()
2393            .insert_account_storage(contract, U256::ZERO, U256::ONE)?;
2394
2395        let tx = TxBuilder::new()
2396            .call(contract, &[])
2397            .gas_limit(2_000_000)
2398            .build();
2399        let signed_tx = key_pair.sign_tx(tx)?;
2400        let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
2401
2402        let result = evm.transact_commit(tx_env)?;
2403        assert!(result.is_success(), "clear tx should succeed");
2404        assert_eq!(
2405            result.gas().inner_refunded(),
2406            0,
2407            "TIP-1060 removes the legacy SSTORE clearing refund"
2408        );
2409        assert_eq!(
2410            storage_credit_balance(&evm, contract),
2411            1,
2412            "clearing a nonzero slot should mint one storage credit"
2413        );
2414
2415        Ok(())
2416    }
2417
2418    /// TIP-1060: `0->1->0` pays creation cost, then mints/settles according to mode.
2419    ///
2420    /// Preserve/Direct keep the clear-minted credit. Refund consumes it against the deferred
2421    /// creation refund at end-of-tx.
2422    #[test]
2423    fn test_tip1060_sstore_create_then_clear_modes() -> eyre::Result<()> {
2424        // PUSH1 0x00 PUSH1 0x00 SSTORE STOP: no-op write to an empty slot.
2425        let noop_body = bytes!("600060005500");
2426        let (noop_gas, _) =
2427            run_tx_on_tip1060_contract(CreditMode::Refund, Address::repeat_byte(0x60), &noop_body)?;
2428
2429        // PUSH1 0x01 PUSH1 0x00 SSTORE  PUSH1 0x00 PUSH1 0x00 SSTORE  STOP
2430        let create_clear_body = bytes!("6001600055600060005500");
2431
2432        // (mode, gas used, final credit balance).
2433        // The 0->1->0 clear restores slot 0 to its transaction-original value (0), so T7 refunds
2434        // only the 5k residual set charge. The 245k credit depends on the TIP-1060 credit mode.
2435        let cases = [
2436            (CreditMode::Refund, 285_968u64, 0u64),
2437            (CreditMode::Preserve, 534_133u64, 1u64),
2438            (CreditMode::Direct, 534_133u64, 1u64),
2439        ];
2440
2441        for (case_id, (mode, expected_gas, expected_balance)) in cases.into_iter().enumerate() {
2442            let contract = Address::repeat_byte(0x60 + case_id as u8);
2443            let (gas_used, evm) = run_tx_on_tip1060_contract(mode, contract, &create_clear_body)?;
2444
2445            assert!(
2446                gas_used >= noop_gas,
2447                "0->x->0 storage churn must not reduce tx gas below no-op: noop={noop_gas}, create_clear={gas_used}"
2448            );
2449
2450            assert_eq!(
2451                gas_used, expected_gas,
2452                "TIP-1060 create+clear gas should be exact in {mode:?} mode"
2453            );
2454
2455            assert_eq!(
2456                storage_credit_balance(&evm, contract),
2457                expected_balance,
2458                "TIP-1060 post-tx storage credit balance should be exact in {mode:?} mode"
2459            );
2460        }
2461
2462        Ok(())
2463    }
2464
2465    /// TIP-1060: the spec transition table classifies every zero-crossing exactly once.
2466    #[test]
2467    fn test_tip1060_spec_transition_classes_credit_accounting_table() -> eyre::Result<()> {
2468        fn append_sstore(body: &mut Vec<u8>, slot: u8, value: u8) {
2469            body.extend_from_slice(&[opcode::PUSH1, value, opcode::PUSH1, slot, opcode::SSTORE]);
2470        }
2471
2472        fn transition_body(setup_value: Option<u8>, new_value: u8) -> Bytes {
2473            let mut body = Vec::new();
2474            if let Some(value) = setup_value {
2475                append_sstore(&mut body, 0, value);
2476            }
2477            append_sstore(&mut body, 0, new_value);
2478            body.push(opcode::STOP);
2479            body.into()
2480        }
2481
2482        #[derive(Clone, Copy)]
2483        struct ModeExpectation {
2484            mode: CreditMode,
2485            expected_gas: u64,
2486            expected_credits: u64,
2487        }
2488
2489        struct CreditCase {
2490            name: &'static str,
2491            initial_credits: u64,
2492            expectations: [ModeExpectation; 3],
2493        }
2494
2495        struct TransitionClass {
2496            name: &'static str,
2497            original: u8,
2498            setup_value: Option<u8>,
2499            new_value: u8,
2500            expected_slot: u64,
2501            credit_cases: Vec<CreditCase>,
2502        }
2503
2504        fn expectations(
2505            refund: (u64, u64),
2506            preserve: (u64, u64),
2507            direct: (u64, u64),
2508        ) -> [ModeExpectation; 3] {
2509            [
2510                ModeExpectation {
2511                    mode: CreditMode::Refund,
2512                    expected_gas: refund.0,
2513                    expected_credits: refund.1,
2514                },
2515                ModeExpectation {
2516                    mode: CreditMode::Preserve,
2517                    expected_gas: preserve.0,
2518                    expected_credits: preserve.1,
2519                },
2520                ModeExpectation {
2521                    mode: CreditMode::Direct,
2522                    expected_gas: direct.0,
2523                    expected_credits: direct.1,
2524                },
2525            ]
2526        }
2527
2528        const SET_MODE_GAS: u64 = 3_165;
2529
2530        fn same_storage_accounting(refund_gas: u64, expected_credits: u64) -> [ModeExpectation; 3] {
2531            expectations(
2532                (refund_gas, expected_credits),
2533                (refund_gas + SET_MODE_GAS, expected_credits),
2534                (refund_gas + SET_MODE_GAS, expected_credits),
2535            )
2536        }
2537
2538        fn no_initial_credits(expectations: [ModeExpectation; 3]) -> Vec<CreditCase> {
2539            vec![CreditCase {
2540                name: "no initial credits",
2541                initial_credits: 0,
2542                expectations,
2543            }]
2544        }
2545
2546        let transition_classes = [
2547            TransitionClass {
2548                name: "O=0 P=0 N=0 no-op",
2549                original: 0,
2550                setup_value: None,
2551                new_value: 0,
2552                expected_slot: 0,
2553                credit_cases: no_initial_credits(same_storage_accounting(30_862, 0)),
2554            },
2555            TransitionClass {
2556                name: "O=0 P=Y N=Y same-tx no-op",
2557                original: 0,
2558                setup_value: Some(2),
2559                new_value: 2,
2560                expected_slot: 2,
2561                credit_cases: no_initial_credits(same_storage_accounting(283_068, 0)),
2562            },
2563            TransitionClass {
2564                name: "O=0 P=Y N=Z dirty overwrite",
2565                original: 0,
2566                setup_value: Some(2),
2567                new_value: 3,
2568                expected_slot: 3,
2569                credit_cases: no_initial_credits(same_storage_accounting(283_068, 0)),
2570            },
2571            TransitionClass {
2572                name: "O=X P=X N=X no-op",
2573                original: 1,
2574                setup_value: None,
2575                new_value: 1,
2576                expected_slot: 1,
2577                credit_cases: no_initial_credits(same_storage_accounting(30_862, 0)),
2578            },
2579            TransitionClass {
2580                name: "O=X P=X N=Y clean overwrite",
2581                original: 1,
2582                setup_value: None,
2583                new_value: 2,
2584                expected_slot: 2,
2585                credit_cases: no_initial_credits(same_storage_accounting(33_662, 0)),
2586            },
2587            TransitionClass {
2588                name: "O=X P=Y N=X dirty restore to original",
2589                original: 1,
2590                setup_value: Some(2),
2591                new_value: 1,
2592                expected_slot: 1,
2593                credit_cases: no_initial_credits(same_storage_accounting(30_968, 0)),
2594            },
2595            TransitionClass {
2596                name: "O=X P=Y N=Z dirty overwrite",
2597                original: 1,
2598                setup_value: Some(2),
2599                new_value: 3,
2600                expected_slot: 3,
2601                credit_cases: no_initial_credits(same_storage_accounting(33_768, 0)),
2602            },
2603            TransitionClass {
2604                name: "O=0 P=Y N=0 clear of same-tx creation",
2605                original: 0,
2606                setup_value: Some(2),
2607                new_value: 0,
2608                expected_slot: 0,
2609                credit_cases: no_initial_credits(expectations(
2610                    (35_968, 0),
2611                    (284_133, 1),
2612                    (284_133, 1),
2613                )),
2614            },
2615            TransitionClass {
2616                name: "O=X P=X N=0 clean clear",
2617                original: 1,
2618                setup_value: None,
2619                new_value: 0,
2620                expected_slot: 0,
2621                credit_cases: no_initial_credits(same_storage_accounting(38_562, 1)),
2622            },
2623            TransitionClass {
2624                name: "O=X P=Y N=0 dirty clear",
2625                original: 1,
2626                setup_value: Some(2),
2627                new_value: 0,
2628                expected_slot: 0,
2629                credit_cases: no_initial_credits(same_storage_accounting(38_668, 1)),
2630            },
2631            TransitionClass {
2632                name: "O=0 P=0 N=Y clean creation",
2633                original: 0,
2634                setup_value: None,
2635                new_value: 2,
2636                expected_slot: 2,
2637                credit_cases: vec![
2638                    CreditCase {
2639                        name: "no initial credits",
2640                        initial_credits: 0,
2641                        expectations: same_storage_accounting(282_962, 0),
2642                    },
2643                    CreditCase {
2644                        name: "one initial credit",
2645                        initial_credits: 1,
2646                        expectations: expectations(
2647                            (37_962, 0),
2648                            (37_962 + SET_MODE_GAS + STORAGE_CREDIT_VALUE, 1),
2649                            (43_927, 0),
2650                        ),
2651                    },
2652                    CreditCase {
2653                        name: "surplus initial credits",
2654                        initial_credits: 2,
2655                        expectations: expectations(
2656                            (37_962, 1),
2657                            (37_962 + SET_MODE_GAS + STORAGE_CREDIT_VALUE, 2),
2658                            (43_927, 1),
2659                        ),
2660                    },
2661                ],
2662            },
2663            TransitionClass {
2664                name: "O=X P=0 N=X recreation to original",
2665                original: 1,
2666                setup_value: Some(0),
2667                new_value: 1,
2668                expected_slot: 1,
2669                credit_cases: vec![
2670                    CreditCase {
2671                        name: "no initial credits",
2672                        initial_credits: 0,
2673                        expectations: expectations(
2674                            (35_968, 0),
2675                            (35_968 + SET_MODE_GAS + STORAGE_CREDIT_VALUE, 1),
2676                            (35_968 + SET_MODE_GAS, 0),
2677                        ),
2678                    },
2679                    CreditCase {
2680                        name: "surplus initial credits",
2681                        initial_credits: 2,
2682                        expectations: expectations(
2683                            (35_968, 2),
2684                            (35_968 + SET_MODE_GAS + STORAGE_CREDIT_VALUE, 3),
2685                            (35_968 + SET_MODE_GAS, 2),
2686                        ),
2687                    },
2688                ],
2689            },
2690            TransitionClass {
2691                name: "O=X P=0 N=Y recreation to other value",
2692                original: 1,
2693                setup_value: Some(0),
2694                new_value: 2,
2695                expected_slot: 2,
2696                credit_cases: vec![
2697                    CreditCase {
2698                        name: "no initial credits",
2699                        initial_credits: 0,
2700                        expectations: expectations(
2701                            (38_768, 0),
2702                            (38_768 + SET_MODE_GAS + STORAGE_CREDIT_VALUE, 1),
2703                            (38_768 + SET_MODE_GAS, 0),
2704                        ),
2705                    },
2706                    CreditCase {
2707                        name: "surplus initial credits",
2708                        initial_credits: 2,
2709                        expectations: expectations(
2710                            (38_768, 2),
2711                            (38_768 + SET_MODE_GAS + STORAGE_CREDIT_VALUE, 3),
2712                            (38_768 + SET_MODE_GAS, 2),
2713                        ),
2714                    },
2715                ],
2716            },
2717        ];
2718
2719        let mut case_id = 0u8;
2720        for scenario in transition_classes {
2721            for credit_case in &scenario.credit_cases {
2722                for expectation in credit_case.expectations {
2723                    let mode = expectation.mode;
2724                    let key_pair = P256KeyPair::random();
2725                    let caller = key_pair.address;
2726                    let contract = Address::repeat_byte(0x80 + case_id);
2727                    case_id += 1;
2728                    let body = transition_body(scenario.setup_value, scenario.new_value);
2729
2730                    let mut evm = create_funded_evm_t7(caller);
2731                    fund_account_with_nonce(&mut evm, caller, 1);
2732                    if scenario.original != 0 {
2733                        evm.ctx.db_mut().insert_account_storage(
2734                            contract,
2735                            U256::ZERO,
2736                            U256::from(scenario.original),
2737                        )?;
2738                    }
2739                    let nonce = mint_storage_credits_with_clears(
2740                        &mut evm,
2741                        &key_pair,
2742                        caller,
2743                        contract,
2744                        1,
2745                        credit_case.initial_credits,
2746                    )?;
2747
2748                    evm.ctx.db_mut().insert_account_info(
2749                        contract,
2750                        AccountInfo {
2751                            code: Some(bytecode_with_tip1060_mode(mode, &body)),
2752                            ..Default::default()
2753                        },
2754                    );
2755
2756                    let tx = TxBuilder::new()
2757                        .call(contract, &[])
2758                        .nonce(nonce)
2759                        .gas_limit(2_000_000)
2760                        .build();
2761                    let signed_tx = key_pair.sign_tx(tx)?;
2762                    let result =
2763                        evm.transact_commit(TempoTxEnv::from_recovered_tx(&signed_tx, caller))?;
2764                    assert!(
2765                        result.is_success(),
2766                        "{} / {} in {mode:?} should succeed",
2767                        scenario.name,
2768                        credit_case.name
2769                    );
2770
2771                    assert_eq!(
2772                        result.tx_gas_used(),
2773                        expectation.expected_gas,
2774                        "{} / {} gas should stay exact in {mode:?}",
2775                        scenario.name,
2776                        credit_case.name
2777                    );
2778                    assert_eq!(
2779                        storage_credit_balance(&evm, contract),
2780                        expectation.expected_credits,
2781                        "{} / {} final persistent credit balance should stay exact in {mode:?}",
2782                        scenario.name,
2783                        credit_case.name
2784                    );
2785                    assert_eq!(
2786                        evm.ctx.db().storage_ref(contract, U256::ZERO)?,
2787                        U256::from(scenario.expected_slot),
2788                        "{} / {} should leave the expected slot value in {mode:?}",
2789                        scenario.name,
2790                        credit_case.name
2791                    );
2792                }
2793            }
2794        }
2795
2796        Ok(())
2797    }
2798
2799    /// TIP-1060: Preserve churn of a pre-existing slot cannot subsidize fresh Direct creates.
2800    ///
2801    /// Each clear mints a credit, but in Preserve mode the matching recreation pays the 245k
2802    /// creditable portion as gas, so accumulating churned credits costs full price per credit and
2803    /// cannot fund cheaper Direct creates. Here the 500-cycle Preserve churn exhausts gas (each
2804    /// recreation costs 245k) before the Direct phase runs, so the transaction reverts.
2805    #[test]
2806    fn test_tip1060_preserve_churn_attack() -> eyre::Result<()> {
2807        use alloy_primitives::{Address, Bytes, TxKind, U256, hex};
2808        use revm::{
2809            Context, Database, ExecuteCommitEvm, MainContext,
2810            context::{CfgEnv, TxEnv},
2811            database::{CacheDB, EmptyDB},
2812            state::AccountInfo,
2813        };
2814        use tempo_chainspec::hardfork::TempoHardfork;
2815        use tempo_precompiles::{STORAGE_CREDITS_ADDRESS, storage_credits::StorageCredits};
2816
2817        use crate::{TempoBlockEnv, TempoEvm, gas_params::tempo_gas_params};
2818
2819        // Initcode:
2820        //   constructor: SSTORE(0, 1)
2821        //   runtime: setMode(Preserve); 500x clear/restore slot0; setMode(Direct); 500x create
2822        // Selector 0x21175b4a = setMode(uint8); precompile = 0x1060...0000.
2823        let init = Bytes::from(
2824            hex!(
2825                "60016000556100a660136000396100a66000f3\
2826                60216000536017600153605b600253604a6003536001602353\
2827                6000600060246000600073106000000000000000000000000000000000000\
2828                05af1506101f45b60006000556002600055600190038061003e5750\
2829                60216000536017600153605b600253604a6003536002602353\
2830                6000600060246000600073106000000000000000000000000000000000000\
2831                05af1506101f45b8061010001600190556001900380610091575000"
2832            )
2833            .to_vec(),
2834        );
2835
2836        let caller = Address::repeat_byte(0x11);
2837        let mut cfg = CfgEnv::<TempoHardfork>::default();
2838        cfg.spec = TempoHardfork::T7;
2839        cfg.gas_params = tempo_gas_params(TempoHardfork::T7);
2840        const GAS_LIMIT: u64 = 16_777_216;
2841        let mut block = TempoBlockEnv::default();
2842        block.inner.gas_limit = GAS_LIMIT;
2843        let ctx = Context::mainnet()
2844            .with_db(CacheDB::new(EmptyDB::new()))
2845            .with_block(block)
2846            .with_cfg(cfg)
2847            .with_tx(Default::default());
2848        let mut evm = TempoEvm::new(ctx, ());
2849        evm.ctx.db_mut().insert_account_info(
2850            caller,
2851            AccountInfo {
2852                balance: U256::from(1_000_000_000_000_000_000u128),
2853                ..Default::default()
2854            },
2855        );
2856
2857        // tx#1: deploy; constructor pays the one-time bootstrap creation.
2858        let deploy = evm.transact_commit(
2859            TxEnv {
2860                caller,
2861                kind: TxKind::Create,
2862                data: init,
2863                gas_limit: GAS_LIMIT,
2864                ..Default::default()
2865            }
2866            .into(),
2867        )?;
2868        assert!(deploy.is_success(), "deploy reverted/halted: {deploy:?}");
2869        let contract = deploy
2870            .created_address()
2871            .expect("CREATE should yield an address");
2872
2873        // tx#2: churn in Preserve, then try to spend credits in Direct.
2874        let call = evm.transact_commit(
2875            TxEnv {
2876                caller,
2877                nonce: 1,
2878                kind: TxKind::Call(contract),
2879                gas_limit: GAS_LIMIT,
2880                ..Default::default()
2881            }
2882            .into(),
2883        )?;
2884
2885        let balance = evm
2886            .ctx
2887            .db_mut()
2888            .storage(STORAGE_CREDITS_ADDRESS, StorageCredits::slot(contract))?
2889            .as_limbs()[0];
2890        let slots = evm
2891            .ctx
2892            .db_mut()
2893            .cache
2894            .accounts
2895            .get(&contract)
2896            .map(|a| a.storage.iter().filter(|(_, v)| !v.is_zero()).count())
2897            .unwrap_or(0);
2898        eprintln!(
2899            "tx#2 success/gas: {}/{}  slots: {slots}  bal: {balance}",
2900            call.is_success(),
2901            call.tx_gas_used()
2902        );
2903
2904        // Each Preserve recreation costs the full 245k creditable portion, so the churn loop
2905        // exhausts gas and reverts before any churned credit can subsidize a Direct create.
2906        assert!(!call.is_success());
2907        assert_eq!(slots, 1);
2908        assert_eq!(balance, 0);
2909        Ok(())
2910    }
2911
2912    /// TIP-1060: Preserve clear+restore churn mints one credit per clear.
2913    ///
2914    /// Slot 0 starts non-zero. TIP-1060 keys minting on the present -> new transition, so every
2915    /// `nonzero -> 0` clear mints a credit regardless of the slot's original value. In Preserve
2916    /// mode the matching `0 -> nonzero` recreation pays the 245k creditable portion as gas and does
2917    /// not consume a credit, so three churn cycles accumulate three credits — each paid for in full,
2918    /// so the balance growth is not a subsidy.
2919    #[test]
2920    fn test_tip1060_preserve_churn_mints_one_credit_per_clear() -> eyre::Result<()> {
2921        let key_pair = P256KeyPair::random();
2922        let caller = key_pair.address;
2923        let contract = Address::repeat_byte(0x6c);
2924
2925        // Bytecode body: (SSTORE(0, 0); SSTORE(0, 2)) x 3; STOP.
2926        let mut body = Vec::new();
2927        for _ in 0..3 {
2928            body.extend_from_slice(&bytes!("6000600055")); // SSTORE(0, 0)  clear
2929            body.extend_from_slice(&bytes!("6002600055")); // SSTORE(0, 2)  dirty restore
2930        }
2931        body.push(opcode::STOP);
2932
2933        let mut evm = create_funded_evm_t7(caller);
2934        evm.ctx.db_mut().insert_account_info(
2935            contract,
2936            AccountInfo {
2937                code: Some(bytecode_with_tip1060_mode(CreditMode::Preserve, &body)),
2938                ..Default::default()
2939            },
2940        );
2941        // Slot 0 starts non-zero.
2942        evm.ctx
2943            .db_mut()
2944            .insert_account_storage(contract, U256::ZERO, U256::from(1))
2945            .unwrap();
2946        seed_storage_credit_balance(&mut evm, contract, 0);
2947
2948        let tx = TxBuilder::new()
2949            .call(contract, &[])
2950            .gas_limit(2_000_000)
2951            .build();
2952        let result = evm.transact_commit(TempoTxEnv::from_recovered_tx(
2953            &key_pair.sign_tx(tx)?,
2954            caller,
2955        ))?;
2956        assert!(result.is_success(), "preserve churn tx should succeed");
2957        assert_eq!(
2958            result.tx_gas_used(),
2959            1_027_757,
2960            "three Preserve churn cycles pay the full 245k creditable portion per recreation"
2961        );
2962
2963        assert_eq!(
2964            storage_credit_balance(&evm, contract),
2965            3,
2966            "each clear mints a credit and Preserve recreations pay 245k without consuming, so \
2967             three churn cycles accumulate three credits"
2968        );
2969        Ok(())
2970    }
2971
2972    /// TIP-1060: if a churn credit is spent before restore, the restore repays its value.
2973    ///
2974    /// This exercises the reordered case: clear slot 0, spend the credit on a fresh Direct create,
2975    /// then restore slot 0. With no credit left to burn, the dirty restore must charge repayment.
2976    #[test]
2977    fn test_tip1060_dirty_restore_after_direct_spend_repays_credit_value() -> eyre::Result<()> {
2978        let key_pair = P256KeyPair::random();
2979        let caller = key_pair.address;
2980        let contract = Address::repeat_byte(0x6d);
2981
2982        // Bytecode body: SSTORE(0,0); SSTORE(1,1); SSTORE(0,2); STOP.
2983        let mut body = Vec::new();
2984        body.extend_from_slice(&bytes!("6000600055")); // SSTORE(0, 0)  clear, mints
2985        body.extend_from_slice(&bytes!("6001600155")); // SSTORE(1, 1)  fresh create, Direct spend
2986        body.extend_from_slice(&bytes!("6002600055")); // SSTORE(0, 2)  dirty restore, repays
2987        body.push(opcode::STOP);
2988
2989        let mut evm = create_funded_evm_t7(caller);
2990        evm.ctx.db_mut().insert_account_info(
2991            contract,
2992            AccountInfo {
2993                code: Some(bytecode_with_tip1060_mode(CreditMode::Direct, &body)),
2994                ..Default::default()
2995            },
2996        );
2997        evm.ctx
2998            .db_mut()
2999            .insert_account_storage(contract, U256::ZERO, U256::from(1))
3000            .unwrap();
3001        seed_storage_credit_balance(&mut evm, contract, 0);
3002
3003        let tx = TxBuilder::new()
3004            .call(contract, &[])
3005            .gas_limit(2_000_000)
3006            .build();
3007        let result = evm.transact_commit(TempoTxEnv::from_recovered_tx(
3008            &key_pair.sign_tx(tx)?,
3009            caller,
3010        ))?;
3011        assert!(result.is_success(), "direct reorder tx should succeed");
3012
3013        // The fresh slot exists, the original slot is restored, and the credit balance nets to zero.
3014        assert_eq!(
3015            evm.ctx.db().storage_ref(contract, U256::from(1)).unwrap(),
3016            U256::from(1),
3017            "the genuinely new slot must be created"
3018        );
3019        assert_eq!(
3020            evm.ctx.db().storage_ref(contract, U256::ZERO).unwrap(),
3021            U256::from(2),
3022            "the churned slot must be restored"
3023        );
3024        assert_eq!(
3025            storage_credit_balance(&evm, contract),
3026            0,
3027            "balance must net to zero after mint + Direct spend + dirty-restore repay"
3028        );
3029        assert!(
3030            result.tx_gas_used() > STORAGE_CREDIT_VALUE,
3031            "the dirty restore must repay the {STORAGE_CREDIT_VALUE} credit value, so the new slot \
3032             costs full price; got {} gas",
3033            result.tx_gas_used()
3034        );
3035        Ok(())
3036    }
3037
3038    /// TIP-1060: credits minted in one tx affect later creates according to the selected mode.
3039    ///
3040    /// Empty calldata runs create+clear to mint a credit. Non-empty calldata runs create-only;
3041    /// Direct spends the prior credit, while Preserve/Refund pay the full creation path.
3042    #[test]
3043    fn test_tip1060_minted_storage_credits_affect_second_tx() -> eyre::Result<()> {
3044        // Branching bytecode: empty calldata -> create+clear; non-empty -> create-only.
3045        // (mode, tx2 gas, balance after tx1/tx2).
3046        let cases = [
3047            (CreditMode::Refund, 282_994u64, 0u64, 0u64),
3048            (CreditMode::Preserve, 286_159u64, 1u64, 1u64),
3049            (CreditMode::Direct, 43_959u64, 1u64, 0u64),
3050        ];
3051
3052        for (mode, expected_second_gas, expected_credit_tx1, expected_credit_tx2) in cases {
3053            let key_pair = P256KeyPair::random();
3054            let caller = key_pair.address;
3055            let contract = Address::repeat_byte(0x61);
3056
3057            let mut evm = create_funded_evm_t7(caller);
3058
3059            evm.ctx.db_mut().insert_account_info(
3060                contract,
3061                AccountInfo {
3062                    code: Some(branching_bytecode_with_tip1060_mode(mode)),
3063                    ..Default::default()
3064                },
3065            );
3066            seed_storage_credit_balance(&mut evm, contract, 0);
3067
3068            // First tx: create+clear to mint.
3069            let tx1 = TxBuilder::new()
3070                .call(contract, &[])
3071                .nonce(0)
3072                .gas_limit(2_000_000)
3073                .build();
3074            let signed_tx1 = key_pair.sign_tx(tx1)?;
3075            let tx_env1 = TempoTxEnv::from_recovered_tx(&signed_tx1, caller);
3076            let result1 = evm.transact_commit(tx_env1)?;
3077            assert!(
3078                result1.is_success(),
3079                "minting tx should succeed in {mode:?} mode"
3080            );
3081            assert_eq!(
3082                storage_credit_balance(&evm, contract),
3083                expected_credit_tx1,
3084                "storage credit balance after the minting tx should be exact in {mode:?} mode"
3085            );
3086
3087            // Second transaction: create one slot; only Direct spends the prior credit.
3088            let tx2 = TxBuilder::new()
3089                .call(contract, &[0x01])
3090                .nonce(1)
3091                .gas_limit(2_000_000)
3092                .build();
3093            let signed_tx2 = key_pair.sign_tx(tx2)?;
3094            let tx_env2 = TempoTxEnv::from_recovered_tx(&signed_tx2, caller);
3095            let result2 = evm.transact_commit(tx_env2)?;
3096            assert!(
3097                result2.is_success(),
3098                "create-only tx should succeed in {mode:?} mode"
3099            );
3100
3101            let second_gas = result2.tx_gas_used();
3102            assert_eq!(
3103                second_gas, expected_second_gas,
3104                "TIP-1060 second-tx create gas should be exact in {mode:?} mode"
3105            );
3106
3107            assert_eq!(
3108                storage_credit_balance(&evm, contract),
3109                expected_credit_tx2,
3110                "storage credit balance after the create-only tx should be exact in {mode:?} mode"
3111            );
3112        }
3113
3114        Ok(())
3115    }
3116
3117    /// TIP-1060: `setBudget(n)` caps Direct credit spending.
3118    ///
3119    /// Both contracts start with two credits and create two slots. `setBudget(1)` should discount
3120    /// only the first create; after the budget is exhausted, Direct stays selected but charges like
3121    /// Preserve. Plain `setMode(Direct)` has unlimited budget and discounts both.
3122    #[test]
3123    fn test_tip1060_direct_budget_caps_credit_consumption() -> eyre::Result<()> {
3124        let key_pair = P256KeyPair::random();
3125        let caller = key_pair.address;
3126        let budgeted_contract = Address::repeat_byte(0x86);
3127        let unlimited_contract = Address::repeat_byte(0x87);
3128
3129        let mut budgeted_bytecode = Vec::new();
3130        let set_budget_input = IStorageCredits::setBudgetCall { creditBudget: 1 }.abi_encode();
3131        append_tip1060_precompile_call(&mut budgeted_bytecode, &set_budget_input);
3132        budgeted_bytecode.extend_from_slice(&bytes!("6001600055600160015500"));
3133
3134        let unlimited_bytecode =
3135            bytecode_with_tip1060_mode(CreditMode::Direct, &bytes!("6001600055600160015500"));
3136
3137        let mut evm = create_funded_evm_t7(caller);
3138        evm.ctx.db_mut().insert_account_info(
3139            budgeted_contract,
3140            AccountInfo {
3141                code: Some(Bytecode::new_raw(budgeted_bytecode.into())),
3142                ..Default::default()
3143            },
3144        );
3145        evm.ctx.db_mut().insert_account_info(
3146            unlimited_contract,
3147            AccountInfo {
3148                code: Some(unlimited_bytecode),
3149                ..Default::default()
3150            },
3151        );
3152        seed_storage_credit_balance(&mut evm, budgeted_contract, 2);
3153        seed_storage_credit_balance(&mut evm, unlimited_contract, 2);
3154
3155        let budgeted_tx = TxBuilder::new()
3156            .call(budgeted_contract, &[])
3157            .nonce(0)
3158            .gas_limit(2_000_000)
3159            .build();
3160        let budgeted_result = evm.transact_commit(TempoTxEnv::from_recovered_tx(
3161            &key_pair.sign_tx(budgeted_tx)?,
3162            caller,
3163        ))?;
3164        assert!(budgeted_result.is_success());
3165        assert_eq!(
3166            storage_credit_balance(&evm, budgeted_contract),
3167            1,
3168            "budget 1 must consume exactly one of the two available credits"
3169        );
3170
3171        let unlimited_tx = TxBuilder::new()
3172            .call(unlimited_contract, &[])
3173            .nonce(1)
3174            .gas_limit(2_000_000)
3175            .build();
3176        let unlimited_result = evm.transact_commit(TempoTxEnv::from_recovered_tx(
3177            &key_pair.sign_tx(unlimited_tx)?,
3178            caller,
3179        ))?;
3180        assert!(unlimited_result.is_success());
3181        assert_eq!(
3182            storage_credit_balance(&evm, unlimited_contract),
3183            0,
3184            "setMode(Direct) has unlimited budget and must consume both available credits"
3185        );
3186        assert!(
3187            budgeted_result.tx_gas_used() > unlimited_result.tx_gas_used(),
3188            "the budgeted second create should pay full creation gas after the Direct budget is exhausted"
3189        );
3190
3191        Ok(())
3192    }
3193
3194    #[test]
3195    fn test_tip1060_exhausted_direct_budget_stays_direct() -> eyre::Result<()> {
3196        const MODE_SLOT: u8 = 3;
3197        const EXHAUSTED_GAS: (u8, u8) = (4, 5);
3198        const AFTER_CLEAR_GAS: (u8, u8) = (6, 7);
3199
3200        let store_gas = |bytecode: &mut Vec<u8>, slot: u8| {
3201            bytecode.extend_from_slice(&[opcode::GAS, opcode::PUSH1, slot, opcode::SSTORE]);
3202        };
3203        let contract = Address::repeat_byte(0x88);
3204        let mut bytecode = Vec::new();
3205
3206        append_tip1060_precompile_call(
3207            &mut bytecode,
3208            &IStorageCredits::setBudgetCall { creditBudget: 1 }.abi_encode(),
3209        );
3210        bytecode.extend_from_slice(&bytes!("6001600055")); // budgeted create
3211        append_tip1060_precompile_call_store_return(
3212            &mut bytecode,
3213            &IStorageCredits::modeOfCall { account: contract }.abi_encode(),
3214            Some(MODE_SLOT),
3215        );
3216        store_gas(&mut bytecode, EXHAUSTED_GAS.0);
3217        bytecode.extend_from_slice(&bytes!("6001600155")); // create while exhausted
3218        store_gas(&mut bytecode, EXHAUSTED_GAS.1);
3219        bytecode.extend_from_slice(&bytes!("6000600155")); // clear: mint credit, not budget
3220        store_gas(&mut bytecode, AFTER_CLEAR_GAS.0);
3221        bytecode.extend_from_slice(&bytes!("6001600255")); // still exhausted after clear
3222        store_gas(&mut bytecode, AFTER_CLEAR_GAS.1);
3223        bytecode.push(opcode::STOP);
3224
3225        let (_, evm) = run_tx_on_tip1060_contract_with_setup(
3226            CreditMode::Refund,
3227            contract,
3228            &bytecode,
3229            |evm, contract| {
3230                seed_storage_credit_balance(evm, contract, 2);
3231                for slot in MODE_SLOT..=AFTER_CLEAR_GAS.1 {
3232                    evm.ctx.db_mut().insert_account_storage(
3233                        contract,
3234                        U256::from(slot),
3235                        U256::ONE,
3236                    )?;
3237                }
3238                Ok(())
3239            },
3240        )?;
3241
3242        let word = |slot: u8| {
3243            evm.ctx
3244                .db()
3245                .storage_ref(contract, U256::from(slot))
3246                .unwrap()
3247        };
3248        let delta = |slots: (u8, u8)| word(slots.0).as_limbs()[0] - word(slots.1).as_limbs()[0];
3249
3250        assert!(
3251            delta(EXHAUSTED_GAS) > STORAGE_CREDIT_VALUE
3252                && delta(AFTER_CLEAR_GAS) > STORAGE_CREDIT_VALUE,
3253            "both exhausted creates must pay full creditable gas"
3254        );
3255        let expected = (U256::from(CreditMode::Direct as u8), U256::ZERO, U256::ONE);
3256        assert_eq!((word(MODE_SLOT), word(1), word(2)), expected);
3257        assert_eq!(storage_credit_balance(&evm, contract), 2);
3258
3259        Ok(())
3260    }
3261
3262    #[test]
3263    fn test_tip1060_reverted_scopes_unwind_credit_accounting() -> eyre::Result<()> {
3264        struct RevertedCase {
3265            name: &'static str,
3266            callee: Address,
3267            bytecode: Bytecode,
3268            initial_slot: U256,
3269            initial_credits: u64,
3270            expected_slot: U256,
3271            expected_credits: u64,
3272        }
3273
3274        fn caller_ignoring_reverted_callee(callee: Address) -> Bytecode {
3275            let mut bytecode = bytes!("60006000600060006000").to_vec();
3276            bytecode.push(opcode::PUSH20);
3277            bytecode.extend_from_slice(callee.as_slice());
3278            bytecode.extend_from_slice(&bytes!("620f4240f15000"));
3279            Bytecode::new_raw(bytecode.into())
3280        }
3281
3282        let mut direct_create_revert = Vec::new();
3283        append_tip1060_set_mode_call(&mut direct_create_revert, CreditMode::Direct);
3284        direct_create_revert.extend_from_slice(&bytes!("600160005560006000fd"));
3285
3286        let cases = [
3287            RevertedCase {
3288                name: "clear mint",
3289                callee: Address::repeat_byte(0x89),
3290                // SSTORE(0, 0); REVERT(0, 0)
3291                bytecode: Bytecode::new_raw(bytes!("600060005560006000fd")),
3292                initial_slot: U256::ONE,
3293                initial_credits: 0,
3294                expected_slot: U256::ONE,
3295                expected_credits: 0,
3296            },
3297            RevertedCase {
3298                name: "Refund pending creation",
3299                callee: Address::repeat_byte(0x8a),
3300                // SSTORE(0, 1); REVERT(0, 0)
3301                bytecode: Bytecode::new_raw(bytes!("600160005560006000fd")),
3302                initial_slot: U256::ZERO,
3303                initial_credits: 1,
3304                expected_slot: U256::ZERO,
3305                expected_credits: 1,
3306            },
3307            RevertedCase {
3308                name: "Direct debit",
3309                callee: Address::repeat_byte(0x8b),
3310                bytecode: Bytecode::new_raw(direct_create_revert.into()),
3311                initial_slot: U256::ZERO,
3312                initial_credits: 1,
3313                expected_slot: U256::ZERO,
3314                expected_credits: 1,
3315            },
3316        ];
3317
3318        for case in cases {
3319            let key_pair = P256KeyPair::random();
3320            let caller = key_pair.address;
3321            let caller_contract = Address::repeat_byte(case.callee[0] ^ 0xff);
3322            let mut evm = create_funded_evm_t7(caller);
3323
3324            evm.ctx.db_mut().insert_account_info(
3325                caller_contract,
3326                AccountInfo {
3327                    code: Some(caller_ignoring_reverted_callee(case.callee)),
3328                    ..Default::default()
3329                },
3330            );
3331            evm.ctx.db_mut().insert_account_info(
3332                case.callee,
3333                AccountInfo {
3334                    code: Some(case.bytecode.clone()),
3335                    ..Default::default()
3336                },
3337            );
3338            if !case.initial_slot.is_zero() {
3339                evm.ctx.db_mut().insert_account_storage(
3340                    case.callee,
3341                    U256::ZERO,
3342                    case.initial_slot,
3343                )?;
3344            }
3345            seed_storage_credit_balance(&mut evm, case.callee, case.initial_credits);
3346
3347            let tx = TxBuilder::new()
3348                .call(caller_contract, &[])
3349                .gas_limit(2_000_000)
3350                .build();
3351            let result = evm.transact_commit(TempoTxEnv::from_recovered_tx(
3352                &key_pair.sign_tx(tx)?,
3353                caller,
3354            ))?;
3355            assert!(
3356                result.is_success(),
3357                "top-level caller should ignore the reverted {} subcall",
3358                case.name
3359            );
3360
3361            assert_eq!(
3362                evm.ctx.db().storage_ref(case.callee, U256::ZERO)?,
3363                case.expected_slot,
3364                "reverted {} storage write must unwind",
3365                case.name
3366            );
3367            assert_eq!(
3368                storage_credit_balance(&evm, case.callee),
3369                case.expected_credits,
3370                "reverted {} credit accounting must unwind",
3371                case.name
3372            );
3373        }
3374
3375        Ok(())
3376    }
3377
3378    /// TIP-1060: Refund settlement consumes exactly `min(pending_creations, balance)`.
3379    ///
3380    /// End-of-tx settlement may consume no more credits than either pending creations or starting
3381    /// balance.
3382    #[test]
3383    fn test_tip1060_refund_settlement_min_pending_balance() -> eyre::Result<()> {
3384        // (creates, starting balance, final balance).
3385        let cases = [(2u8, 1u64, 0u64), (1u8, 2u64, 1u64)];
3386
3387        for (creates, starting_balance, expected_balance) in cases {
3388            let contract = Address::repeat_byte(0x70 + creates);
3389
3390            // Write 0x01 to `creates` fresh slots, then STOP.
3391            let mut bytecode = Vec::new();
3392            for slot in 0..creates {
3393                bytecode.extend_from_slice(&[
3394                    opcode::PUSH1,
3395                    0x01,
3396                    opcode::PUSH1,
3397                    slot,
3398                    opcode::SSTORE,
3399                ]);
3400            }
3401            bytecode.push(opcode::STOP);
3402
3403            let (_, evm) = run_tx_on_tip1060_contract_with_setup(
3404                CreditMode::Refund,
3405                contract,
3406                &bytecode,
3407                |evm, contract| {
3408                    seed_storage_credit_balance(evm, contract, starting_balance);
3409                    Ok(())
3410                },
3411            )?;
3412
3413            assert_eq!(
3414                storage_credit_balance(&evm, contract),
3415                expected_balance,
3416                "settlement must consume exactly min(pending, balance) storage credits"
3417            );
3418        }
3419
3420        Ok(())
3421    }
3422
3423    /// TIP-1060: Refund settlement is per-account, including across nested calls.
3424    ///
3425    /// A creates one slot and calls B, which creates one slot too. A's surplus credits must not be
3426    /// used to settle B's pending Refund creation.
3427    #[test]
3428    fn test_tip1060_refund_settlement_is_per_account() -> eyre::Result<()> {
3429        let key_pair = P256KeyPair::random();
3430        let caller = key_pair.address;
3431        let account_a = Address::repeat_byte(0xa0);
3432        let account_b = Address::repeat_byte(0xb0);
3433
3434        // A bytecode: SSTORE(0,1), then CALL B.
3435        let mut account_a_bytecode = bytes!("600160005560006000600060006000").to_vec();
3436        account_a_bytecode.push(opcode::PUSH20);
3437        account_a_bytecode.extend_from_slice(account_b.as_slice());
3438        account_a_bytecode.extend_from_slice(&bytes!("620f4240f15000"));
3439
3440        // B bytecode: SSTORE(0,1); STOP.
3441        let account_b_bytecode = Bytecode::new_raw(bytes!("600160005500"));
3442
3443        let mut evm = create_funded_evm_t7(caller);
3444        evm.ctx.db_mut().insert_account_info(
3445            account_a,
3446            AccountInfo {
3447                code: Some(Bytecode::new_raw(account_a_bytecode.into())),
3448                ..Default::default()
3449            },
3450        );
3451        evm.ctx.db_mut().insert_account_info(
3452            account_b,
3453            AccountInfo {
3454                code: Some(account_b_bytecode),
3455                ..Default::default()
3456            },
3457        );
3458        seed_storage_credit_balance(&mut evm, account_a, 2);
3459        seed_storage_credit_balance(&mut evm, account_b, 0);
3460
3461        let tx = TxBuilder::new()
3462            .call(account_a, &[])
3463            .gas_limit(2_000_000)
3464            .build();
3465        let signed_tx = key_pair.sign_tx(tx)?;
3466        let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
3467        let result = evm.transact_commit(tx_env)?;
3468        assert!(result.is_success(), "multi-account tx should succeed");
3469
3470        assert_eq!(
3471            storage_credit_balance(&evm, account_a),
3472            1,
3473            "A consumes its own storage credits"
3474        );
3475        assert_eq!(
3476            storage_credit_balance(&evm, account_b),
3477            0,
3478            "B cannot consume A's extra storage credits"
3479        );
3480
3481        Ok(())
3482    }
3483
3484    /// TIP-1060: same-tx clear can settle an earlier Refund create on another slot.
3485    ///
3486    /// The tx creates slot 0 in Refund mode, then clears pre-existing slot 1. Settlement should use
3487    /// the later clear-minted credit to refund the earlier creation.
3488    #[test]
3489    fn test_tip1060_same_tx_create_before_delete_different_slots() -> eyre::Result<()> {
3490        let key_pair = P256KeyPair::random();
3491        let caller = key_pair.address;
3492        let contract = Address::repeat_byte(0x64);
3493
3494        // Bytecode: SSTORE(0,1); SSTORE(1,0); STOP.
3495        let bytecode = Bytecode::new_raw(bytes!("6001600055600060015500"));
3496
3497        let mut evm = create_funded_evm_t7(caller);
3498        evm.ctx.db_mut().insert_account_info(
3499            contract,
3500            AccountInfo {
3501                code: Some(bytecode),
3502                ..Default::default()
3503            },
3504        );
3505        evm.ctx
3506            .db_mut()
3507            .insert_account_storage(contract, U256::from(1), U256::ONE)?;
3508        seed_storage_credit_balance(&mut evm, contract, 0);
3509
3510        let tx = TxBuilder::new()
3511            .call(contract, &[])
3512            .gas_limit(1_000_000)
3513            .build();
3514        let signed_tx = key_pair.sign_tx(tx)?;
3515        let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
3516        let result = evm.transact_commit(tx_env)?;
3517        assert!(
3518            result.is_success(),
3519            "create-before-delete tx should succeed"
3520        );
3521
3522        assert_eq!(storage_credit_balance(&evm, contract), 0);
3523        assert_eq!(
3524            result.tx_gas_used(),
3525            295_868,
3526            "one 245k deferred storage credit is applied"
3527        );
3528
3529        Ok(())
3530    }
3531
3532    /// TIP-1060: Direct spends synchronously and must not also get Refund settlement.
3533    ///
3534    /// Direct and Refund each create one slot. Direct starts with a surplus credit so an accidental
3535    /// deferred settlement would consume an observable extra credit.
3536    #[test]
3537    fn test_tip1060_direct_storage_credits_no_end_of_tx_double_benefit() -> eyre::Result<()> {
3538        let key_pair = P256KeyPair::random();
3539        let caller = key_pair.address;
3540        let direct_contract = Address::repeat_byte(0x65);
3541        let refund_contract = Address::repeat_byte(0x66);
3542
3543        // Bytecode body: SSTORE(0,1); STOP.
3544        let create_body = bytes!("600160005500");
3545
3546        let mut evm = create_funded_evm_t7(caller);
3547        evm.ctx.db_mut().insert_account_info(
3548            direct_contract,
3549            AccountInfo {
3550                code: Some(bytecode_with_tip1060_mode(CreditMode::Direct, &create_body)),
3551                ..Default::default()
3552            },
3553        );
3554        evm.ctx.db_mut().insert_account_info(
3555            refund_contract,
3556            AccountInfo {
3557                code: Some(bytecode_with_tip1060_mode(CreditMode::Refund, &create_body)),
3558                ..Default::default()
3559            },
3560        );
3561        // Start with a surplus credit so accidental settlement would be observable.
3562        seed_storage_credit_balance(&mut evm, direct_contract, 2);
3563        seed_storage_credit_balance(&mut evm, refund_contract, 1);
3564
3565        let direct_tx = TxBuilder::new()
3566            .call(direct_contract, &[])
3567            .nonce(0)
3568            .gas_limit(1_000_000)
3569            .build();
3570        let signed_direct = key_pair.sign_tx(direct_tx)?;
3571        let direct = evm.transact_commit(TempoTxEnv::from_recovered_tx(&signed_direct, caller))?;
3572        assert!(direct.is_success());
3573        assert_eq!(
3574            storage_credit_balance(&evm, direct_contract),
3575            1,
3576            "Direct must not consume the surplus storage credit at settlement"
3577        );
3578
3579        let refund_tx = TxBuilder::new()
3580            .call(refund_contract, &[])
3581            .nonce(1)
3582            .gas_limit(1_000_000)
3583            .build();
3584        let signed_refund = key_pair.sign_tx(refund_tx)?;
3585        let refund = evm.transact_commit(TempoTxEnv::from_recovered_tx(&signed_refund, caller))?;
3586        assert!(refund.is_success());
3587        assert_eq!(storage_credit_balance(&evm, refund_contract), 0);
3588
3589        assert_eq!(
3590            direct.tx_gas_used(),
3591            293_927,
3592            "Direct gets the synchronous discount without an additional settlement refund"
3593        );
3594        assert_eq!(
3595            refund.tx_gas_used(),
3596            37_962,
3597            "Refund applies the deferred 245k settlement refund for comparison"
3598        );
3599
3600        Ok(())
3601    }
3602
3603    /// TIP-1060: clear-mint saturates at `u64::MAX`.
3604    ///
3605    /// Slot 0 starts non-zero and the credit balance starts at max. Clearing the slot must keep the
3606    /// balance pinned at `u64::MAX` rather than overflowing.
3607    #[test]
3608    fn test_tip1060_sstore_clear_mint_saturates_at_u64_max() -> eyre::Result<()> {
3609        let contract = Address::repeat_byte(0x67);
3610
3611        // Bytecode: SSTORE(0,0); STOP.
3612        let clear_bytecode = bytes!("600060005500");
3613
3614        let (_, evm) = run_tx_on_tip1060_contract_with_setup(
3615            CreditMode::Refund,
3616            contract,
3617            &clear_bytecode,
3618            |evm, contract| {
3619                evm.ctx
3620                    .db_mut()
3621                    .insert_account_storage(contract, U256::ZERO, U256::ONE)?;
3622                seed_storage_credit_balance(evm, contract, u64::MAX);
3623                Ok(())
3624            },
3625        )?;
3626        assert_eq!(storage_credit_balance(&evm, contract), u64::MAX);
3627
3628        Ok(())
3629    }
3630
3631    /// Expiring nonce writes are charged manually by intrinsic gas and must not use TIP-1060 accounting.
3632    #[test]
3633    fn test_expiring_nonce_indexed_path_does_not_settle_storage_credits() -> eyre::Result<()> {
3634        use tempo_primitives::transaction::TEMPO_EXPIRING_NONCE_KEY;
3635
3636        let key_pair = P256KeyPair::random();
3637        let caller = key_pair.address;
3638        let timestamp = 1000u64;
3639        let valid_before = timestamp + 30;
3640
3641        let tx = TxBuilder::new()
3642            .call_identity(&[])
3643            .nonce_key(TEMPO_EXPIRING_NONCE_KEY)
3644            .valid_before(Some(valid_before))
3645            .gas_limit(500_000)
3646            .build();
3647        let signed_tx = key_pair.sign_tx(tx)?;
3648        let unindexed_tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
3649
3650        let mut indexed_tx_env = unindexed_tx_env.clone();
3651        indexed_tx_env
3652            .tempo_tx_env
3653            .as_mut()
3654            .expect("expiring nonce tx must be AA")
3655            .expiring_nonce_idx = Some(1);
3656
3657        let mut unindexed_evm = create_funded_evm_t7_with_timestamp(caller, timestamp);
3658        let unindexed_result = unindexed_evm.transact_commit(unindexed_tx_env)?;
3659        assert!(
3660            unindexed_result.is_success(),
3661            "unindexed expiring nonce tx should succeed"
3662        );
3663
3664        let mut indexed_evm = create_funded_evm_t7_with_timestamp(caller, timestamp);
3665        let indexed_result = indexed_evm.transact_commit(indexed_tx_env)?;
3666        assert!(
3667            indexed_result.is_success(),
3668            "indexed expiring nonce tx should succeed"
3669        );
3670
3671        assert_eq!(
3672            indexed_result.tx_gas_used(),
3673            unindexed_result.tx_gas_used(),
3674            "pointer restore must not create a TIP-1060 settlement discount"
3675        );
3676        assert_eq!(
3677            storage_credit_balance(&indexed_evm, NONCE_PRECOMPILE_ADDRESS),
3678            0,
3679            "expiring nonce bookkeeping must not accrue storage credits"
3680        );
3681
3682        Ok(())
3683    }
3684
3685    /// 2D nonce writes are charged manually by intrinsic gas and must not use TIP-1060 accounting.
3686    #[test]
3687    fn test_2d_nonce_preexecution_does_not_settle_nonce_storage_credits() -> eyre::Result<()> {
3688        let key_pair = P256KeyPair::random();
3689        let caller = key_pair.address;
3690        let nonce_key = U256::from(42_u64);
3691
3692        let tx = TxBuilder::new()
3693            .call_identity(&[])
3694            .nonce_key(nonce_key)
3695            .gas_limit(1_000_000)
3696            .build();
3697        let signed_tx = key_pair.sign_tx(tx)?;
3698        let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
3699
3700        let mut baseline_evm = create_funded_evm_t7(caller);
3701        let baseline_result = baseline_evm.transact_commit(tx_env.clone())?;
3702        assert!(
3703            baseline_result.is_success(),
3704            "baseline 2D nonce tx should succeed"
3705        );
3706
3707        let mut credited_evm = create_funded_evm_t7(caller);
3708        seed_storage_credit_balance(&mut credited_evm, NONCE_PRECOMPILE_ADDRESS, 1);
3709        let credited_result = credited_evm.transact_commit(tx_env)?;
3710        assert!(
3711            credited_result.is_success(),
3712            "preseeded-credit 2D nonce tx should succeed"
3713        );
3714
3715        assert_eq!(
3716            credited_result.tx_gas_used(),
3717            baseline_result.tx_gas_used(),
3718            "2D nonce bookkeeping must not consume nonce storage credits for a gas discount"
3719        );
3720        assert_eq!(
3721            storage_credit_balance(&credited_evm, NONCE_PRECOMPILE_ADDRESS),
3722            1,
3723            "2D nonce bookkeeping must not consume pre-existing nonce storage credits"
3724        );
3725
3726        Ok(())
3727    }
3728
3729    /// Test AA transaction gas for contract creation (CREATE).
3730    /// TIP-1000 increases TX create cost to 500,000 and new account cost to 250,000.
3731    /// Uses T1 hardfork for TIP-1000 gas costs.
3732    #[test]
3733    fn test_aa_tx_gas_create_contract() -> eyre::Result<()> {
3734        let key_pair = P256KeyPair::random();
3735        let caller = key_pair.address;
3736
3737        let mut evm = create_funded_evm_t1(caller);
3738
3739        // Simple initcode: PUSH1 0x00 PUSH1 0x00 RETURN (deploys empty contract)
3740        let initcode = vec![0x60, 0x00, 0x60, 0x00, 0xF3];
3741
3742        // T1 costs: CREATE cost (500k, fixed upfront contract creation cost) + new account for sender (250k) + base costs
3743        let tx = TxBuilder::new()
3744            .create(&initcode)
3745            .gas_limit(1_000_000)
3746            .build();
3747
3748        let signed_tx = key_pair.sign_tx(tx)?;
3749        let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
3750
3751        let result = evm.transact_commit(tx_env)?;
3752        assert!(result.is_success(), "CREATE transaction should succeed");
3753
3754        // With TIP-1000: CREATE cost (500k) + new account for sender (250k) + base costs
3755        let gas_used = result.tx_gas_used();
3756        assert_eq!(gas_used, 778720, "T1 CREATE contract gas should be exact");
3757
3758        Ok(())
3759    }
3760
3761    /// TIP-1016: generic EVM CREATE charges deployed-bytecode HASH_COST(L)
3762    /// in addition to CREATE base gas and code deposit gas on the success path.
3763    #[test]
3764    fn test_t4_create_tx_charges_hash_cost() -> eyre::Result<()> {
3765        let key_pair = P256KeyPair::random();
3766        let caller = key_pair.address;
3767
3768        // Initcode that returns a 1-byte runtime (`STOP`), so HASH_COST(L) = 6.
3769        let tx = TxBuilder::new()
3770            .create(&hex!("6001600c60003960016000f300"))
3771            .gas_limit(1_000_000)
3772            .build();
3773        let signed_tx = key_pair.sign_tx(tx)?;
3774
3775        let run_create = |without_word_cost: bool| -> eyre::Result<u64> {
3776            let mut evm = create_funded_evm_t4(caller);
3777            if without_word_cost {
3778                evm.ctx.cfg.gas_params.override_gas(vec![(
3779                    revm::context_interface::cfg::GasId::keccak256_per_word(),
3780                    0,
3781                )]);
3782            }
3783
3784            let result = evm.transact_commit(TempoTxEnv::from_recovered_tx(&signed_tx, caller))?;
3785            assert!(
3786                result.is_success(),
3787                "T4 CREATE transaction should succeed with keccak256_per_word={without_word_cost:?}"
3788            );
3789            Ok(result.tx_gas_used())
3790        };
3791
3792        assert_eq!(
3793            run_create(false)? - run_create(true)?, // gas_with_hash - gas_without_hash (test fixture)
3794            tempo_gas_params(TempoHardfork::T4).keccak256_cost(1),
3795            "generic CREATE should add HASH_COST(L) on top of the non-hash baseline"
3796        );
3797        Ok(())
3798    }
3799
3800    /// Test AA transaction gas for CREATE with 2D nonce (nonce_key != 0).
3801    /// When caller account nonce is 0, an additional 250k gas is charged for account creation.
3802    /// Uses T1 hardfork for TIP-1000 gas costs.
3803    #[test]
3804    fn test_aa_tx_gas_create_with_2d_nonce() -> eyre::Result<()> {
3805        let key_pair = P256KeyPair::random();
3806        let caller = key_pair.address;
3807
3808        let mut evm = create_funded_evm_t1(caller);
3809
3810        // Simple initcode: PUSH1 0x00 PUSH1 0x00 RETURN (deploys empty contract)
3811        let initcode = vec![0x60, 0x00, 0x60, 0x00, 0xF3];
3812        let nonce_key_2d = U256::from(42);
3813
3814        // Test 1: CREATE tx with 2D nonce, caller account nonce = 0
3815        // Should include: CREATE cost (500k) + new account for sender (250k) + 2D nonce sender creation (250k)
3816        let tx1 = TxBuilder::new()
3817            .create(&initcode)
3818            .nonce_key(nonce_key_2d)
3819            .gas_limit(2_000_000)
3820            .build();
3821
3822        // Verify that account nonce is 0 before transaction
3823        assert_eq!(
3824            evm.ctx
3825                .db()
3826                .basic_ref(caller)
3827                .ok()
3828                .flatten()
3829                .map(|a| a.nonce)
3830                .unwrap_or(0),
3831            0,
3832            "Caller account nonce should be 0 before first tx"
3833        );
3834
3835        let signed_tx1 = key_pair.sign_tx(tx1)?;
3836        let tx_env1 = TempoTxEnv::from_recovered_tx(&signed_tx1, caller);
3837
3838        let result1 = evm.transact_commit(tx_env1)?;
3839        assert!(result1.is_success(), "CREATE with 2D nonce should succeed");
3840
3841        // With TIP-1000: CREATE cost (500k) + new account (250k) + 2D nonce sender creation (250k) + base
3842        assert_eq!(
3843            result1.tx_gas_used(),
3844            1028720,
3845            "T1 CREATE with 2D nonce (caller.nonce=0) gas should be exact"
3846        );
3847
3848        // Test 2: Second CREATE tx with 2D nonce (different nonce_key)
3849        // Caller account nonce is now 1, so no extra 250k for caller account creation
3850        // Should include: CREATE cost (500k) + new account for sender (250k from nonce==0 check)
3851        // but NOT the extra 250k for 2D nonce caller creation since account.nonce != 0
3852        let nonce_key_2d_2 = U256::from(43);
3853        let tx2 = TxBuilder::new()
3854            .create(&initcode)
3855            .nonce_key(nonce_key_2d_2)
3856            .nonce(0) // 2D nonce = 0 (new key, starts at 0)
3857            .gas_limit(2_000_000)
3858            .build();
3859
3860        let signed_tx2 = key_pair.sign_tx(tx2)?;
3861        let tx_env2 = TempoTxEnv::from_recovered_tx(&signed_tx2, caller);
3862
3863        let result2 = evm.transact_commit(tx_env2)?;
3864        assert!(
3865            result2.is_success(),
3866            "Second CREATE with 2D nonce should succeed"
3867        );
3868
3869        // With TIP-1000: CREATE cost (500k) + new account (250k) + base (no extra 250k since caller.nonce != 0)
3870        assert_eq!(
3871            result2.tx_gas_used(),
3872            778720,
3873            "T1 CREATE with 2D nonce (caller.nonce=1) gas should be exact"
3874        );
3875
3876        // Verify the gas difference is exactly 250,000 (new_account_cost)
3877        let gas_difference = result1.tx_gas_used() - result2.tx_gas_used();
3878        assert_eq!(
3879            gas_difference, 250_000,
3880            "Gas difference should be exactly new_account_cost (250,000), got {gas_difference:?}",
3881        );
3882
3883        Ok(())
3884    }
3885
3886    /// Test that CREATE with expiring nonce charges 250k new_account_cost when caller.nonce == 0.
3887    /// This validates the fix for audit issue #182.
3888    #[test]
3889    fn test_aa_tx_gas_create_with_expiring_nonce() -> eyre::Result<()> {
3890        use tempo_primitives::transaction::TEMPO_EXPIRING_NONCE_KEY;
3891
3892        let key_pair = P256KeyPair::random();
3893        let caller = key_pair.address;
3894        let initcode = vec![0x60, 0x00, 0x60, 0x00, 0xF3]; // PUSH0 PUSH0 RETURN
3895        let timestamp = 1000u64;
3896        let valid_before = timestamp + 30;
3897
3898        // CREATE with caller.nonce == 0 (should charge extra 250k)
3899        let mut evm1 = create_funded_evm_t1_with_timestamp(caller, timestamp);
3900        let tx1 = TxBuilder::new()
3901            .create(&initcode)
3902            .nonce_key(TEMPO_EXPIRING_NONCE_KEY)
3903            .valid_before(Some(valid_before))
3904            .gas_limit(2_000_000)
3905            .build();
3906        let result1 = evm1.transact_commit(TempoTxEnv::from_recovered_tx(
3907            &key_pair.sign_tx(tx1)?,
3908            caller,
3909        ))?;
3910        assert!(result1.is_success());
3911        let gas_nonce_zero = result1.tx_gas_used();
3912
3913        // CREATE with caller.nonce == 1 (no extra 250k)
3914        let mut evm2 = create_funded_evm_t1_with_timestamp(caller, timestamp);
3915        evm2.ctx.db_mut().insert_account_info(
3916            caller,
3917            AccountInfo {
3918                balance: U256::from(DEFAULT_BALANCE),
3919                nonce: 1,
3920                ..Default::default()
3921            },
3922        );
3923        let tx2 = TxBuilder::new()
3924            .create(&initcode)
3925            .nonce_key(TEMPO_EXPIRING_NONCE_KEY)
3926            .valid_before(Some(valid_before))
3927            .gas_limit(2_000_000)
3928            .build();
3929        let result2 = evm2.transact_commit(TempoTxEnv::from_recovered_tx(
3930            &key_pair.sign_tx(tx2)?,
3931            caller,
3932        ))?;
3933        assert!(result2.is_success());
3934        let gas_nonce_one = result2.tx_gas_used();
3935
3936        // The fix adds 250k when caller.nonce == 0 for CREATE with non-zero nonce_key
3937        assert_eq!(
3938            gas_nonce_zero - gas_nonce_one,
3939            250_000,
3940            "new_account_cost not charged"
3941        );
3942
3943        Ok(())
3944    }
3945
3946    /// Test gas comparison between single call and multiple calls.
3947    /// Uses T1 hardfork for TIP-1000 gas costs.
3948    #[test]
3949    fn test_aa_tx_gas_single_vs_multiple_calls() -> eyre::Result<()> {
3950        let key_pair = P256KeyPair::random();
3951        let caller = key_pair.address;
3952
3953        // Test 1: Single call
3954        // T1 costs: new account (250k) + base costs
3955        let mut evm1 = create_funded_evm_t1(caller);
3956        let tx1 = TxBuilder::new()
3957            .call_identity(&[0x01, 0x02, 0x03, 0x04])
3958            .gas_limit(500_000)
3959            .build();
3960
3961        let signed_tx1 = key_pair.sign_tx(tx1)?;
3962        let tx_env1 = TempoTxEnv::from_recovered_tx(&signed_tx1, caller);
3963        let result1 = evm1.transact_commit(tx_env1)?;
3964        assert!(result1.is_success());
3965        let gas_single = result1.tx_gas_used();
3966
3967        // Test 2: Three calls
3968        // T1 costs: new account (250k) + 3 calls overhead
3969        let mut evm2 = create_funded_evm_t1(caller);
3970        let tx2 = TxBuilder::new()
3971            .call_identity(&[0x01, 0x02, 0x03, 0x04])
3972            .call_identity(&[0x05, 0x06, 0x07, 0x08])
3973            .call_identity(&[0x09, 0x0A, 0x0B, 0x0C])
3974            .gas_limit(500_000)
3975            .build();
3976
3977        let signed_tx2 = key_pair.sign_tx(tx2)?;
3978        let tx_env2 = TempoTxEnv::from_recovered_tx(&signed_tx2, caller);
3979        let result2 = evm2.transact_commit(tx_env2)?;
3980        assert!(result2.is_success());
3981        let gas_triple = result2.tx_gas_used();
3982
3983        // Three calls should cost more than single call
3984        assert_eq!(gas_single, 278738, "T1 single call gas should be exact");
3985        assert_eq!(gas_triple, 284102, "T1 triple call gas should be exact");
3986        assert!(
3987            gas_triple > gas_single,
3988            "3 calls should cost more than 1 call"
3989        );
3990        assert!(
3991            gas_triple < gas_single * 3,
3992            "3 calls should cost less than 3x single call (base costs shared)"
3993        );
3994
3995        Ok(())
3996    }
3997
3998    /// Test AA transaction gas with SLOAD operation (cold vs warm access).
3999    /// Uses T1 hardfork for TIP-1000 gas costs.
4000    #[test]
4001    fn test_aa_tx_gas_sload_cold_vs_warm() -> eyre::Result<()> {
4002        let key_pair = P256KeyPair::random();
4003        let caller = key_pair.address;
4004        let contract = Address::repeat_byte(0x58);
4005
4006        let mut evm = create_funded_evm_t1(caller);
4007
4008        // Deploy contract that does 2 SLOADs from the same slot:
4009        // PUSH1 0x00 SLOAD POP  (cold SLOAD from slot 0)
4010        // PUSH1 0x00 SLOAD POP  (warm SLOAD from slot 0)
4011        // STOP
4012        let sload_bytecode = Bytecode::new_raw(bytes!("6000545060005450"));
4013        evm.ctx.db_mut().insert_account_info(
4014            contract,
4015            AccountInfo {
4016                code: Some(sload_bytecode),
4017                ..Default::default()
4018            },
4019        );
4020
4021        // Pre-populate storage
4022        evm.ctx
4023            .db_mut()
4024            .insert_account_storage(contract, U256::ZERO, U256::from(0x1234))
4025            .unwrap();
4026
4027        // T1 costs: new account (250k) + SLOAD costs + base costs
4028        let tx = TxBuilder::new()
4029            .call(contract, &[])
4030            .gas_limit(500_000)
4031            .build();
4032
4033        let signed_tx = key_pair.sign_tx(tx)?;
4034        let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
4035
4036        let result = evm.transact_commit(tx_env)?;
4037        assert!(result.is_success(), "SLOAD transaction should succeed");
4038
4039        // T1 costs: new account (250k) + cold SLOAD (2100) + warm SLOAD (100) + cold account (~2.6k)
4040        let gas_used = result.tx_gas_used();
4041        assert_eq!(gas_used, 280866, "T1 SLOAD cold/warm gas should be exact");
4042
4043        Ok(())
4044    }
4045
4046    // ==================== End TIP-1000 Tests ====================
4047
4048    /// Test system call functions and inspector management.
4049    /// Tests `system_call_one_with_caller`, `inspect_one_system_call_with_caller`, and `set_inspector`.
4050    #[test]
4051    fn test_system_call_and_inspector() -> eyre::Result<()> {
4052        let caller = Address::repeat_byte(0x01);
4053        let contract = Address::repeat_byte(0x42);
4054
4055        // Deploy a simple contract that returns success
4056        // DIFFICULTY NUMBER PUSH1 0x00 PUSH1 0x00 RETURN (returns empty data)
4057        let bytecode = Bytecode::new_raw(bytes!("444360006000F3"));
4058
4059        // Test system_call_one_with_caller (no inspector needed)
4060        {
4061            let mut evm = create_evm();
4062            evm.ctx.db_mut().insert_account_info(
4063                contract,
4064                AccountInfo {
4065                    code: Some(bytecode.clone()),
4066                    ..Default::default()
4067                },
4068            );
4069
4070            let result = evm.system_call_one_with_caller(caller, contract, Bytes::new())?;
4071            assert!(result.is_success());
4072        }
4073
4074        // Test set_inspector and inspect_one_system_call_with_caller
4075        {
4076            let mut evm = create_evm_with_inspector(CountInspector::new());
4077            evm.ctx.db_mut().insert_account_info(
4078                contract,
4079                AccountInfo {
4080                    code: Some(bytecode),
4081                    ..Default::default()
4082                },
4083            );
4084
4085            // Test inspect_one_system_call_with_caller
4086            let result = evm.inspect_one_system_call_with_caller(caller, contract, Bytes::new())?;
4087            assert!(result.is_success());
4088
4089            // Verify inspector was called
4090            assert!(evm.inspector.call_count() > 0,);
4091
4092            // Test set_inspector - replace with a fresh CountInspector
4093            evm.set_inspector(CountInspector::new());
4094
4095            // Verify the new inspector starts fresh
4096            assert_eq!(evm.inspector.call_count(), 0,);
4097
4098            // Run another system call and verify new inspector records it
4099            let result = evm.inspect_one_system_call_with_caller(caller, contract, Bytes::new())?;
4100            assert!(result.is_success());
4101            assert!(evm.inspector.call_count() > 0);
4102        }
4103
4104        Ok(())
4105    }
4106
4107    /// Test that key_authorization works correctly with T1 hardfork.
4108    ///
4109    /// This test verifies the key_authorization flow works in the T1 EVM.
4110    /// It ensures that:
4111    /// 1. Keys are NOT authorized when transaction fails due to insufficient gas
4112    /// 2. Keys ARE authorized when transaction succeeds with sufficient gas
4113    ///
4114    /// Related fix: The handler creates a checkpoint before key_authorization
4115    /// precompile execution and reverts it on OOG. This ensures storage consistency.
4116    #[test]
4117    fn test_key_authorization_t1() -> eyre::Result<()> {
4118        use tempo_precompiles::account_keychain::AccountKeychain;
4119
4120        let key_pair = P256KeyPair::random();
4121        let caller = key_pair.address;
4122
4123        // Create a T1 EVM (the fix only applies to T1)
4124        let mut evm = create_funded_evm_t1(caller);
4125
4126        // Set up TIP20 for fee payment
4127        let block = TempoBlockEnv::default();
4128        {
4129            let ctx = &mut evm.ctx;
4130            let internals = EvmInternals::new(&mut ctx.journaled_state, &block, &ctx.cfg, &ctx.tx);
4131            let mut provider = EvmPrecompileStorageProvider::new_max_gas(internals, &ctx.cfg);
4132
4133            StorageCtx::enter(&mut provider, || {
4134                TIP20Setup::path_usd(caller)
4135                    .with_issuer(caller)
4136                    .with_mint(caller, U256::from(10_000_000))
4137                    .apply()
4138            })?;
4139        }
4140
4141        // ==================== Test 1: INSUFFICIENT gas ====================
4142        // First, try with insufficient gas - key should NOT be authorized
4143
4144        let access_key = P256KeyPair::random();
4145        let key_auth =
4146            KeyAuthorization::unrestricted(1, SignatureType::WebAuthn, access_key.address);
4147        let key_auth_sig = key_pair.sign_webauthn(key_auth.signature_hash().as_slice())?;
4148        let signed_key_auth = key_auth.into_signed(PrimitiveSignature::WebAuthn(key_auth_sig));
4149
4150        // Verify key does NOT exist before the transaction
4151        {
4152            let ctx = &mut evm.ctx;
4153            let internals = EvmInternals::new(&mut ctx.journaled_state, &block, &ctx.cfg, &ctx.tx);
4154            let mut provider = EvmPrecompileStorageProvider::new_max_gas(internals, &ctx.cfg);
4155
4156            let key_exists = StorageCtx::enter(&mut provider, || {
4157                let keychain = AccountKeychain::default();
4158                keychain.keys[caller][access_key.address].read()
4159            })?;
4160            assert_eq!(
4161                key_exists.expiry, 0,
4162                "Key should not exist before transaction"
4163            );
4164        }
4165
4166        let signed_auth = key_pair.create_signed_authorization(Address::repeat_byte(0x42))?;
4167
4168        // Insufficient gas - will cause OOG during key_authorization processing
4169        let tx_low_gas = TxBuilder::new()
4170            .call_identity(&[0x01])
4171            .authorization(signed_auth)
4172            .key_authorization(signed_key_auth)
4173            .gas_limit(589_000)
4174            .build();
4175
4176        let signed_tx_low = key_pair.sign_tx(tx_low_gas)?;
4177        let tx_env_low = TempoTxEnv::from_recovered_tx(&signed_tx_low, caller);
4178
4179        // Execute the transaction - it should fail due to insufficient gas
4180        let result_low = evm.transact_commit(tx_env_low);
4181
4182        // Transaction should fail (either rejected or OOG).
4183        // Track whether the nonce was incremented (committed OOG vs validation rejection).
4184        let nonce_incremented = match &result_low {
4185            Ok(result) => {
4186                assert_eq!(
4187                    result.tx_gas_used(),
4188                    589_000,
4189                    "Gas used should be gas limit"
4190                );
4191                assert!(
4192                    !result.is_success(),
4193                    "Transaction with insufficient gas should fail"
4194                );
4195                true // OOG: tx committed, nonce incremented
4196            }
4197            Err(e) => {
4198                // Transaction rejected during validation - must be CallGasCostMoreThanGasLimit
4199                assert!(
4200                    matches!(
4201                        e,
4202                        revm::context::result::EVMError::Transaction(
4203                            TempoInvalidTransaction::EthInvalidTransaction(
4204                                revm::context::result::InvalidTransaction::CallGasCostMoreThanGasLimit { .. }
4205                            )
4206                        )
4207                    ),
4208                    "Expected CallGasCostMoreThanGasLimit, got: {e:?}"
4209                );
4210                false // Validation rejection: nonce NOT incremented
4211            }
4212        };
4213
4214        // CRITICAL: Verify the key was NOT authorized
4215        // This tests that storage changes are properly reverted on failure
4216        {
4217            let ctx = &mut evm.ctx;
4218            let internals = EvmInternals::new(&mut ctx.journaled_state, &block, &ctx.cfg, &ctx.tx);
4219            let mut provider = EvmPrecompileStorageProvider::new_max_gas(internals, &ctx.cfg);
4220
4221            let key_after_fail = StorageCtx::enter(&mut provider, || {
4222                let keychain = AccountKeychain::default();
4223                keychain.keys[caller][access_key.address].read()
4224            })?;
4225
4226            assert_eq!(
4227                key_after_fail,
4228                AuthorizedKey::default(),
4229                "Key should NOT be authorized when transaction fails due to insufficient gas"
4230            );
4231        }
4232
4233        // ==================== Test 2: SUFFICIENT gas ====================
4234        // Now try with sufficient gas - key should be authorized
4235
4236        let access_key2 = P256KeyPair::random();
4237        let key_auth2 =
4238            KeyAuthorization::unrestricted(1, SignatureType::WebAuthn, access_key2.address);
4239        let key_auth_sig2 = key_pair.sign_webauthn(key_auth2.signature_hash().as_slice())?;
4240        let signed_key_auth2 = key_auth2.into_signed(PrimitiveSignature::WebAuthn(key_auth_sig2));
4241
4242        let signed_auth2 = key_pair.create_signed_authorization(Address::repeat_byte(0x43))?;
4243
4244        // Execute transaction with sufficient gas
4245        let next_nonce = if nonce_incremented { 1 } else { 0 };
4246        let tx = TxBuilder::new()
4247            .call_identity(&[0x01])
4248            .authorization(signed_auth2)
4249            .key_authorization(signed_key_auth2)
4250            .nonce(next_nonce)
4251            .gas_limit(1_000_000)
4252            .build();
4253
4254        let signed_tx = key_pair.sign_tx(tx)?;
4255        let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
4256
4257        let result = evm.transact_commit(tx_env)?;
4258        assert!(result.is_success(), "Transaction should succeed");
4259
4260        // Verify the key was authorized
4261        {
4262            let ctx = &mut evm.ctx;
4263            let internals = EvmInternals::new(&mut ctx.journaled_state, &block, &ctx.cfg, &ctx.tx);
4264            let mut provider = EvmPrecompileStorageProvider::new_max_gas(internals, &ctx.cfg);
4265
4266            let key_after_success = StorageCtx::enter(&mut provider, || {
4267                let keychain = AccountKeychain::default();
4268                keychain.keys[caller][access_key2.address].read()
4269            })?;
4270
4271            assert_eq!(
4272                key_after_success.expiry,
4273                u64::MAX,
4274                "Key should be authorized after successful transaction"
4275            );
4276        }
4277
4278        Ok(())
4279    }
4280
4281    /// Regression: CREATE nonce replay vulnerability — demonstrates the T1
4282    /// bug and verifies the T1B fix.
4283    ///
4284    /// **The bug (T1):** An AA CREATE transaction with a KeyAuthorization runs
4285    /// `authorize_key` in a gas-metered precompile call. TIP-1000 SSTORE costs
4286    /// (250k) easily exceed the remaining gas after intrinsic deduction, causing
4287    /// OutOfGas. The handler then sets `evm.initial_gas = u64::MAX`, which
4288    /// short-circuits execution before `make_create_frame` bumps the protocol
4289    /// nonce. The nonce stays at 0, making the signed transaction replayable.
4290    ///
4291    /// **The fix (T1B):** The precompile runs with `gas_limit = u64::MAX`,
4292    /// eliminating the OOG path. Gas is accounted for solely in intrinsic gas.
4293    /// The CREATE frame is always constructed, the nonce is always bumped, and
4294    /// replay is impossible.
4295    #[test]
4296    fn test_create_nonce_replay_regression() -> eyre::Result<()> {
4297        use tempo_precompiles::account_keychain::AccountKeychain;
4298
4299        /// Run a CREATE+KeyAuth transaction on the given hardfork and return
4300        /// (caller_nonce_after, key_expiry).
4301        fn run_create_with_key_auth(
4302            spec: TempoHardfork,
4303            gas_limit: u64,
4304        ) -> eyre::Result<(u64, u64)> {
4305            let key_pair = P256KeyPair::random();
4306            let caller = key_pair.address;
4307
4308            let db = CacheDB::new(EmptyDB::new());
4309            let mut cfg = CfgEnv::<TempoHardfork>::default();
4310            cfg.spec = spec;
4311            cfg.gas_params = tempo_gas_params(spec);
4312
4313            let ctx = Context::mainnet()
4314                .with_db(db)
4315                .with_block(Default::default())
4316                .with_cfg(cfg)
4317                .with_tx(Default::default());
4318
4319            let mut evm = TempoEvm::new(ctx, ());
4320            fund_account(&mut evm, caller);
4321
4322            let block = TempoBlockEnv::default();
4323            {
4324                let ctx = &mut evm.ctx;
4325                let internals =
4326                    EvmInternals::new(&mut ctx.journaled_state, &block, &ctx.cfg, &ctx.tx);
4327                // Use default cfg for TIP20 setup — the test infrastructure's
4328                // `is_initialized` check uses an unsafe `as_hashmap()` cast that
4329                // only works with default gas params.
4330                let mut provider =
4331                    EvmPrecompileStorageProvider::new_max_gas(internals, &Default::default());
4332                StorageCtx::enter(&mut provider, || {
4333                    TIP20Setup::path_usd(caller)
4334                        .with_issuer(caller)
4335                        .with_mint(caller, U256::from(100_000_000))
4336                        .apply()
4337                })?;
4338            }
4339
4340            let access_key = P256KeyPair::random();
4341            let key_auth =
4342                KeyAuthorization::unrestricted(1, SignatureType::WebAuthn, access_key.address);
4343            let key_auth_sig = key_pair.sign_webauthn(key_auth.signature_hash().as_slice())?;
4344            let signed_key_auth = key_auth.into_signed(PrimitiveSignature::WebAuthn(key_auth_sig));
4345
4346            let tx = TxBuilder::new()
4347                .create(&[0x60, 0x00, 0x60, 0x00, 0xF3])
4348                .key_authorization(signed_key_auth)
4349                .gas_limit(gas_limit)
4350                .build();
4351
4352            let signed_tx = key_pair.sign_tx(tx)?;
4353            let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
4354            let _result = evm.transact_commit(tx_env);
4355
4356            let nonce = evm
4357                .ctx
4358                .db()
4359                .basic_ref(caller)
4360                .ok()
4361                .flatten()
4362                .map(|a| a.nonce)
4363                .unwrap_or(0);
4364
4365            let key_expiry = {
4366                let ctx = &mut evm.ctx;
4367                let internals =
4368                    EvmInternals::new(&mut ctx.journaled_state, &block, &ctx.cfg, &ctx.tx);
4369                let mut provider =
4370                    EvmPrecompileStorageProvider::new_max_gas(internals, &Default::default());
4371                let key = StorageCtx::enter(&mut provider, || {
4372                    AccountKeychain::default().keys[caller][access_key.address].read()
4373                })?;
4374                key.expiry
4375            };
4376
4377            Ok((nonce, key_expiry))
4378        }
4379
4380        // --- T1: demonstrate the bug ---
4381        // T1 intrinsic gas for this tx is ~560k (21k base + 500k CREATE + 35k
4382        // KeyAuth heuristic). Gas limit 780k leaves ~220k for the precompile,
4383        // which is below the 250k SSTORE cost → OOG → nonce NOT bumped.
4384        let (t1_nonce, t1_key_expiry) = run_create_with_key_auth(TempoHardfork::T1, 780_000)?;
4385        assert_eq!(
4386            t1_nonce, 0,
4387            "T1 bug: nonce must NOT be bumped when keychain OOGs"
4388        );
4389        assert_eq!(
4390            t1_key_expiry, 0,
4391            "T1 bug: key must NOT be authorized when keychain OOGs"
4392        );
4393
4394        // --- T1B: verify the fix ---
4395        // T1B intrinsic gas is ~1.04M (21k base + 500k CREATE + 260k KeyAuth
4396        // + calldata + sig). Gas limit 1.05M is just enough to pass intrinsic
4397        // validation. The precompile runs with unlimited gas, so the nonce is
4398        // always bumped.
4399        let (t1b_nonce, t1b_key_expiry) = run_create_with_key_auth(TempoHardfork::T1B, 1_050_000)?;
4400        assert_eq!(
4401            t1b_nonce, 1,
4402            "T1B fix: nonce must be bumped after CREATE+KeyAuth"
4403        );
4404        assert_eq!(t1b_key_expiry, u64::MAX, "T1B fix: key must be authorized");
4405
4406        Ok(())
4407    }
4408
4409    /// Regression: double gas charging for KeyAuthorization — demonstrates the
4410    /// T1 bug and verifies the T1B fix.
4411    ///
4412    /// **The bug (T1):** The handler charges both a heuristic intrinsic gas
4413    /// estimate AND the metered precompile gas (`evm.initial_gas += gas_used`),
4414    /// resulting in a double charge. With TIP-1000 SSTORE at 250k, a simple
4415    /// KeyAuthorization (0 limits) costs ~530k on T1 instead of ~280k.
4416    ///
4417    /// **The fix (T1B):** Only the intrinsic gas is charged; the precompile runs
4418    /// with unlimited gas and its cost is NOT added to `initial_gas` afterward.
4419    #[test]
4420    fn test_double_charge_key_authorization_regression() -> eyre::Result<()> {
4421        /// Run a CALL+KeyAuth transaction and return gas_used.
4422        fn run_call_with_key_auth(spec: TempoHardfork) -> eyre::Result<u64> {
4423            let key_pair = P256KeyPair::random();
4424            let caller = key_pair.address;
4425
4426            let db = CacheDB::new(EmptyDB::new());
4427            let mut cfg = CfgEnv::<TempoHardfork>::default();
4428            cfg.spec = spec;
4429            cfg.gas_params = tempo_gas_params(spec);
4430
4431            let ctx = Context::mainnet()
4432                .with_db(db)
4433                .with_block(Default::default())
4434                .with_cfg(cfg)
4435                .with_tx(Default::default());
4436
4437            let mut evm = TempoEvm::new(ctx, ());
4438            fund_account(&mut evm, caller);
4439
4440            let block = TempoBlockEnv::default();
4441            {
4442                let ctx = &mut evm.ctx;
4443                let internals =
4444                    EvmInternals::new(&mut ctx.journaled_state, &block, &ctx.cfg, &ctx.tx);
4445                let mut provider =
4446                    EvmPrecompileStorageProvider::new_max_gas(internals, &Default::default());
4447                StorageCtx::enter(&mut provider, || {
4448                    TIP20Setup::path_usd(caller)
4449                        .with_issuer(caller)
4450                        .with_mint(caller, U256::from(100_000_000))
4451                        .apply()
4452                })?;
4453            }
4454
4455            let access_key = P256KeyPair::random();
4456            let key_auth =
4457                KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, access_key.address);
4458            let key_auth_sig = key_pair.sign_webauthn(key_auth.signature_hash().as_slice())?;
4459            let signed_key_auth = key_auth.into_signed(PrimitiveSignature::WebAuthn(key_auth_sig));
4460
4461            let tx = TxBuilder::new()
4462                .call_identity(&[])
4463                .key_authorization(signed_key_auth)
4464                .gas_limit(2_000_000)
4465                .build();
4466
4467            let signed_tx = key_pair.sign_tx(tx)?;
4468            let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
4469            let result = evm.transact_commit(tx_env)?;
4470            assert!(result.is_success());
4471            Ok(result.tx_gas_used())
4472        }
4473
4474        let t1_gas = run_call_with_key_auth(TempoHardfork::T1)?;
4475        let t1b_gas = run_call_with_key_auth(TempoHardfork::T1B)?;
4476
4477        // T1 double-charges: intrinsic heuristic (~35k) + metered precompile
4478        // (~250k SSTORE) on top of base tx gas, resulting in >500k.
4479        assert!(
4480            t1_gas > 500_000,
4481            "T1 bug: should double-charge (got {t1_gas}, expected >500k)"
4482        );
4483
4484        // T1B charges only once via accurate intrinsic gas (~255k for
4485        // sig+sload+sstore) + base tx. Total ~541k, well below the ~790k
4486        // that double-charging would produce.
4487        assert!(
4488            t1b_gas < t1_gas,
4489            "T1B fix: gas ({t1b_gas}) must be less than T1 double-charge ({t1_gas})"
4490        );
4491
4492        Ok(())
4493    }
4494
4495    /// Regression: `eth_estimateGas` must NOT add an extra 250k `new_account_cost` for AA
4496    /// token transfers using the `calls` format when `nonce_key != 0` and
4497    /// `caller.nonce == 0`.
4498    ///
4499    /// Root cause: `tx.kind()` reads `inner.to`, which is `None` for the
4500    /// `calls` format, causing it to return `TxKind::Create` for a plain
4501    /// transfer — incorrectly triggering a second 250k account-creation charge
4502    /// on top of the legitimate 250k already charged by `validate_aa_initial_tx_gas`.
4503    ///
4504    /// The fix inspects `aa_calls[0].to` directly for AA transactions instead
4505    /// of relying on `tx.kind()`.
4506    #[test]
4507    fn test_aa_tx_transfer_calls_format_no_extra_250k() -> eyre::Result<()> {
4508        let key_pair = P256KeyPair::random();
4509        let caller = key_pair.address;
4510        let recipient = Address::with_last_byte(0xff);
4511
4512        // Baseline: calls-format transfer with nonce_key=0 (protocol nonce).
4513        // validate_aa_initial_tx_gas charges 250k (nonce==0 branch).
4514        // handler.rs does NOT fire because !nonce_key.is_zero() is false.
4515        let mut evm_baseline = create_funded_evm_t1(caller);
4516        let tx_baseline = TxBuilder::new()
4517            .call(recipient, &[])
4518            .nonce_key(U256::ZERO)
4519            .nonce(0)
4520            .gas_limit(500_000)
4521            .build();
4522        let result_baseline = evm_baseline.transact_commit(TempoTxEnv::from_recovered_tx(
4523            &key_pair.sign_tx(tx_baseline)?,
4524            caller,
4525        ))?;
4526        assert!(
4527            result_baseline.is_success(),
4528            "baseline transfer should succeed"
4529        );
4530        let gas_baseline = result_baseline.tx_gas_used();
4531
4532        // Issue #3178 scenario: calls-format transfer with nonce_key != 0, caller.nonce == 0.
4533        // validate_aa_initial_tx_gas still charges the same 250k (nonce==0 branch).
4534        // Before fix: handler.rs also fired (tx.kind() wrongly returned Create) → extra 250k.
4535        // After fix:  handler.rs does NOT fire (aa_calls[0].to is Call) → no extra 250k.
4536        let nonce_key = U256::from(42);
4537        let mut evm_2d = create_funded_evm_t1(caller);
4538        let tx_2d = TxBuilder::new()
4539            .call(recipient, &[])
4540            .nonce_key(nonce_key)
4541            .nonce(0)
4542            .gas_limit(500_000)
4543            .build();
4544        let result_2d = evm_2d.transact_commit(TempoTxEnv::from_recovered_tx(
4545            &key_pair.sign_tx(tx_2d)?,
4546            caller,
4547        ))?;
4548        assert!(
4549            result_2d.is_success(),
4550            "calls-format transfer with 2D nonce should succeed"
4551        );
4552        let gas_2d = result_2d.tx_gas_used();
4553
4554        // After the fix the gas should be nearly identical for both cases because
4555        // both go through the same validate_aa_initial_tx_gas branch and handler.rs
4556        // no longer fires for transfers.
4557        // Before the fix gas_2d would have been ~250k higher than gas_baseline.
4558        let diff = gas_2d.saturating_sub(gas_baseline);
4559        assert!(
4560            diff < 10_000,
4561            "calls-format transfer with nonceKey={nonce_key} (gas={gas_2d}) must not cost \
4562             ~250k more than baseline (gas={gas_baseline}, diff={diff}). \
4563             A diff near 250_000 means new_account_cost is incorrectly added for \
4564             transfers (issue #3178)."
4565        );
4566
4567        Ok(())
4568    }
4569}