op_revm/
handler.rs

1//!Handler related to Optimism chain
2use crate::{
3    api::exec::OpContextTr,
4    constants::{BASE_FEE_RECIPIENT, L1_FEE_RECIPIENT, OPERATOR_FEE_RECIPIENT},
5    transaction::{deposit::DEPOSIT_TRANSACTION_TYPE, OpTransactionError, OpTxTr},
6    L1BlockInfo, OpHaltReason, OpSpecId,
7};
8use revm::{
9    context::{journaled_state::JournalCheckpoint, result::InvalidTransaction, LocalContextTr},
10    context_interface::{
11        context::ContextError,
12        result::{EVMError, ExecutionResult, FromStringError},
13        Block, Cfg, ContextTr, JournalTr, Transaction,
14    },
15    handler::{
16        evm::FrameTr,
17        handler::EvmTrError,
18        post_execution::{self, reimburse_caller},
19        pre_execution::{calculate_caller_fee, validate_account_nonce_and_code_with_components},
20        EthFrame, EvmTr, FrameResult, Handler, MainnetHandler,
21    },
22    inspector::{Inspector, InspectorEvmTr, InspectorHandler},
23    interpreter::{interpreter::EthInterpreter, interpreter_action::FrameInit, Gas},
24    primitives::{hardfork::SpecId, U256},
25};
26use std::boxed::Box;
27
28/// Optimism handler extends the [`Handler`] with Optimism specific logic.
29#[derive(Debug, Clone)]
30pub struct OpHandler<EVM, ERROR, FRAME> {
31    /// Mainnet handler allows us to use functions from the mainnet handler inside optimism handler.
32    /// So we dont duplicate the logic
33    pub mainnet: MainnetHandler<EVM, ERROR, FRAME>,
34}
35
36impl<EVM, ERROR, FRAME> OpHandler<EVM, ERROR, FRAME> {
37    /// Create a new Optimism handler.
38    pub fn new() -> Self {
39        Self {
40            mainnet: MainnetHandler::default(),
41        }
42    }
43}
44
45impl<EVM, ERROR, FRAME> Default for OpHandler<EVM, ERROR, FRAME> {
46    fn default() -> Self {
47        Self::new()
48    }
49}
50
51/// Trait to check if the error is a transaction error.
52///
53/// Used in cache_error handler to catch deposit transaction that was halted.
54pub trait IsTxError {
55    /// Check if the error is a transaction error.
56    fn is_tx_error(&self) -> bool;
57}
58
59impl<DB, TX> IsTxError for EVMError<DB, TX> {
60    fn is_tx_error(&self) -> bool {
61        matches!(self, EVMError::Transaction(_))
62    }
63}
64
65impl<EVM, ERROR, FRAME> Handler for OpHandler<EVM, ERROR, FRAME>
66where
67    EVM: EvmTr<Context: OpContextTr, Frame = FRAME>,
68    ERROR: EvmTrError<EVM> + From<OpTransactionError> + FromStringError + IsTxError,
69    // TODO `FrameResult` should be a generic trait.
70    // TODO `FrameInit` should be a generic.
71    FRAME: FrameTr<FrameResult = FrameResult, FrameInit = FrameInit>,
72{
73    type Evm = EVM;
74    type Error = ERROR;
75    type HaltReason = OpHaltReason;
76
77    fn validate_env(&self, evm: &mut Self::Evm) -> Result<(), Self::Error> {
78        // Do not perform any extra validation for deposit transactions, they are pre-verified on L1.
79        let ctx = evm.ctx();
80        let tx = ctx.tx();
81        let tx_type = tx.tx_type();
82        if tx_type == DEPOSIT_TRANSACTION_TYPE {
83            // Do not allow for a system transaction to be processed if Regolith is enabled.
84            if tx.is_system_transaction()
85                && evm.ctx().cfg().spec().is_enabled_in(OpSpecId::REGOLITH)
86            {
87                return Err(OpTransactionError::DepositSystemTxPostRegolith.into());
88            }
89            return Ok(());
90        }
91
92        // Check that non-deposit transactions have enveloped_tx set
93        if tx.enveloped_tx().is_none() {
94            return Err(OpTransactionError::MissingEnvelopedTx.into());
95        }
96
97        self.mainnet.validate_env(evm)
98    }
99
100    fn validate_against_state_and_deduct_caller(
101        &self,
102        evm: &mut Self::Evm,
103    ) -> Result<(), Self::Error> {
104        let (block, tx, cfg, journal, chain, _) = evm.ctx().all_mut();
105        let spec = cfg.spec();
106
107        if tx.tx_type() == DEPOSIT_TRANSACTION_TYPE {
108            let basefee = block.basefee() as u128;
109            let blob_price = block.blob_gasprice().unwrap_or_default();
110            // deposit skips max fee check and just deducts the effective balance spending.
111
112            let mut caller = journal.load_account_with_code_mut(tx.caller())?.data;
113
114            let effective_balance_spending = tx
115                .effective_balance_spending(basefee, blob_price)
116                .expect("Deposit transaction effective balance spending overflow")
117                - tx.value();
118
119            // Mind value should be added first before subtracting the effective balance spending.
120            let mut new_balance = caller
121                .balance()
122                .saturating_add(U256::from(tx.mint().unwrap_or_default()))
123                .saturating_sub(effective_balance_spending);
124
125            if cfg.is_balance_check_disabled() {
126                // Make sure the caller's balance is at least the value of the transaction.
127                // this is not consensus critical, and it is used in testing.
128                new_balance = new_balance.max(tx.value());
129            }
130
131            // set the new balance and bump the nonce if it is a call
132            caller.set_balance(new_balance);
133            if tx.kind().is_call() {
134                caller.bump_nonce();
135            }
136
137            return Ok(());
138        }
139
140        // L1 block info is stored in the context for later use.
141        // and it will be reloaded from the database if it is not for the current block.
142        if chain.l2_block != Some(block.number()) {
143            *chain = L1BlockInfo::try_fetch(journal.db_mut(), block.number(), spec)?;
144        }
145
146        let mut caller_account = journal.load_account_with_code_mut(tx.caller())?.data;
147
148        // validates account nonce and code
149        validate_account_nonce_and_code_with_components(&caller_account.info, tx, cfg)?;
150
151        // check additional cost and deduct it from the caller's balances
152        let mut balance = caller_account.info.balance;
153
154        if !cfg.is_fee_charge_disabled() {
155            let additional_cost = chain.tx_cost_with_tx(tx, spec);
156            let Some(new_balance) = balance.checked_sub(additional_cost) else {
157                return Err(InvalidTransaction::LackOfFundForMaxFee {
158                    fee: Box::new(additional_cost),
159                    balance: Box::new(balance),
160                }
161                .into());
162            };
163            balance = new_balance
164        }
165
166        let balance = calculate_caller_fee(balance, tx, block, cfg)?;
167
168        // make changes to the account
169        caller_account.set_balance(balance);
170        if tx.kind().is_call() {
171            caller_account.bump_nonce();
172        }
173
174        Ok(())
175    }
176
177    fn last_frame_result(
178        &mut self,
179        evm: &mut Self::Evm,
180        frame_result: &mut <<Self::Evm as EvmTr>::Frame as FrameTr>::FrameResult,
181    ) -> Result<(), Self::Error> {
182        let ctx = evm.ctx();
183        let tx = ctx.tx();
184        let is_deposit = tx.tx_type() == DEPOSIT_TRANSACTION_TYPE;
185        let tx_gas_limit = tx.gas_limit();
186        let is_regolith = ctx.cfg().spec().is_enabled_in(OpSpecId::REGOLITH);
187
188        let instruction_result = frame_result.interpreter_result().result;
189        let gas = frame_result.gas_mut();
190        let remaining = gas.remaining();
191        let refunded = gas.refunded();
192
193        // Spend the gas limit. Gas is reimbursed when the tx returns successfully.
194        *gas = Gas::new_spent(tx_gas_limit);
195
196        if instruction_result.is_ok() {
197            // On Optimism, deposit transactions report gas usage uniquely to other
198            // transactions due to them being pre-paid on L1.
199            //
200            // Hardfork Behavior:
201            // - Bedrock (success path):
202            //   - Deposit transactions (non-system) report their gas limit as the usage.
203            //     No refunds.
204            //   - Deposit transactions (system) report 0 gas used. No refunds.
205            //   - Regular transactions report gas usage as normal.
206            // - Regolith (success path):
207            //   - Deposit transactions (all) report their gas used as normal. Refunds
208            //     enabled.
209            //   - Regular transactions report their gas used as normal.
210            if !is_deposit || is_regolith {
211                // For regular transactions prior to Regolith and all transactions after
212                // Regolith, gas is reported as normal.
213                gas.erase_cost(remaining);
214                gas.record_refund(refunded);
215            } else if is_deposit {
216                let tx = ctx.tx();
217                if tx.is_system_transaction() {
218                    // System transactions were a special type of deposit transaction in
219                    // the Bedrock hardfork that did not incur any gas costs.
220                    gas.erase_cost(tx_gas_limit);
221                }
222            }
223        } else if instruction_result.is_revert() {
224            // On Optimism, deposit transactions report gas usage uniquely to other
225            // transactions due to them being pre-paid on L1.
226            //
227            // Hardfork Behavior:
228            // - Bedrock (revert path):
229            //   - Deposit transactions (all) report the gas limit as the amount of gas
230            //     used on failure. No refunds.
231            //   - Regular transactions receive a refund on remaining gas as normal.
232            // - Regolith (revert path):
233            //   - Deposit transactions (all) report the actual gas used as the amount of
234            //     gas used on failure. Refunds on remaining gas enabled.
235            //   - Regular transactions receive a refund on remaining gas as normal.
236            if !is_deposit || is_regolith {
237                gas.erase_cost(remaining);
238            }
239        }
240        Ok(())
241    }
242
243    fn reimburse_caller(
244        &self,
245        evm: &mut Self::Evm,
246        frame_result: &mut <<Self::Evm as EvmTr>::Frame as FrameTr>::FrameResult,
247    ) -> Result<(), Self::Error> {
248        let mut additional_refund = U256::ZERO;
249
250        if evm.ctx().tx().tx_type() != DEPOSIT_TRANSACTION_TYPE
251            && !evm.ctx().cfg().is_fee_charge_disabled()
252        {
253            let spec = evm.ctx().cfg().spec();
254            additional_refund = evm
255                .ctx()
256                .chain()
257                .operator_fee_refund(frame_result.gas(), spec);
258        }
259
260        reimburse_caller(evm.ctx(), frame_result.gas(), additional_refund).map_err(From::from)
261    }
262
263    fn refund(
264        &self,
265        evm: &mut Self::Evm,
266        frame_result: &mut <<Self::Evm as EvmTr>::Frame as FrameTr>::FrameResult,
267        eip7702_refund: i64,
268    ) {
269        frame_result.gas_mut().record_refund(eip7702_refund);
270
271        let is_deposit = evm.ctx().tx().tx_type() == DEPOSIT_TRANSACTION_TYPE;
272        let is_regolith = evm.ctx().cfg().spec().is_enabled_in(OpSpecId::REGOLITH);
273
274        // Prior to Regolith, deposit transactions did not receive gas refunds.
275        let is_gas_refund_disabled = is_deposit && !is_regolith;
276        if !is_gas_refund_disabled {
277            frame_result.gas_mut().set_final_refund(
278                evm.ctx()
279                    .cfg()
280                    .spec()
281                    .into_eth_spec()
282                    .is_enabled_in(SpecId::LONDON),
283            );
284        }
285    }
286
287    fn reward_beneficiary(
288        &self,
289        evm: &mut Self::Evm,
290        frame_result: &mut <<Self::Evm as EvmTr>::Frame as FrameTr>::FrameResult,
291    ) -> Result<(), Self::Error> {
292        let is_deposit = evm.ctx().tx().tx_type() == DEPOSIT_TRANSACTION_TYPE;
293
294        // Transfer fee to coinbase/beneficiary.
295        if is_deposit {
296            return Ok(());
297        }
298
299        self.mainnet.reward_beneficiary(evm, frame_result)?;
300        let basefee = evm.ctx().block().basefee() as u128;
301
302        // If the transaction is not a deposit transaction, fees are paid out
303        // to both the Base Fee Vault as well as the L1 Fee Vault.
304        let ctx = evm.ctx();
305        let enveloped = ctx.tx().enveloped_tx().cloned();
306        let spec = ctx.cfg().spec();
307        let l1_block_info = ctx.chain_mut();
308
309        let Some(enveloped_tx) = &enveloped else {
310            return Err(ERROR::from_string(
311                "[OPTIMISM] Failed to load enveloped transaction.".into(),
312            ));
313        };
314
315        let l1_cost = l1_block_info.calculate_tx_l1_cost(enveloped_tx, spec);
316        let operator_fee_cost = if spec.is_enabled_in(OpSpecId::ISTHMUS) {
317            l1_block_info.operator_fee_charge(
318                enveloped_tx,
319                U256::from(frame_result.gas().used()),
320                spec,
321            )
322        } else {
323            U256::ZERO
324        };
325        let base_fee_amount = U256::from(basefee.saturating_mul(frame_result.gas().used() as u128));
326
327        // Send fees to their respective recipients
328        for (recipient, amount) in [
329            (L1_FEE_RECIPIENT, l1_cost),
330            (BASE_FEE_RECIPIENT, base_fee_amount),
331            (OPERATOR_FEE_RECIPIENT, operator_fee_cost),
332        ] {
333            ctx.journal_mut().balance_incr(recipient, amount)?;
334        }
335
336        Ok(())
337    }
338
339    fn execution_result(
340        &mut self,
341        evm: &mut Self::Evm,
342        frame_result: <<Self::Evm as EvmTr>::Frame as FrameTr>::FrameResult,
343    ) -> Result<ExecutionResult<Self::HaltReason>, Self::Error> {
344        match core::mem::replace(evm.ctx().error(), Ok(())) {
345            Err(ContextError::Db(e)) => return Err(e.into()),
346            Err(ContextError::Custom(e)) => return Err(Self::Error::from_string(e)),
347            Ok(_) => (),
348        }
349
350        let exec_result =
351            post_execution::output(evm.ctx(), frame_result).map_haltreason(OpHaltReason::Base);
352
353        if exec_result.is_halt() {
354            // Post-regolith, if the transaction is a deposit transaction and it halts,
355            // we bubble up to the global return handler. The mint value will be persisted
356            // and the caller nonce will be incremented there.
357            let is_deposit = evm.ctx().tx().tx_type() == DEPOSIT_TRANSACTION_TYPE;
358            if is_deposit && evm.ctx().cfg().spec().is_enabled_in(OpSpecId::REGOLITH) {
359                return Err(ERROR::from(OpTransactionError::HaltedDepositPostRegolith));
360            }
361        }
362        evm.ctx().journal_mut().commit_tx();
363        evm.ctx().chain_mut().clear_tx_l1_cost();
364        evm.ctx().local_mut().clear();
365        evm.frame_stack().clear();
366
367        Ok(exec_result)
368    }
369
370    fn catch_error(
371        &self,
372        evm: &mut Self::Evm,
373        error: Self::Error,
374    ) -> Result<ExecutionResult<Self::HaltReason>, Self::Error> {
375        let is_deposit = evm.ctx().tx().tx_type() == DEPOSIT_TRANSACTION_TYPE;
376        let output = if error.is_tx_error() && is_deposit {
377            let ctx = evm.ctx();
378            let spec = ctx.cfg().spec();
379            let tx = ctx.tx();
380            let caller = tx.caller();
381            let mint = tx.mint();
382            let is_system_tx = tx.is_system_transaction();
383            let gas_limit = tx.gas_limit();
384            let journal = evm.ctx().journal_mut();
385
386            // discard all changes of this transaction
387            // Default JournalCheckpoint is the first checkpoint and will wipe all changes.
388            journal.checkpoint_revert(JournalCheckpoint::default());
389
390            // If the transaction is a deposit transaction and it failed
391            // for any reason, the caller nonce must be bumped, and the
392            // gas reported must be altered depending on the Hardfork. This is
393            // also returned as a special Halt variant so that consumers can more
394            // easily distinguish between a failed deposit and a failed
395            // normal transaction.
396
397            // Increment sender nonce and account balance for the mint amount. Deposits
398            // always persist the mint amount, even if the transaction fails.
399            let mut acc = journal.load_account_mut(caller)?;
400            acc.bump_nonce();
401            acc.incr_balance(U256::from(mint.unwrap_or_default()));
402
403            // We can now commit the changes.
404            journal.commit_tx();
405
406            // The gas used of a failed deposit post-regolith is the gas
407            // limit of the transaction. pre-regolith, it is the gas limit
408            // of the transaction for non system transactions and 0 for system
409            // transactions.
410            let gas_used = if spec.is_enabled_in(OpSpecId::REGOLITH) || !is_system_tx {
411                gas_limit
412            } else {
413                0
414            };
415            // clear the journal
416            Ok(ExecutionResult::Halt {
417                reason: OpHaltReason::FailedDeposit,
418                gas_used,
419            })
420        } else {
421            Err(error)
422        };
423        // do the cleanup
424        evm.ctx().chain_mut().clear_tx_l1_cost();
425        evm.ctx().local_mut().clear();
426        evm.frame_stack().clear();
427
428        output
429    }
430}
431
432impl<EVM, ERROR> InspectorHandler for OpHandler<EVM, ERROR, EthFrame<EthInterpreter>>
433where
434    EVM: InspectorEvmTr<
435        Context: OpContextTr,
436        Frame = EthFrame<EthInterpreter>,
437        Inspector: Inspector<<<Self as Handler>::Evm as EvmTr>::Context, EthInterpreter>,
438    >,
439    ERROR: EvmTrError<EVM> + From<OpTransactionError> + FromStringError + IsTxError,
440{
441    type IT = EthInterpreter;
442}
443
444#[cfg(test)]
445mod tests {
446    use super::*;
447    use crate::{
448        api::default_ctx::OpContext,
449        constants::{
450            BASE_FEE_SCALAR_OFFSET, ECOTONE_L1_BLOB_BASE_FEE_SLOT, ECOTONE_L1_FEE_SCALARS_SLOT,
451            L1_BASE_FEE_SLOT, L1_BLOCK_CONTRACT, OPERATOR_FEE_SCALARS_SLOT,
452        },
453        DefaultOp, OpBuilder, OpTransaction,
454    };
455    use alloy_primitives::uint;
456    use revm::{
457        context::{BlockEnv, Context, TxEnv},
458        context_interface::result::InvalidTransaction,
459        database::InMemoryDB,
460        database_interface::EmptyDB,
461        handler::EthFrame,
462        interpreter::{CallOutcome, InstructionResult, InterpreterResult},
463        primitives::{bytes, Address, Bytes, B256},
464        state::AccountInfo,
465    };
466    use rstest::rstest;
467    use std::boxed::Box;
468
469    /// Creates frame result.
470    fn call_last_frame_return(
471        ctx: OpContext<EmptyDB>,
472        instruction_result: InstructionResult,
473        gas: Gas,
474    ) -> Gas {
475        let mut evm = ctx.build_op();
476
477        let mut exec_result = FrameResult::Call(CallOutcome::new(
478            InterpreterResult {
479                result: instruction_result,
480                output: Bytes::new(),
481                gas,
482            },
483            0..0,
484        ));
485
486        let mut handler =
487            OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
488
489        handler
490            .last_frame_result(&mut evm, &mut exec_result)
491            .unwrap();
492        handler.refund(&mut evm, &mut exec_result, 0);
493        *exec_result.gas()
494    }
495
496    #[test]
497    fn test_revert_gas() {
498        let ctx = Context::op()
499            .with_tx(
500                OpTransaction::builder()
501                    .base(TxEnv::builder().gas_limit(100))
502                    .build_fill(),
503            )
504            .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::BEDROCK);
505
506        let gas = call_last_frame_return(ctx, InstructionResult::Revert, Gas::new(90));
507        assert_eq!(gas.remaining(), 90);
508        assert_eq!(gas.spent(), 10);
509        assert_eq!(gas.refunded(), 0);
510    }
511
512    #[test]
513    fn test_consume_gas() {
514        let ctx = Context::op()
515            .with_tx(
516                OpTransaction::builder()
517                    .base(TxEnv::builder().gas_limit(100))
518                    .build_fill(),
519            )
520            .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::REGOLITH);
521
522        let gas = call_last_frame_return(ctx, InstructionResult::Stop, Gas::new(90));
523        assert_eq!(gas.remaining(), 90);
524        assert_eq!(gas.spent(), 10);
525        assert_eq!(gas.refunded(), 0);
526    }
527
528    #[test]
529    fn test_consume_gas_with_refund() {
530        let ctx = Context::op()
531            .with_tx(
532                OpTransaction::builder()
533                    .base(TxEnv::builder().gas_limit(100))
534                    .source_hash(B256::from([1u8; 32]))
535                    .build_fill(),
536            )
537            .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::REGOLITH);
538
539        let mut ret_gas = Gas::new(90);
540        ret_gas.record_refund(20);
541
542        let gas = call_last_frame_return(ctx.clone(), InstructionResult::Stop, ret_gas);
543        assert_eq!(gas.remaining(), 90);
544        assert_eq!(gas.spent(), 10);
545        assert_eq!(gas.refunded(), 2); // min(20, 10/5)
546
547        let gas = call_last_frame_return(ctx, InstructionResult::Revert, ret_gas);
548        assert_eq!(gas.remaining(), 90);
549        assert_eq!(gas.spent(), 10);
550        assert_eq!(gas.refunded(), 0);
551    }
552
553    #[test]
554    fn test_consume_gas_deposit_tx() {
555        let ctx = Context::op()
556            .with_tx(
557                OpTransaction::builder()
558                    .base(TxEnv::builder().gas_limit(100))
559                    .source_hash(B256::from([1u8; 32]))
560                    .build_fill(),
561            )
562            .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::BEDROCK);
563        let gas = call_last_frame_return(ctx, InstructionResult::Stop, Gas::new(90));
564        assert_eq!(gas.remaining(), 0);
565        assert_eq!(gas.spent(), 100);
566        assert_eq!(gas.refunded(), 0);
567    }
568
569    #[test]
570    fn test_consume_gas_sys_deposit_tx() {
571        let ctx = Context::op()
572            .with_tx(
573                OpTransaction::builder()
574                    .base(TxEnv::builder().gas_limit(100))
575                    .source_hash(B256::from([1u8; 32]))
576                    .is_system_transaction()
577                    .build_fill(),
578            )
579            .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::BEDROCK);
580        let gas = call_last_frame_return(ctx, InstructionResult::Stop, Gas::new(90));
581        assert_eq!(gas.remaining(), 100);
582        assert_eq!(gas.spent(), 0);
583        assert_eq!(gas.refunded(), 0);
584    }
585
586    #[test]
587    fn test_commit_mint_value() {
588        let caller = Address::ZERO;
589        let mut db = InMemoryDB::default();
590        db.insert_account_info(
591            caller,
592            AccountInfo {
593                balance: U256::from(1000),
594                ..Default::default()
595            },
596        );
597
598        let mut ctx = Context::op()
599            .with_db(db)
600            .with_chain(L1BlockInfo {
601                l1_base_fee: U256::from(1_000),
602                l1_fee_overhead: Some(U256::from(1_000)),
603                l1_base_fee_scalar: U256::from(1_000),
604                ..Default::default()
605            })
606            .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::REGOLITH);
607        ctx.modify_tx(|tx| {
608            tx.deposit.source_hash = B256::from([1u8; 32]);
609            tx.deposit.mint = Some(10);
610        });
611
612        let mut evm = ctx.build_op();
613
614        let handler =
615            OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
616        handler
617            .validate_against_state_and_deduct_caller(&mut evm)
618            .unwrap();
619
620        // Check the account balance is updated.
621        let account = evm.ctx().journal_mut().load_account(caller).unwrap();
622        assert_eq!(account.info.balance, U256::from(1010));
623    }
624
625    #[test]
626    fn test_remove_l1_cost_non_deposit() {
627        let caller = Address::ZERO;
628        let mut db = InMemoryDB::default();
629        db.insert_account_info(
630            caller,
631            AccountInfo {
632                balance: U256::from(1058), // Increased to cover L1 fees (1048) + base fees
633                ..Default::default()
634            },
635        );
636        let ctx = Context::op()
637            .with_db(db)
638            .with_chain(L1BlockInfo {
639                l1_base_fee: U256::from(1_000),
640                l1_fee_overhead: Some(U256::from(1_000)),
641                l1_base_fee_scalar: U256::from(1_000),
642                l2_block: Some(U256::from(0)),
643                ..Default::default()
644            })
645            .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::REGOLITH)
646            .with_tx(
647                OpTransaction::builder()
648                    .base(TxEnv::builder().gas_limit(100))
649                    .enveloped_tx(Some(bytes!("FACADE")))
650                    .source_hash(B256::ZERO)
651                    .build()
652                    .unwrap(),
653            );
654
655        let mut evm = ctx.build_op();
656
657        let handler =
658            OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
659        handler
660            .validate_against_state_and_deduct_caller(&mut evm)
661            .unwrap();
662
663        // Check the account balance is updated.
664        let account = evm.ctx().journal_mut().load_account(caller).unwrap();
665        assert_eq!(account.info.balance, U256::from(10)); // 1058 - 1048 = 10
666    }
667
668    #[test]
669    fn test_reload_l1_block_info_isthmus() {
670        const BLOCK_NUM: U256 = uint!(100_U256);
671        const L1_BASE_FEE: U256 = uint!(1_U256);
672        const L1_BLOB_BASE_FEE: U256 = uint!(2_U256);
673        const L1_BASE_FEE_SCALAR: u64 = 3;
674        const L1_BLOB_BASE_FEE_SCALAR: u64 = 4;
675        const L1_FEE_SCALARS: U256 = U256::from_limbs([
676            0,
677            (L1_BASE_FEE_SCALAR << (64 - BASE_FEE_SCALAR_OFFSET * 2)) | L1_BLOB_BASE_FEE_SCALAR,
678            0,
679            0,
680        ]);
681        const OPERATOR_FEE_SCALAR: u64 = 5;
682        const OPERATOR_FEE_CONST: u64 = 6;
683        const OPERATOR_FEE: U256 =
684            U256::from_limbs([OPERATOR_FEE_CONST, OPERATOR_FEE_SCALAR, 0, 0]);
685
686        let mut db = InMemoryDB::default();
687        let l1_block_contract = db.load_account(L1_BLOCK_CONTRACT).unwrap();
688        l1_block_contract
689            .storage
690            .insert(L1_BASE_FEE_SLOT, L1_BASE_FEE);
691        l1_block_contract
692            .storage
693            .insert(ECOTONE_L1_BLOB_BASE_FEE_SLOT, L1_BLOB_BASE_FEE);
694        l1_block_contract
695            .storage
696            .insert(ECOTONE_L1_FEE_SCALARS_SLOT, L1_FEE_SCALARS);
697        l1_block_contract
698            .storage
699            .insert(OPERATOR_FEE_SCALARS_SLOT, OPERATOR_FEE);
700        db.insert_account_info(
701            Address::ZERO,
702            AccountInfo {
703                balance: U256::from(1000),
704                ..Default::default()
705            },
706        );
707
708        let ctx = Context::op()
709            .with_db(db)
710            .with_chain(L1BlockInfo {
711                l2_block: Some(BLOCK_NUM + U256::from(1)), // ahead by one block
712                ..Default::default()
713            })
714            .with_block(BlockEnv {
715                number: BLOCK_NUM,
716                ..Default::default()
717            })
718            .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::ISTHMUS);
719
720        let mut evm = ctx.build_op();
721
722        assert_ne!(evm.ctx().chain().l2_block, Some(BLOCK_NUM));
723
724        let handler =
725            OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
726        handler
727            .validate_against_state_and_deduct_caller(&mut evm)
728            .unwrap();
729
730        assert_eq!(
731            *evm.ctx().chain(),
732            L1BlockInfo {
733                l2_block: Some(BLOCK_NUM),
734                l1_base_fee: L1_BASE_FEE,
735                l1_base_fee_scalar: U256::from(L1_BASE_FEE_SCALAR),
736                l1_blob_base_fee: Some(L1_BLOB_BASE_FEE),
737                l1_blob_base_fee_scalar: Some(U256::from(L1_BLOB_BASE_FEE_SCALAR)),
738                empty_ecotone_scalars: false,
739                l1_fee_overhead: None,
740                operator_fee_scalar: Some(U256::from(OPERATOR_FEE_SCALAR)),
741                operator_fee_constant: Some(U256::from(OPERATOR_FEE_CONST)),
742                tx_l1_cost: Some(U256::ZERO),
743                da_footprint_gas_scalar: None
744            }
745        );
746    }
747
748    #[test]
749    fn test_parse_da_footprint_gas_scalar_jovian() {
750        const BLOCK_NUM: U256 = uint!(100_U256);
751        const L1_BASE_FEE: U256 = uint!(1_U256);
752        const L1_BLOB_BASE_FEE: U256 = uint!(2_U256);
753        const L1_BASE_FEE_SCALAR: u64 = 3;
754        const L1_BLOB_BASE_FEE_SCALAR: u64 = 4;
755        const L1_FEE_SCALARS: U256 = U256::from_limbs([
756            0,
757            (L1_BASE_FEE_SCALAR << (64 - BASE_FEE_SCALAR_OFFSET * 2)) | L1_BLOB_BASE_FEE_SCALAR,
758            0,
759            0,
760        ]);
761        const OPERATOR_FEE_SCALAR: u8 = 5;
762        const OPERATOR_FEE_CONST: u8 = 6;
763        const DA_FOOTPRINT_GAS_SCALAR: u8 = 7;
764        let mut operator_fee_and_da_footprint = [0u8; 32];
765        operator_fee_and_da_footprint[31] = OPERATOR_FEE_CONST;
766        operator_fee_and_da_footprint[23] = OPERATOR_FEE_SCALAR;
767        operator_fee_and_da_footprint[19] = DA_FOOTPRINT_GAS_SCALAR;
768        let operator_fee_and_da_footprint_u256 = U256::from_be_bytes(operator_fee_and_da_footprint);
769
770        let mut db = InMemoryDB::default();
771        let l1_block_contract = db.load_account(L1_BLOCK_CONTRACT).unwrap();
772        l1_block_contract
773            .storage
774            .insert(L1_BASE_FEE_SLOT, L1_BASE_FEE);
775        l1_block_contract
776            .storage
777            .insert(ECOTONE_L1_BLOB_BASE_FEE_SLOT, L1_BLOB_BASE_FEE);
778        l1_block_contract
779            .storage
780            .insert(ECOTONE_L1_FEE_SCALARS_SLOT, L1_FEE_SCALARS);
781        l1_block_contract.storage.insert(
782            OPERATOR_FEE_SCALARS_SLOT,
783            operator_fee_and_da_footprint_u256,
784        );
785        db.insert_account_info(
786            Address::ZERO,
787            AccountInfo {
788                balance: U256::from(6000),
789                ..Default::default()
790            },
791        );
792
793        let ctx = Context::op()
794            .with_db(db)
795            .with_chain(L1BlockInfo {
796                l2_block: Some(BLOCK_NUM + U256::from(1)), // ahead by one block
797                operator_fee_scalar: Some(U256::from(2)),
798                operator_fee_constant: Some(U256::from(50)),
799                ..Default::default()
800            })
801            .with_block(BlockEnv {
802                number: BLOCK_NUM,
803                ..Default::default()
804            })
805            .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::JOVIAN)
806            // set the operator fee to a low value
807            .with_tx(
808                OpTransaction::builder()
809                    .base(TxEnv::builder().gas_limit(10))
810                    .enveloped_tx(Some(bytes!("FACADE")))
811                    .build_fill(),
812            );
813
814        let mut evm = ctx.build_op();
815
816        assert_ne!(evm.ctx().chain().l2_block, Some(BLOCK_NUM));
817
818        let handler =
819            OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
820        handler
821            .validate_against_state_and_deduct_caller(&mut evm)
822            .unwrap();
823
824        assert_eq!(
825            *evm.ctx().chain(),
826            L1BlockInfo {
827                l2_block: Some(BLOCK_NUM),
828                l1_base_fee: L1_BASE_FEE,
829                l1_base_fee_scalar: U256::from(L1_BASE_FEE_SCALAR),
830                l1_blob_base_fee: Some(L1_BLOB_BASE_FEE),
831                l1_blob_base_fee_scalar: Some(U256::from(L1_BLOB_BASE_FEE_SCALAR)),
832                empty_ecotone_scalars: false,
833                l1_fee_overhead: None,
834                operator_fee_scalar: Some(U256::from(OPERATOR_FEE_SCALAR)),
835                operator_fee_constant: Some(U256::from(OPERATOR_FEE_CONST)),
836                tx_l1_cost: Some(U256::ZERO),
837                da_footprint_gas_scalar: Some(DA_FOOTPRINT_GAS_SCALAR as u16),
838            }
839        );
840    }
841
842    #[test]
843    fn test_reload_l1_block_info_regolith() {
844        const BLOCK_NUM: U256 = uint!(200_U256);
845        const L1_BASE_FEE: U256 = uint!(7_U256);
846        const L1_FEE_OVERHEAD: U256 = uint!(9_U256);
847        const L1_BASE_FEE_SCALAR: u64 = 11;
848
849        let mut db = InMemoryDB::default();
850        let l1_block_contract = db.load_account(L1_BLOCK_CONTRACT).unwrap();
851        l1_block_contract
852            .storage
853            .insert(L1_BASE_FEE_SLOT, L1_BASE_FEE);
854        // Pre-ecotone bedrock/regolith slots
855        use crate::constants::{L1_OVERHEAD_SLOT, L1_SCALAR_SLOT};
856        l1_block_contract
857            .storage
858            .insert(L1_OVERHEAD_SLOT, L1_FEE_OVERHEAD);
859        l1_block_contract
860            .storage
861            .insert(L1_SCALAR_SLOT, U256::from(L1_BASE_FEE_SCALAR));
862
863        let ctx = Context::op()
864            .with_db(db)
865            .with_chain(L1BlockInfo {
866                l2_block: Some(BLOCK_NUM + U256::from(1)),
867                ..Default::default()
868            })
869            .with_block(BlockEnv {
870                number: BLOCK_NUM,
871                ..Default::default()
872            })
873            .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::REGOLITH);
874
875        let mut evm = ctx.build_op();
876        assert_ne!(evm.ctx().chain().l2_block, Some(BLOCK_NUM));
877
878        let handler =
879            OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
880        handler
881            .validate_against_state_and_deduct_caller(&mut evm)
882            .unwrap();
883
884        assert_eq!(
885            *evm.ctx().chain(),
886            L1BlockInfo {
887                l2_block: Some(BLOCK_NUM),
888                l1_base_fee: L1_BASE_FEE,
889                l1_fee_overhead: Some(L1_FEE_OVERHEAD),
890                l1_base_fee_scalar: U256::from(L1_BASE_FEE_SCALAR),
891                tx_l1_cost: Some(U256::ZERO),
892                ..Default::default()
893            }
894        );
895    }
896
897    #[test]
898    fn test_reload_l1_block_info_ecotone_pre_isthmus() {
899        const BLOCK_NUM: U256 = uint!(300_U256);
900        const L1_BASE_FEE: U256 = uint!(13_U256);
901        const L1_BLOB_BASE_FEE: U256 = uint!(17_U256);
902        const L1_BASE_FEE_SCALAR: u64 = 19;
903        const L1_BLOB_BASE_FEE_SCALAR: u64 = 23;
904        const L1_FEE_SCALARS: U256 = U256::from_limbs([
905            0,
906            (L1_BASE_FEE_SCALAR << (64 - BASE_FEE_SCALAR_OFFSET * 2)) | L1_BLOB_BASE_FEE_SCALAR,
907            0,
908            0,
909        ]);
910
911        let mut db = InMemoryDB::default();
912        let l1_block_contract = db.load_account(L1_BLOCK_CONTRACT).unwrap();
913        l1_block_contract
914            .storage
915            .insert(L1_BASE_FEE_SLOT, L1_BASE_FEE);
916        l1_block_contract
917            .storage
918            .insert(ECOTONE_L1_BLOB_BASE_FEE_SLOT, L1_BLOB_BASE_FEE);
919        l1_block_contract
920            .storage
921            .insert(ECOTONE_L1_FEE_SCALARS_SLOT, L1_FEE_SCALARS);
922
923        let ctx = Context::op()
924            .with_db(db)
925            .with_chain(L1BlockInfo {
926                l2_block: Some(BLOCK_NUM + U256::from(1)),
927                ..Default::default()
928            })
929            .with_block(BlockEnv {
930                number: BLOCK_NUM,
931                ..Default::default()
932            })
933            .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::ECOTONE);
934
935        let mut evm = ctx.build_op();
936        assert_ne!(evm.ctx().chain().l2_block, Some(BLOCK_NUM));
937
938        let handler =
939            OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
940        handler
941            .validate_against_state_and_deduct_caller(&mut evm)
942            .unwrap();
943
944        assert_eq!(
945            *evm.ctx().chain(),
946            L1BlockInfo {
947                l2_block: Some(BLOCK_NUM),
948                l1_base_fee: L1_BASE_FEE,
949                l1_base_fee_scalar: U256::from(L1_BASE_FEE_SCALAR),
950                l1_blob_base_fee: Some(L1_BLOB_BASE_FEE),
951                l1_blob_base_fee_scalar: Some(U256::from(L1_BLOB_BASE_FEE_SCALAR)),
952                empty_ecotone_scalars: false,
953                l1_fee_overhead: None,
954                tx_l1_cost: Some(U256::ZERO),
955                ..Default::default()
956            }
957        );
958    }
959
960    #[test]
961    fn test_load_l1_block_info_isthmus_none() {
962        const BLOCK_NUM: U256 = uint!(100_U256);
963        const L1_BASE_FEE: U256 = uint!(1_U256);
964        const L1_BLOB_BASE_FEE: U256 = uint!(2_U256);
965        const L1_BASE_FEE_SCALAR: u64 = 3;
966        const L1_BLOB_BASE_FEE_SCALAR: u64 = 4;
967        const L1_FEE_SCALARS: U256 = U256::from_limbs([
968            0,
969            (L1_BASE_FEE_SCALAR << (64 - BASE_FEE_SCALAR_OFFSET * 2)) | L1_BLOB_BASE_FEE_SCALAR,
970            0,
971            0,
972        ]);
973        const OPERATOR_FEE_SCALAR: u64 = 5;
974        const OPERATOR_FEE_CONST: u64 = 6;
975        const OPERATOR_FEE: U256 =
976            U256::from_limbs([OPERATOR_FEE_CONST, OPERATOR_FEE_SCALAR, 0, 0]);
977
978        let mut db = InMemoryDB::default();
979        let l1_block_contract = db.load_account(L1_BLOCK_CONTRACT).unwrap();
980        l1_block_contract
981            .storage
982            .insert(L1_BASE_FEE_SLOT, L1_BASE_FEE);
983        l1_block_contract
984            .storage
985            .insert(ECOTONE_L1_BLOB_BASE_FEE_SLOT, L1_BLOB_BASE_FEE);
986        l1_block_contract
987            .storage
988            .insert(ECOTONE_L1_FEE_SCALARS_SLOT, L1_FEE_SCALARS);
989        l1_block_contract
990            .storage
991            .insert(OPERATOR_FEE_SCALARS_SLOT, OPERATOR_FEE);
992        db.insert_account_info(
993            Address::ZERO,
994            AccountInfo {
995                balance: U256::from(1000),
996                ..Default::default()
997            },
998        );
999
1000        let ctx = Context::op()
1001            .with_db(db)
1002            .with_block(BlockEnv {
1003                number: BLOCK_NUM,
1004                ..Default::default()
1005            })
1006            .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::ISTHMUS);
1007
1008        let mut evm = ctx.build_op();
1009
1010        assert_ne!(evm.ctx().chain().l2_block, Some(BLOCK_NUM));
1011
1012        let handler =
1013            OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
1014        handler
1015            .validate_against_state_and_deduct_caller(&mut evm)
1016            .unwrap();
1017
1018        assert_eq!(
1019            *evm.ctx().chain(),
1020            L1BlockInfo {
1021                l2_block: Some(BLOCK_NUM),
1022                l1_base_fee: L1_BASE_FEE,
1023                l1_base_fee_scalar: U256::from(L1_BASE_FEE_SCALAR),
1024                l1_blob_base_fee: Some(L1_BLOB_BASE_FEE),
1025                l1_blob_base_fee_scalar: Some(U256::from(L1_BLOB_BASE_FEE_SCALAR)),
1026                empty_ecotone_scalars: false,
1027                l1_fee_overhead: None,
1028                operator_fee_scalar: Some(U256::from(OPERATOR_FEE_SCALAR)),
1029                operator_fee_constant: Some(U256::from(OPERATOR_FEE_CONST)),
1030                tx_l1_cost: Some(U256::ZERO),
1031                ..Default::default()
1032            }
1033        );
1034    }
1035
1036    #[test]
1037    fn test_remove_l1_cost() {
1038        let caller = Address::ZERO;
1039        let mut db = InMemoryDB::default();
1040        db.insert_account_info(
1041            caller,
1042            AccountInfo {
1043                balance: U256::from(1049),
1044                ..Default::default()
1045            },
1046        );
1047        let ctx = Context::op()
1048            .with_db(db)
1049            .with_chain(L1BlockInfo {
1050                l1_base_fee: U256::from(1_000),
1051                l1_fee_overhead: Some(U256::from(1_000)),
1052                l1_base_fee_scalar: U256::from(1_000),
1053                l2_block: Some(U256::from(0)),
1054                ..Default::default()
1055            })
1056            .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::REGOLITH)
1057            .with_tx(
1058                OpTransaction::builder()
1059                    .base(TxEnv::builder().gas_limit(100))
1060                    .source_hash(B256::ZERO)
1061                    .enveloped_tx(Some(bytes!("FACADE")))
1062                    .build()
1063                    .unwrap(),
1064            );
1065
1066        let mut evm = ctx.build_op();
1067        let handler =
1068            OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
1069
1070        // l1block cost is 1048 fee.
1071        handler
1072            .validate_against_state_and_deduct_caller(&mut evm)
1073            .unwrap();
1074
1075        // Check the account balance is updated.
1076        let account = evm.ctx().journal_mut().load_account(caller).unwrap();
1077        assert_eq!(account.info.balance, U256::from(1));
1078    }
1079
1080    #[test]
1081    fn test_remove_operator_cost_isthmus() {
1082        let caller = Address::ZERO;
1083        let mut db = InMemoryDB::default();
1084        db.insert_account_info(
1085            caller,
1086            AccountInfo {
1087                balance: U256::from(151),
1088                ..Default::default()
1089            },
1090        );
1091        let ctx = Context::op()
1092            .with_db(db)
1093            .with_chain(L1BlockInfo {
1094                operator_fee_scalar: Some(U256::from(10_000_000)),
1095                operator_fee_constant: Some(U256::from(50)),
1096                l2_block: Some(U256::from(0)),
1097                ..Default::default()
1098            })
1099            .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::ISTHMUS)
1100            .with_tx(
1101                OpTransaction::builder()
1102                    .base(TxEnv::builder().gas_limit(10))
1103                    .enveloped_tx(Some(bytes!("FACADE")))
1104                    .build_fill(),
1105            );
1106
1107        let mut evm = ctx.build_op();
1108        let handler =
1109            OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
1110
1111        // Under Isthmus the operator fee cost is operator_fee_scalar * gas_limit / 1e6 + operator_fee_constant
1112        // 10_000_000 * 10 / 1_000_000 + 50 = 150
1113        handler
1114            .validate_against_state_and_deduct_caller(&mut evm)
1115            .unwrap();
1116
1117        // Check the account balance is updated.
1118        let account = evm.ctx().journal_mut().load_account(caller).unwrap();
1119        assert_eq!(account.info.balance, U256::from(1));
1120    }
1121
1122    #[test]
1123    fn test_remove_operator_cost_jovian() {
1124        let caller = Address::ZERO;
1125        let mut db = InMemoryDB::default();
1126        db.insert_account_info(
1127            caller,
1128            AccountInfo {
1129                balance: U256::from(2_051),
1130                ..Default::default()
1131            },
1132        );
1133        let ctx = Context::op()
1134            .with_db(db)
1135            .with_chain(L1BlockInfo {
1136                operator_fee_scalar: Some(U256::from(2)),
1137                operator_fee_constant: Some(U256::from(50)),
1138                l2_block: Some(U256::from(0)),
1139                ..Default::default()
1140            })
1141            .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::JOVIAN)
1142            .with_tx(
1143                OpTransaction::builder()
1144                    .base(TxEnv::builder().gas_limit(10))
1145                    .enveloped_tx(Some(bytes!("FACADE")))
1146                    .build_fill(),
1147            );
1148
1149        let mut evm = ctx.build_op();
1150        let handler =
1151            OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
1152
1153        // Under Jovian the operator fee cost is operator_fee_scalar * gas_limit * 100 + operator_fee_constant
1154        // 2 * 10 * 100 + 50 = 2_050
1155        handler
1156            .validate_against_state_and_deduct_caller(&mut evm)
1157            .unwrap();
1158
1159        let account = evm.ctx().journal_mut().load_account(caller).unwrap();
1160        assert_eq!(account.info.balance, U256::from(1));
1161    }
1162
1163    #[test]
1164    fn test_remove_l1_cost_lack_of_funds() {
1165        let caller = Address::ZERO;
1166        let mut db = InMemoryDB::default();
1167        db.insert_account_info(
1168            caller,
1169            AccountInfo {
1170                balance: U256::from(48),
1171                ..Default::default()
1172            },
1173        );
1174        let ctx = Context::op()
1175            .with_db(db)
1176            .with_chain(L1BlockInfo {
1177                l1_base_fee: U256::from(1_000),
1178                l1_fee_overhead: Some(U256::from(1_000)),
1179                l1_base_fee_scalar: U256::from(1_000),
1180                l2_block: Some(U256::from(0)),
1181                ..Default::default()
1182            })
1183            .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::REGOLITH)
1184            .modify_tx_chained(|tx| {
1185                tx.enveloped_tx = Some(bytes!("FACADE"));
1186            });
1187
1188        // l1block cost is 1048 fee.
1189        let mut evm = ctx.build_op();
1190        let handler =
1191            OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
1192
1193        // l1block cost is 1048 fee.
1194        assert_eq!(
1195            handler.validate_against_state_and_deduct_caller(&mut evm),
1196            Err(EVMError::Transaction(
1197                InvalidTransaction::LackOfFundForMaxFee {
1198                    fee: Box::new(U256::from(1048)),
1199                    balance: Box::new(U256::from(48)),
1200                }
1201                .into(),
1202            ))
1203        );
1204    }
1205
1206    #[test]
1207    fn test_validate_sys_tx() {
1208        // mark the tx as a system transaction.
1209        let ctx = Context::op()
1210            .modify_tx_chained(|tx| {
1211                tx.deposit.source_hash = B256::from([1u8; 32]);
1212                tx.deposit.is_system_transaction = true;
1213            })
1214            .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::REGOLITH);
1215
1216        let mut evm = ctx.build_op();
1217        let handler =
1218            OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
1219
1220        assert_eq!(
1221            handler.validate_env(&mut evm),
1222            Err(EVMError::Transaction(
1223                OpTransactionError::DepositSystemTxPostRegolith
1224            ))
1225        );
1226
1227        evm.ctx().modify_cfg(|cfg| cfg.spec = OpSpecId::BEDROCK);
1228
1229        // Pre-regolith system transactions should be allowed.
1230        assert!(handler.validate_env(&mut evm).is_ok());
1231    }
1232
1233    #[test]
1234    fn test_validate_deposit_tx() {
1235        // Set source hash.
1236        let ctx = Context::op()
1237            .modify_tx_chained(|tx| {
1238                tx.deposit.source_hash = B256::from([1u8; 32]);
1239            })
1240            .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::REGOLITH);
1241
1242        let mut evm = ctx.build_op();
1243        let handler =
1244            OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
1245
1246        assert!(handler.validate_env(&mut evm).is_ok());
1247    }
1248
1249    #[test]
1250    fn test_validate_tx_against_state_deposit_tx() {
1251        // Set source hash.
1252        let ctx = Context::op()
1253            .modify_tx_chained(|tx| {
1254                tx.deposit.source_hash = B256::from([1u8; 32]);
1255            })
1256            .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::REGOLITH);
1257
1258        let mut evm = ctx.build_op();
1259        let handler =
1260            OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
1261
1262        // Nonce and balance checks should be skipped for deposit transactions.
1263        assert!(handler.validate_env(&mut evm).is_ok());
1264    }
1265
1266    #[test]
1267    fn test_halted_deposit_tx_post_regolith() {
1268        let ctx = Context::op()
1269            .modify_tx_chained(|tx| {
1270                // Set up as deposit transaction by having a deposit with source_hash
1271                tx.deposit.source_hash = B256::from([1u8; 32]);
1272            })
1273            .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::REGOLITH);
1274
1275        let mut evm = ctx.build_op();
1276        let mut handler =
1277            OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
1278
1279        assert_eq!(
1280            handler.execution_result(
1281                &mut evm,
1282                FrameResult::Call(CallOutcome {
1283                    result: InterpreterResult {
1284                        result: InstructionResult::OutOfGas,
1285                        output: Default::default(),
1286                        gas: Default::default(),
1287                    },
1288                    memory_offset: Default::default(),
1289                })
1290            ),
1291            Err(EVMError::Transaction(
1292                OpTransactionError::HaltedDepositPostRegolith
1293            ))
1294        )
1295    }
1296
1297    #[test]
1298    fn test_tx_zero_value_touch_caller() {
1299        let ctx = Context::op();
1300
1301        let mut evm = ctx.build_op();
1302
1303        assert!(!evm
1304            .0
1305            .ctx
1306            .journal_mut()
1307            .load_account(Address::ZERO)
1308            .unwrap()
1309            .is_touched());
1310
1311        let handler =
1312            OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
1313
1314        handler
1315            .validate_against_state_and_deduct_caller(&mut evm)
1316            .unwrap();
1317
1318        assert!(evm
1319            .0
1320            .ctx
1321            .journal_mut()
1322            .load_account(Address::ZERO)
1323            .unwrap()
1324            .is_touched());
1325    }
1326
1327    #[rstest]
1328    #[case::deposit(true)]
1329    #[case::dyn_fee(false)]
1330    fn test_operator_fee_refund(#[case] is_deposit: bool) {
1331        const SENDER: Address = Address::ZERO;
1332        const GAS_PRICE: u128 = 0xFF;
1333        const OP_FEE_MOCK_PARAM: u128 = 0xFFFF;
1334
1335        let ctx = Context::op()
1336            .with_tx(
1337                OpTransaction::builder()
1338                    .base(
1339                        TxEnv::builder()
1340                            .gas_price(GAS_PRICE)
1341                            .gas_priority_fee(None)
1342                            .caller(SENDER),
1343                    )
1344                    .enveloped_tx(if is_deposit {
1345                        None
1346                    } else {
1347                        Some(bytes!("FACADE"))
1348                    })
1349                    .source_hash(if is_deposit {
1350                        B256::from([1u8; 32])
1351                    } else {
1352                        B256::ZERO
1353                    })
1354                    .build_fill(),
1355            )
1356            .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::ISTHMUS);
1357
1358        let mut evm = ctx.build_op();
1359        let handler =
1360            OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
1361
1362        // Set the operator fee scalar & constant to non-zero values in the L1 block info.
1363        evm.ctx().chain.operator_fee_scalar = Some(U256::from(OP_FEE_MOCK_PARAM));
1364        evm.ctx().chain.operator_fee_constant = Some(U256::from(OP_FEE_MOCK_PARAM));
1365
1366        let mut gas = Gas::new(100);
1367        gas.set_spent(10);
1368        let mut exec_result = FrameResult::Call(CallOutcome::new(
1369            InterpreterResult {
1370                result: InstructionResult::Return,
1371                output: Default::default(),
1372                gas,
1373            },
1374            0..0,
1375        ));
1376
1377        // Reimburse the caller for the unspent portion of the fees.
1378        handler
1379            .reimburse_caller(&mut evm, &mut exec_result)
1380            .unwrap();
1381
1382        // Compute the expected refund amount. If the transaction is a deposit, the operator fee refund never
1383        // applies. If the transaction is not a deposit, the operator fee refund is added to the refund amount.
1384        let mut expected_refund =
1385            U256::from(GAS_PRICE * (gas.remaining() + gas.refunded() as u64) as u128);
1386        let op_fee_refund = evm
1387            .ctx()
1388            .chain()
1389            .operator_fee_refund(&gas, OpSpecId::ISTHMUS);
1390        assert!(op_fee_refund > U256::ZERO);
1391
1392        if !is_deposit {
1393            expected_refund += op_fee_refund;
1394        }
1395
1396        // Check that the caller was reimbursed the correct amount of ETH.
1397        let account = evm.ctx().journal_mut().load_account(SENDER).unwrap();
1398        assert_eq!(account.info.balance, expected_refund);
1399    }
1400
1401    #[test]
1402    fn test_tx_low_balance_nonce_unchanged() {
1403        let ctx = Context::op().with_tx(
1404            OpTransaction::builder()
1405                .base(TxEnv::builder().value(U256::from(1000)))
1406                .build_fill(),
1407        );
1408
1409        let mut evm = ctx.build_op();
1410
1411        let handler =
1412            OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
1413
1414        let result = handler.validate_against_state_and_deduct_caller(&mut evm);
1415
1416        assert!(matches!(
1417            result.err().unwrap(),
1418            EVMError::Transaction(OpTransactionError::Base(
1419                InvalidTransaction::LackOfFundForMaxFee { .. }
1420            ))
1421        ));
1422        assert_eq!(
1423            evm.0
1424                .ctx
1425                .journal_mut()
1426                .load_account(Address::ZERO)
1427                .unwrap()
1428                .info
1429                .nonce,
1430            0
1431        );
1432    }
1433
1434    #[test]
1435    fn test_validate_missing_enveloped_tx() {
1436        use crate::transaction::deposit::DepositTransactionParts;
1437
1438        // Create a non-deposit transaction without enveloped_tx
1439        let ctx = Context::op().with_tx(OpTransaction {
1440            base: TxEnv::builder().build_fill(),
1441            enveloped_tx: None, // Missing enveloped_tx for non-deposit transaction
1442            deposit: DepositTransactionParts::default(), // No source_hash means non-deposit
1443        });
1444
1445        let mut evm = ctx.build_op();
1446        let handler =
1447            OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
1448
1449        assert_eq!(
1450            handler.validate_env(&mut evm),
1451            Err(EVMError::Transaction(
1452                OpTransactionError::MissingEnvelopedTx
1453            ))
1454        );
1455    }
1456}