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::{result::InvalidTransaction, LocalContextTr},
10    context_interface::{
11        context::ContextError,
12        result::{EVMError, ExecutionResult, FromStringError},
13        Block, Cfg, ContextTr, JournalTr, Transaction,
14    },
15    handler::{
16        evm::FrameTr,
17        handler::EvmTrError,
18        post_execution::{self, reimburse_caller},
19        pre_execution::validate_account_nonce_and_code,
20        EthFrame, EvmTr, FrameResult, Handler, MainnetHandler,
21    },
22    inspector::{Inspector, InspectorEvmTr, InspectorHandler},
23    interpreter::{interpreter::EthInterpreter, interpreter_action::FrameInit, Gas},
24    primitives::{hardfork::SpecId, U256},
25};
26use std::boxed::Box;
27
28/// Optimism handler extends the [`Handler`] with Optimism specific logic.
29#[derive(Debug, Clone)]
30pub struct OpHandler<EVM, ERROR, FRAME> {
31    /// Mainnet handler allows us to use functions from the mainnet handler inside optimism handler.
32    /// So we dont duplicate the logic
33    pub mainnet: MainnetHandler<EVM, ERROR, FRAME>,
34    /// Phantom data to avoid type inference issues.
35    pub _phantom: core::marker::PhantomData<(EVM, ERROR, FRAME)>,
36}
37
38impl<EVM, ERROR, FRAME> OpHandler<EVM, ERROR, FRAME> {
39    /// Create a new Optimism handler.
40    pub fn new() -> Self {
41        Self {
42            mainnet: MainnetHandler::default(),
43            _phantom: core::marker::PhantomData,
44        }
45    }
46}
47
48impl<EVM, ERROR, FRAME> Default for OpHandler<EVM, ERROR, FRAME> {
49    fn default() -> Self {
50        Self::new()
51    }
52}
53
54/// Trait to check if the error is a transaction error.
55///
56/// Used in cache_error handler to catch deposit transaction that was halted.
57pub trait IsTxError {
58    /// Check if the error is a transaction error.
59    fn is_tx_error(&self) -> bool;
60}
61
62impl<DB, TX> IsTxError for EVMError<DB, TX> {
63    fn is_tx_error(&self) -> bool {
64        matches!(self, EVMError::Transaction(_))
65    }
66}
67
68impl<EVM, ERROR, FRAME> Handler for OpHandler<EVM, ERROR, FRAME>
69where
70    EVM: EvmTr<Context: OpContextTr, Frame = FRAME>,
71    ERROR: EvmTrError<EVM> + From<OpTransactionError> + FromStringError + IsTxError,
72    // TODO `FrameResult` should be a generic trait.
73    // TODO `FrameInit` should be a generic.
74    FRAME: FrameTr<FrameResult = FrameResult, FrameInit = FrameInit>,
75{
76    type Evm = EVM;
77    type Error = ERROR;
78    type HaltReason = OpHaltReason;
79
80    fn validate_env(&self, evm: &mut Self::Evm) -> Result<(), Self::Error> {
81        // Do not perform any extra validation for deposit transactions, they are pre-verified on L1.
82        let ctx = evm.ctx();
83        let tx = ctx.tx();
84        let tx_type = tx.tx_type();
85        if tx_type == DEPOSIT_TRANSACTION_TYPE {
86            // Do not allow for a system transaction to be processed if Regolith is enabled.
87            if tx.is_system_transaction()
88                && evm.ctx().cfg().spec().is_enabled_in(OpSpecId::REGOLITH)
89            {
90                return Err(OpTransactionError::DepositSystemTxPostRegolith.into());
91            }
92            return Ok(());
93        }
94        self.mainnet.validate_env(evm)
95    }
96
97    fn validate_against_state_and_deduct_caller(
98        &self,
99        evm: &mut Self::Evm,
100    ) -> Result<(), Self::Error> {
101        let ctx = evm.ctx();
102
103        let basefee = ctx.block().basefee() as u128;
104        let blob_price = ctx.block().blob_gasprice().unwrap_or_default();
105        let is_deposit = ctx.tx().tx_type() == DEPOSIT_TRANSACTION_TYPE;
106        let spec = ctx.cfg().spec();
107        let block_number = ctx.block().number();
108        let is_balance_check_disabled = ctx.cfg().is_balance_check_disabled();
109        let is_eip3607_disabled = ctx.cfg().is_eip3607_disabled();
110        let is_nonce_check_disabled = ctx.cfg().is_nonce_check_disabled();
111
112        let mint = if is_deposit {
113            ctx.tx().mint().unwrap_or_default()
114        } else {
115            0
116        };
117
118        let mut additional_cost = U256::ZERO;
119
120        // The L1-cost fee is only computed for Optimism non-deposit transactions.
121        if !is_deposit {
122            // L1 block info is stored in the context for later use.
123            // and it will be reloaded from the database if it is not for the current block.
124            if ctx.chain().l2_block != block_number {
125                *ctx.chain_mut() = L1BlockInfo::try_fetch(ctx.db_mut(), block_number, spec)?;
126            }
127
128            // account for additional cost of l1 fee and operator fee
129            let enveloped_tx = ctx
130                .tx()
131                .enveloped_tx()
132                .expect("all not deposit tx have enveloped tx")
133                .clone();
134
135            // compute L1 cost
136            additional_cost = ctx.chain_mut().calculate_tx_l1_cost(&enveloped_tx, spec);
137
138            // compute operator fee
139            if spec.is_enabled_in(OpSpecId::ISTHMUS) {
140                let gas_limit = U256::from(ctx.tx().gas_limit());
141                let operator_fee_charge = ctx.chain().operator_fee_charge(&enveloped_tx, gas_limit);
142                additional_cost = additional_cost.saturating_add(operator_fee_charge);
143            }
144        }
145
146        let (tx, journal) = ctx.tx_journal_mut();
147
148        let caller_account = journal.load_account_code(tx.caller())?.data;
149
150        if !is_deposit {
151            // validates account nonce and code
152            validate_account_nonce_and_code(
153                &mut caller_account.info,
154                tx.nonce(),
155                is_eip3607_disabled,
156                is_nonce_check_disabled,
157            )?;
158        }
159
160        let max_balance_spending = tx.max_balance_spending()?.saturating_add(additional_cost);
161
162        // old balance is journaled before mint is incremented.
163        let old_balance = caller_account.info.balance;
164
165        // If the transaction is a deposit with a `mint` value, add the mint value
166        // in wei to the caller's balance. This should be persisted to the database
167        // prior to the rest of execution.
168        let mut new_balance = caller_account.info.balance.saturating_add(U256::from(mint));
169
170        // Check if account has enough balance for `gas_limit * max_fee`` and value transfer.
171        // Transfer will be done inside `*_inner` functions.
172        if !is_deposit && max_balance_spending > new_balance && !is_balance_check_disabled {
173            // skip max balance check for deposit transactions.
174            // this check for deposit was skipped previously in `validate_tx_against_state` function
175            return Err(InvalidTransaction::LackOfFundForMaxFee {
176                fee: Box::new(max_balance_spending),
177                balance: Box::new(new_balance),
178            }
179            .into());
180        }
181
182        let effective_balance_spending = tx
183            .effective_balance_spending(basefee, blob_price)
184            .expect("effective balance is always smaller than max balance so it can't overflow");
185
186        // subtracting max balance spending with value that is going to be deducted later in the call.
187        let gas_balance_spending = effective_balance_spending - tx.value();
188
189        // If the transaction is not a deposit transaction, subtract the L1 data fee from the
190        // caller's balance directly after minting the requested amount of ETH.
191        // Additionally deduct the operator fee from the caller's account.
192        //
193        // In case of deposit additional cost will be zero.
194        let op_gas_balance_spending = gas_balance_spending.saturating_add(additional_cost);
195
196        new_balance = new_balance.saturating_sub(op_gas_balance_spending);
197
198        if is_balance_check_disabled {
199            // Make sure the caller's balance is at least the value of the transaction.
200            // this is not consensus critical, and it is used in testing.
201            new_balance = new_balance.max(tx.value());
202        }
203
204        // Touch account so we know it is changed.
205        caller_account.mark_touch();
206        caller_account.info.balance = new_balance;
207
208        // Bump the nonce for calls. Nonce for CREATE will be bumped in `handle_create`.
209        if tx.kind().is_call() {
210            caller_account.info.nonce = caller_account.info.nonce.saturating_add(1);
211        }
212
213        // NOTE: all changes to the caller account should journaled so in case of error
214        // we can revert the changes.
215        journal.caller_accounting_journal_entry(tx.caller(), old_balance, tx.kind().is_call());
216
217        Ok(())
218    }
219
220    fn last_frame_result(
221        &mut self,
222        evm: &mut Self::Evm,
223        frame_result: &mut <<Self::Evm as EvmTr>::Frame as FrameTr>::FrameResult,
224    ) -> Result<(), Self::Error> {
225        let ctx = evm.ctx();
226        let tx = ctx.tx();
227        let is_deposit = tx.tx_type() == DEPOSIT_TRANSACTION_TYPE;
228        let tx_gas_limit = tx.gas_limit();
229        let is_regolith = ctx.cfg().spec().is_enabled_in(OpSpecId::REGOLITH);
230
231        let instruction_result = frame_result.interpreter_result().result;
232        let gas = frame_result.gas_mut();
233        let remaining = gas.remaining();
234        let refunded = gas.refunded();
235
236        // Spend the gas limit. Gas is reimbursed when the tx returns successfully.
237        *gas = Gas::new_spent(tx_gas_limit);
238
239        if instruction_result.is_ok() {
240            // On Optimism, deposit transactions report gas usage uniquely to other
241            // transactions due to them being pre-paid on L1.
242            //
243            // Hardfork Behavior:
244            // - Bedrock (success path):
245            //   - Deposit transactions (non-system) report their gas limit as the usage.
246            //     No refunds.
247            //   - Deposit transactions (system) report 0 gas used. No refunds.
248            //   - Regular transactions report gas usage as normal.
249            // - Regolith (success path):
250            //   - Deposit transactions (all) report their gas used as normal. Refunds
251            //     enabled.
252            //   - Regular transactions report their gas used as normal.
253            if !is_deposit || is_regolith {
254                // For regular transactions prior to Regolith and all transactions after
255                // Regolith, gas is reported as normal.
256                gas.erase_cost(remaining);
257                gas.record_refund(refunded);
258            } else if is_deposit {
259                let tx = ctx.tx();
260                if tx.is_system_transaction() {
261                    // System transactions were a special type of deposit transaction in
262                    // the Bedrock hardfork that did not incur any gas costs.
263                    gas.erase_cost(tx_gas_limit);
264                }
265            }
266        } else if instruction_result.is_revert() {
267            // On Optimism, deposit transactions report gas usage uniquely to other
268            // transactions due to them being pre-paid on L1.
269            //
270            // Hardfork Behavior:
271            // - Bedrock (revert path):
272            //   - Deposit transactions (all) report the gas limit as the amount of gas
273            //     used on failure. No refunds.
274            //   - Regular transactions receive a refund on remaining gas as normal.
275            // - Regolith (revert path):
276            //   - Deposit transactions (all) report the actual gas used as the amount of
277            //     gas used on failure. Refunds on remaining gas enabled.
278            //   - Regular transactions receive a refund on remaining gas as normal.
279            if !is_deposit || is_regolith {
280                gas.erase_cost(remaining);
281            }
282        }
283        Ok(())
284    }
285
286    fn reimburse_caller(
287        &self,
288        evm: &mut Self::Evm,
289        frame_result: &mut <<Self::Evm as EvmTr>::Frame as FrameTr>::FrameResult,
290    ) -> Result<(), Self::Error> {
291        let mut additional_refund = U256::ZERO;
292
293        if evm.ctx().tx().tx_type() != DEPOSIT_TRANSACTION_TYPE {
294            let spec = evm.ctx().cfg().spec();
295            additional_refund = evm
296                .ctx()
297                .chain()
298                .operator_fee_refund(frame_result.gas(), spec);
299        }
300
301        reimburse_caller(evm.ctx(), frame_result.gas(), additional_refund).map_err(From::from)
302    }
303
304    fn refund(
305        &self,
306        evm: &mut Self::Evm,
307        frame_result: &mut <<Self::Evm as EvmTr>::Frame as FrameTr>::FrameResult,
308        eip7702_refund: i64,
309    ) {
310        frame_result.gas_mut().record_refund(eip7702_refund);
311
312        let is_deposit = evm.ctx().tx().tx_type() == DEPOSIT_TRANSACTION_TYPE;
313        let is_regolith = evm.ctx().cfg().spec().is_enabled_in(OpSpecId::REGOLITH);
314
315        // Prior to Regolith, deposit transactions did not receive gas refunds.
316        let is_gas_refund_disabled = is_deposit && !is_regolith;
317        if !is_gas_refund_disabled {
318            frame_result.gas_mut().set_final_refund(
319                evm.ctx()
320                    .cfg()
321                    .spec()
322                    .into_eth_spec()
323                    .is_enabled_in(SpecId::LONDON),
324            );
325        }
326    }
327
328    fn reward_beneficiary(
329        &self,
330        evm: &mut Self::Evm,
331        frame_result: &mut <<Self::Evm as EvmTr>::Frame as FrameTr>::FrameResult,
332    ) -> Result<(), Self::Error> {
333        let is_deposit = evm.ctx().tx().tx_type() == DEPOSIT_TRANSACTION_TYPE;
334
335        // Transfer fee to coinbase/beneficiary.
336        if is_deposit {
337            return Ok(());
338        }
339
340        self.mainnet.reward_beneficiary(evm, frame_result)?;
341        let basefee = evm.ctx().block().basefee() as u128;
342
343        // If the transaction is not a deposit transaction, fees are paid out
344        // to both the Base Fee Vault as well as the L1 Fee Vault.
345        let ctx = evm.ctx();
346        let enveloped = ctx.tx().enveloped_tx().cloned();
347        let spec = ctx.cfg().spec();
348        let l1_block_info = ctx.chain_mut();
349
350        let Some(enveloped_tx) = &enveloped else {
351            return Err(ERROR::from_string(
352                "[OPTIMISM] Failed to load enveloped transaction.".into(),
353            ));
354        };
355
356        let l1_cost = l1_block_info.calculate_tx_l1_cost(enveloped_tx, spec);
357        let operator_fee_cost = if spec.is_enabled_in(OpSpecId::ISTHMUS) {
358            l1_block_info.operator_fee_charge(enveloped_tx, U256::from(frame_result.gas().used()))
359        } else {
360            U256::ZERO
361        };
362        let base_fee_amount = U256::from(basefee.saturating_mul(frame_result.gas().used() as u128));
363
364        // Send fees to their respective recipients
365        for (recipient, amount) in [
366            (L1_FEE_RECIPIENT, l1_cost),
367            (BASE_FEE_RECIPIENT, base_fee_amount),
368            (OPERATOR_FEE_RECIPIENT, operator_fee_cost),
369        ] {
370            ctx.journal_mut().balance_incr(recipient, amount)?;
371        }
372
373        Ok(())
374    }
375
376    fn execution_result(
377        &mut self,
378        evm: &mut Self::Evm,
379        frame_result: <<Self::Evm as EvmTr>::Frame as FrameTr>::FrameResult,
380    ) -> Result<ExecutionResult<Self::HaltReason>, Self::Error> {
381        match core::mem::replace(evm.ctx().error(), Ok(())) {
382            Err(ContextError::Db(e)) => return Err(e.into()),
383            Err(ContextError::Custom(e)) => return Err(Self::Error::from_string(e)),
384            Ok(_) => (),
385        }
386
387        let exec_result =
388            post_execution::output(evm.ctx(), frame_result).map_haltreason(OpHaltReason::Base);
389
390        if exec_result.is_halt() {
391            // Post-regolith, if the transaction is a deposit transaction and it halts,
392            // we bubble up to the global return handler. The mint value will be persisted
393            // and the caller nonce will be incremented there.
394            let is_deposit = evm.ctx().tx().tx_type() == DEPOSIT_TRANSACTION_TYPE;
395            if is_deposit && evm.ctx().cfg().spec().is_enabled_in(OpSpecId::REGOLITH) {
396                return Err(ERROR::from(OpTransactionError::HaltedDepositPostRegolith));
397            }
398        }
399        evm.ctx().journal_mut().commit_tx();
400        evm.ctx().chain_mut().clear_tx_l1_cost();
401        evm.ctx().local_mut().clear();
402        evm.frame_stack().clear();
403
404        Ok(exec_result)
405    }
406
407    fn catch_error(
408        &self,
409        evm: &mut Self::Evm,
410        error: Self::Error,
411    ) -> Result<ExecutionResult<Self::HaltReason>, Self::Error> {
412        let is_deposit = evm.ctx().tx().tx_type() == DEPOSIT_TRANSACTION_TYPE;
413        let output = if error.is_tx_error() && is_deposit {
414            let ctx = evm.ctx();
415            let spec = ctx.cfg().spec();
416            let tx = ctx.tx();
417            let caller = tx.caller();
418            let mint = tx.mint();
419            let is_system_tx = tx.is_system_transaction();
420            let gas_limit = tx.gas_limit();
421
422            // discard all changes of this transaction
423            evm.ctx().journal_mut().discard_tx();
424
425            // If the transaction is a deposit transaction and it failed
426            // for any reason, the caller nonce must be bumped, and the
427            // gas reported must be altered depending on the Hardfork. This is
428            // also returned as a special Halt variant so that consumers can more
429            // easily distinguish between a failed deposit and a failed
430            // normal transaction.
431
432            // Increment sender nonce and account balance for the mint amount. Deposits
433            // always persist the mint amount, even if the transaction fails.
434            let acc: &mut revm::state::Account = evm.ctx().journal_mut().load_account(caller)?.data;
435
436            let old_balance = acc.info.balance;
437
438            // decrement transaction id as it was incremented when we discarded the tx.
439            acc.transaction_id -= acc.transaction_id;
440            acc.info.nonce = acc.info.nonce.saturating_add(1);
441            acc.info.balance = acc
442                .info
443                .balance
444                .saturating_add(U256::from(mint.unwrap_or_default()));
445            acc.mark_touch();
446
447            // add journal entry for accounts
448            evm.ctx()
449                .journal_mut()
450                .caller_accounting_journal_entry(caller, old_balance, true);
451
452            // The gas used of a failed deposit post-regolith is the gas
453            // limit of the transaction. pre-regolith, it is the gas limit
454            // of the transaction for non system transactions and 0 for system
455            // transactions.
456            let gas_used = if spec.is_enabled_in(OpSpecId::REGOLITH) || !is_system_tx {
457                gas_limit
458            } else {
459                0
460            };
461            // clear the journal
462            Ok(ExecutionResult::Halt {
463                reason: OpHaltReason::FailedDeposit,
464                gas_used,
465            })
466        } else {
467            Err(error)
468        };
469        // do the cleanup
470        evm.ctx().chain_mut().clear_tx_l1_cost();
471        evm.ctx().local_mut().clear();
472        evm.frame_stack().clear();
473
474        output
475    }
476}
477
478impl<EVM, ERROR> InspectorHandler for OpHandler<EVM, ERROR, EthFrame<EthInterpreter>>
479where
480    EVM: InspectorEvmTr<
481        Context: OpContextTr,
482        Frame = EthFrame<EthInterpreter>,
483        Inspector: Inspector<<<Self as Handler>::Evm as EvmTr>::Context, EthInterpreter>,
484    >,
485    ERROR: EvmTrError<EVM> + From<OpTransactionError> + FromStringError + IsTxError,
486{
487    type IT = EthInterpreter;
488}
489
490#[cfg(test)]
491mod tests {
492    use super::*;
493    use crate::{
494        api::default_ctx::OpContext,
495        constants::{
496            BASE_FEE_SCALAR_OFFSET, ECOTONE_L1_BLOB_BASE_FEE_SLOT, ECOTONE_L1_FEE_SCALARS_SLOT,
497            L1_BASE_FEE_SLOT, L1_BLOCK_CONTRACT, OPERATOR_FEE_SCALARS_SLOT,
498        },
499        DefaultOp, OpBuilder, OpTransaction,
500    };
501    use alloy_primitives::uint;
502    use revm::{
503        context::{BlockEnv, Context, TxEnv},
504        context_interface::result::InvalidTransaction,
505        database::InMemoryDB,
506        database_interface::EmptyDB,
507        handler::EthFrame,
508        interpreter::{CallOutcome, InstructionResult, InterpreterResult},
509        primitives::{bytes, Address, Bytes, B256},
510        state::AccountInfo,
511    };
512    use rstest::rstest;
513    use std::boxed::Box;
514
515    /// Creates frame result.
516    fn call_last_frame_return(
517        ctx: OpContext<EmptyDB>,
518        instruction_result: InstructionResult,
519        gas: Gas,
520    ) -> Gas {
521        let mut evm = ctx.build_op();
522
523        let mut exec_result = FrameResult::Call(CallOutcome::new(
524            InterpreterResult {
525                result: instruction_result,
526                output: Bytes::new(),
527                gas,
528            },
529            0..0,
530        ));
531
532        let mut handler =
533            OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
534
535        handler
536            .last_frame_result(&mut evm, &mut exec_result)
537            .unwrap();
538        handler.refund(&mut evm, &mut exec_result, 0);
539        *exec_result.gas()
540    }
541
542    #[test]
543    fn test_revert_gas() {
544        let ctx = Context::op()
545            .with_tx(
546                OpTransaction::builder()
547                    .base(TxEnv::builder().gas_limit(100))
548                    .build_fill(),
549            )
550            .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::BEDROCK);
551
552        let gas = call_last_frame_return(ctx, InstructionResult::Revert, Gas::new(90));
553        assert_eq!(gas.remaining(), 90);
554        assert_eq!(gas.spent(), 10);
555        assert_eq!(gas.refunded(), 0);
556    }
557
558    #[test]
559    fn test_consume_gas() {
560        let ctx = Context::op()
561            .with_tx(
562                OpTransaction::builder()
563                    .base(TxEnv::builder().gas_limit(100))
564                    .build_fill(),
565            )
566            .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::REGOLITH);
567
568        let gas = call_last_frame_return(ctx, InstructionResult::Stop, Gas::new(90));
569        assert_eq!(gas.remaining(), 90);
570        assert_eq!(gas.spent(), 10);
571        assert_eq!(gas.refunded(), 0);
572    }
573
574    #[test]
575    fn test_consume_gas_with_refund() {
576        let ctx = Context::op()
577            .with_tx(
578                OpTransaction::builder()
579                    .base(TxEnv::builder().gas_limit(100))
580                    .source_hash(B256::from([1u8; 32]))
581                    .build_fill(),
582            )
583            .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::REGOLITH);
584
585        let mut ret_gas = Gas::new(90);
586        ret_gas.record_refund(20);
587
588        let gas = call_last_frame_return(ctx.clone(), InstructionResult::Stop, ret_gas);
589        assert_eq!(gas.remaining(), 90);
590        assert_eq!(gas.spent(), 10);
591        assert_eq!(gas.refunded(), 2); // min(20, 10/5)
592
593        let gas = call_last_frame_return(ctx, InstructionResult::Revert, ret_gas);
594        assert_eq!(gas.remaining(), 90);
595        assert_eq!(gas.spent(), 10);
596        assert_eq!(gas.refunded(), 0);
597    }
598
599    #[test]
600    fn test_consume_gas_deposit_tx() {
601        let ctx = Context::op()
602            .with_tx(
603                OpTransaction::builder()
604                    .base(TxEnv::builder().gas_limit(100))
605                    .source_hash(B256::from([1u8; 32]))
606                    .build_fill(),
607            )
608            .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::BEDROCK);
609        let gas = call_last_frame_return(ctx, InstructionResult::Stop, Gas::new(90));
610        assert_eq!(gas.remaining(), 0);
611        assert_eq!(gas.spent(), 100);
612        assert_eq!(gas.refunded(), 0);
613    }
614
615    #[test]
616    fn test_consume_gas_sys_deposit_tx() {
617        let ctx = Context::op()
618            .with_tx(
619                OpTransaction::builder()
620                    .base(TxEnv::builder().gas_limit(100))
621                    .source_hash(B256::from([1u8; 32]))
622                    .is_system_transaction()
623                    .build_fill(),
624            )
625            .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::BEDROCK);
626        let gas = call_last_frame_return(ctx, InstructionResult::Stop, Gas::new(90));
627        assert_eq!(gas.remaining(), 100);
628        assert_eq!(gas.spent(), 0);
629        assert_eq!(gas.refunded(), 0);
630    }
631
632    #[test]
633    fn test_commit_mint_value() {
634        let caller = Address::ZERO;
635        let mut db = InMemoryDB::default();
636        db.insert_account_info(
637            caller,
638            AccountInfo {
639                balance: U256::from(1000),
640                ..Default::default()
641            },
642        );
643
644        let mut ctx = Context::op()
645            .with_db(db)
646            .with_chain(L1BlockInfo {
647                l1_base_fee: U256::from(1_000),
648                l1_fee_overhead: Some(U256::from(1_000)),
649                l1_base_fee_scalar: U256::from(1_000),
650                ..Default::default()
651            })
652            .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::REGOLITH);
653        ctx.modify_tx(|tx| {
654            tx.deposit.source_hash = B256::from([1u8; 32]);
655            tx.deposit.mint = Some(10);
656        });
657
658        let mut evm = ctx.build_op();
659
660        let handler =
661            OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
662        handler
663            .validate_against_state_and_deduct_caller(&mut evm)
664            .unwrap();
665
666        // Check the account balance is updated.
667        let account = evm.ctx().journal_mut().load_account(caller).unwrap();
668        assert_eq!(account.info.balance, U256::from(1010));
669    }
670
671    #[test]
672    fn test_remove_l1_cost_non_deposit() {
673        let caller = Address::ZERO;
674        let mut db = InMemoryDB::default();
675        db.insert_account_info(
676            caller,
677            AccountInfo {
678                balance: U256::from(1058), // Increased to cover L1 fees (1048) + base fees
679                ..Default::default()
680            },
681        );
682        let ctx = Context::op()
683            .with_db(db)
684            .with_chain(L1BlockInfo {
685                l1_base_fee: U256::from(1_000),
686                l1_fee_overhead: Some(U256::from(1_000)),
687                l1_base_fee_scalar: U256::from(1_000),
688                ..Default::default()
689            })
690            .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::REGOLITH)
691            .with_tx(
692                OpTransaction::builder()
693                    .base(TxEnv::builder().gas_limit(100))
694                    .enveloped_tx(Some(bytes!("FACADE")))
695                    .source_hash(B256::ZERO)
696                    .build()
697                    .unwrap(),
698            );
699
700        let mut evm = ctx.build_op();
701
702        let handler =
703            OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
704        handler
705            .validate_against_state_and_deduct_caller(&mut evm)
706            .unwrap();
707
708        // Check the account balance is updated.
709        let account = evm.ctx().journal_mut().load_account(caller).unwrap();
710        assert_eq!(account.info.balance, U256::from(10)); // 1058 - 1048 = 10
711    }
712
713    #[test]
714    fn test_reload_l1_block_info_isthmus() {
715        const BLOCK_NUM: U256 = uint!(100_U256);
716        const L1_BASE_FEE: U256 = uint!(1_U256);
717        const L1_BLOB_BASE_FEE: U256 = uint!(2_U256);
718        const L1_BASE_FEE_SCALAR: u64 = 3;
719        const L1_BLOB_BASE_FEE_SCALAR: u64 = 4;
720        const L1_FEE_SCALARS: U256 = U256::from_limbs([
721            0,
722            (L1_BASE_FEE_SCALAR << (64 - BASE_FEE_SCALAR_OFFSET * 2)) | L1_BLOB_BASE_FEE_SCALAR,
723            0,
724            0,
725        ]);
726        const OPERATOR_FEE_SCALAR: u64 = 5;
727        const OPERATOR_FEE_CONST: u64 = 6;
728        const OPERATOR_FEE: U256 =
729            U256::from_limbs([OPERATOR_FEE_CONST, OPERATOR_FEE_SCALAR, 0, 0]);
730
731        let mut db = InMemoryDB::default();
732        let l1_block_contract = db.load_account(L1_BLOCK_CONTRACT).unwrap();
733        l1_block_contract
734            .storage
735            .insert(L1_BASE_FEE_SLOT, L1_BASE_FEE);
736        l1_block_contract
737            .storage
738            .insert(ECOTONE_L1_BLOB_BASE_FEE_SLOT, L1_BLOB_BASE_FEE);
739        l1_block_contract
740            .storage
741            .insert(ECOTONE_L1_FEE_SCALARS_SLOT, L1_FEE_SCALARS);
742        l1_block_contract
743            .storage
744            .insert(OPERATOR_FEE_SCALARS_SLOT, OPERATOR_FEE);
745        db.insert_account_info(
746            Address::ZERO,
747            AccountInfo {
748                balance: U256::from(1000),
749                ..Default::default()
750            },
751        );
752
753        let ctx = Context::op()
754            .with_db(db)
755            .with_chain(L1BlockInfo {
756                l2_block: BLOCK_NUM + U256::from(1), // ahead by one block
757                ..Default::default()
758            })
759            .with_block(BlockEnv {
760                number: BLOCK_NUM,
761                ..Default::default()
762            })
763            .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::ISTHMUS);
764
765        let mut evm = ctx.build_op();
766
767        assert_ne!(evm.ctx().chain().l2_block, BLOCK_NUM);
768
769        let handler =
770            OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
771        handler
772            .validate_against_state_and_deduct_caller(&mut evm)
773            .unwrap();
774
775        assert_eq!(
776            *evm.ctx().chain(),
777            L1BlockInfo {
778                l2_block: BLOCK_NUM,
779                l1_base_fee: L1_BASE_FEE,
780                l1_base_fee_scalar: U256::from(L1_BASE_FEE_SCALAR),
781                l1_blob_base_fee: Some(L1_BLOB_BASE_FEE),
782                l1_blob_base_fee_scalar: Some(U256::from(L1_BLOB_BASE_FEE_SCALAR)),
783                empty_ecotone_scalars: false,
784                l1_fee_overhead: None,
785                operator_fee_scalar: Some(U256::from(OPERATOR_FEE_SCALAR)),
786                operator_fee_constant: Some(U256::from(OPERATOR_FEE_CONST)),
787                tx_l1_cost: Some(U256::ZERO),
788            }
789        );
790    }
791
792    #[test]
793    fn test_remove_l1_cost() {
794        let caller = Address::ZERO;
795        let mut db = InMemoryDB::default();
796        db.insert_account_info(
797            caller,
798            AccountInfo {
799                balance: U256::from(1049),
800                ..Default::default()
801            },
802        );
803        let ctx = Context::op()
804            .with_db(db)
805            .with_chain(L1BlockInfo {
806                l1_base_fee: U256::from(1_000),
807                l1_fee_overhead: Some(U256::from(1_000)),
808                l1_base_fee_scalar: U256::from(1_000),
809                ..Default::default()
810            })
811            .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::REGOLITH)
812            .with_tx(
813                OpTransaction::builder()
814                    .base(TxEnv::builder().gas_limit(100))
815                    .source_hash(B256::ZERO)
816                    .enveloped_tx(Some(bytes!("FACADE")))
817                    .build()
818                    .unwrap(),
819            );
820
821        let mut evm = ctx.build_op();
822        let handler =
823            OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
824
825        // l1block cost is 1048 fee.
826        handler
827            .validate_against_state_and_deduct_caller(&mut evm)
828            .unwrap();
829
830        // Check the account balance is updated.
831        let account = evm.ctx().journal_mut().load_account(caller).unwrap();
832        assert_eq!(account.info.balance, U256::from(1));
833    }
834
835    #[test]
836    fn test_remove_operator_cost() {
837        let caller = Address::ZERO;
838        let mut db = InMemoryDB::default();
839        db.insert_account_info(
840            caller,
841            AccountInfo {
842                balance: U256::from(151),
843                ..Default::default()
844            },
845        );
846        let ctx = Context::op()
847            .with_db(db)
848            .with_chain(L1BlockInfo {
849                operator_fee_scalar: Some(U256::from(10_000_000)),
850                operator_fee_constant: Some(U256::from(50)),
851                ..Default::default()
852            })
853            .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::ISTHMUS)
854            .with_tx(
855                OpTransaction::builder()
856                    .base(TxEnv::builder().gas_limit(10))
857                    .enveloped_tx(Some(bytes!("FACADE")))
858                    .build_fill(),
859            );
860
861        let mut evm = ctx.build_op();
862        let handler =
863            OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
864
865        // operator fee cost is operator_fee_scalar * gas_limit / 1e6 + operator_fee_constant
866        // 10_000_000 * 10 / 1_000_000 + 50 = 150
867        handler
868            .validate_against_state_and_deduct_caller(&mut evm)
869            .unwrap();
870
871        // Check the account balance is updated.
872        let account = evm.ctx().journal_mut().load_account(caller).unwrap();
873        assert_eq!(account.info.balance, U256::from(1));
874    }
875
876    #[test]
877    fn test_remove_l1_cost_lack_of_funds() {
878        let caller = Address::ZERO;
879        let mut db = InMemoryDB::default();
880        db.insert_account_info(
881            caller,
882            AccountInfo {
883                balance: U256::from(48),
884                ..Default::default()
885            },
886        );
887        let ctx = Context::op()
888            .with_db(db)
889            .with_chain(L1BlockInfo {
890                l1_base_fee: U256::from(1_000),
891                l1_fee_overhead: Some(U256::from(1_000)),
892                l1_base_fee_scalar: U256::from(1_000),
893                ..Default::default()
894            })
895            .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::REGOLITH)
896            .modify_tx_chained(|tx| {
897                tx.enveloped_tx = Some(bytes!("FACADE"));
898            });
899
900        // l1block cost is 1048 fee.
901        let mut evm = ctx.build_op();
902        let handler =
903            OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
904
905        // l1block cost is 1048 fee.
906        assert_eq!(
907            handler.validate_against_state_and_deduct_caller(&mut evm),
908            Err(EVMError::Transaction(
909                InvalidTransaction::LackOfFundForMaxFee {
910                    fee: Box::new(U256::from(1048)),
911                    balance: Box::new(U256::from(48)),
912                }
913                .into(),
914            ))
915        );
916    }
917
918    #[test]
919    fn test_validate_sys_tx() {
920        // mark the tx as a system transaction.
921        let ctx = Context::op()
922            .modify_tx_chained(|tx| {
923                tx.deposit.source_hash = B256::from([1u8; 32]);
924                tx.deposit.is_system_transaction = true;
925            })
926            .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::REGOLITH);
927
928        let mut evm = ctx.build_op();
929        let handler =
930            OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
931
932        assert_eq!(
933            handler.validate_env(&mut evm),
934            Err(EVMError::Transaction(
935                OpTransactionError::DepositSystemTxPostRegolith
936            ))
937        );
938
939        evm.ctx().modify_cfg(|cfg| cfg.spec = OpSpecId::BEDROCK);
940
941        // Pre-regolith system transactions should be allowed.
942        assert!(handler.validate_env(&mut evm).is_ok());
943    }
944
945    #[test]
946    fn test_validate_deposit_tx() {
947        // Set source hash.
948        let ctx = Context::op()
949            .modify_tx_chained(|tx| {
950                tx.deposit.source_hash = B256::from([1u8; 32]);
951            })
952            .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::REGOLITH);
953
954        let mut evm = ctx.build_op();
955        let handler =
956            OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
957
958        assert!(handler.validate_env(&mut evm).is_ok());
959    }
960
961    #[test]
962    fn test_validate_tx_against_state_deposit_tx() {
963        // Set source hash.
964        let ctx = Context::op()
965            .modify_tx_chained(|tx| {
966                tx.deposit.source_hash = B256::from([1u8; 32]);
967            })
968            .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::REGOLITH);
969
970        let mut evm = ctx.build_op();
971        let handler =
972            OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
973
974        // Nonce and balance checks should be skipped for deposit transactions.
975        assert!(handler.validate_env(&mut evm).is_ok());
976    }
977
978    #[test]
979    fn test_halted_deposit_tx_post_regolith() {
980        let ctx = Context::op()
981            .modify_tx_chained(|tx| {
982                // Set up as deposit transaction by having a deposit with source_hash
983                tx.deposit.source_hash = B256::from([1u8; 32]);
984            })
985            .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::REGOLITH);
986
987        let mut evm = ctx.build_op();
988        let mut handler =
989            OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
990
991        assert_eq!(
992            handler.execution_result(
993                &mut evm,
994                FrameResult::Call(CallOutcome {
995                    result: InterpreterResult {
996                        result: InstructionResult::OutOfGas,
997                        output: Default::default(),
998                        gas: Default::default(),
999                    },
1000                    memory_offset: Default::default(),
1001                })
1002            ),
1003            Err(EVMError::Transaction(
1004                OpTransactionError::HaltedDepositPostRegolith
1005            ))
1006        )
1007    }
1008
1009    #[test]
1010    fn test_tx_zero_value_touch_caller() {
1011        let ctx = Context::op();
1012
1013        let mut evm = ctx.build_op();
1014
1015        assert!(!evm
1016            .0
1017            .ctx
1018            .journal_mut()
1019            .load_account(Address::ZERO)
1020            .unwrap()
1021            .is_touched());
1022
1023        let handler =
1024            OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
1025
1026        handler
1027            .validate_against_state_and_deduct_caller(&mut evm)
1028            .unwrap();
1029
1030        assert!(evm
1031            .0
1032            .ctx
1033            .journal_mut()
1034            .load_account(Address::ZERO)
1035            .unwrap()
1036            .is_touched());
1037    }
1038
1039    #[rstest]
1040    #[case::deposit(true)]
1041    #[case::dyn_fee(false)]
1042    fn test_operator_fee_refund(#[case] is_deposit: bool) {
1043        const SENDER: Address = Address::ZERO;
1044        const GAS_PRICE: u128 = 0xFF;
1045        const OP_FEE_MOCK_PARAM: u128 = 0xFFFF;
1046
1047        let ctx = Context::op()
1048            .with_tx(
1049                OpTransaction::builder()
1050                    .base(
1051                        TxEnv::builder()
1052                            .gas_price(GAS_PRICE)
1053                            .gas_priority_fee(None)
1054                            .caller(SENDER),
1055                    )
1056                    .enveloped_tx(if is_deposit {
1057                        None
1058                    } else {
1059                        Some(bytes!("FACADE"))
1060                    })
1061                    .source_hash(if is_deposit {
1062                        B256::from([1u8; 32])
1063                    } else {
1064                        B256::ZERO
1065                    })
1066                    .build_fill(),
1067            )
1068            .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::ISTHMUS);
1069
1070        let mut evm = ctx.build_op();
1071        let handler =
1072            OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
1073
1074        // Set the operator fee scalar & constant to non-zero values in the L1 block info.
1075        evm.ctx().chain.operator_fee_scalar = Some(U256::from(OP_FEE_MOCK_PARAM));
1076        evm.ctx().chain.operator_fee_constant = Some(U256::from(OP_FEE_MOCK_PARAM));
1077
1078        let mut gas = Gas::new(100);
1079        gas.set_spent(10);
1080        let mut exec_result = FrameResult::Call(CallOutcome::new(
1081            InterpreterResult {
1082                result: InstructionResult::Return,
1083                output: Default::default(),
1084                gas,
1085            },
1086            0..0,
1087        ));
1088
1089        // Reimburse the caller for the unspent portion of the fees.
1090        handler
1091            .reimburse_caller(&mut evm, &mut exec_result)
1092            .unwrap();
1093
1094        // Compute the expected refund amount. If the transaction is a deposit, the operator fee refund never
1095        // applies. If the transaction is not a deposit, the operator fee refund is added to the refund amount.
1096        let mut expected_refund =
1097            U256::from(GAS_PRICE * (gas.remaining() + gas.refunded() as u64) as u128);
1098        let op_fee_refund = evm
1099            .ctx()
1100            .chain()
1101            .operator_fee_refund(&gas, OpSpecId::ISTHMUS);
1102        assert!(op_fee_refund > U256::ZERO);
1103
1104        if !is_deposit {
1105            expected_refund += op_fee_refund;
1106        }
1107
1108        // Check that the caller was reimbursed the correct amount of ETH.
1109        let account = evm.ctx().journal_mut().load_account(SENDER).unwrap();
1110        assert_eq!(account.info.balance, expected_refund);
1111    }
1112
1113    #[test]
1114    fn test_tx_low_balance_nonce_unchanged() {
1115        let ctx = Context::op().with_tx(
1116            OpTransaction::builder()
1117                .base(TxEnv::builder().value(U256::from(1000)))
1118                .build_fill(),
1119        );
1120
1121        let mut evm = ctx.build_op();
1122
1123        let handler =
1124            OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
1125
1126        let result = handler.validate_against_state_and_deduct_caller(&mut evm);
1127
1128        assert!(matches!(
1129            result.err().unwrap(),
1130            EVMError::Transaction(OpTransactionError::Base(
1131                InvalidTransaction::LackOfFundForMaxFee { .. }
1132            ))
1133        ));
1134        assert_eq!(
1135            evm.0
1136                .ctx
1137                .journal_mut()
1138                .load_account(Address::ZERO)
1139                .unwrap()
1140                .info
1141                .nonce,
1142            0
1143        );
1144    }
1145}