op_revm/
handler.rs

1//!Handler related to Optimism chain
2use crate::{
3    api::exec::OpContextTr,
4    constants::{BASE_FEE_RECIPIENT, L1_FEE_RECIPIENT, OPERATOR_FEE_RECIPIENT},
5    transaction::{deposit::DEPOSIT_TRANSACTION_TYPE, OpTransactionError, OpTxTr},
6    L1BlockInfo, OpHaltReason, OpSpecId,
7};
8use revm::{
9    context::{journaled_state::JournalCheckpoint, result::InvalidTransaction, LocalContextTr},
10    context_interface::{
11        context::ContextError,
12        result::{EVMError, ExecutionResult, FromStringError},
13        Block, Cfg, ContextTr, JournalTr, Transaction,
14    },
15    handler::{
16        evm::FrameTr,
17        handler::EvmTrError,
18        post_execution::{self, reimburse_caller},
19        pre_execution::{calculate_caller_fee, validate_account_nonce_and_code_with_components},
20        EthFrame, EvmTr, FrameResult, Handler, MainnetHandler,
21    },
22    inspector::{Inspector, InspectorEvmTr, InspectorHandler},
23    interpreter::{interpreter::EthInterpreter, interpreter_action::FrameInit, Gas},
24    primitives::{hardfork::SpecId, U256},
25};
26use std::boxed::Box;
27
28/// Optimism handler extends the [`Handler`] with Optimism specific logic.
29#[derive(Debug, Clone)]
30pub struct OpHandler<EVM, ERROR, FRAME> {
31    /// Mainnet handler allows us to use functions from the mainnet handler inside optimism handler.
32    /// So we dont duplicate the logic
33    pub mainnet: MainnetHandler<EVM, ERROR, FRAME>,
34}
35
36impl<EVM, ERROR, FRAME> OpHandler<EVM, ERROR, FRAME> {
37    /// Create a new Optimism handler.
38    pub fn new() -> Self {
39        Self {
40            mainnet: MainnetHandler::default(),
41        }
42    }
43}
44
45impl<EVM, ERROR, FRAME> Default for OpHandler<EVM, ERROR, FRAME> {
46    fn default() -> Self {
47        Self::new()
48    }
49}
50
51/// Trait to check if the error is a transaction error.
52///
53/// Used in cache_error handler to catch deposit transaction that was halted.
54pub trait IsTxError {
55    /// Check if the error is a transaction error.
56    fn is_tx_error(&self) -> bool;
57}
58
59impl<DB, TX> IsTxError for EVMError<DB, TX> {
60    fn is_tx_error(&self) -> bool {
61        matches!(self, EVMError::Transaction(_))
62    }
63}
64
65impl<EVM, ERROR, FRAME> Handler for OpHandler<EVM, ERROR, FRAME>
66where
67    EVM: EvmTr<Context: OpContextTr, Frame = FRAME>,
68    ERROR: EvmTrError<EVM> + From<OpTransactionError> + FromStringError + IsTxError,
69    // TODO `FrameResult` should be a generic trait.
70    // TODO `FrameInit` should be a generic.
71    FRAME: FrameTr<FrameResult = FrameResult, FrameInit = FrameInit>,
72{
73    type Evm = EVM;
74    type Error = ERROR;
75    type HaltReason = OpHaltReason;
76
77    fn validate_env(&self, evm: &mut Self::Evm) -> Result<(), Self::Error> {
78        // Do not perform any extra validation for deposit transactions, they are pre-verified on L1.
79        let ctx = evm.ctx();
80        let tx = ctx.tx();
81        let tx_type = tx.tx_type();
82        if tx_type == DEPOSIT_TRANSACTION_TYPE {
83            // Do not allow for a system transaction to be processed if Regolith is enabled.
84            if tx.is_system_transaction()
85                && evm.ctx().cfg().spec().is_enabled_in(OpSpecId::REGOLITH)
86            {
87                return Err(OpTransactionError::DepositSystemTxPostRegolith.into());
88            }
89            return Ok(());
90        }
91
92        // Check that non-deposit transactions have enveloped_tx set
93        if tx.enveloped_tx().is_none() {
94            return Err(OpTransactionError::MissingEnvelopedTx.into());
95        }
96
97        self.mainnet.validate_env(evm)
98    }
99
100    fn validate_against_state_and_deduct_caller(
101        &self,
102        evm: &mut Self::Evm,
103    ) -> Result<(), Self::Error> {
104        let (block, tx, cfg, journal, chain, _) = evm.ctx().all_mut();
105        let spec = cfg.spec();
106
107        if tx.tx_type() == DEPOSIT_TRANSACTION_TYPE {
108            let basefee = block.basefee() as u128;
109            let blob_price = block.blob_gasprice().unwrap_or_default();
110            // deposit skips max fee check and just deducts the effective balance spending.
111
112            let mut caller = journal.load_account_with_code_mut(tx.caller())?.data;
113
114            let effective_balance_spending = tx
115                .effective_balance_spending(basefee, blob_price)
116                .expect("Deposit transaction effective balance spending overflow")
117                - tx.value();
118
119            // Mind value should be added first before subtracting the effective balance spending.
120            let mut new_balance = caller
121                .balance()
122                .saturating_add(U256::from(tx.mint().unwrap_or_default()))
123                .saturating_sub(effective_balance_spending);
124
125            if cfg.is_balance_check_disabled() {
126                // Make sure the caller's balance is at least the value of the transaction.
127                // this is not consensus critical, and it is used in testing.
128                new_balance = new_balance.max(tx.value());
129            }
130
131            // set the new balance and bump the nonce if it is a call
132            caller.set_balance(new_balance);
133            if tx.kind().is_call() {
134                caller.bump_nonce();
135            }
136
137            return Ok(());
138        }
139
140        // L1 block info is stored in the context for later use.
141        // and it will be reloaded from the database if it is not for the current block.
142        if chain.l2_block != Some(block.number()) {
143            *chain = L1BlockInfo::try_fetch(journal.db_mut(), block.number(), spec)?;
144        }
145
146        let mut caller_account = journal.load_account_with_code_mut(tx.caller())?.data;
147
148        // validates account nonce and code
149        validate_account_nonce_and_code_with_components(&caller_account.info, tx, cfg)?;
150
151        // check additional cost and deduct it from the caller's balances
152        let mut balance = caller_account.info.balance;
153
154        if !cfg.is_fee_charge_disabled() {
155            let Some(additional_cost) = chain.tx_cost_with_tx(tx, spec) else {
156                return Err(ERROR::from_string(
157                    "[OPTIMISM] Failed to load enveloped transaction.".into(),
158                ));
159            };
160            let Some(new_balance) = balance.checked_sub(additional_cost) else {
161                return Err(InvalidTransaction::LackOfFundForMaxFee {
162                    fee: Box::new(additional_cost),
163                    balance: Box::new(balance),
164                }
165                .into());
166            };
167            balance = new_balance
168        }
169
170        let balance = calculate_caller_fee(balance, tx, block, cfg)?;
171
172        // make changes to the account
173        caller_account.set_balance(balance);
174        if tx.kind().is_call() {
175            caller_account.bump_nonce();
176        }
177
178        Ok(())
179    }
180
181    fn last_frame_result(
182        &mut self,
183        evm: &mut Self::Evm,
184        frame_result: &mut <<Self::Evm as EvmTr>::Frame as FrameTr>::FrameResult,
185    ) -> Result<(), Self::Error> {
186        let ctx = evm.ctx();
187        let tx = ctx.tx();
188        let is_deposit = tx.tx_type() == DEPOSIT_TRANSACTION_TYPE;
189        let tx_gas_limit = tx.gas_limit();
190        let is_regolith = ctx.cfg().spec().is_enabled_in(OpSpecId::REGOLITH);
191
192        let instruction_result = frame_result.interpreter_result().result;
193        let gas = frame_result.gas_mut();
194        let remaining = gas.remaining();
195        let refunded = gas.refunded();
196
197        // Spend the gas limit. Gas is reimbursed when the tx returns successfully.
198        *gas = Gas::new_spent(tx_gas_limit);
199
200        if instruction_result.is_ok() {
201            // On Optimism, deposit transactions report gas usage uniquely to other
202            // transactions due to them being pre-paid on L1.
203            //
204            // Hardfork Behavior:
205            // - Bedrock (success path):
206            //   - Deposit transactions (non-system) report their gas limit as the usage.
207            //     No refunds.
208            //   - Deposit transactions (system) report 0 gas used. No refunds.
209            //   - Regular transactions report gas usage as normal.
210            // - Regolith (success path):
211            //   - Deposit transactions (all) report their gas used as normal. Refunds
212            //     enabled.
213            //   - Regular transactions report their gas used as normal.
214            if !is_deposit || is_regolith {
215                // For regular transactions prior to Regolith and all transactions after
216                // Regolith, gas is reported as normal.
217                gas.erase_cost(remaining);
218                gas.record_refund(refunded);
219            } else if is_deposit {
220                let tx = ctx.tx();
221                if 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            }
227        } else if instruction_result.is_revert() {
228            // On Optimism, deposit transactions report gas usage uniquely to other
229            // transactions due to them being pre-paid on L1.
230            //
231            // Hardfork Behavior:
232            // - Bedrock (revert path):
233            //   - Deposit transactions (all) report the gas limit as the amount of gas
234            //     used on failure. No refunds.
235            //   - Regular transactions receive a refund on remaining gas as normal.
236            // - Regolith (revert path):
237            //   - Deposit transactions (all) report the actual gas used as the amount of
238            //     gas used on failure. Refunds on remaining gas enabled.
239            //   - Regular transactions receive a refund on remaining gas as normal.
240            if !is_deposit || is_regolith {
241                gas.erase_cost(remaining);
242            }
243        }
244        Ok(())
245    }
246
247    fn reimburse_caller(
248        &self,
249        evm: &mut Self::Evm,
250        frame_result: &mut <<Self::Evm as EvmTr>::Frame as FrameTr>::FrameResult,
251    ) -> Result<(), Self::Error> {
252        let mut additional_refund = U256::ZERO;
253
254        if evm.ctx().tx().tx_type() != DEPOSIT_TRANSACTION_TYPE
255            && !evm.ctx().cfg().is_fee_charge_disabled()
256        {
257            let spec = evm.ctx().cfg().spec();
258            additional_refund = evm
259                .ctx()
260                .chain()
261                .operator_fee_refund(frame_result.gas(), spec);
262        }
263
264        reimburse_caller(evm.ctx(), frame_result.gas(), additional_refund).map_err(From::from)
265    }
266
267    fn refund(
268        &self,
269        evm: &mut Self::Evm,
270        frame_result: &mut <<Self::Evm as EvmTr>::Frame as FrameTr>::FrameResult,
271        eip7702_refund: i64,
272    ) {
273        frame_result.gas_mut().record_refund(eip7702_refund);
274
275        let is_deposit = evm.ctx().tx().tx_type() == DEPOSIT_TRANSACTION_TYPE;
276        let is_regolith = evm.ctx().cfg().spec().is_enabled_in(OpSpecId::REGOLITH);
277
278        // Prior to Regolith, deposit transactions did not receive gas refunds.
279        let is_gas_refund_disabled = is_deposit && !is_regolith;
280        if !is_gas_refund_disabled {
281            frame_result.gas_mut().set_final_refund(
282                evm.ctx()
283                    .cfg()
284                    .spec()
285                    .into_eth_spec()
286                    .is_enabled_in(SpecId::LONDON),
287            );
288        }
289    }
290
291    fn reward_beneficiary(
292        &self,
293        evm: &mut Self::Evm,
294        frame_result: &mut <<Self::Evm as EvmTr>::Frame as FrameTr>::FrameResult,
295    ) -> Result<(), Self::Error> {
296        let is_deposit = evm.ctx().tx().tx_type() == DEPOSIT_TRANSACTION_TYPE;
297
298        // Transfer fee to coinbase/beneficiary.
299        if is_deposit {
300            return Ok(());
301        }
302
303        self.mainnet.reward_beneficiary(evm, frame_result)?;
304        let basefee = evm.ctx().block().basefee() as u128;
305
306        // If the transaction is not a deposit transaction, fees are paid out
307        // to both the Base Fee Vault as well as the L1 Fee Vault.
308        let ctx = evm.ctx();
309        let enveloped = ctx.tx().enveloped_tx().cloned();
310        let spec = ctx.cfg().spec();
311        let l1_block_info = ctx.chain_mut();
312
313        let Some(enveloped_tx) = &enveloped else {
314            return Err(ERROR::from_string(
315                "[OPTIMISM] Failed to load enveloped transaction.".into(),
316            ));
317        };
318
319        let l1_cost = l1_block_info.calculate_tx_l1_cost(enveloped_tx, spec);
320        let operator_fee_cost = if spec.is_enabled_in(OpSpecId::ISTHMUS) {
321            l1_block_info.operator_fee_charge(
322                enveloped_tx,
323                U256::from(frame_result.gas().used()),
324                spec,
325            )
326        } else {
327            U256::ZERO
328        };
329        let base_fee_amount = U256::from(basefee.saturating_mul(frame_result.gas().used() as u128));
330
331        // Send fees to their respective recipients
332        for (recipient, amount) in [
333            (L1_FEE_RECIPIENT, l1_cost),
334            (BASE_FEE_RECIPIENT, base_fee_amount),
335            (OPERATOR_FEE_RECIPIENT, operator_fee_cost),
336        ] {
337            ctx.journal_mut().balance_incr(recipient, amount)?;
338        }
339
340        Ok(())
341    }
342
343    fn execution_result(
344        &mut self,
345        evm: &mut Self::Evm,
346        frame_result: <<Self::Evm as EvmTr>::Frame as FrameTr>::FrameResult,
347    ) -> Result<ExecutionResult<Self::HaltReason>, Self::Error> {
348        match core::mem::replace(evm.ctx().error(), Ok(())) {
349            Err(ContextError::Db(e)) => return Err(e.into()),
350            Err(ContextError::Custom(e)) => return Err(Self::Error::from_string(e)),
351            Ok(_) => (),
352        }
353
354        let exec_result =
355            post_execution::output(evm.ctx(), frame_result).map_haltreason(OpHaltReason::Base);
356
357        if exec_result.is_halt() {
358            // Post-regolith, if the transaction is a deposit transaction and it halts,
359            // we bubble up to the global return handler. The mint value will be persisted
360            // and the caller nonce will be incremented there.
361            let is_deposit = evm.ctx().tx().tx_type() == DEPOSIT_TRANSACTION_TYPE;
362            if is_deposit && evm.ctx().cfg().spec().is_enabled_in(OpSpecId::REGOLITH) {
363                return Err(ERROR::from(OpTransactionError::HaltedDepositPostRegolith));
364            }
365        }
366        evm.ctx().journal_mut().commit_tx();
367        evm.ctx().chain_mut().clear_tx_l1_cost();
368        evm.ctx().local_mut().clear();
369        evm.frame_stack().clear();
370
371        Ok(exec_result)
372    }
373
374    fn catch_error(
375        &self,
376        evm: &mut Self::Evm,
377        error: Self::Error,
378    ) -> Result<ExecutionResult<Self::HaltReason>, Self::Error> {
379        let is_deposit = evm.ctx().tx().tx_type() == DEPOSIT_TRANSACTION_TYPE;
380        let output = if error.is_tx_error() && is_deposit {
381            let ctx = evm.ctx();
382            let spec = ctx.cfg().spec();
383            let tx = ctx.tx();
384            let caller = tx.caller();
385            let mint = tx.mint();
386            let is_system_tx = tx.is_system_transaction();
387            let gas_limit = tx.gas_limit();
388            let journal = evm.ctx().journal_mut();
389
390            // discard all changes of this transaction
391            // Default JournalCheckpoint is the first checkpoint and will wipe all changes.
392            journal.checkpoint_revert(JournalCheckpoint::default());
393
394            // If the transaction is a deposit transaction and it failed
395            // for any reason, the caller nonce must be bumped, and the
396            // gas reported must be altered depending on the Hardfork. This is
397            // also returned as a special Halt variant so that consumers can more
398            // easily distinguish between a failed deposit and a failed
399            // normal transaction.
400
401            // Increment sender nonce and account balance for the mint amount. Deposits
402            // always persist the mint amount, even if the transaction fails.
403            let mut acc = journal.load_account_mut(caller)?;
404            acc.bump_nonce();
405            acc.incr_balance(U256::from(mint.unwrap_or_default()));
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            Ok(ExecutionResult::Halt {
421                reason: OpHaltReason::FailedDeposit,
422                gas_used,
423            })
424        } else {
425            Err(error)
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, 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            .modify_cfg_chained(|cfg| cfg.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            .modify_cfg_chained(|cfg| cfg.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            .modify_cfg_chained(|cfg| cfg.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            .modify_cfg_chained(|cfg| cfg.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            .modify_cfg_chained(|cfg| cfg.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            .modify_cfg_chained(|cfg| cfg.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            .modify_cfg_chained(|cfg| cfg.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            .modify_cfg_chained(|cfg| cfg.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            .modify_cfg_chained(|cfg| cfg.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            .modify_cfg_chained(|cfg| cfg.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            .modify_cfg_chained(|cfg| cfg.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            .modify_cfg_chained(|cfg| cfg.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            .modify_cfg_chained(|cfg| cfg.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            .modify_cfg_chained(|cfg| cfg.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            .modify_cfg_chained(|cfg| cfg.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            .modify_cfg_chained(|cfg| cfg.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            .modify_cfg_chained(|cfg| cfg.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        evm.ctx().modify_cfg(|cfg| cfg.spec = OpSpecId::BEDROCK);
1232
1233        // Pre-regolith system transactions should be allowed.
1234        assert!(handler.validate_env(&mut evm).is_ok());
1235    }
1236
1237    #[test]
1238    fn test_validate_deposit_tx() {
1239        // Set source hash.
1240        let ctx = Context::op()
1241            .modify_tx_chained(|tx| {
1242                tx.deposit.source_hash = B256::from([1u8; 32]);
1243            })
1244            .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::REGOLITH);
1245
1246        let mut evm = ctx.build_op();
1247        let handler =
1248            OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
1249
1250        assert!(handler.validate_env(&mut evm).is_ok());
1251    }
1252
1253    #[test]
1254    fn test_validate_tx_against_state_deposit_tx() {
1255        // Set source hash.
1256        let ctx = Context::op()
1257            .modify_tx_chained(|tx| {
1258                tx.deposit.source_hash = B256::from([1u8; 32]);
1259            })
1260            .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::REGOLITH);
1261
1262        let mut evm = ctx.build_op();
1263        let handler =
1264            OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
1265
1266        // Nonce and balance checks should be skipped for deposit transactions.
1267        assert!(handler.validate_env(&mut evm).is_ok());
1268    }
1269
1270    #[test]
1271    fn test_halted_deposit_tx_post_regolith() {
1272        let ctx = Context::op()
1273            .modify_tx_chained(|tx| {
1274                // Set up as deposit transaction by having a deposit with source_hash
1275                tx.deposit.source_hash = B256::from([1u8; 32]);
1276            })
1277            .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::REGOLITH);
1278
1279        let mut evm = ctx.build_op();
1280        let mut handler =
1281            OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
1282
1283        assert_eq!(
1284            handler.execution_result(
1285                &mut evm,
1286                FrameResult::Call(CallOutcome::new(
1287                    InterpreterResult {
1288                        result: InstructionResult::OutOfGas,
1289                        output: Default::default(),
1290                        gas: Default::default(),
1291                    },
1292                    Default::default()
1293                ))
1294            ),
1295            Err(EVMError::Transaction(
1296                OpTransactionError::HaltedDepositPostRegolith
1297            ))
1298        )
1299    }
1300
1301    #[test]
1302    fn test_tx_zero_value_touch_caller() {
1303        let ctx = Context::op();
1304
1305        let mut evm = ctx.build_op();
1306
1307        assert!(!evm
1308            .0
1309            .ctx
1310            .journal_mut()
1311            .load_account(Address::ZERO)
1312            .unwrap()
1313            .is_touched());
1314
1315        let handler =
1316            OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
1317
1318        handler
1319            .validate_against_state_and_deduct_caller(&mut evm)
1320            .unwrap();
1321
1322        assert!(evm
1323            .0
1324            .ctx
1325            .journal_mut()
1326            .load_account(Address::ZERO)
1327            .unwrap()
1328            .is_touched());
1329    }
1330
1331    #[rstest]
1332    #[case::deposit(true)]
1333    #[case::dyn_fee(false)]
1334    fn test_operator_fee_refund(#[case] is_deposit: bool) {
1335        const SENDER: Address = Address::ZERO;
1336        const GAS_PRICE: u128 = 0xFF;
1337        const OP_FEE_MOCK_PARAM: u128 = 0xFFFF;
1338
1339        let ctx = Context::op()
1340            .with_tx(
1341                OpTransaction::builder()
1342                    .base(
1343                        TxEnv::builder()
1344                            .gas_price(GAS_PRICE)
1345                            .gas_priority_fee(None)
1346                            .caller(SENDER),
1347                    )
1348                    .enveloped_tx(if is_deposit {
1349                        None
1350                    } else {
1351                        Some(bytes!("FACADE"))
1352                    })
1353                    .source_hash(if is_deposit {
1354                        B256::from([1u8; 32])
1355                    } else {
1356                        B256::ZERO
1357                    })
1358                    .build_fill(),
1359            )
1360            .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::ISTHMUS);
1361
1362        let mut evm = ctx.build_op();
1363        let handler =
1364            OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
1365
1366        // Set the operator fee scalar & constant to non-zero values in the L1 block info.
1367        evm.ctx().chain.operator_fee_scalar = Some(U256::from(OP_FEE_MOCK_PARAM));
1368        evm.ctx().chain.operator_fee_constant = Some(U256::from(OP_FEE_MOCK_PARAM));
1369
1370        let mut gas = Gas::new(100);
1371        gas.set_spent(10);
1372        let mut exec_result = FrameResult::Call(CallOutcome::new(
1373            InterpreterResult {
1374                result: InstructionResult::Return,
1375                output: Default::default(),
1376                gas,
1377            },
1378            0..0,
1379        ));
1380
1381        // Reimburse the caller for the unspent portion of the fees.
1382        handler
1383            .reimburse_caller(&mut evm, &mut exec_result)
1384            .unwrap();
1385
1386        // Compute the expected refund amount. If the transaction is a deposit, the operator fee refund never
1387        // applies. If the transaction is not a deposit, the operator fee refund is added to the refund amount.
1388        let mut expected_refund =
1389            U256::from(GAS_PRICE * (gas.remaining() + gas.refunded() as u64) as u128);
1390        let op_fee_refund = evm
1391            .ctx()
1392            .chain()
1393            .operator_fee_refund(&gas, OpSpecId::ISTHMUS);
1394        assert!(op_fee_refund > U256::ZERO);
1395
1396        if !is_deposit {
1397            expected_refund += op_fee_refund;
1398        }
1399
1400        // Check that the caller was reimbursed the correct amount of ETH.
1401        let account = evm.ctx().journal_mut().load_account(SENDER).unwrap();
1402        assert_eq!(account.info.balance, expected_refund);
1403    }
1404
1405    #[test]
1406    fn test_tx_low_balance_nonce_unchanged() {
1407        let ctx = Context::op().with_tx(
1408            OpTransaction::builder()
1409                .base(TxEnv::builder().value(U256::from(1000)))
1410                .build_fill(),
1411        );
1412
1413        let mut evm = ctx.build_op();
1414
1415        let handler =
1416            OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
1417
1418        let result = handler.validate_against_state_and_deduct_caller(&mut evm);
1419
1420        assert!(matches!(
1421            result.err().unwrap(),
1422            EVMError::Transaction(OpTransactionError::Base(
1423                InvalidTransaction::LackOfFundForMaxFee { .. }
1424            ))
1425        ));
1426        assert_eq!(
1427            evm.0
1428                .ctx
1429                .journal_mut()
1430                .load_account(Address::ZERO)
1431                .unwrap()
1432                .info
1433                .nonce,
1434            0
1435        );
1436    }
1437
1438    #[test]
1439    fn test_validate_missing_enveloped_tx() {
1440        use crate::transaction::deposit::DepositTransactionParts;
1441
1442        // Create a non-deposit transaction without enveloped_tx
1443        let ctx = Context::op().with_tx(OpTransaction {
1444            base: TxEnv::builder().build_fill(),
1445            enveloped_tx: None, // Missing enveloped_tx for non-deposit transaction
1446            deposit: DepositTransactionParts::default(), // No source_hash means non-deposit
1447        });
1448
1449        let mut evm = ctx.build_op();
1450        let handler =
1451            OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
1452
1453        assert_eq!(
1454            handler.validate_env(&mut evm),
1455            Err(EVMError::Transaction(
1456                OpTransactionError::MissingEnvelopedTx
1457            ))
1458        );
1459    }
1460}