Skip to content

Commit 7b5e0ae

Browse files
committed
arguments: improve vyper quality
1 parent 88c2fab commit 7b5e0ae

1 file changed

Lines changed: 159 additions & 1 deletion

File tree

‎src/arguments/mod.rs‎

Lines changed: 159 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ struct Info {
4848
}
4949

5050
impl 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 {
130134
struct ArgsResult {
131135
data: Info,
132136
not_bool: HashSet<Vec<u32>>,
137+
decoder_active: bool,
133138
}
134139

135140
impl 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+
212285
fn 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

Comments
 (0)