@@ -20,10 +20,12 @@ use strategies::{
20
20
build_initial_state, collect_state_from_call, fuzz_calldata, fuzz_calldata_from_state,
21
21
EvmFuzzState ,
22
22
} ;
23
+ use types:: { CaseOutcome , CounterExampleOutcome , FuzzCase , FuzzOutcome } ;
23
24
24
25
pub mod error;
25
26
pub mod invariant;
26
27
pub mod strategies;
28
+ pub mod types;
27
29
28
30
/// Wrapper around an [`Executor`] which provides fuzzing support using [`proptest`](https://docs.rs/proptest/1.0.0/proptest/).
29
31
///
@@ -101,72 +103,45 @@ impl<'a> FuzzedExecutor<'a> {
101
103
let strat = proptest:: strategy:: Union :: new_weighted ( weights) ;
102
104
debug ! ( func = ?func. name, should_fail, "fuzzing" ) ;
103
105
let run_result = self . runner . clone ( ) . run ( & strat, |calldata| {
104
- let call = self
105
- . executor
106
- . call_raw ( self . sender , address, calldata. 0 . clone ( ) , 0 . into ( ) )
107
- . map_err ( |_| TestCaseError :: fail ( FuzzError :: FailedContractCall ) ) ?;
108
- let state_changeset = call
109
- . state_changeset
110
- . as_ref ( )
111
- . ok_or_else ( || TestCaseError :: fail ( FuzzError :: EmptyChangeset ) ) ?;
112
-
113
- // Build fuzzer state
114
- collect_state_from_call (
115
- & call. logs ,
116
- state_changeset,
117
- state. clone ( ) ,
118
- & self . config . dictionary ,
119
- ) ;
120
-
121
- // When assume cheat code is triggered return a special string "FOUNDRY::ASSUME"
122
- if call. result . as_ref ( ) == ASSUME_MAGIC_RETURN_CODE {
123
- return Err ( TestCaseError :: reject ( FuzzError :: AssumeReject ) )
124
- }
125
-
126
- let success = self . executor . is_success (
127
- address,
128
- call. reverted ,
129
- state_changeset. clone ( ) ,
130
- should_fail,
131
- ) ;
132
-
133
- if success {
134
- let mut first_case = first_case. borrow_mut ( ) ;
135
- if first_case. is_none ( ) {
136
- first_case. replace ( FuzzCase {
137
- calldata,
138
- gas : call. gas_used ,
139
- stipend : call. stipend ,
140
- } ) ;
106
+ let fuzz_res = self . single_fuzz ( & state, address, should_fail, calldata) ?;
107
+
108
+ match fuzz_res {
109
+ FuzzOutcome :: Case ( case) => {
110
+ let mut first_case = first_case. borrow_mut ( ) ;
111
+ gas_by_case. borrow_mut ( ) . push ( ( case. case . gas , case. case . stipend ) ) ;
112
+ if first_case. is_none ( ) {
113
+ first_case. replace ( case. case ) ;
114
+ }
115
+
116
+ traces. replace ( case. traces ) ;
117
+
118
+ if let Some ( prev) = coverage. take ( ) {
119
+ // Safety: If `Option::or` evaluates to `Some`, then `call.coverage` must
120
+ // necessarily also be `Some`
121
+ coverage. replace ( Some ( prev. merge ( case. coverage . unwrap ( ) ) ) ) ;
122
+ } else {
123
+ coverage. replace ( case. coverage ) ;
124
+ }
125
+
126
+ Ok ( ( ) )
141
127
}
142
- gas_by_case. borrow_mut ( ) . push ( ( call. gas_used , call. stipend ) ) ;
143
-
144
- traces. replace ( call. traces ) ;
145
-
146
- if let Some ( prev) = coverage. take ( ) {
147
- // Safety: If `Option::or` evaluates to `Some`, then `call.coverage` must
148
- // necessarily also be `Some`
149
- coverage. replace ( Some ( prev. merge ( call. coverage . unwrap ( ) ) ) ) ;
150
- } else {
151
- coverage. replace ( call. coverage ) ;
128
+ FuzzOutcome :: CounterExample ( CounterExampleOutcome {
129
+ exit_reason,
130
+ counterexample : _counterexample,
131
+ ..
132
+ } ) => {
133
+ let status = exit_reason;
134
+ // We cannot use the calldata returned by the test runner in `TestError::Fail`,
135
+ // since that input represents the last run case, which may not correspond with
136
+ // our failure - when a fuzz case fails, proptest will try
137
+ // to run at least one more case to find a minimal failure
138
+ // case.
139
+ let call_res = _counterexample. 1 . result . clone ( ) ;
140
+ * counterexample. borrow_mut ( ) = _counterexample;
141
+ Err ( TestCaseError :: fail (
142
+ decode:: decode_revert ( & call_res, errors, Some ( status) ) . unwrap_or_default ( ) ,
143
+ ) )
152
144
}
153
-
154
- Ok ( ( ) )
155
- } else {
156
- let status = call. exit_reason ;
157
- // We cannot use the calldata returned by the test runner in `TestError::Fail`,
158
- // since that input represents the last run case, which may not correspond with our
159
- // failure - when a fuzz case fails, proptest will try to run at least one more
160
- // case to find a minimal failure case.
161
- * counterexample. borrow_mut ( ) = ( calldata, call) ;
162
- Err ( TestCaseError :: fail (
163
- decode:: decode_revert (
164
- counterexample. borrow ( ) . 1 . result . as_ref ( ) ,
165
- errors,
166
- Some ( status) ,
167
- )
168
- . unwrap_or_default ( ) ,
169
- ) )
170
145
}
171
146
} ) ;
172
147
@@ -216,6 +191,63 @@ impl<'a> FuzzedExecutor<'a> {
216
191
217
192
result
218
193
}
194
+
195
+ /// Granular and single-step function that runs only one fuzz and returns either a `CaseOutcome`
196
+ /// or a `CounterExampleOutcome`
197
+ pub fn single_fuzz (
198
+ & self ,
199
+ state : & EvmFuzzState ,
200
+ address : Address ,
201
+ should_fail : bool ,
202
+ calldata : ethers:: types:: Bytes ,
203
+ ) -> Result < FuzzOutcome , TestCaseError > {
204
+ let call = self
205
+ . executor
206
+ . call_raw ( self . sender , address, calldata. 0 . clone ( ) , 0 . into ( ) )
207
+ . map_err ( |_| TestCaseError :: fail ( FuzzError :: FailedContractCall ) ) ?;
208
+ let state_changeset = call
209
+ . state_changeset
210
+ . as_ref ( )
211
+ . ok_or_else ( || TestCaseError :: fail ( FuzzError :: EmptyChangeset ) ) ?;
212
+
213
+ // Build fuzzer state
214
+ collect_state_from_call (
215
+ & call. logs ,
216
+ state_changeset,
217
+ state. clone ( ) ,
218
+ & self . config . dictionary ,
219
+ ) ;
220
+
221
+ // When assume cheat code is triggered return a special string "FOUNDRY::ASSUME"
222
+ if call. result . as_ref ( ) == ASSUME_MAGIC_RETURN_CODE {
223
+ return Err ( TestCaseError :: reject ( FuzzError :: AssumeReject ) )
224
+ }
225
+
226
+ let breakpoints = call
227
+ . cheatcodes
228
+ . as_ref ( )
229
+ . map_or_else ( Default :: default, |cheats| cheats. breakpoints . clone ( ) ) ;
230
+
231
+ let success =
232
+ self . executor . is_success ( address, call. reverted , state_changeset. clone ( ) , should_fail) ;
233
+
234
+ if success {
235
+ Ok ( FuzzOutcome :: Case ( CaseOutcome {
236
+ case : FuzzCase { calldata, gas : call. gas_used , stipend : call. stipend } ,
237
+ traces : call. traces ,
238
+ coverage : call. coverage ,
239
+ debug : call. debug ,
240
+ breakpoints,
241
+ } ) )
242
+ } else {
243
+ Ok ( FuzzOutcome :: CounterExample ( CounterExampleOutcome {
244
+ debug : call. debug . clone ( ) ,
245
+ exit_reason : call. exit_reason ,
246
+ counterexample : ( calldata, call) ,
247
+ breakpoints,
248
+ } ) )
249
+ }
250
+ }
219
251
}
220
252
221
253
#[ derive( Clone , Debug , Serialize , Deserialize ) ]
@@ -444,14 +476,3 @@ impl FuzzedCases {
444
476
self . lowest ( ) . map ( |c| c. gas ) . unwrap_or_default ( )
445
477
}
446
478
}
447
-
448
- /// Data of a single fuzz test case
449
- #[ derive( Clone , Debug , Default , Serialize , Deserialize ) ]
450
- pub struct FuzzCase {
451
- /// The calldata used for this fuzz test
452
- pub calldata : Bytes ,
453
- /// Consumed gas
454
- pub gas : u64 ,
455
- /// The initial gas stipend for the transaction
456
- pub stipend : u64 ,
457
- }
0 commit comments