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)) + ); + } }