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