Skip to main content

foundry_evm_fuzz/invariant/
mod.rs

1use alloy_json_abi::{Event, Function, JsonAbi};
2use alloy_primitives::{Address, B256, Selector, map::HashMap};
3use foundry_compilers::artifacts::StorageLayout;
4use itertools::Either;
5use serde::{Deserialize, Serialize};
6use std::{
7    cell::{Cell, Ref, RefCell},
8    collections::BTreeMap,
9    fmt,
10    rc::Rc,
11    sync::Arc,
12};
13
14mod call_override;
15pub use call_override::RandomCallGenerator;
16
17mod filters;
18use crate::BasicTxDetails;
19pub use filters::{ArtifactFilters, SenderFilters};
20use foundry_common::{ContractsByAddress, ContractsByArtifact};
21use foundry_evm_core::utils::StateChangeset;
22
23type DynamicTargetCacheKey = (Address, B256);
24type DynamicTargetArtifactMatchCache =
25    Rc<RefCell<HashMap<DynamicTargetCacheKey, Option<CachedTargetContract>>>>;
26type FuzzedFunction = (Address, Function);
27type FunctionLookup = HashMap<Selector, Function>;
28
29/// Returns true if the function returns `int256`, indicating optimization mode.
30/// In optimization mode, the fuzzer maximizes the return value instead of checking invariants.
31pub fn is_optimization_invariant(func: &Function) -> bool {
32    func.outputs.len() == 1 && func.outputs[0].ty == "int256"
33}
34
35/// Contracts identified as targets during a fuzz run.
36///
37/// During execution, any newly created contract is added as target and used through the rest of
38/// the fuzz run if the collection is updatable (no `targetContract` specified in `setUp`).
39#[derive(Clone, Debug)]
40pub struct FuzzRunIdentifiedContracts {
41    /// Contracts identified as targets during a fuzz run.
42    targets: Rc<RefCell<TargetedContracts>>,
43    /// Flat cache of all currently fuzzable target functions.
44    fuzzed_functions: Rc<RefCell<Vec<FuzzedFunction>>>,
45    /// Generation counter for cached fuzzed functions.
46    fuzzed_functions_generation: Rc<Cell<u64>>,
47    /// Whether target contracts are updatable or not.
48    pub is_updatable: bool,
49    artifact_matches: DynamicTargetArtifactMatchCache,
50}
51
52impl FuzzRunIdentifiedContracts {
53    /// Creates a new `FuzzRunIdentifiedContracts` instance.
54    pub fn new(targets: TargetedContracts, is_updatable: bool) -> Self {
55        let fuzzed_functions = Self::flatten_fuzzed_functions(&targets);
56        Self {
57            targets: Rc::new(RefCell::new(targets)),
58            fuzzed_functions: Rc::new(RefCell::new(fuzzed_functions)),
59            fuzzed_functions_generation: Rc::new(Cell::new(0)),
60            is_updatable,
61            artifact_matches: Rc::new(RefCell::new(HashMap::default())),
62        }
63    }
64
65    /// Borrows the current targeted contracts.
66    pub fn targets(&self) -> Ref<'_, TargetedContracts> {
67        self.targets.borrow()
68    }
69
70    /// Borrows the current flat list of fuzzed target functions.
71    pub fn fuzzed_functions(&self) -> Ref<'_, [FuzzedFunction]> {
72        Ref::map(self.fuzzed_functions.borrow(), Vec::as_slice)
73    }
74
75    /// Returns the current fuzzed-functions generation.
76    pub fn fuzzed_functions_generation(&self) -> u64 {
77        self.fuzzed_functions_generation.get()
78    }
79
80    fn refresh_fuzzed_functions(&self) {
81        let fuzzed_functions = {
82            let targets = self.targets.borrow();
83            Self::flatten_fuzzed_functions(&targets)
84        };
85        *self.fuzzed_functions.borrow_mut() = fuzzed_functions;
86        self.fuzzed_functions_generation.set(self.fuzzed_functions_generation.get() + 1);
87    }
88
89    fn flatten_fuzzed_functions(targets: &TargetedContracts) -> Vec<FuzzedFunction> {
90        targets.fuzzed_functions().map(|(address, function)| (*address, function.clone())).collect()
91    }
92
93    /// If targets are updatable, collect all contracts created during an invariant run (which
94    /// haven't been discovered yet).
95    pub fn collect_created_contracts(
96        &self,
97        state_changeset: &StateChangeset,
98        project_contracts: &ContractsByArtifact,
99        setup_contracts: &ContractsByAddress,
100        artifact_filters: &ArtifactFilters,
101        created_contracts: &mut Vec<Address>,
102    ) -> eyre::Result<()> {
103        if !self.is_updatable {
104            return Ok(());
105        }
106
107        let mut targets_changed = false;
108        for (address, account) in state_changeset {
109            if setup_contracts.contains_key(address) {
110                continue;
111            }
112            if !account.is_touched() {
113                continue;
114            }
115            let Some(code) = &account.info.code else {
116                continue;
117            };
118            if code.is_empty() {
119                continue;
120            }
121            let code_hash = code.hash_slow();
122            let code = code.original_byte_slice();
123            let Some(contract) = self.target_contract_for_code(
124                *address,
125                code_hash,
126                code,
127                project_contracts,
128                artifact_filters,
129            )?
130            else {
131                continue;
132            };
133            created_contracts.push(*address);
134            self.targets.borrow_mut().insert(*address, contract.into_targeted_contract());
135            targets_changed = true;
136        }
137        if targets_changed {
138            self.refresh_fuzzed_functions();
139        }
140        Ok(())
141    }
142
143    fn target_contract_for_code(
144        &self,
145        address: Address,
146        code_hash: B256,
147        code: &[u8],
148        project_contracts: &ContractsByArtifact,
149        artifact_filters: &ArtifactFilters,
150    ) -> eyre::Result<Option<CachedTargetContract>> {
151        let cache_key = (address, code_hash);
152        if let Some(cached_match) = self.artifact_matches.borrow().get(&cache_key) {
153            return Ok(cached_match.clone());
154        }
155
156        let cached_match = if let Some((artifact, contract_data)) =
157            project_contracts.find_by_deployed_code(code)
158        {
159            artifact_filters.get_targeted_functions(artifact, &contract_data.abi)?.map(
160                |targeted_functions| CachedTargetContract {
161                    identifier: artifact.name.clone(),
162                    abi: contract_data.abi.clone(),
163                    targeted_functions,
164                    storage_layout: contract_data.storage_layout.as_ref().map(Arc::clone),
165                    event_lookup: Arc::new(TargetedContractEvents::new(&contract_data.abi)),
166                },
167            )
168        } else {
169            None
170        };
171        self.artifact_matches.borrow_mut().insert(cache_key, cached_match.clone());
172        Ok(cached_match)
173    }
174
175    /// Clears targeted contracts created during an invariant run.
176    pub fn clear_created_contracts(&self, created_contracts: Vec<Address>) {
177        let mut targets_changed = false;
178        if !created_contracts.is_empty() {
179            let mut targets = self.targets.borrow_mut();
180            for addr in &created_contracts {
181                targets_changed |= targets.remove(addr).is_some();
182            }
183        }
184        if targets_changed {
185            self.refresh_fuzzed_functions();
186        }
187    }
188}
189
190#[derive(Clone, Debug)]
191struct CachedTargetContract {
192    identifier: String,
193    abi: JsonAbi,
194    targeted_functions: Vec<Function>,
195    storage_layout: Option<Arc<StorageLayout>>,
196    event_lookup: Arc<TargetedContractEvents>,
197}
198
199impl CachedTargetContract {
200    fn into_targeted_contract(self) -> TargetedContract {
201        TargetedContract::from_parts(
202            self.identifier,
203            self.abi,
204            self.targeted_functions,
205            Vec::new(),
206            self.storage_layout,
207            self.event_lookup,
208        )
209    }
210}
211
212/// A collection of contracts identified as targets for invariant testing.
213#[derive(Clone, Debug, Default)]
214pub struct TargetedContracts {
215    /// The inner map of targeted contracts.
216    pub inner: BTreeMap<Address, TargetedContract>,
217}
218
219impl TargetedContracts {
220    /// Returns a new `TargetedContracts` instance.
221    pub fn new() -> Self {
222        Self::default()
223    }
224
225    /// Returns fuzzed contract and fuzzed function from address and provided calldata.
226    ///
227    /// Used to decode return values and logs in order to add values into fuzz dictionary.
228    pub fn fuzzed_artifacts(
229        &self,
230        tx: &BasicTxDetails,
231    ) -> (Option<&TargetedContract>, Option<&Function>) {
232        match self.inner.get(&tx.call_details.target) {
233            Some(c) => {
234                let function = tx
235                    .call_details
236                    .calldata
237                    .get(..4)
238                    .and_then(|selector| <[u8; 4]>::try_from(selector).ok())
239                    .map(Selector::from)
240                    .and_then(|selector| c.function_by_selector(selector));
241                (Some(c), function)
242            }
243            None => (None, None),
244        }
245    }
246
247    /// Returns flatten target contract address and functions to be fuzzed.
248    /// Includes contract targeted functions if specified, else all mutable contract functions.
249    pub fn fuzzed_functions(&self) -> impl Iterator<Item = (&Address, &Function)> {
250        self.inner
251            .iter()
252            .filter(|(_, c)| !c.abi.functions.is_empty())
253            .flat_map(|(contract, c)| c.abi_fuzzed_functions().map(move |f| (contract, f)))
254    }
255
256    /// Returns whether the given transaction can be replayed or not with known contracts.
257    pub fn can_replay(&self, tx: &BasicTxDetails) -> bool {
258        match self.inner.get(&tx.call_details.target) {
259            Some(c) => tx
260                .call_details
261                .calldata
262                .get(..4)
263                .and_then(|selector| <[u8; 4]>::try_from(selector).ok())
264                .map(Selector::from)
265                .is_some_and(|selector| c.fuzzed_function_by_selector(selector).is_some()),
266            None => false,
267        }
268    }
269
270    /// Identifies fuzzed contract and function based on given tx details and returns unique metric
271    /// key composed from contract identifier and function name.
272    pub fn fuzzed_metric_key(&self, tx: &BasicTxDetails) -> Option<String> {
273        tx.call_details
274            .calldata
275            .get(..4)
276            .and_then(|selector| <[u8; 4]>::try_from(selector).ok())
277            .map(Selector::from)
278            .and_then(|selector| {
279                self.fuzzed_metric_key_for_selector(tx.call_details.target, selector)
280            })
281    }
282
283    /// Identifies fuzzed contract and function from target and selector and returns unique metric
284    /// key composed from contract identifier and function name.
285    pub fn fuzzed_metric_key_for_selector(
286        &self,
287        target: Address,
288        selector: Selector,
289    ) -> Option<String> {
290        self.inner.get(&target).and_then(|contract| {
291            contract
292                .function_by_selector(selector)
293                .map(|function| format!("{}.{}", contract.identifier.as_str(), function.name))
294        })
295    }
296}
297
298impl std::ops::Deref for TargetedContracts {
299    type Target = BTreeMap<Address, TargetedContract>;
300
301    fn deref(&self) -> &Self::Target {
302        &self.inner
303    }
304}
305
306impl std::ops::DerefMut for TargetedContracts {
307    fn deref_mut(&mut self) -> &mut Self::Target {
308        &mut self.inner
309    }
310}
311
312/// A contract identified as target for invariant testing.
313#[derive(Clone, Debug)]
314pub struct TargetedContract {
315    /// The contract identifier. This is only used in error messages.
316    pub identifier: String,
317    /// The contract's ABI.
318    pub abi: JsonAbi,
319    /// The targeted functions of the contract.
320    pub targeted_functions: Vec<Function>,
321    /// The excluded functions of the contract.
322    pub excluded_functions: Vec<Function>,
323    /// The contract's storage layout, if available.
324    pub storage_layout: Option<Arc<StorageLayout>>,
325    /// Contract events indexed by topic0 and indexed-topic count for log dictionary decoding.
326    pub event_lookup: Arc<TargetedContractEvents>,
327    functions_by_selector: FunctionLookup,
328    fuzzed_functions_by_selector: FunctionLookup,
329}
330
331impl TargetedContract {
332    /// Returns a new `TargetedContract` instance.
333    pub fn new(identifier: String, abi: JsonAbi) -> Self {
334        let event_lookup = Arc::new(TargetedContractEvents::new(&abi));
335        Self::from_parts(identifier, abi, Vec::new(), Vec::new(), None, event_lookup)
336    }
337
338    fn from_parts(
339        identifier: String,
340        abi: JsonAbi,
341        targeted_functions: Vec<Function>,
342        excluded_functions: Vec<Function>,
343        storage_layout: Option<Arc<StorageLayout>>,
344        event_lookup: Arc<TargetedContractEvents>,
345    ) -> Self {
346        let mut contract = Self {
347            identifier,
348            abi,
349            targeted_functions,
350            excluded_functions,
351            storage_layout,
352            event_lookup,
353            functions_by_selector: FunctionLookup::default(),
354            fuzzed_functions_by_selector: FunctionLookup::default(),
355        };
356        contract.rebuild_function_lookups();
357        contract
358    }
359
360    /// Determines contract storage layout from project contracts. Needs `storageLayout` to be
361    /// enabled as extra output in project configuration.
362    pub fn with_project_contracts(mut self, project_contracts: &ContractsByArtifact) -> Self {
363        if let Some((src, name)) = self.identifier.split_once(':')
364            && let Some((_, contract_data)) = project_contracts.iter().find(|(artifact, _)| {
365                artifact.name == name && artifact.source.as_path().ends_with(src)
366            })
367        {
368            self.storage_layout = contract_data.storage_layout.as_ref().map(Arc::clone);
369        }
370        self
371    }
372
373    /// Helper to retrieve functions to fuzz for specified abi.
374    /// Returns specified targeted functions if any, else mutable abi functions, always skipping
375    /// functions marked as excluded.
376    pub fn abi_fuzzed_functions(&self) -> impl Iterator<Item = &Function> {
377        if self.targeted_functions.is_empty() {
378            Either::Right(self.abi.functions().filter(|&func| {
379                !matches!(
380                    func.state_mutability,
381                    alloy_json_abi::StateMutability::Pure | alloy_json_abi::StateMutability::View
382                ) && !self.excluded_functions.contains(func)
383            }))
384        } else {
385            Either::Left(
386                self.targeted_functions
387                    .iter()
388                    .filter(|func| !self.excluded_functions.contains(func)),
389            )
390        }
391    }
392
393    pub fn rebuild_function_lookups(&mut self) {
394        let functions_by_selector =
395            self.abi.functions().fold(FunctionLookup::default(), |mut functions, function| {
396                functions.entry(function.selector()).or_insert_with(|| function.clone());
397                functions
398            });
399        let fuzzed_functions_by_selector = self.abi_fuzzed_functions().fold(
400            FunctionLookup::default(),
401            |mut functions, function| {
402                functions.entry(function.selector()).or_insert_with(|| function.clone());
403                functions
404            },
405        );
406        self.functions_by_selector = functions_by_selector;
407        self.fuzzed_functions_by_selector = fuzzed_functions_by_selector;
408    }
409
410    /// Returns any ABI function for the given selector.
411    pub fn function_by_selector(&self, selector: Selector) -> Option<&Function> {
412        self.functions_by_selector.get(&selector)
413    }
414
415    /// Returns a fuzzable function for the given selector.
416    pub fn fuzzed_function_by_selector(&self, selector: Selector) -> Option<&Function> {
417        self.fuzzed_functions_by_selector.get(&selector)
418    }
419
420    /// Returns the function for the given selector.
421    pub fn get_function(&self, selector: Selector) -> eyre::Result<&Function> {
422        self.function_by_selector(selector)
423            .ok_or_else(|| eyre::eyre!("{} does not have the selector {selector}", self.identifier))
424    }
425
426    /// Adds the specified selectors to the targeted functions.
427    pub fn add_selectors(
428        &mut self,
429        selectors: impl IntoIterator<Item = Selector>,
430        should_exclude: bool,
431    ) -> eyre::Result<()> {
432        for selector in selectors {
433            if should_exclude {
434                self.excluded_functions.push(self.get_function(selector)?.clone());
435            } else {
436                self.targeted_functions.push(self.get_function(selector)?.clone());
437            }
438        }
439        self.rebuild_function_lookups();
440        Ok(())
441    }
442}
443
444/// Events for a targeted contract, pre-indexed for log dictionary decoding.
445#[derive(Clone, Debug, Default)]
446pub struct TargetedContractEvents {
447    by_topic: HashMap<(B256, usize), Vec<TargetedContractEvent>>,
448    anonymous: Vec<TargetedContractEvent>,
449}
450
451impl TargetedContractEvents {
452    fn new(abi: &JsonAbi) -> Self {
453        let mut events = Self::default();
454        for (order, event) in abi.events().enumerate() {
455            let event = TargetedContractEvent { order, event: event.clone() };
456            if event.event.anonymous {
457                events.anonymous.push(event);
458            } else {
459                let indexed_count = event.event.inputs.iter().filter(|input| input.indexed).count();
460                events
461                    .by_topic
462                    .entry((event.event.selector(), indexed_count))
463                    .or_default()
464                    .push(event);
465            }
466        }
467        events
468    }
469
470    pub fn by_topic(
471        &self,
472        selector: &B256,
473        indexed_count: usize,
474    ) -> Option<&[TargetedContractEvent]> {
475        self.by_topic.get(&(*selector, indexed_count)).map(Vec::as_slice)
476    }
477
478    pub fn anonymous(&self) -> &[TargetedContractEvent] {
479        &self.anonymous
480    }
481}
482
483/// Event with its flattened ABI order for preserving log decode priority.
484#[derive(Clone, Debug)]
485pub struct TargetedContractEvent {
486    event: Event,
487    order: usize,
488}
489
490impl TargetedContractEvent {
491    pub const fn order(&self) -> usize {
492        self.order
493    }
494
495    pub const fn event(&self) -> &Event {
496        &self.event
497    }
498}
499
500/// Test contract which is testing its invariants.
501#[derive(Clone, Debug)]
502pub struct InvariantContract<'a> {
503    /// Address of the test contract.
504    pub address: Address,
505    /// Name of the test contract.
506    pub name: &'a str,
507    /// Invariant functions to assert against, paired with their `fail_on_revert` config.
508    /// Stored in **source declaration order** so failure-event attribution and report
509    /// rendering match user expectations.
510    pub invariant_fns: Vec<(&'a Function, bool)>,
511    /// Index into [`Self::invariant_fns`] of the stable campaign anchor. Boolean invariant
512    /// suites use a deterministic contract-local anchor so test filters do not affect
513    /// corpus/failure namespaces.
514    pub anchor_idx: usize,
515    /// If true, `afterInvariant` function is called after each invariant run.
516    pub call_after_invariant: bool,
517    /// ABI of the test contract.
518    pub abi: &'a JsonAbi,
519}
520
521impl<'a> InvariantContract<'a> {
522    /// Creates a new invariant contract.
523    ///
524    /// Caller must ensure `invariant_fns` is non-empty and `anchor_idx < invariant_fns.len()`.
525    pub const fn new(
526        address: Address,
527        name: &'a str,
528        invariant_fns: Vec<(&'a Function, bool)>,
529        anchor_idx: usize,
530        call_after_invariant: bool,
531        abi: &'a JsonAbi,
532    ) -> Self {
533        Self { address, name, invariant_fns, anchor_idx, call_after_invariant, abi }
534    }
535
536    /// Returns the stable campaign anchor.
537    pub fn anchor(&self) -> &'a Function {
538        self.invariant_fns[self.anchor_idx].0
539    }
540
541    /// Returns true if this is an optimization mode invariant (returns int256).
542    pub fn is_optimization(&self) -> bool {
543        is_optimization_invariant(self.anchor())
544    }
545}
546
547/// Settings that determine the validity of a persisted invariant counterexample.
548///
549/// When a counterexample is replayed, it's only valid if the same contracts, selectors,
550/// senders, and fail_on_revert settings are used. Changes to unrelated code (e.g., adding
551/// a log statement) should not invalidate the counterexample.
552#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
553pub struct InvariantSettings {
554    /// Target contracts with their addresses and identifiers.
555    pub target_contracts: BTreeMap<Address, String>,
556    /// Target selectors per contract address.
557    pub target_selectors: BTreeMap<Address, Vec<Selector>>,
558    /// Target senders for the invariant test.
559    pub target_senders: Vec<Address>,
560    /// Excluded senders for the invariant test.
561    pub excluded_senders: Vec<Address>,
562    /// Whether the test should fail on any revert.
563    pub fail_on_revert: bool,
564}
565
566impl InvariantSettings {
567    /// Creates new invariant settings from the given components.
568    pub fn new(
569        targeted_contracts: &TargetedContracts,
570        sender_filters: &SenderFilters,
571        fail_on_revert: bool,
572    ) -> Self {
573        let mut target_contracts = BTreeMap::new();
574        let mut target_selectors = BTreeMap::new();
575        for (addr, contract) in &targeted_contracts.inner {
576            target_contracts.insert(*addr, contract.identifier.clone());
577            target_selectors
578                .insert(*addr, contract.abi_fuzzed_functions().map(|f| f.selector()).collect());
579        }
580
581        let mut target_senders = sender_filters.targeted.clone();
582        target_senders.sort_unstable();
583
584        let mut excluded_senders = sender_filters.excluded.clone();
585        excluded_senders.sort_unstable();
586
587        Self {
588            target_contracts,
589            target_selectors,
590            target_senders,
591            excluded_senders,
592            fail_on_revert,
593        }
594    }
595
596    /// Compares these settings with another and returns a description of what changed.
597    /// Returns `None` if the settings are equivalent.
598    pub fn diff(&self, other: &Self) -> Option<String> {
599        let mut changes = Vec::new();
600
601        if self.target_contracts != other.target_contracts {
602            let added: Vec<_> = other
603                .target_contracts
604                .iter()
605                .filter(|(addr, _)| !self.target_contracts.contains_key(*addr))
606                .map(|(_, name)| name.as_str())
607                .collect();
608            let removed: Vec<_> = self
609                .target_contracts
610                .iter()
611                .filter(|(addr, _)| !other.target_contracts.contains_key(*addr))
612                .map(|(_, name)| name.as_str())
613                .collect();
614
615            if !added.is_empty() {
616                changes.push(format!("added target contracts: {}", added.join(", ")));
617            }
618            if !removed.is_empty() {
619                changes.push(format!("removed target contracts: {}", removed.join(", ")));
620            }
621        }
622
623        if self.target_selectors != other.target_selectors {
624            changes.push("target selectors changed".to_string());
625        }
626
627        if self.target_senders != other.target_senders {
628            changes.push("target senders changed".to_string());
629        }
630
631        if self.excluded_senders != other.excluded_senders {
632            changes.push("excluded senders changed".to_string());
633        }
634
635        if self.fail_on_revert != other.fail_on_revert {
636            changes.push(format!(
637                "fail_on_revert changed from {} to {}",
638                self.fail_on_revert, other.fail_on_revert
639            ));
640        }
641
642        if changes.is_empty() { None } else { Some(changes.join(", ")) }
643    }
644}
645
646impl fmt::Display for InvariantSettings {
647    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
648        write!(
649            f,
650            "targets: {}, selectors: {}, senders: {}, excluded: {}, fail_on_revert: {}",
651            self.target_contracts.len(),
652            self.target_selectors.values().map(|v| v.len()).sum::<usize>(),
653            self.target_senders.len(),
654            self.excluded_senders.len(),
655            self.fail_on_revert,
656        )
657    }
658}
659
660#[cfg(test)]
661mod tests {
662    use super::*;
663    use crate::CallDetails;
664    use alloy_primitives::{Bytes, U256};
665    use foundry_compilers::{
666        ArtifactId,
667        artifacts::{
668            BytecodeObject, CompactBytecode, CompactContractBytecode, CompactDeployedBytecode,
669        },
670    };
671    use revm::{bytecode::Bytecode, state::Account};
672
673    fn abi_with_functions(functions: &[&str]) -> JsonAbi {
674        let mut abi = JsonAbi::new();
675        for function in functions {
676            let function = Function::parse(function).unwrap();
677            abi.functions.entry(function.name.clone()).or_default().push(function);
678        }
679        abi
680    }
681
682    fn targeted_contracts_with_functions(target: Address, functions: &[&str]) -> TargetedContracts {
683        let mut targets = TargetedContracts::new();
684        targets.inner.insert(
685            target,
686            TargetedContract::new("Target".to_string(), abi_with_functions(functions)),
687        );
688        targets
689    }
690
691    fn targeted_contracts_with_function(target: Address, function: Function) -> TargetedContracts {
692        let mut abi = JsonAbi::new();
693        abi.functions.entry(function.name.clone()).or_default().push(function);
694        let mut targets = TargetedContracts::new();
695        targets.inner.insert(target, TargetedContract::new("Target".to_string(), abi));
696        targets
697    }
698
699    fn tx(target: Address, calldata: impl Into<Bytes>) -> BasicTxDetails {
700        BasicTxDetails {
701            warp: None,
702            roll: None,
703            sender: Address::ZERO,
704            call_details: CallDetails { target, calldata: calldata.into(), value: None },
705        }
706    }
707
708    fn artifact_id(name: &str) -> ArtifactId {
709        ArtifactId {
710            path: format!("{name}.json").into(),
711            name: name.to_string(),
712            source: format!("{name}.sol").into(),
713            version: "0.8.30".parse().unwrap(),
714            build_id: "test".to_string(),
715            profile: "test".to_string(),
716        }
717    }
718
719    fn project_contracts_with_runtime_code(name: &str, code: Bytes) -> ContractsByArtifact {
720        project_contracts_with_runtime_code_and_abi(name, code, JsonAbi::new())
721    }
722
723    fn project_contracts_with_runtime_code_and_abi(
724        name: &str,
725        code: Bytes,
726        abi: JsonAbi,
727    ) -> ContractsByArtifact {
728        let deployed_bytecode = CompactDeployedBytecode {
729            bytecode: Some(CompactBytecode {
730                object: BytecodeObject::Bytecode(code),
731                source_map: None,
732                link_references: Default::default(),
733            }),
734            immutable_references: Default::default(),
735        };
736        let artifact = CompactContractBytecode {
737            abi: Some(abi),
738            bytecode: None,
739            deployed_bytecode: Some(deployed_bytecode),
740        };
741        ContractsByArtifact::new([(artifact_id(name), artifact)])
742    }
743
744    fn touched_account_with_code(code: Bytes) -> Account {
745        let mut account = Account::default();
746        account.info.balance = U256::ZERO;
747        account.info.code = Some(Bytecode::new_raw(code));
748        account.mark_touch();
749        account
750    }
751
752    #[test]
753    fn targeted_contracts_short_calldata_is_not_replayable_or_decodable() {
754        let target = Address::from([0x42; 20]);
755        let targets = targeted_contracts_with_function(target, Function::parse("foo()").unwrap());
756        let tx = tx(target, vec![0xde, 0xad, 0xbe]);
757
758        assert!(!targets.can_replay(&tx));
759        assert!(targets.fuzzed_artifacts(&tx).1.is_none());
760        assert!(targets.fuzzed_metric_key(&tx).is_none());
761    }
762
763    #[test]
764    fn abi_fuzzed_functions_filters_excluded_targeted_functions() {
765        let allowed = Function::parse("allowed()").unwrap();
766        let excluded = Function::parse("excluded()").unwrap();
767        let mut contract = TargetedContract::new("Target".to_string(), JsonAbi::new());
768        contract.targeted_functions = vec![allowed.clone(), excluded.clone()];
769        contract.excluded_functions = vec![excluded];
770
771        let selectors = contract.abi_fuzzed_functions().map(Function::selector).collect::<Vec<_>>();
772
773        assert_eq!(selectors, vec![allowed.selector()]);
774    }
775
776    #[test]
777    fn targeted_contracts_refresh_selector_lookup_after_filters() {
778        let target = Address::from([0x42; 20]);
779        let foo = Function::parse("foo()").unwrap();
780        let bar = Function::parse("bar()").unwrap();
781
782        let mut excluded = targeted_contracts_with_functions(target, &["foo()", "bar()"]);
783        excluded.inner.get_mut(&target).unwrap().add_selectors([foo.selector()], true).unwrap();
784        assert!(!excluded.can_replay(&tx(target, foo.selector().to_vec())));
785        assert!(excluded.can_replay(&tx(target, bar.selector().to_vec())));
786        assert_eq!(
787            excluded.fuzzed_artifacts(&tx(target, foo.selector().to_vec())).1.unwrap().name,
788            "foo"
789        );
790
791        let mut targeted = targeted_contracts_with_functions(target, &["foo()", "bar()"]);
792        targeted.inner.get_mut(&target).unwrap().add_selectors([foo.selector()], false).unwrap();
793        assert!(targeted.can_replay(&tx(target, foo.selector().to_vec())));
794        assert!(!targeted.can_replay(&tx(target, bar.selector().to_vec())));
795        assert_eq!(
796            targeted.fuzzed_metric_key_for_selector(target, bar.selector()).unwrap(),
797            "Target.bar"
798        );
799    }
800
801    #[test]
802    fn fuzz_run_identified_contracts_cache_fuzzed_functions_in_target_order() {
803        let first = Address::from([0x01; 20]);
804        let second = Address::from([0x02; 20]);
805        let mut targets = targeted_contracts_with_functions(second, &["bar()", "baz(uint256)"]);
806        targets.inner.insert(
807            first,
808            TargetedContract::new(
809                "First".to_string(),
810                abi_with_functions(&["foo()", "qux(address)"]),
811            ),
812        );
813        let expected = targets
814            .fuzzed_functions()
815            .map(|(address, function)| (*address, function.selector()))
816            .collect::<Vec<_>>();
817
818        let identified = FuzzRunIdentifiedContracts::new(targets, true);
819        let actual = identified
820            .fuzzed_functions()
821            .iter()
822            .map(|(address, function)| (*address, function.selector()))
823            .collect::<Vec<_>>();
824
825        assert_eq!(actual, expected);
826    }
827
828    #[test]
829    fn collect_created_contracts_caches_deployed_code_matches() {
830        let existing = Address::from([0x42; 20]);
831        let created = Address::from([0x43; 20]);
832        let setup = Address::from([0x44; 20]);
833        let untouched = Address::from([0x45; 20]);
834        let runtime_code = Bytes::from_static(&[0x60, 0x00, 0x56]);
835        let project_contracts =
836            project_contracts_with_runtime_code("DynamicTarget", runtime_code.clone());
837        let mut targets = TargetedContracts::new();
838        for address in [existing, setup, untouched] {
839            targets.inner.insert(
840                address,
841                TargetedContract::new("AlreadyTargeted".to_string(), JsonAbi::new()),
842            );
843        }
844        let identified = FuzzRunIdentifiedContracts::new(targets, true);
845
846        let mut state_changeset = StateChangeset::default();
847        state_changeset.insert(existing, touched_account_with_code(runtime_code.clone()));
848        state_changeset.insert(setup, touched_account_with_code(runtime_code.clone()));
849        state_changeset.insert(untouched, Account::default());
850        state_changeset.insert(created, touched_account_with_code(runtime_code));
851        let mut created_contracts = Vec::new();
852        let setup_contracts =
853            ContractsByAddress::from([(setup, ("Setup".to_string(), JsonAbi::new()))]);
854
855        identified
856            .collect_created_contracts(
857                &state_changeset,
858                &project_contracts,
859                &setup_contracts,
860                &ArtifactFilters::default(),
861                &mut created_contracts,
862            )
863            .unwrap();
864
865        created_contracts.sort_unstable();
866        assert_eq!(created_contracts, vec![existing, created]);
867        let targets = identified.targets();
868        assert_eq!(targets[&existing].identifier, "DynamicTarget");
869        assert_eq!(targets[&created].identifier, "DynamicTarget");
870        assert_eq!(targets[&setup].identifier, "AlreadyTargeted");
871        assert_eq!(targets[&untouched].identifier, "AlreadyTargeted");
872        drop(targets);
873
874        identified
875            .collect_created_contracts(
876                &state_changeset,
877                &Default::default(),
878                &setup_contracts,
879                &ArtifactFilters::default(),
880                &mut created_contracts,
881            )
882            .unwrap();
883
884        created_contracts.sort_unstable();
885        assert_eq!(created_contracts, vec![existing, existing, created, created]);
886    }
887
888    #[test]
889    fn collect_and_clear_created_contracts_refresh_fuzzed_function_cache() {
890        let existing = Address::from([0x42; 20]);
891        let created = Address::from([0x43; 20]);
892        let runtime_code = Bytes::from_static(&[0x60, 0x00, 0x56]);
893        let project_contracts = project_contracts_with_runtime_code_and_abi(
894            "DynamicTarget",
895            runtime_code.clone(),
896            abi_with_functions(&["dynamic(uint256)"]),
897        );
898        let identified = FuzzRunIdentifiedContracts::new(
899            targeted_contracts_with_functions(existing, &["existing()"]),
900            true,
901        );
902
903        let initial = identified
904            .fuzzed_functions()
905            .iter()
906            .map(|(address, function)| (*address, function.selector()))
907            .collect::<Vec<_>>();
908        assert_eq!(initial, vec![(existing, Function::parse("existing()").unwrap().selector())]);
909        assert_eq!(identified.fuzzed_functions_generation(), 0);
910
911        let mut state_changeset = StateChangeset::default();
912        state_changeset.insert(created, touched_account_with_code(runtime_code));
913        let mut created_contracts = Vec::new();
914
915        identified
916            .collect_created_contracts(
917                &state_changeset,
918                &project_contracts,
919                &ContractsByAddress::default(),
920                &ArtifactFilters::default(),
921                &mut created_contracts,
922            )
923            .unwrap();
924
925        let with_created = identified
926            .fuzzed_functions()
927            .iter()
928            .map(|(address, function)| (*address, function.selector()))
929            .collect::<Vec<_>>();
930        assert_eq!(identified.fuzzed_functions_generation(), 1);
931        assert_eq!(
932            with_created,
933            vec![
934                (existing, Function::parse("existing()").unwrap().selector()),
935                (created, Function::parse("dynamic(uint256)").unwrap().selector()),
936            ]
937        );
938
939        identified.clear_created_contracts(created_contracts);
940        let cleared = identified
941            .fuzzed_functions()
942            .iter()
943            .map(|(address, function)| (*address, function.selector()))
944            .collect::<Vec<_>>();
945        assert_eq!(cleared, initial);
946        assert_eq!(identified.fuzzed_functions_generation(), 2);
947    }
948}