Skip to main content

foundry_evm_fuzz/strategies/
invariants.rs

1use super::{fuzz_calldata, fuzz_param_from_state};
2use crate::{
3    FuzzFixtures,
4    invariant::{BasicTxDetails, CallDetails, FuzzRunIdentifiedContracts, SenderFilters},
5    strategies::{EvmFuzzState, fuzz_calldata_from_state, fuzz_param},
6};
7use alloy_json_abi::Function;
8use alloy_primitives::{Address, U256};
9use parking_lot::RwLock;
10use proptest::prelude::*;
11use rand::seq::IteratorRandom;
12use std::{rc::Rc, sync::Arc};
13
14/// Given a target address, we generate random calldata.
15pub fn override_call_strat(
16    fuzz_state: EvmFuzzState,
17    contracts: FuzzRunIdentifiedContracts,
18    target: Arc<RwLock<Address>>,
19    fuzz_fixtures: FuzzFixtures,
20    max_fuzz_int: Option<U256>,
21) -> impl Strategy<Value = CallDetails> + Send + Sync + 'static {
22    let contracts_ref = contracts.targets.clone();
23    proptest::prop_oneof![
24        80 => proptest::strategy::LazyJust::new(move || *target.read()),
25        20 => any::<prop::sample::Selector>()
26            .prop_map(move |selector| *selector.select(contracts_ref.lock().keys())),
27    ]
28    .prop_flat_map(move |target_address| {
29        let fuzz_state = fuzz_state.clone();
30        let fuzz_fixtures = fuzz_fixtures.clone();
31
32        let func = {
33            let contracts = contracts.targets.lock();
34            let contract = contracts.get(&target_address).unwrap_or_else(|| {
35                // Choose a random contract if target selected by lazy strategy is not in fuzz run
36                // identified contracts. This can happen when contract is created in `setUp` call
37                // but is not included in targetContracts.
38                contracts.values().choose(&mut rand::rng()).unwrap()
39            });
40            let fuzzed_functions: Vec<_> = contract.abi_fuzzed_functions().cloned().collect();
41            any::<prop::sample::Index>().prop_map(move |index| index.get(&fuzzed_functions).clone())
42        };
43
44        func.prop_flat_map(move |func| {
45            fuzz_contract_with_calldata(
46                &fuzz_state,
47                &fuzz_fixtures,
48                target_address,
49                func,
50                max_fuzz_int,
51            )
52        })
53    })
54}
55
56/// Creates the invariant strategy.
57///
58/// Given the known and future contracts, it generates the next call by fuzzing the `caller`,
59/// `calldata` and `target`. The generated data is evaluated lazily for every single call to fully
60/// leverage the evolving fuzz dictionary.
61///
62/// The fuzzed parameters can be filtered through different methods implemented in the test
63/// contract:
64///
65/// `targetContracts()`, `targetSenders()`, `excludeContracts()`, `targetSelectors()`
66pub fn invariant_strat(
67    fuzz_state: EvmFuzzState,
68    senders: SenderFilters,
69    contracts: FuzzRunIdentifiedContracts,
70    dictionary_weight: u32,
71    fuzz_fixtures: FuzzFixtures,
72    max_fuzz_int: Option<U256>,
73) -> impl Strategy<Value = BasicTxDetails> {
74    let senders = Rc::new(senders);
75    any::<prop::sample::Selector>()
76        .prop_flat_map(move |selector| {
77            let contracts = contracts.targets.lock();
78            let functions = contracts.fuzzed_functions();
79            let (target_address, target_function) = selector.select(functions);
80            let sender =
81                select_random_sender(&fuzz_state, senders.clone(), dictionary_weight, max_fuzz_int);
82            let call_details = fuzz_contract_with_calldata(
83                &fuzz_state,
84                &fuzz_fixtures,
85                *target_address,
86                target_function.clone(),
87                max_fuzz_int,
88            );
89            (sender, call_details)
90        })
91        .prop_map(|(sender, call_details)| BasicTxDetails { sender, call_details })
92}
93
94/// Strategy to select a sender address:
95/// * If `senders` is empty, then it's either a random address (10%) or from the dictionary (90%).
96/// * If `senders` is not empty, a random address is chosen from the list of senders.
97fn select_random_sender(
98    fuzz_state: &EvmFuzzState,
99    senders: Rc<SenderFilters>,
100    dictionary_weight: u32,
101    max_fuzz_int: Option<U256>,
102) -> impl Strategy<Value = Address> + use<> {
103    if !senders.targeted.is_empty() {
104        any::<prop::sample::Index>().prop_map(move |index| *index.get(&senders.targeted)).boxed()
105    } else {
106        assert!(dictionary_weight <= 100, "dictionary_weight must be <= 100");
107        proptest::prop_oneof![
108            100 - dictionary_weight => fuzz_param(&alloy_dyn_abi::DynSolType::Address, max_fuzz_int),
109            dictionary_weight => fuzz_param_from_state(&alloy_dyn_abi::DynSolType::Address, fuzz_state, max_fuzz_int),
110        ]
111        .prop_map(move |addr| {
112            let mut addr = addr.as_address().unwrap();
113            // Make sure the selected address is not in the list of excluded senders.
114            // We don't use proptest's filter to avoid reaching the `PROPTEST_MAX_LOCAL_REJECTS`
115            // max rejects and exiting test before all runs completes.
116            // See <https://github.com/foundry-rs/foundry/issues/11369>.
117            loop {
118                if !senders.excluded.contains(&addr) {
119                    break;
120                }
121                addr = Address::random();
122            }
123            addr
124        })
125        .boxed()
126    }
127}
128
129/// Given a function, it returns a proptest strategy which generates valid abi-encoded calldata
130/// for that function's input types.
131pub fn fuzz_contract_with_calldata(
132    fuzz_state: &EvmFuzzState,
133    fuzz_fixtures: &FuzzFixtures,
134    target: Address,
135    func: Function,
136    max_fuzz_int: Option<U256>,
137) -> impl Strategy<Value = CallDetails> + use<> {
138    // We need to compose all the strategies generated for each parameter in all possible
139    // combinations.
140    // `prop_oneof!` / `TupleUnion` `Arc`s for cheap cloning.
141    prop_oneof![
142        60 => fuzz_calldata(func.clone(), fuzz_fixtures, max_fuzz_int),
143        40 => fuzz_calldata_from_state(func, fuzz_state, max_fuzz_int),
144    ]
145    .prop_map(move |calldata| {
146        trace!(input=?calldata);
147        CallDetails { target, calldata }
148    })
149}