diff --git a/Cargo.lock b/Cargo.lock index d930672..bfc0b09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6438,7 +6438,6 @@ dependencies = [ "ark-serialize 0.5.0", "arrayref", "aurora-engine-modexp", - "blst", "c-kzg", "cfg-if", "k256", diff --git a/Cargo.toml b/Cargo.toml index 883cb0a..d068f44 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,12 +8,12 @@ publish = false [workspace] resolver = "3" members = [ - "crates/chainspec", - "crates/evm", - "crates/payload/builder", - "crates/payload/types", - "crates/primitives", - "crates/revm", + "crates/chainspec", + "crates/evm", + "crates/payload/builder", + "crates/payload/types", + "crates/primitives", + "crates/revm", ] [workspace.lints] @@ -95,7 +95,9 @@ reth-revm = { git = "https://github.com/paradigmxyz/reth", rev = "64909d3", feat "std", "optional-checks", ] } -revm = { version = "33.1.0", features = ["optional_fee_charge"] } +revm = { version = "33.1.0", features = [ + "optional_fee_charge", +], default-features = false } alloy = { version = "1.1.3", default-features = false } alloy-consensus = { version = "1.1.3", default-features = false } @@ -156,6 +158,5 @@ tracing = "0.1.41" tracing-subscriber = "0.3.19" criterion = "0.7.0" test-case = "3" -secp256k1 = "0.30.0" pyroscope = "0.5.8" pyroscope_pprofrs = "0.2.10" diff --git a/crates/chainspec/Cargo.toml b/crates/chainspec/Cargo.toml index 7868036..18fedf4 100644 --- a/crates/chainspec/Cargo.toml +++ b/crates/chainspec/Cargo.toml @@ -28,7 +28,7 @@ serde.workspace = true serde_json.workspace = true [features] -default = ["serde", "cli"] +default = ["serde"] serde = [ "alloy-eips/serde", "alloy-hardforks/serde", diff --git a/crates/chainspec/src/spec.rs b/crates/chainspec/src/spec.rs index 4e5acc8..59594cf 100644 --- a/crates/chainspec/src/spec.rs +++ b/crates/chainspec/src/spec.rs @@ -10,6 +10,7 @@ use reth_chainspec::{ Head, }; use reth_network_peers::NodeRecord; +#[cfg(feature = "cli")] use std::sync::Arc; pub const MORPH_BASE_FEE: u64 = 10_000_000_000; diff --git a/crates/evm/Cargo.toml b/crates/evm/Cargo.toml index 345186e..b7f032a 100644 --- a/crates/evm/Cargo.toml +++ b/crates/evm/Cargo.toml @@ -16,15 +16,15 @@ morph-chainspec.workspace = true morph-primitives.workspace = true morph-revm.workspace = true -reth-chainspec.workspace = true +reth-chainspec = { workspace = true, optional = true } reth-evm.workspace = true reth-evm-ethereum.workspace = true reth-revm.workspace = true -reth-primitives-traits.workspace = true +reth-primitives-traits = { workspace = true, optional = true } reth-rpc-eth-api = { workspace = true, optional = true } alloy-evm.workspace = true -alloy-consensus.workspace = true +alloy-consensus = { workspace = true, optional = true } alloy-primitives.workspace = true derive_more.workspace = true @@ -36,6 +36,7 @@ serde_json.workspace = true alloy-genesis.workspace = true [features] -default = ["rpc"] +default = [] +reth-codec = ["morph-primitives/reth-codec"] rpc = ["dep:reth-rpc-eth-api", "morph-revm/rpc"] engine = [] diff --git a/crates/evm/src/config.rs b/crates/evm/src/config.rs new file mode 100644 index 0000000..1addf92 --- /dev/null +++ b/crates/evm/src/config.rs @@ -0,0 +1,110 @@ +use crate::{ + MorphBlockAssembler, MorphBlockExecutionCtx, MorphEvmConfig, MorphEvmError, + MorphNextBlockEnvAttributes, +}; +use alloy_consensus::BlockHeader; +use morph_chainspec::hardfork::MorphHardforks; +use morph_primitives::Block; +use morph_primitives::{MorphHeader, MorphPrimitives}; +use morph_revm::MorphBlockEnv; +use reth_chainspec::EthChainSpec; +use reth_evm::{ + ConfigureEvm, EvmEnv, EvmEnvFor, + eth::{EthBlockExecutionCtx, NextEvmEnvAttributes}, +}; +use reth_primitives_traits::{SealedBlock, SealedHeader}; +use std::borrow::Cow; + +impl ConfigureEvm for MorphEvmConfig { + type Primitives = MorphPrimitives; + type Error = MorphEvmError; + type NextBlockEnvCtx = MorphNextBlockEnvAttributes; + type BlockExecutorFactory = Self; + type BlockAssembler = MorphBlockAssembler; + + fn block_executor_factory(&self) -> &Self::BlockExecutorFactory { + self + } + + fn block_assembler(&self) -> &Self::BlockAssembler { + &self.block_assembler + } + + fn evm_env(&self, header: &MorphHeader) -> Result, Self::Error> { + let EvmEnv { cfg_env, block_env } = EvmEnv::for_eth_block( + header, + self.chain_spec(), + self.chain_spec().chain().id(), + self.chain_spec() + .blob_params_at_timestamp(header.timestamp()), + ); + + let spec = self.chain_spec().morph_hardfork_at(header.timestamp()); + + Ok(EvmEnv { + cfg_env: cfg_env.with_spec(spec), + block_env: MorphBlockEnv { inner: block_env }, + }) + } + + fn next_evm_env( + &self, + parent: &MorphHeader, + attributes: &Self::NextBlockEnvCtx, + ) -> Result, Self::Error> { + let EvmEnv { cfg_env, block_env } = EvmEnv::for_eth_next_block( + parent, + NextEvmEnvAttributes { + timestamp: attributes.timestamp, + suggested_fee_recipient: attributes.suggested_fee_recipient, + prev_randao: attributes.prev_randao, + gas_limit: attributes.gas_limit, + }, + self.chain_spec() + .next_block_base_fee(parent, attributes.timestamp) + .unwrap_or_default(), + self.chain_spec(), + self.chain_spec().chain().id(), + self.chain_spec() + .blob_params_at_timestamp(attributes.timestamp), + ); + + let spec = self.chain_spec().morph_hardfork_at(attributes.timestamp); + + Ok(EvmEnv { + cfg_env: cfg_env.with_spec(spec), + block_env: MorphBlockEnv { inner: block_env }, + }) + } + + fn context_for_block<'a>( + &self, + block: &'a SealedBlock, + ) -> Result, Self::Error> { + Ok(MorphBlockExecutionCtx { + inner: EthBlockExecutionCtx { + parent_hash: block.header().parent_hash(), + parent_beacon_block_root: block.header().parent_beacon_block_root(), + ommers: &[], + withdrawals: block.body().withdrawals.as_ref().map(Cow::Borrowed), + extra_data: block.extra_data().clone(), + }, + }) + } + + fn context_for_next_block( + &self, + parent: &SealedHeader, + attributes: Self::NextBlockEnvCtx, + ) -> Result, Self::Error> { + Ok(MorphBlockExecutionCtx { + inner: EthBlockExecutionCtx { + parent_hash: parent.hash(), + parent_beacon_block_root: attributes.parent_beacon_block_root, + ommers: &[], + withdrawals: attributes.inner.withdrawals.map(Cow::Owned), + extra_data: attributes.inner.extra_data, + }, + }) + } +} diff --git a/crates/evm/src/lib.rs b/crates/evm/src/lib.rs index e23a693..bb1a9f2 100644 --- a/crates/evm/src/lib.rs +++ b/crates/evm/src/lib.rs @@ -4,9 +4,10 @@ #![cfg_attr(docsrs, feature(doc_cfg))] mod assemble; +#[cfg(feature = "reth-codec")] +mod config; #[cfg(feature = "engine")] mod engine; -use alloy_consensus::BlockHeader as _; pub use assemble::MorphBlockAssembler; mod block; mod context; @@ -15,22 +16,17 @@ pub use context::{MorphBlockExecutionCtx, MorphNextBlockEnvAttributes}; mod error; pub use error::MorphEvmError; pub mod evm; -use std::{borrow::Cow, sync::Arc}; +use std::sync::Arc; +use crate::{block::MorphBlockExecutor, evm::MorphEvm}; use alloy_evm::{ - self, Database, EvmEnv, + self, Database, block::{BlockExecutorFactory, BlockExecutorFor}, - eth::{EthBlockExecutionCtx, NextEvmEnvAttributes}, revm::{Inspector, database::State}, }; pub use evm::MorphEvmFactory; -use morph_primitives::{Block, MorphHeader, MorphPrimitives, MorphReceipt, MorphTxEnvelope}; -use reth_chainspec::EthChainSpec; -use reth_evm::{self, ConfigureEvm, EvmEnvFor}; -use reth_primitives_traits::{SealedBlock, SealedHeader}; - -use crate::{block::MorphBlockExecutor, evm::MorphEvm}; -use morph_chainspec::{MorphChainSpec, hardfork::MorphHardforks}; +use morph_chainspec::MorphChainSpec; +use morph_primitives::{MorphReceipt, MorphTxEnvelope}; use morph_revm::evm::MorphContext; use reth_evm_ethereum::EthEvmConfig; @@ -95,100 +91,7 @@ impl BlockExecutorFactory for MorphEvmConfig { } } -impl ConfigureEvm for MorphEvmConfig { - type Primitives = MorphPrimitives; - type Error = MorphEvmError; - type NextBlockEnvCtx = MorphNextBlockEnvAttributes; - type BlockExecutorFactory = Self; - type BlockAssembler = MorphBlockAssembler; - - fn block_executor_factory(&self) -> &Self::BlockExecutorFactory { - self - } - - fn block_assembler(&self) -> &Self::BlockAssembler { - &self.block_assembler - } - - fn evm_env(&self, header: &MorphHeader) -> Result, Self::Error> { - let EvmEnv { cfg_env, block_env } = EvmEnv::for_eth_block( - header, - self.chain_spec(), - self.chain_spec().chain().id(), - self.chain_spec() - .blob_params_at_timestamp(header.timestamp()), - ); - - let spec = self.chain_spec().morph_hardfork_at(header.timestamp()); - - Ok(EvmEnv { - cfg_env: cfg_env.with_spec(spec), - block_env: MorphBlockEnv { inner: block_env }, - }) - } - - fn next_evm_env( - &self, - parent: &MorphHeader, - attributes: &Self::NextBlockEnvCtx, - ) -> Result, Self::Error> { - let EvmEnv { cfg_env, block_env } = EvmEnv::for_eth_next_block( - parent, - NextEvmEnvAttributes { - timestamp: attributes.timestamp, - suggested_fee_recipient: attributes.suggested_fee_recipient, - prev_randao: attributes.prev_randao, - gas_limit: attributes.gas_limit, - }, - self.chain_spec() - .next_block_base_fee(parent, attributes.timestamp) - .unwrap_or_default(), - self.chain_spec(), - self.chain_spec().chain().id(), - self.chain_spec() - .blob_params_at_timestamp(attributes.timestamp), - ); - - let spec = self.chain_spec().morph_hardfork_at(attributes.timestamp); - - Ok(EvmEnv { - cfg_env: cfg_env.with_spec(spec), - block_env: MorphBlockEnv { inner: block_env }, - }) - } - - fn context_for_block<'a>( - &self, - block: &'a SealedBlock, - ) -> Result, Self::Error> { - Ok(MorphBlockExecutionCtx { - inner: EthBlockExecutionCtx { - parent_hash: block.header().parent_hash(), - parent_beacon_block_root: block.header().parent_beacon_block_root(), - ommers: &[], - withdrawals: block.body().withdrawals.as_ref().map(Cow::Borrowed), - extra_data: block.extra_data().clone(), - }, - }) - } - - fn context_for_next_block( - &self, - parent: &SealedHeader, - attributes: Self::NextBlockEnvCtx, - ) -> Result, Self::Error> { - Ok(MorphBlockExecutionCtx { - inner: EthBlockExecutionCtx { - parent_hash: parent.hash(), - parent_beacon_block_root: attributes.parent_beacon_block_root, - ommers: &[], - withdrawals: attributes.inner.withdrawals.map(Cow::Owned), - extra_data: attributes.inner.extra_data, - }, - }) - } -} - +#[cfg(feature = "reth-codec")] #[cfg(test)] mod tests { use super::*; diff --git a/crates/payload/types/Cargo.toml b/crates/payload/types/Cargo.toml index 846e475..502a70f 100644 --- a/crates/payload/types/Cargo.toml +++ b/crates/payload/types/Cargo.toml @@ -13,7 +13,7 @@ workspace = true [dependencies] # Morph -morph-primitives = { workspace = true, features = ["serde"] } +morph-primitives = { workspace = true, features = ["serde", "reth-codec"] } # Reth reth-payload-primitives.workspace = true diff --git a/crates/primitives/Cargo.toml b/crates/primitives/Cargo.toml index 4b36796..330070e 100644 --- a/crates/primitives/Cargo.toml +++ b/crates/primitives/Cargo.toml @@ -32,7 +32,7 @@ modular-bitfield = { version = "0.11.2", optional = true } [dev-dependencies] [features] -default = ["serde", "reth-codec"] +default = ["serde", "reth"] serde = [ "dep:serde", "dep:alloy-serde", diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index d38fc37..6a0329e 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -42,6 +42,7 @@ pub use transaction::{ #[non_exhaustive] pub struct MorphPrimitives; +#[cfg(feature = "reth-codec")] impl NodePrimitives for MorphPrimitives { type Block = Block; type BlockHeader = MorphHeader; diff --git a/crates/primitives/src/transaction/envelope.rs b/crates/primitives/src/transaction/envelope.rs index 2e367f0..b972a78 100644 --- a/crates/primitives/src/transaction/envelope.rs +++ b/crates/primitives/src/transaction/envelope.rs @@ -1,5 +1,5 @@ use alloy_consensus::{Signed, TransactionEnvelope, TxEip1559, TxEip2930, TxEip7702, TxLegacy}; -use alloy_primitives::{B256, Bytes}; +use alloy_primitives::{B256, Bytes, U256}; use alloy_rlp::BytesMut; use crate::{TxAltFee, TxL1Msg}; @@ -70,6 +70,22 @@ impl MorphTxEnvelope { } Bytes(bytes.freeze()) } + + /// Returns the fee token id if this is an AltFee transaction. + pub fn fee_token_id(&self) -> Option { + match self { + Self::AltFee(tx) => Some(tx.tx().fee_token_id), + _ => None, + } + } + + /// Returns the fee limit if this is an AltFee transaction. + pub fn fee_limit(&self) -> Option { + match self { + Self::AltFee(tx) => Some(tx.tx().fee_limit), + _ => None, + } + } } impl reth_primitives_traits::InMemorySize for MorphTxEnvelope { diff --git a/crates/revm/src/error.rs b/crates/revm/src/error.rs index 41fd53c..0ce1d80 100644 --- a/crates/revm/src/error.rs +++ b/crates/revm/src/error.rs @@ -15,10 +15,20 @@ pub enum MorphInvalidTransaction { #[error("Token with ID {0} is not registered")] TokenNotRegistered(u16), + /// Token ID 0 not supported for gas payment. + #[error("Token ID 0 is not supported for gas payment")] + TokenIdZeroNotSupported, + /// Token is not active for gas payment. #[error("Token with ID {0} is not active for gas payment")] TokenNotActive(u16), + #[error("Token transfer failed: {reason}")] + TokenTransferFailed { + /// Token transfer failure reason. + reason: String, + }, + /// Insufficient token balance for gas payment. #[error( "Insufficient token balance for gas payment: required {required}, available {available}" diff --git a/crates/revm/src/handler.rs b/crates/revm/src/handler.rs index 17b9665..5c25a37 100644 --- a/crates/revm/src/handler.rs +++ b/crates/revm/src/handler.rs @@ -1,21 +1,21 @@ //! Morph EVM Handler implementation. -use std::fmt::Debug; - -use alloy_primitives::U256; +use alloy_primitives::{Address, Bytes, U256}; +use morph_primitives::L1_TX_TYPE_ID; use revm::{ + ExecuteEvm, context::{ Cfg, ContextTr, JournalTr, Transaction, result::{EVMError, ExecutionResult, InvalidTransaction}, }, context_interface::Block, - handler::{EvmTr, FrameTr, Handler, MainnetHandler, pre_execution, validation}, + handler::{EvmTr, FrameTr, Handler, MainnetHandler, post_execution, pre_execution, validation}, inspector::{Inspector, InspectorHandler}, - interpreter::{InitialAndFloorGas, interpreter::EthInterpreter}, + interpreter::{Gas, InitialAndFloorGas, interpreter::EthInterpreter}, }; use crate::{ - MorphEvm, MorphInvalidTransaction, + MorphEvm, MorphInvalidTransaction, MorphTxEnv, error::MorphHaltReason, evm::MorphContext, l1block::L1BlockInfo, @@ -115,16 +115,22 @@ where fn reimburse_caller( &self, evm: &mut Self::Evm, - _exec_result: &mut <::Frame as FrameTr>::FrameResult, + 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() { return Ok(()); } - // For Morph L2, we don't reimburse caller - // The L2 execution fee is handled by the sequencer - // L1 data fee is a fixed cost that is not refunded + // Check if transaction is AltFeeTx (tx_type 0x7F) which uses token fee + if evm.ctx_ref().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(); + return self.reimburse_caller_token_fee(evm, exec_result.gas(), token_id); + } + + // Standard ETH-based fee handling + post_execution::reimburse_caller(evm.ctx(), exec_result.gas(), U256::ZERO)?; Ok(()) } @@ -167,12 +173,10 @@ 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 context components + // Get mutable access to journal components let journal = evm.ctx().journal_mut(); - let gas_spent = exec_result.gas().spent(); - let gas_refunded = exec_result.gas().refunded() as u64; - let gas_used = gas_spent - gas_refunded; + let gas_used = exec_result.gas().used(); let execution_fee = U256::from(effective_gas_price).saturating_mul(U256::from(gas_used)); @@ -298,20 +302,88 @@ where /// Validate and deduct token-based gas fees. /// /// This handles gas payment using ERC20 tokens instead of ETH. - fn validate_and_deduct_token_fee( + fn reimburse_caller_token_fee( &self, evm: &mut MorphEvm, + gas: &Gas, token_id: u16, ) -> Result<(), EVMError> { // Get caller address - let caller_addr = evm.ctx_ref().tx().caller(); + let caller = evm.ctx_ref().tx().caller(); // Get coinbase address let beneficiary = evm.ctx_ref().block().beneficiary(); + let basefee = evm.ctx.block().basefee() as u128; + let effective_gas_price = evm.ctx.tx().effective_gas_price(basefee); + + let reimburse_eth = U256::from( + effective_gas_price.saturating_mul((gas.remaining() + gas.refunded() as u64) as u128), + ); + + if reimburse_eth.is_zero() { + return Ok(()); + } // Fetch token fee info from Token Registry - let token_fee_info = - TokenFeeInfo::try_fetch(evm.ctx_mut().db_mut(), token_id, caller_addr)? - .ok_or(MorphInvalidTransaction::TokenNotRegistered(token_id))?; + let token_fee_info = TokenFeeInfo::try_fetch(evm.ctx_mut().db_mut(), token_id, caller)? + .ok_or(MorphInvalidTransaction::TokenNotRegistered(token_id))?; + + // Check if token is active + if !token_fee_info.is_active { + return Err(MorphInvalidTransaction::TokenNotActive(token_id).into()); + } + + // Calculate token amount required for total fee + let token_amount_required = token_fee_info.calculate_token_amount(reimburse_eth); + + // Get mutable access to journal components + let journal = evm.ctx().journal_mut(); + + if let Some(balance_slot) = token_fee_info.balance_slot { + // Transfer with token slot. + let _ = transfer_erc20_with_slot( + journal, + beneficiary, + caller, + token_fee_info.token_address, + token_amount_required, + balance_slot, + )?; + } else { + // Transfer with evm call. + transfer_erc20_with_evm( + evm, + beneficiary, + token_fee_info.caller, + token_fee_info.token_address, + token_amount_required, + )?; + } + Ok(()) + } + + /// Validate and deduct token-based gas fees. + /// + /// This handles gas payment using ERC20 tokens instead of ETH. + fn validate_and_deduct_token_fee( + &self, + evm: &mut MorphEvm, + token_id: u16, + ) -> Result<(), EVMError> { + // Token ID 0 not supported for gas payment. + if token_id == 0 { + return Err(MorphInvalidTransaction::TokenIdZeroNotSupported.into()); + } + + let (block, tx, cfg, journal, _, _) = evm.ctx_mut().all_mut(); + + // Get caller address + let caller_addr = tx.caller(); + // Get coinbase address + let beneficiary = block.beneficiary(); + + // Fetch token fee info from Token Registry + let token_fee_info = TokenFeeInfo::try_fetch(journal.db_mut(), token_id, caller_addr)? + .ok_or(MorphInvalidTransaction::TokenNotRegistered(token_id))?; // Check if token is active if !token_fee_info.is_active { @@ -319,16 +391,14 @@ where } // 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()) @@ -338,8 +408,8 @@ where let l1_data_fee = l1_block_info.calculate_tx_l1_cost(rlp_bytes, hardfork); // Calculate L2 gas fee (in ETH) - let gas_limit = evm.ctx_ref().tx().gas_limit(); - let gas_price = evm.ctx_ref().tx().gas_price(); + let gas_limit = tx.gas_limit(); + let gas_price = tx.gas_price(); let l2_gas_fee = U256::from(gas_limit).saturating_mul(U256::from(gas_price)); // Total fee in ETH @@ -348,55 +418,80 @@ where // Calculate token amount required for total fee let token_amount_required = token_fee_info.calculate_token_amount(total_eth_fee); + // Determine fee limit + let mut fee_limit = tx.fee_limit.unwrap_or_default(); + if fee_limit.is_zero() || fee_limit > token_fee_info.balance { + fee_limit = token_fee_info.balance + } + // Check if caller has sufficient token balance - if token_fee_info.balance < token_amount_required { + if fee_limit < token_amount_required { return Err(MorphInvalidTransaction::InsufficientTokenBalance { required: token_amount_required, - available: token_fee_info.balance, + available: fee_limit, } .into()); } - // Get mutable access to context components - let (_, tx, cfg, journal, _, _) = evm.ctx().all_mut(); - - // First, deduct token fee from caller's ERC20 balance - // This updates the ERC20 token's storage directly if let Some(balance_slot) = token_fee_info.balance_slot { - // Sub amount - let token_storage_slot = get_mapping_account_slot(balance_slot, caller_addr); - let new_token_balance = token_fee_info.balance.saturating_sub(token_amount_required); - journal.sstore( + // Transfer with token slot. + // Ensure token account is loaded into the journal state, because `sload`/`sstore` + // assume the account is present. + let _ = journal.load_account_mut(token_fee_info.token_address)?; + journal.touch(token_fee_info.token_address); + let (from_storage_slot, to_storage_slot) = transfer_erc20_with_slot( + journal, + caller_addr, + beneficiary, token_fee_info.token_address, - token_storage_slot, - new_token_balance, + token_amount_required, + balance_slot, )?; - - // Add amount - let token_storage_slot = get_mapping_account_slot(balance_slot, beneficiary); - let balance = journal - .sload(token_fee_info.token_address, token_storage_slot) - .unwrap_or_default(); - journal.sstore( + // We don't want the fee-token account/slots we touched during validation to become + // warm for the rest of the transaction execution. + if let Some(token_acc) = journal.state.get_mut(&token_fee_info.token_address) { + token_acc.mark_cold(); + if let Some(slot) = token_acc.storage.get_mut(&from_storage_slot) { + slot.mark_cold(); + } + if let Some(slot) = token_acc.storage.get_mut(&to_storage_slot) { + slot.mark_cold(); + } + } + } else { + // Transfer with evm call. + let tx_origin = evm.tx.clone(); + transfer_erc20_with_evm( + evm, + token_fee_info.caller, beneficiary, - token_storage_slot, - balance.saturating_sub(token_amount_required), + token_fee_info.token_address, + token_amount_required, )?; + // restore the original transaction + evm.tx = tx_origin; } + let (_, tx, cfg, journal, _, _) = evm.ctx().all_mut(); + + // Extract the required tx fields (Copy) before mutating accounts. + let caller_addr = tx.caller(); + let nonce = tx.nonce(); + let is_call = tx.kind().is_call(); + // Load caller's account for nonce/code validation - let mut caller = journal.load_account_with_code_mut(tx.caller())?.data; + let mut caller = journal.load_account_with_code_mut(caller_addr)?.data; // Validate account nonce and code (EIP-3607) pre_execution::validate_account_nonce_and_code( &caller.info, - tx.nonce(), + nonce, cfg.is_eip3607_disabled(), cfg.is_nonce_check_disabled(), )?; // Bump nonce for calls (CREATE nonce is bumped in make_create_frame) - if tx.kind().is_call() { + if is_call { caller.bump_nonce(); } @@ -404,6 +499,99 @@ where } } +/// Performs an ERC20 balance transfer by directly `sload`/`sstore`-ing the token contract storage +/// using the known `balance` mapping base slot, returning the computed storage slots for `from`/`to`. +fn transfer_erc20_with_slot( + journal: &mut revm::Journal, + from: Address, + to: Address, + token: Address, + token_amount: U256, + token_balance_slot: U256, +) -> Result<(U256, U256), EVMError<::Error, MorphInvalidTransaction>> +where + DB: alloy_evm::Database, +{ + // Sub amount + let from_storage_slot = get_mapping_account_slot(token_balance_slot, from); + let balance = journal.sload(token, from_storage_slot)?; + journal.sstore( + token, + from_storage_slot, + balance.saturating_sub(token_amount), + )?; + + // Add amount + let to_storage_slot = get_mapping_account_slot(token_balance_slot, to); + let balance = journal.sload(token, to_storage_slot)?; + journal.sstore(token, to_storage_slot, balance.saturating_add(token_amount))?; + 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, + caller: Address, + to: Address, + token_address: Address, + token_amount: U256, +) -> Result<(), EVMError> +where + DB: alloy_evm::Database, +{ + 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() + }; + + let tx = MorphTxEnv { + inner: tx_env, + rlp_bytes: None, + ..Default::default() + }; + match evm.transact_one(tx) { + Ok(result) => { + if !result.is_success() { + return Err(MorphInvalidTransaction::TokenTransferFailed { + reason: format!("{result:?}"), + } + .into()); + } + } + Err(e) => { + return Err(MorphInvalidTransaction::TokenTransferFailed { + reason: format!("Error: {e:?}"), + } + .into()); + } + }; + Ok(()) +} + +/// Build the calldata for ERC20 transfer(address,amount) call. +/// +/// Method signature: `transfer(address,amount) -> 0xa9059cbb` +fn build_transfer_calldata(to: Address, token_amount: alloy_primitives::Uint<256, 4>) -> Bytes { + let method_id = [0xa9u8, 0x05, 0x9c, 0xbb]; + // Encode calldata: method_id + padded to address + amount + let mut calldata = Vec::with_capacity(68); + calldata.extend_from_slice(&method_id); + let mut address_bytes = [0u8; 32]; + address_bytes[12..32].copy_from_slice(to.as_slice()); + calldata.extend_from_slice(&address_bytes); + calldata.extend_from_slice(&token_amount.to_be_bytes::<32>()); + Bytes::from(calldata) +} + /// Calculate the new balance after deducting L2 fees and L1 data fee. /// /// This is a Morph-specific version of `pre_execution::calculate_caller_fee` that diff --git a/crates/revm/src/token_fee.rs b/crates/revm/src/token_fee.rs index 94f1ae5..6fd71c4 100644 --- a/crates/revm/src/token_fee.rs +++ b/crates/revm/src/token_fee.rs @@ -7,12 +7,11 @@ use alloy_evm::Database; use alloy_primitives::{Address, Bytes, TxKind, U256, address, keccak256}; +use morph_chainspec::hardfork::MorphHardfork; use morph_primitives::L1_TX_TYPE_ID; use revm::{ - ExecuteEvm, Inspector, - context::TxEnv, - context_interface::{ContextTr, result::EVMError}, - handler::EvmTr, + ExecuteEvm, Inspector, context::TxEnv, context_interface::result::EVMError, + inspector::NoOpInspector, }; use crate::evm::MorphContext; @@ -142,9 +141,15 @@ impl TokenFeeInfo { } // token_amount = eth_amount * scale / price_ratio - eth_amount + let (token_amount, remainder) = eth_amount .saturating_mul(self.scale) - .wrapping_div(self.price_ratio) + .div_rem(self.price_ratio); + // If there's a remainder, round up by adding 1 + if !remainder.is_zero() { + token_amount.saturating_add(U256::from(1)) + } else { + token_amount + } } /// Check if the caller has sufficient token balance for the given ETH amount. @@ -188,7 +193,7 @@ fn load_mapping_value( } /// Gas limit for ERC20 balance query calls. -const BALANCE_OF_GAS_LIMIT: u64 = 1_000_000; +const BALANCE_OF_GAS_LIMIT: u64 = 200000; /// Get ERC20 token balance for an account (storage-only version). /// @@ -207,14 +212,28 @@ pub fn get_erc20_balance( if let Some(slot) = token_balance_slot { let mut data = [0u8; 32]; data[12..32].copy_from_slice(account.as_slice()); - if let Ok(balance) = load_mapping_value(db, token, slot, data.to_vec()) { - return Ok(balance); + load_mapping_value(db, token, slot, data.to_vec()) + } else { + // For the EVM fallback we construct a temporary MorphEvm instance. + // + // Notes: + // - `MorphContext::new` requires a hardfork/spec parameter. + // - We pass `&mut DB` as the context database type (so we don't move `db`). + // - `NoOpInspector` satisfies the `Inspector` bound without adding side effects. + let db: &mut dyn Database = db; + + let mut evm = MorphEvm::new( + MorphContext::new(db, MorphHardfork::Curie), + NoOpInspector {}, + ); + + match get_erc20_balance_with_evm(&mut evm, token, account) { + Ok(balance) => Ok(balance), + Err(EVMError::Database(db_err)) => Err(db_err), + // For non-database EVM errors, fall back to zero (matches original behavior). + Err(_) => Ok(U256::ZERO), } } - - // If balance slot is not available, return zero. - // Use get_erc20_balance_with_evm for EVM call fallback. - Ok(U256::ZERO) } /// Get ERC20 token balance for an account with EVM call fallback. @@ -226,22 +245,11 @@ pub fn get_erc20_balance_with_evm( evm: &mut MorphEvm, token: Address, account: Address, - token_balance_slot: Option, ) -> Result> where DB: Database, I: Inspector>, { - // First try storage-based lookup - if let Some(slot) = token_balance_slot { - let mut data = [0u8; 32]; - data[12..32].copy_from_slice(account.as_slice()); - let storage_slot = get_mapping_slot(slot, data.to_vec()); - if let Ok(balance) = evm.ctx_mut().db_mut().storage(token, storage_slot) { - return Ok(balance); - } - } - // Fallback: Execute EVM call to balanceOf(address) let calldata = build_balance_of_calldata(account); diff --git a/crates/revm/src/tx.rs b/crates/revm/src/tx.rs index ad72b11..cc5d235 100644 --- a/crates/revm/src/tx.rs +++ b/crates/revm/src/tx.rs @@ -5,6 +5,7 @@ use alloy_consensus::{EthereumTxEnvelope, Transaction as AlloyTransaction, TxEip4844}; use alloy_eips::eip2718::Encodable2718; use alloy_eips::eip2930::AccessList; +use alloy_eips::eip7702::RecoveredAuthority; use alloy_primitives::{Address, B256, Bytes, TxKind, U256}; use alloy_rlp::Decodable; use morph_primitives::{ALT_FEE_TX_TYPE_ID, L1_TX_TYPE_ID, MorphTxEnvelope, TxAltFee}; @@ -86,10 +87,13 @@ impl MorphTxEnv { let tx_type: u8 = tx.tx_type().into(); // Extract fee_token_id for AltFeeTx (type 0x7F) - let fee_token_id = if tx_type == ALT_FEE_TX_TYPE_ID { - extract_fee_token_id_from_rlp(&rlp_bytes) + 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), + ) } else { - 0 + (0, U256::default()) }; // Build TxEnv from the transaction @@ -110,13 +114,27 @@ impl MorphTxEnv { .map(|h| h.to_vec()) .unwrap_or_default(), max_fee_per_blob_gas: AlloyTransaction::max_fee_per_blob_gas(tx).unwrap_or(0), - authorization_list: Default::default(), + authorization_list: tx + .authorization_list() + .unwrap_or_default() + .iter() + .map(|auth| { + let authority = auth + .recover_authority() + .map_or(RecoveredAuthority::Invalid, RecoveredAuthority::Valid); + Either::Right(RecoveredAuthorization::new_unchecked( + auth.inner().clone(), + authority, + )) + }) + .collect(), }; // Use builder pattern to set Morph-specific fields Self::new(inner) .with_rlp_bytes(rlp_bytes) - .with_fee_token_id(fee_token_id) + .with_fee_token_id(fee_token_info.0) + .with_fee_limit(fee_token_info.1) } } @@ -136,6 +154,22 @@ fn extract_fee_token_id_from_rlp(rlp_bytes: &Bytes) -> u16 { .unwrap_or(0) } +/// Extract fee_limit from RLP-encoded AltFeeTx bytes. +/// +/// The bytes should be EIP-2718 encoded (type byte + RLP payload). +/// Returns 0 if decoding fails. +fn extract_fee_limit_from_rlp(rlp_bytes: &Bytes) -> U256 { + if rlp_bytes.is_empty() { + return U256::default(); + } + + // Skip the type byte (0x7F) and decode the AltFeeTx + let payload = &rlp_bytes[1..]; + TxAltFee::decode(&mut &payload[..]) + .map(|tx| tx.fee_limit) + .unwrap_or_default() +} + impl Deref for MorphTxEnv { type Target = TxEnv;