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