@@ -20,10 +20,12 @@ use strategies::{
2020 build_initial_state, collect_state_from_call, fuzz_calldata, fuzz_calldata_from_state,
2121 EvmFuzzState ,
2222} ;
23+ use types:: { CaseOutcome , CounterExampleOutcome , FuzzCase , FuzzOutcome } ;
2324
2425pub mod error;
2526pub mod invariant;
2627pub mod strategies;
28+ pub mod types;
2729
2830/// Wrapper around an [`Executor`] which provides fuzzing support using [`proptest`](https://docs.rs/proptest/1.0.0/proptest/).
2931///
@@ -101,72 +103,45 @@ impl<'a> FuzzedExecutor<'a> {
101103 let strat = proptest:: strategy:: Union :: new_weighted ( weights) ;
102104 debug ! ( func = ?func. name, should_fail, "fuzzing" ) ;
103105 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 ( ( ) )
141127 }
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+ ) )
152144 }
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- ) )
170145 }
171146 } ) ;
172147
@@ -216,6 +191,63 @@ impl<'a> FuzzedExecutor<'a> {
216191
217192 result
218193 }
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+ }
219251}
220252
221253#[ derive( Clone , Debug , Serialize , Deserialize ) ]
@@ -444,14 +476,3 @@ impl FuzzedCases {
444476 self . lowest ( ) . map ( |c| c. gas ) . unwrap_or_default ( )
445477 }
446478}
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