foundry_evm/executors/invariant/
result.rs1use 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#[derive(Debug)]
29pub struct InvariantFuzzTestResult {
30 pub error: Option<InvariantFuzzError>,
31 pub cases: Vec<FuzzedCases>,
33 pub reverts: usize,
35 pub last_run_inputs: Vec<BasicTxDetails>,
38 pub gas_report_traces: Vec<Vec<CallTraceArena>>,
40 pub line_coverage: Option<HitMaps>,
42 pub metrics: HashMap<String, InvariantMetrics>,
44 pub failed_corpus_replays: usize,
46 pub optimization_best_value: Option<I256>,
49 pub optimization_best_sequence: Vec<BasicTxDetails>,
51}
52
53pub(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
67pub(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
109pub(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
123pub(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 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
166pub(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 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 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 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 invariant_run.inputs.pop();
263 }
264 }
265 Ok(RichInvariantResults::new(true, call_results))
266}
267
268pub(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 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}