Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions crates/config/src/invariant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,9 @@ impl InvariantConfig {
pub fn new(cache_dir: PathBuf) -> Self {
Self { failure_persist_dir: Some(cache_dir), ..Default::default() }
}

/// Returns true if generated invariant calls may advance block time or height.
pub fn has_delay(&self) -> bool {
self.max_block_delay.is_some() || self.max_time_delay.is_some()
}
}
9 changes: 6 additions & 3 deletions crates/evm/evm/src/executors/invariant/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -527,9 +527,12 @@ impl<'a, FEN: FoundryEvmNetwork> InvariantExecutor<'a, FEN> {
invariant_test.test_data.failures.error =
Some(InvariantFuzzError::Revert(case_data));
result::RichInvariantResults::new(false, None)
} else if !invariant_contract.is_optimization() {
// In optimization mode, keep reverted calls to preserve
// warp/roll values for correct replay during shrinking.
} else if !invariant_contract.is_optimization()
&& !self.config.has_delay()
{
// Delay-enabled campaigns keep reverted calls so shrinking can
// preserve their warp/roll contribution when building the final
// counterexample.
current_run.inputs.pop();
result::RichInvariantResults::new(true, None)
} else {
Expand Down
8 changes: 4 additions & 4 deletions crates/evm/evm/src/executors/invariant/result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,10 +183,10 @@ pub(crate) fn can_continue<FEN: FoundryEvmNetwork>(
invariant_data.failures.error = Some(InvariantFuzzError::Revert(case_data));

return Ok(RichInvariantResults::new(false, None));
} else if call_result.reverted && !is_optimization {
// If we don't fail test on revert then remove last reverted call from inputs.
// In optimization mode, we keep reverted calls to preserve warp/roll values
// for correct replay during shrinking.
} else if call_result.reverted && !is_optimization && !invariant_config.has_delay() {
// If we don't fail test on revert then remove the reverted call from inputs.
// Delay-enabled campaigns keep reverted calls so shrinking can preserve their
// warp/roll contribution when building the final counterexample.
invariant_run.inputs.pop();
}
}
Expand Down
173 changes: 152 additions & 21 deletions crates/evm/evm/src/executors/invariant/shrink.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,37 @@ fn apply_warp_roll_to_env<FEN: FoundryEvmNetwork>(
}
}

/// Builds the final shrunk sequence from the shrinker state.
///
/// When `accumulate_warp_roll` is enabled, warp/roll from removed calls is folded into the next
/// kept call so the final sequence remains reproducible.
fn build_shrunk_sequence(
calls: &[BasicTxDetails],
shrinker: &CallSequenceShrinker,
accumulate_warp_roll: bool,
) -> Vec<BasicTxDetails> {
if !accumulate_warp_roll {
return shrinker.current().map(|idx| calls[idx].clone()).collect();
}

let mut result = Vec::new();
let mut accumulated_warp = U256::ZERO;
let mut accumulated_roll = U256::ZERO;

for (idx, call) in calls.iter().enumerate() {
accumulated_warp += call.warp.unwrap_or(U256::ZERO);
accumulated_roll += call.roll.unwrap_or(U256::ZERO);

if shrinker.included_calls.test(idx) {
result.push(apply_warp_roll(call, accumulated_warp, accumulated_roll));
accumulated_warp = U256::ZERO;
accumulated_roll = U256::ZERO;
}
}

result
}

pub(crate) fn shrink_sequence<FEN: FoundryEvmNetwork>(
config: &InvariantConfig,
invariant_contract: &InvariantContract<'_>,
Expand All @@ -108,6 +139,7 @@ pub(crate) fn shrink_sequence<FEN: FoundryEvmNetwork>(
return Ok(vec![]);
}

let accumulate_warp_roll = config.has_delay();
let mut call_idx = 0;
let mut shrinker = CallSequenceShrinker::new(calls.len());

Expand All @@ -125,6 +157,7 @@ pub(crate) fn shrink_sequence<FEN: FoundryEvmNetwork>(
target_address,
calldata.clone(),
CheckSequenceOptions {
accumulate_warp_roll,
fail_on_revert: config.fail_on_revert,
call_after_invariant: invariant_contract.call_after_invariant,
rd: None,
Expand All @@ -144,7 +177,7 @@ pub(crate) fn shrink_sequence<FEN: FoundryEvmNetwork>(
call_idx = shrinker.next_index(call_idx);
}

Ok(shrinker.current().map(|idx| &calls[idx]).cloned().collect())
Ok(build_shrunk_sequence(calls, &shrinker, accumulate_warp_roll))
}

/// Checks if the given call sequence breaks the invariant.
Expand All @@ -153,7 +186,25 @@ pub(crate) fn shrink_sequence<FEN: FoundryEvmNetwork>(
/// persisted failures.
/// Returns the result of invariant check (and afterInvariant call if needed) and if sequence was
/// entirely applied.
///
/// When `options.accumulate_warp_roll` is enabled, warp/roll from removed calls is folded into the
/// next kept call so the candidate sequence stays representable as a concrete counterexample.
pub fn check_sequence<FEN: FoundryEvmNetwork>(
executor: Executor<FEN>,
calls: &[BasicTxDetails],
sequence: Vec<usize>,
test_address: Address,
calldata: Bytes,
options: CheckSequenceOptions<'_>,
) -> eyre::Result<(bool, bool, Option<String>)> {
if options.accumulate_warp_roll {
check_sequence_with_accumulation(executor, calls, sequence, test_address, calldata, options)
} else {
check_sequence_simple(executor, calls, sequence, test_address, calldata, options)
}
}

fn check_sequence_simple<FEN: FoundryEvmNetwork>(
mut executor: Executor<FEN>,
calls: &[BasicTxDetails],
sequence: Vec<usize>,
Expand All @@ -179,9 +230,59 @@ pub fn check_sequence<FEN: FoundryEvmNetwork>(
}
}

// Check the invariant for call sequence.
finish_sequence_check(&executor, test_address, calldata, &options)
}

fn check_sequence_with_accumulation<FEN: FoundryEvmNetwork>(
mut executor: Executor<FEN>,
calls: &[BasicTxDetails],
sequence: Vec<usize>,
test_address: Address,
calldata: Bytes,
options: CheckSequenceOptions<'_>,
) -> eyre::Result<(bool, bool, Option<String>)> {
let mut accumulated_warp = U256::ZERO;
let mut accumulated_roll = U256::ZERO;
let mut seq_iter = sequence.iter().peekable();

for (idx, tx) in calls.iter().enumerate() {
accumulated_warp += tx.warp.unwrap_or(U256::ZERO);
accumulated_roll += tx.roll.unwrap_or(U256::ZERO);

if seq_iter.peek() != Some(&&idx) {
continue;
}

seq_iter.next();

let tx_with_accumulated = apply_warp_roll(tx, accumulated_warp, accumulated_roll);
let mut call_result = execute_tx(&mut executor, &tx_with_accumulated)?;

if call_result.reverted {
if options.fail_on_revert && call_result.result.as_ref() != MAGIC_ASSUME {
return Ok((false, false, call_failure_reason(call_result, options.rd)));
}
} else {
executor.commit(&mut call_result);
}

accumulated_warp = U256::ZERO;
accumulated_roll = U256::ZERO;
}

// Unlike optimization mode we intentionally do not apply trailing warp/roll before the
// invariant call: those delays would not be representable in the final shrunk sequence.
finish_sequence_check(&executor, test_address, calldata, &options)
}

fn finish_sequence_check<FEN: FoundryEvmNetwork>(
executor: &Executor<FEN>,
test_address: Address,
calldata: Bytes,
options: &CheckSequenceOptions<'_>,
) -> eyre::Result<(bool, bool, Option<String>)> {
let (invariant_result, mut success) =
call_invariant_function(&executor, test_address, calldata)?;
call_invariant_function(executor, test_address, calldata)?;
if !success {
return Ok((false, true, call_failure_reason(invariant_result, options.rd)));
}
Expand All @@ -190,7 +291,7 @@ pub fn check_sequence<FEN: FoundryEvmNetwork>(
// declared.
if success && options.call_after_invariant {
let (after_invariant_result, after_invariant_success) =
call_after_invariant_function(&executor, test_address)?;
call_after_invariant_function(executor, test_address)?;
success = after_invariant_success;
if !success {
return Ok((false, true, call_failure_reason(after_invariant_result, options.rd)));
Expand All @@ -201,6 +302,7 @@ pub fn check_sequence<FEN: FoundryEvmNetwork>(
}

pub struct CheckSequenceOptions<'a> {
pub accumulate_warp_roll: bool,
pub fail_on_revert: bool,
pub call_after_invariant: bool,
pub rd: Option<&'a RevertDecoder>,
Expand Down Expand Up @@ -279,23 +381,7 @@ pub(crate) fn shrink_sequence_value<FEN: FoundryEvmNetwork>(
call_idx = shrinker.next_index(call_idx);
}

// Build the final shrunk sequence, accumulating warp/roll from removed calls.
let mut result = Vec::new();
let mut accumulated_warp = U256::ZERO;
let mut accumulated_roll = U256::ZERO;

for (idx, call) in calls.iter().enumerate() {
accumulated_warp += call.warp.unwrap_or(U256::ZERO);
accumulated_roll += call.roll.unwrap_or(U256::ZERO);

if shrinker.included_calls.test(idx) {
result.push(apply_warp_roll(call, accumulated_warp, accumulated_roll));
accumulated_warp = U256::ZERO;
accumulated_roll = U256::ZERO;
}
}

Ok(result)
Ok(build_shrunk_sequence(calls, &shrinker, true))
}

/// Executes a call sequence and returns the optimization value (int256) from the invariant
Expand Down Expand Up @@ -347,3 +433,48 @@ pub fn check_sequence_value<FEN: FoundryEvmNetwork>(

Ok(None)
}

#[cfg(test)]
mod tests {
use super::{CallSequenceShrinker, build_shrunk_sequence};
use alloy_primitives::{Address, Bytes, U256};
use foundry_evm_fuzz::{BasicTxDetails, CallDetails};
use proptest::bits::BitSetLike;

fn tx(warp: Option<u64>, roll: Option<u64>) -> BasicTxDetails {
BasicTxDetails {
warp: warp.map(U256::from),
roll: roll.map(U256::from),
sender: Address::ZERO,
call_details: CallDetails { target: Address::ZERO, calldata: Bytes::new() },
}
}

#[test]
fn build_shrunk_sequence_accumulates_removed_delay_into_next_kept_call() {
let calls = vec![tx(Some(3), Some(5)), tx(Some(7), Some(11)), tx(Some(13), Some(17))];
let mut shrinker = CallSequenceShrinker::new(calls.len());
shrinker.included_calls.clear(0);

let shrunk = build_shrunk_sequence(&calls, &shrinker, true);

assert_eq!(shrunk.len(), 2);
assert_eq!(shrunk[0].warp, Some(U256::from(10)));
assert_eq!(shrunk[0].roll, Some(U256::from(16)));
assert_eq!(shrunk[1].warp, Some(U256::from(13)));
assert_eq!(shrunk[1].roll, Some(U256::from(17)));
}

#[test]
fn build_shrunk_sequence_does_not_move_trailing_delay_backward() {
let calls = vec![tx(Some(3), Some(5)), tx(Some(7), Some(11))];
let mut shrinker = CallSequenceShrinker::new(calls.len());
shrinker.included_calls.clear(1);

let shrunk = build_shrunk_sequence(&calls, &shrinker, true);

assert_eq!(shrunk.len(), 1);
assert_eq!(shrunk[0].warp, Some(U256::from(3)));
assert_eq!(shrunk[0].roll, Some(U256::from(5)));
}
}
1 change: 1 addition & 0 deletions crates/forge/src/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -800,6 +800,7 @@ impl<'a, FEN: FoundryEvmNetwork> FunctionRunner<'a, FEN> {
invariant_contract.address,
invariant_contract.invariant_function.selector().to_vec().into(),
CheckSequenceOptions {
accumulate_warp_roll: invariant_config.has_delay(),
fail_on_revert: invariant_config.fail_on_revert,
call_after_invariant: invariant_contract.call_after_invariant,
rd: Some(self.revert_decoder()),
Expand Down
Loading
Loading