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