1use alloy_dyn_abi::DynSolType;
2use alloy_primitives::{
3 B256, Bytes, I256, U256, keccak256,
4 map::{B256IndexSet, HashMap, IndexSet},
5};
6use foundry_common::Analysis;
7use foundry_compilers::ProjectPathsConfig;
8use solar::{
9 ast::{
10 self,
11 BinOpKind::{Add, BitAnd, BitOr, BitXor, Div, Mul, Pow, Rem, Shl, Shr, Sub},
12 Visit,
13 },
14 interface::{Span, source_map::FileName},
15};
16use std::{
17 cell::RefCell,
18 ops::ControlFlow,
19 sync::{Arc, OnceLock},
20};
21
22const MAX_FOLD_DEPTH: usize = 128;
24
25#[derive(Clone, Debug)]
26pub struct LiteralsDictionary {
27 maps: Arc<OnceLock<LiteralMaps>>,
28}
29
30impl Default for LiteralsDictionary {
31 fn default() -> Self {
32 Self::new(None, None, usize::MAX)
33 }
34}
35
36impl LiteralsDictionary {
37 pub fn new(
38 analysis: Option<Analysis>,
39 paths_config: Option<ProjectPathsConfig>,
40 max_values: usize,
41 ) -> Self {
42 let maps = Arc::new(OnceLock::<LiteralMaps>::new());
43 if let Some(analysis) = analysis
44 && max_values > 0
45 {
46 let maps = maps.clone();
47 let _ = std::thread::Builder::new().name("literal-collector".into()).spawn(move || {
50 let _ = maps.get_or_init(|| {
51 let literals =
52 LiteralsCollector::process(&analysis, paths_config.as_ref(), max_values);
53 debug!(
54 words = literals.words.values().map(|set| set.len()).sum::<usize>(),
55 strings = literals.strings.len(),
56 bytes = literals.bytes.len(),
57 "collected source code literals for fuzz dictionary"
58 );
59 literals
60 });
61 });
62 } else {
63 maps.set(Default::default()).unwrap();
64 }
65 Self { maps }
66 }
67
68 pub fn get(&self) -> &LiteralMaps {
70 self.maps.wait()
71 }
72
73 #[cfg(test)]
75 pub(crate) fn set(&mut self, map: super::LiteralMaps) {
76 self.maps = Arc::new(OnceLock::new());
77 self.maps.set(map).unwrap();
78 }
79}
80
81#[derive(Debug, Default)]
82pub struct LiteralMaps {
83 pub words: HashMap<DynSolType, B256IndexSet>,
84 pub strings: IndexSet<String>,
85 pub bytes: IndexSet<Bytes>,
86}
87
88#[derive(Clone, Debug, Default)]
94pub struct EnumBounds {
95 inner: Arc<HashMap<String, usize>>,
96}
97
98impl EnumBounds {
99 pub fn collect(analysis: &Analysis) -> Self {
103 let bounds = analysis.enter(|compiler| {
104 let mut collector = EnumBoundsCollector::default();
105 for source in compiler.sources().iter() {
106 if let Some(ast) = &source.ast {
107 let _ = collector.visit_source_unit(ast);
108 }
109 }
110 collector
112 .bounds
113 .into_iter()
114 .filter_map(|(key, count)| count.map(|count| (key, count)))
115 .collect()
116 });
117 Self { inner: Arc::new(bounds) }
118 }
119
120 pub fn variant_count(&self, contract: Option<&str>, name: &str) -> Option<usize> {
123 let key = match contract {
124 Some(contract) => format!("{contract}.{name}"),
125 None => name.to_string(),
126 };
127 self.inner.get(&key).copied()
128 }
129}
130
131#[derive(Default)]
134struct EnumBoundsCollector {
135 current_contract: Option<String>,
137 bounds: HashMap<String, Option<usize>>,
140}
141
142impl<'ast> ast::Visit<'ast> for EnumBoundsCollector {
143 type BreakValue = ();
144
145 fn visit_item_contract(&mut self, contract: &'ast ast::ItemContract<'ast>) -> ControlFlow<()> {
146 let prev = self.current_contract.replace(contract.name.as_str().to_string());
147 let r = self.walk_item_contract(contract);
148 self.current_contract = prev;
149 r
150 }
151
152 fn visit_item_enum(&mut self, enum_: &'ast ast::ItemEnum<'ast>) -> ControlFlow<()> {
153 let name = enum_.name.as_str();
154 let count = enum_.variants.len();
155 let key = match &self.current_contract {
156 Some(contract) => format!("{contract}.{name}"),
157 None => name.to_string(),
158 };
159 self.bounds
160 .entry(key)
161 .and_modify(|existing| {
163 if *existing != Some(count) {
164 *existing = None;
165 }
166 })
167 .or_insert(Some(count));
168 self.walk_item_enum(enum_)
169 }
170}
171
172#[derive(Debug, Default)]
173pub struct LiteralsCollector {
174 max_values: usize,
175 total_values: usize,
176 output: LiteralMaps,
177 eval_cache: RefCell<HashMap<Span, Option<Num>>>,
179}
180
181impl LiteralsCollector {
182 fn new(max_values: usize) -> Self {
183 Self { max_values, ..Default::default() }
184 }
185
186 pub fn process(
187 analysis: &Analysis,
188 paths_config: Option<&ProjectPathsConfig>,
189 max_values: usize,
190 ) -> LiteralMaps {
191 analysis.enter(|compiler| {
192 let mut literals_collector = Self::new(max_values);
193 for source in compiler.sources().iter() {
194 if let Some(paths) = paths_config
196 && let FileName::Real(source_path) = &source.file.name
197 && !(source_path.starts_with(&paths.sources) || paths.is_test(source_path))
198 {
199 continue;
200 }
201
202 if let Some(ast) = &source.ast
203 && literals_collector.visit_source_unit(ast).is_break()
204 {
205 break;
206 }
207 }
208
209 literals_collector.output
210 })
211 }
212
213 fn insert_word(&mut self, ty: DynSolType, word: B256) {
215 if self.total_values < self.max_values
216 && self.output.words.entry(ty).or_default().insert(word)
217 {
218 self.total_values += 1;
219 }
220 }
221
222 fn insert_string(&mut self, s: String) {
224 if self.total_values < self.max_values && self.output.strings.insert(s) {
225 self.total_values += 1;
226 }
227 }
228
229 fn insert_bytes(&mut self, bytes: Bytes) {
231 if self.total_values < self.max_values && self.output.bytes.insert(bytes) {
232 self.total_values += 1;
233 }
234 }
235
236 fn seed_uint(&mut self, value: U256) {
238 let word = B256::from(value);
239 for bits in [8, 16, 32, 64, 128, 256] {
240 if can_fit_uint(value, bits) {
241 self.insert_word(DynSolType::Uint(bits), word);
242 }
243 }
244 self.insert_word(DynSolType::FixedBytes(32), word);
245 }
246
247 fn seed_int(&mut self, value: I256) {
249 let word = B256::from(value.into_raw());
250 for bits in [8, 16, 32, 64, 128, 256] {
251 if can_fit_int(value, bits) {
252 self.insert_word(DynSolType::Int(bits), word);
253 }
254 }
255 }
256
257 fn seed_num(&mut self, value: Num) {
260 match value {
261 Num::Int { raw, signed: false, width } => {
262 if let Some(bits) = width {
263 self.insert_word(DynSolType::Uint(bits), B256::from(raw));
264 }
265 self.seed_uint(raw);
266 }
267 Num::Int { signed: true, width, .. } => {
268 let i = value.to_i256().expect("signed values always convert to I256");
269 if let Some(bits) = width {
270 self.insert_word(DynSolType::Int(bits), B256::from(i.into_raw()));
271 }
272 self.seed_int(i);
273 }
274 Num::Bytes { raw, n } => {
276 let word = if n >= 32 { raw } else { raw.wrapping_shl((32 - n) * 8) };
277 self.insert_word(DynSolType::FixedBytes(n), B256::from(word));
278 }
279 }
280 }
281
282 fn fold_and_seed(&mut self, expr: &ast::Expr<'_>) {
287 if let ast::ExprKind::Call(callee, args) = &expr.kind
288 && let Some(arg) = single_arg(args)
289 {
290 match &callee.peel_parens().kind {
291 ast::ExprKind::Ident(id) if id.as_str() == "keccak256" => {
294 if let Some(bytes) = lit_bytes(arg) {
295 self.insert_word(DynSolType::FixedBytes(32), keccak256(bytes));
296 }
297 return;
298 }
299 ast::ExprKind::Type(ty)
302 if matches!(
303 &ty.kind,
304 ast::TypeKind::Elementary(ast::ElementaryType::Address(_))
305 ) =>
306 {
307 if let Some(value) = self.eval(arg) {
308 self.insert_word(
309 DynSolType::Address,
310 B256::from(low_bits(value.full_raw(), 160)),
311 );
312 }
313 return;
314 }
315 _ => {}
316 }
317 }
318
319 if let Some(value) = self.eval(expr) {
320 self.seed_num(value);
321 }
322 }
323
324 fn eval(&self, expr: &ast::Expr<'_>) -> Option<Num> {
327 self.eval_depth(expr, 0)
328 }
329
330 fn eval_depth(&self, expr: &ast::Expr<'_>, depth: usize) -> Option<Num> {
331 if depth > MAX_FOLD_DEPTH {
332 return None;
333 }
334 let expr = expr.peel_parens();
335 if let Some(cached) = self.eval_cache.borrow().get(&expr.span) {
336 return *cached;
337 }
338 let result = self.eval_kind(expr, depth);
339 if result.is_some() {
342 self.eval_cache.borrow_mut().insert(expr.span, result);
343 }
344 result
345 }
346
347 fn eval_kind(&self, expr: &ast::Expr<'_>, depth: usize) -> Option<Num> {
348 match &expr.kind {
349 ast::ExprKind::Lit(lit, _) => match &lit.kind {
350 ast::LitKind::Number(n) => Some(Num::untyped(*n)),
352 _ => None,
353 },
354 ast::ExprKind::Unary(op, inner) => {
355 let value = self.eval_depth(inner, depth + 1)?;
356 match op.kind {
357 ast::UnOpKind::Neg => value.neg(),
358 ast::UnOpKind::BitNot => match value {
360 Num::Int { raw, signed, width } => Some(Num::int(!raw, signed, width)),
361 Num::Bytes { .. } => None,
362 },
363 _ => None,
364 }
365 }
366 ast::ExprKind::Binary(lhs, op, rhs) => {
367 let a = self.eval_depth(lhs, depth + 1)?;
368 let b = self.eval_depth(rhs, depth + 1)?;
369 apply_bin(op.kind, a, b)
370 }
371 ast::ExprKind::Call(callee, args) => {
372 let arg = single_arg(args)?;
373 match &callee.peel_parens().kind {
374 ast::ExprKind::Type(ty) => {
375 let ast::TypeKind::Elementary(et) = &ty.kind else { return None };
376 let value = self.eval_depth(arg, depth + 1)?;
377 cast_num(*et, value)
378 }
379 ast::ExprKind::Ident(id) if id.as_str() == "keccak256" => {
380 let bytes = lit_bytes(arg)?;
381 Some(Num::untyped(keccak256(bytes).into()))
382 }
383 _ => None,
384 }
385 }
386 ast::ExprKind::Member(inner, member) => {
388 let ast::ExprKind::TypeCall(ty) = &inner.peel_parens().kind else { return None };
389 let ast::TypeKind::Elementary(et) = &ty.kind else { return None };
390 type_min_max(*et, member.as_str())
391 }
392 _ => None,
393 }
394 }
395}
396
397impl<'ast> ast::Visit<'ast> for LiteralsCollector {
398 type BreakValue = ();
399
400 fn visit_expr(&mut self, expr: &'ast ast::Expr<'ast>) -> ControlFlow<()> {
401 if self.total_values >= self.max_values {
403 return ControlFlow::Break(());
404 }
405
406 match &expr.kind {
407 ast::ExprKind::Lit(lit, _) => match &lit.kind {
409 ast::LitKind::Number(n) => self.seed_uint(*n),
410 ast::LitKind::Address(addr) => {
411 self.insert_word(DynSolType::Address, addr.into_word())
412 }
413 ast::LitKind::Str(ast::StrKind::Hex, sym, _) => {
414 self.insert_bytes(Bytes::copy_from_slice(sym.as_byte_str()));
415 }
416 ast::LitKind::Str(_, sym, _) => {
417 let s = String::from_utf8_lossy(sym.as_byte_str()).into_owned();
418 self.insert_word(DynSolType::FixedBytes(32), keccak256(s.as_bytes()));
420 if s.len() <= 32 {
422 self.insert_word(
423 DynSolType::FixedBytes(32),
424 B256::right_padding_from(s.as_bytes()),
425 );
426 }
427 self.insert_string(s);
428 }
429 ast::LitKind::Bool(..)
430 | ast::LitKind::BigNumber(..)
431 | ast::LitKind::Rational(..)
432 | ast::LitKind::Err(..) => {
433 }
435 },
436 _ => self.fold_and_seed(expr),
438 }
439
440 self.walk_expr(expr)
441 }
442}
443
444#[derive(Clone, Copy, Debug)]
446enum Num {
447 Int { raw: U256, signed: bool, width: Option<usize> },
450 Bytes { raw: U256, n: usize },
452}
453
454impl Num {
455 const fn untyped(raw: U256) -> Self {
457 Self::Int { raw, signed: false, width: None }
458 }
459
460 fn int(raw: U256, signed: bool, width: Option<usize>) -> Self {
462 let raw = match width {
463 Some(bits) => low_bits(raw, bits),
464 None => raw,
465 };
466 Self::Int { raw, signed, width }
467 }
468
469 const fn as_u256(self) -> U256 {
471 match self {
472 Self::Int { raw, .. } | Self::Bytes { raw, .. } => raw,
473 }
474 }
475
476 fn full_raw(self) -> U256 {
479 match self {
480 Self::Int { raw, signed: true, width } => sign_extend(raw, width),
481 _ => self.as_u256(),
482 }
483 }
484
485 fn to_i256(self) -> Option<I256> {
488 match self {
489 Self::Int { raw, signed: true, width } => Some(I256::from_raw(sign_extend(raw, width))),
490 Self::Int { raw, signed: false, .. } => I256::try_from(raw).ok(),
491 Self::Bytes { .. } => None,
492 }
493 }
494
495 const fn is_signed(self) -> bool {
497 matches!(self, Self::Int { signed: true, .. })
498 }
499
500 const fn width(self) -> Option<usize> {
502 match self {
503 Self::Int { width, .. } => width,
504 Self::Bytes { .. } => None,
505 }
506 }
507
508 fn neg(self) -> Option<Self> {
512 match self {
513 Self::Int { raw, signed: false, width } => {
515 (raw <= I256::MIN.into_raw()).then(|| Self::int(raw.wrapping_neg(), true, width))
516 }
517 Self::Int { signed: true, width, .. } => {
518 let r = self.to_i256()?.checked_neg()?;
519 if let Some(bits) = width
520 && !can_fit_int(r, bits)
521 {
522 return None;
523 }
524 Some(Self::int(r.into_raw(), true, width))
525 }
526 Self::Bytes { .. } => None,
527 }
528 }
529}
530
531fn apply_bin(op: ast::BinOpKind, a: Num, b: Num) -> Option<Num> {
534 if matches!(a, Num::Bytes { .. }) || matches!(b, Num::Bytes { .. }) {
536 return None;
537 }
538
539 let signed = a.is_signed() || b.is_signed();
540 let width =
543 if matches!(op, Shl | Shr | Pow) { a.width() } else { combine_width(a.width(), b.width()) };
544
545 if matches!(op, BitAnd | BitOr | BitXor | Shl) {
548 let (x, y) = (a.as_u256(), b.as_u256());
549 let raw = match op {
550 BitAnd => x & y,
551 BitOr => x | y,
552 BitXor => x ^ y,
553 Shl => shift_amount(y).map_or(U256::ZERO, |s| x.wrapping_shl(s)),
554 _ => unreachable!(),
555 };
556 return Some(Num::int(raw, signed, width));
557 }
558
559 if signed {
562 let (x, y) = (a.to_i256()?, b.to_i256()?);
563 if op == Pow {
564 return signed_pow(x, y, width);
565 }
566 let r = match width {
569 Some(bits) => {
570 let r = match op {
571 Add => x.checked_add(y)?,
572 Sub => x.checked_sub(y)?,
573 Mul => x.checked_mul(y)?,
574 Div => x.checked_div(y)?,
575 Rem => x.checked_rem(y)?,
576 _ => return None,
577 };
578 if !can_fit_int(r, bits) {
579 return None;
580 }
581 r
582 }
583 None => match op {
584 Add => x.wrapping_add(y),
585 Sub => x.wrapping_sub(y),
586 Mul => x.wrapping_mul(y),
587 Div => x.checked_div(y)?,
588 Rem => x.checked_rem(y)?,
589 _ => return None,
590 },
591 };
592 return Some(Num::int(r.into_raw(), true, width));
593 }
594
595 let (x, y) = (a.as_u256(), b.as_u256());
596 let r = match op {
597 Add => checked_arith(x.checked_add(y), x.wrapping_add(y), width)?,
601 Sub => checked_arith(x.checked_sub(y), x.wrapping_sub(y), width)?,
602 Mul => checked_arith(x.checked_mul(y), x.wrapping_mul(y), width)?,
603 Div => x.checked_div(y)?,
604 Rem => x.checked_rem(y)?,
605 Pow => {
609 let r = x.checked_pow(y)?;
610 checked_arith(Some(r), r, width)?
611 }
612 Shr => shift_amount(y).map_or(U256::ZERO, |s| x.wrapping_shr(s)),
614 _ => return None,
616 };
617 Some(Num::int(r, false, width))
618}
619
620fn checked_arith(checked: Option<U256>, wrapping: U256, width: Option<usize>) -> Option<U256> {
627 match width {
628 Some(bits) => checked.filter(|r| can_fit_uint(*r, bits)),
629 None => Some(wrapping),
630 }
631}
632
633fn signed_pow(base: I256, exp: I256, width: Option<usize>) -> Option<Num> {
636 if exp.is_negative() {
637 return None;
638 }
639 let magnitude = base.unsigned_abs().checked_pow(exp.into_raw())?;
640 let negative = base.is_negative() && exp.into_raw().bit(0);
641 let value = if negative {
642 (magnitude <= I256::MIN.into_raw()).then(|| I256::from_raw(magnitude.wrapping_neg()))?
644 } else {
645 (magnitude < I256::MIN.into_raw()).then(|| I256::from_raw(magnitude))?
647 };
648 if let Some(bits) = width
651 && !can_fit_int(value, bits)
652 {
653 return None;
654 }
655 Some(Num::int(value.into_raw(), true, width))
656}
657
658fn combine_width(a: Option<usize>, b: Option<usize>) -> Option<usize> {
661 match (a, b) {
662 (None, None) => None,
663 (Some(a), Some(b)) => Some(a.max(b)),
664 (Some(w), None) | (None, Some(w)) => Some(w),
665 }
666}
667
668fn type_min_max(ty: ast::ElementaryType, member: &str) -> Option<Num> {
670 match (ty, member) {
671 (ast::ElementaryType::UInt(size), "max") => {
672 let bits = size.bits() as usize;
673 Some(Num::int(low_bits(U256::MAX, bits), false, Some(bits)))
674 }
675 (ast::ElementaryType::UInt(size), "min") => {
676 Some(Num::int(U256::ZERO, false, Some(size.bits() as usize)))
677 }
678 (ast::ElementaryType::Int(size), "max") => {
681 let bits = size.bits() as usize;
682 Some(Num::int(low_bits(U256::MAX, bits - 1), true, Some(bits)))
683 }
684 (ast::ElementaryType::Int(size), "min") => {
685 let bits = size.bits() as usize;
686 Some(Num::int(U256::ONE.wrapping_shl(bits - 1), true, Some(bits)))
687 }
688 _ => None,
689 }
690}
691
692fn shift_amount(y: U256) -> Option<usize> {
694 (y < U256::from(256u64)).then(|| y.as_limbs()[0] as usize)
695}
696
697fn cast_num(ty: ast::ElementaryType, value: Num) -> Option<Num> {
701 match ty {
702 ast::ElementaryType::UInt(size) => {
703 Some(Num::int(value.full_raw(), false, Some(size.bits() as usize)))
704 }
705 ast::ElementaryType::Int(size) => {
706 Some(Num::int(value.full_raw(), true, Some(size.bits() as usize)))
707 }
708 ast::ElementaryType::Address(_) => Some(Num::int(value.full_raw(), false, Some(160))),
709 ast::ElementaryType::FixedBytes(size) => Some(cast_to_bytes(value, size.bytes() as usize)),
710 _ => None,
711 }
712}
713
714fn cast_to_bytes(value: Num, n: usize) -> Num {
719 let raw = match value {
720 Num::Bytes { raw, n: m } if n <= m => raw.wrapping_shr((m - n) * 8),
721 Num::Bytes { raw, n: m } => raw.wrapping_shl((n - m) * 8),
722 _ => low_bits(value.full_raw(), n * 8),
723 };
724 Num::Bytes { raw, n }
725}
726
727fn low_bits(value: U256, bits: usize) -> U256 {
729 if bits >= 256 { value } else { value & (U256::ONE.wrapping_shl(bits) - U256::ONE) }
730}
731
732fn sign_extend(raw: U256, width: Option<usize>) -> U256 {
734 match width {
735 Some(bits) if bits < 256 && raw.bit(bits - 1) => raw | U256::MAX.wrapping_shl(bits),
736 _ => raw,
737 }
738}
739
740fn single_arg<'a, 'ast>(args: &'a ast::CallArgs<'ast>) -> Option<&'a ast::Expr<'ast>> {
742 let mut exprs = args.exprs();
743 (exprs.len() == 1).then(|| exprs.next()).flatten()
744}
745
746fn lit_bytes<'a>(expr: &'a ast::Expr<'_>) -> Option<&'a [u8]> {
749 if let ast::ExprKind::Lit(lit, _) = &expr.peel_parens().kind
750 && let ast::LitKind::Str(_, sym, _) = &lit.kind
751 {
752 return Some(sym.as_byte_str());
753 }
754 None
755}
756
757fn can_fit_int(value: I256, bits: usize) -> bool {
759 let max_val = I256::try_from((U256::ONE << (bits - 1)) - U256::ONE)
761 .expect("max value should fit in I256");
762 let min_val = -max_val - I256::ONE;
764
765 value >= min_val && value <= max_val
766}
767
768fn can_fit_uint(value: U256, bits: usize) -> bool {
770 if bits == 256 {
771 return true;
772 }
773 let max_val = (U256::ONE << bits) - U256::ONE;
775 value <= max_val
776}
777
778#[cfg(test)]
779mod tests {
780 use super::*;
781 use alloy_primitives::address;
782 use solar::interface::{Session, source_map};
783
784 const SOURCE: &str = r#"
785 contract Magic {
786 // plain literals
787 address constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
788 uint64 constant MAGIC_NUMBER = 1122334455;
789 int32 constant MAGIC_INT = -777;
790 bytes32 constant MAGIC_WORD = "abcd1234";
791 bytes constant MAGIC_BYTES = hex"deadbeef";
792 string constant MAGIC_STRING = "xyzzy";
793
794 // constant exprs with folding
795 uint256 constant NEG_FOLDING = uint(-2);
796 uint256 constant BIN_FOLDING = 2 * 2 ether;
797 bytes32 constant IMPLEMENTATION_SLOT = bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1);
798 }"#;
799
800 #[test]
801 fn test_literals_collector_coverage() {
802 let map = process_source_literals(SOURCE);
803
804 let addr = address!("0x6B175474E89094C44Da98b954EedeAC495271d0F").into_word();
806 let num = B256::from(U256::from(1122334455u64));
807 let int = B256::from(I256::try_from(-777i32).unwrap().into_raw());
808 let word = B256::right_padding_from(b"abcd1234");
809 let dyn_bytes = Bytes::from_static(&[0xde, 0xad, 0xbe, 0xef]);
810
811 assert_word(&map, DynSolType::Address, addr, "Expected DAI in address set");
812 assert_word(&map, DynSolType::Uint(64), num, "Expected MAGIC_NUMBER in uint64 set");
813 assert_word(&map, DynSolType::FixedBytes(32), num, "Expected MAGIC_NUMBER in bytes32 set");
814 assert_word(&map, DynSolType::Int(32), int, "Expected MAGIC_INT in int32 set");
815 assert_word(&map, DynSolType::FixedBytes(32), word, "Expected MAGIC_WORD in bytes32 set");
816 assert!(map.strings.contains("xyzzy"), "Expected MAGIC_STRING to be collected");
817 assert!(
818 map.strings.contains("eip1967.proxy.implementation"),
819 "Expected IMPLEMENTATION_SLOT in string set"
820 );
821 assert!(map.bytes.contains(&dyn_bytes), "Expected MAGIC_BYTES in bytes set");
822
823 let neg_cast = B256::from(U256::MAX - U256::ONE);
827 assert_word(&map, DynSolType::Uint(256), neg_cast, "Expected uint(-2) to be folded");
828
829 let bin = B256::from(U256::from(4_000_000_000_000_000_000u64));
831 assert_word(&map, DynSolType::Uint(64), bin, "Expected `2 * 2 ether` to be folded");
832
833 let slot =
836 B256::from(Into::<U256>::into(keccak256("eip1967.proxy.implementation")) - U256::ONE);
837 assert_word(
838 &map,
839 DynSolType::FixedBytes(32),
840 slot,
841 "Expected IMPLEMENTATION_SLOT expression to be folded",
842 );
843 }
844
845 #[test]
846 fn test_literals_collector_size() {
847 let literals = process_source_literals(SOURCE);
848
849 let count = |ty: DynSolType| literals.words.get(&ty).map_or(0, |set| set.len());
851
852 assert_eq!(count(DynSolType::Address), 1, "Address literal count mismatch");
853 assert_eq!(literals.strings.len(), 3, "String literals count mismatch");
854 assert_eq!(literals.bytes.len(), 1, "Byte literals count mismatch");
855
856 assert_eq!(count(DynSolType::Uint(8)), 2, "Uint(8) count mismatch");
860 assert_eq!(count(DynSolType::Uint(16)), 3, "Uint(16) count mismatch");
861 assert_eq!(count(DynSolType::Uint(32)), 4, "Uint(32) count mismatch");
862 assert_eq!(count(DynSolType::Uint(64)), 6, "Uint(64) count mismatch");
863 assert_eq!(count(DynSolType::Uint(128)), 6, "Uint(128) count mismatch");
864 assert_eq!(count(DynSolType::Uint(256)), 9, "Uint(256) count mismatch");
865
866 assert_eq!(count(DynSolType::Int(8)), 1, "Int(8) count mismatch");
869 assert_eq!(count(DynSolType::Int(16)), 2, "Int(16) count mismatch");
870 assert_eq!(count(DynSolType::Int(32)), 2, "Int(32) count mismatch");
871 assert_eq!(count(DynSolType::Int(64)), 2, "Int(64) count mismatch");
872 assert_eq!(count(DynSolType::Int(128)), 2, "Int(128) count mismatch");
873 assert_eq!(count(DynSolType::Int(256)), 2, "Int(256) count mismatch");
874
875 assert_eq!(count(DynSolType::FixedBytes(32)), 14, "FixedBytes(32) count mismatch");
881
882 assert_eq!(
884 literals.words.values().map(|set| set.len()).sum::<usize>(),
885 56,
886 "Total word values count mismatch"
887 );
888 }
889
890 #[test]
891 fn test_width_aware_casts() {
892 let source = r#"
895 contract C {
896 uint8 constant A = uint8(-2); // 254
897 uint8 constant B = uint8(257); // 1
898 int8 constant D = int8(255); // -1
899 int256 constant E = int256(1) - 2; // -1
900 int256 constant F = ~int256(0); // -1 (not 2**256 - 1)
901 int16 constant G = int16(int8(-1)); // -1 (sign-extended)
902 int16 constant H = int16(uint8(255)); // 255 (unsigned source)
903 uint16 constant I = uint16(int8(-1)); // 65535
904 int256 constant J = -2 ** 255; // int256 min
905 bytes4 constant K = bytes4(uint32(0x12345678)); // left-aligned
906 }"#;
907 let map = process_source_literals(source);
908
909 let neg_one = B256::from(I256::try_from(-1).unwrap().into_raw());
910 assert_word(&map, DynSolType::Uint(8), B256::with_last_byte(254), "uint8(-2) -> 254");
911 assert_word(&map, DynSolType::Uint(8), B256::with_last_byte(1), "uint8(257) -> 1");
912 assert_word(&map, DynSolType::Int(8), neg_one, "int8(255) -> -1");
913 assert_word(&map, DynSolType::Int(256), neg_one, "int256(1) - 2 -> -1");
914 assert_word(&map, DynSolType::Int(256), neg_one, "~int256(0) -> -1");
915 assert_word(&map, DynSolType::Int(16), neg_one, "int16(int8(-1)) -> -1");
916 assert_word(
917 &map,
918 DynSolType::Int(16),
919 B256::with_last_byte(255),
920 "int16(uint8(255)) -> 255",
921 );
922 assert_word(
923 &map,
924 DynSolType::Uint(16),
925 B256::from(U256::from(65535)),
926 "uint16(int8(-1)) -> 65535",
927 );
928 assert_word(
929 &map,
930 DynSolType::Int(256),
931 B256::from(I256::MIN.into_raw()),
932 "-2 ** 255 -> int256 min",
933 );
934
935 let left_aligned = B256::right_padding_from(&[0x12, 0x34, 0x56, 0x78]);
936 assert_word(&map, DynSolType::FixedBytes(4), left_aligned, "bytes4 is left-aligned");
937 }
938
939 #[test]
940 fn test_width_dependent_ops_stay_in_width() {
941 let source = r#"
944 contract C {
945 uint8 constant A = ~uint8(0); // 255
946 uint8 constant B = uint8(1) << 8; // 0
947 int8 constant D = int8(1) << 7; // -128
948 uint8 constant E = uint8(255) << 256; // 0 (shift amount >= 256)
949 uint8 constant F = uint8(250) + 5; // 255 (in-range, typed + untyped literal)
950 uint8 constant G = uint8(10) ** uint256(2); // 100 (in-range, result type is base uint8)
951 uint8 constant H = uint8(0x80) >> uint256(0); // 128 (result type is left uint8)
952 }"#;
953 let map = process_source_literals(source);
954
955 assert_word(&map, DynSolType::Uint(8), B256::with_last_byte(255), "~uint8(0) -> 255");
956 assert_word(&map, DynSolType::Uint(8), B256::ZERO, "uint8(_) << {8,256} -> 0");
957 assert_word(&map, DynSolType::Uint(8), B256::with_last_byte(255), "uint8(250) + 5 -> 255");
958 assert_word(&map, DynSolType::Uint(8), B256::with_last_byte(100), "uint8(10) ** 2 -> 100");
959 assert_word(
960 &map,
961 DynSolType::Uint(8),
962 B256::with_last_byte(128),
963 "uint8(0x80) >> 0 -> 128",
964 );
965 let neg_128 = B256::from(I256::try_from(-128).unwrap().into_raw());
966 assert_word(&map, DynSolType::Int(8), neg_128, "int8(1) << 7 -> -128");
967
968 assert!(
971 !map.words
972 .get(&DynSolType::Uint(256))
973 .is_some_and(|s| s.contains(&B256::from(U256::MAX))),
974 "~uint8(0) must not seed a uint256 max"
975 );
976 }
977
978 #[test]
979 fn test_checked_overflow_does_not_seed() {
980 let source = r#"
984 contract C {
985 uint8 constant A = uint8(250) + 10; // 260 -> reverts (panic 0x11), not 4
986 uint8 constant B = uint8(200) * 2; // 400 -> reverts, not 144
987 uint8 constant C2 = uint8(1) - 2; // underflow -> reverts, not 255
988 uint8 constant D = uint8(10) ** 3; // 1000 -> reverts, not 232
989 int8 constant E = int8(100) + 100; // 200 -> reverts, not -56
990 int8 constant F = int8(64) * 2; // 128 -> reverts, not -128
991 int8 constant G = int8(5) ** 3; // 125 -> in range, folds
992 }"#;
993 let map = process_source_literals(source);
994
995 let seeded =
997 |ty, raw: U256| map.words.get(&ty).is_some_and(|s| s.contains(&B256::from(raw)));
998 for bits in [8usize, 16, 32, 64, 128, 256] {
999 assert!(
1000 !seeded(DynSolType::Uint(bits), U256::from(4)),
1001 "uint8(250)+10 must not seed 4"
1002 );
1003 assert!(
1004 !seeded(DynSolType::Uint(bits), U256::from(144)),
1005 "uint8(200)*2 must not seed 144"
1006 );
1007 assert!(
1008 !seeded(DynSolType::Uint(bits), U256::from(255)),
1009 "uint8(1)-2 must not seed 255"
1010 );
1011 assert!(
1012 !seeded(DynSolType::Uint(bits), U256::from(232)),
1013 "uint8(10)**3 must not seed 232"
1014 );
1015 }
1016 let neg_56 = I256::try_from(-56).unwrap().into_raw();
1017 let neg_128 = I256::try_from(-128).unwrap().into_raw();
1018 for bits in [8usize, 16, 32, 64, 128, 256] {
1019 assert!(!seeded(DynSolType::Int(bits), neg_56), "int8(100)+100 must not seed -56");
1020 assert!(!seeded(DynSolType::Int(bits), neg_128), "int8(64)*2 must not seed -128");
1021 }
1022
1023 assert_word(&map, DynSolType::Int(8), B256::with_last_byte(125), "int8(5) ** 3 -> 125");
1025 assert_word(&map, DynSolType::Uint(8), B256::with_last_byte(250), "operand uint8(250)");
1026 }
1027
1028 #[test]
1029 fn test_checked_negation_overflow_does_not_seed() {
1030 let source = r#"
1033 contract C {
1034 int8 constant A = -type(int8).min + 1; // -(-128) reverts, must not seed -127
1035 int8 constant B = -int8(1); // -1, folds normally
1036 }"#;
1037 let map = process_source_literals(source);
1038
1039 let neg_127 = I256::try_from(-127).unwrap().into_raw();
1040 let seeded =
1041 |ty, raw: U256| map.words.get(&ty).is_some_and(|s| s.contains(&B256::from(raw)));
1042 for bits in [8usize, 16, 32, 64, 128, 256] {
1043 assert!(
1044 !seeded(DynSolType::Int(bits), neg_127),
1045 "-type(int8).min + 1 must not seed -127"
1046 );
1047 }
1048 let neg_one = B256::from(I256::try_from(-1).unwrap().into_raw());
1049 assert_word(&map, DynSolType::Int(8), neg_one, "-int8(1) -> -1");
1050 }
1051
1052 #[test]
1053 fn test_fixed_bytes_folding() {
1054 let source = r#"
1057 contract C {
1058 bytes4 constant A = bytes4(uint256(0xdeadbeef12345678)); // low 4 bytes, not zero
1059 bytes2 constant B = bytes2(bytes4(uint32(0x12345678))); // keep left -> 0x1234
1060 bytes4 constant D = bytes4(bytes2(uint16(0x1234))); // pad right -> 0x12340000
1061 }"#;
1062 let map = process_source_literals(source);
1063
1064 let low4 = B256::right_padding_from(&[0x12, 0x34, 0x56, 0x78]);
1065 assert_word(&map, DynSolType::FixedBytes(4), low4, "bytes4 must not fold to zero");
1066 let left2 = B256::right_padding_from(&[0x12, 0x34]);
1067 assert_word(&map, DynSolType::FixedBytes(2), left2, "bytes2(bytes4(..)) keeps left bytes");
1068 let padded = B256::right_padding_from(&[0x12, 0x34, 0x00, 0x00]);
1069 assert_word(&map, DynSolType::FixedBytes(4), padded, "bytes4(bytes2(..)) pads right");
1070 }
1071
1072 #[test]
1073 fn test_type_min_max_folding() {
1074 let source = r#"
1075 contract C {
1076 uint256 constant A = type(uint256).max;
1077 uint8 constant B = type(uint8).max; // 255
1078 int256 constant D = type(int256).min;
1079 int256 constant E = type(int256).max;
1080 uint256 constant F = type(uint256).max - 1;
1081 int8 constant G = type(int8).min; // -128
1082 int24 constant H = type(int24).min; // -2**23
1083 }"#;
1084 let map = process_source_literals(source);
1085
1086 assert_word(&map, DynSolType::Uint(256), B256::from(U256::MAX), "type(uint256).max");
1087 assert_word(&map, DynSolType::Uint(8), B256::with_last_byte(255), "type(uint8).max");
1088 assert_word(
1089 &map,
1090 DynSolType::Int(256),
1091 B256::from(I256::MIN.into_raw()),
1092 "type(int256).min",
1093 );
1094 assert_word(
1095 &map,
1096 DynSolType::Int(256),
1097 B256::from(I256::MAX.into_raw()),
1098 "type(int256).max",
1099 );
1100 let max_minus_one = B256::from(U256::MAX - U256::ONE);
1101 assert_word(&map, DynSolType::Uint(256), max_minus_one, "type(uint256).max - 1");
1102 let min8 = B256::from(I256::try_from(-128).unwrap().into_raw());
1103 assert_word(&map, DynSolType::Int(8), min8, "type(int8).min -> -128");
1104 let min24 = B256::from(I256::try_from(-(1i64 << 23)).unwrap().into_raw());
1105 assert_word(&map, DynSolType::Int(24), min24, "type(int24).min -> -2**23");
1106 }
1107
1108 #[test]
1109 fn test_address_cast_seeds_address_type() {
1110 let source = r#"
1113 contract C {
1114 address constant A = address(4660);
1115 }"#;
1116 let map = process_source_literals(source);
1117
1118 assert_word(
1119 &map,
1120 DynSolType::Address,
1121 B256::from(low_bits(U256::from(4660), 160)),
1122 "address(4660) -> address",
1123 );
1124 assert_eq!(map.words.get(&DynSolType::Uint(160)), None, "must not seed a uint160 bucket");
1125 }
1126
1127 #[test]
1128 fn test_max_values_is_respected() {
1129 let source = r#"
1132 contract C {
1133 string constant A = "aaa";
1134 string constant B = "bbb";
1135 string constant D = "ccc";
1136 }"#;
1137 let map = process_source_literals_with_max(source, 2);
1138
1139 let total = map.words.values().map(|set| set.len()).sum::<usize>()
1140 + map.strings.len()
1141 + map.bytes.len();
1142 assert!(total <= 2, "max_values not respected: collected {total} values");
1143 }
1144
1145 #[test]
1146 fn test_fractional_and_wide_literals_are_not_truncated() {
1147 let map = process_source_literals("contract C { function f() external { 0.5; 1e100; } }");
1148 assert!(map.words.is_empty());
1149 assert!(map.strings.is_empty());
1150 assert!(map.bytes.is_empty());
1151 }
1152
1153 fn process_source_literals(source: &str) -> LiteralMaps {
1156 process_source_literals_with_max(source, usize::MAX)
1157 }
1158
1159 fn process_source_literals_with_max(source: &str, max_values: usize) -> LiteralMaps {
1160 let mut compiler =
1161 solar::sema::Compiler::new(Session::builder().with_stderr_emitter().build());
1162 compiler
1163 .enter_mut(|c| -> std::io::Result<()> {
1164 let mut pcx = c.parse();
1165 pcx.set_resolve_imports(false);
1166
1167 pcx.add_file(
1168 c.sess().source_map().new_source_file(source_map::FileName::Stdin, source)?,
1169 );
1170 pcx.parse();
1171 let _ = c.lower_asts();
1172 Ok(())
1173 })
1174 .expect("Failed to compile test source");
1175
1176 LiteralsCollector::process(&std::sync::Arc::new(compiler), None, max_values)
1177 }
1178
1179 fn assert_word(literals: &LiteralMaps, ty: DynSolType, value: B256, msg: &str) {
1180 assert!(literals.words.get(&ty).is_some_and(|set| set.contains(&value)), "{}", msg);
1181 }
1182}