1use alloy_dyn_abi::{DynSolType, DynSolValue};
29use alloy_primitives::{Address, Bytes, U256};
30use edb_common::{
31 types::{CallResult, EdbSolValue, ExecutionFrameId, Trace},
32 EdbContext,
33};
34use eyre::Result;
35use foundry_compilers::{artifacts::Contract, Artifact};
36use revm::{
37 bytecode::OpCode,
38 context::{ContextTr, CreateScheme, JournalTr},
39 database::CacheDB,
40 interpreter::{
41 interpreter_types::{InputsTr, Jumps},
42 CallInputs, CallOutcome, CreateInputs, CreateOutcome, Interpreter,
43 },
44 Database, DatabaseCommit, DatabaseRef, Inspector,
45};
46use serde::{Deserialize, Serialize};
47use std::{
48 collections::HashMap,
49 ops::{Deref, DerefMut},
50 sync::Arc,
51};
52use tracing::{debug, error};
53
54use crate::{
55 analysis::{dyn_sol_type, AnalysisResult, UserDefinedTypeRef, VariableRef, UVID},
56 USID,
57};
58
59pub const MAGIC_SNAPSHOT_NUMBER: U256 = U256::from_be_bytes([
61 0x20, 0x15, 0x05, 0x02, 0xff, 0xff, 0xff, 0xff, 0x20, 0x24, 0x01, 0x02, 0xff, 0xff, 0xff, 0xff,
62 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
63]);
64
65pub const MAGIC_VARIABLE_UPDATE_NUMBER: U256 = U256::from_be_bytes([
67 0x20, 0x25, 0x02, 0x08, 0xff, 0x20, 0x25, 0x09, 0x16, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
68 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
69]);
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct HookSnapshot<DB>
74where
75 DB: Database + DatabaseCommit + DatabaseRef + Clone,
76 <CacheDB<DB> as Database>::Error: Clone,
77 <DB as Database>::Error: Clone,
78{
79 pub target_address: Address,
81 pub bytecode_address: Address,
83 pub database: Arc<CacheDB<DB>>,
85 pub locals: HashMap<String, Option<Arc<EdbSolValue>>>,
87 pub state_variables: HashMap<String, Option<Arc<EdbSolValue>>>,
89 pub usid: USID,
91}
92
93#[derive(Debug, Clone)]
98pub struct HookSnapshots<DB>
99where
100 DB: Database + DatabaseCommit + DatabaseRef + Clone,
101 <CacheDB<DB> as Database>::Error: Clone,
102 <DB as Database>::Error: Clone,
103{
104 snapshots: Vec<(ExecutionFrameId, Option<HookSnapshot<DB>>)>,
107}
108
109impl<DB> Default for HookSnapshots<DB>
110where
111 DB: Database + DatabaseCommit + DatabaseRef + Clone,
112 <CacheDB<DB> as Database>::Error: Clone,
113 <DB as Database>::Error: Clone,
114{
115 fn default() -> Self {
116 Self { snapshots: Vec::new() }
117 }
118}
119
120impl<DB> Deref for HookSnapshots<DB>
121where
122 DB: Database + DatabaseCommit + DatabaseRef + Clone,
123 <CacheDB<DB> as Database>::Error: Clone,
124 <DB as Database>::Error: Clone,
125{
126 type Target = Vec<(ExecutionFrameId, Option<HookSnapshot<DB>>)>;
127
128 fn deref(&self) -> &Self::Target {
129 &self.snapshots
130 }
131}
132
133impl<DB> DerefMut for HookSnapshots<DB>
134where
135 DB: Database + DatabaseCommit + DatabaseRef + Clone,
136 <CacheDB<DB> as Database>::Error: Clone,
137 <DB as Database>::Error: Clone,
138{
139 fn deref_mut(&mut self) -> &mut Self::Target {
140 &mut self.snapshots
141 }
142}
143
144impl<DB> IntoIterator for HookSnapshots<DB>
145where
146 DB: Database + DatabaseCommit + DatabaseRef + Clone,
147 <CacheDB<DB> as Database>::Error: Clone,
148 <DB as Database>::Error: Clone,
149{
150 type Item = (ExecutionFrameId, Option<HookSnapshot<DB>>);
151 type IntoIter = std::vec::IntoIter<Self::Item>;
152
153 fn into_iter(self) -> Self::IntoIter {
154 self.snapshots.into_iter()
155 }
156}
157
158impl<DB> HookSnapshots<DB>
159where
160 DB: Database + DatabaseCommit + DatabaseRef + Clone,
161 <CacheDB<DB> as Database>::Error: Clone,
162 <DB as Database>::Error: Clone,
163{
164 pub fn get_snapshot(&self, frame_id: ExecutionFrameId) -> Option<&HookSnapshot<DB>> {
166 self.snapshots
167 .iter()
168 .find(|(id, _)| *id == frame_id)
169 .and_then(|(_, snapshot)| snapshot.as_ref())
170 }
171
172 pub fn get_frames_with_hooks(&self) -> Vec<ExecutionFrameId> {
174 self.snapshots
175 .iter()
176 .filter_map(
177 |(frame_id, snapshot)| {
178 if snapshot.is_some() {
179 Some(*frame_id)
180 } else {
181 None
182 }
183 },
184 )
185 .collect()
186 }
187
188 fn add_frame_placeholder(&mut self, frame_id: ExecutionFrameId) {
190 self.snapshots.push((frame_id, None));
191 }
192
193 fn update_last_frame_with_snapshot(
195 &mut self,
196 frame_id: ExecutionFrameId,
197 snapshot: HookSnapshot<DB>,
198 ) {
199 if let Some((last_frame_id, slot)) = self.snapshots.last_mut() {
200 if last_frame_id != &frame_id {
201 error!("Mismatched frame IDs: expected {}, got {}", last_frame_id, frame_id);
202 }
203 if slot.is_none() {
204 *slot = Some(snapshot);
206 return;
207 }
208 }
209
210 self.snapshots.push((frame_id, Some(snapshot)));
211 }
212}
213
214#[derive(Debug)]
216pub struct HookSnapshotInspector<'a, DB>
217where
218 DB: Database + DatabaseCommit + DatabaseRef + Clone,
219 <CacheDB<DB> as Database>::Error: Clone,
220 <DB as Database>::Error: Clone,
221{
222 trace: &'a Trace,
224
225 analysis: &'a HashMap<Address, AnalysisResult>,
227
228 pub snapshots: HookSnapshots<DB>,
230
231 frame_stack: Vec<ExecutionFrameId>,
233
234 current_trace_id: usize,
236
237 creation_hooks: Vec<(Bytes, Bytes, Bytes)>,
239
240 uvid_values: HashMap<UVID, Arc<EdbSolValue>>,
242}
243
244impl<'a, DB> HookSnapshotInspector<'a, DB>
245where
246 DB: Database + DatabaseCommit + DatabaseRef + Clone,
247 <CacheDB<DB> as Database>::Error: Clone,
248 <DB as Database>::Error: Clone,
249{
250 pub fn new(trace: &'a Trace, analysis: &'a HashMap<Address, AnalysisResult>) -> Self {
252 Self {
253 trace,
254 analysis,
255 snapshots: HookSnapshots::default(),
256 frame_stack: Vec::new(),
257 current_trace_id: 0,
258 creation_hooks: Vec::new(),
259 uvid_values: HashMap::new(),
260 }
261 }
262
263 pub fn with_creation_hooks(
265 &mut self,
266 hooks: Vec<(&Contract, &Contract, &Bytes)>,
267 ) -> Result<()> {
268 for (original, hooked, args) in hooks {
269 self.creation_hooks.push((
270 original
271 .get_bytecode_bytes()
272 .ok_or(eyre::eyre!("Failed to get bytecode for contract"))?
273 .as_ref()
274 .clone(),
275 hooked
276 .get_bytecode_bytes()
277 .ok_or(eyre::eyre!("Failed to get bytecode for contract"))?
278 .as_ref()
279 .clone(),
280 args.clone(),
281 ));
282 }
283
284 Ok(())
285 }
286
287 pub fn into_snapshots(self) -> HookSnapshots<DB> {
289 self.snapshots
290 }
291
292 fn current_frame_id(&self) -> Option<ExecutionFrameId> {
294 self.frame_stack.last().copied()
295 }
296
297 fn push_frame(&mut self, trace_id: usize) {
299 let frame_id = ExecutionFrameId::new(trace_id, 0);
300 self.frame_stack.push(frame_id);
301
302 self.snapshots.add_frame_placeholder(frame_id);
304 }
305
306 fn pop_frame(&mut self) -> Option<ExecutionFrameId> {
308 if let Some(frame_id) = self.frame_stack.pop() {
309 if let Some(parent_frame_id) = self.frame_stack.last_mut() {
311 parent_frame_id.increment_re_entry();
312 }
313
314 if let Some(current_frame_id) = self.current_frame_id() {
316 self.snapshots.add_frame_placeholder(current_frame_id);
317 }
318
319 Some(frame_id)
320 } else {
321 None
322 }
323 }
324
325 fn check_and_record_hook(
327 &mut self,
328 data: &[u8],
329 interp: &Interpreter,
330 ctx: &mut EdbContext<DB>,
331 ) {
332 let address = self
333 .current_frame_id()
334 .and_then(|frame_id| self.trace.get(frame_id.trace_entry_id()))
335 .map(|entry| entry.code_address)
336 .unwrap_or(interp.input.target_address());
337
338 let usid_opt = if data.len() >= 32 {
339 U256::from_be_slice(&data[..32]).try_into().ok()
340 } else {
341 error!("KECCAK256 input data too short for snapshot, skipping");
342 return;
343 };
344
345 let Some(usid) = usid_opt else {
346 error!("Hook call data does not contain valid USID, skipping snapshot");
347 return;
348 };
349
350 let mut inner = ctx.journal().to_inner();
352 let changes = inner.finalize();
353 let mut snap = ctx.db().clone();
354 snap.commit(changes);
355
356 let Some(step) = self.analysis.get(&address).and_then(|a| a.usid_to_step.get(&usid)) else {
358 error!(
359 address=?address,
360 usid=?usid,
361 "No analysis step found for address and USID, skipping hook snapshot",
362 );
363 return;
364 };
365
366 let mut locals = HashMap::new();
368 for variable in &step.read().accessible_variables {
369 if variable.declaration().state_variable {
370 continue;
371 }
372 let uvid = variable.id();
373 let name = variable.declaration().name.clone();
374 locals.insert(name, self.uvid_values.get(&uvid).cloned());
375 }
376
377 if let Some(current_frame_id) = self.current_frame_id() {
379 if let Some(entry) = self.trace.get(current_frame_id.trace_entry_id()) {
380 let hook_snapshot = HookSnapshot {
382 target_address: entry.target,
383 bytecode_address: entry.code_address,
384 database: Arc::new(snap),
385 locals,
386 usid,
387 state_variables: HashMap::new(), };
389
390 self.snapshots.update_last_frame_with_snapshot(current_frame_id, hook_snapshot);
391 } else {
392 error!("No trace entry found for frame {}", current_frame_id);
393 }
394 } else {
395 error!("No current frame to update with hook snapshot");
396 }
397 }
398
399 fn check_and_record_variable_update(
400 &mut self,
401 data: &[u8],
402 interp: &Interpreter,
403 _ctx: &mut EdbContext<DB>,
404 ) {
405 let address = self
406 .current_frame_id()
407 .and_then(|frame_id| self.trace.get(frame_id.trace_entry_id()))
408 .map(|entry| entry.code_address)
409 .unwrap_or(interp.input.target_address());
410
411 if data.len() < 96 {
419 error!(
420 address=?address,
421 "KECCAK256 input data too short for variable update value, skipping"
422 );
423 return;
424 }
425
426 let Some(uvid) = U256::from_be_slice(&data[..32]).try_into().ok() else {
427 error!("Hook call data does not contain valid UVID, skipping snapshot");
428 return;
429 };
430
431 let offset = U256::from_be_slice(&data[32..64]);
432 if offset != U256::from(0x60) {
433 error!(
434 address=?address,
435 uvid=?uvid,
436 offset=?offset,
437 "Unexpected offset for variable update value, skipping"
438 );
439 return;
440 }
441
442 let length = U256::from_be_slice(&data[64..96]);
443 let length_usize = match usize::try_from(length) {
444 Ok(l) => l,
445 Err(_) => {
446 error!(
447 address=?address,
448 uvid=?uvid,
449 length=?length,
450 "Variable update value length too large, skipping"
451 );
452 return;
453 }
454 };
455
456 let decoded_data = &data[96..96 + length_usize];
457
458 let Some(analysis) = self.analysis.get(&address) else {
459 error!(
460 address=?address,
461 uvid=?uvid,
462 "No analysis found for address, skipping variable update recording",
463 );
464 return;
465 };
466 let Some(variable) = analysis.uvid_to_variable.get(&uvid) else {
467 error!(
468 address=?address,
469 uvid=?uvid,
470 "No variable found for address and UVID, skipping variable update recording",
471 );
472 return;
473 };
474
475 let value =
476 match decode_variable_value(&analysis.user_defined_types, variable, decoded_data) {
477 Ok(v) => v,
478 Err(e) => {
479 error!(
480 address=?address,
481 uvid=?uvid,
482 variable=?variable.declaration().type_descriptions.type_string,
483 type_name = ?variable.declaration().type_name,
484 data=?hex::encode(decoded_data),
485 error=?e,
486 );
487 return;
488 }
489 };
490
491 debug!(
492 uvid=?uvid,
493 address=?address,
494 variable=?variable.declaration().name,
495 value=?value,
496 "Found variable update",
497 );
498
499 self.uvid_values.insert(uvid, Arc::new(value.into()));
500 }
501
502 fn check_and_apply_creation_hooks(
504 &mut self,
505 inputs: &mut CreateInputs,
506 ctx: &mut EdbContext<DB>,
507 ) {
508 let Ok(account) = ctx.journaled_state.load_account(inputs.caller) else {
510 error!("Failed to load account for caller {:?}", inputs.caller);
511 return;
512 };
513
514 let nonce = account.info.nonce;
516 let predicted_address = inputs.created_address(nonce);
517
518 for (original_bytecode, hooked_bytecode, constructor_args) in &self.creation_hooks {
519 if inputs.init_code.len() >= constructor_args.len() {
521 let input_args_start = inputs.init_code.len() - constructor_args.len();
522 let input_args = &inputs.init_code[input_args_start..];
523
524 if input_args == constructor_args.as_ref() {
526 let input_bytecode = &inputs.init_code[..input_args_start];
528
529 if input_bytecode == original_bytecode.as_ref() {
532 let mut new_init_code = Vec::from(hooked_bytecode.as_ref());
534 new_init_code.extend_from_slice(constructor_args.as_ref());
535 inputs.init_code = Bytes::from(new_init_code);
536
537 inputs.scheme = CreateScheme::Custom { address: predicted_address };
539
540 debug!(
542 "Replaced creation bytecode with hooked version for {:?} -> {:?}",
543 inputs.caller, predicted_address
544 );
545
546 break; }
548 }
549 }
550 }
551 }
552
553 pub fn clear(&mut self) {
555 self.snapshots.snapshots.clear();
556 self.frame_stack.clear();
557 self.current_trace_id = 0;
558 }
559}
560
561impl<'a, DB> Inspector<EdbContext<DB>> for HookSnapshotInspector<'a, DB>
562where
563 DB: Database + DatabaseCommit + DatabaseRef + Clone,
564 <CacheDB<DB> as Database>::Error: Clone,
565 <DB as Database>::Error: Clone,
566{
567 fn step(&mut self, interp: &mut Interpreter, ctx: &mut EdbContext<DB>) {
568 let opcode = unsafe { OpCode::new_unchecked(interp.bytecode.opcode()) };
570
571 if opcode != OpCode::KECCAK256 {
572 return;
574 }
575
576 let Some(data) = interp.stack.pop().ok().and_then(|offset_u256| {
577 let data = interp.stack.pop().ok().and_then(|len_u256| {
578 let offset = usize::try_from(offset_u256).ok()?;
579 let len = usize::try_from(len_u256).ok()?;
580 let data = interp.memory.slice_len(offset, len);
581
582 let _ = interp.stack.push(len_u256);
583 Some(data)
584 });
585
586 let _ = interp.stack.push(offset_u256);
587 data
588 }) else {
589 error!("Failed to read KECCAK256 input data from stack");
590 return;
591 };
592
593 if data.len() < 32 {
594 return;
596 }
597
598 let magic_number = U256::from_be_slice(&data[..32]);
599
600 if magic_number == MAGIC_SNAPSHOT_NUMBER {
601 self.check_and_record_hook(&data[32..], interp, ctx);
602 } else if magic_number == MAGIC_VARIABLE_UPDATE_NUMBER {
603 self.check_and_record_variable_update(&data[32..], interp, ctx);
604 }
605 }
606
607 fn call(
608 &mut self,
609 _context: &mut EdbContext<DB>,
610 _inputs: &mut CallInputs,
611 ) -> Option<CallOutcome> {
612 self.push_frame(self.current_trace_id);
614 self.current_trace_id += 1;
615 None
616 }
617
618 fn call_end(
619 &mut self,
620 _context: &mut EdbContext<DB>,
621 inputs: &CallInputs,
622 outcome: &mut CallOutcome,
623 ) {
624 let Some(frame_id) = self.pop_frame() else { return };
625
626 let Some(entry) = self.trace.get(frame_id.trace_entry_id()) else { return };
627
628 if entry.result != Some(outcome.into()) {
629 error!(
631 target_address = inputs.target_address.to_string(),
632 bytecode_address = inputs.bytecode_address.to_string(),
633 "Call outcome mismatch at frame {}: expected {:?}, got {:?} ({:?})",
634 frame_id,
635 entry.result,
636 Into::<CallResult>::into(&outcome),
637 outcome,
638 );
639 }
640 }
641
642 fn create(
643 &mut self,
644 context: &mut EdbContext<DB>,
645 inputs: &mut CreateInputs,
646 ) -> Option<CreateOutcome> {
647 self.check_and_apply_creation_hooks(inputs, context);
649
650 self.push_frame(self.current_trace_id);
652 self.current_trace_id += 1;
653 None
654 }
655
656 fn create_end(
657 &mut self,
658 _context: &mut EdbContext<DB>,
659 _inputs: &CreateInputs,
660 outcome: &mut CreateOutcome,
661 ) {
662 let Some(frame_id) = self.pop_frame() else { return };
664
665 let Some(entry) = self.trace.get(frame_id.trace_entry_id()) else { return };
666
667 if entry.result.as_ref().map(|r| r.result()) != Some(outcome.result.result) {
670 error!(
672 "Create outcome mismatch at frame {}: expected {:?}, got {:?}",
673 frame_id, entry.result, outcome
674 );
675 }
676 }
677}
678
679impl<DB> HookSnapshots<DB>
681where
682 DB: Database + DatabaseCommit + DatabaseRef + Clone,
683 <CacheDB<DB> as Database>::Error: Clone,
684 <DB as Database>::Error: Clone,
685{
686 pub fn print_summary(&self) {
688 println!(
689 "\n\x1b[36m╔══════════════════════════════════════════════════════════════════╗\x1b[0m"
690 );
691 println!(
692 "\x1b[36m║ HOOK SNAPSHOT INSPECTOR SUMMARY ║\x1b[0m"
693 );
694 println!(
695 "\x1b[36m╚══════════════════════════════════════════════════════════════════╝\x1b[0m\n"
696 );
697
698 let total_frames = self.len();
700 let hook_frames = self.get_frames_with_hooks().len();
701
702 println!("\x1b[33m📊 Overall Statistics:\x1b[0m");
703 println!(" Total frames tracked: \x1b[32m{total_frames}\x1b[0m");
704 println!(" Frames with hooks: \x1b[32m{hook_frames}\x1b[0m");
705 println!(
706 " Hook trigger rate: \x1b[32m{:.1}%\x1b[0m",
707 if total_frames > 0 { hook_frames as f64 / total_frames as f64 * 100.0 } else { 0.0 }
708 );
709
710 if self.is_empty() {
711 println!("\n\x1b[90m No execution frames were tracked.\x1b[0m");
712 return;
713 }
714
715 println!("\n\x1b[33m🎯 Hook Trigger Details:\x1b[0m");
716 println!(
717 "\x1b[90m─────────────────────────────────────────────────────────────────\x1b[0m"
718 );
719
720 use std::collections::HashMap;
722 let mut frame_groups: HashMap<ExecutionFrameId, Vec<&HookSnapshot<DB>>> = HashMap::new();
723 let mut frame_order = Vec::new();
724
725 for (frame_id, snapshot) in &self.snapshots {
726 if !frame_groups.contains_key(frame_id) {
727 frame_order.push(*frame_id);
728 }
729
730 match snapshot {
731 Some(hook_snapshot) => {
732 frame_groups.entry(*frame_id).or_default().push(hook_snapshot);
733 }
734 None => {
735 frame_groups.entry(*frame_id).or_default();
737 }
738 }
739 }
740
741 for (display_idx, frame_id) in frame_order.iter().enumerate() {
743 let hooks = frame_groups.get(frame_id).unwrap();
744
745 if hooks.is_empty() {
746 println!(
748 " \x1b[90m[{:3}] Frame {}\x1b[0m (trace.{}, re-entry {}) - No hooks",
749 display_idx,
750 frame_id,
751 frame_id.trace_entry_id(),
752 frame_id.re_entry_count()
753 );
754 } else {
755 let usids: Vec<_> = hooks.iter().map(|h| h.usid).collect();
757 let hook_count = hooks.len();
758 #[allow(deprecated)]
759 let addresses: std::collections::HashSet<_> =
760 hooks.iter().map(|h| h.bytecode_address).collect();
761
762 println!(
763 "\n \x1b[32m[{:3}] Frame {}\x1b[0m (trace.{}, re-entry {})",
764 display_idx,
765 frame_id,
766 frame_id.trace_entry_id(),
767 frame_id.re_entry_count()
768 );
769 println!(
770 " └─ \x1b[33m{} Hook{} Triggered\x1b[0m",
771 hook_count,
772 if hook_count == 1 { "" } else { "s" }
773 );
774
775 for address in &addresses {
777 println!(" ├─ Address: \x1b[36m{address:?}\x1b[0m");
778 }
779
780 if usids.len() == 1 {
782 println!(" └─ USID: \x1b[36m{}\x1b[0m", usids[0]);
783 } else if usids.len() <= 10 {
784 let usid_list: Vec<String> = usids.iter().map(|u| u.to_string()).collect();
786 println!(" └─ USIDs: \x1b[36m[{}]\x1b[0m", usid_list.join(", "));
787 } else {
788 let first_few: Vec<String> =
790 usids.iter().take(3).map(|u| u.to_string()).collect();
791 let last_few: Vec<String> =
792 usids.iter().rev().take(3).rev().map(|u| u.to_string()).collect();
793
794 if first_few.last() == last_few.first() {
795 println!(
797 " └─ USIDs: \x1b[36m[{} ... {} total]\x1b[0m",
798 first_few.join(", "),
799 usids.len()
800 );
801 } else {
802 println!(
803 " └─ USIDs: \x1b[36m[{}, ... {}, {} total]\x1b[0m",
804 first_few.join(", "),
805 last_few.join(", "),
806 usids.len()
807 );
808 }
809 }
810 }
811 }
812
813 println!(
814 "\n\x1b[90m─────────────────────────────────────────────────────────────────\x1b[0m"
815 );
816 println!("\x1b[33m💡 Magic Snapshot Number:\x1b[0m {MAGIC_SNAPSHOT_NUMBER:?}");
817 }
818}
819
820pub fn decode_variable_value(
850 user_defined_types: &HashMap<usize, UserDefinedTypeRef>,
851 variable: &VariableRef,
852 data: &[u8],
853) -> Result<DynSolValue> {
854 let type_name = variable
855 .type_name()
856 .ok_or(eyre::eyre!("Failed to get variable type: no type name in the declaration"))?;
857 let Some(variable_type): Option<DynSolType> = dyn_sol_type(user_defined_types, type_name)
858 else {
859 return Err(eyre::eyre!("Failed to get variable type: no type string in the declaration"));
860 };
861 let value = variable_type
862 .abi_decode(data)
863 .map_err(|e| eyre::eyre!("Failed to decode variable value: {}", e))?;
864 Ok(value)
865}