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