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 }
245
246 fn seed_int(&mut self, value: I256) {
248 let word = B256::from(value.into_raw());
249 for bits in [8, 16, 32, 64, 128, 256] {
250 if can_fit_int(value, bits) {
251 self.insert_word(DynSolType::Int(bits), word);
252 }
253 }
254 }
255
256 fn seed_num(&mut self, value: Num) {
259 match value {
260 Num::Int { raw, signed: false, width } => {
261 if let Some(bits) = width {
262 self.insert_word(DynSolType::Uint(bits), B256::from(raw));
263 }
264 self.seed_uint(raw);
265 }
266 Num::Int { signed: true, width, .. } => {
267 let i = value.to_i256().expect("signed values always convert to I256");
268 if let Some(bits) = width {
269 self.insert_word(DynSolType::Int(bits), B256::from(i.into_raw()));
270 }
271 self.seed_int(i);
272 }
273 Num::Bytes { raw, n } => {
275 let word = if n >= 32 { raw } else { raw.wrapping_shl((32 - n) * 8) };
276 self.insert_word(DynSolType::FixedBytes(n), B256::from(word));
277 }
278 }
279 }
280
281 fn fold_and_seed(&mut self, expr: &ast::Expr<'_>) {
286 if let ast::ExprKind::Call(callee, args) = &expr.kind
287 && let Some(arg) = single_arg(args)
288 {
289 match &callee.peel_parens().kind {
290 ast::ExprKind::Ident(id) if id.as_str() == "keccak256" => {
293 if let Some(bytes) = lit_bytes(arg) {
294 self.insert_word(DynSolType::FixedBytes(32), keccak256(bytes));
295 }
296 return;
297 }
298 ast::ExprKind::Type(ty)
301 if matches!(
302 &ty.kind,
303 ast::TypeKind::Elementary(ast::ElementaryType::Address(_))
304 ) =>
305 {
306 if let Some(value) = self.eval(arg) {
307 self.insert_word(
308 DynSolType::Address,
309 B256::from(low_bits(value.full_raw(), 160)),
310 );
311 }
312 return;
313 }
314 _ => {}
315 }
316 }
317
318 if let Some(value) = self.eval(expr) {
319 self.seed_num(value);
320 }
321 }
322
323 fn eval(&self, expr: &ast::Expr<'_>) -> Option<Num> {
326 self.eval_depth(expr, 0)
327 }
328
329 fn eval_depth(&self, expr: &ast::Expr<'_>, depth: usize) -> Option<Num> {
330 if depth > MAX_FOLD_DEPTH {
331 return None;
332 }
333 let expr = expr.peel_parens();
334 if let Some(cached) = self.eval_cache.borrow().get(&expr.span) {
335 return *cached;
336 }
337 let result = self.eval_kind(expr, depth);
338 if result.is_some() {
341 self.eval_cache.borrow_mut().insert(expr.span, result);
342 }
343 result
344 }
345
346 fn eval_kind(&self, expr: &ast::Expr<'_>, depth: usize) -> Option<Num> {
347 match &expr.kind {
348 ast::ExprKind::Lit(lit, _) => match &lit.kind {
349 ast::LitKind::Number(n) => Some(Num::untyped(U256::from(*n))),
351 _ => None,
352 },
353 ast::ExprKind::Unary(op, inner) => {
354 let value = self.eval_depth(inner, depth + 1)?;
355 match op.kind {
356 ast::UnOpKind::Neg => value.neg(),
357 ast::UnOpKind::BitNot => match value {
359 Num::Int { raw, signed, width } => Some(Num::int(!raw, signed, width)),
360 Num::Bytes { .. } => None,
361 },
362 _ => None,
363 }
364 }
365 ast::ExprKind::Binary(lhs, op, rhs) => {
366 let a = self.eval_depth(lhs, depth + 1)?;
367 let b = self.eval_depth(rhs, depth + 1)?;
368 apply_bin(op.kind, a, b)
369 }
370 ast::ExprKind::Call(callee, args) => {
371 let arg = single_arg(args)?;
372 match &callee.peel_parens().kind {
373 ast::ExprKind::Type(ty) => {
374 let ast::TypeKind::Elementary(et) = &ty.kind else { return None };
375 let value = self.eval_depth(arg, depth + 1)?;
376 cast_num(*et, value)
377 }
378 ast::ExprKind::Ident(id) if id.as_str() == "keccak256" => {
379 let bytes = lit_bytes(arg)?;
380 Some(Num::untyped(U256::from_be_bytes(keccak256(bytes).0)))
381 }
382 _ => None,
383 }
384 }
385 ast::ExprKind::Member(inner, member) => {
387 let ast::ExprKind::TypeCall(ty) = &inner.peel_parens().kind else { return None };
388 let ast::TypeKind::Elementary(et) = &ty.kind else { return None };
389 type_min_max(*et, member.as_str())
390 }
391 _ => None,
392 }
393 }
394}
395
396impl<'ast> ast::Visit<'ast> for LiteralsCollector {
397 type BreakValue = ();
398
399 fn visit_expr(&mut self, expr: &'ast ast::Expr<'ast>) -> ControlFlow<()> {
400 if self.total_values >= self.max_values {
402 return ControlFlow::Break(());
403 }
404
405 match &expr.kind {
406 ast::ExprKind::Lit(lit, _) => match &lit.kind {
408 ast::LitKind::Number(n) => self.seed_uint(U256::from(*n)),
409 ast::LitKind::Address(addr) => {
410 self.insert_word(DynSolType::Address, addr.into_word())
411 }
412 ast::LitKind::Str(ast::StrKind::Hex, sym, _) => {
413 self.insert_bytes(Bytes::copy_from_slice(sym.as_byte_str()));
414 }
415 ast::LitKind::Str(_, sym, _) => {
416 let s = String::from_utf8_lossy(sym.as_byte_str()).into_owned();
417 self.insert_word(DynSolType::FixedBytes(32), keccak256(s.as_bytes()));
419 if s.len() <= 32 {
421 self.insert_word(
422 DynSolType::FixedBytes(32),
423 B256::right_padding_from(s.as_bytes()),
424 );
425 }
426 self.insert_string(s);
427 }
428 ast::LitKind::Bool(..) | ast::LitKind::Rational(..) | ast::LitKind::Err(..) => {
429 }
431 },
432 _ => self.fold_and_seed(expr),
434 }
435
436 self.walk_expr(expr)
437 }
438}
439
440#[derive(Clone, Copy, Debug)]
442enum Num {
443 Int { raw: U256, signed: bool, width: Option<usize> },
446 Bytes { raw: U256, n: usize },
448}
449
450impl Num {
451 const fn untyped(raw: U256) -> Self {
453 Self::Int { raw, signed: false, width: None }
454 }
455
456 fn int(raw: U256, signed: bool, width: Option<usize>) -> Self {
458 let raw = match width {
459 Some(bits) => low_bits(raw, bits),
460 None => raw,
461 };
462 Self::Int { raw, signed, width }
463 }
464
465 const fn as_u256(self) -> U256 {
467 match self {
468 Self::Int { raw, .. } | Self::Bytes { raw, .. } => raw,
469 }
470 }
471
472 fn full_raw(self) -> U256 {
475 match self {
476 Self::Int { raw, signed: true, width } => sign_extend(raw, width),
477 _ => self.as_u256(),
478 }
479 }
480
481 fn to_i256(self) -> Option<I256> {
484 match self {
485 Self::Int { raw, signed: true, width } => Some(I256::from_raw(sign_extend(raw, width))),
486 Self::Int { raw, signed: false, .. } => I256::try_from(raw).ok(),
487 Self::Bytes { .. } => None,
488 }
489 }
490
491 const fn is_signed(self) -> bool {
493 matches!(self, Self::Int { signed: true, .. })
494 }
495
496 const fn width(self) -> Option<usize> {
498 match self {
499 Self::Int { width, .. } => width,
500 Self::Bytes { .. } => None,
501 }
502 }
503
504 fn neg(self) -> Option<Self> {
508 match self {
509 Self::Int { raw, signed: false, width } => {
511 (raw <= I256::MIN.into_raw()).then(|| Self::int(raw.wrapping_neg(), true, width))
512 }
513 Self::Int { signed: true, width, .. } => {
514 let r = self.to_i256()?.checked_neg()?;
515 if let Some(bits) = width
516 && !can_fit_int(r, bits)
517 {
518 return None;
519 }
520 Some(Self::int(r.into_raw(), true, width))
521 }
522 Self::Bytes { .. } => None,
523 }
524 }
525}
526
527fn apply_bin(op: ast::BinOpKind, a: Num, b: Num) -> Option<Num> {
530 if matches!(a, Num::Bytes { .. }) || matches!(b, Num::Bytes { .. }) {
532 return None;
533 }
534
535 let signed = a.is_signed() || b.is_signed();
536 let width =
539 if matches!(op, Shl | Shr | Pow) { a.width() } else { combine_width(a.width(), b.width()) };
540
541 if matches!(op, BitAnd | BitOr | BitXor | Shl) {
544 let (x, y) = (a.as_u256(), b.as_u256());
545 let raw = match op {
546 BitAnd => x & y,
547 BitOr => x | y,
548 BitXor => x ^ y,
549 Shl => shift_amount(y).map_or(U256::ZERO, |s| x.wrapping_shl(s)),
550 _ => unreachable!(),
551 };
552 return Some(Num::int(raw, signed, width));
553 }
554
555 if signed {
558 let (x, y) = (a.to_i256()?, b.to_i256()?);
559 if op == Pow {
560 return signed_pow(x, y, width);
561 }
562 let r = match width {
565 Some(bits) => {
566 let r = match op {
567 Add => x.checked_add(y)?,
568 Sub => x.checked_sub(y)?,
569 Mul => x.checked_mul(y)?,
570 Div => x.checked_div(y)?,
571 Rem => x.checked_rem(y)?,
572 _ => return None,
573 };
574 if !can_fit_int(r, bits) {
575 return None;
576 }
577 r
578 }
579 None => match op {
580 Add => x.wrapping_add(y),
581 Sub => x.wrapping_sub(y),
582 Mul => x.wrapping_mul(y),
583 Div => x.checked_div(y)?,
584 Rem => x.checked_rem(y)?,
585 _ => return None,
586 },
587 };
588 return Some(Num::int(r.into_raw(), true, width));
589 }
590
591 let (x, y) = (a.as_u256(), b.as_u256());
592 let r = match op {
593 Add => checked_arith(x.checked_add(y), x.wrapping_add(y), width)?,
597 Sub => checked_arith(x.checked_sub(y), x.wrapping_sub(y), width)?,
598 Mul => checked_arith(x.checked_mul(y), x.wrapping_mul(y), width)?,
599 Div => x.checked_div(y)?,
600 Rem => x.checked_rem(y)?,
601 Pow => {
605 let r = x.checked_pow(y)?;
606 checked_arith(Some(r), r, width)?
607 }
608 Shr => shift_amount(y).map_or(U256::ZERO, |s| x.wrapping_shr(s)),
610 _ => return None,
612 };
613 Some(Num::int(r, false, width))
614}
615
616fn checked_arith(checked: Option<U256>, wrapping: U256, width: Option<usize>) -> Option<U256> {
623 match width {
624 Some(bits) => checked.filter(|r| can_fit_uint(*r, bits)),
625 None => Some(wrapping),
626 }
627}
628
629fn signed_pow(base: I256, exp: I256, width: Option<usize>) -> Option<Num> {
632 if exp.is_negative() {
633 return None;
634 }
635 let magnitude = base.unsigned_abs().checked_pow(exp.into_raw())?;
636 let negative = base.is_negative() && exp.into_raw().bit(0);
637 let value = if negative {
638 (magnitude <= I256::MIN.into_raw()).then(|| I256::from_raw(magnitude.wrapping_neg()))?
640 } else {
641 (magnitude < I256::MIN.into_raw()).then(|| I256::from_raw(magnitude))?
643 };
644 if let Some(bits) = width
647 && !can_fit_int(value, bits)
648 {
649 return None;
650 }
651 Some(Num::int(value.into_raw(), true, width))
652}
653
654fn combine_width(a: Option<usize>, b: Option<usize>) -> Option<usize> {
657 match (a, b) {
658 (None, None) => None,
659 (Some(a), Some(b)) => Some(a.max(b)),
660 (Some(w), None) | (None, Some(w)) => Some(w),
661 }
662}
663
664fn type_min_max(ty: ast::ElementaryType, member: &str) -> Option<Num> {
666 match (ty, member) {
667 (ast::ElementaryType::UInt(size), "max") => {
668 let bits = size.bits() as usize;
669 Some(Num::int(low_bits(U256::MAX, bits), false, Some(bits)))
670 }
671 (ast::ElementaryType::UInt(size), "min") => {
672 Some(Num::int(U256::ZERO, false, Some(size.bits() as usize)))
673 }
674 (ast::ElementaryType::Int(size), "max") => {
677 let bits = size.bits() as usize;
678 Some(Num::int(low_bits(U256::MAX, bits - 1), true, Some(bits)))
679 }
680 (ast::ElementaryType::Int(size), "min") => {
681 let bits = size.bits() as usize;
682 Some(Num::int(U256::from(1).wrapping_shl(bits - 1), true, Some(bits)))
683 }
684 _ => None,
685 }
686}
687
688fn shift_amount(y: U256) -> Option<usize> {
690 (y < U256::from(256u64)).then(|| y.as_limbs()[0] as usize)
691}
692
693fn cast_num(ty: ast::ElementaryType, value: Num) -> Option<Num> {
697 match ty {
698 ast::ElementaryType::UInt(size) => {
699 Some(Num::int(value.full_raw(), false, Some(size.bits() as usize)))
700 }
701 ast::ElementaryType::Int(size) => {
702 Some(Num::int(value.full_raw(), true, Some(size.bits() as usize)))
703 }
704 ast::ElementaryType::Address(_) => Some(Num::int(value.full_raw(), false, Some(160))),
705 ast::ElementaryType::FixedBytes(size) => Some(cast_to_bytes(value, size.bytes() as usize)),
706 _ => None,
707 }
708}
709
710fn cast_to_bytes(value: Num, n: usize) -> Num {
715 let raw = match value {
716 Num::Bytes { raw, n: m } if n <= m => raw.wrapping_shr((m - n) * 8),
717 Num::Bytes { raw, n: m } => raw.wrapping_shl((n - m) * 8),
718 _ => low_bits(value.full_raw(), n * 8),
719 };
720 Num::Bytes { raw, n }
721}
722
723fn low_bits(value: U256, bits: usize) -> U256 {
725 if bits >= 256 { value } else { value & (U256::from(1).wrapping_shl(bits) - U256::from(1)) }
726}
727
728fn sign_extend(raw: U256, width: Option<usize>) -> U256 {
730 match width {
731 Some(bits) if bits < 256 && raw.bit(bits - 1) => raw | U256::MAX.wrapping_shl(bits),
732 _ => raw,
733 }
734}
735
736fn single_arg<'a, 'ast>(args: &'a ast::CallArgs<'ast>) -> Option<&'a ast::Expr<'ast>> {
738 let mut exprs = args.exprs();
739 (exprs.len() == 1).then(|| exprs.next()).flatten()
740}
741
742fn lit_bytes<'a>(expr: &'a ast::Expr<'_>) -> Option<&'a [u8]> {
745 if let ast::ExprKind::Lit(lit, _) = &expr.peel_parens().kind
746 && let ast::LitKind::Str(_, sym, _) = &lit.kind
747 {
748 return Some(sym.as_byte_str());
749 }
750 None
751}
752
753fn can_fit_int(value: I256, bits: usize) -> bool {
755 let max_val = I256::try_from((U256::from(1) << (bits - 1)) - U256::from(1))
757 .expect("max value should fit in I256");
758 let min_val = -max_val - I256::ONE;
760
761 value >= min_val && value <= max_val
762}
763
764fn can_fit_uint(value: U256, bits: usize) -> bool {
766 if bits == 256 {
767 return true;
768 }
769 let max_val = (U256::from(1) << bits) - U256::from(1);
771 value <= max_val
772}
773
774#[cfg(test)]
775mod tests {
776 use super::*;
777 use alloy_primitives::address;
778 use solar::interface::{Session, source_map};
779
780 const SOURCE: &str = r#"
781 contract Magic {
782 // plain literals
783 address constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
784 uint64 constant MAGIC_NUMBER = 1122334455;
785 int32 constant MAGIC_INT = -777;
786 bytes32 constant MAGIC_WORD = "abcd1234";
787 bytes constant MAGIC_BYTES = hex"deadbeef";
788 string constant MAGIC_STRING = "xyzzy";
789
790 // constant exprs with folding
791 uint256 constant NEG_FOLDING = uint(-2);
792 uint256 constant BIN_FOLDING = 2 * 2 ether;
793 bytes32 constant IMPLEMENTATION_SLOT = bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1);
794 }"#;
795
796 #[test]
797 fn test_literals_collector_coverage() {
798 let map = process_source_literals(SOURCE);
799
800 let addr = address!("0x6B175474E89094C44Da98b954EedeAC495271d0F").into_word();
802 let num = B256::from(U256::from(1122334455u64));
803 let int = B256::from(I256::try_from(-777i32).unwrap().into_raw());
804 let word = B256::right_padding_from(b"abcd1234");
805 let dyn_bytes = Bytes::from_static(&[0xde, 0xad, 0xbe, 0xef]);
806
807 assert_word(&map, DynSolType::Address, addr, "Expected DAI in address set");
808 assert_word(&map, DynSolType::Uint(64), num, "Expected MAGIC_NUMBER in uint64 set");
809 assert_word(&map, DynSolType::Int(32), int, "Expected MAGIC_INT in int32 set");
810 assert_word(&map, DynSolType::FixedBytes(32), word, "Expected MAGIC_WORD in bytes32 set");
811 assert!(map.strings.contains("xyzzy"), "Expected MAGIC_STRING to be collected");
812 assert!(
813 map.strings.contains("eip1967.proxy.implementation"),
814 "Expected IMPLEMENTATION_SLOT in string set"
815 );
816 assert!(map.bytes.contains(&dyn_bytes), "Expected MAGIC_BYTES in bytes set");
817
818 let neg_cast = B256::from(U256::MAX - U256::from(1));
822 assert_word(&map, DynSolType::Uint(256), neg_cast, "Expected uint(-2) to be folded");
823
824 let bin = B256::from(U256::from(4_000_000_000_000_000_000u64));
826 assert_word(&map, DynSolType::Uint(64), bin, "Expected `2 * 2 ether` to be folded");
827
828 let slot = B256::from(
831 U256::from_be_bytes(keccak256("eip1967.proxy.implementation").0) - U256::from(1),
832 );
833 assert_word(
834 &map,
835 DynSolType::FixedBytes(32),
836 slot,
837 "Expected IMPLEMENTATION_SLOT expression to be folded",
838 );
839 }
840
841 #[test]
842 fn test_literals_collector_size() {
843 let literals = process_source_literals(SOURCE);
844
845 let count = |ty: DynSolType| literals.words.get(&ty).map_or(0, |set| set.len());
847
848 assert_eq!(count(DynSolType::Address), 1, "Address literal count mismatch");
849 assert_eq!(literals.strings.len(), 3, "String literals count mismatch");
850 assert_eq!(literals.bytes.len(), 1, "Byte literals count mismatch");
851
852 assert_eq!(count(DynSolType::Uint(8)), 2, "Uint(8) count mismatch");
856 assert_eq!(count(DynSolType::Uint(16)), 3, "Uint(16) count mismatch");
857 assert_eq!(count(DynSolType::Uint(32)), 4, "Uint(32) count mismatch");
858 assert_eq!(count(DynSolType::Uint(64)), 6, "Uint(64) count mismatch");
859 assert_eq!(count(DynSolType::Uint(128)), 6, "Uint(128) count mismatch");
860 assert_eq!(count(DynSolType::Uint(256)), 9, "Uint(256) count mismatch");
861
862 assert_eq!(count(DynSolType::Int(8)), 1, "Int(8) count mismatch");
865 assert_eq!(count(DynSolType::Int(16)), 2, "Int(16) count mismatch");
866 assert_eq!(count(DynSolType::Int(32)), 2, "Int(32) count mismatch");
867 assert_eq!(count(DynSolType::Int(64)), 2, "Int(64) count mismatch");
868 assert_eq!(count(DynSolType::Int(128)), 2, "Int(128) count mismatch");
869 assert_eq!(count(DynSolType::Int(256)), 2, "Int(256) count mismatch");
870
871 assert_eq!(count(DynSolType::FixedBytes(32)), 7, "FixedBytes(32) count mismatch");
876
877 assert_eq!(
879 literals.words.values().map(|set| set.len()).sum::<usize>(),
880 49,
881 "Total word values count mismatch"
882 );
883 }
884
885 #[test]
886 fn test_width_aware_casts() {
887 let source = r#"
890 contract C {
891 uint8 constant A = uint8(-2); // 254
892 uint8 constant B = uint8(257); // 1
893 int8 constant D = int8(255); // -1
894 int256 constant E = int256(1) - 2; // -1
895 int256 constant F = ~int256(0); // -1 (not 2**256 - 1)
896 int16 constant G = int16(int8(-1)); // -1 (sign-extended)
897 int16 constant H = int16(uint8(255)); // 255 (unsigned source)
898 uint16 constant I = uint16(int8(-1)); // 65535
899 int256 constant J = -2 ** 255; // int256 min
900 bytes4 constant K = bytes4(uint32(0x12345678)); // left-aligned
901 }"#;
902 let map = process_source_literals(source);
903
904 let neg_one = B256::from(I256::try_from(-1).unwrap().into_raw());
905 assert_word(&map, DynSolType::Uint(8), B256::from(U256::from(254)), "uint8(-2) -> 254");
906 assert_word(&map, DynSolType::Uint(8), B256::from(U256::from(1)), "uint8(257) -> 1");
907 assert_word(&map, DynSolType::Int(8), neg_one, "int8(255) -> -1");
908 assert_word(&map, DynSolType::Int(256), neg_one, "int256(1) - 2 -> -1");
909 assert_word(&map, DynSolType::Int(256), neg_one, "~int256(0) -> -1");
910 assert_word(&map, DynSolType::Int(16), neg_one, "int16(int8(-1)) -> -1");
911 assert_word(
912 &map,
913 DynSolType::Int(16),
914 B256::from(U256::from(255)),
915 "int16(uint8(255)) -> 255",
916 );
917 assert_word(
918 &map,
919 DynSolType::Uint(16),
920 B256::from(U256::from(65535)),
921 "uint16(int8(-1)) -> 65535",
922 );
923 assert_word(
924 &map,
925 DynSolType::Int(256),
926 B256::from(I256::MIN.into_raw()),
927 "-2 ** 255 -> int256 min",
928 );
929
930 let left_aligned = B256::right_padding_from(&[0x12, 0x34, 0x56, 0x78]);
931 assert_word(&map, DynSolType::FixedBytes(4), left_aligned, "bytes4 is left-aligned");
932 }
933
934 #[test]
935 fn test_width_dependent_ops_stay_in_width() {
936 let source = r#"
939 contract C {
940 uint8 constant A = ~uint8(0); // 255
941 uint8 constant B = uint8(1) << 8; // 0
942 int8 constant D = int8(1) << 7; // -128
943 uint8 constant E = uint8(255) << 256; // 0 (shift amount >= 256)
944 uint8 constant F = uint8(250) + 5; // 255 (in-range, typed + untyped literal)
945 uint8 constant G = uint8(10) ** uint256(2); // 100 (in-range, result type is base uint8)
946 uint8 constant H = uint8(0x80) >> uint256(0); // 128 (result type is left uint8)
947 }"#;
948 let map = process_source_literals(source);
949
950 assert_word(&map, DynSolType::Uint(8), B256::from(U256::from(255)), "~uint8(0) -> 255");
951 assert_word(&map, DynSolType::Uint(8), B256::from(U256::ZERO), "uint8(_) << {8,256} -> 0");
952 assert_word(
953 &map,
954 DynSolType::Uint(8),
955 B256::from(U256::from(255)),
956 "uint8(250) + 5 -> 255",
957 );
958 assert_word(
959 &map,
960 DynSolType::Uint(8),
961 B256::from(U256::from(100)),
962 "uint8(10) ** 2 -> 100",
963 );
964 assert_word(
965 &map,
966 DynSolType::Uint(8),
967 B256::from(U256::from(128)),
968 "uint8(0x80) >> 0 -> 128",
969 );
970 let neg_128 = B256::from(I256::try_from(-128).unwrap().into_raw());
971 assert_word(&map, DynSolType::Int(8), neg_128, "int8(1) << 7 -> -128");
972
973 assert!(
976 !map.words
977 .get(&DynSolType::Uint(256))
978 .is_some_and(|s| s.contains(&B256::from(U256::MAX))),
979 "~uint8(0) must not seed a uint256 max"
980 );
981 }
982
983 #[test]
984 fn test_checked_overflow_does_not_seed() {
985 let source = r#"
989 contract C {
990 uint8 constant A = uint8(250) + 10; // 260 -> reverts (panic 0x11), not 4
991 uint8 constant B = uint8(200) * 2; // 400 -> reverts, not 144
992 uint8 constant C2 = uint8(1) - 2; // underflow -> reverts, not 255
993 uint8 constant D = uint8(10) ** 3; // 1000 -> reverts, not 232
994 int8 constant E = int8(100) + 100; // 200 -> reverts, not -56
995 int8 constant F = int8(64) * 2; // 128 -> reverts, not -128
996 int8 constant G = int8(5) ** 3; // 125 -> in range, folds
997 }"#;
998 let map = process_source_literals(source);
999
1000 let seeded =
1002 |ty, raw: U256| map.words.get(&ty).is_some_and(|s| s.contains(&B256::from(raw)));
1003 for bits in [8usize, 16, 32, 64, 128, 256] {
1004 assert!(
1005 !seeded(DynSolType::Uint(bits), U256::from(4)),
1006 "uint8(250)+10 must not seed 4"
1007 );
1008 assert!(
1009 !seeded(DynSolType::Uint(bits), U256::from(144)),
1010 "uint8(200)*2 must not seed 144"
1011 );
1012 assert!(
1013 !seeded(DynSolType::Uint(bits), U256::from(255)),
1014 "uint8(1)-2 must not seed 255"
1015 );
1016 assert!(
1017 !seeded(DynSolType::Uint(bits), U256::from(232)),
1018 "uint8(10)**3 must not seed 232"
1019 );
1020 }
1021 let neg_56 = I256::try_from(-56).unwrap().into_raw();
1022 let neg_128 = I256::try_from(-128).unwrap().into_raw();
1023 for bits in [8usize, 16, 32, 64, 128, 256] {
1024 assert!(!seeded(DynSolType::Int(bits), neg_56), "int8(100)+100 must not seed -56");
1025 assert!(!seeded(DynSolType::Int(bits), neg_128), "int8(64)*2 must not seed -128");
1026 }
1027
1028 assert_word(&map, DynSolType::Int(8), B256::from(U256::from(125)), "int8(5) ** 3 -> 125");
1030 assert_word(&map, DynSolType::Uint(8), B256::from(U256::from(250)), "operand uint8(250)");
1031 }
1032
1033 #[test]
1034 fn test_checked_negation_overflow_does_not_seed() {
1035 let source = r#"
1038 contract C {
1039 int8 constant A = -type(int8).min + 1; // -(-128) reverts, must not seed -127
1040 int8 constant B = -int8(1); // -1, folds normally
1041 }"#;
1042 let map = process_source_literals(source);
1043
1044 let neg_127 = I256::try_from(-127).unwrap().into_raw();
1045 let seeded =
1046 |ty, raw: U256| map.words.get(&ty).is_some_and(|s| s.contains(&B256::from(raw)));
1047 for bits in [8usize, 16, 32, 64, 128, 256] {
1048 assert!(
1049 !seeded(DynSolType::Int(bits), neg_127),
1050 "-type(int8).min + 1 must not seed -127"
1051 );
1052 }
1053 let neg_one = B256::from(I256::try_from(-1).unwrap().into_raw());
1054 assert_word(&map, DynSolType::Int(8), neg_one, "-int8(1) -> -1");
1055 }
1056
1057 #[test]
1058 fn test_fixed_bytes_folding() {
1059 let source = r#"
1062 contract C {
1063 bytes4 constant A = bytes4(uint256(0xdeadbeef12345678)); // low 4 bytes, not zero
1064 bytes2 constant B = bytes2(bytes4(uint32(0x12345678))); // keep left -> 0x1234
1065 bytes4 constant D = bytes4(bytes2(uint16(0x1234))); // pad right -> 0x12340000
1066 }"#;
1067 let map = process_source_literals(source);
1068
1069 let low4 = B256::right_padding_from(&[0x12, 0x34, 0x56, 0x78]);
1070 assert_word(&map, DynSolType::FixedBytes(4), low4, "bytes4 must not fold to zero");
1071 let left2 = B256::right_padding_from(&[0x12, 0x34]);
1072 assert_word(&map, DynSolType::FixedBytes(2), left2, "bytes2(bytes4(..)) keeps left bytes");
1073 let padded = B256::right_padding_from(&[0x12, 0x34, 0x00, 0x00]);
1074 assert_word(&map, DynSolType::FixedBytes(4), padded, "bytes4(bytes2(..)) pads right");
1075 }
1076
1077 #[test]
1078 fn test_type_min_max_folding() {
1079 let source = r#"
1080 contract C {
1081 uint256 constant A = type(uint256).max;
1082 uint8 constant B = type(uint8).max; // 255
1083 int256 constant D = type(int256).min;
1084 int256 constant E = type(int256).max;
1085 uint256 constant F = type(uint256).max - 1;
1086 int8 constant G = type(int8).min; // -128
1087 int24 constant H = type(int24).min; // -2**23
1088 }"#;
1089 let map = process_source_literals(source);
1090
1091 assert_word(&map, DynSolType::Uint(256), B256::from(U256::MAX), "type(uint256).max");
1092 assert_word(&map, DynSolType::Uint(8), B256::from(U256::from(255)), "type(uint8).max");
1093 assert_word(
1094 &map,
1095 DynSolType::Int(256),
1096 B256::from(I256::MIN.into_raw()),
1097 "type(int256).min",
1098 );
1099 assert_word(
1100 &map,
1101 DynSolType::Int(256),
1102 B256::from(I256::MAX.into_raw()),
1103 "type(int256).max",
1104 );
1105 let max_minus_one = B256::from(U256::MAX - U256::from(1));
1106 assert_word(&map, DynSolType::Uint(256), max_minus_one, "type(uint256).max - 1");
1107 let min8 = B256::from(I256::try_from(-128).unwrap().into_raw());
1108 assert_word(&map, DynSolType::Int(8), min8, "type(int8).min -> -128");
1109 let min24 = B256::from(I256::try_from(-(1i64 << 23)).unwrap().into_raw());
1110 assert_word(&map, DynSolType::Int(24), min24, "type(int24).min -> -2**23");
1111 }
1112
1113 #[test]
1114 fn test_address_cast_seeds_address_type() {
1115 let source = r#"
1118 contract C {
1119 address constant A = address(4660);
1120 }"#;
1121 let map = process_source_literals(source);
1122
1123 assert_word(
1124 &map,
1125 DynSolType::Address,
1126 B256::from(low_bits(U256::from(4660), 160)),
1127 "address(4660) -> address",
1128 );
1129 assert_eq!(map.words.get(&DynSolType::Uint(160)), None, "must not seed a uint160 bucket");
1130 }
1131
1132 #[test]
1133 fn test_max_values_is_respected() {
1134 let source = r#"
1137 contract C {
1138 string constant A = "aaa";
1139 string constant B = "bbb";
1140 string constant D = "ccc";
1141 }"#;
1142 let map = process_source_literals_with_max(source, 2);
1143
1144 let total = map.words.values().map(|set| set.len()).sum::<usize>()
1145 + map.strings.len()
1146 + map.bytes.len();
1147 assert!(total <= 2, "max_values not respected: collected {total} values");
1148 }
1149
1150 fn process_source_literals(source: &str) -> LiteralMaps {
1153 process_source_literals_with_max(source, usize::MAX)
1154 }
1155
1156 fn process_source_literals_with_max(source: &str, max_values: usize) -> LiteralMaps {
1157 let mut compiler =
1158 solar::sema::Compiler::new(Session::builder().with_stderr_emitter().build());
1159 compiler
1160 .enter_mut(|c| -> std::io::Result<()> {
1161 let mut pcx = c.parse();
1162 pcx.set_resolve_imports(false);
1163
1164 pcx.add_file(
1165 c.sess().source_map().new_source_file(source_map::FileName::Stdin, source)?,
1166 );
1167 pcx.parse();
1168 let _ = c.lower_asts();
1169 Ok(())
1170 })
1171 .expect("Failed to compile test source");
1172
1173 LiteralsCollector::process(&std::sync::Arc::new(compiler), None, max_values)
1174 }
1175
1176 fn assert_word(literals: &LiteralMaps, ty: DynSolType, value: B256, msg: &str) {
1177 assert!(literals.words.get(&ty).is_some_and(|set| set.contains(&value)), "{}", msg);
1178 }
1179}