Skip to main content

foundry_evm/inspectors/
tempo_labels.rs

1use alloy_primitives::{U256, keccak256, map::AddressMap};
2use foundry_evm_core::backend::DatabaseError;
3use revm::{
4    Database, Inspector,
5    context::ContextTr,
6    inspector::JournalExt,
7    interpreter::{CallInputs, CallOutcome, interpreter::EthInterpreter},
8};
9use tempo_primitives::TempoAddressExt;
10
11// Limit labels to 256 bytes so long names require at most eight additional storage reads.
12const MAX_NAME_BYTES: usize = 256;
13
14/// Inspector that labels TIP20 token precompile addresses with their on-chain names.
15///
16/// During execution, when a call targets a TIP20 address, this inspector reads the token's
17/// name from storage and records the `address -> name` mapping. These labels are later merged
18/// into trace output for better readability.
19#[derive(Default, Clone, Debug)]
20pub struct TempoLabels {
21    pub(crate) labels: AddressMap<String>,
22}
23
24impl<CTX, D> Inspector<CTX, EthInterpreter> for TempoLabels
25where
26    D: Database<Error = DatabaseError>,
27    CTX: ContextTr<Db = D>,
28    CTX::Journal: JournalExt,
29{
30    fn call(&mut self, ctx: &mut CTX, inputs: &mut CallInputs) -> Option<CallOutcome> {
31        if inputs.target_address.is_tip20() && !self.labels.contains_key(&inputs.target_address) {
32            let name = 'decode: {
33                let db = ctx.db_mut();
34                let address = inputs.target_address;
35                let slot = tempo_precompiles::tip20::slots::NAME;
36                let Ok(value) = db.storage(address, slot) else { break 'decode None };
37                let bytes = value.to_be_bytes::<32>();
38                if bytes[31] & 1 == 0 {
39                    let len = usize::from(bytes[31] / 2);
40                    break 'decode (1..=31)
41                        .contains(&len)
42                        .then(|| String::from_utf8_lossy(&bytes[..len]).into_owned());
43                }
44
45                // Long strings store length * 2 + 1 in the base slot and data at keccak256(slot).
46                let len = value >> 1usize;
47                if len < U256::from(32) || len > U256::from(MAX_NAME_BYTES) {
48                    break 'decode None;
49                }
50                let len = len.to::<usize>();
51                let start = U256::from_be_bytes(keccak256(slot.to_be_bytes::<32>()).0);
52                let mut data = Vec::with_capacity(len);
53                for i in 0..len.div_ceil(32) {
54                    let Ok(chunk) = db.storage(address, start + U256::from(i)) else {
55                        break 'decode None;
56                    };
57                    let chunk = chunk.to_be_bytes::<32>();
58                    data.extend_from_slice(&chunk[..(len - data.len()).min(32)]);
59                }
60                Some(String::from_utf8_lossy(&data).into_owned())
61            }
62            .unwrap_or_else(|| "TIP20".to_string());
63            self.labels.insert(inputs.target_address, name);
64        }
65
66        None
67    }
68}