Skip to main content

foundry_evm_traces/decoder/
mod.rs

1use crate::{
2    CallTrace, CallTraceArena, CallTraceNode, DecodedCallData,
3    debug::DebugTraceIdentifier,
4    identifier::{IdentifiedAddress, LocalTraceIdentifier, SignaturesIdentifier, TraceIdentifier},
5};
6use alloy_dyn_abi::{DecodedEvent, DynSolValue, EventExt, FunctionExt, JsonAbiExt};
7use alloy_json_abi::{Error, Event, Function, JsonAbi};
8use alloy_primitives::{
9    Address, B256, LogData, Selector,
10    map::{HashMap, HashSet},
11};
12use foundry_common::{
13    ContractsByArtifact, SELECTOR_LEN, abi::get_indexed_event, fmt::format_token,
14    get_contract_name, selectors::SelectorKind,
15};
16use foundry_evm_core::{
17    abi::{Vm, console},
18    constants::{CALLER, CHEATCODE_ADDRESS, DEFAULT_CREATE2_DEPLOYER, HARDHAT_CONSOLE_ADDRESS},
19    decode::RevertDecoder,
20    precompiles::{
21        BLAKE_2F, BLS12_G1ADD, BLS12_G1MSM, BLS12_G2ADD, BLS12_G2MSM, BLS12_MAP_FP_TO_G1,
22        BLS12_MAP_FP2_TO_G2, BLS12_PAIRING_CHECK, EC_ADD, EC_MUL, EC_PAIRING, EC_RECOVER, IDENTITY,
23        MOD_EXP, P256_VERIFY, POINT_EVALUATION, RIPEMD_160, SHA_256,
24    },
25};
26use foundry_evm_hardforks::TempoHardfork;
27use itertools::Itertools;
28use revm_inspectors::tracing::types::{DecodedCallLog, DecodedCallTrace};
29use std::{collections::BTreeMap, sync::OnceLock};
30use tempo_contracts::precompiles::{
31    IAccountKeychain, IAddressRegistry, IFeeManager, IReceivePolicyGuard, ISignatureVerifier,
32    IStablecoinDEX, ITIP20ChannelReserve, ITIP20Factory, ITIP403Registry, IValidatorConfig,
33};
34use tempo_precompiles::{
35    ACCOUNT_KEYCHAIN_ADDRESS, ADDRESS_REGISTRY_ADDRESS, NONCE_PRECOMPILE_ADDRESS, PATH_USD_ADDRESS,
36    RECEIVE_POLICY_GUARD_ADDRESS, SIGNATURE_VERIFIER_ADDRESS, STABLECOIN_DEX_ADDRESS,
37    TIP_FEE_MANAGER_ADDRESS, TIP20_CHANNEL_RESERVE_ADDRESS, TIP20_FACTORY_ADDRESS,
38    TIP403_REGISTRY_ADDRESS, VALIDATOR_CONFIG_ADDRESS, nonce::INonce, tip20::ITIP20,
39};
40
41mod precompiles;
42
43/// Build a new [CallTraceDecoder].
44#[derive(Default)]
45#[must_use = "builders do nothing unless you call `build` on them"]
46pub struct CallTraceDecoderBuilder {
47    decoder: CallTraceDecoder,
48}
49
50impl CallTraceDecoderBuilder {
51    /// Create a new builder.
52    #[inline]
53    pub fn new() -> Self {
54        Self { decoder: CallTraceDecoder::new().clone() }
55    }
56
57    /// Add known labels to the decoder.
58    #[inline]
59    pub fn with_labels(mut self, labels: impl IntoIterator<Item = (Address, String)>) -> Self {
60        self.decoder.labels.extend(labels);
61        self
62    }
63
64    /// Add known errors to the decoder.
65    #[inline]
66    pub fn with_abi(mut self, abi: &JsonAbi) -> Self {
67        self.decoder.collect_abi(abi, None);
68        self
69    }
70
71    /// Add known contracts to the decoder.
72    #[inline]
73    pub fn with_known_contracts(mut self, contracts: &ContractsByArtifact) -> Self {
74        trace!(target: "evm::traces", len=contracts.len(), "collecting known contract ABIs");
75        for contract in contracts.values() {
76            self.decoder.collect_abi(&contract.abi, None);
77        }
78        self
79    }
80
81    /// Add known contracts to the decoder from a `LocalTraceIdentifier`.
82    #[inline]
83    pub fn with_local_identifier_abis(self, identifier: &LocalTraceIdentifier<'_>) -> Self {
84        self.with_known_contracts(identifier.contracts())
85    }
86
87    /// Sets the verbosity level of the decoder.
88    #[inline]
89    pub const fn with_verbosity(mut self, level: u8) -> Self {
90        self.decoder.verbosity = level;
91        self
92    }
93
94    /// Sets the signature identifier for events and functions.
95    #[inline]
96    pub fn with_signature_identifier(mut self, identifier: SignaturesIdentifier) -> Self {
97        self.decoder.signature_identifier = Some(identifier);
98        self
99    }
100
101    /// Sets the signature identifier for events and functions.
102    #[inline]
103    pub const fn with_label_disabled(mut self, disable_alias: bool) -> Self {
104        self.decoder.disable_labels = disable_alias;
105        self
106    }
107
108    /// Sets the chain ID for network-specific precompile detection.
109    #[inline]
110    pub const fn with_chain_id(mut self, chain_id: Option<u64>) -> Self {
111        self.decoder.chain_id = chain_id;
112        self
113    }
114
115    /// Sets the Tempo hardfork for hardfork-specific precompile detection.
116    #[inline]
117    pub fn with_tempo_hardfork(mut self, hardfork: Option<TempoHardfork>) -> Self {
118        self.decoder.tempo_hardfork = hardfork;
119        if hardfork.is_some_and(|hardfork| hardfork.is_t5()) {
120            self.decoder
121                .labels
122                .entry(TIP20_CHANNEL_RESERVE_ADDRESS)
123                .or_insert_with(|| "TIP20ChannelReserve".to_string());
124        }
125        if hardfork.is_some_and(|hardfork| hardfork.is_t6()) {
126            self.decoder
127                .labels
128                .entry(RECEIVE_POLICY_GUARD_ADDRESS)
129                .or_insert_with(|| "ReceivePolicyGuard".to_string());
130        }
131        self
132    }
133
134    /// Sets the debug identifier for the decoder.
135    #[inline]
136    pub fn with_debug_identifier(mut self, identifier: DebugTraceIdentifier) -> Self {
137        self.decoder.debug_identifier = Some(identifier);
138        self
139    }
140
141    /// Build the decoder.
142    #[inline]
143    pub fn build(self) -> CallTraceDecoder {
144        self.decoder
145    }
146}
147
148/// The call trace decoder.
149///
150/// The decoder collects address labels and ABIs from any number of [TraceIdentifier]s, which it
151/// then uses to decode the call trace.
152///
153/// Note that a call trace decoder is required for each new set of traces, since addresses in
154/// different sets might overlap.
155#[derive(Clone, Debug, Default)]
156pub struct CallTraceDecoder {
157    /// Addresses identified to be a specific contract.
158    ///
159    /// The values are in the form `"<artifact>:<contract>"`.
160    pub contracts: HashMap<Address, String>,
161    /// Address labels.
162    pub labels: HashMap<Address, String>,
163    /// Contract addresses that have a receive function.
164    pub receive_contracts: HashSet<Address>,
165    /// Contract addresses that have fallback functions, mapped to function selectors of that
166    /// contract.
167    pub fallback_contracts: HashMap<Address, HashSet<Selector>>,
168    /// Contract addresses that have do NOT have fallback functions, mapped to function selectors
169    /// of that contract.
170    pub non_fallback_contracts: HashMap<Address, HashSet<Selector>>,
171
172    /// All known functions.
173    pub functions: HashMap<Selector, Vec<Function>>,
174    /// Functions identified for a specific contract address.
175    pub functions_by_address: HashMap<Address, HashMap<Selector, Vec<Function>>>,
176    /// All known events.
177    ///
178    /// Key is: `(topics[0], topics.len() - 1)`.
179    pub events: BTreeMap<(B256, usize), Vec<Event>>,
180    /// Revert decoder. Contains all known custom errors.
181    pub revert_decoder: RevertDecoder,
182
183    /// A signature identifier for events and functions.
184    pub signature_identifier: Option<SignaturesIdentifier>,
185    /// Verbosity level
186    pub verbosity: u8,
187
188    /// Optional identifier of individual trace steps.
189    pub debug_identifier: Option<DebugTraceIdentifier>,
190
191    /// Disable showing of labels.
192    pub disable_labels: bool,
193
194    /// The chain ID, used to determine network-specific precompiles.
195    pub chain_id: Option<u64>,
196
197    /// The Tempo hardfork, used to determine hardfork-specific precompiles.
198    pub tempo_hardfork: Option<TempoHardfork>,
199}
200
201impl CallTraceDecoder {
202    /// Creates a new call trace decoder.
203    ///
204    /// The call trace decoder always knows how to decode calls to the cheatcode address, as well
205    /// as DSTest-style logs.
206    pub fn new() -> &'static Self {
207        // If you want to take arguments in this function, assign them to the fields of the cloned
208        // lazy instead of removing it
209        static INIT: OnceLock<CallTraceDecoder> = OnceLock::new();
210        INIT.get_or_init(Self::init)
211    }
212
213    #[instrument(name = "CallTraceDecoder::init", level = "debug")]
214    fn init() -> Self {
215        // Materialized once so the revert decoder can take references below.
216        let tempo_abis = [
217            IFeeManager::abi::contract(),
218            ITIP20::abi::contract(),
219            ITIP403Registry::abi::contract(),
220            ITIP20Factory::abi::contract(),
221            IStablecoinDEX::abi::contract(),
222            INonce::abi::contract(),
223            IValidatorConfig::abi::contract(),
224            IAccountKeychain::abi::contract(),
225            IAddressRegistry::abi::contract(),
226            ITIP20ChannelReserve::abi::contract(),
227            ISignatureVerifier::abi::contract(),
228            IReceivePolicyGuard::abi::contract(),
229        ];
230        Self {
231            contracts: Default::default(),
232            labels: HashMap::from_iter([
233                (CHEATCODE_ADDRESS, "VM".to_string()),
234                (HARDHAT_CONSOLE_ADDRESS, "console".to_string()),
235                (DEFAULT_CREATE2_DEPLOYER, "Create2Deployer".to_string()),
236                (CALLER, "DefaultSender".to_string()),
237                (EC_RECOVER, "ECRecover".to_string()),
238                (SHA_256, "SHA-256".to_string()),
239                (RIPEMD_160, "RIPEMD-160".to_string()),
240                (IDENTITY, "Identity".to_string()),
241                (MOD_EXP, "ModExp".to_string()),
242                (EC_ADD, "ECAdd".to_string()),
243                (EC_MUL, "ECMul".to_string()),
244                (EC_PAIRING, "ECPairing".to_string()),
245                (BLAKE_2F, "Blake2F".to_string()),
246                (POINT_EVALUATION, "PointEvaluation".to_string()),
247                (BLS12_G1ADD, "BLS12_G1ADD".to_string()),
248                (BLS12_G1MSM, "BLS12_G1MSM".to_string()),
249                (BLS12_G2ADD, "BLS12_G2ADD".to_string()),
250                (BLS12_G2MSM, "BLS12_G2MSM".to_string()),
251                (BLS12_PAIRING_CHECK, "BLS12_PAIRING_CHECK".to_string()),
252                (BLS12_MAP_FP_TO_G1, "BLS12_MAP_FP_TO_G1".to_string()),
253                (BLS12_MAP_FP2_TO_G2, "BLS12_MAP_FP2_TO_G2".to_string()),
254                (P256_VERIFY, "P256VERIFY".to_string()),
255                // Tempo
256                (TIP_FEE_MANAGER_ADDRESS, "FeeManager".to_string()),
257                (TIP403_REGISTRY_ADDRESS, "TIP403Registry".to_string()),
258                (TIP20_FACTORY_ADDRESS, "TIP20Factory".to_string()),
259                (STABLECOIN_DEX_ADDRESS, "StablecoinDex".to_string()),
260                (NONCE_PRECOMPILE_ADDRESS, "Nonce".to_string()),
261                (VALIDATOR_CONFIG_ADDRESS, "ValidatorConfig".to_string()),
262                (ACCOUNT_KEYCHAIN_ADDRESS, "AccountKeychain".to_string()),
263                (ADDRESS_REGISTRY_ADDRESS, "AddressRegistry".to_string()),
264                (TIP20_CHANNEL_RESERVE_ADDRESS, "TIP20ChannelReserve".to_string()),
265                (SIGNATURE_VERIFIER_ADDRESS, "SignatureVerifier".to_string()),
266                (RECEIVE_POLICY_GUARD_ADDRESS, "ReceivePolicyGuard".to_string()),
267                (PATH_USD_ADDRESS, "PathUSD".to_string()),
268            ]),
269            receive_contracts: Default::default(),
270            fallback_contracts: Default::default(),
271            non_fallback_contracts: Default::default(),
272
273            functions: console::hh::abi::functions()
274                .into_values()
275                .chain(Vm::abi::functions().into_values())
276                // Tempo
277                .chain(IFeeManager::abi::functions().into_values())
278                .chain(ITIP20::abi::functions().into_values())
279                .chain(ITIP403Registry::abi::functions().into_values())
280                .chain(ITIP20Factory::abi::functions().into_values())
281                .chain(IStablecoinDEX::abi::functions().into_values())
282                .chain(INonce::abi::functions().into_values())
283                .chain(IValidatorConfig::abi::functions().into_values())
284                .chain(IAccountKeychain::abi::functions().into_values())
285                .chain(IAddressRegistry::abi::functions().into_values())
286                .chain(ITIP20ChannelReserve::abi::functions().into_values())
287                .chain(ISignatureVerifier::abi::functions().into_values())
288                .chain(IReceivePolicyGuard::abi::functions().into_values())
289                .flatten()
290                .map(|func| (func.selector(), vec![func]))
291                .collect(),
292            functions_by_address: Default::default(),
293            events: console::ds::abi::events()
294                .into_values()
295                // Tempo
296                .chain(IFeeManager::abi::events().into_values())
297                .chain(ITIP20::abi::events().into_values())
298                .chain(ITIP403Registry::abi::events().into_values())
299                .chain(ITIP20Factory::abi::events().into_values())
300                .chain(IStablecoinDEX::abi::events().into_values())
301                .chain(INonce::abi::events().into_values())
302                .chain(IValidatorConfig::abi::events().into_values())
303                .chain(IAccountKeychain::abi::events().into_values())
304                .chain(IAddressRegistry::abi::events().into_values())
305                .chain(ITIP20ChannelReserve::abi::events().into_values())
306                .chain(ISignatureVerifier::abi::events().into_values())
307                .chain(IReceivePolicyGuard::abi::events().into_values())
308                .flatten()
309                .map(|event| ((event.selector(), indexed_inputs(&event)), vec![event]))
310                .collect(),
311            // Decode Tempo precompile custom errors by name in traces.
312            revert_decoder: RevertDecoder::new().with_abis(tempo_abis.iter()),
313
314            signature_identifier: None,
315            verbosity: 0,
316
317            debug_identifier: None,
318
319            disable_labels: false,
320
321            chain_id: None,
322
323            tempo_hardfork: None,
324        }
325    }
326
327    /// Clears all known addresses.
328    pub fn clear_addresses(&mut self) {
329        self.contracts.clear();
330
331        let default_labels = &Self::new().labels;
332        if self.labels.len() > default_labels.len() {
333            self.labels.clone_from(default_labels);
334        }
335
336        self.receive_contracts.clear();
337        self.fallback_contracts.clear();
338        self.non_fallback_contracts.clear();
339        self.functions_by_address.clear();
340    }
341
342    /// Identify unknown addresses in the specified call trace using the specified identifier.
343    ///
344    /// Unknown contracts are contracts that either lack a label or an ABI.
345    pub fn identify(&mut self, arena: &CallTraceArena, identifier: &mut impl TraceIdentifier) {
346        self.collect_identified_addresses(self.identify_addresses(arena, identifier));
347    }
348
349    /// Identify unknown addresses in the specified call trace using the specified identifier.
350    ///
351    /// Unknown contracts are contracts that either lack a label or an ABI.
352    pub fn identify_addresses<'a>(
353        &self,
354        arena: &CallTraceArena,
355        identifier: &'a mut impl TraceIdentifier,
356    ) -> Vec<IdentifiedAddress<'a>> {
357        let nodes = arena.nodes().iter().filter(|node| {
358            // Skip precompile addresses, they will never resolve externally.
359            if node.is_precompile()
360                || precompiles::is_known_precompile(
361                    node.trace.address,
362                    self.chain_id,
363                    self.tempo_hardfork,
364                )
365            {
366                return false;
367            }
368            let address = &node.trace.address;
369            !self.labels.contains_key(address) || !self.contracts.contains_key(address)
370        });
371        identifier.identify_addresses(&nodes.collect::<Vec<_>>())
372    }
373
374    /// Adds a single event to the decoder.
375    pub fn push_event(&mut self, event: Event) {
376        self.events.entry((event.selector(), indexed_inputs(&event))).or_default().push(event);
377    }
378
379    /// Adds a single function to the decoder.
380    pub fn push_function(&mut self, function: Function) {
381        let selector = function.selector();
382        let functions = self.functions.entry(selector).or_default();
383
384        if Self::push_function_to(functions, function) && functions.len() > 1 {
385            let function = functions.last().expect("function was just inserted");
386            let signature = function.signature();
387            trace!(target: "evm::traces", %selector, new=%signature, "duplicate function selector");
388        }
389    }
390
391    /// Adds a single function to the decoder for a specific contract address.
392    pub fn push_address_function(&mut self, address: Address, function: Function) {
393        let functions = self
394            .functions_by_address
395            .entry(address)
396            .or_default()
397            .entry(function.selector())
398            .or_default();
399        Self::push_function_to(functions, function);
400    }
401
402    fn push_function_to(functions: &mut Vec<Function>, function: Function) -> bool {
403        if functions.contains(&function) {
404            false
405        } else {
406            functions.push(function);
407            true
408        }
409    }
410
411    fn functions_for_selector(&self, address: Address, selector: &Selector) -> Option<&[Function]> {
412        self.functions_by_address
413            .get(&address)
414            .and_then(|functions| functions.get(selector))
415            .or_else(|| self.functions.get(selector))
416            .map(Vec::as_slice)
417    }
418
419    /// Selects the appropriate function from a list of functions with the same selector by
420    /// checking which one decodes the calldata.
421    ///
422    /// Address-scoped function lookup should happen before this to avoid using ABI metadata from a
423    /// different contract when multiple functions have the same input types.
424    fn select_contract_function<'a>(
425        &self,
426        functions: &'a [Function],
427        trace: &CallTrace,
428    ) -> &'a [Function] {
429        // When there are selector collisions, try to decode the calldata with each function
430        // to determine which one is actually being called. The correct function should
431        // decode successfully while the wrong ones will fail due to parameter type mismatches.
432        if functions.len() > 1 {
433            for (i, func) in functions.iter().enumerate() {
434                if trace.data.len() >= SELECTOR_LEN
435                    && func.abi_decode_input(&trace.data[SELECTOR_LEN..]).is_ok()
436                {
437                    return &functions[i..i + 1];
438                }
439            }
440        }
441        functions
442    }
443
444    /// Adds a single error to the decoder.
445    pub fn push_error(&mut self, error: Error) {
446        self.revert_decoder.push_error(error);
447    }
448
449    pub const fn without_label(&mut self, disable: bool) {
450        self.disable_labels = disable;
451    }
452
453    fn collect_identified_addresses(&mut self, mut addrs: Vec<IdentifiedAddress<'_>>) {
454        addrs.sort_by_key(|identity| identity.address);
455        addrs.dedup_by_key(|identity| identity.address);
456        if addrs.is_empty() {
457            return;
458        }
459
460        trace!(target: "evm::traces", len=addrs.len(), "collecting address identities");
461        for IdentifiedAddress { address, label, contract, abi, artifact_id: _ } in addrs {
462            let _span = trace_span!(target: "evm::traces", "identity", ?contract, ?label).entered();
463
464            if let Some(contract) = contract {
465                self.contracts.entry(address).or_insert(contract);
466            }
467
468            if let Some(label) = label.filter(|s| !s.is_empty()) {
469                self.labels.entry(address).or_insert(label);
470            }
471
472            if let Some(abi) = abi {
473                self.collect_abi(&abi, Some(address));
474            }
475        }
476    }
477
478    fn collect_abi(&mut self, abi: &JsonAbi, address: Option<Address>) {
479        let len = abi.len();
480        if len == 0 {
481            return;
482        }
483        trace!(target: "evm::traces", len, ?address, "collecting ABI");
484        for function in abi.functions() {
485            if let Some(address) = address {
486                self.push_address_function(address, function.clone());
487            }
488            self.push_function(function.clone());
489        }
490        for event in abi.events() {
491            self.push_event(event.clone());
492        }
493        for error in abi.errors() {
494            self.push_error(error.clone());
495        }
496        if let Some(address) = address {
497            if abi.receive.is_some() {
498                self.receive_contracts.insert(address);
499            }
500
501            if abi.fallback.is_some() {
502                self.fallback_contracts
503                    .insert(address, abi.functions().map(|f| f.selector()).collect());
504            } else {
505                self.non_fallback_contracts
506                    .insert(address, abi.functions().map(|f| f.selector()).collect());
507            }
508        }
509    }
510
511    /// Populates the traces with decoded data by mutating the
512    /// [CallTrace] in place. See [CallTraceDecoder::decode_function] and
513    /// [CallTraceDecoder::decode_event] for more details.
514    pub async fn populate_traces(&self, traces: &mut Vec<CallTraceNode>) {
515        for node in traces {
516            node.trace.decoded = Some(Box::new(self.decode_function(&node.trace).await));
517            for log in &mut node.logs {
518                log.decoded = Some(Box::new(self.decode_event(&log.raw_log).await));
519            }
520
521            if let Some(debug) = self.debug_identifier.as_ref()
522                && let Some(identified) = self.contracts.get(&node.trace.address)
523            {
524                debug.identify_node_steps(node, get_contract_name(identified))
525            }
526        }
527    }
528
529    /// Decodes a call trace.
530    pub async fn decode_function(&self, trace: &CallTrace) -> DecodedCallTrace {
531        let label =
532            if self.disable_labels { None } else { self.labels.get(&trace.address).cloned() };
533
534        if trace.kind.is_any_create() {
535            return DecodedCallTrace { label, ..Default::default() };
536        }
537
538        if let Some(trace) = precompiles::decode(trace, self.chain_id, self.tempo_hardfork) {
539            return trace;
540        }
541
542        let cdata = &trace.data;
543        if trace.address == DEFAULT_CREATE2_DEPLOYER {
544            return DecodedCallTrace {
545                label,
546                call_data: Some(DecodedCallData { signature: "create2".to_string(), args: vec![] }),
547                return_data: self.default_return_data(trace),
548            };
549        }
550
551        if is_abi_call_data(cdata) {
552            let selector = Selector::try_from(&cdata[..SELECTOR_LEN]).unwrap();
553            let mut identified_functions = Vec::new();
554            let functions = match self.functions_for_selector(trace.address, &selector) {
555                Some(functions) => functions,
556                None => {
557                    if let Some(identifier) = &self.signature_identifier
558                        && let Some(function) = identifier.identify_function(selector).await
559                    {
560                        identified_functions.push(function);
561                    }
562                    &identified_functions
563                }
564            };
565
566            // Check if unsupported fn selector: calldata dooes NOT point to one of its selectors +
567            // non-fallback contract + no receive
568            if let Some(contract_selectors) = self.non_fallback_contracts.get(&trace.address)
569                && !contract_selectors.contains(&selector)
570                && (!cdata.is_empty() || !self.receive_contracts.contains(&trace.address))
571            {
572                let return_data = if trace.success {
573                    None
574                } else {
575                    let revert_msg = self.revert_decoder.decode(&trace.output, trace.status);
576
577                    if trace.output.is_empty() || revert_msg.contains("EvmError: Revert") {
578                        Some(format!(
579                            "unrecognized function selector {} for contract {}, which has no fallback function.",
580                            selector, trace.address
581                        ))
582                    } else {
583                        Some(revert_msg)
584                    }
585                };
586
587                return if let Some(func) = functions.first() {
588                    DecodedCallTrace {
589                        label,
590                        call_data: Some(self.decode_function_input(trace, func)),
591                        return_data,
592                    }
593                } else {
594                    DecodedCallTrace {
595                        label,
596                        call_data: self.fallback_call_data(trace),
597                        return_data,
598                    }
599                };
600            }
601
602            let contract_functions = self.select_contract_function(functions, trace);
603            let [func, ..] = contract_functions else {
604                return DecodedCallTrace {
605                    label,
606                    call_data: self.fallback_call_data(trace),
607                    return_data: self.default_return_data(trace),
608                };
609            };
610
611            // If traced contract is a fallback contract, check if it has the decoded function.
612            // If not, then replace call data signature with `fallback`.
613            let mut call_data = self.decode_function_input(trace, func);
614            if let Some(fallback_functions) = self.fallback_contracts.get(&trace.address)
615                && !fallback_functions.contains(&selector)
616                && let Some(cd) = self.fallback_call_data(trace)
617            {
618                call_data.signature = cd.signature;
619            }
620
621            DecodedCallTrace {
622                label,
623                call_data: Some(call_data),
624                return_data: self.decode_function_output(trace, contract_functions),
625            }
626        } else {
627            DecodedCallTrace {
628                label,
629                call_data: self.fallback_call_data(trace),
630                return_data: self.default_return_data(trace),
631            }
632        }
633    }
634
635    /// Decodes a function's input into the given trace.
636    fn decode_function_input(&self, trace: &CallTrace, func: &Function) -> DecodedCallData {
637        let mut args = None;
638        if trace.data.len() >= SELECTOR_LEN {
639            if trace.address == CHEATCODE_ADDRESS {
640                // Try to decode cheatcode inputs in a more custom way
641                if let Some(v) = self.decode_cheatcode_inputs(func, &trace.data) {
642                    args = Some(v);
643                }
644            }
645
646            if args.is_none()
647                && let Ok(v) = func.abi_decode_input(&trace.data[SELECTOR_LEN..])
648            {
649                args = Some(v.iter().map(|value| self.format_value(value)).collect());
650            }
651        }
652
653        DecodedCallData { signature: func.signature(), args: args.unwrap_or_default() }
654    }
655
656    /// Custom decoding for cheatcode inputs.
657    fn decode_cheatcode_inputs(&self, func: &Function, data: &[u8]) -> Option<Vec<String>> {
658        match func.name.as_str() {
659            "expectRevert" => {
660                let decoded = match data.get(SELECTOR_LEN..) {
661                    Some(data) => func.abi_decode_input(data).ok(),
662                    None => None,
663                };
664                let Some(decoded) = decoded else {
665                    return Some(vec![self.revert_decoder.decode(data, None)]);
666                };
667                let Some(first) = decoded.first() else {
668                    return Some(vec![self.revert_decoder.decode(data, None)]);
669                };
670                let expected_revert = match first {
671                    DynSolValue::Bytes(bytes) => bytes.as_slice(),
672                    DynSolValue::FixedBytes(word, size) => &word[..*size],
673                    _ => return None,
674                };
675                Some(
676                    std::iter::once(self.revert_decoder.decode(expected_revert, None))
677                        .chain(decoded.iter().skip(1).map(|value| self.format_value(value)))
678                        .collect(),
679                )
680            }
681            "addr" | "createWallet" | "deriveKey" | "rememberKey" => {
682                // Redact private key in all cases
683                Some(vec!["<pk>".to_string()])
684            }
685            "broadcast" | "startBroadcast" => {
686                // Redact private key if defined
687                // broadcast(uint256) / startBroadcast(uint256)
688                (!func.inputs.is_empty() && func.inputs[0].ty == "uint256").then(|| vec!["<pk>".to_string()])
689            }
690            "getNonce" => {
691                // Redact private key if defined
692                // getNonce(Wallet)
693                (!func.inputs.is_empty() && func.inputs[0].ty == "tuple").then(|| vec!["<pk>".to_string()])
694            }
695            "sign" | "signP256" => {
696                let mut decoded = func.abi_decode_input(&data[SELECTOR_LEN..]).ok()?;
697
698                // Redact private key and replace in trace
699                // sign(uint256,bytes32) / signP256(uint256,bytes32) / sign(Wallet,bytes32)
700                if !decoded.is_empty() &&
701                    (func.inputs[0].ty == "uint256" || func.inputs[0].ty == "tuple")
702                {
703                    decoded[0] = DynSolValue::String("<pk>".to_string());
704                }
705
706                Some(decoded.iter().map(format_token).collect())
707            }
708            "signDelegation" | "signAndAttachDelegation" => {
709                let mut decoded = func.abi_decode_input(&data[SELECTOR_LEN..]).ok()?;
710                // Redact private key and replace in trace for
711                // signAndAttachDelegation(address implementation, uint256 privateKey)
712                // signDelegation(address implementation, uint256 privateKey)
713                decoded[1] = DynSolValue::String("<pk>".to_string());
714                Some(decoded.iter().map(format_token).collect())
715            }
716            "parseJson" |
717            "parseJsonUint" |
718            "parseJsonUintArray" |
719            "parseJsonInt" |
720            "parseJsonIntArray" |
721            "parseJsonString" |
722            "parseJsonStringArray" |
723            "parseJsonAddress" |
724            "parseJsonAddressArray" |
725            "parseJsonBool" |
726            "parseJsonBoolArray" |
727            "parseJsonBytes" |
728            "parseJsonBytesArray" |
729            "parseJsonBytes32" |
730            "parseJsonBytes32Array" |
731            "writeJson" |
732            // `keyExists` is being deprecated in favor of `keyExistsJson`. It will be removed in future versions.
733            "keyExists" |
734            "keyExistsJson" |
735            "serializeBool" |
736            "serializeUint" |
737            "serializeUintToHex" |
738            "serializeInt" |
739            "serializeAddress" |
740            "serializeBytes32" |
741            "serializeString" |
742            "serializeBytes" => {
743                if self.verbosity >= 5 {
744                    None
745                } else {
746                    let mut decoded = func.abi_decode_input(&data[SELECTOR_LEN..]).ok()?;
747                    let token = if func.name.as_str() == "parseJson" ||
748                        // `keyExists` is being deprecated in favor of `keyExistsJson`. It will be removed in future versions.
749                        func.name.as_str() == "keyExists" ||
750                        func.name.as_str() == "keyExistsJson"
751                    {
752                        "<JSON file>"
753                    } else {
754                        "<stringified JSON>"
755                    };
756                    decoded[0] = DynSolValue::String(token.to_string());
757                    Some(decoded.iter().map(format_token).collect())
758                }
759            }
760            s if s.contains("Toml") => {
761                if self.verbosity >= 5 {
762                    None
763                } else {
764                    let mut decoded = func.abi_decode_input(&data[SELECTOR_LEN..]).ok()?;
765                    let token = if func.name.as_str() == "parseToml" ||
766                        func.name.as_str() == "keyExistsToml"
767                    {
768                        "<TOML file>"
769                    } else {
770                        "<stringified TOML>"
771                    };
772                    decoded[0] = DynSolValue::String(token.to_string());
773                    Some(decoded.iter().map(format_token).collect())
774                }
775            }
776            "createFork" |
777            "createSelectFork" |
778            "rpc" => {
779                let mut decoded = func.abi_decode_input(&data[SELECTOR_LEN..]).ok()?;
780
781                // Redact RPC URL except if referenced by an alias
782                if !decoded.is_empty() && func.inputs[0].ty == "string" {
783                    let url_or_alias = decoded[0].as_str().unwrap_or_default();
784
785                    if url_or_alias.starts_with("http") || url_or_alias.starts_with("ws") {
786                        decoded[0] = DynSolValue::String("<rpc url>".to_string());
787                    }
788                } else {
789                    return None;
790                }
791
792                Some(decoded.iter().map(format_token).collect())
793            }
794            _ => None,
795        }
796    }
797
798    /// Decodes a function's output into the given trace.
799    fn decode_function_output(&self, trace: &CallTrace, funcs: &[Function]) -> Option<String> {
800        if !trace.success {
801            return self.default_return_data(trace);
802        }
803
804        if trace.address == CHEATCODE_ADDRESS
805            && let Some(decoded) = funcs.iter().find_map(|func| self.decode_cheatcode_outputs(func))
806        {
807            return Some(decoded);
808        }
809
810        if let Some(values) =
811            funcs.iter().find_map(|func| func.abi_decode_output(&trace.output).ok())
812        {
813            // Functions coming from an external database do not have any outputs specified,
814            // and will lead to returning an empty list of values.
815            if values.is_empty() {
816                return None;
817            }
818
819            return Some(
820                values.iter().map(|value| self.format_value(value)).format(", ").to_string(),
821            );
822        }
823
824        None
825    }
826
827    /// Custom decoding for cheatcode outputs.
828    fn decode_cheatcode_outputs(&self, func: &Function) -> Option<String> {
829        match func.name.as_str() {
830            s if s.starts_with("env") => Some("<env var value>"),
831            "createWallet" | "deriveKey" => Some("<pk>"),
832            "promptSecret" | "promptSecretUint" => Some("<secret>"),
833            "parseJson" if self.verbosity < 5 => Some("<encoded JSON value>"),
834            "readFile" if self.verbosity < 5 => Some("<file>"),
835            "rpcUrl" | "rpcUrls" | "rpcUrlStructs" => Some("<rpc url>"),
836            _ => None,
837        }
838        .map(Into::into)
839    }
840
841    #[track_caller]
842    fn fallback_call_data(&self, trace: &CallTrace) -> Option<DecodedCallData> {
843        let cdata = &trace.data;
844        let signature = if cdata.is_empty() && self.receive_contracts.contains(&trace.address) {
845            "receive()"
846        } else if self.fallback_contracts.contains_key(&trace.address) {
847            "fallback()"
848        } else {
849            return None;
850        }
851        .to_string();
852        let args = if cdata.is_empty() { Vec::new() } else { vec![cdata.to_string()] };
853        Some(DecodedCallData { signature, args })
854    }
855
856    /// The default decoded return data for a trace.
857    fn default_return_data(&self, trace: &CallTrace) -> Option<String> {
858        // For calls with status None or successful status, don't decode revert data
859        // This is due to trace.status is derived from the revm_interpreter::InstructionResult in
860        // revm-inspectors status will `None` post revm 27, as `InstructionResult::Continue` does
861        // not exists anymore.
862        if trace.status.is_none_or(|s| s.is_ok()) {
863            return None;
864        }
865        (!trace.success).then(|| self.revert_decoder.decode(&trace.output, trace.status))
866    }
867
868    /// Decodes an event.
869    pub async fn decode_event(&self, log: &LogData) -> DecodedCallLog {
870        let &[t0, ..] = log.topics() else { return DecodedCallLog { name: None, params: None } };
871
872        let mut events = Vec::new();
873        let events = match self.events.get(&(t0, log.topics().len() - 1)) {
874            Some(es) => es,
875            None => {
876                if let Some(identifier) = &self.signature_identifier
877                    && let Some(event) = identifier.identify_event(t0).await
878                {
879                    events.push(get_indexed_event(event, log));
880                }
881                &events
882            }
883        };
884        for event in events {
885            if let Ok(decoded) = event.decode_log(log) {
886                let params = reconstruct_params(event, &decoded);
887                return DecodedCallLog {
888                    name: Some(event.name.clone()),
889                    params: Some(
890                        params
891                            .into_iter()
892                            .zip(event.inputs.iter())
893                            .map(|(param, input)| {
894                                // undo patched names
895                                let name = input.name.clone();
896                                (name, self.format_value(&param))
897                            })
898                            .collect(),
899                    ),
900                };
901            }
902        }
903
904        DecodedCallLog { name: None, params: None }
905    }
906
907    /// Prefetches function and event signatures into the identifier cache
908    pub async fn prefetch_signatures(&self, nodes: &[CallTraceNode]) {
909        let Some(identifier) = &self.signature_identifier else { return };
910        let events = nodes
911            .iter()
912            .flat_map(|node| {
913                node.logs
914                    .iter()
915                    .map(|log| log.raw_log.topics())
916                    .filter(|&topics| {
917                        if let Some(&first) = topics.first()
918                            && self.events.contains_key(&(first, topics.len() - 1))
919                        {
920                            return false;
921                        }
922                        true
923                    })
924                    .filter_map(|topics| topics.first())
925            })
926            .copied();
927        let functions = nodes
928            .iter()
929            .filter(|&n| {
930                // Ignore known addresses.
931                if n.trace.address == DEFAULT_CREATE2_DEPLOYER
932                    || n.is_precompile()
933                    || precompiles::is_known_precompile(
934                        n.trace.address,
935                        self.chain_id,
936                        self.tempo_hardfork,
937                    )
938                {
939                    return false;
940                }
941                // Ignore non-ABI calldata.
942                if n.trace.kind.is_any_create() || !is_abi_call_data(&n.trace.data) {
943                    return false;
944                }
945                true
946            })
947            .filter_map(|n| n.trace.data.first_chunk().map(Selector::from))
948            .filter(|selector| !self.functions.contains_key(selector));
949        let selectors = events
950            .map(SelectorKind::Event)
951            .chain(functions.map(SelectorKind::Function))
952            .unique()
953            .collect::<Vec<_>>();
954        let _ = identifier.identify(&selectors).await;
955    }
956
957    /// Pretty-prints a value.
958    fn format_value(&self, value: &DynSolValue) -> String {
959        if let DynSolValue::Address(addr) = value
960            && let Some(label) = self.labels.get(addr)
961        {
962            return format!("{label}: [{addr}]");
963        }
964        format_token(value)
965    }
966}
967
968/// Returns `true` if the given function calldata (including function selector) is ABI-encoded.
969///
970/// This is a simple heuristic to avoid fetching non ABI-encoded selectors.
971fn is_abi_call_data(data: &[u8]) -> bool {
972    match data.len().cmp(&SELECTOR_LEN) {
973        std::cmp::Ordering::Less => false,
974        std::cmp::Ordering::Equal => true,
975        std::cmp::Ordering::Greater => is_abi_data(&data[SELECTOR_LEN..]),
976    }
977}
978
979/// Returns `true` if the given data is ABI-encoded.
980///
981/// See [`is_abi_call_data`] for more details.
982fn is_abi_data(data: &[u8]) -> bool {
983    let rem = data.len() % 32;
984    if rem == 0 || data.is_empty() {
985        return true;
986    }
987    // If the length is not a multiple of 32, also accept when the last remainder bytes are all 0.
988    data[data.len() - rem..].iter().all(|byte| *byte == 0)
989}
990
991/// Restore the order of the params of a decoded event,
992/// as Alloy returns the indexed and unindexed params separately.
993fn reconstruct_params(event: &Event, decoded: &DecodedEvent) -> Vec<DynSolValue> {
994    let mut indexed = 0;
995    let mut unindexed = 0;
996    let mut inputs = vec![];
997    for input in &event.inputs {
998        // Prevent panic of event `Transfer(from, to)` decoded with a signature
999        // `Transfer(address indexed from, address indexed to, uint256 indexed tokenId)` by making
1000        // sure the event inputs is not higher than decoded indexed / un-indexed values.
1001        if input.indexed && indexed < decoded.indexed.len() {
1002            inputs.push(decoded.indexed[indexed].clone());
1003            indexed += 1;
1004        } else if unindexed < decoded.body.len() {
1005            inputs.push(decoded.body[unindexed].clone());
1006            unindexed += 1;
1007        }
1008    }
1009
1010    inputs
1011}
1012
1013fn indexed_inputs(event: &Event) -> usize {
1014    event.inputs.iter().filter(|param| param.indexed).count()
1015}
1016
1017#[cfg(test)]
1018mod tests {
1019    use super::*;
1020    use alloy_primitives::{U256, address, aliases::U96, hex};
1021    use alloy_sol_types::{SolCall, SolEvent};
1022
1023    #[test]
1024    fn test_selector_collision_resolution() {
1025        use alloy_json_abi::Function;
1026        use alloy_primitives::Address;
1027
1028        // Create two functions with the same selector but different signatures
1029        let func1 = Function::parse("transferFrom(address,address,uint256)").unwrap();
1030        let func2 = Function::parse("gasprice_bit_ether(int128)").unwrap();
1031
1032        // Verify they have the same selector (this is the collision)
1033        assert_eq!(func1.selector(), func2.selector());
1034
1035        let functions = vec![func1, func2];
1036
1037        // Create a mock trace with calldata that matches func1
1038        let trace = CallTrace {
1039            address: Address::from([0x12; 20]),
1040            data: hex!("23b872dd000000000000000000000000000000000000000000000000000000000000012300000000000000000000000000000000000000000000000000000000000004560000000000000000000000000000000000000000000000000000000000000064").to_vec().into(),
1041            ..Default::default()
1042        };
1043
1044        let decoder = CallTraceDecoder::new();
1045        let result = decoder.select_contract_function(&functions, &trace);
1046
1047        // Should return only the function that can decode the calldata (func1)
1048        assert_eq!(result.len(), 1);
1049        assert_eq!(result[0].signature(), "transferFrom(address,address,uint256)");
1050    }
1051
1052    #[test]
1053    fn test_selector_collision_resolution_second_function() {
1054        use alloy_json_abi::Function;
1055        use alloy_primitives::Address;
1056
1057        // Create two functions with the same selector but different signatures
1058        let func1 = Function::parse("transferFrom(address,address,uint256)").unwrap();
1059        let func2 = Function::parse("gasprice_bit_ether(int128)").unwrap();
1060
1061        let functions = vec![func1, func2];
1062
1063        // Create a mock trace with calldata that matches func2
1064        let trace = CallTrace {
1065            address: Address::from([0x12; 20]),
1066            data: hex!("23b872dd0000000000000000000000000000000000000000000000000000000000000064")
1067                .to_vec()
1068                .into(),
1069            ..Default::default()
1070        };
1071
1072        let decoder = CallTraceDecoder::new();
1073        let result = decoder.select_contract_function(&functions, &trace);
1074
1075        // Should return only the function that can decode the calldata (func2)
1076        assert_eq!(result.len(), 1);
1077        assert_eq!(result[0].signature(), "gasprice_bit_ether(int128)");
1078    }
1079
1080    #[test]
1081    fn test_should_redact() {
1082        let decoder = CallTraceDecoder::new();
1083
1084        let expected_revert_bytes4 = vec![0xde, 0xad, 0xbe, 0xef];
1085        let expect_revert_bytes4_data = Function::parse("expectRevert(bytes4)")
1086            .unwrap()
1087            .abi_encode_input(&[DynSolValue::FixedBytes(
1088                B256::right_padding_from(expected_revert_bytes4.as_slice()),
1089                4,
1090            )])
1091            .unwrap();
1092
1093        let expected_revert_bytes = hex!(
1094            "08c379a000000000000000000000000000000000000000000000000000000000\
1095             0000002000000000000000000000000000000000000000000000000000000000\
1096             00000004626f6f6d000000000000000000000000000000000000000000000000"
1097        )
1098        .to_vec();
1099        let expect_revert_bytes_data = Function::parse("expectRevert(bytes)")
1100            .unwrap()
1101            .abi_encode_input(&[DynSolValue::Bytes(expected_revert_bytes.clone())])
1102            .unwrap();
1103
1104        let reverter = Address::from([0x11; 20]);
1105        let expect_revert_bytes4_address_data = Function::parse("expectRevert(bytes4,address)")
1106            .unwrap()
1107            .abi_encode_input(&[
1108                DynSolValue::FixedBytes(
1109                    B256::right_padding_from(expected_revert_bytes4.as_slice()),
1110                    4,
1111                ),
1112                DynSolValue::Address(reverter),
1113            ])
1114            .unwrap();
1115
1116        let count = 42_u64;
1117        let expect_revert_bytes_count_data = Function::parse("expectRevert(bytes,uint64)")
1118            .unwrap()
1119            .abi_encode_input(&[
1120                DynSolValue::Bytes(expected_revert_bytes.clone()),
1121                DynSolValue::Uint(alloy_primitives::U256::from(count), 64),
1122            ])
1123            .unwrap();
1124
1125        let expect_revert_bytes_address_count_data =
1126            Function::parse("expectRevert(bytes,address,uint64)")
1127                .unwrap()
1128                .abi_encode_input(&[
1129                    DynSolValue::Bytes(expected_revert_bytes.clone()),
1130                    DynSolValue::Address(reverter),
1131                    DynSolValue::Uint(alloy_primitives::U256::from(count), 64),
1132                ])
1133                .unwrap();
1134
1135        let expect_revert_runtime_data = expected_revert_bytes4.clone();
1136
1137        // [function_signature, data, expected]
1138        let cheatcode_input_test_cases = vec![
1139            // Should decode the expected revert payload, not full cheatcode calldata:
1140            (
1141                "expectRevert(bytes4)",
1142                expect_revert_bytes4_data,
1143                Some(vec![decoder.revert_decoder.decode(expected_revert_bytes4.as_slice(), None)]),
1144            ),
1145            (
1146                "expectRevert(bytes)",
1147                expect_revert_bytes_data,
1148                Some(vec![decoder.revert_decoder.decode(expected_revert_bytes.as_slice(), None)]),
1149            ),
1150            (
1151                "expectRevert(bytes4)",
1152                expect_revert_runtime_data.clone(),
1153                Some(vec![
1154                    decoder.revert_decoder.decode(expect_revert_runtime_data.as_slice(), None),
1155                ]),
1156            ),
1157            (
1158                "expectRevert(bytes4,address)",
1159                expect_revert_bytes4_address_data,
1160                Some(vec![
1161                    decoder.revert_decoder.decode(expected_revert_bytes4.as_slice(), None),
1162                    decoder.format_value(&DynSolValue::Address(reverter)),
1163                ]),
1164            ),
1165            (
1166                "expectRevert(bytes,uint64)",
1167                expect_revert_bytes_count_data,
1168                Some(vec![
1169                    decoder.revert_decoder.decode(expected_revert_bytes.as_slice(), None),
1170                    decoder
1171                        .format_value(&DynSolValue::Uint(alloy_primitives::U256::from(count), 64)),
1172                ]),
1173            ),
1174            (
1175                "expectRevert(bytes,address,uint64)",
1176                expect_revert_bytes_address_count_data,
1177                Some(vec![
1178                    decoder.revert_decoder.decode(expected_revert_bytes.as_slice(), None),
1179                    decoder.format_value(&DynSolValue::Address(reverter)),
1180                    decoder
1181                        .format_value(&DynSolValue::Uint(alloy_primitives::U256::from(count), 64)),
1182                ]),
1183            ),
1184            (
1185                "expectRevert()",
1186                expect_revert_runtime_data.clone(),
1187                Some(vec![
1188                    decoder.revert_decoder.decode(expect_revert_runtime_data.as_slice(), None),
1189                ]),
1190            ),
1191            // Should redact private key from traces in all cases:
1192            ("addr(uint256)", vec![], Some(vec!["<pk>".to_string()])),
1193            ("createWallet(string)", vec![], Some(vec!["<pk>".to_string()])),
1194            ("createWallet(uint256)", vec![], Some(vec!["<pk>".to_string()])),
1195            ("deriveKey(string,uint32)", vec![], Some(vec!["<pk>".to_string()])),
1196            ("deriveKey(string,string,uint32)", vec![], Some(vec!["<pk>".to_string()])),
1197            ("deriveKey(string,uint32,string)", vec![], Some(vec!["<pk>".to_string()])),
1198            ("deriveKey(string,string,uint32,string)", vec![], Some(vec!["<pk>".to_string()])),
1199            ("rememberKey(uint256)", vec![], Some(vec!["<pk>".to_string()])),
1200            //
1201            // Should redact private key from traces in specific cases with exceptions:
1202            ("broadcast(uint256)", vec![], Some(vec!["<pk>".to_string()])),
1203            ("broadcast()", vec![], None), // Ignore: `private key` is not passed.
1204            ("startBroadcast(uint256)", vec![], Some(vec!["<pk>".to_string()])),
1205            ("startBroadcast()", vec![], None), // Ignore: `private key` is not passed.
1206            ("getNonce((address,uint256,uint256,uint256))", vec![], Some(vec!["<pk>".to_string()])),
1207            ("getNonce(address)", vec![], None), // Ignore: `address` is public.
1208            //
1209            // Should redact private key and replace in trace in cases:
1210            (
1211                "sign(uint256,bytes32)",
1212                hex!(
1213                    "
1214                    e341eaa4
1215                    7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6
1216                    0000000000000000000000000000000000000000000000000000000000000000
1217                "
1218                )
1219                .to_vec(),
1220                Some(vec![
1221                    "\"<pk>\"".to_string(),
1222                    "0x0000000000000000000000000000000000000000000000000000000000000000"
1223                        .to_string(),
1224                ]),
1225            ),
1226            (
1227                "signP256(uint256,bytes32)",
1228                hex!(
1229                    "
1230                    83211b40
1231                    7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6
1232                    0000000000000000000000000000000000000000000000000000000000000000
1233                "
1234                )
1235                .to_vec(),
1236                Some(vec![
1237                    "\"<pk>\"".to_string(),
1238                    "0x0000000000000000000000000000000000000000000000000000000000000000"
1239                        .to_string(),
1240                ]),
1241            ),
1242            (
1243                // cast calldata "createFork(string)" "https://eth-mainnet.g.alchemy.com/v2/api_key"
1244                "createFork(string)",
1245                hex!(
1246                    "
1247                    31ba3498
1248                    0000000000000000000000000000000000000000000000000000000000000020
1249                    000000000000000000000000000000000000000000000000000000000000002c
1250                    68747470733a2f2f6574682d6d61696e6e65742e672e616c6368656d792e636f
1251                    6d2f76322f6170695f6b65790000000000000000000000000000000000000000
1252                    "
1253                )
1254                .to_vec(),
1255                Some(vec!["\"<rpc url>\"".to_string()]),
1256            ),
1257            (
1258                // cast calldata "createFork(string)" "wss://eth-mainnet.g.alchemy.com/v2/api_key"
1259                "createFork(string)",
1260                hex!(
1261                    "
1262                    31ba3498
1263                    0000000000000000000000000000000000000000000000000000000000000020
1264                    000000000000000000000000000000000000000000000000000000000000002a
1265                    7773733a2f2f6574682d6d61696e6e65742e672e616c6368656d792e636f6d2f
1266                    76322f6170695f6b657900000000000000000000000000000000000000000000
1267                    "
1268                )
1269                .to_vec(),
1270                Some(vec!["\"<rpc url>\"".to_string()]),
1271            ),
1272            (
1273                // cast calldata "createFork(string)" "mainnet"
1274                "createFork(string)",
1275                hex!(
1276                    "
1277                    31ba3498
1278                    0000000000000000000000000000000000000000000000000000000000000020
1279                    0000000000000000000000000000000000000000000000000000000000000007
1280                    6d61696e6e657400000000000000000000000000000000000000000000000000
1281                    "
1282                )
1283                .to_vec(),
1284                Some(vec!["\"mainnet\"".to_string()]),
1285            ),
1286            (
1287                // cast calldata "createFork(string,uint256)" "https://eth-mainnet.g.alchemy.com/v2/api_key" 1
1288                "createFork(string,uint256)",
1289                hex!(
1290                    "
1291                    6ba3ba2b
1292                    0000000000000000000000000000000000000000000000000000000000000040
1293                    0000000000000000000000000000000000000000000000000000000000000001
1294                    000000000000000000000000000000000000000000000000000000000000002c
1295                    68747470733a2f2f6574682d6d61696e6e65742e672e616c6368656d792e636f
1296                    6d2f76322f6170695f6b65790000000000000000000000000000000000000000
1297                "
1298                )
1299                .to_vec(),
1300                Some(vec!["\"<rpc url>\"".to_string(), "1".to_string()]),
1301            ),
1302            (
1303                // cast calldata "createFork(string,uint256)" "mainnet" 1
1304                "createFork(string,uint256)",
1305                hex!(
1306                    "
1307                    6ba3ba2b
1308                    0000000000000000000000000000000000000000000000000000000000000040
1309                    0000000000000000000000000000000000000000000000000000000000000001
1310                    0000000000000000000000000000000000000000000000000000000000000007
1311                    6d61696e6e657400000000000000000000000000000000000000000000000000
1312                "
1313                )
1314                .to_vec(),
1315                Some(vec!["\"mainnet\"".to_string(), "1".to_string()]),
1316            ),
1317            (
1318                // cast calldata "createFork(string,bytes32)" "https://eth-mainnet.g.alchemy.com/v2/api_key" 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
1319                "createFork(string,bytes32)",
1320                hex!(
1321                    "
1322                    7ca29682
1323                    0000000000000000000000000000000000000000000000000000000000000040
1324                    ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
1325                    000000000000000000000000000000000000000000000000000000000000002c
1326                    68747470733a2f2f6574682d6d61696e6e65742e672e616c6368656d792e636f
1327                    6d2f76322f6170695f6b65790000000000000000000000000000000000000000
1328                "
1329                )
1330                .to_vec(),
1331                Some(vec![
1332                    "\"<rpc url>\"".to_string(),
1333                    "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
1334                        .to_string(),
1335                ]),
1336            ),
1337            (
1338                // cast calldata "createFork(string,bytes32)" "mainnet"
1339                // 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
1340                "createFork(string,bytes32)",
1341                hex!(
1342                    "
1343                    7ca29682
1344                    0000000000000000000000000000000000000000000000000000000000000040
1345                    ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
1346                    0000000000000000000000000000000000000000000000000000000000000007
1347                    6d61696e6e657400000000000000000000000000000000000000000000000000
1348                "
1349                )
1350                .to_vec(),
1351                Some(vec![
1352                    "\"mainnet\"".to_string(),
1353                    "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
1354                        .to_string(),
1355                ]),
1356            ),
1357            (
1358                // cast calldata "createSelectFork(string)" "https://eth-mainnet.g.alchemy.com/v2/api_key"
1359                "createSelectFork(string)",
1360                hex!(
1361                    "
1362                    98680034
1363                    0000000000000000000000000000000000000000000000000000000000000020
1364                    000000000000000000000000000000000000000000000000000000000000002c
1365                    68747470733a2f2f6574682d6d61696e6e65742e672e616c6368656d792e636f
1366                    6d2f76322f6170695f6b65790000000000000000000000000000000000000000
1367                    "
1368                )
1369                .to_vec(),
1370                Some(vec!["\"<rpc url>\"".to_string()]),
1371            ),
1372            (
1373                // cast calldata "createSelectFork(string)" "mainnet"
1374                "createSelectFork(string)",
1375                hex!(
1376                    "
1377                    98680034
1378                    0000000000000000000000000000000000000000000000000000000000000020
1379                    0000000000000000000000000000000000000000000000000000000000000007
1380                    6d61696e6e657400000000000000000000000000000000000000000000000000
1381                    "
1382                )
1383                .to_vec(),
1384                Some(vec!["\"mainnet\"".to_string()]),
1385            ),
1386            (
1387                // cast calldata "createSelectFork(string,uint256)" "https://eth-mainnet.g.alchemy.com/v2/api_key" 1
1388                "createSelectFork(string,uint256)",
1389                hex!(
1390                    "
1391                    71ee464d
1392                    0000000000000000000000000000000000000000000000000000000000000040
1393                    0000000000000000000000000000000000000000000000000000000000000001
1394                    000000000000000000000000000000000000000000000000000000000000002c
1395                    68747470733a2f2f6574682d6d61696e6e65742e672e616c6368656d792e636f
1396                    6d2f76322f6170695f6b65790000000000000000000000000000000000000000
1397                "
1398                )
1399                .to_vec(),
1400                Some(vec!["\"<rpc url>\"".to_string(), "1".to_string()]),
1401            ),
1402            (
1403                // cast calldata "createSelectFork(string,uint256)" "mainnet" 1
1404                "createSelectFork(string,uint256)",
1405                hex!(
1406                    "
1407                    71ee464d
1408                    0000000000000000000000000000000000000000000000000000000000000040
1409                    0000000000000000000000000000000000000000000000000000000000000001
1410                    0000000000000000000000000000000000000000000000000000000000000007
1411                    6d61696e6e657400000000000000000000000000000000000000000000000000
1412                "
1413                )
1414                .to_vec(),
1415                Some(vec!["\"mainnet\"".to_string(), "1".to_string()]),
1416            ),
1417            (
1418                // cast calldata "createSelectFork(string,bytes32)" "https://eth-mainnet.g.alchemy.com/v2/api_key" 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
1419                "createSelectFork(string,bytes32)",
1420                hex!(
1421                    "
1422                    84d52b7a
1423                    0000000000000000000000000000000000000000000000000000000000000040
1424                    ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
1425                    000000000000000000000000000000000000000000000000000000000000002c
1426                    68747470733a2f2f6574682d6d61696e6e65742e672e616c6368656d792e636f
1427                    6d2f76322f6170695f6b65790000000000000000000000000000000000000000
1428                "
1429                )
1430                .to_vec(),
1431                Some(vec![
1432                    "\"<rpc url>\"".to_string(),
1433                    "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
1434                        .to_string(),
1435                ]),
1436            ),
1437            (
1438                // cast calldata "createSelectFork(string,bytes32)" "mainnet"
1439                // 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
1440                "createSelectFork(string,bytes32)",
1441                hex!(
1442                    "
1443                    84d52b7a
1444                    0000000000000000000000000000000000000000000000000000000000000040
1445                    ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
1446                    0000000000000000000000000000000000000000000000000000000000000007
1447                    6d61696e6e657400000000000000000000000000000000000000000000000000
1448                "
1449                )
1450                .to_vec(),
1451                Some(vec![
1452                    "\"mainnet\"".to_string(),
1453                    "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
1454                        .to_string(),
1455                ]),
1456            ),
1457            (
1458                // cast calldata "rpc(string,string,string)" "https://eth-mainnet.g.alchemy.com/v2/api_key" "eth_getBalance" "[\"0x551e7784778ef8e048e495df49f2614f84a4f1dc\",\"0x0\"]"
1459                "rpc(string,string,string)",
1460                hex!(
1461                    "
1462                    0199a220
1463                    0000000000000000000000000000000000000000000000000000000000000060
1464                    00000000000000000000000000000000000000000000000000000000000000c0
1465                    0000000000000000000000000000000000000000000000000000000000000100
1466                    000000000000000000000000000000000000000000000000000000000000002c
1467                    68747470733a2f2f6574682d6d61696e6e65742e672e616c6368656d792e636f
1468                    6d2f76322f6170695f6b65790000000000000000000000000000000000000000
1469                    000000000000000000000000000000000000000000000000000000000000000e
1470                    6574685f67657442616c616e6365000000000000000000000000000000000000
1471                    0000000000000000000000000000000000000000000000000000000000000034
1472                    5b22307835353165373738343737386566386530343865343935646634396632
1473                    363134663834613466316463222c22307830225d000000000000000000000000
1474                "
1475                )
1476                .to_vec(),
1477                Some(vec![
1478                    "\"<rpc url>\"".to_string(),
1479                    "\"eth_getBalance\"".to_string(),
1480                    "\"[\\\"0x551e7784778ef8e048e495df49f2614f84a4f1dc\\\",\\\"0x0\\\"]\""
1481                        .to_string(),
1482                ]),
1483            ),
1484            (
1485                // cast calldata "rpc(string,string,string)" "mainnet" "eth_getBalance"
1486                // "[\"0x551e7784778ef8e048e495df49f2614f84a4f1dc\",\"0x0\"]"
1487                "rpc(string,string,string)",
1488                hex!(
1489                    "
1490                    0199a220
1491                    0000000000000000000000000000000000000000000000000000000000000060
1492                    00000000000000000000000000000000000000000000000000000000000000a0
1493                    00000000000000000000000000000000000000000000000000000000000000e0
1494                    0000000000000000000000000000000000000000000000000000000000000007
1495                    6d61696e6e657400000000000000000000000000000000000000000000000000
1496                    000000000000000000000000000000000000000000000000000000000000000e
1497                    6574685f67657442616c616e6365000000000000000000000000000000000000
1498                    0000000000000000000000000000000000000000000000000000000000000034
1499                    5b22307835353165373738343737386566386530343865343935646634396632
1500                    363134663834613466316463222c22307830225d000000000000000000000000
1501                "
1502                )
1503                .to_vec(),
1504                Some(vec![
1505                    "\"mainnet\"".to_string(),
1506                    "\"eth_getBalance\"".to_string(),
1507                    "\"[\\\"0x551e7784778ef8e048e495df49f2614f84a4f1dc\\\",\\\"0x0\\\"]\""
1508                        .to_string(),
1509                ]),
1510            ),
1511        ];
1512
1513        // [function_signature, expected]
1514        let cheatcode_output_test_cases = vec![
1515            // Should redact private key on output in all cases:
1516            ("createWallet(string)", Some("<pk>".to_string())),
1517            ("deriveKey(string,uint32)", Some("<pk>".to_string())),
1518            // Should redact RPC URL if defined, except if referenced by an alias:
1519            ("rpcUrl(string)", Some("<rpc url>".to_string())),
1520            ("rpcUrls()", Some("<rpc url>".to_string())),
1521            ("rpcUrlStructs()", Some("<rpc url>".to_string())),
1522        ];
1523
1524        for (function_signature, data, expected) in cheatcode_input_test_cases {
1525            let function = Function::parse(function_signature).unwrap();
1526            let result = decoder.decode_cheatcode_inputs(&function, &data);
1527            assert_eq!(result, expected, "Input case failed for: {function_signature}");
1528        }
1529
1530        for (function_signature, expected) in cheatcode_output_test_cases {
1531            let function = Function::parse(function_signature).unwrap();
1532            let result = Some(decoder.decode_cheatcode_outputs(&function).unwrap_or_default());
1533            assert_eq!(result, expected, "Output case failed for: {function_signature}");
1534        }
1535    }
1536
1537    #[tokio::test]
1538    async fn test_tempo_decode_preserves_existing_labels() {
1539        let decoder = CallTraceDecoder::new();
1540        let trace = CallTrace { address: PATH_USD_ADDRESS, success: true, ..Default::default() };
1541
1542        let decoded = decoder.decode_function(&trace).await;
1543        assert_eq!(decoded.label.as_deref(), Some("PathUSD"));
1544    }
1545
1546    #[tokio::test]
1547    async fn test_t5_decode_does_not_synthesize_general_target_label() {
1548        let mut decoder = CallTraceDecoder::new().clone();
1549        decoder.chain_id = Some(4217);
1550        let trace = CallTrace {
1551            address: address!("0x0000000000000000000000000000000000000123"),
1552            depth: 0,
1553            success: true,
1554            ..Default::default()
1555        };
1556
1557        let decoded = decoder.decode_function(&trace).await;
1558        assert_eq!(decoded.label, None);
1559    }
1560
1561    #[tokio::test]
1562    async fn test_t5_tip20_logo_uri_calls_and_events_decode() {
1563        let decoder = CallTraceDecoder::new();
1564
1565        let call = ITIP20::setLogoURICall { newLogoURI: "https://example.com/logo.png".into() };
1566        let trace = CallTrace {
1567            address: PATH_USD_ADDRESS,
1568            data: call.abi_encode().into(),
1569            success: true,
1570            ..Default::default()
1571        };
1572        let decoded = decoder.decode_function(&trace).await;
1573        let call_data = decoded.call_data.expect("setLogoURI should decode");
1574        assert_eq!(call_data.signature, "setLogoURI(string)");
1575        assert_eq!(call_data.args, vec!["\"https://example.com/logo.png\"".to_string()]);
1576
1577        let call = ITIP20::logoURICall {};
1578        let trace = CallTrace {
1579            address: PATH_USD_ADDRESS,
1580            data: call.abi_encode().into(),
1581            success: true,
1582            ..Default::default()
1583        };
1584        let decoded = decoder.decode_function(&trace).await;
1585        assert_eq!(decoded.call_data.expect("logoURI should decode").signature, "logoURI()");
1586
1587        let event = ITIP20::LogoURIUpdated {
1588            updater: address!("0x0000000000000000000000000000000000000abc"),
1589            newLogoURI: "ipfs://logo".into(),
1590        };
1591        let decoded = decoder.decode_event(&event.encode_log_data()).await;
1592        assert_eq!(decoded.name.as_deref(), Some("LogoURIUpdated"));
1593        let params = decoded.params.expect("LogoURIUpdated params should decode");
1594        assert_eq!(params[0].0, "updater");
1595        assert!(
1596            params[0].1.to_ascii_lowercase().contains("0000000000000000000000000000000000000abc")
1597        );
1598        assert_eq!(params[1], ("newLogoURI".into(), "\"ipfs://logo\"".into()));
1599    }
1600
1601    #[tokio::test]
1602    async fn test_t5_tip20_factory_create_token_with_logo_decodes() {
1603        let decoder = CallTraceDecoder::new();
1604        let call = ITIP20Factory::createToken_1Call {
1605            name: "Example USD".into(),
1606            symbol: "xUSD".into(),
1607            currency: "USD".into(),
1608            quoteToken: PATH_USD_ADDRESS,
1609            admin: address!("0x0000000000000000000000000000000000000abc"),
1610            salt: B256::repeat_byte(0x11),
1611            logoURI: "https://example.com/xusd.png".into(),
1612        };
1613        let trace = CallTrace {
1614            address: TIP20_FACTORY_ADDRESS,
1615            data: call.abi_encode().into(),
1616            success: true,
1617            ..Default::default()
1618        };
1619        let decoded = decoder.decode_function(&trace).await;
1620        let call_data = decoded.call_data.expect("createToken overload should decode");
1621        assert_eq!(
1622            call_data.signature,
1623            "createToken(string,string,string,address,address,bytes32,string)"
1624        );
1625        assert_eq!(call_data.args[6], "\"https://example.com/xusd.png\"");
1626    }
1627
1628    #[tokio::test]
1629    async fn test_t5_stablecoin_dex_order_flipped_event_decodes() {
1630        let decoder = CallTraceDecoder::new();
1631        let event = IStablecoinDEX::OrderFlipped {
1632            orderId: 42,
1633            maker: address!("0x0000000000000000000000000000000000000abc"),
1634            token: PATH_USD_ADDRESS,
1635            amount: 1_000_000,
1636            isBid: false,
1637            tick: 100,
1638            flipTick: 100,
1639        };
1640        let decoded = decoder.decode_event(&event.encode_log_data()).await;
1641        assert_eq!(decoded.name.as_deref(), Some("OrderFlipped"));
1642        let params = decoded.params.expect("OrderFlipped params should decode");
1643        assert_eq!(params[0], ("orderId".into(), "42".into()));
1644        assert_eq!(params[4], ("isBid".into(), "false".into()));
1645        assert_eq!(params[5], ("tick".into(), "100".into()));
1646        assert_eq!(params[6], ("flipTick".into(), "100".into()));
1647    }
1648
1649    #[tokio::test]
1650    async fn test_t5_channel_reserve_call_and_event_decode() {
1651        let mut decoder = CallTraceDecoder::new().clone();
1652        decoder.chain_id = Some(4217);
1653
1654        let open = ITIP20ChannelReserve::openCall {
1655            payee: address!("0x0000000000000000000000000000000000000abc"),
1656            operator: Address::ZERO,
1657            token: PATH_USD_ADDRESS,
1658            deposit: U96::from(1_000_000u64),
1659            salt: B256::repeat_byte(0x22),
1660            authorizedSigner: Address::ZERO,
1661        };
1662        let trace = CallTrace {
1663            address: TIP20_CHANNEL_RESERVE_ADDRESS,
1664            data: open.abi_encode().into(),
1665            depth: 0,
1666            success: true,
1667            ..Default::default()
1668        };
1669        let decoded = decoder.decode_function(&trace).await;
1670        assert_eq!(decoded.label.as_deref(), Some("TIP20ChannelReserve"));
1671        assert_eq!(
1672            decoded.call_data.expect("open should decode").signature,
1673            "open(address,address,address,uint96,bytes32,address)"
1674        );
1675
1676        let transfer = ITIP20::transferCall {
1677            to: address!("0x0000000000000000000000000000000000000def"),
1678            amount: U256::from(1_000_000u64),
1679        };
1680        let trace = CallTrace {
1681            address: PATH_USD_ADDRESS,
1682            data: transfer.abi_encode().into(),
1683            depth: 0,
1684            success: true,
1685            ..Default::default()
1686        };
1687        let decoded = decoder.decode_function(&trace).await;
1688        assert_eq!(decoded.label.as_deref(), Some("PathUSD"));
1689        let json = serde_json::to_string(&decoded).expect("decoded trace serializes");
1690        assert!(json.contains(r#""label":"PathUSD""#));
1691        assert!(!json.contains("payment-lane"));
1692
1693        let balance_of = ITIP20::balanceOfCall {
1694            account: address!("0x0000000000000000000000000000000000000def"),
1695        };
1696        let trace = CallTrace {
1697            address: PATH_USD_ADDRESS,
1698            data: balance_of.abi_encode().into(),
1699            depth: 0,
1700            success: true,
1701            ..Default::default()
1702        };
1703        let decoded = decoder.decode_function(&trace).await;
1704        assert_eq!(decoded.label.as_deref(), Some("PathUSD"));
1705
1706        let event = ITIP20ChannelReserve::ChannelOpened {
1707            channelId: B256::repeat_byte(0x33),
1708            payer: address!("0x0000000000000000000000000000000000000123"),
1709            payee: address!("0x0000000000000000000000000000000000000abc"),
1710            operator: Address::ZERO,
1711            token: PATH_USD_ADDRESS,
1712            authorizedSigner: Address::ZERO,
1713            salt: B256::repeat_byte(0x22),
1714            expiringNonceHash: B256::repeat_byte(0x44),
1715            deposit: U96::from(1_000_000u64),
1716        };
1717        let decoded = decoder.decode_event(&event.encode_log_data()).await;
1718        assert_eq!(decoded.name.as_deref(), Some("ChannelOpened"));
1719        let params = decoded.params.expect("ChannelOpened params should decode");
1720        assert_eq!(params[0].0, "channelId");
1721        assert_eq!(params[8].0, "deposit");
1722        assert!(params[8].1.starts_with("1000000"));
1723    }
1724
1725    // A mock identifier that records which addresses it was asked to identify.
1726    struct RecordingIdentifier {
1727        queried: Vec<Address>,
1728    }
1729    impl TraceIdentifier for RecordingIdentifier {
1730        fn identify_addresses(&mut self, nodes: &[&CallTraceNode]) -> Vec<IdentifiedAddress<'_>> {
1731            self.queried.extend(nodes.iter().map(|n| n.trace.address));
1732            Vec::new()
1733        }
1734    }
1735
1736    #[test]
1737    fn test_identify_addresses_skips_evm_precompiles() {
1738        use foundry_evm_core::precompiles::SHA_256;
1739
1740        let decoder = CallTraceDecoder::new();
1741
1742        let mut arena = CallTraceArena::default();
1743        let regular_addr = Address::from([0x42; 20]);
1744        arena.nodes_mut()[0].trace.address = regular_addr;
1745
1746        // Standard EVM precompile flagged by the inspector.
1747        arena.nodes_mut().push(CallTraceNode {
1748            trace: CallTrace {
1749                address: SHA_256,
1750                depth: 1,
1751                maybe_precompile: Some(true),
1752                ..Default::default()
1753            },
1754            idx: 1,
1755            ..Default::default()
1756        });
1757
1758        // Standard EVM precompile NOT flagged, caught by is_known_precompile.
1759        arena.nodes_mut().push(CallTraceNode {
1760            trace: CallTrace {
1761                address: SHA_256,
1762                depth: 1,
1763                maybe_precompile: None,
1764                ..Default::default()
1765            },
1766            idx: 2,
1767            ..Default::default()
1768        });
1769
1770        let mut identifier = RecordingIdentifier { queried: Vec::new() };
1771        decoder.identify_addresses(&arena, &mut identifier);
1772
1773        assert_eq!(identifier.queried, vec![regular_addr]);
1774    }
1775
1776    #[test]
1777    fn test_identify_addresses_skips_tempo_precompiles() {
1778        use foundry_evm_core::tempo::{TEMPO_PRECOMPILE_ADDRESSES, TIP20_CHANNEL_RESERVE_ADDRESS};
1779
1780        // Decoder with Tempo chain ID (4217).
1781        let decoder = CallTraceDecoderBuilder::new()
1782            .with_chain_id(Some(4217))
1783            .with_tempo_hardfork(Some(TempoHardfork::T5))
1784            .build();
1785
1786        assert_eq!(
1787            decoder.labels.get(&TIP20_CHANNEL_RESERVE_ADDRESS),
1788            Some(&"TIP20ChannelReserve".to_string())
1789        );
1790
1791        let mut arena = CallTraceArena::default();
1792        let regular_addr = Address::from([0x42; 20]);
1793        arena.nodes_mut()[0].trace.address = regular_addr;
1794
1795        // Tempo precompile — not flagged by inspector, caught by is_known_precompile
1796        // only when chain_id is a Tempo chain.
1797        let tempo_precompile = TEMPO_PRECOMPILE_ADDRESSES[0];
1798        arena.nodes_mut().push(CallTraceNode {
1799            trace: CallTrace {
1800                address: tempo_precompile,
1801                depth: 1,
1802                maybe_precompile: None,
1803                ..Default::default()
1804            },
1805            idx: 1,
1806            ..Default::default()
1807        });
1808
1809        let mut identifier = RecordingIdentifier { queried: Vec::new() };
1810        decoder.identify_addresses(&arena, &mut identifier);
1811
1812        // On a Tempo chain, the Tempo precompile should be filtered out.
1813        assert_eq!(identifier.queried, vec![regular_addr]);
1814    }
1815
1816    #[test]
1817    fn test_tempo_hardfork_labels_do_not_clobber_user_labels() {
1818        use foundry_evm_core::tempo::TIP20_CHANNEL_RESERVE_ADDRESS;
1819
1820        let reserve_label = "UserReserve".to_string();
1821        let guard_label = "UserGuard".to_string();
1822        let decoder = CallTraceDecoderBuilder::new()
1823            .with_labels([
1824                (TIP20_CHANNEL_RESERVE_ADDRESS, reserve_label.clone()),
1825                (RECEIVE_POLICY_GUARD_ADDRESS, guard_label.clone()),
1826            ])
1827            .with_tempo_hardfork(Some(TempoHardfork::T6))
1828            .build();
1829
1830        assert_eq!(decoder.labels.get(&TIP20_CHANNEL_RESERVE_ADDRESS), Some(&reserve_label));
1831        assert_eq!(decoder.labels.get(&RECEIVE_POLICY_GUARD_ADDRESS), Some(&guard_label));
1832    }
1833
1834    #[test]
1835    fn test_tempo_hardfork_none_does_not_remove_user_reserve_label() {
1836        use foundry_evm_core::tempo::TIP20_CHANNEL_RESERVE_ADDRESS;
1837
1838        let reserve_label = "UserReserve".to_string();
1839        let decoder = CallTraceDecoderBuilder::new()
1840            .with_labels([(TIP20_CHANNEL_RESERVE_ADDRESS, reserve_label.clone())])
1841            .with_tempo_hardfork(None)
1842            .build();
1843
1844        assert_eq!(decoder.labels.get(&TIP20_CHANNEL_RESERVE_ADDRESS), Some(&reserve_label));
1845    }
1846
1847    #[tokio::test]
1848    async fn test_decode_receive_policy_guard_at_t6() {
1849        let function = Function::parse("claim(address,bytes)").unwrap();
1850        let data = function
1851            .abi_encode_input(&[
1852                DynSolValue::Address(Address::from([0x11; 20])),
1853                DynSolValue::Bytes(vec![0x12, 0x34]),
1854            ])
1855            .unwrap();
1856        let trace = CallTrace {
1857            address: RECEIVE_POLICY_GUARD_ADDRESS,
1858            data: data.into(),
1859            success: true,
1860            ..Default::default()
1861        };
1862
1863        let decoder = CallTraceDecoderBuilder::new()
1864            .with_chain_id(Some(4217))
1865            .with_tempo_hardfork(Some(TempoHardfork::T6))
1866            .build();
1867        let decoded = decoder.decode_function(&trace).await;
1868
1869        assert_eq!(decoded.label, Some("ReceivePolicyGuard".to_string()));
1870        assert_eq!(decoded.call_data.unwrap().signature, "claim(address,bytes)");
1871    }
1872
1873    #[test]
1874    fn test_identify_addresses_does_not_skip_future_tempo_precompiles() {
1875        use foundry_evm_core::tempo::TIP20_CHANNEL_RESERVE_ADDRESS;
1876
1877        let decoder = CallTraceDecoderBuilder::new()
1878            .with_chain_id(Some(4217))
1879            .with_tempo_hardfork(Some(TempoHardfork::T4))
1880            .build();
1881
1882        let mut arena = CallTraceArena::default();
1883        let regular_addr = Address::from([0x42; 20]);
1884        arena.nodes_mut()[0].trace.address = regular_addr;
1885
1886        arena.nodes_mut().push(CallTraceNode {
1887            trace: CallTrace {
1888                address: TIP20_CHANNEL_RESERVE_ADDRESS,
1889                depth: 1,
1890                maybe_precompile: None,
1891                ..Default::default()
1892            },
1893            idx: 1,
1894            ..Default::default()
1895        });
1896
1897        let mut identifier = RecordingIdentifier { queried: Vec::new() };
1898        decoder.identify_addresses(&arena, &mut identifier);
1899
1900        assert_eq!(identifier.queried, vec![regular_addr, TIP20_CHANNEL_RESERVE_ADDRESS]);
1901    }
1902
1903    #[test]
1904    fn test_identify_addresses_does_not_skip_tempo_precompiles_on_other_chains() {
1905        use foundry_evm_core::tempo::TEMPO_PRECOMPILE_ADDRESSES;
1906
1907        // Decoder with Ethereum mainnet chain ID (1).
1908        let mut decoder = CallTraceDecoder::new().clone();
1909        decoder.chain_id = Some(1);
1910
1911        let mut arena = CallTraceArena::default();
1912        let regular_addr = Address::from([0x42; 20]);
1913        arena.nodes_mut()[0].trace.address = regular_addr;
1914
1915        let tempo_precompile = TEMPO_PRECOMPILE_ADDRESSES[0];
1916        arena.nodes_mut().push(CallTraceNode {
1917            trace: CallTrace {
1918                address: tempo_precompile,
1919                depth: 1,
1920                maybe_precompile: None,
1921                ..Default::default()
1922            },
1923            idx: 1,
1924            ..Default::default()
1925        });
1926
1927        let mut identifier = RecordingIdentifier { queried: Vec::new() };
1928        decoder.identify_addresses(&arena, &mut identifier);
1929
1930        // On Ethereum, Tempo precompile addresses are regular contracts — should NOT be filtered.
1931        assert_eq!(identifier.queried, vec![regular_addr, tempo_precompile]);
1932    }
1933}