diff --git a/crates/revm/src/evm.rs b/crates/revm/src/evm.rs index f0015384dd..e57d8ff136 100644 --- a/crates/revm/src/evm.rs +++ b/crates/revm/src/evm.rs @@ -1,6 +1,6 @@ use crate::{TempoBlockEnv, TempoTxEnv, instructions}; use alloy_evm::{Database, precompiles::PrecompilesMap}; -use alloy_primitives::{Log, U256}; +use alloy_primitives::{Address, Log, U256}; use revm::{ Context, Inspector, context::{CfgEnv, ContextError, Evm, FrameStack}, @@ -37,6 +37,8 @@ pub struct TempoEvm { /// /// Additional initial gas cost is added for authorization_key setting in pre execution. pub(crate) initial_gas: u64, + /// The fee token used to pay fees for the current transaction. + pub(crate) fee_token: Option
, } impl TempoEvm { @@ -70,6 +72,7 @@ impl TempoEvm { logs: Vec::new(), collected_fee: U256::ZERO, initial_gas: 0, + fee_token: None, } } } diff --git a/crates/revm/src/handler.rs b/crates/revm/src/handler.rs index 2556ffdc41..f271e84729 100644 --- a/crates/revm/src/handler.rs +++ b/crates/revm/src/handler.rs @@ -271,10 +271,6 @@ fn adjusted_initial_gas( /// Fees are paid in fee tokens instead of account balance. #[derive(Debug)] pub struct TempoEvmHandler { - /// Fee token used for the transaction. - fee_token: Address, - /// Fee payer for the transaction. - fee_payer: Address, /// Phantom data to avoid type inference issues. _phantom: core::marker::PhantomData<(DB, I)>, } @@ -283,8 +279,6 @@ impl TempoEvmHandler { /// Create a new [`TempoEvmHandler`] handler instance pub fn new() -> Self { Self { - fee_token: Address::default(), - fee_payer: Address::default(), _phantom: core::marker::PhantomData, } } @@ -311,56 +305,6 @@ impl TempoEvmHandler { ) .map_err(|e| EVMError::Custom(e.to_string())) } - - /// Loads the fee token and fee payer from the transaction environment. - /// - /// Resolves and validates the fee fields used by Tempo's fee system: - /// - Fee payer: determined from the transaction - /// - Fee token: resolved via the journaled state and validated (TIP20 prefix + USD currency) - /// - /// Must be called before `validate_against_state_and_deduct_caller`, which uses the - /// loaded fee fields for balance checks. - /// - /// Exposed for consumers like `FoundryHandler` that override the default run flow - /// but still need Tempo fee setup. - pub fn load_fee_fields( - &mut self, - evm: &mut TempoEvm, - ) -> Result<(), EVMError> { - let ctx = evm.ctx_mut(); - - self.fee_payer = ctx.tx.fee_payer()?; - if ctx.cfg.spec.is_t2() - && ctx.tx.has_fee_payer_signature() - && self.fee_payer == ctx.tx.caller() - { - return Err(TempoInvalidTransaction::SelfSponsoredFeePayer.into()); - } - - self.fee_token = ctx - .journaled_state - .get_fee_token(&ctx.tx, self.fee_payer, ctx.cfg.spec) - .map_err(|err| EVMError::Custom(err.to_string()))?; - - // Always validate TIP20 prefix to prevent panics in get_token_balance. - // This is a protocol-level check since validators could bypass initial validation. - if !is_tip20_prefix(self.fee_token) { - return Err(TempoInvalidTransaction::InvalidFeeToken(self.fee_token).into()); - } - - // Skip USD currency check for cases when the transaction is free and is not a part of a subblock. - // Since we already validated the TIP20 prefix above, we only need to check the USD currency. - if (!ctx.tx.max_balance_spending()?.is_zero() || ctx.tx.is_subblock_transaction()) - && !ctx - .journaled_state - .is_tip20_usd(ctx.cfg.spec, self.fee_token) - .map_err(|err| EVMError::Custom(err.to_string()))? - { - return Err(TempoInvalidTransaction::InvalidFeeToken(self.fee_token).into()); - } - - Ok(()) - } } impl TempoEvmHandler @@ -661,20 +605,6 @@ where type Error = EVMError; type HaltReason = TempoHaltReason; - #[inline] - fn run( - &mut self, - evm: &mut Self::Evm, - ) -> Result, Self::Error> { - self.load_fee_fields(evm)?; - - // Standard handler flow - execution() handles single vs multi-call dispatch - match self.run_without_catch_error(evm) { - Ok(output) => Ok(output), - Err(err) => self.catch_error(evm, err), - } - } - /// Overridden execution method that handles AA vs standard transactions. /// /// Dispatches based on transaction type: @@ -713,6 +643,8 @@ where evm.logs.clear(); // reset initial gas to 0 to avoid gas limit check errors evm.initial_gas = 0; + evm.fee_token = None; + if !result.instruction_result().is_ok() { evm.logs = evm.journal_mut().take_logs(); } @@ -781,17 +713,31 @@ where let cfg = &evm.inner.ctx.cfg; let journal = &mut evm.inner.ctx.journaled_state; - // Validate fee token has TIP20 prefix before loading balance. - // This prevents panics in get_token_balance for invalid fee tokens. - // Note: Full fee token validation (currency check) happens in load_fee_fields, - // but is skipped for free non-subblock transactions. This prefix check ensures - // we don't panic even for those cases. - if !is_tip20_prefix(self.fee_token) { - return Err(TempoInvalidTransaction::InvalidFeeToken(self.fee_token).into()); + let fee_payer = tx.fee_payer().expect("pre-validated in `validate_env`"); + let fee_token = journal + .get_fee_token(tx, fee_payer, cfg.spec) + .map_err(|err| EVMError::Custom(err.to_string()))?; + + evm.fee_token = Some(fee_token); + + // Always validate TIP20 prefix to prevent panics in get_token_balance. + // This is a protocol-level check since validators could bypass initial validation. + if !is_tip20_prefix(fee_token) { + return Err(TempoInvalidTransaction::InvalidFeeToken(fee_token).into()); + } + + // Skip USD currency check for cases when the transaction is free and is not a part of a subblock. + // Since we already validated the TIP20 prefix above, we only need to check the USD currency. + if (!tx.max_balance_spending()?.is_zero() || tx.is_subblock_transaction()) + && !journal + .is_tip20_usd(cfg.spec, fee_token) + .map_err(|err| EVMError::Custom(err.to_string()))? + { + return Err(TempoInvalidTransaction::InvalidFeeToken(fee_token).into()); } // Load the fee payer balance - let account_balance = get_token_balance(journal, self.fee_token, self.fee_payer)?; + let account_balance = get_token_balance(journal, fee_token, fee_payer)?; // Load caller's account let mut caller_account = journal.load_account_with_code_mut(tx.caller())?.data; @@ -1289,8 +1235,8 @@ where let result = StorageCtx::enter_evm(journal, &block, cfg, tx, || { TipFeeManager::new().collect_fee_pre_tx( - self.fee_payer, - self.fee_token, + fee_payer, + fee_token, gas_balance_spending, block.beneficiary(), ) @@ -1370,13 +1316,17 @@ where let mut fee_manager = TipFeeManager::new(); if !actual_spending.is_zero() || !refund_amount.is_zero() { + let fee_payer = tx.fee_payer().expect("pre-validated in `validate_env`"); + let fee_token = evm + .fee_token + .expect("set in `validate_against_state_and_deduct_caller`"); // Call collectFeePostTx (handles both refund and fee queuing) fee_manager .collect_fee_post_tx( - self.fee_payer, + fee_payer, actual_spending, refund_amount, - self.fee_token, + fee_token, beneficiary, ) .map_err(|e| EVMError::Custom(format!("{e:?}")))?; @@ -1404,6 +1354,16 @@ where /// - Time window validation (validAfter/validBefore) #[inline] fn validate_env(&self, evm: &mut Self::Evm) -> Result<(), Self::Error> { + // Validate the fee payer signature + let fee_payer = evm.ctx.tx.fee_payer()?; + + if evm.ctx.cfg.spec.is_t2() + && evm.ctx.tx.has_fee_payer_signature() + && fee_payer == evm.ctx.tx.caller() + { + return Err(TempoInvalidTransaction::SelfSponsoredFeePayer.into()); + } + // All accounts have zero balance so transfer of value is not possible. // Check added in https://github.com/tempoxyz/tempo/pull/759 if !evm.ctx.tx.value().is_zero() { @@ -1560,6 +1520,7 @@ where ) -> Result, Self::Error> { // reset initial gas to 0 to avoid gas limit check errors evm.initial_gas = 0; + evm.fee_token = None; // For subblock transactions that failed `collectFeePreTx` call we catch error and treat such transactions as valid. if evm.ctx.tx.is_subblock_transaction() @@ -1830,18 +1791,6 @@ where { type IT = EthInterpreter; - fn inspect_run( - &mut self, - evm: &mut Self::Evm, - ) -> Result, Self::Error> { - self.load_fee_fields(evm)?; - - match self.inspect_run_without_catch_error(evm) { - Ok(output) => Ok(output), - Err(e) => self.catch_error(evm, e), - } - } - /// Overridden execution method with inspector support that handles AA vs standard transactions. /// /// Delegates to [`inspect_execution_with`](TempoEvmHandler::inspect_execution_with) with @@ -1954,15 +1903,15 @@ mod tests { #[test] fn test_invalid_fee_token_rejected() { // Test that an invalid fee token (non-TIP20 address) is rejected with InvalidFeeToken error - // rather than panicking. This validates the check in load_fee_fields that guards against - // invalid tokens reaching get_token_balance. + // rather than panicking. This validates the check in validate_against_state_and_deduct_caller that + // guards against invalid tokens reaching get_token_balance. let invalid_token = Address::random(); // Random address won't have TIP20 prefix assert!( !is_tip20_prefix(invalid_token), "Test requires a non-TIP20 address" ); - let mut handler: TempoEvmHandler, ()> = TempoEvmHandler::default(); + let handler: TempoEvmHandler, ()> = TempoEvmHandler::default(); // Set up tx with the invalid token as fee_token let tx_env = TempoTxEnv { @@ -1979,7 +1928,7 @@ mod tests { (), ); - let result = handler.load_fee_fields(&mut evm); + let result = handler.validate_against_state_and_deduct_caller(&mut evm); assert!( matches!( @@ -1995,7 +1944,7 @@ mod tests { let caller = Address::random(); let invalid_token = Address::random(); - let mut handler: TempoEvmHandler, ()> = TempoEvmHandler::default(); + let handler: TempoEvmHandler, ()> = TempoEvmHandler::default(); let mut cfg = CfgEnv::::default(); cfg.spec = TempoHardfork::T2; @@ -2018,7 +1967,7 @@ mod tests { (), ); - let result = handler.load_fee_fields(&mut evm); + let result = handler.validate_env(&mut evm); assert!(matches!( result, Err(EVMError::Transaction( @@ -2032,7 +1981,7 @@ mod tests { let caller = Address::random(); let invalid_token = Address::random(); - let mut handler: TempoEvmHandler, ()> = TempoEvmHandler::default(); + let handler: TempoEvmHandler, ()> = TempoEvmHandler::default(); let mut cfg = CfgEnv::::default(); cfg.spec = TempoHardfork::T1C; @@ -2055,12 +2004,8 @@ mod tests { (), ); - let result = handler.load_fee_fields(&mut evm); - assert!(matches!( - result, - Err(EVMError::Transaction(TempoInvalidTransaction::InvalidFeeToken(addr))) - if addr == invalid_token - )); + let result = handler.validate_env(&mut evm); + assert!(result.is_ok()); } #[test]