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