Skip to content

Conversation

@anylots
Copy link
Contributor

@anylots anylots commented Jan 23, 2026

This PR introduces several significant changes to the Morph EVM and chain specification.

Key Changes:

  • Use system_call for Alt-Fee Gas: The mechanism for paying transaction fees using alternative tokens (alt-fee) has been fundamentally refactored. It now utilizes a system_call to execute the ERC20 token transfer for the fee payment. This isolates the fee payment logic from the main transaction's execution context, leading to a more robust and secure implementation.

  • Enable Dynamic Hardfork Configuration: The MorphChainSpec now includes a set_hardfork function. This allows for the dynamic configuration of hardfork activation times, which is essential for testing and managing network upgrades more flexibly.

  • Disable Opcodes: To enhance security and align with the specific requirements of the Morph network, the following opcodes have been disabled:

    • SELFDESTRUCT
    • BLOBHASH
    • BLOBBASEFEE

Other Changes:

  • The gas limit for system calls has been adjusted to a more appropriate value.
  • The EVM handler logic has been refactored for better clarity and to properly differentiate between regular transactions, L1 messages, and system calls.

These changes improve the security, flexibility, and robustness of the Morph chain.

Summary by CodeRabbit

  • Updates
    • Disabled SELFDESTRUCT, BLOBHASH, and BLOBBASEFEE opcodes in the Morph EVM.
    • Enhanced validation and fee handling for system transactions and L1-message transactions.
    • Reduced system call gas limits and improved fee token configuration logic.
    • Refined ERC20-based fee payment flows and L1 fee calculations.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 23, 2026

📝 Walkthrough

Walkthrough

This change set introduces runtime hardfork configuration capability, disables specific Ethereum opcodes (SELFDESTRUCT, BLOBHASH, BLOBBASEFEE) for Morph execution, reduces system call gas limits, and restructures fee handling to support L1 messages, system transactions, and ERC20-based fee payments with improved L1 cost calculations.

Changes

Cohort / File(s) Summary
Hardfork Configuration
crates/chainspec/src/spec.rs
Adds set_hardfork() method to enable programmatic modification of hardfork activation times via timestamp-based fork conditions after construction.
Opcode Disabling
crates/revm/src/evm.rs
Disables SELFDESTRUCT (0xff), BLOBHASH (0x49), and BLOBBASEFEE (0x4a) opcodes by default in Morph's EVM instance through instruction set customization.
Gas Limits
crates/revm/src/exec.rs
Reduces SYSTEM_CALL_GAS_LIMIT from 250,000,000 to 200,000, lowering maximum gas available for system call operations.
Fee Handling & L1 Integration
crates/revm/src/handler.rs, crates/revm/src/token_fee.rs, crates/revm/src/tx.rs
Restructures fee validation and payment logic: system transactions and L1 messages bypass normal fee validation; adds L1 cost calculations for fee deductions; implements ERC20-based fee payments via system calls with state restoration; conditionally populates fee token fields based on ALT_FEE_TX presence.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Refactor: Tx process and Validation #8: Modifies overlapping revm token-fee and tx-processing code paths (reimburse_caller, token-fee logic, ERC20 transfer helpers, fee_limit extraction into MorphTxEnv).
  • feat: set precompiles for each spec #11: Related through hardfork/spec handling where runtime selection of behavior is based on active MorphHardfork (complements the new set_hardfork capability).

Suggested reviewers

  • chengwenxi

Poem

🐰 A hardfork blooms at runtime's call,
Opcodes disabled, fees refine,
L1 costs flow through it all,
System transactions align,
ERC20 tokens take their toll.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main changes: refactoring the alt-fee mechanism and updating EVM configuration, which are the primary focuses across multiple files.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/revm/src/token_fee.rs (1)

271-288: The disable_fee_charge flag is not restored after the EVM call.

Setting evm.cfg.disable_fee_charge = true modifies the EVM's configuration, but it's not restored to its original value after transact_one completes. If the same EVM instance is reused after this function call, subsequent transactions will unexpectedly have fee charging disabled.

🐛 Proposed fix
     // Execute using transact_one
+    let original_disable_fee_charge = evm.cfg.disable_fee_charge;
     evm.cfg.disable_fee_charge = true; // Disable fee charge for system call
-    match evm.transact_one(morph_tx) {
+    let result = match evm.transact_one(morph_tx) {
         Ok(result) => {
             if result.is_success() {
                 // Parse the returned balance (32 bytes)
                 if let Some(output) = result.output()
                     && output.len() >= 32
                 {
                     return Ok(U256::from_be_slice(&output[..32]));
                 }
             }
             Ok(U256::ZERO)
         }
         Err(_) => {
             // On error, return zero (matches original behavior)
             Ok(U256::ZERO)
         }
-    }
+    };
+    evm.cfg.disable_fee_charge = original_disable_fee_charge;
+    result
 }
🧹 Nitpick comments (5)
crates/chainspec/src/spec.rs (1)

169-175: The set_hardfork method only supports timestamp-based activation.

This method unconditionally uses ForkCondition::Timestamp(time), but some Morph hardforks (Bernoulli and Curie) are block-based according to lines 142-147. If this method is intended to support all hardforks, consider adding a variant that accepts block-based conditions or documenting this limitation.

♻️ Suggested improvement
 impl MorphChainSpec {
-    pub fn set_hardfork(&mut self, hardfork: MorphHardfork, time: u64) {
+    /// Set a timestamp-based hardfork activation.
+    ///
+    /// Note: This only supports timestamp-based hardforks (Morph203, Viridian, Emerald, MPTFork).
+    /// For block-based hardforks (Bernoulli, Curie), use `set_hardfork_at_block`.
+    pub fn set_hardfork_at_timestamp(&mut self, hardfork: MorphHardfork, time: u64) {
         self.inner
             .hardforks
             .insert(hardfork, ForkCondition::Timestamp(time));
     }
+
+    /// Set a block-based hardfork activation.
+    pub fn set_hardfork_at_block(&mut self, hardfork: MorphHardfork, block: u64) {
+        self.inner
+            .hardforks
+            .insert(hardfork, ForkCondition::Block(block));
+    }
 }
crates/revm/src/tx.rs (1)

145-175: Silent failure in RLP extraction could mask malformed transactions.

Both extract_fee_token_id_from_rlp and extract_fee_limit_from_rlp return default values (0 / U256::default()) when decoding fails. For an ALT_FEE_TX with malformed RLP, this causes fee_token_id to become 0, which later triggers a TokenIdZeroNotSupported error in handler.rs. This masks the actual issue (malformed RLP) with a misleading error.

Consider logging a warning or returning an Option/Result to surface decoding failures.

♻️ Suggested improvement
 fn extract_fee_token_id_from_rlp(rlp_bytes: &Bytes) -> u16 {
     if rlp_bytes.is_empty() {
         return 0;
     }

     // Skip the type byte (0x7F) and decode the AltFeeTx
     let payload = &rlp_bytes[1..];
-    TxAltFee::decode(&mut &payload[..])
-        .map(|tx| tx.fee_token_id)
-        .unwrap_or(0)
+    match TxAltFee::decode(&mut &payload[..]) {
+        Ok(tx) => tx.fee_token_id,
+        Err(e) => {
+            tracing::warn!(error = ?e, "Failed to decode AltFeeTx for fee_token_id extraction");
+            0
+        }
+    }
 }
crates/revm/src/evm.rs (1)

45-51: Consider using named constants for disabled opcodes.

The opcode bytes are hardcoded as hex values. Using named constants would improve readability and maintainability.

♻️ Suggested improvement
+// Disabled opcodes in Morph EVM
+const OPCODE_SELFDESTRUCT: u8 = 0xff;
+const OPCODE_BLOBHASH: u8 = 0x49;
+const OPCODE_BLOBBASEFEE: u8 = 0x4a;
+
 impl<DB: Database, I> MorphEvm<DB, I> {
     pub fn new(ctx: MorphContext<DB>, inspector: I) -> Self {
         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());
+        // Disable unsupported opcodes in Morph L2
+        instructions.insert_instruction(OPCODE_SELFDESTRUCT, Instruction::unknown());
+        instructions.insert_instruction(OPCODE_BLOBHASH, Instruction::unknown());
+        instructions.insert_instruction(OPCODE_BLOBBASEFEE, Instruction::unknown());
crates/revm/src/handler.rs (2)

363-372: Minor inconsistency: Using token_fee_info.caller instead of local caller variable.

Line 368 passes token_fee_info.caller while the slot-based transfer at line 357-361 uses the local caller variable. While they should be equivalent (since token_fee_info was fetched with caller at line 339), using the local variable consistently would be clearer.

♻️ Suggested fix
         } else {
             // Transfer with evm call.
             transfer_erc20_with_evm(
                 evm,
                 beneficiary,
-                token_fee_info.caller,
+                caller,
                 token_fee_info.token_address,
                 token_amount_required,
             )?;
         }

473-482: Same inconsistency: token_fee_info.caller vs caller_addr in EVM transfer.

The slot-based transfer (line 454) uses caller_addr, but the EVM-based transfer (line 477) uses token_fee_info.caller. For consistency and clarity, both should use caller_addr.

♻️ Suggested fix
         } else {
             // Transfer with evm call.
             transfer_erc20_with_evm(
                 evm,
-                token_fee_info.caller,
+                caller_addr,
                 beneficiary,
                 token_fee_info.token_address,
                 token_amount_required,
             )?;
         }

@anylots anylots merged commit 739ed44 into main Jan 25, 2026
9 checks passed
@anylots anylots deleted the execute branch January 25, 2026 15:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants