Skip to main content

foundry_debugger/tui/
storage.rs

1//! Storage access helpers for debugger TUI views and commands.
2
3use crate::DebugNode;
4use alloy_primitives::{U256, map::IndexMap};
5use revm::{bytecode::opcode, interpreter::InstructionResult};
6use revm_inspectors::tracing::types::{CallTraceStep, StorageChangeReason};
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9enum StorageAccessKind {
10    Load,
11    Store,
12}
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub(super) enum StorageSpace {
16    Persistent,
17    Transient,
18}
19
20impl StorageSpace {
21    pub(super) const fn noun(self) -> &'static str {
22        match self {
23            Self::Persistent => "storage",
24            Self::Transient => "transient storage",
25        }
26    }
27
28    pub(super) const fn label(self) -> &'static str {
29        match self {
30            Self::Persistent => "Storage",
31            Self::Transient => "Transient storage",
32        }
33    }
34
35    pub(super) const fn command(self) -> &'static str {
36        match self {
37            Self::Persistent => "storage",
38            Self::Transient => "transient",
39        }
40    }
41}
42
43#[derive(Clone, Copy, Debug, PartialEq, Eq)]
44pub(super) struct StorageAccess {
45    step_index: usize,
46    pc: usize,
47    space: StorageSpace,
48    kind: StorageAccessKind,
49    slot: U256,
50    value: U256,
51    previous: Option<U256>,
52}
53
54impl StorageAccess {
55    pub(super) const fn step_index(self) -> usize {
56        self.step_index
57    }
58
59    pub(super) const fn pc(self) -> usize {
60        self.pc
61    }
62
63    pub(super) const fn slot(self) -> U256 {
64        self.slot
65    }
66
67    pub(super) const fn space(self) -> StorageSpace {
68        self.space
69    }
70
71    pub(super) const fn value(self) -> U256 {
72        self.value
73    }
74
75    pub(super) const fn op(self) -> &'static str {
76        match (self.space, self.kind) {
77            (StorageSpace::Persistent, StorageAccessKind::Load) => "SLOAD",
78            (StorageSpace::Persistent, StorageAccessKind::Store) => "SSTORE",
79            (StorageSpace::Transient, StorageAccessKind::Load) => "TLOAD",
80            (StorageSpace::Transient, StorageAccessKind::Store) => "TSTORE",
81        }
82    }
83
84    pub(super) fn describe(self) -> String {
85        let op = self.op();
86        let space = self.space.noun();
87
88        match (self.kind, self.previous) {
89            (StorageAccessKind::Store, Some(previous)) => format!(
90                "{space} {op} slot {}: {} -> {}",
91                hex_u256(self.slot),
92                hex_u256(previous),
93                hex_u256(self.value)
94            ),
95            _ => format!("{space} {op} slot {} = {}", hex_u256(self.slot), hex_u256(self.value)),
96        }
97    }
98}
99
100pub(super) fn storage_accesses_until(
101    arena: &[DebugNode],
102    current_node_index: usize,
103    current_step_index: usize,
104    space: StorageSpace,
105) -> IndexMap<U256, StorageAccess> {
106    let current_node = &arena[current_node_index];
107    let current_absolute_step = current_node.step_offset.saturating_add(current_step_index);
108    let mut accesses = IndexMap::default();
109
110    for node in arena.iter().filter(|node| node.trace_node_idx == current_node.trace_node_idx) {
111        for (step_index, _) in node.steps.iter().enumerate() {
112            if node.step_offset.saturating_add(step_index) > current_absolute_step {
113                break;
114            }
115            if let Some(access) = storage_access_at(&node.steps, step_index)
116                && access.space() == space
117            {
118                accesses.insert(access.slot(), access);
119            }
120        }
121    }
122
123    accesses
124}
125
126pub(super) fn storage_access_at(
127    steps: &[CallTraceStep],
128    step_index: usize,
129) -> Option<StorageAccess> {
130    let step = steps.get(step_index)?;
131    if matches!(step.op.get(), opcode::SSTORE | opcode::TSTORE)
132        && !step.status.is_none_or(InstructionResult::is_ok)
133    {
134        return None;
135    }
136
137    if let Some(change) = step.storage_change.as_deref() {
138        let kind = match change.reason {
139            StorageChangeReason::SLOAD => StorageAccessKind::Load,
140            StorageChangeReason::SSTORE => StorageAccessKind::Store,
141        };
142        return Some(StorageAccess {
143            step_index,
144            pc: step.pc,
145            space: StorageSpace::Persistent,
146            kind,
147            slot: change.key,
148            value: change.value,
149            previous: change.had_value,
150        });
151    }
152
153    let (space, kind) = match step.op.get() {
154        opcode::SLOAD => (StorageSpace::Persistent, StorageAccessKind::Load),
155        opcode::SSTORE => (StorageSpace::Persistent, StorageAccessKind::Store),
156        opcode::TLOAD => (StorageSpace::Transient, StorageAccessKind::Load),
157        opcode::TSTORE => (StorageSpace::Transient, StorageAccessKind::Store),
158        _ => return None,
159    };
160
161    if kind == StorageAccessKind::Load {
162        return Some(StorageAccess {
163            step_index,
164            pc: step.pc,
165            space,
166            kind,
167            slot: step.stack.as_deref()?.last().copied()?,
168            value: steps.get(step_index.checked_add(1)?)?.stack.as_deref()?.last().copied()?,
169            previous: None,
170        });
171    }
172
173    let mut stack = step.stack.as_deref()?.iter().rev();
174    let slot = stack.next().copied()?;
175    let value = stack.next().copied()?;
176    Some(StorageAccess { step_index, pc: step.pc, space, kind, slot, value, previous: None })
177}
178
179pub(super) fn hex_u256(value: U256) -> String {
180    format!("{value:#x}")
181}