Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion crates/revm/src/evm.rs
Original file line number Diff line number Diff line change
@@ -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},
Expand Down Expand Up @@ -37,6 +37,8 @@ pub struct TempoEvm<DB: Database, I> {
///
/// 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<Address>,
}

impl<DB: Database, I> TempoEvm<DB, I> {
Expand Down Expand Up @@ -70,6 +72,7 @@ impl<DB: Database, I> TempoEvm<DB, I> {
logs: Vec::new(),
collected_fee: U256::ZERO,
initial_gas: 0,
fee_token: None,
}
}
}
Expand Down
159 changes: 52 additions & 107 deletions crates/revm/src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,10 +271,6 @@ fn adjusted_initial_gas(
/// Fees are paid in fee tokens instead of account balance.
#[derive(Debug)]
pub struct TempoEvmHandler<DB, I> {
/// 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)>,
}
Expand All @@ -283,8 +279,6 @@ impl<DB, I> TempoEvmHandler<DB, I> {
/// Create a new [`TempoEvmHandler`] handler instance
pub fn new() -> Self {
Self {
fee_token: Address::default(),
fee_payer: Address::default(),
_phantom: core::marker::PhantomData,
}
}
Expand All @@ -311,56 +305,6 @@ impl<DB: alloy_evm::Database, I> TempoEvmHandler<DB, I> {
)
.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<DB, I>,
) -> Result<(), EVMError<DB::Error, TempoInvalidTransaction>> {
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<DB, I> TempoEvmHandler<DB, I>
Expand Down Expand Up @@ -661,20 +605,6 @@ where
type Error = EVMError<DB::Error, TempoInvalidTransaction>;
type HaltReason = TempoHaltReason;

#[inline]
fn run(
&mut self,
evm: &mut Self::Evm,
) -> Result<ExecutionResult<Self::HaltReason>, 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:
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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(),
)
Expand Down Expand Up @@ -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:?}")))?;
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -1560,6 +1520,7 @@ where
) -> Result<ExecutionResult<Self::HaltReason>, 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()
Expand Down Expand Up @@ -1830,18 +1791,6 @@ where
{
type IT = EthInterpreter;

fn inspect_run(
&mut self,
evm: &mut Self::Evm,
) -> Result<ExecutionResult<Self::HaltReason>, 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
Expand Down Expand Up @@ -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<CacheDB<EmptyDB>, ()> = TempoEvmHandler::default();
let handler: TempoEvmHandler<CacheDB<EmptyDB>, ()> = TempoEvmHandler::default();

// Set up tx with the invalid token as fee_token
let tx_env = TempoTxEnv {
Expand All @@ -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!(
Expand All @@ -1995,7 +1944,7 @@ mod tests {
let caller = Address::random();
let invalid_token = Address::random();

let mut handler: TempoEvmHandler<CacheDB<EmptyDB>, ()> = TempoEvmHandler::default();
let handler: TempoEvmHandler<CacheDB<EmptyDB>, ()> = TempoEvmHandler::default();
let mut cfg = CfgEnv::<TempoHardfork>::default();
cfg.spec = TempoHardfork::T2;

Expand All @@ -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(
Expand All @@ -2032,7 +1981,7 @@ mod tests {
let caller = Address::random();
let invalid_token = Address::random();

let mut handler: TempoEvmHandler<CacheDB<EmptyDB>, ()> = TempoEvmHandler::default();
let handler: TempoEvmHandler<CacheDB<EmptyDB>, ()> = TempoEvmHandler::default();
let mut cfg = CfgEnv::<TempoHardfork>::default();
cfg.spec = TempoHardfork::T1C;

Expand All @@ -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]
Expand Down
Loading