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::{
5    B256, U256,
6    map::{B256Map, IndexMap},
7};
8use revm::{bytecode::opcode, interpreter::InstructionResult};
9use revm_inspectors::tracing::types::{CallTraceStep, StorageChangeReason};
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12enum StorageAccessKind {
13    Load,
14    Store,
15}
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub(super) enum StorageSpace {
19    Persistent,
20    Transient,
21}
22
23impl StorageSpace {
24    pub(super) const fn noun(self) -> &'static str {
25        match self {
26            Self::Persistent => "storage",
27            Self::Transient => "transient storage",
28        }
29    }
30
31    pub(super) const fn label(self) -> &'static str {
32        match self {
33            Self::Persistent => "Storage",
34            Self::Transient => "Transient storage",
35        }
36    }
37
38    pub(super) const fn command(self) -> &'static str {
39        match self {
40            Self::Persistent => "storage",
41            Self::Transient => "transient",
42        }
43    }
44}
45
46#[derive(Clone, Copy, Debug, PartialEq, Eq)]
47pub(super) struct StorageAccess {
48    step_index: usize,
49    pc: usize,
50    space: StorageSpace,
51    kind: StorageAccessKind,
52    slot: U256,
53    value: U256,
54    previous: Option<U256>,
55}
56
57impl StorageAccess {
58    pub(super) const fn step_index(self) -> usize {
59        self.step_index
60    }
61
62    pub(super) const fn pc(self) -> usize {
63        self.pc
64    }
65
66    pub(super) const fn slot(self) -> U256 {
67        self.slot
68    }
69
70    pub(super) const fn space(self) -> StorageSpace {
71        self.space
72    }
73
74    pub(super) const fn value(self) -> U256 {
75        self.value
76    }
77
78    pub(super) const fn op(self) -> &'static str {
79        match (self.space, self.kind) {
80            (StorageSpace::Persistent, StorageAccessKind::Load) => "SLOAD",
81            (StorageSpace::Persistent, StorageAccessKind::Store) => "SSTORE",
82            (StorageSpace::Transient, StorageAccessKind::Load) => "TLOAD",
83            (StorageSpace::Transient, StorageAccessKind::Store) => "TSTORE",
84        }
85    }
86
87    pub(super) fn describe(self) -> String {
88        let op = self.op();
89        let space = self.space.noun();
90
91        match (self.kind, self.previous) {
92            (StorageAccessKind::Store, Some(previous)) => format!(
93                "{space} {op} slot {}: {} -> {}",
94                hex_u256(self.slot),
95                hex_u256(previous),
96                hex_u256(self.value)
97            ),
98            _ => format!("{space} {op} slot {} = {}", hex_u256(self.slot), hex_u256(self.value)),
99        }
100    }
101}
102
103pub(super) fn storage_accesses_until(
104    arena: &[DebugNode],
105    current_node_index: usize,
106    current_step_index: usize,
107    space: StorageSpace,
108) -> IndexMap<U256, StorageAccess> {
109    let current_node = &arena[current_node_index];
110    let current_absolute_step = current_node.step_offset.saturating_add(current_step_index);
111    let mut accesses = IndexMap::default();
112
113    for node in arena.iter().filter(|node| node.trace_node_idx == current_node.trace_node_idx) {
114        for (step_index, _) in node.steps.iter().enumerate() {
115            if node.step_offset.saturating_add(step_index) > current_absolute_step {
116                break;
117            }
118            if let Some(access) = storage_access_at(&node.steps, step_index)
119                && access.space() == space
120            {
121                accesses.insert(access.slot(), access);
122            }
123        }
124    }
125
126    accesses
127}
128
129pub(super) fn storage_values(accesses: &IndexMap<U256, StorageAccess>) -> B256Map<B256> {
130    accesses.iter().map(|(slot, access)| (B256::from(*slot), B256::from(access.value()))).collect()
131}
132
133/// Returns the value from the next persistent storage write to each slot in the current trace.
134pub(super) fn next_storage_write_values(
135    arena: &[DebugNode],
136    current_node_index: usize,
137    current_step_index: usize,
138) -> B256Map<B256> {
139    let current_node = &arena[current_node_index];
140    let current_absolute_step = current_node.step_offset.saturating_add(current_step_index);
141    let mut next_accesses = B256Map::<(usize, B256)>::default();
142
143    for node in arena.iter().filter(|node| node.trace_node_idx == current_node.trace_node_idx) {
144        for (step_index, _) in node.steps.iter().enumerate() {
145            let absolute_step = node.step_offset.saturating_add(step_index);
146            if absolute_step <= current_absolute_step {
147                continue;
148            }
149            let Some(access) = storage_access_at(&node.steps, step_index).filter(|access| {
150                access.space() == StorageSpace::Persistent
151                    && access.kind == StorageAccessKind::Store
152            }) else {
153                continue;
154            };
155            let slot = B256::from(access.slot());
156            let value = B256::from(access.value());
157            match next_accesses.get_mut(&slot) {
158                Some((next_step, next_value)) if absolute_step < *next_step => {
159                    *next_step = absolute_step;
160                    *next_value = value;
161                }
162                None => {
163                    next_accesses.insert(slot, (absolute_step, value));
164                }
165                _ => {}
166            }
167        }
168    }
169
170    next_accesses.into_iter().map(|(slot, (_, value))| (slot, value)).collect()
171}
172
173pub(super) fn storage_access_at(
174    steps: &[CallTraceStep],
175    step_index: usize,
176) -> Option<StorageAccess> {
177    let step = steps.get(step_index)?;
178    if matches!(step.op.get(), opcode::SSTORE | opcode::TSTORE)
179        && !step.status.is_none_or(InstructionResult::is_ok)
180    {
181        return None;
182    }
183
184    if let Some(change) = step.storage_change.as_deref() {
185        let kind = match change.reason {
186            StorageChangeReason::SLOAD => StorageAccessKind::Load,
187            StorageChangeReason::SSTORE => StorageAccessKind::Store,
188        };
189        return Some(StorageAccess {
190            step_index,
191            pc: step.pc,
192            space: StorageSpace::Persistent,
193            kind,
194            slot: change.key,
195            value: change.value,
196            previous: change.had_value,
197        });
198    }
199
200    let (space, kind) = match step.op.get() {
201        opcode::SLOAD => (StorageSpace::Persistent, StorageAccessKind::Load),
202        opcode::SSTORE => (StorageSpace::Persistent, StorageAccessKind::Store),
203        opcode::TLOAD => (StorageSpace::Transient, StorageAccessKind::Load),
204        opcode::TSTORE => (StorageSpace::Transient, StorageAccessKind::Store),
205        _ => return None,
206    };
207
208    if kind == StorageAccessKind::Load {
209        return Some(StorageAccess {
210            step_index,
211            pc: step.pc,
212            space,
213            kind,
214            slot: step.stack.as_deref()?.last().copied()?,
215            value: steps.get(step_index.checked_add(1)?)?.stack.as_deref()?.last().copied()?,
216            previous: None,
217        });
218    }
219
220    let mut stack = step.stack.as_deref()?.iter().rev();
221    let slot = stack.next().copied()?;
222    let value = stack.next().copied()?;
223    Some(StorageAccess { step_index, pc: step.pc, space, kind, slot, value, previous: None })
224}
225
226pub(super) fn hex_u256(value: U256) -> String {
227    format!("{value:#x}")
228}