foundry_evm_fuzz/invariant/
mod.rs1use 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
17pub fn is_optimization_invariant(func: &Function) -> bool {
20 func.outputs.len() == 1 && func.outputs[0].ty == "int256"
21}
22
23#[derive(Clone, Debug)]
28pub struct FuzzRunIdentifiedContracts {
29 pub targets: Arc<Mutex<TargetedContracts>>,
31 pub is_updatable: bool,
33}
34
35impl FuzzRunIdentifiedContracts {
36 pub fn new(targets: TargetedContracts, is_updatable: bool) -> Self {
38 Self { targets: Arc::new(Mutex::new(targets)), is_updatable }
39 }
40
41 pub fn collect_created_contracts(
44 &self,
45 state_changeset: &StateChangeset,
46 project_contracts: &ContractsByArtifact,
47 setup_contracts: &ContractsByAddress,
48 artifact_filters: &ArtifactFilters,
49 created_contracts: &mut Vec<Address>,
50 ) -> eyre::Result<()> {
51 if !self.is_updatable {
52 return Ok(());
53 }
54
55 let mut targets = self.targets.lock();
56 for (address, account) in state_changeset {
57 if setup_contracts.contains_key(address) {
58 continue;
59 }
60 if !account.is_touched() {
61 continue;
62 }
63 let Some(code) = &account.info.code else {
64 continue;
65 };
66 if code.is_empty() {
67 continue;
68 }
69 let Some((artifact, contract)) =
70 project_contracts.find_by_deployed_code(code.original_byte_slice())
71 else {
72 continue;
73 };
74 let Some(functions) =
75 artifact_filters.get_targeted_functions(artifact, &contract.abi)?
76 else {
77 continue;
78 };
79 created_contracts.push(*address);
80 let contract = TargetedContract {
81 identifier: artifact.name.clone(),
82 abi: contract.abi.clone(),
83 targeted_functions: functions,
84 excluded_functions: Vec::new(),
85 storage_layout: contract.storage_layout.as_ref().map(Arc::clone),
86 };
87 targets.insert(*address, contract);
88 }
89 Ok(())
90 }
91
92 pub fn clear_created_contracts(&self, created_contracts: Vec<Address>) {
94 if !created_contracts.is_empty() {
95 let mut targets = self.targets.lock();
96 for addr in &created_contracts {
97 targets.remove(addr);
98 }
99 }
100 }
101}
102
103#[derive(Clone, Debug, Default)]
105pub struct TargetedContracts {
106 pub inner: BTreeMap<Address, TargetedContract>,
108}
109
110impl TargetedContracts {
111 pub fn new() -> Self {
113 Self::default()
114 }
115
116 pub fn fuzzed_artifacts(&self, tx: &BasicTxDetails) -> (Option<&JsonAbi>, Option<&Function>) {
120 match self.inner.get(&tx.call_details.target) {
121 Some(c) => (
122 Some(&c.abi),
123 c.abi.functions().find(|f| f.selector() == tx.call_details.calldata[..4]),
124 ),
125 None => (None, None),
126 }
127 }
128
129 pub fn fuzzed_functions(&self) -> impl Iterator<Item = (&Address, &Function)> {
132 self.inner
133 .iter()
134 .filter(|(_, c)| !c.abi.functions.is_empty())
135 .flat_map(|(contract, c)| c.abi_fuzzed_functions().map(move |f| (contract, f)))
136 }
137
138 pub fn can_replay(&self, tx: &BasicTxDetails) -> bool {
140 match self.inner.get(&tx.call_details.target) {
141 Some(c) => c.abi.functions().any(|f| f.selector() == tx.call_details.calldata[..4]),
142 None => false,
143 }
144 }
145
146 pub fn fuzzed_metric_key(&self, tx: &BasicTxDetails) -> Option<String> {
149 self.inner.get(&tx.call_details.target).and_then(|contract| {
150 contract
151 .abi
152 .functions()
153 .find(|f| f.selector() == tx.call_details.calldata[..4])
154 .map(|function| format!("{}.{}", contract.identifier.clone(), function.name))
155 })
156 }
157
158 pub fn get_storage_layouts(&self) -> HashMap<Address, Arc<StorageLayout>> {
160 self.inner
161 .iter()
162 .filter_map(|(addr, c)| {
163 c.storage_layout.as_ref().map(|layout| (*addr, Arc::clone(layout)))
164 })
165 .collect()
166 }
167}
168
169impl std::ops::Deref for TargetedContracts {
170 type Target = BTreeMap<Address, TargetedContract>;
171
172 fn deref(&self) -> &Self::Target {
173 &self.inner
174 }
175}
176
177impl std::ops::DerefMut for TargetedContracts {
178 fn deref_mut(&mut self) -> &mut Self::Target {
179 &mut self.inner
180 }
181}
182
183#[derive(Clone, Debug)]
185pub struct TargetedContract {
186 pub identifier: String,
188 pub abi: JsonAbi,
190 pub targeted_functions: Vec<Function>,
192 pub excluded_functions: Vec<Function>,
194 pub storage_layout: Option<Arc<StorageLayout>>,
196}
197
198impl TargetedContract {
199 pub fn new(identifier: String, abi: JsonAbi) -> Self {
201 Self {
202 identifier,
203 abi,
204 targeted_functions: Vec::new(),
205 excluded_functions: Vec::new(),
206 storage_layout: None,
207 }
208 }
209
210 pub fn with_project_contracts(mut self, project_contracts: &ContractsByArtifact) -> Self {
213 if let Some((src, name)) = self.identifier.split_once(':')
214 && let Some((_, contract_data)) = project_contracts.iter().find(|(artifact, _)| {
215 artifact.name == name && artifact.source.as_path().ends_with(src)
216 })
217 {
218 self.storage_layout = contract_data.storage_layout.as_ref().map(Arc::clone);
219 }
220 self
221 }
222
223 pub fn abi_fuzzed_functions(&self) -> impl Iterator<Item = &Function> {
227 if !self.targeted_functions.is_empty() {
228 Either::Left(self.targeted_functions.iter())
229 } else {
230 Either::Right(self.abi.functions().filter(|&func| {
231 !matches!(
232 func.state_mutability,
233 alloy_json_abi::StateMutability::Pure | alloy_json_abi::StateMutability::View
234 ) && !self.excluded_functions.contains(func)
235 }))
236 }
237 }
238
239 pub fn get_function(&self, selector: Selector) -> eyre::Result<&Function> {
241 get_function(&self.identifier, selector, &self.abi)
242 }
243
244 pub fn add_selectors(
246 &mut self,
247 selectors: impl IntoIterator<Item = Selector>,
248 should_exclude: bool,
249 ) -> eyre::Result<()> {
250 for selector in selectors {
251 if should_exclude {
252 self.excluded_functions.push(self.get_function(selector)?.clone());
253 } else {
254 self.targeted_functions.push(self.get_function(selector)?.clone());
255 }
256 }
257 Ok(())
258 }
259}
260
261#[derive(Clone, Debug)]
263pub struct InvariantContract<'a> {
264 pub address: Address,
266 pub invariant_function: &'a Function,
268 pub call_after_invariant: bool,
270 pub abi: &'a JsonAbi,
272}
273
274impl<'a> InvariantContract<'a> {
275 pub fn new(
277 address: Address,
278 invariant_function: &'a Function,
279 call_after_invariant: bool,
280 abi: &'a JsonAbi,
281 ) -> Self {
282 Self { address, invariant_function, call_after_invariant, abi }
283 }
284
285 pub fn is_optimization(&self) -> bool {
287 is_optimization_invariant(self.invariant_function)
288 }
289}