@@ -87,6 +87,37 @@ fn apply_warp_roll_to_env<FEN: FoundryEvmNetwork>(
8787 }
8888}
8989
90+ /// Builds the final shrunk sequence from the shrinker state.
91+ ///
92+ /// When `accumulate_warp_roll` is enabled, warp/roll from removed calls is folded into the next
93+ /// kept call so the final sequence remains reproducible.
94+ fn build_shrunk_sequence (
95+ calls : & [ BasicTxDetails ] ,
96+ shrinker : & CallSequenceShrinker ,
97+ accumulate_warp_roll : bool ,
98+ ) -> Vec < BasicTxDetails > {
99+ if !accumulate_warp_roll {
100+ return shrinker. current ( ) . map ( |idx| calls[ idx] . clone ( ) ) . collect ( ) ;
101+ }
102+
103+ let mut result = Vec :: new ( ) ;
104+ let mut accumulated_warp = U256 :: ZERO ;
105+ let mut accumulated_roll = U256 :: ZERO ;
106+
107+ for ( idx, call) in calls. iter ( ) . enumerate ( ) {
108+ accumulated_warp += call. warp . unwrap_or ( U256 :: ZERO ) ;
109+ accumulated_roll += call. roll . unwrap_or ( U256 :: ZERO ) ;
110+
111+ if shrinker. included_calls . test ( idx) {
112+ result. push ( apply_warp_roll ( call, accumulated_warp, accumulated_roll) ) ;
113+ accumulated_warp = U256 :: ZERO ;
114+ accumulated_roll = U256 :: ZERO ;
115+ }
116+ }
117+
118+ result
119+ }
120+
90121pub ( crate ) fn shrink_sequence < FEN : FoundryEvmNetwork > (
91122 config : & InvariantConfig ,
92123 invariant_contract : & InvariantContract < ' _ > ,
@@ -108,6 +139,7 @@ pub(crate) fn shrink_sequence<FEN: FoundryEvmNetwork>(
108139 return Ok ( vec ! [ ] ) ;
109140 }
110141
142+ let accumulate_warp_roll = config. has_delay ( ) ;
111143 let mut call_idx = 0 ;
112144 let mut shrinker = CallSequenceShrinker :: new ( calls. len ( ) ) ;
113145
@@ -125,6 +157,7 @@ pub(crate) fn shrink_sequence<FEN: FoundryEvmNetwork>(
125157 target_address,
126158 calldata. clone ( ) ,
127159 CheckSequenceOptions {
160+ accumulate_warp_roll,
128161 fail_on_revert : config. fail_on_revert ,
129162 call_after_invariant : invariant_contract. call_after_invariant ,
130163 rd : None ,
@@ -144,7 +177,7 @@ pub(crate) fn shrink_sequence<FEN: FoundryEvmNetwork>(
144177 call_idx = shrinker. next_index ( call_idx) ;
145178 }
146179
147- Ok ( shrinker . current ( ) . map ( |idx| & calls [ idx ] ) . cloned ( ) . collect ( ) )
180+ Ok ( build_shrunk_sequence ( calls , & shrinker , accumulate_warp_roll ) )
148181}
149182
150183/// Checks if the given call sequence breaks the invariant.
@@ -153,7 +186,25 @@ pub(crate) fn shrink_sequence<FEN: FoundryEvmNetwork>(
153186/// persisted failures.
154187/// Returns the result of invariant check (and afterInvariant call if needed) and if sequence was
155188/// entirely applied.
189+ ///
190+ /// When `options.accumulate_warp_roll` is enabled, warp/roll from removed calls is folded into the
191+ /// next kept call so the candidate sequence stays representable as a concrete counterexample.
156192pub fn check_sequence < FEN : FoundryEvmNetwork > (
193+ executor : Executor < FEN > ,
194+ calls : & [ BasicTxDetails ] ,
195+ sequence : Vec < usize > ,
196+ test_address : Address ,
197+ calldata : Bytes ,
198+ options : CheckSequenceOptions < ' _ > ,
199+ ) -> eyre:: Result < ( bool , bool , Option < String > ) > {
200+ if options. accumulate_warp_roll {
201+ check_sequence_with_accumulation ( executor, calls, sequence, test_address, calldata, options)
202+ } else {
203+ check_sequence_simple ( executor, calls, sequence, test_address, calldata, options)
204+ }
205+ }
206+
207+ fn check_sequence_simple < FEN : FoundryEvmNetwork > (
157208 mut executor : Executor < FEN > ,
158209 calls : & [ BasicTxDetails ] ,
159210 sequence : Vec < usize > ,
@@ -179,9 +230,59 @@ pub fn check_sequence<FEN: FoundryEvmNetwork>(
179230 }
180231 }
181232
182- // Check the invariant for call sequence.
233+ finish_sequence_check ( & executor, test_address, calldata, & options)
234+ }
235+
236+ fn check_sequence_with_accumulation < FEN : FoundryEvmNetwork > (
237+ mut executor : Executor < FEN > ,
238+ calls : & [ BasicTxDetails ] ,
239+ sequence : Vec < usize > ,
240+ test_address : Address ,
241+ calldata : Bytes ,
242+ options : CheckSequenceOptions < ' _ > ,
243+ ) -> eyre:: Result < ( bool , bool , Option < String > ) > {
244+ let mut accumulated_warp = U256 :: ZERO ;
245+ let mut accumulated_roll = U256 :: ZERO ;
246+ let mut seq_iter = sequence. iter ( ) . peekable ( ) ;
247+
248+ for ( idx, tx) in calls. iter ( ) . enumerate ( ) {
249+ accumulated_warp += tx. warp . unwrap_or ( U256 :: ZERO ) ;
250+ accumulated_roll += tx. roll . unwrap_or ( U256 :: ZERO ) ;
251+
252+ if seq_iter. peek ( ) != Some ( & & idx) {
253+ continue ;
254+ }
255+
256+ seq_iter. next ( ) ;
257+
258+ let tx_with_accumulated = apply_warp_roll ( tx, accumulated_warp, accumulated_roll) ;
259+ let mut call_result = execute_tx ( & mut executor, & tx_with_accumulated) ?;
260+
261+ if call_result. reverted {
262+ if options. fail_on_revert && call_result. result . as_ref ( ) != MAGIC_ASSUME {
263+ return Ok ( ( false , false , call_failure_reason ( call_result, options. rd ) ) ) ;
264+ }
265+ } else {
266+ executor. commit ( & mut call_result) ;
267+ }
268+
269+ accumulated_warp = U256 :: ZERO ;
270+ accumulated_roll = U256 :: ZERO ;
271+ }
272+
273+ // Unlike optimization mode we intentionally do not apply trailing warp/roll before the
274+ // invariant call: those delays would not be representable in the final shrunk sequence.
275+ finish_sequence_check ( & executor, test_address, calldata, & options)
276+ }
277+
278+ fn finish_sequence_check < FEN : FoundryEvmNetwork > (
279+ executor : & Executor < FEN > ,
280+ test_address : Address ,
281+ calldata : Bytes ,
282+ options : & CheckSequenceOptions < ' _ > ,
283+ ) -> eyre:: Result < ( bool , bool , Option < String > ) > {
183284 let ( invariant_result, mut success) =
184- call_invariant_function ( & executor, test_address, calldata) ?;
285+ call_invariant_function ( executor, test_address, calldata) ?;
185286 if !success {
186287 return Ok ( ( false , true , call_failure_reason ( invariant_result, options. rd ) ) ) ;
187288 }
@@ -190,7 +291,7 @@ pub fn check_sequence<FEN: FoundryEvmNetwork>(
190291 // declared.
191292 if success && options. call_after_invariant {
192293 let ( after_invariant_result, after_invariant_success) =
193- call_after_invariant_function ( & executor, test_address) ?;
294+ call_after_invariant_function ( executor, test_address) ?;
194295 success = after_invariant_success;
195296 if !success {
196297 return Ok ( ( false , true , call_failure_reason ( after_invariant_result, options. rd ) ) ) ;
@@ -201,6 +302,7 @@ pub fn check_sequence<FEN: FoundryEvmNetwork>(
201302}
202303
203304pub struct CheckSequenceOptions < ' a > {
305+ pub accumulate_warp_roll : bool ,
204306 pub fail_on_revert : bool ,
205307 pub call_after_invariant : bool ,
206308 pub rd : Option < & ' a RevertDecoder > ,
@@ -279,23 +381,7 @@ pub(crate) fn shrink_sequence_value<FEN: FoundryEvmNetwork>(
279381 call_idx = shrinker. next_index ( call_idx) ;
280382 }
281383
282- // Build the final shrunk sequence, accumulating warp/roll from removed calls.
283- let mut result = Vec :: new ( ) ;
284- let mut accumulated_warp = U256 :: ZERO ;
285- let mut accumulated_roll = U256 :: ZERO ;
286-
287- for ( idx, call) in calls. iter ( ) . enumerate ( ) {
288- accumulated_warp += call. warp . unwrap_or ( U256 :: ZERO ) ;
289- accumulated_roll += call. roll . unwrap_or ( U256 :: ZERO ) ;
290-
291- if shrinker. included_calls . test ( idx) {
292- result. push ( apply_warp_roll ( call, accumulated_warp, accumulated_roll) ) ;
293- accumulated_warp = U256 :: ZERO ;
294- accumulated_roll = U256 :: ZERO ;
295- }
296- }
297-
298- Ok ( result)
384+ Ok ( build_shrunk_sequence ( calls, & shrinker, true ) )
299385}
300386
301387/// Executes a call sequence and returns the optimization value (int256) from the invariant
@@ -347,3 +433,48 @@ pub fn check_sequence_value<FEN: FoundryEvmNetwork>(
347433
348434 Ok ( None )
349435}
436+
437+ #[ cfg( test) ]
438+ mod tests {
439+ use super :: { CallSequenceShrinker , build_shrunk_sequence} ;
440+ use alloy_primitives:: { Address , Bytes , U256 } ;
441+ use foundry_evm_fuzz:: { BasicTxDetails , CallDetails } ;
442+ use proptest:: bits:: BitSetLike ;
443+
444+ fn tx ( warp : Option < u64 > , roll : Option < u64 > ) -> BasicTxDetails {
445+ BasicTxDetails {
446+ warp : warp. map ( U256 :: from) ,
447+ roll : roll. map ( U256 :: from) ,
448+ sender : Address :: ZERO ,
449+ call_details : CallDetails { target : Address :: ZERO , calldata : Bytes :: new ( ) } ,
450+ }
451+ }
452+
453+ #[ test]
454+ fn build_shrunk_sequence_accumulates_removed_delay_into_next_kept_call ( ) {
455+ let calls = vec ! [ tx( Some ( 3 ) , Some ( 5 ) ) , tx( Some ( 7 ) , Some ( 11 ) ) , tx( Some ( 13 ) , Some ( 17 ) ) ] ;
456+ let mut shrinker = CallSequenceShrinker :: new ( calls. len ( ) ) ;
457+ shrinker. included_calls . clear ( 0 ) ;
458+
459+ let shrunk = build_shrunk_sequence ( & calls, & shrinker, true ) ;
460+
461+ assert_eq ! ( shrunk. len( ) , 2 ) ;
462+ assert_eq ! ( shrunk[ 0 ] . warp, Some ( U256 :: from( 10 ) ) ) ;
463+ assert_eq ! ( shrunk[ 0 ] . roll, Some ( U256 :: from( 16 ) ) ) ;
464+ assert_eq ! ( shrunk[ 1 ] . warp, Some ( U256 :: from( 13 ) ) ) ;
465+ assert_eq ! ( shrunk[ 1 ] . roll, Some ( U256 :: from( 17 ) ) ) ;
466+ }
467+
468+ #[ test]
469+ fn build_shrunk_sequence_does_not_move_trailing_delay_backward ( ) {
470+ let calls = vec ! [ tx( Some ( 3 ) , Some ( 5 ) ) , tx( Some ( 7 ) , Some ( 11 ) ) ] ;
471+ let mut shrinker = CallSequenceShrinker :: new ( calls. len ( ) ) ;
472+ shrinker. included_calls . clear ( 1 ) ;
473+
474+ let shrunk = build_shrunk_sequence ( & calls, & shrinker, true ) ;
475+
476+ assert_eq ! ( shrunk. len( ) , 1 ) ;
477+ assert_eq ! ( shrunk[ 0 ] . warp, Some ( U256 :: from( 3 ) ) ) ;
478+ assert_eq ! ( shrunk[ 0 ] . roll, Some ( U256 :: from( 5 ) ) ) ;
479+ }
480+ }
0 commit comments