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 alloy_sol_types::{Panic, PanicKind, Revert, SolError, SolInterface};
9use eyre::Result;
10use foundry_config::InvariantConfig;
11use foundry_evm_core::{
12    abi::Vm,
13    constants::CHEATCODE_ADDRESS,
14    decode::{ASSERTION_FAILED_PREFIX, decode_console_log},
15    evm::FoundryEvmNetwork,
16    utils::StateChangeset,
17};
18use foundry_evm_coverage::HitMaps;
19use foundry_evm_fuzz::{
20    BasicTxDetails, FuzzedCases,
21    invariant::{FuzzRunIdentifiedContracts, InvariantContract},
22};
23use revm::interpreter::InstructionResult;
24use revm_inspectors::tracing::CallTraceArena;
25use std::{borrow::Cow, collections::HashMap};
26
27/// The outcome of an invariant fuzz test
28#[derive(Debug)]
29pub struct InvariantFuzzTestResult {
30    pub error: Option<InvariantFuzzError>,
31    /// Every successful fuzz test case
32    pub cases: Vec<FuzzedCases>,
33    /// Number of reverted fuzz calls
34    pub reverts: usize,
35    /// The entire inputs of the last run of the invariant campaign, used for
36    /// replaying the run for collecting traces.
37    pub last_run_inputs: Vec<BasicTxDetails>,
38    /// Additional traces used for gas report construction.
39    pub gas_report_traces: Vec<Vec<CallTraceArena>>,
40    /// The coverage info collected during the invariant test runs.
41    pub line_coverage: Option<HitMaps>,
42    /// Fuzzed selectors metrics collected during the invariant test runs.
43    pub metrics: HashMap<String, InvariantMetrics>,
44    /// Number of failed replays from persisted corpus.
45    pub failed_corpus_replays: usize,
46    /// For optimization mode (int256 return): the best (maximum) value achieved.
47    /// None means standard invariant check mode.
48    pub optimization_best_value: Option<I256>,
49    /// For optimization mode: the call sequence that produced the best value.
50    pub optimization_best_sequence: Vec<BasicTxDetails>,
51}
52
53/// Enriched results of an invariant run check.
54///
55/// Contains the success condition and call results of the last run
56pub(crate) struct RichInvariantResults<FEN: FoundryEvmNetwork> {
57    pub(crate) can_continue: bool,
58    pub(crate) call_result: Option<RawCallResult<FEN>>,
59}
60
61impl<FEN: FoundryEvmNetwork> RichInvariantResults<FEN> {
62    pub(crate) const fn new(can_continue: bool, call_result: Option<RawCallResult<FEN>>) -> Self {
63        Self { can_continue, call_result }
64    }
65}
66
67/// Returns true if this call failed due to a Solidity assertion:
68/// - `Panic(0x01)`, or
69/// - legacy invalid opcode assert behavior.
70pub(crate) fn is_assertion_failure<FEN: FoundryEvmNetwork>(
71    call_result: &RawCallResult<FEN>,
72) -> bool {
73    if !call_result.reverted {
74        return false;
75    }
76
77    is_assert_panic(call_result.result.as_ref())
78        || matches!(call_result.exit_reason, Some(InstructionResult::InvalidFEOpcode))
79        || is_revert_assertion_failure(call_result.result.as_ref())
80        || is_cheatcode_assert_revert(call_result)
81}
82
83fn is_assert_panic(data: &[u8]) -> bool {
84    Panic::abi_decode(data).is_ok_and(|panic| panic == PanicKind::Assert.into())
85}
86
87fn is_revert_assertion_failure(data: &[u8]) -> bool {
88    Revert::abi_decode(data).is_ok_and(|revert| revert.reason.contains(ASSERTION_FAILED_PREFIX))
89}
90
91fn is_cheatcode_assert_revert<FEN: FoundryEvmNetwork>(call_result: &RawCallResult<FEN>) -> bool {
92    fn decoded_cheatcode_message(data: &[u8]) -> Option<String> {
93        Vm::VmErrors::abi_decode(data).ok().map(|error| error.to_string())
94    }
95
96    call_result.reverter == Some(CHEATCODE_ADDRESS)
97        && decoded_cheatcode_message(call_result.result.as_ref())
98            .is_some_and(|message| message.starts_with(ASSERTION_FAILED_PREFIX))
99}
100
101fn logged_assertion_failure<FEN: FoundryEvmNetwork>(call_result: &RawCallResult<FEN>) -> bool {
102    call_result
103        .logs
104        .iter()
105        .filter_map(decode_console_log)
106        .any(|msg| msg.starts_with(ASSERTION_FAILED_PREFIX))
107}
108
109/// Returns whether the current fuzz call should be treated as an assertion failure.
110///
111/// This covers Solidity `assert`, legacy invalid-opcode assertions, `vm.assert*` reverts, and the
112/// non-reverting `GLOBAL_FAIL_SLOT` path used when `assertions_revert = false`.
113pub(crate) fn did_fail_on_assert<FEN: FoundryEvmNetwork>(
114    call_result: &RawCallResult<FEN>,
115    state_changeset: &StateChangeset,
116) -> bool {
117    is_assertion_failure(call_result)
118        || call_result.has_state_snapshot_failure
119        || Executor::<FEN>::has_pending_global_failure(state_changeset)
120        || logged_assertion_failure(call_result)
121}
122
123/// Given the executor state, asserts that no invariant has been broken. Otherwise, it fills the
124/// external `invariant_failures.failed_invariant` map and returns a generic error.
125/// Either returns the call result if successful, or nothing if there was an error.
126pub(crate) fn assert_invariants<FEN: FoundryEvmNetwork>(
127    invariant_contract: &InvariantContract<'_>,
128    invariant_config: &InvariantConfig,
129    targeted_contracts: &FuzzRunIdentifiedContracts,
130    executor: &Executor<FEN>,
131    calldata: &[BasicTxDetails],
132    invariant_failures: &mut InvariantFailures,
133) -> Result<Option<RawCallResult<FEN>>> {
134    let mut inner_sequence = vec![];
135
136    if let Some(fuzzer) = &executor.inspector().fuzzer
137        && let Some(call_generator) = &fuzzer.call_generator
138    {
139        inner_sequence.extend(call_generator.last_sequence.read().iter().cloned());
140    }
141
142    let (call_result, success) = call_invariant_function(
143        executor,
144        invariant_contract.address,
145        invariant_contract.invariant_function.abi_encode_input(&[])?.into(),
146    )?;
147    if !success {
148        // We only care about invariants which we haven't broken yet.
149        if invariant_failures.error.is_none() {
150            let case_data = FailedInvariantCaseData::new(
151                invariant_contract,
152                invariant_config,
153                targeted_contracts,
154                calldata,
155                call_result,
156                &inner_sequence,
157            );
158            invariant_failures.error = Some(InvariantFuzzError::BrokenInvariant(case_data));
159            return Ok(None);
160        }
161    }
162
163    Ok(Some(call_result))
164}
165
166/// Returns if invariant test can continue and last successful call result of the invariant test
167/// function (if it can continue).
168///
169/// For optimization mode (int256 return), tracks the max value but never fails on invariant.
170/// For check mode, asserts the invariant and fails if broken.
171pub(crate) fn can_continue<FEN: FoundryEvmNetwork>(
172    invariant_contract: &InvariantContract<'_>,
173    invariant_test: &mut InvariantTest<FEN>,
174    invariant_run: &mut InvariantTestRun<FEN>,
175    invariant_config: &InvariantConfig,
176    call_result: RawCallResult<FEN>,
177    state_changeset: &StateChangeset,
178) -> Result<RichInvariantResults<FEN>> {
179    let mut call_results = None;
180    let is_optimization = invariant_contract.is_optimization();
181
182    let handlers_succeeded = || {
183        invariant_test.targeted_contracts.targets.lock().keys().all(|address| {
184            invariant_run.executor.is_success(
185                *address,
186                false,
187                Cow::Borrowed(state_changeset),
188                false,
189            )
190        })
191    };
192
193    if !call_result.reverted && handlers_succeeded() {
194        if let Some(traces) = call_result.traces {
195            invariant_run.run_traces.push(traces);
196        }
197
198        if is_optimization {
199            // Optimization mode: call invariant and track max value, never fail.
200            let (inv_result, success) = call_invariant_function(
201                &invariant_run.executor,
202                invariant_contract.address,
203                invariant_contract.invariant_function.abi_encode_input(&[])?.into(),
204            )?;
205            if success
206                && inv_result.result.len() >= 32
207                && let Some(value) = I256::try_from_be_slice(&inv_result.result[..32])
208            {
209                invariant_test.update_optimization_value(value, &invariant_run.inputs);
210                // Track the best value and its prefix length for this run
211                // (used for corpus persistence — materialized once at run end).
212                if invariant_run.optimization_value.is_none_or(|prev| value > prev) {
213                    invariant_run.optimization_value = Some(value);
214                    invariant_run.optimization_prefix_len = invariant_run.inputs.len();
215                }
216            }
217            call_results = Some(inv_result);
218        } else {
219            // Check mode: assert invariants and fail if broken.
220            call_results = assert_invariants(
221                invariant_contract,
222                invariant_config,
223                &invariant_test.targeted_contracts,
224                &invariant_run.executor,
225                &invariant_run.inputs,
226                &mut invariant_test.test_data.failures,
227            )?;
228            if call_results.is_none() {
229                return Ok(RichInvariantResults::new(false, None));
230            }
231        }
232    } else {
233        let invariant_data = &mut invariant_test.test_data;
234        let is_assert_failure = did_fail_on_assert(&call_result, state_changeset);
235
236        if call_result.reverted {
237            invariant_data.failures.reverts += 1;
238        }
239
240        if is_assert_failure || (call_result.reverted && invariant_config.fail_on_revert) {
241            let case_data = FailedInvariantCaseData::new(
242                invariant_contract,
243                invariant_config,
244                &invariant_test.targeted_contracts,
245                &invariant_run.inputs,
246                call_result,
247                &[],
248            )
249            .with_assertion_failure(is_assert_failure);
250            invariant_data.failures.revert_reason = Some(case_data.revert_reason.clone());
251            invariant_data.failures.error = Some(if is_assert_failure {
252                InvariantFuzzError::BrokenInvariant(case_data)
253            } else {
254                InvariantFuzzError::Revert(case_data)
255            });
256
257            return Ok(RichInvariantResults::new(false, None));
258        } else if call_result.reverted && !is_optimization && !invariant_config.has_delay() {
259            // If we don't fail test on revert then remove the reverted call from inputs.
260            // Delay-enabled campaigns keep reverted calls so shrinking can preserve their
261            // warp/roll contribution when building the final counterexample.
262            invariant_run.inputs.pop();
263        }
264    }
265    Ok(RichInvariantResults::new(true, call_results))
266}
267
268/// Given the executor state, asserts conditions within `afterInvariant` function.
269/// If call fails then the invariant test is considered failed.
270pub(crate) fn assert_after_invariant<FEN: FoundryEvmNetwork>(
271    invariant_contract: &InvariantContract<'_>,
272    invariant_test: &mut InvariantTest<FEN>,
273    invariant_run: &InvariantTestRun<FEN>,
274    invariant_config: &InvariantConfig,
275) -> Result<bool> {
276    let (call_result, success) =
277        call_after_invariant_function(&invariant_run.executor, invariant_contract.address)?;
278    // Fail the test case if `afterInvariant` doesn't succeed.
279    if !success {
280        let case_data = FailedInvariantCaseData::new(
281            invariant_contract,
282            invariant_config,
283            &invariant_test.targeted_contracts,
284            &invariant_run.inputs,
285            call_result,
286            &[],
287        );
288        invariant_test.set_error(InvariantFuzzError::BrokenInvariant(case_data));
289    }
290    Ok(success)
291}
292
293#[cfg(test)]
294mod tests {
295    use super::*;
296    use alloy_primitives::Bytes;
297    use foundry_evm_core::evm::EthEvmNetwork;
298
299    fn panic_payload(code: u8) -> Bytes {
300        let mut payload = vec![0_u8; 36];
301        payload[..4].copy_from_slice(&[0x4e, 0x48, 0x7b, 0x71]);
302        payload[35] = code;
303        payload.into()
304    }
305
306    #[test]
307    fn detects_assert_panic_code() {
308        let call_result = RawCallResult::<EthEvmNetwork> {
309            reverted: true,
310            result: panic_payload(0x01),
311            ..Default::default()
312        };
313        assert!(is_assertion_failure(&call_result));
314    }
315
316    #[test]
317    fn ignores_non_assert_panic_code() {
318        let call_result = RawCallResult::<EthEvmNetwork> {
319            reverted: true,
320            result: panic_payload(0x11),
321            ..Default::default()
322        };
323        assert!(!is_assertion_failure(&call_result));
324    }
325
326    #[test]
327    fn detects_legacy_invalid_opcode_assert() {
328        let call_result = RawCallResult::<EthEvmNetwork> {
329            reverted: true,
330            exit_reason: Some(InstructionResult::InvalidFEOpcode),
331            ..Default::default()
332        };
333        assert!(is_assertion_failure(&call_result));
334    }
335
336    #[test]
337    fn detects_vm_assert_revert() {
338        let call_result = RawCallResult::<EthEvmNetwork> {
339            reverted: true,
340            result: Vm::CheatcodeError { message: format!("{ASSERTION_FAILED_PREFIX}: 1 != 2") }
341                .abi_encode()
342                .into(),
343            reverter: Some(CHEATCODE_ADDRESS),
344            ..Default::default()
345        };
346        assert!(is_assertion_failure(&call_result));
347    }
348
349    #[test]
350    fn detects_assertion_failure_revert_reason() {
351        let call_result = RawCallResult::<EthEvmNetwork> {
352            reverted: true,
353            result: Revert { reason: format!("{ASSERTION_FAILED_PREFIX}: expected") }
354                .abi_encode()
355                .into(),
356            ..Default::default()
357        };
358        assert!(is_assertion_failure(&call_result));
359    }
360
361    #[test]
362    fn ignores_empty_cheatcode_revert() {
363        let call_result = RawCallResult::<EthEvmNetwork> {
364            reverted: true,
365            result: Bytes::new(),
366            reverter: Some(CHEATCODE_ADDRESS),
367            ..Default::default()
368        };
369        assert!(!is_assertion_failure(&call_result));
370    }
371}