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        // Sort accounts before inserting to ensure deterministic dictionary.
501        let mut accounts = state_changeset.iter().collect::<Vec<_>>();
502        accounts.sort_unstable_by_key(|(address, _)| **address);
503        for (address, account) in accounts {
504            if is_history_storage_address(address) {
505                continue;
506            }
507
508            // Insert basic account information.
509            self.insert_value(address.into_word());
510            // Insert push bytes.
511            self.insert_push_bytes_values(address, &account.info);
512            // Insert storage values.
513            if self.config.include_storage && !account.storage.is_empty() {
514                let slot_identifier_key = targets.get(address).and_then(|contract| {
515                    contract.storage_layout.as_ref().map(|layout| {
516                        let key = Arc::as_ptr(layout) as usize;
517                        self.slot_identifiers
518                            .entry(key)
519                            .or_insert_with(|| SlotIdentifier::new(Arc::clone(layout)));
520                        key
521                    })
522                });
523                trace!(
524                    "{address:?} has mapping_slots {}",
525                    mapping_slots.is_some_and(|m| m.contains_key(address))
526                );
527                let mapping_slots = mapping_slots.and_then(|m| m.get(address));
528                // Sort storage values before inserting to ensure deterministic dictionary.
529                let mut storage = account.storage.iter().collect::<Vec<_>>();
530                storage.sort_unstable_by_key(|(slot, _)| **slot);
531                for (slot, value) in storage {
532                    let slot_info = slot_identifier_key.and_then(|key| {
533                        let slot = B256::from(*slot);
534                        let value_word = B256::from(value.present_value());
535                        self.identify_storage_slot(key, slot, mapping_slots)
536                            .filter(|slot_info| slot_info.decode(value_word).is_some())
537                    });
538                    self.insert_storage_value(slot, &value.present_value(), slot_info);
539                }
540            }
541        }
542    }
543
544    fn identify_storage_slot(
545        &mut self,
546        key: usize,
547        slot: B256,
548        mapping_slots: Option<&MappingSlots>,
549    ) -> Option<SlotInfo> {
550        if mapping_slots.is_some() {
551            return self
552                .slot_identifiers
553                .get(&key)
554                .and_then(|identifier| identifier.identify(&slot, mapping_slots));
555        }
556
557        let cache_key = (key, slot);
558        if let Some(slot_info) = self.slot_info_cache.get(&cache_key) {
559            return slot_info.clone();
560        }
561
562        let slot_info =
563            self.slot_identifiers.get(&key).and_then(|identifier| identifier.identify(&slot, None));
564        if self.slot_info_cache.len() < MAX_SLOT_INFO_CACHE_ENTRIES {
565            self.slot_info_cache.insert(cache_key, slot_info.clone());
566        }
567        slot_info
568    }
569
570    /// Insert values from push bytes into fuzz dictionary.
571    /// Values are collected only once for a given bytecode.
572    /// If values are newly collected then they are removed at the end of current run.
573    fn insert_push_bytes_values(&mut self, address: &Address, account_info: &AccountInfo) {
574        if !self.config.include_push_bytes {
575            return;
576        }
577
578        let Some(code) = &account_info.code else {
579            return;
580        };
581        self.insert_address(*address);
582        if self.values_full() {
583            return;
584        }
585        if self.push_bytecode_hashes.insert(account_info.code_hash()) {
586            self.collect_push_bytes(ignore_metadata_hash(code.original_byte_slice()));
587        }
588    }
589
590    fn collect_push_bytes(&mut self, code: &[u8]) {
591        let len = code.len().min(PUSH_BYTE_ANALYSIS_LIMIT);
592        let code = &code[..len];
593        let mut seen = HashSet::default();
594        for inst in InstIter::new(code) {
595            if self.values_full() {
596                break;
597            }
598            // Don't add 0 to the dictionary as it's already present.
599            if !inst.immediate.is_empty()
600                && let Some(push_value) = U256::try_from_be_slice(inst.immediate)
601                && !push_value.is_zero()
602            {
603                self.insert_push_value_u256(push_value, &mut seen);
604            }
605        }
606    }
607
608    /// Insert values from single storage slot and storage value into fuzz dictionary.
609    /// Uses [`SlotIdentifier`] to identify storage slots types.
610    fn insert_storage_value(&mut self, slot: &U256, value: &U256, slot_info: Option<SlotInfo>) {
611        let slot = B256::from(*slot);
612        let value_word = B256::from(*value);
613
614        // Always insert the slot itself
615        self.insert_value(slot);
616
617        if let Some(slot_info) = slot_info {
618            trace!(?slot_info, "inserting typed storage value");
619            if !self.samples_seeded {
620                self.seed_samples();
621            }
622            self.sample_values
623                .entry(slot_info.slot_type.dyn_sol_type)
624                .or_default()
625                .insert(value_word);
626        } else {
627            self.insert_value_u256(*value);
628        }
629    }
630
631    /// Insert address into fuzz dictionary.
632    /// If address is newly collected then it is removed by index at the end of current run.
633    fn insert_address(&mut self, address: Address) {
634        if self.addresses.len() < self.config.max_fuzz_dictionary_addresses {
635            self.addresses.insert(address);
636        }
637    }
638
639    /// Insert raw value into fuzz dictionary.
640    ///
641    /// If value is newly collected then it is removed by index at the end of current run.
642    ///
643    /// Returns true if the value was inserted.
644    fn insert_value(&mut self, value: B256) -> bool {
645        let insert = !self.values_full();
646        if insert {
647            let new_value = self.state_values.insert(value);
648            let counter = if new_value { &mut self.misses } else { &mut self.hits };
649            *counter += 1;
650        }
651        insert
652    }
653
654    /// Insert a persistent value that survives `revert()` across invariant runs.
655    /// Used for trace-cmp operands that should compound over time.
656    fn insert_persistent_value(&mut self, value: B256) {
657        if self.persistent_values.len() >= MAX_PERSISTENT_VALUES {
658            return;
659        }
660        if !self.persistent_values.insert(value) {
661            return;
662        }
663        // `revert()` truncates `state_values` down to the first `db_state_values` entries, so the
664        // value must be placed inside that prefix; a plain `insert` appends past it and would be
665        // truncated at the end of the run.
666        match self.state_values.get_index_of(&value) {
667            // Already inside the persisted prefix.
668            Some(index) if index < self.db_state_values => {}
669            // Collected as an ephemeral value earlier in this run: move it into the prefix.
670            Some(index) => {
671                self.state_values.move_index(index, self.db_state_values);
672                self.db_state_values += 1;
673            }
674            None => {
675                self.state_values.shift_insert(self.db_state_values, value);
676                self.db_state_values += 1;
677            }
678        }
679    }
680
681    /// Insert a typed trace-cmp value into the `sample_values` map.
682    /// Maps sancov width to `DynSolType` buckets and promotes to larger types.
683    fn insert_typed_cmp_value(&mut self, width: u8, value: B256) {
684        if !self.samples_seeded {
685            self.seed_samples();
686        }
687
688        const MAX_TYPED_CMP_PER_BUCKET: usize = 1024;
689
690        let native_type = match width {
691            8 => DynSolType::Uint(8),
692            16 => DynSolType::Uint(16),
693            32 => DynSolType::Uint(32),
694            64 => DynSolType::Uint(64),
695            _ => DynSolType::Uint(256),
696        };
697
698        let insert = |map: &mut HashMap<DynSolType, B256IndexSet>, ty: DynSolType, val: B256| {
699            let bucket = map.entry(ty).or_default();
700            if bucket.len() < MAX_TYPED_CMP_PER_BUCKET {
701                bucket.insert(val);
702            }
703        };
704
705        insert(&mut self.sample_values, native_type, value);
706
707        if width <= 64 {
708            insert(&mut self.sample_values, DynSolType::Uint(128), value);
709            insert(&mut self.sample_values, DynSolType::Uint(256), value);
710            insert(&mut self.sample_values, DynSolType::Int(256), value);
711        }
712    }
713
714    fn insert_value_u256(&mut self, value: U256) -> bool {
715        // Also add the value below and above the push value to the dictionary.
716        let one = U256::ONE;
717        let mut inserted = self.insert_value(value.into());
718        if !self.values_full() {
719            inserted |= self.insert_value((value.wrapping_sub(one)).into());
720        }
721        if !self.values_full() {
722            inserted |= self.insert_value((value.wrapping_add(one)).into());
723        }
724        inserted
725    }
726
727    fn insert_push_value_u256(&mut self, value: U256, seen: &mut HashSet<B256>) -> bool {
728        // Also add the value below and above the push value to the dictionary.
729        let one = U256::ONE;
730        let mut inserted = false;
731        for value in [value, value.wrapping_sub(one), value.wrapping_add(one)] {
732            if self.values_full() {
733                break;
734            }
735            let value = value.into();
736            if seen.insert(value) {
737                inserted |= self.insert_value(value);
738            }
739        }
740        inserted
741    }
742
743    fn values_full(&self) -> bool {
744        self.state_values.len() >= self.config.max_fuzz_dictionary_values
745    }
746
747    /// Insert sample values that are reused across multiple runs.
748    /// The number of samples is limited to invariant run depth.
749    /// If collected samples limit is reached then values are inserted as regular values.
750    pub fn insert_sample_values(
751        &mut self,
752        sample_values: impl IntoIterator<Item = DynSolValue>,
753        limit: u32,
754    ) {
755        if !self.samples_seeded {
756            self.seed_samples();
757        }
758        for sample in sample_values {
759            if let (Some(sample_type), Some(sample_value)) = (sample.as_type(), sample.as_word()) {
760                if let Some(values) = self.sample_values.get_mut(&sample_type) {
761                    if values.len() < limit as usize {
762                        values.insert(sample_value);
763                    } else {
764                        // Insert as state value (will be removed at the end of the run).
765                        self.insert_value(sample_value);
766                    }
767                } else {
768                    self.sample_values.entry(sample_type).or_default().insert(sample_value);
769                }
770            }
771        }
772    }
773
774    pub const fn values(&self) -> &B256IndexSet {
775        &self.state_values
776    }
777
778    pub fn len(&self) -> usize {
779        self.state_values.len()
780    }
781
782    pub fn is_empty(&self) -> bool {
783        self.state_values.is_empty()
784    }
785
786    /// Returns sample values for a given type, checking both runtime samples and literals.
787    ///
788    /// Before `seed_samples()` is called, checks both `literal_values` and `sample_values`
789    /// separately. After seeding, all literal values are merged into `sample_values`.
790    #[inline]
791    pub fn samples(&self, param_type: &DynSolType) -> Option<&B256IndexSet> {
792        // If not seeded yet, return literals
793        if !self.samples_seeded {
794            return self.literal_values.get().words.get(param_type);
795        }
796
797        self.sample_values.get(param_type)
798    }
799
800    /// Returns the collected literal strings, triggering initialization if needed.
801    #[inline]
802    pub fn ast_strings(&self) -> &IndexSet<String> {
803        &self.literal_values.get().strings
804    }
805
806    /// Returns the collected literal bytes (hex strings), triggering initialization if needed.
807    #[inline]
808    pub fn ast_bytes(&self) -> &IndexSet<Bytes> {
809        &self.literal_values.get().bytes
810    }
811
812    #[inline]
813    pub const fn addresses(&self) -> &AddressIndexSet {
814        &self.addresses
815    }
816
817    /// Revert values and addresses collected during the run by truncating to initial db len.
818    pub fn revert(&mut self) {
819        self.state_values.truncate(self.db_state_values);
820        self.addresses.truncate(self.db_addresses);
821        self.push_bytecode_hashes.truncate(self.db_push_bytecode_hashes);
822    }
823
824    pub fn log_stats(&self) {
825        trace!(
826            addresses.len = self.addresses.len(),
827            sample.len = self.sample_values.len(),
828            state.len = self.state_values.len(),
829            state.misses = self.misses,
830            state.hits = self.hits,
831            "FuzzDictionary stats",
832        );
833    }
834
835    #[cfg(test)]
836    /// Test-only helper to seed the dictionary with literal values.
837    pub(crate) fn seed_literals(&mut self, map: super::LiteralMaps) {
838        self.literal_values.set(map);
839    }
840}
841
842#[cfg(test)]
843mod tests {
844
845    use super::*;
846    use alloy_json_abi::{Event, JsonAbi};
847    use foundry_evm_core::eip2935::HISTORY_STORAGE_ADDRESS;
848    use revm::{
849        bytecode::Bytecode,
850        state::{Account, EvmStorageSlot, TransactionId},
851    };
852
853    fn account_with_code(raw: &'static [u8]) -> AccountInfo {
854        AccountInfo::default().with_code(Bytecode::new_raw(Bytes::from_static(raw)))
855    }
856
857    #[test]
858    fn log_decoding_preserves_anonymous_event_priority() {
859        let mut abi = JsonAbi::new();
860        let anonymous_event =
861            Event::parse("event AEvent(bytes32 indexed topic, uint256 value) anonymous").unwrap();
862        let matched_event = Event::parse("event ZEvent(uint256 value)").unwrap();
863        let selector = matched_event.selector();
864        abi.events.entry(anonymous_event.name.clone()).or_default().push(anonymous_event);
865        abi.events.entry(matched_event.name.clone()).or_default().push(matched_event);
866        let contract = TargetedContract::new("Target".to_string(), abi);
867        let matched_events = contract.event_lookup.by_topic(&selector, 0).unwrap();
868        let word: B256 = U256::from(42).into();
869        let log = Log::new_unchecked(Address::ZERO, vec![selector], Bytes::from(word));
870        let mut samples = Vec::new();
871
872        assert!(FuzzDictionary::decode_log_events(
873            matched_events,
874            contract.event_lookup.anonymous(),
875            &log,
876            &mut samples,
877        ));
878
879        assert_eq!(samples.len(), 2);
880        assert_eq!(samples[0], DynSolValue::FixedBytes(selector, 32));
881        assert_eq!(samples[1], DynSolValue::Uint(U256::from(42), 256));
882    }
883
884    #[test]
885    fn push_byte_collection_stops_when_dictionary_is_full() {
886        let mut dictionary = FuzzDictionary::new(FuzzDictionaryConfig {
887            max_fuzz_dictionary_values: 3,
888            ..Default::default()
889        });
890
891        dictionary.collect_push_bytes(&[0x60, 0x01, 0x60, 0x03]);
892
893        assert_eq!(dictionary.len(), 3);
894        assert!(dictionary.state_values.contains(&B256::ZERO));
895        assert!(dictionary.state_values.contains(&B256::with_last_byte(1)));
896        assert!(dictionary.state_values.contains(&B256::with_last_byte(2)));
897        assert!(!dictionary.state_values.contains(&B256::with_last_byte(3)));
898    }
899
900    #[test]
901    fn zero_value_capacity_keeps_only_required_seed() {
902        let mut dictionary = FuzzDictionary::new(FuzzDictionaryConfig {
903            max_fuzz_dictionary_values: 0,
904            ..Default::default()
905        });
906
907        assert_eq!(dictionary.config.max_fuzz_dictionary_values, 1);
908        assert_eq!(dictionary.state_values.as_slice(), &[B256::ZERO]);
909        assert!(!dictionary.insert_value(B256::with_last_byte(1)));
910        assert_eq!(dictionary.state_values.as_slice(), &[B256::ZERO]);
911    }
912
913    #[test]
914    fn duplicate_push_values_in_same_bytecode_are_collected_once() {
915        let mut dictionary = FuzzDictionary::default();
916
917        dictionary.collect_push_bytes(&[0x60, 0x01, 0x60, 0x01]);
918
919        assert!(dictionary.state_values.contains(&B256::ZERO));
920        assert!(dictionary.state_values.contains(&B256::with_last_byte(1)));
921        assert!(dictionary.state_values.contains(&B256::with_last_byte(2)));
922        assert_eq!(dictionary.hits, 1);
923    }
924
925    #[test]
926    fn duplicate_bytecode_push_bytes_are_collected_once() {
927        let mut dictionary = FuzzDictionary::default();
928        let account = account_with_code(&[0x60, 0x01]);
929
930        dictionary.insert_push_bytes_values(&Address::repeat_byte(0x11), &account);
931        let hits_after_first_scan = dictionary.hits;
932
933        dictionary.insert_push_bytes_values(&Address::repeat_byte(0x22), &account);
934
935        assert_eq!(dictionary.push_bytecode_hashes.len(), 1);
936        assert_eq!(dictionary.addresses.len(), 2);
937        assert_eq!(dictionary.hits, hits_after_first_scan);
938    }
939
940    #[test]
941    fn same_address_with_new_bytecode_is_scanned_again() {
942        let mut dictionary = FuzzDictionary::default();
943        let address = Address::repeat_byte(0x22);
944
945        dictionary.insert_push_bytes_values(&address, &account_with_code(&[0x60, 0x01]));
946        dictionary.insert_push_bytes_values(&address, &account_with_code(&[0x60, 0x04]));
947
948        assert_eq!(dictionary.addresses.len(), 1);
949        assert_eq!(dictionary.push_bytecode_hashes.len(), 2);
950        assert!(dictionary.state_values.contains(&B256::with_last_byte(1)));
951        assert!(dictionary.state_values.contains(&B256::with_last_byte(4)));
952    }
953
954    #[test]
955    fn no_code_account_does_not_block_later_push_byte_scan() {
956        let mut dictionary = FuzzDictionary::default();
957        let address = Address::repeat_byte(0x33);
958        let account_without_code = AccountInfo { code: None, ..Default::default() };
959
960        dictionary.insert_push_bytes_values(&address, &account_without_code);
961
962        assert!(!dictionary.addresses.contains(&address));
963        assert_eq!(dictionary.push_bytecode_hashes.len(), 0);
964
965        dictionary.insert_push_bytes_values(&address, &account_with_code(&[0x60, 0x04]));
966
967        assert!(dictionary.addresses.contains(&address));
968        assert_eq!(dictionary.push_bytecode_hashes.len(), 1);
969        assert!(dictionary.state_values.contains(&B256::with_last_byte(4)));
970    }
971
972    #[test]
973    fn revert_removes_runtime_bytecode_scan_cache() {
974        let mut dictionary = FuzzDictionary::default();
975        dictionary.db_state_values = dictionary.state_values.len();
976        dictionary.db_addresses = dictionary.addresses.len();
977        dictionary.db_push_bytecode_hashes = dictionary.push_bytecode_hashes.len();
978
979        let account = account_with_code(&[0x60, 0x01]);
980        dictionary.insert_push_bytes_values(&Address::repeat_byte(0x11), &account);
981        assert_eq!(dictionary.push_bytecode_hashes.len(), 1);
982
983        dictionary.revert();
984        assert_eq!(dictionary.push_bytecode_hashes.len(), 0);
985
986        dictionary.insert_push_bytes_values(&Address::repeat_byte(0x22), &account);
987        assert_eq!(dictionary.push_bytecode_hashes.len(), 1);
988        assert!(dictionary.state_values.contains(&B256::with_last_byte(1)));
989    }
990
991    #[test]
992    fn persistent_value_survives_revert() {
993        let mut dictionary = FuzzDictionary::default();
994        dictionary.db_state_values = dictionary.state_values.len();
995
996        let ephemeral = B256::from(U256::from(0xbeef_u64));
997        let persistent = B256::from(U256::from(0xcafe_u64));
998        dictionary.insert_value(ephemeral);
999        dictionary.insert_persistent_value(persistent);
1000
1001        dictionary.revert();
1002
1003        assert!(dictionary.state_values.contains(&persistent));
1004        assert!(!dictionary.state_values.contains(&ephemeral));
1005        assert!(dictionary.db_state_values <= dictionary.state_values.len());
1006
1007        // Values already inside the persisted prefix are left untouched.
1008        let watermark = dictionary.db_state_values;
1009        let len = dictionary.state_values.len();
1010        dictionary.insert_persistent_value(B256::ZERO);
1011        assert_eq!(dictionary.db_state_values, watermark);
1012        assert_eq!(dictionary.state_values.len(), len);
1013    }
1014
1015    #[test]
1016    fn persistent_value_promotes_existing_ephemeral() {
1017        let mut dictionary = FuzzDictionary::default();
1018        dictionary.db_state_values = dictionary.state_values.len();
1019
1020        let ephemeral = B256::from(U256::from(0xbeef_u64));
1021        let value = B256::from(U256::from(0xdead_u64));
1022        dictionary.insert_value(ephemeral);
1023        dictionary.insert_value(value);
1024        dictionary.insert_persistent_value(value);
1025
1026        dictionary.revert();
1027
1028        assert!(dictionary.state_values.contains(&value));
1029        assert!(!dictionary.state_values.contains(&ephemeral));
1030        assert!(dictionary.db_state_values <= dictionary.state_values.len());
1031    }
1032
1033    #[test]
1034    fn history_storage_account_is_excluded_from_initial_dictionary() {
1035        let mut db = InMemoryDB::default();
1036        let code = Bytecode::new_raw(Bytes::from_static(&[0x61, 0x01, 0x23, 0x00]));
1037        db.insert_account_info(HISTORY_STORAGE_ADDRESS, AccountInfo::default().with_code(code));
1038        db.insert_account_storage(HISTORY_STORAGE_ADDRESS, U256::from(7), U256::from(0xdead_u64))
1039            .unwrap();
1040
1041        let state = EvmFuzzState::new(&[], &db, FuzzDictionaryConfig::default(), None);
1042
1043        state.with_dictionary(|dict| {
1044            assert!(!dict.values().contains(&HISTORY_STORAGE_ADDRESS.into_word()));
1045            assert!(!dict.values().contains(&B256::from(U256::from(0x123))));
1046            assert!(!dict.values().contains(&B256::with_last_byte(7)));
1047            assert!(!dict.values().contains(&B256::from(U256::from(0xdead_u64))));
1048        });
1049    }
1050
1051    #[test]
1052    fn worker_modes_share_seed_but_isolate_feedback() {
1053        let seed = EvmFuzzState::test();
1054        let readonly = seed.stateless_worker();
1055        let first = seed.clone().into_invariant();
1056        let second = seed.into_invariant();
1057        let transient = B256::from(U256::from(0xdead_u64));
1058        let persistent = B256::from(U256::from(0xbeef_u64));
1059
1060        readonly.collect_values([transient]);
1061        assert!(!readonly.with_dictionary(|dict| dict.state_values.contains(&transient)));
1062
1063        first.collect_values([transient]);
1064        first.collect_typed_cmp_values([(8, persistent)]);
1065        assert!(first.with_dictionary(|dict| dict.state_values.contains(&transient)));
1066        assert!(!second.with_dictionary(|dict| dict.state_values.contains(&transient)));
1067
1068        first.revert();
1069        assert!(!first.with_dictionary(|dict| dict.state_values.contains(&transient)));
1070        first.with_dictionary(|dict| {
1071            assert!(dict.state_values.contains(&persistent));
1072            assert!(dict.persistent_values.contains(&persistent));
1073            assert!(dict.sample_values[&DynSolType::Uint(8)].contains(&persistent));
1074        });
1075    }
1076
1077    #[test]
1078    fn new_state_values_are_inserted_in_sorted_order() {
1079        // Account `i` holds slots `i * 0x100 + j` with values `i * 0x10000 + j * 0x10`, so no
1080        // address, slot, or value (+/- 1) collides with another.
1081        let entry =
1082            |i: u64, j: u64| (U256::from(i * 0x100 + j), U256::from(i * 0x10000 + j * 0x10));
1083        let address = |i: u64| Address::repeat_byte(i as u8 * 0x11);
1084
1085        let mut state_changeset = StateChangeset::default();
1086        for i in [3, 1, 4, 2] {
1087            let storage = [3, 1, 2].map(|j| {
1088                let (slot, value) = entry(i, j);
1089                (slot, EvmStorageSlot::new(value, TransactionId::ZERO))
1090            });
1091            state_changeset
1092                .insert(address(i), Account::default().with_storage(storage.into_iter()));
1093        }
1094
1095        let mut dictionary = FuzzDictionary::default();
1096        dictionary.insert_new_state_values(&state_changeset, &TargetedContracts::new(), None);
1097
1098        let mut expected = vec![B256::ZERO];
1099        for i in 1..=4 {
1100            expected.push(address(i).into_word());
1101            for j in 1..=3 {
1102                let (slot, value) = entry(i, j);
1103                expected
1104                    .extend([slot, value, value - U256::ONE, value + U256::ONE].map(B256::from));
1105            }
1106        }
1107        assert_eq!(dictionary.state_values.as_slice(), expected.as_slice());
1108    }
1109}