Skip to content

Commit b74f48a

Browse files
authored
fix: fork errors getting overriden by RevertDiagnostic inspector (#10954)
* fix: fork revert diagnostic * feat(`cheatcodes`): introduces `ForkRevertDiagnostic`
1 parent 4461e0d commit b74f48a

3 files changed

Lines changed: 74 additions & 31 deletions

File tree

crates/cheatcodes/src/inspector.rs

Lines changed: 64 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ use foundry_common::{SELECTOR_LEN, TransactionMaybeSigned, evm::Breakpoints};
3737
use foundry_evm_core::{
3838
InspectorExt,
3939
abi::Vm::stopExpectSafeMemoryCall,
40-
backend::{DatabaseError, DatabaseExt, RevertDiagnostic},
40+
backend::{DatabaseError, DatabaseExt, ForkRevertDiagnostic},
4141
constants::{CHEATCODE_ADDRESS, HARDHAT_CONSOLE_ADDRESS, MAGIC_ASSUME},
4242
evm::{FoundryEvm, new_evm_with_existing_context},
4343
};
@@ -395,8 +395,8 @@ pub struct Cheatcodes {
395395
/// Assume next call can revert and discard fuzz run if it does.
396396
pub assume_no_revert: Option<AssumeNoRevert>,
397397

398-
/// Additional diagnostic for reverts
399-
pub fork_revert_diagnostic: Option<RevertDiagnostic>,
398+
/// Fork revert diagnostic information
399+
pub fork_revert_diagnostic: Option<ForkRevertDiagnostic>,
400400

401401
/// Recorded storage reads and writes
402402
pub accesses: RecordAccess,
@@ -666,6 +666,12 @@ impl Cheatcodes {
666666
call: &mut CallInputs,
667667
executor: &mut impl CheatcodesExecutor,
668668
) -> Option<CallOutcome> {
669+
// Initialize fork revert diagnostic for this call if we're in fork mode
670+
if ecx.journaled_state.db().is_forked_mode() {
671+
let revert_diag = self.fork_revert_diagnostic.get_or_insert_default();
672+
revert_diag.current_call_target = Some(call.target_address);
673+
}
674+
669675
let gas = Gas::new(call.gas_limit);
670676
let curr_depth = ecx.journaled_state.depth();
671677

@@ -1036,6 +1042,36 @@ impl Cheatcodes {
10361042
None => false,
10371043
}
10381044
}
1045+
1046+
/// Checks if we should diagnose a fork revert for the current state
1047+
fn should_diagnose_fork_revert(&self, interpreter: &Interpreter, ecx: Ecx) -> Option<Address> {
1048+
// Only proceed if this is a REVERT opcode
1049+
if interpreter.bytecode.opcode() != op::REVERT {
1050+
return None;
1051+
}
1052+
1053+
// Only handle reverts with empty data (size = 0)
1054+
// Reverts with data already have meaningful error messages
1055+
let size = interpreter.stack.peek(1).ok()?;
1056+
if !size.is_zero() {
1057+
return None;
1058+
}
1059+
1060+
// Check if fork diagnostic tracking is enabled
1061+
// This is only set when we're in fork mode
1062+
let fork_diagnostic = self.fork_revert_diagnostic.as_ref()?;
1063+
let call_target = fork_diagnostic.current_call_target?;
1064+
1065+
// Only diagnose if the call target is different from the test contract
1066+
// We don't want to diagnose reverts within the test contract itself
1067+
if let TxKind::Call(test_contract) = ecx.tx.kind
1068+
&& call_target != test_contract
1069+
{
1070+
return Some(call_target);
1071+
}
1072+
1073+
None
1074+
}
10391075
}
10401076

10411077
impl Inspector<EthEvmContext<&mut dyn DatabaseExt>> for Cheatcodes {
@@ -1063,6 +1099,20 @@ impl Inspector<EthEvmContext<&mut dyn DatabaseExt>> for Cheatcodes {
10631099

10641100
#[inline]
10651101
fn step(&mut self, interpreter: &mut Interpreter, ecx: Ecx) {
1102+
// Check for REVERT opcode to handle fork diagnostics before RevertDiagnostic
1103+
if let Some(call_target) = self.should_diagnose_fork_revert(interpreter, ecx) {
1104+
let journaled_state = ecx.journaled_state.clone();
1105+
if let Some(diagnostic) =
1106+
ecx.journaled_state.db().diagnose_revert(call_target, &journaled_state)
1107+
{
1108+
// Store the diagnostic
1109+
if let Some(ref mut fork_diagnostic) = self.fork_revert_diagnostic {
1110+
fork_diagnostic.diagnostic = Some(diagnostic);
1111+
}
1112+
return;
1113+
}
1114+
}
1115+
10661116
self.pc = interpreter.bytecode.pc();
10671117

10681118
// `pauseGasMetering`: pause / resume interpreter gas.
@@ -1140,6 +1190,16 @@ impl Inspector<EthEvmContext<&mut dyn DatabaseExt>> for Cheatcodes {
11401190
}
11411191

11421192
fn call_end(&mut self, ecx: Ecx, call: &CallInputs, outcome: &mut CallOutcome) {
1193+
// Apply fork diagnostic if we have one
1194+
if let Some(mut fork_diagnostic) = self.fork_revert_diagnostic.take() {
1195+
if outcome.result.is_revert() {
1196+
if let Some(diagnostic) = fork_diagnostic.diagnostic.take() {
1197+
outcome.result.output = Error::encode(diagnostic.to_error_msg(&self.labels));
1198+
return;
1199+
}
1200+
}
1201+
}
1202+
11431203
let cheatcode_call = call.target_address == CHEATCODE_ADDRESS
11441204
|| call.target_address == HARDHAT_CONSOLE_ADDRESS;
11451205

@@ -1411,33 +1471,7 @@ impl Inspector<EthEvmContext<&mut dyn DatabaseExt>> for Cheatcodes {
14111471
self.expected_emits.clear()
14121472
}
14131473

1414-
// this will ensure we don't have false positives when trying to diagnose reverts in fork
1415-
// mode
1416-
let diag = self.fork_revert_diagnostic.take();
1417-
1418-
// if there's a revert and a previous call was diagnosed as fork related revert then we can
1419-
// return a better error here
1420-
if outcome.result.is_revert()
1421-
&& let Some(err) = diag
1422-
{
1423-
outcome.result.output = Error::encode(err.to_error_msg(&self.labels));
1424-
return;
1425-
}
1426-
1427-
// try to diagnose reverts in multi-fork mode where a call is made to an address that does
1428-
// not exist
1429-
if let TxKind::Call(test_contract) = ecx.tx.kind {
1430-
// if a call to a different contract than the original test contract returned with
1431-
// `Stop` we check if the contract actually exists on the active fork
1432-
if ecx.journaled_state.db().is_forked_mode()
1433-
&& outcome.result.result == InstructionResult::Stop
1434-
&& call.target_address != test_contract
1435-
{
1436-
let journaled_state = ecx.journaled_state.clone();
1437-
self.fork_revert_diagnostic =
1438-
ecx.journaled_state.db().diagnose_revert(call.target_address, &journaled_state);
1439-
}
1440-
}
1474+
// Fork diagnostics are now handled in the step method to run before RevertDiagnostic
14411475

14421476
// If the depth is 0, then this is the root call terminating
14431477
if ecx.journaled_state.depth() == 0 {

crates/evm/core/src/backend/diagnostic.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@ pub enum RevertDiagnostic {
1818
},
1919
}
2020

21+
/// Contains fork-specific revert diagnostic information
22+
#[derive(Clone, Debug, Default)]
23+
pub struct ForkRevertDiagnostic {
24+
/// The target address of the current call
25+
pub current_call_target: Option<Address>,
26+
/// The diagnostic information if available
27+
pub diagnostic: Option<RevertDiagnostic>,
28+
}
29+
2130
impl RevertDiagnostic {
2231
/// Converts the diagnostic to a readable error message
2332
pub fn to_error_msg(&self, labels: &AddressHashMap<String>) -> String {

crates/evm/core/src/backend/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ use std::{
3939
};
4040

4141
mod diagnostic;
42-
pub use diagnostic::RevertDiagnostic;
42+
pub use diagnostic::{ForkRevertDiagnostic, RevertDiagnostic};
4343

4444
mod error;
4545
pub use error::{BackendError, BackendResult, DatabaseError, DatabaseResult};

0 commit comments

Comments
 (0)