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::{DecodedEvent, DynSolValue, EventExt, FunctionExt, JsonAbiExt};
7use alloy_json_abi::{Constructor, Error, Event, Function, JsonAbi};
8use alloy_primitives::{
9    Address, B256, LogData, Selector, U256,
10    map::{AddressHashMap, HashMap, HashSet},
11};
12use alloy_sol_types::SolValue;
13use foundry_common::{
14    ContractsByArtifact, SELECTOR_LEN, abi::get_indexed_event, fmt::format_token,
15    get_contract_name, selectors::SelectorKind,
16};
17use foundry_config::TracingConfig;
18use foundry_evm_core::{
19    abi::{Vm, console},
20    constants::{CALLER, CHEATCODE_ADDRESS, DEFAULT_CREATE2_DEPLOYER, HARDHAT_CONSOLE_ADDRESS},
21    decode::RevertDecoder,
22    precompiles::{
23        BLAKE_2F, BLS12_G1ADD, BLS12_G1MSM, BLS12_G2ADD, BLS12_G2MSM, BLS12_MAP_FP_TO_G1,
24        BLS12_MAP_FP2_TO_G2, BLS12_PAIRING_CHECK, EC_ADD, EC_MUL, EC_PAIRING, EC_RECOVER, IDENTITY,
25        MOD_EXP, P256_VERIFY, POINT_EVALUATION, RIPEMD_160, SHA_256,
26    },
27};
28use foundry_evm_hardforks::TempoHardfork;
29use itertools::Itertools;
30use revm::{bytecode::opcode::OpCode, interpreter::InstructionResult};
31use revm_inspectors::tracing::types::{DecodedCallLog, DecodedCallTrace};
32
33use std::{collections::BTreeMap, sync::OnceLock};
34use tempo_contracts::precompiles::{
35    CURRENT_COMMITTEE_ADDRESS, IAccountKeychain, IAddressRegistry, ICurrentCommittee, IFeeManager,
36    IReceivePolicyGuard, ISignatureVerifier, IStablecoinDEX, IStorageCredits, ITIP20ChannelReserve,
37    ITIP20Factory, ITIP403Registry, IValidatorConfig,
38};
39use tempo_precompiles::{
40    ACCOUNT_KEYCHAIN_ADDRESS, ADDRESS_REGISTRY_ADDRESS, NONCE_PRECOMPILE_ADDRESS, PATH_USD_ADDRESS,
41    RECEIVE_POLICY_GUARD_ADDRESS, SIGNATURE_VERIFIER_ADDRESS, STABLECOIN_DEX_ADDRESS,
42    STORAGE_CREDITS_ADDRESS, TIP_FEE_MANAGER_ADDRESS, TIP20_CHANNEL_RESERVE_ADDRESS,
43    TIP20_FACTORY_ADDRESS, TIP403_REGISTRY_ADDRESS, VALIDATOR_CONFIG_ADDRESS, nonce::INonce,
44    tip20::ITIP20,
45};
46
47pub(crate) mod precompiles;
48
49/// Build a new [CallTraceDecoder].
50#[derive(Default)]
51#[must_use = "builders do nothing unless you call `build` on them"]
52pub struct CallTraceDecoderBuilder {
53    decoder: CallTraceDecoder,
54}
55
56impl CallTraceDecoderBuilder {
57    /// Create a new builder.
58    #[inline]
59    pub fn new() -> Self {
60        Self { decoder: CallTraceDecoder::new().clone() }
61    }
62
63    /// Add known labels to the decoder.
64    #[inline]
65    pub fn with_labels(mut self, labels: impl IntoIterator<Item = (Address, String)>) -> Self {
66        self.decoder.labels.extend(labels);
67        self
68    }
69
70    /// Add known errors to the decoder.
71    #[inline]
72    pub fn with_abi(mut self, abi: &JsonAbi) -> Self {
73        self.decoder.collect_abi(abi, None);
74        self
75    }
76
77    /// Add known contracts to the decoder.
78    #[inline]
79    pub fn with_known_contracts(mut self, contracts: &ContractsByArtifact) -> Self {
80        trace!(target: "evm::traces", len=contracts.len(), "collecting known contract ABIs");
81        for contract in contracts.values() {
82            self.decoder.collect_abi(&contract.abi, None);
83        }
84        self
85    }
86
87    /// Add known contracts to the decoder from a `LocalTraceIdentifier`.
88    #[inline]
89    pub fn with_local_identifier_abis(self, identifier: &LocalTraceIdentifier<'_>) -> Self {
90        self.with_known_contracts(identifier.contracts())
91    }
92
93    /// Sets the verbosity level of the decoder.
94    #[inline]
95    pub const fn with_verbosity(mut self, level: u8) -> Self {
96        self.decoder.verbosity = level;
97        self
98    }
99
100    /// Applies trace rendering settings.
101    #[inline]
102    pub fn with_tracing_config(mut self, config: &TracingConfig) -> Self {
103        self.decoder.labels.extend(config.labels.clone());
104        self.decoder.verbosity = config.verbosity;
105        self.decoder.disable_labels = config.disable_labels;
106        self.decoder.compact_labels = config.compact_labels;
107        self
108    }
109
110    /// Sets the signature identifier for events and functions.
111    #[inline]
112    pub fn with_signature_identifier(mut self, identifier: SignaturesIdentifier) -> Self {
113        self.decoder.signature_identifier = Some(identifier);
114        self
115    }
116
117    /// Sets the signature identifier for events and functions.
118    #[inline]
119    pub const fn with_label_disabled(mut self, disable_alias: bool) -> Self {
120        self.decoder.disable_labels = disable_alias;
121        self
122    }
123
124    /// Sets the chain ID for network-specific precompile detection.
125    #[inline]
126    pub const fn with_chain_id(mut self, chain_id: Option<u64>) -> Self {
127        self.decoder.chain_id = chain_id;
128        self
129    }
130
131    /// Sets the Tempo hardfork for hardfork-specific precompile detection.
132    #[inline]
133    pub fn with_tempo_hardfork(mut self, hardfork: Option<TempoHardfork>) -> Self {
134        self.decoder.tempo_hardfork = hardfork;
135        if hardfork.is_some_and(|hardfork| hardfork.is_t5()) {
136            self.decoder
137                .labels
138                .entry(TIP20_CHANNEL_RESERVE_ADDRESS)
139                .or_insert_with(|| "TIP20ChannelReserve".to_string());
140        }
141        if hardfork.is_some_and(|hardfork| hardfork.is_t6()) {
142            self.decoder
143                .labels
144                .entry(RECEIVE_POLICY_GUARD_ADDRESS)
145                .or_insert_with(|| "ReceivePolicyGuard".to_string());
146        }
147        self
148    }
149
150    /// Hides addresses in trace parameters when a label is available.
151    #[inline]
152    pub const fn with_compact_labels(mut self, compact: bool) -> Self {
153        self.decoder.compact_labels = compact;
154        self
155    }
156
157    /// Sets the debug identifier for the decoder.
158    #[inline]
159    pub fn with_debug_identifier(mut self, identifier: DebugTraceIdentifier) -> Self {
160        self.decoder.debug_identifier = Some(identifier);
161        self
162    }
163
164    /// Build the decoder.
165    #[inline]
166    pub fn build(mut self) -> CallTraceDecoder {
167        self.decoder.base_labels = self.decoder.labels.clone();
168        self.decoder
169    }
170}
171
172/// The call trace decoder.
173///
174/// The decoder collects address labels and ABIs from any number of [TraceIdentifier]s, which it
175/// then uses to decode the call trace.
176///
177/// Note that a call trace decoder is required for each new set of traces, since addresses in
178/// different sets might overlap.
179#[derive(Clone, Debug, Default)]
180pub struct CallTraceDecoder {
181    /// Addresses identified to be a specific contract.
182    ///
183    /// The values are in the form `"<artifact>:<contract>"`.
184    pub contracts: HashMap<Address, String>,
185    /// Address labels.
186    pub labels: HashMap<Address, String>,
187    /// Labels configured when the decoder was built.
188    base_labels: HashMap<Address, String>,
189    /// Contract addresses that have a receive function.
190    pub receive_contracts: HashSet<Address>,
191    /// Contract addresses that have fallback functions, mapped to function selectors of that
192    /// contract.
193    pub fallback_contracts: HashMap<Address, HashSet<Selector>>,
194    /// Contract addresses that have do NOT have fallback functions, mapped to function selectors
195    /// of that contract.
196    pub non_fallback_contracts: HashMap<Address, HashSet<Selector>>,
197
198    /// All known functions.
199    pub functions: HashMap<Selector, Vec<Function>>,
200    /// Functions identified for a specific contract address.
201    pub functions_by_address: HashMap<Address, HashMap<Selector, Vec<Function>>>,
202    /// Constructors identified for a specific contract address.
203    pub constructors_by_address: HashMap<Address, Constructor>,
204    /// Creation input offsets where ABI-encoded constructor arguments begin.
205    pub constructor_args_offsets: HashMap<Address, usize>,
206    /// All known events.
207    ///
208    /// Key is: `(topics[0], topics.len() - 1)`.
209    pub events: BTreeMap<(B256, usize), Vec<Event>>,
210    /// Revert decoder. Contains all known custom errors.
211    pub revert_decoder: RevertDecoder,
212
213    /// A signature identifier for events and functions.
214    pub signature_identifier: Option<SignaturesIdentifier>,
215    /// Verbosity level
216    pub verbosity: u8,
217
218    /// Optional identifier of individual trace steps.
219    pub debug_identifier: Option<DebugTraceIdentifier>,
220
221    /// Disable showing of labels.
222    pub disable_labels: bool,
223
224    /// The chain ID, used to determine network-specific precompiles.
225    pub chain_id: Option<u64>,
226
227    /// Detailed opcodes for analysis.
228    pub opcodes: Vec<OpCode>,
229
230    /// The Tempo hardfork, used to determine hardfork-specific precompiles.
231    pub tempo_hardfork: Option<TempoHardfork>,
232
233    /// Hide addresses when a label is available, showing only the label.
234    pub compact_labels: bool,
235}
236
237impl CallTraceDecoder {
238    /// Creates a new call trace decoder.
239    ///
240    /// The call trace decoder always knows how to decode calls to the cheatcode address, as well
241    /// as DSTest-style logs.
242    pub fn new() -> &'static Self {
243        // If you want to take arguments in this function, assign them to the fields of the cloned
244        // lazy instead of removing it
245        static INIT: OnceLock<CallTraceDecoder> = OnceLock::new();
246        INIT.get_or_init(Self::init)
247    }
248
249    #[instrument(name = "CallTraceDecoder::init", level = "debug")]
250    fn init() -> Self {
251        // Materialized once so the revert decoder can take references below.
252        let tempo_abis = [
253            IFeeManager::abi::contract(),
254            ITIP20::abi::contract(),
255            ITIP403Registry::abi::contract(),
256            ITIP20Factory::abi::contract(),
257            IStablecoinDEX::abi::contract(),
258            IStorageCredits::abi::contract(),
259            INonce::abi::contract(),
260            IValidatorConfig::abi::contract(),
261            IAccountKeychain::abi::contract(),
262            IAddressRegistry::abi::contract(),
263            ITIP20ChannelReserve::abi::contract(),
264            ISignatureVerifier::abi::contract(),
265            IReceivePolicyGuard::abi::contract(),
266        ];
267        Self {
268            contracts: Default::default(),
269            labels: HashMap::from_iter([
270                (CHEATCODE_ADDRESS, "VM".to_string()),
271                (HARDHAT_CONSOLE_ADDRESS, "console".to_string()),
272                (DEFAULT_CREATE2_DEPLOYER, "Create2Deployer".to_string()),
273                (CALLER, "DefaultSender".to_string()),
274                (EC_RECOVER, "ECRecover".to_string()),
275                (SHA_256, "SHA-256".to_string()),
276                (RIPEMD_160, "RIPEMD-160".to_string()),
277                (IDENTITY, "Identity".to_string()),
278                (MOD_EXP, "ModExp".to_string()),
279                (EC_ADD, "ECAdd".to_string()),
280                (EC_MUL, "ECMul".to_string()),
281                (EC_PAIRING, "ECPairing".to_string()),
282                (BLAKE_2F, "Blake2F".to_string()),
283                (POINT_EVALUATION, "PointEvaluation".to_string()),
284                (BLS12_G1ADD, "BLS12_G1ADD".to_string()),
285                (BLS12_G1MSM, "BLS12_G1MSM".to_string()),
286                (BLS12_G2ADD, "BLS12_G2ADD".to_string()),
287                (BLS12_G2MSM, "BLS12_G2MSM".to_string()),
288                (BLS12_PAIRING_CHECK, "BLS12_PAIRING_CHECK".to_string()),
289                (BLS12_MAP_FP_TO_G1, "BLS12_MAP_FP_TO_G1".to_string()),
290                (BLS12_MAP_FP2_TO_G2, "BLS12_MAP_FP2_TO_G2".to_string()),
291                (P256_VERIFY, "P256VERIFY".to_string()),
292                // Tempo
293                (TIP_FEE_MANAGER_ADDRESS, "FeeManager".to_string()),
294                (TIP403_REGISTRY_ADDRESS, "TIP403Registry".to_string()),
295                (TIP20_FACTORY_ADDRESS, "TIP20Factory".to_string()),
296                (STABLECOIN_DEX_ADDRESS, "StablecoinDex".to_string()),
297                (NONCE_PRECOMPILE_ADDRESS, "Nonce".to_string()),
298                (VALIDATOR_CONFIG_ADDRESS, "ValidatorConfig".to_string()),
299                (ACCOUNT_KEYCHAIN_ADDRESS, "AccountKeychain".to_string()),
300                (ADDRESS_REGISTRY_ADDRESS, "AddressRegistry".to_string()),
301                (TIP20_CHANNEL_RESERVE_ADDRESS, "TIP20ChannelReserve".to_string()),
302                (SIGNATURE_VERIFIER_ADDRESS, "SignatureVerifier".to_string()),
303                (RECEIVE_POLICY_GUARD_ADDRESS, "ReceivePolicyGuard".to_string()),
304                (STORAGE_CREDITS_ADDRESS, "StorageCredits".to_string()),
305                (PATH_USD_ADDRESS, "PathUSD".to_string()),
306            ]),
307            base_labels: Default::default(),
308            receive_contracts: Default::default(),
309            fallback_contracts: Default::default(),
310            non_fallback_contracts: Default::default(),
311
312            functions: console::hh::abi::functions()
313                .into_values()
314                .chain(Vm::abi::functions().into_values())
315                // Tempo
316                .chain(IFeeManager::abi::functions().into_values())
317                // `IStorageCredits` shares the `balanceOf(address)` selector with `ITIP20`, so it
318                // must be chained first to keep `ITIP20`'s `uint256` return as the global fallback.
319                .chain(IStorageCredits::abi::functions().into_values())
320                .chain(ITIP20::abi::functions().into_values())
321                .chain(ITIP403Registry::abi::functions().into_values())
322                .chain(ITIP20Factory::abi::functions().into_values())
323                .chain(IStablecoinDEX::abi::functions().into_values())
324                .chain(INonce::abi::functions().into_values())
325                .chain(IValidatorConfig::abi::functions().into_values())
326                .chain(IAccountKeychain::abi::functions().into_values())
327                .chain(IAddressRegistry::abi::functions().into_values())
328                .chain(ITIP20ChannelReserve::abi::functions().into_values())
329                .chain(ISignatureVerifier::abi::functions().into_values())
330                .chain(IReceivePolicyGuard::abi::functions().into_values())
331                .flatten()
332                .map(|func| (func.selector(), vec![func]))
333                .collect(),
334            functions_by_address: Default::default(),
335            constructors_by_address: Default::default(),
336            constructor_args_offsets: Default::default(),
337            events: console::ds::abi::events()
338                .into_values()
339                // Tempo
340                .chain(IFeeManager::abi::events().into_values())
341                .chain(ITIP20::abi::events().into_values())
342                .chain(ITIP403Registry::abi::events().into_values())
343                .chain(ITIP20Factory::abi::events().into_values())
344                .chain(IStablecoinDEX::abi::events().into_values())
345                .chain(INonce::abi::events().into_values())
346                .chain(IValidatorConfig::abi::events().into_values())
347                .chain(IAccountKeychain::abi::events().into_values())
348                .chain(IAddressRegistry::abi::events().into_values())
349                .chain(ITIP20ChannelReserve::abi::events().into_values())
350                .chain(ISignatureVerifier::abi::events().into_values())
351                .chain(IReceivePolicyGuard::abi::events().into_values())
352                .flatten()
353                .map(|event| ((event.selector(), indexed_inputs(&event)), vec![event]))
354                .collect(),
355            // Decode Tempo precompile custom errors by name in traces.
356            revert_decoder: RevertDecoder::new().with_abis(tempo_abis.iter()),
357
358            signature_identifier: None,
359            verbosity: 0,
360
361            debug_identifier: None,
362
363            disable_labels: false,
364
365            chain_id: None,
366
367            opcodes: Vec::new(),
368
369            tempo_hardfork: None,
370
371            compact_labels: false,
372        }
373    }
374
375    /// Clears all known addresses.
376    pub fn clear_addresses(&mut self) {
377        self.contracts.clear();
378
379        if self.base_labels.is_empty() {
380            self.labels.clone_from(&Self::new().labels);
381        } else {
382            self.labels.clone_from(&self.base_labels);
383        }
384
385        self.receive_contracts.clear();
386        self.fallback_contracts.clear();
387        self.non_fallback_contracts.clear();
388        self.functions_by_address.clear();
389        self.constructors_by_address.clear();
390        self.constructor_args_offsets.clear();
391    }
392
393    /// Returns labels for precompiles active in this decoder's chain context.
394    pub fn precompile_labels(&self) -> AddressHashMap<String> {
395        self.labels
396            .iter()
397            .filter(|(address, _)| {
398                precompiles::is_known_precompile(**address, self.chain_id, self.tempo_hardfork)
399            })
400            .map(|(address, label)| (*address, label.clone()))
401            .collect()
402    }
403
404    /// Identify unknown addresses in the specified call trace using the specified identifier.
405    ///
406    /// Unknown contracts are contracts that either lack a label or an ABI.
407    pub fn identify(&mut self, arena: &CallTraceArena, identifier: &mut impl TraceIdentifier) {
408        self.collect_identified_addresses(self.identify_addresses(arena, identifier));
409    }
410
411    /// Identify unknown addresses in the specified call trace using the specified identifier.
412    ///
413    /// Unknown contracts are contracts that either lack a label or an ABI.
414    pub fn identify_addresses<'a>(
415        &self,
416        arena: &CallTraceArena,
417        identifier: &'a mut impl TraceIdentifier,
418    ) -> Vec<IdentifiedAddress<'a>> {
419        let nodes = arena.nodes().iter().filter(|node| {
420            // Skip precompile addresses, they will never resolve externally.
421            if node.is_precompile()
422                || precompiles::is_known_precompile(
423                    node.trace.address,
424                    self.chain_id,
425                    self.tempo_hardfork,
426                )
427            {
428                return false;
429            }
430            let address = &node.trace.address;
431            !self.labels.contains_key(address) || !self.contracts.contains_key(address)
432        });
433        identifier.identify_addresses(&nodes.collect::<Vec<_>>())
434    }
435
436    /// Adds a single event to the decoder.
437    pub fn push_event(&mut self, event: Event) {
438        self.events.entry((event.selector(), indexed_inputs(&event))).or_default().push(event);
439    }
440
441    /// Adds a single function to the decoder.
442    pub fn push_function(&mut self, function: Function) {
443        let selector = function.selector();
444        let functions = self.functions.entry(selector).or_default();
445
446        if Self::push_function_to(functions, function) && functions.len() > 1 {
447            let function = functions.last().expect("function was just inserted");
448            let signature = function.signature();
449            trace!(target: "evm::traces", %selector, new=%signature, "duplicate function selector");
450        }
451    }
452
453    /// Adds a single function to the decoder for a specific contract address.
454    pub fn push_address_function(&mut self, address: Address, function: Function) {
455        let functions = self
456            .functions_by_address
457            .entry(address)
458            .or_default()
459            .entry(function.selector())
460            .or_default();
461        Self::push_function_to(functions, function);
462    }
463
464    fn push_function_to(functions: &mut Vec<Function>, function: Function) -> bool {
465        if functions.contains(&function) {
466            false
467        } else {
468            functions.push(function);
469            true
470        }
471    }
472
473    fn functions_for_selector(&self, address: Address, selector: &Selector) -> Option<&[Function]> {
474        if self.is_current_committee_active(address) {
475            static FUNCTIONS: OnceLock<HashMap<Selector, Vec<Function>>> = OnceLock::new();
476            if let Some(functions) = FUNCTIONS
477                .get_or_init(|| {
478                    ICurrentCommittee::abi::functions()
479                        .into_values()
480                        .flatten()
481                        .map(|function| (function.selector(), vec![function]))
482                        .collect()
483                })
484                .get(selector)
485            {
486                return Some(functions);
487            }
488        }
489        self.functions_by_address
490            .get(&address)
491            .and_then(|functions| functions.get(selector))
492            .or_else(|| self.functions.get(selector))
493            .map(Vec::as_slice)
494    }
495
496    fn is_current_committee_active(&self, address: Address) -> bool {
497        address == CURRENT_COMMITTEE_ADDRESS
498            && self.tempo_hardfork.is_some_and(|hardfork| hardfork.is_t8())
499            && precompiles::is_known_precompile(address, self.chain_id, self.tempo_hardfork)
500    }
501
502    /// Selects the appropriate function from a list of functions with the same selector by
503    /// checking which one decodes the calldata.
504    ///
505    /// Address-scoped function lookup should happen before this to avoid using ABI metadata from a
506    /// different contract when multiple functions have the same input types.
507    fn select_contract_function<'a>(
508        &self,
509        functions: &'a [Function],
510        trace: &CallTrace,
511    ) -> &'a [Function] {
512        // When there are selector collisions, try to decode the calldata with each function
513        // to determine which one is actually being called. The correct function should
514        // decode successfully while the wrong ones will fail due to parameter type mismatches.
515        if functions.len() > 1 {
516            for (i, func) in functions.iter().enumerate() {
517                if trace.data.len() >= SELECTOR_LEN
518                    && func.abi_decode_input(&trace.data[SELECTOR_LEN..]).is_ok()
519                {
520                    return &functions[i..i + 1];
521                }
522            }
523        }
524        functions
525    }
526
527    /// Adds a single error to the decoder.
528    pub fn push_error(&mut self, error: Error) {
529        self.revert_decoder.push_error(error);
530    }
531
532    pub const fn without_label(&mut self, disable: bool) {
533        self.disable_labels = disable;
534    }
535
536    fn collect_identified_addresses(&mut self, mut addrs: Vec<IdentifiedAddress<'_>>) {
537        addrs.sort_by_key(|identity| identity.address);
538        addrs.dedup_by_key(|identity| identity.address);
539        if addrs.is_empty() {
540            return;
541        }
542
543        trace!(target: "evm::traces", len=addrs.len(), "collecting address identities");
544        for IdentifiedAddress {
545            address,
546            label,
547            contract,
548            abi,
549            constructor_args_offset,
550            artifact_id: _,
551        } in addrs
552        {
553            let _span = trace_span!(target: "evm::traces", "identity", ?contract, ?label).entered();
554
555            if let Some(contract) = contract {
556                self.contracts.entry(address).or_insert(contract);
557            }
558
559            if let Some(label) = label.filter(|s| !s.is_empty()) {
560                self.labels.entry(address).or_insert(label);
561            }
562
563            if let Some(abi) = abi {
564                self.collect_abi(&abi, Some(address));
565            }
566
567            if let Some(offset) = constructor_args_offset {
568                self.constructor_args_offsets.entry(address).or_insert(offset);
569            }
570        }
571    }
572
573    fn collect_abi(&mut self, abi: &JsonAbi, address: Option<Address>) {
574        let len = abi.len();
575        if len == 0 {
576            return;
577        }
578        trace!(target: "evm::traces", len, ?address, "collecting ABI");
579        for function in abi.functions() {
580            if let Some(address) = address {
581                self.push_address_function(address, function.clone());
582            }
583            self.push_function(function.clone());
584        }
585        if let Some(address) = address
586            && let Some(constructor) = abi.constructor()
587        {
588            self.constructors_by_address.entry(address).or_insert_with(|| constructor.clone());
589        }
590        for event in abi.events() {
591            self.push_event(event.clone());
592        }
593        for error in abi.errors() {
594            self.push_error(error.clone());
595        }
596        if let Some(address) = address {
597            if abi.receive.is_some() {
598                self.receive_contracts.insert(address);
599            }
600
601            if abi.fallback.is_some() {
602                self.fallback_contracts
603                    .insert(address, abi.functions().map(|f| f.selector()).collect());
604            } else {
605                self.non_fallback_contracts
606                    .insert(address, abi.functions().map(|f| f.selector()).collect());
607            }
608        }
609    }
610
611    /// Populates the traces with decoded data by mutating the
612    /// [CallTrace] in place. See [CallTraceDecoder::decode_function] and
613    /// [CallTraceDecoder::decode_event] for more details.
614    pub async fn populate_traces(&self, traces: &mut Vec<CallTraceNode>) {
615        for node in traces {
616            if !self.opcodes.is_empty() {
617                for step in &mut node.trace.steps {
618                    if step.decoded.is_some() {
619                        continue;
620                    }
621                    for opcode in &self.opcodes {
622                        if step.op == *opcode {
623                            let res = match &step.storage_change {
624                                Some(change) if step.op == OpCode::SSTORE => {
625                                    if let Some(had_value) = change.had_value {
626                                        format!(
627                                            "[{}] {} 0x{:x}: 0x{:x} → 0x{:x}",
628                                            step.gas_cost,
629                                            opcode,
630                                            change.key,
631                                            had_value,
632                                            change.value
633                                        )
634                                    } else {
635                                        format!(
636                                            "[{}] {} 0x{:x} → (0x{:x})",
637                                            step.gas_cost, opcode, change.key, change.value
638                                        )
639                                    }
640                                }
641                                Some(change) => format!(
642                                    "[{}] {} 0x{:x} → (0x{:x})",
643                                    step.gas_cost, opcode, change.key, change.value
644                                ),
645                                None => format!("[{}] {}", step.gas_cost, opcode),
646                            };
647
648                            step.decoded = Some(Box::new(DecodedTraceStep::Line(res)));
649                            break;
650                        }
651                    }
652                }
653            }
654
655            node.trace.decoded = Some(Box::new(self.decode_function(&node.trace).await));
656            for log in &mut node.logs {
657                log.decoded =
658                    Some(Box::new(self.decode_event_with_address(log.address, &log.raw_log).await));
659            }
660
661            if let Some(debug) = self.debug_identifier.as_ref()
662                && let Some(identified) = self.contracts.get(&node.trace.address)
663            {
664                debug.identify_node_steps(node, get_contract_name(identified))
665            }
666        }
667    }
668
669    /// Decodes a call trace.
670    pub async fn decode_function(&self, trace: &CallTrace) -> DecodedCallTrace {
671        let label = if self.disable_labels {
672            None
673        } else if let Some(label) = self.labels.get(&trace.address) {
674            Some(label.clone())
675        } else if self.is_current_committee_active(trace.address) {
676            Some("CurrentCommittee".to_string())
677        } else {
678            None
679        };
680
681        if trace.kind.is_any_create() {
682            return DecodedCallTrace {
683                label,
684                call_data: self.decode_constructor_input(trace),
685                return_data: None,
686            };
687        }
688
689        if let Some(trace) = precompiles::decode(trace, self.chain_id, self.tempo_hardfork) {
690            return trace;
691        }
692
693        let cdata = &trace.data;
694        if trace.address == DEFAULT_CREATE2_DEPLOYER {
695            return DecodedCallTrace {
696                label,
697                call_data: Some(DecodedCallData { signature: "create2".to_string(), args: vec![] }),
698                return_data: self.default_return_data(trace).await,
699            };
700        }
701
702        if is_abi_call_data(cdata) {
703            let selector = Selector::try_from(&cdata[..SELECTOR_LEN]).unwrap();
704            let mut identified_functions = Vec::new();
705            let functions = match self.functions_for_selector(trace.address, &selector) {
706                Some(functions) => functions,
707                None => {
708                    if let Some(identifier) = &self.signature_identifier
709                        && let Some(function) = identifier.identify_function(selector).await
710                    {
711                        identified_functions.push(function);
712                    }
713                    &identified_functions
714                }
715            };
716
717            // Check if unsupported fn selector: calldata dooes NOT point to one of its selectors +
718            // non-fallback contract + no receive
719            if let Some(contract_selectors) = self.non_fallback_contracts.get(&trace.address)
720                && !contract_selectors.contains(&selector)
721                && (!cdata.is_empty() || !self.receive_contracts.contains(&trace.address))
722            {
723                let return_data = if trace.success {
724                    None
725                } else {
726                    let revert_msg =
727                        self.decode_revert_at(trace.address, &trace.output, trace.status).await;
728
729                    if trace.output.is_empty() || revert_msg.contains("EvmError: Revert") {
730                        Some(format!(
731                            "unrecognized function selector {} for contract {}, which has no fallback function.",
732                            selector, trace.address
733                        ))
734                    } else {
735                        Some(revert_msg)
736                    }
737                };
738
739                return if let Some(func) = functions.first() {
740                    DecodedCallTrace {
741                        label,
742                        call_data: Some(self.decode_function_input(trace, func)),
743                        return_data,
744                    }
745                } else {
746                    DecodedCallTrace {
747                        label,
748                        call_data: self.fallback_call_data(trace),
749                        return_data,
750                    }
751                };
752            }
753
754            let contract_functions = self.select_contract_function(functions, trace);
755            let [func, ..] = contract_functions else {
756                return DecodedCallTrace {
757                    label,
758                    call_data: self.fallback_call_data(trace),
759                    return_data: self.default_return_data(trace).await,
760                };
761            };
762
763            // If traced contract is a fallback contract, check if it has the decoded function.
764            // If not, then replace call data signature with `fallback`.
765            let mut call_data = self.decode_function_input(trace, func);
766            if let Some(fallback_functions) = self.fallback_contracts.get(&trace.address)
767                && !fallback_functions.contains(&selector)
768                && let Some(cd) = self.fallback_call_data(trace)
769            {
770                call_data.signature = cd.signature;
771            }
772
773            DecodedCallTrace {
774                label,
775                call_data: Some(call_data),
776                return_data: self.decode_function_output(trace, contract_functions).await,
777            }
778        } else {
779            DecodedCallTrace {
780                label,
781                call_data: self.fallback_call_data(trace),
782                return_data: self.default_return_data(trace).await,
783            }
784        }
785    }
786
787    /// Decodes a function's input into the given trace.
788    fn decode_function_input(&self, trace: &CallTrace, func: &Function) -> DecodedCallData {
789        let mut args = None;
790        if trace.data.len() >= SELECTOR_LEN {
791            if trace.address == CHEATCODE_ADDRESS {
792                // Try to decode cheatcode inputs in a more custom way
793                if let Some(v) = self.decode_cheatcode_inputs(func, &trace.data) {
794                    args = Some(v);
795                }
796            }
797
798            if args.is_none()
799                && let Ok(decoded) = func.abi_decode_input(&trace.data[SELECTOR_LEN..])
800            {
801                args = Some(
802                    decoded
803                        .iter()
804                        .zip(&func.inputs)
805                        .map(|(value, input)| {
806                            self.format_param_value(
807                                Some(trace.address),
808                                &func.name,
809                                &input.name,
810                                &input.ty,
811                                value,
812                            )
813                        })
814                        .collect(),
815                );
816            }
817        }
818
819        DecodedCallData { signature: func.signature(), args: args.unwrap_or_default() }
820    }
821
822    /// Decodes constructor input from a creation trace.
823    fn decode_constructor_input(&self, trace: &CallTrace) -> Option<DecodedCallData> {
824        let constructor = self.constructors_by_address.get(&trace.address)?;
825        let offset = *self.constructor_args_offsets.get(&trace.address)?;
826        let data = trace.data.get(offset..)?;
827        let decoded = constructor.abi_decode_input(data).ok()?;
828        let args = decoded
829            .iter()
830            .zip(&constructor.inputs)
831            .map(|(value, input)| {
832                self.format_param_value(
833                    Some(trace.address),
834                    "constructor",
835                    &input.name,
836                    &input.ty,
837                    value,
838                )
839            })
840            .collect();
841
842        Some(DecodedCallData { signature: constructor_signature(constructor), args })
843    }
844
845    /// Custom decoding for cheatcode inputs.
846    fn decode_cheatcode_inputs(&self, func: &Function, data: &[u8]) -> Option<Vec<String>> {
847        match func.name.as_str() {
848            "expectRevert" => {
849                let decoded = match data.get(SELECTOR_LEN..) {
850                    Some(data) => func.abi_decode_input(data).ok(),
851                    None => None,
852                };
853                let Some(decoded) = decoded else {
854                    return Some(vec![self.revert_decoder.decode(data, None)]);
855                };
856                let Some(first) = decoded.first() else {
857                    return Some(vec![self.revert_decoder.decode(data, None)]);
858                };
859                let expected_revert = match first {
860                    DynSolValue::Bytes(bytes) => bytes.as_slice(),
861                    DynSolValue::FixedBytes(word, size) => &word[..*size],
862                    _ => return None,
863                };
864                Some(
865                    std::iter::once(self.revert_decoder.decode(expected_revert, None))
866                        .chain(decoded.iter().skip(1).map(|value| self.format_value(value)))
867                        .collect(),
868                )
869            }
870            "addr" | "createWallet" | "deriveKey" | "rememberKey" => {
871                // Redact private key in all cases
872                Some(vec!["<pk>".to_string()])
873            }
874            "broadcast" | "startBroadcast" => {
875                // Redact private key if defined
876                // broadcast(uint256) / startBroadcast(uint256)
877                (!func.inputs.is_empty() && func.inputs[0].ty == "uint256").then(|| vec!["<pk>".to_string()])
878            }
879            "getNonce" => {
880                // Redact private key if defined
881                // getNonce(Wallet)
882                (!func.inputs.is_empty() && func.inputs[0].ty == "tuple").then(|| vec!["<pk>".to_string()])
883            }
884            "sign" | "signP256" => {
885                let mut decoded = func.abi_decode_input(&data[SELECTOR_LEN..]).ok()?;
886
887                // Redact private key and replace in trace
888                // sign(uint256,bytes32) / signP256(uint256,bytes32) / sign(Wallet,bytes32)
889                if !decoded.is_empty() &&
890                    (func.inputs[0].ty == "uint256" || func.inputs[0].ty == "tuple")
891                {
892                    decoded[0] = DynSolValue::String("<pk>".to_string());
893                }
894
895                Some(decoded.iter().map(format_token).collect())
896            }
897            "signDelegation" | "signAndAttachDelegation" => {
898                let mut decoded = func.abi_decode_input(&data[SELECTOR_LEN..]).ok()?;
899                // Redact private key and replace in trace for
900                // signAndAttachDelegation(address implementation, uint256 privateKey)
901                // signDelegation(address implementation, uint256 privateKey)
902                decoded[1] = DynSolValue::String("<pk>".to_string());
903                Some(decoded.iter().map(format_token).collect())
904            }
905            "parseJson" |
906            "parseJsonUint" |
907            "parseJsonUintArray" |
908            "parseJsonInt" |
909            "parseJsonIntArray" |
910            "parseJsonString" |
911            "parseJsonStringArray" |
912            "parseJsonAddress" |
913            "parseJsonAddressArray" |
914            "parseJsonBool" |
915            "parseJsonBoolArray" |
916            "parseJsonBytes" |
917            "parseJsonBytesArray" |
918            "parseJsonBytes32" |
919            "parseJsonBytes32Array" |
920            "writeJson" |
921            // `keyExists` is being deprecated in favor of `keyExistsJson`. It will be removed in future versions.
922            "keyExists" |
923            "keyExistsJson" |
924            "serializeBool" |
925            "serializeUint" |
926            "serializeUintToHex" |
927            "serializeInt" |
928            "serializeAddress" |
929            "serializeBytes32" |
930            "serializeString" |
931            "serializeBytes" => {
932                if self.verbosity >= 5 {
933                    None
934                } else {
935                    let mut decoded = func.abi_decode_input(&data[SELECTOR_LEN..]).ok()?;
936                    let token = if func.name.as_str() == "parseJson" ||
937                        // `keyExists` is being deprecated in favor of `keyExistsJson`. It will be removed in future versions.
938                        func.name.as_str() == "keyExists" ||
939                        func.name.as_str() == "keyExistsJson"
940                    {
941                        "<JSON file>"
942                    } else {
943                        "<stringified JSON>"
944                    };
945                    decoded[0] = DynSolValue::String(token.to_string());
946                    Some(decoded.iter().map(format_token).collect())
947                }
948            }
949            s if s.contains("Toml") => {
950                if self.verbosity >= 5 {
951                    None
952                } else {
953                    let mut decoded = func.abi_decode_input(&data[SELECTOR_LEN..]).ok()?;
954                    let token = if func.name.as_str() == "parseToml" ||
955                        func.name.as_str() == "keyExistsToml"
956                    {
957                        "<TOML file>"
958                    } else {
959                        "<stringified TOML>"
960                    };
961                    decoded[0] = DynSolValue::String(token.to_string());
962                    Some(decoded.iter().map(format_token).collect())
963                }
964            }
965            "createFork" |
966            "createSelectFork" |
967            "rpc" => {
968                let mut decoded = func.abi_decode_input(&data[SELECTOR_LEN..]).ok()?;
969
970                // Redact RPC URL except if referenced by an alias
971                if !decoded.is_empty() && func.inputs[0].ty == "string" {
972                    let url_or_alias = decoded[0].as_str().unwrap_or_default();
973
974                    if url_or_alias.starts_with("http") || url_or_alias.starts_with("ws") {
975                        decoded[0] = DynSolValue::String("<rpc url>".to_string());
976                    }
977                } else {
978                    return None;
979                }
980
981                Some(decoded.iter().map(format_token).collect())
982            }
983            _ => None,
984        }
985    }
986
987    /// Decodes a function's output into the given trace.
988    async fn decode_function_output(
989        &self,
990        trace: &CallTrace,
991        funcs: &[Function],
992    ) -> Option<String> {
993        if !trace.success {
994            return self.default_return_data(trace).await;
995        }
996
997        if trace.address == CHEATCODE_ADDRESS
998            && let Some(decoded) = funcs.iter().find_map(|func| self.decode_cheatcode_outputs(func))
999        {
1000            return Some(decoded);
1001        }
1002
1003        if let Some(values) =
1004            funcs.iter().find_map(|func| func.abi_decode_output(&trace.output).ok())
1005        {
1006            // Functions coming from an external database do not have any outputs specified,
1007            // and will lead to returning an empty list of values.
1008            if values.is_empty() {
1009                return None;
1010            }
1011
1012            return Some(
1013                values.iter().map(|value| self.format_value(value)).format(", ").to_string(),
1014            );
1015        }
1016
1017        None
1018    }
1019
1020    /// Custom decoding for cheatcode outputs.
1021    fn decode_cheatcode_outputs(&self, func: &Function) -> Option<String> {
1022        match func.name.as_str() {
1023            s if s.starts_with("env") => Some("<env var value>"),
1024            "createWallet" | "deriveKey" => Some("<pk>"),
1025            "promptSecret" | "promptSecretUint" => Some("<secret>"),
1026            "parseJson" if self.verbosity < 5 => Some("<encoded JSON value>"),
1027            "readFile" if self.verbosity < 5 => Some("<file>"),
1028            "rpcUrl" | "rpcUrls" | "rpcUrlStructs" => Some("<rpc url>"),
1029            _ => None,
1030        }
1031        .map(Into::into)
1032    }
1033
1034    #[track_caller]
1035    fn fallback_call_data(&self, trace: &CallTrace) -> Option<DecodedCallData> {
1036        let cdata = &trace.data;
1037        let signature = if cdata.is_empty() && self.receive_contracts.contains(&trace.address) {
1038            "receive()"
1039        } else if self.fallback_contracts.contains_key(&trace.address) {
1040            "fallback()"
1041        } else {
1042            return None;
1043        }
1044        .to_string();
1045        let args = if cdata.is_empty() { Vec::new() } else { vec![cdata.to_string()] };
1046        Some(DecodedCallData { signature, args })
1047    }
1048
1049    /// The default decoded return data for a trace.
1050    async fn default_return_data(&self, trace: &CallTrace) -> Option<String> {
1051        // For calls with status None or successful status, don't decode revert data
1052        // This is due to trace.status is derived from the revm_interpreter::InstructionResult in
1053        // revm-inspectors status will `None` post revm 27, as `InstructionResult::Continue` does
1054        // not exists anymore.
1055        if trace.status.is_none_or(|s| s.is_ok()) || trace.success {
1056            return None;
1057        }
1058        Some(self.decode_revert_at(trace.address, &trace.output, trace.status).await)
1059    }
1060
1061    /// Decodes revert data into a human-readable representation.
1062    ///
1063    /// If the revert decoder does not know the custom error, tries identifying the error selector
1064    /// with the signatures identifier, like unknown function and event signatures. This resolves
1065    /// errors from the local signatures cache populated by `forge build`, or from remote signature
1066    /// databases.
1067    async fn decode_revert(&self, output: &[u8], status: Option<InstructionResult>) -> String {
1068        if let Some(reason) = self.revert_decoder.maybe_decode_known(output) {
1069            return reason;
1070        }
1071        if let Some(identifier) = &self.signature_identifier
1072            && let Some((selector, data)) = output.split_first_chunk::<SELECTOR_LEN>()
1073            && let Some(error) = identifier.identify_error(Selector::from(*selector)).await
1074            && let Ok(decoded) = error.abi_decode_input(data)
1075        {
1076            return format!("{}({})", error.name, decoded.iter().map(format_token).format(", "));
1077        }
1078        self.revert_decoder.decode(output, status)
1079    }
1080
1081    async fn decode_revert_at(
1082        &self,
1083        address: Address,
1084        output: &[u8],
1085        status: Option<InstructionResult>,
1086    ) -> String {
1087        if self.is_current_committee_active(address) {
1088            static DECODER: OnceLock<RevertDecoder> = OnceLock::new();
1089            let decoder = DECODER
1090                .get_or_init(|| RevertDecoder::new().with_abi(&ICurrentCommittee::abi::contract()));
1091            if let Some(reason) = decoder.maybe_decode_known(output) {
1092                return reason;
1093            }
1094        }
1095        self.decode_revert(output, status).await
1096    }
1097
1098    /// Decodes an event.
1099    pub async fn decode_event(&self, log: &LogData) -> DecodedCallLog {
1100        self.decode_event_inner(None, log).await
1101    }
1102
1103    /// Decodes an event emitted by a known address.
1104    pub async fn decode_event_with_address(
1105        &self,
1106        address: Address,
1107        log: &LogData,
1108    ) -> DecodedCallLog {
1109        self.decode_event_inner(Some(address), log).await
1110    }
1111
1112    async fn decode_event_inner(&self, address: Option<Address>, log: &LogData) -> DecodedCallLog {
1113        let &[t0, ..] = log.topics() else { return DecodedCallLog { name: None, params: None } };
1114
1115        let mut events = Vec::new();
1116        let events = match self.events.get(&(t0, log.topics().len() - 1)) {
1117            Some(es) => es,
1118            None => {
1119                if let Some(identifier) = &self.signature_identifier
1120                    && let Some(event) = identifier.identify_event(t0).await
1121                {
1122                    events.push(get_indexed_event(event, log));
1123                }
1124                &events
1125            }
1126        };
1127        for event in events {
1128            if let Ok(decoded) = event.decode_log(log) {
1129                let params = reconstruct_params(event, &decoded);
1130                return DecodedCallLog {
1131                    name: Some(event.name.clone()),
1132                    params: Some(
1133                        params
1134                            .into_iter()
1135                            .zip(event.inputs.iter())
1136                            .map(|(param, input)| {
1137                                // undo patched names
1138                                let name = input.name.clone();
1139                                (
1140                                    name,
1141                                    self.format_param_value(
1142                                        address,
1143                                        &event.name,
1144                                        &input.name,
1145                                        &input.ty,
1146                                        &param,
1147                                    ),
1148                                )
1149                            })
1150                            .collect(),
1151                    ),
1152                };
1153            }
1154        }
1155
1156        DecodedCallLog { name: None, params: None }
1157    }
1158
1159    /// Prefetches function and event signatures into the identifier cache
1160    pub async fn prefetch_signatures(&self, nodes: &[CallTraceNode]) {
1161        let Some(identifier) = &self.signature_identifier else { return };
1162        let events = nodes
1163            .iter()
1164            .flat_map(|node| {
1165                node.logs
1166                    .iter()
1167                    .map(|log| log.raw_log.topics())
1168                    .filter(|&topics| {
1169                        if let Some(&first) = topics.first()
1170                            && self.events.contains_key(&(first, topics.len() - 1))
1171                        {
1172                            return false;
1173                        }
1174                        true
1175                    })
1176                    .filter_map(|topics| topics.first())
1177            })
1178            .copied();
1179        let functions = nodes
1180            .iter()
1181            .filter(|&n| {
1182                // Ignore known addresses.
1183                if n.trace.address == DEFAULT_CREATE2_DEPLOYER
1184                    || n.is_precompile()
1185                    || precompiles::is_known_precompile(
1186                        n.trace.address,
1187                        self.chain_id,
1188                        self.tempo_hardfork,
1189                    )
1190                {
1191                    return false;
1192                }
1193                // Ignore non-ABI calldata.
1194                if n.trace.kind.is_any_create() || !is_abi_call_data(&n.trace.data) {
1195                    return false;
1196                }
1197                true
1198            })
1199            .filter_map(|n| n.trace.data.first_chunk().map(Selector::from))
1200            .filter(|selector| !self.functions.contains_key(selector));
1201        let errors = nodes
1202            .iter()
1203            .filter(|&n| {
1204                // Only consider reverted traces whose output the revert decoder cannot decode.
1205                n.trace.status.is_some_and(|s| !s.is_ok())
1206                    && !n.trace.success
1207                    && self.revert_decoder.maybe_decode_known(&n.trace.output).is_none()
1208            })
1209            .filter_map(|n| n.trace.output.first_chunk().map(Selector::from));
1210        let selectors = events
1211            .map(SelectorKind::Event)
1212            .chain(functions.map(SelectorKind::Function))
1213            .chain(errors.map(SelectorKind::Error))
1214            .unique()
1215            .collect::<Vec<_>>();
1216        let _ = identifier.identify(&selectors).await;
1217    }
1218
1219    /// Pretty-prints a value.
1220    fn format_value(&self, value: &DynSolValue) -> String {
1221        if let DynSolValue::Address(addr) = value
1222            && !self.disable_labels
1223            && let Some(label) = self.labels.get(addr)
1224        {
1225            if self.compact_labels {
1226                return label.clone();
1227            }
1228            return format!("{label}: [{addr}]");
1229        }
1230        format_token(value)
1231    }
1232
1233    fn format_param_value(
1234        &self,
1235        address: Option<Address>,
1236        context_name: &str,
1237        input_name: &str,
1238        input_ty: &str,
1239        value: &DynSolValue,
1240    ) -> String {
1241        self.format_claim_receipt_bytes(address, context_name, input_name, input_ty, value)
1242            .map(|value| self.format_value(&value))
1243            .unwrap_or_else(|| self.format_value(value))
1244    }
1245
1246    fn format_claim_receipt_bytes(
1247        &self,
1248        address: Option<Address>,
1249        context_name: &str,
1250        input_name: &str,
1251        input_ty: &str,
1252        value: &DynSolValue,
1253    ) -> Option<DynSolValue> {
1254        if !matches!(address, Some(RECEIVE_POLICY_GUARD_ADDRESS | TIP403_REGISTRY_ADDRESS)) {
1255            return None;
1256        }
1257        if input_name != "receipt" || input_ty != "bytes" {
1258            return None;
1259        }
1260        if !matches!(context_name, "balanceOf" | "claim" | "burnBlockedReceipt" | "TransferBlocked")
1261        {
1262            return None;
1263        }
1264        let DynSolValue::Bytes(bytes) = value else { return None };
1265        let decoded = IReceivePolicyGuard::ClaimReceiptV1::abi_decode(bytes).ok()?;
1266
1267        Some(DynSolValue::CustomStruct {
1268            name: "ClaimReceiptV1".to_string(),
1269            prop_names: vec![
1270                "version".to_string(),
1271                "token".to_string(),
1272                "recoveryAuthority".to_string(),
1273                "originator".to_string(),
1274                "recipient".to_string(),
1275                "blockedAt".to_string(),
1276                "blockedNonce".to_string(),
1277                "blockedReason".to_string(),
1278                "kind".to_string(),
1279                "memo".to_string(),
1280            ],
1281            tuple: vec![
1282                DynSolValue::Uint(U256::from(decoded.version), 8),
1283                DynSolValue::Address(decoded.token),
1284                DynSolValue::Address(decoded.recoveryAuthority),
1285                DynSolValue::Address(decoded.originator),
1286                DynSolValue::Address(decoded.recipient),
1287                DynSolValue::Uint(U256::from(decoded.blockedAt), 64),
1288                DynSolValue::Uint(U256::from(decoded.blockedNonce), 64),
1289                DynSolValue::Uint(U256::from(decoded.blockedReason), 8),
1290                DynSolValue::Uint(U256::from(decoded.kind as u8), 8),
1291                DynSolValue::FixedBytes(decoded.memo, 32),
1292            ],
1293        })
1294    }
1295}
1296
1297/// Returns `true` if the given function calldata (including function selector) is ABI-encoded.
1298///
1299/// This is a simple heuristic to avoid fetching non ABI-encoded selectors.
1300fn is_abi_call_data(data: &[u8]) -> bool {
1301    match data.len().cmp(&SELECTOR_LEN) {
1302        std::cmp::Ordering::Less => false,
1303        std::cmp::Ordering::Equal => true,
1304        std::cmp::Ordering::Greater => is_abi_data(&data[SELECTOR_LEN..]),
1305    }
1306}
1307
1308/// Returns `true` if the given data is ABI-encoded.
1309///
1310/// See [`is_abi_call_data`] for more details.
1311fn is_abi_data(data: &[u8]) -> bool {
1312    let rem = data.len() % 32;
1313    if rem == 0 || data.is_empty() {
1314        return true;
1315    }
1316    // If the length is not a multiple of 32, also accept when the last remainder bytes are all 0.
1317    data[data.len() - rem..].iter().all(|byte| *byte == 0)
1318}
1319
1320/// Restore the order of the params of a decoded event,
1321/// as Alloy returns the indexed and unindexed params separately.
1322fn reconstruct_params(event: &Event, decoded: &DecodedEvent) -> Vec<DynSolValue> {
1323    let mut indexed = 0;
1324    let mut unindexed = 0;
1325    let mut inputs = vec![];
1326    for input in &event.inputs {
1327        // Prevent panic of event `Transfer(from, to)` decoded with a signature
1328        // `Transfer(address indexed from, address indexed to, uint256 indexed tokenId)` by making
1329        // sure the event inputs is not higher than decoded indexed / un-indexed values.
1330        if input.indexed && indexed < decoded.indexed.len() {
1331            inputs.push(decoded.indexed[indexed].clone());
1332            indexed += 1;
1333        } else if unindexed < decoded.body.len() {
1334            inputs.push(decoded.body[unindexed].clone());
1335            unindexed += 1;
1336        }
1337    }
1338
1339    inputs
1340}
1341
1342fn indexed_inputs(event: &Event) -> usize {
1343    event.inputs.iter().filter(|param| param.indexed).count()
1344}
1345
1346fn constructor_signature(constructor: &Constructor) -> String {
1347    format!(
1348        "constructor({})",
1349        constructor.inputs.iter().map(|input| input.selector_type()).format(",")
1350    )
1351}
1352
1353#[cfg(test)]
1354mod tests {
1355    use super::*;
1356    use alloy_primitives::{address, aliases::U96, hex};
1357    use alloy_sol_types::{SolCall, SolError, SolEvent};
1358
1359    #[test]
1360    fn test_selector_collision_resolution() {
1361        use alloy_json_abi::Function;
1362        use alloy_primitives::Address;
1363
1364        // Create two functions with the same selector but different signatures
1365        let func1 = Function::parse("transferFrom(address,address,uint256)").unwrap();
1366        let func2 = Function::parse("gasprice_bit_ether(int128)").unwrap();
1367
1368        // Verify they have the same selector (this is the collision)
1369        assert_eq!(func1.selector(), func2.selector());
1370
1371        let functions = vec![func1, func2];
1372
1373        // Create a mock trace with calldata that matches func1
1374        let trace = CallTrace {
1375            address: Address::from([0x12; 20]),
1376            data: hex!("23b872dd000000000000000000000000000000000000000000000000000000000000012300000000000000000000000000000000000000000000000000000000000004560000000000000000000000000000000000000000000000000000000000000064").to_vec().into(),
1377            ..Default::default()
1378        };
1379
1380        let decoder = CallTraceDecoder::new();
1381        let result = decoder.select_contract_function(&functions, &trace);
1382
1383        // Should return only the function that can decode the calldata (func1)
1384        assert_eq!(result.len(), 1);
1385        assert_eq!(result[0].signature(), "transferFrom(address,address,uint256)");
1386    }
1387
1388    #[test]
1389    fn test_selector_collision_resolution_second_function() {
1390        use alloy_json_abi::Function;
1391        use alloy_primitives::Address;
1392
1393        // Create two functions with the same selector but different signatures
1394        let func1 = Function::parse("transferFrom(address,address,uint256)").unwrap();
1395        let func2 = Function::parse("gasprice_bit_ether(int128)").unwrap();
1396
1397        let functions = vec![func1, func2];
1398
1399        // Create a mock trace with calldata that matches func2
1400        let trace = CallTrace {
1401            address: Address::from([0x12; 20]),
1402            data: hex!("23b872dd0000000000000000000000000000000000000000000000000000000000000064")
1403                .to_vec()
1404                .into(),
1405            ..Default::default()
1406        };
1407
1408        let decoder = CallTraceDecoder::new();
1409        let result = decoder.select_contract_function(&functions, &trace);
1410
1411        // Should return only the function that can decode the calldata (func2)
1412        assert_eq!(result.len(), 1);
1413        assert_eq!(result[0].signature(), "gasprice_bit_ether(int128)");
1414    }
1415
1416    #[test]
1417    fn compact_labels_hide_address_in_trace_parameters() {
1418        let address = address!("0x0000000000000000000000000000000000000001");
1419        let value = DynSolValue::Address(address);
1420        let tracing = TracingConfig {
1421            labels: AddressHashMap::from_iter([(address, "Alice".to_string())]),
1422            ..Default::default()
1423        };
1424        let decoder = CallTraceDecoderBuilder::new().with_tracing_config(&tracing).build();
1425        assert_eq!(decoder.format_value(&value), format!("Alice: [{address}]"));
1426
1427        let tracing = TracingConfig { compact_labels: true, ..tracing };
1428        let decoder = CallTraceDecoderBuilder::new().with_tracing_config(&tracing).build();
1429        assert_eq!(decoder.format_value(&value), "Alice");
1430
1431        let tracing = TracingConfig { disable_labels: true, ..tracing };
1432        let decoder = CallTraceDecoderBuilder::new().with_tracing_config(&tracing).build();
1433        assert_eq!(decoder.format_value(&value), address.to_string());
1434    }
1435
1436    #[test]
1437    fn configured_labels_survive_address_reset() {
1438        let configured = address!("0x0000000000000000000000000000000000000100");
1439        let discovered = address!("0x0000000000000000000000000000000000000200");
1440        let tracing = TracingConfig {
1441            labels: AddressHashMap::from_iter([(configured, "configured".to_string())]),
1442            ..Default::default()
1443        };
1444        let mut decoder = CallTraceDecoderBuilder::new().with_tracing_config(&tracing).build();
1445        decoder.labels.insert(discovered, "discovered".to_string());
1446
1447        decoder.clear_addresses();
1448
1449        assert_eq!(decoder.labels.get(&configured).map(String::as_str), Some("configured"));
1450        assert!(!decoder.labels.contains_key(&discovered));
1451    }
1452
1453    #[tokio::test]
1454    async fn test_decode_constructor_input() {
1455        let address = Address::repeat_byte(0x12);
1456        let constructor = Constructor::parse("constructor(uint256 amount, address owner)").unwrap();
1457        let args = constructor
1458            .abi_encode_input(&[
1459                DynSolValue::Uint(U256::from(42), 256),
1460                DynSolValue::Address(Address::repeat_byte(0x34)),
1461            ])
1462            .unwrap();
1463        let mut data = vec![0x60, 0x80, 0x60, 0x40];
1464        let offset = data.len();
1465        data.extend_from_slice(&args);
1466        let trace = CallTrace {
1467            address,
1468            kind: revm_inspectors::tracing::types::CallKind::Create,
1469            data: data.into(),
1470            ..Default::default()
1471        };
1472        let mut decoder = CallTraceDecoder::new().clone();
1473        decoder.constructors_by_address.insert(address, constructor);
1474        decoder.constructor_args_offsets.insert(address, offset);
1475
1476        let decoded = decoder.decode_function(&trace).await;
1477        let call_data = decoded.call_data.expect("constructor input should decode");
1478
1479        assert_eq!(call_data.signature, "constructor(uint256,address)");
1480        assert_eq!(call_data.args[0], "42");
1481        assert_eq!(call_data.args[1], format!("{}", Address::repeat_byte(0x34)));
1482    }
1483
1484    #[test]
1485    fn test_should_redact() {
1486        let decoder = CallTraceDecoder::new();
1487
1488        let expected_revert_bytes4 = vec![0xde, 0xad, 0xbe, 0xef];
1489        let expect_revert_bytes4_data = Function::parse("expectRevert(bytes4)")
1490            .unwrap()
1491            .abi_encode_input(&[DynSolValue::FixedBytes(
1492                B256::right_padding_from(expected_revert_bytes4.as_slice()),
1493                4,
1494            )])
1495            .unwrap();
1496
1497        let expected_revert_bytes = hex!(
1498            "08c379a000000000000000000000000000000000000000000000000000000000\
1499             0000002000000000000000000000000000000000000000000000000000000000\
1500             00000004626f6f6d000000000000000000000000000000000000000000000000"
1501        )
1502        .to_vec();
1503        let expect_revert_bytes_data = Function::parse("expectRevert(bytes)")
1504            .unwrap()
1505            .abi_encode_input(&[DynSolValue::Bytes(expected_revert_bytes.clone())])
1506            .unwrap();
1507
1508        let reverter = Address::from([0x11; 20]);
1509        let expect_revert_bytes4_address_data = Function::parse("expectRevert(bytes4,address)")
1510            .unwrap()
1511            .abi_encode_input(&[
1512                DynSolValue::FixedBytes(
1513                    B256::right_padding_from(expected_revert_bytes4.as_slice()),
1514                    4,
1515                ),
1516                DynSolValue::Address(reverter),
1517            ])
1518            .unwrap();
1519
1520        let count = 42_u64;
1521        let expect_revert_bytes_count_data = Function::parse("expectRevert(bytes,uint64)")
1522            .unwrap()
1523            .abi_encode_input(&[
1524                DynSolValue::Bytes(expected_revert_bytes.clone()),
1525                DynSolValue::Uint(alloy_primitives::U256::from(count), 64),
1526            ])
1527            .unwrap();
1528
1529        let expect_revert_bytes_address_count_data =
1530            Function::parse("expectRevert(bytes,address,uint64)")
1531                .unwrap()
1532                .abi_encode_input(&[
1533                    DynSolValue::Bytes(expected_revert_bytes.clone()),
1534                    DynSolValue::Address(reverter),
1535                    DynSolValue::Uint(alloy_primitives::U256::from(count), 64),
1536                ])
1537                .unwrap();
1538
1539        let expect_revert_runtime_data = expected_revert_bytes4.clone();
1540
1541        // [function_signature, data, expected]
1542        let cheatcode_input_test_cases = vec![
1543            // Should decode the expected revert payload, not full cheatcode calldata:
1544            (
1545                "expectRevert(bytes4)",
1546                expect_revert_bytes4_data,
1547                Some(vec![decoder.revert_decoder.decode(expected_revert_bytes4.as_slice(), None)]),
1548            ),
1549            (
1550                "expectRevert(bytes)",
1551                expect_revert_bytes_data,
1552                Some(vec![decoder.revert_decoder.decode(expected_revert_bytes.as_slice(), None)]),
1553            ),
1554            (
1555                "expectRevert(bytes4)",
1556                expect_revert_runtime_data.clone(),
1557                Some(vec![
1558                    decoder.revert_decoder.decode(expect_revert_runtime_data.as_slice(), None),
1559                ]),
1560            ),
1561            (
1562                "expectRevert(bytes4,address)",
1563                expect_revert_bytes4_address_data,
1564                Some(vec![
1565                    decoder.revert_decoder.decode(expected_revert_bytes4.as_slice(), None),
1566                    decoder.format_value(&DynSolValue::Address(reverter)),
1567                ]),
1568            ),
1569            (
1570                "expectRevert(bytes,uint64)",
1571                expect_revert_bytes_count_data,
1572                Some(vec![
1573                    decoder.revert_decoder.decode(expected_revert_bytes.as_slice(), None),
1574                    decoder
1575                        .format_value(&DynSolValue::Uint(alloy_primitives::U256::from(count), 64)),
1576                ]),
1577            ),
1578            (
1579                "expectRevert(bytes,address,uint64)",
1580                expect_revert_bytes_address_count_data,
1581                Some(vec![
1582                    decoder.revert_decoder.decode(expected_revert_bytes.as_slice(), None),
1583                    decoder.format_value(&DynSolValue::Address(reverter)),
1584                    decoder
1585                        .format_value(&DynSolValue::Uint(alloy_primitives::U256::from(count), 64)),
1586                ]),
1587            ),
1588            (
1589                "expectRevert()",
1590                expect_revert_runtime_data.clone(),
1591                Some(vec![
1592                    decoder.revert_decoder.decode(expect_revert_runtime_data.as_slice(), None),
1593                ]),
1594            ),
1595            // Should redact private key from traces in all cases:
1596            ("addr(uint256)", vec![], Some(vec!["<pk>".to_string()])),
1597            ("createWallet(string)", vec![], Some(vec!["<pk>".to_string()])),
1598            ("createWallet(uint256)", vec![], Some(vec!["<pk>".to_string()])),
1599            ("deriveKey(string,uint32)", vec![], Some(vec!["<pk>".to_string()])),
1600            ("deriveKey(string,string,uint32)", vec![], Some(vec!["<pk>".to_string()])),
1601            ("deriveKey(string,uint32,string)", vec![], Some(vec!["<pk>".to_string()])),
1602            ("deriveKey(string,string,uint32,string)", vec![], Some(vec!["<pk>".to_string()])),
1603            ("rememberKey(uint256)", vec![], Some(vec!["<pk>".to_string()])),
1604            //
1605            // Should redact private key from traces in specific cases with exceptions:
1606            ("broadcast(uint256)", vec![], Some(vec!["<pk>".to_string()])),
1607            ("broadcast()", vec![], None), // Ignore: `private key` is not passed.
1608            ("startBroadcast(uint256)", vec![], Some(vec!["<pk>".to_string()])),
1609            ("startBroadcast()", vec![], None), // Ignore: `private key` is not passed.
1610            ("getNonce((address,uint256,uint256,uint256))", vec![], Some(vec!["<pk>".to_string()])),
1611            ("getNonce(address)", vec![], None), // Ignore: `address` is public.
1612            //
1613            // Should redact private key and replace in trace in cases:
1614            (
1615                "sign(uint256,bytes32)",
1616                hex!(
1617                    "
1618                    e341eaa4
1619                    7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6
1620                    0000000000000000000000000000000000000000000000000000000000000000
1621                "
1622                )
1623                .to_vec(),
1624                Some(vec![
1625                    "\"<pk>\"".to_string(),
1626                    "0x0000000000000000000000000000000000000000000000000000000000000000"
1627                        .to_string(),
1628                ]),
1629            ),
1630            (
1631                "signP256(uint256,bytes32)",
1632                hex!(
1633                    "
1634                    83211b40
1635                    7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6
1636                    0000000000000000000000000000000000000000000000000000000000000000
1637                "
1638                )
1639                .to_vec(),
1640                Some(vec![
1641                    "\"<pk>\"".to_string(),
1642                    "0x0000000000000000000000000000000000000000000000000000000000000000"
1643                        .to_string(),
1644                ]),
1645            ),
1646            (
1647                // cast calldata "createFork(string)" "https://eth-mainnet.g.alchemy.com/v2/api_key"
1648                "createFork(string)",
1649                hex!(
1650                    "
1651                    31ba3498
1652                    0000000000000000000000000000000000000000000000000000000000000020
1653                    000000000000000000000000000000000000000000000000000000000000002c
1654                    68747470733a2f2f6574682d6d61696e6e65742e672e616c6368656d792e636f
1655                    6d2f76322f6170695f6b65790000000000000000000000000000000000000000
1656                    "
1657                )
1658                .to_vec(),
1659                Some(vec!["\"<rpc url>\"".to_string()]),
1660            ),
1661            (
1662                // cast calldata "createFork(string)" "wss://eth-mainnet.g.alchemy.com/v2/api_key"
1663                "createFork(string)",
1664                hex!(
1665                    "
1666                    31ba3498
1667                    0000000000000000000000000000000000000000000000000000000000000020
1668                    000000000000000000000000000000000000000000000000000000000000002a
1669                    7773733a2f2f6574682d6d61696e6e65742e672e616c6368656d792e636f6d2f
1670                    76322f6170695f6b657900000000000000000000000000000000000000000000
1671                    "
1672                )
1673                .to_vec(),
1674                Some(vec!["\"<rpc url>\"".to_string()]),
1675            ),
1676            (
1677                // cast calldata "createFork(string)" "mainnet"
1678                "createFork(string)",
1679                hex!(
1680                    "
1681                    31ba3498
1682                    0000000000000000000000000000000000000000000000000000000000000020
1683                    0000000000000000000000000000000000000000000000000000000000000007
1684                    6d61696e6e657400000000000000000000000000000000000000000000000000
1685                    "
1686                )
1687                .to_vec(),
1688                Some(vec!["\"mainnet\"".to_string()]),
1689            ),
1690            (
1691                // cast calldata "createFork(string,uint256)" "https://eth-mainnet.g.alchemy.com/v2/api_key" 1
1692                "createFork(string,uint256)",
1693                hex!(
1694                    "
1695                    6ba3ba2b
1696                    0000000000000000000000000000000000000000000000000000000000000040
1697                    0000000000000000000000000000000000000000000000000000000000000001
1698                    000000000000000000000000000000000000000000000000000000000000002c
1699                    68747470733a2f2f6574682d6d61696e6e65742e672e616c6368656d792e636f
1700                    6d2f76322f6170695f6b65790000000000000000000000000000000000000000
1701                "
1702                )
1703                .to_vec(),
1704                Some(vec!["\"<rpc url>\"".to_string(), "1".to_string()]),
1705            ),
1706            (
1707                // cast calldata "createFork(string,uint256)" "mainnet" 1
1708                "createFork(string,uint256)",
1709                hex!(
1710                    "
1711                    6ba3ba2b
1712                    0000000000000000000000000000000000000000000000000000000000000040
1713                    0000000000000000000000000000000000000000000000000000000000000001
1714                    0000000000000000000000000000000000000000000000000000000000000007
1715                    6d61696e6e657400000000000000000000000000000000000000000000000000
1716                "
1717                )
1718                .to_vec(),
1719                Some(vec!["\"mainnet\"".to_string(), "1".to_string()]),
1720            ),
1721            (
1722                // cast calldata "createFork(string,bytes32)" "https://eth-mainnet.g.alchemy.com/v2/api_key" 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
1723                "createFork(string,bytes32)",
1724                hex!(
1725                    "
1726                    7ca29682
1727                    0000000000000000000000000000000000000000000000000000000000000040
1728                    ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
1729                    000000000000000000000000000000000000000000000000000000000000002c
1730                    68747470733a2f2f6574682d6d61696e6e65742e672e616c6368656d792e636f
1731                    6d2f76322f6170695f6b65790000000000000000000000000000000000000000
1732                "
1733                )
1734                .to_vec(),
1735                Some(vec![
1736                    "\"<rpc url>\"".to_string(),
1737                    "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
1738                        .to_string(),
1739                ]),
1740            ),
1741            (
1742                // cast calldata "createFork(string,bytes32)" "mainnet"
1743                // 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
1744                "createFork(string,bytes32)",
1745                hex!(
1746                    "
1747                    7ca29682
1748                    0000000000000000000000000000000000000000000000000000000000000040
1749                    ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
1750                    0000000000000000000000000000000000000000000000000000000000000007
1751                    6d61696e6e657400000000000000000000000000000000000000000000000000
1752                "
1753                )
1754                .to_vec(),
1755                Some(vec![
1756                    "\"mainnet\"".to_string(),
1757                    "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
1758                        .to_string(),
1759                ]),
1760            ),
1761            (
1762                // cast calldata "createSelectFork(string)" "https://eth-mainnet.g.alchemy.com/v2/api_key"
1763                "createSelectFork(string)",
1764                hex!(
1765                    "
1766                    98680034
1767                    0000000000000000000000000000000000000000000000000000000000000020
1768                    000000000000000000000000000000000000000000000000000000000000002c
1769                    68747470733a2f2f6574682d6d61696e6e65742e672e616c6368656d792e636f
1770                    6d2f76322f6170695f6b65790000000000000000000000000000000000000000
1771                    "
1772                )
1773                .to_vec(),
1774                Some(vec!["\"<rpc url>\"".to_string()]),
1775            ),
1776            (
1777                // cast calldata "createSelectFork(string)" "mainnet"
1778                "createSelectFork(string)",
1779                hex!(
1780                    "
1781                    98680034
1782                    0000000000000000000000000000000000000000000000000000000000000020
1783                    0000000000000000000000000000000000000000000000000000000000000007
1784                    6d61696e6e657400000000000000000000000000000000000000000000000000
1785                    "
1786                )
1787                .to_vec(),
1788                Some(vec!["\"mainnet\"".to_string()]),
1789            ),
1790            (
1791                // cast calldata "createSelectFork(string,uint256)" "https://eth-mainnet.g.alchemy.com/v2/api_key" 1
1792                "createSelectFork(string,uint256)",
1793                hex!(
1794                    "
1795                    71ee464d
1796                    0000000000000000000000000000000000000000000000000000000000000040
1797                    0000000000000000000000000000000000000000000000000000000000000001
1798                    000000000000000000000000000000000000000000000000000000000000002c
1799                    68747470733a2f2f6574682d6d61696e6e65742e672e616c6368656d792e636f
1800                    6d2f76322f6170695f6b65790000000000000000000000000000000000000000
1801                "
1802                )
1803                .to_vec(),
1804                Some(vec!["\"<rpc url>\"".to_string(), "1".to_string()]),
1805            ),
1806            (
1807                // cast calldata "createSelectFork(string,uint256)" "mainnet" 1
1808                "createSelectFork(string,uint256)",
1809                hex!(
1810                    "
1811                    71ee464d
1812                    0000000000000000000000000000000000000000000000000000000000000040
1813                    0000000000000000000000000000000000000000000000000000000000000001
1814                    0000000000000000000000000000000000000000000000000000000000000007
1815                    6d61696e6e657400000000000000000000000000000000000000000000000000
1816                "
1817                )
1818                .to_vec(),
1819                Some(vec!["\"mainnet\"".to_string(), "1".to_string()]),
1820            ),
1821            (
1822                // cast calldata "createSelectFork(string,bytes32)" "https://eth-mainnet.g.alchemy.com/v2/api_key" 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
1823                "createSelectFork(string,bytes32)",
1824                hex!(
1825                    "
1826                    84d52b7a
1827                    0000000000000000000000000000000000000000000000000000000000000040
1828                    ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
1829                    000000000000000000000000000000000000000000000000000000000000002c
1830                    68747470733a2f2f6574682d6d61696e6e65742e672e616c6368656d792e636f
1831                    6d2f76322f6170695f6b65790000000000000000000000000000000000000000
1832                "
1833                )
1834                .to_vec(),
1835                Some(vec![
1836                    "\"<rpc url>\"".to_string(),
1837                    "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
1838                        .to_string(),
1839                ]),
1840            ),
1841            (
1842                // cast calldata "createSelectFork(string,bytes32)" "mainnet"
1843                // 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
1844                "createSelectFork(string,bytes32)",
1845                hex!(
1846                    "
1847                    84d52b7a
1848                    0000000000000000000000000000000000000000000000000000000000000040
1849                    ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
1850                    0000000000000000000000000000000000000000000000000000000000000007
1851                    6d61696e6e657400000000000000000000000000000000000000000000000000
1852                "
1853                )
1854                .to_vec(),
1855                Some(vec![
1856                    "\"mainnet\"".to_string(),
1857                    "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
1858                        .to_string(),
1859                ]),
1860            ),
1861            (
1862                // cast calldata "rpc(string,string,string)" "https://eth-mainnet.g.alchemy.com/v2/api_key" "eth_getBalance" "[\"0x551e7784778ef8e048e495df49f2614f84a4f1dc\",\"0x0\"]"
1863                "rpc(string,string,string)",
1864                hex!(
1865                    "
1866                    0199a220
1867                    0000000000000000000000000000000000000000000000000000000000000060
1868                    00000000000000000000000000000000000000000000000000000000000000c0
1869                    0000000000000000000000000000000000000000000000000000000000000100
1870                    000000000000000000000000000000000000000000000000000000000000002c
1871                    68747470733a2f2f6574682d6d61696e6e65742e672e616c6368656d792e636f
1872                    6d2f76322f6170695f6b65790000000000000000000000000000000000000000
1873                    000000000000000000000000000000000000000000000000000000000000000e
1874                    6574685f67657442616c616e6365000000000000000000000000000000000000
1875                    0000000000000000000000000000000000000000000000000000000000000034
1876                    5b22307835353165373738343737386566386530343865343935646634396632
1877                    363134663834613466316463222c22307830225d000000000000000000000000
1878                "
1879                )
1880                .to_vec(),
1881                Some(vec![
1882                    "\"<rpc url>\"".to_string(),
1883                    "\"eth_getBalance\"".to_string(),
1884                    "\"[\\\"0x551e7784778ef8e048e495df49f2614f84a4f1dc\\\",\\\"0x0\\\"]\""
1885                        .to_string(),
1886                ]),
1887            ),
1888            (
1889                // cast calldata "rpc(string,string,string)" "mainnet" "eth_getBalance"
1890                // "[\"0x551e7784778ef8e048e495df49f2614f84a4f1dc\",\"0x0\"]"
1891                "rpc(string,string,string)",
1892                hex!(
1893                    "
1894                    0199a220
1895                    0000000000000000000000000000000000000000000000000000000000000060
1896                    00000000000000000000000000000000000000000000000000000000000000a0
1897                    00000000000000000000000000000000000000000000000000000000000000e0
1898                    0000000000000000000000000000000000000000000000000000000000000007
1899                    6d61696e6e657400000000000000000000000000000000000000000000000000
1900                    000000000000000000000000000000000000000000000000000000000000000e
1901                    6574685f67657442616c616e6365000000000000000000000000000000000000
1902                    0000000000000000000000000000000000000000000000000000000000000034
1903                    5b22307835353165373738343737386566386530343865343935646634396632
1904                    363134663834613466316463222c22307830225d000000000000000000000000
1905                "
1906                )
1907                .to_vec(),
1908                Some(vec![
1909                    "\"mainnet\"".to_string(),
1910                    "\"eth_getBalance\"".to_string(),
1911                    "\"[\\\"0x551e7784778ef8e048e495df49f2614f84a4f1dc\\\",\\\"0x0\\\"]\""
1912                        .to_string(),
1913                ]),
1914            ),
1915        ];
1916
1917        // [function_signature, expected]
1918        let cheatcode_output_test_cases = vec![
1919            // Should redact private key on output in all cases:
1920            ("createWallet(string)", Some("<pk>".to_string())),
1921            ("deriveKey(string,uint32)", Some("<pk>".to_string())),
1922            // Should redact RPC URL if defined, except if referenced by an alias:
1923            ("rpcUrl(string)", Some("<rpc url>".to_string())),
1924            ("rpcUrls()", Some("<rpc url>".to_string())),
1925            ("rpcUrlStructs()", Some("<rpc url>".to_string())),
1926        ];
1927
1928        for (function_signature, data, expected) in cheatcode_input_test_cases {
1929            let function = Function::parse(function_signature).unwrap();
1930            let result = decoder.decode_cheatcode_inputs(&function, &data);
1931            assert_eq!(result, expected, "Input case failed for: {function_signature}");
1932        }
1933
1934        for (function_signature, expected) in cheatcode_output_test_cases {
1935            let function = Function::parse(function_signature).unwrap();
1936            let result = Some(decoder.decode_cheatcode_outputs(&function).unwrap_or_default());
1937            assert_eq!(result, expected, "Output case failed for: {function_signature}");
1938        }
1939    }
1940
1941    #[tokio::test]
1942    async fn test_tempo_decode_preserves_existing_labels() {
1943        let decoder = CallTraceDecoder::new();
1944        let trace = CallTrace { address: PATH_USD_ADDRESS, success: true, ..Default::default() };
1945
1946        let decoded = decoder.decode_function(&trace).await;
1947        assert_eq!(decoded.label.as_deref(), Some("PathUSD"));
1948    }
1949
1950    #[tokio::test]
1951    async fn test_t5_decode_does_not_synthesize_general_target_label() {
1952        let mut decoder = CallTraceDecoder::new().clone();
1953        decoder.chain_id = Some(4217);
1954        let trace = CallTrace {
1955            address: address!("0x0000000000000000000000000000000000000123"),
1956            depth: 0,
1957            success: true,
1958            ..Default::default()
1959        };
1960
1961        let decoded = decoder.decode_function(&trace).await;
1962        assert_eq!(decoded.label, None);
1963    }
1964
1965    #[tokio::test]
1966    async fn test_t5_tip20_logo_uri_calls_and_events_decode() {
1967        let decoder = CallTraceDecoder::new();
1968
1969        let call = ITIP20::setLogoURICall { newLogoURI: "https://example.com/logo.png".into() };
1970        let trace = CallTrace {
1971            address: PATH_USD_ADDRESS,
1972            data: call.abi_encode().into(),
1973            success: true,
1974            ..Default::default()
1975        };
1976        let decoded = decoder.decode_function(&trace).await;
1977        let call_data = decoded.call_data.expect("setLogoURI should decode");
1978        assert_eq!(call_data.signature, "setLogoURI(string)");
1979        assert_eq!(call_data.args, vec!["\"https://example.com/logo.png\"".to_string()]);
1980
1981        let call = ITIP20::logoURICall {};
1982        let trace = CallTrace {
1983            address: PATH_USD_ADDRESS,
1984            data: call.abi_encode().into(),
1985            success: true,
1986            ..Default::default()
1987        };
1988        let decoded = decoder.decode_function(&trace).await;
1989        assert_eq!(decoded.call_data.expect("logoURI should decode").signature, "logoURI()");
1990
1991        let event = ITIP20::LogoURIUpdated {
1992            updater: address!("0x0000000000000000000000000000000000000abc"),
1993            newLogoURI: "ipfs://logo".into(),
1994        };
1995        let decoded = decoder.decode_event(&event.encode_log_data()).await;
1996        assert_eq!(decoded.name.as_deref(), Some("LogoURIUpdated"));
1997        let params = decoded.params.expect("LogoURIUpdated params should decode");
1998        assert_eq!(params[0].0, "updater");
1999        assert!(
2000            params[0].1.to_ascii_lowercase().contains("0000000000000000000000000000000000000abc")
2001        );
2002        assert_eq!(params[1], ("newLogoURI".into(), "\"ipfs://logo\"".into()));
2003    }
2004
2005    #[tokio::test]
2006    async fn test_t5_tip20_factory_create_token_with_logo_decodes() {
2007        let decoder = CallTraceDecoder::new();
2008        let call = ITIP20Factory::createToken_1Call {
2009            name: "Example USD".into(),
2010            symbol: "xUSD".into(),
2011            currency: "USD".into(),
2012            quoteToken: PATH_USD_ADDRESS,
2013            admin: address!("0x0000000000000000000000000000000000000abc"),
2014            salt: B256::repeat_byte(0x11),
2015            logoURI: "https://example.com/xusd.png".into(),
2016        };
2017        let trace = CallTrace {
2018            address: TIP20_FACTORY_ADDRESS,
2019            data: call.abi_encode().into(),
2020            success: true,
2021            ..Default::default()
2022        };
2023        let decoded = decoder.decode_function(&trace).await;
2024        let call_data = decoded.call_data.expect("createToken overload should decode");
2025        assert_eq!(
2026            call_data.signature,
2027            "createToken(string,string,string,address,address,bytes32,string)"
2028        );
2029        assert_eq!(call_data.args[6], "\"https://example.com/xusd.png\"");
2030    }
2031
2032    #[tokio::test]
2033    async fn test_t5_stablecoin_dex_order_flipped_event_decodes() {
2034        let decoder = CallTraceDecoder::new();
2035        let event = IStablecoinDEX::OrderFlipped {
2036            orderId: 42,
2037            maker: address!("0x0000000000000000000000000000000000000abc"),
2038            token: PATH_USD_ADDRESS,
2039            amount: 1_000_000,
2040            isBid: false,
2041            tick: 100,
2042            flipTick: 100,
2043        };
2044        let decoded = decoder.decode_event(&event.encode_log_data()).await;
2045        assert_eq!(decoded.name.as_deref(), Some("OrderFlipped"));
2046        let params = decoded.params.expect("OrderFlipped params should decode");
2047        assert_eq!(params[0], ("orderId".into(), "42".into()));
2048        assert_eq!(params[4], ("isBid".into(), "false".into()));
2049        assert_eq!(params[5], ("tick".into(), "100".into()));
2050        assert_eq!(params[6], ("flipTick".into(), "100".into()));
2051    }
2052
2053    #[tokio::test]
2054    async fn test_t5_channel_reserve_call_and_event_decode() {
2055        let mut decoder = CallTraceDecoder::new().clone();
2056        decoder.chain_id = Some(4217);
2057
2058        let open = ITIP20ChannelReserve::openCall {
2059            payee: address!("0x0000000000000000000000000000000000000abc"),
2060            operator: Address::ZERO,
2061            token: PATH_USD_ADDRESS,
2062            deposit: U96::from(1_000_000u64),
2063            salt: B256::repeat_byte(0x22),
2064            authorizedSigner: Address::ZERO,
2065        };
2066        let trace = CallTrace {
2067            address: TIP20_CHANNEL_RESERVE_ADDRESS,
2068            data: open.abi_encode().into(),
2069            depth: 0,
2070            success: true,
2071            ..Default::default()
2072        };
2073        let decoded = decoder.decode_function(&trace).await;
2074        assert_eq!(decoded.label.as_deref(), Some("TIP20ChannelReserve"));
2075        assert_eq!(
2076            decoded.call_data.expect("open should decode").signature,
2077            "open(address,address,address,uint96,bytes32,address)"
2078        );
2079
2080        let transfer = ITIP20::transferCall {
2081            to: address!("0x0000000000000000000000000000000000000def"),
2082            amount: U256::from(1_000_000u64),
2083        };
2084        let trace = CallTrace {
2085            address: PATH_USD_ADDRESS,
2086            data: transfer.abi_encode().into(),
2087            depth: 0,
2088            success: true,
2089            ..Default::default()
2090        };
2091        let decoded = decoder.decode_function(&trace).await;
2092        assert_eq!(decoded.label.as_deref(), Some("PathUSD"));
2093        let json = serde_json::to_string(&decoded).expect("decoded trace serializes");
2094        assert!(json.contains(r#""label":"PathUSD""#));
2095        assert!(!json.contains("payment-lane"));
2096
2097        let balance_of = ITIP20::balanceOfCall {
2098            account: address!("0x0000000000000000000000000000000000000def"),
2099        };
2100        let trace = CallTrace {
2101            address: PATH_USD_ADDRESS,
2102            data: balance_of.abi_encode().into(),
2103            depth: 0,
2104            success: true,
2105            ..Default::default()
2106        };
2107        let decoded = decoder.decode_function(&trace).await;
2108        assert_eq!(decoded.label.as_deref(), Some("PathUSD"));
2109
2110        let event = ITIP20ChannelReserve::ChannelOpened {
2111            channelId: B256::repeat_byte(0x33),
2112            payer: address!("0x0000000000000000000000000000000000000123"),
2113            payee: address!("0x0000000000000000000000000000000000000abc"),
2114            operator: Address::ZERO,
2115            token: PATH_USD_ADDRESS,
2116            authorizedSigner: Address::ZERO,
2117            salt: B256::repeat_byte(0x22),
2118            expiringNonceHash: B256::repeat_byte(0x44),
2119            deposit: U96::from(1_000_000u64),
2120        };
2121        let decoded = decoder.decode_event(&event.encode_log_data()).await;
2122        assert_eq!(decoded.name.as_deref(), Some("ChannelOpened"));
2123        let params = decoded.params.expect("ChannelOpened params should decode");
2124        assert_eq!(params[0].0, "channelId");
2125        assert_eq!(params[8].0, "deposit");
2126        assert!(params[8].1.starts_with("1000000"));
2127    }
2128
2129    #[tokio::test]
2130    async fn test_t7_storage_credits_call_and_error_decode() {
2131        let mut decoder = CallTraceDecoder::new().clone();
2132        decoder.chain_id = Some(4217);
2133
2134        // A write call decodes to its signature and the precompile address is labeled.
2135        let set_mode = IStorageCredits::setModeCall { newMode: IStorageCredits::Mode::Direct };
2136        let trace = CallTrace {
2137            address: STORAGE_CREDITS_ADDRESS,
2138            data: set_mode.abi_encode().into(),
2139            depth: 0,
2140            success: true,
2141            ..Default::default()
2142        };
2143        let decoded = decoder.decode_function(&trace).await;
2144        assert_eq!(decoded.label.as_deref(), Some("StorageCredits"));
2145        assert_eq!(decoded.call_data.expect("setMode should decode").signature, "setMode(uint8)");
2146
2147        // A view call unique to this precompile also decodes.
2148        let mode_of = IStorageCredits::modeOfCall { account: Address::repeat_byte(0x11) };
2149        let trace = CallTrace {
2150            address: STORAGE_CREDITS_ADDRESS,
2151            data: mode_of.abi_encode().into(),
2152            depth: 0,
2153            success: true,
2154            ..Default::default()
2155        };
2156        let decoded = decoder.decode_function(&trace).await;
2157        assert_eq!(decoded.call_data.expect("modeOf should decode").signature, "modeOf(address)");
2158
2159        // The precompile's custom errors decode by name in reverts.
2160        let revert = decoder
2161            .revert_decoder
2162            .decode(IStorageCredits::InvalidMode {}.abi_encode().as_slice(), None);
2163        assert!(revert.contains("InvalidMode"), "{revert}");
2164
2165        // `balanceOf(address)` collides with `ITIP20`'s selector; the global map must keep
2166        // `ITIP20`'s `uint256` return so ordinary token balances above `u64::MAX` still decode.
2167        let selector = IStorageCredits::balanceOfCall::SELECTOR;
2168        let funcs = decoder.functions.get(&selector).expect("balanceOf selector is registered");
2169        assert!(
2170            funcs.iter().any(|f| f.outputs.first().is_some_and(|o| o.ty == "uint256")),
2171            "global balanceOf must return uint256"
2172        );
2173    }
2174
2175    // A mock identifier that records which addresses it was asked to identify.
2176    struct RecordingIdentifier {
2177        queried: Vec<Address>,
2178    }
2179    impl TraceIdentifier for RecordingIdentifier {
2180        fn identify_addresses(&mut self, nodes: &[&CallTraceNode]) -> Vec<IdentifiedAddress<'_>> {
2181            self.queried.extend(nodes.iter().map(|n| n.trace.address));
2182            Vec::new()
2183        }
2184    }
2185
2186    #[test]
2187    fn test_identify_addresses_skips_evm_precompiles() {
2188        use foundry_evm_core::precompiles::SHA_256;
2189
2190        let decoder = CallTraceDecoder::new();
2191
2192        let mut arena = CallTraceArena::default();
2193        let regular_addr = Address::from([0x42; 20]);
2194        arena.nodes_mut()[0].trace.address = regular_addr;
2195
2196        // Standard EVM precompile flagged by the inspector.
2197        arena.nodes_mut().push(CallTraceNode {
2198            trace: CallTrace {
2199                address: SHA_256,
2200                depth: 1,
2201                maybe_precompile: Some(true),
2202                ..Default::default()
2203            },
2204            idx: 1,
2205            ..Default::default()
2206        });
2207
2208        // Standard EVM precompile NOT flagged, caught by is_known_precompile.
2209        arena.nodes_mut().push(CallTraceNode {
2210            trace: CallTrace {
2211                address: SHA_256,
2212                depth: 1,
2213                maybe_precompile: None,
2214                ..Default::default()
2215            },
2216            idx: 2,
2217            ..Default::default()
2218        });
2219
2220        let mut identifier = RecordingIdentifier { queried: Vec::new() };
2221        decoder.identify_addresses(&arena, &mut identifier);
2222
2223        assert_eq!(identifier.queried, vec![regular_addr]);
2224    }
2225
2226    #[test]
2227    fn test_identify_addresses_skips_tempo_precompiles() {
2228        use foundry_evm_core::tempo::{TEMPO_PRECOMPILE_ADDRESSES, TIP20_CHANNEL_RESERVE_ADDRESS};
2229
2230        // Decoder with Tempo chain ID (4217).
2231        let decoder = CallTraceDecoderBuilder::new()
2232            .with_chain_id(Some(4217))
2233            .with_tempo_hardfork(Some(TempoHardfork::T5))
2234            .build();
2235
2236        assert_eq!(
2237            decoder.labels.get(&TIP20_CHANNEL_RESERVE_ADDRESS),
2238            Some(&"TIP20ChannelReserve".to_string())
2239        );
2240
2241        let mut arena = CallTraceArena::default();
2242        let regular_addr = Address::from([0x42; 20]);
2243        arena.nodes_mut()[0].trace.address = regular_addr;
2244
2245        // Tempo precompile — not flagged by inspector, caught by is_known_precompile
2246        // only when chain_id is a Tempo chain.
2247        let tempo_precompile = TEMPO_PRECOMPILE_ADDRESSES[0];
2248        arena.nodes_mut().push(CallTraceNode {
2249            trace: CallTrace {
2250                address: tempo_precompile,
2251                depth: 1,
2252                maybe_precompile: None,
2253                ..Default::default()
2254            },
2255            idx: 1,
2256            ..Default::default()
2257        });
2258
2259        let mut identifier = RecordingIdentifier { queried: Vec::new() };
2260        decoder.identify_addresses(&arena, &mut identifier);
2261
2262        // On a Tempo chain, the Tempo precompile should be filtered out.
2263        assert_eq!(identifier.queried, vec![regular_addr]);
2264    }
2265
2266    #[test]
2267    fn test_precompile_labels_follow_tempo_hardfork_activation_boundaries() {
2268        let labels_for_hardfork = |hardfork| {
2269            CallTraceDecoderBuilder::new()
2270                .with_tempo_hardfork(Some(hardfork))
2271                .build()
2272                .precompile_labels()
2273        };
2274
2275        let t4_labels = labels_for_hardfork(TempoHardfork::T4);
2276        assert_eq!(t4_labels.get(&TIP_FEE_MANAGER_ADDRESS), Some(&"FeeManager".to_string()));
2277        assert!(!t4_labels.contains_key(&TIP20_CHANNEL_RESERVE_ADDRESS));
2278        assert!(!t4_labels.contains_key(&RECEIVE_POLICY_GUARD_ADDRESS));
2279        assert!(!t4_labels.contains_key(&STORAGE_CREDITS_ADDRESS));
2280
2281        let t5_labels = labels_for_hardfork(TempoHardfork::T5);
2282        assert_eq!(t5_labels.get(&TIP_FEE_MANAGER_ADDRESS), Some(&"FeeManager".to_string()));
2283        assert_eq!(
2284            t5_labels.get(&TIP20_CHANNEL_RESERVE_ADDRESS),
2285            Some(&"TIP20ChannelReserve".to_string())
2286        );
2287        assert!(!t5_labels.contains_key(&RECEIVE_POLICY_GUARD_ADDRESS));
2288        assert!(!t5_labels.contains_key(&STORAGE_CREDITS_ADDRESS));
2289
2290        let t6_labels = labels_for_hardfork(TempoHardfork::T6);
2291        assert_eq!(
2292            t6_labels.get(&TIP20_CHANNEL_RESERVE_ADDRESS),
2293            Some(&"TIP20ChannelReserve".to_string())
2294        );
2295        assert_eq!(
2296            t6_labels.get(&RECEIVE_POLICY_GUARD_ADDRESS),
2297            Some(&"ReceivePolicyGuard".to_string())
2298        );
2299        assert!(!t6_labels.contains_key(&STORAGE_CREDITS_ADDRESS));
2300
2301        let t7_labels = labels_for_hardfork(TempoHardfork::T7);
2302        assert_eq!(
2303            t7_labels.get(&RECEIVE_POLICY_GUARD_ADDRESS),
2304            Some(&"ReceivePolicyGuard".to_string())
2305        );
2306        assert_eq!(t7_labels.get(&STORAGE_CREDITS_ADDRESS), Some(&"StorageCredits".to_string()));
2307    }
2308
2309    #[tokio::test]
2310    async fn test_current_committee_decoding_is_durable_and_context_gated() {
2311        let abi = ICurrentCommittee::abi::contract();
2312        let function = abi.functions.get("getCommitteeMembers").unwrap().first().unwrap();
2313        let output = function
2314            .abi_encode_output(&[
2315                DynSolValue::Uint(U256::from(7), 64),
2316                DynSolValue::Array(vec![DynSolValue::FixedBytes(B256::with_last_byte(1), 32)]),
2317            ])
2318            .unwrap();
2319        let trace = CallTrace {
2320            address: CURRENT_COMMITTEE_ADDRESS,
2321            data: function.selector().to_vec().into(),
2322            output: output.into(),
2323            success: true,
2324            ..Default::default()
2325        };
2326
2327        let mut decoder = CallTraceDecoderBuilder::new()
2328            .with_chain_id(Some(4217))
2329            .with_tempo_hardfork(Some(TempoHardfork::T8))
2330            .build();
2331        decoder.clear_addresses();
2332        let decoded = decoder.decode_function(&trace).await;
2333        assert_eq!(decoded.label.as_deref(), Some("CurrentCommittee"));
2334        assert_eq!(decoded.call_data.unwrap().signature, "getCommitteeMembers()");
2335        assert_eq!(
2336            decoded.return_data.unwrap(),
2337            "7, [0x0000000000000000000000000000000000000000000000000000000000000001]"
2338        );
2339
2340        for decoder in [
2341            CallTraceDecoderBuilder::new()
2342                .with_chain_id(Some(4217))
2343                .with_tempo_hardfork(Some(TempoHardfork::T7))
2344                .build(),
2345            CallTraceDecoderBuilder::new()
2346                .with_chain_id(Some(1))
2347                .with_tempo_hardfork(Some(TempoHardfork::T8))
2348                .build(),
2349        ] {
2350            let decoded = decoder.decode_function(&trace).await;
2351            assert_ne!(decoded.label.as_deref(), Some("CurrentCommittee"));
2352            assert!(decoded.call_data.is_none());
2353        }
2354    }
2355
2356    #[tokio::test]
2357    async fn test_current_committee_unauthorized_is_address_scoped() {
2358        let output = ICurrentCommittee::Unauthorized {}.abi_encode();
2359        let trace = CallTrace {
2360            address: CURRENT_COMMITTEE_ADDRESS,
2361            output: output.clone().into(),
2362            success: false,
2363            status: Some(InstructionResult::Revert),
2364            ..Default::default()
2365        };
2366        let decoder = CallTraceDecoderBuilder::new()
2367            .with_chain_id(Some(4217))
2368            .with_tempo_hardfork(Some(TempoHardfork::T8))
2369            .build();
2370        assert_eq!(
2371            decoder.decode_function(&trace).await.return_data.as_deref(),
2372            Some("Unauthorized()")
2373        );
2374
2375        let unrelated = CallTrace { address: Address::ZERO, ..trace };
2376        let baseline = CallTraceDecoder::new().decode_function(&unrelated).await;
2377        assert_eq!(decoder.decode_function(&unrelated).await.return_data, baseline.return_data);
2378    }
2379
2380    #[test]
2381    fn test_precompile_labels_skip_tempo_precompiles_on_other_chains() {
2382        let decoder = CallTraceDecoderBuilder::new()
2383            .with_chain_id(Some(1))
2384            .with_tempo_hardfork(Some(TempoHardfork::T6))
2385            .build();
2386
2387        let labels = decoder.precompile_labels();
2388        assert!(!labels.contains_key(&TIP_FEE_MANAGER_ADDRESS));
2389        assert!(!labels.contains_key(&RECEIVE_POLICY_GUARD_ADDRESS));
2390        assert!(!labels.contains_key(&STORAGE_CREDITS_ADDRESS));
2391    }
2392
2393    #[test]
2394    fn test_tempo_hardfork_labels_do_not_clobber_user_labels() {
2395        use foundry_evm_core::tempo::TIP20_CHANNEL_RESERVE_ADDRESS;
2396
2397        let reserve_label = "UserReserve".to_string();
2398        let guard_label = "UserGuard".to_string();
2399        let decoder = CallTraceDecoderBuilder::new()
2400            .with_labels([
2401                (TIP20_CHANNEL_RESERVE_ADDRESS, reserve_label.clone()),
2402                (RECEIVE_POLICY_GUARD_ADDRESS, guard_label.clone()),
2403            ])
2404            .with_tempo_hardfork(Some(TempoHardfork::T6))
2405            .build();
2406
2407        assert_eq!(decoder.labels.get(&TIP20_CHANNEL_RESERVE_ADDRESS), Some(&reserve_label));
2408        assert_eq!(decoder.labels.get(&RECEIVE_POLICY_GUARD_ADDRESS), Some(&guard_label));
2409    }
2410
2411    #[test]
2412    fn test_tempo_hardfork_none_does_not_remove_user_reserve_label() {
2413        use foundry_evm_core::tempo::TIP20_CHANNEL_RESERVE_ADDRESS;
2414
2415        let reserve_label = "UserReserve".to_string();
2416        let decoder = CallTraceDecoderBuilder::new()
2417            .with_labels([(TIP20_CHANNEL_RESERVE_ADDRESS, reserve_label.clone())])
2418            .with_tempo_hardfork(None)
2419            .build();
2420
2421        assert_eq!(decoder.labels.get(&TIP20_CHANNEL_RESERVE_ADDRESS), Some(&reserve_label));
2422    }
2423
2424    #[tokio::test]
2425    async fn test_decode_receive_policy_guard_at_t6() {
2426        let function = Function::parse("claim(address,bytes)").unwrap();
2427        let data = function
2428            .abi_encode_input(&[
2429                DynSolValue::Address(Address::from([0x11; 20])),
2430                DynSolValue::Bytes(vec![0x12, 0x34]),
2431            ])
2432            .unwrap();
2433        let trace = CallTrace {
2434            address: RECEIVE_POLICY_GUARD_ADDRESS,
2435            data: data.into(),
2436            success: true,
2437            ..Default::default()
2438        };
2439
2440        let decoder = CallTraceDecoderBuilder::new()
2441            .with_chain_id(Some(4217))
2442            .with_tempo_hardfork(Some(TempoHardfork::T6))
2443            .build();
2444        let decoded = decoder.decode_function(&trace).await;
2445
2446        assert_eq!(decoded.label, Some("ReceivePolicyGuard".to_string()));
2447        assert_eq!(decoded.call_data.unwrap().signature, "claim(address,bytes)");
2448    }
2449
2450    #[tokio::test]
2451    async fn test_t6_receive_policy_calls_decode() {
2452        let decoder = CallTraceDecoderBuilder::new()
2453            .with_chain_id(Some(4217))
2454            .with_tempo_hardfork(Some(TempoHardfork::T6))
2455            .build();
2456
2457        let set_policy = ITIP403Registry::setReceivePolicyCall {
2458            senderPolicyId: 7,
2459            tokenFilterId: 9,
2460            recoveryAuthority: address!("0x0000000000000000000000000000000000000abc"),
2461        };
2462        let decoded = decoder
2463            .decode_function(&CallTrace {
2464                address: TIP403_REGISTRY_ADDRESS,
2465                data: set_policy.abi_encode().into(),
2466                success: true,
2467                ..Default::default()
2468            })
2469            .await;
2470        let call_data = decoded.call_data.expect("setReceivePolicy should decode");
2471        assert_eq!(decoded.label.as_deref(), Some("TIP403Registry"));
2472        assert_eq!(call_data.signature, "setReceivePolicy(uint64,uint64,address)");
2473        assert_eq!(call_data.args[0], "7");
2474        assert_eq!(call_data.args[1], "9");
2475
2476        let validate = ITIP403Registry::validateReceivePolicyCall {
2477            token: PATH_USD_ADDRESS,
2478            sender: address!("0x0000000000000000000000000000000000000def"),
2479            receiver: address!("0x0000000000000000000000000000000000000123"),
2480        };
2481        let decoded = decoder
2482            .decode_function(&CallTrace {
2483                address: TIP403_REGISTRY_ADDRESS,
2484                data: validate.abi_encode().into(),
2485                success: true,
2486                ..Default::default()
2487            })
2488            .await;
2489        let call_data = decoded.call_data.expect("validateReceivePolicy should decode");
2490        assert_eq!(call_data.signature, "validateReceivePolicy(address,address,address)");
2491        assert!(call_data.args[0].contains("PathUSD"));
2492    }
2493
2494    #[tokio::test]
2495    async fn test_t6_admin_key_calls_decode() {
2496        let decoder = CallTraceDecoderBuilder::new()
2497            .with_chain_id(Some(4217))
2498            .with_tempo_hardfork(Some(TempoHardfork::T6))
2499            .build();
2500        let account = address!("0x0000000000000000000000000000000000000abc");
2501        let key = address!("0x0000000000000000000000000000000000000def");
2502        let signature = vec![0x04, 0xaa, 0xbb];
2503
2504        let cases = [
2505            (
2506                ACCOUNT_KEYCHAIN_ADDRESS,
2507                IAccountKeychain::authorizeAdminKeyCall {
2508                    keyId: key,
2509                    signatureType: IAccountKeychain::SignatureType::Secp256k1,
2510                    witness: B256::repeat_byte(0x11),
2511                }
2512                .abi_encode()
2513                .into(),
2514                "authorizeAdminKey(address,uint8,bytes32)",
2515            ),
2516            (
2517                ACCOUNT_KEYCHAIN_ADDRESS,
2518                IAccountKeychain::isAdminKeyCall { account, keyId: key }.abi_encode().into(),
2519                "isAdminKey(address,address)",
2520            ),
2521            (
2522                SIGNATURE_VERIFIER_ADDRESS,
2523                ISignatureVerifier::verifyKeychainCall {
2524                    account,
2525                    hash: B256::repeat_byte(0x22),
2526                    signature: signature.clone().into(),
2527                }
2528                .abi_encode()
2529                .into(),
2530                "verifyKeychain(address,bytes32,bytes)",
2531            ),
2532            (
2533                SIGNATURE_VERIFIER_ADDRESS,
2534                ISignatureVerifier::verifyKeychainAdminCall {
2535                    account,
2536                    hash: B256::repeat_byte(0x33),
2537                    signature: signature.into(),
2538                }
2539                .abi_encode()
2540                .into(),
2541                "verifyKeychainAdmin(address,bytes32,bytes)",
2542            ),
2543        ];
2544
2545        for (address, data, signature) in cases {
2546            let decoded = decoder
2547                .decode_function(&CallTrace { address, data, success: true, ..Default::default() })
2548                .await;
2549            assert_eq!(
2550                decoded.call_data.expect("T6 keychain call should decode").signature,
2551                signature
2552            );
2553        }
2554    }
2555
2556    #[tokio::test]
2557    async fn test_t6_receive_policy_and_admin_events_decode() {
2558        let decoder = CallTraceDecoder::new();
2559        let account = address!("0x0000000000000000000000000000000000000abc");
2560        let key = address!("0x0000000000000000000000000000000000000def");
2561
2562        let events = [
2563            (
2564                ITIP403Registry::ReceivePolicyUpdated {
2565                    account,
2566                    senderPolicyId: 7,
2567                    tokenFilterId: 9,
2568                    recoveryAuthority: key,
2569                }
2570                .encode_log_data(),
2571                "ReceivePolicyUpdated",
2572            ),
2573            (
2574                IAccountKeychain::AdminKeyAuthorized { account, publicKey: key }.encode_log_data(),
2575                "AdminKeyAuthorized",
2576            ),
2577            (
2578                IReceivePolicyGuard::ReceiptClaimed {
2579                    token: PATH_USD_ADDRESS,
2580                    receiver: account,
2581                    blockedNonce: 11,
2582                    blockedAt: 12,
2583                    receiptVersion: 1,
2584                    originator: key,
2585                    recipient: account,
2586                    recoveryAuthority: key,
2587                    caller: key,
2588                    to: account,
2589                    amount: U256::from(123),
2590                }
2591                .encode_log_data(),
2592                "ReceiptClaimed",
2593            ),
2594            (
2595                IReceivePolicyGuard::ReceiptBurned {
2596                    token: PATH_USD_ADDRESS,
2597                    receiver: account,
2598                    blockedNonce: 11,
2599                    blockedAt: 12,
2600                    receiptVersion: 1,
2601                    originator: key,
2602                    recipient: account,
2603                    recoveryAuthority: key,
2604                    caller: key,
2605                    amount: U256::from(123),
2606                }
2607                .encode_log_data(),
2608                "ReceiptBurned",
2609            ),
2610        ];
2611
2612        for (log, expected_name) in events {
2613            let decoded = decoder.decode_event(&log).await;
2614            assert_eq!(decoded.name.as_deref(), Some(expected_name));
2615            assert!(decoded.params.expect("event params should decode").len() >= 2);
2616        }
2617    }
2618
2619    #[tokio::test]
2620    async fn test_t6_claim_receipt_bytes_decode_in_calls_and_transfer_blocked_event() {
2621        let decoder = CallTraceDecoder::new();
2622        let recovery = address!("0x0000000000000000000000000000000000000abc");
2623        let originator = address!("0x0000000000000000000000000000000000000def");
2624        let recipient = address!("0x0000000000000000000000000000000000000123");
2625        let receipt = IReceivePolicyGuard::ClaimReceiptV1::new(
2626            PATH_USD_ADDRESS,
2627            recovery,
2628            originator,
2629            recipient,
2630            12,
2631            34,
2632            ITIP403Registry::BlockedReason::RECEIVE_POLICY as u8,
2633            IReceivePolicyGuard::InboundKind::TRANSFER,
2634            B256::repeat_byte(0x44),
2635        )
2636        .abi_encode();
2637
2638        let claim =
2639            IReceivePolicyGuard::claimCall { to: recipient, receipt: receipt.clone().into() };
2640        let decoded = decoder
2641            .decode_function(&CallTrace {
2642                address: RECEIVE_POLICY_GUARD_ADDRESS,
2643                data: claim.abi_encode().into(),
2644                success: true,
2645                ..Default::default()
2646            })
2647            .await;
2648        let call_data = decoded.call_data.expect("claim should decode");
2649        assert_eq!(call_data.signature, "claim(address,bytes)");
2650        assert!(call_data.args[1].contains("ClaimReceiptV1"));
2651        assert!(call_data.args[1].contains("blockedNonce: 34"));
2652        assert!(call_data.args[1].contains(&originator.to_string()));
2653
2654        let decoded = decoder
2655            .decode_function(&CallTrace {
2656                address: Address::from([0x77; 20]),
2657                data: claim.abi_encode().into(),
2658                success: true,
2659                ..Default::default()
2660            })
2661            .await;
2662        let call_data = decoded.call_data.expect("matching claim selector should decode");
2663        assert_eq!(call_data.signature, "claim(address,bytes)");
2664        assert!(!call_data.args[1].contains("ClaimReceiptV1"));
2665        assert!(call_data.args[1].starts_with("0x"));
2666
2667        let blocked = IReceivePolicyGuard::TransferBlocked {
2668            token: PATH_USD_ADDRESS,
2669            receiver: recipient,
2670            blockedNonce: 34,
2671            amount: U256::from(123),
2672            receiptVersion: 1,
2673            receipt: receipt.into(),
2674        };
2675        let blocked_log = blocked.encode_log_data();
2676        let decoded =
2677            decoder.decode_event_with_address(RECEIVE_POLICY_GUARD_ADDRESS, &blocked_log).await;
2678        assert_eq!(decoded.name.as_deref(), Some("TransferBlocked"));
2679        let params = decoded.params.expect("TransferBlocked params should decode");
2680        let receipt = params.iter().find(|(name, _)| name == "receipt").unwrap();
2681        assert!(receipt.1.contains("ClaimReceiptV1"));
2682        assert!(receipt.1.contains("blockedReason: 2"));
2683        assert!(receipt.1.contains("kind: 0"));
2684
2685        let decoded =
2686            decoder.decode_event_with_address(Address::from([0x77; 20]), &blocked_log).await;
2687        assert_eq!(decoded.name.as_deref(), Some("TransferBlocked"));
2688        let params = decoded.params.expect("TransferBlocked params should decode");
2689        let receipt = params.iter().find(|(name, _)| name == "receipt").unwrap();
2690        assert!(!receipt.1.contains("ClaimReceiptV1"));
2691        assert!(receipt.1.starts_with("0x"));
2692    }
2693
2694    #[test]
2695    fn test_identify_addresses_does_not_skip_future_tempo_precompiles() {
2696        use foundry_evm_core::tempo::TIP20_CHANNEL_RESERVE_ADDRESS;
2697
2698        let decoder = CallTraceDecoderBuilder::new()
2699            .with_chain_id(Some(4217))
2700            .with_tempo_hardfork(Some(TempoHardfork::T4))
2701            .build();
2702
2703        let mut arena = CallTraceArena::default();
2704        let regular_addr = Address::from([0x42; 20]);
2705        arena.nodes_mut()[0].trace.address = regular_addr;
2706
2707        arena.nodes_mut().push(CallTraceNode {
2708            trace: CallTrace {
2709                address: TIP20_CHANNEL_RESERVE_ADDRESS,
2710                depth: 1,
2711                maybe_precompile: None,
2712                ..Default::default()
2713            },
2714            idx: 1,
2715            ..Default::default()
2716        });
2717
2718        let mut identifier = RecordingIdentifier { queried: Vec::new() };
2719        decoder.identify_addresses(&arena, &mut identifier);
2720
2721        assert_eq!(identifier.queried, vec![regular_addr, TIP20_CHANNEL_RESERVE_ADDRESS]);
2722    }
2723
2724    #[test]
2725    fn test_identify_addresses_does_not_skip_tempo_precompiles_on_other_chains() {
2726        use foundry_evm_core::tempo::TEMPO_PRECOMPILE_ADDRESSES;
2727
2728        // Decoder with Ethereum mainnet chain ID (1).
2729        let mut decoder = CallTraceDecoder::new().clone();
2730        decoder.chain_id = Some(1);
2731
2732        let mut arena = CallTraceArena::default();
2733        let regular_addr = Address::from([0x42; 20]);
2734        arena.nodes_mut()[0].trace.address = regular_addr;
2735
2736        let tempo_precompile = TEMPO_PRECOMPILE_ADDRESSES[0];
2737        arena.nodes_mut().push(CallTraceNode {
2738            trace: CallTrace {
2739                address: tempo_precompile,
2740                depth: 1,
2741                maybe_precompile: None,
2742                ..Default::default()
2743            },
2744            idx: 1,
2745            ..Default::default()
2746        });
2747
2748        let mut identifier = RecordingIdentifier { queried: Vec::new() };
2749        decoder.identify_addresses(&arena, &mut identifier);
2750
2751        // On Ethereum, Tempo precompile addresses are regular contracts — should NOT be filtered.
2752        assert_eq!(identifier.queried, vec![regular_addr, tempo_precompile]);
2753    }
2754}