@nomicfoundation/edr 0.5.2 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +7 -0
- package/index.d.ts +277 -1
- package/index.js +23 -1
- package/package.json +44 -35
- package/src/logger.rs +1 -0
- package/src/provider.rs +0 -6
- package/src/trace/compiler.rs +27 -0
- package/src/trace/debug.rs +318 -0
- package/src/trace/error_inferrer.rs +2219 -0
- package/src/trace/exit.rs +86 -0
- package/src/trace/library_utils.rs +6 -0
- package/src/trace/mapped_inlined_internal_functions_heuristics.rs +180 -0
- package/src/trace/message_trace.rs +179 -0
- package/src/trace/model.rs +59 -0
- package/src/trace/return_data.rs +84 -0
- package/src/trace/solidity_stack_trace.rs +694 -0
- package/src/trace/solidity_tracer.rs +315 -0
- package/src/trace/vm_trace_decoder.rs +234 -0
- package/src/trace/vm_tracer.rs +71 -0
- package/src/trace.rs +23 -1
|
@@ -0,0 +1,2219 @@
|
|
|
1
|
+
use std::{borrow::Cow, collections::HashSet};
|
|
2
|
+
|
|
3
|
+
use alloy_dyn_abi::{DynSolValue, JsonAbiExt};
|
|
4
|
+
use edr_evm::{hex, interpreter::OpCode};
|
|
5
|
+
use edr_solidity::build_model::{
|
|
6
|
+
Bytecode, ContractFunction, ContractFunctionType, ContractKind, Instruction, JumpType,
|
|
7
|
+
SourceLocation,
|
|
8
|
+
};
|
|
9
|
+
use napi::{
|
|
10
|
+
bindgen_prelude::{BigInt, Either24, Either3, Either4},
|
|
11
|
+
Either,
|
|
12
|
+
};
|
|
13
|
+
use semver::{Version, VersionReq};
|
|
14
|
+
|
|
15
|
+
use super::{
|
|
16
|
+
exit::ExitCode,
|
|
17
|
+
message_trace::{CallMessageTrace, CreateMessageTrace, EvmStep, PrecompileMessageTrace},
|
|
18
|
+
model::ContractFunctionType as ContractFunctionTypeNapi,
|
|
19
|
+
return_data::ReturnData,
|
|
20
|
+
solidity_stack_trace::{
|
|
21
|
+
CallFailedErrorStackTraceEntry, CallstackEntryStackTraceEntry, CustomErrorStackTraceEntry,
|
|
22
|
+
FallbackNotPayableErrorStackTraceEntry, InternalFunctionCallStackEntry,
|
|
23
|
+
InvalidParamsErrorStackTraceEntry, NonContractAccountCalledErrorStackTraceEntry,
|
|
24
|
+
PanicErrorStackTraceEntry, SolidityStackTrace, SolidityStackTraceEntry,
|
|
25
|
+
SolidityStackTraceEntryExt, SourceReference, UnmappedSolc063RevertErrorStackTraceEntry,
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
use crate::trace::solidity_stack_trace::{
|
|
29
|
+
ContractCallRunOutOfGasError, ContractTooLargeErrorStackTraceEntry,
|
|
30
|
+
DirectLibraryCallErrorStackTraceEntry, FallbackNotPayableAndNoReceiveErrorStackTraceEntry,
|
|
31
|
+
FunctionNotPayableErrorStackTraceEntry, MissingFallbackOrReceiveErrorStackTraceEntry,
|
|
32
|
+
OtherExecutionErrorStackTraceEntry, ReturndataSizeErrorStackTraceEntry,
|
|
33
|
+
RevertErrorStackTraceEntry, StackTraceEntryTypeConst,
|
|
34
|
+
UnrecognizedFunctionWithoutFallbackErrorStackTraceEntry, CONSTRUCTOR_FUNCTION_NAME,
|
|
35
|
+
FALLBACK_FUNCTION_NAME, RECEIVE_FUNCTION_NAME,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/// Specifies whether a heuristic was applied and modified the stack trace.
|
|
39
|
+
///
|
|
40
|
+
/// Think of it as happy [`Result`] - the [`Heuristic::Hit`] should be
|
|
41
|
+
/// propagated to the caller.
|
|
42
|
+
#[must_use]
|
|
43
|
+
pub enum Heuristic {
|
|
44
|
+
/// The heuristic was applied and modified the stack trace.
|
|
45
|
+
Hit(SolidityStackTrace),
|
|
46
|
+
/// The heuristic did not apply; the stack trace is unchanged.
|
|
47
|
+
Miss(SolidityStackTrace),
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const FIRST_SOLC_VERSION_CREATE_PARAMS_VALIDATION: Version = Version::new(0, 5, 9);
|
|
51
|
+
const FIRST_SOLC_VERSION_RECEIVE_FUNCTION: Version = Version::new(0, 6, 0);
|
|
52
|
+
const FIRST_SOLC_VERSION_WITH_UNMAPPED_REVERTS: &str = "0.6.3";
|
|
53
|
+
|
|
54
|
+
/// Port of `SubmessageData` from Hardhat to Rust
|
|
55
|
+
// However, it borrows the traces (instead of copying them) because the traces
|
|
56
|
+
// do not implement `Clone` due to inner references
|
|
57
|
+
pub struct SubmessageDataRef<'a> {
|
|
58
|
+
pub message_trace:
|
|
59
|
+
Either3<&'a PrecompileMessageTrace, &'a CallMessageTrace, &'a CreateMessageTrace>,
|
|
60
|
+
pub stacktrace: SolidityStackTrace,
|
|
61
|
+
pub step_index: u32,
|
|
62
|
+
}
|
|
63
|
+
#[derive(Default)]
|
|
64
|
+
pub struct ErrorInferrer;
|
|
65
|
+
|
|
66
|
+
impl ErrorInferrer {
|
|
67
|
+
pub fn infer_before_tracing_call_message(
|
|
68
|
+
trace: &CallMessageTrace,
|
|
69
|
+
) -> napi::Result<Option<SolidityStackTrace>> {
|
|
70
|
+
if Self::is_direct_library_call(trace)? {
|
|
71
|
+
return Ok(Some(Self::get_direct_library_call_error_stack_trace(
|
|
72
|
+
trace,
|
|
73
|
+
)?));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let bytecode = trace
|
|
77
|
+
.bytecode
|
|
78
|
+
.as_ref()
|
|
79
|
+
.expect("The TS code type-checks this to always have bytecode");
|
|
80
|
+
let contract = bytecode.contract.borrow();
|
|
81
|
+
|
|
82
|
+
let called_function = contract
|
|
83
|
+
.get_function_from_selector(trace.calldata.get(..4).unwrap_or(&trace.calldata[..]));
|
|
84
|
+
|
|
85
|
+
if let Some(called_function) = called_function {
|
|
86
|
+
if Self::is_function_not_payable_error(trace, called_function)? {
|
|
87
|
+
return Ok(Some(vec![FunctionNotPayableErrorStackTraceEntry {
|
|
88
|
+
type_: StackTraceEntryTypeConst,
|
|
89
|
+
source_reference: Self::get_function_start_source_reference(
|
|
90
|
+
Either::A(trace),
|
|
91
|
+
called_function,
|
|
92
|
+
)?,
|
|
93
|
+
value: trace.value.clone(),
|
|
94
|
+
}
|
|
95
|
+
.into()]));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let called_function = called_function.map(AsRef::as_ref);
|
|
100
|
+
|
|
101
|
+
if Self::is_missing_function_and_fallback_error(trace, called_function)? {
|
|
102
|
+
let source_reference =
|
|
103
|
+
Self::get_contract_start_without_function_source_reference(Either::A(trace))?;
|
|
104
|
+
|
|
105
|
+
if Self::empty_calldata_and_no_receive(trace)? {
|
|
106
|
+
return Ok(Some(vec![MissingFallbackOrReceiveErrorStackTraceEntry {
|
|
107
|
+
type_: StackTraceEntryTypeConst,
|
|
108
|
+
source_reference,
|
|
109
|
+
}
|
|
110
|
+
.into()]));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return Ok(Some(vec![
|
|
114
|
+
UnrecognizedFunctionWithoutFallbackErrorStackTraceEntry {
|
|
115
|
+
type_: StackTraceEntryTypeConst,
|
|
116
|
+
source_reference,
|
|
117
|
+
}
|
|
118
|
+
.into(),
|
|
119
|
+
]));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if Self::is_fallback_not_payable_error(trace, called_function)? {
|
|
123
|
+
let source_reference = Self::get_fallback_start_source_reference(trace)?;
|
|
124
|
+
|
|
125
|
+
if Self::empty_calldata_and_no_receive(trace)? {
|
|
126
|
+
return Ok(Some(vec![
|
|
127
|
+
FallbackNotPayableAndNoReceiveErrorStackTraceEntry {
|
|
128
|
+
type_: StackTraceEntryTypeConst,
|
|
129
|
+
source_reference,
|
|
130
|
+
value: trace.value.clone(),
|
|
131
|
+
}
|
|
132
|
+
.into(),
|
|
133
|
+
]));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return Ok(Some(vec![FallbackNotPayableErrorStackTraceEntry {
|
|
137
|
+
type_: StackTraceEntryTypeConst,
|
|
138
|
+
source_reference,
|
|
139
|
+
value: trace.value.clone(),
|
|
140
|
+
}
|
|
141
|
+
.into()]));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
Ok(None)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
pub fn infer_before_tracing_create_message(
|
|
148
|
+
trace: &CreateMessageTrace,
|
|
149
|
+
) -> napi::Result<Option<SolidityStackTrace>> {
|
|
150
|
+
if Self::is_constructor_not_payable_error(trace)? {
|
|
151
|
+
return Ok(Some(vec![FunctionNotPayableErrorStackTraceEntry {
|
|
152
|
+
type_: StackTraceEntryTypeConst,
|
|
153
|
+
source_reference: Self::get_constructor_start_source_reference(trace)?,
|
|
154
|
+
value: trace.value.clone(),
|
|
155
|
+
}
|
|
156
|
+
.into()]));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if Self::is_constructor_invalid_arguments_error(trace)? {
|
|
160
|
+
return Ok(Some(vec![InvalidParamsErrorStackTraceEntry {
|
|
161
|
+
type_: StackTraceEntryTypeConst,
|
|
162
|
+
source_reference: Self::get_constructor_start_source_reference(trace)?,
|
|
163
|
+
}
|
|
164
|
+
.into()]));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
Ok(None)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
pub fn infer_after_tracing(
|
|
171
|
+
trace: Either<&CallMessageTrace, &CreateMessageTrace>,
|
|
172
|
+
stacktrace: SolidityStackTrace,
|
|
173
|
+
function_jumpdests: &[&Instruction],
|
|
174
|
+
jumped_into_function: bool,
|
|
175
|
+
last_submessage_data: Option<SubmessageDataRef<'_>>,
|
|
176
|
+
) -> napi::Result<SolidityStackTrace> {
|
|
177
|
+
/// Convenience macro to early return the result if a heuristic hits.
|
|
178
|
+
macro_rules! return_if_hit {
|
|
179
|
+
($heuristic: expr) => {
|
|
180
|
+
match $heuristic {
|
|
181
|
+
Heuristic::Hit(stacktrace) => return Ok(stacktrace),
|
|
182
|
+
Heuristic::Miss(stacktrace) => stacktrace,
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let result = Self::check_last_submessage(trace, stacktrace, last_submessage_data)?;
|
|
188
|
+
let stacktrace = return_if_hit!(result);
|
|
189
|
+
|
|
190
|
+
let result = Self::check_failed_last_call(trace, stacktrace)?;
|
|
191
|
+
let stacktrace = return_if_hit!(result);
|
|
192
|
+
|
|
193
|
+
let result = Self::check_last_instruction(
|
|
194
|
+
trace,
|
|
195
|
+
stacktrace,
|
|
196
|
+
function_jumpdests,
|
|
197
|
+
jumped_into_function,
|
|
198
|
+
)?;
|
|
199
|
+
let stacktrace = return_if_hit!(result);
|
|
200
|
+
|
|
201
|
+
let result = Self::check_non_contract_called(trace, stacktrace)?;
|
|
202
|
+
let stacktrace = return_if_hit!(result);
|
|
203
|
+
|
|
204
|
+
let result = Self::check_solidity_0_6_3_unmapped_revert(trace, stacktrace)?;
|
|
205
|
+
let stacktrace = return_if_hit!(result);
|
|
206
|
+
|
|
207
|
+
if let Some(result) = Self::check_contract_too_large(trace)? {
|
|
208
|
+
return Ok(result);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
let stacktrace = Self::other_execution_error_stacktrace(trace, stacktrace)?;
|
|
212
|
+
Ok(stacktrace)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
pub fn filter_redundant_frames(
|
|
216
|
+
stacktrace: SolidityStackTrace,
|
|
217
|
+
) -> napi::Result<SolidityStackTrace> {
|
|
218
|
+
// To work around the borrow checker, we'll collect the indices of the frames we
|
|
219
|
+
// want to keep. We can't clone the frames, because some of them contain
|
|
220
|
+
// non-Clone `ClassInstance`s`
|
|
221
|
+
let retained_indices: HashSet<_> = stacktrace
|
|
222
|
+
.iter()
|
|
223
|
+
.enumerate()
|
|
224
|
+
.filter(|(idx, frame)| {
|
|
225
|
+
let next_frame = stacktrace.get(idx + 1);
|
|
226
|
+
let next_next_frame = stacktrace.get(idx + 2);
|
|
227
|
+
|
|
228
|
+
let Some(next_frame) = next_frame else {
|
|
229
|
+
return true;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// we can only filter frames if we know their sourceReference
|
|
233
|
+
// and the one from the next frame
|
|
234
|
+
let (Some(frame_source), Some(next_frame_source)) =
|
|
235
|
+
(frame.source_reference(), next_frame.source_reference())
|
|
236
|
+
else {
|
|
237
|
+
return true;
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
// look TWO frames ahead to determine if this is a specific occurrence of
|
|
241
|
+
// a redundant CALLSTACK_ENTRY frame observed when using Solidity 0.8.5:
|
|
242
|
+
match (&frame, next_next_frame) {
|
|
243
|
+
(
|
|
244
|
+
Either24::A(CallstackEntryStackTraceEntry {
|
|
245
|
+
source_reference, ..
|
|
246
|
+
}),
|
|
247
|
+
Some(Either24::N(ReturndataSizeErrorStackTraceEntry {
|
|
248
|
+
source_reference: next_next_source_reference,
|
|
249
|
+
..
|
|
250
|
+
})),
|
|
251
|
+
) if source_reference.range == next_next_source_reference.range
|
|
252
|
+
&& source_reference.line == next_next_source_reference.line =>
|
|
253
|
+
{
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
_ => {}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if frame_source.function.as_deref() == Some("constructor")
|
|
260
|
+
&& next_frame_source.function.as_deref() != Some("constructor")
|
|
261
|
+
{
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// this is probably a recursive call
|
|
266
|
+
if *idx > 0
|
|
267
|
+
&& frame.type_() == next_frame.type_()
|
|
268
|
+
&& frame_source.range == next_frame_source.range
|
|
269
|
+
&& frame_source.line == next_frame_source.line
|
|
270
|
+
{
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if frame_source.range[0] <= next_frame_source.range[0]
|
|
275
|
+
&& frame_source.range[1] >= next_frame_source.range[1]
|
|
276
|
+
{
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
true
|
|
281
|
+
})
|
|
282
|
+
.map(|(idx, _)| idx)
|
|
283
|
+
.collect();
|
|
284
|
+
|
|
285
|
+
Ok(stacktrace
|
|
286
|
+
.into_iter()
|
|
287
|
+
.enumerate()
|
|
288
|
+
.filter(|(idx, _)| retained_indices.contains(idx))
|
|
289
|
+
.map(|(_, frame)| frame)
|
|
290
|
+
.collect())
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Heuristics
|
|
294
|
+
|
|
295
|
+
fn check_contract_too_large(
|
|
296
|
+
trace: Either<&CallMessageTrace, &CreateMessageTrace>,
|
|
297
|
+
) -> napi::Result<Option<SolidityStackTrace>> {
|
|
298
|
+
Ok(match trace {
|
|
299
|
+
Either::B(create @ CreateMessageTrace { .. })
|
|
300
|
+
if create.exit.kind() == ExitCode::CODESIZE_EXCEEDS_MAXIMUM =>
|
|
301
|
+
{
|
|
302
|
+
Some(vec![ContractTooLargeErrorStackTraceEntry {
|
|
303
|
+
type_: StackTraceEntryTypeConst,
|
|
304
|
+
source_reference: Some(Self::get_constructor_start_source_reference(create)?),
|
|
305
|
+
}
|
|
306
|
+
.into()])
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
_ => None,
|
|
310
|
+
})
|
|
311
|
+
}
|
|
312
|
+
/// Check if the last call/create that was done failed.
|
|
313
|
+
fn check_failed_last_call(
|
|
314
|
+
trace: Either<&CallMessageTrace, &CreateMessageTrace>,
|
|
315
|
+
stacktrace: SolidityStackTrace,
|
|
316
|
+
) -> napi::Result<Heuristic> {
|
|
317
|
+
let (bytecode, steps) = match &trace {
|
|
318
|
+
Either::A(call) => (&call.bytecode, &call.steps),
|
|
319
|
+
Either::B(create) => (&create.bytecode, &create.steps),
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
let bytecode = bytecode.as_ref().expect("JS code asserts");
|
|
323
|
+
|
|
324
|
+
if steps.is_empty() {
|
|
325
|
+
return Ok(Heuristic::Miss(stacktrace));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
for step_index in (0..steps.len() - 1).rev() {
|
|
329
|
+
let (step, next_step) = match &steps[step_index..][..2] {
|
|
330
|
+
&[Either4::A(ref step), ref next_step] => (step, next_step),
|
|
331
|
+
_ => return Ok(Heuristic::Miss(stacktrace)),
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
let inst = bytecode.get_instruction(step.pc)?;
|
|
335
|
+
|
|
336
|
+
if let (OpCode::CALL | OpCode::CREATE, Either4::A(EvmStep { .. })) =
|
|
337
|
+
(inst.opcode, next_step)
|
|
338
|
+
{
|
|
339
|
+
if Self::is_call_failed_error(trace, step_index as u32, inst)? {
|
|
340
|
+
let mut inferred_stacktrace = stacktrace.clone();
|
|
341
|
+
inferred_stacktrace.push(
|
|
342
|
+
Self::call_instruction_to_call_failed_to_execute_stack_trace_entry(
|
|
343
|
+
bytecode, inst,
|
|
344
|
+
)?
|
|
345
|
+
.into(),
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
return Ok(Heuristic::Hit(Self::fix_initial_modifier(
|
|
349
|
+
trace,
|
|
350
|
+
inferred_stacktrace,
|
|
351
|
+
)?));
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
Ok(Heuristic::Miss(stacktrace))
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/// Check if the trace reverted with a panic error.
|
|
360
|
+
fn check_panic(
|
|
361
|
+
trace: Either<&CallMessageTrace, &CreateMessageTrace>,
|
|
362
|
+
mut stacktrace: SolidityStackTrace,
|
|
363
|
+
last_instruction: &Instruction,
|
|
364
|
+
) -> napi::Result<Heuristic> {
|
|
365
|
+
let return_data = ReturnData::new(
|
|
366
|
+
match &trace {
|
|
367
|
+
Either::A(call) => &call.return_data,
|
|
368
|
+
Either::B(create) => &create.return_data,
|
|
369
|
+
}
|
|
370
|
+
.clone(),
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
if !return_data.is_panic_return_data() {
|
|
374
|
+
return Ok(Heuristic::Miss(stacktrace));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// If the last frame is an internal function, it means that the trace
|
|
378
|
+
// jumped there to return the panic. If that's the case, we remove that
|
|
379
|
+
// frame.
|
|
380
|
+
if let Some(Either24::W(InternalFunctionCallStackEntry { .. })) = stacktrace.last() {
|
|
381
|
+
stacktrace.pop();
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// if the error comes from a call to a zero-initialized function,
|
|
385
|
+
// we remove the last frame, which represents the call, to avoid
|
|
386
|
+
// having duplicated frames
|
|
387
|
+
let error_code = return_data.decode_panic()?;
|
|
388
|
+
let (panic_error_code, lossless) = error_code.get_i64();
|
|
389
|
+
if let (0x51, false) = (panic_error_code, lossless) {
|
|
390
|
+
stacktrace.pop();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
stacktrace.push(
|
|
394
|
+
Self::instruction_within_function_to_panic_stack_trace_entry(
|
|
395
|
+
trace,
|
|
396
|
+
last_instruction,
|
|
397
|
+
error_code,
|
|
398
|
+
)?
|
|
399
|
+
.into(),
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
Self::fix_initial_modifier(trace, stacktrace).map(Heuristic::Hit)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
fn check_custom_errors(
|
|
406
|
+
trace: Either<&CallMessageTrace, &CreateMessageTrace>,
|
|
407
|
+
stacktrace: SolidityStackTrace,
|
|
408
|
+
last_instruction: &Instruction,
|
|
409
|
+
) -> napi::Result<Heuristic> {
|
|
410
|
+
let (bytecode, return_data) = match &trace {
|
|
411
|
+
Either::A(call) => (&call.bytecode, &call.return_data),
|
|
412
|
+
Either::B(create) => (&create.bytecode, &create.return_data),
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
let bytecode = bytecode.as_ref().expect("JS code asserts");
|
|
416
|
+
let contract = bytecode.contract.borrow();
|
|
417
|
+
|
|
418
|
+
let return_data = ReturnData::new(return_data.clone());
|
|
419
|
+
|
|
420
|
+
if return_data.is_empty() || return_data.is_error_return_data() {
|
|
421
|
+
// if there is no return data, or if it's a Error(string),
|
|
422
|
+
// then it can't be a custom error
|
|
423
|
+
return Ok(Heuristic::Miss(stacktrace));
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
let raw_return_data = hex::encode(&*return_data.value);
|
|
427
|
+
let mut error_message = format!(
|
|
428
|
+
"reverted with an unrecognized custom error (return data: 0x{raw_return_data})",
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
for custom_error in &contract.custom_errors {
|
|
432
|
+
if return_data.matches_selector(custom_error.selector) {
|
|
433
|
+
// if the return data matches a custom error in the called contract,
|
|
434
|
+
// we format the message using the returnData and the custom error instance
|
|
435
|
+
let decoded = custom_error
|
|
436
|
+
.decode_error_data(&return_data.value)
|
|
437
|
+
.map_err(|e| {
|
|
438
|
+
napi::Error::from_reason(format!("Error decoding custom error: {e}"))
|
|
439
|
+
})?;
|
|
440
|
+
|
|
441
|
+
let params = decoded
|
|
442
|
+
.body
|
|
443
|
+
.iter()
|
|
444
|
+
.map(format_dyn_sol_value)
|
|
445
|
+
.collect::<Vec<_>>();
|
|
446
|
+
|
|
447
|
+
error_message = format!(
|
|
448
|
+
"reverted with custom error '{name}({params})'",
|
|
449
|
+
name = custom_error.name,
|
|
450
|
+
params = params.join(", ")
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
let mut stacktrace = stacktrace;
|
|
458
|
+
stacktrace.push(
|
|
459
|
+
Self::instruction_within_function_to_custom_error_stack_trace_entry(
|
|
460
|
+
trace,
|
|
461
|
+
last_instruction,
|
|
462
|
+
error_message,
|
|
463
|
+
)?
|
|
464
|
+
.into(),
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
Self::fix_initial_modifier(trace, stacktrace).map(Heuristic::Hit)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
fn check_solidity_0_6_3_unmapped_revert(
|
|
471
|
+
trace: Either<&CallMessageTrace, &CreateMessageTrace>,
|
|
472
|
+
mut stacktrace: SolidityStackTrace,
|
|
473
|
+
) -> napi::Result<Heuristic> {
|
|
474
|
+
if Self::solidity_0_6_3_maybe_unmapped_revert(trace)? {
|
|
475
|
+
let revert_frame =
|
|
476
|
+
Self::solidity_0_6_3_get_frame_for_unmapped_revert_within_function(trace)?;
|
|
477
|
+
|
|
478
|
+
if let Some(revert_frame) = revert_frame {
|
|
479
|
+
stacktrace.push(revert_frame.into());
|
|
480
|
+
|
|
481
|
+
return Ok(Heuristic::Hit(stacktrace));
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return Ok(Heuristic::Hit(stacktrace));
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
Ok(Heuristic::Miss(stacktrace))
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
fn check_non_contract_called(
|
|
491
|
+
trace: Either<&CallMessageTrace, &CreateMessageTrace>,
|
|
492
|
+
mut stacktrace: SolidityStackTrace,
|
|
493
|
+
) -> napi::Result<Heuristic> {
|
|
494
|
+
if Self::is_called_non_contract_account_error(trace)? {
|
|
495
|
+
let source_reference = Self::get_last_source_reference(trace)?;
|
|
496
|
+
|
|
497
|
+
// We are sure this is not undefined because there was at least a call
|
|
498
|
+
// instruction
|
|
499
|
+
let source_reference =
|
|
500
|
+
source_reference.expect("Expected source reference to be defined");
|
|
501
|
+
|
|
502
|
+
let non_contract_called_frame = NonContractAccountCalledErrorStackTraceEntry {
|
|
503
|
+
type_: StackTraceEntryTypeConst,
|
|
504
|
+
source_reference,
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
stacktrace.push(non_contract_called_frame.into());
|
|
508
|
+
|
|
509
|
+
Ok(Heuristic::Hit(stacktrace))
|
|
510
|
+
} else {
|
|
511
|
+
Ok(Heuristic::Miss(stacktrace))
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/// Check if the last submessage can be used to generate the stack trace.
|
|
516
|
+
fn check_last_submessage(
|
|
517
|
+
trace: Either<&CallMessageTrace, &CreateMessageTrace>,
|
|
518
|
+
stacktrace: SolidityStackTrace,
|
|
519
|
+
last_submessage_data: Option<SubmessageDataRef<'_>>,
|
|
520
|
+
) -> napi::Result<Heuristic> {
|
|
521
|
+
let (bytecode, steps) = match &trace {
|
|
522
|
+
Either::A(call) => (&call.bytecode, &call.steps),
|
|
523
|
+
Either::B(create) => (&create.bytecode, &create.steps),
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
let bytecode = bytecode.as_ref().expect("JS code asserts");
|
|
527
|
+
|
|
528
|
+
let Some(last_submessage_data) = last_submessage_data else {
|
|
529
|
+
return Ok(Heuristic::Miss(stacktrace));
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
let mut inferred_stacktrace = Cow::from(&stacktrace);
|
|
533
|
+
|
|
534
|
+
// get the instruction before the submessage and add it to the stack trace
|
|
535
|
+
let call_step = match steps.get(last_submessage_data.step_index as usize - 1) {
|
|
536
|
+
Some(Either4::A(call_step)) => call_step,
|
|
537
|
+
_ => panic!("This should not happen: MessageTrace should be preceded by a EVM step"),
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
let call_inst = bytecode.get_instruction(call_step.pc)?;
|
|
541
|
+
let call_stack_frame = instruction_to_callstack_stack_trace_entry(bytecode, call_inst)?;
|
|
542
|
+
|
|
543
|
+
let (call_stack_frame_source_reference, call_stack_frame) = match call_stack_frame {
|
|
544
|
+
Either::A(frame) => (frame.source_reference.clone(), frame.into()),
|
|
545
|
+
Either::B(frame) => (frame.source_reference.clone(), frame.into()),
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
let last_message_failed = match last_submessage_data.message_trace {
|
|
549
|
+
Either3::A(precompile) => precompile.exit.is_error(),
|
|
550
|
+
Either3::B(call) => call.exit.is_error(),
|
|
551
|
+
Either3::C(create) => create.exit.is_error(),
|
|
552
|
+
};
|
|
553
|
+
if last_message_failed {
|
|
554
|
+
// add the call/create that generated the message to the stack trace
|
|
555
|
+
let inferred_stacktrace = inferred_stacktrace.to_mut();
|
|
556
|
+
inferred_stacktrace.push(call_stack_frame);
|
|
557
|
+
|
|
558
|
+
if Self::is_subtrace_error_propagated(trace, last_submessage_data.step_index)?
|
|
559
|
+
|| Self::is_proxy_error_propagated(trace, last_submessage_data.step_index)?
|
|
560
|
+
{
|
|
561
|
+
inferred_stacktrace.extend(last_submessage_data.stacktrace);
|
|
562
|
+
|
|
563
|
+
if Self::is_contract_call_run_out_of_gas_error(
|
|
564
|
+
trace,
|
|
565
|
+
last_submessage_data.step_index,
|
|
566
|
+
)? {
|
|
567
|
+
let last_frame = match inferred_stacktrace.pop() {
|
|
568
|
+
Some(frame) => frame,
|
|
569
|
+
_ => panic!("Expected inferred stack trace to have at least one frame"),
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
inferred_stacktrace.push(
|
|
573
|
+
ContractCallRunOutOfGasError {
|
|
574
|
+
type_: StackTraceEntryTypeConst,
|
|
575
|
+
source_reference: last_frame.source_reference().cloned(),
|
|
576
|
+
}
|
|
577
|
+
.into(),
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return Self::fix_initial_modifier(trace, inferred_stacktrace.to_owned())
|
|
582
|
+
.map(Heuristic::Hit);
|
|
583
|
+
}
|
|
584
|
+
} else {
|
|
585
|
+
let is_return_data_size_error =
|
|
586
|
+
Self::fails_right_after_call(trace, last_submessage_data.step_index)?;
|
|
587
|
+
if is_return_data_size_error {
|
|
588
|
+
inferred_stacktrace.to_mut().push(
|
|
589
|
+
ReturndataSizeErrorStackTraceEntry {
|
|
590
|
+
type_: StackTraceEntryTypeConst,
|
|
591
|
+
source_reference: call_stack_frame_source_reference,
|
|
592
|
+
}
|
|
593
|
+
.into(),
|
|
594
|
+
);
|
|
595
|
+
|
|
596
|
+
return Self::fix_initial_modifier(trace, inferred_stacktrace.into_owned())
|
|
597
|
+
.map(Heuristic::Hit);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
Ok(Heuristic::Miss(stacktrace))
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/// Check if the execution stopped with a revert or an invalid opcode.
|
|
605
|
+
fn check_revert_or_invalid_opcode(
|
|
606
|
+
trace: Either<&CallMessageTrace, &CreateMessageTrace>,
|
|
607
|
+
stacktrace: SolidityStackTrace,
|
|
608
|
+
last_instruction: &Instruction,
|
|
609
|
+
function_jumpdests: &[&Instruction],
|
|
610
|
+
jumped_into_function: bool,
|
|
611
|
+
) -> napi::Result<Heuristic> {
|
|
612
|
+
match last_instruction.opcode {
|
|
613
|
+
OpCode::REVERT | OpCode::INVALID => {}
|
|
614
|
+
_ => return Ok(Heuristic::Miss(stacktrace)),
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
let (bytecode, return_data) = match &trace {
|
|
618
|
+
Either::A(call) => (&call.bytecode, &call.return_data),
|
|
619
|
+
Either::B(create) => (&create.bytecode, &create.return_data),
|
|
620
|
+
};
|
|
621
|
+
let bytecode = bytecode.as_ref().expect("JS code asserts");
|
|
622
|
+
|
|
623
|
+
let mut inferred_stacktrace = stacktrace.clone();
|
|
624
|
+
|
|
625
|
+
if let Some(location) = &last_instruction.location {
|
|
626
|
+
if jumped_into_function || matches!(trace, Either::B(CreateMessageTrace { .. })) {
|
|
627
|
+
// There should always be a function here, but that's not the case with
|
|
628
|
+
// optimizations.
|
|
629
|
+
//
|
|
630
|
+
// If this is a create trace, we already checked args and nonpayable failures
|
|
631
|
+
// before calling this function.
|
|
632
|
+
//
|
|
633
|
+
// If it's a call trace, we already jumped into a function. But optimizations
|
|
634
|
+
// can happen.
|
|
635
|
+
let failing_function = location.get_containing_function();
|
|
636
|
+
|
|
637
|
+
// If the failure is in a modifier we add an entry with the function/constructor
|
|
638
|
+
match failing_function {
|
|
639
|
+
Some(func) if func.r#type == ContractFunctionType::Modifier => {
|
|
640
|
+
let frame =
|
|
641
|
+
Self::get_entry_before_failure_in_modifier(trace, function_jumpdests)?;
|
|
642
|
+
|
|
643
|
+
inferred_stacktrace.push(match frame {
|
|
644
|
+
Either::A(frame) => frame.into(),
|
|
645
|
+
Either::B(frame) => frame.into(),
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
_ => {}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
let panic_stacktrace = Self::check_panic(trace, inferred_stacktrace, last_instruction)?;
|
|
654
|
+
let inferred_stacktrace = match panic_stacktrace {
|
|
655
|
+
hit @ Heuristic::Hit(..) => return Ok(hit),
|
|
656
|
+
Heuristic::Miss(stacktrace) => stacktrace,
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
let custom_error_stacktrace =
|
|
660
|
+
Self::check_custom_errors(trace, inferred_stacktrace, last_instruction)?;
|
|
661
|
+
let mut inferred_stacktrace = match custom_error_stacktrace {
|
|
662
|
+
hit @ Heuristic::Hit(..) => return Ok(hit),
|
|
663
|
+
Heuristic::Miss(stacktrace) => stacktrace,
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
if let Some(location) = &last_instruction.location {
|
|
667
|
+
if jumped_into_function || matches!(trace, Either::B(CreateMessageTrace { .. })) {
|
|
668
|
+
let failing_function = location.get_containing_function();
|
|
669
|
+
|
|
670
|
+
if failing_function.is_some() {
|
|
671
|
+
let frame = Self::instruction_within_function_to_revert_stack_trace_entry(
|
|
672
|
+
trace,
|
|
673
|
+
last_instruction,
|
|
674
|
+
)?;
|
|
675
|
+
|
|
676
|
+
inferred_stacktrace.push(frame.into());
|
|
677
|
+
} else {
|
|
678
|
+
let is_invalid_opcode_error = last_instruction.opcode == OpCode::INVALID;
|
|
679
|
+
|
|
680
|
+
match &trace {
|
|
681
|
+
Either::A(CallMessageTrace { calldata, .. }) => {
|
|
682
|
+
let contract = bytecode.contract.borrow();
|
|
683
|
+
|
|
684
|
+
// This is here because of the optimizations
|
|
685
|
+
let function_from_selector = contract.get_function_from_selector(
|
|
686
|
+
calldata.get(..4).unwrap_or(&calldata[..]),
|
|
687
|
+
);
|
|
688
|
+
|
|
689
|
+
// in general this shouldn't happen, but it does when viaIR is enabled,
|
|
690
|
+
// "optimizerSteps": "u" is used, and the called function is fallback or
|
|
691
|
+
// receive
|
|
692
|
+
let Some(function) = function_from_selector else {
|
|
693
|
+
return Ok(Heuristic::Miss(inferred_stacktrace));
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
let frame = RevertErrorStackTraceEntry {
|
|
697
|
+
type_: StackTraceEntryTypeConst,
|
|
698
|
+
source_reference: Self::get_function_start_source_reference(
|
|
699
|
+
trace, function,
|
|
700
|
+
)?,
|
|
701
|
+
return_data: return_data.clone(),
|
|
702
|
+
is_invalid_opcode_error,
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
inferred_stacktrace.push(frame.into());
|
|
706
|
+
}
|
|
707
|
+
Either::B(trace @ CreateMessageTrace { .. }) => {
|
|
708
|
+
// This is here because of the optimizations
|
|
709
|
+
let frame = RevertErrorStackTraceEntry {
|
|
710
|
+
type_: StackTraceEntryTypeConst,
|
|
711
|
+
source_reference: Self::get_constructor_start_source_reference(
|
|
712
|
+
trace,
|
|
713
|
+
)?,
|
|
714
|
+
return_data: return_data.clone(),
|
|
715
|
+
is_invalid_opcode_error,
|
|
716
|
+
};
|
|
717
|
+
|
|
718
|
+
inferred_stacktrace.push(frame.into());
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
return Self::fix_initial_modifier(trace, inferred_stacktrace).map(Heuristic::Hit);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// If the revert instruction is not mapped but there is return data,
|
|
728
|
+
// we add the frame anyway, sith the best sourceReference we can get
|
|
729
|
+
if last_instruction.location.is_none() && !return_data.is_empty() {
|
|
730
|
+
let revert_frame = RevertErrorStackTraceEntry {
|
|
731
|
+
type_: StackTraceEntryTypeConst,
|
|
732
|
+
source_reference: Self::get_contract_start_without_function_source_reference(
|
|
733
|
+
trace,
|
|
734
|
+
)?,
|
|
735
|
+
return_data: return_data.clone(),
|
|
736
|
+
is_invalid_opcode_error: last_instruction.opcode == OpCode::INVALID,
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
inferred_stacktrace.push(revert_frame.into());
|
|
740
|
+
|
|
741
|
+
return Self::fix_initial_modifier(trace, inferred_stacktrace).map(Heuristic::Hit);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
Ok(Heuristic::Miss(stacktrace))
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
fn check_last_instruction(
|
|
748
|
+
trace: Either<&CallMessageTrace, &CreateMessageTrace>,
|
|
749
|
+
stacktrace: SolidityStackTrace,
|
|
750
|
+
function_jumpdests: &[&Instruction],
|
|
751
|
+
jumped_into_function: bool,
|
|
752
|
+
) -> napi::Result<Heuristic> {
|
|
753
|
+
let (bytecode, steps) = match &trace {
|
|
754
|
+
Either::A(call) => (&call.bytecode, &call.steps),
|
|
755
|
+
Either::B(create) => (&create.bytecode, &create.steps),
|
|
756
|
+
};
|
|
757
|
+
let bytecode = bytecode.as_ref().expect("JS code asserts");
|
|
758
|
+
|
|
759
|
+
if steps.is_empty() {
|
|
760
|
+
return Ok(Heuristic::Miss(stacktrace));
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
let last_step = match steps.last() {
|
|
764
|
+
Some(Either4::A(step)) => step,
|
|
765
|
+
_ => panic!("This should not happen: MessageTrace ends with a subtrace"),
|
|
766
|
+
};
|
|
767
|
+
|
|
768
|
+
let last_instruction = bytecode.get_instruction(last_step.pc)?;
|
|
769
|
+
|
|
770
|
+
let revert_or_invalid_stacktrace = Self::check_revert_or_invalid_opcode(
|
|
771
|
+
trace,
|
|
772
|
+
stacktrace,
|
|
773
|
+
last_instruction,
|
|
774
|
+
function_jumpdests,
|
|
775
|
+
jumped_into_function,
|
|
776
|
+
)?;
|
|
777
|
+
let stacktrace = match revert_or_invalid_stacktrace {
|
|
778
|
+
hit @ Heuristic::Hit(..) => return Ok(hit),
|
|
779
|
+
Heuristic::Miss(stacktrace) => stacktrace,
|
|
780
|
+
};
|
|
781
|
+
|
|
782
|
+
let (Either::A(trace @ CallMessageTrace { ref calldata, .. }), false) =
|
|
783
|
+
(&trace, jumped_into_function)
|
|
784
|
+
else {
|
|
785
|
+
return Ok(Heuristic::Miss(stacktrace));
|
|
786
|
+
};
|
|
787
|
+
|
|
788
|
+
if Self::has_failed_inside_the_fallback_function(trace)?
|
|
789
|
+
|| Self::has_failed_inside_the_receive_function(trace)?
|
|
790
|
+
{
|
|
791
|
+
let frame = Self::instruction_within_function_to_revert_stack_trace_entry(
|
|
792
|
+
Either::A(trace),
|
|
793
|
+
last_instruction,
|
|
794
|
+
)?;
|
|
795
|
+
|
|
796
|
+
return Ok(Heuristic::Hit(vec![frame.into()]));
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Sometimes we do fail inside of a function but there's no jump into
|
|
800
|
+
if let Some(location) = &last_instruction.location {
|
|
801
|
+
let failing_function = location.get_containing_function();
|
|
802
|
+
|
|
803
|
+
if let Some(failing_function) = failing_function {
|
|
804
|
+
let frame = RevertErrorStackTraceEntry {
|
|
805
|
+
type_: StackTraceEntryTypeConst,
|
|
806
|
+
source_reference: Self::get_function_start_source_reference(
|
|
807
|
+
Either::A(trace),
|
|
808
|
+
&failing_function,
|
|
809
|
+
)?,
|
|
810
|
+
return_data: trace.return_data.clone(),
|
|
811
|
+
is_invalid_opcode_error: last_instruction.opcode == OpCode::INVALID,
|
|
812
|
+
};
|
|
813
|
+
|
|
814
|
+
return Ok(Heuristic::Hit(vec![frame.into()]));
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
let contract = bytecode.contract.borrow();
|
|
819
|
+
|
|
820
|
+
let selector = calldata.get(..4).unwrap_or(&calldata[..]);
|
|
821
|
+
let calldata = &calldata.get(4..).unwrap_or(&[]);
|
|
822
|
+
|
|
823
|
+
let called_function = contract.get_function_from_selector(selector);
|
|
824
|
+
|
|
825
|
+
if let Some(called_function) = called_function {
|
|
826
|
+
let abi = alloy_json_abi::Function::try_from(&**called_function).map_err(|e| {
|
|
827
|
+
napi::Error::from_reason(format!("Error converting to alloy ABI: {e}"))
|
|
828
|
+
})?;
|
|
829
|
+
|
|
830
|
+
let is_valid_calldata = match &called_function.param_types {
|
|
831
|
+
Some(_) => abi.abi_decode_input(calldata, true).is_ok(),
|
|
832
|
+
// if we don't know the param types, we just assume that the call is valid
|
|
833
|
+
None => true,
|
|
834
|
+
};
|
|
835
|
+
|
|
836
|
+
if !is_valid_calldata {
|
|
837
|
+
let frame = InvalidParamsErrorStackTraceEntry {
|
|
838
|
+
type_: StackTraceEntryTypeConst,
|
|
839
|
+
source_reference: Self::get_function_start_source_reference(
|
|
840
|
+
Either::A(trace),
|
|
841
|
+
called_function,
|
|
842
|
+
)?,
|
|
843
|
+
};
|
|
844
|
+
|
|
845
|
+
return Ok(Heuristic::Hit(vec![frame.into()]));
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
if Self::solidity_0_6_3_maybe_unmapped_revert(Either::A(trace))? {
|
|
850
|
+
let revert_frame =
|
|
851
|
+
Self::solidity_0_6_3_get_frame_for_unmapped_revert_before_function(trace)?;
|
|
852
|
+
|
|
853
|
+
if let Some(revert_frame) = revert_frame {
|
|
854
|
+
return Ok(Heuristic::Hit(vec![revert_frame.into()]));
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
let frame = Self::get_other_error_before_called_function_stack_trace_entry(trace)?;
|
|
859
|
+
|
|
860
|
+
Ok(Heuristic::Hit(vec![frame.into()]))
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// Helpers
|
|
864
|
+
|
|
865
|
+
fn fix_initial_modifier(
|
|
866
|
+
trace: Either<&CallMessageTrace, &CreateMessageTrace>,
|
|
867
|
+
mut stacktrace: SolidityStackTrace,
|
|
868
|
+
) -> napi::Result<SolidityStackTrace> {
|
|
869
|
+
if let Some(Either24::A(CallstackEntryStackTraceEntry {
|
|
870
|
+
function_type: ContractFunctionTypeNapi::MODIFIER,
|
|
871
|
+
..
|
|
872
|
+
})) = stacktrace.first()
|
|
873
|
+
{
|
|
874
|
+
let entry_before_initial_modifier =
|
|
875
|
+
Self::get_entry_before_initial_modifier_callstack_entry(trace)?;
|
|
876
|
+
|
|
877
|
+
stacktrace.insert(0, entry_before_initial_modifier);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
Ok(stacktrace)
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
fn get_entry_before_initial_modifier_callstack_entry(
|
|
884
|
+
trace: Either<&CallMessageTrace, &CreateMessageTrace>,
|
|
885
|
+
) -> napi::Result<SolidityStackTraceEntry> {
|
|
886
|
+
let trace = match trace {
|
|
887
|
+
Either::B(create) => {
|
|
888
|
+
return Ok(CallstackEntryStackTraceEntry {
|
|
889
|
+
type_: StackTraceEntryTypeConst,
|
|
890
|
+
source_reference: Self::get_constructor_start_source_reference(create)?,
|
|
891
|
+
function_type: ContractFunctionType::Constructor.into(),
|
|
892
|
+
}
|
|
893
|
+
.into())
|
|
894
|
+
}
|
|
895
|
+
Either::A(call) => call,
|
|
896
|
+
};
|
|
897
|
+
|
|
898
|
+
let bytecode = trace.bytecode.as_ref().expect("JS code asserts");
|
|
899
|
+
let contract = bytecode.contract.borrow();
|
|
900
|
+
|
|
901
|
+
let called_function = contract
|
|
902
|
+
.get_function_from_selector(trace.calldata.get(..4).unwrap_or(&trace.calldata[..]));
|
|
903
|
+
|
|
904
|
+
let source_reference = match called_function {
|
|
905
|
+
Some(called_function) => {
|
|
906
|
+
Self::get_function_start_source_reference(Either::A(trace), called_function)?
|
|
907
|
+
}
|
|
908
|
+
None => Self::get_fallback_start_source_reference(trace)?,
|
|
909
|
+
};
|
|
910
|
+
|
|
911
|
+
let function_type = match called_function {
|
|
912
|
+
Some(_) => ContractFunctionType::Function,
|
|
913
|
+
None => ContractFunctionType::Fallback,
|
|
914
|
+
};
|
|
915
|
+
|
|
916
|
+
Ok(CallstackEntryStackTraceEntry {
|
|
917
|
+
type_: StackTraceEntryTypeConst,
|
|
918
|
+
source_reference,
|
|
919
|
+
function_type: function_type.into(),
|
|
920
|
+
}
|
|
921
|
+
.into())
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
fn call_instruction_to_call_failed_to_execute_stack_trace_entry(
|
|
925
|
+
bytecode: &Bytecode,
|
|
926
|
+
call_inst: &Instruction,
|
|
927
|
+
) -> napi::Result<CallFailedErrorStackTraceEntry> {
|
|
928
|
+
let location = call_inst.location.as_deref();
|
|
929
|
+
|
|
930
|
+
let source_reference = source_location_to_source_reference(bytecode, location)?;
|
|
931
|
+
let source_reference = source_reference.expect("Expected source reference to be defined");
|
|
932
|
+
|
|
933
|
+
// Calls only happen within functions
|
|
934
|
+
Ok(CallFailedErrorStackTraceEntry {
|
|
935
|
+
type_: StackTraceEntryTypeConst,
|
|
936
|
+
source_reference,
|
|
937
|
+
})
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
fn get_entry_before_failure_in_modifier(
|
|
941
|
+
trace: Either<&CallMessageTrace, &CreateMessageTrace>,
|
|
942
|
+
function_jumpdests: &[&Instruction],
|
|
943
|
+
) -> napi::Result<Either<CallstackEntryStackTraceEntry, InternalFunctionCallStackEntry>> {
|
|
944
|
+
let bytecode = match &trace {
|
|
945
|
+
Either::A(call) => &call.bytecode,
|
|
946
|
+
Either::B(create) => &create.bytecode,
|
|
947
|
+
};
|
|
948
|
+
let bytecode = bytecode.as_ref().expect("JS code asserts");
|
|
949
|
+
|
|
950
|
+
// If there's a jumpdest, this modifier belongs to the last function that it
|
|
951
|
+
// represents
|
|
952
|
+
if let Some(last_jumpdest) = function_jumpdests.last() {
|
|
953
|
+
let entry = instruction_to_callstack_stack_trace_entry(bytecode, last_jumpdest)?;
|
|
954
|
+
|
|
955
|
+
return Ok(entry);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// This function is only called after we jumped into the initial function in
|
|
959
|
+
// call traces, so there should always be at least a function jumpdest.
|
|
960
|
+
let trace = match trace {
|
|
961
|
+
Either::A(_call) => return Err(
|
|
962
|
+
napi::Error::new(
|
|
963
|
+
napi::Status::GenericFailure,
|
|
964
|
+
"This shouldn't happen: a call trace has no functionJumpdest but has already jumped into a function"
|
|
965
|
+
)),
|
|
966
|
+
Either::B(create) => create,
|
|
967
|
+
};
|
|
968
|
+
|
|
969
|
+
// If there's no jump dest, we point to the constructor.
|
|
970
|
+
Ok(Either::A(CallstackEntryStackTraceEntry {
|
|
971
|
+
type_: StackTraceEntryTypeConst,
|
|
972
|
+
source_reference: Self::get_constructor_start_source_reference(trace)?,
|
|
973
|
+
function_type: ContractFunctionType::Constructor.into(),
|
|
974
|
+
}))
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
fn fails_right_after_call(
|
|
978
|
+
trace: Either<&CallMessageTrace, &CreateMessageTrace>,
|
|
979
|
+
call_subtrace_step_index: u32,
|
|
980
|
+
) -> napi::Result<bool> {
|
|
981
|
+
let (bytecode, steps) = match &trace {
|
|
982
|
+
Either::A(call) => (&call.bytecode, &call.steps),
|
|
983
|
+
Either::B(create) => (&create.bytecode, &create.steps),
|
|
984
|
+
};
|
|
985
|
+
|
|
986
|
+
let bytecode = bytecode.as_ref().expect("JS code asserts");
|
|
987
|
+
|
|
988
|
+
let Some(Either4::A(last_step)) = steps.last() else {
|
|
989
|
+
return Ok(false);
|
|
990
|
+
};
|
|
991
|
+
|
|
992
|
+
let last_inst = bytecode.get_instruction(last_step.pc)?;
|
|
993
|
+
if last_inst.opcode != OpCode::REVERT {
|
|
994
|
+
return Ok(false);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
let call_opcode_step = steps.get(call_subtrace_step_index as usize - 1);
|
|
998
|
+
let call_opcode_step = match call_opcode_step {
|
|
999
|
+
Some(Either4::A(step)) => step,
|
|
1000
|
+
_ => panic!("JS code asserts this is always an EvmStep"),
|
|
1001
|
+
};
|
|
1002
|
+
let call_inst = bytecode.get_instruction(call_opcode_step.pc)?;
|
|
1003
|
+
|
|
1004
|
+
// Calls are always made from within functions
|
|
1005
|
+
let call_inst_location = call_inst
|
|
1006
|
+
.location
|
|
1007
|
+
.as_ref()
|
|
1008
|
+
.expect("Expected call instruction location to be defined");
|
|
1009
|
+
|
|
1010
|
+
Self::is_last_location(trace, call_subtrace_step_index + 1, call_inst_location)
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
fn is_call_failed_error(
|
|
1014
|
+
trace: Either<&CallMessageTrace, &CreateMessageTrace>,
|
|
1015
|
+
inst_index: u32,
|
|
1016
|
+
call_instruction: &Instruction,
|
|
1017
|
+
) -> napi::Result<bool> {
|
|
1018
|
+
let call_location = match &call_instruction.location {
|
|
1019
|
+
Some(location) => location,
|
|
1020
|
+
None => panic!("Expected call location to be defined"),
|
|
1021
|
+
};
|
|
1022
|
+
|
|
1023
|
+
Self::is_last_location(trace, inst_index, call_location)
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
fn is_last_location(
|
|
1027
|
+
trace: Either<&CallMessageTrace, &CreateMessageTrace>,
|
|
1028
|
+
from_step: u32,
|
|
1029
|
+
location: &SourceLocation,
|
|
1030
|
+
) -> napi::Result<bool> {
|
|
1031
|
+
let (bytecode, steps) = match &trace {
|
|
1032
|
+
Either::A(call) => (&call.bytecode, &call.steps),
|
|
1033
|
+
Either::B(create) => (&create.bytecode, &create.steps),
|
|
1034
|
+
};
|
|
1035
|
+
|
|
1036
|
+
let bytecode = bytecode.as_ref().expect("JS code asserts");
|
|
1037
|
+
|
|
1038
|
+
for step in steps.iter().skip(from_step as usize) {
|
|
1039
|
+
let step = match step {
|
|
1040
|
+
Either4::A(step) => step,
|
|
1041
|
+
_ => return Ok(false),
|
|
1042
|
+
};
|
|
1043
|
+
|
|
1044
|
+
let step_inst = bytecode.get_instruction(step.pc)?;
|
|
1045
|
+
|
|
1046
|
+
if let Some(step_inst_location) = &step_inst.location {
|
|
1047
|
+
if **step_inst_location != *location {
|
|
1048
|
+
return Ok(false);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
Ok(true)
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
fn is_subtrace_error_propagated(
|
|
1057
|
+
trace: Either<&CallMessageTrace, &CreateMessageTrace>,
|
|
1058
|
+
call_subtrace_step_index: u32,
|
|
1059
|
+
) -> napi::Result<bool> {
|
|
1060
|
+
let (return_data, exit, steps) = match &trace {
|
|
1061
|
+
Either::A(call) => (&call.return_data, call.exit.kind(), &call.steps),
|
|
1062
|
+
Either::B(create) => (&create.return_data, create.exit.kind(), &create.steps),
|
|
1063
|
+
};
|
|
1064
|
+
|
|
1065
|
+
let (call_return_data, call_exit) = match steps.get(call_subtrace_step_index as usize) {
|
|
1066
|
+
None | Some(Either4::A(_)) => panic!("Expected call to be a message trace"),
|
|
1067
|
+
Some(Either4::B(precompile)) => (&precompile.return_data, precompile.exit.kind()),
|
|
1068
|
+
Some(Either4::C(call)) => (&call.return_data, call.exit.kind()),
|
|
1069
|
+
Some(Either4::D(create)) => (&create.return_data, create.exit.kind()),
|
|
1070
|
+
};
|
|
1071
|
+
|
|
1072
|
+
if return_data.as_ref() != call_return_data.as_ref() {
|
|
1073
|
+
return Ok(false);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
if exit == ExitCode::OUT_OF_GAS && call_exit == ExitCode::OUT_OF_GAS {
|
|
1077
|
+
return Ok(true);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// If the return data is not empty, and it's still the same, we assume it
|
|
1081
|
+
// is being propagated
|
|
1082
|
+
if return_data.len() > 0 {
|
|
1083
|
+
return Ok(true);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
Self::fails_right_after_call(trace, call_subtrace_step_index)
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
fn is_contract_call_run_out_of_gas_error(
|
|
1090
|
+
trace: Either<&CallMessageTrace, &CreateMessageTrace>,
|
|
1091
|
+
call_step_index: u32,
|
|
1092
|
+
) -> napi::Result<bool> {
|
|
1093
|
+
let (steps, return_data, exit_code) = match &trace {
|
|
1094
|
+
Either::A(call) => (&call.steps, &call.return_data, call.exit.kind()),
|
|
1095
|
+
Either::B(create) => (&create.steps, &create.return_data, create.exit.kind()),
|
|
1096
|
+
};
|
|
1097
|
+
|
|
1098
|
+
if return_data.len() > 0 {
|
|
1099
|
+
return Ok(false);
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
if exit_code != ExitCode::REVERT {
|
|
1103
|
+
return Ok(false);
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
let call_exit = match steps.get(call_step_index as usize) {
|
|
1107
|
+
None | Some(Either4::A(_)) => panic!("Expected call to be a message trace"),
|
|
1108
|
+
Some(Either4::B(precompile)) => precompile.exit.kind(),
|
|
1109
|
+
Some(Either4::C(call)) => call.exit.kind(),
|
|
1110
|
+
Some(Either4::D(create)) => create.exit.kind(),
|
|
1111
|
+
};
|
|
1112
|
+
|
|
1113
|
+
if call_exit != ExitCode::OUT_OF_GAS {
|
|
1114
|
+
return Ok(false);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
Self::fails_right_after_call(trace, call_step_index)
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
fn is_proxy_error_propagated(
|
|
1121
|
+
trace: Either<&CallMessageTrace, &CreateMessageTrace>,
|
|
1122
|
+
call_subtrace_step_index: u32,
|
|
1123
|
+
) -> napi::Result<bool> {
|
|
1124
|
+
let trace = match &trace {
|
|
1125
|
+
Either::A(call) => call,
|
|
1126
|
+
Either::B(_) => return Ok(false),
|
|
1127
|
+
};
|
|
1128
|
+
|
|
1129
|
+
let bytecode = trace.bytecode.as_ref().expect("JS code asserts");
|
|
1130
|
+
|
|
1131
|
+
let call_step = match trace.steps.get(call_subtrace_step_index as usize - 1) {
|
|
1132
|
+
Some(Either4::A(step)) => step,
|
|
1133
|
+
_ => return Ok(false),
|
|
1134
|
+
};
|
|
1135
|
+
|
|
1136
|
+
let call_inst = bytecode.get_instruction(call_step.pc)?;
|
|
1137
|
+
|
|
1138
|
+
if call_inst.opcode != OpCode::DELEGATECALL {
|
|
1139
|
+
return Ok(false);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
let subtrace = match trace.steps.get(call_subtrace_step_index as usize) {
|
|
1143
|
+
None | Some(Either4::A(_) | Either4::B(_)) => return Ok(false),
|
|
1144
|
+
Some(Either4::C(call)) => Either::A(call),
|
|
1145
|
+
Some(Either4::D(create)) => Either::B(create),
|
|
1146
|
+
};
|
|
1147
|
+
|
|
1148
|
+
let (subtrace_bytecode, subtrace_return_data) = match &subtrace {
|
|
1149
|
+
Either::A(call) => (&call.bytecode, &call.return_data),
|
|
1150
|
+
Either::B(create) => (&create.bytecode, &create.return_data),
|
|
1151
|
+
};
|
|
1152
|
+
let subtrace_bytecode = match subtrace_bytecode {
|
|
1153
|
+
Some(bytecode) => bytecode,
|
|
1154
|
+
// If we can't recognize the implementation we'd better don't consider it as such
|
|
1155
|
+
None => return Ok(false),
|
|
1156
|
+
};
|
|
1157
|
+
|
|
1158
|
+
if subtrace_bytecode.contract.borrow().r#type == ContractKind::Library {
|
|
1159
|
+
return Ok(false);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
if trace.return_data.as_ref() != subtrace_return_data.as_ref() {
|
|
1163
|
+
return Ok(false);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
for step in trace
|
|
1167
|
+
.steps
|
|
1168
|
+
.iter()
|
|
1169
|
+
.skip(call_subtrace_step_index as usize + 1)
|
|
1170
|
+
{
|
|
1171
|
+
let step = match step {
|
|
1172
|
+
Either4::A(step) => step,
|
|
1173
|
+
_ => return Ok(false),
|
|
1174
|
+
};
|
|
1175
|
+
|
|
1176
|
+
let inst = subtrace_bytecode.get_instruction(step.pc)?;
|
|
1177
|
+
|
|
1178
|
+
// All the remaining locations should be valid, as they are part of the inline
|
|
1179
|
+
// asm
|
|
1180
|
+
if inst.location.is_none() {
|
|
1181
|
+
return Ok(false);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
if matches!(
|
|
1185
|
+
inst.jump_type,
|
|
1186
|
+
JumpType::IntoFunction | JumpType::OutofFunction
|
|
1187
|
+
) {
|
|
1188
|
+
return Ok(false);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
let last_step = match trace.steps.last() {
|
|
1193
|
+
Some(Either4::A(step)) => step,
|
|
1194
|
+
_ => panic!("Expected last step to be an EvmStep"),
|
|
1195
|
+
};
|
|
1196
|
+
let last_inst = bytecode.get_instruction(last_step.pc)?;
|
|
1197
|
+
|
|
1198
|
+
Ok(last_inst.opcode == OpCode::REVERT)
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
fn other_execution_error_stacktrace(
|
|
1202
|
+
trace: Either<&CallMessageTrace, &CreateMessageTrace>,
|
|
1203
|
+
mut stacktrace: SolidityStackTrace,
|
|
1204
|
+
) -> napi::Result<SolidityStackTrace> {
|
|
1205
|
+
let other_execution_error_frame = OtherExecutionErrorStackTraceEntry {
|
|
1206
|
+
type_: StackTraceEntryTypeConst,
|
|
1207
|
+
source_reference: Self::get_last_source_reference(trace)?,
|
|
1208
|
+
};
|
|
1209
|
+
|
|
1210
|
+
stacktrace.push(other_execution_error_frame.into());
|
|
1211
|
+
Ok(stacktrace)
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
fn is_direct_library_call(trace: &CallMessageTrace) -> napi::Result<bool> {
|
|
1215
|
+
let contract = &trace.bytecode.as_ref().expect("JS code asserts").contract;
|
|
1216
|
+
let contract = contract.borrow();
|
|
1217
|
+
|
|
1218
|
+
Ok(trace.depth == 0 && contract.r#type == ContractKind::Library)
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
fn get_direct_library_call_error_stack_trace(
|
|
1222
|
+
trace: &CallMessageTrace,
|
|
1223
|
+
) -> napi::Result<SolidityStackTrace> {
|
|
1224
|
+
let contract = &trace.bytecode.as_ref().expect("JS code asserts").contract;
|
|
1225
|
+
let contract = contract.borrow();
|
|
1226
|
+
|
|
1227
|
+
let func = contract
|
|
1228
|
+
.get_function_from_selector(trace.calldata.get(..4).unwrap_or(&trace.calldata[..]));
|
|
1229
|
+
|
|
1230
|
+
let source_reference = match func {
|
|
1231
|
+
Some(func) => Self::get_function_start_source_reference(Either::A(trace), func)?,
|
|
1232
|
+
None => Self::get_contract_start_without_function_source_reference(Either::A(trace))?,
|
|
1233
|
+
};
|
|
1234
|
+
|
|
1235
|
+
Ok(vec![DirectLibraryCallErrorStackTraceEntry {
|
|
1236
|
+
type_: StackTraceEntryTypeConst,
|
|
1237
|
+
source_reference,
|
|
1238
|
+
}
|
|
1239
|
+
.into()])
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
fn get_function_start_source_reference(
|
|
1243
|
+
trace: Either<&CallMessageTrace, &CreateMessageTrace>,
|
|
1244
|
+
func: &ContractFunction,
|
|
1245
|
+
) -> napi::Result<SourceReference> {
|
|
1246
|
+
let bytecode = match &trace {
|
|
1247
|
+
Either::A(create) => &create.bytecode,
|
|
1248
|
+
Either::B(call) => &call.bytecode,
|
|
1249
|
+
};
|
|
1250
|
+
|
|
1251
|
+
let contract = &bytecode.as_ref().expect("JS code asserts").contract;
|
|
1252
|
+
let contract = contract.borrow();
|
|
1253
|
+
|
|
1254
|
+
let file = func.location.file();
|
|
1255
|
+
let file = file.borrow();
|
|
1256
|
+
|
|
1257
|
+
let location = &func.location;
|
|
1258
|
+
|
|
1259
|
+
Ok(SourceReference {
|
|
1260
|
+
source_name: file.source_name.clone(),
|
|
1261
|
+
source_content: file.content.clone(),
|
|
1262
|
+
contract: Some(contract.name.clone()),
|
|
1263
|
+
|
|
1264
|
+
function: Some(func.name.clone()),
|
|
1265
|
+
line: location.get_starting_line_number(),
|
|
1266
|
+
range: [location.offset, location.offset + location.length].to_vec(),
|
|
1267
|
+
})
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
fn is_missing_function_and_fallback_error(
|
|
1271
|
+
trace: &CallMessageTrace,
|
|
1272
|
+
called_function: Option<&ContractFunction>,
|
|
1273
|
+
) -> napi::Result<bool> {
|
|
1274
|
+
// This error doesn't return data
|
|
1275
|
+
if trace.return_data.len() > 0 {
|
|
1276
|
+
return Ok(false);
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// the called function exists in the contract
|
|
1280
|
+
if called_function.is_some() {
|
|
1281
|
+
return Ok(false);
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
let bytecode = trace
|
|
1285
|
+
.bytecode
|
|
1286
|
+
.as_ref()
|
|
1287
|
+
.expect("The TS code type-checks this to always have bytecode");
|
|
1288
|
+
let contract = bytecode.contract.borrow();
|
|
1289
|
+
|
|
1290
|
+
// there's a receive function and no calldata
|
|
1291
|
+
if trace.calldata.len() == 0 && contract.receive.is_some() {
|
|
1292
|
+
return Ok(false);
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
Ok(contract.fallback.is_none())
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
fn empty_calldata_and_no_receive(trace: &CallMessageTrace) -> napi::Result<bool> {
|
|
1299
|
+
let bytecode = trace
|
|
1300
|
+
.bytecode
|
|
1301
|
+
.as_ref()
|
|
1302
|
+
.expect("The TS code type-checks this to always have bytecode");
|
|
1303
|
+
let contract = bytecode.contract.borrow();
|
|
1304
|
+
|
|
1305
|
+
let version = Version::parse(&bytecode.compiler_version).unwrap();
|
|
1306
|
+
// this only makes sense when receive functions are available
|
|
1307
|
+
if version < FIRST_SOLC_VERSION_RECEIVE_FUNCTION {
|
|
1308
|
+
return Ok(false);
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
Ok(trace.calldata.is_empty() && contract.receive.is_none())
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
fn is_fallback_not_payable_error(
|
|
1315
|
+
trace: &CallMessageTrace,
|
|
1316
|
+
called_function: Option<&ContractFunction>,
|
|
1317
|
+
) -> napi::Result<bool> {
|
|
1318
|
+
// This error doesn't return data
|
|
1319
|
+
if !trace.return_data.is_empty() {
|
|
1320
|
+
return Ok(false);
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
let (neg, value, _) = trace.value.get_u64();
|
|
1324
|
+
if neg || value == 0 {
|
|
1325
|
+
return Ok(false);
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
// the called function exists in the contract
|
|
1329
|
+
if called_function.is_some() {
|
|
1330
|
+
return Ok(false);
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
let bytecode = trace.bytecode.as_ref().expect("JS code asserts");
|
|
1334
|
+
let contract = bytecode.contract.borrow();
|
|
1335
|
+
|
|
1336
|
+
match &contract.fallback {
|
|
1337
|
+
Some(fallback) => Ok(fallback.is_payable != Some(true)),
|
|
1338
|
+
None => Ok(false),
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
fn get_fallback_start_source_reference(
|
|
1343
|
+
trace: &CallMessageTrace,
|
|
1344
|
+
) -> napi::Result<SourceReference> {
|
|
1345
|
+
let bytecode = trace.bytecode.as_ref().expect("JS code asserts");
|
|
1346
|
+
let contract = bytecode.contract.borrow();
|
|
1347
|
+
|
|
1348
|
+
let func = match &contract.fallback {
|
|
1349
|
+
Some(func) => func,
|
|
1350
|
+
None => panic!("This shouldn't happen: trying to get fallback source reference from a contract without fallback"),
|
|
1351
|
+
};
|
|
1352
|
+
|
|
1353
|
+
let location = &func.location;
|
|
1354
|
+
let file = location.file();
|
|
1355
|
+
let file = file.borrow();
|
|
1356
|
+
|
|
1357
|
+
Ok(SourceReference {
|
|
1358
|
+
source_name: file.source_name.clone(),
|
|
1359
|
+
source_content: file.content.clone(),
|
|
1360
|
+
contract: Some(contract.name.clone()),
|
|
1361
|
+
function: Some(FALLBACK_FUNCTION_NAME.to_string()),
|
|
1362
|
+
line: location.get_starting_line_number(),
|
|
1363
|
+
range: [location.offset, location.offset + location.length].to_vec(),
|
|
1364
|
+
})
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
fn is_constructor_not_payable_error(trace: &CreateMessageTrace) -> napi::Result<bool> {
|
|
1368
|
+
// This error doesn't return data
|
|
1369
|
+
if !trace.return_data.is_empty() {
|
|
1370
|
+
return Ok(false);
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
let bytecode = trace.bytecode.as_ref().expect("JS code asserts");
|
|
1374
|
+
let contract = bytecode.contract.borrow();
|
|
1375
|
+
|
|
1376
|
+
// This function is only matters with contracts that have constructors defined.
|
|
1377
|
+
// The ones that don't are abstract contracts, or their constructor
|
|
1378
|
+
// doesn't take any argument.
|
|
1379
|
+
let constructor = match &contract.constructor {
|
|
1380
|
+
Some(constructor) => constructor,
|
|
1381
|
+
None => return Ok(false),
|
|
1382
|
+
};
|
|
1383
|
+
|
|
1384
|
+
let (neg, value, _) = trace.value.get_u64();
|
|
1385
|
+
if neg || value == 0 {
|
|
1386
|
+
return Ok(false);
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
Ok(constructor.is_payable != Some(true))
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
/// Returns a source reference pointing to the constructor if it exists, or
|
|
1393
|
+
/// to the contract otherwise.
|
|
1394
|
+
fn get_constructor_start_source_reference(
|
|
1395
|
+
trace: &CreateMessageTrace,
|
|
1396
|
+
) -> napi::Result<SourceReference> {
|
|
1397
|
+
let bytecode = trace.bytecode.as_ref().expect("JS code asserts");
|
|
1398
|
+
let contract = bytecode.contract.borrow();
|
|
1399
|
+
let contract_location = &contract.location;
|
|
1400
|
+
|
|
1401
|
+
let line = match &contract.constructor {
|
|
1402
|
+
Some(constructor) => constructor.location.get_starting_line_number(),
|
|
1403
|
+
None => contract_location.get_starting_line_number(),
|
|
1404
|
+
};
|
|
1405
|
+
|
|
1406
|
+
let file = contract_location.file();
|
|
1407
|
+
let file = file.borrow();
|
|
1408
|
+
|
|
1409
|
+
Ok(SourceReference {
|
|
1410
|
+
source_name: file.source_name.clone(),
|
|
1411
|
+
source_content: file.content.clone(),
|
|
1412
|
+
contract: Some(contract.name.clone()),
|
|
1413
|
+
function: Some(CONSTRUCTOR_FUNCTION_NAME.to_string()),
|
|
1414
|
+
line,
|
|
1415
|
+
range: [
|
|
1416
|
+
contract_location.offset,
|
|
1417
|
+
contract_location.offset + contract_location.length,
|
|
1418
|
+
]
|
|
1419
|
+
.to_vec(),
|
|
1420
|
+
})
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
fn is_constructor_invalid_arguments_error(trace: &CreateMessageTrace) -> napi::Result<bool> {
|
|
1424
|
+
if trace.return_data.len() > 0 {
|
|
1425
|
+
return Ok(false);
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
let bytecode = trace.bytecode.as_ref().expect("JS code asserts");
|
|
1429
|
+
let contract = bytecode.contract.borrow();
|
|
1430
|
+
|
|
1431
|
+
// This function is only matters with contracts that have constructors defined.
|
|
1432
|
+
// The ones that don't are abstract contracts, or their constructor
|
|
1433
|
+
// doesn't take any argument.
|
|
1434
|
+
let Some(constructor) = &contract.constructor else {
|
|
1435
|
+
return Ok(false);
|
|
1436
|
+
};
|
|
1437
|
+
|
|
1438
|
+
let Ok(version) = Version::parse(&bytecode.compiler_version) else {
|
|
1439
|
+
return Ok(false);
|
|
1440
|
+
};
|
|
1441
|
+
if version < FIRST_SOLC_VERSION_CREATE_PARAMS_VALIDATION {
|
|
1442
|
+
return Ok(false);
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
let last_step = trace.steps.last();
|
|
1446
|
+
let Some(Either4::A(last_step)) = last_step else {
|
|
1447
|
+
return Ok(false);
|
|
1448
|
+
};
|
|
1449
|
+
|
|
1450
|
+
let last_inst = bytecode.get_instruction(last_step.pc)?;
|
|
1451
|
+
|
|
1452
|
+
if last_inst.opcode != OpCode::REVERT || last_inst.location.is_some() {
|
|
1453
|
+
return Ok(false);
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
let mut has_read_deployment_code_size = false;
|
|
1457
|
+
for step in &trace.steps {
|
|
1458
|
+
let step = match step {
|
|
1459
|
+
Either4::A(step) => step,
|
|
1460
|
+
_ => return Ok(false),
|
|
1461
|
+
};
|
|
1462
|
+
|
|
1463
|
+
let inst = bytecode.get_instruction(step.pc)?;
|
|
1464
|
+
|
|
1465
|
+
if let Some(inst_location) = &inst.location {
|
|
1466
|
+
if contract.location != *inst_location && constructor.location != *inst_location {
|
|
1467
|
+
return Ok(false);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
if inst.opcode == OpCode::CODESIZE {
|
|
1472
|
+
has_read_deployment_code_size = true;
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
Ok(has_read_deployment_code_size)
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
fn get_contract_start_without_function_source_reference(
|
|
1480
|
+
trace: Either<&CallMessageTrace, &CreateMessageTrace>,
|
|
1481
|
+
) -> napi::Result<SourceReference> {
|
|
1482
|
+
let bytecode = match &trace {
|
|
1483
|
+
Either::A(create) => &create.bytecode,
|
|
1484
|
+
Either::B(call) => &call.bytecode,
|
|
1485
|
+
};
|
|
1486
|
+
|
|
1487
|
+
let contract = &bytecode.as_ref().expect("JS code asserts").contract;
|
|
1488
|
+
|
|
1489
|
+
let contract = contract.borrow();
|
|
1490
|
+
|
|
1491
|
+
let location = &contract.location;
|
|
1492
|
+
let file = location.file();
|
|
1493
|
+
let file = file.borrow();
|
|
1494
|
+
|
|
1495
|
+
Ok(SourceReference {
|
|
1496
|
+
source_name: file.source_name.clone(),
|
|
1497
|
+
source_content: file.content.clone(),
|
|
1498
|
+
contract: Some(contract.name.clone()),
|
|
1499
|
+
|
|
1500
|
+
function: None,
|
|
1501
|
+
line: location.get_starting_line_number(),
|
|
1502
|
+
range: [location.offset, location.offset + location.length].to_vec(),
|
|
1503
|
+
})
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
fn is_function_not_payable_error(
|
|
1507
|
+
trace: &CallMessageTrace,
|
|
1508
|
+
called_function: &ContractFunction,
|
|
1509
|
+
) -> napi::Result<bool> {
|
|
1510
|
+
// This error doesn't return data
|
|
1511
|
+
if !trace.return_data.is_empty() {
|
|
1512
|
+
return Ok(false);
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
let (neg, value, _) = trace.value.get_u64();
|
|
1516
|
+
if neg || value == 0 {
|
|
1517
|
+
return Ok(false);
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
let bytecode = trace.bytecode.as_ref().expect("JS code asserts");
|
|
1521
|
+
let contract = bytecode.contract.borrow();
|
|
1522
|
+
|
|
1523
|
+
// Libraries don't have a nonpayable check
|
|
1524
|
+
if contract.r#type == ContractKind::Library {
|
|
1525
|
+
return Ok(false);
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
Ok(called_function.is_payable != Some(true))
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
fn get_last_source_reference(
|
|
1532
|
+
trace: Either<&CallMessageTrace, &CreateMessageTrace>,
|
|
1533
|
+
) -> napi::Result<Option<SourceReference>> {
|
|
1534
|
+
let (bytecode, steps) = match trace {
|
|
1535
|
+
Either::A(create) => (&create.bytecode, &create.steps),
|
|
1536
|
+
Either::B(call) => (&call.bytecode, &call.steps),
|
|
1537
|
+
};
|
|
1538
|
+
|
|
1539
|
+
let bytecode = bytecode
|
|
1540
|
+
.as_ref()
|
|
1541
|
+
.expect("JS code only accepted variants that had bytecode defined");
|
|
1542
|
+
|
|
1543
|
+
for step in steps.iter().rev() {
|
|
1544
|
+
let step = match step {
|
|
1545
|
+
Either4::A(step) => step,
|
|
1546
|
+
_ => continue,
|
|
1547
|
+
};
|
|
1548
|
+
|
|
1549
|
+
let inst = bytecode.get_instruction(step.pc)?;
|
|
1550
|
+
|
|
1551
|
+
let Some(location) = &inst.location else {
|
|
1552
|
+
continue;
|
|
1553
|
+
};
|
|
1554
|
+
|
|
1555
|
+
let source_reference = source_location_to_source_reference(bytecode, Some(location))?;
|
|
1556
|
+
|
|
1557
|
+
if let Some(source_reference) = source_reference {
|
|
1558
|
+
return Ok(Some(source_reference));
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
Ok(None)
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
fn has_failed_inside_the_fallback_function(trace: &CallMessageTrace) -> napi::Result<bool> {
|
|
1566
|
+
let contract = &trace.bytecode.as_ref().expect("JS code asserts").contract;
|
|
1567
|
+
let contract = contract.borrow();
|
|
1568
|
+
|
|
1569
|
+
match &contract.fallback {
|
|
1570
|
+
Some(fallback) => Self::has_failed_inside_function(trace, fallback),
|
|
1571
|
+
None => Ok(false),
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
fn has_failed_inside_the_receive_function(trace: &CallMessageTrace) -> napi::Result<bool> {
|
|
1576
|
+
let contract = &trace.bytecode.as_ref().expect("JS code asserts").contract;
|
|
1577
|
+
let contract = contract.borrow();
|
|
1578
|
+
|
|
1579
|
+
match &contract.receive {
|
|
1580
|
+
Some(receive) => Self::has_failed_inside_function(trace, receive),
|
|
1581
|
+
None => Ok(false),
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
fn has_failed_inside_function(
|
|
1586
|
+
trace: &CallMessageTrace,
|
|
1587
|
+
func: &ContractFunction,
|
|
1588
|
+
) -> napi::Result<bool> {
|
|
1589
|
+
let last_step = trace.steps.last().unwrap();
|
|
1590
|
+
let last_step = match last_step {
|
|
1591
|
+
Either4::A(step) => step,
|
|
1592
|
+
_ => panic!("JS code asserted this is always an EvmStep"),
|
|
1593
|
+
};
|
|
1594
|
+
|
|
1595
|
+
let last_instruction = trace
|
|
1596
|
+
.bytecode
|
|
1597
|
+
.as_ref()
|
|
1598
|
+
.expect("The TS code type-checks this to always have bytecode")
|
|
1599
|
+
.get_instruction(last_step.pc)?;
|
|
1600
|
+
|
|
1601
|
+
Ok(match &last_instruction.location {
|
|
1602
|
+
Some(last_instruction_location) => {
|
|
1603
|
+
last_instruction.opcode == OpCode::REVERT
|
|
1604
|
+
&& func.location.contains(last_instruction_location)
|
|
1605
|
+
}
|
|
1606
|
+
_ => false,
|
|
1607
|
+
})
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
fn instruction_within_function_to_revert_stack_trace_entry(
|
|
1611
|
+
trace: Either<&CallMessageTrace, &CreateMessageTrace>,
|
|
1612
|
+
inst: &Instruction,
|
|
1613
|
+
) -> napi::Result<RevertErrorStackTraceEntry> {
|
|
1614
|
+
let bytecode = match &trace {
|
|
1615
|
+
Either::A(create) => &create.bytecode,
|
|
1616
|
+
Either::B(call) => &call.bytecode,
|
|
1617
|
+
}
|
|
1618
|
+
.as_ref()
|
|
1619
|
+
.expect("JS code asserts");
|
|
1620
|
+
|
|
1621
|
+
let source_reference =
|
|
1622
|
+
source_location_to_source_reference(bytecode, inst.location.as_deref())?
|
|
1623
|
+
.expect("Expected source reference to be defined");
|
|
1624
|
+
|
|
1625
|
+
let return_data = match &trace {
|
|
1626
|
+
Either::A(create) => &create.return_data,
|
|
1627
|
+
Either::B(call) => &call.return_data,
|
|
1628
|
+
};
|
|
1629
|
+
|
|
1630
|
+
Ok(RevertErrorStackTraceEntry {
|
|
1631
|
+
type_: StackTraceEntryTypeConst,
|
|
1632
|
+
source_reference,
|
|
1633
|
+
is_invalid_opcode_error: inst.opcode == OpCode::INVALID,
|
|
1634
|
+
return_data: return_data.clone(),
|
|
1635
|
+
})
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
fn instruction_within_function_to_unmapped_solc_0_6_3_revert_error_stack_trace_entry(
|
|
1639
|
+
trace: Either<&CallMessageTrace, &CreateMessageTrace>,
|
|
1640
|
+
inst: &Instruction,
|
|
1641
|
+
) -> napi::Result<UnmappedSolc063RevertErrorStackTraceEntry> {
|
|
1642
|
+
let bytecode = match &trace {
|
|
1643
|
+
Either::A(create) => &create.bytecode,
|
|
1644
|
+
Either::B(call) => &call.bytecode,
|
|
1645
|
+
}
|
|
1646
|
+
.as_ref()
|
|
1647
|
+
.expect("JS code asserts");
|
|
1648
|
+
|
|
1649
|
+
let source_reference =
|
|
1650
|
+
source_location_to_source_reference(bytecode, inst.location.as_deref())?;
|
|
1651
|
+
|
|
1652
|
+
Ok(UnmappedSolc063RevertErrorStackTraceEntry {
|
|
1653
|
+
type_: StackTraceEntryTypeConst,
|
|
1654
|
+
source_reference,
|
|
1655
|
+
})
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
fn instruction_within_function_to_panic_stack_trace_entry(
|
|
1659
|
+
trace: Either<&CallMessageTrace, &CreateMessageTrace>,
|
|
1660
|
+
inst: &Instruction,
|
|
1661
|
+
error_code: BigInt,
|
|
1662
|
+
) -> napi::Result<PanicErrorStackTraceEntry> {
|
|
1663
|
+
let last_source_reference = Self::get_last_source_reference(trace)?;
|
|
1664
|
+
|
|
1665
|
+
let bytecode = match &trace {
|
|
1666
|
+
Either::A(create) => &create.bytecode,
|
|
1667
|
+
Either::B(call) => &call.bytecode,
|
|
1668
|
+
}
|
|
1669
|
+
.as_ref()
|
|
1670
|
+
.expect("JS code asserts");
|
|
1671
|
+
|
|
1672
|
+
let source_reference =
|
|
1673
|
+
source_location_to_source_reference(bytecode, inst.location.as_deref())?;
|
|
1674
|
+
|
|
1675
|
+
let source_reference = source_reference.or(last_source_reference);
|
|
1676
|
+
|
|
1677
|
+
Ok(PanicErrorStackTraceEntry {
|
|
1678
|
+
type_: StackTraceEntryTypeConst,
|
|
1679
|
+
source_reference,
|
|
1680
|
+
error_code,
|
|
1681
|
+
})
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
fn instruction_within_function_to_custom_error_stack_trace_entry(
|
|
1685
|
+
trace: Either<&CallMessageTrace, &CreateMessageTrace>,
|
|
1686
|
+
inst: &Instruction,
|
|
1687
|
+
message: String,
|
|
1688
|
+
) -> napi::Result<CustomErrorStackTraceEntry> {
|
|
1689
|
+
let last_source_reference = Self::get_last_source_reference(trace)?;
|
|
1690
|
+
let last_source_reference =
|
|
1691
|
+
last_source_reference.expect("Expected source reference to be defined");
|
|
1692
|
+
|
|
1693
|
+
let bytecode = match &trace {
|
|
1694
|
+
Either::A(create) => &create.bytecode,
|
|
1695
|
+
Either::B(call) => &call.bytecode,
|
|
1696
|
+
}
|
|
1697
|
+
.as_ref()
|
|
1698
|
+
.expect("JS code asserts");
|
|
1699
|
+
|
|
1700
|
+
let source_reference =
|
|
1701
|
+
source_location_to_source_reference(bytecode, inst.location.as_deref())?;
|
|
1702
|
+
|
|
1703
|
+
let source_reference = source_reference.unwrap_or(last_source_reference);
|
|
1704
|
+
|
|
1705
|
+
Ok(CustomErrorStackTraceEntry {
|
|
1706
|
+
type_: StackTraceEntryTypeConst,
|
|
1707
|
+
source_reference,
|
|
1708
|
+
message,
|
|
1709
|
+
})
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
fn solidity_0_6_3_maybe_unmapped_revert(
|
|
1713
|
+
trace: Either<&CallMessageTrace, &CreateMessageTrace>,
|
|
1714
|
+
) -> napi::Result<bool> {
|
|
1715
|
+
let (bytecode, steps) = match &trace {
|
|
1716
|
+
Either::A(create) => (&create.bytecode, &create.steps),
|
|
1717
|
+
Either::B(call) => (&call.bytecode, &call.steps),
|
|
1718
|
+
};
|
|
1719
|
+
|
|
1720
|
+
let bytecode = bytecode
|
|
1721
|
+
.as_ref()
|
|
1722
|
+
.expect("JS code only accepted variants that had bytecode defined");
|
|
1723
|
+
|
|
1724
|
+
if steps.is_empty() {
|
|
1725
|
+
return Ok(false);
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
let last_step = steps.last();
|
|
1729
|
+
let last_step = match last_step {
|
|
1730
|
+
Some(Either4::A(step)) => step,
|
|
1731
|
+
_ => return Ok(false),
|
|
1732
|
+
};
|
|
1733
|
+
|
|
1734
|
+
let last_instruction = bytecode.get_instruction(last_step.pc)?;
|
|
1735
|
+
|
|
1736
|
+
let Ok(version) = Version::parse(&bytecode.compiler_version) else {
|
|
1737
|
+
return Ok(false);
|
|
1738
|
+
};
|
|
1739
|
+
let req = VersionReq::parse(&format!("^{FIRST_SOLC_VERSION_WITH_UNMAPPED_REVERTS}"))
|
|
1740
|
+
.expect("valid semver");
|
|
1741
|
+
|
|
1742
|
+
Ok(req.matches(&version) && last_instruction.opcode == OpCode::REVERT)
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
// Solidity 0.6.3 unmapped reverts special handling
|
|
1746
|
+
// For more info: https://github.com/ethereum/solidity/issues/9006
|
|
1747
|
+
fn solidity_0_6_3_get_frame_for_unmapped_revert_before_function(
|
|
1748
|
+
trace: &CallMessageTrace,
|
|
1749
|
+
) -> napi::Result<Option<UnmappedSolc063RevertErrorStackTraceEntry>> {
|
|
1750
|
+
let bytecode = trace.bytecode.as_ref().expect("JS code asserts");
|
|
1751
|
+
let contract = bytecode.contract.borrow();
|
|
1752
|
+
|
|
1753
|
+
let revert_frame =
|
|
1754
|
+
Self::solidity_0_6_3_get_frame_for_unmapped_revert_within_function(Either::A(trace))?;
|
|
1755
|
+
|
|
1756
|
+
let revert_frame = match revert_frame {
|
|
1757
|
+
None
|
|
1758
|
+
| Some(UnmappedSolc063RevertErrorStackTraceEntry {
|
|
1759
|
+
source_reference: None,
|
|
1760
|
+
..
|
|
1761
|
+
}) => {
|
|
1762
|
+
if contract.receive.is_none() || trace.calldata.len() > 0 {
|
|
1763
|
+
// Failed within the fallback
|
|
1764
|
+
if let Some(fallback) = &contract.fallback {
|
|
1765
|
+
let location = &fallback.location;
|
|
1766
|
+
let file = location.file();
|
|
1767
|
+
let file = file.borrow();
|
|
1768
|
+
|
|
1769
|
+
let revert_frame = UnmappedSolc063RevertErrorStackTraceEntry {
|
|
1770
|
+
type_: StackTraceEntryTypeConst,
|
|
1771
|
+
source_reference: Some(SourceReference {
|
|
1772
|
+
contract: Some(contract.name.clone()),
|
|
1773
|
+
function: Some(FALLBACK_FUNCTION_NAME.to_string()),
|
|
1774
|
+
source_name: file.source_name.clone(),
|
|
1775
|
+
source_content: file.content.clone(),
|
|
1776
|
+
line: location.get_starting_line_number(),
|
|
1777
|
+
range: [location.offset, location.offset + location.length]
|
|
1778
|
+
.to_vec(),
|
|
1779
|
+
}),
|
|
1780
|
+
};
|
|
1781
|
+
|
|
1782
|
+
Some(Self::solidity_0_6_3_correct_line_number(revert_frame))
|
|
1783
|
+
} else {
|
|
1784
|
+
None
|
|
1785
|
+
}
|
|
1786
|
+
} else {
|
|
1787
|
+
let receive = contract
|
|
1788
|
+
.receive
|
|
1789
|
+
.as_ref()
|
|
1790
|
+
.expect("None always hits branch above");
|
|
1791
|
+
|
|
1792
|
+
let location = &receive.location;
|
|
1793
|
+
let file = location.file();
|
|
1794
|
+
let file = file.borrow();
|
|
1795
|
+
|
|
1796
|
+
let revert_frame = UnmappedSolc063RevertErrorStackTraceEntry {
|
|
1797
|
+
type_: StackTraceEntryTypeConst,
|
|
1798
|
+
source_reference: Some(SourceReference {
|
|
1799
|
+
contract: Some(contract.name.clone()),
|
|
1800
|
+
function: Some(RECEIVE_FUNCTION_NAME.to_string()),
|
|
1801
|
+
source_name: file.source_name.clone(),
|
|
1802
|
+
source_content: file.content.clone(),
|
|
1803
|
+
line: location.get_starting_line_number(),
|
|
1804
|
+
range: [location.offset, location.offset + location.length].to_vec(),
|
|
1805
|
+
}),
|
|
1806
|
+
};
|
|
1807
|
+
|
|
1808
|
+
Some(Self::solidity_0_6_3_correct_line_number(revert_frame))
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
Some(revert_frame) => Some(revert_frame),
|
|
1812
|
+
};
|
|
1813
|
+
|
|
1814
|
+
Ok(revert_frame)
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
fn solidity_0_6_3_get_frame_for_unmapped_revert_within_function(
|
|
1818
|
+
trace: Either<&CallMessageTrace, &CreateMessageTrace>,
|
|
1819
|
+
) -> napi::Result<Option<UnmappedSolc063RevertErrorStackTraceEntry>> {
|
|
1820
|
+
let (bytecode, steps) = match &trace {
|
|
1821
|
+
Either::A(create) => (&create.bytecode, &create.steps),
|
|
1822
|
+
Either::B(call) => (&call.bytecode, &call.steps),
|
|
1823
|
+
};
|
|
1824
|
+
|
|
1825
|
+
let bytecode = bytecode
|
|
1826
|
+
.as_ref()
|
|
1827
|
+
.expect("JS code only accepted variants that had bytecode defined");
|
|
1828
|
+
|
|
1829
|
+
let contract = bytecode.contract.borrow();
|
|
1830
|
+
|
|
1831
|
+
// If we are within a function there's a last valid location. It may
|
|
1832
|
+
// be the entire contract.
|
|
1833
|
+
let prev_inst = Self::get_last_instruction_with_valid_location(trace)?;
|
|
1834
|
+
let last_step = match steps.last() {
|
|
1835
|
+
Some(Either4::A(step)) => step,
|
|
1836
|
+
_ => panic!("JS code asserts this is always an EvmStep"),
|
|
1837
|
+
};
|
|
1838
|
+
let next_inst_pc = last_step.pc + 1;
|
|
1839
|
+
let has_next_inst = bytecode.has_instruction(next_inst_pc);
|
|
1840
|
+
|
|
1841
|
+
if has_next_inst {
|
|
1842
|
+
let next_inst = bytecode.get_instruction(next_inst_pc)?;
|
|
1843
|
+
|
|
1844
|
+
let prev_loc = prev_inst.and_then(|i| i.location.as_deref());
|
|
1845
|
+
let next_loc = next_inst.location.as_deref();
|
|
1846
|
+
|
|
1847
|
+
let prev_func = prev_loc.and_then(SourceLocation::get_containing_function);
|
|
1848
|
+
let next_func = next_loc.and_then(SourceLocation::get_containing_function);
|
|
1849
|
+
|
|
1850
|
+
// This is probably a require. This means that we have the exact
|
|
1851
|
+
// line, but the stack trace may be degraded (e.g. missing our
|
|
1852
|
+
// synthetic call frames when failing in a modifier) so we still
|
|
1853
|
+
// add this frame as UNMAPPED_SOLC_0_6_3_REVERT_ERROR
|
|
1854
|
+
match (&prev_func, &next_loc, &prev_loc) {
|
|
1855
|
+
(Some(_), Some(next_loc), Some(prev_loc)) if prev_loc == next_loc => {
|
|
1856
|
+
return Ok(Some(Self::instruction_within_function_to_unmapped_solc_0_6_3_revert_error_stack_trace_entry(
|
|
1857
|
+
trace,
|
|
1858
|
+
next_inst,
|
|
1859
|
+
|
|
1860
|
+
)?));
|
|
1861
|
+
}
|
|
1862
|
+
_ => {}
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
let revert_frame = if prev_func.is_some() && prev_inst.is_some() {
|
|
1866
|
+
Some(Self::instruction_within_function_to_unmapped_solc_0_6_3_revert_error_stack_trace_entry(
|
|
1867
|
+
trace,
|
|
1868
|
+
prev_inst.as_ref().unwrap(),
|
|
1869
|
+
|
|
1870
|
+
)?)
|
|
1871
|
+
} else if next_func.is_some() {
|
|
1872
|
+
Some(Self::instruction_within_function_to_unmapped_solc_0_6_3_revert_error_stack_trace_entry(
|
|
1873
|
+
trace,
|
|
1874
|
+
next_inst,
|
|
1875
|
+
|
|
1876
|
+
)?)
|
|
1877
|
+
} else {
|
|
1878
|
+
None
|
|
1879
|
+
};
|
|
1880
|
+
|
|
1881
|
+
return Ok(revert_frame.map(Self::solidity_0_6_3_correct_line_number));
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
if matches!(trace, Either::B(CreateMessageTrace { .. })) && prev_inst.is_some() {
|
|
1885
|
+
// Solidity is smart enough to stop emitting extra instructions after
|
|
1886
|
+
// an unconditional revert happens in a constructor. If this is the case
|
|
1887
|
+
// we just return a special error.
|
|
1888
|
+
|
|
1889
|
+
let mut constructor_revert_frame = Self::instruction_within_function_to_unmapped_solc_0_6_3_revert_error_stack_trace_entry(
|
|
1890
|
+
trace,
|
|
1891
|
+
prev_inst.as_ref().unwrap(),
|
|
1892
|
+
|
|
1893
|
+
)?;
|
|
1894
|
+
|
|
1895
|
+
// When the latest instruction is not within a function we need
|
|
1896
|
+
// some default sourceReference to show to the user
|
|
1897
|
+
if constructor_revert_frame.source_reference.is_none() {
|
|
1898
|
+
let location = &contract.location;
|
|
1899
|
+
let file = location.file();
|
|
1900
|
+
let file = file.borrow();
|
|
1901
|
+
|
|
1902
|
+
let mut default_source_reference = SourceReference {
|
|
1903
|
+
function: Some(CONSTRUCTOR_FUNCTION_NAME.to_string()),
|
|
1904
|
+
contract: Some(contract.name.clone()),
|
|
1905
|
+
source_name: file.source_name.clone(),
|
|
1906
|
+
source_content: file.content.clone(),
|
|
1907
|
+
line: location.get_starting_line_number(),
|
|
1908
|
+
range: [location.offset, location.offset + location.length].to_vec(),
|
|
1909
|
+
};
|
|
1910
|
+
|
|
1911
|
+
if let Some(constructor) = &contract.constructor {
|
|
1912
|
+
default_source_reference.line = constructor.location.get_starting_line_number();
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
constructor_revert_frame.source_reference = Some(default_source_reference);
|
|
1916
|
+
} else {
|
|
1917
|
+
constructor_revert_frame =
|
|
1918
|
+
Self::solidity_0_6_3_correct_line_number(constructor_revert_frame);
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
return Ok(Some(constructor_revert_frame));
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
if let Some(prev_inst) = prev_inst {
|
|
1925
|
+
// We may as well just be in a function or modifier and just happen
|
|
1926
|
+
// to be at the last instruction of the runtime bytecode.
|
|
1927
|
+
// In this case we just return whatever the last mapped intruction
|
|
1928
|
+
// points to.
|
|
1929
|
+
let mut latest_instruction_revert_frame = Self::instruction_within_function_to_unmapped_solc_0_6_3_revert_error_stack_trace_entry(
|
|
1930
|
+
trace,
|
|
1931
|
+
prev_inst,
|
|
1932
|
+
|
|
1933
|
+
)?;
|
|
1934
|
+
|
|
1935
|
+
if latest_instruction_revert_frame.source_reference.is_some() {
|
|
1936
|
+
latest_instruction_revert_frame =
|
|
1937
|
+
Self::solidity_0_6_3_correct_line_number(latest_instruction_revert_frame);
|
|
1938
|
+
}
|
|
1939
|
+
return Ok(Some(latest_instruction_revert_frame));
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
Ok(None)
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
fn solidity_0_6_3_correct_line_number(
|
|
1946
|
+
mut revert_frame: UnmappedSolc063RevertErrorStackTraceEntry,
|
|
1947
|
+
) -> UnmappedSolc063RevertErrorStackTraceEntry {
|
|
1948
|
+
let Some(source_reference) = &mut revert_frame.source_reference else {
|
|
1949
|
+
return revert_frame;
|
|
1950
|
+
};
|
|
1951
|
+
|
|
1952
|
+
let lines: Vec<_> = source_reference.source_content.split('\n').collect();
|
|
1953
|
+
|
|
1954
|
+
let current_line = lines[source_reference.line as usize - 1];
|
|
1955
|
+
if current_line.contains("require") || current_line.contains("revert") {
|
|
1956
|
+
return revert_frame;
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
let next_lines = &lines
|
|
1960
|
+
.get(source_reference.line as usize..)
|
|
1961
|
+
.unwrap_or_default();
|
|
1962
|
+
let first_non_empty_line = next_lines.iter().position(|l| !l.trim().is_empty());
|
|
1963
|
+
|
|
1964
|
+
let Some(first_non_empty_line) = first_non_empty_line else {
|
|
1965
|
+
return revert_frame;
|
|
1966
|
+
};
|
|
1967
|
+
|
|
1968
|
+
let next_line = next_lines[first_non_empty_line];
|
|
1969
|
+
if next_line.contains("require") || next_line.contains("revert") {
|
|
1970
|
+
source_reference.line += 1 + first_non_empty_line as u32;
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
revert_frame
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
fn get_other_error_before_called_function_stack_trace_entry(
|
|
1977
|
+
trace: &CallMessageTrace,
|
|
1978
|
+
) -> napi::Result<OtherExecutionErrorStackTraceEntry> {
|
|
1979
|
+
let source_reference =
|
|
1980
|
+
Self::get_contract_start_without_function_source_reference(Either::A(trace))?;
|
|
1981
|
+
|
|
1982
|
+
Ok(OtherExecutionErrorStackTraceEntry {
|
|
1983
|
+
type_: StackTraceEntryTypeConst,
|
|
1984
|
+
source_reference: Some(source_reference),
|
|
1985
|
+
})
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
fn is_called_non_contract_account_error(
|
|
1989
|
+
trace: Either<&CallMessageTrace, &CreateMessageTrace>,
|
|
1990
|
+
) -> napi::Result<bool> {
|
|
1991
|
+
// We could change this to checking that the last valid location maps to a call,
|
|
1992
|
+
// but it's way more complex as we need to get the ast node from that
|
|
1993
|
+
// location.
|
|
1994
|
+
|
|
1995
|
+
let (bytecode, steps) = match &trace {
|
|
1996
|
+
Either::A(create) => (&create.bytecode, &create.steps),
|
|
1997
|
+
Either::B(call) => (&call.bytecode, &call.steps),
|
|
1998
|
+
};
|
|
1999
|
+
|
|
2000
|
+
let bytecode = bytecode
|
|
2001
|
+
.as_ref()
|
|
2002
|
+
.expect("JS code only accepted variants that had bytecode defined");
|
|
2003
|
+
|
|
2004
|
+
let last_index = Self::get_last_instruction_with_valid_location_step_index(trace)?;
|
|
2005
|
+
|
|
2006
|
+
let last_index = match last_index {
|
|
2007
|
+
None | Some(0) => return Ok(false),
|
|
2008
|
+
Some(last_index) => last_index as usize,
|
|
2009
|
+
};
|
|
2010
|
+
|
|
2011
|
+
let last_step = match &steps[last_index] {
|
|
2012
|
+
Either4::A(step) => step,
|
|
2013
|
+
_ => panic!("We know this is an EVM step"),
|
|
2014
|
+
};
|
|
2015
|
+
|
|
2016
|
+
let last_inst = bytecode.get_instruction(last_step.pc)?;
|
|
2017
|
+
|
|
2018
|
+
if last_inst.opcode != OpCode::ISZERO {
|
|
2019
|
+
return Ok(false);
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
let prev_step = match &steps[last_index - 1] {
|
|
2023
|
+
Either4::A(step) => step,
|
|
2024
|
+
_ => panic!("We know this is an EVM step"),
|
|
2025
|
+
};
|
|
2026
|
+
|
|
2027
|
+
let prev_inst = bytecode.get_instruction(prev_step.pc)?;
|
|
2028
|
+
|
|
2029
|
+
Ok(prev_inst.opcode == OpCode::EXTCODESIZE)
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
fn get_last_instruction_with_valid_location_step_index(
|
|
2033
|
+
trace: Either<&CallMessageTrace, &CreateMessageTrace>,
|
|
2034
|
+
) -> napi::Result<Option<u32>> {
|
|
2035
|
+
let (bytecode, steps) = match &trace {
|
|
2036
|
+
Either::A(create) => (&create.bytecode, &create.steps),
|
|
2037
|
+
Either::B(call) => (&call.bytecode, &call.steps),
|
|
2038
|
+
};
|
|
2039
|
+
|
|
2040
|
+
let bytecode = bytecode
|
|
2041
|
+
.as_ref()
|
|
2042
|
+
.expect("JS code only accepted variants that had bytecode defined");
|
|
2043
|
+
|
|
2044
|
+
for (i, step) in steps.iter().enumerate().rev() {
|
|
2045
|
+
let step = match step {
|
|
2046
|
+
Either4::A(step) => step,
|
|
2047
|
+
_ => return Ok(None),
|
|
2048
|
+
};
|
|
2049
|
+
|
|
2050
|
+
let inst = bytecode.get_instruction(step.pc)?;
|
|
2051
|
+
|
|
2052
|
+
if inst.location.is_some() {
|
|
2053
|
+
return Ok(Some(i as u32));
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
Ok(None)
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
fn get_last_instruction_with_valid_location<'a>(
|
|
2061
|
+
trace: Either<&'a CallMessageTrace, &'a CreateMessageTrace>,
|
|
2062
|
+
) -> napi::Result<Option<&'a Instruction>> {
|
|
2063
|
+
let last_location_index = Self::get_last_instruction_with_valid_location_step_index(trace)?;
|
|
2064
|
+
|
|
2065
|
+
let Some(last_location_index) = last_location_index else {
|
|
2066
|
+
return Ok(None);
|
|
2067
|
+
};
|
|
2068
|
+
|
|
2069
|
+
let (bytecode, steps) = match &trace {
|
|
2070
|
+
Either::A(create) => (&create.bytecode, &create.steps),
|
|
2071
|
+
Either::B(call) => (&call.bytecode, &call.steps),
|
|
2072
|
+
};
|
|
2073
|
+
|
|
2074
|
+
let bytecode = bytecode
|
|
2075
|
+
.as_ref()
|
|
2076
|
+
.expect("JS code only accepted variants that had bytecode defined");
|
|
2077
|
+
|
|
2078
|
+
match &steps.get(last_location_index as usize) {
|
|
2079
|
+
Some(Either4::A(step)) => {
|
|
2080
|
+
let inst = bytecode.get_instruction(step.pc)?;
|
|
2081
|
+
|
|
2082
|
+
Ok(Some(inst))
|
|
2083
|
+
}
|
|
2084
|
+
_ => Ok(None),
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
fn source_location_to_source_reference(
|
|
2090
|
+
bytecode: &Bytecode,
|
|
2091
|
+
location: Option<&SourceLocation>,
|
|
2092
|
+
) -> napi::Result<Option<SourceReference>> {
|
|
2093
|
+
let Some(location) = location else {
|
|
2094
|
+
return Ok(None);
|
|
2095
|
+
};
|
|
2096
|
+
let Some(func) = location.get_containing_function() else {
|
|
2097
|
+
return Ok(None);
|
|
2098
|
+
};
|
|
2099
|
+
|
|
2100
|
+
let func_name = match func.r#type {
|
|
2101
|
+
ContractFunctionType::Constructor => CONSTRUCTOR_FUNCTION_NAME.to_string(),
|
|
2102
|
+
ContractFunctionType::Fallback => FALLBACK_FUNCTION_NAME.to_string(),
|
|
2103
|
+
ContractFunctionType::Receive => RECEIVE_FUNCTION_NAME.to_string(),
|
|
2104
|
+
_ => func.name.clone(),
|
|
2105
|
+
};
|
|
2106
|
+
|
|
2107
|
+
let func_location_file = func.location.file();
|
|
2108
|
+
let func_location_file = func_location_file.borrow();
|
|
2109
|
+
|
|
2110
|
+
Ok(Some(SourceReference {
|
|
2111
|
+
function: Some(func_name.clone()),
|
|
2112
|
+
contract: if func.r#type == ContractFunctionType::FreeFunction {
|
|
2113
|
+
None
|
|
2114
|
+
} else {
|
|
2115
|
+
Some(bytecode.contract.borrow().name.clone())
|
|
2116
|
+
},
|
|
2117
|
+
source_name: func_location_file.source_name.clone(),
|
|
2118
|
+
source_content: func_location_file.content.clone(),
|
|
2119
|
+
line: location.get_starting_line_number(),
|
|
2120
|
+
range: [location.offset, location.offset + location.length].to_vec(),
|
|
2121
|
+
}))
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
pub fn instruction_to_callstack_stack_trace_entry(
|
|
2125
|
+
bytecode: &Bytecode,
|
|
2126
|
+
inst: &Instruction,
|
|
2127
|
+
) -> napi::Result<Either<CallstackEntryStackTraceEntry, InternalFunctionCallStackEntry>> {
|
|
2128
|
+
let contract = bytecode.contract.borrow();
|
|
2129
|
+
|
|
2130
|
+
// This means that a jump is made from within an internal solc function.
|
|
2131
|
+
// These are normally made from yul code, so they don't map to any Solidity
|
|
2132
|
+
// function
|
|
2133
|
+
let inst_location = match &inst.location {
|
|
2134
|
+
None => {
|
|
2135
|
+
let location = &contract.location;
|
|
2136
|
+
let file = location.file();
|
|
2137
|
+
let file = file.borrow();
|
|
2138
|
+
|
|
2139
|
+
return Ok(Either::B(InternalFunctionCallStackEntry {
|
|
2140
|
+
type_: StackTraceEntryTypeConst,
|
|
2141
|
+
pc: inst.pc,
|
|
2142
|
+
source_reference: SourceReference {
|
|
2143
|
+
source_name: file.source_name.clone(),
|
|
2144
|
+
source_content: file.content.clone(),
|
|
2145
|
+
contract: Some(contract.name.clone()),
|
|
2146
|
+
function: None,
|
|
2147
|
+
line: location.get_starting_line_number(),
|
|
2148
|
+
range: [location.offset, location.offset + location.length].to_vec(),
|
|
2149
|
+
},
|
|
2150
|
+
}));
|
|
2151
|
+
}
|
|
2152
|
+
Some(inst_location) => inst_location,
|
|
2153
|
+
};
|
|
2154
|
+
|
|
2155
|
+
if let Some(func) = inst_location.get_containing_function() {
|
|
2156
|
+
let source_reference = source_location_to_source_reference(bytecode, Some(inst_location))?
|
|
2157
|
+
.expect("Expected source reference to be defined");
|
|
2158
|
+
|
|
2159
|
+
return Ok(Either::A(CallstackEntryStackTraceEntry {
|
|
2160
|
+
type_: StackTraceEntryTypeConst,
|
|
2161
|
+
source_reference,
|
|
2162
|
+
function_type: func.r#type.into(),
|
|
2163
|
+
}));
|
|
2164
|
+
};
|
|
2165
|
+
|
|
2166
|
+
let file = inst_location.file();
|
|
2167
|
+
let file = file.borrow();
|
|
2168
|
+
|
|
2169
|
+
Ok(Either::A(CallstackEntryStackTraceEntry {
|
|
2170
|
+
type_: StackTraceEntryTypeConst,
|
|
2171
|
+
source_reference: SourceReference {
|
|
2172
|
+
function: None,
|
|
2173
|
+
contract: Some(contract.name.clone()),
|
|
2174
|
+
source_name: file.source_name.clone(),
|
|
2175
|
+
source_content: file.content.clone(),
|
|
2176
|
+
line: inst_location.get_starting_line_number(),
|
|
2177
|
+
range: [
|
|
2178
|
+
inst_location.offset,
|
|
2179
|
+
inst_location.offset + inst_location.length,
|
|
2180
|
+
]
|
|
2181
|
+
.to_vec(),
|
|
2182
|
+
},
|
|
2183
|
+
function_type: ContractFunctionType::Function.into(),
|
|
2184
|
+
}))
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
// Rewrite of `AbiHelpers.formatValues` from Hardhat
|
|
2188
|
+
fn format_dyn_sol_value(val: &DynSolValue) -> String {
|
|
2189
|
+
match val {
|
|
2190
|
+
// print nested values as [value1, value2, ...]
|
|
2191
|
+
DynSolValue::Array(items)
|
|
2192
|
+
| DynSolValue::Tuple(items)
|
|
2193
|
+
| DynSolValue::FixedArray(items)
|
|
2194
|
+
| DynSolValue::CustomStruct { tuple: items, .. } => {
|
|
2195
|
+
let mut result = String::from("[");
|
|
2196
|
+
for (i, val) in items.iter().enumerate() {
|
|
2197
|
+
if i > 0 {
|
|
2198
|
+
result.push_str(", ");
|
|
2199
|
+
}
|
|
2200
|
+
result.push_str(&format_dyn_sol_value(val));
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
result.push(']');
|
|
2204
|
+
result
|
|
2205
|
+
}
|
|
2206
|
+
// surround string values with quotes
|
|
2207
|
+
DynSolValue::String(s) => format!("\"{s}\""),
|
|
2208
|
+
|
|
2209
|
+
DynSolValue::Address(address) => format!("\"0x{address}\""),
|
|
2210
|
+
DynSolValue::Bytes(bytes) => format!("\"0x{}\"", hex::encode(bytes)),
|
|
2211
|
+
DynSolValue::FixedBytes(word, size) => {
|
|
2212
|
+
format!("\"0x{}\"", hex::encode(&word.0.as_slice()[..*size]))
|
|
2213
|
+
}
|
|
2214
|
+
DynSolValue::Bool(b) => b.to_string(),
|
|
2215
|
+
DynSolValue::Function(_) => "<function>".to_string(),
|
|
2216
|
+
DynSolValue::Int(int, _bits) => int.to_string(),
|
|
2217
|
+
DynSolValue::Uint(uint, _bits) => uint.to_string(),
|
|
2218
|
+
}
|
|
2219
|
+
}
|