foundry_evm/executors/invariant/
result.rs

1use super::{
2    InvariantFailures, InvariantFuzzError, InvariantMetrics, InvariantTest, InvariantTestRun,
3    call_after_invariant_function, call_invariant_function, error::FailedInvariantCaseData,
4};
5use crate::executors::{Executor, RawCallResult};
6use alloy_dyn_abi::JsonAbiExt;
7use eyre::Result;
8use foundry_config::InvariantConfig;
9use foundry_evm_core::utils::StateChangeset;
10use foundry_evm_coverage::HitMaps;
11use foundry_evm_fuzz::{
12    FuzzedCases,
13    invariant::{BasicTxDetails, FuzzRunIdentifiedContracts, InvariantContract},
14};
15use revm_inspectors::tracing::CallTraceArena;
16use std::{borrow::Cow, collections::HashMap};
17
18/// The outcome of an invariant fuzz test
19#[derive(Debug)]
20pub struct InvariantFuzzTestResult {
21    pub error: Option<InvariantFuzzError>,
22    /// Every successful fuzz test case
23    pub cases: Vec<FuzzedCases>,
24    /// Number of reverted fuzz calls
25    pub reverts: usize,
26    /// The entire inputs of the last run of the invariant campaign, used for
27    /// replaying the run for collecting traces.
28    pub last_run_inputs: Vec<BasicTxDetails>,
29    /// Additional traces used for gas report construction.
30    pub gas_report_traces: Vec<Vec<CallTraceArena>>,
31    /// The coverage info collected during the invariant test runs.
32    pub line_coverage: Option<HitMaps>,
33    /// Fuzzed selectors metrics collected during the invariant test runs.
34    pub metrics: HashMap<String, InvariantMetrics>,
35    /// NUmber of failed replays from persisted corpus.
36    pub failed_corpus_replays: usize,
37}
38
39/// Enriched results of an invariant run check.
40///
41/// Contains the success condition and call results of the last run
42pub(crate) struct RichInvariantResults {
43    pub(crate) can_continue: bool,
44    pub(crate) call_result: Option<RawCallResult>,
45}
46
47impl RichInvariantResults {
48    fn new(can_continue: bool, call_result: Option<RawCallResult>) -> Self {
49        Self { can_continue, call_result }
50    }
51}
52
53/// Given the executor state, asserts that no invariant has been broken. Otherwise, it fills the
54/// external `invariant_failures.failed_invariant` map and returns a generic error.
55/// Either returns the call result if successful, or nothing if there was an error.
56pub(crate) fn assert_invariants(
57    invariant_contract: &InvariantContract<'_>,
58    invariant_config: &InvariantConfig,
59    targeted_contracts: &FuzzRunIdentifiedContracts,
60    executor: &Executor,
61    calldata: &[BasicTxDetails],
62    invariant_failures: &mut InvariantFailures,
63) -> Result<Option<RawCallResult>> {
64    let mut inner_sequence = vec![];
65
66    if let Some(fuzzer) = &executor.inspector().fuzzer
67        && let Some(call_generator) = &fuzzer.call_generator
68    {
69        inner_sequence.extend(call_generator.last_sequence.read().iter().cloned());
70    }
71
72    let (call_result, success) = call_invariant_function(
73        executor,
74        invariant_contract.address,
75        invariant_contract.invariant_function.abi_encode_input(&[])?.into(),
76    )?;
77    if !success {
78        // We only care about invariants which we haven't broken yet.
79        if invariant_failures.error.is_none() {
80            let case_data = FailedInvariantCaseData::new(
81                invariant_contract,
82                invariant_config,
83                targeted_contracts,
84                calldata,
85                call_result,
86                &inner_sequence,
87            );
88            invariant_failures.error = Some(InvariantFuzzError::BrokenInvariant(case_data));
89            return Ok(None);
90        }
91    }
92
93    Ok(Some(call_result))
94}
95
96/// Returns if invariant test can continue and last successful call result of the invariant test
97/// function (if it can continue).
98pub(crate) fn can_continue(
99    invariant_contract: &InvariantContract<'_>,
100    invariant_test: &InvariantTest,
101    invariant_run: &mut InvariantTestRun,
102    invariant_config: &InvariantConfig,
103    call_result: RawCallResult,
104    state_changeset: &StateChangeset,
105) -> Result<RichInvariantResults> {
106    let mut call_results = None;
107
108    let handlers_succeeded = || {
109        invariant_test.targeted_contracts.targets.lock().keys().all(|address| {
110            invariant_run.executor.is_success(
111                *address,
112                false,
113                Cow::Borrowed(state_changeset),
114                false,
115            )
116        })
117    };
118
119    // Assert invariants if the call did not revert and the handlers did not fail.
120    if !call_result.reverted && handlers_succeeded() {
121        if let Some(traces) = call_result.traces {
122            invariant_run.run_traces.push(traces);
123        }
124
125        call_results = assert_invariants(
126            invariant_contract,
127            invariant_config,
128            &invariant_test.targeted_contracts,
129            &invariant_run.executor,
130            &invariant_run.inputs,
131            &mut invariant_test.execution_data.borrow_mut().failures,
132        )?;
133        if call_results.is_none() {
134            return Ok(RichInvariantResults::new(false, None));
135        }
136    } else {
137        // Increase the amount of reverts.
138        let mut invariant_data = invariant_test.execution_data.borrow_mut();
139        invariant_data.failures.reverts += 1;
140        // If fail on revert is set, we must return immediately.
141        if invariant_config.fail_on_revert {
142            let case_data = FailedInvariantCaseData::new(
143                invariant_contract,
144                invariant_config,
145                &invariant_test.targeted_contracts,
146                &invariant_run.inputs,
147                call_result,
148                &[],
149            );
150            invariant_data.failures.revert_reason = Some(case_data.revert_reason.clone());
151            invariant_data.failures.error = Some(InvariantFuzzError::Revert(case_data));
152
153            return Ok(RichInvariantResults::new(false, None));
154        } else if call_result.reverted {
155            // If we don't fail test on revert then remove last reverted call from inputs.
156            // This improves shrinking performance as irrelevant calls won't be checked again.
157            invariant_run.inputs.pop();
158        }
159    }
160    Ok(RichInvariantResults::new(true, call_results))
161}
162
163/// Given the executor state, asserts conditions within `afterInvariant` function.
164/// If call fails then the invariant test is considered failed.
165pub(crate) fn assert_after_invariant(
166    invariant_contract: &InvariantContract<'_>,
167    invariant_test: &InvariantTest,
168    invariant_run: &InvariantTestRun,
169    invariant_config: &InvariantConfig,
170) -> Result<bool> {
171    let (call_result, success) =
172        call_after_invariant_function(&invariant_run.executor, invariant_contract.address)?;
173    // Fail the test case if `afterInvariant` doesn't succeed.
174    if !success {
175        let case_data = FailedInvariantCaseData::new(
176            invariant_contract,
177            invariant_config,
178            &invariant_test.targeted_contracts,
179            &invariant_run.inputs,
180            call_result,
181            &[],
182        );
183        invariant_test.set_error(InvariantFuzzError::BrokenInvariant(case_data));
184    }
185    Ok(success)
186}