foundry_evm_fuzz/invariant/
mod.rs

1use alloy_json_abi::{Function, JsonAbi};
2use alloy_primitives::{Address, Selector, map::HashMap};
3use foundry_compilers::artifacts::StorageLayout;
4use itertools::Either;
5use parking_lot::Mutex;
6use std::{collections::BTreeMap, sync::Arc};
7
8mod call_override;
9pub use call_override::RandomCallGenerator;
10
11mod filters;
12use crate::BasicTxDetails;
13pub use filters::{ArtifactFilters, SenderFilters};
14use foundry_common::{ContractsByAddress, ContractsByArtifact};
15use foundry_evm_core::utils::{StateChangeset, get_function};
16
17/// Contracts identified as targets during a fuzz run.
18///
19/// During execution, any newly created contract is added as target and used through the rest of
20/// the fuzz run if the collection is updatable (no `targetContract` specified in `setUp`).
21#[derive(Clone, Debug)]
22pub struct FuzzRunIdentifiedContracts {
23    /// Contracts identified as targets during a fuzz run.
24    pub targets: Arc<Mutex<TargetedContracts>>,
25    /// Whether target contracts are updatable or not.
26    pub is_updatable: bool,
27}
28
29impl FuzzRunIdentifiedContracts {
30    /// Creates a new `FuzzRunIdentifiedContracts` instance.
31    pub fn new(targets: TargetedContracts, is_updatable: bool) -> Self {
32        Self { targets: Arc::new(Mutex::new(targets)), is_updatable }
33    }
34
35    /// If targets are updatable, collect all contracts created during an invariant run (which
36    /// haven't been discovered yet).
37    pub fn collect_created_contracts(
38        &self,
39        state_changeset: &StateChangeset,
40        project_contracts: &ContractsByArtifact,
41        setup_contracts: &ContractsByAddress,
42        artifact_filters: &ArtifactFilters,
43        created_contracts: &mut Vec<Address>,
44    ) -> eyre::Result<()> {
45        if !self.is_updatable {
46            return Ok(());
47        }
48
49        let mut targets = self.targets.lock();
50        for (address, account) in state_changeset {
51            if setup_contracts.contains_key(address) {
52                continue;
53            }
54            if !account.is_touched() {
55                continue;
56            }
57            let Some(code) = &account.info.code else {
58                continue;
59            };
60            if code.is_empty() {
61                continue;
62            }
63            let Some((artifact, contract)) =
64                project_contracts.find_by_deployed_code(code.original_byte_slice())
65            else {
66                continue;
67            };
68            let Some(functions) =
69                artifact_filters.get_targeted_functions(artifact, &contract.abi)?
70            else {
71                continue;
72            };
73            created_contracts.push(*address);
74            let contract = TargetedContract {
75                identifier: artifact.name.clone(),
76                abi: contract.abi.clone(),
77                targeted_functions: functions,
78                excluded_functions: Vec::new(),
79                storage_layout: contract.storage_layout.as_ref().map(Arc::clone),
80            };
81            targets.insert(*address, contract);
82        }
83        Ok(())
84    }
85
86    /// Clears targeted contracts created during an invariant run.
87    pub fn clear_created_contracts(&self, created_contracts: Vec<Address>) {
88        if !created_contracts.is_empty() {
89            let mut targets = self.targets.lock();
90            for addr in &created_contracts {
91                targets.remove(addr);
92            }
93        }
94    }
95}
96
97/// A collection of contracts identified as targets for invariant testing.
98#[derive(Clone, Debug, Default)]
99pub struct TargetedContracts {
100    /// The inner map of targeted contracts.
101    pub inner: BTreeMap<Address, TargetedContract>,
102}
103
104impl TargetedContracts {
105    /// Returns a new `TargetedContracts` instance.
106    pub fn new() -> Self {
107        Self::default()
108    }
109
110    /// Returns fuzzed contract abi and fuzzed function from address and provided calldata.
111    ///
112    /// Used to decode return values and logs in order to add values into fuzz dictionary.
113    pub fn fuzzed_artifacts(&self, tx: &BasicTxDetails) -> (Option<&JsonAbi>, Option<&Function>) {
114        match self.inner.get(&tx.call_details.target) {
115            Some(c) => (
116                Some(&c.abi),
117                c.abi.functions().find(|f| f.selector() == tx.call_details.calldata[..4]),
118            ),
119            None => (None, None),
120        }
121    }
122
123    /// Returns flatten target contract address and functions to be fuzzed.
124    /// Includes contract targeted functions if specified, else all mutable contract functions.
125    pub fn fuzzed_functions(&self) -> impl Iterator<Item = (&Address, &Function)> {
126        self.inner
127            .iter()
128            .filter(|(_, c)| !c.abi.functions.is_empty())
129            .flat_map(|(contract, c)| c.abi_fuzzed_functions().map(move |f| (contract, f)))
130    }
131
132    /// Returns whether the given transaction can be replayed or not with known contracts.
133    pub fn can_replay(&self, tx: &BasicTxDetails) -> bool {
134        match self.inner.get(&tx.call_details.target) {
135            Some(c) => c.abi.functions().any(|f| f.selector() == tx.call_details.calldata[..4]),
136            None => false,
137        }
138    }
139
140    /// Identifies fuzzed contract and function based on given tx details and returns unique metric
141    /// key composed from contract identifier and function name.
142    pub fn fuzzed_metric_key(&self, tx: &BasicTxDetails) -> Option<String> {
143        self.inner.get(&tx.call_details.target).and_then(|contract| {
144            contract
145                .abi
146                .functions()
147                .find(|f| f.selector() == tx.call_details.calldata[..4])
148                .map(|function| format!("{}.{}", contract.identifier.clone(), function.name))
149        })
150    }
151
152    /// Returns a map of contract addresses to their storage layouts.
153    pub fn get_storage_layouts(&self) -> HashMap<Address, Arc<StorageLayout>> {
154        self.inner
155            .iter()
156            .filter_map(|(addr, c)| {
157                c.storage_layout.as_ref().map(|layout| (*addr, Arc::clone(layout)))
158            })
159            .collect()
160    }
161}
162
163impl std::ops::Deref for TargetedContracts {
164    type Target = BTreeMap<Address, TargetedContract>;
165
166    fn deref(&self) -> &Self::Target {
167        &self.inner
168    }
169}
170
171impl std::ops::DerefMut for TargetedContracts {
172    fn deref_mut(&mut self) -> &mut Self::Target {
173        &mut self.inner
174    }
175}
176
177/// A contract identified as target for invariant testing.
178#[derive(Clone, Debug)]
179pub struct TargetedContract {
180    /// The contract identifier. This is only used in error messages.
181    pub identifier: String,
182    /// The contract's ABI.
183    pub abi: JsonAbi,
184    /// The targeted functions of the contract.
185    pub targeted_functions: Vec<Function>,
186    /// The excluded functions of the contract.
187    pub excluded_functions: Vec<Function>,
188    /// The contract's storage layout, if available.
189    pub storage_layout: Option<Arc<StorageLayout>>,
190}
191
192impl TargetedContract {
193    /// Returns a new `TargetedContract` instance.
194    pub fn new(identifier: String, abi: JsonAbi) -> Self {
195        Self {
196            identifier,
197            abi,
198            targeted_functions: Vec::new(),
199            excluded_functions: Vec::new(),
200            storage_layout: None,
201        }
202    }
203
204    /// Determines contract storage layout from project contracts. Needs `storageLayout` to be
205    /// enabled as extra output in project configuration.
206    pub fn with_project_contracts(mut self, project_contracts: &ContractsByArtifact) -> Self {
207        if let Some((src, name)) = self.identifier.split_once(':')
208            && let Some((_, contract_data)) = project_contracts.iter().find(|(artifact, _)| {
209                artifact.name == name && artifact.source.as_path().ends_with(src)
210            })
211        {
212            self.storage_layout = contract_data.storage_layout.as_ref().map(Arc::clone);
213        }
214        self
215    }
216
217    /// Helper to retrieve functions to fuzz for specified abi.
218    /// Returns specified targeted functions if any, else mutable abi functions that are not
219    /// marked as excluded.
220    pub fn abi_fuzzed_functions(&self) -> impl Iterator<Item = &Function> {
221        if !self.targeted_functions.is_empty() {
222            Either::Left(self.targeted_functions.iter())
223        } else {
224            Either::Right(self.abi.functions().filter(|&func| {
225                !matches!(
226                    func.state_mutability,
227                    alloy_json_abi::StateMutability::Pure | alloy_json_abi::StateMutability::View
228                ) && !self.excluded_functions.contains(func)
229            }))
230        }
231    }
232
233    /// Returns the function for the given selector.
234    pub fn get_function(&self, selector: Selector) -> eyre::Result<&Function> {
235        get_function(&self.identifier, selector, &self.abi)
236    }
237
238    /// Adds the specified selectors to the targeted functions.
239    pub fn add_selectors(
240        &mut self,
241        selectors: impl IntoIterator<Item = Selector>,
242        should_exclude: bool,
243    ) -> eyre::Result<()> {
244        for selector in selectors {
245            if should_exclude {
246                self.excluded_functions.push(self.get_function(selector)?.clone());
247            } else {
248                self.targeted_functions.push(self.get_function(selector)?.clone());
249            }
250        }
251        Ok(())
252    }
253}
254
255/// Test contract which is testing its invariants.
256#[derive(Clone, Debug)]
257pub struct InvariantContract<'a> {
258    /// Address of the test contract.
259    pub address: Address,
260    /// Invariant function present in the test contract.
261    pub invariant_function: &'a Function,
262    /// If true, `afterInvariant` function is called after each invariant run.
263    pub call_after_invariant: bool,
264    /// ABI of the test contract.
265    pub abi: &'a JsonAbi,
266}