@@ -48,6 +48,10 @@ struct Info {
4848}
4949
5050impl Info {
51+ fn has_dynamic ( & self ) -> bool {
52+ self . tinfo . is_some ( ) || self . children . values ( ) . any ( Self :: has_dynamic)
53+ }
54+
5155 fn to_alloy_type ( & self , is_root : bool ) -> Vec < DynSolType > {
5256 if let Some ( ( name, _) ) = & self . tname {
5357 if matches ! ( name, DynSolType :: Bytes ) {
@@ -130,6 +134,7 @@ impl Info {
130134struct ArgsResult {
131135 data : Info ,
132136 not_bool : HashSet < Vec < u32 > > ,
137+ decoder_active : bool ,
133138}
134139
135140impl ArgsResult {
@@ -209,11 +214,101 @@ impl ArgsResult {
209214 }
210215}
211216
217+ fn is_conditional_guard ( code : & [ u8 ] , pc : usize ) -> bool {
218+ let Some ( ( & next_op, rest) ) = code. get ( pc..) . and_then ( |code| code. split_first ( ) ) else {
219+ return false ;
220+ } ;
221+ let ( next_op, rest) = if next_op == op:: ISZERO {
222+ let Some ( ( & next_op, rest) ) = rest. split_first ( ) else {
223+ return false ;
224+ } ;
225+ ( next_op, rest)
226+ } else {
227+ ( next_op, rest)
228+ } ;
229+ if !( op:: PUSH1 ..=op:: PUSH4 ) . contains ( & next_op) {
230+ return false ;
231+ }
232+ let push_size = ( next_op - op:: PUSH0 ) as usize ;
233+ rest. get ( push_size) == Some ( & op:: JUMPI )
234+ }
235+
236+ fn direct_min_calldata_words ( code : & [ u8 ] , pc : usize ) -> Option < usize > {
237+ let pc = pc + usize:: from ( code. get ( pc) == Some ( & op:: JUMPDEST ) ) ;
238+ let & push_op = code. get ( pc) ?;
239+ if !( op:: PUSH1 ..=op:: PUSH4 ) . contains ( & push_op) {
240+ return None ;
241+ }
242+ let push_size = ( push_op - op:: PUSH0 ) as usize ;
243+ if code. get ( pc + 1 + push_size) != Some ( & op:: CALLDATASIZE )
244+ || code. get ( pc + 2 + push_size) != Some ( & op:: LT )
245+ {
246+ return None ;
247+ }
248+ let min_size = code
249+ . get ( pc + 1 ..pc + 1 + push_size) ?
250+ . iter ( )
251+ . fold ( 0usize , |value, & byte| ( value << 8 ) | usize:: from ( byte) ) ;
252+ ( min_size >= 4 && ( min_size - 4 ) . is_multiple_of ( 32 ) ) . then_some ( ( min_size - 4 ) / 32 )
253+ }
254+
255+ fn dense_min_calldata_words ( code : & [ u8 ] , pc : usize , packed : & Element < Label > ) -> Option < usize > {
256+ let pc = pc + usize:: from ( code. get ( pc) == Some ( & op:: JUMPDEST ) ) ;
257+ if code. get ( pc) != Some ( & op:: DUP1 ) {
258+ return None ;
259+ }
260+ let & push_op = code. get ( pc + 1 ) ?;
261+ if !( op:: PUSH1 ..=op:: PUSH3 ) . contains ( & push_op) {
262+ return None ;
263+ }
264+ let push_size = ( push_op - op:: PUSH0 ) as usize ;
265+ let mask_bytes = code. get ( pc + 2 ..pc + 2 + push_size) ?;
266+ if code. get ( pc + 2 + push_size) != Some ( & op:: AND )
267+ || code. get ( pc + 3 + push_size) != Some ( & op:: CALLDATASIZE )
268+ || code. get ( pc + 4 + push_size) != Some ( & op:: LT )
269+ || mask_bytes[ ..mask_bytes. len ( ) - 1 ]
270+ . iter ( )
271+ . any ( |& byte| byte != 0xff )
272+ || mask_bytes. last ( ) != Some ( & 0xfe )
273+ {
274+ return None ;
275+ }
276+
277+ let mask = mask_bytes
278+ . iter ( )
279+ . fold ( U256 :: ZERO , |value, & byte| ( value << 8 ) | U256 :: from ( byte) ) ;
280+ let packed = U256 :: from_be_bytes ( packed. data ) ;
281+ let min_size: usize = ( packed & mask) . try_into ( ) . ok ( ) ?;
282+ ( min_size >= 4 && ( min_size - 4 ) . is_multiple_of ( 32 ) ) . then_some ( ( min_size - 4 ) / 32 )
283+ }
284+
212285fn analyze (
213286 vm : & mut Vm < Label , CallDataImpl > ,
214287 args : & mut ArgsResult ,
215288 ret : StepResult < Label > ,
216289) -> Result < ( ) , Box < dyn std:: error:: Error > > {
290+ if matches ! (
291+ ret. op,
292+ op:: SLOAD
293+ | op:: SSTORE
294+ | op:: TLOAD
295+ | op:: TSTORE
296+ | op:: KECCAK256
297+ | op:: BALANCE
298+ | op:: EXTCODESIZE
299+ | op:: EXTCODEHASH
300+ | op:: EXTCODECOPY
301+ | op:: CALL
302+ | op:: CALLCODE
303+ | op:: DELEGATECALL
304+ | op:: STATICCALL
305+ | op:: CREATE
306+ | op:: CREATE2
307+ | op:: SELFDESTRUCT
308+ | op:: LOG0 ..=op:: LOG4
309+ ) {
310+ args. decoder_active = false ;
311+ }
217312 match ret {
218313 StepResult {
219314 op : op @ ( op:: CALLDATALOAD | op:: CALLDATACOPY ) ,
@@ -402,6 +497,20 @@ fn analyze(
402497 }
403498 }
404499
500+ StepResult {
501+ op : op:: SHL ,
502+ args : [ shift, elabel ! ( Label :: Arg ( Val { offset, path, .. } ) ) , ..] ,
503+ ..
504+ } if path. is_empty ( ) && args. decoder_active => {
505+ if is_conditional_guard ( vm. code , vm. pc )
506+ && let Ok ( shift) = usize:: try_from ( shift)
507+ && ( 8 ..=256 ) . contains ( & shift)
508+ && shift. is_multiple_of ( 8 )
509+ {
510+ args. set_tname ( & path, offset, DynSolType :: FixedBytes ( shift / 8 ) , 20 ) ;
511+ }
512+ }
513+
405514 StepResult {
406515 op : op @ op:: MUL ,
407516 args :
@@ -551,6 +660,33 @@ fn analyze(
551660 args. set_tname ( & path, offset, DynSolType :: Uint ( 8 ) , 12 ) ;
552661 }
553662
663+ // Vyper validates narrow ABI scalars by rejecting non-zero high bits.
664+ // Modern code branches directly on SHR, while older versions insert
665+ // ISZERO before the branch.
666+ StepResult {
667+ op : op:: SHR ,
668+ args : [ shift, elabel ! ( Label :: Arg ( Val { offset, path, .. } ) ) , ..] ,
669+ ..
670+ } if args. decoder_active => {
671+ if is_conditional_guard ( vm. code , vm. pc )
672+ && let Ok ( width) = usize:: try_from ( shift)
673+ && width < 256
674+ {
675+ let inferred_type = if width == 1 {
676+ Some ( DynSolType :: Bool )
677+ } else if width == 160 {
678+ Some ( DynSolType :: Address )
679+ } else if width. is_multiple_of ( 8 ) {
680+ Some ( DynSolType :: Uint ( width) )
681+ } else {
682+ None
683+ } ;
684+ if let Some ( inferred_type) = inferred_type {
685+ args. set_tname ( & path, offset, inferred_type, 20 ) ;
686+ }
687+ }
688+ }
689+
554690 StepResult {
555691 op : op:: SDIV | op:: SMOD ,
556692 args : match_first_two ! ( elabel!( Label :: Arg ( Val { offset, path, .. } ) ) , _) ,
@@ -710,7 +846,10 @@ pub fn function_arguments(code: &[u8], selector: &Selector, gas_limit: u32) -> V
710846 selector : * selector,
711847 } ;
712848 let mut vm = Vm :: new ( code, & calldata) ;
713- let mut args = ArgsResult :: default ( ) ;
849+ let mut args = ArgsResult {
850+ decoder_active : true ,
851+ ..ArgsResult :: default ( )
852+ } ;
714853 let mut gas_used = 0 ;
715854 let real_gas_limit = if gas_limit == 0 {
716855 5e4 as u32
@@ -724,6 +863,15 @@ pub fn function_arguments(code: &[u8], selector: &Selector, gas_limit: u32) -> V
724863 return vec ! [ ] ;
725864 }
726865
866+ // Seed Vyper's static ABI head from its minimum-calldata-size check. This
867+ // preserves arguments that the one concrete execution path never reads.
868+ let mut min_static_words = direct_min_calldata_words ( vm. code , vm. pc ) ;
869+ if let Ok ( packed) = vm. stack . peek ( )
870+ && let Some ( words) = dense_min_calldata_words ( vm. code , vm. pc , packed)
871+ {
872+ min_static_words = Some ( words) ;
873+ }
874+
727875 while !vm. stopped {
728876 if cfg ! ( feature = "trace_arguments" ) {
729877 println ! ( "args: {args:?}" ) ;
@@ -748,6 +896,16 @@ pub fn function_arguments(code: &[u8], selector: &Selector, gas_limit: u32) -> V
748896 }
749897 }
750898
899+ // Dynamic minimum sizes include tail words, so only apply this fallback
900+ // when execution found no dynamic structure at all.
901+ if !args. data . has_dynamic ( )
902+ && let Some ( words) = min_static_words
903+ {
904+ for offset in ( 0 ..words * 32 ) . step_by ( 32 ) {
905+ args. get_or_create ( & [ offset as u32 ] ) ;
906+ }
907+ }
908+
751909 if args. data . children . is_empty ( ) {
752910 vec ! [ ]
753911 } else {
0 commit comments