foundry_evm_fuzz/strategies/
invariants.rs

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