From 38184bf902a7b43cd4b33b76fe54cd11b9de59b7 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Sat, 29 Aug 2026 02:25:29 +0200 Subject: [PATCH] fix(anvil): coerce decoder hardfork to executed network family A cross-namespace hardfork override on a tempo or monad node (e.g. --hardfork prague with an inferred fork network) left the trace decoder without a network hardfork while execution still coerced via the lossy From conversions, dropping monad metadata and widening the tempo precompile set. The decoder now derives its hardfork through the same coercion execution applies. --- .changelog/anvil-decoder-executed-hardfork.md | 5 ++++ crates/anvil/src/config.rs | 2 +- crates/anvil/src/eth/backend/mem/mod.rs | 16 +++++++++- crates/evm/networks/src/lib.rs | 29 +++++++++++++++++++ 4 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 .changelog/anvil-decoder-executed-hardfork.md diff --git a/.changelog/anvil-decoder-executed-hardfork.md b/.changelog/anvil-decoder-executed-hardfork.md new file mode 100644 index 0000000000000..e534e2fd430a0 --- /dev/null +++ b/.changelog/anvil-decoder-executed-hardfork.md @@ -0,0 +1,5 @@ +--- +anvil: patch +--- + +Fixed anvil trace decoding to follow the executed network hardfork when a cross-namespace hardfork override is configured. diff --git a/crates/anvil/src/config.rs b/crates/anvil/src/config.rs index 6e2605b9d0779..6d24e31424eb4 100644 --- a/crates/anvil/src/config.rs +++ b/crates/anvil/src/config.rs @@ -1422,7 +1422,7 @@ impl NodeConfig { .unwrap_or_else(|| self.get_hardfork()); let mut decoder_builder = CallTraceDecoderBuilder::new() .with_networks(self.networks) - .with_hardfork(Some(active_hardfork)); + .with_hardfork(Some(self.networks.executed_hardfork(active_hardfork))); if self.print_traces { // if traces should get printed we configure the decoder with the signatures cache if let Ok(identifier) = SignaturesIdentifier::new(false) { diff --git a/crates/anvil/src/eth/backend/mem/mod.rs b/crates/anvil/src/eth/backend/mem/mod.rs index 5cfe96e45b44b..e89ccefc1d683 100644 --- a/crates/anvil/src/eth/backend/mem/mod.rs +++ b/crates/anvil/src/eth/backend/mem/mod.rs @@ -1490,7 +1490,7 @@ impl Backend { /// Returns a trace decoder configured for the currently resolved hardfork. fn call_trace_decoder(&self) -> Arc { - let hardfork = Some(self.hardfork()); + let hardfork = Some(self.networks.executed_hardfork(self.hardfork())); let decoder = self.call_trace_decoder.read(); if decoder.hardfork() == hardfork { return Arc::clone(&decoder); @@ -9883,6 +9883,20 @@ mod tests { assert_eq!(outcome.block_number, original_best_number + 1); } + #[tokio::test] + async fn trace_decoder_follows_executed_hardfork_for_cross_namespace_override() { + let (api, _) = spawn( + NodeConfig::test_tempo() + .with_hardfork(Some(FoundryHardfork::Ethereum(EthereumHardfork::Prague))), + ) + .await; + + let decoder = api.backend.call_trace_decoder(); + assert_eq!(decoder.hardfork(), Some(FoundryHardfork::Tempo(api.backend.tempo_hardfork()))); + // The refresh compares the same coerced value, so repeated calls stay stable. + assert!(Arc::ptr_eq(&decoder, &api.backend.call_trace_decoder())); + } + #[cfg(feature = "monad")] #[tokio::test] async fn monad_trace_decoder_follows_resolved_hardfork() { diff --git a/crates/evm/networks/src/lib.rs b/crates/evm/networks/src/lib.rs index 4328ae3f3ff2e..910fb36d7ed08 100644 --- a/crates/evm/networks/src/lib.rs +++ b/crates/evm/networks/src/lib.rs @@ -461,6 +461,23 @@ impl NetworkConfigs { false } + /// Coerces `hardfork` into this network's family. + /// + /// Execution and fee rules apply the same lossy `From` conversions, so a cross-namespace + /// override such as `--hardfork prague` on a Tempo node runs as a Tempo hardfork. Callers that + /// need to describe what actually executed, like trace decoding, go through here rather than + /// carrying the configured value. + pub fn executed_hardfork(&self, hardfork: FoundryHardfork) -> FoundryHardfork { + if self.is_tempo() { + return TempoHardfork::from(hardfork).into(); + } + #[cfg(feature = "monad")] + if self.is_monad() { + return MonadHardfork::from(hardfork).into(); + } + hardfork + } + /// Returns additional cheatcode contract addresses for the active network. pub const fn extra_cheatcode_addresses(&self) -> &'static [Address] { #[cfg(feature = "monad")] @@ -1789,4 +1806,16 @@ mod tests { assert!(cfg_optimism.is_optimism()); } } + + #[test] + fn executed_hardfork_follows_the_network_family() { + // A cross-namespace override runs as the configured network's hardfork, so the value used + // to describe execution has to be coerced the same way. + let prague = FoundryHardfork::Ethereum(EthereumHardfork::Prague); + assert_eq!(NetworkConfigs::default().executed_hardfork(prague), prague); + assert_eq!( + NetworkConfigs::with_tempo().executed_hardfork(prague), + FoundryHardfork::Tempo(TempoHardfork::from(prague)) + ); + } }