Skip to main content

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