Skip to main content

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