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