diff --git a/crates/evm/evm/src/executors/invariant/error.rs b/crates/evm/evm/src/executors/invariant/error.rs index da29e657e0ef..b324fddee42e 100644 --- a/crates/evm/evm/src/executors/invariant/error.rs +++ b/crates/evm/evm/src/executors/invariant/error.rs @@ -150,7 +150,7 @@ impl FailedInvariantCaseData { }; if self.shrink { - calls = self.try_shrinking(&calls, &executor).into_iter().cloned().collect(); + calls = self.try_shrinking(&calls, &executor)?.into_iter().cloned().collect(); } else { trace!(target: "forge::test", "Shrinking disabled."); } @@ -162,9 +162,8 @@ impl FailedInvariantCaseData { // Replay each call from the sequence until we break the invariant. for (sender, (addr, bytes)) in calls.iter() { - let call_result = executor - .call_raw_committing(*sender, *addr, bytes.clone(), U256::ZERO) - .expect("bad call to evm"); + let call_result = + executor.call_raw_committing(*sender, *addr, bytes.clone(), U256::ZERO)?; logs.extend(call_result.logs); traces.push((TraceKind::Execution, call_result.traces.clone().unwrap())); @@ -185,9 +184,8 @@ impl FailedInvariantCaseData { // Checks the invariant. if let Some(func) = &self.func { - let error_call_result = executor - .call_raw(CALLER, self.addr, func.clone(), U256::ZERO) - .expect("bad call to evm"); + let error_call_result = + executor.call_raw(CALLER, self.addr, func.clone(), U256::ZERO)?; traces.push((TraceKind::Execution, error_call_result.traces.clone().unwrap())); @@ -210,10 +208,10 @@ impl FailedInvariantCaseData { calls: &[BasicTxDetails], use_calls: &[usize], curr_seq: Arc>>, - ) { + ) -> eyre::Result<()> { if curr_seq.read().len() == 1 { // if current sequence is already the smallest possible, just return - return; + return Ok(()); } let mut new_sequence = Vec::with_capacity(calls.len()); @@ -226,22 +224,19 @@ impl FailedInvariantCaseData { // If the new sequence is already longer than the known best, skip execution if new_sequence.len() >= curr_seq.read().len() { - return + return Ok(()); } } for (seq_idx, call_index) in new_sequence.iter().enumerate() { let (sender, (addr, bytes)) = &calls[*call_index]; - executor - .call_raw_committing(*sender, *addr, bytes.clone(), U256::ZERO) - .expect("bad call to evm"); + executor.call_raw_committing(*sender, *addr, bytes.clone(), U256::ZERO)?; // Checks the invariant. If we revert or fail before the last call, all the better. if let Some(func) = &self.func { - let mut call_result = executor - .call_raw(CALLER, self.addr, func.clone(), U256::ZERO) - .expect("bad call to evm"); + let mut call_result = + executor.call_raw(CALLER, self.addr, func.clone(), U256::ZERO)?; let is_success = executor.is_raw_call_success( self.addr, call_result.state_changeset.take().unwrap(), @@ -257,6 +252,7 @@ impl FailedInvariantCaseData { } } } + Ok(()) } /// Tries to shrink the failure case to its smallest sequence of calls. @@ -266,21 +262,20 @@ impl FailedInvariantCaseData { &self, calls: &'a [BasicTxDetails], executor: &Executor, - ) -> Vec<&'a BasicTxDetails> { + ) -> eyre::Result> { trace!(target: "forge::test", "Shrinking."); // Special case test: the invariant is *unsatisfiable* - it took 0 calls to // break the invariant -- consider emitting a warning. if let Some(func) = &self.func { - let error_call_result = executor - .call_raw(CALLER, self.addr, func.clone(), U256::ZERO) - .expect("bad call to evm"); + let error_call_result = + executor.call_raw(CALLER, self.addr, func.clone(), U256::ZERO)?; if error_call_result.reverted { - return vec![]; + return Ok(vec![]); } } - let shrunk_call_indices = self.try_shrinking_recurse(calls, executor, 0, 0); + let shrunk_call_indices = self.try_shrinking_recurse(calls, executor, 0, 0)?; // We recreate the call sequence in the same order as they reproduce the failure, // otherwise we could end up with inverted sequence. @@ -289,7 +284,7 @@ impl FailedInvariantCaseData { // 2. Bob calls transferOwnership to Alice // 3. Alice calls acceptOwnership and test fails // we shrink to indices of [2, 1] and we recreate call sequence in same order. - shrunk_call_indices.iter().map(|idx| &calls[*idx]).collect() + Ok(shrunk_call_indices.iter().map(|idx| &calls[*idx]).collect()) } /// We try to construct a [powerset](https://en.wikipedia.org/wiki/Power_set) of the sequence if @@ -310,7 +305,7 @@ impl FailedInvariantCaseData { executor: &Executor, runs: usize, retries: usize, - ) -> Vec { + ) -> eyre::Result> { // Construct a ArcRwLock vector of indices of `calls` let shrunk_call_indices = Arc::new(RwLock::new((0..calls.len()).collect())); let shrink_limit = self.shrink_run_limit - runs; @@ -319,7 +314,7 @@ impl FailedInvariantCaseData { // We construct either a full powerset (this guarantees we maximally shrunk for the given // calls) or a random subset let (set_of_indices, is_powerset): (Vec<_>, bool) = if calls.len() <= 64 && - 2_usize.pow(calls.len() as u32) <= shrink_limit + (1 << calls.len() as u32) <= shrink_limit { // We add the last tx always because thats ultimately what broke the invariant let powerset = (0..upper_bound) @@ -357,14 +352,17 @@ impl FailedInvariantCaseData { let new_runs = set_of_indices.len(); // just try all of them in parallel - set_of_indices.par_iter().for_each(|use_calls| { - self.set_fails_successfully( - executor.clone(), - calls, - use_calls, - Arc::clone(&shrunk_call_indices), - ); - }); + set_of_indices + .par_iter() + .map(|use_calls| { + self.set_fails_successfully( + executor.clone(), + calls, + use_calls, + Arc::clone(&shrunk_call_indices), + ) + }) + .collect::>()?; // There are no more live references to shrunk_call_indices as the parallel execution is // finished, so it is fine to get the inner value via `Arc::unwrap`. @@ -372,7 +370,7 @@ impl FailedInvariantCaseData { if is_powerset { // A powerset is guaranteed to be smallest local subset, so we return early. - return shrunk_call_indices + return Ok(shrunk_call_indices); } let computation_budget_not_hit = new_runs + runs < self.shrink_run_limit; @@ -399,13 +397,8 @@ impl FailedInvariantCaseData { let new_calls: Vec<_> = calls .iter() .enumerate() - .filter_map(|(i, call)| { - if shrunk_call_indices.contains(&i) { - Some(call.clone()) - } else { - None - } - }) + .filter(|(i, _)| shrunk_call_indices.contains(i)) + .map(|(_, call)| call.clone()) .collect(); // We rerun this algorithm as if the new smaller subset above were the original @@ -415,13 +408,13 @@ impl FailedInvariantCaseData { // returns [1]. This means `call3` is all that is required to break // the invariant. let new_calls_idxs = - self.try_shrinking_recurse(&new_calls, executor, runs + new_runs, 0); + self.try_shrinking_recurse(&new_calls, executor, runs + new_runs, 0)?; // Notably, the indices returned above are relative to `new_calls`, *not* the // originally passed in `calls`. So we map back by filtering // `new_calls` by index if the index was returned above, and finding the position // of the `new_call` in the passed in `call` - new_calls + Ok(new_calls .iter() .enumerate() .filter_map(|(idx, new_call)| { @@ -431,12 +424,12 @@ impl FailedInvariantCaseData { calls.iter().position(|r| r == new_call) } }) - .collect() + .collect()) } _ => { // The computation budget has been hit or no retries remaining, stop trying to make // progress - shrunk_call_indices + Ok(shrunk_call_indices) } } } diff --git a/crates/evm/evm/src/executors/invariant/funcs.rs b/crates/evm/evm/src/executors/invariant/funcs.rs index 237b4dad84ec..9672ec7da16a 100644 --- a/crates/evm/evm/src/executors/invariant/funcs.rs +++ b/crates/evm/evm/src/executors/invariant/funcs.rs @@ -20,7 +20,7 @@ pub fn assert_invariants( invariant_failures: &mut InvariantFailures, shrink_sequence: bool, shrink_run_limit: usize, -) -> Option { +) -> eyre::Result> { let mut inner_sequence = vec![]; if let Some(fuzzer) = &executor.inspector.fuzzer { @@ -30,16 +30,13 @@ pub fn assert_invariants( } let func = invariant_contract.invariant_function; - let mut call_result = executor - .call_raw( - CALLER, - invariant_contract.address, - func.abi_encode_input(&[]).expect("invariant should have no inputs").into(), - U256::ZERO, - ) - .expect("EVM error"); + let mut call_result = executor.call_raw( + CALLER, + invariant_contract.address, + func.abi_encode_input(&[]).expect("invariant should have no inputs").into(), + U256::ZERO, + )?; - // This will panic and get caught by the executor let is_err = call_result.reverted || !executor.is_raw_call_success( invariant_contract.address, @@ -60,11 +57,11 @@ pub fn assert_invariants( shrink_run_limit, ); invariant_failures.error = Some(InvariantFuzzError::BrokenInvariant(case_data)); - return None + return Ok(None); } } - Some(call_result) + Ok(Some(call_result)) } /// Replays the provided invariant run for collecting the logs and traces from all depths. @@ -79,7 +76,7 @@ pub fn replay_run( coverage: &mut Option, func: Function, inputs: Vec, -) { +) -> eyre::Result<()> { // We want traces for a failed case. executor.set_tracing(true); @@ -87,25 +84,18 @@ pub fn replay_run( // Replay each call from the sequence until we break the invariant. for (sender, (addr, bytes)) in inputs.iter() { - let call_result = executor - .call_raw_committing(*sender, *addr, bytes.clone(), U256::ZERO) - .expect("bad call to evm"); + let call_result = + executor.call_raw_committing(*sender, *addr, bytes.clone(), U256::ZERO)?; logs.extend(call_result.logs); traces.push((TraceKind::Execution, call_result.traces.clone().unwrap())); - let old_coverage = std::mem::take(coverage); - match (old_coverage, call_result.coverage) { - (Some(old_coverage), Some(call_coverage)) => { - *coverage = Some(old_coverage.merge(call_coverage)); - } - (None, Some(call_coverage)) => { - *coverage = Some(call_coverage); + if let Some(new_coverage) = call_result.coverage { + if let Some(old_coverage) = coverage { + *coverage = Some(std::mem::take(old_coverage).merge(new_coverage)); + } else { + *coverage = Some(new_coverage); } - (Some(old_coverage), None) => { - *coverage = Some(old_coverage); - } - (None, None) => {} } // Identify newly generated contracts, if they exist. @@ -115,17 +105,16 @@ pub fn replay_run( )); // Checks the invariant. - let error_call_result = executor - .call_raw( - CALLER, - invariant_contract.address, - func.abi_encode_input(&[]).expect("invariant should have no inputs").into(), - U256::ZERO, - ) - .expect("bad call to evm"); + let error_call_result = executor.call_raw( + CALLER, + invariant_contract.address, + func.abi_encode_input(&[]).expect("invariant should have no inputs").into(), + U256::ZERO, + )?; traces.push((TraceKind::Execution, error_call_result.traces.clone().unwrap())); logs.extend(error_call_result.logs); } + Ok(()) } diff --git a/crates/evm/evm/src/executors/invariant/mod.rs b/crates/evm/evm/src/executors/invariant/mod.rs index 591fa7cf444d..e3b3b433b4d6 100644 --- a/crates/evm/evm/src/executors/invariant/mod.rs +++ b/crates/evm/evm/src/executors/invariant/mod.rs @@ -138,7 +138,7 @@ impl<'a> InvariantExecutor<'a> { &mut failures.borrow_mut(), self.config.shrink_sequence, self.config.shrink_run_limit, - )); + )?); if last_call_results.borrow().is_none() { fuzz_cases.borrow_mut().push(FuzzedCases::new(vec![])); @@ -241,7 +241,8 @@ impl<'a> InvariantExecutor<'a> { self.config.shrink_sequence, self.config.shrink_run_limit, &mut run_traces, - ); + ) + .map_err(|e| TestCaseError::fail(e.to_string()))?; if !can_continue || current_run == self.config.depth - 1 { last_run_calldata.borrow_mut().clone_from(&inputs); @@ -777,7 +778,7 @@ fn can_continue( shrink_sequence: bool, shrink_run_limit: usize, run_traces: &mut Vec, -) -> RichInvariantResults { +) -> eyre::Result { let mut call_results = None; // Detect handler assertion failures first. @@ -799,9 +800,9 @@ fn can_continue( failures, shrink_sequence, shrink_run_limit, - ); + )?; if call_results.is_none() { - return RichInvariantResults::new(false, None) + return Ok(RichInvariantResults::new(false, None)); } } else { // Increase the amount of reverts. @@ -821,8 +822,8 @@ fn can_continue( let error = InvariantFuzzError::Revert(case_data); failures.error = Some(error); - return RichInvariantResults::new(false, None) + return Ok(RichInvariantResults::new(false, None)); } } - RichInvariantResults::new(true, call_results) + Ok(RichInvariantResults::new(true, call_results)) } diff --git a/crates/forge/src/runner.rs b/crates/forge/src/runner.rs index e8dba45a234c..dbe58b3173e8 100644 --- a/crates/forge/src/runner.rs +++ b/crates/forge/src/runner.rs @@ -460,7 +460,7 @@ impl<'a> ContractRunner<'a> { // First, run the test normally to see if it needs to be skipped. let start = Instant::now(); - if let Err(EvmError::SkipError) = self.executor.clone().execute_test::<_, _>( + if let Err(EvmError::SkipError) = self.executor.clone().execute_test( self.sender, address, func.clone(), @@ -541,7 +541,7 @@ impl<'a> ContractRunner<'a> { // If invariants ran successfully, replay the last run to collect logs and // traces. _ => { - replay_run( + if let Err(err) = replay_run( &invariant_contract, self.executor.clone(), known_contracts, @@ -551,7 +551,9 @@ impl<'a> ContractRunner<'a> { &mut coverage, func.clone(), last_run_inputs.clone(), - ); + ) { + error!(%err, "Failed to replay last invariant run"); + } } }