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