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#[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 #[inline]
59 pub fn new() -> Self {
60 Self { decoder: CallTraceDecoder::new().clone() }
61 }
62
63 #[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 #[inline]
72 pub fn with_abi(mut self, abi: &JsonAbi) -> Self {
73 self.decoder.collect_abi(abi, None);
74 self
75 }
76
77 #[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 #[inline]
89 pub fn with_local_identifier_abis(self, identifier: &LocalTraceIdentifier<'_>) -> Self {
90 self.with_known_contracts(identifier.contracts())
91 }
92
93 #[inline]
95 pub const fn with_verbosity(mut self, level: u8) -> Self {
96 self.decoder.verbosity = level;
97 self
98 }
99
100 #[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 #[inline]
112 pub fn with_signature_identifier(mut self, identifier: SignaturesIdentifier) -> Self {
113 self.decoder.signature_identifier = Some(identifier);
114 self
115 }
116
117 #[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 #[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 #[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 #[inline]
152 pub const fn with_compact_labels(mut self, compact: bool) -> Self {
153 self.decoder.compact_labels = compact;
154 self
155 }
156
157 #[inline]
159 pub fn with_debug_identifier(mut self, identifier: DebugTraceIdentifier) -> Self {
160 self.decoder.debug_identifier = Some(identifier);
161 self
162 }
163
164 #[inline]
166 pub fn build(mut self) -> CallTraceDecoder {
167 self.decoder.base_labels = self.decoder.labels.clone();
168 self.decoder
169 }
170}
171
172#[derive(Clone, Debug, Default)]
180pub struct CallTraceDecoder {
181 pub contracts: HashMap<Address, String>,
185 pub labels: HashMap<Address, String>,
187 base_labels: HashMap<Address, String>,
189 pub receive_contracts: HashSet<Address>,
191 pub fallback_contracts: HashMap<Address, HashSet<Selector>>,
194 pub non_fallback_contracts: HashMap<Address, HashSet<Selector>>,
197
198 pub functions: HashMap<Selector, Vec<Function>>,
200 pub functions_by_address: HashMap<Address, HashMap<Selector, Vec<Function>>>,
202 pub constructors_by_address: HashMap<Address, Constructor>,
204 pub constructor_args_offsets: HashMap<Address, usize>,
206 pub events: BTreeMap<(B256, usize), Vec<Event>>,
210 pub revert_decoder: RevertDecoder,
212
213 pub signature_identifier: Option<SignaturesIdentifier>,
215 pub verbosity: u8,
217
218 pub debug_identifier: Option<DebugTraceIdentifier>,
220
221 pub disable_labels: bool,
223
224 pub chain_id: Option<u64>,
226
227 pub opcodes: Vec<OpCode>,
229
230 pub tempo_hardfork: Option<TempoHardfork>,
232
233 pub compact_labels: bool,
235}
236
237impl CallTraceDecoder {
238 pub fn new() -> &'static Self {
243 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 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 (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 .chain(IFeeManager::abi::functions().into_values())
317 .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 .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 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 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 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 pub fn identify(&mut self, arena: &CallTraceArena, identifier: &mut impl TraceIdentifier) {
408 self.collect_identified_addresses(self.identify_addresses(arena, identifier));
409 }
410
411 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 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 pub fn push_event(&mut self, event: Event) {
438 self.events.entry((event.selector(), indexed_inputs(&event))).or_default().push(event);
439 }
440
441 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 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 fn select_contract_function<'a>(
508 &self,
509 functions: &'a [Function],
510 trace: &CallTrace,
511 ) -> &'a [Function] {
512 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 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 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 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 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 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 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 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 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 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 Some(vec!["<pk>".to_string()])
873 }
874 "broadcast" | "startBroadcast" => {
875 (!func.inputs.is_empty() && func.inputs[0].ty == "uint256").then(|| vec!["<pk>".to_string()])
878 }
879 "getNonce" => {
880 (!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 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 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" |
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 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 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 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 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 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 async fn default_return_data(&self, trace: &CallTrace) -> Option<String> {
1051 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 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 pub async fn decode_event(&self, log: &LogData) -> DecodedCallLog {
1100 self.decode_event_inner(None, log).await
1101 }
1102
1103 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 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 ¶m,
1147 ),
1148 )
1149 })
1150 .collect(),
1151 ),
1152 };
1153 }
1154 }
1155
1156 DecodedCallLog { name: None, params: None }
1157 }
1158
1159 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 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 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 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 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
1297fn 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
1308fn is_abi_data(data: &[u8]) -> bool {
1312 let rem = data.len() % 32;
1313 if rem == 0 || data.is_empty() {
1314 return true;
1315 }
1316 data[data.len() - rem..].iter().all(|byte| *byte == 0)
1318}
1319
1320fn 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 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 let func1 = Function::parse("transferFrom(address,address,uint256)").unwrap();
1366 let func2 = Function::parse("gasprice_bit_ether(int128)").unwrap();
1367
1368 assert_eq!(func1.selector(), func2.selector());
1370
1371 let functions = vec![func1, func2];
1372
1373 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 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 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 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 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 let cheatcode_input_test_cases = vec![
1543 (
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 ("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 ("broadcast(uint256)", vec![], Some(vec!["<pk>".to_string()])),
1607 ("broadcast()", vec![], None), ("startBroadcast(uint256)", vec![], Some(vec!["<pk>".to_string()])),
1609 ("startBroadcast()", vec![], None), ("getNonce((address,uint256,uint256,uint256))", vec![], Some(vec!["<pk>".to_string()])),
1611 ("getNonce(address)", vec![], None), (
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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 let cheatcode_output_test_cases = vec![
1919 ("createWallet(string)", Some("<pk>".to_string())),
1921 ("deriveKey(string,uint32)", Some("<pk>".to_string())),
1922 ("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 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 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 let revert = decoder
2161 .revert_decoder
2162 .decode(IStorageCredits::InvalidMode {}.abi_encode().as_slice(), None);
2163 assert!(revert.contains("InvalidMode"), "{revert}");
2164
2165 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 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 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 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 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 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 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 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 assert_eq!(identifier.queried, vec![regular_addr, tempo_precompile]);
2753 }
2754}