foundry_evm_fuzz/strategies/
tx.rs1use 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#[derive(Clone)]
25pub struct TxGenerator {
26 strategy: BoxedStrategy<BasicTxDetails>,
27}
28
29impl TxGenerator {
30 pub const fn from_strategy(strategy: BoxedStrategy<BasicTxDetails>) -> Self {
32 Self { strategy }
33 }
34 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 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 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 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 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}