@@ -6,7 +6,7 @@ use eyre::Result;
6
6
use foundry_common:: evm:: Breakpoints ;
7
7
use foundry_config:: FuzzConfig ;
8
8
use foundry_evm_core:: {
9
- constants:: { CHEATCODE_ADDRESS , MAGIC_ASSUME , TEST_TIMEOUT } ,
9
+ constants:: { CHEATCODE_ADDRESS , MAGIC_ASSUME } ,
10
10
decode:: { RevertDecoder , SkipReason } ,
11
11
} ;
12
12
use foundry_evm_coverage:: HitMaps ;
@@ -16,7 +16,10 @@ use foundry_evm_fuzz::{
16
16
} ;
17
17
use foundry_evm_traces:: SparsedTraceArena ;
18
18
use indicatif:: ProgressBar ;
19
- use proptest:: test_runner:: { TestCaseError , TestError , TestRunner } ;
19
+ use proptest:: {
20
+ strategy:: { Strategy , ValueTree } ,
21
+ test_runner:: { TestCaseError , TestRunner } ,
22
+ } ;
20
23
use std:: { cell:: RefCell , collections:: BTreeMap } ;
21
24
22
25
mod types;
@@ -78,7 +81,7 @@ impl FuzzedExecutor {
78
81
///
79
82
/// Returns a list of all the consumed gas and calldata of every fuzz case
80
83
pub fn fuzz (
81
- & self ,
84
+ & mut self ,
82
85
func : & Function ,
83
86
fuzz_fixtures : & FuzzFixtures ,
84
87
deployed_libs : & [ Address ] ,
@@ -101,69 +104,99 @@ impl FuzzedExecutor {
101
104
// Start timer for this fuzz test.
102
105
let timer = FuzzTestTimer :: new ( self . config . timeout ) ;
103
106
104
- let run_result = self . runner . clone ( ) . run ( & strategy , |calldata | {
105
- // Check if the timeout has been reached .
106
- if timer . is_timed_out ( ) {
107
- return Err ( TestCaseError :: fail ( TEST_TIMEOUT ) ) ;
107
+ let continue_campaign = | runs : u32 | {
108
+ // If timeout is configured, then perform invariant runs until expires .
109
+ if self . config . timeout . is_some ( ) {
110
+ return !timer . is_timed_out ( ) ;
108
111
}
112
+ // If no timeout configured then loop until configured runs.
113
+ runs < self . config . runs
114
+ } ;
109
115
110
- let fuzz_res = self . single_fuzz ( address, calldata) ?;
116
+ let mut runs = 0 ;
117
+ let mut rejects = 0 ;
118
+ let mut run_failure = None ;
119
+ ' stop: while continue_campaign ( runs) {
120
+ let Ok ( strategy) = strategy. new_tree ( & mut self . runner ) else {
121
+ run_failure = Some ( TestCaseError :: fail ( "no input generated to call fuzzed target" ) ) ;
122
+ break ' stop;
123
+ } ;
111
124
112
125
// If running with progress then increment current run.
113
126
if let Some ( progress) = progress {
114
127
progress. inc ( 1 ) ;
115
128
} ;
116
129
117
- match fuzz_res {
118
- FuzzOutcome :: Case ( case) => {
119
- let mut data = execution_data. borrow_mut ( ) ;
120
- data. gas_by_case . push ( ( case. case . gas , case. case . stipend ) ) ;
130
+ match self . single_fuzz ( address, strategy. current ( ) ) {
131
+ Ok ( fuzz_outcome) => {
132
+ match fuzz_outcome {
133
+ FuzzOutcome :: Case ( case) => {
134
+ let mut data = execution_data. borrow_mut ( ) ;
135
+ data. gas_by_case . push ( ( case. case . gas , case. case . stipend ) ) ;
121
136
122
- if data. first_case . is_none ( ) {
123
- data. first_case . replace ( case. case ) ;
124
- }
137
+ if data. first_case . is_none ( ) {
138
+ data. first_case . replace ( case. case ) ;
139
+ }
125
140
126
- if let Some ( call_traces) = case. traces {
127
- if data. traces . len ( ) == max_traces_to_collect {
128
- data. traces . pop ( ) ;
129
- }
130
- data. traces . push ( call_traces) ;
131
- data. breakpoints . replace ( case. breakpoints ) ;
132
- }
133
-
134
- if show_logs {
135
- data. logs . extend ( case. logs ) ;
136
- }
141
+ if let Some ( call_traces) = case. traces {
142
+ if data. traces . len ( ) == max_traces_to_collect {
143
+ data. traces . pop ( ) ;
144
+ }
145
+ data. traces . push ( call_traces) ;
146
+ data. breakpoints . replace ( case. breakpoints ) ;
147
+ }
137
148
138
- HitMaps :: merge_opt ( & mut data. coverage , case. coverage ) ;
149
+ if show_logs {
150
+ data. logs . extend ( case. logs ) ;
151
+ }
139
152
140
- data. deprecated_cheatcodes = case. deprecated_cheatcodes ;
141
-
142
- Ok ( ( ) )
153
+ HitMaps :: merge_opt ( & mut data. coverage , case. coverage ) ;
154
+ data. deprecated_cheatcodes = case. deprecated_cheatcodes ;
155
+ }
156
+ FuzzOutcome :: CounterExample ( CounterExampleOutcome {
157
+ exit_reason : status,
158
+ counterexample : outcome,
159
+ ..
160
+ } ) => {
161
+ let reason = rd. maybe_decode ( & outcome. 1 . result , status) ;
162
+ execution_data. borrow_mut ( ) . logs . extend ( outcome. 1 . logs . clone ( ) ) ;
163
+ execution_data. borrow_mut ( ) . counterexample = outcome;
164
+ // HACK: we have to use an empty string here to denote `None`.
165
+ run_failure = Some ( TestCaseError :: fail ( reason. unwrap_or_default ( ) ) ) ;
166
+ break ' stop;
167
+ }
168
+ }
143
169
}
144
- FuzzOutcome :: CounterExample ( CounterExampleOutcome {
145
- exit_reason : status,
146
- counterexample : outcome,
147
- ..
148
- } ) => {
149
- // We cannot use the calldata returned by the test runner in `TestError::Fail`,
150
- // since that input represents the last run case, which may not correspond with
151
- // our failure - when a fuzz case fails, proptest will try to run at least one
152
- // more case to find a minimal failure case.
153
- let reason = rd. maybe_decode ( & outcome. 1 . result , status) ;
154
- execution_data. borrow_mut ( ) . logs . extend ( outcome. 1 . logs . clone ( ) ) ;
155
- execution_data. borrow_mut ( ) . counterexample = outcome;
156
- // HACK: we have to use an empty string here to denote `None`.
157
- Err ( TestCaseError :: fail ( reason. unwrap_or_default ( ) ) )
170
+ Err ( err) => {
171
+ match err {
172
+ TestCaseError :: Fail ( _) => {
173
+ run_failure = Some ( err) ;
174
+ break ' stop;
175
+ }
176
+ TestCaseError :: Reject ( _) => {
177
+ // Apply max rejects only if configured, otherwise silently discard run.
178
+ if self . config . max_test_rejects > 0 {
179
+ rejects += 1 ;
180
+ if rejects >= self . config . max_test_rejects {
181
+ run_failure = Some ( err) ;
182
+ break ' stop;
183
+ }
184
+ } else {
185
+ continue ' stop;
186
+ }
187
+ }
188
+ }
158
189
}
159
190
}
160
- } ) ;
191
+
192
+ runs += 1 ;
193
+ }
161
194
162
195
let fuzz_result = execution_data. into_inner ( ) ;
163
196
let ( calldata, call) = fuzz_result. counterexample ;
164
197
165
198
let mut traces = fuzz_result. traces ;
166
- let ( last_run_traces, last_run_breakpoints) = if run_result . is_ok ( ) {
199
+ let ( last_run_traces, last_run_breakpoints) = if run_failure . is_none ( ) {
167
200
( traces. pop ( ) , fuzz_result. breakpoints )
168
201
} else {
169
202
( call. traces . clone ( ) , call. cheatcodes . map ( |c| c. breakpoints ) )
@@ -172,7 +205,7 @@ impl FuzzedExecutor {
172
205
let mut result = FuzzTestResult {
173
206
first_case : fuzz_result. first_case . unwrap_or_default ( ) ,
174
207
gas_by_case : fuzz_result. gas_by_case ,
175
- success : run_result . is_ok ( ) ,
208
+ success : run_failure . is_none ( ) ,
176
209
skipped : false ,
177
210
reason : None ,
178
211
counterexample : None ,
@@ -185,38 +218,24 @@ impl FuzzedExecutor {
185
218
deprecated_cheatcodes : fuzz_result. deprecated_cheatcodes ,
186
219
} ;
187
220
188
- match run_result {
189
- Ok ( ( ) ) => { }
190
- Err ( TestError :: Abort ( reason) ) => {
191
- let msg = reason. message ( ) ;
192
- // Currently the only operation that can trigger proptest global rejects is the
193
- // `vm.assume` cheatcode, thus we surface this info to the user when the fuzz test
194
- // aborts due to too many global rejects, making the error message more actionable.
195
- result. reason = if msg == "Too many global rejects" {
196
- let error = FuzzError :: TooManyRejects ( self . runner . config ( ) . max_global_rejects ) ;
197
- Some ( error. to_string ( ) )
221
+ match run_failure {
222
+ Some ( TestCaseError :: Fail ( reason) ) => {
223
+ let reason = reason. to_string ( ) ;
224
+ result. reason = ( !reason. is_empty ( ) ) . then_some ( reason) ;
225
+ let args = if let Some ( data) = calldata. get ( 4 ..) {
226
+ func. abi_decode_input ( data) . unwrap_or_default ( )
198
227
} else {
199
- Some ( msg . to_string ( ) )
228
+ vec ! [ ]
200
229
} ;
230
+ result. counterexample = Some ( CounterExample :: Single (
231
+ BaseCounterExample :: from_fuzz_call ( calldata, args, call. traces ) ,
232
+ ) ) ;
201
233
}
202
- Err ( TestError :: Fail ( reason, _ ) ) => {
234
+ Some ( TestCaseError :: Reject ( reason) ) => {
203
235
let reason = reason. to_string ( ) ;
204
- if reason == TEST_TIMEOUT {
205
- // If the reason is a timeout, we consider the fuzz test successful.
206
- result. success = true ;
207
- } else {
208
- result. reason = ( !reason. is_empty ( ) ) . then_some ( reason) ;
209
- let args = if let Some ( data) = calldata. get ( 4 ..) {
210
- func. abi_decode_input ( data) . unwrap_or_default ( )
211
- } else {
212
- vec ! [ ]
213
- } ;
214
-
215
- result. counterexample = Some ( CounterExample :: Single (
216
- BaseCounterExample :: from_fuzz_call ( calldata, args, call. traces ) ,
217
- ) ) ;
218
- }
236
+ result. reason = ( !reason. is_empty ( ) ) . then_some ( reason) ;
219
237
}
238
+ None => { }
220
239
}
221
240
222
241
if let Some ( reason) = & result. reason
@@ -245,7 +264,9 @@ impl FuzzedExecutor {
245
264
246
265
// Handle `vm.assume`.
247
266
if call. result . as_ref ( ) == MAGIC_ASSUME {
248
- return Err ( TestCaseError :: reject ( FuzzError :: AssumeReject ) ) ;
267
+ return Err ( TestCaseError :: reject ( FuzzError :: TooManyRejects (
268
+ self . config . max_test_rejects ,
269
+ ) ) ) ;
249
270
}
250
271
251
272
let ( breakpoints, deprecated_cheatcodes) =
0 commit comments