diff --git a/crates/anvil/src/config.rs b/crates/anvil/src/config.rs index 44e4a251bf478..a3795779a2ed7 100644 --- a/crates/anvil/src/config.rs +++ b/crates/anvil/src/config.rs @@ -70,6 +70,7 @@ use foundry_evm::{ utils::get_blob_params, }; use foundry_evm_networks::NetworkConfigs; +use tempo_precompiles::TIP_FEE_MANAGER_ADDRESS; /// Default port the rpc will open pub const NODE_PORT: u16 = 8545; @@ -523,6 +524,23 @@ impl Default for NodeConfig { } impl NodeConfig { + /// Applies Tempo's safe default beneficiary for forked nodes while preserving + /// explicit coinbase selections. + pub(crate) fn apply_tempo_fork_beneficiary_default(&self, evm_env: &mut EvmEnv) { + if self.networks.is_tempo() + && !self.fork_urls.is_empty() + && evm_env.block_env.beneficiary.is_zero() + { + // Tempo mainnet maps the zero validator token to a DONOTUSE sentinel. + // Forked transactions with the default zero beneficiary can therefore + // fail fee collection before producing a receipt. Use the same neutral + // fee-recipient sentinel as Tempo's simulation path so validator token + // lookup falls back to the default PathUSD token unless the user has + // explicitly supplied a non-zero coinbase. + evm_env.block_env.beneficiary = TIP_FEE_MANAGER_ADDRESS; + } + } + /// Returns the memory limit of the node #[must_use] pub const fn with_memory_limit(mut self, mems_value: Option) -> Self { @@ -1171,6 +1189,8 @@ impl NodeConfig { }, ); + self.apply_tempo_fork_beneficiary_default(&mut evm_env); + let base_fee_params: BaseFeeParams = self.networks.base_fee_params(self.get_genesis_timestamp()); @@ -1208,6 +1228,8 @@ impl NodeConfig { evm_env.block_env.beneficiary = genesis.coinbase; } + self.apply_tempo_fork_beneficiary_default(&mut evm_env); + let genesis = GenesisConfig { number: self.get_genesis_number(), timestamp: self.get_genesis_timestamp(), diff --git a/crates/anvil/src/eth/backend/mem/mod.rs b/crates/anvil/src/eth/backend/mem/mod.rs index 1dfc711bfb7fb..ebc2b8031abfb 100644 --- a/crates/anvil/src/eth/backend/mem/mod.rs +++ b/crates/anvil/src/eth/backend/mem/mod.rs @@ -183,7 +183,7 @@ use storage::{Blockchain, DEFAULT_HISTORY_LIMIT, MinedTransaction}; use tempo_chainspec::hardfork::TempoHardfork; use tempo_evm::evm::TempoEvmFactory; use tempo_precompiles::{ - extend_tempo_precompiles, + TIP_FEE_MANAGER_ADDRESS, extend_tempo_precompiles, storage::StorageCtx, tip_fee_manager::{IFeeManager, TipFeeManager}, tip20::{ISSUER_ROLE, ITIP20, TIP20Token}, @@ -2157,6 +2157,7 @@ impl Backend { // `setup_fork_db_config` node_config.base_fee.take(); node_config.fork_urls = vec![eth_rpc_url.clone()]; + node_config.apply_tempo_fork_beneficiary_default(&mut evm_env); node_config.setup_fork_db_config(eth_rpc_url, &mut evm_env, &self.fees).await? }; @@ -3897,7 +3898,13 @@ impl> Backend { } // reset the block env if let Some(block) = state.block.clone() { - self.evm_env.write().block_env = block.clone(); + { + let mut env = self.evm_env.write(); + env.block_env = block.clone(); + if self.is_tempo() && self.is_fork() && env.block_env.beneficiary.is_zero() { + env.block_env.beneficiary = TIP_FEE_MANAGER_ADDRESS; + } + } // Set the current best block number. // Defaults to block number for compatibility with existing state files. diff --git a/crates/anvil/tests/it/tempo.rs b/crates/anvil/tests/it/tempo.rs index 70034b114942f..b930b646fce19 100644 --- a/crates/anvil/tests/it/tempo.rs +++ b/crates/anvil/tests/it/tempo.rs @@ -19,10 +19,11 @@ use std::{ use crate::utils::http_provider; use alloy_consensus::Typed2718; use alloy_eips::eip2718::Encodable2718; +use alloy_genesis::Genesis; use alloy_network::{ReceiptResponse, TransactionBuilder, TransactionResponse}; use alloy_primitives::{Address, B256, Bytes, TxKind, U256, address, aliases::U96, keccak256}; use alloy_provider::{Provider, ext::TxPoolApi}; -use alloy_rpc_types::{BlockId, BlockNumberOrTag, TransactionRequest}; +use alloy_rpc_types::{BlockId, BlockNumberOrTag, TransactionRequest, anvil::Forking}; use alloy_serde::WithOtherFields; use alloy_signer::Signer; use alloy_signer_local::PrivateKeySigner; @@ -92,11 +93,131 @@ async fn test_tempo_fork_detects_hardfork_from_fork_timestamp() { ) .await; - let (api, _handle) = + let (api, handle) = spawn(NodeConfig::test_tempo().with_eth_rpc_url(Some(source_handle.http_endpoint()))).await; let node_info = api.anvil_node_info().await.unwrap(); assert_eq!(node_info.hard_fork, "T3"); + + api.mine_one().await; + let latest_block = handle + .http_provider() + .get_block_by_number(BlockNumberOrTag::Latest) + .await + .unwrap() + .unwrap(); + assert_eq!(latest_block.header.beneficiary, TIP_FEE_MANAGER_ADDRESS); +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_tempo_reset_to_fork_uses_fee_manager_beneficiary() { + let (_source_api, source_handle) = spawn(NodeConfig::test()).await; + + let (api, handle) = spawn(NodeConfig::test_tempo()).await; + api.anvil_reset(Some(Forking { + json_rpc_url: Some(source_handle.http_endpoint()), + block_number: None, + })) + .await + .unwrap(); + + api.mine_one().await; + let latest_block = handle + .http_provider() + .get_block_by_number(BlockNumberOrTag::Latest) + .await + .unwrap() + .unwrap(); + assert_eq!(latest_block.header.beneficiary, TIP_FEE_MANAGER_ADDRESS); +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_tempo_reset_to_fork_preserves_explicit_coinbase() { + let (_source_api, source_handle) = spawn(NodeConfig::test()).await; + let custom_coinbase = address!("0x1111111111111111111111111111111111111111"); + + let (api, handle) = spawn(NodeConfig::test_tempo()).await; + api.anvil_set_coinbase(custom_coinbase).await.unwrap(); + api.anvil_reset(Some(Forking { + json_rpc_url: Some(source_handle.http_endpoint()), + block_number: None, + })) + .await + .unwrap(); + + api.mine_one().await; + let latest_block = handle + .http_provider() + .get_block_by_number(BlockNumberOrTag::Latest) + .await + .unwrap() + .unwrap(); + assert_eq!(latest_block.header.beneficiary, custom_coinbase); +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_tempo_fork_with_default_genesis_uses_fee_manager_beneficiary() { + let (_source_api, source_handle) = spawn(NodeConfig::test()).await; + + let (api, handle) = spawn( + NodeConfig::test_tempo() + .with_eth_rpc_url(Some(source_handle.http_endpoint())) + .with_genesis(Some(Genesis::default())), + ) + .await; + + api.mine_one().await; + let latest_block = handle + .http_provider() + .get_block_by_number(BlockNumberOrTag::Latest) + .await + .unwrap() + .unwrap(); + assert_eq!(latest_block.header.beneficiary, TIP_FEE_MANAGER_ADDRESS); +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_tempo_fork_with_loaded_zero_beneficiary_state_uses_fee_manager_beneficiary() { + let (source_api, source_handle) = spawn(NodeConfig::test()).await; + let mut state = source_api.serialized_state(false).await.unwrap(); + state.block.as_mut().unwrap().beneficiary = Address::ZERO; + + let (api, handle) = spawn( + NodeConfig::test_tempo() + .with_eth_rpc_url(Some(source_handle.http_endpoint())) + .with_init_state(Some(state)), + ) + .await; + + api.mine_one().await; + let latest_block = handle + .http_provider() + .get_block_by_number(BlockNumberOrTag::Latest) + .await + .unwrap() + .unwrap(); + assert_eq!(latest_block.header.beneficiary, TIP_FEE_MANAGER_ADDRESS); +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_tempo_fork_runtime_load_state_uses_fee_manager_beneficiary() { + let (source_api, source_handle) = spawn(NodeConfig::test()).await; + let mut state = source_api.serialized_state(false).await.unwrap(); + state.block.as_mut().unwrap().beneficiary = Address::ZERO; + + let (api, handle) = + spawn(NodeConfig::test_tempo().with_eth_rpc_url(Some(source_handle.http_endpoint()))).await; + + api.anvil_load_state(Bytes::from(serde_json::to_vec(&state).unwrap())).await.unwrap(); + + api.mine_one().await; + let latest_block = handle + .http_provider() + .get_block_by_number(BlockNumberOrTag::Latest) + .await + .unwrap() + .unwrap(); + assert_eq!(latest_block.header.beneficiary, TIP_FEE_MANAGER_ADDRESS); } sol! {