Skip to content

Commit a6667e3

Browse files
Tests and Nits
Co-authored-by: Amp <amp@ampcode.com> Amp-Thread-ID: https://ampcode.com/threads/T-019dbf48-3fb0-7762-a01f-b5e966339e73
1 parent e1db2ff commit a6667e3

11 files changed

Lines changed: 146 additions & 60 deletions

File tree

crates/config/src/invariant.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ pub struct InvariantConfig {
4949
///
5050
/// Example: `check_interval = 10` means assert after calls 10, 20, 30, ... and the last call.
5151
pub check_interval: u32,
52+
/// Continue invariant run until all invariants declared in current test suite breaks.
53+
pub continuous_run: bool,
5254
}
5355

5456
impl Default for InvariantConfig {
@@ -70,6 +72,7 @@ impl Default for InvariantConfig {
7072
max_time_delay: None,
7173
max_block_delay: None,
7274
check_interval: 1,
75+
continuous_run: false,
7376
}
7477
}
7578
}

crates/evm/evm/src/executors/invariant/error.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ use super::InvariantContract;
22
use crate::executors::RawCallResult;
33
use alloy_json_abi::Function;
44
use alloy_primitives::{Address, Bytes};
5-
use foundry_config::InvariantConfig;
65
use foundry_evm_core::{
76
decode::{ASSERTION_FAILED_PREFIX, EMPTY_REVERT_DATA, RevertDecoder},
87
evm::FoundryEvmNetwork,

crates/evm/evm/src/executors/invariant/mod.rs

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use crate::{
66
inspectors::Fuzzer,
77
};
88
use alloy_json_abi::Function;
9-
use alloy_primitives::{Address, Bytes, FixedBytes, I256, Selector, U256, map::{AddressMap, HashMap}};
9+
use alloy_primitives::{Address, Bytes, FixedBytes, I256, Selector, U256, map::AddressMap};
1010
use alloy_sol_types::{SolCall, sol};
1111
use eyre::{ContextCompat, Result, eyre};
1212
use foundry_common::{
@@ -35,7 +35,7 @@ use foundry_evm_traces::{CallTraceArena, SparsedTraceArena};
3535
use indicatif::ProgressBar;
3636
use parking_lot::RwLock;
3737
use proptest::{strategy::Strategy, test_runner::TestRunner};
38-
use result::{assert_after_invariant, assert_invariants, can_continue, did_fail_on_assert, invariant_preflight_check};
38+
use result::{assert_after_invariant, can_continue, did_fail_on_assert, invariant_preflight_check};
3939
use revm::{context::Block, state::Account};
4040
use serde::{Deserialize, Serialize};
4141
use serde_json::json;
@@ -220,7 +220,7 @@ fn build_invariant_progress_json<M: Serialize>(
220220
}
221221

222222
/// Contains data collected during invariant test runs.
223-
struct InvariantTestData<FEN: FoundryEvmNetwork> {
223+
struct InvariantTestData {
224224
// Consumed gas and calldata of every successful fuzz call.
225225
fuzz_cases: Vec<FuzzedCases>,
226226
// Data related to reverts or failed assertions of the test.
@@ -229,8 +229,6 @@ struct InvariantTestData<FEN: FoundryEvmNetwork> {
229229
last_run_inputs: Vec<BasicTxDetails>,
230230
// Additional traces for gas report.
231231
gas_report_traces: Vec<Vec<CallTraceArena>>,
232-
// Last call results of the invariant test.
233-
last_call_results: Option<RawCallResult<FEN>>,
234232
// Line coverage information collected from all fuzzed calls.
235233
line_coverage: Option<HitMaps>,
236234
// Metrics for each fuzzed selector.
@@ -249,30 +247,28 @@ struct InvariantTestData<FEN: FoundryEvmNetwork> {
249247
}
250248

251249
/// Contains invariant test data.
252-
struct InvariantTest<FEN: FoundryEvmNetwork> {
250+
struct InvariantTest {
253251
// Fuzz state of invariant test.
254252
fuzz_state: EvmFuzzState,
255253
// Contracts fuzzed by the invariant test.
256254
targeted_contracts: FuzzRunIdentifiedContracts,
257255
// Data collected during invariant runs.
258-
test_data: InvariantTestData<FEN>,
256+
test_data: InvariantTestData,
259257
}
260258

261-
impl<FEN: FoundryEvmNetwork> InvariantTest<FEN> {
259+
impl InvariantTest {
262260
/// Instantiates an invariant test.
263261
fn new(
264262
fuzz_state: EvmFuzzState,
265263
targeted_contracts: FuzzRunIdentifiedContracts,
266264
failures: InvariantFailures,
267-
last_call_results: Option<RawCallResult<FEN>>,
268265
branch_runner: TestRunner,
269266
) -> Self {
270267
let test_data = InvariantTestData {
271268
fuzz_cases: vec![],
272269
failures,
273270
last_run_inputs: vec![],
274271
gas_report_traces: vec![],
275-
last_call_results,
276272
line_coverage: None,
277273
metrics: Map::default(),
278274
branch_runner,
@@ -297,11 +293,6 @@ impl<FEN: FoundryEvmNetwork> InvariantTest<FEN> {
297293
self.test_data.failures.record_failure(invariant, error);
298294
}
299295

300-
/// Set last invariant test call results.
301-
fn set_last_call_results(&mut self, call_result: Option<RawCallResult<FEN>>) {
302-
self.test_data.last_call_results = call_result;
303-
}
304-
305296
/// Set last invariant run call sequence.
306297
fn set_last_run_inputs(&mut self, inputs: &Vec<BasicTxDetails>) {
307298
self.test_data.last_run_inputs.clone_from(inputs);
@@ -332,7 +323,7 @@ impl<FEN: FoundryEvmNetwork> InvariantTest<FEN> {
332323

333324
/// End invariant test run by collecting results, cleaning collected artifacts and reverting
334325
/// created fuzz state.
335-
fn end_run(&mut self, run: InvariantTestRun<FEN>, gas_samples: usize) {
326+
fn end_run<FEN: FoundryEvmNetwork>(&mut self, run: InvariantTestRun<FEN>, gas_samples: usize) {
336327
// We clear all the targeted contracts created during this run.
337328
self.targeted_contracts.clear_created_contracts(run.created_contracts);
338329

@@ -600,7 +591,7 @@ impl<'a, FEN: FoundryEvmNetwork> InvariantExecutor<'a, FEN> {
600591
|| is_last_call
601592
};
602593

603-
let can_continue_result = if should_check_invariant {
594+
let result = if should_check_invariant {
604595
can_continue(
605596
&invariant_contract,
606597
&mut invariant_test,
@@ -649,11 +640,11 @@ impl<'a, FEN: FoundryEvmNetwork> InvariantExecutor<'a, FEN> {
649640
}
650641
};
651642

652-
if !can_continue_result || current_run.depth == self.config.depth - 1 {
643+
if !result || current_run.depth == self.config.depth - 1 {
653644
invariant_test.set_last_run_inputs(&current_run.inputs);
654645
}
655646
// If test cannot continue then stop current run and exit test suite.
656-
if !can_continue_result {
647+
if !result {
657648
let reason = invariant_test
658649
.test_data
659650
.failures
@@ -787,7 +778,7 @@ impl<'a, FEN: FoundryEvmNetwork> InvariantExecutor<'a, FEN> {
787778
invariant_contract: &InvariantContract<'_>,
788779
fuzz_fixtures: &FuzzFixtures,
789780
fuzz_state: EvmFuzzState,
790-
) -> Result<(InvariantTest<FEN>, WorkerCorpus)> {
781+
) -> Result<(InvariantTest, WorkerCorpus)> {
791782
// Finds out the chosen deployed contracts and/or senders.
792783
self.select_contract_artifacts(invariant_contract.address)?;
793784
let (targeted_senders, targeted_contracts) =
@@ -880,7 +871,6 @@ impl<'a, FEN: FoundryEvmNetwork> InvariantExecutor<'a, FEN> {
880871
fuzz_state,
881872
targeted_contracts,
882873
failures,
883-
None,
884874
self.runner.clone(),
885875
);
886876

@@ -1228,7 +1218,7 @@ impl<'a, FEN: FoundryEvmNetwork> InvariantExecutor<'a, FEN> {
12281218
/// before inserting it into the dictionary. Otherwise, we flood the dictionary with
12291219
/// randomly generated addresses.
12301220
fn collect_data<FEN: FoundryEvmNetwork>(
1231-
invariant_test: &InvariantTest<FEN>,
1221+
invariant_test: &InvariantTest,
12321222
state_changeset: &mut AddressMap<Account>,
12331223
tx: &BasicTxDetails,
12341224
call_result: &RawCallResult<FEN>,

crates/evm/evm/src/executors/invariant/result.rs

Lines changed: 12 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -51,20 +51,6 @@ pub struct InvariantFuzzTestResult {
5151
pub optimization_best_sequence: Vec<BasicTxDetails>,
5252
}
5353

54-
/// Enriched results of an invariant run check.
55-
///
56-
/// Contains the success condition and call results of the last run
57-
pub(crate) struct RichInvariantResults<FEN: FoundryEvmNetwork> {
58-
pub(crate) can_continue: bool,
59-
pub(crate) call_result: Option<RawCallResult<FEN>>,
60-
}
61-
62-
impl<FEN: FoundryEvmNetwork> RichInvariantResults<FEN> {
63-
pub(crate) const fn new(can_continue: bool, call_result: Option<RawCallResult<FEN>>) -> Self {
64-
Self { can_continue, call_result }
65-
}
66-
}
67-
6854
/// Given the executor state, asserts that no invariant has been broken. Otherwise, it fills the
6955
/// external `invariant_failures.failed_invariant` map and returns a generic error.
7056
/// Either returns the call result if successful, or nothing if there was an error.
@@ -223,13 +209,12 @@ fn invariant_inner_sequence<FEN: FoundryEvmNetwork>(executor: &Executor<FEN>) ->
223209
/// For check mode, asserts the invariant and fails if broken.
224210
pub(crate) fn can_continue<FEN: FoundryEvmNetwork>(
225211
invariant_contract: &InvariantContract<'_>,
226-
invariant_test: &mut InvariantTest<FEN>,
212+
invariant_test: &mut InvariantTest,
227213
invariant_run: &mut InvariantTestRun<FEN>,
228214
invariant_config: &InvariantConfig,
229215
call_result: RawCallResult<FEN>,
230216
state_changeset: &StateChangeset,
231-
) -> Result<RichInvariantResults<FEN>> {
232-
let mut call_results = None;
217+
) -> Result<bool> {
233218
let is_optimization = invariant_contract.is_optimization();
234219

235220
let handlers_succeeded = || {
@@ -243,8 +228,6 @@ pub(crate) fn can_continue<FEN: FoundryEvmNetwork>(
243228
})
244229
};
245230

246-
let failures = &mut invariant_test.test_data.failures;
247-
248231
if !call_result.reverted && handlers_succeeded() {
249232
if let Some(traces) = call_result.traces {
250233
invariant_run.run_traces.push(traces);
@@ -269,26 +252,25 @@ pub(crate) fn can_continue<FEN: FoundryEvmNetwork>(
269252
invariant_run.optimization_prefix_len = invariant_run.inputs.len();
270253
}
271254
}
272-
call_results = Some(inv_result);
273255
} else {
274256
// Check mode: assert invariants and fail if broken.
275-
call_results = assert_invariants(
257+
let call_results = assert_invariants(
276258
invariant_contract,
277259
invariant_config,
278260
&invariant_test.targeted_contracts,
279261
&invariant_run.executor,
280262
&invariant_run.inputs,
281-
failures,
263+
&mut invariant_test.test_data.failures,
282264
)?;
283265
if call_results.is_none() {
284-
return Ok(RichInvariantResults::new(false, None));
266+
return Ok(false);
285267
}
286268
}
287269
} else {
288270
let is_assert_failure = did_fail_on_assert(&call_result, state_changeset);
289271

290272
if call_result.reverted {
291-
failures.reverts += 1;
273+
invariant_test.test_data.failures.reverts += 1;
292274
}
293275

294276
if is_assert_failure || (call_result.reverted && invariant_config.fail_on_revert) {
@@ -302,8 +284,9 @@ pub(crate) fn can_continue<FEN: FoundryEvmNetwork>(
302284
&[],
303285
)
304286
.with_assertion_failure(is_assert_failure);
305-
failures.revert_reason = Some(case_data.revert_reason.clone());
306-
failures.record_failure(
287+
invariant_test.test_data.failures.revert_reason =
288+
Some(case_data.revert_reason.clone());
289+
invariant_test.test_data.failures.record_failure(
307290
invariant_contract.invariant_fn,
308291
if is_assert_failure {
309292
InvariantFuzzError::BrokenInvariant(case_data)
@@ -312,7 +295,7 @@ pub(crate) fn can_continue<FEN: FoundryEvmNetwork>(
312295
},
313296
);
314297

315-
return Ok(RichInvariantResults::new(false, None));
298+
return Ok(false);
316299
} else if call_result.reverted && !is_optimization && !invariant_config.has_delay() {
317300
// If we don't fail test on revert then remove the reverted call from inputs.
318301
// Delay-enabled campaigns keep reverted calls so shrinking can preserve their
@@ -321,17 +304,14 @@ pub(crate) fn can_continue<FEN: FoundryEvmNetwork>(
321304
}
322305
}
323306

324-
Ok(RichInvariantResults::new(
325-
failures.can_continue(invariant_contract.invariant_fns.len()),
326-
call_results,
327-
))
307+
Ok(invariant_test.test_data.failures.can_continue(invariant_contract.invariant_fns.len()))
328308
}
329309

330310
/// Given the executor state, asserts conditions within `afterInvariant` function.
331311
/// If call fails then the invariant test is considered failed.
332312
pub(crate) fn assert_after_invariant<FEN: FoundryEvmNetwork>(
333313
invariant_contract: &InvariantContract<'_>,
334-
invariant_test: &mut InvariantTest<FEN>,
314+
invariant_test: &mut InvariantTest,
335315
invariant_run: &InvariantTestRun<FEN>,
336316
invariant_config: &InvariantConfig,
337317
) -> Result<bool> {

crates/evm/evm/src/executors/invariant/shrink.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,7 @@ pub(crate) fn shrink_sequence_value<FEN: FoundryEvmNetwork>(
388388
reset_shrink_progress(config, progress);
389389

390390
let target_address = invariant_contract.address;
391-
let calldata: Bytes = invariant_contract.invariant_function.selector().to_vec().into();
391+
let calldata: Bytes = invariant_contract.invariant_fn.selector().to_vec().into();
392392

393393
// Special case: check if target value is achieved with 0 calls.
394394
if check_sequence_value(executor.clone(), calls, vec![], target_address, calldata.clone())?

crates/forge/src/result.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,9 @@ pub struct TestResult {
421421
/// still be successful (i.e self.success == true) when it's expected to fail.
422422
pub reason: Option<String>,
423423

424+
/// This field will be populated if there are additional invariant broken besides the main one.
425+
pub other_failures: Vec<String>,
426+
424427
/// Minimal reproduction test case for failing test
425428
pub counterexample: Option<CounterExample>,
426429

@@ -527,6 +530,12 @@ impl fmt::Display for TestResult {
527530
} else {
528531
s.push(']');
529532
}
533+
if !self.other_failures.is_empty() {
534+
writeln!(s).unwrap();
535+
for failure in &self.other_failures {
536+
writeln!(s, "{failure}").unwrap();
537+
}
538+
}
530539
s.red().wrap().fmt(f)
531540
}
532541
}
@@ -730,6 +739,7 @@ impl TestResult {
730739
gas_report_traces: Vec<Vec<CallTraceArena>>,
731740
success: bool,
732741
reason: Option<String>,
742+
other_failures: Vec<String>,
733743
counterexample: Option<CounterExample>,
734744
cases: Vec<FuzzedCases>,
735745
reverts: usize,
@@ -752,6 +762,7 @@ impl TestResult {
752762
TestStatus::Failure
753763
};
754764
self.reason = reason;
765+
self.other_failures = other_failures;
755766
self.counterexample = counterexample;
756767
self.gas_report_traces = gas_report_traces;
757768
}

crates/forge/src/runner.rs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -781,11 +781,19 @@ impl<'a, FEN: FoundryEvmNetwork> FunctionRunner<'a, FEN> {
781781
identified_contracts,
782782
&self.cr.mcr.known_contracts,
783783
);
784+
// Filter out additional invariants to test if we already have a persisted failure.
784785
let invariant_contract = InvariantContract::new(
785786
self.address,
786787
self.cr.name,
787788
func,
788-
invariants,
789+
invariants
790+
.into_iter()
791+
.filter(|(invariant_fn, _)| {
792+
*invariant_fn == func
793+
|| (invariant_config.continuous_run
794+
&& !canonicalized(failure_dir.join(invariant_fn.name.clone())).exists())
795+
})
796+
.collect(),
789797
call_after_invariant,
790798
&self.cr.contract.abi,
791799
);
@@ -922,6 +930,7 @@ impl<'a, FEN: FoundryEvmNetwork> FunctionRunner<'a, FEN> {
922930
.errors
923931
.get(&invariant_contract.invariant_fn.name)
924932
.and_then(|err| err.revert_reason());
933+
let mut other_failures = vec![];
925934

926935
if success {
927936
if let Some(best_value) = invariant_result.optimization_best_value {
@@ -1031,11 +1040,20 @@ impl<'a, FEN: FoundryEvmNetwork> FunctionRunner<'a, FEN> {
10311040
continue;
10321041
}
10331042

1034-
if let Some(error) = invariant_result.errors.get(&invariant.name)
1043+
// Generate counterexamples for broken invariant, if there is no failure persisted
1044+
// already.
1045+
let persisted_failure = canonicalized(failure_dir.join(invariant.name.clone()));
1046+
if !persisted_failure.exists()
1047+
&& let Some(error) = invariant_result.errors.get(&invariant.name)
10351048
&& let InvariantFuzzError::BrokenInvariant(case_data)
10361049
| InvariantFuzzError::Revert(case_data) = error
10371050
&& let TestError::Fail(_, ref calls) = case_data.test_error
10381051
{
1052+
other_failures.push(format!(
1053+
"{}: {}",
1054+
invariant.name,
1055+
error.revert_reason().unwrap_or_default()
1056+
));
10391057
match generate_counterexample(
10401058
self.clone_executor(),
10411059
&self.cr.mcr.known_contracts,
@@ -1046,7 +1064,7 @@ impl<'a, FEN: FoundryEvmNetwork> FunctionRunner<'a, FEN> {
10461064
Ok(call_sequence) => {
10471065
record_invariant_failure(
10481066
failure_dir.as_path(),
1049-
canonicalized(failure_dir.join(invariant.name.clone())).as_path(),
1067+
persisted_failure.as_path(),
10501068
&call_sequence,
10511069
&current_settings,
10521070
false,
@@ -1064,6 +1082,7 @@ impl<'a, FEN: FoundryEvmNetwork> FunctionRunner<'a, FEN> {
10641082
invariant_result.gas_report_traces,
10651083
success,
10661084
reason,
1085+
other_failures,
10671086
counterexample,
10681087
invariant_result.cases,
10691088
invariant_result.reverts,

0 commit comments

Comments
 (0)