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