Skip to main content

foundry_evm_fuzz/strategies/
state.rs

1use crate::{
2    BasicTxDetails, Fuzzer,
3    invariant::{
4        FuzzRunIdentifiedContracts, TargetedContract, TargetedContractEvent, TargetedContracts,
5    },
6    strategies::literals::LiteralsDictionary,
7};
8use alloy_dyn_abi::{DynSolType, DynSolValue, EventExt, FunctionExt};
9use alloy_json_abi::Function;
10use alloy_primitives::{
11    Address, B256, Bytes, Log, U256,
12    map::{AddressIndexSet, AddressMap, B256IndexSet, HashMap, HashSet, IndexSet},
13};
14use foundry_common::{
15    ignore_metadata_hash,
16    mapping_slots::MappingSlots,
17    slot_identifier::{SlotIdentifier, SlotInfo},
18};
19use foundry_config::FuzzDictionaryConfig;
20use foundry_evm_core::{
21    bytecode::InstIter, eip2935::is_history_storage_address, utils::StateChangeset,
22};
23use revm::{
24    database::{CacheDB, DatabaseRef, DbAccount},
25    state::AccountInfo,
26};
27use std::{cell::RefCell, fmt, rc::Rc, sync::Arc};
28
29#[cfg(test)]
30use revm::database::InMemoryDB;
31
32/// The maximum number of bytes we will look at in bytecodes to find push bytes (24 KiB).
33///
34/// This is to limit the performance impact of fuzz tests that might deploy arbitrarily sized
35/// bytecode (as is the case with Solmate).
36const PUSH_BYTE_ANALYSIS_LIMIT: usize = 24 * 1024;
37
38/// Immutable fuzz dictionary seed used by parallel stateless fuzz workers.
39#[derive(Clone, Debug)]
40pub struct EvmFuzzState {
41    inner: Arc<FuzzDictionary>,
42    /// Addresses of external libraries deployed in test setup, excluded from fuzz test inputs.
43    pub deployed_libs: Vec<Address>,
44}
45
46/// Worker-local fuzz state.
47///
48/// Stateless workers share the immutable campaign seed, while invariant workers own an isolated
49/// mutable dictionary. The latter deliberately uses `Rc<RefCell<_>>`: worker state is not shared
50/// across threads.
51#[derive(Clone, Debug)]
52pub struct FuzzState {
53    inner: FuzzStateInner,
54    /// Addresses of external libraries deployed in test setup, excluded from fuzz test inputs.
55    pub deployed_libs: Vec<Address>,
56}
57
58#[derive(Clone, Debug)]
59enum FuzzStateInner {
60    Stateless(Arc<FuzzDictionary>),
61    Invariant(Rc<RefCell<FuzzDictionary>>),
62}
63
64pub(crate) trait DictionaryRead: Clone + 'static {
65    fn deployed_libs(&self) -> &[Address];
66    fn with_dictionary<R>(&self, f: impl FnOnce(&FuzzDictionary) -> R) -> R;
67}
68
69impl EvmFuzzState {
70    #[cfg(test)]
71    pub(crate) fn test() -> Self {
72        Self::new(&[], &InMemoryDB::default(), FuzzDictionaryConfig::default(), None)
73    }
74
75    pub fn new<DB: DatabaseRef>(
76        deployed_libs: &[Address],
77        db: &CacheDB<DB>,
78        config: FuzzDictionaryConfig,
79        literals: Option<&LiteralsDictionary>,
80    ) -> Self {
81        // Sort accounts to ensure deterministic dictionary generation from the same setUp state.
82        let mut accs = db.cache.accounts.iter().collect::<Vec<_>>();
83        accs.sort_by_key(|(address, _)| *address);
84
85        // Create fuzz dictionary and insert values from db state.
86        let mut dictionary = FuzzDictionary::new(config);
87        dictionary.insert_db_values(accs);
88        if let Some(literals) = literals {
89            dictionary.literal_values = literals.clone();
90        }
91
92        Self { inner: Arc::new(dictionary), deployed_libs: deployed_libs.to_vec() }
93    }
94
95    pub fn stateless_worker(&self) -> FuzzState {
96        FuzzState {
97            inner: FuzzStateInner::Stateless(Arc::clone(&self.inner)),
98            deployed_libs: self.deployed_libs.clone(),
99        }
100    }
101
102    pub fn into_invariant(self) -> FuzzState {
103        FuzzState {
104            inner: FuzzStateInner::Invariant(Rc::new(RefCell::new((*self.inner).clone()))),
105            deployed_libs: self.deployed_libs,
106        }
107    }
108
109    pub fn fork(&self) -> Self {
110        Self { inner: Arc::clone(&self.inner), deployed_libs: self.deployed_libs.clone() }
111    }
112
113    pub fn collect_values(&mut self, values: impl IntoIterator<Item = B256>) {
114        let dict = Arc::make_mut(&mut self.inner);
115        for value in values {
116            dict.insert_value(value);
117        }
118    }
119
120    /// Logs stats about the current state.
121    pub fn log_stats(&self) {
122        self.inner.log_stats();
123    }
124
125    /// Test-only helper to seed the dictionary with literal values.
126    #[cfg(test)]
127    pub(crate) fn seed_literals(&mut self, map: super::LiteralMaps) {
128        Arc::make_mut(&mut self.inner).seed_literals(map);
129    }
130}
131
132impl FuzzState {
133    pub fn snapshot(&self) -> EvmFuzzState {
134        EvmFuzzState {
135            inner: Arc::new(self.with_dictionary(Clone::clone)),
136            deployed_libs: self.deployed_libs.clone(),
137        }
138    }
139
140    pub fn collect_values(&self, values: impl IntoIterator<Item = B256>) {
141        let FuzzStateInner::Invariant(inner) = &self.inner else { return };
142        let mut dict = inner.borrow_mut();
143        for value in values {
144            dict.insert_value(value);
145        }
146    }
147
148    pub fn collect_fuzzer_values(&self, fuzzer: &mut Fuzzer) {
149        if fuzzer.collected_values.is_empty() {
150            return;
151        }
152
153        let FuzzStateInner::Invariant(inner) = &self.inner else { return };
154        let mut dict = inner.borrow_mut();
155        for value in fuzzer.collected_values.drain(..) {
156            dict.insert_value(value);
157        }
158    }
159
160    /// Collects state changes from a [StateChangeset] and logs into a worker state
161    /// according to the given [FuzzDictionaryConfig].
162    #[allow(clippy::too_many_arguments)]
163    pub fn collect_values_from_call(
164        &self,
165        fuzzed_contracts: &FuzzRunIdentifiedContracts,
166        tx: &BasicTxDetails,
167        result: &Bytes,
168        logs: &[Log],
169        state_changeset: &StateChangeset,
170        run_depth: u32,
171        mapping_slots: Option<&AddressMap<MappingSlots>>,
172    ) {
173        if logs.is_empty() && result.is_empty() && state_changeset.is_empty() {
174            return;
175        }
176
177        let FuzzStateInner::Invariant(inner) = &self.inner else { return };
178        let mut dict = inner.borrow_mut();
179        let targets = fuzzed_contracts.targets();
180        let (target_contract, target_function) = if logs.is_empty() && result.is_empty() {
181            (None, None)
182        } else {
183            targets.fuzzed_artifacts(tx)
184        };
185        if !logs.is_empty() {
186            dict.insert_logs_values(target_contract, logs, run_depth);
187        }
188        if !result.is_empty() {
189            dict.insert_result_values(target_function, result, run_depth);
190        }
191        dict.insert_new_state_values(state_changeset, &targets, mapping_slots);
192    }
193
194    /// Collects typed trace-cmp operands from sancov-instrumented code.
195    /// Values are inserted into both persistent state values (survive reverts) and typed
196    /// sample buckets (for ABI-aware mutation).
197    pub fn collect_typed_cmp_values(&self, values: impl IntoIterator<Item = (u8, B256)>) {
198        let FuzzStateInner::Invariant(inner) = &self.inner else { return };
199        let mut dict = inner.borrow_mut();
200        for (width, value) in values {
201            dict.insert_persistent_value(value);
202            dict.insert_typed_cmp_value(width, value);
203        }
204    }
205
206    /// Removes all newly added entries from the dictionary.
207    ///
208    /// Should be called between fuzz/invariant runs to avoid accumulating data derived from fuzz
209    /// inputs.
210    pub fn revert(&self) {
211        if let FuzzStateInner::Invariant(inner) = &self.inner {
212            inner.borrow_mut().revert();
213        }
214    }
215
216    /// Logs stats about the current state.
217    pub fn log_stats(&self) {
218        self.with_dictionary(FuzzDictionary::log_stats);
219    }
220
221    pub fn deployed_libs(&self) -> &[Address] {
222        &self.deployed_libs
223    }
224
225    pub fn with_dictionary<R>(&self, f: impl FnOnce(&FuzzDictionary) -> R) -> R {
226        match &self.inner {
227            FuzzStateInner::Stateless(inner) => f(inner),
228            FuzzStateInner::Invariant(inner) => f(&inner.borrow()),
229        }
230    }
231}
232
233impl DictionaryRead for FuzzState {
234    fn deployed_libs(&self) -> &[Address] {
235        self.deployed_libs()
236    }
237
238    fn with_dictionary<R>(&self, f: impl FnOnce(&FuzzDictionary) -> R) -> R {
239        self.with_dictionary(f)
240    }
241}
242
243impl DictionaryRead for EvmFuzzState {
244    fn deployed_libs(&self) -> &[Address] {
245        &self.deployed_libs
246    }
247
248    fn with_dictionary<R>(&self, f: impl FnOnce(&FuzzDictionary) -> R) -> R {
249        f(&self.inner)
250    }
251}
252
253impl From<EvmFuzzState> for FuzzState {
254    fn from(state: EvmFuzzState) -> Self {
255        state.into_invariant()
256    }
257}
258
259// We're using `IndexSet` to have a stable element order when restoring persisted state, as well as
260// for performance when iterating over the sets.
261/// Maximum number of persistent values from sancov trace-cmp.
262const MAX_PERSISTENT_VALUES: usize = 2048;
263/// Maximum cached storage slot layout lookups per fuzz dictionary.
264const MAX_SLOT_INFO_CACHE_ENTRIES: usize = 4096;
265
266#[derive(Clone)]
267pub struct FuzzDictionary {
268    /// Collected state values.
269    state_values: B256IndexSet,
270    /// Addresses that already had their PUSH bytes collected.
271    addresses: AddressIndexSet,
272    /// Code hashes that already had their PUSH bytes collected.
273    push_bytecode_hashes: B256IndexSet,
274    /// Configuration for the dictionary.
275    config: FuzzDictionaryConfig,
276    /// Number of state values initially collected from db.
277    /// Used to revert new collected values at the end of each run.
278    db_state_values: usize,
279    /// Number of address values initially collected from db.
280    /// Used to revert new collected addresses at the end of each run.
281    db_addresses: usize,
282    /// Number of bytecode hashes initially collected from db.
283    /// Used to revert new collected bytecode hashes at the end of each run.
284    db_push_bytecode_hashes: usize,
285    /// Typed runtime sample values persisted across invariant runs.
286    /// Initially seeded with literal values collected from the source code.
287    sample_values: HashMap<DynSolType, B256IndexSet>,
288    /// Lazily initialized dictionary of literal values collected from the source code.
289    literal_values: LiteralsDictionary,
290    /// Tracks whether literals from `literal_values` have been merged into `sample_values`.
291    ///
292    /// Set to `true` on first call to `seed_samples()`. Before seeding, `samples()` checks both
293    /// maps separately. After seeding, literals are merged in, so only `sample_values` is checked.
294    samples_seeded: bool,
295    /// Persistent values from sancov trace-cmp that survive `revert()` across runs.
296    persistent_values: B256IndexSet,
297    /// Parsed storage layout identifiers keyed by the layout allocation.
298    slot_identifiers: HashMap<usize, SlotIdentifier>,
299    /// Cached non-mapping storage slot identification keyed by layout allocation and slot.
300    slot_info_cache: HashMap<(usize, B256), Option<SlotInfo>>,
301
302    misses: usize,
303    hits: usize,
304}
305
306impl fmt::Debug for FuzzDictionary {
307    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
308        f.debug_struct("FuzzDictionary")
309            .field("state_values", &self.state_values.len())
310            .field("addresses", &self.addresses)
311            .field("persistent_values", &self.persistent_values.len())
312            .finish()
313    }
314}
315
316impl Default for FuzzDictionary {
317    fn default() -> Self {
318        Self::new(Default::default())
319    }
320}
321
322impl FuzzDictionary {
323    pub fn new(mut config: FuzzDictionaryConfig) -> Self {
324        config.max_fuzz_dictionary_values = config.max_fuzz_dictionary_values.max(1);
325        let mut dictionary = Self {
326            config,
327            samples_seeded: false,
328
329            state_values: Default::default(),
330            addresses: Default::default(),
331            push_bytecode_hashes: Default::default(),
332            db_state_values: Default::default(),
333            db_addresses: Default::default(),
334            db_push_bytecode_hashes: Default::default(),
335            sample_values: Default::default(),
336            literal_values: Default::default(),
337            persistent_values: Default::default(),
338            slot_identifiers: Default::default(),
339            slot_info_cache: Default::default(),
340            misses: Default::default(),
341            hits: Default::default(),
342        };
343        // Zero is a useful default seed even before state or literals populate the dictionary.
344        dictionary.insert_value(B256::ZERO);
345        dictionary
346    }
347
348    /// Seeds `sample_values` with all words from the [`LiteralsDictionary`].
349    /// Should only be called once per dictionary lifetime.
350    #[cold]
351    fn seed_samples(&mut self) {
352        trace!("seeding `sample_values` from literal dictionary");
353        self.sample_values
354            .extend(self.literal_values.get().words.iter().map(|(k, v)| (k.clone(), v.clone())));
355        self.samples_seeded = true;
356    }
357
358    /// Insert values from initial db state into fuzz dictionary.
359    /// These values are persisted across invariant runs.
360    fn insert_db_values(&mut self, db_state: Vec<(&Address, &DbAccount)>) {
361        for (address, account) in db_state {
362            if is_history_storage_address(address) {
363                continue;
364            }
365
366            // Insert basic account information
367            self.insert_value(address.into_word());
368            // Insert push bytes
369            self.insert_push_bytes_values(address, &account.info);
370            // Insert storage values.
371            if self.config.include_storage {
372                // Sort storage values before inserting to ensure deterministic dictionary.
373                let mut values = account.storage.iter().collect::<Vec<_>>();
374                values.sort_unstable_by_key(|(slot, _)| **slot);
375                for (slot, value) in values {
376                    self.insert_storage_value(slot, value, None);
377                }
378            }
379        }
380
381        // We need at least some state data if DB is empty,
382        // otherwise we can't select random data for state fuzzing.
383        if self.values().is_empty() {
384            // Prefill with a random address.
385            self.insert_value(Address::random().into_word());
386        }
387
388        // Record number of values and addresses inserted from db to be used for reverting at the
389        // end of each run.
390        self.db_state_values = self.state_values.len();
391        self.db_addresses = self.addresses.len();
392        self.db_push_bytecode_hashes = self.push_bytecode_hashes.len();
393    }
394
395    /// Insert values collected from call result into fuzz dictionary.
396    fn insert_result_values(
397        &mut self,
398        function: Option<&Function>,
399        result: &Bytes,
400        run_depth: u32,
401    ) {
402        if let Some(function) = function
403            && !function.outputs.is_empty()
404        {
405            // Decode result and collect samples to be used in subsequent fuzz runs.
406            if let Ok(decoded_result) = function.abi_decode_output(result) {
407                self.insert_sample_values(decoded_result, run_depth);
408            }
409        }
410    }
411
412    /// Insert values from call log topics and data into fuzz dictionary.
413    fn insert_logs_values(
414        &mut self,
415        target_contract: Option<&TargetedContract>,
416        logs: &[Log],
417        run_depth: u32,
418    ) {
419        let mut samples = Vec::new();
420        // Decode logs with known events and collect samples from indexed fields and event body.
421        for log in logs {
422            // Try to decode log with events from contract abi.
423            let log_decoded = if let Some(contract) = target_contract {
424                let matched_events = log
425                    .topics()
426                    .first()
427                    .and_then(|selector| {
428                        contract.event_lookup.by_topic(selector, log.topics().len() - 1)
429                    })
430                    .unwrap_or(&[]);
431                Self::decode_log_events(
432                    matched_events,
433                    contract.event_lookup.anonymous(),
434                    log,
435                    &mut samples,
436                )
437            } else {
438                false
439            };
440
441            // If we weren't able to decode event then we insert raw data in fuzz dictionary.
442            if !log_decoded {
443                for &topic in log.topics() {
444                    self.insert_value(topic);
445                }
446                let (chunks, rem) = log.data.data.as_chunks::<32>();
447                for chunk in chunks {
448                    self.insert_value((*chunk).into());
449                }
450                if !rem.is_empty() {
451                    self.insert_value(B256::right_padding_from(rem));
452                }
453            }
454        }
455
456        // Insert samples collected from current call in fuzz dictionary.
457        if !samples.is_empty() {
458            self.insert_sample_values(samples, run_depth);
459        }
460    }
461
462    fn decode_log_events(
463        matched_events: &[TargetedContractEvent],
464        anonymous_events: &[TargetedContractEvent],
465        log: &Log,
466        samples: &mut Vec<DynSolValue>,
467    ) -> bool {
468        let mut matched = matched_events.iter().peekable();
469        let mut anonymous = anonymous_events.iter().peekable();
470        while matched.peek().is_some() || anonymous.peek().is_some() {
471            let event = match (matched.peek(), anonymous.peek()) {
472                (Some(matched_event), Some(anonymous_event)) => {
473                    if matched_event.order() < anonymous_event.order() {
474                        matched.next().unwrap()
475                    } else {
476                        anonymous.next().unwrap()
477                    }
478                }
479                (Some(_), None) => matched.next().unwrap(),
480                (None, Some(_)) => anonymous.next().unwrap(),
481                (None, None) => unreachable!(),
482            };
483            if let Ok(decoded_event) = event.event().decode_log(log) {
484                samples.extend(decoded_event.indexed);
485                samples.extend(decoded_event.body);
486                return true;
487            }
488        }
489        false
490    }
491
492    /// Insert values from call state changeset into fuzz dictionary.
493    /// These values are removed at the end of current run.
494    fn insert_new_state_values(
495        &mut self,
496        state_changeset: &StateChangeset,
497        targets: &TargetedContracts,
498        mapping_slots: Option<&AddressMap<MappingSlots>>,
499    ) {
500        for (address, account) in state_changeset {
501            if is_history_storage_address(address) {
502                continue;
503            }
504
505            // Insert basic account information.
506            self.insert_value(address.into_word());
507            // Insert push bytes.
508            self.insert_push_bytes_values(address, &account.info);
509            // Insert storage values.
510            if self.config.include_storage && !account.storage.is_empty() {
511                let slot_identifier_key = targets.get(address).and_then(|contract| {
512                    contract.storage_layout.as_ref().map(|layout| {
513                        let key = Arc::as_ptr(layout) as usize;
514                        self.slot_identifiers
515                            .entry(key)
516                            .or_insert_with(|| SlotIdentifier::new(Arc::clone(layout)));
517                        key
518                    })
519                });
520                trace!(
521                    "{address:?} has mapping_slots {}",
522                    mapping_slots.is_some_and(|m| m.contains_key(address))
523                );
524                let mapping_slots = mapping_slots.and_then(|m| m.get(address));
525                for (slot, value) in &account.storage {
526                    let slot_info = slot_identifier_key.and_then(|key| {
527                        let slot = B256::from(*slot);
528                        let value_word = B256::from(value.present_value());
529                        self.identify_storage_slot(key, slot, mapping_slots)
530                            .filter(|slot_info| slot_info.decode(value_word).is_some())
531                    });
532                    self.insert_storage_value(slot, &value.present_value(), slot_info);
533                }
534            }
535        }
536    }
537
538    fn identify_storage_slot(
539        &mut self,
540        key: usize,
541        slot: B256,
542        mapping_slots: Option<&MappingSlots>,
543    ) -> Option<SlotInfo> {
544        if mapping_slots.is_some() {
545            return self
546                .slot_identifiers
547                .get(&key)
548                .and_then(|identifier| identifier.identify(&slot, mapping_slots));
549        }
550
551        let cache_key = (key, slot);
552        if let Some(slot_info) = self.slot_info_cache.get(&cache_key) {
553            return slot_info.clone();
554        }
555
556        let slot_info =
557            self.slot_identifiers.get(&key).and_then(|identifier| identifier.identify(&slot, None));
558        if self.slot_info_cache.len() < MAX_SLOT_INFO_CACHE_ENTRIES {
559            self.slot_info_cache.insert(cache_key, slot_info.clone());
560        }
561        slot_info
562    }
563
564    /// Insert values from push bytes into fuzz dictionary.
565    /// Values are collected only once for a given bytecode.
566    /// If values are newly collected then they are removed at the end of current run.
567    fn insert_push_bytes_values(&mut self, address: &Address, account_info: &AccountInfo) {
568        if !self.config.include_push_bytes {
569            return;
570        }
571
572        let Some(code) = &account_info.code else {
573            return;
574        };
575        self.insert_address(*address);
576        if self.values_full() {
577            return;
578        }
579        if self.push_bytecode_hashes.insert(account_info.code_hash()) {
580            self.collect_push_bytes(ignore_metadata_hash(code.original_byte_slice()));
581        }
582    }
583
584    fn collect_push_bytes(&mut self, code: &[u8]) {
585        let len = code.len().min(PUSH_BYTE_ANALYSIS_LIMIT);
586        let code = &code[..len];
587        let mut seen = HashSet::default();
588        for inst in InstIter::new(code) {
589            if self.values_full() {
590                break;
591            }
592            // Don't add 0 to the dictionary as it's already present.
593            if !inst.immediate.is_empty()
594                && let Some(push_value) = U256::try_from_be_slice(inst.immediate)
595                && !push_value.is_zero()
596            {
597                self.insert_push_value_u256(push_value, &mut seen);
598            }
599        }
600    }
601
602    /// Insert values from single storage slot and storage value into fuzz dictionary.
603    /// Uses [`SlotIdentifier`] to identify storage slots types.
604    fn insert_storage_value(&mut self, slot: &U256, value: &U256, slot_info: Option<SlotInfo>) {
605        let slot = B256::from(*slot);
606        let value_word = B256::from(*value);
607
608        // Always insert the slot itself
609        self.insert_value(slot);
610
611        if let Some(slot_info) = slot_info {
612            trace!(?slot_info, "inserting typed storage value");
613            if !self.samples_seeded {
614                self.seed_samples();
615            }
616            self.sample_values
617                .entry(slot_info.slot_type.dyn_sol_type)
618                .or_default()
619                .insert(value_word);
620        } else {
621            self.insert_value_u256(*value);
622        }
623    }
624
625    /// Insert address into fuzz dictionary.
626    /// If address is newly collected then it is removed by index at the end of current run.
627    fn insert_address(&mut self, address: Address) {
628        if self.addresses.len() < self.config.max_fuzz_dictionary_addresses {
629            self.addresses.insert(address);
630        }
631    }
632
633    /// Insert raw value into fuzz dictionary.
634    ///
635    /// If value is newly collected then it is removed by index at the end of current run.
636    ///
637    /// Returns true if the value was inserted.
638    fn insert_value(&mut self, value: B256) -> bool {
639        let insert = !self.values_full();
640        if insert {
641            let new_value = self.state_values.insert(value);
642            let counter = if new_value { &mut self.misses } else { &mut self.hits };
643            *counter += 1;
644        }
645        insert
646    }
647
648    /// Insert a persistent value that survives `revert()` across invariant runs.
649    /// Used for trace-cmp operands that should compound over time.
650    fn insert_persistent_value(&mut self, value: B256) {
651        if self.persistent_values.len() >= MAX_PERSISTENT_VALUES {
652            return;
653        }
654        if !self.persistent_values.insert(value) {
655            return;
656        }
657        // `revert()` truncates `state_values` down to the first `db_state_values` entries, so the
658        // value must be placed inside that prefix; a plain `insert` appends past it and would be
659        // truncated at the end of the run.
660        match self.state_values.get_index_of(&value) {
661            // Already inside the persisted prefix.
662            Some(index) if index < self.db_state_values => {}
663            // Collected as an ephemeral value earlier in this run: move it into the prefix.
664            Some(index) => {
665                self.state_values.move_index(index, self.db_state_values);
666                self.db_state_values += 1;
667            }
668            None => {
669                self.state_values.shift_insert(self.db_state_values, value);
670                self.db_state_values += 1;
671            }
672        }
673    }
674
675    /// Insert a typed trace-cmp value into the `sample_values` map.
676    /// Maps sancov width to `DynSolType` buckets and promotes to larger types.
677    fn insert_typed_cmp_value(&mut self, width: u8, value: B256) {
678        if !self.samples_seeded {
679            self.seed_samples();
680        }
681
682        const MAX_TYPED_CMP_PER_BUCKET: usize = 1024;
683
684        let native_type = match width {
685            8 => DynSolType::Uint(8),
686            16 => DynSolType::Uint(16),
687            32 => DynSolType::Uint(32),
688            64 => DynSolType::Uint(64),
689            _ => DynSolType::Uint(256),
690        };
691
692        let insert = |map: &mut HashMap<DynSolType, B256IndexSet>, ty: DynSolType, val: B256| {
693            let bucket = map.entry(ty).or_default();
694            if bucket.len() < MAX_TYPED_CMP_PER_BUCKET {
695                bucket.insert(val);
696            }
697        };
698
699        insert(&mut self.sample_values, native_type, value);
700
701        if width <= 64 {
702            insert(&mut self.sample_values, DynSolType::Uint(128), value);
703            insert(&mut self.sample_values, DynSolType::Uint(256), value);
704            insert(&mut self.sample_values, DynSolType::Int(256), value);
705        }
706    }
707
708    fn insert_value_u256(&mut self, value: U256) -> bool {
709        // Also add the value below and above the push value to the dictionary.
710        let one = U256::ONE;
711        let mut inserted = self.insert_value(value.into());
712        if !self.values_full() {
713            inserted |= self.insert_value((value.wrapping_sub(one)).into());
714        }
715        if !self.values_full() {
716            inserted |= self.insert_value((value.wrapping_add(one)).into());
717        }
718        inserted
719    }
720
721    fn insert_push_value_u256(&mut self, value: U256, seen: &mut HashSet<B256>) -> bool {
722        // Also add the value below and above the push value to the dictionary.
723        let one = U256::ONE;
724        let mut inserted = false;
725        for value in [value, value.wrapping_sub(one), value.wrapping_add(one)] {
726            if self.values_full() {
727                break;
728            }
729            let value = value.into();
730            if seen.insert(value) {
731                inserted |= self.insert_value(value);
732            }
733        }
734        inserted
735    }
736
737    fn values_full(&self) -> bool {
738        self.state_values.len() >= self.config.max_fuzz_dictionary_values
739    }
740
741    /// Insert sample values that are reused across multiple runs.
742    /// The number of samples is limited to invariant run depth.
743    /// If collected samples limit is reached then values are inserted as regular values.
744    pub fn insert_sample_values(
745        &mut self,
746        sample_values: impl IntoIterator<Item = DynSolValue>,
747        limit: u32,
748    ) {
749        if !self.samples_seeded {
750            self.seed_samples();
751        }
752        for sample in sample_values {
753            if let (Some(sample_type), Some(sample_value)) = (sample.as_type(), sample.as_word()) {
754                if let Some(values) = self.sample_values.get_mut(&sample_type) {
755                    if values.len() < limit as usize {
756                        values.insert(sample_value);
757                    } else {
758                        // Insert as state value (will be removed at the end of the run).
759                        self.insert_value(sample_value);
760                    }
761                } else {
762                    self.sample_values.entry(sample_type).or_default().insert(sample_value);
763                }
764            }
765        }
766    }
767
768    pub const fn values(&self) -> &B256IndexSet {
769        &self.state_values
770    }
771
772    pub fn len(&self) -> usize {
773        self.state_values.len()
774    }
775
776    pub fn is_empty(&self) -> bool {
777        self.state_values.is_empty()
778    }
779
780    /// Returns sample values for a given type, checking both runtime samples and literals.
781    ///
782    /// Before `seed_samples()` is called, checks both `literal_values` and `sample_values`
783    /// separately. After seeding, all literal values are merged into `sample_values`.
784    #[inline]
785    pub fn samples(&self, param_type: &DynSolType) -> Option<&B256IndexSet> {
786        // If not seeded yet, return literals
787        if !self.samples_seeded {
788            return self.literal_values.get().words.get(param_type);
789        }
790
791        self.sample_values.get(param_type)
792    }
793
794    /// Returns the collected literal strings, triggering initialization if needed.
795    #[inline]
796    pub fn ast_strings(&self) -> &IndexSet<String> {
797        &self.literal_values.get().strings
798    }
799
800    /// Returns the collected literal bytes (hex strings), triggering initialization if needed.
801    #[inline]
802    pub fn ast_bytes(&self) -> &IndexSet<Bytes> {
803        &self.literal_values.get().bytes
804    }
805
806    #[inline]
807    pub const fn addresses(&self) -> &AddressIndexSet {
808        &self.addresses
809    }
810
811    /// Revert values and addresses collected during the run by truncating to initial db len.
812    pub fn revert(&mut self) {
813        self.state_values.truncate(self.db_state_values);
814        self.addresses.truncate(self.db_addresses);
815        self.push_bytecode_hashes.truncate(self.db_push_bytecode_hashes);
816    }
817
818    pub fn log_stats(&self) {
819        trace!(
820            addresses.len = self.addresses.len(),
821            sample.len = self.sample_values.len(),
822            state.len = self.state_values.len(),
823            state.misses = self.misses,
824            state.hits = self.hits,
825            "FuzzDictionary stats",
826        );
827    }
828
829    #[cfg(test)]
830    /// Test-only helper to seed the dictionary with literal values.
831    pub(crate) fn seed_literals(&mut self, map: super::LiteralMaps) {
832        self.literal_values.set(map);
833    }
834}
835
836#[cfg(test)]
837mod tests {
838
839    use super::*;
840    use alloy_json_abi::{Event, JsonAbi};
841    use foundry_evm_core::eip2935::HISTORY_STORAGE_ADDRESS;
842    use revm::bytecode::Bytecode;
843
844    fn account_with_code(raw: &'static [u8]) -> AccountInfo {
845        AccountInfo::default().with_code(Bytecode::new_raw(Bytes::from_static(raw)))
846    }
847
848    #[test]
849    fn log_decoding_preserves_anonymous_event_priority() {
850        let mut abi = JsonAbi::new();
851        let anonymous_event =
852            Event::parse("event AEvent(bytes32 indexed topic, uint256 value) anonymous").unwrap();
853        let matched_event = Event::parse("event ZEvent(uint256 value)").unwrap();
854        let selector = matched_event.selector();
855        abi.events.entry(anonymous_event.name.clone()).or_default().push(anonymous_event);
856        abi.events.entry(matched_event.name.clone()).or_default().push(matched_event);
857        let contract = TargetedContract::new("Target".to_string(), abi);
858        let matched_events = contract.event_lookup.by_topic(&selector, 0).unwrap();
859        let word: B256 = U256::from(42).into();
860        let log = Log::new_unchecked(Address::ZERO, vec![selector], Bytes::from(word));
861        let mut samples = Vec::new();
862
863        assert!(FuzzDictionary::decode_log_events(
864            matched_events,
865            contract.event_lookup.anonymous(),
866            &log,
867            &mut samples,
868        ));
869
870        assert_eq!(samples.len(), 2);
871        assert_eq!(samples[0], DynSolValue::FixedBytes(selector, 32));
872        assert_eq!(samples[1], DynSolValue::Uint(U256::from(42), 256));
873    }
874
875    #[test]
876    fn push_byte_collection_stops_when_dictionary_is_full() {
877        let mut dictionary = FuzzDictionary::new(FuzzDictionaryConfig {
878            max_fuzz_dictionary_values: 3,
879            ..Default::default()
880        });
881
882        dictionary.collect_push_bytes(&[0x60, 0x01, 0x60, 0x03]);
883
884        assert_eq!(dictionary.len(), 3);
885        assert!(dictionary.state_values.contains(&B256::ZERO));
886        assert!(dictionary.state_values.contains(&B256::with_last_byte(1)));
887        assert!(dictionary.state_values.contains(&B256::with_last_byte(2)));
888        assert!(!dictionary.state_values.contains(&B256::with_last_byte(3)));
889    }
890
891    #[test]
892    fn zero_value_capacity_keeps_only_required_seed() {
893        let mut dictionary = FuzzDictionary::new(FuzzDictionaryConfig {
894            max_fuzz_dictionary_values: 0,
895            ..Default::default()
896        });
897
898        assert_eq!(dictionary.config.max_fuzz_dictionary_values, 1);
899        assert_eq!(dictionary.state_values.as_slice(), &[B256::ZERO]);
900        assert!(!dictionary.insert_value(B256::with_last_byte(1)));
901        assert_eq!(dictionary.state_values.as_slice(), &[B256::ZERO]);
902    }
903
904    #[test]
905    fn duplicate_push_values_in_same_bytecode_are_collected_once() {
906        let mut dictionary = FuzzDictionary::default();
907
908        dictionary.collect_push_bytes(&[0x60, 0x01, 0x60, 0x01]);
909
910        assert!(dictionary.state_values.contains(&B256::ZERO));
911        assert!(dictionary.state_values.contains(&B256::with_last_byte(1)));
912        assert!(dictionary.state_values.contains(&B256::with_last_byte(2)));
913        assert_eq!(dictionary.hits, 1);
914    }
915
916    #[test]
917    fn duplicate_bytecode_push_bytes_are_collected_once() {
918        let mut dictionary = FuzzDictionary::default();
919        let account = account_with_code(&[0x60, 0x01]);
920
921        dictionary.insert_push_bytes_values(&Address::repeat_byte(0x11), &account);
922        let hits_after_first_scan = dictionary.hits;
923
924        dictionary.insert_push_bytes_values(&Address::repeat_byte(0x22), &account);
925
926        assert_eq!(dictionary.push_bytecode_hashes.len(), 1);
927        assert_eq!(dictionary.addresses.len(), 2);
928        assert_eq!(dictionary.hits, hits_after_first_scan);
929    }
930
931    #[test]
932    fn same_address_with_new_bytecode_is_scanned_again() {
933        let mut dictionary = FuzzDictionary::default();
934        let address = Address::repeat_byte(0x22);
935
936        dictionary.insert_push_bytes_values(&address, &account_with_code(&[0x60, 0x01]));
937        dictionary.insert_push_bytes_values(&address, &account_with_code(&[0x60, 0x04]));
938
939        assert_eq!(dictionary.addresses.len(), 1);
940        assert_eq!(dictionary.push_bytecode_hashes.len(), 2);
941        assert!(dictionary.state_values.contains(&B256::with_last_byte(1)));
942        assert!(dictionary.state_values.contains(&B256::with_last_byte(4)));
943    }
944
945    #[test]
946    fn no_code_account_does_not_block_later_push_byte_scan() {
947        let mut dictionary = FuzzDictionary::default();
948        let address = Address::repeat_byte(0x33);
949        let account_without_code = AccountInfo { code: None, ..Default::default() };
950
951        dictionary.insert_push_bytes_values(&address, &account_without_code);
952
953        assert!(!dictionary.addresses.contains(&address));
954        assert_eq!(dictionary.push_bytecode_hashes.len(), 0);
955
956        dictionary.insert_push_bytes_values(&address, &account_with_code(&[0x60, 0x04]));
957
958        assert!(dictionary.addresses.contains(&address));
959        assert_eq!(dictionary.push_bytecode_hashes.len(), 1);
960        assert!(dictionary.state_values.contains(&B256::with_last_byte(4)));
961    }
962
963    #[test]
964    fn revert_removes_runtime_bytecode_scan_cache() {
965        let mut dictionary = FuzzDictionary::default();
966        dictionary.db_state_values = dictionary.state_values.len();
967        dictionary.db_addresses = dictionary.addresses.len();
968        dictionary.db_push_bytecode_hashes = dictionary.push_bytecode_hashes.len();
969
970        let account = account_with_code(&[0x60, 0x01]);
971        dictionary.insert_push_bytes_values(&Address::repeat_byte(0x11), &account);
972        assert_eq!(dictionary.push_bytecode_hashes.len(), 1);
973
974        dictionary.revert();
975        assert_eq!(dictionary.push_bytecode_hashes.len(), 0);
976
977        dictionary.insert_push_bytes_values(&Address::repeat_byte(0x22), &account);
978        assert_eq!(dictionary.push_bytecode_hashes.len(), 1);
979        assert!(dictionary.state_values.contains(&B256::with_last_byte(1)));
980    }
981
982    #[test]
983    fn persistent_value_survives_revert() {
984        let mut dictionary = FuzzDictionary::default();
985        dictionary.db_state_values = dictionary.state_values.len();
986
987        let ephemeral = B256::from(U256::from(0xbeef_u64));
988        let persistent = B256::from(U256::from(0xcafe_u64));
989        dictionary.insert_value(ephemeral);
990        dictionary.insert_persistent_value(persistent);
991
992        dictionary.revert();
993
994        assert!(dictionary.state_values.contains(&persistent));
995        assert!(!dictionary.state_values.contains(&ephemeral));
996        assert!(dictionary.db_state_values <= dictionary.state_values.len());
997
998        // Values already inside the persisted prefix are left untouched.
999        let watermark = dictionary.db_state_values;
1000        let len = dictionary.state_values.len();
1001        dictionary.insert_persistent_value(B256::ZERO);
1002        assert_eq!(dictionary.db_state_values, watermark);
1003        assert_eq!(dictionary.state_values.len(), len);
1004    }
1005
1006    #[test]
1007    fn persistent_value_promotes_existing_ephemeral() {
1008        let mut dictionary = FuzzDictionary::default();
1009        dictionary.db_state_values = dictionary.state_values.len();
1010
1011        let ephemeral = B256::from(U256::from(0xbeef_u64));
1012        let value = B256::from(U256::from(0xdead_u64));
1013        dictionary.insert_value(ephemeral);
1014        dictionary.insert_value(value);
1015        dictionary.insert_persistent_value(value);
1016
1017        dictionary.revert();
1018
1019        assert!(dictionary.state_values.contains(&value));
1020        assert!(!dictionary.state_values.contains(&ephemeral));
1021        assert!(dictionary.db_state_values <= dictionary.state_values.len());
1022    }
1023
1024    #[test]
1025    fn history_storage_account_is_excluded_from_initial_dictionary() {
1026        let mut db = InMemoryDB::default();
1027        let code = Bytecode::new_raw(Bytes::from_static(&[0x61, 0x01, 0x23, 0x00]));
1028        db.insert_account_info(HISTORY_STORAGE_ADDRESS, AccountInfo::default().with_code(code));
1029        db.insert_account_storage(HISTORY_STORAGE_ADDRESS, U256::from(7), U256::from(0xdead_u64))
1030            .unwrap();
1031
1032        let state = EvmFuzzState::new(&[], &db, FuzzDictionaryConfig::default(), None);
1033
1034        state.with_dictionary(|dict| {
1035            assert!(!dict.values().contains(&HISTORY_STORAGE_ADDRESS.into_word()));
1036            assert!(!dict.values().contains(&B256::from(U256::from(0x123))));
1037            assert!(!dict.values().contains(&B256::with_last_byte(7)));
1038            assert!(!dict.values().contains(&B256::from(U256::from(0xdead_u64))));
1039        });
1040    }
1041
1042    #[test]
1043    fn worker_modes_share_seed_but_isolate_feedback() {
1044        let seed = EvmFuzzState::test();
1045        let readonly = seed.stateless_worker();
1046        let first = seed.clone().into_invariant();
1047        let second = seed.into_invariant();
1048        let transient = B256::from(U256::from(0xdead_u64));
1049        let persistent = B256::from(U256::from(0xbeef_u64));
1050
1051        readonly.collect_values([transient]);
1052        assert!(!readonly.with_dictionary(|dict| dict.state_values.contains(&transient)));
1053
1054        first.collect_values([transient]);
1055        first.collect_typed_cmp_values([(8, persistent)]);
1056        assert!(first.with_dictionary(|dict| dict.state_values.contains(&transient)));
1057        assert!(!second.with_dictionary(|dict| dict.state_values.contains(&transient)));
1058
1059        first.revert();
1060        assert!(!first.with_dictionary(|dict| dict.state_values.contains(&transient)));
1061        first.with_dictionary(|dict| {
1062            assert!(dict.state_values.contains(&persistent));
1063            assert!(dict.persistent_values.contains(&persistent));
1064            assert!(dict.sample_values[&DynSolType::Uint(8)].contains(&persistent));
1065        });
1066    }
1067}