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