@@ -15,8 +15,10 @@ impl Aggregator {
1515 pub fn build_collapsed_stacks ( steps : & [ TraceStep ] ) -> Vec < CollapsedStack > {
1616 debug ! ( "Building collapsed stacks from {} execution steps" , steps. len( ) ) ;
1717
18- // Map to aggregate stacks: stack_string -> (total_gas, last_pc, target_address, reverted)
19- let mut stack_map: HashMap < String , ( u64 , u64 , Option < String > , bool ) > = HashMap :: new ( ) ;
18+ let registry = atupa_adapters:: AdapterRegistry :: new ( ) ;
19+
20+ // Map to aggregate stacks: stack_string -> (total_gas, last_pc, target_address, resolved_label, reverted)
21+ let mut stack_map: HashMap < String , ( u64 , u64 , Option < String > , Option < String > , bool ) > = HashMap :: new ( ) ;
2022
2123 // Current call stack
2224 let mut call_stack: Vec < String > = Vec :: new ( ) ;
@@ -35,21 +37,75 @@ impl Aggregator {
3537 call_stack. push ( "CALL" . to_string ( ) ) ;
3638 }
3739
38- // Extract Target Address if this is a Call opcode
40+ // Extract Target Address & Parse Function Selector if this is a Call opcode
3941 let mut target_address = None ;
42+ let mut resolved_label = None ;
43+
4044 if operation == "CALL" || operation == "STATICCALL" || operation == "DELEGATECALL" || operation == "CALLCODE" {
4145 if let Some ( stack) = & step. stack {
4246 if stack. len ( ) >= 2 {
43- // In Geth/Anvil trace stack array, the end of the array is the top of the stack.
44- // CALL takes: gas, address, value, argsOffset, argsLength, retOffset, retLength
47+ // Extract target address (second item from top)
4548 let hex_addr = & stack[ stack. len ( ) - 2 ] ;
4649 let clean_hex = hex_addr. trim_start_matches ( "0x" ) ;
47- // EVM addresses are exactly 40 chars, padded to 64 chars in stack elements
4850 if clean_hex. len ( ) >= 40 {
4951 let extracted = & clean_hex[ clean_hex. len ( ) - 40 ..] ;
5052 target_address = Some ( format ! ( "0x{}" , extracted) ) ;
5153 }
5254 }
55+
56+ // Attempt to extract the 4-byte selector from Memory using Offset & Length
57+ let mut args_offset_idx = None ;
58+ let mut args_length_idx = None ;
59+
60+ if operation == "CALL" || operation == "CALLCODE" {
61+ if stack. len ( ) >= 5 {
62+ args_offset_idx = Some ( stack. len ( ) - 4 ) ;
63+ args_length_idx = Some ( stack. len ( ) - 5 ) ;
64+ }
65+ } else if ( operation == "DELEGATECALL" || operation == "STATICCALL" ) && stack. len ( ) >= 4 {
66+ args_offset_idx = Some ( stack. len ( ) - 3 ) ;
67+ args_length_idx = Some ( stack. len ( ) - 4 ) ;
68+ }
69+
70+ if let ( Some ( off_idx) , Some ( len_idx) ) = ( args_offset_idx, args_length_idx) {
71+ let offset_str = stack[ off_idx] . trim_start_matches ( "0x" ) ;
72+ let len_str = stack[ len_idx] . trim_start_matches ( "0x" ) ;
73+
74+ if let ( Ok ( offset) , Ok ( length) ) = (
75+ usize:: from_str_radix ( offset_str, 16 ) ,
76+ usize:: from_str_radix ( len_str, 16 )
77+ ) {
78+ if length >= 4 {
79+ if let Some ( mem) = & step. memory {
80+ let word_idx = offset / 32 ;
81+ let byte_offset = offset % 32 ;
82+ let hex_offset = byte_offset * 2 ; // Each byte is 2 hex chars
83+
84+ if let Some ( word) = mem. get ( word_idx) {
85+ let clean_word = word. trim_start_matches ( "0x" ) ;
86+ let selector_opt = if clean_word. len ( ) >= hex_offset + 8 {
87+ let selector = & clean_word[ hex_offset..hex_offset + 8 ] ;
88+ Some ( format ! ( "0x{}" , selector) )
89+ } else if word_idx + 1 < mem. len ( ) {
90+ // The 4-byte selector spans across two memory boundary words
91+ let p1 = & clean_word[ hex_offset..] ;
92+ let needed = 8 - p1. len ( ) ;
93+ let next_word = mem[ word_idx + 1 ] . trim_start_matches ( "0x" ) ;
94+ if next_word. len ( ) >= needed {
95+ let p2 = & next_word[ ..needed] ;
96+ Some ( format ! ( "0x{}{}" , p1, p2) )
97+ } else { None }
98+ } else { None } ;
99+
100+ // Try resolving the label
101+ if let Some ( sel) = selector_opt {
102+ resolved_label = registry. resolve ( target_address. as_deref ( ) , Some ( & sel) ) ;
103+ }
104+ }
105+ }
106+ }
107+ }
108+ }
53109 }
54110 }
55111
@@ -61,27 +117,28 @@ impl Aggregator {
61117 } ;
62118
63119 // Accumulate gas cost and flags
64- let entry = stack_map. entry ( stack_str) . or_insert ( ( 0 , 0 , None , false ) ) ;
120+ let entry = stack_map. entry ( stack_str) . or_insert ( ( 0 , 0 , None , None , false ) ) ;
65121 entry. 0 += step. gas_cost ;
66122 entry. 1 = step. pc ;
67123 if target_address. is_some ( ) {
68124 entry. 2 = target_address;
69125 }
126+ if resolved_label. is_some ( ) {
127+ entry. 3 = resolved_label;
128+ }
70129 if step. reverted {
71- entry. 3 = true ;
130+ entry. 4 = true ;
72131 }
73-
74- // NOTE: Reverts naturally bubble up visually because if an internal call hits REVERT,
75- // the specific reverting stack path gets the `entry.3 = true` flag.
76132 }
77133
78134 let mut stacks: Vec < CollapsedStack > = stack_map
79135 . into_iter ( )
80- . map ( |( stack, ( weight, pc, target_address, reverted) ) | CollapsedStack {
136+ . map ( |( stack, ( weight, pc, target_address, resolved_label , reverted) ) | CollapsedStack {
81137 stack,
82138 weight,
83139 last_pc : Some ( pc) ,
84140 target_address,
141+ resolved_label,
85142 reverted,
86143 } )
87144 . collect ( ) ;
@@ -144,5 +201,46 @@ mod tests {
144201 assert ! ( revert_stack. reverted) ;
145202 assert_eq ! ( revert_stack. weight, 200 ) ;
146203 }
147- }
148204
205+ #[ test]
206+ fn test_aggregator_memory_selector_extraction ( ) {
207+ // Stack for CALL:
208+ // gas, address, value, argsOffset, argsLength, retOffset, retLength
209+ // Top of stack is at the end.
210+ // We want argsOffset to be "0x20" (32 bytes), argsLength to be "0x04" (4 bytes)
211+ // stack[len-4] = argsOffset
212+ // stack[len-5] = argsLength
213+
214+ let stack = vec ! [
215+ "0x0" . to_string( ) , // retLength
216+ "0x0" . to_string( ) , // retOffset
217+ "0x4" . to_string( ) , // argsLength
218+ "0x20" . to_string( ) , // argsOffset (byte 32)
219+ "0x0" . to_string( ) , // value
220+ "0x0000000000000000000000001111111111111111111111111111111111111111" . to_string( ) , // target address
221+ "0x1000" . to_string( ) , // gas
222+ ] ;
223+
224+ // Memory array (32-byte chunks as 64-char hex strings)
225+ // We set argsOffset = 32, so it looks in mem[1].
226+ // "beforeInitialize" selector is 0x18a9d381. We'll pad the rest with zeroes.
227+ let memory = vec ! [
228+ "0000000000000000000000000000000000000000000000000000000000000000" . to_string( ) , // word 0
229+ "18a9d38100000000000000000000000000000000000000000000000000000000" . to_string( ) , // word 1
230+ ] ;
231+
232+ let steps = vec ! [
233+ TraceStep { pc: 0 , op: "CALL" . into( ) , gas: 1000 , gas_cost: 50 , depth: 1 , stack: Some ( stack) , memory: Some ( memory) , error: None , reverted: false } ,
234+ TraceStep { pc: 1 , op: "STOP" . into( ) , gas: 900 , gas_cost: 0 , depth: 1 , stack: None , memory: None , error: None , reverted: false } ,
235+ ] ;
236+
237+ let stacks = Aggregator :: build_collapsed_stacks ( & steps) ;
238+ let call_stack = stacks. iter ( ) . find ( |s| s. stack == "CALL;CALL" ) . expect ( "Should find CALL" ) ;
239+
240+ // Ensure that the target address was resolved successfully
241+ assert_eq ! ( call_stack. target_address. as_deref( ) , Some ( "0x1111111111111111111111111111111111111111" ) ) ;
242+
243+ // Ensure that the specific Uniswap v4 Hook was decoded
244+ assert_eq ! ( call_stack. resolved_label. as_deref( ) , Some ( "Uniswapv4: beforeInitialize" ) ) ;
245+ }
246+ }
0 commit comments