Skip to main content

foundry_evm_fuzz/strategies/
invariants.rs

1use 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
23/// Given a target address, we generate random calldata.
24pub 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            // If the target address is in the contracts map, use it directly.
49            // Otherwise, fall back to a random contract from the targeted contracts.
50            // This can happen when call_override sets target_reference to a contract
51            // that is not in targetContracts (e.g., the protocol contract during reentrancy).
52            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
83/// Creates the invariant strategy.
84///
85/// Given the known and future contracts, it generates the next call by fuzzing the `caller`,
86/// `calldata` and `target`. The generated data is evaluated lazily for every single call to fully
87/// leverage the evolving fuzz dictionary.
88///
89/// The fuzzed parameters can be filtered through different methods implemented in the test
90/// contract:
91///
92/// `targetContracts()`, `targetSenders()`, `excludeContracts()`, `targetSelectors()`
93pub 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    // Strategy to generate values for tx warp and roll.
106    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
151/// Strategy to select a sender address:
152/// * If `senders` is empty, then it's either a random address or one sampled from the dictionary
153///   according to the configured dictionary weight.
154/// * If `senders` is not empty, a random address is chosen from the list of senders.
155fn 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            // Make sure the selected address is not in the list of excluded senders.
169            // We don't use proptest's filter to avoid reaching the `PROPTEST_MAX_LOCAL_REJECTS`
170            // max rejects and exiting test before all runs completes.
171            // See <https://github.com/foundry-rs/foundry/issues/11369>.
172            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
186/// Given a function, it returns a proptest strategy which generates valid abi-encoded calldata
187/// for that function's input types.
188pub 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    // We need to compose all the strategies generated for each parameter in all possible
200    // combinations.
201    // `prop_oneof!` / `TupleUnion` `Arc`s for cheap cloning.
202    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    // For payable functions, generate random value using shared strategy.
208    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}