Skip to content

Commit e0c5f9e

Browse files
committed
improve docs + make diagnostics more restrictive
1 parent 959c713 commit e0c5f9e

File tree

2 files changed

+72
-39
lines changed

2 files changed

+72
-39
lines changed
Lines changed: 69 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
use alloy_primitives::{Address, U256};
1+
use alloy_primitives::{Address, Bytes, U256};
22
use foundry_evm_core::{
33
backend::DatabaseExt,
44
constants::{CHEATCODE_ADDRESS, HARDHAT_CONSOLE_ADDRESS},
55
decode::DetailedRevertReason,
66
};
77
use revm::{
88
interpreter::{
9-
opcode::EXTCODESIZE, CallInputs, CallOutcome, CallScheme, InstructionResult, Interpreter,
9+
opcode::{EXTCODESIZE, REVERT},
10+
CallInputs, CallOutcome, CallScheme, InstructionResult, Interpreter,
1011
},
1112
precompile::{PrecompileSpecId, Precompiles},
1213
primitives::SpecId,
@@ -15,8 +16,27 @@ use revm::{
1516

1617
const IGNORE: [Address; 2] = [HARDHAT_CONSOLE_ADDRESS, CHEATCODE_ADDRESS];
1718

19+
/// Checks if the call scheme corresponds to any sort of delegate call
20+
pub fn is_delegatecall(scheme: CallScheme) -> bool {
21+
matches!(scheme, CallScheme::DelegateCall | CallScheme::ExtDelegateCall | CallScheme::CallCode)
22+
}
23+
1824
/// An inspector that tracks call context to enhances revert diagnostics.
1925
/// Useful for understanding reverts that are not linked to custom errors or revert strings.
26+
///
27+
/// Supported diagnostics:
28+
/// 1. **Non-void call to non-contract address:** the soldity compiler adds some validation to the
29+
/// return data of the call, so despite the call succeeds, as doesn't return data, the
30+
/// validation causes a revert.
31+
///
32+
/// Identified when: a call to an address with no code and non-empty calldata is made, followed
33+
/// by an empty revert at the same depth
34+
///
35+
/// 2. **Void call to non-contract address:** in this case the solidity compiler adds some checks
36+
/// before doing the call, so it never takes place.
37+
///
38+
/// Identified when: extcodesize for the target address returns 0 + empty revert at the same
39+
/// depth
2040
#[derive(Clone, Debug, Default)]
2141
pub struct RevertDiagnostic {
2242
/// Tracks calls with calldata that target an address without executable code.
@@ -30,51 +50,48 @@ pub struct RevertDiagnostic {
3050
}
3151

3252
impl RevertDiagnostic {
33-
fn is_delegatecall(&self, scheme: CallScheme) -> bool {
34-
matches!(
35-
scheme,
36-
CallScheme::DelegateCall | CallScheme::ExtDelegateCall | CallScheme::CallCode
37-
)
38-
}
39-
53+
/// Checks if the `target` address is a precompile for the given `spec_id`.
4054
fn is_precompile(&self, spec_id: SpecId, target: Address) -> bool {
4155
let precompiles = Precompiles::new(PrecompileSpecId::from_spec_id(spec_id));
4256
precompiles.contains(&target)
4357
}
4458

59+
/// Returns the effective target address whose code would be executed.
60+
/// For delegate calls, this is the `bytecode_address`. Otherwise, it's the `target_address`.
4561
pub fn no_code_target_address(&self, inputs: &mut CallInputs) -> Address {
46-
if self.is_delegatecall(inputs.scheme) {
62+
if is_delegatecall(inputs.scheme) {
4763
inputs.bytecode_address
4864
} else {
4965
inputs.target_address
5066
}
5167
}
5268

69+
/// Derives the revert reason based on the cached information.
5370
pub fn reason(&self) -> Option<DetailedRevertReason> {
5471
if self.reverted {
5572
if let Some((addr, scheme, _)) = self.non_contract_call {
56-
let reason = if self.is_delegatecall(scheme) {
73+
let reason = if is_delegatecall(scheme) {
5774
DetailedRevertReason::DelegateCallToNonContract(addr)
5875
} else {
5976
DetailedRevertReason::CallToNonContract(addr)
6077
};
6178

6279
return Some(reason);
6380
}
64-
}
6581

66-
if let Some((addr, _)) = self.non_contract_size_check {
67-
// call never took place, so schema is unknown --> output most generic msg
68-
return Some(DetailedRevertReason::CallToNonContract(addr));
82+
if let Some((addr, _)) = self.non_contract_size_check {
83+
// unknown schema as the call never took place --> output most generic reason
84+
return Some(DetailedRevertReason::CallToNonContract(addr));
85+
}
6986
}
7087

7188
None
7289
}
7390
}
7491

7592
impl<DB: Database + DatabaseExt> Inspector<DB> for RevertDiagnostic {
76-
/// Tracks the first call, with non-zero calldata, that targeted a non-contract address.
77-
/// Excludes precompiles and test addresses.
93+
/// Tracks the first call with non-zero calldata that targets a non-contract address. Excludes
94+
/// precompiles and test addresses.
7895
fn call(&mut self, ctx: &mut EvmContext<DB>, inputs: &mut CallInputs) -> Option<CallOutcome> {
7996
let target = self.no_code_target_address(inputs);
8097

@@ -90,13 +107,47 @@ impl<DB: Database + DatabaseExt> Inspector<DB> for RevertDiagnostic {
90107
None
91108
}
92109

93-
/// Tracks `EXTCODESIZE` opcodes. Clears the cache when the call stack depth changes.
110+
/// If a `non_contract_call` was previously recorded, will check if the call reverted without
111+
/// data at the same depth. If so, flags `reverted` as `true`.
112+
fn call_end(
113+
&mut self,
114+
ctx: &mut EvmContext<DB>,
115+
_inputs: &CallInputs,
116+
outcome: CallOutcome,
117+
) -> CallOutcome {
118+
if let Some((_, _, depth)) = self.non_contract_call {
119+
if outcome.result.result == InstructionResult::Revert &&
120+
outcome.result.output == Bytes::new() &&
121+
ctx.journaled_state.depth() == depth - 1
122+
{
123+
self.reverted = true
124+
};
125+
}
126+
127+
outcome
128+
}
129+
130+
/// When the current opcode is `EXTCODESIZE`:
131+
/// - Tracks addresses being checked and the current depth (if not ignored or a precompile)
132+
/// on `non_contract_size_check`.
133+
///
134+
/// When `non_contract_size_check` is `Some`:
135+
/// - If the call stack depth changes clears the cached data.
136+
/// - If the current opcode is `REVERT` and its size is zero, sets `reverted` to `true`.
94137
fn step(&mut self, interp: &mut Interpreter, ctx: &mut EvmContext<DB>) {
95138
if let Some((_, depth)) = self.non_contract_size_check {
96139
if depth != ctx.journaled_state.depth() {
97140
self.non_contract_size_check = None;
98141
}
99142

143+
if REVERT == interp.current_opcode() {
144+
if let Ok(size) = interp.stack().peek(1) {
145+
if size == U256::ZERO {
146+
self.reverted = true;
147+
}
148+
}
149+
}
150+
100151
return;
101152
}
102153

@@ -125,23 +176,4 @@ impl<DB: Database + DatabaseExt> Inspector<DB> for RevertDiagnostic {
125176
self.is_extcodesize_step = false;
126177
}
127178
}
128-
129-
/// Records whether the call reverted or not. Only if the revert call depth matches the
130-
/// inspector cache.
131-
fn call_end(
132-
&mut self,
133-
ctx: &mut EvmContext<DB>,
134-
_inputs: &CallInputs,
135-
outcome: CallOutcome,
136-
) -> CallOutcome {
137-
if let Some((_, _, depth)) = self.non_contract_call {
138-
if outcome.result.result == InstructionResult::Revert &&
139-
ctx.journaled_state.depth() == depth - 1
140-
{
141-
self.reverted = true
142-
};
143-
}
144-
145-
outcome
146-
}
147179
}

crates/forge/tests/cli/test_cmd.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3662,6 +3662,7 @@ interface ICounter {
36623662
36633663
contract NonContractCallRevertTest is Test {
36643664
Counter public counter;
3665+
address constant ADDRESS = 0xdEADBEeF00000000000000000000000000000000;
36653666
36663667
function setUp() public {
36673668
counter = new Counter();
@@ -3675,12 +3676,12 @@ contract NonContractCallRevertTest is Test {
36753676
36763677
function test_non_contract_call_failure() public {
36773678
console.log("test non contract call failure");
3678-
ICounter(address(0xdEADBEeF00000000000000000000000000000000)).number();
3679+
ICounter(ADDRESS).number();
36793680
}
36803681
36813682
function test_non_contract_void_call_failure() public {
36823683
console.log("test non contract (void) call failure");
3683-
ICounter(address(0xdEADBEeF00000000000000000000000000000000)).increment();
3684+
ICounter(ADDRESS).increment();
36843685
}
36853686
}
36863687
"#,

0 commit comments

Comments
 (0)