1use std::collections::{BTreeMap, HashMap};
33
34use soldb_core::{TransactionTrace, Word as StackWord};
35use soldb_ethdebug::{
36 function_selector, parse_path, CodeGenerator, EthdebugInfo, FunctionExit, Instruction,
37 PathSegment, SourceLocation, StorageLayout,
38};
39
40use crate::condition::Value;
41use crate::decode::{Place, ValueReader};
42use crate::state::StorageWords;
43use crate::types::SourceTypes;
44use crate::{
45 decode_arguments, is_value_type, parse_source_functions, readable_parameter, ArgumentLayout,
46 ArgumentOrder, ByteRange, DebugLocation, DebugValue, DebugValueStatus, DebugVariable,
47 FrameArgument, FrameState, SourceFunction, SourceParam,
48};
49
50pub const INFERRED_LOCALS_WARNING: &str = "local variables are inferred from the legacy source \
52map and the stack layout of solc's legacy code generator, not from compiler-reported \
53variable locations";
54
55pub const INFERRED_LOCALS_NOTE: &str = "values can be wrong under the optimizer, and a \
57variable whose frame could not be placed shows as unavailable; ETHDebug variable \
58information will replace this once compilers emit it";
59
60const CALL_OPCODES: [&str; 4] = ["CALL", "CALLCODE", "DELEGATECALL", "STATICCALL"];
61
62#[derive(Debug, Clone, PartialEq, Eq)]
66pub struct ContractDebugInfo {
67 pub address: Option<String>,
69 pub name: String,
70 pub info: EthdebugInfo,
71 pub source_contents: BTreeMap<u64, String>,
72 pub functions: Vec<SourceFunction>,
73 pub types: SourceTypes,
76 pub storage_layout: Option<StorageLayout>,
79 pub code_generator: Option<CodeGenerator>,
83 line_starts: BTreeMap<u64, Vec<usize>>,
85 pc_index: HashMap<u64, usize>,
87 function_entries: HashMap<u64, usize>,
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95enum CodeEnvironment {
96 Create,
97 Call,
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub enum JumpMarker {
103 None,
104 Call,
106 Return,
108}
109
110impl ContractDebugInfo {
111 #[must_use]
115 pub fn new(
116 address: Option<&str>,
117 name: &str,
118 info: EthdebugInfo,
119 source_contents: BTreeMap<u64, String>,
120 ) -> Self {
121 let functions = source_contents
122 .iter()
123 .flat_map(|(source_id, source)| parse_source_functions(*source_id, source))
124 .collect::<Vec<SourceFunction>>();
125 let mut types = SourceTypes::default();
126 for (source_id, source) in &source_contents {
127 types.add_source(*source_id, source);
128 }
129 let line_starts = source_contents
130 .iter()
131 .map(|(source_id, source)| (*source_id, line_starts(source)))
132 .collect();
133 let pc_index = info
134 .instructions
135 .iter()
136 .enumerate()
137 .map(|(index, instruction)| (instruction.offset, index))
138 .collect();
139 let function_entries = functions
140 .iter()
141 .enumerate()
142 .filter_map(|(function_index, function)| {
143 info.instructions
144 .iter()
145 .filter(|instruction| instruction.mnemonic() == Some("JUMPDEST"))
146 .filter(|instruction| {
147 instruction.source_location().is_some_and(|location| {
148 location.source_id == function.source_id
149 && location.offset == function.declaration_start
150 })
151 })
152 .map(|instruction| instruction.offset)
153 .min()
154 .map(|pc| (pc, function_index))
155 })
156 .collect();
157 Self {
158 address: address.map(normalize_address),
159 name: name.to_owned(),
160 info,
161 source_contents,
162 functions,
163 types,
164 storage_layout: None,
165 code_generator: None,
166 line_starts,
167 pc_index,
168 function_entries,
169 }
170 }
171
172 #[must_use]
174 pub const fn with_code_generator(mut self, code_generator: Option<CodeGenerator>) -> Self {
175 self.code_generator = code_generator;
176 self
177 }
178
179 #[must_use]
183 pub fn with_storage_layout(mut self, storage_layout: Option<StorageLayout>) -> Self {
184 self.storage_layout = storage_layout.map(|mut layout| {
185 layout.enum_variants = self.types.enum_variants_by_name();
186 layout
187 });
188 self
189 }
190
191 fn code_environment(&self) -> Option<CodeEnvironment> {
194 match self.info.environment.as_str() {
195 "create" => Some(CodeEnvironment::Create),
196 "call" => Some(CodeEnvironment::Call),
197 _ => None,
198 }
199 }
200
201 #[must_use]
203 pub fn function_entry_at_pc(&self, pc: u64) -> Option<usize> {
204 self.function_entries.get(&pc).copied()
205 }
206
207 #[must_use]
209 pub fn is_jumpdest(&self, pc: u64) -> bool {
210 self.pc_index
211 .get(&pc)
212 .and_then(|index| self.info.instructions.get(*index))
213 .is_some_and(|instruction| instruction.mnemonic() == Some("JUMPDEST"))
214 }
215
216 #[must_use]
218 pub fn jump_marker_at_pc(&self, pc: u64) -> JumpMarker {
219 let Some(instruction) = self
220 .pc_index
221 .get(&pc)
222 .and_then(|index| self.info.instructions.get(*index))
223 else {
224 return JumpMarker::None;
225 };
226 if !instruction.function_invocations().is_empty() {
227 JumpMarker::Call
228 } else if instruction.function_exit() == Some(FunctionExit::Return) {
229 JumpMarker::Return
230 } else {
231 JumpMarker::None
232 }
233 }
234
235 #[must_use]
236 pub fn source_path(&self, source_id: u64) -> Option<&str> {
237 self.info.sources.get(&source_id).map(String::as_str)
238 }
239
240 #[must_use]
242 pub fn line_of(&self, source_id: u64, offset: u64) -> Option<u64> {
243 let starts = self.line_starts.get(&source_id)?;
244 let offset = usize::try_from(offset).ok()?;
245 Some(starts.partition_point(|start| *start <= offset) as u64)
246 }
247
248 #[must_use]
249 pub fn column_of(&self, source_id: u64, offset: u64) -> Option<u64> {
250 let starts = self.line_starts.get(&source_id)?;
251 let line = self.line_of(source_id, offset)?;
252 let line_start = *starts.get(usize::try_from(line).ok()?.checked_sub(1)?)?;
253 Some(offset.saturating_sub(line_start as u64) + 1)
254 }
255
256 #[must_use]
257 pub fn line_count(&self, source_id: u64) -> Option<u64> {
258 self.line_starts
259 .get(&source_id)
260 .map(|starts| starts.len() as u64)
261 }
262
263 #[must_use]
265 pub fn line_text(&self, source_id: u64, line: u64) -> Option<&str> {
266 let source = self.source_contents.get(&source_id)?;
267 let index = usize::try_from(line).ok()?.checked_sub(1)?;
268 source.lines().nth(index)
269 }
270
271 #[must_use]
273 pub fn location_at_pc(&self, pc: u64) -> Option<SourceLocation> {
274 let index = *self.pc_index.get(&pc)?;
275 self.info.instructions.get(index)?.source_location()
276 }
277
278 #[must_use]
280 pub fn instruction_at_pc(&self, pc: u64) -> Option<&Instruction> {
281 self.info.instructions.get(*self.pc_index.get(&pc)?)
282 }
283
284 #[must_use]
286 pub fn modifier_depth_at_pc(&self, pc: u64) -> Option<i64> {
287 let index = *self.pc_index.get(&pc)?;
288 self.info.instructions.get(index)?.modifier_depth()
289 }
290
291 #[must_use]
293 pub fn function_for_location(&self, location: &SourceLocation) -> Option<usize> {
294 self.functions
295 .iter()
296 .enumerate()
297 .filter(|(_, function)| {
298 function.source_id == location.source_id
299 && function.declaration_start <= location.offset
300 && location.offset <= function.body_end
301 })
302 .min_by_key(|(_, function)| {
303 function.body_end.saturating_sub(function.declaration_start)
304 })
305 .map(|(index, _)| index)
306 }
307
308 #[must_use]
309 pub fn function_at_pc(&self, pc: u64) -> Option<&SourceFunction> {
310 let location = self.location_at_pc(pc)?;
311 self.function_for_location(&location)
312 .and_then(|index| self.functions.get(index))
313 }
314
315 #[must_use]
322 pub fn effective_line(&self, source_id: u64, line: u64) -> Option<u64> {
323 if line == 0 || line > self.line_count(source_id)? {
324 return None;
325 }
326 let mut narrowest = None::<(u64, u64)>;
327 for instruction in &self.info.instructions {
328 let Some(location) = instruction.source_location() else {
329 continue;
330 };
331 if location.source_id != source_id {
332 continue;
333 }
334 let start = self.line_of(source_id, location.offset)?;
335 let end = self.line_of(
336 source_id,
337 location
338 .offset
339 .saturating_add(location.length.saturating_sub(1)),
340 )?;
341 if start > line || line > end {
342 continue;
343 }
344 if start == line {
345 return Some(line);
346 }
347 if narrowest.is_none_or(|(length, _)| location.length < length) {
348 narrowest = Some((location.length, start));
349 }
350 }
351 narrowest.map(|(_, start)| start)
352 }
353}
354
355#[must_use]
358pub fn normalize_address(value: &str) -> String {
359 let hex = value.trim_start_matches("0x").trim_start_matches("0X");
360 format!("0x{}", hex.to_ascii_lowercase())
361}
362
363#[must_use]
367pub fn source_path_matches(source_path: &str, requested: &str) -> bool {
368 fn ends_with_path(longer: &str, shorter: &str) -> bool {
369 longer.ends_with(shorter)
370 && longer
371 .as_bytes()
372 .get(longer.len() - shorter.len() - 1)
373 .is_none_or(|byte| *byte == b'/')
374 }
375 fn file_name(path: &str) -> &str {
376 path.rsplit('/').next().unwrap_or(path)
377 }
378 source_path == requested
379 || ends_with_path(source_path, requested)
380 || ends_with_path(requested, source_path)
381 || (!requested.is_empty() && file_name(source_path) == file_name(requested))
382}
383
384fn line_starts(source: &str) -> Vec<usize> {
385 let mut starts = vec![0];
386 starts.extend(
387 source
388 .bytes()
389 .enumerate()
390 .filter(|(_, byte)| *byte == b'\n')
391 .map(|(index, _)| index + 1),
392 );
393 starts
394}
395
396#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
398pub struct LineKey {
399 pub contract: usize,
400 pub source_id: u64,
401 pub line: u64,
402}
403
404#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
406pub struct FunctionId {
407 pub contract: usize,
408 pub function: usize,
409}
410
411#[derive(Debug, Clone, PartialEq, Eq)]
413pub struct StepLocation {
414 pub key: LineKey,
415 pub generated: bool,
418 pub contract_name: String,
419 pub path: String,
420 pub offset: u64,
421 pub length: u64,
422 pub line: u64,
423 pub column: u64,
424 pub function: Option<FunctionId>,
425 pub function_name: Option<String>,
426}
427
428#[derive(Debug, Clone, PartialEq, Eq)]
430pub struct ResolvedLine {
431 pub key: LineKey,
432 pub path: String,
433 pub requested_line: u64,
435}
436
437#[derive(Debug, Clone, PartialEq, Eq)]
439pub struct ResolvedFunction {
440 pub id: FunctionId,
441 pub name: String,
442 pub contract_name: String,
443 pub path: String,
444 pub line: u64,
445}
446
447#[derive(Debug, Clone, PartialEq, Eq)]
449pub struct Frame {
450 pub depth: u32,
452 pub external: bool,
455 pub address: Option<String>,
456 pub contract_name: Option<String>,
457 pub function_name: Option<String>,
458 pub entry_step: usize,
460 pub step: usize,
463 pub pc: u64,
464 pub location: Option<StepLocation>,
465 pub arguments: Vec<FrameArgument>,
469}
470
471#[derive(Debug, Clone, PartialEq, Eq)]
473pub struct SourceListing {
474 pub path: String,
475 pub current_line: u64,
476 pub lines: Vec<(u64, String)>,
478}
479
480#[derive(Debug, Clone, Copy, PartialEq, Eq)]
481struct LocationRef {
482 key: LineKey,
483 offset: u64,
484 length: u64,
485 function: Option<usize>,
486}
487
488#[derive(Debug, Clone, Copy, PartialEq, Eq)]
489struct StepInfo {
490 location: Option<LocationRef>,
493 generated: bool,
495 key: Option<LineKey>,
498 contract: Option<usize>,
500 address: Option<usize>,
502 storage: Option<usize>,
505 evm_depth: u32,
507 frame_depth: u32,
509 frame_entry: bool,
511 line_start: bool,
514}
515
516#[derive(Debug, Clone, PartialEq, Eq)]
518pub struct StepMap {
519 contracts: Vec<ContractDebugInfo>,
520 steps: Vec<StepInfo>,
521 addresses: Vec<String>,
522 pcs: Vec<u64>,
523 reverted: Vec<(usize, usize)>,
526 argument_layouts: Vec<Evidence>,
529 locals_support: Vec<Result<(), &'static str>>,
531 variable_layouts: Vec<Vec<InferredVariable>>,
534 step_variables: Vec<u32>,
536}
537
538const NO_LAYOUT: u32 = u32::MAX;
539const INLINED_LAYOUT: u32 = u32::MAX - 1;
541
542#[derive(Debug, Clone, Copy, PartialEq, Eq)]
544pub enum VariableKind {
545 Parameter,
546 Return,
547 Local,
548}
549
550#[derive(Debug, Clone, PartialEq, Eq)]
552pub struct InferredVariable {
553 pub name: String,
554 pub ty: String,
556 pub kind: VariableKind,
557 pub slot: Option<usize>,
560 pub words: usize,
563}
564
565#[derive(Debug, Clone, Copy, PartialEq, Eq)]
567pub enum LocalsStatus<'a> {
568 Inferred(&'a [InferredVariable]),
570 Unavailable(&'static str),
572}
573
574#[derive(Debug, Clone, Copy, PartialEq, Eq)]
578enum Evidence {
579 Unknown,
581 Contradicted,
583 TopWords,
585 Ordered(ArgumentOrder),
587}
588
589impl Evidence {
590 fn merge(self, other: Self) -> Self {
593 match (self, other) {
594 (Self::Contradicted, _) | (_, Self::Contradicted) => Self::Contradicted,
595 (Self::Ordered(order), _) | (_, Self::Ordered(order)) => Self::Ordered(order),
596 (Self::TopWords, _) | (_, Self::TopWords) => Self::TopWords,
597 _ => Self::Unknown,
598 }
599 }
600
601 fn layout(self) -> Option<ArgumentLayout> {
602 match self {
603 Self::Ordered(order) => Some(ArgumentLayout::Ordered(order)),
604 Self::TopWords => Some(ArgumentLayout::TopWords),
605 Self::Unknown | Self::Contradicted => None,
606 }
607 }
608}
609
610#[derive(Debug, Clone, Copy)]
612struct FramedStep {
613 evm_depth: u32,
614 frame_id: u32,
615 contract: Option<usize>,
616 address: Option<usize>,
617 storage: Option<usize>,
618 location: Option<LocationRef>,
619 entry: Option<usize>,
621 marker: JumpMarker,
623}
624
625struct EvmFrame {
626 id: u32,
627 contract: Option<usize>,
628 address: Option<usize>,
629 storage: Option<usize>,
631 entry_step: usize,
633}
634
635struct FrameEntered {
637 address: Option<String>,
640 environment: CodeEnvironment,
641 delegated: bool,
643}
644
645impl FrameEntered {
646 fn recorded(trace: &TransactionTrace, step: usize) -> Option<Self> {
650 let spans_steps = |entry: Option<usize>, exit: Option<usize>| {
651 entry == Some(step) && exit.is_none_or(|exit| exit > step)
652 };
653 let artifacts = &trace.artifacts;
654 if let Some(call) = artifacts
655 .calls
656 .iter()
657 .find(|call| spans_steps(call.entry_step, call.exit_step))
658 {
659 return Some(Self {
660 address: Some(call.bytecode_address.clone()),
661 environment: CodeEnvironment::Call,
662 delegated: matches!(call.call_type.as_str(), "DELEGATECALL" | "CALLCODE"),
663 });
664 }
665 let creation = artifacts
666 .creations
667 .iter()
668 .find(|creation| spans_steps(creation.entry_step, creation.exit_step))?;
669 Some(Self {
670 address: creation.address.clone(),
671 environment: CodeEnvironment::Create,
672 delegated: false,
673 })
674 }
675
676 fn from_call(trace: &TransactionTrace, caller: Option<usize>) -> Self {
678 let op = caller.map(|caller| &*trace.steps[caller].op);
679 Self {
680 address: caller.and_then(|caller| call_target(&trace.steps[caller])),
681 environment: if matches!(op, Some("CREATE" | "CREATE2")) {
682 CodeEnvironment::Create
683 } else {
684 CodeEnvironment::Call
685 },
686 delegated: matches!(op, Some("DELEGATECALL" | "CALLCODE")),
687 }
688 }
689}
690
691#[derive(Debug, Clone, PartialEq, Eq)]
695struct FrameEntry {
696 function: Option<usize>,
697 return_pc: Option<u64>,
702 variables: Option<FrameVariables>,
705}
706
707#[derive(Debug, Clone, PartialEq, Eq)]
723struct FrameVariables {
724 params_base: Option<usize>,
726 returns: Vec<Option<usize>>,
728 locals_base: Option<usize>,
730 body_seen: bool,
732 layout: Option<u32>,
735 modifier_slots: Option<usize>,
739 inlined: bool,
744}
745
746impl FrameVariables {
747 fn new(
748 function: &SourceFunction,
749 params_base: Option<usize>,
750 modifier_slots: Option<usize>,
751 ) -> Self {
752 Self {
753 params_base,
754 returns: vec![None; function.returns.len()],
755 locals_base: None,
756 body_seen: false,
757 layout: None,
758 modifier_slots,
759 inlined: false,
760 }
761 }
762
763 fn place_from_parameters(&mut self, function: &SourceFunction) {
766 let Some(base) = self.params_base else {
767 return;
768 };
769 let returns_base = base + parameter_slots(&function.params);
770 for (index, slot) in self.returns.iter_mut().enumerate() {
771 slot.get_or_insert(returns_base + index);
772 }
773 if self.locals_base.is_none() {
774 if let Some(modifier_slots) = self.modifier_slots {
775 self.locals_base = Some(returns_base + function.returns.len() + modifier_slots);
776 }
777 }
778 }
779
780 fn track(
783 &mut self,
784 function: &SourceFunction,
785 span: ByteRange,
786 height: usize,
787 layouts: &mut Vec<Vec<InferredVariable>>,
788 ) -> u32 {
789 if self.inlined {
790 self.layout = Some(INLINED_LAYOUT);
791 return INLINED_LAYOUT;
792 }
793 let length = span.end - span.start;
794 let param_slots = parameter_slots(&function.params);
795 self.place_from_parameters(function);
797 let at_beginning = function
802 .placeholder
803 .is_none_or(|placeholder| span.start < placeholder);
804 if !self.body_seen
805 && at_beginning
806 && span.start >= function.body_start
807 && span.start < function.body_end
808 {
809 self.body_seen = true;
810 if self.locals_base.is_none() {
811 self.locals_base = Some(height);
812 }
813 if self.params_base.is_none() && !function.has_modifiers {
814 self.params_base = height.checked_sub(param_slots + function.returns.len());
815 }
816 }
817 for (index, parameter) in function.returns.iter().enumerate() {
820 if self.returns[index].is_none() && parameter.declaration.covers(span.start, length) {
821 self.returns[index] = Some(height);
822 if index == 0 && self.params_base.is_none() {
823 self.params_base = height.checked_sub(param_slots);
824 }
825 }
826 }
827 if self.locals_base.is_none() {
830 if let Some((index, _)) = function
831 .locals
832 .iter()
833 .enumerate()
834 .find(|(_, local)| local.statement.covers(span.start, length))
835 {
836 self.locals_base = height.checked_sub(live_index(function, index, span.start));
837 }
838 }
839 self.place_from_parameters(function);
841
842 let mut layout = Vec::new();
843 if let Some(base) = self.params_base {
844 let mut slot = base;
845 for parameter in &function.params {
846 let size = parameter_stack_slots(parameter);
847 layout.push(InferredVariable {
848 name: parameter.name.clone(),
849 ty: match ¶meter.location {
850 Some(location) => format!("{} {location}", parameter.ty),
851 None => parameter.ty.clone(),
852 },
853 kind: VariableKind::Parameter,
854 slot: Some(slot),
855 words: size,
856 });
857 slot += size;
858 }
859 }
860 for (index, parameter) in function.returns.iter().enumerate() {
861 layout.push(InferredVariable {
862 name: parameter.name.clone(),
863 ty: parameter.declared_type(),
864 kind: VariableKind::Return,
865 slot: self.returns[index],
866 words: 1,
867 });
868 }
869 for (index, local) in function.locals.iter().enumerate() {
870 if local.scope.contains(span.start) {
871 layout.push(InferredVariable {
872 name: local.name.clone(),
873 ty: local.declared_type(),
874 kind: VariableKind::Local,
875 slot: self
876 .locals_base
877 .map(|base| base + live_index(function, index, span.start)),
878 words: 1,
879 });
880 }
881 }
882 let id = match self.layout {
883 Some(id) if layouts[id as usize] == layout => id,
884 _ => {
885 layouts.push(layout);
886 (layouts.len() - 1) as u32
887 }
888 };
889 self.layout = Some(id);
890 id
891 }
892}
893
894fn live_index(function: &SourceFunction, index: usize, offset: u64) -> usize {
897 let declaration = function.locals[index].declaration.start;
898 function
899 .locals
900 .iter()
901 .filter(|other| other.scope.contains(offset) && other.declaration.start < declaration)
902 .count()
903}
904
905fn calldata_for_step(trace: &TransactionTrace, step: usize) -> &str {
908 let root_depth = trace.steps.first().map_or(0, |first| first.depth);
909 let depth = trace
910 .steps
911 .get(step)
912 .map_or(root_depth, |current| current.depth);
913 if depth == root_depth {
914 return &trace.input_data;
915 }
916 trace
917 .artifacts
918 .calls
919 .iter()
920 .filter(|call| {
921 call.entry_step.is_some_and(|entry| entry <= step)
922 && call.exit_step.is_none_or(|exit| step < exit)
923 })
924 .max_by_key(|call| call.depth)
925 .map_or("", |call| call.input.as_str())
926}
927
928fn parameter_stack_slots(parameter: &SourceParam) -> usize {
932 let dynamic =
933 parameter.ty == "bytes" || parameter.ty == "string" || parameter.ty.ends_with("[]");
934 if parameter.location.as_deref() == Some("calldata") && dynamic
935 || parameter.ty.starts_with("function")
936 {
937 2
938 } else {
939 1
940 }
941}
942
943fn parameter_slots(parameters: &[SourceParam]) -> usize {
944 parameters.iter().map(parameter_stack_slots).sum()
945}
946
947fn modifier_slots(
952 contract: &ContractDebugInfo,
953 function: &SourceFunction,
954 count: usize,
955) -> Option<usize> {
956 let mut slots = 0;
957 for name in function.modifiers.iter().take(count) {
958 let modifier = contract.functions.iter().find(|candidate| {
959 candidate.placeholder.is_some()
960 && candidate.name == *name
961 && candidate.source_id == function.source_id
962 });
963 let modifier = modifier.or_else(|| {
964 contract
965 .functions
966 .iter()
967 .find(|candidate| candidate.placeholder.is_some() && candidate.name == *name)
968 })?;
969 let placeholder = modifier.placeholder?;
970 let live = modifier
971 .locals
972 .iter()
973 .filter(|local| {
974 local.declaration.start < placeholder && local.scope.contains(placeholder)
975 })
976 .count();
977 slots += parameter_slots(&modifier.params) + live;
978 }
979 Some(slots)
980}
981
982fn locals_support(contract: &ContractDebugInfo, evidence: Evidence) -> Result<(), &'static str> {
984 match (contract.code_generator, evidence.layout()) {
985 (_, Some(ArgumentLayout::Ordered(ArgumentOrder::FirstOnTop))) => Err(
986 "this trace shows the via-IR calling convention, whose stack layout cannot be \
987 recovered without compiler-reported variable locations",
988 ),
989 (Some(CodeGenerator::ViaIr), _) => Err(
990 "this contract was compiled through the via-IR pipeline, whose stack layout cannot \
991 be recovered without compiler-reported variable locations",
992 ),
993 (Some(CodeGenerator::Legacy), _) | (None, Some(ArgumentLayout::Ordered(_))) => Ok(()),
994 (None, _) => Err(
995 "the code generator is not known; only solc's legacy pipeline keeps variables at \
996 fixed stack slots",
997 ),
998 }
999}
1000
1001#[derive(Default)]
1003struct InternalFrame {
1004 functions: Vec<FrameEntry>,
1006 statement: Option<LocationRef>,
1008 remembered: HashMap<usize, FrameVariables>,
1011}
1012
1013impl InternalFrame {
1014 fn active(&self) -> Option<usize> {
1016 self.functions.iter().rev().find_map(|entry| entry.function)
1017 }
1018
1019 fn is_active(&self, function: usize) -> bool {
1020 self.active() == Some(function)
1021 }
1022
1023 fn push_function(
1024 &mut self,
1025 function: usize,
1026 return_pc: Option<u64>,
1027 variables: Option<FrameVariables>,
1028 ) {
1029 self.functions.push(FrameEntry {
1030 function: Some(function),
1031 return_pc,
1032 variables,
1033 });
1034 }
1035
1036 fn push_placeholder(&mut self) {
1037 self.functions.push(FrameEntry {
1038 function: None,
1039 return_pc: None,
1040 variables: None,
1041 });
1042 }
1043
1044 fn returns_to(&self, pc: u64) -> bool {
1046 self.functions
1047 .last()
1048 .is_some_and(|entry| entry.return_pc == Some(pc))
1049 }
1050}
1051
1052#[allow(clippy::too_many_arguments)]
1061fn place_at_entry_landing(
1062 frame: &mut InternalFrame,
1063 contract: &ContractDebugInfo,
1064 function: usize,
1065 trace: &TransactionTrace,
1066 index: usize,
1067 own: LocationRef,
1068 height: usize,
1069 from_call: bool,
1070) {
1071 let Some(declared) = contract.functions.get(function) else {
1072 return;
1073 };
1074 let at_declaration =
1075 contract.is_jumpdest(trace.steps[index].pc) && own.offset == declared.declaration_start;
1076 let from_dispatcher =
1077 !from_call || (index > 0 && framed_declaration(trace, contract, index - 1, declared));
1078 if !at_declaration || !from_dispatcher {
1079 return;
1080 }
1081 let entry = frame
1082 .functions
1083 .iter_mut()
1084 .rev()
1085 .find(|entry| entry.function == Some(function));
1086 let Some(variables) = entry.and_then(|entry| entry.variables.as_mut()) else {
1087 return;
1088 };
1089 if variables.params_base.is_some() || variables.body_seen {
1090 return;
1091 }
1092 let stack = trace.steps[index].snapshot_ref().stack;
1093 let calldata = calldata_for_step(trace, index);
1094 if argument_evidence(declared, calldata, stack) == Evidence::Contradicted {
1095 return;
1096 }
1097 variables.params_base = height.checked_sub(parameter_slots(&declared.params));
1098}
1099
1100fn framed_declaration(
1103 trace: &TransactionTrace,
1104 contract: &ContractDebugInfo,
1105 index: usize,
1106 function: &SourceFunction,
1107) -> bool {
1108 if &*trace.steps[index].op != "JUMP" {
1109 return false;
1110 }
1111 contract
1112 .location_at_pc(trace.steps[index].pc)
1113 .is_some_and(|location| {
1114 location.source_id == function.source_id
1115 && location.offset == function.declaration_start
1116 })
1117}
1118
1119fn modifier_base(
1123 frame: &InternalFrame,
1124 contract: &ContractDebugInfo,
1125 function: usize,
1126) -> Option<usize> {
1127 let modifier = contract.functions.get(function)?;
1128 modifier.placeholder?;
1129 let parent = frame
1130 .functions
1131 .iter()
1132 .rev()
1133 .find(|entry| entry.function.is_some())?;
1134 let parent_function = contract.functions.get(parent.function?)?;
1135 let position = parent_function
1136 .modifiers
1137 .iter()
1138 .position(|name| *name == modifier.name)?;
1139 let parent_base = parent.variables.as_ref()?.params_base?;
1140 let before = modifier_slots(contract, parent_function, position)?;
1141 Some(
1142 parent_base
1143 + parameter_slots(&parent_function.params)
1144 + parent_function.returns.len()
1145 + before,
1146 )
1147}
1148
1149impl StepMap {
1150 #[must_use]
1157 pub fn new(trace: &TransactionTrace, contracts: Vec<ContractDebugInfo>) -> Self {
1158 Self::build(trace, contracts, true)
1159 }
1160
1161 #[must_use]
1166 pub fn for_debug_diff(trace: &TransactionTrace, contracts: Vec<ContractDebugInfo>) -> Self {
1167 Self::build(trace, contracts, false)
1168 }
1169
1170 fn build(trace: &TransactionTrace, contracts: Vec<ContractDebugInfo>, smooth: bool) -> Self {
1171 let mut addresses = Vec::<String>::new();
1172 let mut address_index = HashMap::<String, usize>::new();
1173 let mut intern = |address: &str| -> usize {
1174 let address = normalize_address(address);
1175 *address_index.entry(address.clone()).or_insert_with(|| {
1176 addresses.push(address);
1177 addresses.len() - 1
1178 })
1179 };
1180 let contract_for =
1186 |address: Option<&str>, environment: CodeEnvironment, root: bool| -> Option<usize> {
1187 let address = address.map(normalize_address);
1188 if let Some(address) = &address {
1189 let mut at_address = contracts
1190 .iter()
1191 .enumerate()
1192 .filter(|(_, contract)| contract.address.as_deref() == Some(address));
1193 let exact = at_address.clone().find(|(_, contract)| {
1194 contract
1195 .code_environment()
1196 .is_none_or(|candidate| candidate == environment)
1197 });
1198 if let Some((index, _)) = exact.or_else(|| at_address.next()) {
1199 return Some(index);
1200 }
1201 }
1202 (root && contracts.len() == 1).then_some(0)
1203 };
1204
1205 let root_address = trace
1207 .to_addr
1208 .as_deref()
1209 .or(trace.contract_address.as_deref());
1210 let root_environment = if trace.to_addr.is_none() && trace.contract_address.is_some() {
1211 CodeEnvironment::Create
1212 } else {
1213 CodeEnvironment::Call
1214 };
1215 let root_depth = trace.steps.first().map_or(0, |step| step.depth);
1216 let mut next_frame_id = 1_u32;
1217 let root_storage = root_address.map(&mut intern);
1218 let mut evm_frames = vec![EvmFrame {
1219 id: 0,
1220 contract: contract_for(root_address, root_environment, true),
1221 address: root_storage,
1222 storage: root_storage,
1223 entry_step: 0,
1224 }];
1225 let mut reverted = Vec::new();
1226 let mut framed = Vec::with_capacity(trace.steps.len());
1227 for (index, step) in trace.steps.iter().enumerate() {
1228 let evm_depth = step.depth.saturating_sub(root_depth) as usize;
1229 evm_frames.truncate(evm_depth + 1);
1230 while evm_frames.len() <= evm_depth {
1231 let call = (evm_frames.len() == evm_depth)
1236 .then(|| index.checked_sub(1))
1237 .flatten();
1238 let entered = FrameEntered::recorded(trace, index)
1239 .filter(|_| evm_frames.len() == evm_depth)
1240 .unwrap_or_else(|| FrameEntered::from_call(trace, call));
1241 let address = entered.address.as_deref().map(&mut intern);
1242 let storage = if entered.delegated {
1245 evm_frames.last().and_then(|frame| frame.storage)
1246 } else {
1247 address
1248 };
1249 evm_frames.push(EvmFrame {
1250 id: next_frame_id,
1251 contract: contract_for(entered.address.as_deref(), entered.environment, false),
1252 address,
1253 storage,
1254 entry_step: index,
1255 });
1256 next_frame_id += 1;
1257 }
1258 if &*step.op == "REVERT" || step.error.is_some() {
1259 let entry_step = evm_frames
1260 .last()
1261 .expect("the root frame is never popped")
1262 .entry_step;
1263 reverted.push((entry_step, index));
1264 }
1265 let frame = evm_frames.last().expect("the root frame is never popped");
1266 let location = frame.contract.and_then(|contract_index| {
1267 let contract = &contracts[contract_index];
1268 let location = contract.location_at_pc(step.pc)?;
1269 let line = contract.line_of(location.source_id, location.offset)?;
1270 Some(LocationRef {
1271 key: LineKey {
1272 contract: contract_index,
1273 source_id: location.source_id,
1274 line,
1275 },
1276 offset: location.offset,
1277 length: location.length,
1278 function: contract.function_for_location(&location),
1279 })
1280 });
1281 let contract = frame.contract.map(|index| &contracts[index]);
1282 framed.push(FramedStep {
1283 evm_depth: evm_depth as u32,
1284 frame_id: frame.id,
1285 contract: frame.contract,
1286 address: frame.address,
1287 storage: frame.storage,
1288 location,
1289 entry: contract.and_then(|contract| contract.function_entry_at_pc(step.pc)),
1290 marker: contract.map_or(JumpMarker::None, |contract| {
1291 contract.jump_marker_at_pc(step.pc)
1292 }),
1293 });
1294 }
1295
1296 let structured = |contract: Option<usize>| {
1300 contract.is_some_and(|index| !contracts[index].functions.is_empty())
1301 };
1302 let mut function_follows = std::collections::HashSet::<u32>::new();
1303 let mut epilogue = vec![false; framed.len()];
1304 for (index, step) in framed.iter().enumerate().rev() {
1305 match step.location.map(|location| location.function) {
1306 Some(Some(_)) => {
1307 function_follows.insert(step.frame_id);
1308 }
1309 Some(None) if structured(step.contract) => {
1310 epilogue[index] = !function_follows.contains(&step.frame_id);
1311 }
1312 _ => {}
1313 }
1314 }
1315
1316 let argument_layouts = prove_argument_layouts(trace, &contracts, &framed);
1317 let locals_support = contracts
1318 .iter()
1319 .zip(&argument_layouts)
1320 .map(|(contract, evidence)| locals_support(contract, *evidence))
1321 .collect::<Vec<_>>();
1322
1323 let mut internal = Vec::<InternalFrame>::new();
1326 let mut steps = Vec::with_capacity(framed.len());
1327 let mut pcs = Vec::with_capacity(framed.len());
1328 let mut variable_layouts = Vec::<Vec<InferredVariable>>::new();
1329 let mut step_variables = Vec::with_capacity(framed.len());
1330 for (index, step) in framed.iter().enumerate() {
1331 let height = trace.steps[index].snapshot_ref().stack.len();
1332 let frame_variables = |function: usize| -> Option<FrameVariables> {
1337 let contract_index = step.contract?;
1338 locals_support[contract_index].ok()?;
1339 let contract = &contracts[contract_index];
1340 let function = contract.functions.get(function)?;
1341 let landed_from_function = index > 0
1342 && framed[index - 1].evm_depth == step.evm_depth
1343 && &*trace.steps[index - 1].op == "JUMP"
1344 && framed[index - 1]
1345 .location
1346 .is_some_and(|location| location.function.is_some());
1347 let params_base = landed_from_function
1348 .then(|| height.checked_sub(parameter_slots(&function.params)))
1349 .flatten();
1350 Some(FrameVariables::new(
1351 function,
1352 params_base,
1353 modifier_slots(contract, function, function.modifiers.len()),
1354 ))
1355 };
1356 let contract_info = step.contract.map(|index| &contracts[index]);
1357 let evm_depth = step.evm_depth as usize;
1358 let mut frame_entry = false;
1359 internal.truncate(evm_depth + 1);
1360 while internal.len() <= evm_depth {
1361 internal.push(InternalFrame::default());
1362 frame_entry = true;
1363 }
1364 let frame = internal.last_mut().expect("the root frame is never popped");
1365
1366 let landed_by_jump = index > 0
1369 && framed[index - 1].evm_depth == step.evm_depth
1370 && &*trace.steps[index - 1].op == "JUMP";
1371 let from_function_code = index > 0
1374 && framed[index - 1].evm_depth == step.evm_depth
1375 && framed[index - 1].location.is_some_and(|location| {
1376 location.function.is_some()
1377 && location.function != step.location.and_then(|own| own.function)
1378 });
1379 let previous_marker = if landed_by_jump {
1380 framed[index - 1].marker
1381 } else {
1382 JumpMarker::None
1383 };
1384 let structured = structured(step.contract);
1385 let mut generated = false;
1386 let mut pending_call = false;
1390 if landed_by_jump {
1391 let pc = trace.steps[index].pc;
1392 match previous_marker {
1393 JumpMarker::Call => pending_call = true,
1394 JumpMarker::Return => {
1395 frame.functions.pop();
1396 }
1397 JumpMarker::None if frame.returns_to(pc) => {
1398 frame.functions.pop();
1399 }
1400 JumpMarker::None => {}
1401 }
1402 }
1403 let return_address = |function: usize| -> Option<u64> {
1406 if !landed_by_jump {
1407 return None;
1408 }
1409 let contract = &contracts[step.contract?];
1410 let arguments = contract.functions.get(function)?.params.len();
1411 let stack = trace.steps[index - 1].snapshot_ref().stack;
1412 let word = stack.get(stack.len().checked_sub(2 + arguments)?)?;
1413 let pc = u64::from_str_radix(word.trim_start_matches("0x"), 16).ok()?;
1414 contract.is_jumpdest(pc).then_some(pc)
1415 };
1416 let (location, key) = match step.location {
1417 None => {
1418 if pending_call {
1421 frame.push_placeholder();
1422 }
1423 (None, None)
1424 }
1425 Some(own) if !structured => {
1426 frame.statement = Some(own);
1428 (Some(own), Some(own.key))
1429 }
1430 Some(own) => match own.function {
1431 Some(function) => {
1432 let entering = landed_by_jump && step.entry == Some(function);
1433 if landed_by_jump
1437 && previous_marker == JumpMarker::Return
1438 && frame.is_active(function)
1439 {
1440 if let Some(contract) = contract_info {
1441 place_at_entry_landing(
1442 frame, contract, function, trace, index, own, height, false,
1443 );
1444 }
1445 }
1446 if entering || (pending_call && !frame.is_active(function)) {
1447 frame.push_function(
1450 function,
1451 return_address(function),
1452 frame_variables(function),
1453 );
1454 frame_entry = true;
1455 } else if pending_call {
1456 if let Some(contract) = contract_info {
1462 place_at_entry_landing(
1463 frame, contract, function, trace, index, own, height, true,
1464 );
1465 }
1466 frame.push_placeholder();
1467 } else if !frame.is_active(function) {
1468 match frame
1469 .functions
1470 .iter()
1471 .rposition(|entry| entry.function == Some(function))
1472 {
1473 Some(position) => {
1474 for left in frame.functions.drain(position + 1..) {
1477 if let (Some(function), Some(variables)) =
1478 (left.function, left.variables)
1479 {
1480 frame.remembered.insert(function, variables);
1481 }
1482 }
1483 }
1484 None => {
1485 let mut variables = frame.remembered.remove(&function);
1486 if variables.is_none() {
1487 variables = frame_variables(function);
1488 if let (Some(variables), Some(contract)) =
1489 (variables.as_mut(), contract_info)
1490 {
1491 let declared = &contract.functions[function];
1492 if declared.placeholder.is_some() {
1493 if variables.params_base.is_none() {
1499 variables.params_base =
1500 modifier_base(frame, contract, function);
1501 }
1502 } else if declared.name != "constructor"
1503 && from_function_code
1504 && step.entry != Some(function)
1505 {
1506 variables.inlined = true;
1510 }
1511 }
1512 }
1513 frame.push_function(function, None, variables);
1514 frame_entry = true;
1515 }
1516 }
1517 }
1518 frame.statement = Some(own);
1519 (Some(own), Some(own.key))
1520 }
1521 None if epilogue[index] => {
1522 frame.functions.clear();
1523 frame.statement = None;
1524 (Some(own), None)
1525 }
1526 None => {
1527 if pending_call {
1528 frame.push_placeholder();
1529 }
1530 match frame.statement {
1531 Some(statement) => {
1532 generated = true;
1533 (Some(statement), Some(statement.key))
1534 }
1535 None => (Some(own), None),
1538 }
1539 }
1540 },
1541 };
1542 let layout = {
1545 let frame = internal.last_mut().expect("the root frame is never popped");
1546 let contract = step.contract.map(|index| &contracts[index]);
1547 let active = frame
1548 .functions
1549 .iter()
1550 .rposition(|entry| entry.function.is_some());
1551 match (active, step.location, contract) {
1552 (Some(position), Some(own), Some(contract))
1553 if own.function.is_some()
1554 && own.function == frame.functions[position].function =>
1555 {
1556 let function_index = own.function.unwrap_or_default();
1557 let function = &contract.functions[function_index];
1558 let span = ByteRange {
1559 start: own.offset,
1560 end: own.offset + own.length,
1561 };
1562 let (below, rest) = frame.functions.split_at_mut(position);
1563 let entry = &mut rest[0];
1564 let layout = entry.variables.as_mut().map(|variables| {
1565 variables.track(function, span, height, &mut variable_layouts)
1566 });
1567 let modifier_base = entry
1572 .variables
1573 .as_ref()
1574 .and_then(|variables| variables.params_base);
1575 let parent = below
1576 .iter_mut()
1577 .rev()
1578 .find(|entry| entry.function.is_some());
1579 if let (Some(modifier_base), Some(parent)) = (modifier_base, parent) {
1580 let parent_function =
1581 &contract.functions[parent.function.unwrap_or_default()];
1582 let position = parent_function
1583 .modifiers
1584 .iter()
1585 .position(|name| *name == function.name);
1586 if let (Some(position), Some(variables)) =
1587 (position, parent.variables.as_mut())
1588 {
1589 if variables.params_base.is_none() {
1590 variables.params_base =
1591 modifier_slots(contract, parent_function, position)
1592 .and_then(|before| {
1593 modifier_base.checked_sub(
1594 parameter_slots(&parent_function.params)
1595 + parent_function.returns.len()
1596 + before,
1597 )
1598 });
1599 }
1600 }
1601 }
1602 layout
1603 }
1604 (Some(position), _, _) => frame.functions[position]
1605 .variables
1606 .as_ref()
1607 .and_then(|variables| variables.layout),
1608 _ => None,
1609 }
1610 };
1611 step_variables.push(layout.unwrap_or(NO_LAYOUT));
1612 let internal_total = internal
1613 .iter()
1614 .map(|frame| {
1615 frame
1616 .functions
1617 .iter()
1618 .filter(|entry| entry.function.is_some())
1619 .count()
1620 })
1621 .sum::<usize>();
1622 steps.push(StepInfo {
1623 location,
1624 generated,
1625 key,
1626 contract: step.contract,
1627 address: step.address,
1628 storage: step.storage,
1629 evm_depth: step.evm_depth,
1630 frame_depth: (evm_depth + internal_total) as u32,
1631 frame_entry,
1632 line_start: false,
1633 });
1634 pcs.push(trace.steps[index].pc);
1635 }
1636
1637 let mut map = Self {
1638 contracts,
1639 steps,
1640 addresses,
1641 pcs,
1642 reverted,
1643 argument_layouts,
1644 locals_support,
1645 variable_layouts,
1646 step_variables,
1647 };
1648 if smooth {
1649 map.smooth_single_step_excursions();
1650 }
1651 map.mark_line_starts();
1652 map
1653 }
1654
1655 fn smooth_single_step_excursions(&mut self) {
1659 let neighbours =
1660 |steps: &[StepInfo], index: usize, forward: bool| -> Option<Option<LineKey>> {
1661 let depth = steps[index].frame_depth;
1662 let range: Box<dyn Iterator<Item = usize>> = if forward {
1663 Box::new((index + 1)..steps.len())
1664 } else {
1665 Box::new((0..index).rev())
1666 };
1667 for candidate in range {
1668 let step = &steps[candidate];
1669 if step.frame_depth > depth {
1670 continue;
1671 }
1672 if step.frame_depth < depth {
1673 return None;
1674 }
1675 if step.key.is_some() {
1676 return Some(step.key);
1677 }
1678 }
1679 None
1680 };
1681 let mut smoothed = Vec::new();
1682 for index in 0..self.steps.len() {
1683 let Some(key) = self.steps[index].key else {
1684 continue;
1685 };
1686 let Some(before) = neighbours(&self.steps, index, false) else {
1687 continue;
1688 };
1689 let Some(after) = neighbours(&self.steps, index, true) else {
1690 continue;
1691 };
1692 if before == after && before.is_some() && before != Some(key) {
1693 smoothed.push((index, before));
1694 }
1695 }
1696 for (index, key) in smoothed {
1697 self.steps[index].key = key;
1698 }
1699 }
1700
1701 fn mark_line_starts(&mut self) {
1704 let mut last_key_at_or_below = Vec::<Option<LineKey>>::new();
1705 for step in &mut self.steps {
1706 let depth = step.frame_depth as usize;
1707 if last_key_at_or_below.len() <= depth {
1708 last_key_at_or_below.resize(depth + 1, None);
1709 }
1710 let Some(key) = step.key else {
1711 step.line_start = false;
1712 continue;
1713 };
1714 step.line_start = last_key_at_or_below[depth] != Some(key);
1715 for entry in last_key_at_or_below.iter_mut().skip(depth) {
1716 *entry = Some(key);
1717 }
1718 }
1719 }
1720
1721 #[must_use]
1722 pub fn contracts(&self) -> &[ContractDebugInfo] {
1723 &self.contracts
1724 }
1725
1726 #[must_use]
1728 pub fn contract_at_step(&self, step: usize) -> Option<&ContractDebugInfo> {
1729 self.contracts.get(self.steps.get(step)?.contract?)
1730 }
1731
1732 #[must_use]
1733 pub fn step_count(&self) -> usize {
1734 self.steps.len()
1735 }
1736
1737 #[must_use]
1739 pub fn has_source(&self) -> bool {
1740 self.steps.iter().any(|step| step.location.is_some())
1741 }
1742
1743 #[must_use]
1745 pub fn line_key(&self, step: usize) -> Option<LineKey> {
1746 self.steps.get(step)?.key
1747 }
1748
1749 #[must_use]
1750 pub fn frame_depth(&self, step: usize) -> Option<u32> {
1751 self.steps.get(step).map(|step| step.frame_depth)
1752 }
1753
1754 #[must_use]
1755 pub fn is_line_start(&self, step: usize) -> bool {
1756 self.steps.get(step).is_some_and(|step| step.line_start)
1757 }
1758
1759 #[must_use]
1760 pub fn is_frame_entry(&self, step: usize) -> bool {
1761 self.steps.get(step).is_some_and(|step| step.frame_entry)
1762 }
1763
1764 #[must_use]
1766 pub fn function_id(&self, step: usize) -> Option<FunctionId> {
1767 let location = self.steps.get(step)?.location?;
1768 location.function.map(|function| FunctionId {
1769 contract: location.key.contract,
1770 function,
1771 })
1772 }
1773
1774 #[must_use]
1776 pub fn executing_address(&self, step: usize) -> Option<&str> {
1777 let index = self.steps.get(step)?.address?;
1778 self.addresses.get(index).map(String::as_str)
1779 }
1780
1781 #[must_use]
1784 pub fn storage_address(&self, step: usize) -> Option<&str> {
1785 let index = self.storage_context_index(step)?;
1786 self.addresses.get(index).map(String::as_str)
1787 }
1788
1789 #[must_use]
1791 pub fn storage_context_index(&self, step: usize) -> Option<usize> {
1792 self.steps.get(step)?.storage
1793 }
1794
1795 #[must_use]
1798 pub fn reverted_spans(&self) -> &[(usize, usize)] {
1799 &self.reverted
1800 }
1801
1802 #[must_use]
1806 pub fn argument_layout(&self, contract: usize) -> Option<ArgumentLayout> {
1807 self.argument_layouts.get(contract).copied()?.layout()
1808 }
1809
1810 #[must_use]
1820 pub fn locals_at(&self, step: usize) -> LocalsStatus<'_> {
1821 let Some(info) = self.steps.get(step) else {
1822 return LocalsStatus::Unavailable("the step is outside the trace");
1823 };
1824 let Some(contract) = info.contract else {
1825 return LocalsStatus::Unavailable(
1826 "no sources matched the contract executing at this step",
1827 );
1828 };
1829 if let Err(reason) = self.locals_support[contract] {
1830 return LocalsStatus::Unavailable(reason);
1831 }
1832 match self.step_variables.get(step).copied() {
1833 Some(INLINED_LAYOUT) => LocalsStatus::Unavailable(
1834 "this function was inlined by the optimizer, so its variables have no stack \
1835 slots of their own",
1836 ),
1837 Some(id) if id != NO_LAYOUT => {
1838 LocalsStatus::Inferred(&self.variable_layouts[id as usize])
1839 }
1840 _ => LocalsStatus::Unavailable("no function is executing at this step"),
1841 }
1842 }
1843
1844 pub(crate) fn scope_at_step(&self, step: usize) -> Option<&str> {
1847 let info = self.steps.get(step)?;
1848 let contract = self.contracts.get(info.contract?)?;
1849 let function = contract.functions.get(info.location?.function?)?;
1850 contract
1851 .types
1852 .scope_at(function.source_id, function.declaration_start)
1853 }
1854
1855 #[must_use]
1861 pub fn local_path(
1862 &self,
1863 trace: &TransactionTrace,
1864 step: usize,
1865 words: Option<&StorageWords<'_>>,
1866 path: &str,
1867 ) -> Option<Result<DebugVariable, String>> {
1868 let found = match self.local_place(trace, step, words, path)? {
1869 Ok(found) => found,
1870 Err(reason) => return Some(Err(reason)),
1871 };
1872 let (reader, slot, place) = found;
1873 let (ty, value) = match place {
1874 Ok(place) => (place.ty(), reader.show(&place)),
1875 Err((ty, shown)) => (ty, shown),
1876 };
1877 Some(Ok(DebugVariable {
1878 name: path.trim().to_owned(),
1879 ty,
1880 location: DebugLocation {
1881 kind: "stack".to_owned(),
1882 offset: slot as u64,
1883 },
1884 value,
1885 }))
1886 }
1887
1888 #[must_use]
1891 pub fn local_condition_value(
1892 &self,
1893 trace: &TransactionTrace,
1894 step: usize,
1895 words: Option<&StorageWords<'_>>,
1896 path: &str,
1897 ) -> Option<Result<Value, String>> {
1898 let (reader, _, place) = match self.local_place(trace, step, words, path)? {
1899 Ok(found) => found,
1900 Err(reason) => return Some(Err(reason)),
1901 };
1902 Some(match place {
1903 Ok(place) => reader.condition_value(&place, path.trim()),
1904 Err((_, shown)) => Err(format!("`{}` is {}", path.trim(), shown.display)),
1905 })
1906 }
1907
1908 #[allow(clippy::type_complexity)]
1913 fn local_place<'a>(
1914 &'a self,
1915 trace: &'a TransactionTrace,
1916 step: usize,
1917 words: Option<&'a StorageWords<'a>>,
1918 path: &str,
1919 ) -> Option<Result<(ValueReader<'a>, usize, Result<Place, (String, DebugValue)>), String>> {
1920 let LocalsStatus::Inferred(layout) = self.locals_at(step) else {
1921 return None;
1922 };
1923 let segments = parse_path(path).ok()?;
1924 let Some(PathSegment::Name(name)) = segments.first() else {
1925 return None;
1926 };
1927 let variable = layout
1929 .iter()
1930 .rev()
1931 .find(|variable| variable.name == *name)?;
1932 let contract = self.contract_at_step(step)?;
1933 let snapshot = trace
1934 .steps
1935 .get(step)
1936 .map(soldb_core::TraceStep::snapshot_ref);
1937 let stack = snapshot.map_or(&[][..], |snapshot| snapshot.stack);
1938 let reader = ValueReader {
1939 memory: snapshot.and_then(|snapshot| snapshot.memory),
1940 calldata: calldata_for_step(trace, step),
1941 storage: words,
1942 layout: contract.storage_layout.as_ref(),
1943 types: &contract.types,
1944 scope: self.scope_at_step(step),
1945 };
1946 let Some(slot) = variable.slot else {
1947 return Some(Err(format!(
1948 "`{name}` is in scope but its stack slot could not be placed here"
1949 )));
1950 };
1951 let Some(stack_words) = stack.get(slot..slot + variable.words) else {
1952 return Some(Err(format!(
1953 "`{name}` is in scope but its stack slot is above the stack here"
1954 )));
1955 };
1956 let stack_words = stack_words.iter().map(|word| &**word).collect::<Vec<_>>();
1957 let mut place = match reader.root(&variable.ty, &stack_words) {
1958 Ok(place) => place,
1959 Err(shown) if segments.len() == 1 => {
1960 return Some(Ok((reader, slot, Err((variable.ty.clone(), shown)))));
1961 }
1962 Err(shown) => return Some(Err(format!("`{name}` is {}", shown.display))),
1963 };
1964 let mut so_far = name.clone();
1965 for segment in &segments[1..] {
1966 place = match reader.follow(place, segment, &so_far) {
1967 Ok(place) => place,
1968 Err(reason) => return Some(Err(reason)),
1969 };
1970 match segment {
1971 PathSegment::Member(member) => so_far = format!("{so_far}.{member}"),
1972 PathSegment::Index(key) => so_far = format!("{so_far}[{key}]"),
1973 PathSegment::Name(_) => {}
1974 }
1975 }
1976 Some(Ok((reader, slot, Ok(place))))
1977 }
1978
1979 #[must_use]
1987 pub fn inferred_variables(
1988 &self,
1989 trace: &TransactionTrace,
1990 step: usize,
1991 words: Option<&StorageWords<'_>>,
1992 ) -> Vec<DebugVariable> {
1993 let LocalsStatus::Inferred(layout) = self.locals_at(step) else {
1994 return Vec::new();
1995 };
1996 let Some(contract) = self.contract_at_step(step) else {
1997 return Vec::new();
1998 };
1999 let snapshot = trace
2000 .steps
2001 .get(step)
2002 .map(soldb_core::TraceStep::snapshot_ref);
2003 let stack = snapshot.map_or(&[][..], |snapshot| snapshot.stack);
2004 let reader = ValueReader {
2005 memory: snapshot.and_then(|snapshot| snapshot.memory),
2006 calldata: calldata_for_step(trace, step),
2007 storage: words,
2008 layout: contract.storage_layout.as_ref(),
2009 types: &contract.types,
2010 scope: self.scope_at_step(step),
2011 };
2012 let unavailable = || DebugValue {
2013 display: "<unavailable>".to_owned(),
2014 raw: None,
2015 status: DebugValueStatus::Unavailable,
2016 };
2017 layout
2018 .iter()
2019 .map(|variable| {
2020 let words = variable
2021 .slot
2022 .and_then(|slot| stack.get(slot..slot + variable.words))
2023 .map(|words| words.iter().map(|word| &**word).collect::<Vec<_>>());
2024 let value = match words {
2025 Some(words) => reader.variable(&variable.ty, &words),
2026 None => unavailable(),
2027 };
2028 DebugVariable {
2029 name: variable.name.clone(),
2030 ty: variable.ty.clone(),
2031 location: DebugLocation {
2032 kind: "stack".to_owned(),
2033 offset: variable.slot.unwrap_or_default() as u64,
2034 },
2035 value,
2036 }
2037 })
2038 .collect()
2039 }
2040
2041 #[must_use]
2049 pub fn frame_arguments(&self, frame: &Frame, state: FrameState<'_>) -> Vec<FrameArgument> {
2050 let Some(info) = self.steps.get(frame.entry_step) else {
2051 return Vec::new();
2052 };
2053 let Some(location) = info.location else {
2054 return Vec::new();
2055 };
2056 let Some(function_index) = location.function else {
2057 return Vec::new();
2058 };
2059 let Some(contract) = self.contracts.get(location.key.contract) else {
2060 return Vec::new();
2061 };
2062 if contract.function_entry_at_pc(self.pcs[frame.entry_step]) != Some(function_index) {
2063 return Vec::new();
2064 }
2065 let Some(function) = contract.functions.get(function_index) else {
2066 return Vec::new();
2067 };
2068 let Some(layout) = self.argument_layout(location.key.contract) else {
2069 return Vec::new();
2070 };
2071 decode_arguments(
2072 &function.params,
2073 state,
2074 layout,
2075 &contract.types,
2076 contract
2077 .types
2078 .scope_at(function.source_id, function.declaration_start),
2079 )
2080 }
2081
2082 #[must_use]
2083 pub fn location(&self, step: usize) -> Option<StepLocation> {
2084 let info = self.steps.get(step)?;
2085 self.describe(&info.location?, info.generated)
2086 }
2087
2088 fn describe(&self, location: &LocationRef, generated: bool) -> Option<StepLocation> {
2089 let contract = self.contracts.get(location.key.contract)?;
2090 let function = location
2091 .function
2092 .and_then(|index| contract.functions.get(index));
2093 Some(StepLocation {
2094 key: location.key,
2095 generated,
2096 contract_name: contract.name.clone(),
2097 path: contract
2098 .source_path(location.key.source_id)
2099 .map(str::to_owned)
2100 .unwrap_or_else(|| format!("source:{}", location.key.source_id)),
2101 offset: location.offset,
2102 length: location.length,
2103 line: location.key.line,
2104 column: contract
2105 .column_of(location.key.source_id, location.offset)
2106 .unwrap_or(0),
2107 function: location.function.map(|function| FunctionId {
2108 contract: location.key.contract,
2109 function,
2110 }),
2111 function_name: function.map(|function| function.name.clone()),
2112 })
2113 }
2114
2115 #[must_use]
2125 pub fn next_source(&self, step: usize) -> Option<usize> {
2126 let current = *self.steps.get(step)?;
2127 if self.outside_functions(¤t) {
2128 return self.step_into(step);
2129 }
2130 let key = current.key;
2131 ((step + 1)..self.steps.len()).find(|&index| {
2132 let candidate = &self.steps[index];
2133 if candidate.frame_depth > current.frame_depth {
2134 return false;
2135 }
2136 candidate.frame_depth < current.frame_depth
2137 || candidate
2138 .key
2139 .is_some_and(|candidate| Some(candidate) != key)
2140 })
2141 }
2142
2143 #[must_use]
2146 pub fn step_into(&self, step: usize) -> Option<usize> {
2147 let current = *self.steps.get(step)?;
2148 let key = current.key;
2149 ((step + 1)..self.steps.len()).find(|&index| {
2150 let candidate = &self.steps[index];
2151 candidate.frame_depth != current.frame_depth
2152 || candidate
2153 .key
2154 .is_some_and(|candidate| Some(candidate) != key)
2155 })
2156 }
2157
2158 #[must_use]
2160 pub fn finish(&self, step: usize) -> Option<usize> {
2161 let current = *self.steps.get(step)?;
2162 ((step + 1)..self.steps.len())
2163 .find(|&index| self.steps[index].frame_depth < current.frame_depth)
2164 }
2165
2166 #[must_use]
2171 pub fn previous_source(&self, step: usize) -> Option<usize> {
2172 let current = *self.steps.get(step)?;
2173 if self.outside_functions(¤t) {
2174 return self.reverse_step_into(step);
2175 }
2176 self.previous_line_start(step, true)
2177 }
2178
2179 #[must_use]
2181 pub fn reverse_step_into(&self, step: usize) -> Option<usize> {
2182 self.steps.get(step)?;
2183 self.previous_line_start(step, false)
2184 }
2185
2186 fn previous_line_start(&self, step: usize, skip_deeper: bool) -> Option<usize> {
2187 let current = self.steps[step];
2188 let anchor = if current.key.is_some() {
2190 step
2191 } else {
2192 self.previous_mapped(step, current.frame_depth, skip_deeper)?
2193 };
2194 let start = self.run_start(anchor, skip_deeper);
2195 if start < step {
2196 return Some(start);
2197 }
2198 let previous = self.previous_mapped(start, current.frame_depth, skip_deeper)?;
2199 Some(self.run_start(previous, skip_deeper))
2200 }
2201
2202 #[must_use]
2204 pub fn reverse_finish(&self, step: usize) -> Option<usize> {
2205 let current = *self.steps.get(step)?;
2206 (0..step)
2207 .rev()
2208 .find(|&index| self.steps[index].frame_depth < current.frame_depth)
2209 }
2210
2211 fn run_start(&self, step: usize, skip_deeper: bool) -> usize {
2215 let info = self.steps[step];
2216 let key = info.key;
2217 let mut current = step;
2218 while let Some(previous) = self.previous_mapped(current, info.frame_depth, skip_deeper) {
2219 let candidate = self.steps[previous];
2220 if candidate.frame_depth != info.frame_depth || candidate.key != key {
2221 break;
2222 }
2223 current = previous;
2224 }
2225 current
2226 }
2227
2228 fn outside_functions(&self, info: &StepInfo) -> bool {
2231 info.location.is_some() && info.key.is_none()
2232 }
2233
2234 fn previous_mapped(&self, step: usize, depth: u32, skip_deeper: bool) -> Option<usize> {
2237 (0..step).rev().find(|&index| {
2238 let candidate = &self.steps[index];
2239 candidate.key.is_some() && (!skip_deeper || candidate.frame_depth <= depth)
2240 })
2241 }
2242
2243 pub fn resolve_line(&self, file: Option<&str>, line: u64) -> Result<Vec<ResolvedLine>, String> {
2246 let mut sources = Vec::<(usize, u64, String)>::new();
2247 for (contract_index, contract) in self.contracts.iter().enumerate() {
2248 for (source_id, path) in &contract.info.sources {
2249 if file.is_none_or(|file| source_path_matches(path, file)) {
2250 sources.push((contract_index, *source_id, path.clone()));
2251 }
2252 }
2253 }
2254 if sources.is_empty() {
2255 return Err(match file {
2256 Some(file) => format!("source file not found: {file}"),
2257 None => "no source files are available".to_owned(),
2258 });
2259 }
2260 if file.is_none() {
2261 let mut paths = sources.iter().map(|(_, _, path)| path).collect::<Vec<_>>();
2262 paths.sort();
2263 paths.dedup();
2264 if paths.len() > 1 {
2265 return Err("line breakpoint is ambiguous; use break <file>:<line>".to_owned());
2266 }
2267 }
2268
2269 let mut resolved = sources
2270 .into_iter()
2271 .filter_map(|(contract_index, source_id, path)| {
2272 let effective = self.contracts[contract_index].effective_line(source_id, line)?;
2273 Some(ResolvedLine {
2274 key: LineKey {
2275 contract: contract_index,
2276 source_id,
2277 line: effective,
2278 },
2279 path,
2280 requested_line: line,
2281 })
2282 })
2283 .collect::<Vec<_>>();
2284 if resolved.is_empty() {
2285 return Err(match file {
2286 Some(file) => format!("no instruction maps to {file}:{line}"),
2287 None => format!("no instruction maps to line {line}"),
2288 });
2289 }
2290 if resolved.iter().any(|candidate| candidate.key.line == line) {
2295 resolved.retain(|candidate| candidate.key.line == line);
2296 }
2297 Ok(resolved)
2298 }
2299
2300 pub fn resolve_function(&self, name: &str) -> Result<Vec<ResolvedFunction>, String> {
2303 let (contract_name, function_name) = match name.rsplit_once('.') {
2304 Some((contract, function)) => (Some(contract), function),
2305 None => (None, name),
2306 };
2307 let resolved = self
2308 .contracts
2309 .iter()
2310 .enumerate()
2311 .filter(|(_, contract)| contract_name.is_none_or(|name| contract.name == name))
2312 .flat_map(|(contract_index, contract)| {
2313 contract
2314 .functions
2315 .iter()
2316 .enumerate()
2317 .filter(move |(_, function)| function.name == function_name)
2318 .map(move |(function_index, function)| ResolvedFunction {
2319 id: FunctionId {
2320 contract: contract_index,
2321 function: function_index,
2322 },
2323 name: function.name.clone(),
2324 contract_name: contract.name.clone(),
2325 path: contract
2326 .source_path(function.source_id)
2327 .map(str::to_owned)
2328 .unwrap_or_else(|| format!("source:{}", function.source_id)),
2329 line: function.declaration_line,
2330 })
2331 })
2332 .collect::<Vec<_>>();
2333 if resolved.is_empty() {
2334 return Err(format!("no function named `{name}` in the loaded sources"));
2335 }
2336 Ok(resolved)
2337 }
2338
2339 #[must_use]
2341 pub fn frames(&self, step: usize) -> Vec<Frame> {
2342 struct Record {
2343 entry_step: usize,
2344 last_step: usize,
2345 external: bool,
2346 }
2347 if step >= self.steps.len() {
2348 return Vec::new();
2349 }
2350 let mut stack = Vec::<Record>::new();
2351 for (index, info) in self.steps.iter().enumerate().take(step + 1) {
2352 let depth = info.frame_depth as usize;
2353 stack.truncate(depth + 1);
2354 let entered_call = index == 0 || info.evm_depth > self.steps[index - 1].evm_depth;
2357 let mut first_new = true;
2358 while stack.len() <= depth {
2359 stack.push(Record {
2360 entry_step: index,
2361 last_step: index,
2362 external: first_new && entered_call,
2363 });
2364 first_new = false;
2365 }
2366 if let Some(top) = stack.last_mut() {
2367 top.last_step = index;
2368 }
2369 }
2370
2371 stack
2372 .iter()
2373 .enumerate()
2374 .rev()
2375 .map(|(depth, record)| {
2376 let info = self.steps[record.last_step];
2377 let location = info
2378 .location
2379 .and_then(|location| self.describe(&location, info.generated));
2380 Frame {
2381 depth: depth as u32,
2382 external: record.external,
2383 address: info
2384 .address
2385 .and_then(|index| self.addresses.get(index))
2386 .cloned(),
2387 contract_name: info
2388 .contract
2389 .and_then(|index| self.contracts.get(index))
2390 .map(|contract| contract.name.clone()),
2391 function_name: location
2392 .as_ref()
2393 .and_then(|location| location.function_name.clone()),
2394 entry_step: record.entry_step,
2395 step: record.last_step,
2396 pc: self.pcs[record.last_step],
2397 location,
2398 arguments: Vec::new(),
2399 }
2400 })
2401 .collect()
2402 }
2403
2404 #[must_use]
2406 pub fn source_listing(&self, step: usize, radius: u64) -> Option<SourceListing> {
2407 let location = self.steps.get(step)?.location?;
2408 let contract = self.contracts.get(location.key.contract)?;
2409 let count = contract.line_count(location.key.source_id)?;
2410 let first = location.key.line.saturating_sub(radius).max(1);
2411 let last = location.key.line.saturating_add(radius).min(count);
2412 let lines = (first..=last)
2413 .filter_map(|line| {
2414 contract
2415 .line_text(location.key.source_id, line)
2416 .map(|text| (line, text.to_owned()))
2417 })
2418 .collect();
2419 Some(SourceListing {
2420 path: contract
2421 .source_path(location.key.source_id)
2422 .map(str::to_owned)
2423 .unwrap_or_else(|| format!("source:{}", location.key.source_id)),
2424 current_line: location.key.line,
2425 lines,
2426 })
2427 }
2428}
2429
2430fn prove_argument_layouts(
2445 trace: &TransactionTrace,
2446 contracts: &[ContractDebugInfo],
2447 framed: &[FramedStep],
2448) -> Vec<Evidence> {
2449 let mut evidence = vec![Evidence::Unknown; contracts.len()];
2450 if contracts.is_empty() || framed.is_empty() {
2451 return evidence;
2452 }
2453 let mut calldata_by_entry = HashMap::<usize, &str>::new();
2456 calldata_by_entry.insert(0, trace.input_data.as_str());
2457 for call in &trace.artifacts.calls {
2458 if let Some(entry_step) = call.entry_step {
2459 calldata_by_entry.insert(entry_step, call.input.as_str());
2460 }
2461 }
2462
2463 let mut examined = std::collections::HashSet::<u32>::new();
2464 let mut frame_calldata = HashMap::<u32, &str>::new();
2465 for (index, step) in framed.iter().enumerate() {
2466 let entered = index == 0 || framed[index - 1].frame_id != step.frame_id;
2467 if entered {
2468 if let Some(calldata) = calldata_by_entry.get(&index) {
2469 frame_calldata.insert(step.frame_id, calldata);
2470 }
2471 }
2472 let (Some(contract_index), Some(function_index)) = (step.contract, step.entry) else {
2473 continue;
2474 };
2475 if !examined.insert(step.frame_id) {
2478 continue;
2479 }
2480 let Some(calldata) = frame_calldata.get(&step.frame_id) else {
2481 continue;
2482 };
2483 let Some(function) = contracts[contract_index].functions.get(function_index) else {
2484 continue;
2485 };
2486 let stack = trace.steps[index].snapshot_ref().stack;
2487 let observed = argument_evidence(function, calldata, stack);
2488 evidence[contract_index] = evidence[contract_index].merge(observed);
2489 }
2490 evidence
2491}
2492
2493fn argument_evidence(function: &SourceFunction, calldata: &str, stack: &[StackWord]) -> Evidence {
2501 let count = function.params.len();
2502 if count == 0 || stack.len() < count {
2503 return Evidence::Unknown;
2504 }
2505 if !function.params.iter().all(readable_parameter) {
2507 return Evidence::Unknown;
2508 }
2509 let signature = format!(
2510 "{}({})",
2511 function.name,
2512 function
2513 .params
2514 .iter()
2515 .map(|param| canonical_value_type(¶m.ty))
2516 .collect::<Vec<_>>()
2517 .join(",")
2518 );
2519 let Ok(selector) = function_selector(&signature) else {
2520 return Evidence::Unknown;
2521 };
2522 let data = calldata.trim_start_matches("0x");
2523 let Some((encoded_selector, arguments)) = data.split_at_checked(8) else {
2524 return Evidence::Unknown;
2525 };
2526 if encoded_selector.to_ascii_lowercase() != hex_bytes(&selector) {
2528 return Evidence::Unknown;
2529 }
2530 let expected = (0..count)
2531 .map(|index| {
2532 arguments
2533 .get(index * 64..(index + 1) * 64)
2534 .map(str::to_ascii_lowercase)
2535 })
2536 .collect::<Option<Vec<_>>>();
2537 let Some(expected) = expected else {
2538 return Evidence::Unknown;
2539 };
2540 let checkable = (0..count)
2543 .filter(|index| is_value_type(&function.params[*index].ty))
2544 .collect::<Vec<_>>();
2545 if checkable.is_empty() {
2546 return Evidence::Unknown;
2547 }
2548 let matches = |order: ArgumentOrder| {
2549 checkable.iter().all(|index| {
2550 let candidate = &stack[order.word_index(*index, count, stack.len())];
2551 normalize_word(candidate) == expected[*index]
2552 })
2553 };
2554 let distinguishing = checkable.iter().any(|index| {
2556 ArgumentOrder::FirstOnTop.word_index(*index, count, stack.len())
2557 != ArgumentOrder::LastOnTop.word_index(*index, count, stack.len())
2558 });
2559 let first = matches(ArgumentOrder::FirstOnTop);
2560 let last = matches(ArgumentOrder::LastOnTop);
2561 match (first, last) {
2562 (true, true) => Evidence::TopWords,
2565 (true, false) if distinguishing => Evidence::Ordered(ArgumentOrder::FirstOnTop),
2566 (false, true) if distinguishing => Evidence::Ordered(ArgumentOrder::LastOnTop),
2567 (true, false) | (false, true) => Evidence::TopWords,
2569 (false, false) => Evidence::Contradicted,
2571 }
2572}
2573
2574fn canonical_value_type(ty: &str) -> String {
2577 match ty {
2578 "uint" => "uint256".to_owned(),
2579 "int" => "int256".to_owned(),
2580 "address payable" => "address".to_owned(),
2581 other => other.to_owned(),
2582 }
2583}
2584
2585fn normalize_word(word: &str) -> String {
2587 let digits = word.trim_start_matches("0x").trim_start_matches('0');
2588 format!("{:0>64}", digits.to_ascii_lowercase())
2589}
2590
2591fn hex_bytes(bytes: &[u8]) -> String {
2592 bytes.iter().map(|byte| format!("{byte:02x}")).collect()
2593}
2594
2595#[must_use]
2597pub fn call_target(step: &soldb_core::TraceStep) -> Option<String> {
2598 if !CALL_OPCODES.contains(&&*step.op) {
2599 return None;
2600 }
2601 let stack = step.snapshot_ref().stack;
2602 let word = stack.get(stack.len().checked_sub(2)?)?;
2603 address_from_word(word)
2604}
2605
2606#[must_use]
2609pub fn address_from_word(word: &str) -> Option<String> {
2610 let hex = word.trim_start_matches("0x");
2611 if !hex.bytes().all(|byte| byte.is_ascii_hexdigit()) {
2612 return None;
2613 }
2614 let padded = format!("{hex:0>40}");
2615 let address = padded.get(padded.len() - 40..)?;
2616 if address.bytes().all(|byte| byte == b'0') {
2617 return None;
2618 }
2619 Some(format!("0x{}", address.to_ascii_lowercase()))
2620}
2621
2622#[cfg(test)]
2623mod tests {
2624 use std::collections::BTreeMap;
2625
2626 use serde_json::json;
2627 use soldb_core::{ContractCreation, ExecutionCall, StepSnapshot, TraceStep, TransactionTrace};
2628 use soldb_ethdebug::{function_selector, CodeGenerator, EthdebugInfo, Instruction};
2629
2630 use soldb_core::Word as StackWord;
2631
2632 use crate::FrameState;
2633
2634 use super::{
2635 address_from_word, normalize_address, ArgumentLayout, ArgumentOrder, ContractDebugInfo,
2636 InferredVariable, JumpMarker, LineKey, LocalsStatus, StepMap, VariableKind,
2637 };
2638
2639 const SOURCE: &str = "\
2641contract C {
2642 function outer(uint256 a) public {
2643 uint256 b = a + 1;
2644 inner(b);
2645 b = 0;
2646 }
2647 function inner(uint256 x) internal {
2648 x += 1;
2649 }
2650}
2651";
2652
2653 fn offset_of(needle: &str) -> u64 {
2654 SOURCE.find(needle).expect(needle) as u64
2655 }
2656
2657 fn instruction(pc: u64, offset: u64, length: u64) -> Instruction {
2658 instruction_with(pc, offset, length, json!({}))
2659 }
2660
2661 fn instruction_with(
2663 pc: u64,
2664 offset: u64,
2665 length: u64,
2666 extra: serde_json::Value,
2667 ) -> Instruction {
2668 let mut context =
2669 json!({"code": {"source": {"id": 0}, "range": {"offset": offset, "length": length}}});
2670 for (key, value) in extra.as_object().expect("object") {
2671 context[key] = value.clone();
2672 }
2673 serde_json::from_value(json!({
2674 "offset": pc,
2675 "operation": {"mnemonic": "JUMPDEST"},
2676 "context": context
2677 }))
2678 .expect("instruction")
2679 }
2680
2681 fn contract(address: Option<&str>) -> ContractDebugInfo {
2682 let whole = SOURCE.len() as u64;
2683 let info = EthdebugInfo {
2684 compilation: serde_json::Value::Null,
2685 contract_name: "C".to_owned(),
2686 environment: "runtime".to_owned(),
2687 instructions: vec![
2688 instruction(0, 0, whole),
2690 instruction(1, 0, whole),
2691 instruction(10, offset_of("function outer"), 90),
2693 instruction(11, offset_of("uint256 b = a + 1;"), 18),
2694 instruction(12, offset_of("inner(b);"), 9),
2695 instruction(13, offset_of("inner(b);"), 9),
2696 instruction(14, offset_of("b = 0;"), 6),
2697 instruction(20, offset_of("function inner"), 50),
2699 instruction(21, offset_of("x += 1;"), 7),
2700 instruction(30, 0, whole),
2702 instruction(40, 0, whole),
2704 ],
2705 sources: BTreeMap::from([(0, "C.sol".to_owned())]),
2706 variable_locations: BTreeMap::new(),
2707 };
2708 ContractDebugInfo::new(address, "C", info, BTreeMap::from([(0, SOURCE.to_owned())]))
2709 }
2710
2711 fn step(pc: u64, depth: u64, op: &str, stack: &[&str]) -> TraceStep {
2712 TraceStep {
2713 pc,
2714 op: op.into(),
2715 gas: 0,
2716 gas_cost: 0,
2717 depth,
2718 stack: stack.iter().map(|word| StackWord::from(*word)).collect(),
2719 memory: None,
2720 storage: None,
2721 error: None,
2722 snapshot: StepSnapshot::default(),
2723 }
2724 }
2725
2726 fn trace(steps: Vec<TraceStep>) -> TransactionTrace {
2727 TransactionTrace {
2728 tx_hash: None,
2729 from_addr: "0x1".to_owned(),
2730 to_addr: Some("0xAAaa000000000000000000000000000000000001".to_owned()),
2731 value: "0x0".to_owned(),
2732 input_data: "0x".to_owned(),
2733 gas_used: 0,
2734 output: "0x".to_owned(),
2735 success: true,
2736 error: None,
2737 debug_trace_available: true,
2738 contract_address: None,
2739 backend: None,
2740 capabilities: Default::default(),
2741 artifacts: Default::default(),
2742 steps,
2743 }
2744 }
2745
2746 fn outer_calls_inner() -> TransactionTrace {
2749 trace(vec![
2750 step(0, 1, "PUSH1", &[]),
2751 step(1, 1, "JUMPI", &[]),
2752 step(10, 1, "JUMPDEST", &[]),
2753 step(11, 1, "ADD", &[]),
2754 step(12, 1, "PUSH1", &[]),
2755 step(20, 1, "JUMPDEST", &[]),
2756 step(21, 1, "ADD", &[]),
2757 step(21, 1, "SWAP1", &[]),
2758 step(13, 1, "JUMPDEST", &[]),
2759 step(14, 1, "POP", &[]),
2760 step(30, 1, "STOP", &[]),
2761 ])
2762 }
2763
2764 const PAY_SOURCE: &str = "\
2767contract P {
2768 function pay(address to, uint256 amount) public {
2769 total(to, amount);
2770 }
2771 function total(address to, uint256 amount) internal {
2772 amount += 1;
2773 }
2774}
2775";
2776
2777 fn pay_contract() -> ContractDebugInfo {
2778 let whole = PAY_SOURCE.len() as u64;
2779 let at = |needle: &str| PAY_SOURCE.find(needle).expect(needle) as u64;
2780 let span = |pc: u64, offset: u64, length: u64| {
2781 serde_json::from_value::<Instruction>(json!({
2782 "offset": pc,
2783 "operation": {"mnemonic": "JUMPDEST"},
2784 "context": {"code": {
2785 "source": {"id": 0},
2786 "range": {"offset": offset, "length": length}
2787 }}
2788 }))
2789 .expect("instruction")
2790 };
2791 let info = EthdebugInfo {
2792 compilation: serde_json::Value::Null,
2793 contract_name: "P".to_owned(),
2794 environment: "runtime".to_owned(),
2795 instructions: vec![
2796 span(0, 0, whole),
2797 span(10, at("function pay"), 80),
2798 span(11, at("total(to, amount);"), 18),
2799 span(20, at("function total"), 70),
2800 span(21, at("amount += 1;"), 12),
2801 ],
2802 sources: BTreeMap::from([(0, "P.sol".to_owned())]),
2803 variable_locations: BTreeMap::new(),
2804 };
2805 ContractDebugInfo::new(
2806 None,
2807 "P",
2808 info,
2809 BTreeMap::from([(0, PAY_SOURCE.to_owned())]),
2810 )
2811 }
2812
2813 fn word_of(value: &str) -> StackWord {
2814 StackWord::from(format!("0x{:0>64}", value.trim_start_matches("0x")).as_str())
2815 }
2816
2817 fn pay_trace(entry_stack: &[StackWord], inner_stack: &[StackWord]) -> TransactionTrace {
2820 let selector = soldb_ethdebug::function_selector("pay(address,uint256)").expect("selector");
2821 let calldata = format!(
2822 "0x{}{}{}",
2823 selector
2824 .iter()
2825 .map(|byte| format!("{byte:02x}"))
2826 .collect::<String>(),
2827 &word_of(PAY_TO)[2..],
2828 &word_of(PAY_AMOUNT)[2..]
2829 );
2830 fn borrow(stack: &[StackWord]) -> Vec<&str> {
2831 stack.iter().map(|word| &**word).collect()
2832 }
2833 let mut trace = trace(vec![
2834 step(0, 1, "PUSH1", &[]),
2835 step(10, 1, "JUMPDEST", &borrow(entry_stack)),
2836 step(11, 1, "JUMP", &borrow(inner_stack)),
2837 step(20, 1, "JUMPDEST", &borrow(inner_stack)),
2838 step(21, 1, "ADD", &[]),
2839 ]);
2840 trace.input_data = calldata;
2841 trace
2842 }
2843
2844 const PAY_TO: &str = "00000000000000000000000000000000000000aa";
2845 const PAY_AMOUNT: &str = "7";
2846
2847 #[test]
2848 fn the_trace_proves_which_end_the_first_argument_is_at() {
2849 let to = word_of(PAY_TO);
2850 let amount = word_of(PAY_AMOUNT);
2851 let tag = word_of("2a");
2852
2853 let first_on_top = [tag.clone(), amount.clone(), to.clone()];
2855 let trace = pay_trace(&first_on_top, &first_on_top);
2856 let map = StepMap::new(&trace, vec![pay_contract()]);
2857 assert_eq!(
2858 map.argument_layout(0),
2859 Some(ArgumentLayout::Ordered(ArgumentOrder::FirstOnTop))
2860 );
2861 let frames = map.frames(4);
2862 let inner = frames.first().expect("innermost frame");
2863 assert_eq!(inner.function_name.as_deref(), Some("total"));
2864 let arguments = map.frame_arguments(
2865 inner,
2866 FrameState {
2867 stack: &first_on_top,
2868 memory: None,
2869 },
2870 );
2871 assert_eq!(
2872 arguments
2873 .iter()
2874 .map(|argument| format!("{} = {}", argument.name, argument.value.display))
2875 .collect::<Vec<_>>(),
2876 [
2877 "to = 0x00000000000000000000000000000000000000aa".to_owned(),
2878 "amount = 7".to_owned()
2879 ]
2880 );
2881
2882 let last_on_top = [tag.clone(), to.clone(), amount.clone()];
2884 let trace = pay_trace(&last_on_top, &last_on_top);
2885 let map = StepMap::new(&trace, vec![pay_contract()]);
2886 assert_eq!(
2887 map.argument_layout(0),
2888 Some(ArgumentLayout::Ordered(ArgumentOrder::LastOnTop))
2889 );
2890 let frames = map.frames(4);
2891 let arguments = map.frame_arguments(
2892 frames.first().expect("frame"),
2893 FrameState {
2894 stack: &last_on_top,
2895 memory: None,
2896 },
2897 );
2898 assert_eq!(
2899 arguments[0].value.display,
2900 "0x00000000000000000000000000000000000000aa"
2901 );
2902 assert_eq!(arguments[1].value.display, "7");
2903
2904 let wrapper = [tag.clone(), word_of("dead"), word_of("beef")];
2907 let trace = pay_trace(&wrapper, &first_on_top);
2908 let map = StepMap::new(&trace, vec![pay_contract()]);
2909 assert_eq!(map.argument_layout(0), None);
2910 let frames = map.frames(4);
2911 assert!(map
2912 .frame_arguments(
2913 frames.first().expect("frame"),
2914 FrameState {
2915 stack: &first_on_top,
2916 memory: None
2917 }
2918 )
2919 .is_empty());
2920
2921 let mut unknown = pay_trace(&first_on_top, &first_on_top);
2923 unknown.input_data = "0x".to_owned();
2924 let map = StepMap::new(&unknown, vec![pay_contract()]);
2925 assert_eq!(map.argument_layout(0), None);
2926 let frames = map.frames(4);
2927 assert!(map
2928 .frame_arguments(
2929 frames.first().expect("frame"),
2930 FrameState {
2931 stack: &first_on_top,
2932 memory: None
2933 }
2934 )
2935 .is_empty());
2936 }
2937
2938 #[test]
2939 fn one_parameter_is_on_top_under_either_order() {
2940 let mut trace = outer_calls_inner();
2943 let selector = soldb_ethdebug::function_selector("outer(uint256)").expect("selector");
2944 let amount = word_of("5");
2945 trace.input_data = format!(
2946 "0x{}{}",
2947 selector
2948 .iter()
2949 .map(|byte| format!("{byte:02x}"))
2950 .collect::<String>(),
2951 &amount[2..]
2952 );
2953 let stack = [word_of("2a"), amount.clone()];
2954 trace.steps[2].snapshot =
2955 StepSnapshot::new(stack.to_vec(), None, BTreeMap::new(), BTreeMap::new());
2956 let map = StepMap::new(&trace, vec![contract(None)]);
2957 assert_eq!(map.argument_layout(0), Some(ArgumentLayout::TopWords));
2958 let frames = map.frames(3);
2959 let outer = frames
2960 .iter()
2961 .find(|frame| frame.function_name.as_deref() == Some("outer"))
2962 .expect("outer frame");
2963 let arguments = map.frame_arguments(
2964 outer,
2965 FrameState {
2966 stack: &stack,
2967 memory: None,
2968 },
2969 );
2970 assert_eq!(arguments.len(), 1);
2971 assert_eq!(arguments[0].name, "a");
2972 assert_eq!(arguments[0].value.display, "5");
2973
2974 let map = StepMap::new(&trace, vec![pay_contract()]);
2976 assert_eq!(map.argument_layout(0), None);
2977 }
2978
2979 fn key(line: u64) -> Option<LineKey> {
2980 Some(LineKey {
2981 contract: 0,
2982 source_id: 0,
2983 line,
2984 })
2985 }
2986
2987 #[test]
2988 fn maps_steps_to_lines_and_infers_internal_frames() {
2989 let map = StepMap::new(&outer_calls_inner(), vec![contract(None)]);
2990 assert!(map.has_source());
2991 assert_eq!(map.line_key(0), None);
2993 assert_eq!(map.location(0).expect("location").line, 1);
2994 assert!(!map.location(0).expect("location").generated);
2995 assert_eq!(map.line_key(2), key(2));
2996 assert_eq!(map.line_key(3), key(3));
2997 assert_eq!(map.line_key(4), key(4));
2998 assert_eq!(map.line_key(5), key(7));
2999 assert_eq!(map.line_key(6), key(8));
3000 assert_eq!(map.line_key(8), key(4));
3001 assert_eq!(map.line_key(10), None);
3002
3003 let depths = (0..11)
3005 .map(|step| map.frame_depth(step).expect("depth"))
3006 .collect::<Vec<_>>();
3007 assert_eq!(depths, vec![0, 0, 1, 1, 1, 2, 2, 2, 1, 1, 0]);
3008 assert!(map.is_frame_entry(2));
3009 assert!(map.is_frame_entry(5));
3010 assert!(!map.is_frame_entry(8));
3011 assert_eq!(
3012 map.location(6).expect("location").function_name.as_deref(),
3013 Some("inner")
3014 );
3015 assert_eq!(map.location(6).expect("location").path, "C.sol");
3016 assert_eq!(map.location(6).expect("location").column, 9);
3017 }
3018
3019 #[test]
3020 fn next_steps_over_calls_and_step_enters_them() {
3021 let map = StepMap::new(&outer_calls_inner(), vec![contract(None)]);
3022 assert_eq!(map.next_source(0), Some(2));
3024 assert_eq!(map.next_source(2), Some(3));
3025 assert_eq!(map.next_source(4), Some(9));
3027 assert_eq!(map.step_into(4), Some(5));
3028 assert_eq!(map.step_into(5), Some(6));
3029 assert_eq!(map.next_source(6), Some(8));
3031 assert_eq!(map.finish(6), Some(8));
3032 assert_eq!(map.next_source(9), Some(10));
3034 assert_eq!(map.finish(9), Some(10));
3035 assert_eq!(map.next_source(10), None);
3036 assert_eq!(map.finish(10), None);
3037 }
3038
3039 #[test]
3040 fn reverse_stepping_lands_on_line_starts() {
3041 let map = StepMap::new(&outer_calls_inner(), vec![contract(None)]);
3042 assert_eq!(map.previous_source(9), Some(4));
3044 assert_eq!(map.previous_source(7), Some(6));
3046 assert_eq!(map.previous_source(6), Some(5));
3047 assert_eq!(map.reverse_step_into(9), Some(8));
3049 assert_eq!(map.reverse_step_into(8), Some(6));
3050 assert_eq!(map.reverse_finish(6), Some(4));
3052 assert_eq!(map.reverse_finish(0), None);
3053 assert_eq!(map.previous_source(0), None);
3054 assert_eq!(map.previous_source(10), Some(9));
3057 }
3058
3059 #[test]
3060 fn line_starts_skip_returns_into_the_middle_of_a_line() {
3061 let map = StepMap::new(&outer_calls_inner(), vec![contract(None)]);
3062 assert!(map.is_line_start(3));
3063 assert!(map.is_line_start(4));
3064 assert!(map.is_line_start(6));
3065 assert!(!map.is_line_start(7));
3066 assert!(!map.is_line_start(8));
3068 assert!(map.is_line_start(9));
3069 }
3070
3071 #[test]
3072 fn resolves_line_and_function_breakpoints() {
3073 let map = StepMap::new(&outer_calls_inner(), vec![contract(None)]);
3074 let resolved = map.resolve_line(Some("C.sol"), 4).expect("line 4");
3075 assert_eq!(resolved.len(), 1);
3076 assert_eq!(resolved[0].key, key(4).unwrap());
3077 assert_eq!(resolved[0].path, "C.sol");
3078 assert_eq!(
3080 map.resolve_line(None, 8).expect("line 8")[0].key,
3081 key(8).unwrap()
3082 );
3083 assert_eq!(
3086 map.resolve_line(None, 10).expect("line 10")[0].key,
3087 key(1).unwrap()
3088 );
3089 assert_eq!(
3090 map.resolve_line(None, 99).expect_err("past the end"),
3091 "no instruction maps to line 99"
3092 );
3093 assert_eq!(
3094 map.resolve_line(Some("D.sol"), 1)
3095 .expect_err("unknown file"),
3096 "source file not found: D.sol"
3097 );
3098
3099 let inner = map.resolve_function("inner").expect("inner");
3100 assert_eq!(inner.len(), 1);
3101 assert_eq!(inner[0].line, 7);
3102 assert_eq!(inner[0].contract_name, "C");
3103 assert_eq!(
3104 map.resolve_function("C.outer").expect("qualified")[0].line,
3105 2
3106 );
3107 assert!(map.resolve_function("D.outer").is_err());
3108 assert!(map.resolve_function("missing").is_err());
3109 }
3110
3111 #[test]
3112 fn frames_list_the_call_structure_innermost_first() {
3113 let map = StepMap::new(&outer_calls_inner(), vec![contract(None)]);
3114 let frames = map.frames(6);
3115 assert_eq!(frames.len(), 3);
3116 assert_eq!(frames[0].function_name.as_deref(), Some("inner"));
3117 assert_eq!(frames[0].depth, 2);
3118 assert!(!frames[0].external);
3119 assert_eq!(frames[0].step, 6);
3120 assert_eq!(frames[1].function_name.as_deref(), Some("outer"));
3121 assert_eq!(
3122 frames[1].location.as_ref().map(|location| location.line),
3123 Some(4)
3124 );
3125 assert_eq!(frames[1].step, 4);
3126 assert!(frames[2].external);
3127 assert_eq!(frames[2].depth, 0);
3128 assert_eq!(
3129 frames[2].address.as_deref(),
3130 Some("0xaaaa000000000000000000000000000000000001")
3131 );
3132 assert!(map.frames(99).is_empty());
3133
3134 let listing = map.source_listing(6, 1).expect("listing");
3135 assert_eq!(listing.current_line, 8);
3136 assert_eq!(listing.lines.len(), 3);
3137 assert_eq!(listing.lines[1].0, 8);
3138 assert_eq!(listing.lines[1].1.trim(), "x += 1;");
3139 }
3140
3141 #[test]
3142 fn external_calls_map_through_the_callee_contract() {
3143 let callee = "0xbbbb000000000000000000000000000000000002";
3145 let word = format!("0x{:0>64}", callee.trim_start_matches("0x"));
3146 let steps = vec![
3147 step(0, 1, "PUSH1", &[]),
3148 step(
3149 12,
3150 1,
3151 "CALL",
3152 &["0x0", "0x0", "0x0", "0x0", "0x0", &word, "0x0"],
3153 ),
3154 step(21, 2, "JUMPDEST", &[]),
3155 step(21, 2, "ADD", &[]),
3156 step(13, 1, "JUMPDEST", &[]),
3157 step(30, 1, "STOP", &[]),
3158 ];
3159 let root = contract(Some("0xAAAA000000000000000000000000000000000001"));
3160 let map = StepMap::new(&trace(steps), vec![root, contract(Some(callee))]);
3161 assert_eq!(
3162 map.executing_address(1),
3163 Some("0xaaaa000000000000000000000000000000000001")
3164 );
3165 assert_eq!(map.executing_address(2), Some(callee));
3166 assert_eq!(
3167 map.line_key(2),
3168 Some(LineKey {
3169 contract: 1,
3170 source_id: 0,
3171 line: 8
3172 })
3173 );
3174 assert_eq!(map.frame_depth(1), Some(1));
3175 assert_eq!(map.frame_depth(2), Some(3));
3177 assert!(map.is_frame_entry(2));
3178 assert_eq!(map.frame_depth(4), Some(1));
3179 assert_eq!(map.next_source(1), Some(5));
3181 assert_eq!(map.step_into(1), Some(2));
3182 let frames = map.frames(2);
3183 assert_eq!(frames.len(), 4);
3184 assert!(frames[1].external);
3185 assert_eq!(frames[1].address.as_deref(), Some(callee));
3186 assert_eq!(frames[0].function_name.as_deref(), Some("inner"));
3187 }
3188
3189 fn program(address: &str, environment: &str, spans: &[(u64, &str)]) -> ContractDebugInfo {
3192 let info = EthdebugInfo {
3193 compilation: serde_json::Value::Null,
3194 contract_name: "C".to_owned(),
3195 environment: environment.to_owned(),
3196 instructions: spans
3197 .iter()
3198 .map(|(pc, needle)| instruction(*pc, offset_of(needle), needle.len() as u64))
3199 .collect(),
3200 sources: BTreeMap::from([(0, "C.sol".to_owned())]),
3201 variable_locations: BTreeMap::new(),
3202 };
3203 ContractDebugInfo::new(
3204 Some(address),
3205 "C",
3206 info,
3207 BTreeMap::from([(0, SOURCE.to_owned())]),
3208 )
3209 }
3210
3211 const ROOT: &str = "0xaaaa000000000000000000000000000000000001";
3212 const CREATED: &str = "0xcccc000000000000000000000000000000000003";
3213
3214 fn creation(entry_step: usize, exit_step: usize, address: Option<&str>) -> ContractCreation {
3215 ContractCreation {
3216 id: 0,
3217 parent_id: None,
3218 depth: 2,
3219 entry_step: Some(entry_step),
3220 exit_step: Some(exit_step),
3221 create_type: "CREATE".to_owned(),
3222 caller: ROOT.to_owned(),
3223 address: address.map(str::to_owned),
3224 value: "0x0".to_owned(),
3225 init_code: "0x".to_owned(),
3226 gas_limit: 0,
3227 gas_used: None,
3228 output: None,
3229 success: Some(address.is_some()),
3230 error: None,
3231 }
3232 }
3233
3234 fn call(
3235 id: usize,
3236 entry_step: usize,
3237 exit_step: usize,
3238 call_type: &str,
3239 bytecode_address: &str,
3240 ) -> ExecutionCall {
3241 ExecutionCall {
3242 id,
3243 parent_id: None,
3244 depth: 2,
3245 entry_step: Some(entry_step),
3246 exit_step: Some(exit_step),
3247 call_type: call_type.to_owned(),
3248 from: ROOT.to_owned(),
3249 to: ROOT.to_owned(),
3250 bytecode_address: bytecode_address.to_owned(),
3251 value: "0x0".to_owned(),
3252 input: "0x".to_owned(),
3253 gas_limit: 0,
3254 gas_used: None,
3255 output: None,
3256 success: Some(true),
3257 error: None,
3258 }
3259 }
3260
3261 fn root_creates_then_calls() -> TransactionTrace {
3264 let word = format!("0x{:0>64}", CREATED.trim_start_matches("0x"));
3265 let mut trace = trace(vec![
3266 step(0, 1, "PUSH1", &[]),
3267 step(12, 1, "CREATE", &["0x0", "0x0", "0x0"]),
3269 step(21, 2, "JUMPDEST", &[]),
3270 step(21, 2, "RETURN", &[]),
3271 step(13, 1, "JUMPDEST", &[]),
3272 step(
3273 14,
3274 1,
3275 "CALL",
3276 &["0x0", "0x0", "0x0", "0x0", "0x0", &word, "0x0"],
3277 ),
3278 step(21, 2, "JUMPDEST", &[]),
3279 step(30, 1, "STOP", &[]),
3280 ]);
3281 trace
3282 .artifacts
3283 .creations
3284 .push(creation(2, 4, Some(CREATED)));
3285 trace.artifacts.calls.push(call(0, 6, 7, "CALL", CREATED));
3286 trace
3287 }
3288
3289 #[test]
3290 fn creations_map_through_the_created_contracts_creation_program() {
3291 let trace = root_creates_then_calls();
3292 let deployed = program(CREATED, "call", &[(21, "b = 0;")]);
3296 let creation = program(CREATED, "create", &[(21, "x += 1;")]);
3297 let map = StepMap::new(&trace, vec![contract(Some(ROOT)), deployed, creation]);
3298
3299 assert_eq!(map.executing_address(2), Some(CREATED));
3300 assert_eq!(map.storage_address(2), Some(CREATED));
3301 assert_eq!(
3302 map.line_key(2).map(|key| (key.contract, key.line)),
3303 Some((2, 8))
3304 );
3305 assert_eq!(
3306 map.line_key(6).map(|key| (key.contract, key.line)),
3307 Some((1, 5))
3308 );
3309 let frames = map.frames(2);
3311 assert_eq!(frames[0].function_name.as_deref(), Some("inner"));
3312 assert!(frames[1].external);
3313 assert_eq!(frames[1].address.as_deref(), Some(CREATED));
3314 assert_eq!(map.frame_depth(4), Some(1));
3315 }
3316
3317 #[test]
3318 fn a_line_resolves_only_in_the_programs_that_map_it() {
3319 let trace = root_creates_then_calls();
3323 let whole = SOURCE.len() as u64;
3324 let mut deployed = program(CREATED, "call", &[(21, "b = 0;")]);
3325 deployed.info.instructions.push(instruction(20, 0, whole));
3326 let mut creation = program(CREATED, "create", &[(21, "x += 1;")]);
3327 creation.info.instructions.push(instruction(20, 0, whole));
3328 let map = StepMap::new(&trace, vec![deployed, creation]);
3329 let resolved = map.resolve_line(Some("C.sol"), 8).expect("line 8");
3330 assert_eq!(resolved.len(), 1);
3331 assert_eq!(resolved[0].key.contract, 1);
3332 assert_eq!(resolved[0].key.line, 8);
3333 let resolved = map.resolve_line(Some("C.sol"), 10).expect("line 10");
3335 assert_eq!(resolved.len(), 2);
3336 assert!(resolved.iter().all(|line| line.key.line == 1));
3337 }
3338
3339 #[test]
3340 fn a_creation_uses_the_only_program_at_its_address() {
3341 let trace = root_creates_then_calls();
3344 let deployed = program(CREATED, "call", &[(21, "b = 0;")]);
3345 let map = StepMap::new(&trace, vec![contract(Some(ROOT)), deployed]);
3346 assert_eq!(
3347 map.line_key(2).map(|key| (key.contract, key.line)),
3348 Some((1, 5))
3349 );
3350 }
3351
3352 #[test]
3353 fn a_creation_the_backend_did_not_record_has_no_contract() {
3354 let mut trace = root_creates_then_calls();
3355 trace.artifacts.creations.clear();
3356 let creation = program(CREATED, "create", &[(21, "x += 1;")]);
3357 let map = StepMap::new(&trace, vec![contract(Some(ROOT)), creation]);
3358 assert_eq!(map.executing_address(2), None);
3359 assert_eq!(map.line_key(2), None);
3360 assert_eq!(map.executing_address(6), Some(CREATED));
3362 }
3363
3364 #[test]
3365 fn a_deployment_root_prefers_the_creation_program() {
3366 let mut trace = trace(vec![step(21, 1, "JUMPDEST", &[]), step(30, 1, "STOP", &[])]);
3367 trace.to_addr = None;
3368 trace.contract_address = Some(CREATED.to_owned());
3369 let deployed = program(CREATED, "call", &[(21, "b = 0;")]);
3370 let creation = program(CREATED, "create", &[(21, "x += 1;")]);
3371 let map = StepMap::new(&trace, vec![deployed, creation]);
3372 assert_eq!(
3373 map.line_key(0).map(|key| (key.contract, key.line)),
3374 Some((1, 8))
3375 );
3376 }
3377
3378 #[test]
3379 fn frames_are_resolved_from_recorded_calls_before_the_stack() {
3380 let callee = "0xbbbb000000000000000000000000000000000002";
3381 let precompile = "0x0000000000000000000000000000000000000001";
3382 let mut trace = trace(vec![
3384 step(0, 1, "PUSH1", &[]),
3385 step(11, 1, "STATICCALL", &[]),
3386 step(12, 1, "DELEGATECALL", &[]),
3387 step(21, 2, "JUMPDEST", &[]),
3388 step(21, 2, "ADD", &[]),
3389 step(13, 1, "JUMPDEST", &[]),
3390 ]);
3391 trace
3394 .artifacts
3395 .calls
3396 .push(call(0, 2, 2, "STATICCALL", precompile));
3397 trace
3398 .artifacts
3399 .calls
3400 .push(call(1, 3, 5, "DELEGATECALL", callee));
3401 let map = StepMap::new(&trace, vec![contract(Some(ROOT)), contract(Some(callee))]);
3402
3403 assert_eq!(map.executing_address(2), Some(ROOT));
3404 assert_eq!(map.frame_depth(2), Some(1));
3405 assert_eq!(map.executing_address(3), Some(callee));
3406 assert_eq!(map.line_key(3).map(|key| key.contract), Some(1));
3407 assert_eq!(map.storage_address(3), Some(ROOT));
3409 }
3410
3411 const LEGACY_SOURCE: &str = "contract L {
3414 uint256 total;
3415 modifier tracked(uint256 tag) {
3416 uint256 before = total;
3417 _;
3418 total = before + tag;
3419 }
3420 function outer(uint256 a) public returns (uint256 sum) {
3421 uint256 twice = a * 2;
3422 if (twice > 2) {
3423 uint256 inner = twice - 2;
3424 sum = inner;
3425 }
3426 sum = twice + helper(twice);
3427 }
3428 function helper(uint256 x) internal pure returns (uint256 r) {
3429 uint256 y = x + 1;
3430 r = y;
3431 }
3432 function guarded(uint256 v) public tracked(9) {
3433 uint256 kept = v;
3434 total = kept;
3435 }
3436 function set(uint256 v) public {
3437 total = v;
3438 }
3439 function take(bytes calldata data, uint256[] calldata items, uint256 n) public {
3440 total = n;
3441 }
3442}
3443";
3444
3445 fn legacy_offset(needle: &str) -> u64 {
3446 LEGACY_SOURCE.find(needle).expect(needle) as u64
3447 }
3448
3449 fn legacy_contract(code_generator: Option<CodeGenerator>) -> ContractDebugInfo {
3453 let spans: &[(u64, &str)] = &[
3454 (0, "contract L"),
3455 (10, "function outer"),
3458 (11, "uint256 sum"),
3459 (12, "uint256 twice"),
3460 (13, "a * 2"),
3461 (14, "twice > 2"),
3462 (15, "uint256 inner"),
3463 (16, "sum = inner"),
3464 (17, "helper(twice)"),
3465 (18, "sum = twice + helper(twice)"),
3466 (19, "function outer"),
3467 (20, "function helper"),
3469 (21, "uint256 y"),
3470 (22, "r = y"),
3471 (23, "function helper"),
3472 (24, "uint256 r"),
3473 (30, "function guarded"),
3477 (31, "uint256 before"),
3478 (32, "uint256 kept"),
3479 (33, "total = kept"),
3480 (34, "total = before + tag"),
3481 (35, "function guarded"),
3482 (36, "function guarded"),
3483 (40, "function set"),
3486 (41, "total = v"),
3487 (42, "function set"),
3488 (43, "function set"),
3489 (50, "function take"),
3491 (51, "total = n"),
3492 ];
3493 let instructions = spans
3494 .iter()
3495 .map(|(pc, needle)| {
3496 if matches!(pc, 35 | 42) {
3498 instruction_with(
3499 *pc,
3500 legacy_offset(needle),
3501 needle.len() as u64,
3502 json!({"invoke": {}}),
3503 )
3504 } else {
3505 instruction(*pc, legacy_offset(needle), needle.len() as u64)
3506 }
3507 })
3508 .collect();
3509 let info = EthdebugInfo {
3510 compilation: serde_json::Value::Null,
3511 contract_name: "L".to_owned(),
3512 environment: "call".to_owned(),
3513 instructions,
3514 sources: BTreeMap::from([(0, "L.sol".to_owned())]),
3515 variable_locations: BTreeMap::new(),
3516 };
3517 ContractDebugInfo::new(
3518 Some(ROOT),
3519 "L",
3520 info,
3521 BTreeMap::from([(0, LEGACY_SOURCE.to_owned())]),
3522 )
3523 .with_code_generator(code_generator)
3524 }
3525
3526 fn outer_trace() -> TransactionTrace {
3529 let ret = "0x9";
3530 trace(vec![
3531 step(0, 1, "PUSH1", &[]),
3532 step(10, 1, "JUMPDEST", &[ret, "0x5"]),
3533 step(11, 1, "PUSH0", &[ret, "0x5"]),
3534 step(12, 1, "PUSH0", &[ret, "0x5", "0x0"]),
3535 step(13, 1, "MUL", &[ret, "0x5", "0x0", "0x0"]),
3536 step(14, 1, "GT", &[ret, "0x5", "0x0", "0xa"]),
3537 step(15, 1, "PUSH0", &[ret, "0x5", "0x0", "0xa"]),
3538 step(16, 1, "DUP1", &[ret, "0x5", "0x0", "0xa", "0x8"]),
3539 step(18, 1, "DUP2", &[ret, "0x5", "0x8", "0xa"]),
3540 step(17, 1, "JUMP", &[ret, "0x5", "0x8", "0xa", "0x12", "0xa"]),
3541 step(
3542 20,
3543 1,
3544 "JUMPDEST",
3545 &[ret, "0x5", "0x8", "0xa", "0x12", "0xa"],
3546 ),
3547 step(24, 1, "PUSH0", &[ret, "0x5", "0x8", "0xa", "0x12", "0xa"]),
3548 step(
3549 21,
3550 1,
3551 "PUSH0",
3552 &[ret, "0x5", "0x8", "0xa", "0x12", "0xa", "0x0"],
3553 ),
3554 step(
3555 22,
3556 1,
3557 "DUP1",
3558 &[ret, "0x5", "0x8", "0xa", "0x12", "0xa", "0x0", "0xb"],
3559 ),
3560 step(23, 1, "JUMP", &[ret, "0x5", "0x8", "0xa", "0xb", "0x12"]),
3561 step(18, 1, "ADD", &[ret, "0x5", "0x8", "0xa", "0xb"]),
3562 step(19, 1, "JUMP", &[ret, "0x5", "0x15", "0xa"]),
3563 step(0, 1, "STOP", &[]),
3564 ])
3565 }
3566
3567 fn named(variables: &[InferredVariable]) -> Vec<(&str, VariableKind, Option<usize>)> {
3568 variables
3569 .iter()
3570 .map(|variable| (variable.name.as_str(), variable.kind, variable.slot))
3571 .collect()
3572 }
3573
3574 #[test]
3575 fn locals_are_inferred_from_the_legacy_stack_layout() {
3576 let map = StepMap::new(
3577 &outer_trace(),
3578 vec![legacy_contract(Some(CodeGenerator::Legacy))],
3579 );
3580 let trace = outer_trace();
3581
3582 assert_eq!(
3584 map.locals_at(0),
3585 LocalsStatus::Unavailable("no function is executing at this step")
3586 );
3587 let LocalsStatus::Inferred(at_entry) = map.locals_at(1) else {
3591 panic!("{:?}", map.locals_at(1));
3592 };
3593 assert_eq!(named(at_entry), [("sum", VariableKind::Return, None)]);
3594 let LocalsStatus::Inferred(at_return) = map.locals_at(2) else {
3595 panic!("{:?}", map.locals_at(2));
3596 };
3597 assert_eq!(
3598 named(at_return),
3599 [
3600 ("a", VariableKind::Parameter, Some(1)),
3601 ("sum", VariableKind::Return, Some(2))
3602 ]
3603 );
3604 let LocalsStatus::Inferred(at_twice) = map.locals_at(5) else {
3606 panic!("{:?}", map.locals_at(5));
3607 };
3608 assert_eq!(
3609 named(at_twice),
3610 [
3611 ("a", VariableKind::Parameter, Some(1)),
3612 ("sum", VariableKind::Return, Some(2)),
3613 ("twice", VariableKind::Local, Some(3))
3614 ]
3615 );
3616 let values = map.inferred_variables(&trace, 5, None);
3617 assert_eq!(values[0].value.display, "5");
3618 assert_eq!(values[2].value.display, "10");
3619 assert_eq!(values[2].location.offset, 3);
3620 let LocalsStatus::Inferred(in_block) = map.locals_at(7) else {
3622 panic!("{:?}", map.locals_at(7));
3623 };
3624 assert_eq!(
3625 named(in_block).last(),
3626 Some(&("inner", VariableKind::Local, Some(4)))
3627 );
3628 assert_eq!(
3629 map.inferred_variables(&trace, 7, None)[3].value.display,
3630 "8"
3631 );
3632 let LocalsStatus::Inferred(after_block) = map.locals_at(8) else {
3633 panic!("{:?}", map.locals_at(8));
3634 };
3635 assert_eq!(after_block.len(), 3);
3636 assert_eq!(
3637 map.inferred_variables(&trace, 8, None)[1].value.display,
3638 "8"
3639 );
3640
3641 let LocalsStatus::Inferred(helper_entry) = map.locals_at(10) else {
3646 panic!("{:?}", map.locals_at(10));
3647 };
3648 assert_eq!(
3649 named(helper_entry),
3650 [
3651 ("x", VariableKind::Parameter, Some(5)),
3652 ("r", VariableKind::Return, Some(6))
3653 ]
3654 );
3655 let at_entry = map.inferred_variables(&trace, 10, None);
3656 assert_eq!(
3657 at_entry[1].value.status,
3658 crate::DebugValueStatus::Unavailable
3659 );
3660 let helper_values = map.inferred_variables(&trace, 13, None);
3661 assert_eq!(
3662 helper_values
3663 .iter()
3664 .map(|variable| (variable.name.as_str(), variable.value.display.as_str()))
3665 .collect::<Vec<_>>(),
3666 [("x", "10"), ("r", "0"), ("y", "11")]
3667 );
3668 assert_eq!(helper_values[2].location.offset, 7);
3669 let LocalsStatus::Inferred(back) = map.locals_at(15) else {
3670 panic!("{:?}", map.locals_at(15));
3671 };
3672 assert_eq!(named(back).len(), 3);
3673 assert_eq!(
3674 map.inferred_variables(&trace, 15, None)[2].value.display,
3675 "10"
3676 );
3677 }
3678
3679 #[test]
3680 fn a_modifier_places_itself_and_the_function_it_runs_for() {
3681 let ret = "0x9";
3682 let trace = trace(vec![
3683 step(30, 1, "JUMPDEST", &[ret, "0x7"]),
3684 step(31, 1, "PUSH0", &[ret, "0x7", "0x9"]),
3687 step(32, 1, "PUSH0", &[ret, "0x7", "0x9", "0x3"]),
3689 step(33, 1, "SSTORE", &[ret, "0x7", "0x9", "0x3", "0x7"]),
3690 step(34, 1, "SSTORE", &[ret, "0x7", "0x9", "0x3"]),
3692 ]);
3693 let map = StepMap::new(&trace, vec![legacy_contract(Some(CodeGenerator::Legacy))]);
3694 let LocalsStatus::Inferred(modifier) = map.locals_at(1) else {
3695 panic!("{:?}", map.locals_at(1));
3696 };
3697 assert_eq!(
3698 named(modifier),
3699 [
3700 ("tag", VariableKind::Parameter, Some(2)),
3701 ("before", VariableKind::Local, Some(3))
3702 ]
3703 );
3704 let LocalsStatus::Inferred(body) = map.locals_at(3) else {
3707 panic!("{:?}", map.locals_at(3));
3708 };
3709 assert_eq!(
3710 named(body),
3711 [
3712 ("v", VariableKind::Parameter, Some(1)),
3713 ("kept", VariableKind::Local, Some(4))
3714 ]
3715 );
3716 assert_eq!(
3717 map.inferred_variables(&trace, 3, None)[1].value.display,
3718 "7"
3719 );
3720 let LocalsStatus::Inferred(resumed) = map.locals_at(4) else {
3721 panic!("{:?}", map.locals_at(4));
3722 };
3723 assert_eq!(
3724 named(resumed),
3725 [
3726 ("tag", VariableKind::Parameter, Some(2)),
3727 ("before", VariableKind::Local, Some(3))
3728 ]
3729 );
3730 assert_eq!(
3731 map.inferred_variables(&trace, 4, None)[1].value.display,
3732 "3"
3733 );
3734 }
3735
3736 #[test]
3737 fn calldata_slices_read_the_frames_calldata() {
3738 let ret = "0x9";
3743 let mut trace = trace(vec![
3744 step(
3745 50,
3746 1,
3747 "JUMPDEST",
3748 &[ret, "0x44", "0x2", "0x4", "0x2", "0x5"],
3749 ),
3750 step(51, 1, "PUSH0", &[ret, "0x44", "0x2", "0x4", "0x2", "0x5"]),
3751 ]);
3752 trace.input_data = format!(
3753 "0xaabbccdd{}{}6162",
3754 "0000000000000000000000000000000000000000000000000000000000000003",
3755 "0000000000000000000000000000000000000000000000000000000000000004"
3756 );
3757 let map = StepMap::new(&trace, vec![legacy_contract(Some(CodeGenerator::Legacy))]);
3758 let LocalsStatus::Inferred(layout) = map.locals_at(1) else {
3759 panic!("{:?}", map.locals_at(1));
3760 };
3761 assert_eq!(
3762 layout
3763 .iter()
3764 .map(|variable| (variable.name.as_str(), variable.slot, variable.words))
3765 .collect::<Vec<_>>(),
3766 [
3767 ("data", Some(1), 2),
3768 ("items", Some(3), 2),
3769 ("n", Some(5), 1)
3770 ]
3771 );
3772 let values = map.inferred_variables(&trace, 1, None);
3773 assert_eq!(
3774 values
3775 .iter()
3776 .map(|variable| (variable.name.as_str(), variable.value.display.as_str()))
3777 .collect::<Vec<_>>(),
3778 [("data", "0x6162"), ("items", "[3, 4]"), ("n", "5")]
3779 );
3780 assert_eq!(values[0].value.raw.as_deref(), Some("0x6162"));
3781 }
3782
3783 #[test]
3784 fn parameters_are_placed_by_the_first_statement_of_a_body_without_locals() {
3785 let trace = trace(vec![
3789 step(40, 1, "JUMPDEST", &["0x9", "0x7"]),
3790 step(41, 1, "DUP1", &["0x9", "0x7"]),
3791 step(41, 1, "SSTORE", &["0x9", "0x7", "0x7", "0x0"]),
3792 ]);
3793 let map = StepMap::new(&trace, vec![legacy_contract(Some(CodeGenerator::Legacy))]);
3794 let LocalsStatus::Inferred(at_entry) = map.locals_at(0) else {
3795 panic!("{:?}", map.locals_at(0));
3796 };
3797 assert!(at_entry.is_empty());
3798 let LocalsStatus::Inferred(in_body) = map.locals_at(2) else {
3799 panic!("{:?}", map.locals_at(2));
3800 };
3801 assert_eq!(named(in_body), [("v", VariableKind::Parameter, Some(1))]);
3802 assert_eq!(
3803 map.inferred_variables(&trace, 2, None)[0].value.display,
3804 "7"
3805 );
3806 }
3807
3808 #[test]
3809 fn the_dispatchers_jump_to_the_body_places_a_public_function() {
3810 let selector = "0x9c";
3815 let mut trace = trace(vec![
3816 step(0, 1, "PUSH1", &[]),
3817 step(40, 1, "JUMPDEST", &[selector]),
3818 step(42, 1, "JUMP", &[selector, "0x9", "0x7", "0x2b"]),
3819 step(43, 1, "JUMPDEST", &[selector, "0x9", "0x7"]),
3820 step(41, 1, "SSTORE", &[selector, "0x9", "0x7", "0x0"]),
3821 ]);
3822 let map = StepMap::new(&trace, vec![legacy_contract(Some(CodeGenerator::Legacy))]);
3823 let LocalsStatus::Inferred(at_entry) = map.locals_at(1) else {
3824 panic!("{:?}", map.locals_at(1));
3825 };
3826 assert!(at_entry.is_empty());
3827 let LocalsStatus::Inferred(in_body) = map.locals_at(4) else {
3828 panic!("{:?}", map.locals_at(4));
3829 };
3830 assert_eq!(named(in_body), [("v", VariableKind::Parameter, Some(2))]);
3831 assert_eq!(
3832 map.inferred_variables(&trace, 4, None)[0].value.display,
3833 "7"
3834 );
3835
3836 let selector = super::hex_bytes(&function_selector("set(uint256)").expect("selector"));
3840 trace.input_data = format!("0x{selector}{:064x}", 8);
3841 let map = StepMap::new(&trace, vec![legacy_contract(Some(CodeGenerator::Legacy))]);
3842 let LocalsStatus::Inferred(in_body) = map.locals_at(4) else {
3843 panic!("{:?}", map.locals_at(4));
3844 };
3845 assert_eq!(named(in_body), [("v", VariableKind::Parameter, Some(3))]);
3846 trace.input_data = format!("0x{selector}{:064x}", 7);
3848 let map = StepMap::new(&trace, vec![legacy_contract(Some(CodeGenerator::Legacy))]);
3849 let LocalsStatus::Inferred(in_body) = map.locals_at(4) else {
3850 panic!("{:?}", map.locals_at(4));
3851 };
3852 assert_eq!(named(in_body), [("v", VariableKind::Parameter, Some(2))]);
3853 }
3854
3855 #[test]
3856 fn a_placed_function_places_its_modifiers() {
3857 let selector = "0x9c";
3863 let ret = "0x9";
3864 let trace = trace(vec![
3865 step(0, 1, "PUSH1", &[]),
3866 step(30, 1, "JUMPDEST", &[selector]),
3867 step(35, 1, "JUMP", &[selector, ret, "0x5", "0x24"]),
3868 step(36, 1, "JUMPDEST", &[selector, ret, "0x5"]),
3869 step(31, 1, "DUP1", &[selector, ret, "0x5", "0x9", "0x9"]),
3870 step(32, 1, "PUSH0", &[selector, ret, "0x5", "0x9", "0x9"]),
3871 step(
3872 33,
3873 1,
3874 "SSTORE",
3875 &[selector, ret, "0x5", "0x9", "0x9", "0x5"],
3876 ),
3877 step(34, 1, "SSTORE", &[selector, ret, "0x5", "0x9", "0x9"]),
3878 ]);
3879 let map = StepMap::new(&trace, vec![legacy_contract(Some(CodeGenerator::Legacy))]);
3880 let LocalsStatus::Inferred(modifier) = map.locals_at(4) else {
3881 panic!("{:?}", map.locals_at(4));
3882 };
3883 assert_eq!(
3884 named(modifier),
3885 [
3886 ("tag", VariableKind::Parameter, Some(3)),
3887 ("before", VariableKind::Local, Some(4))
3888 ]
3889 );
3890 let LocalsStatus::Inferred(body) = map.locals_at(6) else {
3891 panic!("{:?}", map.locals_at(6));
3892 };
3893 assert_eq!(
3894 named(body),
3895 [
3896 ("v", VariableKind::Parameter, Some(2)),
3897 ("kept", VariableKind::Local, Some(5))
3898 ]
3899 );
3900 assert_eq!(
3901 map.inferred_variables(&trace, 6, None)[1].value.display,
3902 "5"
3903 );
3904 let LocalsStatus::Inferred(resumed) = map.locals_at(7) else {
3905 panic!("{:?}", map.locals_at(7));
3906 };
3907 assert_eq!(
3908 named(resumed),
3909 [
3910 ("tag", VariableKind::Parameter, Some(3)),
3911 ("before", VariableKind::Local, Some(4))
3912 ]
3913 );
3914 let values = map.inferred_variables(&trace, 7, None);
3915 assert_eq!(values[0].value.display, "9");
3916 assert_eq!(values[1].value.display, "9");
3917 }
3918
3919 #[test]
3920 fn a_function_reached_by_fallthrough_was_inlined_and_has_no_slots() {
3921 let ret = "0x9";
3925 let trace = trace(vec![
3926 step(0, 1, "PUSH1", &[]),
3927 step(10, 1, "JUMPDEST", &[ret, "0x5"]),
3928 step(11, 1, "PUSH0", &[ret, "0x5"]),
3929 step(12, 1, "PUSH0", &[ret, "0x5", "0x0"]),
3930 step(21, 1, "PUSH0", &[ret, "0x5", "0x0", "0xa"]),
3931 step(22, 1, "DUP1", &[ret, "0x5", "0x0", "0xa", "0xb"]),
3932 step(18, 1, "ADD", &[ret, "0x5", "0x0", "0xa", "0xb"]),
3933 ]);
3934 let map = StepMap::new(&trace, vec![legacy_contract(Some(CodeGenerator::Legacy))]);
3935 assert_eq!(
3936 map.location(4).and_then(|location| location.function_name),
3937 Some("helper".to_owned())
3938 );
3939 assert_eq!(
3940 map.locals_at(4),
3941 LocalsStatus::Unavailable(
3942 "this function was inlined by the optimizer, so its variables have no stack \
3943 slots of their own"
3944 )
3945 );
3946 assert_eq!(
3947 map.location(6).and_then(|location| location.function_name),
3948 Some("outer".to_owned())
3949 );
3950 let LocalsStatus::Inferred(back) = map.locals_at(6) else {
3951 panic!("{:?}", map.locals_at(6));
3952 };
3953 assert_eq!(
3954 named(back),
3955 [
3956 ("a", VariableKind::Parameter, Some(1)),
3957 ("sum", VariableKind::Return, Some(2)),
3958 ("twice", VariableKind::Local, Some(3))
3959 ]
3960 );
3961 }
3962
3963 #[test]
3964 fn locals_are_not_inferred_for_the_via_ir_pipeline_or_an_unknown_one() {
3965 let via_ir = StepMap::new(
3966 &outer_trace(),
3967 vec![legacy_contract(Some(CodeGenerator::ViaIr))],
3968 );
3969 assert!(matches!(
3970 via_ir.locals_at(5),
3971 LocalsStatus::Unavailable(reason) if reason.contains("via-IR pipeline")
3972 ));
3973 assert!(via_ir
3974 .inferred_variables(&outer_trace(), 5, None)
3975 .is_empty());
3976 let unknown = StepMap::new(&outer_trace(), vec![legacy_contract(None)]);
3977 assert!(matches!(
3978 unknown.locals_at(5),
3979 LocalsStatus::Unavailable(reason) if reason.contains("not known")
3980 ));
3981 }
3982
3983 #[test]
3984 fn steps_without_debug_info_have_no_location() {
3985 let map = StepMap::new(&outer_calls_inner(), Vec::new());
3986 assert!(!map.has_source());
3987 assert_eq!(map.line_key(3), None);
3988 assert_eq!(map.frame_depth(3), Some(0));
3989 assert_eq!(map.next_source(3), None);
3991 assert_eq!(map.previous_source(3), None);
3992 assert!(map.resolve_line(None, 3).is_err());
3993 assert_eq!(map.frames(3).len(), 1);
3994 }
3995
3996 #[test]
3997 fn steps_without_source_are_transparent_to_line_stepping() {
3998 let map = StepMap::new(
4000 &trace(vec![
4001 step(0, 1, "PUSH1", &[]),
4002 step(10, 1, "JUMPDEST", &[]),
4003 step(11, 1, "ADD", &[]),
4004 step(99, 1, "MUL", &[]),
4005 step(11, 1, "SWAP1", &[]),
4006 step(14, 1, "POP", &[]),
4007 step(30, 1, "STOP", &[]),
4008 ]),
4009 vec![contract(None)],
4010 );
4011 assert_eq!(map.line_key(3), None);
4012 assert_eq!(map.frame_depth(3), Some(1));
4013 assert_eq!(map.next_source(2), Some(5));
4015 assert_eq!(map.step_into(2), Some(5));
4016 assert!(!map.is_line_start(3));
4017 assert!(!map.is_line_start(4));
4018 assert_eq!(map.previous_source(5), Some(2));
4020 assert_eq!(map.previous_source(4), Some(2));
4021 assert_eq!(map.previous_source(3), Some(2));
4022 assert_eq!(map.previous_source(2), Some(1));
4023 }
4024
4025 #[test]
4026 fn generated_code_belongs_to_the_statement_and_the_epilogue_pops_the_function() {
4027 let map = StepMap::new(
4030 &trace(vec![
4031 step(0, 1, "PUSH1", &[]),
4032 step(10, 1, "JUMPDEST", &[]),
4033 step(11, 1, "ADD", &[]),
4034 step(40, 1, "JUMPDEST", &[]),
4035 step(40, 1, "SSTORE", &[]),
4036 step(11, 1, "SWAP1", &[]),
4037 step(14, 1, "POP", &[]),
4038 step(30, 1, "STOP", &[]),
4039 ]),
4040 vec![contract(None)],
4041 );
4042 let helper = map.location(4).expect("location");
4043 assert!(helper.generated);
4044 assert_eq!(helper.line, 3);
4045 assert_eq!(helper.function_name.as_deref(), Some("outer"));
4046 assert_eq!(map.line_key(4), key(3));
4047 assert_eq!(map.function_id(4).map(|id| id.function), Some(0));
4048 assert!(!map.is_line_start(4));
4049 assert!(!map.is_line_start(5));
4050 assert_eq!(map.frame_depth(4), Some(1));
4052 assert_eq!(map.frame_depth(7), Some(0));
4053 assert_eq!(map.line_key(7), None);
4054 assert!(!map.location(7).expect("location").generated);
4055 assert_eq!(map.next_source(2), Some(6));
4056 assert_eq!(map.finish(2), Some(7));
4057 assert_eq!(map.reverse_finish(4), Some(0));
4058 assert_eq!(map.previous_source(6), Some(2));
4059 }
4060
4061 #[test]
4062 fn a_lone_instruction_on_another_line_belongs_to_the_line_around_it() {
4063 let map = StepMap::new(
4066 &trace(vec![
4067 step(0, 1, "PUSH1", &[]),
4068 step(10, 1, "JUMPDEST", &[]),
4069 step(12, 1, "PUSH2", &[]),
4070 step(10, 1, "SWAP1", &[]),
4071 step(11, 1, "ADD", &[]),
4072 step(14, 1, "POP", &[]),
4073 step(30, 1, "STOP", &[]),
4074 ]),
4075 vec![contract(None)],
4076 );
4077 assert_eq!(map.line_key(2), key(2));
4078 assert_eq!(map.location(2).expect("location").line, 4);
4079 assert!(!map.is_line_start(2));
4080 assert!(!map.is_line_start(3));
4081 assert_eq!(map.next_source(1), Some(4));
4082 assert_eq!(map.previous_source(4), Some(1));
4083 assert_eq!(map.line_key(5), key(5));
4085 assert!(map.is_line_start(5));
4086 }
4087
4088 #[test]
4089 fn a_jump_onto_a_function_entry_is_a_call_even_when_it_is_active() {
4090 let map = StepMap::new(
4094 &trace(vec![
4095 step(0, 1, "PUSH1", &[]),
4096 step(1, 1, "JUMP", &[]),
4097 step(10, 1, "JUMPDEST", &[]),
4098 step(12, 1, "JUMP", &[]),
4099 step(20, 1, "JUMPDEST", &[]),
4100 step(21, 1, "JUMP", &[]),
4101 step(10, 1, "JUMPDEST", &[]),
4102 step(14, 1, "JUMP", &[]),
4103 step(21, 1, "SWAP1", &[]),
4104 step(21, 1, "JUMP", &[]),
4105 step(13, 1, "JUMPDEST", &[]),
4106 step(14, 1, "POP", &[]),
4107 step(30, 1, "STOP", &[]),
4108 ]),
4109 vec![contract(None)],
4110 );
4111 let contract = &map.contracts()[0];
4112 assert_eq!(contract.function_entry_at_pc(10), Some(0));
4113 assert_eq!(contract.function_entry_at_pc(20), Some(1));
4114 assert_eq!(contract.function_entry_at_pc(11), None);
4115 let depths = (0..13)
4116 .map(|step| map.frame_depth(step).expect("depth"))
4117 .collect::<Vec<_>>();
4118 assert_eq!(depths, vec![0, 0, 1, 1, 2, 2, 3, 3, 2, 2, 1, 1, 0]);
4119 assert!(map.is_frame_entry(6));
4120 let frames = map.frames(6);
4121 assert_eq!(
4122 frames
4123 .iter()
4124 .map(|frame| frame.function_name.as_deref().unwrap_or("-"))
4125 .collect::<Vec<_>>(),
4126 vec!["outer", "inner", "outer", "-"]
4127 );
4128 assert_eq!(map.finish(6), Some(8));
4129 assert_eq!(map.reverse_finish(6), Some(5));
4130 assert_eq!(map.finish(4), Some(10));
4131 }
4132
4133 #[test]
4134 fn a_recursive_call_returns_at_the_address_read_off_the_stack() {
4135 let map = StepMap::new(
4140 &trace(vec![
4141 step(0, 1, "PUSH1", &[]),
4142 step(1, 1, "JUMP", &[]),
4143 step(10, 1, "JUMPDEST", &[]),
4144 step(12, 1, "JUMP", &["0xd", "0x1", "0xa"]),
4145 step(10, 1, "JUMPDEST", &["0x1"]),
4146 step(11, 1, "ADD", &["0x2"]),
4147 step(14, 1, "JUMP", &["0xd"]),
4148 step(13, 1, "JUMPDEST", &[]),
4149 step(14, 1, "POP", &[]),
4150 step(30, 1, "STOP", &[]),
4151 ]),
4152 vec![contract(None)],
4153 );
4154 let depths = (0..10)
4155 .map(|step| map.frame_depth(step).expect("depth"))
4156 .collect::<Vec<_>>();
4157 assert_eq!(depths, vec![0, 0, 1, 1, 2, 2, 2, 1, 1, 0]);
4158 assert!(map.is_frame_entry(4));
4159 assert_eq!(map.finish(4), Some(7));
4160 assert_eq!(map.frames(5).len(), 3);
4161 assert_eq!(map.frames(7).len(), 2);
4162 assert!(map.contracts()[0].is_jumpdest(13));
4163 assert!(!map.contracts()[0].is_jumpdest(99));
4164 }
4165
4166 #[test]
4167 fn jump_markers_decide_calls_and_returns_without_entry_points() {
4168 let whole = SOURCE.len() as u64;
4173 let info = EthdebugInfo {
4174 compilation: serde_json::Value::Null,
4175 contract_name: "C".to_owned(),
4176 environment: "runtime".to_owned(),
4177 instructions: vec![
4178 instruction(0, 0, whole),
4179 instruction(1, 0, whole),
4180 instruction(10, offset_of("function outer"), 90),
4181 instruction(11, offset_of("uint256 b = a + 1;"), 18),
4182 instruction_with(
4183 12,
4184 offset_of("inner(b);"),
4185 9,
4186 json!({"invoke": {"identifier": "outer"}}),
4187 ),
4188 instruction(13, offset_of("inner(b);"), 9),
4189 instruction_with(14, offset_of("b = 0;"), 6, json!({"return": {}})),
4190 instruction(30, 0, whole),
4191 ],
4192 sources: BTreeMap::from([(0, "C.sol".to_owned())]),
4193 variable_locations: BTreeMap::new(),
4194 };
4195 let marked =
4196 ContractDebugInfo::new(None, "C", info, BTreeMap::from([(0, SOURCE.to_owned())]));
4197 assert_eq!(marked.jump_marker_at_pc(12), JumpMarker::Call);
4198 assert_eq!(marked.jump_marker_at_pc(14), JumpMarker::Return);
4199 assert_eq!(marked.jump_marker_at_pc(13), JumpMarker::None);
4200 let map = StepMap::new(
4201 &trace(vec![
4202 step(0, 1, "PUSH1", &[]),
4203 step(1, 1, "JUMP", &[]),
4204 step(10, 1, "JUMPDEST", &[]),
4205 step(12, 1, "JUMP", &[]),
4206 step(11, 1, "ADD", &[]),
4207 step(14, 1, "JUMP", &[]),
4208 step(13, 1, "JUMPDEST", &[]),
4209 step(14, 1, "POP", &[]),
4210 step(30, 1, "STOP", &[]),
4211 ]),
4212 vec![marked],
4213 );
4214 let depths = (0..9)
4215 .map(|step| map.frame_depth(step).expect("depth"))
4216 .collect::<Vec<_>>();
4217 assert_eq!(depths, vec![0, 0, 1, 1, 1, 1, 1, 1, 0]);
4218 assert!(!map.is_frame_entry(4));
4219 assert_eq!(map.frames(4).len(), 2);
4220 assert_eq!(map.finish(4), Some(8));
4221 let map = StepMap::new(
4223 &trace(vec![
4224 step(0, 1, "PUSH1", &[]),
4225 step(1, 1, "JUMP", &[]),
4226 step(10, 1, "JUMPDEST", &[]),
4227 step(12, 1, "JUMP", &[]),
4228 step(20, 1, "JUMPDEST", &[]),
4229 step(21, 1, "JUMP", &[]),
4230 step(13, 1, "JUMPDEST", &[]),
4231 step(30, 1, "STOP", &[]),
4232 ]),
4233 vec![contract(None)],
4234 );
4235 assert_eq!(map.frame_depth(4), Some(2));
4236 assert_eq!(map.frame_depth(6), Some(1));
4237 }
4238
4239 #[test]
4240 fn marked_jumps_into_generated_helpers_do_not_pop_the_function() {
4241 let whole = SOURCE.len() as u64;
4245 let info = EthdebugInfo {
4246 compilation: serde_json::Value::Null,
4247 contract_name: "C".to_owned(),
4248 environment: "runtime".to_owned(),
4249 instructions: vec![
4250 instruction(0, 0, whole),
4251 instruction(1, 0, whole),
4252 instruction(10, offset_of("function outer"), 90),
4253 instruction_with(
4254 11,
4255 offset_of("uint256 b = a + 1;"),
4256 18,
4257 json!({"invoke": {}}),
4258 ),
4259 instruction_with(40, 0, whole, json!({"return": {}})),
4260 instruction(14, offset_of("b = 0;"), 6),
4261 instruction(30, 0, whole),
4262 ],
4263 sources: BTreeMap::from([(0, "C.sol".to_owned())]),
4264 variable_locations: BTreeMap::new(),
4265 };
4266 let contract =
4267 ContractDebugInfo::new(None, "C", info, BTreeMap::from([(0, SOURCE.to_owned())]));
4268 let map = StepMap::new(
4269 &trace(vec![
4270 step(0, 1, "PUSH1", &[]),
4271 step(1, 1, "JUMP", &[]),
4272 step(10, 1, "JUMPDEST", &[]),
4273 step(11, 1, "JUMP", &[]),
4274 step(40, 1, "JUMPDEST", &[]),
4275 step(40, 1, "JUMP", &[]),
4276 step(14, 1, "JUMPDEST", &[]),
4277 step(30, 1, "STOP", &[]),
4278 ]),
4279 vec![contract],
4280 );
4281 let depths = (0..8)
4282 .map(|step| map.frame_depth(step).expect("depth"))
4283 .collect::<Vec<_>>();
4284 assert_eq!(depths, vec![0, 0, 1, 1, 1, 1, 1, 0]);
4285 assert!(map.location(4).expect("location").generated);
4286 assert_eq!(
4287 map.location(6).expect("location").function_name.as_deref(),
4288 Some("outer")
4289 );
4290 assert_eq!(map.next_source(3), Some(6));
4291 assert_eq!(map.finish(3), Some(7));
4292 }
4293
4294 #[test]
4295 fn contract_info_indexes_lines_and_functions() {
4296 let contract = contract(Some("0xABCD"));
4297 assert_eq!(contract.address.as_deref(), Some("0xabcd"));
4298 assert_eq!(contract.line_of(0, 0), Some(1));
4299 assert_eq!(
4300 contract.line_of(0, SOURCE.find("inner(b)").unwrap() as u64),
4301 Some(4)
4302 );
4303 assert_eq!(contract.line_count(0), Some(11));
4304 assert_eq!(contract.line_text(0, 8).map(str::trim), Some("x += 1;"));
4305 assert_eq!(contract.line_text(0, 99), None);
4306 assert_eq!(
4307 contract.function_at_pc(21).map(|f| f.name.as_str()),
4308 Some("inner")
4309 );
4310 assert_eq!(contract.function_at_pc(0), None);
4311 assert_eq!(contract.function_at_pc(999), None);
4312 assert_eq!(contract.functions[0].declaration_line, 2);
4313 assert_eq!(contract.effective_line(0, 3), Some(3));
4314 assert_eq!(contract.effective_line(0, 6), Some(1));
4315 assert_eq!(contract.effective_line(0, 0), None);
4316 assert_eq!(contract.effective_line(0, 12), None);
4317 assert_eq!(contract.effective_line(7, 1), None);
4318
4319 assert_eq!(normalize_address("0XAbC"), "0xabc");
4320 assert_eq!(
4321 address_from_word("0x000000000000000000000000AAAA000000000000000000000000000000000001"),
4322 Some("0xaaaa000000000000000000000000000000000001".to_owned())
4323 );
4324 assert_eq!(address_from_word("0x0"), None);
4325 assert_eq!(address_from_word("zz"), None);
4326 }
4327}