Skip to main content

foundry_evm_traces/decoder/
mod.rs

1use crate::{
2    CallTrace, CallTraceArena, CallTraceNode, DecodedCallData, DecodedTraceStep,
3    debug::DebugTraceIdentifier,
4    identifier::{IdentifiedAddress, LocalTraceIdentifier, SignaturesIdentifier, TraceIdentifier},
5};
6use alloy_dyn_abi::{
7    DecodedEvent as AlloyDecodedEvent, DynSolValue, EventExt, FunctionExt, JsonAbiExt,
8};
9use alloy_json_abi::{Constructor, Error, Event, Function, JsonAbi};
10use alloy_primitives::{
11    Address, B256, LogData, Selector, U256,
12    map::{AddressHashMap, HashMap, HashSet},
13};
14use alloy_sol_types::SolValue;
15use foundry_common::{
16    ContractsByArtifact, SELECTOR_LEN, abi::get_indexed_event, fmt::format_token,
17    get_contract_name, selectors::SelectorKind,
18};
19use foundry_config::TracingConfig;
20use foundry_evm_core::{
21    abi::{Vm, console},
22    constants::{CALLER, CHEATCODE_ADDRESS, DEFAULT_CREATE2_DEPLOYER, HARDHAT_CONSOLE_ADDRESS},
23    decode::RevertDecoder,
24    precompiles::{
25        BLAKE_2F, BLS12_G1ADD, BLS12_G1MSM, BLS12_G2ADD, BLS12_G2MSM, BLS12_MAP_FP_TO_G1,
26        BLS12_MAP_FP2_TO_G2, BLS12_PAIRING_CHECK, CELO_TRANSFER, EC_ADD, EC_MUL, EC_PAIRING,
27        EC_RECOVER, IDENTITY, MOD_EXP, POINT_EVALUATION, RIPEMD_160, SHA_256,
28    },
29};
30use foundry_evm_hardforks::{ExecutionSpec, FoundryHardfork, TempoHardfork};
31use foundry_evm_networks::{NetworkConfigs, NetworkVariant, celo::transfer::CELO_TRANSFER_LABEL};
32use itertools::Itertools;
33use revm::{bytecode::opcode::OpCode, interpreter::InstructionResult};
34use revm_inspectors::tracing::types::{DecodedCallLog, DecodedCallTrace};
35use std::{collections::BTreeMap, sync::OnceLock};
36use tempo_contracts::precompiles::{
37    CURRENT_COMMITTEE_ADDRESS, IAccountKeychain, IAddressRegistry, ICurrentCommittee, IFeeManager,
38    IReceivePolicyGuard, ISignatureVerifier, IStablecoinDEX, IStorageCredits, ITIP20ChannelReserve,
39    ITIP20Factory, ITIP403Registry, IValidatorConfig,
40};
41use tempo_precompiles::{
42    ACCOUNT_KEYCHAIN_ADDRESS, ADDRESS_REGISTRY_ADDRESS, NONCE_PRECOMPILE_ADDRESS, PATH_USD_ADDRESS,
43    RECEIVE_POLICY_GUARD_ADDRESS, SIGNATURE_VERIFIER_ADDRESS, STABLECOIN_DEX_ADDRESS,
44    STORAGE_CREDITS_ADDRESS, TIP_FEE_MANAGER_ADDRESS, TIP20_CHANNEL_RESERVE_ADDRESS,
45    TIP20_FACTORY_ADDRESS, TIP403_REGISTRY_ADDRESS, VALIDATOR_CONFIG_ADDRESS, nonce::INonce,
46    tip20::ITIP20,
47};
48
49#[cfg(feature = "base")]
50use base_common_precompiles::{
51    ActivationRegistryStorage, B20FactoryStorage, NonceManagerStorage, PolicyRegistryStorage,
52    TxContextStorage,
53};
54#[cfg(feature = "base")]
55use foundry_evm_hardforks::BaseSpecId;
56#[cfg(feature = "base")]
57use foundry_evm_networks::{active_base_precompiles, is_base_precompile_active_at};
58
59#[cfg(feature = "monad")]
60type MonadHardfork = foundry_evm_hardforks::MonadHardfork;
61
62#[cfg(feature = "base")]
63mod base;
64#[cfg(feature = "monad")]
65mod monad;
66pub(crate) mod precompiles;
67
68/// Address-scoped events keyed by signature and indexed input count; anonymous events have no
69/// signature.
70type AddressEvents = HashMap<Address, BTreeMap<(Option<B256>, usize), Vec<Event>>>;
71
72/// A decoded event with both display-formatted parameters and their underlying ABI values.
73#[derive(Debug, Default)]
74pub struct DecodedEvent {
75    /// The event name or canonical signature.
76    pub name: Option<String>,
77    /// Event parameter names, display-formatted values, and decoded ABI values.
78    pub params: Option<Vec<(String, String, DynSolValue)>>,
79}
80
81impl DecodedEvent {
82    fn into_call_log(self) -> DecodedCallLog {
83        DecodedCallLog {
84            name: self.name,
85            params: self.params.map(|params| {
86                params.into_iter().map(|(name, display, _)| (name, display)).collect()
87            }),
88        }
89    }
90}
91
92/// Build a new [CallTraceDecoder].
93#[derive(Default)]
94#[must_use = "builders do nothing unless you call `build` on them"]
95pub struct CallTraceDecoderBuilder {
96    decoder: CallTraceDecoder,
97}
98
99impl CallTraceDecoderBuilder {
100    /// Create a new builder.
101    #[inline]
102    pub fn new() -> Self {
103        Self { decoder: CallTraceDecoder::new().clone() }
104    }
105
106    /// Add known labels to the decoder.
107    #[inline]
108    pub fn with_labels(mut self, labels: impl IntoIterator<Item = (Address, String)>) -> Self {
109        self.decoder.labels.extend(labels);
110        self
111    }
112
113    /// Add known errors to the decoder.
114    #[inline]
115    pub fn with_abi(mut self, abi: &JsonAbi) -> Self {
116        self.decoder.collect_abi(abi, None, true);
117        self
118    }
119
120    /// Add events from an ABI for a specific contract address.
121    #[inline]
122    pub fn with_address_events(mut self, address: Address, abi: &JsonAbi) -> Self {
123        for event in abi.events() {
124            self.decoder.push_address_event(address, event.clone());
125        }
126        self
127    }
128
129    /// Add known contracts to the decoder.
130    #[inline]
131    pub fn with_known_contracts(mut self, contracts: &ContractsByArtifact) -> Self {
132        trace!(target: "evm::traces", len=contracts.len(), "collecting known contract ABIs");
133        for contract in contracts.values() {
134            self.decoder.collect_abi(&contract.abi, None, true);
135        }
136        self
137    }
138
139    /// Add known contracts to the decoder from a `LocalTraceIdentifier`.
140    #[inline]
141    pub fn with_local_identifier_abis(self, identifier: &LocalTraceIdentifier<'_>) -> Self {
142        self.with_known_contracts(identifier.contracts())
143    }
144
145    /// Sets the verbosity level of the decoder.
146    #[inline]
147    pub const fn with_verbosity(mut self, level: u8) -> Self {
148        self.decoder.verbosity = level;
149        self
150    }
151
152    /// Applies trace rendering settings.
153    #[inline]
154    pub fn with_tracing_config(mut self, config: &TracingConfig) -> Self {
155        self.decoder.labels.extend(config.labels.clone());
156        self.decoder.verbosity = config.verbosity;
157        self.decoder.disable_labels = config.disable_labels;
158        self.decoder.compact_labels = config.compact_labels;
159        self
160    }
161
162    /// Sets the signature identifier for events and functions.
163    #[inline]
164    pub fn with_signature_identifier(mut self, identifier: SignaturesIdentifier) -> Self {
165        self.decoder.signature_identifier = Some(identifier);
166        self
167    }
168
169    /// Sets the signature identifier for events and functions.
170    #[inline]
171    pub const fn with_label_disabled(mut self, disable_alias: bool) -> Self {
172        self.decoder.disable_labels = disable_alias;
173        self
174    }
175
176    /// Sets the chain ID for network-specific precompile detection.
177    #[inline]
178    pub const fn with_chain_id(mut self, chain_id: Option<u64>) -> Self {
179        self.decoder.chain_id = chain_id;
180        self
181    }
182
183    /// Sets the execution network used for network-specific precompile detection.
184    #[inline]
185    pub fn with_execution_network(self, network: NetworkVariant) -> Self {
186        self.with_networks(network.into())
187    }
188
189    /// Sets the complete execution profile used for network-specific precompile detection.
190    #[inline]
191    pub const fn with_networks(mut self, networks: NetworkConfigs) -> Self {
192        self.decoder.networks = Some(networks);
193        self
194    }
195
196    /// Sets the hardfork used for network-specific metadata and precompile detection.
197    #[inline]
198    pub const fn with_hardfork(mut self, hardfork: Option<FoundryHardfork>) -> Self {
199        self.decoder.hardfork = hardfork;
200        self
201    }
202
203    /// Hides addresses in trace parameters when a label is available.
204    #[inline]
205    pub const fn with_compact_labels(mut self, compact: bool) -> Self {
206        self.decoder.compact_labels = compact;
207        self
208    }
209
210    /// Sets the debug identifier for the decoder.
211    #[inline]
212    pub fn with_debug_identifier(mut self, identifier: DebugTraceIdentifier) -> Self {
213        self.decoder.debug_identifier = Some(identifier);
214        self
215    }
216
217    /// Build the decoder.
218    #[inline]
219    pub fn build(mut self) -> CallTraceDecoder {
220        self.decoder.base_labels = self.decoder.labels.clone();
221        self.decoder.register_celo_metadata();
222        self.decoder.register_tempo_metadata();
223        #[cfg(feature = "monad")]
224        self.decoder.register_monad_metadata();
225        #[cfg(feature = "base")]
226        self.decoder.register_base_metadata();
227        self.decoder
228    }
229}
230
231/// The call trace decoder.
232///
233/// The decoder collects address labels and ABIs from any number of [TraceIdentifier]s, which it
234/// then uses to decode the call trace.
235///
236/// Note that a call trace decoder is required for each new set of traces, since addresses in
237/// different sets might overlap.
238#[derive(Clone, Debug, Default)]
239pub struct CallTraceDecoder {
240    /// Addresses identified to be a specific contract.
241    ///
242    /// The values are in the form `"<artifact>:<contract>"`.
243    pub contracts: HashMap<Address, String>,
244    /// Address labels.
245    pub labels: HashMap<Address, String>,
246    /// Labels configured when the decoder was built.
247    base_labels: HashMap<Address, String>,
248    /// Contract addresses that have a receive function.
249    pub receive_contracts: HashSet<Address>,
250    /// Contract addresses that have fallback functions, mapped to function selectors of that
251    /// contract.
252    pub fallback_contracts: HashMap<Address, HashSet<Selector>>,
253    /// Contract addresses that have do NOT have fallback functions, mapped to function selectors
254    /// of that contract.
255    pub non_fallback_contracts: HashMap<Address, HashSet<Selector>>,
256
257    /// All known functions.
258    pub functions: HashMap<Selector, Vec<Function>>,
259    /// Functions identified for a specific contract address.
260    pub functions_by_address: HashMap<Address, HashMap<Selector, Vec<Function>>>,
261    /// Constructors identified for a specific contract address.
262    pub constructors_by_address: HashMap<Address, Constructor>,
263    /// Creation input offsets where ABI-encoded constructor arguments begin.
264    pub constructor_args_offsets: HashMap<Address, usize>,
265    /// All known events.
266    ///
267    /// Key is: `(topics[0], topics.len() - 1)`.
268    pub events: BTreeMap<(B256, usize), Vec<Event>>,
269    /// Regular and anonymous events identified for a specific contract address.
270    events_by_address: Option<Box<AddressEvents>>,
271    /// Revert decoder. Contains all known custom errors.
272    pub revert_decoder: RevertDecoder,
273
274    /// A signature identifier for events and functions.
275    pub signature_identifier: Option<SignaturesIdentifier>,
276    /// Verbosity level
277    pub verbosity: u8,
278
279    /// Optional identifier of individual trace steps.
280    pub debug_identifier: Option<DebugTraceIdentifier>,
281
282    /// Disable showing of labels.
283    pub disable_labels: bool,
284
285    /// The chain ID, used to determine network-specific precompiles.
286    pub chain_id: Option<u64>,
287
288    /// Execution profile, kept separate from the source chain used for external identification.
289    networks: Option<NetworkConfigs>,
290
291    /// Detailed opcodes for analysis.
292    pub opcodes: Vec<OpCode>,
293
294    /// The hardfork used to determine network-specific metadata and precompiles.
295    hardfork: Option<FoundryHardfork>,
296
297    /// Hide addresses when a label is available, showing only the label.
298    pub compact_labels: bool,
299}
300
301impl CallTraceDecoder {
302    fn register_celo_metadata(&mut self) {
303        if precompiles::is_known_precompile(
304            CELO_TRANSFER,
305            self.networks,
306            self.chain_id,
307            self.hardfork,
308        ) {
309            self.labels.entry(CELO_TRANSFER).or_insert_with(|| CELO_TRANSFER_LABEL.to_string());
310        }
311    }
312
313    fn register_tempo_metadata(&mut self) {
314        if self.networks.is_some_and(|networks| !networks.is_tempo()) {
315            return;
316        }
317        let hardfork = self.hardfork.and_then(TempoHardfork::from_foundry_hardfork);
318        if hardfork.is_some_and(|hardfork| hardfork.is_t5()) {
319            self.labels
320                .entry(TIP20_CHANNEL_RESERVE_ADDRESS)
321                .or_insert_with(|| "TIP20ChannelReserve".to_string());
322        }
323        if hardfork.is_some_and(|hardfork| hardfork.is_t6()) {
324            self.labels
325                .entry(RECEIVE_POLICY_GUARD_ADDRESS)
326                .or_insert_with(|| "ReceivePolicyGuard".to_string());
327        }
328    }
329
330    #[cfg(feature = "base")]
331    fn register_base_metadata(&mut self) {
332        if self.networks.is_some_and(|networks| !networks.is_base()) {
333            return;
334        }
335        let Some(upgrade) =
336            self.hardfork.and_then(BaseSpecId::from_foundry_hardfork).map(|spec| spec.upgrade())
337        else {
338            return;
339        };
340
341        for (label, address) in active_base_precompiles(upgrade) {
342            self.labels.entry(address).or_insert_with(|| label.to_string());
343        }
344
345        // Labels alone leave calls showing a raw selector, so register the ABIs too. Each is
346        // scoped to its address and gated on the upgrade that installs it.
347        if is_base_precompile_active_at(ActivationRegistryStorage::ADDRESS, upgrade) {
348            self.register_address_abi(
349                ActivationRegistryStorage::ADDRESS,
350                &base::IActivationRegistry::abi::contract(),
351            );
352        }
353        if is_base_precompile_active_at(PolicyRegistryStorage::ADDRESS, upgrade) {
354            self.register_address_abi(
355                PolicyRegistryStorage::ADDRESS,
356                &base::IPolicyRegistry::abi::contract(),
357            );
358        }
359        if is_base_precompile_active_at(B20FactoryStorage::ADDRESS, upgrade) {
360            self.register_address_abi(
361                B20FactoryStorage::ADDRESS,
362                &base::IB20Factory::abi::contract(),
363            );
364        }
365        if is_base_precompile_active_at(NonceManagerStorage::ADDRESS, upgrade) {
366            self.register_address_abi(
367                NonceManagerStorage::ADDRESS,
368                &base::INonceManager::abi::contract(),
369            );
370        }
371        if is_base_precompile_active_at(TxContextStorage::ADDRESS, upgrade) {
372            self.register_address_abi(
373                TxContextStorage::ADDRESS,
374                &base::ITransactionContext::abi::contract(),
375            );
376        }
377    }
378
379    /// Creates a new call trace decoder.
380    ///
381    /// The call trace decoder always knows how to decode calls to the cheatcode address, as well
382    /// as DSTest-style logs.
383    pub fn new() -> &'static Self {
384        // If you want to take arguments in this function, assign them to the fields of the cloned
385        // lazy instead of removing it
386        static INIT: OnceLock<CallTraceDecoder> = OnceLock::new();
387        INIT.get_or_init(Self::init)
388    }
389
390    /// Returns the hardfork used for network-specific metadata.
391    pub const fn hardfork(&self) -> Option<FoundryHardfork> {
392        self.hardfork
393    }
394
395    /// Rebuilds address-scoped metadata for a new hardfork.
396    ///
397    /// Hardfork changes invalidate previously identified addresses because the set of active
398    /// precompiles can change. Global ABI and signature metadata is preserved.
399    pub fn set_hardfork(&mut self, hardfork: Option<FoundryHardfork>) {
400        if self.hardfork == hardfork {
401            return;
402        }
403        self.hardfork = hardfork;
404        self.clear_addresses();
405    }
406
407    #[instrument(name = "CallTraceDecoder::init", level = "debug")]
408    fn init() -> Self {
409        // Materialized once so the revert decoder can take references below.
410        let tempo_abis = [
411            IFeeManager::abi::contract(),
412            ITIP20::abi::contract(),
413            ITIP403Registry::abi::contract(),
414            ITIP20Factory::abi::contract(),
415            IStablecoinDEX::abi::contract(),
416            IStorageCredits::abi::contract(),
417            INonce::abi::contract(),
418            IValidatorConfig::abi::contract(),
419            IAccountKeychain::abi::contract(),
420            IAddressRegistry::abi::contract(),
421            ITIP20ChannelReserve::abi::contract(),
422            ISignatureVerifier::abi::contract(),
423            IReceivePolicyGuard::abi::contract(),
424        ];
425        let labels = HashMap::from_iter([
426            (CHEATCODE_ADDRESS, "VM".to_string()),
427            (HARDHAT_CONSOLE_ADDRESS, "console".to_string()),
428            (DEFAULT_CREATE2_DEPLOYER, "Create2Deployer".to_string()),
429            (CALLER, "DefaultSender".to_string()),
430            (EC_RECOVER, "ECRecover".to_string()),
431            (SHA_256, "SHA-256".to_string()),
432            (RIPEMD_160, "RIPEMD-160".to_string()),
433            (IDENTITY, "Identity".to_string()),
434            (MOD_EXP, "ModExp".to_string()),
435            (EC_ADD, "ECAdd".to_string()),
436            (EC_MUL, "ECMul".to_string()),
437            (EC_PAIRING, "ECPairing".to_string()),
438            (BLAKE_2F, "Blake2F".to_string()),
439            (POINT_EVALUATION, "PointEvaluation".to_string()),
440            (BLS12_G1ADD, "BLS12_G1ADD".to_string()),
441            (BLS12_G1MSM, "BLS12_G1MSM".to_string()),
442            (BLS12_G2ADD, "BLS12_G2ADD".to_string()),
443            (BLS12_G2MSM, "BLS12_G2MSM".to_string()),
444            (BLS12_PAIRING_CHECK, "BLS12_PAIRING_CHECK".to_string()),
445            (BLS12_MAP_FP_TO_G1, "BLS12_MAP_FP_TO_G1".to_string()),
446            (BLS12_MAP_FP2_TO_G2, "BLS12_MAP_FP2_TO_G2".to_string()),
447            // Tempo
448            (TIP_FEE_MANAGER_ADDRESS, "FeeManager".to_string()),
449            (TIP403_REGISTRY_ADDRESS, "TIP403Registry".to_string()),
450            (TIP20_FACTORY_ADDRESS, "TIP20Factory".to_string()),
451            (STABLECOIN_DEX_ADDRESS, "StablecoinDex".to_string()),
452            (NONCE_PRECOMPILE_ADDRESS, "Nonce".to_string()),
453            (VALIDATOR_CONFIG_ADDRESS, "ValidatorConfig".to_string()),
454            (ACCOUNT_KEYCHAIN_ADDRESS, "AccountKeychain".to_string()),
455            (ADDRESS_REGISTRY_ADDRESS, "AddressRegistry".to_string()),
456            (TIP20_CHANNEL_RESERVE_ADDRESS, "TIP20ChannelReserve".to_string()),
457            (SIGNATURE_VERIFIER_ADDRESS, "SignatureVerifier".to_string()),
458            (RECEIVE_POLICY_GUARD_ADDRESS, "ReceivePolicyGuard".to_string()),
459            (STORAGE_CREDITS_ADDRESS, "StorageCredits".to_string()),
460            (PATH_USD_ADDRESS, "PathUSD".to_string()),
461        ]);
462
463        let functions = console::hh::abi::functions()
464            .into_values()
465            .chain(Vm::abi::functions().into_values())
466            .chain(IFeeManager::abi::functions().into_values())
467            // `IStorageCredits` shares the `balanceOf(address)` selector with `ITIP20`, so it
468            // must be chained first to keep `ITIP20`'s `uint256` return as the global fallback.
469            .chain(IStorageCredits::abi::functions().into_values())
470            .chain(ITIP20::abi::functions().into_values())
471            .chain(ITIP403Registry::abi::functions().into_values())
472            .chain(ITIP20Factory::abi::functions().into_values())
473            .chain(IStablecoinDEX::abi::functions().into_values())
474            .chain(INonce::abi::functions().into_values())
475            .chain(IValidatorConfig::abi::functions().into_values())
476            .chain(IAccountKeychain::abi::functions().into_values())
477            .chain(IAddressRegistry::abi::functions().into_values())
478            .chain(ITIP20ChannelReserve::abi::functions().into_values())
479            .chain(ISignatureVerifier::abi::functions().into_values())
480            .chain(IReceivePolicyGuard::abi::functions().into_values())
481            .flatten()
482            .map(|func| (func.selector(), vec![func]))
483            .collect();
484
485        let events = console::ds::abi::events()
486            .into_values()
487            .chain(IFeeManager::abi::events().into_values())
488            .chain(ITIP20::abi::events().into_values())
489            .chain(ITIP403Registry::abi::events().into_values())
490            .chain(ITIP20Factory::abi::events().into_values())
491            .chain(IStablecoinDEX::abi::events().into_values())
492            .chain(INonce::abi::events().into_values())
493            .chain(IValidatorConfig::abi::events().into_values())
494            .chain(IAccountKeychain::abi::events().into_values())
495            .chain(IAddressRegistry::abi::events().into_values())
496            .chain(ITIP20ChannelReserve::abi::events().into_values())
497            .chain(ISignatureVerifier::abi::events().into_values())
498            .chain(IReceivePolicyGuard::abi::events().into_values())
499            .flatten()
500            .map(|event| ((event.selector(), indexed_inputs(&event)), vec![event]))
501            .collect();
502
503        Self {
504            contracts: Default::default(),
505            labels,
506            base_labels: Default::default(),
507            receive_contracts: Default::default(),
508            fallback_contracts: Default::default(),
509            non_fallback_contracts: Default::default(),
510            functions,
511            functions_by_address: Default::default(),
512            constructors_by_address: Default::default(),
513            constructor_args_offsets: Default::default(),
514            events,
515            events_by_address: None,
516            // Decode Tempo precompile custom errors by name in traces.
517            revert_decoder: RevertDecoder::new().with_abis(tempo_abis.iter()),
518
519            signature_identifier: None,
520            verbosity: 0,
521
522            debug_identifier: None,
523
524            disable_labels: false,
525
526            chain_id: None,
527            networks: None,
528
529            opcodes: Vec::new(),
530
531            hardfork: None,
532            compact_labels: false,
533        }
534    }
535
536    /// Clears all known addresses.
537    pub fn clear_addresses(&mut self) {
538        self.contracts.clear();
539
540        if self.base_labels.is_empty() {
541            self.labels.clone_from(&Self::new().labels);
542        } else {
543            self.labels.clone_from(&self.base_labels);
544        }
545
546        self.receive_contracts.clear();
547        self.fallback_contracts.clear();
548        self.non_fallback_contracts.clear();
549        self.functions_by_address.clear();
550        self.events_by_address = None;
551        self.constructors_by_address.clear();
552        self.constructor_args_offsets.clear();
553
554        self.register_celo_metadata();
555        self.register_tempo_metadata();
556        #[cfg(feature = "monad")]
557        self.register_monad_metadata();
558        #[cfg(feature = "base")]
559        self.register_base_metadata();
560    }
561
562    /// Returns labels for precompiles active in this decoder's chain context.
563    pub fn precompile_labels(&self) -> AddressHashMap<String> {
564        self.labels
565            .iter()
566            .filter(|(address, _)| {
567                precompiles::is_known_precompile(
568                    **address,
569                    self.networks,
570                    self.chain_id,
571                    self.hardfork,
572                )
573            })
574            .map(|(address, label)| (*address, label.clone()))
575            .collect()
576    }
577
578    /// Identify unknown addresses in the specified call trace using the specified identifier.
579    ///
580    /// Unknown contracts are contracts that either lack a label or an ABI.
581    pub fn identify(&mut self, arena: &CallTraceArena, identifier: &mut impl TraceIdentifier) {
582        self.collect_identified_addresses(self.identify_addresses(arena, identifier), true);
583    }
584
585    /// Identifies unknown addresses without re-registering their ABIs globally.
586    ///
587    /// The identified ABIs must already be registered in this decoder.
588    pub fn identify_scoped(
589        &mut self,
590        arena: &CallTraceArena,
591        identifier: &mut impl TraceIdentifier,
592    ) {
593        self.collect_identified_addresses(self.identify_addresses(arena, identifier), false);
594    }
595
596    /// Identify unknown addresses in the specified call trace using the specified identifier.
597    ///
598    /// Unknown contracts are contracts that either lack a label or an ABI.
599    pub fn identify_addresses<'a>(
600        &self,
601        arena: &CallTraceArena,
602        identifier: &'a mut impl TraceIdentifier,
603    ) -> Vec<IdentifiedAddress<'a>> {
604        let nodes = arena.nodes().iter().filter(|node| {
605            // Skip precompile addresses, they will never resolve externally.
606            if node.is_precompile()
607                || precompiles::is_known_precompile_call(
608                    &node.trace,
609                    self.networks,
610                    self.chain_id,
611                    self.hardfork,
612                )
613            {
614                return false;
615            }
616            let address = &node.trace.address;
617            !self.labels.contains_key(address) || !self.contracts.contains_key(address)
618        });
619        identifier.identify_addresses(&nodes.collect::<Vec<_>>())
620    }
621
622    /// Adds a single event to the decoder.
623    pub fn push_event(&mut self, event: Event) {
624        self.events.entry((event.selector(), indexed_inputs(&event))).or_default().push(event);
625    }
626
627    /// Adds a single event to the decoder for a specific contract address.
628    pub fn push_address_event(&mut self, address: Address, event: Event) {
629        let events = self
630            .events_by_address
631            .get_or_insert_with(Default::default)
632            .entry(address)
633            .or_default()
634            .entry(((!event.anonymous).then(|| event.selector()), indexed_inputs(&event)))
635            .or_default();
636        if !events.contains(&event) {
637            events.push(event);
638        }
639    }
640
641    /// Adds a single function to the decoder.
642    pub fn push_function(&mut self, function: Function) {
643        let selector = function.selector();
644        let functions = self.functions.entry(selector).or_default();
645
646        if Self::push_function_to(functions, function) && functions.len() > 1 {
647            let function = functions.last().expect("function was just inserted");
648            let signature = function.signature();
649            trace!(target: "evm::traces", %selector, new=%signature, "duplicate function selector");
650        }
651    }
652
653    /// Adds a single function to the decoder for a specific contract address.
654    pub fn push_address_function(&mut self, address: Address, function: Function) {
655        let functions = self
656            .functions_by_address
657            .entry(address)
658            .or_default()
659            .entry(function.selector())
660            .or_default();
661        Self::push_function_to(functions, function);
662    }
663
664    fn push_function_to(functions: &mut Vec<Function>, function: Function) -> bool {
665        if functions.contains(&function) {
666            false
667        } else {
668            functions.push(function);
669            true
670        }
671    }
672
673    /// Returns the functions registered for `selector` at `address`.
674    ///
675    /// Address-scoped and caller-supplied metadata takes precedence over network fallbacks.
676    pub fn functions_for_selector(
677        &self,
678        address: Address,
679        selector: &Selector,
680    ) -> Option<&[Function]> {
681        if self.is_current_committee_active(address) {
682            static FUNCTIONS: OnceLock<HashMap<Selector, Vec<Function>>> = OnceLock::new();
683            if let Some(functions) = FUNCTIONS
684                .get_or_init(|| {
685                    ICurrentCommittee::abi::functions()
686                        .into_values()
687                        .flatten()
688                        .map(|function| (function.selector(), vec![function]))
689                        .collect()
690                })
691                .get(selector)
692            {
693                return Some(functions);
694            }
695        }
696        if let Some(functions) =
697            self.functions_by_address.get(&address).and_then(|functions| functions.get(selector))
698        {
699            return Some(functions);
700        }
701        #[cfg(feature = "base")]
702        let base_functions = self.base_functions_for_selector(selector);
703        // Tempo's function with this selector has no output, while B20 returns a bool.
704        #[cfg(feature = "base")]
705        if let Some(functions) = base_functions
706            && functions.first().is_some_and(|function| function.name == "transferWithMemo")
707        {
708            if let Some(global) = self.functions.get(selector) {
709                let tempo_fallback = global.first().is_some_and(|function| {
710                    function.name == "transferWithMemo" && function.outputs.is_empty()
711                });
712                if !tempo_fallback {
713                    return Some(global);
714                }
715                if global.len() > 1 {
716                    return Some(&global[1..]);
717                }
718            }
719            return Some(functions);
720        }
721        let functions = self.functions.get(selector);
722        #[cfg(feature = "base")]
723        let functions = functions.or(base_functions);
724        functions.map(Vec::as_slice)
725    }
726
727    #[cfg(feature = "base")]
728    fn is_base_context(&self) -> bool {
729        self.networks.map_or_else(
730            || self.hardfork.and_then(BaseSpecId::from_foundry_hardfork).is_some(),
731            |networks| networks.is_base(),
732        )
733    }
734
735    #[cfg(feature = "base")]
736    fn base_functions_for_selector(&self, selector: &Selector) -> Option<&'static Vec<Function>> {
737        if !self.is_base_context() {
738            return None;
739        }
740        static FUNCTIONS: OnceLock<HashMap<Selector, Vec<Function>>> = OnceLock::new();
741        FUNCTIONS
742            .get_or_init(|| {
743                base::IB20Extensions::abi::functions()
744                    .into_values()
745                    .flatten()
746                    .map(|function| (function.selector(), vec![function]))
747                    .collect()
748            })
749            .get(selector)
750    }
751
752    #[cfg(feature = "base")]
753    fn base_events(&self) -> Option<&'static BTreeMap<(B256, usize), Vec<Event>>> {
754        if !self.is_base_context() {
755            return None;
756        }
757        static EVENTS: OnceLock<BTreeMap<(B256, usize), Vec<Event>>> = OnceLock::new();
758        Some(EVENTS.get_or_init(|| {
759            base::IB20Extensions::abi::events()
760                .into_values()
761                .flatten()
762                .map(|event| ((event.selector(), indexed_inputs(&event)), vec![event]))
763                .collect()
764        }))
765    }
766
767    #[cfg(feature = "base")]
768    fn base_revert_decoder(&self, address: Address) -> Option<&'static RevertDecoder> {
769        let upgrade =
770            self.hardfork.and_then(BaseSpecId::from_foundry_hardfork).map(|spec| spec.upgrade())?;
771        if !self.is_base_context() || !is_base_precompile_active_at(address, upgrade) {
772            return None;
773        }
774
775        static DECODERS: OnceLock<HashMap<Address, RevertDecoder>> = OnceLock::new();
776        DECODERS
777            .get_or_init(|| {
778                HashMap::from_iter([
779                    (
780                        ActivationRegistryStorage::ADDRESS,
781                        RevertDecoder::new().with_abi(&base::IActivationRegistry::abi::contract()),
782                    ),
783                    (
784                        PolicyRegistryStorage::ADDRESS,
785                        RevertDecoder::new().with_abi(&base::IPolicyRegistry::abi::contract()),
786                    ),
787                    (
788                        B20FactoryStorage::ADDRESS,
789                        RevertDecoder::new().with_abi(&base::IB20Factory::abi::contract()),
790                    ),
791                    (
792                        NonceManagerStorage::ADDRESS,
793                        RevertDecoder::new().with_abi(&base::INonceManager::abi::contract()),
794                    ),
795                    (
796                        TxContextStorage::ADDRESS,
797                        RevertDecoder::new().with_abi(&base::ITransactionContext::abi::contract()),
798                    ),
799                ])
800            })
801            .get(&address)
802    }
803
804    #[cfg(feature = "monad")]
805    fn register_monad_metadata(&mut self) {
806        if self.networks.is_some_and(|networks| !networks.is_monad()) {
807            return;
808        }
809        let Some(hardfork) = self.hardfork.and_then(MonadHardfork::from_foundry_hardfork) else {
810            return;
811        };
812
813        self.labels
814            .entry(foundry_evm_core::constants::MONAD_CHEATCODE_ADDRESS)
815            .or_insert_with(|| "MonadVM".to_string());
816        self.register_address_abi(
817            monad_revm::staking::STAKING_ADDRESS,
818            &monad::IMonadStaking::abi::contract(),
819        );
820        self.register_address_abi(
821            monad_revm::staking::STAKING_ADDRESS,
822            &monad::IMonadStakingSyscalls::abi::contract(),
823        );
824        self.labels
825            .entry(monad_revm::staking::STAKING_ADDRESS)
826            .or_insert_with(|| "Staking".to_string());
827
828        if foundry_evm_networks::is_monad_precompile_active_at(
829            monad_revm::reserve_balance::abi::RESERVE_BALANCE_ADDRESS,
830            hardfork,
831        ) {
832            self.register_address_abi(
833                monad_revm::reserve_balance::abi::RESERVE_BALANCE_ADDRESS,
834                &monad::IReserveBalance::abi::contract(),
835            );
836            self.labels
837                .entry(monad_revm::reserve_balance::abi::RESERVE_BALANCE_ADDRESS)
838                .or_insert_with(|| "ReserveBalance".to_string());
839        }
840    }
841
842    #[cfg(any(feature = "base", feature = "monad"))]
843    fn register_address_abi(&mut self, address: Address, abi: &JsonAbi) {
844        for function in abi.functions() {
845            self.push_address_function(address, function.clone());
846        }
847        for event in abi.events() {
848            self.push_address_event(address, event.clone());
849        }
850    }
851
852    fn is_current_committee_active(&self, address: Address) -> bool {
853        address == CURRENT_COMMITTEE_ADDRESS
854            && self
855                .hardfork
856                .and_then(TempoHardfork::from_foundry_hardfork)
857                .is_some_and(|hardfork| hardfork.is_t8())
858            && precompiles::is_known_precompile(
859                address,
860                self.networks,
861                self.chain_id,
862                self.hardfork,
863            )
864    }
865
866    /// Selects the appropriate function from a list of functions with the same selector by
867    /// checking which one decodes the calldata.
868    ///
869    /// Address-scoped function lookup should happen before this to avoid using ABI metadata from a
870    /// different contract when multiple functions have the same input types.
871    fn select_contract_function<'a>(
872        &self,
873        functions: &'a [Function],
874        trace: &CallTrace,
875    ) -> &'a [Function] {
876        // When there are selector collisions, try to decode the calldata with each function
877        // to determine which one is actually being called. The correct function should
878        // decode successfully while the wrong ones will fail due to parameter type mismatches.
879        if functions.len() > 1 {
880            for (i, func) in functions.iter().enumerate() {
881                if trace.data.len() >= SELECTOR_LEN
882                    && func.abi_decode_input(&trace.data[SELECTOR_LEN..]).is_ok()
883                {
884                    return &functions[i..i + 1];
885                }
886            }
887        }
888        functions
889    }
890
891    /// Adds a single error to the decoder.
892    pub fn push_error(&mut self, error: Error) {
893        self.revert_decoder.push_error(error);
894    }
895
896    pub const fn without_label(&mut self, disable: bool) {
897        self.disable_labels = disable;
898    }
899
900    fn collect_identified_addresses(
901        &mut self,
902        mut addrs: Vec<IdentifiedAddress<'_>>,
903        global: bool,
904    ) {
905        addrs.sort_by_key(|identity| identity.address);
906        addrs.dedup_by_key(|identity| identity.address);
907        if addrs.is_empty() {
908            return;
909        }
910
911        trace!(target: "evm::traces", len=addrs.len(), "collecting address identities");
912        for IdentifiedAddress {
913            address,
914            label,
915            contract,
916            abi,
917            constructor_args_offset,
918            artifact_id: _,
919        } in addrs
920        {
921            let _span = trace_span!(target: "evm::traces", "identity", ?contract, ?label).entered();
922
923            if let Some(contract) = contract {
924                self.contracts.entry(address).or_insert(contract);
925            }
926
927            if let Some(label) = label.filter(|s| !s.is_empty()) {
928                self.labels.entry(address).or_insert(label);
929            }
930
931            if let Some(abi) = abi {
932                self.collect_abi(&abi, Some(address), global);
933            }
934
935            if let Some(offset) = constructor_args_offset {
936                self.constructor_args_offsets.entry(address).or_insert(offset);
937            }
938        }
939    }
940
941    fn collect_abi(&mut self, abi: &JsonAbi, address: Option<Address>, global: bool) {
942        let len = abi.len();
943        if len == 0 {
944            return;
945        }
946        trace!(target: "evm::traces", len, ?address, "collecting ABI");
947        for function in abi.functions() {
948            if let Some(address) = address {
949                self.push_address_function(address, function.clone());
950            }
951            if global {
952                self.push_function(function.clone());
953            }
954        }
955        if let Some(address) = address
956            && let Some(constructor) = abi.constructor()
957        {
958            self.constructors_by_address.entry(address).or_insert_with(|| constructor.clone());
959        }
960        if global {
961            for event in abi.events() {
962                self.push_event(event.clone());
963            }
964            for error in abi.errors() {
965                self.push_error(error.clone());
966            }
967        }
968        if let Some(address) = address {
969            if abi.receive.is_some() {
970                self.receive_contracts.insert(address);
971            }
972
973            if abi.fallback.is_some() {
974                self.fallback_contracts
975                    .insert(address, abi.functions().map(|f| f.selector()).collect());
976            } else {
977                self.non_fallback_contracts
978                    .insert(address, abi.functions().map(|f| f.selector()).collect());
979            }
980        }
981    }
982
983    /// Populates the traces with decoded data by mutating the
984    /// [CallTrace] in place. See [CallTraceDecoder::decode_function] and
985    /// [CallTraceDecoder::decode_event] for more details.
986    pub async fn populate_traces(&self, traces: &mut Vec<CallTraceNode>) {
987        for node in traces {
988            if !self.opcodes.is_empty() {
989                for step in &mut node.trace.steps {
990                    if step.decoded.is_some() {
991                        continue;
992                    }
993                    for opcode in &self.opcodes {
994                        if step.op == *opcode {
995                            let res = match &step.storage_change {
996                                Some(change) if step.op == OpCode::SSTORE => {
997                                    if let Some(had_value) = change.had_value {
998                                        format!(
999                                            "[{}] {} 0x{:x}: 0x{:x} → 0x{:x}",
1000                                            step.gas_cost,
1001                                            opcode,
1002                                            change.key,
1003                                            had_value,
1004                                            change.value
1005                                        )
1006                                    } else {
1007                                        format!(
1008                                            "[{}] {} 0x{:x} → (0x{:x})",
1009                                            step.gas_cost, opcode, change.key, change.value
1010                                        )
1011                                    }
1012                                }
1013                                Some(change) => format!(
1014                                    "[{}] {} 0x{:x} → (0x{:x})",
1015                                    step.gas_cost, opcode, change.key, change.value
1016                                ),
1017                                None => format!("[{}] {}", step.gas_cost, opcode),
1018                            };
1019
1020                            step.decoded = Some(Box::new(DecodedTraceStep::Line(res)));
1021                            break;
1022                        }
1023                    }
1024                }
1025            }
1026
1027            node.trace.decoded = Some(Box::new(self.decode_function(&node.trace).await));
1028            for log in &mut node.logs {
1029                log.decoded =
1030                    Some(Box::new(self.decode_event_with_address(log.address, &log.raw_log).await));
1031            }
1032
1033            if let Some(debug) = self.debug_identifier.as_ref()
1034                && let Some(identified) = self.contracts.get(&node.trace.address)
1035            {
1036                debug.identify_node_steps(node, get_contract_name(identified))
1037            }
1038        }
1039    }
1040
1041    /// Decodes a call trace.
1042    pub async fn decode_function(&self, trace: &CallTrace) -> DecodedCallTrace {
1043        let label = if self.disable_labels {
1044            None
1045        } else if let Some(label) = self.labels.get(&trace.address) {
1046            Some(label.clone())
1047        } else if self.is_current_committee_active(trace.address) {
1048            Some("CurrentCommittee".to_string())
1049        } else {
1050            None
1051        };
1052
1053        if trace.kind.is_any_create() {
1054            return DecodedCallTrace {
1055                label,
1056                call_data: self.decode_constructor_input(trace),
1057                return_data: None,
1058            };
1059        }
1060
1061        if let Some(trace) = precompiles::decode(trace, self.networks, self.chain_id, self.hardfork)
1062        {
1063            return trace;
1064        }
1065
1066        let cdata = &trace.data;
1067        if trace.address == DEFAULT_CREATE2_DEPLOYER {
1068            return DecodedCallTrace {
1069                label,
1070                call_data: Some(DecodedCallData { signature: "create2".to_string(), args: vec![] }),
1071                return_data: self.default_return_data(trace).await,
1072            };
1073        }
1074
1075        if is_abi_call_data(cdata) {
1076            let selector = Selector::try_from(&cdata[..SELECTOR_LEN]).unwrap();
1077            let mut identified_functions = Vec::new();
1078            let functions = match self.functions_for_selector(trace.address, &selector) {
1079                Some(functions) => functions,
1080                None => {
1081                    if let Some(identifier) = &self.signature_identifier
1082                        && let Some(function) = identifier.identify_function(selector).await
1083                    {
1084                        identified_functions.push(function);
1085                    }
1086                    &identified_functions
1087                }
1088            };
1089
1090            // Check if unsupported fn selector: calldata dooes NOT point to one of its selectors +
1091            // non-fallback contract + no receive
1092            if let Some(contract_selectors) = self.non_fallback_contracts.get(&trace.address)
1093                && !contract_selectors.contains(&selector)
1094                && (!cdata.is_empty() || !self.receive_contracts.contains(&trace.address))
1095            {
1096                let return_data = if trace.success {
1097                    None
1098                } else {
1099                    let revert_msg =
1100                        self.decode_revert_at(trace.address, &trace.output, trace.status).await;
1101
1102                    if trace.output.is_empty() || revert_msg.contains("EvmError: Revert") {
1103                        Some(format!(
1104                            "unrecognized function selector {} for contract {}, which has no fallback function.",
1105                            selector, trace.address
1106                        ))
1107                    } else {
1108                        Some(revert_msg)
1109                    }
1110                };
1111
1112                return if let Some(func) = functions.first() {
1113                    DecodedCallTrace {
1114                        label,
1115                        call_data: Some(self.decode_function_input(trace, func)),
1116                        return_data,
1117                    }
1118                } else {
1119                    DecodedCallTrace {
1120                        label,
1121                        call_data: self.fallback_call_data(trace),
1122                        return_data,
1123                    }
1124                };
1125            }
1126
1127            let contract_functions = self.select_contract_function(functions, trace);
1128            let [func, ..] = contract_functions else {
1129                return DecodedCallTrace {
1130                    label,
1131                    call_data: self.fallback_call_data(trace),
1132                    return_data: self.default_return_data(trace).await,
1133                };
1134            };
1135
1136            // If traced contract is a fallback contract, check if it has the decoded function.
1137            // If not, then replace call data signature with `fallback`.
1138            let mut call_data = self.decode_function_input(trace, func);
1139            if let Some(fallback_functions) = self.fallback_contracts.get(&trace.address)
1140                && !fallback_functions.contains(&selector)
1141                && let Some(cd) = self.fallback_call_data(trace)
1142            {
1143                call_data.signature = cd.signature;
1144            }
1145
1146            DecodedCallTrace {
1147                label,
1148                call_data: Some(call_data),
1149                return_data: self.decode_function_output(trace, contract_functions).await,
1150            }
1151        } else {
1152            // Calls to the cheatcode address with a known selector must not fall back to raw
1153            // calldata rendering: even malformed calldata can contain sensitive inputs like
1154            // private keys that the custom cheatcode decoding redacts.
1155            if trace.address == CHEATCODE_ADDRESS
1156                && let Some(selector) = cdata.first_chunk().map(Selector::from)
1157                && let Some([func, ..]) = self.functions_for_selector(trace.address, &selector)
1158            {
1159                return DecodedCallTrace {
1160                    label,
1161                    call_data: Some(self.decode_function_input(trace, func)),
1162                    return_data: self.default_return_data(trace).await,
1163                };
1164            }
1165            DecodedCallTrace {
1166                label,
1167                call_data: self.fallback_call_data(trace),
1168                return_data: self.default_return_data(trace).await,
1169            }
1170        }
1171    }
1172
1173    /// Decodes a function's input into the given trace.
1174    fn decode_function_input(&self, trace: &CallTrace, func: &Function) -> DecodedCallData {
1175        let mut args = None;
1176        if trace.data.len() >= SELECTOR_LEN {
1177            if trace.address == CHEATCODE_ADDRESS {
1178                // Try to decode cheatcode inputs in a more custom way
1179                if let Some(v) = self.decode_cheatcode_inputs(func, &trace.data) {
1180                    args = Some(v);
1181                }
1182            }
1183
1184            if args.is_none()
1185                && let Ok(decoded) = func.abi_decode_input(&trace.data[SELECTOR_LEN..])
1186            {
1187                args = Some(
1188                    decoded
1189                        .iter()
1190                        .zip(&func.inputs)
1191                        .map(|(value, input)| {
1192                            self.format_param_value(
1193                                Some(trace.address),
1194                                &func.name,
1195                                &input.name,
1196                                &input.ty,
1197                                value,
1198                            )
1199                        })
1200                        .collect(),
1201                );
1202            }
1203        }
1204
1205        DecodedCallData { signature: func.signature(), args: args.unwrap_or_default() }
1206    }
1207
1208    /// Decodes constructor input from a creation trace.
1209    fn decode_constructor_input(&self, trace: &CallTrace) -> Option<DecodedCallData> {
1210        let constructor = self.constructors_by_address.get(&trace.address)?;
1211        let offset = *self.constructor_args_offsets.get(&trace.address)?;
1212        let data = trace.data.get(offset..)?;
1213        let decoded = constructor.abi_decode_input(data).ok()?;
1214        let args = decoded
1215            .iter()
1216            .zip(&constructor.inputs)
1217            .map(|(value, input)| {
1218                self.format_param_value(
1219                    Some(trace.address),
1220                    "constructor",
1221                    &input.name,
1222                    &input.ty,
1223                    value,
1224                )
1225            })
1226            .collect();
1227
1228        Some(DecodedCallData { signature: constructor_signature(constructor), args })
1229    }
1230
1231    /// Custom decoding for cheatcode inputs.
1232    fn decode_cheatcode_inputs(&self, func: &Function, data: &[u8]) -> Option<Vec<String>> {
1233        match func.name.as_str() {
1234            "expectRevert" => {
1235                let decoded = match data.get(SELECTOR_LEN..) {
1236                    Some(data) => func.abi_decode_input(data).ok(),
1237                    None => None,
1238                };
1239                let Some(decoded) = decoded else {
1240                    return Some(vec![self.revert_decoder.decode(data, None)]);
1241                };
1242                let Some(first) = decoded.first() else {
1243                    return Some(vec![self.revert_decoder.decode(data, None)]);
1244                };
1245                let expected_revert = match first {
1246                    DynSolValue::Bytes(bytes) => bytes.as_slice(),
1247                    DynSolValue::FixedBytes(word, size) => &word[..*size],
1248                    _ => return None,
1249                };
1250                Some(
1251                    std::iter::once(self.revert_decoder.decode(expected_revert, None))
1252                        .chain(decoded.iter().skip(1).map(|value| self.format_value(value)))
1253                        .collect(),
1254                )
1255            }
1256            "addr" | "createEd25519Key" | "createWallet" | "deriveKey" | "publicKeyEd25519" |
1257            "publicKeyP256" | "rememberKey" => {
1258                // Redact private key in all cases
1259                Some(vec!["<pk>".to_string()])
1260            }
1261            "broadcast" | "startBroadcast" => {
1262                // Redact private key if defined
1263                // broadcast(uint256) / startBroadcast(uint256)
1264                (!func.inputs.is_empty() && func.inputs[0].ty == "uint256").then(|| vec!["<pk>".to_string()])
1265            }
1266            "getNonce" => {
1267                // Redact private key if defined
1268                // getNonce(Wallet)
1269                (!func.inputs.is_empty() && func.inputs[0].ty == "tuple").then(|| vec!["<pk>".to_string()])
1270            }
1271            "sign" | "signCompact" | "signP256" | "signKeychain" | "signKeychainAdmin" => {
1272                // Fail closed: when the arguments of a key-taking cheatcode cannot be
1273                // decoded, redact everything instead of deferring to generic rendering.
1274                let Ok(mut decoded) = func.abi_decode_input(&data[SELECTOR_LEN..]) else {
1275                    return Some(vec!["<redacted>".to_string()]);
1276                };
1277
1278                // Redact private key and replace in trace when the first parameter is a raw
1279                // private key (uint256) or a Wallet struct (tuple); digest-only and
1280                // signer-address overloads carry no key material and are left as-is.
1281                if !decoded.is_empty() &&
1282                    (func.inputs[0].ty == "uint256" || func.inputs[0].ty == "tuple")
1283                {
1284                    decoded[0] = DynSolValue::String("<pk>".to_string());
1285                }
1286
1287                Some(decoded.iter().map(|value| self.format_value(value)).collect())
1288            }
1289            "signWithNonceUnsafe" => {
1290                let Ok(mut decoded) = func.abi_decode_input(&data[SELECTOR_LEN..]) else {
1291                    return Some(vec!["<redacted>".to_string()]);
1292                };
1293                // Redact private key and nonce and replace in trace for
1294                // signWithNonceUnsafe(uint256 privateKey, bytes32 digest, uint256 nonce).
1295                // The nonce is the raw ECDSA k value: together with the digest and the
1296                // returned signature it allows recovering the private key.
1297                decoded[0] = DynSolValue::String("<pk>".to_string());
1298                decoded[2] = DynSolValue::String("<nonce>".to_string());
1299                Some(decoded.iter().map(|value| self.format_value(value)).collect())
1300            }
1301            "signEd25519" => {
1302                let Ok(mut decoded) = func.abi_decode_input(&data[SELECTOR_LEN..]) else {
1303                    return Some(vec!["<redacted>".to_string()]);
1304                };
1305                // Redact private key and replace in trace for
1306                // signEd25519(bytes namespace, bytes message, bytes32 privateKey)
1307                decoded[2] = DynSolValue::String("<pk>".to_string());
1308                Some(decoded.iter().map(|value| self.format_value(value)).collect())
1309            }
1310            "signDelegation" | "signAndAttachDelegation" => {
1311                let Ok(mut decoded) = func.abi_decode_input(&data[SELECTOR_LEN..]) else {
1312                    return Some(vec!["<redacted>".to_string()]);
1313                };
1314                // Redact private key and replace in trace for
1315                // signAndAttachDelegation(address implementation, uint256 privateKey)
1316                // signDelegation(address implementation, uint256 privateKey)
1317                decoded[1] = DynSolValue::String("<pk>".to_string());
1318                Some(decoded.iter().map(format_token).collect())
1319            }
1320            "parseJson" |
1321            "parseJsonUint" |
1322            "parseJsonUintArray" |
1323            "parseJsonInt" |
1324            "parseJsonIntArray" |
1325            "parseJsonString" |
1326            "parseJsonStringArray" |
1327            "parseJsonAddress" |
1328            "parseJsonAddressArray" |
1329            "parseJsonBool" |
1330            "parseJsonBoolArray" |
1331            "parseJsonBytes" |
1332            "parseJsonBytesArray" |
1333            "parseJsonBytes32" |
1334            "parseJsonBytes32Array" |
1335            "writeJson" |
1336            // `keyExists` is being deprecated in favor of `keyExistsJson`. It will be removed in future versions.
1337            "keyExists" |
1338            "keyExistsJson" |
1339            "serializeBool" |
1340            "serializeUint" |
1341            "serializeUintToHex" |
1342            "serializeInt" |
1343            "serializeAddress" |
1344            "serializeBytes32" |
1345            "serializeString" |
1346            "serializeBytes" => {
1347                if self.verbosity >= 5 {
1348                    None
1349                } else {
1350                    let mut decoded = func.abi_decode_input(&data[SELECTOR_LEN..]).ok()?;
1351                    let token = if func.name.as_str() == "parseJson" ||
1352                        // `keyExists` is being deprecated in favor of `keyExistsJson`. It will be removed in future versions.
1353                        func.name.as_str() == "keyExists" ||
1354                        func.name.as_str() == "keyExistsJson"
1355                    {
1356                        "<JSON file>"
1357                    } else {
1358                        "<stringified JSON>"
1359                    };
1360                    decoded[0] = DynSolValue::String(token.to_string());
1361                    Some(decoded.iter().map(format_token).collect())
1362                }
1363            }
1364            s if s.contains("Toml") => {
1365                if self.verbosity >= 5 {
1366                    None
1367                } else {
1368                    let mut decoded = func.abi_decode_input(&data[SELECTOR_LEN..]).ok()?;
1369                    let token = if func.name.as_str() == "parseToml" ||
1370                        func.name.as_str() == "keyExistsToml"
1371                    {
1372                        "<TOML file>"
1373                    } else {
1374                        "<stringified TOML>"
1375                    };
1376                    decoded[0] = DynSolValue::String(token.to_string());
1377                    Some(decoded.iter().map(format_token).collect())
1378                }
1379            }
1380            "createFork" |
1381            "createSelectFork" |
1382            "rpc" => {
1383                let mut decoded = func.abi_decode_input(&data[SELECTOR_LEN..]).ok()?;
1384
1385                // Redact RPC URL except if referenced by an alias
1386                if !decoded.is_empty() && func.inputs[0].ty == "string" {
1387                    let url_or_alias = decoded[0].as_str().unwrap_or_default();
1388
1389                    if url_or_alias.starts_with("http") || url_or_alias.starts_with("ws") {
1390                        decoded[0] = DynSolValue::String("<rpc url>".to_string());
1391                    }
1392                } else {
1393                    return None;
1394                }
1395
1396                Some(decoded.iter().map(format_token).collect())
1397            }
1398            _ => None,
1399        }
1400    }
1401
1402    /// Decodes a function's output into the given trace.
1403    async fn decode_function_output(
1404        &self,
1405        trace: &CallTrace,
1406        funcs: &[Function],
1407    ) -> Option<String> {
1408        if !trace.success {
1409            return self.default_return_data(trace).await;
1410        }
1411
1412        if trace.address == CHEATCODE_ADDRESS
1413            && let Some(decoded) = funcs.iter().find_map(|func| self.decode_cheatcode_outputs(func))
1414        {
1415            return Some(decoded);
1416        }
1417
1418        if let Some(values) =
1419            funcs.iter().find_map(|func| func.abi_decode_output(&trace.output).ok())
1420        {
1421            // Functions coming from an external database do not have any outputs specified,
1422            // and will lead to returning an empty list of values.
1423            if values.is_empty() {
1424                return None;
1425            }
1426
1427            return Some(
1428                values.iter().map(|value| self.format_value(value)).format(", ").to_string(),
1429            );
1430        }
1431
1432        None
1433    }
1434
1435    /// Custom decoding for cheatcode outputs.
1436    fn decode_cheatcode_outputs(&self, func: &Function) -> Option<String> {
1437        match func.name.as_str() {
1438            s if s.starts_with("env") => Some("<env var value>"),
1439            "createEd25519Key" | "createWallet" | "deriveKey" => Some("<pk>"),
1440            "promptSecret" | "promptSecretUint" => Some("<secret>"),
1441            "parseJson" if self.verbosity < 5 => Some("<encoded JSON value>"),
1442            "readFile" if self.verbosity < 5 => Some("<file>"),
1443            "rpcUrl" | "rpcUrls" | "rpcUrlStructs" => Some("<rpc url>"),
1444            _ => None,
1445        }
1446        .map(Into::into)
1447    }
1448
1449    #[track_caller]
1450    fn fallback_call_data(&self, trace: &CallTrace) -> Option<DecodedCallData> {
1451        let cdata = &trace.data;
1452        let signature = if cdata.is_empty() && self.receive_contracts.contains(&trace.address) {
1453            "receive()"
1454        } else if self.fallback_contracts.contains_key(&trace.address) {
1455            "fallback()"
1456        } else {
1457            return None;
1458        }
1459        .to_string();
1460        let args = if cdata.is_empty() { Vec::new() } else { vec![cdata.to_string()] };
1461        Some(DecodedCallData { signature, args })
1462    }
1463
1464    /// The default decoded return data for a trace.
1465    async fn default_return_data(&self, trace: &CallTrace) -> Option<String> {
1466        // For calls with status None or successful status, don't decode revert data
1467        // This is due to trace.status is derived from the revm_interpreter::InstructionResult in
1468        // revm-inspectors status will `None` post revm 27, as `InstructionResult::Continue` does
1469        // not exists anymore.
1470        if trace.status.is_none_or(|s| s.is_ok()) || trace.success {
1471            return None;
1472        }
1473        Some(self.decode_revert_at(trace.address, &trace.output, trace.status).await)
1474    }
1475
1476    /// Decodes revert data into a human-readable representation.
1477    ///
1478    /// If the revert decoder does not know the custom error, tries identifying the error selector
1479    /// with the signatures identifier, like unknown function and event signatures. This resolves
1480    /// errors from the local signatures cache populated by `forge build`, or from remote signature
1481    /// databases.
1482    async fn decode_revert(&self, output: &[u8], status: Option<InstructionResult>) -> String {
1483        if let Some(reason) = self.revert_decoder.maybe_decode_known(output) {
1484            return reason;
1485        }
1486        if let Some(identifier) = &self.signature_identifier
1487            && let Some((selector, data)) = output.split_first_chunk::<SELECTOR_LEN>()
1488            && let Some(error) = identifier.identify_error(Selector::from(*selector)).await
1489            && let Ok(decoded) = error.abi_decode_input(data)
1490        {
1491            return format!("{}({})", error.name, decoded.iter().map(format_token).format(", "));
1492        }
1493        self.revert_decoder.decode(output, status)
1494    }
1495
1496    async fn decode_revert_at(
1497        &self,
1498        address: Address,
1499        output: &[u8],
1500        status: Option<InstructionResult>,
1501    ) -> String {
1502        #[cfg(feature = "base")]
1503        if let Some(reason) =
1504            self.base_revert_decoder(address).and_then(|decoder| decoder.maybe_decode_known(output))
1505        {
1506            return reason;
1507        }
1508        if self.is_current_committee_active(address) {
1509            static DECODER: OnceLock<RevertDecoder> = OnceLock::new();
1510            let decoder = DECODER
1511                .get_or_init(|| RevertDecoder::new().with_abi(&ICurrentCommittee::abi::contract()));
1512            if let Some(reason) = decoder.maybe_decode_known(output) {
1513                return reason;
1514            }
1515        }
1516        self.decode_revert(output, status).await
1517    }
1518
1519    /// Decodes an event.
1520    pub async fn decode_event(&self, log: &LogData) -> DecodedCallLog {
1521        self.decode_event_inner(None, log, false).await.into_call_log()
1522    }
1523
1524    /// Decodes an event emitted by a known address.
1525    pub async fn decode_event_with_address(
1526        &self,
1527        address: Address,
1528        log: &LogData,
1529    ) -> DecodedCallLog {
1530        self.decode_event_inner(Some(address), log, false).await.into_call_log()
1531    }
1532
1533    /// Decodes an event emitted by a known address, using its canonical signature as the name.
1534    pub async fn decode_event_with_address_signature(
1535        &self,
1536        address: Address,
1537        log: &LogData,
1538    ) -> DecodedEvent {
1539        self.decode_event_inner(Some(address), log, true).await
1540    }
1541
1542    async fn decode_event_inner(
1543        &self,
1544        address: Option<Address>,
1545        log: &LogData,
1546        canonical_signature: bool,
1547    ) -> DecodedEvent {
1548        let key = log.topics().first().map(|&topic| (topic, log.topics().len() - 1));
1549        let events = address.and_then(|address| self.events_by_address.as_deref()?.get(&address));
1550        let address_events = key.and_then(|(topic, count)| events?.get(&(Some(topic), count)));
1551        let global_events = key.and_then(|key| self.events.get(&key));
1552        #[cfg(feature = "base")]
1553        let base_events = key.and_then(|key| self.base_events()?.get(&key));
1554        #[cfg(not(feature = "base"))]
1555        let base_events: Option<&Vec<Event>> = None;
1556        let fallback_events = global_events.or(base_events);
1557        let regular_events = address_events.or(fallback_events);
1558        let anonymous_events = events.and_then(|events| events.get(&(None, log.topics().len())));
1559
1560        let decoded = if canonical_signature && address_events.is_none() {
1561            // Topic zero does not encode indexed parameter placement, so global metadata is only
1562            // a hint. Try every compatible layout and reject ambiguity.
1563            let mut events = fallback_events
1564                .into_iter()
1565                .flatten()
1566                .flat_map(|event| indexed_event_candidates(event.clone(), log))
1567                .collect::<Vec<_>>();
1568            events.extend(anonymous_events.into_iter().flatten().cloned());
1569            self.decode_unique_event_candidates(address, log, &events, true)
1570        } else if canonical_signature || anonymous_events.is_some() {
1571            self.decode_unique_event_candidates(
1572                address,
1573                log,
1574                regular_events.into_iter().flatten().chain(anonymous_events.into_iter().flatten()),
1575                canonical_signature,
1576            )
1577        } else {
1578            self.decode_event_candidates(address, log, regular_events.into_iter().flatten(), false)
1579        };
1580        if let Some(decoded) = decoded {
1581            return decoded;
1582        }
1583
1584        if regular_events.is_some() || anonymous_events.is_some() {
1585            return DecodedEvent::default();
1586        }
1587
1588        if let Some(&topic) = log.topics().first()
1589            && let Some(identifier) = &self.signature_identifier
1590            && let Some(event) = identifier.identify_event(topic).await
1591        {
1592            if !canonical_signature {
1593                let event = get_indexed_event(event, log);
1594                return self
1595                    .decode_event_candidates(address, log, [&event], false)
1596                    .unwrap_or_default();
1597            }
1598            let mut decoded = indexed_event_candidates(event, log)
1599                .filter_map(|event| self.decode_event_candidates(address, log, [&event], true));
1600            let event = decoded.next();
1601            if event.is_some() && decoded.next().is_some() {
1602                return DecodedEvent::default();
1603            }
1604            if let Some(decoded) = event {
1605                return decoded;
1606            }
1607        }
1608
1609        DecodedEvent::default()
1610    }
1611
1612    fn decode_event_candidates<'a>(
1613        &self,
1614        address: Option<Address>,
1615        log: &LogData,
1616        events: impl IntoIterator<Item = &'a Event>,
1617        canonical_signature: bool,
1618    ) -> Option<DecodedEvent> {
1619        for event in events {
1620            if let Ok(decoded) = event.decode_log(log) {
1621                let params = reconstruct_params(event, &decoded);
1622                let params = params
1623                    .into_iter()
1624                    .zip(event.inputs.iter())
1625                    .map(|(param, input)| {
1626                        // Undo patched names.
1627                        let name = input.name.clone();
1628                        let display = self.format_param_value(
1629                            address,
1630                            &event.name,
1631                            &input.name,
1632                            &input.ty,
1633                            &param,
1634                        );
1635                        (name, display, param)
1636                    })
1637                    .collect();
1638                return Some(DecodedEvent {
1639                    name: Some(if canonical_signature {
1640                        event.signature()
1641                    } else {
1642                        event.name.clone()
1643                    }),
1644                    params: Some(params),
1645                });
1646            }
1647        }
1648        None
1649    }
1650
1651    fn decode_unique_event_candidates<'a>(
1652        &self,
1653        address: Option<Address>,
1654        log: &LogData,
1655        events: impl IntoIterator<Item = &'a Event>,
1656        canonical_signature: bool,
1657    ) -> Option<DecodedEvent> {
1658        let mut decoded = events.into_iter().filter_map(|event| {
1659            self.decode_event_candidates(address, log, [event], canonical_signature)
1660        });
1661        let event = decoded.next()?;
1662        decoded.next().is_none().then_some(event)
1663    }
1664
1665    /// Prefetches function and event signatures into the identifier cache
1666    pub async fn prefetch_signatures(&self, nodes: &[CallTraceNode]) {
1667        let Some(identifier) = &self.signature_identifier else { return };
1668        let events = nodes
1669            .iter()
1670            .flat_map(|node| {
1671                node.logs
1672                    .iter()
1673                    .map(|log| log.raw_log.topics())
1674                    .filter(|&topics| {
1675                        if let Some(&first) = topics.first()
1676                            && self.events.contains_key(&(first, topics.len() - 1))
1677                        {
1678                            return false;
1679                        }
1680                        true
1681                    })
1682                    .filter_map(|topics| topics.first())
1683            })
1684            .copied();
1685        let functions = nodes
1686            .iter()
1687            .filter(|&n| {
1688                // Ignore known addresses.
1689                if n.trace.address == DEFAULT_CREATE2_DEPLOYER
1690                    || n.is_precompile()
1691                    || precompiles::is_known_precompile_call(
1692                        &n.trace,
1693                        self.networks,
1694                        self.chain_id,
1695                        self.hardfork,
1696                    )
1697                {
1698                    return false;
1699                }
1700                // Ignore non-ABI calldata.
1701                if n.trace.kind.is_any_create() || !is_abi_call_data(&n.trace.data) {
1702                    return false;
1703                }
1704                true
1705            })
1706            .filter_map(|n| n.trace.data.first_chunk().map(Selector::from))
1707            .filter(|selector| !self.functions.contains_key(selector));
1708        let errors = nodes
1709            .iter()
1710            .filter(|&n| {
1711                // Only consider reverted traces whose output the revert decoder cannot decode.
1712                n.trace.status.is_some_and(|s| !s.is_ok())
1713                    && !n.trace.success
1714                    && self.revert_decoder.maybe_decode_known(&n.trace.output).is_none()
1715            })
1716            .filter_map(|n| n.trace.output.first_chunk().map(Selector::from));
1717        let selectors = events
1718            .map(SelectorKind::Event)
1719            .chain(functions.map(SelectorKind::Function))
1720            .chain(errors.map(SelectorKind::Error))
1721            .unique()
1722            .collect::<Vec<_>>();
1723        let _ = identifier.identify(&selectors).await;
1724    }
1725
1726    /// Pretty-prints a value.
1727    fn format_value(&self, value: &DynSolValue) -> String {
1728        if let DynSolValue::Address(addr) = value
1729            && !self.disable_labels
1730            && let Some(label) = self.labels.get(addr)
1731        {
1732            if self.compact_labels {
1733                return label.clone();
1734            }
1735            return format!("{label}: [{addr}]");
1736        }
1737        format_token(value)
1738    }
1739
1740    fn format_param_value(
1741        &self,
1742        address: Option<Address>,
1743        context_name: &str,
1744        input_name: &str,
1745        input_ty: &str,
1746        value: &DynSolValue,
1747    ) -> String {
1748        self.format_claim_receipt_bytes(address, context_name, input_name, input_ty, value)
1749            .map(|value| self.format_value(&value))
1750            .unwrap_or_else(|| self.format_value(value))
1751    }
1752
1753    fn format_claim_receipt_bytes(
1754        &self,
1755        address: Option<Address>,
1756        context_name: &str,
1757        input_name: &str,
1758        input_ty: &str,
1759        value: &DynSolValue,
1760    ) -> Option<DynSolValue> {
1761        if !matches!(address, Some(RECEIVE_POLICY_GUARD_ADDRESS | TIP403_REGISTRY_ADDRESS)) {
1762            return None;
1763        }
1764        if input_name != "receipt" || input_ty != "bytes" {
1765            return None;
1766        }
1767        if !matches!(context_name, "balanceOf" | "claim" | "burnBlockedReceipt" | "TransferBlocked")
1768        {
1769            return None;
1770        }
1771        let DynSolValue::Bytes(bytes) = value else { return None };
1772        let decoded = IReceivePolicyGuard::ClaimReceiptV1::abi_decode(bytes).ok()?;
1773
1774        Some(DynSolValue::CustomStruct {
1775            name: "ClaimReceiptV1".to_string(),
1776            prop_names: vec![
1777                "version".to_string(),
1778                "token".to_string(),
1779                "recoveryAuthority".to_string(),
1780                "originator".to_string(),
1781                "recipient".to_string(),
1782                "blockedAt".to_string(),
1783                "blockedNonce".to_string(),
1784                "blockedReason".to_string(),
1785                "kind".to_string(),
1786                "memo".to_string(),
1787            ],
1788            tuple: vec![
1789                DynSolValue::Uint(U256::from(decoded.version), 8),
1790                DynSolValue::Address(decoded.token),
1791                DynSolValue::Address(decoded.recoveryAuthority),
1792                DynSolValue::Address(decoded.originator),
1793                DynSolValue::Address(decoded.recipient),
1794                DynSolValue::Uint(U256::from(decoded.blockedAt), 64),
1795                DynSolValue::Uint(U256::from(decoded.blockedNonce), 64),
1796                DynSolValue::Uint(U256::from(decoded.blockedReason), 8),
1797                DynSolValue::Uint(U256::from(decoded.kind as u8), 8),
1798                DynSolValue::FixedBytes(decoded.memo, 32),
1799            ],
1800        })
1801    }
1802}
1803
1804/// Returns `true` if the given function calldata (including function selector) is ABI-encoded.
1805///
1806/// This is a simple heuristic to avoid fetching non ABI-encoded selectors.
1807fn is_abi_call_data(data: &[u8]) -> bool {
1808    match data.len().cmp(&SELECTOR_LEN) {
1809        std::cmp::Ordering::Less => false,
1810        std::cmp::Ordering::Equal => true,
1811        std::cmp::Ordering::Greater => is_abi_data(&data[SELECTOR_LEN..]),
1812    }
1813}
1814
1815/// Returns `true` if the given data is ABI-encoded.
1816///
1817/// See [`is_abi_call_data`] for more details.
1818fn is_abi_data(data: &[u8]) -> bool {
1819    let rem = data.len() % 32;
1820    if rem == 0 || data.is_empty() {
1821        return true;
1822    }
1823    // If the length is not a multiple of 32, also accept when the last remainder bytes are all 0.
1824    data[data.len() - rem..].iter().all(|byte| *byte == 0)
1825}
1826
1827/// Restore the order of the params of a decoded event,
1828/// as Alloy returns the indexed and unindexed params separately.
1829fn reconstruct_params(event: &Event, decoded: &AlloyDecodedEvent) -> Vec<DynSolValue> {
1830    let mut indexed = 0;
1831    let mut unindexed = 0;
1832    let mut inputs = vec![];
1833    for input in &event.inputs {
1834        // Prevent panic of event `Transfer(from, to)` decoded with a signature
1835        // `Transfer(address indexed from, address indexed to, uint256 indexed tokenId)` by making
1836        // sure the event inputs is not higher than decoded indexed / un-indexed values.
1837        if input.indexed && indexed < decoded.indexed.len() {
1838            inputs.push(decoded.indexed[indexed].clone());
1839            indexed += 1;
1840        } else if unindexed < decoded.body.len() {
1841            inputs.push(decoded.body[unindexed].clone());
1842            unindexed += 1;
1843        }
1844    }
1845
1846    inputs
1847}
1848
1849fn indexed_inputs(event: &Event) -> usize {
1850    event.inputs.iter().filter(|param| param.indexed).count()
1851}
1852
1853fn indexed_event_candidates(mut event: Event, log: &LogData) -> impl Iterator<Item = Event> {
1854    for (index, input) in event.inputs.iter_mut().enumerate() {
1855        if input.name.is_empty() {
1856            input.name = format!("param{index}");
1857        }
1858        input.indexed = false;
1859    }
1860    (0..event.inputs.len()).combinations(log.topics().len() - 1).map(move |indexed| {
1861        let mut event = event.clone();
1862        for index in indexed {
1863            event.inputs[index].indexed = true;
1864        }
1865        event
1866    })
1867}
1868
1869fn constructor_signature(constructor: &Constructor) -> String {
1870    format!(
1871        "constructor({})",
1872        constructor.inputs.iter().map(|input| input.selector_type()).format(",")
1873    )
1874}
1875
1876#[cfg(test)]
1877mod tests {
1878    use super::*;
1879    use alloy_primitives::{address, aliases::U96, hex};
1880    use alloy_sol_types::{SolCall, SolError, SolEvent};
1881    use foundry_evm_core::precompiles::P256_VERIFY;
1882    use std::borrow::Cow;
1883
1884    #[cfg(feature = "base")]
1885    use foundry_evm_hardforks::BaseUpgrade;
1886    #[cfg(feature = "base")]
1887    use foundry_evm_networks::BASE_PRECOMPILE_ADDRESSES;
1888
1889    #[cfg(feature = "monad")]
1890    fn function_abi_items(functions: impl IntoIterator<Item = Function>) -> Vec<(String, String)> {
1891        let mut items = functions
1892            .into_iter()
1893            .map(|function| (function.selector().to_string(), function.signature()))
1894            .collect::<Vec<_>>();
1895        items.sort();
1896        items
1897    }
1898
1899    #[cfg(feature = "monad")]
1900    fn event_abi_items(events: impl IntoIterator<Item = Event>) -> Vec<(String, usize, String)> {
1901        let mut items = events
1902            .into_iter()
1903            .map(|event| (event.selector().to_string(), indexed_inputs(&event), event.signature()))
1904            .collect::<Vec<_>>();
1905        items.sort();
1906        items
1907    }
1908
1909    #[cfg(feature = "monad")]
1910    fn typed_call_abi_item<C: SolCall>() -> (String, String) {
1911        (Selector::from(C::SELECTOR).to_string(), C::SIGNATURE.to_string())
1912    }
1913
1914    #[cfg(feature = "monad")]
1915    fn typed_event_abi_item<E: SolEvent>() -> (String, usize, String) {
1916        let signature_topics = usize::from(!E::ANONYMOUS);
1917        let indexed_inputs = <E::TopicList as alloy_sol_types::TopicList>::COUNT - signature_topics;
1918        (E::SIGNATURE_HASH.to_string(), indexed_inputs, E::SIGNATURE.to_string())
1919    }
1920
1921    #[cfg(feature = "monad")]
1922    fn monad_decoder(hardfork: MonadHardfork) -> CallTraceDecoder {
1923        CallTraceDecoderBuilder::new()
1924            .with_execution_network(NetworkVariant::Monad)
1925            .with_chain_id(Some(143))
1926            .with_hardfork(Some(hardfork.into()))
1927            .build()
1928    }
1929
1930    #[test]
1931    #[cfg(feature = "monad")]
1932    fn test_monad_decoder_abis_match_monad_revm() {
1933        use monad_revm::{
1934            reserve_balance::interface::IReserveBalance as RevmReserveBalance,
1935            staking::interface::IMonadStaking as RevmMonadStaking,
1936        };
1937
1938        let local_staking_functions = monad::IMonadStaking::abi::functions()
1939            .into_values()
1940            .chain(monad::IMonadStakingSyscalls::abi::functions().into_values())
1941            .flatten();
1942        let mut revm_staking_functions = vec![
1943            typed_call_abi_item::<RevmMonadStaking::addValidatorCall>(),
1944            typed_call_abi_item::<RevmMonadStaking::delegateCall>(),
1945            typed_call_abi_item::<RevmMonadStaking::undelegateCall>(),
1946            typed_call_abi_item::<RevmMonadStaking::withdrawCall>(),
1947            typed_call_abi_item::<RevmMonadStaking::compoundCall>(),
1948            typed_call_abi_item::<RevmMonadStaking::claimRewardsCall>(),
1949            typed_call_abi_item::<RevmMonadStaking::changeCommissionCall>(),
1950            typed_call_abi_item::<RevmMonadStaking::externalRewardCall>(),
1951            typed_call_abi_item::<RevmMonadStaking::getEpochCall>(),
1952            typed_call_abi_item::<RevmMonadStaking::getProposerValIdCall>(),
1953            typed_call_abi_item::<RevmMonadStaking::getValidatorCall>(),
1954            typed_call_abi_item::<RevmMonadStaking::getDelegatorCall>(),
1955            typed_call_abi_item::<RevmMonadStaking::getWithdrawalRequestCall>(),
1956            typed_call_abi_item::<RevmMonadStaking::getConsensusValidatorSetCall>(),
1957            typed_call_abi_item::<RevmMonadStaking::getSnapshotValidatorSetCall>(),
1958            typed_call_abi_item::<RevmMonadStaking::getExecutionValidatorSetCall>(),
1959            typed_call_abi_item::<RevmMonadStaking::getDelegationsCall>(),
1960            typed_call_abi_item::<RevmMonadStaking::getDelegatorsCall>(),
1961            typed_call_abi_item::<RevmMonadStaking::syscallOnEpochChangeCall>(),
1962            typed_call_abi_item::<RevmMonadStaking::syscallRewardCall>(),
1963            typed_call_abi_item::<RevmMonadStaking::syscallSnapshotCall>(),
1964        ];
1965        revm_staking_functions.sort();
1966        assert_eq!(
1967            function_abi_items(local_staking_functions),
1968            revm_staking_functions,
1969            "Monad staking function selectors drifted from monad_revm",
1970        );
1971
1972        let mut revm_staking_events = vec![
1973            typed_event_abi_item::<RevmMonadStaking::ClaimRewards>(),
1974            typed_event_abi_item::<RevmMonadStaking::CommissionChanged>(),
1975            typed_event_abi_item::<RevmMonadStaking::Delegate>(),
1976            typed_event_abi_item::<RevmMonadStaking::EpochChanged>(),
1977            typed_event_abi_item::<RevmMonadStaking::Undelegate>(),
1978            typed_event_abi_item::<RevmMonadStaking::ValidatorCreated>(),
1979            typed_event_abi_item::<RevmMonadStaking::ValidatorRewarded>(),
1980            typed_event_abi_item::<RevmMonadStaking::ValidatorStatusChanged>(),
1981            typed_event_abi_item::<RevmMonadStaking::Withdraw>(),
1982        ];
1983        revm_staking_events.sort();
1984        assert_eq!(
1985            event_abi_items(monad::IMonadStaking::abi::events().into_values().flatten()),
1986            revm_staking_events,
1987            "Monad staking event selectors drifted from monad_revm",
1988        );
1989
1990        assert_eq!(
1991            function_abi_items(monad::IReserveBalance::abi::functions().into_values().flatten()),
1992            vec![typed_call_abi_item::<RevmReserveBalance::dippedIntoReserveCall>()],
1993            "Monad reserve-balance function selectors drifted from monad_revm",
1994        );
1995
1996        assert_eq!(
1997            event_abi_items(monad::IReserveBalance::abi::events().into_values().flatten()),
1998            Vec::<(String, usize, String)>::new(),
1999            "Monad reserve-balance event selectors drifted from monad_revm",
2000        );
2001    }
2002
2003    #[test]
2004    fn test_selector_collision_resolution() {
2005        use alloy_json_abi::Function;
2006        use alloy_primitives::Address;
2007
2008        // Create two functions with the same selector but different signatures
2009        let func1 = Function::parse("transferFrom(address,address,uint256)").unwrap();
2010        let func2 = Function::parse("gasprice_bit_ether(int128)").unwrap();
2011
2012        // Verify they have the same selector (this is the collision)
2013        assert_eq!(func1.selector(), func2.selector());
2014
2015        let functions = vec![func1, func2];
2016
2017        // Create a mock trace with calldata that matches func1
2018        let trace = CallTrace {
2019            address: Address::from([0x12; 20]),
2020            data: hex!("23b872dd000000000000000000000000000000000000000000000000000000000000012300000000000000000000000000000000000000000000000000000000000004560000000000000000000000000000000000000000000000000000000000000064").to_vec().into(),
2021            ..Default::default()
2022        };
2023
2024        let decoder = CallTraceDecoder::new();
2025        let result = decoder.select_contract_function(&functions, &trace);
2026
2027        // Should return only the function that can decode the calldata (func1)
2028        assert_eq!(result.len(), 1);
2029        assert_eq!(result[0].signature(), "transferFrom(address,address,uint256)");
2030    }
2031
2032    #[test]
2033    fn test_selector_collision_resolution_second_function() {
2034        use alloy_json_abi::Function;
2035        use alloy_primitives::Address;
2036
2037        // Create two functions with the same selector but different signatures
2038        let func1 = Function::parse("transferFrom(address,address,uint256)").unwrap();
2039        let func2 = Function::parse("gasprice_bit_ether(int128)").unwrap();
2040
2041        let functions = vec![func1, func2];
2042
2043        // Create a mock trace with calldata that matches func2
2044        let trace = CallTrace {
2045            address: Address::from([0x12; 20]),
2046            data: hex!("23b872dd0000000000000000000000000000000000000000000000000000000000000064")
2047                .to_vec()
2048                .into(),
2049            ..Default::default()
2050        };
2051
2052        let decoder = CallTraceDecoder::new();
2053        let result = decoder.select_contract_function(&functions, &trace);
2054
2055        // Should return only the function that can decode the calldata (func2)
2056        assert_eq!(result.len(), 1);
2057        assert_eq!(result[0].signature(), "gasprice_bit_ether(int128)");
2058    }
2059
2060    #[cfg(feature = "base")]
2061    #[tokio::test]
2062    async fn base_fallback_does_not_shadow_ethereum_abi() {
2063        let abi = JsonAbi::parse(["function pause(uint8[] features) returns (bool)"]).unwrap();
2064        let function = abi.functions().next().unwrap();
2065        let trace = CallTrace {
2066            address: Address::repeat_byte(0x12),
2067            data: function
2068                .abi_encode_input(&[DynSolValue::Array(vec![DynSolValue::Uint(U256::ZERO, 8)])])
2069                .unwrap()
2070                .into(),
2071            output: function.abi_encode_output(&[DynSolValue::Bool(true)]).unwrap().into(),
2072            success: true,
2073            ..Default::default()
2074        };
2075
2076        let ethereum = CallTraceDecoderBuilder::new()
2077            .with_execution_network(NetworkVariant::Ethereum)
2078            .with_abi(&abi)
2079            .build();
2080        let decoded = ethereum.decode_function(&trace).await;
2081        assert_eq!(decoded.call_data.unwrap().signature, "pause(uint8[])");
2082        assert_eq!(decoded.return_data.as_deref(), Some("true"));
2083
2084        let base =
2085            CallTraceDecoderBuilder::new().with_execution_network(NetworkVariant::Base).build();
2086        let decoded = base.decode_function(&trace).await;
2087        assert_eq!(decoded.call_data.unwrap().signature, "pause(uint8[])");
2088        assert_eq!(decoded.return_data, None);
2089    }
2090
2091    #[tokio::test]
2092    async fn identified_event_does_not_shadow_builtin_metadata() {
2093        let address = Address::from([0x12; 20]);
2094        let mut decoder = CallTraceDecoder::new().clone();
2095        let abi = JsonAbi::parse(["event log_string(string)"]).unwrap();
2096        decoder.collect_abi(&abi, Some(address), true);
2097
2098        let event = Event::parse("event log_string(string val)").unwrap();
2099        let log = LogData::new_unchecked(
2100            vec![event.selector()],
2101            ("script ran".to_string(),).abi_encode().into(),
2102        );
2103        let decoded = decoder.decode_event_with_address(address, &log).await;
2104
2105        assert_eq!(decoded.name.as_deref(), Some("log_string"));
2106        assert_eq!(decoded.params.unwrap()[0].0, "val");
2107    }
2108
2109    #[tokio::test]
2110    async fn address_scoped_abis_decode_all_registered_events() {
2111        let address = Address::from([0x12; 20]);
2112        let proxy = JsonAbi::parse(["event ProxyEvent()"]).unwrap();
2113        let implementation = JsonAbi::parse(["event ImplementationEvent()"]).unwrap();
2114        let decoder = CallTraceDecoderBuilder::new()
2115            .with_address_events(address, &implementation)
2116            .with_address_events(address, &proxy)
2117            .build();
2118
2119        for event in proxy.events().chain(implementation.events()) {
2120            let log = LogData::new_unchecked(vec![event.selector()], Default::default());
2121            let decoded = decoder.decode_event_with_address(address, &log).await;
2122            assert_eq!(decoded.name.as_deref(), Some(event.name.as_str()));
2123            assert!(decoder.decode_event_with_address(Address::ZERO, &log).await.name.is_none());
2124        }
2125    }
2126
2127    #[tokio::test]
2128    async fn clearing_addresses_removes_regular_and_anonymous_events() {
2129        let address = Address::from([0x12; 20]);
2130        let abi = JsonAbi::parse([
2131            "event ScopedValue(uint256 value)",
2132            "event AnonymousValue(uint256 value) anonymous",
2133        ])
2134        .unwrap();
2135        let mut decoder = CallTraceDecoderBuilder::new()
2136            .with_address_events(address, &abi)
2137            .with_address_events(address, &abi)
2138            .build();
2139        let global = Event::parse("event GlobalValue()").unwrap();
2140        let global_log = LogData::new_unchecked(vec![global.selector()], Default::default());
2141        decoder.push_event(global);
2142
2143        for event in abi.events() {
2144            let topics = if event.anonymous { vec![] } else { vec![event.selector()] };
2145            let log = LogData::new_unchecked(topics, (U256::from(7),).abi_encode().into());
2146            assert_eq!(
2147                decoder.decode_event_with_address(address, &log).await.name.as_deref(),
2148                Some(event.name.as_str())
2149            );
2150
2151            let mut cleared = decoder.clone();
2152            cleared.clear_addresses();
2153            assert!(cleared.decode_event_with_address(address, &log).await.name.is_none());
2154            assert_eq!(
2155                cleared.decode_event(&global_log).await.name.as_deref(),
2156                Some("GlobalValue")
2157            );
2158        }
2159    }
2160
2161    #[tokio::test]
2162    async fn canonical_address_events_require_a_unique_match() {
2163        let address = Address::from([0x12; 20]);
2164        let implementation =
2165            JsonAbi::parse(["event Value(address indexed who, uint256 amount)"]).unwrap();
2166        let proxy = JsonAbi::parse(["event Value(address who, uint256 indexed amount)"]).unwrap();
2167        let event = proxy.events().next().unwrap();
2168        let log = LogData::new_unchecked(
2169            vec![event.selector(), U256::from(42).into()],
2170            (Address::from([0x34; 20]),).abi_encode().into(),
2171        );
2172        let decoder = CallTraceDecoderBuilder::new()
2173            .with_address_events(address, &implementation)
2174            .with_address_events(address, &proxy)
2175            .build();
2176
2177        let decoded = decoder.decode_event_with_address_signature(address, &log).await;
2178        assert!(decoded.name.is_none());
2179        assert!(decoded.params.is_none());
2180    }
2181
2182    #[tokio::test]
2183    async fn canonical_global_events_require_a_unique_indexed_placement() {
2184        let known = Event::parse(
2185            "event AmbiguousLayout(address indexed from, address indexed to, uint256 amount)",
2186        )
2187        .unwrap();
2188        let emitted = Event::parse(
2189            "event AmbiguousLayout(address from, address indexed to, uint256 indexed amount)",
2190        )
2191        .unwrap();
2192        assert_eq!(known.selector(), emitted.selector());
2193
2194        let address = Address::from([0x12; 20]);
2195        let from = Address::from([0x34; 20]);
2196        let to = Address::from([0x56; 20]);
2197        let amount = U256::from(42);
2198        let log = LogData::new_unchecked(
2199            vec![emitted.selector(), to.into_word(), amount.into()],
2200            (from,).abi_encode().into(),
2201        );
2202        let mut decoder = CallTraceDecoder::new().clone();
2203        decoder.push_event(known);
2204
2205        let decoded = decoder.decode_event_with_address_signature(address, &log).await;
2206        assert!(decoded.name.is_none());
2207        assert!(decoded.params.is_none());
2208
2209        decoder.push_address_event(address, emitted);
2210        let decoded = decoder.decode_event_with_address_signature(address, &log).await;
2211        assert_eq!(decoded.name.as_deref(), Some("AmbiguousLayout(address,address,uint256)"));
2212        let params = decoded.params.unwrap();
2213        assert_eq!(params[0].2, DynSolValue::Address(from));
2214        assert_eq!(params[1].2, DynSolValue::Address(to));
2215        assert_eq!(params[2].2, DynSolValue::Uint(amount, 256));
2216    }
2217
2218    #[tokio::test]
2219    async fn identified_events_preserve_legacy_indexed_placement_for_traces() {
2220        let event = Event::parse(
2221            "event DecoderAmbiguousIndexedPlacement(address indexed owner, uint256 id)",
2222        )
2223        .unwrap();
2224        let mut abi = JsonAbi::default();
2225        abi.events.insert(event.name.clone(), vec![event.clone()]);
2226        let log = LogData::new_unchecked(
2227            vec![event.selector(), U256::from(42).into()],
2228            (Address::from([0x34; 20]),).abi_encode().into(),
2229        );
2230        let identifier = SignaturesIdentifier::new_offline_with_abis([&abi]).unwrap();
2231        let decoder = CallTraceDecoderBuilder::new().with_signature_identifier(identifier).build();
2232
2233        let decoded = decoder.decode_event(&log).await;
2234        assert_eq!(decoded.name.as_deref(), Some("DecoderAmbiguousIndexedPlacement"));
2235
2236        let decoded = decoder.decode_event_with_address_signature(Address::ZERO, &log).await;
2237        assert!(decoded.name.is_none());
2238        assert!(decoded.params.is_none());
2239    }
2240
2241    #[tokio::test]
2242    async fn address_scoped_anonymous_events_require_a_unique_match() {
2243        let address = Address::from([0x12; 20]);
2244        let abi = JsonAbi::parse(["event AnonymousValue(uint256 value) anonymous"]).unwrap();
2245        let log = LogData::new_unchecked(Vec::new(), (U256::from(7),).abi_encode().into());
2246        let decoded = CallTraceDecoderBuilder::new()
2247            .with_address_events(address, &abi)
2248            .build()
2249            .decode_event_with_address(address, &log)
2250            .await;
2251        assert_eq!(decoded.name.as_deref(), Some("AnonymousValue"));
2252
2253        let abi = JsonAbi::parse([
2254            "event AnonymousValue(uint256 value) anonymous",
2255            "event AnonymousAmount(uint256 amount) anonymous",
2256        ])
2257        .unwrap();
2258        let decoder = CallTraceDecoderBuilder::new().with_address_events(address, &abi).build();
2259
2260        let decoded = decoder.decode_event_with_address(address, &log).await;
2261
2262        assert!(decoded.name.is_none());
2263        assert!(decoded.params.is_none());
2264
2265        let abi = JsonAbi::parse([
2266            "event RegularValue(uint256 value)",
2267            "event AnonymousValue(uint256 indexed value) anonymous",
2268        ])
2269        .unwrap();
2270        let regular = abi.events().find(|event| !event.anonymous).unwrap();
2271        let log = LogData::new_unchecked(vec![regular.selector()], Default::default());
2272        let decoded = CallTraceDecoderBuilder::new()
2273            .with_address_events(address, &abi)
2274            .build()
2275            .decode_event_with_address_signature(address, &log)
2276            .await;
2277        assert_eq!(decoded.name.as_deref(), Some("AnonymousValue(uint256)"));
2278
2279        let abi = JsonAbi::parse([
2280            "event RegularValue()",
2281            "event AnonymousValue(bytes32 indexed value) anonymous",
2282        ])
2283        .unwrap();
2284        let regular = abi.events().find(|event| !event.anonymous).unwrap();
2285        let log = LogData::new_unchecked(vec![regular.selector()], Default::default());
2286        let decoder = CallTraceDecoderBuilder::new().with_address_events(address, &abi).build();
2287        let decoded = decoder.decode_event_with_address_signature(address, &log).await;
2288        assert!(decoded.name.is_none());
2289        assert!(decoded.params.is_none());
2290        let decoded = decoder.decode_event_with_address(address, &log).await;
2291        assert!(decoded.name.is_none());
2292        assert!(decoded.params.is_none());
2293    }
2294
2295    #[test]
2296    fn compact_labels_hide_address_in_trace_parameters() {
2297        let address = address!("0x0000000000000000000000000000000000000001");
2298        let value = DynSolValue::Address(address);
2299        let tracing = TracingConfig {
2300            labels: AddressHashMap::from_iter([(address, "Alice".to_string())]),
2301            ..Default::default()
2302        };
2303        let decoder = CallTraceDecoderBuilder::new().with_tracing_config(&tracing).build();
2304        assert_eq!(decoder.format_value(&value), format!("Alice: [{address}]"));
2305
2306        let tracing = TracingConfig { compact_labels: true, ..tracing };
2307        let decoder = CallTraceDecoderBuilder::new().with_tracing_config(&tracing).build();
2308        assert_eq!(decoder.format_value(&value), "Alice");
2309
2310        let tracing = TracingConfig { disable_labels: true, ..tracing };
2311        let decoder = CallTraceDecoderBuilder::new().with_tracing_config(&tracing).build();
2312        assert_eq!(decoder.format_value(&value), address.to_string());
2313    }
2314
2315    #[test]
2316    fn configured_labels_survive_address_reset() {
2317        let configured = address!("0x0000000000000000000000000000000000000100");
2318        let discovered = address!("0x0000000000000000000000000000000000000200");
2319        let tracing = TracingConfig {
2320            labels: AddressHashMap::from_iter([(configured, "configured".to_string())]),
2321            ..Default::default()
2322        };
2323        let mut decoder = CallTraceDecoderBuilder::new().with_tracing_config(&tracing).build();
2324        decoder.labels.insert(discovered, "discovered".to_string());
2325
2326        decoder.clear_addresses();
2327
2328        assert_eq!(decoder.labels.get(&configured).map(String::as_str), Some("configured"));
2329        assert!(!decoder.labels.contains_key(&discovered));
2330    }
2331
2332    #[tokio::test]
2333    async fn test_decode_constructor_input() {
2334        let address = Address::repeat_byte(0x12);
2335        let constructor = Constructor::parse("constructor(uint256 amount, address owner)").unwrap();
2336        let args = constructor
2337            .abi_encode_input(&[
2338                DynSolValue::Uint(U256::from(42), 256),
2339                DynSolValue::Address(Address::repeat_byte(0x34)),
2340            ])
2341            .unwrap();
2342        let mut data = vec![0x60, 0x80, 0x60, 0x40];
2343        let offset = data.len();
2344        data.extend_from_slice(&args);
2345        let trace = CallTrace {
2346            address,
2347            kind: revm_inspectors::tracing::types::CallKind::Create,
2348            data: data.into(),
2349            ..Default::default()
2350        };
2351        let mut decoder = CallTraceDecoder::new().clone();
2352        decoder.constructors_by_address.insert(address, constructor);
2353        decoder.constructor_args_offsets.insert(address, offset);
2354
2355        let decoded = decoder.decode_function(&trace).await;
2356        let call_data = decoded.call_data.expect("constructor input should decode");
2357
2358        assert_eq!(call_data.signature, "constructor(uint256,address)");
2359        assert_eq!(call_data.args[0], "42");
2360        assert_eq!(call_data.args[1], format!("{}", Address::repeat_byte(0x34)));
2361    }
2362
2363    #[test]
2364    fn test_should_redact() {
2365        let mut decoder = CallTraceDecoder::new().clone();
2366        decoder.labels.insert(Address::from([0x22; 20]), "signer".to_string());
2367
2368        let expected_revert_bytes4 = vec![0xde, 0xad, 0xbe, 0xef];
2369        let expect_revert_bytes4_data = Function::parse("expectRevert(bytes4)")
2370            .unwrap()
2371            .abi_encode_input(&[DynSolValue::FixedBytes(
2372                B256::right_padding_from(expected_revert_bytes4.as_slice()),
2373                4,
2374            )])
2375            .unwrap();
2376
2377        let expected_revert_bytes = hex!(
2378            "08c379a000000000000000000000000000000000000000000000000000000000\
2379             0000002000000000000000000000000000000000000000000000000000000000\
2380             00000004626f6f6d000000000000000000000000000000000000000000000000"
2381        )
2382        .to_vec();
2383        let expect_revert_bytes_data = Function::parse("expectRevert(bytes)")
2384            .unwrap()
2385            .abi_encode_input(&[DynSolValue::Bytes(expected_revert_bytes.clone())])
2386            .unwrap();
2387
2388        let reverter = Address::from([0x11; 20]);
2389        let expect_revert_bytes4_address_data = Function::parse("expectRevert(bytes4,address)")
2390            .unwrap()
2391            .abi_encode_input(&[
2392                DynSolValue::FixedBytes(
2393                    B256::right_padding_from(expected_revert_bytes4.as_slice()),
2394                    4,
2395                ),
2396                DynSolValue::Address(reverter),
2397            ])
2398            .unwrap();
2399
2400        let count = 42_u64;
2401        let expect_revert_bytes_count_data = Function::parse("expectRevert(bytes,uint64)")
2402            .unwrap()
2403            .abi_encode_input(&[
2404                DynSolValue::Bytes(expected_revert_bytes.clone()),
2405                DynSolValue::Uint(alloy_primitives::U256::from(count), 64),
2406            ])
2407            .unwrap();
2408
2409        let expect_revert_bytes_address_count_data =
2410            Function::parse("expectRevert(bytes,address,uint64)")
2411                .unwrap()
2412                .abi_encode_input(&[
2413                    DynSolValue::Bytes(expected_revert_bytes.clone()),
2414                    DynSolValue::Address(reverter),
2415                    DynSolValue::Uint(alloy_primitives::U256::from(count), 64),
2416                ])
2417                .unwrap();
2418
2419        let expect_revert_runtime_data = expected_revert_bytes4.clone();
2420
2421        // [function_signature, data, expected]
2422        let cheatcode_input_test_cases = vec![
2423            // Should decode the expected revert payload, not full cheatcode calldata:
2424            (
2425                "expectRevert(bytes4)",
2426                expect_revert_bytes4_data,
2427                Some(vec![decoder.revert_decoder.decode(expected_revert_bytes4.as_slice(), None)]),
2428            ),
2429            (
2430                "expectRevert(bytes)",
2431                expect_revert_bytes_data,
2432                Some(vec![decoder.revert_decoder.decode(expected_revert_bytes.as_slice(), None)]),
2433            ),
2434            (
2435                "expectRevert(bytes4)",
2436                expect_revert_runtime_data.clone(),
2437                Some(vec![
2438                    decoder.revert_decoder.decode(expect_revert_runtime_data.as_slice(), None),
2439                ]),
2440            ),
2441            (
2442                "expectRevert(bytes4,address)",
2443                expect_revert_bytes4_address_data,
2444                Some(vec![
2445                    decoder.revert_decoder.decode(expected_revert_bytes4.as_slice(), None),
2446                    decoder.format_value(&DynSolValue::Address(reverter)),
2447                ]),
2448            ),
2449            (
2450                "expectRevert(bytes,uint64)",
2451                expect_revert_bytes_count_data,
2452                Some(vec![
2453                    decoder.revert_decoder.decode(expected_revert_bytes.as_slice(), None),
2454                    decoder
2455                        .format_value(&DynSolValue::Uint(alloy_primitives::U256::from(count), 64)),
2456                ]),
2457            ),
2458            (
2459                "expectRevert(bytes,address,uint64)",
2460                expect_revert_bytes_address_count_data,
2461                Some(vec![
2462                    decoder.revert_decoder.decode(expected_revert_bytes.as_slice(), None),
2463                    decoder.format_value(&DynSolValue::Address(reverter)),
2464                    decoder
2465                        .format_value(&DynSolValue::Uint(alloy_primitives::U256::from(count), 64)),
2466                ]),
2467            ),
2468            (
2469                "expectRevert()",
2470                expect_revert_runtime_data.clone(),
2471                Some(vec![
2472                    decoder.revert_decoder.decode(expect_revert_runtime_data.as_slice(), None),
2473                ]),
2474            ),
2475            // Should redact private key from traces in all cases:
2476            ("addr(uint256)", vec![], Some(vec!["<pk>".to_string()])),
2477            ("createEd25519Key(bytes32)", vec![], Some(vec!["<pk>".to_string()])),
2478            ("createWallet(string)", vec![], Some(vec!["<pk>".to_string()])),
2479            ("createWallet(uint256)", vec![], Some(vec!["<pk>".to_string()])),
2480            ("deriveKey(string,uint32)", vec![], Some(vec!["<pk>".to_string()])),
2481            ("deriveKey(string,string,uint32)", vec![], Some(vec!["<pk>".to_string()])),
2482            ("deriveKey(string,uint32,string)", vec![], Some(vec!["<pk>".to_string()])),
2483            ("deriveKey(string,string,uint32,string)", vec![], Some(vec!["<pk>".to_string()])),
2484            ("publicKeyEd25519(bytes32)", vec![], Some(vec!["<pk>".to_string()])),
2485            ("publicKeyP256(uint256)", vec![], Some(vec!["<pk>".to_string()])),
2486            ("rememberKey(uint256)", vec![], Some(vec!["<pk>".to_string()])),
2487            //
2488            // Should redact private key from traces in specific cases with exceptions:
2489            ("broadcast(uint256)", vec![], Some(vec!["<pk>".to_string()])),
2490            ("broadcast()", vec![], None), // Ignore: `private key` is not passed.
2491            ("startBroadcast(uint256)", vec![], Some(vec!["<pk>".to_string()])),
2492            ("startBroadcast()", vec![], None), // Ignore: `private key` is not passed.
2493            ("getNonce((address,uint256,uint256,uint256))", vec![], Some(vec!["<pk>".to_string()])),
2494            ("getNonce(address)", vec![], None), // Ignore: `address` is public.
2495            //
2496            // Should redact private key and replace in trace in cases:
2497            (
2498                "sign(uint256,bytes32)",
2499                hex!(
2500                    "
2501                    e341eaa4
2502                    7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6
2503                    0000000000000000000000000000000000000000000000000000000000000000
2504                "
2505                )
2506                .to_vec(),
2507                Some(vec![
2508                    "\"<pk>\"".to_string(),
2509                    "0x0000000000000000000000000000000000000000000000000000000000000000"
2510                        .to_string(),
2511                ]),
2512            ),
2513            (
2514                "signP256(uint256,bytes32)",
2515                hex!(
2516                    "
2517                    83211b40
2518                    7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6
2519                    0000000000000000000000000000000000000000000000000000000000000000
2520                "
2521                )
2522                .to_vec(),
2523                Some(vec![
2524                    "\"<pk>\"".to_string(),
2525                    "0x0000000000000000000000000000000000000000000000000000000000000000"
2526                        .to_string(),
2527                ]),
2528            ),
2529            (
2530                "signCompact(uint256,bytes32)",
2531                hex!(
2532                    "
2533                    cc2a781f
2534                    7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6
2535                    0000000000000000000000000000000000000000000000000000000000000000
2536                "
2537                )
2538                .to_vec(),
2539                Some(vec![
2540                    "\"<pk>\"".to_string(),
2541                    "0x0000000000000000000000000000000000000000000000000000000000000000"
2542                        .to_string(),
2543                ]),
2544            ),
2545            (
2546                // signCompact(Wallet,bytes32)
2547                "signCompact((address,uint256,uint256,uint256),bytes32)",
2548                hex!(
2549                    "
2550                    3d0e292f
2551                    0000000000000000000000001111111111111111111111111111111111111111
2552                    0000000000000000000000000000000000000000000000000000000000000001
2553                    0000000000000000000000000000000000000000000000000000000000000002
2554                    7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6
2555                    0000000000000000000000000000000000000000000000000000000000000000
2556                "
2557                )
2558                .to_vec(),
2559                Some(vec![
2560                    "\"<pk>\"".to_string(),
2561                    "0x0000000000000000000000000000000000000000000000000000000000000000"
2562                        .to_string(),
2563                ]),
2564            ),
2565            (
2566                // Ignore: `private key` is not passed, digest is signed by the script signer.
2567                "signCompact(bytes32)",
2568                hex!(
2569                    "
2570                    a282dc4b
2571                    0000000000000000000000000000000000000000000000000000000000000000
2572                "
2573                )
2574                .to_vec(),
2575                Some(vec![
2576                    "0x0000000000000000000000000000000000000000000000000000000000000000"
2577                        .to_string(),
2578                ]),
2579            ),
2580            (
2581                // Ignore: `signer` is a public address.
2582                "signCompact(address,bytes32)",
2583                hex!(
2584                    "
2585                    8e2f97bf
2586                    0000000000000000000000001111111111111111111111111111111111111111
2587                    0000000000000000000000000000000000000000000000000000000000000000
2588                "
2589                )
2590                .to_vec(),
2591                Some(vec![
2592                    "0x1111111111111111111111111111111111111111".to_string(),
2593                    "0x0000000000000000000000000000000000000000000000000000000000000000"
2594                        .to_string(),
2595                ]),
2596            ),
2597            (
2598                "signWithNonceUnsafe(uint256,bytes32,uint256)",
2599                hex!(
2600                    "
2601                    2012783a
2602                    7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6
2603                    0000000000000000000000000000000000000000000000000000000000000000
2604                    0000000000000000000000000000000000000000000000000000000000000001
2605                "
2606                )
2607                .to_vec(),
2608                Some(vec![
2609                    "\"<pk>\"".to_string(),
2610                    "0x0000000000000000000000000000000000000000000000000000000000000000"
2611                        .to_string(),
2612                    "\"<nonce>\"".to_string(),
2613                ]),
2614            ),
2615            (
2616                "signKeychain(uint256,address,bytes32)",
2617                hex!(
2618                    "
2619                    5804c690
2620                    7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6
2621                    0000000000000000000000001111111111111111111111111111111111111111
2622                    0000000000000000000000000000000000000000000000000000000000000000
2623                "
2624                )
2625                .to_vec(),
2626                Some(vec![
2627                    "\"<pk>\"".to_string(),
2628                    "0x1111111111111111111111111111111111111111".to_string(),
2629                    "0x0000000000000000000000000000000000000000000000000000000000000000"
2630                        .to_string(),
2631                ]),
2632            ),
2633            (
2634                "signKeychainAdmin(uint256,address,bytes32)",
2635                hex!(
2636                    "
2637                    bc5fb2f7
2638                    7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6
2639                    0000000000000000000000001111111111111111111111111111111111111111
2640                    0000000000000000000000000000000000000000000000000000000000000000
2641                "
2642                )
2643                .to_vec(),
2644                Some(vec![
2645                    "\"<pk>\"".to_string(),
2646                    "0x1111111111111111111111111111111111111111".to_string(),
2647                    "0x0000000000000000000000000000000000000000000000000000000000000000"
2648                        .to_string(),
2649                ]),
2650            ),
2651            (
2652                // Labels are applied to address arguments while the key is redacted.
2653                "signKeychain(uint256,address,bytes32)",
2654                hex!(
2655                    "
2656                    5804c690
2657                    7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6
2658                    0000000000000000000000002222222222222222222222222222222222222222
2659                    0000000000000000000000000000000000000000000000000000000000000000
2660                "
2661                )
2662                .to_vec(),
2663                Some(vec![
2664                    "\"<pk>\"".to_string(),
2665                    "signer: [0x2222222222222222222222222222222222222222]".to_string(),
2666                    "0x0000000000000000000000000000000000000000000000000000000000000000"
2667                        .to_string(),
2668                ]),
2669            ),
2670            (
2671                // cast calldata "signEd25519(bytes,bytes,bytes32)" "0x6e73" "0x6d7367"
2672                // 0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6
2673                "signEd25519(bytes,bytes,bytes32)",
2674                hex!(
2675                    "
2676                    ef609c65
2677                    0000000000000000000000000000000000000000000000000000000000000060
2678                    00000000000000000000000000000000000000000000000000000000000000a0
2679                    7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6
2680                    0000000000000000000000000000000000000000000000000000000000000002
2681                    6e73000000000000000000000000000000000000000000000000000000000000
2682                    0000000000000000000000000000000000000000000000000000000000000003
2683                    6d73670000000000000000000000000000000000000000000000000000000000
2684                "
2685                )
2686                .to_vec(),
2687                Some(vec!["0x6e73".to_string(), "0x6d7367".to_string(), "\"<pk>\"".to_string()]),
2688            ),
2689            (
2690                // cast calldata "createFork(string)" "https://eth-mainnet.g.alchemy.com/v2/api_key"
2691                "createFork(string)",
2692                hex!(
2693                    "
2694                    31ba3498
2695                    0000000000000000000000000000000000000000000000000000000000000020
2696                    000000000000000000000000000000000000000000000000000000000000002c
2697                    68747470733a2f2f6574682d6d61696e6e65742e672e616c6368656d792e636f
2698                    6d2f76322f6170695f6b65790000000000000000000000000000000000000000
2699                    "
2700                )
2701                .to_vec(),
2702                Some(vec!["\"<rpc url>\"".to_string()]),
2703            ),
2704            (
2705                // cast calldata "createFork(string)" "wss://eth-mainnet.g.alchemy.com/v2/api_key"
2706                "createFork(string)",
2707                hex!(
2708                    "
2709                    31ba3498
2710                    0000000000000000000000000000000000000000000000000000000000000020
2711                    000000000000000000000000000000000000000000000000000000000000002a
2712                    7773733a2f2f6574682d6d61696e6e65742e672e616c6368656d792e636f6d2f
2713                    76322f6170695f6b657900000000000000000000000000000000000000000000
2714                    "
2715                )
2716                .to_vec(),
2717                Some(vec!["\"<rpc url>\"".to_string()]),
2718            ),
2719            (
2720                // cast calldata "createFork(string)" "mainnet"
2721                "createFork(string)",
2722                hex!(
2723                    "
2724                    31ba3498
2725                    0000000000000000000000000000000000000000000000000000000000000020
2726                    0000000000000000000000000000000000000000000000000000000000000007
2727                    6d61696e6e657400000000000000000000000000000000000000000000000000
2728                    "
2729                )
2730                .to_vec(),
2731                Some(vec!["\"mainnet\"".to_string()]),
2732            ),
2733            (
2734                // cast calldata "createFork(string,uint256)" "https://eth-mainnet.g.alchemy.com/v2/api_key" 1
2735                "createFork(string,uint256)",
2736                hex!(
2737                    "
2738                    6ba3ba2b
2739                    0000000000000000000000000000000000000000000000000000000000000040
2740                    0000000000000000000000000000000000000000000000000000000000000001
2741                    000000000000000000000000000000000000000000000000000000000000002c
2742                    68747470733a2f2f6574682d6d61696e6e65742e672e616c6368656d792e636f
2743                    6d2f76322f6170695f6b65790000000000000000000000000000000000000000
2744                "
2745                )
2746                .to_vec(),
2747                Some(vec!["\"<rpc url>\"".to_string(), "1".to_string()]),
2748            ),
2749            (
2750                // cast calldata "createFork(string,uint256)" "mainnet" 1
2751                "createFork(string,uint256)",
2752                hex!(
2753                    "
2754                    6ba3ba2b
2755                    0000000000000000000000000000000000000000000000000000000000000040
2756                    0000000000000000000000000000000000000000000000000000000000000001
2757                    0000000000000000000000000000000000000000000000000000000000000007
2758                    6d61696e6e657400000000000000000000000000000000000000000000000000
2759                "
2760                )
2761                .to_vec(),
2762                Some(vec!["\"mainnet\"".to_string(), "1".to_string()]),
2763            ),
2764            (
2765                // cast calldata "createFork(string,bytes32)" "https://eth-mainnet.g.alchemy.com/v2/api_key" 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
2766                "createFork(string,bytes32)",
2767                hex!(
2768                    "
2769                    7ca29682
2770                    0000000000000000000000000000000000000000000000000000000000000040
2771                    ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
2772                    000000000000000000000000000000000000000000000000000000000000002c
2773                    68747470733a2f2f6574682d6d61696e6e65742e672e616c6368656d792e636f
2774                    6d2f76322f6170695f6b65790000000000000000000000000000000000000000
2775                "
2776                )
2777                .to_vec(),
2778                Some(vec![
2779                    "\"<rpc url>\"".to_string(),
2780                    "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
2781                        .to_string(),
2782                ]),
2783            ),
2784            (
2785                // cast calldata "createFork(string,bytes32)" "mainnet"
2786                // 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
2787                "createFork(string,bytes32)",
2788                hex!(
2789                    "
2790                    7ca29682
2791                    0000000000000000000000000000000000000000000000000000000000000040
2792                    ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
2793                    0000000000000000000000000000000000000000000000000000000000000007
2794                    6d61696e6e657400000000000000000000000000000000000000000000000000
2795                "
2796                )
2797                .to_vec(),
2798                Some(vec![
2799                    "\"mainnet\"".to_string(),
2800                    "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
2801                        .to_string(),
2802                ]),
2803            ),
2804            (
2805                // cast calldata "createSelectFork(string)" "https://eth-mainnet.g.alchemy.com/v2/api_key"
2806                "createSelectFork(string)",
2807                hex!(
2808                    "
2809                    98680034
2810                    0000000000000000000000000000000000000000000000000000000000000020
2811                    000000000000000000000000000000000000000000000000000000000000002c
2812                    68747470733a2f2f6574682d6d61696e6e65742e672e616c6368656d792e636f
2813                    6d2f76322f6170695f6b65790000000000000000000000000000000000000000
2814                    "
2815                )
2816                .to_vec(),
2817                Some(vec!["\"<rpc url>\"".to_string()]),
2818            ),
2819            (
2820                // cast calldata "createSelectFork(string)" "mainnet"
2821                "createSelectFork(string)",
2822                hex!(
2823                    "
2824                    98680034
2825                    0000000000000000000000000000000000000000000000000000000000000020
2826                    0000000000000000000000000000000000000000000000000000000000000007
2827                    6d61696e6e657400000000000000000000000000000000000000000000000000
2828                    "
2829                )
2830                .to_vec(),
2831                Some(vec!["\"mainnet\"".to_string()]),
2832            ),
2833            (
2834                // cast calldata "createSelectFork(string,uint256)" "https://eth-mainnet.g.alchemy.com/v2/api_key" 1
2835                "createSelectFork(string,uint256)",
2836                hex!(
2837                    "
2838                    71ee464d
2839                    0000000000000000000000000000000000000000000000000000000000000040
2840                    0000000000000000000000000000000000000000000000000000000000000001
2841                    000000000000000000000000000000000000000000000000000000000000002c
2842                    68747470733a2f2f6574682d6d61696e6e65742e672e616c6368656d792e636f
2843                    6d2f76322f6170695f6b65790000000000000000000000000000000000000000
2844                "
2845                )
2846                .to_vec(),
2847                Some(vec!["\"<rpc url>\"".to_string(), "1".to_string()]),
2848            ),
2849            (
2850                // cast calldata "createSelectFork(string,uint256)" "mainnet" 1
2851                "createSelectFork(string,uint256)",
2852                hex!(
2853                    "
2854                    71ee464d
2855                    0000000000000000000000000000000000000000000000000000000000000040
2856                    0000000000000000000000000000000000000000000000000000000000000001
2857                    0000000000000000000000000000000000000000000000000000000000000007
2858                    6d61696e6e657400000000000000000000000000000000000000000000000000
2859                "
2860                )
2861                .to_vec(),
2862                Some(vec!["\"mainnet\"".to_string(), "1".to_string()]),
2863            ),
2864            (
2865                // cast calldata "createSelectFork(string,bytes32)" "https://eth-mainnet.g.alchemy.com/v2/api_key" 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
2866                "createSelectFork(string,bytes32)",
2867                hex!(
2868                    "
2869                    84d52b7a
2870                    0000000000000000000000000000000000000000000000000000000000000040
2871                    ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
2872                    000000000000000000000000000000000000000000000000000000000000002c
2873                    68747470733a2f2f6574682d6d61696e6e65742e672e616c6368656d792e636f
2874                    6d2f76322f6170695f6b65790000000000000000000000000000000000000000
2875                "
2876                )
2877                .to_vec(),
2878                Some(vec![
2879                    "\"<rpc url>\"".to_string(),
2880                    "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
2881                        .to_string(),
2882                ]),
2883            ),
2884            (
2885                // cast calldata "createSelectFork(string,bytes32)" "mainnet"
2886                // 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
2887                "createSelectFork(string,bytes32)",
2888                hex!(
2889                    "
2890                    84d52b7a
2891                    0000000000000000000000000000000000000000000000000000000000000040
2892                    ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
2893                    0000000000000000000000000000000000000000000000000000000000000007
2894                    6d61696e6e657400000000000000000000000000000000000000000000000000
2895                "
2896                )
2897                .to_vec(),
2898                Some(vec![
2899                    "\"mainnet\"".to_string(),
2900                    "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
2901                        .to_string(),
2902                ]),
2903            ),
2904            (
2905                // cast calldata "rpc(string,string,string)" "https://eth-mainnet.g.alchemy.com/v2/api_key" "eth_getBalance" "[\"0x551e7784778ef8e048e495df49f2614f84a4f1dc\",\"0x0\"]"
2906                "rpc(string,string,string)",
2907                hex!(
2908                    "
2909                    0199a220
2910                    0000000000000000000000000000000000000000000000000000000000000060
2911                    00000000000000000000000000000000000000000000000000000000000000c0
2912                    0000000000000000000000000000000000000000000000000000000000000100
2913                    000000000000000000000000000000000000000000000000000000000000002c
2914                    68747470733a2f2f6574682d6d61696e6e65742e672e616c6368656d792e636f
2915                    6d2f76322f6170695f6b65790000000000000000000000000000000000000000
2916                    000000000000000000000000000000000000000000000000000000000000000e
2917                    6574685f67657442616c616e6365000000000000000000000000000000000000
2918                    0000000000000000000000000000000000000000000000000000000000000034
2919                    5b22307835353165373738343737386566386530343865343935646634396632
2920                    363134663834613466316463222c22307830225d000000000000000000000000
2921                "
2922                )
2923                .to_vec(),
2924                Some(vec![
2925                    "\"<rpc url>\"".to_string(),
2926                    "\"eth_getBalance\"".to_string(),
2927                    "\"[\\\"0x551e7784778ef8e048e495df49f2614f84a4f1dc\\\",\\\"0x0\\\"]\""
2928                        .to_string(),
2929                ]),
2930            ),
2931            (
2932                // cast calldata "rpc(string,string,string)" "mainnet" "eth_getBalance"
2933                // "[\"0x551e7784778ef8e048e495df49f2614f84a4f1dc\",\"0x0\"]"
2934                "rpc(string,string,string)",
2935                hex!(
2936                    "
2937                    0199a220
2938                    0000000000000000000000000000000000000000000000000000000000000060
2939                    00000000000000000000000000000000000000000000000000000000000000a0
2940                    00000000000000000000000000000000000000000000000000000000000000e0
2941                    0000000000000000000000000000000000000000000000000000000000000007
2942                    6d61696e6e657400000000000000000000000000000000000000000000000000
2943                    000000000000000000000000000000000000000000000000000000000000000e
2944                    6574685f67657442616c616e6365000000000000000000000000000000000000
2945                    0000000000000000000000000000000000000000000000000000000000000034
2946                    5b22307835353165373738343737386566386530343865343935646634396632
2947                    363134663834613466316463222c22307830225d000000000000000000000000
2948                "
2949                )
2950                .to_vec(),
2951                Some(vec![
2952                    "\"mainnet\"".to_string(),
2953                    "\"eth_getBalance\"".to_string(),
2954                    "\"[\\\"0x551e7784778ef8e048e495df49f2614f84a4f1dc\\\",\\\"0x0\\\"]\""
2955                        .to_string(),
2956                ]),
2957            ),
2958        ];
2959
2960        // [function_signature, expected]
2961        let cheatcode_output_test_cases = vec![
2962            // Should redact private key on output in all cases:
2963            ("createEd25519Key(bytes32)", Some("<pk>".to_string())),
2964            ("createWallet(string)", Some("<pk>".to_string())),
2965            ("deriveKey(string,uint32)", Some("<pk>".to_string())),
2966            // Should redact RPC URL if defined, except if referenced by an alias:
2967            ("rpcUrl(string)", Some("<rpc url>".to_string())),
2968            ("rpcUrls()", Some("<rpc url>".to_string())),
2969            ("rpcUrlStructs()", Some("<rpc url>".to_string())),
2970        ];
2971
2972        for (function_signature, data, expected) in cheatcode_input_test_cases {
2973            let function = Function::parse(function_signature).unwrap();
2974            let result = decoder.decode_cheatcode_inputs(&function, &data);
2975            assert_eq!(result, expected, "Input case failed for: {function_signature}");
2976        }
2977
2978        for (function_signature, expected) in cheatcode_output_test_cases {
2979            let function = Function::parse(function_signature).unwrap();
2980            let result = Some(decoder.decode_cheatcode_outputs(&function).unwrap_or_default());
2981            assert_eq!(result, expected, "Output case failed for: {function_signature}");
2982        }
2983    }
2984
2985    #[tokio::test]
2986    async fn test_should_redact_end_to_end() {
2987        let decoder = CallTraceDecoder::new();
2988        let pk_hex = "7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6";
2989        let pk = hex!("7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6");
2990        let cheatcode_trace = |data: Vec<u8>| CallTrace {
2991            address: CHEATCODE_ADDRESS,
2992            data: data.into(),
2993            success: true,
2994            ..Default::default()
2995        };
2996
2997        // Well-formed sign(uint256,bytes32) resolves through selector lookup and redacts the
2998        // private key.
2999        let call = Vm::sign_1Call { privateKey: U256::from_be_bytes(pk), digest: B256::ZERO };
3000        let decoded = decoder.decode_function(&cheatcode_trace(call.abi_encode())).await;
3001        let call_data = decoded.call_data.expect("sign should decode");
3002        assert_eq!(call_data.signature, "sign(uint256,bytes32)");
3003        assert_eq!(call_data.args[0], "\"<pk>\"");
3004        assert!(!call_data.args.join(",").contains(pk_hex));
3005
3006        // signEd25519 redacts the trailing private key argument.
3007        let call = Vm::signEd25519Call {
3008            namespace: b"ns".to_vec().into(),
3009            message: b"msg".to_vec().into(),
3010            privateKey: B256::from(pk),
3011        };
3012        let decoded = decoder.decode_function(&cheatcode_trace(call.abi_encode())).await;
3013        let call_data = decoded.call_data.expect("signEd25519 should decode");
3014        assert_eq!(call_data.signature, "signEd25519(bytes,bytes,bytes32)");
3015        assert_eq!(call_data.args[2], "\"<pk>\"");
3016        assert!(!call_data.args.join(",").contains(pk_hex));
3017
3018        // Malformed calldata (nonzero trailing byte fails the ABI shape heuristic) must not
3019        // fall back to raw calldata rendering.
3020        let call = Vm::sign_1Call { privateKey: U256::from_be_bytes(pk), digest: B256::ZERO };
3021        let mut data = call.abi_encode();
3022        data.push(0xff);
3023        let decoded = decoder.decode_function(&cheatcode_trace(data)).await;
3024        let call_data = decoded.call_data.expect("malformed calldata must not fall back to raw");
3025        assert_eq!(call_data.signature, "sign(uint256,bytes32)");
3026        assert!(!call_data.args.join(",").contains(pk_hex));
3027
3028        // Truncated calldata cannot be decoded at all and fails closed.
3029        let data = Vm::sign_1Call::SELECTOR.iter().copied().chain(pk).collect::<Vec<u8>>();
3030        let decoded = decoder.decode_function(&cheatcode_trace(data)).await;
3031        let call_data = decoded.call_data.expect("truncated calldata must not fall back to raw");
3032        assert_eq!(call_data.signature, "sign(uint256,bytes32)");
3033        assert_eq!(call_data.args, vec!["<redacted>".to_string()]);
3034    }
3035
3036    #[tokio::test]
3037    async fn test_tempo_decode_preserves_existing_labels() {
3038        let decoder = CallTraceDecoder::new();
3039        let trace = CallTrace { address: PATH_USD_ADDRESS, success: true, ..Default::default() };
3040
3041        let decoded = decoder.decode_function(&trace).await;
3042        assert_eq!(decoded.label.as_deref(), Some("PathUSD"));
3043    }
3044
3045    #[tokio::test]
3046    async fn test_t5_decode_does_not_synthesize_general_target_label() {
3047        let mut decoder = CallTraceDecoder::new().clone();
3048        decoder.chain_id = Some(4217);
3049        let trace = CallTrace {
3050            address: address!("0x0000000000000000000000000000000000000123"),
3051            depth: 0,
3052            success: true,
3053            ..Default::default()
3054        };
3055
3056        let decoded = decoder.decode_function(&trace).await;
3057        assert_eq!(decoded.label, None);
3058    }
3059
3060    #[tokio::test]
3061    async fn test_t5_tip20_logo_uri_calls_and_events_decode() {
3062        let decoder = CallTraceDecoder::new();
3063
3064        let call = ITIP20::setLogoURICall { newLogoURI: "https://example.com/logo.png".into() };
3065        let trace = CallTrace {
3066            address: PATH_USD_ADDRESS,
3067            data: call.abi_encode().into(),
3068            success: true,
3069            ..Default::default()
3070        };
3071        let decoded = decoder.decode_function(&trace).await;
3072        let call_data = decoded.call_data.expect("setLogoURI should decode");
3073        assert_eq!(call_data.signature, "setLogoURI(string)");
3074        assert_eq!(call_data.args, vec!["\"https://example.com/logo.png\"".to_string()]);
3075
3076        let call = ITIP20::logoURICall {};
3077        let trace = CallTrace {
3078            address: PATH_USD_ADDRESS,
3079            data: call.abi_encode().into(),
3080            success: true,
3081            ..Default::default()
3082        };
3083        let decoded = decoder.decode_function(&trace).await;
3084        assert_eq!(decoded.call_data.expect("logoURI should decode").signature, "logoURI()");
3085
3086        let event = ITIP20::LogoURIUpdated {
3087            updater: address!("0x0000000000000000000000000000000000000abc"),
3088            newLogoURI: "ipfs://logo".into(),
3089        };
3090        let decoded = decoder.decode_event(&event.encode_log_data()).await;
3091        assert_eq!(decoded.name.as_deref(), Some("LogoURIUpdated"));
3092        let params = decoded.params.expect("LogoURIUpdated params should decode");
3093        assert_eq!(params[0].0, "updater");
3094        assert!(
3095            params[0].1.to_ascii_lowercase().contains("0000000000000000000000000000000000000abc")
3096        );
3097        assert_eq!(params[1], ("newLogoURI".into(), "\"ipfs://logo\"".into()));
3098    }
3099
3100    #[tokio::test]
3101    async fn test_t5_tip20_factory_create_token_with_logo_decodes() {
3102        let decoder = CallTraceDecoder::new();
3103        let call = ITIP20Factory::createToken_1Call {
3104            name: "Example USD".into(),
3105            symbol: "xUSD".into(),
3106            currency: "USD".into(),
3107            quoteToken: PATH_USD_ADDRESS,
3108            admin: address!("0x0000000000000000000000000000000000000abc"),
3109            salt: B256::repeat_byte(0x11),
3110            logoURI: "https://example.com/xusd.png".into(),
3111        };
3112        let trace = CallTrace {
3113            address: TIP20_FACTORY_ADDRESS,
3114            data: call.abi_encode().into(),
3115            success: true,
3116            ..Default::default()
3117        };
3118        let decoded = decoder.decode_function(&trace).await;
3119        let call_data = decoded.call_data.expect("createToken overload should decode");
3120        assert_eq!(
3121            call_data.signature,
3122            "createToken(string,string,string,address,address,bytes32,string)"
3123        );
3124        assert_eq!(call_data.args[6], "\"https://example.com/xusd.png\"");
3125    }
3126
3127    #[tokio::test]
3128    async fn test_t5_stablecoin_dex_order_flipped_event_decodes() {
3129        let decoder = CallTraceDecoder::new();
3130        let event = IStablecoinDEX::OrderFlipped {
3131            orderId: 42,
3132            maker: address!("0x0000000000000000000000000000000000000abc"),
3133            token: PATH_USD_ADDRESS,
3134            amount: 1_000_000,
3135            isBid: false,
3136            tick: 100,
3137            flipTick: 100,
3138        };
3139        let decoded = decoder.decode_event(&event.encode_log_data()).await;
3140        assert_eq!(decoded.name.as_deref(), Some("OrderFlipped"));
3141        let params = decoded.params.expect("OrderFlipped params should decode");
3142        assert_eq!(params[0], ("orderId".into(), "42".into()));
3143        assert_eq!(params[4], ("isBid".into(), "false".into()));
3144        assert_eq!(params[5], ("tick".into(), "100".into()));
3145        assert_eq!(params[6], ("flipTick".into(), "100".into()));
3146    }
3147
3148    #[tokio::test]
3149    async fn test_t5_channel_reserve_call_and_event_decode() {
3150        let mut decoder = CallTraceDecoder::new().clone();
3151        decoder.chain_id = Some(4217);
3152
3153        let open = ITIP20ChannelReserve::openCall {
3154            payee: address!("0x0000000000000000000000000000000000000abc"),
3155            operator: Address::ZERO,
3156            token: PATH_USD_ADDRESS,
3157            deposit: U96::from(1_000_000u64),
3158            salt: B256::repeat_byte(0x22),
3159            authorizedSigner: Address::ZERO,
3160        };
3161        let trace = CallTrace {
3162            address: TIP20_CHANNEL_RESERVE_ADDRESS,
3163            data: open.abi_encode().into(),
3164            depth: 0,
3165            success: true,
3166            ..Default::default()
3167        };
3168        let decoded = decoder.decode_function(&trace).await;
3169        assert_eq!(decoded.label.as_deref(), Some("TIP20ChannelReserve"));
3170        assert_eq!(
3171            decoded.call_data.expect("open should decode").signature,
3172            "open(address,address,address,uint96,bytes32,address)"
3173        );
3174
3175        let transfer = ITIP20::transferCall {
3176            to: address!("0x0000000000000000000000000000000000000def"),
3177            amount: U256::from(1_000_000u64),
3178        };
3179        let trace = CallTrace {
3180            address: PATH_USD_ADDRESS,
3181            data: transfer.abi_encode().into(),
3182            depth: 0,
3183            success: true,
3184            ..Default::default()
3185        };
3186        let decoded = decoder.decode_function(&trace).await;
3187        assert_eq!(decoded.label.as_deref(), Some("PathUSD"));
3188        let json = serde_json::to_string(&decoded).expect("decoded trace serializes");
3189        assert!(json.contains(r#""label":"PathUSD""#));
3190        assert!(!json.contains("payment-lane"));
3191
3192        let balance_of = ITIP20::balanceOfCall {
3193            account: address!("0x0000000000000000000000000000000000000def"),
3194        };
3195        let trace = CallTrace {
3196            address: PATH_USD_ADDRESS,
3197            data: balance_of.abi_encode().into(),
3198            depth: 0,
3199            success: true,
3200            ..Default::default()
3201        };
3202        let decoded = decoder.decode_function(&trace).await;
3203        assert_eq!(decoded.label.as_deref(), Some("PathUSD"));
3204
3205        let event = ITIP20ChannelReserve::ChannelOpened {
3206            channelId: B256::repeat_byte(0x33),
3207            payer: address!("0x0000000000000000000000000000000000000123"),
3208            payee: address!("0x0000000000000000000000000000000000000abc"),
3209            operator: Address::ZERO,
3210            token: PATH_USD_ADDRESS,
3211            authorizedSigner: Address::ZERO,
3212            salt: B256::repeat_byte(0x22),
3213            expiringNonceHash: B256::repeat_byte(0x44),
3214            deposit: U96::from(1_000_000u64),
3215        };
3216        let decoded = decoder.decode_event(&event.encode_log_data()).await;
3217        assert_eq!(decoded.name.as_deref(), Some("ChannelOpened"));
3218        let params = decoded.params.expect("ChannelOpened params should decode");
3219        assert_eq!(params[0].0, "channelId");
3220        assert_eq!(params[8].0, "deposit");
3221        assert!(params[8].1.starts_with("1000000"));
3222    }
3223
3224    #[tokio::test]
3225    #[cfg(feature = "monad")]
3226    async fn test_decodes_monad_staking_precompile_call() {
3227        let trace = CallTrace {
3228            address: monad_revm::staking::STAKING_ADDRESS,
3229            data: monad_revm::staking::interface::IMonadStaking::getEpochCall::SELECTOR
3230                .to_vec()
3231                .into(),
3232            output:
3233                monad_revm::staking::interface::IMonadStaking::getEpochCall::abi_encode_returns(
3234                    &monad_revm::staking::interface::IMonadStaking::getEpochReturn {
3235                        epoch: 42,
3236                        inEpochDelayPeriod: true,
3237                    },
3238                )
3239                .into(),
3240            success: true,
3241            ..Default::default()
3242        };
3243
3244        let decoded = monad_decoder(MonadHardfork::MonadEight).decode_function(&trace).await;
3245
3246        assert_eq!(decoded.label.as_deref(), Some("Staking"));
3247        let call_data = decoded.call_data.expect("call data");
3248        assert_eq!(call_data.signature, "getEpoch()");
3249        assert!(call_data.args.is_empty());
3250        assert_eq!(decoded.return_data.as_deref(), Some("42, true"));
3251    }
3252
3253    #[tokio::test]
3254    #[cfg(feature = "monad")]
3255    async fn test_decodes_monad_staking_syscall() {
3256        let block_author = Address::from([0x42; 20]);
3257        let trace = CallTrace {
3258            address: monad_revm::staking::STAKING_ADDRESS,
3259            data: monad::IMonadStakingSyscalls::syscallRewardCall { blockAuthor: block_author }
3260                .abi_encode()
3261                .into(),
3262            success: true,
3263            ..Default::default()
3264        };
3265
3266        let decoder = monad_decoder(MonadHardfork::MonadEight);
3267        let expected_author = decoder.format_value(&DynSolValue::Address(block_author));
3268        let decoded = decoder.decode_function(&trace).await;
3269
3270        assert_eq!(decoded.label.as_deref(), Some("Staking"));
3271        let call_data = decoded.call_data.expect("call data");
3272        assert_eq!(call_data.signature, "syscallReward(address)");
3273        assert_eq!(call_data.args, vec![expected_author]);
3274    }
3275
3276    #[tokio::test]
3277    #[cfg(feature = "monad")]
3278    async fn test_decodes_monad_reserve_balance_precompile_call() {
3279        let trace = CallTrace {
3280            address: monad_revm::reserve_balance::abi::RESERVE_BALANCE_ADDRESS,
3281            data: monad_revm::reserve_balance::interface::IReserveBalance::dippedIntoReserveCall::SELECTOR
3282                .to_vec()
3283                .into(),
3284            output: true.abi_encode().into(),
3285            success: true,
3286            ..Default::default()
3287        };
3288
3289        let decoded = monad_decoder(MonadHardfork::MonadNine).decode_function(&trace).await;
3290
3291        assert_eq!(decoded.label.as_deref(), Some("ReserveBalance"));
3292        let call_data = decoded.call_data.expect("call data");
3293        assert_eq!(call_data.signature, "dippedIntoReserve()");
3294        assert!(call_data.args.is_empty());
3295        assert_eq!(decoded.return_data.as_deref(), Some("true"));
3296    }
3297
3298    #[tokio::test]
3299    #[cfg(feature = "monad")]
3300    async fn test_decodes_monad_staking_precompile_event() {
3301        let event = Event::parse(
3302            "event Delegate(uint64 indexed validatorId,address indexed delegator,uint256 amount,uint64 activationEpoch)",
3303        )
3304        .unwrap();
3305        let delegator = Address::from([0x11; 20]);
3306        let log = LogData::new_unchecked(
3307            vec![event.selector(), topic_from_u64(7), topic_from_address(delegator)],
3308            (U256::from(1000), 9_u64).abi_encode().into(),
3309        );
3310
3311        let decoder = monad_decoder(MonadHardfork::MonadEight);
3312        let decoded =
3313            decoder.decode_event_with_address(monad_revm::staking::STAKING_ADDRESS, &log).await;
3314
3315        assert_eq!(decoded.name.as_deref(), Some("Delegate"));
3316        let params = decoded.params.expect("params");
3317        assert_eq!(params[0], ("validatorId".to_string(), "7".to_string()));
3318        assert_eq!(params[1].0, "delegator");
3319        assert_eq!(params[2], ("amount".to_string(), "1000".to_string()));
3320        assert_eq!(params[3], ("activationEpoch".to_string(), "9".to_string()));
3321
3322        let collision = decoder
3323            .decode_event_with_address(
3324                monad_revm::reserve_balance::abi::RESERVE_BALANCE_ADDRESS,
3325                &log,
3326            )
3327            .await;
3328        assert_eq!(collision.name, None);
3329    }
3330
3331    #[tokio::test]
3332    #[cfg(feature = "monad")]
3333    async fn test_monad_metadata_is_not_registered_for_ethereum() {
3334        let decoder = CallTraceDecoderBuilder::new().with_chain_id(Some(1)).build();
3335        for address in [
3336            foundry_evm_core::constants::MONAD_CHEATCODE_ADDRESS,
3337            monad_revm::staking::STAKING_ADDRESS,
3338            monad_revm::reserve_balance::abi::RESERVE_BALANCE_ADDRESS,
3339        ] {
3340            assert!(!decoder.labels.contains_key(&address));
3341        }
3342
3343        let staking_trace = CallTrace {
3344            address: monad_revm::staking::STAKING_ADDRESS,
3345            data: monad_revm::staking::interface::IMonadStaking::getEpochCall::SELECTOR
3346                .to_vec()
3347                .into(),
3348            output:
3349                monad_revm::staking::interface::IMonadStaking::getEpochCall::abi_encode_returns(
3350                    &monad_revm::staking::interface::IMonadStaking::getEpochReturn {
3351                        epoch: 42,
3352                        inEpochDelayPeriod: true,
3353                    },
3354                )
3355                .into(),
3356            success: true,
3357            ..Default::default()
3358        };
3359        let staking = decoder.decode_function(&staking_trace).await;
3360        assert_ne!(
3361            staking.call_data.as_ref().map(|call| call.signature.as_str()),
3362            Some("getEpoch()")
3363        );
3364
3365        let reserve_trace = CallTrace {
3366            address: monad_revm::reserve_balance::abi::RESERVE_BALANCE_ADDRESS,
3367            data: monad_revm::reserve_balance::interface::IReserveBalance::dippedIntoReserveCall::SELECTOR
3368                .to_vec()
3369                .into(),
3370            output: true.abi_encode().into(),
3371            success: true,
3372            ..Default::default()
3373        };
3374        let reserve = decoder.decode_function(&reserve_trace).await;
3375        assert_ne!(
3376            reserve.call_data.as_ref().map(|call| call.signature.as_str()),
3377            Some("dippedIntoReserve()")
3378        );
3379
3380        let event = Event::parse(
3381            "event Delegate(uint64 indexed validatorId,address indexed delegator,uint256 amount,uint64 activationEpoch)",
3382        )
3383        .unwrap();
3384        let log = LogData::new_unchecked(
3385            vec![
3386                event.selector(),
3387                topic_from_u64(7),
3388                topic_from_address(Address::from([0x11; 20])),
3389            ],
3390            (U256::from(1000), 9_u64).abi_encode().into(),
3391        );
3392        for address in [
3393            monad_revm::staking::STAKING_ADDRESS,
3394            monad_revm::reserve_balance::abi::RESERVE_BALANCE_ADDRESS,
3395        ] {
3396            let decoded = decoder.decode_event_with_address(address, &log).await;
3397            assert_eq!(decoded.name, None);
3398        }
3399    }
3400
3401    #[tokio::test]
3402    #[cfg(feature = "monad")]
3403    async fn test_monad_reserve_metadata_starts_at_monad_nine() {
3404        let mut monad_eight = monad_decoder(MonadHardfork::MonadEight);
3405        monad_eight.clear_addresses();
3406        assert_eq!(
3407            monad_eight
3408                .labels
3409                .get(&foundry_evm_core::constants::MONAD_CHEATCODE_ADDRESS)
3410                .map(String::as_str),
3411            Some("MonadVM")
3412        );
3413        assert_eq!(
3414            monad_eight.labels.get(&monad_revm::staking::STAKING_ADDRESS).map(String::as_str),
3415            Some("Staking")
3416        );
3417        assert!(
3418            !monad_eight
3419                .labels
3420                .contains_key(&monad_revm::reserve_balance::abi::RESERVE_BALANCE_ADDRESS)
3421        );
3422
3423        let trace = CallTrace {
3424            address: monad_revm::reserve_balance::abi::RESERVE_BALANCE_ADDRESS,
3425            data: monad_revm::reserve_balance::interface::IReserveBalance::dippedIntoReserveCall::SELECTOR
3426                .to_vec()
3427                .into(),
3428            output: true.abi_encode().into(),
3429            success: true,
3430            ..Default::default()
3431        };
3432        let before = monad_eight.decode_function(&trace).await;
3433        assert_ne!(
3434            before.call_data.as_ref().map(|call| call.signature.as_str()),
3435            Some("dippedIntoReserve()")
3436        );
3437
3438        let mut monad_nine = monad_decoder(MonadHardfork::MonadNine);
3439        monad_nine.clear_addresses();
3440        assert_eq!(
3441            monad_nine
3442                .labels
3443                .get(&monad_revm::reserve_balance::abi::RESERVE_BALANCE_ADDRESS)
3444                .map(String::as_str),
3445            Some("ReserveBalance")
3446        );
3447        let after = monad_nine.decode_function(&trace).await;
3448        assert_eq!(
3449            after.call_data.as_ref().map(|call| call.signature.as_str()),
3450            Some("dippedIntoReserve()")
3451        );
3452    }
3453
3454    #[tokio::test]
3455    #[cfg(feature = "monad")]
3456    async fn test_monad_metadata_refreshes_across_hardforks() {
3457        let trace = CallTrace {
3458            address: monad_revm::reserve_balance::abi::RESERVE_BALANCE_ADDRESS,
3459            data: monad_revm::reserve_balance::interface::IReserveBalance::dippedIntoReserveCall::SELECTOR
3460                .to_vec()
3461                .into(),
3462            output: true.abi_encode().into(),
3463            success: true,
3464            ..Default::default()
3465        };
3466        let mut decoder = monad_decoder(MonadHardfork::MonadEight);
3467
3468        decoder.set_hardfork(Some(MonadHardfork::MonadNine.into()));
3469        assert_eq!(decoder.hardfork(), Some(MonadHardfork::MonadNine.into()));
3470        assert_eq!(
3471            decoder
3472                .labels
3473                .get(&monad_revm::reserve_balance::abi::RESERVE_BALANCE_ADDRESS)
3474                .map(String::as_str),
3475            Some("ReserveBalance")
3476        );
3477        let monad_nine = decoder.decode_function(&trace).await;
3478        assert_eq!(
3479            monad_nine.call_data.as_ref().map(|call| call.signature.as_str()),
3480            Some("dippedIntoReserve()")
3481        );
3482
3483        decoder.set_hardfork(Some(MonadHardfork::MonadEight.into()));
3484        assert_eq!(decoder.hardfork(), Some(MonadHardfork::MonadEight.into()));
3485        assert!(
3486            !decoder
3487                .labels
3488                .contains_key(&monad_revm::reserve_balance::abi::RESERVE_BALANCE_ADDRESS)
3489        );
3490        assert_eq!(
3491            decoder.labels.get(&monad_revm::staking::STAKING_ADDRESS).map(String::as_str),
3492            Some("Staking")
3493        );
3494        let monad_eight = decoder.decode_function(&trace).await;
3495        assert_ne!(
3496            monad_eight.call_data.as_ref().map(|call| call.signature.as_str()),
3497            Some("dippedIntoReserve()")
3498        );
3499    }
3500
3501    #[cfg(feature = "monad")]
3502    fn topic_from_u64(value: u64) -> B256 {
3503        let mut topic = [0u8; 32];
3504        topic[24..].copy_from_slice(&value.to_be_bytes());
3505        B256::from(topic)
3506    }
3507
3508    #[cfg(feature = "monad")]
3509    fn topic_from_address(address: Address) -> B256 {
3510        let mut topic = [0u8; 32];
3511        topic[12..].copy_from_slice(address.as_slice());
3512        B256::from(topic)
3513    }
3514
3515    #[tokio::test]
3516    async fn test_t7_storage_credits_call_and_error_decode() {
3517        let mut decoder = CallTraceDecoder::new().clone();
3518        decoder.chain_id = Some(4217);
3519
3520        // A write call decodes to its signature and the precompile address is labeled.
3521        let set_mode = IStorageCredits::setModeCall { newMode: IStorageCredits::Mode::Direct };
3522        let trace = CallTrace {
3523            address: STORAGE_CREDITS_ADDRESS,
3524            data: set_mode.abi_encode().into(),
3525            depth: 0,
3526            success: true,
3527            ..Default::default()
3528        };
3529        let decoded = decoder.decode_function(&trace).await;
3530        assert_eq!(decoded.label.as_deref(), Some("StorageCredits"));
3531        assert_eq!(decoded.call_data.expect("setMode should decode").signature, "setMode(uint8)");
3532
3533        // A view call unique to this precompile also decodes.
3534        let mode_of = IStorageCredits::modeOfCall { account: Address::repeat_byte(0x11) };
3535        let trace = CallTrace {
3536            address: STORAGE_CREDITS_ADDRESS,
3537            data: mode_of.abi_encode().into(),
3538            depth: 0,
3539            success: true,
3540            ..Default::default()
3541        };
3542        let decoded = decoder.decode_function(&trace).await;
3543        assert_eq!(decoded.call_data.expect("modeOf should decode").signature, "modeOf(address)");
3544
3545        // The precompile's custom errors decode by name in reverts.
3546        let revert = decoder
3547            .revert_decoder
3548            .decode(IStorageCredits::InvalidMode {}.abi_encode().as_slice(), None);
3549        assert!(revert.contains("InvalidMode"), "{revert}");
3550
3551        // `balanceOf(address)` collides with `ITIP20`'s selector; the global map must keep
3552        // `ITIP20`'s `uint256` return so ordinary token balances above `u64::MAX` still decode.
3553        let selector = IStorageCredits::balanceOfCall::SELECTOR;
3554        let funcs = decoder.functions.get(&selector).expect("balanceOf selector is registered");
3555        assert!(
3556            funcs.iter().any(|f| f.outputs.first().is_some_and(|o| o.ty == "uint256")),
3557            "global balanceOf must return uint256"
3558        );
3559    }
3560
3561    // A mock identifier that records which addresses it was asked to identify.
3562    struct RecordingIdentifier {
3563        queried: Vec<Address>,
3564    }
3565    impl TraceIdentifier for RecordingIdentifier {
3566        fn identify_addresses(&mut self, nodes: &[&CallTraceNode]) -> Vec<IdentifiedAddress<'_>> {
3567            self.queried.extend(nodes.iter().map(|n| n.trace.address));
3568            Vec::new()
3569        }
3570    }
3571
3572    struct AbiIdentifier {
3573        abi: JsonAbi,
3574    }
3575
3576    impl TraceIdentifier for AbiIdentifier {
3577        fn identify_addresses(&mut self, nodes: &[&CallTraceNode]) -> Vec<IdentifiedAddress<'_>> {
3578            nodes
3579                .iter()
3580                .map(|node| IdentifiedAddress {
3581                    address: node.trace.address,
3582                    label: Some("Scoped".to_string()),
3583                    contract: Some("Scoped".to_string()),
3584                    abi: Some(Cow::Borrowed(&self.abi)),
3585                    constructor_args_offset: None,
3586                    artifact_id: None,
3587                })
3588                .collect()
3589        }
3590    }
3591
3592    #[test]
3593    fn scoped_identification_reuses_global_abi() {
3594        let abi = JsonAbi::parse(["function scoped(uint256)"]).unwrap();
3595        let function = abi.functions().next().unwrap().clone();
3596        let selector = function.selector();
3597        let mut decoder = CallTraceDecoderBuilder::new().with_abi(&abi).build();
3598        let global_functions = decoder.functions.get(&selector).unwrap().len();
3599
3600        let address = Address::repeat_byte(0x42);
3601        let mut arena = CallTraceArena::default();
3602        arena.nodes_mut()[0].trace.address = address;
3603        decoder.identify_scoped(&arena, &mut AbiIdentifier { abi });
3604
3605        assert_eq!(decoder.functions.get(&selector).unwrap().len(), global_functions);
3606        assert_eq!(decoder.functions_by_address[&address][&selector], [function]);
3607    }
3608
3609    #[tokio::test]
3610    async fn inactive_p256_address_uses_contract_identity() {
3611        let abi = JsonAbi::parse(["function ordinaryCode()"]).unwrap();
3612        let function = abi.functions().next().unwrap();
3613        let mut arena = CallTraceArena::default();
3614        arena.nodes_mut()[0].trace = CallTrace {
3615            address: P256_VERIFY,
3616            maybe_precompile: Some(false),
3617            data: function.selector().to_vec().into(),
3618            success: true,
3619            ..Default::default()
3620        };
3621
3622        let mut decoder = CallTraceDecoder::new().clone();
3623        decoder.identify(&arena, &mut AbiIdentifier { abi });
3624        let decoded = decoder.decode_function(&arena.nodes()[0].trace).await;
3625
3626        assert_eq!(decoded.label.as_deref(), Some("Scoped"));
3627        assert_eq!(decoded.call_data.unwrap().signature, "ordinaryCode()");
3628    }
3629
3630    #[test]
3631    fn p256_is_not_an_unconditional_precompile_label() {
3632        assert!(!CallTraceDecoder::new().precompile_labels().contains_key(&P256_VERIFY));
3633    }
3634
3635    #[test]
3636    fn test_identify_addresses_skips_evm_precompiles() {
3637        use foundry_evm_core::precompiles::SHA_256;
3638
3639        let decoder = CallTraceDecoder::new();
3640
3641        let mut arena = CallTraceArena::default();
3642        let regular_addr = Address::from([0x42; 20]);
3643        arena.nodes_mut()[0].trace.address = regular_addr;
3644
3645        // Standard EVM precompile flagged by the inspector.
3646        arena.nodes_mut().push(CallTraceNode {
3647            trace: CallTrace {
3648                address: SHA_256,
3649                depth: 1,
3650                maybe_precompile: Some(true),
3651                ..Default::default()
3652            },
3653            idx: 1,
3654            ..Default::default()
3655        });
3656
3657        // Standard EVM precompile NOT flagged, caught by is_known_precompile.
3658        arena.nodes_mut().push(CallTraceNode {
3659            trace: CallTrace {
3660                address: SHA_256,
3661                depth: 1,
3662                maybe_precompile: None,
3663                ..Default::default()
3664            },
3665            idx: 2,
3666            ..Default::default()
3667        });
3668
3669        let mut identifier = RecordingIdentifier { queried: Vec::new() };
3670        decoder.identify_addresses(&arena, &mut identifier);
3671
3672        assert_eq!(identifier.queried, vec![regular_addr]);
3673    }
3674
3675    #[test]
3676    fn test_identify_addresses_skips_tempo_precompiles() {
3677        use foundry_evm_core::tempo::{TEMPO_PRECOMPILE_ADDRESSES, TIP20_CHANNEL_RESERVE_ADDRESS};
3678
3679        // Decoder with Tempo chain ID (4217).
3680        let decoder = CallTraceDecoderBuilder::new()
3681            .with_chain_id(Some(4217))
3682            .with_hardfork(Some(TempoHardfork::T5.into()))
3683            .build();
3684
3685        assert_eq!(
3686            decoder.labels.get(&TIP20_CHANNEL_RESERVE_ADDRESS),
3687            Some(&"TIP20ChannelReserve".to_string())
3688        );
3689
3690        let mut arena = CallTraceArena::default();
3691        let regular_addr = Address::from([0x42; 20]);
3692        arena.nodes_mut()[0].trace.address = regular_addr;
3693
3694        // Tempo precompile — not flagged by inspector, caught by is_known_precompile
3695        // only when chain_id is a Tempo chain.
3696        let tempo_precompile = TEMPO_PRECOMPILE_ADDRESSES[0];
3697        arena.nodes_mut().push(CallTraceNode {
3698            trace: CallTrace {
3699                address: tempo_precompile,
3700                depth: 1,
3701                maybe_precompile: None,
3702                ..Default::default()
3703            },
3704            idx: 1,
3705            ..Default::default()
3706        });
3707
3708        let mut identifier = RecordingIdentifier { queried: Vec::new() };
3709        decoder.identify_addresses(&arena, &mut identifier);
3710
3711        // On a Tempo chain, the Tempo precompile should be filtered out.
3712        assert_eq!(identifier.queried, vec![regular_addr]);
3713    }
3714
3715    #[test]
3716    fn test_precompile_labels_follow_tempo_hardfork_activation_boundaries() {
3717        let labels_for_hardfork = |hardfork: TempoHardfork| {
3718            CallTraceDecoderBuilder::new()
3719                .with_hardfork(Some(hardfork.into()))
3720                .build()
3721                .precompile_labels()
3722        };
3723
3724        let t4_labels = labels_for_hardfork(TempoHardfork::T4);
3725        assert_eq!(t4_labels.get(&TIP_FEE_MANAGER_ADDRESS), Some(&"FeeManager".to_string()));
3726        assert!(!t4_labels.contains_key(&TIP20_CHANNEL_RESERVE_ADDRESS));
3727        assert!(!t4_labels.contains_key(&RECEIVE_POLICY_GUARD_ADDRESS));
3728        assert!(!t4_labels.contains_key(&STORAGE_CREDITS_ADDRESS));
3729
3730        let t5_labels = labels_for_hardfork(TempoHardfork::T5);
3731        assert_eq!(t5_labels.get(&TIP_FEE_MANAGER_ADDRESS), Some(&"FeeManager".to_string()));
3732        assert_eq!(
3733            t5_labels.get(&TIP20_CHANNEL_RESERVE_ADDRESS),
3734            Some(&"TIP20ChannelReserve".to_string())
3735        );
3736        assert!(!t5_labels.contains_key(&RECEIVE_POLICY_GUARD_ADDRESS));
3737        assert!(!t5_labels.contains_key(&STORAGE_CREDITS_ADDRESS));
3738
3739        let t6_labels = labels_for_hardfork(TempoHardfork::T6);
3740        assert_eq!(
3741            t6_labels.get(&TIP20_CHANNEL_RESERVE_ADDRESS),
3742            Some(&"TIP20ChannelReserve".to_string())
3743        );
3744        assert_eq!(
3745            t6_labels.get(&RECEIVE_POLICY_GUARD_ADDRESS),
3746            Some(&"ReceivePolicyGuard".to_string())
3747        );
3748        assert!(!t6_labels.contains_key(&STORAGE_CREDITS_ADDRESS));
3749
3750        let t7_labels = labels_for_hardfork(TempoHardfork::T7);
3751        assert_eq!(
3752            t7_labels.get(&RECEIVE_POLICY_GUARD_ADDRESS),
3753            Some(&"ReceivePolicyGuard".to_string())
3754        );
3755        assert_eq!(t7_labels.get(&STORAGE_CREDITS_ADDRESS), Some(&"StorageCredits".to_string()));
3756
3757        let ethereum_labels = CallTraceDecoderBuilder::new()
3758            .with_execution_network(NetworkVariant::Ethereum)
3759            .with_chain_id(Some(4217))
3760            .with_hardfork(Some(TempoHardfork::T7.into()))
3761            .build()
3762            .precompile_labels();
3763        assert!(!ethereum_labels.contains_key(&TIP20_CHANNEL_RESERVE_ADDRESS));
3764        assert!(!ethereum_labels.contains_key(&RECEIVE_POLICY_GUARD_ADDRESS));
3765        assert!(!ethereum_labels.contains_key(&STORAGE_CREDITS_ADDRESS));
3766    }
3767
3768    #[test]
3769    fn celo_profile_is_authoritative_for_custom_precompile_labels() {
3770        let mut custom_celo = CallTraceDecoderBuilder::new()
3771            .with_networks(NetworkConfigs::with_celo())
3772            .with_chain_id(Some(98_765_432))
3773            .build();
3774        assert_eq!(
3775            custom_celo.precompile_labels().get(&CELO_TRANSFER),
3776            Some(&CELO_TRANSFER_LABEL.to_string())
3777        );
3778        custom_celo.clear_addresses();
3779        assert_eq!(
3780            custom_celo.precompile_labels().get(&CELO_TRANSFER),
3781            Some(&CELO_TRANSFER_LABEL.to_string())
3782        );
3783
3784        let explicit_ethereum = CallTraceDecoderBuilder::new()
3785            .with_networks(NetworkConfigs::default())
3786            .with_chain_id(Some(42_220))
3787            .build();
3788        assert!(!explicit_ethereum.precompile_labels().contains_key(&CELO_TRANSFER));
3789
3790        let inferred_celo = CallTraceDecoderBuilder::new().with_chain_id(Some(42_220)).build();
3791        assert_eq!(
3792            inferred_celo.precompile_labels().get(&CELO_TRANSFER),
3793            Some(&CELO_TRANSFER_LABEL.to_string())
3794        );
3795    }
3796
3797    #[cfg(feature = "base")]
3798    #[test]
3799    fn test_precompile_labels_follow_base_upgrade_boundaries() {
3800        let labels_for_upgrade = |upgrade| {
3801            CallTraceDecoderBuilder::new()
3802                .with_chain_id(Some(8453))
3803                .with_hardfork(Some(FoundryHardfork::Base(upgrade)))
3804                .build()
3805                .precompile_labels()
3806        };
3807        let base_label_count = |labels: &AddressHashMap<String>| {
3808            BASE_PRECOMPILE_ADDRESSES.iter().filter(|address| labels.contains_key(*address)).count()
3809        };
3810
3811        assert_eq!(base_label_count(&labels_for_upgrade(BaseUpgrade::Azul)), 0);
3812        assert_eq!(base_label_count(&labels_for_upgrade(BaseUpgrade::Beryl)), 3);
3813        assert_eq!(base_label_count(&labels_for_upgrade(BaseUpgrade::Cobalt)), 5);
3814    }
3815
3816    #[tokio::test]
3817    async fn test_current_committee_decoding_is_durable_and_context_gated() {
3818        let abi = ICurrentCommittee::abi::contract();
3819        let function = abi.functions.get("getCommitteeMembers").unwrap().first().unwrap();
3820        let output = function
3821            .abi_encode_output(&[
3822                DynSolValue::Uint(U256::from(7), 64),
3823                DynSolValue::Array(vec![DynSolValue::FixedBytes(B256::with_last_byte(1), 32)]),
3824            ])
3825            .unwrap();
3826        let trace = CallTrace {
3827            address: CURRENT_COMMITTEE_ADDRESS,
3828            data: function.selector().to_vec().into(),
3829            output: output.into(),
3830            success: true,
3831            ..Default::default()
3832        };
3833
3834        let mut decoder = CallTraceDecoderBuilder::new()
3835            .with_chain_id(Some(4217))
3836            .with_hardfork(Some(TempoHardfork::T8.into()))
3837            .build();
3838        decoder.clear_addresses();
3839        let decoded = decoder.decode_function(&trace).await;
3840        assert_eq!(decoded.label.as_deref(), Some("CurrentCommittee"));
3841        assert_eq!(decoded.call_data.unwrap().signature, "getCommitteeMembers()");
3842        assert_eq!(
3843            decoded.return_data.unwrap(),
3844            "7, [0x0000000000000000000000000000000000000000000000000000000000000001]"
3845        );
3846
3847        for decoder in [
3848            CallTraceDecoderBuilder::new()
3849                .with_chain_id(Some(4217))
3850                .with_hardfork(Some(TempoHardfork::T7.into()))
3851                .build(),
3852            CallTraceDecoderBuilder::new()
3853                .with_chain_id(Some(1))
3854                .with_hardfork(Some(TempoHardfork::T8.into()))
3855                .build(),
3856        ] {
3857            let decoded = decoder.decode_function(&trace).await;
3858            assert_ne!(decoded.label.as_deref(), Some("CurrentCommittee"));
3859            assert!(decoded.call_data.is_none());
3860        }
3861    }
3862
3863    #[tokio::test]
3864    async fn test_current_committee_unauthorized_is_address_scoped() {
3865        let output = ICurrentCommittee::Unauthorized {}.abi_encode();
3866        let trace = CallTrace {
3867            address: CURRENT_COMMITTEE_ADDRESS,
3868            output: output.clone().into(),
3869            success: false,
3870            status: Some(InstructionResult::Revert),
3871            ..Default::default()
3872        };
3873        let decoder = CallTraceDecoderBuilder::new()
3874            .with_chain_id(Some(4217))
3875            .with_hardfork(Some(TempoHardfork::T8.into()))
3876            .build();
3877        assert_eq!(
3878            decoder.decode_function(&trace).await.return_data.as_deref(),
3879            Some("Unauthorized()")
3880        );
3881
3882        let unrelated = CallTrace { address: Address::ZERO, ..trace };
3883        let baseline = CallTraceDecoder::new().decode_function(&unrelated).await;
3884        assert_eq!(decoder.decode_function(&unrelated).await.return_data, baseline.return_data);
3885    }
3886
3887    #[test]
3888    fn test_precompile_labels_skip_tempo_precompiles_on_other_chains() {
3889        let decoder = CallTraceDecoderBuilder::new()
3890            .with_chain_id(Some(1))
3891            .with_hardfork(Some(TempoHardfork::T6.into()))
3892            .build();
3893
3894        let labels = decoder.precompile_labels();
3895        assert!(!labels.contains_key(&TIP_FEE_MANAGER_ADDRESS));
3896        assert!(!labels.contains_key(&RECEIVE_POLICY_GUARD_ADDRESS));
3897        assert!(!labels.contains_key(&STORAGE_CREDITS_ADDRESS));
3898    }
3899
3900    #[test]
3901    fn test_tempo_hardfork_labels_do_not_clobber_user_labels() {
3902        let reserve_label = "UserReserve".to_string();
3903        let guard_label = "UserGuard".to_string();
3904        let decoder = CallTraceDecoderBuilder::new()
3905            .with_labels([
3906                (TIP20_CHANNEL_RESERVE_ADDRESS, reserve_label.clone()),
3907                (RECEIVE_POLICY_GUARD_ADDRESS, guard_label.clone()),
3908            ])
3909            .with_hardfork(Some(TempoHardfork::T6.into()))
3910            .build();
3911
3912        assert_eq!(decoder.labels.get(&TIP20_CHANNEL_RESERVE_ADDRESS), Some(&reserve_label));
3913        assert_eq!(decoder.labels.get(&RECEIVE_POLICY_GUARD_ADDRESS), Some(&guard_label));
3914    }
3915
3916    #[test]
3917    fn test_tempo_hardfork_transitions_rebuild_address_metadata() {
3918        let user_address = address!("0000000000000000000000000000000000001234");
3919        let user_label = "UserLabel".to_string();
3920        let mut decoder = CallTraceDecoderBuilder::new()
3921            .with_execution_network(NetworkVariant::Tempo)
3922            .with_labels([(user_address, user_label.clone())])
3923            .with_hardfork(Some(TempoHardfork::T4.into()))
3924            .build();
3925
3926        let labels = decoder.precompile_labels();
3927        assert!(labels.contains_key(&TIP_FEE_MANAGER_ADDRESS));
3928        assert!(!labels.contains_key(&TIP20_CHANNEL_RESERVE_ADDRESS));
3929        assert!(!labels.contains_key(&RECEIVE_POLICY_GUARD_ADDRESS));
3930
3931        decoder.set_hardfork(Some(TempoHardfork::T6.into()));
3932        let labels = decoder.precompile_labels();
3933        assert!(labels.contains_key(&TIP_FEE_MANAGER_ADDRESS));
3934        assert!(labels.contains_key(&TIP20_CHANNEL_RESERVE_ADDRESS));
3935        assert!(labels.contains_key(&RECEIVE_POLICY_GUARD_ADDRESS));
3936        assert_eq!(decoder.labels.get(&user_address), Some(&user_label));
3937
3938        decoder.set_hardfork(Some(TempoHardfork::T4.into()));
3939        let labels = decoder.precompile_labels();
3940        assert!(labels.contains_key(&TIP_FEE_MANAGER_ADDRESS));
3941        assert!(!labels.contains_key(&TIP20_CHANNEL_RESERVE_ADDRESS));
3942        assert!(!labels.contains_key(&RECEIVE_POLICY_GUARD_ADDRESS));
3943        assert_eq!(decoder.labels.get(&user_address), Some(&user_label));
3944
3945        decoder.set_hardfork(None);
3946        assert_eq!(decoder.labels.get(&user_address), Some(&user_label));
3947    }
3948
3949    #[test]
3950    fn test_tempo_hardfork_none_does_not_remove_user_reserve_label() {
3951        use foundry_evm_core::tempo::TIP20_CHANNEL_RESERVE_ADDRESS;
3952
3953        let reserve_label = "UserReserve".to_string();
3954        let decoder = CallTraceDecoderBuilder::new()
3955            .with_labels([(TIP20_CHANNEL_RESERVE_ADDRESS, reserve_label.clone())])
3956            .with_hardfork(None)
3957            .build();
3958
3959        assert_eq!(decoder.labels.get(&TIP20_CHANNEL_RESERVE_ADDRESS), Some(&reserve_label));
3960    }
3961
3962    #[tokio::test]
3963    async fn test_decode_receive_policy_guard_at_t6() {
3964        let function = Function::parse("claim(address,bytes)").unwrap();
3965        let data = function
3966            .abi_encode_input(&[
3967                DynSolValue::Address(Address::from([0x11; 20])),
3968                DynSolValue::Bytes(vec![0x12, 0x34]),
3969            ])
3970            .unwrap();
3971        let trace = CallTrace {
3972            address: RECEIVE_POLICY_GUARD_ADDRESS,
3973            data: data.into(),
3974            success: true,
3975            ..Default::default()
3976        };
3977
3978        let decoder = CallTraceDecoderBuilder::new()
3979            .with_chain_id(Some(4217))
3980            .with_hardfork(Some(TempoHardfork::T6.into()))
3981            .build();
3982        let decoded = decoder.decode_function(&trace).await;
3983
3984        assert_eq!(decoded.label, Some("ReceivePolicyGuard".to_string()));
3985        assert_eq!(decoded.call_data.unwrap().signature, "claim(address,bytes)");
3986    }
3987
3988    #[tokio::test]
3989    async fn test_t6_receive_policy_calls_decode() {
3990        let decoder = CallTraceDecoderBuilder::new()
3991            .with_chain_id(Some(4217))
3992            .with_hardfork(Some(TempoHardfork::T6.into()))
3993            .build();
3994
3995        let set_policy = ITIP403Registry::setReceivePolicyCall {
3996            senderPolicyId: 7,
3997            tokenFilterId: 9,
3998            recoveryAuthority: address!("0x0000000000000000000000000000000000000abc"),
3999        };
4000        let decoded = decoder
4001            .decode_function(&CallTrace {
4002                address: TIP403_REGISTRY_ADDRESS,
4003                data: set_policy.abi_encode().into(),
4004                success: true,
4005                ..Default::default()
4006            })
4007            .await;
4008        let call_data = decoded.call_data.expect("setReceivePolicy should decode");
4009        assert_eq!(decoded.label.as_deref(), Some("TIP403Registry"));
4010        assert_eq!(call_data.signature, "setReceivePolicy(uint64,uint64,address)");
4011        assert_eq!(call_data.args[0], "7");
4012        assert_eq!(call_data.args[1], "9");
4013
4014        let validate = ITIP403Registry::validateReceivePolicyCall {
4015            token: PATH_USD_ADDRESS,
4016            sender: address!("0x0000000000000000000000000000000000000def"),
4017            receiver: address!("0x0000000000000000000000000000000000000123"),
4018        };
4019        let decoded = decoder
4020            .decode_function(&CallTrace {
4021                address: TIP403_REGISTRY_ADDRESS,
4022                data: validate.abi_encode().into(),
4023                success: true,
4024                ..Default::default()
4025            })
4026            .await;
4027        let call_data = decoded.call_data.expect("validateReceivePolicy should decode");
4028        assert_eq!(call_data.signature, "validateReceivePolicy(address,address,address)");
4029        assert!(call_data.args[0].contains("PathUSD"));
4030    }
4031
4032    #[tokio::test]
4033    async fn test_t6_admin_key_calls_decode() {
4034        let decoder = CallTraceDecoderBuilder::new()
4035            .with_chain_id(Some(4217))
4036            .with_hardfork(Some(TempoHardfork::T6.into()))
4037            .build();
4038        let account = address!("0x0000000000000000000000000000000000000abc");
4039        let key = address!("0x0000000000000000000000000000000000000def");
4040        let signature = vec![0x04, 0xaa, 0xbb];
4041
4042        let cases = [
4043            (
4044                ACCOUNT_KEYCHAIN_ADDRESS,
4045                IAccountKeychain::authorizeAdminKeyCall {
4046                    keyId: key,
4047                    signatureType: IAccountKeychain::SignatureType::Secp256k1,
4048                    witness: B256::repeat_byte(0x11),
4049                }
4050                .abi_encode()
4051                .into(),
4052                "authorizeAdminKey(address,uint8,bytes32)",
4053            ),
4054            (
4055                ACCOUNT_KEYCHAIN_ADDRESS,
4056                IAccountKeychain::isAdminKeyCall { account, keyId: key }.abi_encode().into(),
4057                "isAdminKey(address,address)",
4058            ),
4059            (
4060                SIGNATURE_VERIFIER_ADDRESS,
4061                ISignatureVerifier::verifyKeychainCall {
4062                    account,
4063                    hash: B256::repeat_byte(0x22),
4064                    signature: signature.clone().into(),
4065                }
4066                .abi_encode()
4067                .into(),
4068                "verifyKeychain(address,bytes32,bytes)",
4069            ),
4070            (
4071                SIGNATURE_VERIFIER_ADDRESS,
4072                ISignatureVerifier::verifyKeychainAdminCall {
4073                    account,
4074                    hash: B256::repeat_byte(0x33),
4075                    signature: signature.into(),
4076                }
4077                .abi_encode()
4078                .into(),
4079                "verifyKeychainAdmin(address,bytes32,bytes)",
4080            ),
4081        ];
4082
4083        for (address, data, signature) in cases {
4084            let decoded = decoder
4085                .decode_function(&CallTrace { address, data, success: true, ..Default::default() })
4086                .await;
4087            assert_eq!(
4088                decoded.call_data.expect("T6 keychain call should decode").signature,
4089                signature
4090            );
4091        }
4092    }
4093
4094    #[tokio::test]
4095    async fn test_t6_receive_policy_and_admin_events_decode() {
4096        let decoder = CallTraceDecoder::new();
4097        let account = address!("0x0000000000000000000000000000000000000abc");
4098        let key = address!("0x0000000000000000000000000000000000000def");
4099
4100        let events = [
4101            (
4102                ITIP403Registry::ReceivePolicyUpdated {
4103                    account,
4104                    senderPolicyId: 7,
4105                    tokenFilterId: 9,
4106                    recoveryAuthority: key,
4107                }
4108                .encode_log_data(),
4109                "ReceivePolicyUpdated",
4110            ),
4111            (
4112                IAccountKeychain::AdminKeyAuthorized { account, publicKey: key }.encode_log_data(),
4113                "AdminKeyAuthorized",
4114            ),
4115            (
4116                IReceivePolicyGuard::ReceiptClaimed {
4117                    token: PATH_USD_ADDRESS,
4118                    receiver: account,
4119                    blockedNonce: 11,
4120                    blockedAt: 12,
4121                    receiptVersion: 1,
4122                    originator: key,
4123                    recipient: account,
4124                    recoveryAuthority: key,
4125                    caller: key,
4126                    to: account,
4127                    amount: U256::from(123),
4128                }
4129                .encode_log_data(),
4130                "ReceiptClaimed",
4131            ),
4132            (
4133                IReceivePolicyGuard::ReceiptBurned {
4134                    token: PATH_USD_ADDRESS,
4135                    receiver: account,
4136                    blockedNonce: 11,
4137                    blockedAt: 12,
4138                    receiptVersion: 1,
4139                    originator: key,
4140                    recipient: account,
4141                    recoveryAuthority: key,
4142                    caller: key,
4143                    amount: U256::from(123),
4144                }
4145                .encode_log_data(),
4146                "ReceiptBurned",
4147            ),
4148        ];
4149
4150        for (log, expected_name) in events {
4151            let decoded = decoder.decode_event(&log).await;
4152            assert_eq!(decoded.name.as_deref(), Some(expected_name));
4153            assert!(decoded.params.expect("event params should decode").len() >= 2);
4154        }
4155    }
4156
4157    #[tokio::test]
4158    async fn test_t6_claim_receipt_bytes_decode_in_calls_and_transfer_blocked_event() {
4159        let decoder = CallTraceDecoder::new();
4160        let recovery = address!("0x0000000000000000000000000000000000000abc");
4161        let originator = address!("0x0000000000000000000000000000000000000def");
4162        let recipient = address!("0x0000000000000000000000000000000000000123");
4163        let receipt = IReceivePolicyGuard::ClaimReceiptV1::new(
4164            PATH_USD_ADDRESS,
4165            recovery,
4166            originator,
4167            recipient,
4168            12,
4169            34,
4170            ITIP403Registry::BlockedReason::RECEIVE_POLICY as u8,
4171            IReceivePolicyGuard::InboundKind::TRANSFER,
4172            B256::repeat_byte(0x44),
4173        )
4174        .abi_encode();
4175
4176        let claim =
4177            IReceivePolicyGuard::claimCall { to: recipient, receipt: receipt.clone().into() };
4178        let decoded = decoder
4179            .decode_function(&CallTrace {
4180                address: RECEIVE_POLICY_GUARD_ADDRESS,
4181                data: claim.abi_encode().into(),
4182                success: true,
4183                ..Default::default()
4184            })
4185            .await;
4186        let call_data = decoded.call_data.expect("claim should decode");
4187        assert_eq!(call_data.signature, "claim(address,bytes)");
4188        assert!(call_data.args[1].contains("ClaimReceiptV1"));
4189        assert!(call_data.args[1].contains("blockedNonce: 34"));
4190        assert!(call_data.args[1].contains(&originator.to_string()));
4191
4192        let decoded = decoder
4193            .decode_function(&CallTrace {
4194                address: Address::from([0x77; 20]),
4195                data: claim.abi_encode().into(),
4196                success: true,
4197                ..Default::default()
4198            })
4199            .await;
4200        let call_data = decoded.call_data.expect("matching claim selector should decode");
4201        assert_eq!(call_data.signature, "claim(address,bytes)");
4202        assert!(!call_data.args[1].contains("ClaimReceiptV1"));
4203        assert!(call_data.args[1].starts_with("0x"));
4204
4205        let blocked = IReceivePolicyGuard::TransferBlocked {
4206            token: PATH_USD_ADDRESS,
4207            receiver: recipient,
4208            blockedNonce: 34,
4209            amount: U256::from(123),
4210            receiptVersion: 1,
4211            receipt: receipt.into(),
4212        };
4213        let blocked_log = blocked.encode_log_data();
4214        let decoded =
4215            decoder.decode_event_with_address(RECEIVE_POLICY_GUARD_ADDRESS, &blocked_log).await;
4216        assert_eq!(decoded.name.as_deref(), Some("TransferBlocked"));
4217        let params = decoded.params.expect("TransferBlocked params should decode");
4218        let receipt = params.iter().find(|(name, _)| name == "receipt").unwrap();
4219        assert!(receipt.1.contains("ClaimReceiptV1"));
4220        assert!(receipt.1.contains("blockedReason: 2"));
4221        assert!(receipt.1.contains("kind: 0"));
4222
4223        let decoded =
4224            decoder.decode_event_with_address(Address::from([0x77; 20]), &blocked_log).await;
4225        assert_eq!(decoded.name.as_deref(), Some("TransferBlocked"));
4226        let params = decoded.params.expect("TransferBlocked params should decode");
4227        let receipt = params.iter().find(|(name, _)| name == "receipt").unwrap();
4228        assert!(!receipt.1.contains("ClaimReceiptV1"));
4229        assert!(receipt.1.starts_with("0x"));
4230    }
4231
4232    #[test]
4233    fn test_identify_addresses_does_not_skip_future_tempo_precompiles() {
4234        use foundry_evm_core::tempo::TIP20_CHANNEL_RESERVE_ADDRESS;
4235
4236        let decoder = CallTraceDecoderBuilder::new()
4237            .with_chain_id(Some(4217))
4238            .with_hardfork(Some(TempoHardfork::T4.into()))
4239            .build();
4240
4241        let mut arena = CallTraceArena::default();
4242        let regular_addr = Address::from([0x42; 20]);
4243        arena.nodes_mut()[0].trace.address = regular_addr;
4244
4245        arena.nodes_mut().push(CallTraceNode {
4246            trace: CallTrace {
4247                address: TIP20_CHANNEL_RESERVE_ADDRESS,
4248                depth: 1,
4249                maybe_precompile: None,
4250                ..Default::default()
4251            },
4252            idx: 1,
4253            ..Default::default()
4254        });
4255
4256        let mut identifier = RecordingIdentifier { queried: Vec::new() };
4257        decoder.identify_addresses(&arena, &mut identifier);
4258
4259        assert_eq!(identifier.queried, vec![regular_addr, TIP20_CHANNEL_RESERVE_ADDRESS]);
4260    }
4261
4262    #[test]
4263    fn test_identify_addresses_does_not_skip_tempo_precompiles_on_other_chains() {
4264        use foundry_evm_core::tempo::TEMPO_PRECOMPILE_ADDRESSES;
4265
4266        // Decoder with Ethereum mainnet chain ID (1).
4267        let mut decoder = CallTraceDecoder::new().clone();
4268        decoder.chain_id = Some(1);
4269
4270        let mut arena = CallTraceArena::default();
4271        let regular_addr = Address::from([0x42; 20]);
4272        arena.nodes_mut()[0].trace.address = regular_addr;
4273
4274        let tempo_precompile = TEMPO_PRECOMPILE_ADDRESSES[0];
4275        arena.nodes_mut().push(CallTraceNode {
4276            trace: CallTrace {
4277                address: tempo_precompile,
4278                depth: 1,
4279                maybe_precompile: None,
4280                ..Default::default()
4281            },
4282            idx: 1,
4283            ..Default::default()
4284        });
4285
4286        let mut identifier = RecordingIdentifier { queried: Vec::new() };
4287        decoder.identify_addresses(&arena, &mut identifier);
4288
4289        // On Ethereum, Tempo precompile addresses are regular contracts — should NOT be filtered.
4290        assert_eq!(identifier.queried, vec![regular_addr, tempo_precompile]);
4291    }
4292
4293    #[test]
4294    #[cfg(feature = "monad")]
4295    fn test_identify_addresses_skips_monad_precompiles() {
4296        let decoder = monad_decoder(MonadHardfork::MonadNine);
4297
4298        let mut arena = CallTraceArena::default();
4299        let regular_addr = Address::from([0x42; 20]);
4300        arena.nodes_mut()[0].trace.address = regular_addr;
4301
4302        arena.nodes_mut().push(CallTraceNode {
4303            trace: CallTrace {
4304                address: monad_revm::staking::STAKING_ADDRESS,
4305                depth: 1,
4306                maybe_precompile: None,
4307                ..Default::default()
4308            },
4309            idx: 1,
4310            ..Default::default()
4311        });
4312        arena.nodes_mut().push(CallTraceNode {
4313            trace: CallTrace {
4314                address: monad_revm::reserve_balance::abi::RESERVE_BALANCE_ADDRESS,
4315                depth: 1,
4316                maybe_precompile: None,
4317                ..Default::default()
4318            },
4319            idx: 2,
4320            ..Default::default()
4321        });
4322
4323        let mut identifier = RecordingIdentifier { queried: Vec::new() };
4324        decoder.identify_addresses(&arena, &mut identifier);
4325
4326        assert_eq!(identifier.queried, vec![regular_addr]);
4327    }
4328
4329    #[test]
4330    #[cfg(feature = "monad")]
4331    fn test_identify_addresses_does_not_skip_monad_precompiles_on_other_chains() {
4332        let decoder = CallTraceDecoderBuilder::new().with_chain_id(Some(1)).build();
4333
4334        let mut arena = CallTraceArena::default();
4335        let regular_addr = Address::from([0x42; 20]);
4336        arena.nodes_mut()[0].trace.address = regular_addr;
4337
4338        arena.nodes_mut().push(CallTraceNode {
4339            trace: CallTrace {
4340                address: monad_revm::staking::STAKING_ADDRESS,
4341                depth: 1,
4342                maybe_precompile: None,
4343                ..Default::default()
4344            },
4345            idx: 1,
4346            ..Default::default()
4347        });
4348        arena.nodes_mut().push(CallTraceNode {
4349            trace: CallTrace {
4350                address: monad_revm::reserve_balance::abi::RESERVE_BALANCE_ADDRESS,
4351                depth: 1,
4352                maybe_precompile: None,
4353                ..Default::default()
4354            },
4355            idx: 2,
4356            ..Default::default()
4357        });
4358
4359        let mut identifier = RecordingIdentifier { queried: Vec::new() };
4360        decoder.identify_addresses(&arena, &mut identifier);
4361
4362        assert_eq!(
4363            identifier.queried,
4364            vec![
4365                regular_addr,
4366                monad_revm::staking::STAKING_ADDRESS,
4367                monad_revm::reserve_balance::abi::RESERVE_BALANCE_ADDRESS,
4368            ]
4369        );
4370    }
4371
4372    #[test]
4373    #[cfg(feature = "monad")]
4374    fn execution_network_overrides_nested_source_monad_precompile_detection() {
4375        assert!(!precompiles::is_known_precompile(
4376            monad_revm::reserve_balance::abi::RESERVE_BALANCE_ADDRESS,
4377            Some(NetworkVariant::Ethereum.into()),
4378            Some(143),
4379            Some(MonadHardfork::MonadNine.into()),
4380        ));
4381    }
4382}