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