1
- use alloy_primitives:: { Address , U256 } ;
1
+ use alloy_primitives:: { Address , Bytes , U256 } ;
2
2
use foundry_evm_core:: {
3
3
backend:: DatabaseExt ,
4
4
constants:: { CHEATCODE_ADDRESS , HARDHAT_CONSOLE_ADDRESS } ,
5
5
decode:: DetailedRevertReason ,
6
6
} ;
7
7
use revm:: {
8
8
interpreter:: {
9
- opcode:: EXTCODESIZE , CallInputs , CallOutcome , CallScheme , InstructionResult , Interpreter ,
9
+ opcode:: { EXTCODESIZE , REVERT } ,
10
+ CallInputs , CallOutcome , CallScheme , InstructionResult , Interpreter ,
10
11
} ,
11
12
precompile:: { PrecompileSpecId , Precompiles } ,
12
13
primitives:: SpecId ,
@@ -15,8 +16,27 @@ use revm::{
15
16
16
17
const IGNORE : [ Address ; 2 ] = [ HARDHAT_CONSOLE_ADDRESS , CHEATCODE_ADDRESS ] ;
17
18
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
+
18
24
/// An inspector that tracks call context to enhances revert diagnostics.
19
25
/// 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
20
40
#[ derive( Clone , Debug , Default ) ]
21
41
pub struct RevertDiagnostic {
22
42
/// Tracks calls with calldata that target an address without executable code.
@@ -30,51 +50,48 @@ pub struct RevertDiagnostic {
30
50
}
31
51
32
52
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`.
40
54
fn is_precompile ( & self , spec_id : SpecId , target : Address ) -> bool {
41
55
let precompiles = Precompiles :: new ( PrecompileSpecId :: from_spec_id ( spec_id) ) ;
42
56
precompiles. contains ( & target)
43
57
}
44
58
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`.
45
61
pub fn no_code_target_address ( & self , inputs : & mut CallInputs ) -> Address {
46
- if self . is_delegatecall ( inputs. scheme ) {
62
+ if is_delegatecall ( inputs. scheme ) {
47
63
inputs. bytecode_address
48
64
} else {
49
65
inputs. target_address
50
66
}
51
67
}
52
68
69
+ /// Derives the revert reason based on the cached information.
53
70
pub fn reason ( & self ) -> Option < DetailedRevertReason > {
54
71
if self . reverted {
55
72
if let Some ( ( addr, scheme, _) ) = self . non_contract_call {
56
- let reason = if self . is_delegatecall ( scheme) {
73
+ let reason = if is_delegatecall ( scheme) {
57
74
DetailedRevertReason :: DelegateCallToNonContract ( addr)
58
75
} else {
59
76
DetailedRevertReason :: CallToNonContract ( addr)
60
77
} ;
61
78
62
79
return Some ( reason) ;
63
80
}
64
- }
65
81
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
+ }
69
86
}
70
87
71
88
None
72
89
}
73
90
}
74
91
75
92
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.
78
95
fn call ( & mut self , ctx : & mut EvmContext < DB > , inputs : & mut CallInputs ) -> Option < CallOutcome > {
79
96
let target = self . no_code_target_address ( inputs) ;
80
97
@@ -90,13 +107,47 @@ impl<DB: Database + DatabaseExt> Inspector<DB> for RevertDiagnostic {
90
107
None
91
108
}
92
109
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`.
94
137
fn step ( & mut self , interp : & mut Interpreter , ctx : & mut EvmContext < DB > ) {
95
138
if let Some ( ( _, depth) ) = self . non_contract_size_check {
96
139
if depth != ctx. journaled_state . depth ( ) {
97
140
self . non_contract_size_check = None ;
98
141
}
99
142
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
+
100
151
return ;
101
152
}
102
153
@@ -125,23 +176,4 @@ impl<DB: Database + DatabaseExt> Inspector<DB> for RevertDiagnostic {
125
176
self . is_extcodesize_step = false ;
126
177
}
127
178
}
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
- }
147
179
}
0 commit comments