Skip to main content

evm_fork_cache/
inspector.rs

1//! ERC20 `Transfer`-event capture for reconstructing balance deltas.
2//!
3//! This module provides a [`revm::Inspector`] that watches logs emitted during
4//! a simulation, matches the canonical ERC20 `Transfer(address,address,uint256)`
5//! signature, and records each transfer. The captured transfers let callers
6//! compute net balance changes per token and account without re-reading storage
7//! after the call.
8//!
9//! # Parsing assumptions
10//!
11//! Transfers are decoded assuming the standard ERC20 event layout:
12//! `from` and `to` come from the indexed topics (via [`Address::from_word`], i.e.
13//! the low 20 bytes of each 32-byte topic) and `value` is read from the first 32
14//! data bytes. A non-standard or packed `Transfer` event (e.g. one that does not
15//! index `from`/`to`, or packs additional fields into the data) may parse
16//! incorrectly or be silently skipped.
17//!
18//! Balance deltas are computed symmetrically: a self-transfer where `from == to`
19//! is both subtracted and added, netting to zero for that owner.
20
21use std::collections::HashMap;
22
23use alloy_primitives::{Address, B256, I256, Log, U256};
24use revm::Inspector;
25use revm::interpreter::InterpreterTypes;
26
27/// ERC20 `Transfer` event signature: `keccak256("Transfer(address,address,uint256)")`.
28///
29/// A log's first topic must equal this value to be treated as a transfer.
30const TRANSFER_EVENT_SIGNATURE: B256 = B256::new([
31    0xdd, 0xf2, 0x52, 0xad, 0x1b, 0xe2, 0xc8, 0x9b, 0x69, 0xc2, 0xb0, 0x68, 0xfc, 0x37, 0x8d, 0xaa,
32    0x95, 0x2b, 0xa7, 0xf1, 0x63, 0xc4, 0xa1, 0x16, 0x28, 0xf5, 0x5a, 0x4d, 0xf5, 0x23, 0xb3, 0xef,
33]);
34
35/// A single ERC20 token transfer decoded from a `Transfer` log.
36///
37/// Fields are populated from the standard ERC20 event layout (see the
38/// [module docs](crate::inspector) for caveats on non-standard events).
39#[derive(Clone, Debug, PartialEq, Eq)]
40pub struct TokenTransfer {
41    /// Address of the token contract that emitted the event (the log's address).
42    pub token: Address,
43    /// Sender, decoded from the first indexed topic.
44    pub from: Address,
45    /// Recipient, decoded from the second indexed topic.
46    pub to: Address,
47    /// Amount transferred, decoded from the first 32 data bytes.
48    pub value: U256,
49}
50
51/// Inspector that captures ERC20 `Transfer` events during EVM execution.
52///
53/// Attach to a simulation and the [`Inspector::log`] hook records every emitted
54/// log; logs matching the ERC20 `Transfer` layout are additionally decoded into
55/// [`TokenTransfer`]s. Reconstruct net balance changes afterward with
56/// [`balance_deltas`](Self::balance_deltas) or
57/// [`balance_deltas_for_tokens`](Self::balance_deltas_for_tokens), and reuse the
58/// inspector across calls via [`clear`](Self::clear).
59#[derive(Clone, Debug, Default)]
60pub struct TransferInspector {
61    /// Token transfers decoded from captured logs.
62    pub transfers: Vec<TokenTransfer>,
63    /// Every log emitted during execution, retained for debugging/analysis.
64    pub logs: Vec<Log>,
65}
66
67impl TransferInspector {
68    /// Create an empty inspector with no captured transfers or logs.
69    ///
70    /// ```
71    /// use evm_fork_cache::inspector::TransferInspector;
72    ///
73    /// let inspector = TransferInspector::new();
74    /// assert!(inspector.transfers.is_empty());
75    /// assert!(inspector.logs.is_empty());
76    /// ```
77    pub fn new() -> Self {
78        Self {
79            transfers: Vec::new(),
80            logs: Vec::new(),
81        }
82    }
83
84    /// Compute balance deltas for a specific owner from captured transfers
85    ///
86    /// Returns a map of token address -> signed balance change
87    /// Positive values indicate tokens received, negative indicates tokens sent
88    pub fn balance_deltas(&self, owner: Address) -> HashMap<Address, I256> {
89        let mut deltas: HashMap<Address, I256> = HashMap::new();
90
91        for transfer in &self.transfers {
92            if transfer.from == owner {
93                // Outgoing transfer - subtract from balance
94                let entry = deltas.entry(transfer.token).or_insert(I256::ZERO);
95                *entry -= I256::from_raw(transfer.value);
96            }
97            if transfer.to == owner {
98                // Incoming transfer - add to balance
99                let entry = deltas.entry(transfer.token).or_insert(I256::ZERO);
100                *entry += I256::from_raw(transfer.value);
101            }
102        }
103
104        deltas
105    }
106
107    /// Like [`balance_deltas`](Self::balance_deltas), but restricted to the
108    /// given set of token addresses.
109    ///
110    /// Tokens in `tokens` with no transfers touching `owner` are simply absent
111    /// from the result; tokens not in `tokens` are excluded even if `owner`
112    /// transacted in them.
113    ///
114    /// ```
115    /// # use evm_fork_cache::inspector::{TransferInspector, TokenTransfer};
116    /// # use alloy_primitives::{Address, I256, U256};
117    /// let mut inspector = TransferInspector::new();
118    /// let token_a = Address::repeat_byte(0xAA);
119    /// let token_b = Address::repeat_byte(0xBB);
120    /// let owner = Address::repeat_byte(0x11);
121    /// let other = Address::repeat_byte(0x22);
122    /// inspector.transfers.push(TokenTransfer { token: token_a, from: owner, to: other, value: U256::from(100u64) });
123    /// inspector.transfers.push(TokenTransfer { token: token_b, from: other, to: owner, value: U256::from(50u64) });
124    ///
125    /// let deltas = inspector.balance_deltas_for_tokens(owner, [token_a]);
126    /// assert_eq!(deltas.len(), 1);
127    /// assert_eq!(deltas.get(&token_a), Some(&(-I256::from_raw(U256::from(100u64)))));
128    /// assert!(!deltas.contains_key(&token_b));
129    /// ```
130    pub fn balance_deltas_for_tokens(
131        &self,
132        owner: Address,
133        tokens: impl IntoIterator<Item = Address>,
134    ) -> HashMap<Address, I256> {
135        let token_set: std::collections::HashSet<Address> = tokens.into_iter().collect();
136        let all_deltas = self.balance_deltas(owner);
137
138        all_deltas
139            .into_iter()
140            .filter(|(token, _)| token_set.contains(token))
141            .collect()
142    }
143
144    /// Drop all captured transfers and logs so the inspector can be reused
145    /// across simulations.
146    pub fn clear(&mut self) {
147        self.transfers.clear();
148        self.logs.clear();
149    }
150
151    /// Parse a Transfer event from log topics and data
152    pub fn parse_transfer(log: &Log) -> Option<TokenTransfer> {
153        // ERC20 Transfer event has:
154        // - topic[0]: event signature
155        // - topic[1]: from address (indexed, 32 bytes padded)
156        // - topic[2]: to address (indexed, 32 bytes padded)
157        // - data: value (uint256)
158
159        let topics = log.topics();
160        if topics.len() < 3 {
161            return None;
162        }
163
164        // Check event signature
165        if topics[0] != TRANSFER_EVENT_SIGNATURE {
166            return None;
167        }
168
169        // Extract addresses from topics (last 20 bytes of 32-byte topic)
170        let from = Address::from_word(topics[1]);
171        let to = Address::from_word(topics[2]);
172
173        // Extract value from data (should be exactly 32 bytes for standard ERC20)
174        let data = log.data.data.as_ref();
175        if data.len() < 32 {
176            return None;
177        }
178        let value = U256::from_be_slice(&data[..32]);
179
180        Some(TokenTransfer {
181            token: log.address,
182            from,
183            to,
184            value,
185        })
186    }
187}
188
189/// Captures every emitted log via the [`Inspector::log`] hook.
190///
191/// Each log is pushed to [`logs`](TransferInspector::logs); logs whose first
192/// topic matches the ERC20 `Transfer` signature and that carry the standard ERC20
193/// layout are additionally decoded into [`transfers`](TransferInspector::transfers).
194/// Logs that do not match (wrong signature, fewer than three topics, or fewer
195/// than 32 data bytes) are retained in `logs` but produce no transfer.
196impl<CTX, INTR> Inspector<CTX, INTR> for TransferInspector
197where
198    INTR: InterpreterTypes,
199{
200    /// Records `log` and, if it parses as an ERC20 `Transfer`, the decoded transfer.
201    fn log(&mut self, _context: &mut CTX, log: Log) {
202        // Try to parse as ERC20 Transfer event
203        if let Some(transfer) = Self::parse_transfer(&log) {
204            self.transfers.push(transfer);
205        }
206
207        // Store all logs for potential debugging/analysis
208        self.logs.push(log);
209    }
210}
211
212/// Parse ERC20 Transfer events from transaction receipt logs and compute
213/// balance deltas for the given owner address.
214///
215/// This mirrors `TransferInspector::balance_deltas()` but operates on receipt
216/// logs from an on-chain transaction rather than from an EVM inspector.
217pub fn parse_receipt_deltas(receipt_logs: &[Log], owner: Address) -> HashMap<Address, I256> {
218    let mut deltas: HashMap<Address, I256> = HashMap::new();
219
220    for log in receipt_logs {
221        if let Some(transfer) = TransferInspector::parse_transfer(log) {
222            if transfer.from == owner {
223                let entry = deltas.entry(transfer.token).or_insert(I256::ZERO);
224                *entry -= I256::from_raw(transfer.value);
225            }
226            if transfer.to == owner {
227                let entry = deltas.entry(transfer.token).or_insert(I256::ZERO);
228                *entry += I256::from_raw(transfer.value);
229            }
230        }
231    }
232
233    deltas
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239    use alloy_primitives::{address, bytes};
240
241    fn make_transfer_log(token: Address, from: Address, to: Address, value: U256) -> Log {
242        Log::new(
243            token,
244            vec![TRANSFER_EVENT_SIGNATURE, from.into_word(), to.into_word()],
245            value.to_be_bytes_vec().into(),
246        )
247        .unwrap()
248    }
249
250    #[test]
251    fn test_parse_single_transfer() {
252        let token = address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); // USDC
253        let from = address!("1111111111111111111111111111111111111111");
254        let to = address!("2222222222222222222222222222222222222222");
255        let value = U256::from(1000000u64); // 1 USDC
256
257        let log = make_transfer_log(token, from, to, value);
258        let transfer = TransferInspector::parse_transfer(&log).expect("should parse");
259
260        assert_eq!(transfer.token, token);
261        assert_eq!(transfer.from, from);
262        assert_eq!(transfer.to, to);
263        assert_eq!(transfer.value, value);
264    }
265
266    #[test]
267    fn test_balance_deltas_outgoing() {
268        let mut inspector = TransferInspector::new();
269        let token = address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
270        let owner = address!("1111111111111111111111111111111111111111");
271        let recipient = address!("2222222222222222222222222222222222222222");
272        let value = U256::from(1000000u64);
273
274        inspector.transfers.push(TokenTransfer {
275            token,
276            from: owner,
277            to: recipient,
278            value,
279        });
280
281        let deltas = inspector.balance_deltas(owner);
282        assert_eq!(deltas.get(&token), Some(&(-I256::from_raw(value))));
283    }
284
285    #[test]
286    fn test_balance_deltas_incoming() {
287        let mut inspector = TransferInspector::new();
288        let token = address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
289        let sender = address!("1111111111111111111111111111111111111111");
290        let owner = address!("2222222222222222222222222222222222222222");
291        let value = U256::from(1000000u64);
292
293        inspector.transfers.push(TokenTransfer {
294            token,
295            from: sender,
296            to: owner,
297            value,
298        });
299
300        let deltas = inspector.balance_deltas(owner);
301        assert_eq!(deltas.get(&token), Some(&I256::from_raw(value)));
302    }
303
304    #[test]
305    fn test_balance_deltas_multiple_transfers() {
306        let mut inspector = TransferInspector::new();
307        let token_a = address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
308        let token_b = address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
309        let owner = address!("1111111111111111111111111111111111111111");
310        let other = address!("2222222222222222222222222222222222222222");
311
312        // Owner sends 100 token_a
313        inspector.transfers.push(TokenTransfer {
314            token: token_a,
315            from: owner,
316            to: other,
317            value: U256::from(100u64),
318        });
319
320        // Owner receives 50 token_b
321        inspector.transfers.push(TokenTransfer {
322            token: token_b,
323            from: other,
324            to: owner,
325            value: U256::from(50u64),
326        });
327
328        // Owner receives 25 more token_a
329        inspector.transfers.push(TokenTransfer {
330            token: token_a,
331            from: other,
332            to: owner,
333            value: U256::from(25u64),
334        });
335
336        let deltas = inspector.balance_deltas(owner);
337
338        // token_a: -100 + 25 = -75
339        assert_eq!(deltas.get(&token_a), Some(&I256::try_from(-75i64).unwrap()));
340        // token_b: +50
341        assert_eq!(deltas.get(&token_b), Some(&I256::try_from(50i64).unwrap()));
342    }
343
344    #[test]
345    fn test_balance_deltas_for_tokens_filter() {
346        let mut inspector = TransferInspector::new();
347        let token_a = address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
348        let token_b = address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
349        let token_c = address!("6B175474E89094C44Da98b954EeecdE504D571D1");
350        let owner = address!("1111111111111111111111111111111111111111");
351        let other = address!("2222222222222222222222222222222222222222");
352
353        inspector.transfers.push(TokenTransfer {
354            token: token_a,
355            from: owner,
356            to: other,
357            value: U256::from(100u64),
358        });
359
360        inspector.transfers.push(TokenTransfer {
361            token: token_b,
362            from: other,
363            to: owner,
364            value: U256::from(50u64),
365        });
366
367        inspector.transfers.push(TokenTransfer {
368            token: token_c,
369            from: other,
370            to: owner,
371            value: U256::from(200u64),
372        });
373
374        // Only request deltas for token_a and token_b
375        let deltas = inspector.balance_deltas_for_tokens(owner, vec![token_a, token_b]);
376
377        assert_eq!(deltas.len(), 2);
378        assert!(deltas.contains_key(&token_a));
379        assert!(deltas.contains_key(&token_b));
380        assert!(!deltas.contains_key(&token_c));
381    }
382
383    #[test]
384    fn test_non_transfer_log_ignored() {
385        // Create a log with wrong signature
386        let log = Log::new(
387            address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
388            vec![
389                B256::ZERO, // Wrong signature
390                address!("1111111111111111111111111111111111111111").into_word(),
391                address!("2222222222222222222222222222222222222222").into_word(),
392            ],
393            U256::from(100u64).to_be_bytes_vec().into(),
394        )
395        .unwrap();
396
397        assert!(TransferInspector::parse_transfer(&log).is_none());
398    }
399
400    #[test]
401    fn test_inspector_clear() {
402        let mut inspector = TransferInspector::new();
403        inspector.transfers.push(TokenTransfer {
404            token: Address::ZERO,
405            from: Address::ZERO,
406            to: Address::ZERO,
407            value: U256::ZERO,
408        });
409        inspector
410            .logs
411            .push(Log::new_unchecked(Address::ZERO, vec![], bytes!("")));
412
413        assert!(!inspector.transfers.is_empty());
414        assert!(!inspector.logs.is_empty());
415
416        inspector.clear();
417
418        assert!(inspector.transfers.is_empty());
419        assert!(inspector.logs.is_empty());
420    }
421
422    #[test]
423    fn test_parse_receipt_deltas() {
424        let token_a = address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
425        let token_b = address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
426        let owner = address!("1111111111111111111111111111111111111111");
427        let other = address!("2222222222222222222222222222222222222222");
428
429        let logs = vec![
430            // Owner sends 100 token_a
431            make_transfer_log(token_a, owner, other, U256::from(100u64)),
432            // Owner receives 50 token_b
433            make_transfer_log(token_b, other, owner, U256::from(50u64)),
434            // Non-transfer log (wrong signature) - should be ignored
435            Log::new(
436                token_a,
437                vec![B256::ZERO, owner.into_word(), other.into_word()],
438                U256::from(999u64).to_be_bytes_vec().into(),
439            )
440            .unwrap(),
441            // Owner receives 25 token_a
442            make_transfer_log(token_a, other, owner, U256::from(25u64)),
443        ];
444
445        let deltas = super::parse_receipt_deltas(&logs, owner);
446
447        // token_a: -100 + 25 = -75
448        assert_eq!(deltas.get(&token_a), Some(&I256::try_from(-75i64).unwrap()));
449        // token_b: +50
450        assert_eq!(deltas.get(&token_b), Some(&I256::try_from(50i64).unwrap()));
451    }
452
453    #[test]
454    fn test_parse_receipt_deltas_empty() {
455        let owner = address!("1111111111111111111111111111111111111111");
456        let deltas = super::parse_receipt_deltas(&[], owner);
457        assert!(deltas.is_empty());
458    }
459}