revm_optimism/
handler.rs

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