From f9e8e66830e856e06eb9280f594e982cb7b36191 Mon Sep 17 00:00:00 2001 From: anylots <22675649+anylots@users.noreply.github.com> Date: Tue, 20 Jan 2026 17:21:19 +0800 Subject: [PATCH 1/6] disable_fee_charge for sys tx --- .../src/transaction/l1_transaction.rs | 2 +- crates/revm/src/handler.rs | 90 +++++++++++-------- crates/revm/src/token_fee.rs | 1 + 3 files changed, 54 insertions(+), 39 deletions(-) diff --git a/crates/primitives/src/transaction/l1_transaction.rs b/crates/primitives/src/transaction/l1_transaction.rs index c1c4641..f23ad82 100644 --- a/crates/primitives/src/transaction/l1_transaction.rs +++ b/crates/primitives/src/transaction/l1_transaction.rs @@ -158,7 +158,7 @@ impl Transaction for TxL1Msg { } fn nonce(&self) -> u64 { - 0 + self.nonce } fn gas_limit(&self) -> u64 { diff --git a/crates/revm/src/handler.rs b/crates/revm/src/handler.rs index 5c25a37..70ffe15 100644 --- a/crates/revm/src/handler.rs +++ b/crates/revm/src/handler.rs @@ -1,7 +1,6 @@ //! Morph EVM Handler implementation. use alloy_primitives::{Address, Bytes, U256}; -use morph_primitives::L1_TX_TYPE_ID; use revm::{ ExecuteEvm, context::{ @@ -96,8 +95,20 @@ where &self, evm: &mut Self::Evm, ) -> Result<(), Self::Error> { - // L1 message transactions skip all validation - everything is handled on L1 side - if evm.ctx_ref().tx().is_l1_msg() { + let (_, tx, cfg, journal, _, _) = evm.ctx().all_mut(); + // System transaction - skip all validation + if cfg.disable_fee_charge { + return Ok(()); + } + // L1 message - skip fee validation + if tx.is_l1_msg() { + // Load caller's account + let mut caller = journal.load_account_with_code_mut(tx.caller())?.data; + + // Bump nonce for calls (CREATE nonce is bumped in make_create_frame) + if tx.kind().is_call() { + caller.bump_nonce(); + } return Ok(()); } @@ -117,15 +128,17 @@ where evm: &mut Self::Evm, exec_result: &mut <::Frame as FrameTr>::FrameResult, ) -> Result<(), Self::Error> { - // For L1 message transactions, no reimbursement is needed - if evm.ctx_ref().tx().is_l1_msg() { + let (_, tx, cfg, _, _, _) = evm.ctx().all_mut(); + + // For L1 message transactions & system transactions, no reimbursement is needed + if tx.is_l1_msg() || cfg.disable_fee_charge { return Ok(()); } // Check if transaction is AltFeeTx (tx_type 0x7F) which uses token fee - if evm.ctx_ref().tx().is_alt_fee_tx() { + if tx.is_alt_fee_tx() { // Get fee_token_id directly from MorphTxEnv - let token_id = evm.ctx_ref().tx().fee_token_id.unwrap_or_default(); + let token_id = tx.fee_token_id.unwrap_or_default(); return self.reimburse_caller_token_fee(evm, exec_result.gas(), token_id); } @@ -140,31 +153,31 @@ where evm: &mut Self::Evm, exec_result: &mut <::Frame as FrameTr>::FrameResult, ) -> Result<(), Self::Error> { - // L1 message transactions skip all validation - everything is handled on L1 side - if evm.ctx_ref().tx().is_l1_msg() { + let (block, tx, cfg, journal, _, _) = evm.ctx().all_mut(); + // System transaction - skip all reward + if cfg.disable_fee_charge { return Ok(()); } + // L1 message transactions skip all reward. // AltFeeTx rewards are already applied when gasFee is deducted. - if evm.ctx_ref().tx().is_alt_fee_tx() { + if tx.is_l1_msg() || tx.is_alt_fee_tx() { return Ok(()); } - let beneficiary = evm.ctx_ref().block().beneficiary(); + let beneficiary = block.beneficiary(); - let basefee = evm.ctx_ref().block().basefee() as u128; - let effective_gas_price = evm.ctx_ref().tx().effective_gas_price(basefee); + let basefee = block.basefee() as u128; + let effective_gas_price = tx.effective_gas_price(basefee); // Get the current hardfork for L1 fee calculation - let hardfork = evm.ctx_ref().cfg().spec(); + let hardfork = cfg.spec(); // Fetch L1 block info from the L1 Gas Price Oracle contract - let l1_block_info = L1BlockInfo::try_fetch(evm.ctx_mut().db_mut(), hardfork)?; + let l1_block_info = L1BlockInfo::try_fetch(journal.db_mut(), hardfork)?; // Get RLP-encoded transaction bytes for L1 fee calculation // This represents the full transaction data posted to L1 for data availability - let rlp_bytes = evm - .ctx_ref() - .tx() + let rlp_bytes = tx .rlp_bytes .as_ref() .map(|b| b.as_ref()) @@ -173,9 +186,6 @@ where // Calculate L1 data fee based on full RLP-encoded transaction let l1_data_fee = l1_block_info.calculate_tx_l1_cost(rlp_bytes, hardfork); - // Get mutable access to journal components - let journal = evm.ctx().journal_mut(); - let gas_used = exec_result.gas().used(); let execution_fee = U256::from(effective_gas_price).saturating_mul(U256::from(gas_used)); @@ -190,8 +200,8 @@ where #[inline] fn validate_env(&self, evm: &mut Self::Evm) -> Result<(), Self::Error> { - // For L1 message transactions, skip certain validations - if evm.ctx_ref().tx().is_l1_msg() { + // For L1 message transactions & System transaction, skip certain validations + if evm.ctx_ref().tx().is_l1_msg() || evm.ctx_ref().cfg().disable_fee_charge { // L1 messages have zero gas price, so skip gas price validation return Ok(()); } @@ -460,7 +470,6 @@ where } } else { // Transfer with evm call. - let tx_origin = evm.tx.clone(); transfer_erc20_with_evm( evm, token_fee_info.caller, @@ -468,8 +477,6 @@ where token_fee_info.token_address, token_amount_required, )?; - // restore the original transaction - evm.tx = tx_origin; } let (_, tx, cfg, journal, _, _) = evm.ctx().all_mut(); @@ -542,14 +549,14 @@ fn transfer_erc20_with_evm( where DB: alloy_evm::Database, { - let calldata = build_transfer_calldata(to, token_amount); + let tx_origin = evm.tx.clone(); + let calldata = build_transfer_calldata(to, token_amount); let tx_env = revm::context::TxEnv { caller, gas_limit: TRANSFER_GAS_LIMIT, kind: token_address.into(), data: calldata, - tx_type: L1_TX_TYPE_ID, // Mark as L1 message to skip gas validation ..Default::default() }; @@ -558,23 +565,30 @@ where rlp_bytes: None, ..Default::default() }; - match evm.transact_one(tx) { + + evm.cfg.disable_fee_charge = true; // Disable fee charge for system call + let res = match evm.transact_one(tx) { Ok(result) => { - if !result.is_success() { - return Err(MorphInvalidTransaction::TokenTransferFailed { + if result.is_success() { + Ok(()) + } else { + Err(MorphInvalidTransaction::TokenTransferFailed { reason: format!("{result:?}"), } - .into()); + .into()) } } - Err(e) => { - return Err(MorphInvalidTransaction::TokenTransferFailed { - reason: format!("Error: {e:?}"), - } - .into()); + Err(e) => Err(MorphInvalidTransaction::TokenTransferFailed { + reason: format!("Error: {e:?}"), } + .into()), }; - Ok(()) + + // restore the original transaction + evm.cfg.disable_fee_charge = false; + evm.tx = tx_origin; + + res } /// Build the calldata for ERC20 transfer(address,amount) call. diff --git a/crates/revm/src/token_fee.rs b/crates/revm/src/token_fee.rs index 6fd71c4..92cede0 100644 --- a/crates/revm/src/token_fee.rs +++ b/crates/revm/src/token_fee.rs @@ -269,6 +269,7 @@ where let morph_tx = crate::MorphTxEnv::new(tx); // Execute using transact_one + evm.cfg.disable_fee_charge = true; // Disable fee charge for system call match evm.transact_one(morph_tx) { Ok(result) => { if result.is_success() { From e746cd7ab8ae9c4e8915533c4baf11ad4fb5b960 Mon Sep 17 00:00:00 2001 From: anylots <22675649+anylots@users.noreply.github.com> Date: Wed, 21 Jan 2026 12:37:37 +0800 Subject: [PATCH 2/6] use system_call for altfee gas --- crates/primitives/src/transaction/envelope.rs | 3 ++- .../src/transaction/l1_transaction.rs | 2 -- crates/revm/src/exec.rs | 2 +- crates/revm/src/handler.rs | 25 +++---------------- 4 files changed, 6 insertions(+), 26 deletions(-) diff --git a/crates/primitives/src/transaction/envelope.rs b/crates/primitives/src/transaction/envelope.rs index 8ce2a9c..4893509 100644 --- a/crates/primitives/src/transaction/envelope.rs +++ b/crates/primitives/src/transaction/envelope.rs @@ -163,7 +163,8 @@ impl alloy_consensus::transaction::SignerRecoverable for MorphTxEnvelope { Self::Eip7702(tx) => { alloy_consensus::transaction::SignerRecoverable::recover_signer(tx) } - Self::L1Msg(tx) => alloy_consensus::transaction::SignerRecoverable::recover_signer(tx), + // L1 msg does not have a signature. Directly return the `from` address. + Self::L1Msg(tx) => Ok(tx.tx().from), Self::AltFee(tx) => alloy_consensus::transaction::SignerRecoverable::recover_signer(tx), } } diff --git a/crates/primitives/src/transaction/l1_transaction.rs b/crates/primitives/src/transaction/l1_transaction.rs index 9ba3233..9f71afe 100644 --- a/crates/primitives/src/transaction/l1_transaction.rs +++ b/crates/primitives/src/transaction/l1_transaction.rs @@ -102,7 +102,6 @@ impl TxL1Msg { pub fn fields_len(&self) -> usize { let mut len = 0; len += self.queue_index.length(); - len += self.nonce.length(); len += self.gas_limit.length(); len += self.to.length(); len += self.value.length(); @@ -114,7 +113,6 @@ impl TxL1Msg { /// Encode the transaction fields (without the RLP header). pub fn encode_fields(&self, out: &mut dyn BufMut) { self.queue_index.encode(out); - self.nonce.encode(out); self.gas_limit.encode(out); self.to.encode(out); self.value.encode(out); diff --git a/crates/revm/src/exec.rs b/crates/revm/src/exec.rs index 6a9579e..c96c08b 100644 --- a/crates/revm/src/exec.rs +++ b/crates/revm/src/exec.rs @@ -20,7 +20,7 @@ use revm::{ }; /// Total gas system transactions are allowed to use. -const SYSTEM_CALL_GAS_LIMIT: u64 = 250_000_000; +const SYSTEM_CALL_GAS_LIMIT: u64 = 200_000; impl ExecuteEvm for MorphEvm where diff --git a/crates/revm/src/handler.rs b/crates/revm/src/handler.rs index 70ffe15..752b8eb 100644 --- a/crates/revm/src/handler.rs +++ b/crates/revm/src/handler.rs @@ -2,7 +2,7 @@ use alloy_primitives::{Address, Bytes, U256}; use revm::{ - ExecuteEvm, + SystemCallEvm, context::{ Cfg, ContextTr, JournalTr, Transaction, result::{EVMError, ExecutionResult, InvalidTransaction}, @@ -14,7 +14,7 @@ use revm::{ }; use crate::{ - MorphEvm, MorphInvalidTransaction, MorphTxEnv, + MorphEvm, MorphInvalidTransaction, error::MorphHaltReason, evm::MorphContext, l1block::L1BlockInfo, @@ -535,9 +535,6 @@ where Ok((from_storage_slot, to_storage_slot)) } -/// Gas limit for ERC20 transfer calls. -const TRANSFER_GAS_LIMIT: u64 = 200000; - /// Transfers ERC20 tokens by executing a `transfer(address,uint256)` call via the EVM. fn transfer_erc20_with_evm( evm: &mut MorphEvm, @@ -552,22 +549,7 @@ where let tx_origin = evm.tx.clone(); let calldata = build_transfer_calldata(to, token_amount); - let tx_env = revm::context::TxEnv { - caller, - gas_limit: TRANSFER_GAS_LIMIT, - kind: token_address.into(), - data: calldata, - ..Default::default() - }; - - let tx = MorphTxEnv { - inner: tx_env, - rlp_bytes: None, - ..Default::default() - }; - - evm.cfg.disable_fee_charge = true; // Disable fee charge for system call - let res = match evm.transact_one(tx) { + let res = match evm.system_call_one_with_caller(caller, token_address, calldata) { Ok(result) => { if result.is_success() { Ok(()) @@ -585,7 +567,6 @@ where }; // restore the original transaction - evm.cfg.disable_fee_charge = false; evm.tx = tx_origin; res From bb51603128650ea26ea358727b5b613e4250b6f3 Mon Sep 17 00:00:00 2001 From: anylots <22675649+anylots@users.noreply.github.com> Date: Fri, 23 Jan 2026 00:07:16 +0800 Subject: [PATCH 3/6] set precompiles by spec_id --- crates/revm/src/evm.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/revm/src/evm.rs b/crates/revm/src/evm.rs index 72d0778..2b66d9d 100644 --- a/crates/revm/src/evm.rs +++ b/crates/revm/src/evm.rs @@ -6,11 +6,11 @@ use revm::{ Context, Inspector, context::{CfgEnv, ContextError, Evm, FrameStack}, handler::{ - EthFrame, EthPrecompiles, EvmTr, FrameInitOrResult, FrameTr, ItemOrResult, - instructions::EthInstructions, + EthFrame, EvmTr, FrameInitOrResult, FrameTr, ItemOrResult, instructions::EthInstructions, }, inspector::InspectorEvmTr, interpreter::interpreter::EthInterpreter, + precompile::{PrecompileSpecId, Precompiles}, }; /// The Morph EVM context type. @@ -37,8 +37,9 @@ pub struct MorphEvm { impl MorphEvm { /// Create a new Morph EVM. pub fn new(ctx: MorphContext, inspector: I) -> Self { - let precompiles = PrecompilesMap::from_static(EthPrecompiles::default().precompiles); - + let precompiles = PrecompilesMap::from_static(Precompiles::new( + PrecompileSpecId::from_spec_id(ctx.cfg.spec.into()), + )); Self::new_inner(Evm { ctx, inspector, From c22bbae06819ed77561851b1368ba3c8bd81cc0b Mon Sep 17 00:00:00 2001 From: anylots <22675649+anylots@users.noreply.github.com> Date: Fri, 23 Jan 2026 13:13:26 +0800 Subject: [PATCH 4/6] tx repair --- crates/primitives/src/lib.rs | 1 + crates/primitives/src/transaction/envelope.rs | 5 ++--- crates/revm/src/tx.rs | 18 +++++++++++------- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index a3b6a11..d9517a6 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -51,6 +51,7 @@ pub use alloy_consensus::Header; /// Header alias for backwards compatibility. pub type MorphHeader = Header; +#[cfg(feature = "reth-codec")] use reth_primitives_traits::NodePrimitives; /// Morph block. diff --git a/crates/primitives/src/transaction/envelope.rs b/crates/primitives/src/transaction/envelope.rs index 4893509..d6faf31 100644 --- a/crates/primitives/src/transaction/envelope.rs +++ b/crates/primitives/src/transaction/envelope.rs @@ -185,9 +185,8 @@ impl alloy_consensus::transaction::SignerRecoverable for MorphTxEnvelope { Self::Eip7702(tx) => { alloy_consensus::transaction::SignerRecoverable::recover_signer_unchecked(tx) } - Self::L1Msg(tx) => { - alloy_consensus::transaction::SignerRecoverable::recover_signer_unchecked(tx) - } + // L1 msg does not have a signature. Directly return the `from` address. + Self::L1Msg(tx) => Ok(tx.tx().from), Self::AltFee(tx) => { alloy_consensus::transaction::SignerRecoverable::recover_signer_unchecked(tx) } diff --git a/crates/revm/src/tx.rs b/crates/revm/src/tx.rs index cc5d235..5c49fc2 100644 --- a/crates/revm/src/tx.rs +++ b/crates/revm/src/tx.rs @@ -89,11 +89,11 @@ impl MorphTxEnv { // Extract fee_token_id for AltFeeTx (type 0x7F) let fee_token_info = if tx_type == ALT_FEE_TX_TYPE_ID { ( - extract_fee_token_id_from_rlp(&rlp_bytes), - extract_fee_limit_from_rlp(&rlp_bytes), + Some(extract_fee_token_id_from_rlp(&rlp_bytes)), + Some(extract_fee_limit_from_rlp(&rlp_bytes)), ) } else { - (0, U256::default()) + (None, None) }; // Build TxEnv from the transaction @@ -131,10 +131,14 @@ impl MorphTxEnv { }; // Use builder pattern to set Morph-specific fields - Self::new(inner) - .with_rlp_bytes(rlp_bytes) - .with_fee_token_id(fee_token_info.0) - .with_fee_limit(fee_token_info.1) + let mut env = Self::new(inner).with_rlp_bytes(rlp_bytes); + if let Some(fee_token_id) = fee_token_info.0 { + env = env.with_fee_token_id(fee_token_id); + }; + if let Some(fee_limit) = fee_token_info.1 { + env = env.with_fee_limit(fee_limit); + }; + env } } From e403cab17d3a82705bb410f2060b48f1aad3a180 Mon Sep 17 00:00:00 2001 From: anylots <22675649+anylots@users.noreply.github.com> Date: Fri, 23 Jan 2026 14:40:55 +0800 Subject: [PATCH 5/6] enable set_hardfork for spec --- crates/chainspec/src/spec.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/chainspec/src/spec.rs b/crates/chainspec/src/spec.rs index 6bfda8c..7df0e93 100644 --- a/crates/chainspec/src/spec.rs +++ b/crates/chainspec/src/spec.rs @@ -164,6 +164,14 @@ impl From for MorphChainSpec { } } +impl MorphChainSpec { + pub fn set_hardfork(&mut self, hardfork: MorphHardfork, time: u64) { + self.inner + .hardforks + .insert(hardfork, ForkCondition::Timestamp(time)); + } +} + // ============================================================================= // Trait Implementations // ============================================================================= From 2e50849882ef8b6fed31c4bac1f60210c37b4527 Mon Sep 17 00:00:00 2001 From: anylots <22675649+anylots@users.noreply.github.com> Date: Fri, 23 Jan 2026 18:42:17 +0800 Subject: [PATCH 6/6] disable SELFDESTRUCT BLOBHASH BLOBBASEFEE opcode --- crates/revm/src/evm.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/crates/revm/src/evm.rs b/crates/revm/src/evm.rs index 39a62ba..03658c4 100644 --- a/crates/revm/src/evm.rs +++ b/crates/revm/src/evm.rs @@ -9,7 +9,7 @@ use revm::{ EthFrame, EvmTr, FrameInitOrResult, FrameTr, ItemOrResult, instructions::EthInstructions, }, inspector::InspectorEvmTr, - interpreter::interpreter::EthInterpreter, + interpreter::{Instruction, interpreter::EthInterpreter}, }; /// The Morph EVM context type. @@ -42,11 +42,17 @@ impl MorphEvm { // Get the current hardfork spec from context and create matching precompiles let spec = ctx.cfg.spec; let precompiles = MorphPrecompiles::new_with_spec(spec); - + let mut instructions = EthInstructions::new_mainnet(); + // SELFDESTRUCT is disabled in Morph + instructions.insert_instruction(0xff, Instruction::unknown()); + // BLOBHASH is disabled in Morph + instructions.insert_instruction(0x49, Instruction::unknown()); + // BLOBBASEFEE is disabled in Morph + instructions.insert_instruction(0x4a, Instruction::unknown()); Self::new_inner(Evm { ctx, inspector, - instruction: EthInstructions::new_mainnet(), + instruction: instructions, precompiles, frame_stack: FrameStack::new(), })