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