foundry_evm_fuzz/strategies/
state.rs

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