foundry_evm/executors/invariant/
error.rs1use super::InvariantContract;
2use crate::{
3 executors::RawCallResult,
4 inspectors::{EdgeCovHit, EdgeCoverage},
5};
6use alloy_json_abi::Function;
7use alloy_primitives::{Address, B256, Bytes, Selector, keccak256};
8use foundry_config::InvariantConfig;
9use foundry_evm_core::{
10 decode::{ASSERTION_FAILED_PREFIX, EMPTY_REVERT_DATA, RevertDecoder},
11 evm::FoundryEvmNetwork,
12};
13use foundry_evm_fuzz::{BasicTxDetails, Reason, invariant::FuzzRunIdentifiedContracts};
14use proptest::test_runner::TestError;
15use std::{collections::HashMap, fmt};
16
17#[derive(Clone, Debug)]
21pub struct HandlerAssertionFailure {
22 pub reverter: Address,
24 pub selector: Selector,
26 pub call_sequence: Vec<BasicTxDetails>,
28 pub original_sequence_len: usize,
30 pub revert_reason: String,
32 pub fork_block_number: Option<u64>,
34 pub edge_fingerprint: B256,
37}
38
39impl HandlerAssertionFailure {
40 pub const fn from_replayed_sequence(
42 call_sequence: Vec<BasicTxDetails>,
43 reverter: Address,
44 selector: Selector,
45 edge_fingerprint: B256,
46 revert_reason: String,
47 ) -> Self {
48 let original_sequence_len = call_sequence.len();
49 Self {
50 reverter,
51 selector,
52 call_sequence,
53 original_sequence_len,
54 revert_reason,
55 fork_block_number: None,
56 edge_fingerprint,
57 }
58 }
59}
60
61pub struct InvariantRunCtx<'a> {
63 pub contract: &'a InvariantContract<'a>,
65 pub config: &'a InvariantConfig,
67 pub targeted_contracts: &'a FuzzRunIdentifiedContracts,
69 pub calldata: &'a [BasicTxDetails],
71}
72
73impl<'a> InvariantRunCtx<'a> {
74 pub fn failed_case<FEN: FoundryEvmNetwork>(
78 &self,
79 broken_fn: &Function,
80 fail_on_revert: bool,
81 assertion_failure: bool,
82 call_result: RawCallResult<FEN>,
83 inner_sequence: &[Option<BasicTxDetails>],
84 ) -> FailedInvariantCaseData {
85 let revert_reason = self.decode_revert_reason(&call_result, assertion_failure);
86 let origin = broken_fn.name.as_str();
87 FailedInvariantCaseData {
88 test_error: TestError::Fail(
89 format!("{origin}, reason: {revert_reason}").into(),
90 self.calldata.to_vec(),
91 ),
92 return_reason: "".into(),
93 revert_reason,
94 addr: self.contract.address,
95 calldata: broken_fn.selector().to_vec().into(),
96 inner_sequence: inner_sequence.to_vec(),
97 shrink_run_limit: self.config.shrink_run_limit,
98 fail_on_revert,
99 assertion_failure,
100 fork_block_number: call_result.fork_block_number,
101 }
102 }
103
104 pub fn decode_revert_reason<FEN: FoundryEvmNetwork>(
107 &self,
108 call_result: &RawCallResult<FEN>,
109 assertion_failure: bool,
110 ) -> String {
111 let revert_reason = RevertDecoder::new()
112 .with_abis(self.targeted_contracts.targets().values().map(|c| &c.abi))
113 .with_abi(self.contract.abi)
114 .decode(call_result.result.as_ref(), call_result.exit_reason);
115 let needs_fallback = matches!(revert_reason.as_str(), "" | EMPTY_REVERT_DATA);
118 if needs_fallback && (!call_result.reverted || assertion_failure) {
119 ASSERTION_FAILED_PREFIX.to_string()
120 } else {
121 revert_reason
122 }
123 }
124}
125
126pub fn handler_edge_fingerprint(
129 pre_merge_edges_hash: Option<B256>,
130 target: Address,
131 selector: Selector,
132) -> B256 {
133 if let Some(hash) = pre_merge_edges_hash {
134 return hash;
135 }
136 let mut buf = [0u8; 24];
137 buf[..20].copy_from_slice(target.as_slice());
138 buf[20..].copy_from_slice(selector.as_slice());
139 keccak256(buf)
140}
141
142#[expect(clippy::too_many_arguments)]
146pub(crate) fn record_handler_assertion_bug<FEN: FoundryEvmNetwork>(
147 invariant_contract: &InvariantContract<'_>,
148 config: &InvariantConfig,
149 targeted_contracts: &FuzzRunIdentifiedContracts,
150 failures: &mut InvariantFailures,
151 inputs: &mut Vec<BasicTxDetails>,
152 handler_target: Address,
153 handler_selector: Selector,
154 pre_merge_edges_hash: Option<B256>,
155 call_result: RawCallResult<FEN>,
156 call_reverted: bool,
157 is_optimization: bool,
158) {
159 let fingerprint =
160 handler_edge_fingerprint(pre_merge_edges_hash, handler_target, handler_selector);
161
162 if !handler_site_already_minimal(
163 &failures.failures,
164 (handler_target, handler_selector),
165 inputs.len(),
166 ) {
167 let revert_reason = InvariantRunCtx {
169 contract: invariant_contract,
170 config,
171 targeted_contracts,
172 calldata: inputs,
173 }
174 .decode_revert_reason(&call_result, true);
175 let call_sequence = inputs.clone();
176 let original_sequence_len = call_sequence.len();
177 failures.record_handler_failure(HandlerAssertionFailure {
178 reverter: handler_target,
179 selector: handler_selector,
180 call_sequence,
181 original_sequence_len,
182 revert_reason,
183 fork_block_number: call_result.fork_block_number,
184 edge_fingerprint: fingerprint,
185 });
186 }
187
188 if call_reverted && !is_optimization && !config.has_delay() {
191 inputs.pop();
192 }
193}
194
195pub fn handler_site_already_minimal(
198 failures: &HashMap<FailureKey, InvariantFuzzError>,
199 site: (Address, Selector),
200 candidate_len: usize,
201) -> bool {
202 failures
203 .get(&FailureKey::Handler(site.0, site.1))
204 .and_then(InvariantFuzzError::as_handler_assertion)
205 .is_some_and(|existing| existing.call_sequence.len() <= candidate_len)
206}
207
208pub fn snapshot_edge_fingerprint<FEN: FoundryEvmNetwork>(
211 call_result: &RawCallResult<FEN>,
212) -> Option<B256> {
213 let edges = call_result.edge_coverage.as_ref()?;
214 if edges.is_empty() {
215 return None;
216 }
217 match edges {
218 EdgeCoverage::Hash(edges) => Some(keccak256(edges)),
219 EdgeCoverage::CollisionFree(hits) => {
220 let mut sorted: Vec<&EdgeCovHit> = hits.iter().collect();
225 sorted.sort_unstable_by_key(|hit| hit.edge);
226
227 let mut bytes = Vec::with_capacity(sorted.len() * (20 + 8 + 32 + 1 + 8 + 1));
229 for hit in sorted {
230 bytes.extend_from_slice(hit.edge.address.as_slice());
231 bytes.extend_from_slice(&hit.edge.pc.to_le_bytes());
232 bytes.extend_from_slice(&hit.edge.jump_dest.to_be_bytes::<32>());
233 bytes.push(u8::from(hit.edge.depth.is_some()));
236 bytes.extend_from_slice(&hit.edge.depth.unwrap_or(0).to_le_bytes());
237 bytes.push(hit.count);
238 }
239 Some(keccak256(bytes))
240 }
241 }
242}
243
244#[derive(Clone, Debug, Eq, Hash, PartialEq)]
247pub enum FailureKey {
248 Invariant(String),
250 Handler(Address, Selector),
253}
254
255#[derive(Clone, Default)]
260pub struct InvariantFailures {
261 pub reverts: usize,
263 pub(crate) failures: HashMap<FailureKey, InvariantFuzzError>,
267 invariant_count: usize,
269 handler_count: usize,
271 revision: usize,
273}
274
275impl InvariantFailures {
276 pub fn new() -> Self {
277 Self::default()
278 }
279
280 pub fn partition(
282 self,
283 ) -> (HashMap<String, InvariantFuzzError>, HashMap<(Address, Selector), InvariantFuzzError>)
284 {
285 let mut invariant_errors = HashMap::new();
286 let mut handler_errors = HashMap::new();
287 for (key, err) in self.failures {
288 match key {
289 FailureKey::Invariant(name) => {
290 invariant_errors.insert(name, err);
291 }
292 FailureKey::Handler(addr, sel) => {
293 handler_errors.insert((addr, sel), err);
294 }
295 }
296 }
297 (invariant_errors, handler_errors)
298 }
299
300 pub fn record_failure(&mut self, invariant: &Function, failure: InvariantFuzzError) {
301 let prev = self.failures.insert(FailureKey::Invariant(invariant.name.clone()), failure);
302 self.revision = self.revision.wrapping_add(1);
303 if prev.is_none() {
304 self.invariant_count += 1;
305 }
306 }
307
308 pub fn has_failure(&self, invariant: &Function) -> bool {
309 self.failures.contains_key(&FailureKey::Invariant(invariant.name.clone()))
310 }
311
312 pub fn get_failure(&self, invariant: &Function) -> Option<&InvariantFuzzError> {
313 self.failures.get(&FailureKey::Invariant(invariant.name.clone()))
314 }
315
316 pub fn broken_reason(&self, invariant: &Function) -> String {
319 self.get_failure(invariant).and_then(|e| e.revert_reason()).unwrap_or_default()
320 }
321
322 pub const fn can_continue(&self, invariants: usize) -> bool {
323 self.invariant_count() < invariants
324 }
325
326 pub const fn invariant_count(&self) -> usize {
328 self.invariant_count
329 }
330
331 pub const fn handler_count(&self) -> usize {
333 self.handler_count
334 }
335
336 pub const fn revision(&self) -> usize {
338 self.revision
339 }
340
341 pub fn handler_failures_mut(&mut self) -> impl Iterator<Item = &mut InvariantFuzzError> {
342 self.failures.iter_mut().filter_map(|(key, error)| match key {
343 FailureKey::Handler(_, _) => Some(error),
344 FailureKey::Invariant(_) => None,
345 })
346 }
347
348 pub fn record_handler_failure(&mut self, failure: HandlerAssertionFailure) {
351 let site = (failure.reverter, failure.selector);
352 if !handler_site_already_minimal(&self.failures, site, failure.call_sequence.len()) {
353 let prev = self.failures.insert(
354 FailureKey::Handler(site.0, site.1),
355 InvariantFuzzError::HandlerAssertion(failure),
356 );
357 self.revision = self.revision.wrapping_add(1);
358 if prev.is_none() {
359 self.handler_count += 1;
360 }
361 }
362 }
363
364 pub fn seed_handler_failure(
367 &mut self,
368 target: Address,
369 selector: Selector,
370 err: InvariantFuzzError,
371 ) {
372 let prev = self.failures.insert(FailureKey::Handler(target, selector), err);
373 self.revision = self.revision.wrapping_add(1);
374 if prev.is_none() {
375 self.handler_count += 1;
376 }
377 }
378
379 pub fn has_handler_failure(&self, target: Address, selector: Selector) -> bool {
381 self.failures.contains_key(&FailureKey::Handler(target, selector))
382 }
383}
384
385impl fmt::Display for InvariantFailures {
386 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
387 writeln!(f)?;
388 writeln!(f, " ❌ Failures: {}", self.invariant_count())?;
389 Ok(())
390 }
391}
392
393#[derive(Clone, Debug)]
394pub enum InvariantFuzzError {
395 Revert(FailedInvariantCaseData),
397 BrokenInvariant(FailedInvariantCaseData),
399 HandlerAssertion(HandlerAssertionFailure),
402 MaxAssumeRejects(u32),
404}
405
406impl InvariantFuzzError {
407 #[expect(clippy::too_many_arguments)]
409 pub fn from_replayed_invariant(
410 invariant_address: Address,
411 invariant: &Function,
412 call_sequence: Vec<BasicTxDetails>,
413 reason: Option<String>,
414 config: &InvariantConfig,
415 fail_on_revert: bool,
416 assertion_failure: bool,
417 is_revert: bool,
418 ) -> Self {
419 let revert_reason = reason.unwrap_or_default();
420 let origin = invariant.name.as_str();
421 let failure = FailedInvariantCaseData {
422 test_error: TestError::Fail(
423 format!("{origin}, reason: {revert_reason}").into(),
424 call_sequence,
425 ),
426 return_reason: "".into(),
427 revert_reason,
428 addr: invariant_address,
429 calldata: invariant.selector().to_vec().into(),
430 inner_sequence: Vec::new(),
431 shrink_run_limit: config.shrink_run_limit,
432 fail_on_revert,
433 assertion_failure,
434 fork_block_number: None,
435 };
436 if is_revert { Self::Revert(failure) } else { Self::BrokenInvariant(failure) }
437 }
438
439 pub const fn fork_block_number(&self) -> Option<u64> {
441 match self {
442 Self::BrokenInvariant(case_data) | Self::Revert(case_data) => {
443 case_data.fork_block_number
444 }
445 Self::HandlerAssertion(failure) => failure.fork_block_number,
446 Self::MaxAssumeRejects(_) => None,
447 }
448 }
449
450 pub fn revert_reason(&self) -> Option<String> {
451 match self {
452 Self::BrokenInvariant(case_data) | Self::Revert(case_data) => {
453 (!case_data.revert_reason.is_empty()).then(|| case_data.revert_reason.clone())
454 }
455 Self::HandlerAssertion(failure) => {
456 (!failure.revert_reason.is_empty()).then(|| failure.revert_reason.clone())
457 }
458 Self::MaxAssumeRejects(allowed) => {
459 Some(format!("`vm.assume` rejected too many inputs ({allowed} allowed)"))
460 }
461 }
462 }
463
464 pub const fn as_handler_assertion(&self) -> Option<&HandlerAssertionFailure> {
466 match self {
467 Self::HandlerAssertion(failure) => Some(failure),
468 _ => None,
469 }
470 }
471
472 pub const fn as_handler_assertion_mut(&mut self) -> Option<&mut HandlerAssertionFailure> {
474 match self {
475 Self::HandlerAssertion(failure) => Some(failure),
476 _ => None,
477 }
478 }
479}
480
481#[derive(Clone, Debug)]
482pub struct FailedInvariantCaseData {
483 pub test_error: TestError<Vec<BasicTxDetails>>,
485 pub return_reason: Reason,
487 pub revert_reason: String,
489 pub addr: Address,
491 pub calldata: Bytes,
493 pub inner_sequence: Vec<Option<BasicTxDetails>>,
495 pub shrink_run_limit: u32,
497 pub fail_on_revert: bool,
499 pub assertion_failure: bool,
501 pub fork_block_number: Option<u64>,
503}
504
505#[cfg(test)]
506mod tests {
507 use super::*;
508 use foundry_evm_fuzz::CallDetails;
509
510 fn handler_failure(sequence_len: usize) -> HandlerAssertionFailure {
511 let tx = BasicTxDetails {
512 warp: None,
513 roll: None,
514 sender: Address::ZERO,
515 call_details: CallDetails {
516 target: Address::ZERO,
517 calldata: Bytes::new(),
518 value: None,
519 },
520 };
521 HandlerAssertionFailure::from_replayed_sequence(
522 vec![tx; sequence_len],
523 Address::ZERO,
524 Selector::ZERO,
525 B256::ZERO,
526 "assertion failed".to_string(),
527 )
528 }
529
530 #[test]
531 fn failure_revision_tracks_shorter_handler_reproducer() {
532 let mut failures = InvariantFailures::new();
533 failures.record_handler_failure(handler_failure(2));
534 let checkpoint = failures.revision();
535
536 failures.record_handler_failure(handler_failure(2));
537 assert_eq!(failures.revision(), checkpoint);
538
539 failures.record_handler_failure(handler_failure(1));
540 assert_ne!(failures.revision(), checkpoint);
541 let failure = failures
542 .failures
543 .get(&FailureKey::Handler(Address::ZERO, Selector::ZERO))
544 .and_then(InvariantFuzzError::as_handler_assertion)
545 .unwrap();
546 assert_eq!(failure.call_sequence.len(), 1);
547 }
548}