foundry_evm_fuzz/strategies/
invariants.rs1use super::{fuzz_calldata, fuzz_msg_value, fuzz_param_from_state};
2use crate::{
3 BasicTxDetails, CallDetails, FuzzFixtures,
4 invariant::{FuzzRunIdentifiedContracts, SenderFilters},
5 strategies::{
6 EvmFuzzState, FuzzStateReader, InvariantFuzzState, fuzz_calldata_from_state, fuzz_param,
7 },
8};
9use alloy_json_abi::Function;
10use alloy_primitives::{Address, U256};
11use foundry_config::InvariantConfig;
12use parking_lot::RwLock;
13use proptest::prelude::*;
14use rand::seq::IteratorRandom;
15use std::{cell::RefCell, rc::Rc, sync::Arc};
16
17#[derive(Default)]
18struct PlannedFuzzedCalls {
19 generation: u64,
20 calls: Vec<BoxedStrategy<CallDetails>>,
21}
22
23pub fn override_call_strat(
25 fuzz_state: EvmFuzzState,
26 contracts: Vec<(Address, Vec<Function>)>,
27 target: Arc<RwLock<Address>>,
28 fuzz_fixtures: FuzzFixtures,
29 dictionary_weight: u32,
30 payable_value_weight: u32,
31) -> impl Strategy<Value = CallDetails> + Send + Sync + 'static {
32 let contracts = Arc::new(contracts);
33 let contracts_ref = contracts.clone();
34 proptest::prop_oneof![
35 80 => proptest::strategy::LazyJust::new(move || *target.read()),
36 20 => any::<prop::sample::Selector>()
37 .prop_map(move |selector| {
38 let (target, _) = selector.select(contracts_ref.iter());
39 *target
40 }),
41 ]
42 .prop_flat_map(move |target_address| {
43 let fuzz_state = fuzz_state.clone();
44 let fuzz_fixtures = fuzz_fixtures.clone();
45 let contracts = contracts.clone();
46
47 let (actual_target, func) = {
48 let (actual_target, fuzzed_functions) = contracts
53 .iter()
54 .find(|(address, _)| *address == target_address)
55 .map(|(address, functions)| (*address, functions.clone()))
56 .unwrap_or_else(|| {
57 let (address, functions) = contracts
58 .iter()
59 .choose(&mut rand::rng())
60 .expect("at least one target contract");
61 (*address, functions.clone())
62 });
63 (
64 actual_target,
65 any::<prop::sample::Index>()
66 .prop_map(move |index| index.get(&fuzzed_functions).clone()),
67 )
68 };
69
70 func.prop_flat_map(move |func| {
71 fuzz_contract_with_calldata(
72 &fuzz_state,
73 &fuzz_fixtures,
74 actual_target,
75 func,
76 dictionary_weight,
77 payable_value_weight,
78 )
79 })
80 })
81}
82
83pub fn invariant_strat(
94 fuzz_state: InvariantFuzzState,
95 senders: SenderFilters,
96 contracts: FuzzRunIdentifiedContracts,
97 config: InvariantConfig,
98 fuzz_fixtures: FuzzFixtures,
99) -> impl Strategy<Value = BasicTxDetails> {
100 let senders = Rc::new(senders);
101 let dictionary_weight = config.dictionary.dictionary_weight;
102 let payable_value_weight = config.corpus.payable_value_weight;
103 let planned_calls = Rc::new(RefCell::new(PlannedFuzzedCalls::default()));
104
105 let warp_roll_strat = |cond: bool| {
107 if cond { any::<U256>().prop_map(Some).boxed() } else { Just(None).boxed() }
108 };
109
110 any::<prop::sample::Selector>()
111 .prop_flat_map(move |selector| {
112 let sender = select_random_sender(&fuzz_state, senders.clone(), dictionary_weight);
113 let call_details = {
114 let generation = contracts.fuzzed_functions_generation();
115 let mut planned_calls = planned_calls.borrow_mut();
116 if planned_calls.generation != generation || planned_calls.calls.is_empty() {
117 let functions = contracts.fuzzed_functions();
118 planned_calls.calls = functions
119 .iter()
120 .map(|(target_address, target_function)| {
121 fuzz_contract_with_calldata(
122 &fuzz_state,
123 &fuzz_fixtures,
124 *target_address,
125 target_function.clone(),
126 dictionary_weight,
127 payable_value_weight,
128 )
129 .boxed()
130 })
131 .collect();
132 planned_calls.generation = generation;
133 }
134 selector.select(planned_calls.calls.iter()).clone()
135 };
136
137 let warp = warp_roll_strat(config.max_time_delay.is_some());
138 let roll = warp_roll_strat(config.max_block_delay.is_some());
139
140 (warp, roll, sender, call_details)
141 })
142 .prop_map(move |(warp, roll, sender, call_details)| {
143 let warp =
144 warp.map(|time| time % U256::from(config.max_time_delay.unwrap_or_default()));
145 let roll =
146 roll.map(|block| block % U256::from(config.max_block_delay.unwrap_or_default()));
147 BasicTxDetails { warp, roll, sender, call_details }
148 })
149}
150
151fn select_random_sender<S: FuzzStateReader>(
156 fuzz_state: &S,
157 senders: Rc<SenderFilters>,
158 dictionary_weight: u32,
159) -> impl Strategy<Value = Address> + use<S> {
160 if senders.targeted.is_empty() {
161 let dictionary_weight = dictionary_weight.min(100);
162 proptest::prop_oneof![
163 100 - dictionary_weight => fuzz_param(&alloy_dyn_abi::DynSolType::Address),
164 dictionary_weight => fuzz_param_from_state(&alloy_dyn_abi::DynSolType::Address, fuzz_state),
165 ]
166 .prop_map(move |addr| {
167 let mut addr = addr.as_address().unwrap();
168 loop {
173 if !senders.excluded.contains(&addr) {
174 break;
175 }
176 addr = Address::random();
177 }
178 addr
179 })
180 .boxed()
181 } else {
182 any::<prop::sample::Index>().prop_map(move |index| *index.get(&senders.targeted)).boxed()
183 }
184}
185
186pub fn fuzz_contract_with_calldata<S: FuzzStateReader>(
189 fuzz_state: &S,
190 fuzz_fixtures: &FuzzFixtures,
191 target: Address,
192 func: Function,
193 dictionary_weight: u32,
194 payable_value_weight: u32,
195) -> impl Strategy<Value = CallDetails> + use<S> {
196 let is_payable = func.state_mutability == alloy_json_abi::StateMutability::Payable;
197 let dictionary_weight = dictionary_weight.min(100);
198
199 let calldata_strategy = prop_oneof![
203 100 - dictionary_weight => fuzz_calldata(func.clone(), fuzz_fixtures),
204 dictionary_weight => fuzz_calldata_from_state(func, fuzz_state, fuzz_fixtures),
205 ];
206
207 let value_strategy =
209 if is_payable { fuzz_msg_value(payable_value_weight).boxed() } else { Just(None).boxed() };
210
211 (calldata_strategy, value_strategy).prop_map(move |(calldata, value)| {
212 trace!(input=?calldata, ?value);
213 CallDetails { target, calldata, value }
214 })
215}