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