foundry_evm/executors/invariant/
result.rs

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