Skip to main content

foundry_evm_fuzz/strategies/
tx.rs

1use super::{
2    DictionaryRead, EvmFuzzState, FuzzState, fuzz_calldata, fuzz_calldata_from_state,
3    fuzz_msg_value, fuzz_param, fuzz_param_from_state,
4};
5use crate::{
6    BasicTxDetails, CallDetails, FuzzFixtures,
7    invariant::{FuzzRunIdentifiedContracts, SenderFilters},
8};
9use alloy_dyn_abi::DynSolType;
10use alloy_json_abi::Function;
11use alloy_primitives::{Address, U256};
12use eyre::{Result, eyre};
13use foundry_config::InvariantConfig;
14use proptest::{prelude::*, test_runner::TestRunner};
15use std::{cell::RefCell, rc::Rc};
16
17#[derive(Default)]
18struct PlannedCalls {
19    generation: u64,
20    calls: Vec<BoxedStrategy<CallDetails>>,
21}
22
23/// Concrete generator for stateless and invariant transactions.
24#[derive(Clone)]
25pub struct TxGenerator {
26    strategy: BoxedStrategy<BasicTxDetails>,
27}
28
29impl TxGenerator {
30    /// Wraps a prebuilt strategy, primarily for deterministic tests.
31    pub const fn from_strategy(strategy: BoxedStrategy<BasicTxDetails>) -> Self {
32        Self { strategy }
33    }
34    /// Creates a fixed-target, fixed-sender stateless generator.
35    pub fn stateless(
36        state: EvmFuzzState,
37        fixtures: FuzzFixtures,
38        target: Address,
39        sender: Address,
40        function: Function,
41        dictionary_weight: u32,
42        payable_value_weight: u32,
43    ) -> Self {
44        let call = Self::call_strategy(
45            &state,
46            &fixtures,
47            target,
48            function,
49            dictionary_weight,
50            payable_value_weight,
51        );
52        Self {
53            strategy: call
54                .prop_map(move |call_details| BasicTxDetails {
55                    warp: None,
56                    roll: None,
57                    sender,
58                    call_details,
59                })
60                .boxed(),
61        }
62    }
63
64    /// Creates a lazy invariant generator whose target list follows deployed contracts.
65    pub fn invariant(
66        state: FuzzState,
67        senders: SenderFilters,
68        contracts: FuzzRunIdentifiedContracts,
69        config: InvariantConfig,
70        fixtures: FuzzFixtures,
71    ) -> Self {
72        let senders = Rc::new(senders);
73        let dictionary_weight = config.dictionary.dictionary_weight;
74        let payable_value_weight = config.corpus.payable_value_weight;
75        let planned = Rc::new(RefCell::new(PlannedCalls::default()));
76        let strategy = any::<prop::sample::Selector>()
77            .prop_flat_map(move |selector| {
78                let sender = select_sender(&state, senders.clone(), dictionary_weight);
79                let call = {
80                    let generation = contracts.fuzzed_functions_generation();
81                    let mut planned = planned.borrow_mut();
82                    if planned.generation != generation || planned.calls.is_empty() {
83                        planned.calls = contracts
84                            .fuzzed_functions()
85                            .iter()
86                            .map(|(target, function)| {
87                                Self::call_strategy(
88                                    &state,
89                                    &fixtures,
90                                    *target,
91                                    function.clone(),
92                                    dictionary_weight,
93                                    payable_value_weight,
94                                )
95                            })
96                            .collect();
97                        planned.generation = generation;
98                    }
99                    selector.select(planned.calls.iter()).clone()
100                };
101                let warp = optional_delay(config.max_time_delay);
102                let roll = optional_delay(config.max_block_delay);
103                (warp, roll, sender, call)
104            })
105            .prop_map(move |(warp, roll, sender, call_details)| BasicTxDetails {
106                warp,
107                roll,
108                sender,
109                call_details,
110            })
111            .boxed();
112        Self { strategy }
113    }
114
115    /// Draws the next transaction from this generator.
116    pub fn next_tx(&self, runner: &mut TestRunner) -> Result<BasicTxDetails> {
117        Ok(self.strategy.new_tree(runner).map_err(|_| eyre!("Could not generate case"))?.current())
118    }
119
120    /// Generates calldata and payable value for one contract call.
121    pub(crate) fn call_strategy<S: DictionaryRead>(
122        state: &S,
123        fixtures: &FuzzFixtures,
124        target: Address,
125        function: Function,
126        dictionary_weight: u32,
127        payable_value_weight: u32,
128    ) -> BoxedStrategy<CallDetails> {
129        let payable = function.state_mutability == alloy_json_abi::StateMutability::Payable;
130        let dictionary_weight = dictionary_weight.min(100);
131        let calldata = prop_oneof![
132            100 - dictionary_weight => fuzz_calldata(function.clone(), fixtures),
133            dictionary_weight => fuzz_calldata_from_state(function, state, fixtures),
134        ];
135        let value =
136            if payable { fuzz_msg_value(payable_value_weight).boxed() } else { Just(None).boxed() };
137        (calldata, value)
138            .prop_map(move |(calldata, value)| CallDetails { target, calldata, value })
139            .boxed()
140    }
141}
142
143fn optional_delay(max: Option<u32>) -> BoxedStrategy<Option<U256>> {
144    if let Some(max) = max.filter(|max| *max > 0) {
145        any::<U256>().prop_map(move |value| Some(value % U256::from(max))).boxed()
146    } else {
147        Just(None).boxed()
148    }
149}
150
151fn select_sender(
152    state: &FuzzState,
153    senders: Rc<SenderFilters>,
154    dictionary_weight: u32,
155) -> BoxedStrategy<Address> {
156    if senders.targeted.is_empty() {
157        let dictionary_weight = dictionary_weight.min(100);
158        prop_oneof![
159            100 - dictionary_weight => fuzz_param(&DynSolType::Address),
160            dictionary_weight => fuzz_param_from_state(&DynSolType::Address, state),
161        ]
162        .prop_map(move |value| {
163            let mut sender = value.as_address().unwrap();
164            while senders.excluded.contains(&sender) {
165                sender = Address::random();
166            }
167            sender
168        })
169        .boxed()
170    } else {
171        any::<prop::sample::Index>().prop_map(move |index| *index.get(&senders.targeted)).boxed()
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178    use crate::invariant::{TargetedContract, TargetedContracts};
179    use alloy_json_abi::JsonAbi;
180    use foundry_config::FuzzDictionaryConfig;
181    use revm::database::{CacheDB, EmptyDB};
182
183    #[test]
184    fn zero_delay_is_disabled() {
185        let mut runner = TestRunner::deterministic();
186        assert_eq!(optional_delay(Some(0)).new_tree(&mut runner).unwrap().current(), None);
187        assert_eq!(
188            optional_delay(Some(1)).new_tree(&mut runner).unwrap().current(),
189            Some(U256::ZERO)
190        );
191    }
192
193    #[test]
194    fn stateless_generator_has_fixed_metadata() {
195        let target = Address::with_last_byte(1);
196        let sender = Address::with_last_byte(2);
197        let function = Function::parse("fuzz(uint256)").unwrap();
198        let generator = TxGenerator::stateless(
199            EvmFuzzState::test(),
200            FuzzFixtures::default(),
201            target,
202            sender,
203            function,
204            40,
205            10,
206        );
207        let mut runner = TestRunner::deterministic();
208        let tx = generator.next_tx(&mut runner).unwrap();
209        assert_eq!(tx.sender, sender);
210        assert_eq!(tx.call_details.target, target);
211        assert_eq!(tx.warp, None);
212        assert_eq!(tx.roll, None);
213    }
214
215    #[test]
216    fn invariant_generator_refreshes_removed_targets_lazily() {
217        let retained = Address::with_last_byte(1);
218        let removed = Address::with_last_byte(2);
219        let function = Function::parse("fuzz(uint256)").unwrap();
220        let mut abi = JsonAbi::new();
221        abi.functions.entry(function.name.clone()).or_default().push(function);
222        let mut targets = TargetedContracts::new();
223        targets.insert(retained, TargetedContract::new("Retained".into(), abi.clone()));
224        targets.insert(removed, TargetedContract::new("Removed".into(), abi));
225        let identified = FuzzRunIdentifiedContracts::new(targets, false);
226        let state = EvmFuzzState::new(
227            &[],
228            &CacheDB::<EmptyDB>::default(),
229            FuzzDictionaryConfig::default(),
230            None,
231        )
232        .into_invariant();
233        let generator = TxGenerator::invariant(
234            state,
235            SenderFilters::default(),
236            identified.clone(),
237            InvariantConfig::default(),
238            FuzzFixtures::default(),
239        );
240        let mut runner = TestRunner::deterministic();
241
242        // Populate the lazy cache while both calls are available, then invalidate it solely via
243        // the public lifecycle API used after invariant runs.
244        let _ = generator.next_tx(&mut runner).unwrap();
245        identified.clear_created_contracts(vec![removed]);
246        for _ in 0..32 {
247            assert_eq!(generator.next_tx(&mut runner).unwrap().call_details.target, retained);
248        }
249    }
250}