edb_engine/inspector/
hook_snapshot_inspector.rs

1// EDB - Ethereum Debugger
2// Copyright (C) 2024 Zhuo Zhang and Wuqi Zhang
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU Affero General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU Affero General Public License for more details.
13//
14// You should have received a copy of the GNU Affero General Public License
15// along with this program. If not, see <https://www.gnu.org/licenses/>.
16
17//! Hook snapshot inspector for recording VM state at specific trigger points
18//!
19//! This inspector captures database snapshots only when execution calls
20//! the magic trigger address 0x0000000000000000000000000000000000023333.
21//! The call data contains an ABI-encoded number (usid) that identifies
22//! the specific hook point.
23//!
24//! Unlike OpcodeSnapshotInspector which captures every instruction,
25//! HookSnapshotInspector only captures at predetermined breakpoints,
26//! making it more efficient for tracking specific execution states.
27
28use alloy_dyn_abi::{DynSolType, DynSolValue};
29use alloy_primitives::{Address, Bytes, U256};
30use edb_common::{
31    types::{CallResult, EdbSolValue, ExecutionFrameId, Trace},
32    EdbContext,
33};
34use eyre::Result;
35use foundry_compilers::{artifacts::Contract, Artifact};
36use revm::{
37    bytecode::OpCode,
38    context::{ContextTr, CreateScheme, JournalTr},
39    database::CacheDB,
40    interpreter::{
41        interpreter_types::{InputsTr, Jumps},
42        CallInputs, CallOutcome, CreateInputs, CreateOutcome, Interpreter,
43    },
44    Database, DatabaseCommit, DatabaseRef, Inspector,
45};
46use serde::{Deserialize, Serialize};
47use std::{
48    collections::HashMap,
49    ops::{Deref, DerefMut},
50    sync::Arc,
51};
52use tracing::{debug, error};
53
54use crate::{
55    analysis::{dyn_sol_type, AnalysisResult, UserDefinedTypeRef, VariableRef, UVID},
56    USID,
57};
58
59/// Magic number that indicates a snapshot to be taken
60pub const MAGIC_SNAPSHOT_NUMBER: U256 = U256::from_be_bytes([
61    0x20, 0x15, 0x05, 0x02, 0xff, 0xff, 0xff, 0xff, 0x20, 0x24, 0x01, 0x02, 0xff, 0xff, 0xff, 0xff,
62    0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
63]);
64
65/// Magic number that indicates a variable update to be recorded
66pub const MAGIC_VARIABLE_UPDATE_NUMBER: U256 = U256::from_be_bytes([
67    0x20, 0x25, 0x02, 0x08, 0xff, 0x20, 0x25, 0x09, 0x16, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
68    0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
69]);
70
71/// Single hook execution snapshot
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct HookSnapshot<DB>
74where
75    DB: Database + DatabaseCommit + DatabaseRef + Clone,
76    <CacheDB<DB> as Database>::Error: Clone,
77    <DB as Database>::Error: Clone,
78{
79    /// Target address that triggered the hook
80    pub target_address: Address,
81    /// Bytecode address that the current snapshot is running
82    pub bytecode_address: Address,
83    /// Database state at the hook point
84    pub database: Arc<CacheDB<DB>>,
85    /// Value of accessible local variables
86    pub locals: HashMap<String, Option<Arc<EdbSolValue>>>,
87    /// Value of state variables at this point (e.g., code address)
88    pub state_variables: HashMap<String, Option<Arc<EdbSolValue>>>,
89    /// User-defined snapshot ID from call data
90    pub usid: USID,
91}
92
93/// Collection of hook snapshots organized by execution order
94///
95/// Unlike OpcodeSnapshots which use a HashMap, HookSnapshots maintains
96/// insertion order to track when each snapshot was taken during execution.
97#[derive(Debug, Clone)]
98pub struct HookSnapshots<DB>
99where
100    DB: Database + DatabaseCommit + DatabaseRef + Clone,
101    <CacheDB<DB> as Database>::Error: Clone,
102    <DB as Database>::Error: Clone,
103{
104    /// Vector of (frame_id, optional_snapshot) pairs in execution order
105    /// None indicates a frame where no hook was triggered
106    snapshots: Vec<(ExecutionFrameId, Option<HookSnapshot<DB>>)>,
107}
108
109impl<DB> Default for HookSnapshots<DB>
110where
111    DB: Database + DatabaseCommit + DatabaseRef + Clone,
112    <CacheDB<DB> as Database>::Error: Clone,
113    <DB as Database>::Error: Clone,
114{
115    fn default() -> Self {
116        Self { snapshots: Vec::new() }
117    }
118}
119
120impl<DB> Deref for HookSnapshots<DB>
121where
122    DB: Database + DatabaseCommit + DatabaseRef + Clone,
123    <CacheDB<DB> as Database>::Error: Clone,
124    <DB as Database>::Error: Clone,
125{
126    type Target = Vec<(ExecutionFrameId, Option<HookSnapshot<DB>>)>;
127
128    fn deref(&self) -> &Self::Target {
129        &self.snapshots
130    }
131}
132
133impl<DB> DerefMut for HookSnapshots<DB>
134where
135    DB: Database + DatabaseCommit + DatabaseRef + Clone,
136    <CacheDB<DB> as Database>::Error: Clone,
137    <DB as Database>::Error: Clone,
138{
139    fn deref_mut(&mut self) -> &mut Self::Target {
140        &mut self.snapshots
141    }
142}
143
144impl<DB> IntoIterator for HookSnapshots<DB>
145where
146    DB: Database + DatabaseCommit + DatabaseRef + Clone,
147    <CacheDB<DB> as Database>::Error: Clone,
148    <DB as Database>::Error: Clone,
149{
150    type Item = (ExecutionFrameId, Option<HookSnapshot<DB>>);
151    type IntoIter = std::vec::IntoIter<Self::Item>;
152
153    fn into_iter(self) -> Self::IntoIter {
154        self.snapshots.into_iter()
155    }
156}
157
158impl<DB> HookSnapshots<DB>
159where
160    DB: Database + DatabaseCommit + DatabaseRef + Clone,
161    <CacheDB<DB> as Database>::Error: Clone,
162    <DB as Database>::Error: Clone,
163{
164    /// Get snapshot for a specific frame ID
165    pub fn get_snapshot(&self, frame_id: ExecutionFrameId) -> Option<&HookSnapshot<DB>> {
166        self.snapshots
167            .iter()
168            .find(|(id, _)| *id == frame_id)
169            .and_then(|(_, snapshot)| snapshot.as_ref())
170    }
171
172    /// Get all frames that have actual hook snapshots (non-None)
173    pub fn get_frames_with_hooks(&self) -> Vec<ExecutionFrameId> {
174        self.snapshots
175            .iter()
176            .filter_map(
177                |(frame_id, snapshot)| {
178                    if snapshot.is_some() {
179                        Some(*frame_id)
180                    } else {
181                        None
182                    }
183                },
184            )
185            .collect()
186    }
187
188    /// Add a frame placeholder (will be None if no hook is triggered)
189    fn add_frame_placeholder(&mut self, frame_id: ExecutionFrameId) {
190        self.snapshots.push((frame_id, None));
191    }
192
193    /// Update the last frame with a hook snapshot
194    fn update_last_frame_with_snapshot(
195        &mut self,
196        frame_id: ExecutionFrameId,
197        snapshot: HookSnapshot<DB>,
198    ) {
199        if let Some((last_frame_id, slot)) = self.snapshots.last_mut() {
200            if last_frame_id != &frame_id {
201                error!("Mismatched frame IDs: expected {}, got {}", last_frame_id, frame_id);
202            }
203            if slot.is_none() {
204                // If the last frame was empty, fill it with this snapshot
205                *slot = Some(snapshot);
206                return;
207            }
208        }
209
210        self.snapshots.push((frame_id, Some(snapshot)));
211    }
212}
213
214/// Inspector that records hook-triggered snapshots
215#[derive(Debug)]
216pub struct HookSnapshotInspector<'a, DB>
217where
218    DB: Database + DatabaseCommit + DatabaseRef + Clone,
219    <CacheDB<DB> as Database>::Error: Clone,
220    <DB as Database>::Error: Clone,
221{
222    /// The trace of the current tx
223    trace: &'a Trace,
224
225    /// Source code analysis results
226    analysis: &'a HashMap<Address, AnalysisResult>,
227
228    /// Collection of hook snapshots
229    pub snapshots: HookSnapshots<DB>,
230
231    /// Stack to track current execution frames
232    frame_stack: Vec<ExecutionFrameId>,
233
234    /// Current trace entry counter
235    current_trace_id: usize,
236
237    /// Creation hooks (original contract bytecode, hooked bytecode, constructor args)
238    creation_hooks: Vec<(Bytes, Bytes, Bytes)>,
239
240    /// The latest value of each UVID encountered (for variable tracking)
241    uvid_values: HashMap<UVID, Arc<EdbSolValue>>,
242}
243
244impl<'a, DB> HookSnapshotInspector<'a, DB>
245where
246    DB: Database + DatabaseCommit + DatabaseRef + Clone,
247    <CacheDB<DB> as Database>::Error: Clone,
248    <DB as Database>::Error: Clone,
249{
250    /// Create a new hook snapshot inspector
251    pub fn new(trace: &'a Trace, analysis: &'a HashMap<Address, AnalysisResult>) -> Self {
252        Self {
253            trace,
254            analysis,
255            snapshots: HookSnapshots::default(),
256            frame_stack: Vec::new(),
257            current_trace_id: 0,
258            creation_hooks: Vec::new(),
259            uvid_values: HashMap::new(),
260        }
261    }
262
263    /// Add creation hooks
264    pub fn with_creation_hooks(
265        &mut self,
266        hooks: Vec<(&Contract, &Contract, &Bytes)>,
267    ) -> Result<()> {
268        for (original, hooked, args) in hooks {
269            self.creation_hooks.push((
270                original
271                    .get_bytecode_bytes()
272                    .ok_or(eyre::eyre!("Failed to get bytecode for contract"))?
273                    .as_ref()
274                    .clone(),
275                hooked
276                    .get_bytecode_bytes()
277                    .ok_or(eyre::eyre!("Failed to get bytecode for contract"))?
278                    .as_ref()
279                    .clone(),
280                args.clone(),
281            ));
282        }
283
284        Ok(())
285    }
286
287    /// Consume the inspector and return the collected snapshots
288    pub fn into_snapshots(self) -> HookSnapshots<DB> {
289        self.snapshots
290    }
291
292    /// Get the current execution frame ID
293    fn current_frame_id(&self) -> Option<ExecutionFrameId> {
294        self.frame_stack.last().copied()
295    }
296
297    /// Start tracking a new execution frame
298    fn push_frame(&mut self, trace_id: usize) {
299        let frame_id = ExecutionFrameId::new(trace_id, 0);
300        self.frame_stack.push(frame_id);
301
302        // Add placeholder for this frame
303        self.snapshots.add_frame_placeholder(frame_id);
304    }
305
306    /// Stop tracking current execution frame and increment re-entry count
307    fn pop_frame(&mut self) -> Option<ExecutionFrameId> {
308        if let Some(frame_id) = self.frame_stack.pop() {
309            // Increment re-entry count for parent frame if it exists
310            if let Some(parent_frame_id) = self.frame_stack.last_mut() {
311                parent_frame_id.increment_re_entry();
312            }
313
314            // Add placeholder for the new frame
315            if let Some(current_frame_id) = self.current_frame_id() {
316                self.snapshots.add_frame_placeholder(current_frame_id);
317            }
318
319            Some(frame_id)
320        } else {
321            None
322        }
323    }
324
325    /// Check if this is a hook trigger call and record snapshot if so
326    fn check_and_record_hook(
327        &mut self,
328        data: &[u8],
329        interp: &Interpreter,
330        ctx: &mut EdbContext<DB>,
331    ) {
332        let address = self
333            .current_frame_id()
334            .and_then(|frame_id| self.trace.get(frame_id.trace_entry_id()))
335            .map(|entry| entry.code_address)
336            .unwrap_or(interp.input.target_address());
337
338        let usid_opt = if data.len() >= 32 {
339            U256::from_be_slice(&data[..32]).try_into().ok()
340        } else {
341            error!("KECCAK256 input data too short for snapshot, skipping");
342            return;
343        };
344
345        let Some(usid) = usid_opt else {
346            error!("Hook call data does not contain valid USID, skipping snapshot");
347            return;
348        };
349
350        // Clone current database state
351        let mut inner = ctx.journal().to_inner();
352        let changes = inner.finalize();
353        let mut snap = ctx.db().clone();
354        snap.commit(changes);
355
356        // Check variables that are valid at this point
357        let Some(step) = self.analysis.get(&address).and_then(|a| a.usid_to_step.get(&usid)) else {
358            error!(
359                address=?address,
360                usid=?usid,
361                "No analysis step found for address and USID, skipping hook snapshot",
362            );
363            return;
364        };
365
366        // Collect values of accessible variables
367        let mut locals = HashMap::new();
368        for variable in &step.read().accessible_variables {
369            if variable.declaration().state_variable {
370                continue;
371            }
372            let uvid = variable.id();
373            let name = variable.declaration().name.clone();
374            locals.insert(name, self.uvid_values.get(&uvid).cloned());
375        }
376
377        // Update the last frame with this snapshot
378        if let Some(current_frame_id) = self.current_frame_id() {
379            if let Some(entry) = self.trace.get(current_frame_id.trace_entry_id()) {
380                // Create hook snapshot
381                let hook_snapshot = HookSnapshot {
382                    target_address: entry.target,
383                    bytecode_address: entry.code_address,
384                    database: Arc::new(snap),
385                    locals,
386                    usid,
387                    state_variables: HashMap::new(), // State variables can be filled in later
388                };
389
390                self.snapshots.update_last_frame_with_snapshot(current_frame_id, hook_snapshot);
391            } else {
392                error!("No trace entry found for frame {}", current_frame_id);
393            }
394        } else {
395            error!("No current frame to update with hook snapshot");
396        }
397    }
398
399    fn check_and_record_variable_update(
400        &mut self,
401        data: &[u8],
402        interp: &Interpreter,
403        _ctx: &mut EdbContext<DB>,
404    ) {
405        let address = self
406            .current_frame_id()
407            .and_then(|frame_id| self.trace.get(frame_id.trace_entry_id()))
408            .map(|entry| entry.code_address)
409            .unwrap_or(interp.input.target_address());
410
411        // The data is decoded as (uint256 magic, uint256 uvid, abi.encode(value))
412        // So the data will be organized as
413        //      -32 .. 0 : [uint256 magic] (parsed before this function)
414        //        0 .. 32: [uint256 uvid]
415        //       32 .. 64: [offset] (should be 0x60 considering the first two uint256)
416        //       64 .. 96: [length of encoded value]
417        //       96 .. _ : [encoded value]
418        if data.len() < 96 {
419            error!(
420                address=?address,
421                "KECCAK256 input data too short for variable update value, skipping"
422            );
423            return;
424        }
425
426        let Some(uvid) = U256::from_be_slice(&data[..32]).try_into().ok() else {
427            error!("Hook call data does not contain valid UVID, skipping snapshot");
428            return;
429        };
430
431        let offset = U256::from_be_slice(&data[32..64]);
432        if offset != U256::from(0x60) {
433            error!(
434                address=?address,
435                uvid=?uvid,
436                offset=?offset,
437                "Unexpected offset for variable update value, skipping"
438            );
439            return;
440        }
441
442        let length = U256::from_be_slice(&data[64..96]);
443        let length_usize = match usize::try_from(length) {
444            Ok(l) => l,
445            Err(_) => {
446                error!(
447                    address=?address,
448                    uvid=?uvid,
449                    length=?length,
450                    "Variable update value length too large, skipping"
451                );
452                return;
453            }
454        };
455
456        let decoded_data = &data[96..96 + length_usize];
457
458        let Some(analysis) = self.analysis.get(&address) else {
459            error!(
460                address=?address,
461                uvid=?uvid,
462                "No analysis found for address, skipping variable update recording",
463            );
464            return;
465        };
466        let Some(variable) = analysis.uvid_to_variable.get(&uvid) else {
467            error!(
468                address=?address,
469                uvid=?uvid,
470                "No variable found for address and UVID, skipping variable update recording",
471            );
472            return;
473        };
474
475        let value =
476            match decode_variable_value(&analysis.user_defined_types, variable, decoded_data) {
477                Ok(v) => v,
478                Err(e) => {
479                    error!(
480                        address=?address,
481                        uvid=?uvid,
482                        variable=?variable.declaration().type_descriptions.type_string,
483                        type_name = ?variable.declaration().type_name,
484                        data=?hex::encode(decoded_data),
485                        error=?e,
486                    );
487                    return;
488                }
489            };
490
491        debug!(
492            uvid=?uvid,
493            address=?address,
494            variable=?variable.declaration().name,
495            value=?value,
496            "Found variable update",
497        );
498
499        self.uvid_values.insert(uvid, Arc::new(value.into()));
500    }
501
502    /// Check and apply creation hooks if the bytecode matches
503    fn check_and_apply_creation_hooks(
504        &mut self,
505        inputs: &mut CreateInputs,
506        ctx: &mut EdbContext<DB>,
507    ) {
508        // Get the nonce from the caller account
509        let Ok(account) = ctx.journaled_state.load_account(inputs.caller) else {
510            error!("Failed to load account for caller {:?}", inputs.caller);
511            return;
512        };
513
514        // Calculate what address would be created using the built-in method
515        let nonce = account.info.nonce;
516        let predicted_address = inputs.created_address(nonce);
517
518        for (original_bytecode, hooked_bytecode, constructor_args) in &self.creation_hooks {
519            // Check if constructor arguments are at the tail of input bytes
520            if inputs.init_code.len() >= constructor_args.len() {
521                let input_args_start = inputs.init_code.len() - constructor_args.len();
522                let input_args = &inputs.init_code[input_args_start..];
523
524                // Check if constructor args match
525                if input_args == constructor_args.as_ref() {
526                    // Get the creation bytecode (without constructor args)
527                    let input_bytecode = &inputs.init_code[..input_args_start];
528
529                    // Check if bytecode is very similar to original
530                    // For now, we do exact match, but could be made fuzzy
531                    if input_bytecode == original_bytecode.as_ref() {
532                        // Match found! Replace with hooked bytecode + constructor args
533                        let mut new_init_code = Vec::from(hooked_bytecode.as_ref());
534                        new_init_code.extend_from_slice(constructor_args.as_ref());
535                        inputs.init_code = Bytes::from(new_init_code);
536
537                        // Update creation schema
538                        inputs.scheme = CreateScheme::Custom { address: predicted_address };
539
540                        // Log the replacement
541                        debug!(
542                            "Replaced creation bytecode with hooked version for {:?} -> {:?}",
543                            inputs.caller, predicted_address
544                        );
545
546                        break; // Found a match, no need to check other hooks
547                    }
548                }
549            }
550        }
551    }
552
553    /// Clear all recorded data
554    pub fn clear(&mut self) {
555        self.snapshots.snapshots.clear();
556        self.frame_stack.clear();
557        self.current_trace_id = 0;
558    }
559}
560
561impl<'a, DB> Inspector<EdbContext<DB>> for HookSnapshotInspector<'a, DB>
562where
563    DB: Database + DatabaseCommit + DatabaseRef + Clone,
564    <CacheDB<DB> as Database>::Error: Clone,
565    <DB as Database>::Error: Clone,
566{
567    fn step(&mut self, interp: &mut Interpreter, ctx: &mut EdbContext<DB>) {
568        // Get current opcode safely
569        let opcode = unsafe { OpCode::new_unchecked(interp.bytecode.opcode()) };
570
571        if opcode != OpCode::KECCAK256 {
572            // KECCAK256 is the only hooked opcode.
573            return;
574        }
575
576        let Some(data) = interp.stack.pop().ok().and_then(|offset_u256| {
577            let data = interp.stack.pop().ok().and_then(|len_u256| {
578                let offset = usize::try_from(offset_u256).ok()?;
579                let len = usize::try_from(len_u256).ok()?;
580                let data = interp.memory.slice_len(offset, len);
581
582                let _ = interp.stack.push(len_u256);
583                Some(data)
584            });
585
586            let _ = interp.stack.push(offset_u256);
587            data
588        }) else {
589            error!("Failed to read KECCAK256 input data from stack");
590            return;
591        };
592
593        if data.len() < 32 {
594            // Not enough data for at least two U256 values
595            return;
596        }
597
598        let magic_number = U256::from_be_slice(&data[..32]);
599
600        if magic_number == MAGIC_SNAPSHOT_NUMBER {
601            self.check_and_record_hook(&data[32..], interp, ctx);
602        } else if magic_number == MAGIC_VARIABLE_UPDATE_NUMBER {
603            self.check_and_record_variable_update(&data[32..], interp, ctx);
604        }
605    }
606
607    fn call(
608        &mut self,
609        _context: &mut EdbContext<DB>,
610        _inputs: &mut CallInputs,
611    ) -> Option<CallOutcome> {
612        // Start tracking new execution frame for regular calls only
613        self.push_frame(self.current_trace_id);
614        self.current_trace_id += 1;
615        None
616    }
617
618    fn call_end(
619        &mut self,
620        _context: &mut EdbContext<DB>,
621        inputs: &CallInputs,
622        outcome: &mut CallOutcome,
623    ) {
624        let Some(frame_id) = self.pop_frame() else { return };
625
626        let Some(entry) = self.trace.get(frame_id.trace_entry_id()) else { return };
627
628        if entry.result != Some(outcome.into()) {
629            // Mismatched call outcome
630            error!(
631                target_address = inputs.target_address.to_string(),
632                bytecode_address = inputs.bytecode_address.to_string(),
633                "Call outcome mismatch at frame {}: expected {:?}, got {:?} ({:?})",
634                frame_id,
635                entry.result,
636                Into::<CallResult>::into(&outcome),
637                outcome,
638            );
639        }
640    }
641
642    fn create(
643        &mut self,
644        context: &mut EdbContext<DB>,
645        inputs: &mut CreateInputs,
646    ) -> Option<CreateOutcome> {
647        // Check and apply creation hooks if applicable
648        self.check_and_apply_creation_hooks(inputs, context);
649
650        // Start tracking new execution frame for contract creation
651        self.push_frame(self.current_trace_id);
652        self.current_trace_id += 1;
653        None
654    }
655
656    fn create_end(
657        &mut self,
658        _context: &mut EdbContext<DB>,
659        _inputs: &CreateInputs,
660        outcome: &mut CreateOutcome,
661    ) {
662        // Stop tracking current execution frame
663        let Some(frame_id) = self.pop_frame() else { return };
664
665        let Some(entry) = self.trace.get(frame_id.trace_entry_id()) else { return };
666
667        // For creation, we only check the return status, not the actually bytecode, since we
668        // will instrument the code
669        if entry.result.as_ref().map(|r| r.result()) != Some(outcome.result.result) {
670            // Mismatch create outcome
671            error!(
672                "Create outcome mismatch at frame {}: expected {:?}, got {:?}",
673                frame_id, entry.result, outcome
674            );
675        }
676    }
677}
678
679/// Pretty printing utilities for debugging
680impl<DB> HookSnapshots<DB>
681where
682    DB: Database + DatabaseCommit + DatabaseRef + Clone,
683    <CacheDB<DB> as Database>::Error: Clone,
684    <DB as Database>::Error: Clone,
685{
686    /// Print comprehensive summary of hook snapshots
687    pub fn print_summary(&self) {
688        println!(
689            "\n\x1b[36m╔══════════════════════════════════════════════════════════════════╗\x1b[0m"
690        );
691        println!(
692            "\x1b[36m║              HOOK SNAPSHOT INSPECTOR SUMMARY                     ║\x1b[0m"
693        );
694        println!(
695            "\x1b[36m╚══════════════════════════════════════════════════════════════════╝\x1b[0m\n"
696        );
697
698        // Overall statistics
699        let total_frames = self.len();
700        let hook_frames = self.get_frames_with_hooks().len();
701
702        println!("\x1b[33m📊 Overall Statistics:\x1b[0m");
703        println!("  Total frames tracked: \x1b[32m{total_frames}\x1b[0m");
704        println!("  Frames with hooks: \x1b[32m{hook_frames}\x1b[0m");
705        println!(
706            "  Hook trigger rate: \x1b[32m{:.1}%\x1b[0m",
707            if total_frames > 0 { hook_frames as f64 / total_frames as f64 * 100.0 } else { 0.0 }
708        );
709
710        if self.is_empty() {
711            println!("\n\x1b[90m  No execution frames were tracked.\x1b[0m");
712            return;
713        }
714
715        println!("\n\x1b[33m🎯 Hook Trigger Details:\x1b[0m");
716        println!(
717            "\x1b[90m─────────────────────────────────────────────────────────────────\x1b[0m"
718        );
719
720        // Group snapshots by frame ID
721        use std::collections::HashMap;
722        let mut frame_groups: HashMap<ExecutionFrameId, Vec<&HookSnapshot<DB>>> = HashMap::new();
723        let mut frame_order = Vec::new();
724
725        for (frame_id, snapshot) in &self.snapshots {
726            if !frame_groups.contains_key(frame_id) {
727                frame_order.push(*frame_id);
728            }
729
730            match snapshot {
731                Some(hook_snapshot) => {
732                    frame_groups.entry(*frame_id).or_default().push(hook_snapshot);
733                }
734                None => {
735                    // Ensure frame exists in map even if empty
736                    frame_groups.entry(*frame_id).or_default();
737                }
738            }
739        }
740
741        // Print grouped results in original order
742        for (display_idx, frame_id) in frame_order.iter().enumerate() {
743            let hooks = frame_groups.get(frame_id).unwrap();
744
745            if hooks.is_empty() {
746                // Frame with no hooks
747                println!(
748                    "  \x1b[90m[{:3}] Frame {}\x1b[0m (trace.{}, re-entry {}) - No hooks",
749                    display_idx,
750                    frame_id,
751                    frame_id.trace_entry_id(),
752                    frame_id.re_entry_count()
753                );
754            } else {
755                // Frame with hooks - collect all USIDs in execution order (no sorting)
756                let usids: Vec<_> = hooks.iter().map(|h| h.usid).collect();
757                let hook_count = hooks.len();
758                #[allow(deprecated)]
759                let addresses: std::collections::HashSet<_> =
760                    hooks.iter().map(|h| h.bytecode_address).collect();
761
762                println!(
763                    "\n  \x1b[32m[{:3}] Frame {}\x1b[0m (trace.{}, re-entry {})",
764                    display_idx,
765                    frame_id,
766                    frame_id.trace_entry_id(),
767                    frame_id.re_entry_count()
768                );
769                println!(
770                    "       └─ \x1b[33m{} Hook{} Triggered\x1b[0m",
771                    hook_count,
772                    if hook_count == 1 { "" } else { "s" }
773                );
774
775                // Show addresses (usually just one per frame)
776                for address in &addresses {
777                    println!("          ├─ Address: \x1b[36m{address:?}\x1b[0m");
778                }
779
780                // Show USIDs in execution order with smart formatting
781                if usids.len() == 1 {
782                    println!("          └─ USID: \x1b[36m{}\x1b[0m", usids[0]);
783                } else if usids.len() <= 10 {
784                    // Show all USIDs for small lists
785                    let usid_list: Vec<String> = usids.iter().map(|u| u.to_string()).collect();
786                    println!("          └─ USIDs: \x1b[36m[{}]\x1b[0m", usid_list.join(", "));
787                } else {
788                    // For large lists, show first few, count, and last few
789                    let first_few: Vec<String> =
790                        usids.iter().take(3).map(|u| u.to_string()).collect();
791                    let last_few: Vec<String> =
792                        usids.iter().rev().take(3).rev().map(|u| u.to_string()).collect();
793
794                    if first_few.last() == last_few.first() {
795                        // Handle overlap case (shouldn't happen with take(3) for >10 items, but defensive)
796                        println!(
797                            "          └─ USIDs: \x1b[36m[{} ... {} total]\x1b[0m",
798                            first_few.join(", "),
799                            usids.len()
800                        );
801                    } else {
802                        println!(
803                            "          └─ USIDs: \x1b[36m[{}, ... {}, {} total]\x1b[0m",
804                            first_few.join(", "),
805                            last_few.join(", "),
806                            usids.len()
807                        );
808                    }
809                }
810            }
811        }
812
813        println!(
814            "\n\x1b[90m─────────────────────────────────────────────────────────────────\x1b[0m"
815        );
816        println!("\x1b[33m💡 Magic Snapshot Number:\x1b[0m {MAGIC_SNAPSHOT_NUMBER:?}");
817    }
818}
819
820/// Decode the variable value from the given ABI-encoded data according to the variable declaration.
821///
822/// This function takes raw ABI-encoded data and decodes it according to the variable's
823/// type information from its declaration. It handles both primitive Solidity types
824/// (uint, address, bool, etc.) and user-defined types (structs, enums).
825///
826/// # Arguments
827/// * `user_defined_types` - Mapping of type IDs to user-defined type references for resolving custom types
828/// * `variable` - The variable reference containing the declaration and type information
829/// * `data` - The ABI-encoded variable value as raw bytes
830///
831/// # Returns
832/// The decoded variable value as a [`DynSolValue`] that can be used in expression evaluation
833///
834/// # Errors
835/// Returns an error if:
836/// - The variable declaration lacks type information
837/// - The type cannot be resolved from the declaration
838/// - The data cannot be ABI-decoded according to the resolved type
839///
840/// # Example
841/// ```rust,ignore
842/// let decoded = decode_variable_value(&user_types, &variable_ref, &encoded_data)?;
843/// match decoded {
844///     DynSolValue::Uint(val, _) => println!("Uint value: {}", val),
845///     DynSolValue::Address(addr) => println!("Address: {}", addr),
846///     _ => println!("Other type: {:?}", decoded),
847/// }
848/// ```
849pub fn decode_variable_value(
850    user_defined_types: &HashMap<usize, UserDefinedTypeRef>,
851    variable: &VariableRef,
852    data: &[u8],
853) -> Result<DynSolValue> {
854    let type_name = variable
855        .type_name()
856        .ok_or(eyre::eyre!("Failed to get variable type: no type name in the declaration"))?;
857    let Some(variable_type): Option<DynSolType> = dyn_sol_type(user_defined_types, type_name)
858    else {
859        return Err(eyre::eyre!("Failed to get variable type: no type string in the declaration"));
860    };
861    let value = variable_type
862        .abi_decode(data)
863        .map_err(|e| eyre::eyre!("Failed to decode variable value: {}", e))?;
864    Ok(value)
865}