Skip to main content

op_revm/
handler.rs

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