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