foundry_evm_fuzz/invariant/
mod.rs

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