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