foundry_evm_fuzz/strategies/
state.rs

1use crate::{BasicTxDetails, invariant::FuzzRunIdentifiedContracts};
2use alloy_dyn_abi::{DynSolType, DynSolValue, EventExt, FunctionExt};
3use alloy_json_abi::{Function, JsonAbi};
4use alloy_primitives::{
5    Address, B256, Bytes, Log, U256,
6    map::{AddressIndexSet, AddressMap, B256IndexSet, HashMap},
7};
8use foundry_common::{
9    ignore_metadata_hash, mapping_slots::MappingSlots, slot_identifier::SlotIdentifier,
10};
11use foundry_compilers::artifacts::StorageLayout;
12use foundry_config::FuzzDictionaryConfig;
13use foundry_evm_core::{bytecode::InstIter, utils::StateChangeset};
14use parking_lot::{RawRwLock, RwLock, lock_api::RwLockReadGuard};
15use revm::{
16    database::{CacheDB, DatabaseRef, DbAccount},
17    state::AccountInfo,
18};
19use std::{collections::BTreeMap, fmt, sync::Arc};
20
21/// The maximum number of bytes we will look at in bytecodes to find push bytes (24 KiB).
22///
23/// This is to limit the performance impact of fuzz tests that might deploy arbitrarily sized
24/// bytecode (as is the case with Solmate).
25const PUSH_BYTE_ANALYSIS_LIMIT: usize = 24 * 1024;
26
27/// A set of arbitrary 32 byte data from the VM used to generate values for the strategy.
28///
29/// Wrapped in a shareable container.
30#[derive(Clone, Debug)]
31pub struct EvmFuzzState {
32    inner: Arc<RwLock<FuzzDictionary>>,
33    /// Addresses of external libraries deployed in test setup, excluded from fuzz test inputs.
34    pub deployed_libs: Vec<Address>,
35    /// Records mapping accesses. Used to identify storage slots belonging to mappings and sampling
36    /// the values in the [`FuzzDictionary`].
37    ///
38    /// Only needed when [`StorageLayout`] is available.
39    pub(crate) mapping_slots: Option<AddressMap<MappingSlots>>,
40}
41
42impl EvmFuzzState {
43    pub fn new<DB: DatabaseRef>(
44        db: &CacheDB<DB>,
45        config: FuzzDictionaryConfig,
46        deployed_libs: &[Address],
47    ) -> Self {
48        // Sort accounts to ensure deterministic dictionary generation from the same setUp state.
49        let mut accs = db.cache.accounts.iter().collect::<Vec<_>>();
50        accs.sort_by_key(|(address, _)| *address);
51
52        // Create fuzz dictionary and insert values from db state.
53        let mut dictionary = FuzzDictionary::new(config);
54        dictionary.insert_db_values(accs);
55        Self {
56            inner: Arc::new(RwLock::new(dictionary)),
57            deployed_libs: deployed_libs.to_vec(),
58            mapping_slots: None,
59        }
60    }
61
62    pub fn with_mapping_slots(mut self, mapping_slots: AddressMap<MappingSlots>) -> Self {
63        self.mapping_slots = Some(mapping_slots);
64        self
65    }
66
67    pub fn collect_values(&self, values: impl IntoIterator<Item = B256>) {
68        let mut dict = self.inner.write();
69        for value in values {
70            dict.insert_value(value);
71        }
72    }
73
74    /// Collects state changes from a [StateChangeset] and logs into an [EvmFuzzState] according to
75    /// the given [FuzzDictionaryConfig].
76    pub fn collect_values_from_call(
77        &self,
78        fuzzed_contracts: &FuzzRunIdentifiedContracts,
79        tx: &BasicTxDetails,
80        result: &Bytes,
81        logs: &[Log],
82        state_changeset: &StateChangeset,
83        run_depth: u32,
84    ) {
85        let mut dict = self.inner.write();
86        {
87            let targets = fuzzed_contracts.targets.lock();
88            let (target_abi, target_function) = targets.fuzzed_artifacts(tx);
89            dict.insert_logs_values(target_abi, logs, run_depth);
90            dict.insert_result_values(target_function, result, run_depth);
91            // Get storage layouts for contracts in the state changeset
92            let storage_layouts = targets.get_storage_layouts();
93            dict.insert_new_state_values(
94                state_changeset,
95                &storage_layouts,
96                self.mapping_slots.as_ref(),
97            );
98        }
99    }
100
101    /// Removes all newly added entries from the dictionary.
102    ///
103    /// Should be called between fuzz/invariant runs to avoid accumulating data derived from fuzz
104    /// inputs.
105    pub fn revert(&self) {
106        self.inner.write().revert();
107    }
108
109    pub fn dictionary_read(&self) -> RwLockReadGuard<'_, RawRwLock, FuzzDictionary> {
110        self.inner.read()
111    }
112
113    /// Logs stats about the current state.
114    pub fn log_stats(&self) {
115        self.inner.read().log_stats();
116    }
117}
118
119// We're using `IndexSet` to have a stable element order when restoring persisted state, as well as
120// for performance when iterating over the sets.
121#[derive(Default)]
122pub struct FuzzDictionary {
123    /// Collected state values.
124    state_values: B256IndexSet,
125    /// Addresses that already had their PUSH bytes collected.
126    addresses: AddressIndexSet,
127    /// Configuration for the dictionary.
128    config: FuzzDictionaryConfig,
129    /// Number of state values initially collected from db.
130    /// Used to revert new collected values at the end of each run.
131    db_state_values: usize,
132    /// Number of address values initially collected from db.
133    /// Used to revert new collected addresses at the end of each run.
134    db_addresses: usize,
135    /// Sample typed values that are collected from call result and used across invariant runs.
136    sample_values: HashMap<DynSolType, B256IndexSet>,
137
138    misses: usize,
139    hits: usize,
140}
141
142impl fmt::Debug for FuzzDictionary {
143    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144        f.debug_struct("FuzzDictionary")
145            .field("state_values", &self.state_values.len())
146            .field("addresses", &self.addresses)
147            .finish()
148    }
149}
150
151impl FuzzDictionary {
152    pub fn new(config: FuzzDictionaryConfig) -> Self {
153        let mut dictionary = Self { config, ..Default::default() };
154        dictionary.prefill();
155        dictionary
156    }
157
158    /// Insert common values into the dictionary at initialization.
159    fn prefill(&mut self) {
160        self.insert_value(B256::ZERO);
161    }
162
163    /// Insert values from initial db state into fuzz dictionary.
164    /// These values are persisted across invariant runs.
165    fn insert_db_values(&mut self, db_state: Vec<(&Address, &DbAccount)>) {
166        for (address, account) in db_state {
167            // Insert basic account information
168            self.insert_value(address.into_word());
169            // Insert push bytes
170            self.insert_push_bytes_values(address, &account.info);
171            // Insert storage values.
172            if self.config.include_storage {
173                // Sort storage values before inserting to ensure deterministic dictionary.
174                let values = account.storage.iter().collect::<BTreeMap<_, _>>();
175                for (slot, value) in values {
176                    self.insert_storage_value(slot, value, None, None);
177                }
178            }
179        }
180
181        // We need at least some state data if DB is empty,
182        // otherwise we can't select random data for state fuzzing.
183        if self.values().is_empty() {
184            // Prefill with a random address.
185            self.insert_value(Address::random().into_word());
186        }
187
188        // Record number of values and addresses inserted from db to be used for reverting at the
189        // end of each run.
190        self.db_state_values = self.state_values.len();
191        self.db_addresses = self.addresses.len();
192    }
193
194    /// Insert values collected from call result into fuzz dictionary.
195    fn insert_result_values(
196        &mut self,
197        function: Option<&Function>,
198        result: &Bytes,
199        run_depth: u32,
200    ) {
201        if let Some(function) = function
202            && !function.outputs.is_empty()
203        {
204            // Decode result and collect samples to be used in subsequent fuzz runs.
205            if let Ok(decoded_result) = function.abi_decode_output(result) {
206                self.insert_sample_values(decoded_result, run_depth);
207            }
208        }
209    }
210
211    /// Insert values from call log topics and data into fuzz dictionary.
212    fn insert_logs_values(&mut self, abi: Option<&JsonAbi>, logs: &[Log], run_depth: u32) {
213        let mut samples = Vec::new();
214        // Decode logs with known events and collect samples from indexed fields and event body.
215        for log in logs {
216            let mut log_decoded = false;
217            // Try to decode log with events from contract abi.
218            if let Some(abi) = abi {
219                for event in abi.events() {
220                    if let Ok(decoded_event) = event.decode_log(log) {
221                        samples.extend(decoded_event.indexed);
222                        samples.extend(decoded_event.body);
223                        log_decoded = true;
224                        break;
225                    }
226                }
227            }
228
229            // If we weren't able to decode event then we insert raw data in fuzz dictionary.
230            if !log_decoded {
231                for &topic in log.topics() {
232                    self.insert_value(topic);
233                }
234                let chunks = log.data.data.chunks_exact(32);
235                let rem = chunks.remainder();
236                for chunk in chunks {
237                    self.insert_value(chunk.try_into().unwrap());
238                }
239                if !rem.is_empty() {
240                    self.insert_value(B256::right_padding_from(rem));
241                }
242            }
243        }
244
245        // Insert samples collected from current call in fuzz dictionary.
246        self.insert_sample_values(samples, run_depth);
247    }
248
249    /// Insert values from call state changeset into fuzz dictionary.
250    /// These values are removed at the end of current run.
251    fn insert_new_state_values(
252        &mut self,
253        state_changeset: &StateChangeset,
254        storage_layouts: &HashMap<Address, Arc<StorageLayout>>,
255        mapping_slots: Option<&AddressMap<MappingSlots>>,
256    ) {
257        for (address, account) in state_changeset {
258            // Insert basic account information.
259            self.insert_value(address.into_word());
260            // Insert push bytes.
261            self.insert_push_bytes_values(address, &account.info);
262            // Insert storage values.
263            if self.config.include_storage {
264                let storage_layout = storage_layouts.get(address).cloned();
265                trace!(
266                    "{address:?} has mapping_slots {}",
267                    mapping_slots.is_some_and(|m| m.contains_key(address))
268                );
269                let mapping_slots = mapping_slots.and_then(|m| m.get(address));
270                for (slot, value) in &account.storage {
271                    self.insert_storage_value(
272                        slot,
273                        &value.present_value,
274                        storage_layout.as_deref(),
275                        mapping_slots,
276                    );
277                }
278            }
279        }
280    }
281
282    /// Insert values from push bytes into fuzz dictionary.
283    /// Values are collected only once for a given address.
284    /// If values are newly collected then they are removed at the end of current run.
285    fn insert_push_bytes_values(&mut self, address: &Address, account_info: &AccountInfo) {
286        if self.config.include_push_bytes
287            && !self.addresses.contains(address)
288            && let Some(code) = &account_info.code
289        {
290            self.insert_address(*address);
291            if !self.values_full() {
292                self.collect_push_bytes(ignore_metadata_hash(code.original_byte_slice()));
293            }
294        }
295    }
296
297    fn collect_push_bytes(&mut self, code: &[u8]) {
298        let len = code.len().min(PUSH_BYTE_ANALYSIS_LIMIT);
299        let code = &code[..len];
300        for inst in InstIter::new(code) {
301            // Don't add 0 to the dictionary as it's already present.
302            if !inst.immediate.is_empty()
303                && let Some(push_value) = U256::try_from_be_slice(inst.immediate)
304                && push_value != U256::ZERO
305            {
306                self.insert_value_u256(push_value);
307            }
308        }
309    }
310
311    /// Insert values from single storage slot and storage value into fuzz dictionary.
312    /// Uses [`SlotIdentifier`] to identify storage slots types.
313    fn insert_storage_value(
314        &mut self,
315        slot: &U256,
316        value: &U256,
317        layout: Option<&StorageLayout>,
318        mapping_slots: Option<&MappingSlots>,
319    ) {
320        let slot = B256::from(*slot);
321        let value = B256::from(*value);
322
323        // Always insert the slot itself
324        self.insert_value(slot);
325
326        // If we have a storage layout, use SlotIdentifier for better type identification
327        if let Some(slot_identifier) =
328            layout.map(|l| SlotIdentifier::new(l.clone().into()))
329            // Identify Slot Type
330            && let Some(slot_info) = slot_identifier.identify(&slot, mapping_slots) && slot_info.decode(value).is_some()
331        {
332            trace!(?slot_info, "inserting typed storage value");
333            self.sample_values.entry(slot_info.slot_type.dyn_sol_type).or_default().insert(value);
334        } else {
335            self.insert_value_u256(value.into());
336        }
337    }
338
339    /// Insert address into fuzz dictionary.
340    /// If address is newly collected then it is removed by index at the end of current run.
341    fn insert_address(&mut self, address: Address) {
342        if self.addresses.len() < self.config.max_fuzz_dictionary_addresses {
343            self.addresses.insert(address);
344        }
345    }
346
347    /// Insert raw value into fuzz dictionary.
348    ///
349    /// If value is newly collected then it is removed by index at the end of current run.
350    ///
351    /// Returns true if the value was inserted.
352    fn insert_value(&mut self, value: B256) -> bool {
353        let insert = !self.values_full();
354        if insert {
355            let new_value = self.state_values.insert(value);
356            let counter = if new_value { &mut self.misses } else { &mut self.hits };
357            *counter += 1;
358        }
359        insert
360    }
361
362    fn insert_value_u256(&mut self, value: U256) -> bool {
363        // Also add the value below and above the push value to the dictionary.
364        let one = U256::from(1);
365        self.insert_value(value.into())
366            | self.insert_value((value.wrapping_sub(one)).into())
367            | self.insert_value((value.wrapping_add(one)).into())
368    }
369
370    fn values_full(&self) -> bool {
371        self.state_values.len() >= self.config.max_fuzz_dictionary_values
372    }
373
374    /// Insert sample values that are reused across multiple runs.
375    /// The number of samples is limited to invariant run depth.
376    /// If collected samples limit is reached then values are inserted as regular values.
377    pub fn insert_sample_values(
378        &mut self,
379        sample_values: impl IntoIterator<Item = DynSolValue>,
380        limit: u32,
381    ) {
382        for sample in sample_values {
383            if let (Some(sample_type), Some(sample_value)) = (sample.as_type(), sample.as_word()) {
384                if let Some(values) = self.sample_values.get_mut(&sample_type) {
385                    if values.len() < limit as usize {
386                        values.insert(sample_value);
387                    } else {
388                        // Insert as state value (will be removed at the end of the run).
389                        self.insert_value(sample_value);
390                    }
391                } else {
392                    self.sample_values.entry(sample_type).or_default().insert(sample_value);
393                }
394            }
395        }
396    }
397
398    pub fn values(&self) -> &B256IndexSet {
399        &self.state_values
400    }
401
402    pub fn len(&self) -> usize {
403        self.state_values.len()
404    }
405
406    pub fn is_empty(&self) -> bool {
407        self.state_values.is_empty()
408    }
409
410    #[inline]
411    pub fn samples(&self, param_type: &DynSolType) -> Option<&B256IndexSet> {
412        self.sample_values.get(param_type)
413    }
414
415    #[inline]
416    pub fn addresses(&self) -> &AddressIndexSet {
417        &self.addresses
418    }
419
420    /// Revert values and addresses collected during the run by truncating to initial db len.
421    pub fn revert(&mut self) {
422        self.state_values.truncate(self.db_state_values);
423        self.addresses.truncate(self.db_addresses);
424    }
425
426    pub fn log_stats(&self) {
427        trace!(
428            addresses.len = self.addresses.len(),
429            sample.len = self.sample_values.len(),
430            state.len = self.state_values.len(),
431            state.misses = self.misses,
432            state.hits = self.hits,
433            "FuzzDictionary stats",
434        );
435    }
436}