foundry_evm_traces/decoder/
mod.rs

1use crate::{
2    debug::DebugTraceIdentifier,
3    identifier::{
4        AddressIdentity, LocalTraceIdentifier, SingleSignaturesIdentifier, TraceIdentifier,
5    },
6    CallTrace, CallTraceArena, CallTraceNode, DecodedCallData,
7};
8use alloy_dyn_abi::{DecodedEvent, DynSolValue, EventExt, FunctionExt, JsonAbiExt};
9use alloy_json_abi::{Error, Event, Function, JsonAbi};
10use alloy_primitives::{
11    map::{hash_map::Entry, HashMap},
12    Address, LogData, Selector, B256,
13};
14use foundry_common::{
15    abi::get_indexed_event, fmt::format_token, get_contract_name, ContractsByArtifact, SELECTOR_LEN,
16};
17use foundry_evm_core::{
18    abi::{console, Vm},
19    constants::{
20        CALLER, CHEATCODE_ADDRESS, DEFAULT_CREATE2_DEPLOYER, HARDHAT_CONSOLE_ADDRESS,
21        TEST_CONTRACT_ADDRESS,
22    },
23    decode::RevertDecoder,
24    precompiles::{
25        BLAKE_2F, EC_ADD, EC_MUL, EC_PAIRING, EC_RECOVER, IDENTITY, MOD_EXP, POINT_EVALUATION,
26        RIPEMD_160, SHA_256,
27    },
28};
29use itertools::Itertools;
30use revm_inspectors::tracing::types::{DecodedCallLog, DecodedCallTrace};
31use std::{collections::BTreeMap, sync::OnceLock};
32
33mod precompiles;
34
35/// Build a new [CallTraceDecoder].
36#[derive(Default)]
37#[must_use = "builders do nothing unless you call `build` on them"]
38pub struct CallTraceDecoderBuilder {
39    decoder: CallTraceDecoder,
40}
41
42impl CallTraceDecoderBuilder {
43    /// Create a new builder.
44    #[inline]
45    pub fn new() -> Self {
46        Self { decoder: CallTraceDecoder::new().clone() }
47    }
48
49    /// Add known labels to the decoder.
50    #[inline]
51    pub fn with_labels(mut self, labels: impl IntoIterator<Item = (Address, String)>) -> Self {
52        self.decoder.labels.extend(labels);
53        self
54    }
55
56    /// Add known errors to the decoder.
57    #[inline]
58    pub fn with_abi(mut self, abi: &JsonAbi) -> Self {
59        self.decoder.collect_abi(abi, None);
60        self
61    }
62
63    /// Add known contracts to the decoder.
64    #[inline]
65    pub fn with_known_contracts(mut self, contracts: &ContractsByArtifact) -> Self {
66        trace!(target: "evm::traces", len=contracts.len(), "collecting known contract ABIs");
67        for contract in contracts.values() {
68            self.decoder.collect_abi(&contract.abi, None);
69        }
70        self
71    }
72
73    /// Add known contracts to the decoder from a `LocalTraceIdentifier`.
74    #[inline]
75    pub fn with_local_identifier_abis(self, identifier: &LocalTraceIdentifier<'_>) -> Self {
76        self.with_known_contracts(identifier.contracts())
77    }
78
79    /// Sets the verbosity level of the decoder.
80    #[inline]
81    pub fn with_verbosity(mut self, level: u8) -> Self {
82        self.decoder.verbosity = level;
83        self
84    }
85
86    /// Sets the signature identifier for events and functions.
87    #[inline]
88    pub fn with_signature_identifier(mut self, identifier: SingleSignaturesIdentifier) -> Self {
89        self.decoder.signature_identifier = Some(identifier);
90        self
91    }
92
93    /// Sets the debug identifier for the decoder.
94    #[inline]
95    pub fn with_debug_identifier(mut self, identifier: DebugTraceIdentifier) -> Self {
96        self.decoder.debug_identifier = Some(identifier);
97        self
98    }
99
100    /// Build the decoder.
101    #[inline]
102    pub fn build(self) -> CallTraceDecoder {
103        self.decoder
104    }
105}
106
107/// The call trace decoder.
108///
109/// The decoder collects address labels and ABIs from any number of [TraceIdentifier]s, which it
110/// then uses to decode the call trace.
111///
112/// Note that a call trace decoder is required for each new set of traces, since addresses in
113/// different sets might overlap.
114#[derive(Clone, Debug, Default)]
115pub struct CallTraceDecoder {
116    /// Addresses identified to be a specific contract.
117    ///
118    /// The values are in the form `"<artifact>:<contract>"`.
119    pub contracts: HashMap<Address, String>,
120    /// Address labels.
121    pub labels: HashMap<Address, String>,
122    /// Contract addresses that have a receive function.
123    pub receive_contracts: Vec<Address>,
124    /// Contract addresses that have fallback functions, mapped to function sigs.
125    pub fallback_contracts: HashMap<Address, Vec<String>>,
126
127    /// All known functions.
128    pub functions: HashMap<Selector, Vec<Function>>,
129    /// All known events.
130    pub events: BTreeMap<(B256, usize), Vec<Event>>,
131    /// Revert decoder. Contains all known custom errors.
132    pub revert_decoder: RevertDecoder,
133
134    /// A signature identifier for events and functions.
135    pub signature_identifier: Option<SingleSignaturesIdentifier>,
136    /// Verbosity level
137    pub verbosity: u8,
138
139    /// Optional identifier of individual trace steps.
140    pub debug_identifier: Option<DebugTraceIdentifier>,
141}
142
143impl CallTraceDecoder {
144    /// Creates a new call trace decoder.
145    ///
146    /// The call trace decoder always knows how to decode calls to the cheatcode address, as well
147    /// as DSTest-style logs.
148    pub fn new() -> &'static Self {
149        // If you want to take arguments in this function, assign them to the fields of the cloned
150        // lazy instead of removing it
151        static INIT: OnceLock<CallTraceDecoder> = OnceLock::new();
152        INIT.get_or_init(Self::init)
153    }
154
155    fn init() -> Self {
156        Self {
157            contracts: Default::default(),
158            labels: HashMap::from_iter([
159                (CHEATCODE_ADDRESS, "VM".to_string()),
160                (HARDHAT_CONSOLE_ADDRESS, "console".to_string()),
161                (DEFAULT_CREATE2_DEPLOYER, "Create2Deployer".to_string()),
162                (CALLER, "DefaultSender".to_string()),
163                (TEST_CONTRACT_ADDRESS, "DefaultTestContract".to_string()),
164                (EC_RECOVER, "ECRecover".to_string()),
165                (SHA_256, "SHA-256".to_string()),
166                (RIPEMD_160, "RIPEMD-160".to_string()),
167                (IDENTITY, "Identity".to_string()),
168                (MOD_EXP, "ModExp".to_string()),
169                (EC_ADD, "ECAdd".to_string()),
170                (EC_MUL, "ECMul".to_string()),
171                (EC_PAIRING, "ECPairing".to_string()),
172                (BLAKE_2F, "Blake2F".to_string()),
173                (POINT_EVALUATION, "PointEvaluation".to_string()),
174            ]),
175            receive_contracts: Default::default(),
176            fallback_contracts: Default::default(),
177
178            functions: console::hh::abi::functions()
179                .into_values()
180                .chain(Vm::abi::functions().into_values())
181                .flatten()
182                .map(|func| (func.selector(), vec![func]))
183                .collect(),
184            events: console::ds::abi::events()
185                .into_values()
186                .flatten()
187                .map(|event| ((event.selector(), indexed_inputs(&event)), vec![event]))
188                .collect(),
189            revert_decoder: Default::default(),
190
191            signature_identifier: None,
192            verbosity: 0,
193
194            debug_identifier: None,
195        }
196    }
197
198    /// Clears all known addresses.
199    pub fn clear_addresses(&mut self) {
200        self.contracts.clear();
201
202        let default_labels = &Self::new().labels;
203        if self.labels.len() > default_labels.len() {
204            self.labels.clone_from(default_labels);
205        }
206
207        self.receive_contracts.clear();
208        self.fallback_contracts.clear();
209    }
210
211    /// Identify unknown addresses in the specified call trace using the specified identifier.
212    ///
213    /// Unknown contracts are contracts that either lack a label or an ABI.
214    pub fn identify(&mut self, trace: &CallTraceArena, identifier: &mut impl TraceIdentifier) {
215        self.collect_identities(identifier.identify_addresses(self.trace_addresses(trace)));
216    }
217
218    /// Adds a single event to the decoder.
219    pub fn push_event(&mut self, event: Event) {
220        self.events.entry((event.selector(), indexed_inputs(&event))).or_default().push(event);
221    }
222
223    /// Adds a single function to the decoder.
224    pub fn push_function(&mut self, function: Function) {
225        match self.functions.entry(function.selector()) {
226            Entry::Occupied(entry) => {
227                // This shouldn't happen that often.
228                if entry.get().contains(&function) {
229                    return;
230                }
231                trace!(target: "evm::traces", selector=%entry.key(), new=%function.signature(), "duplicate function selector");
232                entry.into_mut().push(function);
233            }
234            Entry::Vacant(entry) => {
235                entry.insert(vec![function]);
236            }
237        }
238    }
239
240    /// Adds a single error to the decoder.
241    pub fn push_error(&mut self, error: Error) {
242        self.revert_decoder.push_error(error);
243    }
244
245    /// Returns an iterator over the trace addresses.
246    pub fn trace_addresses<'a>(
247        &'a self,
248        arena: &'a CallTraceArena,
249    ) -> impl Iterator<Item = (&'a Address, Option<&'a [u8]>, Option<&'a [u8]>)> + Clone + 'a {
250        arena
251            .nodes()
252            .iter()
253            .map(|node| {
254                (
255                    &node.trace.address,
256                    node.trace.kind.is_any_create().then_some(&node.trace.output[..]),
257                    node.trace.kind.is_any_create().then_some(&node.trace.data[..]),
258                )
259            })
260            .filter(|&(address, _, _)| {
261                !self.labels.contains_key(address) || !self.contracts.contains_key(address)
262            })
263    }
264
265    fn collect_identities(&mut self, identities: Vec<AddressIdentity<'_>>) {
266        // Skip logging if there are no identities.
267        if identities.is_empty() {
268            return;
269        }
270
271        trace!(target: "evm::traces", len=identities.len(), "collecting address identities");
272        for AddressIdentity { address, label, contract, abi, artifact_id: _ } in identities {
273            let _span = trace_span!(target: "evm::traces", "identity", ?contract, ?label).entered();
274
275            if let Some(contract) = contract {
276                self.contracts.entry(address).or_insert(contract);
277            }
278
279            if let Some(label) = label {
280                self.labels.entry(address).or_insert(label);
281            }
282
283            if let Some(abi) = abi {
284                self.collect_abi(&abi, Some(&address));
285            }
286        }
287    }
288
289    fn collect_abi(&mut self, abi: &JsonAbi, address: Option<&Address>) {
290        trace!(target: "evm::traces", len=abi.len(), ?address, "collecting ABI");
291        for function in abi.functions() {
292            self.push_function(function.clone());
293        }
294        for event in abi.events() {
295            self.push_event(event.clone());
296        }
297        for error in abi.errors() {
298            self.push_error(error.clone());
299        }
300        if let Some(address) = address {
301            if abi.receive.is_some() {
302                self.receive_contracts.push(*address);
303            }
304
305            if abi.fallback.is_some() {
306                let mut functions_sig = vec![];
307                for function in abi.functions() {
308                    functions_sig.push(function.signature());
309                }
310                self.fallback_contracts.insert(*address, functions_sig);
311            }
312        }
313    }
314
315    /// Populates the traces with decoded data by mutating the
316    /// [CallTrace] in place. See [CallTraceDecoder::decode_function] and
317    /// [CallTraceDecoder::decode_event] for more details.
318    pub async fn populate_traces(&self, traces: &mut Vec<CallTraceNode>) {
319        for node in traces {
320            node.trace.decoded = self.decode_function(&node.trace).await;
321            for log in &mut node.logs {
322                log.decoded = self.decode_event(&log.raw_log).await;
323            }
324
325            if let Some(debug) = self.debug_identifier.as_ref() {
326                if let Some(identified) = self.contracts.get(&node.trace.address) {
327                    debug.identify_node_steps(node, get_contract_name(identified))
328                }
329            }
330        }
331    }
332
333    /// Decodes a call trace.
334    pub async fn decode_function(&self, trace: &CallTrace) -> DecodedCallTrace {
335        if let Some(trace) = precompiles::decode(trace, 1) {
336            return trace;
337        }
338
339        let label = self.labels.get(&trace.address).cloned();
340
341        let cdata = &trace.data;
342        if trace.address == DEFAULT_CREATE2_DEPLOYER {
343            return DecodedCallTrace {
344                label,
345                call_data: Some(DecodedCallData { signature: "create2".to_string(), args: vec![] }),
346                return_data: self.default_return_data(trace),
347            };
348        }
349
350        if cdata.len() >= SELECTOR_LEN {
351            let selector = &cdata[..SELECTOR_LEN];
352            let mut functions = Vec::new();
353            let functions = match self.functions.get(selector) {
354                Some(fs) => fs,
355                None => {
356                    if let Some(identifier) = &self.signature_identifier {
357                        if let Some(function) =
358                            identifier.write().await.identify_function(selector).await
359                        {
360                            functions.push(function);
361                        }
362                    }
363                    &functions
364                }
365            };
366            let [func, ..] = &functions[..] else {
367                return DecodedCallTrace {
368                    label,
369                    call_data: self.fallback_contracts.get(&trace.address).map(|_| {
370                        DecodedCallData {
371                            signature: "fallback()".to_string(),
372                            args: vec![cdata.to_string()],
373                        }
374                    }),
375                    return_data: self.default_return_data(trace),
376                };
377            };
378
379            // If traced contract is a fallback contract, check if it has the decoded function.
380            // If not, then replace call data signature with `fallback`.
381            let mut call_data = self.decode_function_input(trace, func);
382            if let Some(fallback_functions) = self.fallback_contracts.get(&trace.address) {
383                if !fallback_functions.contains(&func.signature()) {
384                    call_data.signature = "fallback()".to_string();
385                }
386            }
387
388            DecodedCallTrace {
389                label,
390                call_data: Some(call_data),
391                return_data: self.decode_function_output(trace, functions),
392            }
393        } else {
394            let has_receive = self.receive_contracts.contains(&trace.address);
395            let signature =
396                if cdata.is_empty() && has_receive { "receive()" } else { "fallback()" }
397                    .to_string();
398            let args = if cdata.is_empty() { Vec::new() } else { vec![cdata.to_string()] };
399            DecodedCallTrace {
400                label,
401                call_data: Some(DecodedCallData { signature, args }),
402                return_data: self.default_return_data(trace),
403            }
404        }
405    }
406
407    /// Decodes a function's input into the given trace.
408    fn decode_function_input(&self, trace: &CallTrace, func: &Function) -> DecodedCallData {
409        let mut args = None;
410        if trace.data.len() >= SELECTOR_LEN {
411            if trace.address == CHEATCODE_ADDRESS {
412                // Try to decode cheatcode inputs in a more custom way
413                if let Some(v) = self.decode_cheatcode_inputs(func, &trace.data) {
414                    args = Some(v);
415                }
416            }
417
418            if args.is_none() {
419                if let Ok(v) = func.abi_decode_input(&trace.data[SELECTOR_LEN..], false) {
420                    args = Some(v.iter().map(|value| self.format_value(value)).collect());
421                }
422            }
423        }
424
425        DecodedCallData { signature: func.signature(), args: args.unwrap_or_default() }
426    }
427
428    /// Custom decoding for cheatcode inputs.
429    fn decode_cheatcode_inputs(&self, func: &Function, data: &[u8]) -> Option<Vec<String>> {
430        match func.name.as_str() {
431            "expectRevert" => Some(vec![self.revert_decoder.decode(data, None)]),
432            "addr" | "createWallet" | "deriveKey" | "rememberKey" => {
433                // Redact private key in all cases
434                Some(vec!["<pk>".to_string()])
435            }
436            "broadcast" | "startBroadcast" => {
437                // Redact private key if defined
438                // broadcast(uint256) / startBroadcast(uint256)
439                if !func.inputs.is_empty() && func.inputs[0].ty == "uint256" {
440                    Some(vec!["<pk>".to_string()])
441                } else {
442                    None
443                }
444            }
445            "getNonce" => {
446                // Redact private key if defined
447                // getNonce(Wallet)
448                if !func.inputs.is_empty() && func.inputs[0].ty == "tuple" {
449                    Some(vec!["<pk>".to_string()])
450                } else {
451                    None
452                }
453            }
454            "sign" | "signP256" => {
455                let mut decoded = func.abi_decode_input(&data[SELECTOR_LEN..], false).ok()?;
456
457                // Redact private key and replace in trace
458                // sign(uint256,bytes32) / signP256(uint256,bytes32) / sign(Wallet,bytes32)
459                if !decoded.is_empty() &&
460                    (func.inputs[0].ty == "uint256" || func.inputs[0].ty == "tuple")
461                {
462                    decoded[0] = DynSolValue::String("<pk>".to_string());
463                }
464
465                Some(decoded.iter().map(format_token).collect())
466            }
467            "signDelegation" | "signAndAttachDelegation" => {
468                let mut decoded = func.abi_decode_input(&data[SELECTOR_LEN..], false).ok()?;
469                // Redact private key and replace in trace for
470                // signAndAttachDelegation(address implementation, uint256 privateKey)
471                // signDelegation(address implementation, uint256 privateKey)
472                decoded[1] = DynSolValue::String("<pk>".to_string());
473                Some(decoded.iter().map(format_token).collect())
474            }
475            "parseJson" |
476            "parseJsonUint" |
477            "parseJsonUintArray" |
478            "parseJsonInt" |
479            "parseJsonIntArray" |
480            "parseJsonString" |
481            "parseJsonStringArray" |
482            "parseJsonAddress" |
483            "parseJsonAddressArray" |
484            "parseJsonBool" |
485            "parseJsonBoolArray" |
486            "parseJsonBytes" |
487            "parseJsonBytesArray" |
488            "parseJsonBytes32" |
489            "parseJsonBytes32Array" |
490            "writeJson" |
491            // `keyExists` is being deprecated in favor of `keyExistsJson`. It will be removed in future versions.
492            "keyExists" | 
493            "keyExistsJson" |
494            "serializeBool" |
495            "serializeUint" |
496            "serializeUintToHex" |
497            "serializeInt" |
498            "serializeAddress" |
499            "serializeBytes32" |
500            "serializeString" |
501            "serializeBytes" => {
502                if self.verbosity >= 5 {
503                    None
504                } else {
505                    let mut decoded = func.abi_decode_input(&data[SELECTOR_LEN..], false).ok()?;
506                    let token = if func.name.as_str() == "parseJson" ||
507                        // `keyExists` is being deprecated in favor of `keyExistsJson`. It will be removed in future versions.
508                        func.name.as_str() == "keyExists" || 
509                        func.name.as_str() == "keyExistsJson"
510                    {
511                        "<JSON file>"
512                    } else {
513                        "<stringified JSON>"
514                    };
515                    decoded[0] = DynSolValue::String(token.to_string());
516                    Some(decoded.iter().map(format_token).collect())
517                }
518            }
519            s if s.contains("Toml") => {
520                if self.verbosity >= 5 {
521                    None
522                } else {
523                    let mut decoded = func.abi_decode_input(&data[SELECTOR_LEN..], false).ok()?;
524                    let token = if func.name.as_str() == "parseToml" ||
525                        func.name.as_str() == "keyExistsToml"
526                    {
527                        "<TOML file>"
528                    } else {
529                        "<stringified TOML>"
530                    };
531                    decoded[0] = DynSolValue::String(token.to_string());
532                    Some(decoded.iter().map(format_token).collect())
533                }
534            }
535            "createFork" |
536            "createSelectFork" |
537            "rpc" => {
538                let mut decoded = func.abi_decode_input(&data[SELECTOR_LEN..], false).ok()?;
539
540                // Redact RPC URL except if referenced by an alias
541                if !decoded.is_empty() && func.inputs[0].ty == "string" {
542                    let url_or_alias = decoded[0].as_str().unwrap_or_default();
543
544                    if url_or_alias.starts_with("http") || url_or_alias.starts_with("ws") {
545                        decoded[0] = DynSolValue::String("<rpc url>".to_string());
546                    }
547                } else {
548                    return None;
549                }
550
551                Some(decoded.iter().map(format_token).collect())
552            }
553            _ => None,
554        }
555    }
556
557    /// Decodes a function's output into the given trace.
558    fn decode_function_output(&self, trace: &CallTrace, funcs: &[Function]) -> Option<String> {
559        if !trace.success {
560            return self.default_return_data(trace);
561        }
562
563        if trace.address == CHEATCODE_ADDRESS {
564            if let Some(decoded) = funcs.iter().find_map(|func| self.decode_cheatcode_outputs(func))
565            {
566                return Some(decoded);
567            }
568        }
569
570        if let Some(values) =
571            funcs.iter().find_map(|func| func.abi_decode_output(&trace.output, false).ok())
572        {
573            // Functions coming from an external database do not have any outputs specified,
574            // and will lead to returning an empty list of values.
575            if values.is_empty() {
576                return None;
577            }
578
579            return Some(
580                values.iter().map(|value| self.format_value(value)).format(", ").to_string(),
581            );
582        }
583
584        None
585    }
586
587    /// Custom decoding for cheatcode outputs.
588    fn decode_cheatcode_outputs(&self, func: &Function) -> Option<String> {
589        match func.name.as_str() {
590            s if s.starts_with("env") => Some("<env var value>"),
591            "createWallet" | "deriveKey" => Some("<pk>"),
592            "promptSecret" | "promptSecretUint" => Some("<secret>"),
593            "parseJson" if self.verbosity < 5 => Some("<encoded JSON value>"),
594            "readFile" if self.verbosity < 5 => Some("<file>"),
595            "rpcUrl" | "rpcUrls" | "rpcUrlStructs" => Some("<rpc url>"),
596            _ => None,
597        }
598        .map(Into::into)
599    }
600
601    /// The default decoded return data for a trace.
602    fn default_return_data(&self, trace: &CallTrace) -> Option<String> {
603        (!trace.success).then(|| self.revert_decoder.decode(&trace.output, Some(trace.status)))
604    }
605
606    /// Decodes an event.
607    pub async fn decode_event(&self, log: &LogData) -> DecodedCallLog {
608        let &[t0, ..] = log.topics() else { return DecodedCallLog { name: None, params: None } };
609
610        let mut events = Vec::new();
611        let events = match self.events.get(&(t0, log.topics().len() - 1)) {
612            Some(es) => es,
613            None => {
614                if let Some(identifier) = &self.signature_identifier {
615                    if let Some(event) = identifier.write().await.identify_event(&t0[..]).await {
616                        events.push(get_indexed_event(event, log));
617                    }
618                }
619                &events
620            }
621        };
622        for event in events {
623            if let Ok(decoded) = event.decode_log(log, false) {
624                let params = reconstruct_params(event, &decoded);
625                return DecodedCallLog {
626                    name: Some(event.name.clone()),
627                    params: Some(
628                        params
629                            .into_iter()
630                            .zip(event.inputs.iter())
631                            .map(|(param, input)| {
632                                // undo patched names
633                                let name = input.name.clone();
634                                (name, self.format_value(&param))
635                            })
636                            .collect(),
637                    ),
638                };
639            }
640        }
641
642        DecodedCallLog { name: None, params: None }
643    }
644
645    /// Prefetches function and event signatures into the identifier cache
646    pub async fn prefetch_signatures(&self, nodes: &[CallTraceNode]) {
647        let Some(identifier) = &self.signature_identifier else { return };
648
649        let events_it = nodes
650            .iter()
651            .flat_map(|node| node.logs.iter().filter_map(|log| log.raw_log.topics().first()))
652            .unique();
653        identifier.write().await.identify_events(events_it).await;
654
655        const DEFAULT_CREATE2_DEPLOYER_BYTES: [u8; 20] = DEFAULT_CREATE2_DEPLOYER.0 .0;
656        let funcs_it = nodes
657            .iter()
658            .filter_map(|n| match n.trace.address.0 .0 {
659                DEFAULT_CREATE2_DEPLOYER_BYTES => None,
660                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x01..=0x0a] => None,
661                _ => n.trace.data.get(..SELECTOR_LEN),
662            })
663            .filter(|v| !self.functions.contains_key(*v))
664            .unique();
665        identifier.write().await.identify_functions(funcs_it).await;
666    }
667
668    /// Pretty-prints a value.
669    fn format_value(&self, value: &DynSolValue) -> String {
670        if let DynSolValue::Address(addr) = value {
671            if let Some(label) = self.labels.get(addr) {
672                return format!("{label}: [{addr}]");
673            }
674        }
675        format_token(value)
676    }
677}
678
679/// Restore the order of the params of a decoded event,
680/// as Alloy returns the indexed and unindexed params separately.
681fn reconstruct_params(event: &Event, decoded: &DecodedEvent) -> Vec<DynSolValue> {
682    let mut indexed = 0;
683    let mut unindexed = 0;
684    let mut inputs = vec![];
685    for input in &event.inputs {
686        // Prevent panic of event `Transfer(from, to)` decoded with a signature
687        // `Transfer(address indexed from, address indexed to, uint256 indexed tokenId)` by making
688        // sure the event inputs is not higher than decoded indexed / un-indexed values.
689        if input.indexed && indexed < decoded.indexed.len() {
690            inputs.push(decoded.indexed[indexed].clone());
691            indexed += 1;
692        } else if unindexed < decoded.body.len() {
693            inputs.push(decoded.body[unindexed].clone());
694            unindexed += 1;
695        }
696    }
697
698    inputs
699}
700
701fn indexed_inputs(event: &Event) -> usize {
702    event.inputs.iter().filter(|param| param.indexed).count()
703}
704
705#[cfg(test)]
706mod tests {
707    use super::*;
708    use alloy_primitives::hex;
709
710    #[test]
711    fn test_should_redact() {
712        let decoder = CallTraceDecoder::new();
713
714        // [function_signature, data, expected]
715        let cheatcode_input_test_cases = vec![
716            // Should redact private key from traces in all cases:
717            ("addr(uint256)", vec![], Some(vec!["<pk>".to_string()])),
718            ("createWallet(string)", vec![], Some(vec!["<pk>".to_string()])),
719            ("createWallet(uint256)", vec![], Some(vec!["<pk>".to_string()])),
720            ("deriveKey(string,uint32)", vec![], Some(vec!["<pk>".to_string()])),
721            ("deriveKey(string,string,uint32)", vec![], Some(vec!["<pk>".to_string()])),
722            ("deriveKey(string,uint32,string)", vec![], Some(vec!["<pk>".to_string()])),
723            ("deriveKey(string,string,uint32,string)", vec![], Some(vec!["<pk>".to_string()])),
724            ("rememberKey(uint256)", vec![], Some(vec!["<pk>".to_string()])),
725            //
726            // Should redact private key from traces in specific cases with exceptions:
727            ("broadcast(uint256)", vec![], Some(vec!["<pk>".to_string()])),
728            ("broadcast()", vec![], None), // Ignore: `private key` is not passed.
729            ("startBroadcast(uint256)", vec![], Some(vec!["<pk>".to_string()])),
730            ("startBroadcast()", vec![], None), // Ignore: `private key` is not passed.
731            ("getNonce((address,uint256,uint256,uint256))", vec![], Some(vec!["<pk>".to_string()])),
732            ("getNonce(address)", vec![], None), // Ignore: `address` is public.
733            //
734            // Should redact private key and replace in trace in cases:
735            (
736                "sign(uint256,bytes32)",
737                hex!(
738                    "
739                    e341eaa4
740                    7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6
741                    0000000000000000000000000000000000000000000000000000000000000000
742                "
743                )
744                .to_vec(),
745                Some(vec![
746                    "\"<pk>\"".to_string(),
747                    "0x0000000000000000000000000000000000000000000000000000000000000000"
748                        .to_string(),
749                ]),
750            ),
751            (
752                "signP256(uint256,bytes32)",
753                hex!(
754                    "
755                    83211b40
756                    7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6
757                    0000000000000000000000000000000000000000000000000000000000000000
758                "
759                )
760                .to_vec(),
761                Some(vec![
762                    "\"<pk>\"".to_string(),
763                    "0x0000000000000000000000000000000000000000000000000000000000000000"
764                        .to_string(),
765                ]),
766            ),
767            (
768                // cast calldata "createFork(string)" "https://eth-mainnet.g.alchemy.com/v2/api_key"
769                "createFork(string)",
770                hex!(
771                    "
772                    31ba3498
773                    0000000000000000000000000000000000000000000000000000000000000020
774                    000000000000000000000000000000000000000000000000000000000000002c
775                    68747470733a2f2f6574682d6d61696e6e65742e672e616c6368656d792e636f
776                    6d2f76322f6170695f6b65790000000000000000000000000000000000000000
777                    "
778                )
779                .to_vec(),
780                Some(vec!["\"<rpc url>\"".to_string()]),
781            ),
782            (
783                // cast calldata "createFork(string)" "wss://eth-mainnet.g.alchemy.com/v2/api_key"
784                "createFork(string)",
785                hex!(
786                    "
787                    31ba3498
788                    0000000000000000000000000000000000000000000000000000000000000020
789                    000000000000000000000000000000000000000000000000000000000000002a
790                    7773733a2f2f6574682d6d61696e6e65742e672e616c6368656d792e636f6d2f
791                    76322f6170695f6b657900000000000000000000000000000000000000000000
792                    "
793                )
794                .to_vec(),
795                Some(vec!["\"<rpc url>\"".to_string()]),
796            ),
797            (
798                // cast calldata "createFork(string)" "mainnet"
799                "createFork(string)",
800                hex!(
801                    "
802                    31ba3498
803                    0000000000000000000000000000000000000000000000000000000000000020
804                    0000000000000000000000000000000000000000000000000000000000000007
805                    6d61696e6e657400000000000000000000000000000000000000000000000000
806                    "
807                )
808                .to_vec(),
809                Some(vec!["\"mainnet\"".to_string()]),
810            ),
811            (
812                // cast calldata "createFork(string,uint256)" "https://eth-mainnet.g.alchemy.com/v2/api_key" 1
813                "createFork(string,uint256)",
814                hex!(
815                    "
816                    6ba3ba2b
817                    0000000000000000000000000000000000000000000000000000000000000040
818                    0000000000000000000000000000000000000000000000000000000000000001
819                    000000000000000000000000000000000000000000000000000000000000002c
820                    68747470733a2f2f6574682d6d61696e6e65742e672e616c6368656d792e636f
821                    6d2f76322f6170695f6b65790000000000000000000000000000000000000000
822                "
823                )
824                .to_vec(),
825                Some(vec!["\"<rpc url>\"".to_string(), "1".to_string()]),
826            ),
827            (
828                // cast calldata "createFork(string,uint256)" "mainnet" 1
829                "createFork(string,uint256)",
830                hex!(
831                    "
832                    6ba3ba2b
833                    0000000000000000000000000000000000000000000000000000000000000040
834                    0000000000000000000000000000000000000000000000000000000000000001
835                    0000000000000000000000000000000000000000000000000000000000000007
836                    6d61696e6e657400000000000000000000000000000000000000000000000000
837                "
838                )
839                .to_vec(),
840                Some(vec!["\"mainnet\"".to_string(), "1".to_string()]),
841            ),
842            (
843                // cast calldata "createFork(string,bytes32)" "https://eth-mainnet.g.alchemy.com/v2/api_key" 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
844                "createFork(string,bytes32)",
845                hex!(
846                    "
847                    7ca29682
848                    0000000000000000000000000000000000000000000000000000000000000040
849                    ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
850                    000000000000000000000000000000000000000000000000000000000000002c
851                    68747470733a2f2f6574682d6d61696e6e65742e672e616c6368656d792e636f
852                    6d2f76322f6170695f6b65790000000000000000000000000000000000000000
853                "
854                )
855                .to_vec(),
856                Some(vec![
857                    "\"<rpc url>\"".to_string(),
858                    "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
859                        .to_string(),
860                ]),
861            ),
862            (
863                // cast calldata "createFork(string,bytes32)" "mainnet"
864                // 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
865                "createFork(string,bytes32)",
866                hex!(
867                    "
868                    7ca29682
869                    0000000000000000000000000000000000000000000000000000000000000040
870                    ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
871                    0000000000000000000000000000000000000000000000000000000000000007
872                    6d61696e6e657400000000000000000000000000000000000000000000000000
873                "
874                )
875                .to_vec(),
876                Some(vec![
877                    "\"mainnet\"".to_string(),
878                    "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
879                        .to_string(),
880                ]),
881            ),
882            (
883                // cast calldata "createSelectFork(string)" "https://eth-mainnet.g.alchemy.com/v2/api_key"
884                "createSelectFork(string)",
885                hex!(
886                    "
887                    98680034
888                    0000000000000000000000000000000000000000000000000000000000000020
889                    000000000000000000000000000000000000000000000000000000000000002c
890                    68747470733a2f2f6574682d6d61696e6e65742e672e616c6368656d792e636f
891                    6d2f76322f6170695f6b65790000000000000000000000000000000000000000
892                    "
893                )
894                .to_vec(),
895                Some(vec!["\"<rpc url>\"".to_string()]),
896            ),
897            (
898                // cast calldata "createSelectFork(string)" "mainnet"
899                "createSelectFork(string)",
900                hex!(
901                    "
902                    98680034
903                    0000000000000000000000000000000000000000000000000000000000000020
904                    0000000000000000000000000000000000000000000000000000000000000007
905                    6d61696e6e657400000000000000000000000000000000000000000000000000
906                    "
907                )
908                .to_vec(),
909                Some(vec!["\"mainnet\"".to_string()]),
910            ),
911            (
912                // cast calldata "createSelectFork(string,uint256)" "https://eth-mainnet.g.alchemy.com/v2/api_key" 1
913                "createSelectFork(string,uint256)",
914                hex!(
915                    "
916                    71ee464d
917                    0000000000000000000000000000000000000000000000000000000000000040
918                    0000000000000000000000000000000000000000000000000000000000000001
919                    000000000000000000000000000000000000000000000000000000000000002c
920                    68747470733a2f2f6574682d6d61696e6e65742e672e616c6368656d792e636f
921                    6d2f76322f6170695f6b65790000000000000000000000000000000000000000
922                "
923                )
924                .to_vec(),
925                Some(vec!["\"<rpc url>\"".to_string(), "1".to_string()]),
926            ),
927            (
928                // cast calldata "createSelectFork(string,uint256)" "mainnet" 1
929                "createSelectFork(string,uint256)",
930                hex!(
931                    "
932                    71ee464d
933                    0000000000000000000000000000000000000000000000000000000000000040
934                    0000000000000000000000000000000000000000000000000000000000000001
935                    0000000000000000000000000000000000000000000000000000000000000007
936                    6d61696e6e657400000000000000000000000000000000000000000000000000
937                "
938                )
939                .to_vec(),
940                Some(vec!["\"mainnet\"".to_string(), "1".to_string()]),
941            ),
942            (
943                // cast calldata "createSelectFork(string,bytes32)" "https://eth-mainnet.g.alchemy.com/v2/api_key" 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
944                "createSelectFork(string,bytes32)",
945                hex!(
946                    "
947                    84d52b7a
948                    0000000000000000000000000000000000000000000000000000000000000040
949                    ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
950                    000000000000000000000000000000000000000000000000000000000000002c
951                    68747470733a2f2f6574682d6d61696e6e65742e672e616c6368656d792e636f
952                    6d2f76322f6170695f6b65790000000000000000000000000000000000000000
953                "
954                )
955                .to_vec(),
956                Some(vec![
957                    "\"<rpc url>\"".to_string(),
958                    "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
959                        .to_string(),
960                ]),
961            ),
962            (
963                // cast calldata "createSelectFork(string,bytes32)" "mainnet"
964                // 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
965                "createSelectFork(string,bytes32)",
966                hex!(
967                    "
968                    84d52b7a
969                    0000000000000000000000000000000000000000000000000000000000000040
970                    ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
971                    0000000000000000000000000000000000000000000000000000000000000007
972                    6d61696e6e657400000000000000000000000000000000000000000000000000
973                "
974                )
975                .to_vec(),
976                Some(vec![
977                    "\"mainnet\"".to_string(),
978                    "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
979                        .to_string(),
980                ]),
981            ),
982            (
983                // cast calldata "rpc(string,string,string)" "https://eth-mainnet.g.alchemy.com/v2/api_key" "eth_getBalance" "[\"0x551e7784778ef8e048e495df49f2614f84a4f1dc\",\"0x0\"]"
984                "rpc(string,string,string)",
985                hex!(
986                    "
987                    0199a220
988                    0000000000000000000000000000000000000000000000000000000000000060
989                    00000000000000000000000000000000000000000000000000000000000000c0
990                    0000000000000000000000000000000000000000000000000000000000000100
991                    000000000000000000000000000000000000000000000000000000000000002c
992                    68747470733a2f2f6574682d6d61696e6e65742e672e616c6368656d792e636f
993                    6d2f76322f6170695f6b65790000000000000000000000000000000000000000
994                    000000000000000000000000000000000000000000000000000000000000000e
995                    6574685f67657442616c616e6365000000000000000000000000000000000000
996                    0000000000000000000000000000000000000000000000000000000000000034
997                    5b22307835353165373738343737386566386530343865343935646634396632
998                    363134663834613466316463222c22307830225d000000000000000000000000
999                "
1000                )
1001                .to_vec(),
1002                Some(vec![
1003                    "\"<rpc url>\"".to_string(),
1004                    "\"eth_getBalance\"".to_string(),
1005                    "\"[\\\"0x551e7784778ef8e048e495df49f2614f84a4f1dc\\\",\\\"0x0\\\"]\""
1006                        .to_string(),
1007                ]),
1008            ),
1009            (
1010                // cast calldata "rpc(string,string,string)" "mainnet" "eth_getBalance"
1011                // "[\"0x551e7784778ef8e048e495df49f2614f84a4f1dc\",\"0x0\"]"
1012                "rpc(string,string,string)",
1013                hex!(
1014                    "
1015                    0199a220
1016                    0000000000000000000000000000000000000000000000000000000000000060
1017                    00000000000000000000000000000000000000000000000000000000000000a0
1018                    00000000000000000000000000000000000000000000000000000000000000e0
1019                    0000000000000000000000000000000000000000000000000000000000000007
1020                    6d61696e6e657400000000000000000000000000000000000000000000000000
1021                    000000000000000000000000000000000000000000000000000000000000000e
1022                    6574685f67657442616c616e6365000000000000000000000000000000000000
1023                    0000000000000000000000000000000000000000000000000000000000000034
1024                    5b22307835353165373738343737386566386530343865343935646634396632
1025                    363134663834613466316463222c22307830225d000000000000000000000000
1026                "
1027                )
1028                .to_vec(),
1029                Some(vec![
1030                    "\"mainnet\"".to_string(),
1031                    "\"eth_getBalance\"".to_string(),
1032                    "\"[\\\"0x551e7784778ef8e048e495df49f2614f84a4f1dc\\\",\\\"0x0\\\"]\""
1033                        .to_string(),
1034                ]),
1035            ),
1036        ];
1037
1038        // [function_signature, expected]
1039        let cheatcode_output_test_cases = vec![
1040            // Should redact private key on output in all cases:
1041            ("createWallet(string)", Some("<pk>".to_string())),
1042            ("deriveKey(string,uint32)", Some("<pk>".to_string())),
1043            // Should redact RPC URL if defined, except if referenced by an alias:
1044            ("rpcUrl(string)", Some("<rpc url>".to_string())),
1045            ("rpcUrls()", Some("<rpc url>".to_string())),
1046            ("rpcUrlStructs()", Some("<rpc url>".to_string())),
1047        ];
1048
1049        for (function_signature, data, expected) in cheatcode_input_test_cases {
1050            let function = Function::parse(function_signature).unwrap();
1051            let result = decoder.decode_cheatcode_inputs(&function, &data);
1052            assert_eq!(result, expected, "Input case failed for: {function_signature}");
1053        }
1054
1055        for (function_signature, expected) in cheatcode_output_test_cases {
1056            let function = Function::parse(function_signature).unwrap();
1057            let result = Some(decoder.decode_cheatcode_outputs(&function).unwrap_or_default());
1058            assert_eq!(result, expected, "Output case failed for: {function_signature}");
1059        }
1060    }
1061}