Skip to main content

foundry_cheatcodes/inspector/
env_overrides.rs

1//! Cheatcode overrides of the `BASEFEE`, `GASPRICE` and `BLOBHASH` opcode values.
2
3use alloy_primitives::{B256, U256, map::HashMap};
4use foundry_evm_core::backend::LocalForkId;
5
6/// Env overrides keyed by fork ID (`None` is the local, non-forked state).
7type EnvOverridesByFork = HashMap<Option<LocalForkId>, EnvOverrides>;
8
9/// Environment overrides applied at the opcode level.
10///
11/// In isolation mode (and inside the synthetic transactions used by
12/// `--gas-report` / `--isolate`) the transaction environment is zeroed for
13/// fee-accounting purposes, so cheatcodes that mutate the env (e.g.
14/// `vm.fee`, `vm.txGasPrice`, `vm.blobhashes`) cannot rely on those
15/// mutations being visible to contracts via the `BASEFEE`, `GASPRICE` and
16/// `BLOBHASH` opcodes. These overrides are applied in `step_end` to fix
17/// the value that was just pushed onto the stack.
18///
19/// # Semantics when invoked from inside the synthetic isolation transaction
20///
21/// `vm.fee` / `vm.txGasPrice` / `vm.blobhashes` consult
22/// [`Cheatcodes::in_isolation_context`](crate::Cheatcodes::in_isolation_context); when set, they
23/// only update these overrides (so `tx.gas_price = 0` continues to apply to fee accounting and
24/// EIP-4844 inner-tx validation does not reject the synthetic call) and
25/// leave the real env untouched. After the inner transaction returns, the
26/// outer env is restored from the cached snapshot taken before
27/// `transact_inner`, which means:
28///
29/// - the override **does** persist for subsequent `BASEFEE`, `GASPRICE` and `BLOBHASH` reads (this
30///   hook fires in `step_end` regardless of isolation),
31/// - `vm.getBlobhashes()` also consults these overrides, so it returns the correct value.
32/// - but the real `block.basefee` / `tx.gas_price` / `tx.blob_hashes` do **not** reflect the
33///   cheatcode value, so other non-opcode env consumers will not see it.
34///
35/// Calling these cheatcodes outside isolation behaves as before (real env
36/// is also mutated and the override mirrors it).
37#[derive(Clone, Debug, Default)]
38pub struct EnvOverrides {
39    /// Override for the `BASEFEE` opcode (set via `vm.fee`).
40    pub basefee: Option<u64>,
41    /// Base fee restored from a snapshot during isolation, valid until the fork is rolled.
42    pub implicit_basefee: Option<u64>,
43    /// Override for the `GASPRICE` opcode (set via `vm.txGasPrice`).
44    pub gas_price: Option<u128>,
45    /// Override for the `BLOBHASH` opcode (set via `vm.blobhashes`).
46    pub blob_hashes: Option<Vec<B256>>,
47    /// `tx.gas_price` captured at snapshot time when no gas_price override was
48    /// active. `sync_tx_after_env_override_restore` uses this to restore the
49    /// real pre-override value (not hardcoded 0) on revert.
50    pub pre_override_gas_price: Option<u128>,
51    /// `tx.tx_type` captured at snapshot time when no blob_hashes override was
52    /// active. Prevents tx_type being stuck at EIP4844 after reverting from a
53    /// blobhashes-set state.
54    pub pre_override_tx_type: Option<u8>,
55    /// `tx.blob_hashes` captured at snapshot time when no blob_hashes override
56    /// was active.
57    pub pre_override_blob_hashes: Option<Vec<B256>>,
58    /// The opcode about to run (captured in `step`, consumed in `step_end`),
59    /// used to know what was just executed when `step_end` fires — at that
60    /// point `interpreter.bytecode.opcode()` already points at the *next*
61    /// instruction.
62    pub(super) pending_opcode: Option<u8>,
63    /// Pending index for the `BLOBHASH` opcode, captured in `step` (where
64    /// the index is still on top of the stack) for use in `step_end` (after
65    /// the opcode has consumed it and pushed the looked-up hash).
66    pub(super) pending_blobhash_index: Option<u64>,
67}
68
69impl EnvOverrides {
70    /// Whether any override is set.
71    #[inline]
72    pub const fn is_any_set(&self) -> bool {
73        self.basefee.is_some()
74            || self.implicit_basefee.is_some()
75            || self.gas_price.is_some()
76            || self.blob_hashes.is_some()
77    }
78
79    /// Returns the value the `BASEFEE` opcode reads, if overridden.
80    #[inline]
81    pub fn basefee_override(&self) -> Option<u64> {
82        self.basefee.or(self.implicit_basefee)
83    }
84
85    /// Returns the value the `GASPRICE` opcode reads, if overridden.
86    #[inline]
87    pub const fn gas_price_override(&self) -> Option<u128> {
88        self.gas_price
89    }
90
91    /// Returns the hash the `BLOBHASH` opcode reads at `index`, if overridden.
92    ///
93    /// Out-of-range indices read the zero hash, per EIP-4844.
94    #[inline]
95    pub fn blob_hash_override(&self, index: u64) -> Option<B256> {
96        let blob_hashes = self.blob_hashes.as_ref()?;
97        Some(blob_hashes.get(index as usize).copied().unwrap_or_default())
98    }
99}
100
101/// Per-fork environment overrides and their state-snapshot copies.
102///
103/// Snapshot copies are required because backend snapshots do not include inspector state.
104#[derive(Clone, Debug, Default)]
105pub struct EnvOverrideState {
106    by_fork: EnvOverridesByFork,
107    snapshots: HashMap<U256, EnvOverridesByFork>,
108}
109
110impl EnvOverrideState {
111    /// Returns whether no fork has an override entry, active or not.
112    #[inline]
113    pub fn is_empty(&self) -> bool {
114        self.by_fork.is_empty()
115    }
116
117    /// Returns whether any fork, not only the active one, has an override set.
118    #[inline]
119    pub fn is_any_set(&self) -> bool {
120        self.by_fork.values().any(EnvOverrides::is_any_set)
121    }
122
123    /// Returns the overrides entry for `fork_id`, even if no override is set.
124    #[inline]
125    pub fn get(&self, fork_id: Option<LocalForkId>) -> Option<&EnvOverrides> {
126        self.by_fork.get(&fork_id)
127    }
128
129    /// Returns the overrides entry for `fork_id` mutably, without inserting one.
130    #[inline]
131    pub(super) fn get_mut(&mut self, fork_id: Option<LocalForkId>) -> Option<&mut EnvOverrides> {
132        self.by_fork.get_mut(&fork_id)
133    }
134
135    /// Updates the overrides for `fork_id`, inserting an empty entry first if absent.
136    pub(crate) fn update(
137        &mut self,
138        fork_id: Option<LocalForkId>,
139        f: impl FnOnce(&mut EnvOverrides),
140    ) {
141        f(self.by_fork.entry(fork_id).or_default());
142    }
143
144    /// Clears the base fee restored for isolation on `fork_id`, without inserting an entry.
145    pub(crate) fn clear_implicit_basefee(&mut self, fork_id: Option<LocalForkId>) {
146        if let Some(overrides) = self.by_fork.get_mut(&fork_id) {
147            overrides.implicit_basefee = None;
148        }
149    }
150
151    /// Removes the entry for `fork_id` if no override is set on it.
152    pub(crate) fn remove_if_unset(&mut self, fork_id: Option<LocalForkId>) {
153        if self.by_fork.get(&fork_id).is_some_and(|overrides| !overrides.is_any_set()) {
154            self.by_fork.remove(&fork_id);
155        }
156    }
157
158    /// Saves all overrides, recording the active fork's non-overridden transaction values.
159    pub(crate) fn save_snapshot(
160        &mut self,
161        snapshot_id: U256,
162        active_fork_id: Option<LocalForkId>,
163        tx_gas_price: u128,
164        tx_type: u8,
165        tx_blob_hashes: &[B256],
166    ) {
167        let mut snapshot = self.by_fork.clone();
168        let active = snapshot.entry(active_fork_id).or_default();
169        if active.gas_price.is_none() {
170            active.pre_override_gas_price = Some(tx_gas_price);
171        }
172        if active.blob_hashes.is_none() {
173            active.pre_override_tx_type = Some(tx_type);
174            active.pre_override_blob_hashes = Some(tx_blob_hashes.to_vec());
175        }
176        self.snapshots.insert(snapshot_id, snapshot);
177    }
178
179    /// Restores the overrides saved under `snapshot_id`, deleting the copy if `delete` is set.
180    ///
181    /// Does nothing if no copy was saved under `snapshot_id`.
182    pub(crate) fn restore_snapshot(&mut self, snapshot_id: U256, delete: bool) {
183        let snapshot = if delete {
184            self.snapshots.remove(&snapshot_id)
185        } else {
186            self.snapshots.get(&snapshot_id).cloned()
187        };
188        if let Some(snapshot) = snapshot {
189            self.by_fork = snapshot;
190        }
191    }
192
193    /// Deletes the copy saved under `snapshot_id`.
194    pub(crate) fn delete_snapshot(&mut self, snapshot_id: U256) {
195        self.snapshots.remove(&snapshot_id);
196    }
197
198    /// Deletes all saved copies.
199    pub(crate) fn clear_snapshots(&mut self) {
200        self.snapshots.clear();
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    const FORK: Option<LocalForkId> = Some(U256::from_limbs([1, 0, 0, 0]));
209    const SNAPSHOT: U256 = U256::from_limbs([7, 0, 0, 0]);
210
211    #[test]
212    fn override_values() {
213        let mut overrides = EnvOverrides::default();
214        assert_eq!(overrides.basefee_override(), None);
215        assert_eq!(overrides.gas_price_override(), None);
216        assert_eq!(overrides.blob_hash_override(0), None);
217
218        overrides.implicit_basefee = Some(1);
219        assert_eq!(overrides.basefee_override(), Some(1));
220        overrides.basefee = Some(2);
221        assert_eq!(overrides.basefee_override(), Some(2));
222
223        overrides.gas_price = Some(3);
224        assert_eq!(overrides.gas_price_override(), Some(3));
225
226        let hash = B256::repeat_byte(1);
227        overrides.blob_hashes = Some(vec![hash]);
228        assert_eq!(overrides.blob_hash_override(0), Some(hash));
229        assert_eq!(overrides.blob_hash_override(1), Some(B256::ZERO));
230        assert_eq!(overrides.blob_hash_override(u64::MAX), Some(B256::ZERO));
231    }
232
233    #[test]
234    fn tracks_overrides_on_every_fork() {
235        let mut state = EnvOverrideState::default();
236        assert!(state.is_empty());
237        assert!(!state.is_any_set());
238
239        state.update(None, |_| {});
240        assert!(!state.is_empty());
241        assert!(!state.is_any_set());
242
243        state.update(FORK, |o| o.gas_price = Some(1));
244        assert!(state.is_any_set());
245
246        state.remove_if_unset(None);
247        state.remove_if_unset(FORK);
248        assert!(state.get(None).is_none());
249        assert_eq!(state.get(FORK).and_then(EnvOverrides::gas_price_override), Some(1));
250
251        state.clear_implicit_basefee(None);
252        assert!(state.get(None).is_none(), "clearing must not insert an entry");
253        state.update(FORK, |o| o.implicit_basefee = Some(2));
254        state.clear_implicit_basefee(FORK);
255        assert_eq!(state.get(FORK).and_then(EnvOverrides::basefee_override), None);
256    }
257
258    #[test]
259    fn save_snapshot_records_tx_values_that_are_not_overridden() {
260        let mut state = EnvOverrideState::default();
261        let tx_hashes = [B256::repeat_byte(1)];
262
263        state.save_snapshot(SNAPSHOT, None, 10, 3, &tx_hashes);
264        let snapshot = &state.snapshots[&SNAPSHOT];
265        let active = &snapshot[&None];
266        assert!(!active.is_any_set());
267        assert_eq!(active.pre_override_gas_price, Some(10));
268        assert_eq!(active.pre_override_tx_type, Some(3));
269        assert_eq!(active.pre_override_blob_hashes.as_deref(), Some(&tx_hashes[..]));
270        assert!(state.is_empty(), "saving a snapshot must not change the live overrides");
271
272        state.update(None, |o| {
273            o.gas_price = Some(1);
274            o.blob_hashes = Some(vec![]);
275        });
276        state.update(FORK, |o| o.basefee = Some(2));
277        state.save_snapshot(SNAPSHOT, None, 10, 3, &tx_hashes);
278        let snapshot = &state.snapshots[&SNAPSHOT];
279        let active = &snapshot[&None];
280        assert_eq!(active.pre_override_gas_price, None);
281        assert_eq!(active.pre_override_tx_type, None);
282        assert_eq!(active.pre_override_blob_hashes, None);
283        assert_eq!(snapshot[&FORK].pre_override_gas_price, None, "only the active fork");
284    }
285
286    #[test]
287    fn restores_snapshots() {
288        let mut state = EnvOverrideState::default();
289        state.update(None, |o| o.basefee = Some(1));
290        state.save_snapshot(SNAPSHOT, None, 0, 0, &[]);
291
292        state.update(None, |o| o.basefee = Some(2));
293        state.restore_snapshot(SNAPSHOT, false);
294        assert_eq!(state.get(None).and_then(EnvOverrides::basefee_override), Some(1));
295
296        // A kept snapshot can be restored again.
297        state.update(None, |o| o.basefee = Some(3));
298        state.restore_snapshot(SNAPSHOT, true);
299        assert_eq!(state.get(None).and_then(EnvOverrides::basefee_override), Some(1));
300
301        // A deleted snapshot is gone, so restoring it leaves the overrides untouched.
302        state.update(None, |o| o.basefee = Some(4));
303        state.restore_snapshot(SNAPSHOT, false);
304        assert_eq!(state.get(None).and_then(EnvOverrides::basefee_override), Some(4));
305    }
306
307    #[test]
308    fn deletes_snapshots() {
309        let mut state = EnvOverrideState::default();
310        state.save_snapshot(SNAPSHOT, None, 0, 0, &[]);
311        state.save_snapshot(SNAPSHOT + U256::from(1), None, 0, 0, &[]);
312        state.update(None, |o| o.basefee = Some(1));
313
314        state.delete_snapshot(SNAPSHOT);
315        state.restore_snapshot(SNAPSHOT, false);
316        assert!(state.is_any_set());
317
318        state.clear_snapshots();
319        state.restore_snapshot(SNAPSHOT + U256::from(1), false);
320        assert!(state.is_any_set());
321    }
322}