foundry_evm_fuzz/strategies/
invariants.rs

1use super::{fuzz_calldata, fuzz_param_from_state};
2use crate::{
3    invariant::{BasicTxDetails, CallDetails, FuzzRunIdentifiedContracts, SenderFilters},
4    strategies::{fuzz_calldata_from_state, fuzz_param, EvmFuzzState},
5    FuzzFixtures,
6};
7use alloy_json_abi::Function;
8use alloy_primitives::Address;
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) -> impl Strategy<Value = CallDetails> + Send + Sync + 'static {
21    let contracts_ref = contracts.targets.clone();
22    proptest::prop_oneof![
23        80 => proptest::strategy::LazyJust::new(move || *target.read()),
24        20 => any::<prop::sample::Selector>()
25            .prop_map(move |selector| *selector.select(contracts_ref.lock().keys())),
26    ]
27    .prop_flat_map(move |target_address| {
28        let fuzz_state = fuzz_state.clone();
29        let fuzz_fixtures = fuzz_fixtures.clone();
30
31        let func = {
32            let contracts = contracts.targets.lock();
33            let contract = contracts.get(&target_address).unwrap_or_else(|| {
34                // Choose a random contract if target selected by lazy strategy is not in fuzz run
35                // identified contracts. This can happen when contract is created in `setUp` call
36                // but is not included in targetContracts.
37                contracts.values().choose(&mut rand::thread_rng()).unwrap()
38            });
39            let fuzzed_functions: Vec<_> = contract.abi_fuzzed_functions().cloned().collect();
40            any::<prop::sample::Index>().prop_map(move |index| index.get(&fuzzed_functions).clone())
41        };
42
43        func.prop_flat_map(move |func| {
44            fuzz_contract_with_calldata(&fuzz_state, &fuzz_fixtures, target_address, func)
45        })
46    })
47}
48
49/// Creates the invariant strategy.
50///
51/// Given the known and future contracts, it generates the next call by fuzzing the `caller`,
52/// `calldata` and `target`. The generated data is evaluated lazily for every single call to fully
53/// leverage the evolving fuzz dictionary.
54///
55/// The fuzzed parameters can be filtered through different methods implemented in the test
56/// contract:
57///
58/// `targetContracts()`, `targetSenders()`, `excludeContracts()`, `targetSelectors()`
59pub fn invariant_strat(
60    fuzz_state: EvmFuzzState,
61    senders: SenderFilters,
62    contracts: FuzzRunIdentifiedContracts,
63    dictionary_weight: u32,
64    fuzz_fixtures: FuzzFixtures,
65) -> impl Strategy<Value = BasicTxDetails> {
66    let senders = Rc::new(senders);
67    any::<prop::sample::Selector>()
68        .prop_flat_map(move |selector| {
69            let contracts = contracts.targets.lock();
70            let functions = contracts.fuzzed_functions();
71            let (target_address, target_function) = selector.select(functions);
72            let sender = select_random_sender(&fuzz_state, senders.clone(), dictionary_weight);
73            let call_details = fuzz_contract_with_calldata(
74                &fuzz_state,
75                &fuzz_fixtures,
76                *target_address,
77                target_function.clone(),
78            );
79            (sender, call_details)
80        })
81        .prop_map(|(sender, call_details)| BasicTxDetails { sender, call_details })
82}
83
84/// Strategy to select a sender address:
85/// * If `senders` is empty, then it's either a random address (10%) or from the dictionary (90%).
86/// * If `senders` is not empty, a random address is chosen from the list of senders.
87fn select_random_sender(
88    fuzz_state: &EvmFuzzState,
89    senders: Rc<SenderFilters>,
90    dictionary_weight: u32,
91) -> impl Strategy<Value = Address> {
92    if !senders.targeted.is_empty() {
93        any::<prop::sample::Index>().prop_map(move |index| *index.get(&senders.targeted)).boxed()
94    } else {
95        assert!(dictionary_weight <= 100, "dictionary_weight must be <= 100");
96        proptest::prop_oneof![
97            100 - dictionary_weight => fuzz_param(&alloy_dyn_abi::DynSolType::Address),
98            dictionary_weight => fuzz_param_from_state(&alloy_dyn_abi::DynSolType::Address, fuzz_state),
99        ]
100        .prop_map(move |addr| addr.as_address().unwrap())
101        // Too many exclusions can slow down testing.
102        .prop_filter("excluded sender", move |addr| !senders.excluded.contains(addr))
103        .boxed()
104    }
105}
106
107/// Given a function, it returns a proptest strategy which generates valid abi-encoded calldata
108/// for that function's input types.
109pub fn fuzz_contract_with_calldata(
110    fuzz_state: &EvmFuzzState,
111    fuzz_fixtures: &FuzzFixtures,
112    target: Address,
113    func: Function,
114) -> impl Strategy<Value = CallDetails> {
115    // We need to compose all the strategies generated for each parameter in all possible
116    // combinations.
117    // `prop_oneof!` / `TupleUnion` `Arc`s for cheap cloning.
118    prop_oneof![
119        60 => fuzz_calldata(func.clone(), fuzz_fixtures),
120        40 => fuzz_calldata_from_state(func, fuzz_state),
121    ]
122    .prop_map(move |calldata| {
123        trace!(input=?calldata);
124        CallDetails { target, calldata }
125    })
126}