diff --git a/Cargo.lock b/Cargo.lock index 431407f4..af2cf417 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -762,6 +762,16 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + [[package]] name = "fastrand" version = "2.0.1" @@ -1143,6 +1153,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + [[package]] name = "indexmap" version = "1.9.3" @@ -2402,10 +2418,12 @@ version = "0.2.0" dependencies = [ "analyzers", "ethers-core", + "eyre", "graph", "hex", "keccak-hash", "petgraph", + "pyrometer", "shared", "solang-parser", "tracing", diff --git a/Cargo.toml b/Cargo.toml index e3e5e410..172c831a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ debug = true [profile.dev] # opt-level = 1 # Enable some optimizations like tail call -inline = true +# inline = true [profile.bench] debug = true diff --git a/crates/graph/src/nodes/concrete.rs b/crates/graph/src/nodes/concrete.rs index f0521974..4dea2f35 100644 --- a/crates/graph/src/nodes/concrete.rs +++ b/crates/graph/src/nodes/concrete.rs @@ -878,6 +878,29 @@ impl Concrete { } } + pub fn needed_size(&self) -> Option { + match self { + Concrete::Uint(_, val) => Some(((32 - (val.leading_zeros() / 8)) * 8).max(8) as u16), + Concrete::Int(_, val) => { + let unsigned = val.unsigned_abs(); + Some(((32 - (unsigned.leading_zeros() / 8)) * 8).max(8) as u16) + } + _ => None, + } + } + + pub fn fit_size(self) -> Self { + if let Some(needed) = self.needed_size() { + match self { + Concrete::Uint(_, val) => Concrete::Uint(needed, val), + Concrete::Int(_, val) => Concrete::Int(needed, val), + _ => self, + } + } else { + self + } + } + /// If its `Concrete::Uint`, gets the value pub fn uint_val(&self) -> Option { match self { diff --git a/crates/graph/src/range/solc_range.rs b/crates/graph/src/range/solc_range.rs index 6fb513cb..151971fe 100644 --- a/crates/graph/src/range/solc_range.rs +++ b/crates/graph/src/range/solc_range.rs @@ -171,30 +171,12 @@ impl SolcRange { | c @ Concrete::Int(_, _) | c @ Concrete::Bool(_) | c @ Concrete::Address(_) + | c @ Concrete::String(_) | c @ Concrete::Bytes(_, _) => Some(SolcRange::new( Elem::Concrete(RangeConcrete::new(c.clone(), Loc::Implicit)), Elem::Concrete(RangeConcrete::new(c, Loc::Implicit)), vec![], )), - Concrete::String(s) => { - let val = s - .chars() - .enumerate() - .map(|(i, v)| { - let idx = Elem::from(Concrete::from(U256::from(i))); - let mut bytes = [0x00; 32]; - v.encode_utf8(&mut bytes[..]); - let v = Elem::from(Concrete::Bytes(1, H256::from(bytes))); - (idx, v) - }) - .collect::>(); - let r = Elem::ConcreteDyn(RangeDyn::new( - Elem::from(Concrete::from(U256::from(s.len()))), - val, - Loc::Implicit, - )); - Some(SolcRange::new(r.clone(), r, vec![])) - } Concrete::DynBytes(b) => { let val = b .iter() diff --git a/crates/graph/src/solvers/dl.rs b/crates/graph/src/solvers/dl.rs index 443d0a3d..df850525 100644 --- a/crates/graph/src/solvers/dl.rs +++ b/crates/graph/src/solvers/dl.rs @@ -570,10 +570,10 @@ impl DLSolver { added_atoms.push((*dyn_elem).clone()); self.graph_map.insert((*dyn_elem).clone(), idx); if let Some(dep) = dep { - if !self.var_to_atom_idx.contains_key(&dep) { + self.var_to_atom_idx.entry(dep).or_insert_with(|| { added_deps.push(dep); - self.var_to_atom_idx.insert(dep, idx); - } + idx + }); } idx }; diff --git a/crates/graph/src/var_type.rs b/crates/graph/src/var_type.rs index 3951cd77..f7cfa9d1 100644 --- a/crates/graph/src/var_type.rs +++ b/crates/graph/src/var_type.rs @@ -407,6 +407,7 @@ impl VarType { } pub fn range(&self, analyzer: &impl GraphBackend) -> Result, GraphError> { + println!("here: {:?}", self); match self { Self::User(_, Some(range)) => Ok(Some(range.clone())), Self::BuiltIn(_, Some(range)) => Ok(Some(range.clone())), diff --git a/crates/pyrometer/src/graph_backend.rs b/crates/pyrometer/src/graph_backend.rs index 8fd62682..1f3a7523 100644 --- a/crates/pyrometer/src/graph_backend.rs +++ b/crates/pyrometer/src/graph_backend.rs @@ -572,10 +572,10 @@ pub fn arena_mermaid_node( format!("{indent}{}{{{{\"{}\"}}}}", idx.index(), arena_idx) } ArenaNode::ELEM(label) => { - format!("{indent}{}(\"{}\")", idx.index(), label.replace("\"", "'")) + format!("{indent}{}(\"{}\")", idx.index(), label.replace('"', "'")) } ArenaNode::CVAR(label) => { - format!("{indent}{}(\"{}\")", idx.index(), label.replace("\"", "'")) + format!("{indent}{}(\"{}\")", idx.index(), label.replace('"', "'")) } }; diff --git a/crates/solc-expressions/Cargo.toml b/crates/solc-expressions/Cargo.toml index d94fd868..38a5a4b8 100644 --- a/crates/solc-expressions/Cargo.toml +++ b/crates/solc-expressions/Cargo.toml @@ -23,3 +23,7 @@ tracing.workspace = true tracing-subscriber.workspace = true keccak-hash = "0.10.0" + +[dev-dependencies] +pyrometer.workspace = true +eyre = "0.6" diff --git a/crates/solc-expressions/src/context_builder/expr.rs b/crates/solc-expressions/src/context_builder/expr.rs index 476e980f..2cf18a49 100644 --- a/crates/solc-expressions/src/context_builder/expr.rs +++ b/crates/solc-expressions/src/context_builder/expr.rs @@ -81,13 +81,15 @@ pub trait ExpressionParser: HexNumberLiteral(loc, b, _unit) => self.hex_num_literal(ctx, *loc, b, false), HexLiteral(hexes) => self.hex_literals(ctx, hexes), RationalNumberLiteral(loc, integer, fraction, exp, unit) => { - self.rational_number_literal(arena, ctx, *loc, integer, fraction, exp, unit) + self.rational_number_literal(arena, ctx, *loc, integer, fraction, exp, unit, false) } Negate(_loc, expr) => match &**expr { NumberLiteral(loc, int, exp, unit) => { self.number_literal(ctx, *loc, int, exp, true, unit) } HexNumberLiteral(loc, b, _unit) => self.hex_num_literal(ctx, *loc, b, true), + RationalNumberLiteral(loc, integer, fraction, exp, unit) => self + .rational_number_literal(arena, ctx, *loc, integer, fraction, exp, unit, true), e => { self.parse_ctx_expr(arena, e, ctx)?; self.apply_to_edges(ctx, e.loc(), arena, &|analyzer, arena, ctx, loc| { diff --git a/crates/solc-expressions/src/literal.rs b/crates/solc-expressions/src/literal.rs index dac3bcf6..3ed3e6c3 100644 --- a/crates/solc-expressions/src/literal.rs +++ b/crates/solc-expressions/src/literal.rs @@ -1,6 +1,6 @@ use graph::{ elem::*, - nodes::{Builtin, Concrete, ConcreteNode, ContextNode, ContextVar, ContextVarNode, ExprRet}, + nodes::{Concrete, ConcreteNode, ContextNode, ContextVar, ContextVarNode, ExprRet}, AnalyzerBackend, ContextEdge, Edge, Node, }; use shared::{ExprErr, IntoExprErr, RangeArena}; @@ -14,6 +14,7 @@ impl Literal for T where T: AnalyzerBackend + Sized {} /// Dealing with literal expression and parsing them into nodes pub trait Literal: AnalyzerBackend + Sized { + /// 123, 1e18, -56 fn number_literal( &mut self, ctx: ContextNode, @@ -23,9 +24,11 @@ pub trait Literal: AnalyzerBackend + Sized { negative: bool, unit: &Option, ) -> Result<(), ExprErr> { - let int = U256::from_dec_str(integer).unwrap(); + let int = + U256::from_dec_str(integer).map_err(|e| ExprErr::ParseError(loc, e.to_string()))?; let val = if !exponent.is_empty() { - let exp = U256::from_dec_str(exponent).unwrap(); + let exp = U256::from_dec_str(exponent) + .map_err(|e| ExprErr::ParseError(loc, e.to_string()))?; int * U256::from(10).pow(exp) } else { int @@ -43,7 +46,14 @@ pub trait Literal: AnalyzerBackend + Sized { // no need to set upper bit I256::from_raw(val) } else { - I256::from(-1i32) * I256::from_raw(val) + let raw = I256::from_raw(val); + if raw < 0.into() { + return Err(ExprErr::ParseError( + loc, + "Negative value cannot fit into int256".to_string(), + )); + } + I256::from(-1i32) * raw }; ConcreteNode::from(self.add_node(Node::Concrete(Concrete::Int(size, val)))) } else { @@ -73,6 +83,7 @@ pub trait Literal: AnalyzerBackend + Sized { } } + /// 1.0001e18 fn rational_number_literal( &mut self, arena: &mut RangeArena>, @@ -82,39 +93,84 @@ pub trait Literal: AnalyzerBackend + Sized { fraction: &str, exponent: &str, unit: &Option, + negative: bool, ) -> Result<(), ExprErr> { - let int = U256::from_dec_str(integer).unwrap(); + let int = + U256::from_dec_str(integer).map_err(|e| ExprErr::ParseError(loc, e.to_string()))?; let exp = if !exponent.is_empty() { - U256::from_dec_str(exponent).unwrap() + U256::from_dec_str(exponent).map_err(|e| ExprErr::ParseError(loc, e.to_string()))? } else { U256::from(0) }; let fraction_len = fraction.len(); let fraction_denom = U256::from(10).pow(fraction_len.into()); - let fraction = U256::from_dec_str(fraction).unwrap(); + let fraction = + U256::from_dec_str(fraction).map_err(|e| ExprErr::ParseError(loc, e.to_string()))?; - let int_elem = Elem::min( + let unit_num = if let Some(unit) = unit { + self.unit_to_uint(unit) + } else { + U256::from(1) + }; + + let int_elem = Elem::max( Elem::from(Concrete::from(int)), Elem::from(Concrete::from(U256::from(1))), ); - let exp_elem = Elem::from(Concrete::from(exp)); - let mut rational_range = (Elem::from(Concrete::from(fraction)) - + int_elem * Elem::from(Concrete::from(fraction_denom))) - * Elem::from(Concrete::from(U256::from(10))).pow(exp_elem); - if let Some(unit) = unit { - rational_range = rational_range * Elem::from(Concrete::from(self.unit_to_uint(unit))) + // move the decimal place to the right + let mut rational_range = int_elem * Elem::from(Concrete::from(fraction_denom)); + // add the fraction + rational_range = rational_range + Elem::from(Concrete::from(fraction)); + let mut rhs_power_res = U256::from(10).pow(exp) * unit_num; + + if fraction > rhs_power_res { + return Err(ExprErr::ParseError( + loc, + format!("Invalid rational number: fraction part ({fraction}) has more precision than exponent ({exp}) and unit provide ({unit_num})"), + )); } - let cvar = - ContextVar::new_from_builtin(loc, self.builtin_or_add(Builtin::Uint(256)).into(), self) - .into_expr_err(loc)?; - let node = ContextVarNode::from(self.add_node(Node::ContextVar(cvar))); - node.set_range_max(self, arena, rational_range.clone()) - .into_expr_err(loc)?; - node.set_range_min(self, arena, rational_range) - .into_expr_err(loc)?; + // decrease the exponentiation by the number of places we moved the decimal over + rhs_power_res /= fraction_denom; + + rational_range = rational_range * Elem::from(Concrete::from(rhs_power_res)); + + let concrete_node = if negative { + let evaled = rational_range.maximize(self, arena).into_expr_err(loc)?; + let val = evaled.maybe_concrete().unwrap().val.uint_val().unwrap(); + if val > U256::from(2).pow(255.into()) { + return Err(ExprErr::ParseError( + loc, + "Negative value cannot fit into int256".to_string(), + )); + } + rational_range = rational_range * Elem::from(Concrete::from(I256::from(-1i32))); + let evaled = rational_range + .maximize(self, arena) + .into_expr_err(loc)? + .maybe_concrete() + .unwrap() + .val + .fit_size(); + + ConcreteNode::from(self.add_node(Node::Concrete(evaled))) + } else { + let evaled = rational_range + .maximize(self, arena) + .into_expr_err(loc)? + .maybe_concrete() + .unwrap() + .val + .fit_size(); + ConcreteNode::from(self.add_node(Node::Concrete(evaled))) + }; + let ccvar = Node::ContextVar( + ContextVar::new_from_concrete(loc, ctx, concrete_node, self).into_expr_err(loc)?, + ); + + let node = ContextVarNode::from(self.add_node(ccvar)); ctx.add_var(node, self).into_expr_err(loc)?; self.add_edge(node, ctx, Edge::Context(ContextEdge::Variable)); ctx.push_expr(ExprRet::SingleLiteral(node.into()), self) @@ -122,6 +178,7 @@ pub trait Literal: AnalyzerBackend + Sized { Ok(()) } + /// 0x7B fn hex_num_literal( &mut self, ctx: ContextNode, @@ -130,10 +187,18 @@ pub trait Literal: AnalyzerBackend + Sized { negative: bool, ) -> Result<(), ExprErr> { let integer: String = integer.chars().filter(|c| *c != '_').collect(); - let val = U256::from_str_radix(&integer, 16).unwrap(); - let size: u16 = ((32 - (val.leading_zeros() / 8)) * 8) as u16; + let val = U256::from_str_radix(&integer, 16) + .map_err(|e| ExprErr::ParseError(loc, e.to_string()))?; + let size: u16 = (((32 - (val.leading_zeros() / 8)) * 8).max(8)) as u16; let concrete_node = if negative { - let val = I256::from(-1i32) * I256::from_raw(val); + let raw = I256::from_raw(val); + if raw < 0.into() { + return Err(ExprErr::ParseError( + loc, + "Negative value cannot fit into int256".to_string(), + )); + } + let val = I256::from(-1i32) * raw; ConcreteNode::from(self.add_node(Node::Concrete(Concrete::Int(size, val)))) } else { ConcreteNode::from(self.add_node(Node::Concrete(Concrete::Uint(size, val)))) @@ -150,57 +215,45 @@ pub trait Literal: AnalyzerBackend + Sized { Ok(()) } + /// hex"123123" fn hex_literals(&mut self, ctx: ContextNode, hexes: &[HexLiteral]) -> Result<(), ExprErr> { - if hexes.len() == 1 { - let mut h = H256::default(); - let mut max = 0; - if let Ok(hex_val) = hex::decode(&hexes[0].hex) { - hex_val.iter().enumerate().for_each(|(i, hex_byte)| { - if *hex_byte != 0x00u8 { - max = i as u8; - } - h.0[i] = *hex_byte; - }); + let mut h = vec![]; + hexes.iter().for_each(|sub_hex| { + if let Ok(hex_val) = hex::decode(&sub_hex.hex) { + h.extend(hex_val) } + }); - let concrete_node = - ConcreteNode::from(self.add_node(Node::Concrete(Concrete::Bytes(max + 1, h)))); - let ccvar = Node::ContextVar( - ContextVar::new_from_concrete(hexes[0].loc, ctx, concrete_node, self) - .into_expr_err(hexes[0].loc)?, - ); - let node = self.add_node(ccvar); - ctx.add_var(node.into(), self).into_expr_err(hexes[0].loc)?; - self.add_edge(node, ctx, Edge::Context(ContextEdge::Variable)); - ctx.push_expr(ExprRet::SingleLiteral(node), self) - .into_expr_err(hexes[0].loc)?; - Ok(()) - } else { - let mut h = vec![]; - hexes.iter().for_each(|sub_hex| { - if let Ok(hex_val) = hex::decode(&sub_hex.hex) { - h.extend(hex_val) + let mut loc = hexes[0].loc; + loc.use_end_from(&hexes[hexes.len() - 1].loc); + + let concrete_node = if h.len() <= 32 { + let mut target = H256::default(); + let mut max = 1; + h.iter().enumerate().for_each(|(i, hex_byte)| { + if *hex_byte != 0x00u8 { + max = i as u8 + 1; } + target.0[i] = *hex_byte; }); + ConcreteNode::from(self.add_node(Node::Concrete(Concrete::Bytes(max, target)))) + } else { + ConcreteNode::from(self.add_node(Node::Concrete(Concrete::DynBytes(h)))) + }; - let mut loc = hexes[0].loc; - loc.use_end_from(&hexes[hexes.len() - 1].loc); - let concrete_node = - ConcreteNode::from(self.add_node(Node::Concrete(Concrete::DynBytes(h)))); - let ccvar = Node::ContextVar( - ContextVar::new_from_concrete(loc, ctx, concrete_node, self).into_expr_err(loc)?, - ); - let node = self.add_node(ccvar); - ctx.add_var(node.into(), self).into_expr_err(loc)?; - self.add_edge(node, ctx, Edge::Context(ContextEdge::Variable)); - ctx.push_expr(ExprRet::SingleLiteral(node), self) - .into_expr_err(loc)?; - Ok(()) - } + let ccvar = Node::ContextVar( + ContextVar::new_from_concrete(loc, ctx, concrete_node, self).into_expr_err(loc)?, + ); + let node = self.add_node(ccvar); + ctx.add_var(node.into(), self).into_expr_err(loc)?; + self.add_edge(node, ctx, Edge::Context(ContextEdge::Variable)); + ctx.push_expr(ExprRet::SingleLiteral(node), self) + .into_expr_err(loc)?; + Ok(()) } fn address_literal(&mut self, ctx: ContextNode, loc: Loc, addr: &str) -> Result<(), ExprErr> { - let addr = Address::from_str(addr).unwrap(); + let addr = Address::from_str(addr).map_err(|e| ExprErr::ParseError(loc, e.to_string()))?; let concrete_node = ConcreteNode::from(self.add_node(Node::Concrete(Concrete::Address(addr)))); @@ -242,3 +295,759 @@ pub trait Literal: AnalyzerBackend + Sized { Ok(()) } } + +#[cfg(test)] +mod tests { + + use super::*; + use eyre::Result; + use graph::nodes::Context; + use graph::nodes::Function; + use pyrometer::Analyzer; + use solang_parser::pt::Loc; + + fn make_context_node_for_analyzer(analyzer: &mut Analyzer) -> ContextNode { + // need to make a function, then provide the function to the new Context + let func = Function::default(); + let func_node = analyzer.graph.add_node(Node::Function(func)).into(); + + let loc = Loc::File(0, 0, 0); + let ctx = Context::new(func_node, "test_fn".to_string(), loc); + + ContextNode::from(analyzer.graph.add_node(Node::Context(ctx))) + } + + fn test_number_literal( + num_literal: &str, + exponent: &str, + negative: bool, + unit: Option, + expected: Concrete, + ) -> Result<()> { + // setup + let mut analyzer = Analyzer { + debug_panic: true, + ..Default::default() + }; + let mut arena_base = RangeArena::default(); + let arena = &mut arena_base; + let ctx = make_context_node_for_analyzer(&mut analyzer); + let loc = Loc::File(0, 0, 0); + + // create a number literal + analyzer.number_literal(ctx, loc, num_literal, exponent, negative, &unit)?; + + // checks + let stack = &ctx.underlying(&analyzer)?.expr_ret_stack; + assert!( + stack.len() == 1, + "ret stack length should be 1, got {}", + stack.len() + ); + assert!( + stack[0].is_single(), + "ret stack[0] should be a single literal, got {:?}", + stack[0] + ); + assert!( + stack[0].has_literal(), + "ret stack[0] should have a literal, got {:?}", + stack[0] + ); + assert!( + stack[0].literals_list()?.len() == 1, + "ret stack[0] should have a single literal in the literal list" + ); + let cvar_node = ContextVarNode::from(stack[0].expect_single()?); + assert!(cvar_node.is_const(&analyzer, arena)?); + let min = cvar_node.evaled_range_min(&analyzer, arena)?.unwrap(); + let conc_value = min.maybe_concrete().unwrap().val; + assert!( + conc_value == expected, + "Values do not match: {:?} != {:?}", + conc_value, + expected + ); + Ok(()) + } + + #[test] + fn test_number_literal_positive() -> Result<()> { + let num_literal = "123"; + let expected = Concrete::Uint(8, U256::from_dec_str(num_literal).unwrap()); + test_number_literal(num_literal, "", false, None, expected) + } + + #[test] + fn test_number_literal_positive_overflow() -> Result<()> { + let num_literal = + "115792089237316195423570985008687907853269984665640564039457584007913129639936"; + let expected = Concrete::Uint(8, U256::default()); // we aren't using `expected` + let result = test_number_literal(num_literal, "", false, None, expected); + assert!(result.is_err(), "expected an error, got {:?}", result); + Ok(()) + } + + #[test] + fn test_number_literal_positive_with_exponent() -> Result<()> { + // 123e18 + let num_literal = "123"; + let exponent = "10"; + let expected = Concrete::Uint(48, U256::from_dec_str("1230000000000").unwrap()); + test_number_literal(num_literal, exponent, false, None, expected) + } + + #[test] + fn test_number_literal_positive_with_zero_exponent() -> Result<()> { + let num_literal = "123"; + let exponent = "0"; + let expected = Concrete::Uint(8, U256::from_dec_str("123").unwrap()); + test_number_literal(num_literal, exponent, false, None, expected) + } + + #[test] + fn test_number_literal_positive_with_zero_exponent_and_unit() -> Result<()> { + let num_literal = "123"; + let exponent = "0"; + let unit = Some(Identifier { + name: "ether".into(), + loc: Loc::File(0, 0, 0), + }); + let expected = Concrete::Uint(72, U256::from_dec_str("123000000000000000000").unwrap()); + test_number_literal(num_literal, exponent, false, unit, expected) + } + + #[test] + fn test_number_literal_positive_with_unit() -> Result<()> { + let num_literal = "123"; + let exponent = ""; + let unit = Some(Identifier { + name: "ether".into(), + loc: Loc::File(0, 0, 0), + }); + let expected = Concrete::Uint(72, U256::from_dec_str("123000000000000000000").unwrap()); + test_number_literal(num_literal, exponent, false, unit, expected) + } + + #[test] + fn test_number_literal_negative() -> Result<()> { + let num_literal = "123"; + let expected = Concrete::Int(8, I256::from_dec_str("-123").unwrap()); + test_number_literal(num_literal, "", true, None, expected) + } + + #[test] + fn test_number_literal_negative_zero() -> Result<()> { + let num_literal = "0"; + let expected = Concrete::Int(8, I256::from_dec_str("0").unwrap()); + test_number_literal(num_literal, "", true, None, expected) + } + + #[test] + fn test_number_literal_max() -> Result<()> { + let num_literal = + "57896044618658097711785492504343953926634992332820282019728792003956564819968"; + let expected = Concrete::Int( + 256, + I256::from_dec_str( + "-57896044618658097711785492504343953926634992332820282019728792003956564819968", + ) + .unwrap(), + ); + test_number_literal(num_literal, "", true, None, expected) + } + + #[test] + fn test_number_literal_negative_too_large() -> Result<()> { + let num_literal = + "57896044618658097711785492504343953926634992332820282019728792003956564819969"; + let expected = Concrete::Int(8, I256::default()); // this doesn't matter since we arent using `expected` + let result = test_number_literal(num_literal, "", true, None, expected); + assert!(result.is_err(), "expected an error, got {:?}", result); + Ok(()) + } + + fn test_rational_number_literal( + integer: &str, + fraction: &str, + exponent: &str, + negative: bool, + unit: Option, + expected: Concrete, + ) -> Result<()> { + // setup + let mut analyzer = Analyzer { + debug_panic: true, + ..Default::default() + }; + let mut arena_base = RangeArena::default(); + let arena = &mut arena_base; + let ctx = make_context_node_for_analyzer(&mut analyzer); + let loc = Loc::File(0, 0, 0); + + // create a rational number literal + analyzer.rational_number_literal( + arena, ctx, loc, integer, fraction, exponent, &unit, negative, + )?; + + // checks + let stack = &ctx.underlying(&analyzer)?.expr_ret_stack; + assert!( + stack.len() == 1, + "ret stack length should be 1, got {}", + stack.len() + ); + assert!( + stack[0].is_single(), + "ret stack[0] should be a single literal, got {:?}", + stack[0] + ); + assert!( + stack[0].has_literal(), + "ret stack[0] should have a literal, got {:?}", + stack[0] + ); + assert!( + stack[0].literals_list()?.len() == 1, + "ret stack[0] should have a single literal in the literal list" + ); + let cvar_node = ContextVarNode::from(stack[0].expect_single()?); + assert!(cvar_node.is_const(&analyzer, arena)?); + let min = cvar_node.evaled_range_min(&analyzer, arena)?.unwrap(); + let conc_value = min.maybe_concrete().unwrap().val; + assert!( + conc_value == expected, + "Values do not match: {:?} != {:?}", + conc_value, + expected + ); + Ok(()) + } + + #[test] + fn test_rational_number_literal_positive() -> Result<()> { + let integer = "1"; + let fraction = "00001"; + let exponent = "18"; + let expected = Concrete::Uint(64, U256::from_dec_str("1000010000000000000").unwrap()); + test_rational_number_literal(integer, fraction, exponent, false, None, expected) + } + + #[test] + fn test_rational_number_literal_positive_fraction() -> Result<()> { + let integer = "23"; + let fraction = "5"; + let exponent = "5"; + let expected = Concrete::Uint(24, U256::from_dec_str("2350000").unwrap()); + test_rational_number_literal(integer, fraction, exponent, false, None, expected) + } + + #[test] + fn test_rational_number_literal_negative() -> Result<()> { + let integer = "23"; + let fraction = "5"; + let exponent = "5"; + let expected = Concrete::Int(24, I256::from_dec_str("-2350000").unwrap()); + test_rational_number_literal(integer, fraction, exponent, true, None, expected) + } + + #[test] + fn test_rational_number_literal_with_unit() -> Result<()> { + let integer = "1"; + let fraction = "5"; + let exponent = "0"; + let unit = Some(Identifier { + name: "ether".into(), + loc: Loc::File(0, 0, 0), + }); + let expected = Concrete::Uint(64, U256::from_dec_str("1500000000000000000").unwrap()); + test_rational_number_literal(integer, fraction, exponent, false, unit, expected) + } + + fn test_hex_num_literal(hex_literal: &str, negative: bool, expected: Concrete) -> Result<()> { + // setup + let mut analyzer = Analyzer { + debug_panic: true, + ..Default::default() + }; + let mut arena_base = RangeArena::default(); + let arena = &mut arena_base; + let ctx = make_context_node_for_analyzer(&mut analyzer); + let loc = Loc::File(0, 0, 0); + + // create a hex number literal + analyzer.hex_num_literal(ctx, loc, hex_literal, negative)?; + + // checks + let stack = &ctx.underlying(&analyzer)?.expr_ret_stack; + assert!( + stack.len() == 1, + "ret stack length should be 1, got {}", + stack.len() + ); + assert!( + stack[0].is_single(), + "ret stack[0] should be a single literal, got {:?}", + stack[0] + ); + assert!( + stack[0].has_literal(), + "ret stack[0] should have a literal, got {:?}", + stack[0] + ); + assert!( + stack[0].literals_list()?.len() == 1, + "ret stack[0] should have a single literal in the literal list" + ); + let cvar_node = ContextVarNode::from(stack[0].expect_single()?); + assert!(cvar_node.is_const(&analyzer, arena)?); + let min = cvar_node.evaled_range_min(&analyzer, arena)?.unwrap(); + let conc_value = min.maybe_concrete().unwrap().val; + assert!( + conc_value == expected, + "Values do not match: {:?} != {:?}", + conc_value, + expected + ); + Ok(()) + } + + #[test] + fn test_hex_num_literal_positive() -> Result<()> { + let hex_literal = "7B"; // 123 in decimal + let expected = Concrete::Uint(8, U256::from_dec_str("123").unwrap()); + test_hex_num_literal(hex_literal, false, expected) + } + + #[test] + fn test_hex_num_literal_negative() -> Result<()> { + let hex_literal = "7B"; // 123 in decimal + let expected = Concrete::Int(8, I256::from_dec_str("-123").unwrap()); + test_hex_num_literal(hex_literal, true, expected) + } + + #[test] + fn test_hex_num_literal_large_positive() -> Result<()> { + let hex_literal = "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"; // max U256 + let expected = Concrete::Uint( + 256, + U256::from_dec_str( + "115792089237316195423570985008687907853269984665640564039457584007913129639935", + ) + .unwrap(), + ); + test_hex_num_literal(hex_literal, false, expected) + } + + #[test] + fn test_hex_num_literal_large_negative() -> Result<()> { + let hex_literal = "7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"; // -1 + let expected = Concrete::Int( + 256, + I256::from_dec_str( + "-57896044618658097711785492504343953926634992332820282019728792003956564819967", + ) + .unwrap(), + ); + test_hex_num_literal(hex_literal, true, expected) + } + + #[test] + fn test_hex_num_literal_too_large_negative() -> Result<()> { + let hex_literal = "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"; // max U256 + let expected = Concrete::Int(256, I256::default()); // doesn't matter since it's out of range + assert!(test_hex_num_literal(hex_literal, true, expected).is_err()); + Ok(()) + } + + #[test] + fn test_hex_num_literal_zero() -> Result<()> { + let hex_literal = "0"; // zero + let expected = Concrete::Uint(8, U256::from_dec_str("0").unwrap()); + test_hex_num_literal(hex_literal, false, expected) + } + + #[test] + fn test_hex_num_literal_min_positive() -> Result<()> { + let hex_literal = "1"; // smallest positive value + let expected = Concrete::Uint(8, U256::from_dec_str("1").unwrap()); + test_hex_num_literal(hex_literal, false, expected) + } + + #[test] + fn test_hex_num_literal_min_negative() -> Result<()> { + let hex_literal = "1"; // smallest negative value + let expected = Concrete::Int(8, I256::from_dec_str("-1").unwrap()); + test_hex_num_literal(hex_literal, true, expected) + } + + #[test] + fn test_hex_num_literal_just_below_max_positive() -> Result<()> { + let hex_literal = "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE"; // just below max U256 + let expected = Concrete::Uint( + 256, + U256::from_dec_str( + "115792089237316195423570985008687907853269984665640564039457584007913129639934", + ) + .unwrap(), + ); + test_hex_num_literal(hex_literal, false, expected) + } + + #[test] + fn test_hex_num_literal_negative_just_above_min_negative() -> Result<()> { + let hex_literal = "7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE"; // just above min I256 + let expected = Concrete::Int( + 256, + I256::from_dec_str( + "-57896044618658097711785492504343953926634992332820282019728792003956564819966", + ) + .unwrap(), + ); + test_hex_num_literal(hex_literal, true, expected) + } + + fn test_hex_literals(hex_literals: &[HexLiteral], expected: Concrete) -> Result<()> { + // setup + let mut analyzer = Analyzer { + debug_panic: true, + ..Default::default() + }; + let mut arena_base = RangeArena::default(); + let arena = &mut arena_base; + let ctx = make_context_node_for_analyzer(&mut analyzer); + + // create hex literals + analyzer.hex_literals(ctx, hex_literals)?; + + // checks + let stack = &ctx.underlying(&analyzer)?.expr_ret_stack; + assert!( + stack.len() == 1, + "ret stack length should be 1, got {}", + stack.len() + ); + assert!( + stack[0].is_single(), + "ret stack[0] should be a single literal, got {:?}", + stack[0] + ); + assert!( + stack[0].has_literal(), + "ret stack[0] should have a literal, got {:?}", + stack[0] + ); + assert!( + stack[0].literals_list()?.len() == 1, + "ret stack[0] should have a single literal in the literal list" + ); + println!( + "{:#?}", + analyzer.graph.node_weight(stack[0].expect_single()?) + ); + let cvar_node = ContextVarNode::from(stack[0].expect_single()?); + assert!(cvar_node.is_const(&analyzer, arena)?); + let min = cvar_node.evaled_range_min(&analyzer, arena)?.unwrap(); + + let conc_value = min.maybe_concrete().unwrap().val; + assert!( + conc_value == expected, + "Values do not match: {:?} != {:?}", + conc_value, + expected + ); + Ok(()) + } + + #[test] + fn test_hex_literals_single() -> Result<()> { + let hex_literal = HexLiteral { + hex: "7B".to_string(), // 123 in decimal + loc: Loc::File(0, 0, 0), + }; + let mut bytes = [0u8; 32]; + bytes[0] = 0x7B; // Set the first byte to 0x7B as solidity does + let expected = Concrete::Bytes(1, H256::from_slice(&bytes)); + test_hex_literals(&[hex_literal], expected) + } + + #[test] + fn test_hex_literals_multiple() -> Result<()> { + let hex_literals = vec![ + HexLiteral { + hex: "7B".to_string(), // 123 in decimal + loc: Loc::File(0, 0, 0), + }, + HexLiteral { + hex: "FF".to_string(), // 255 in decimal + loc: Loc::File(0, 0, 0), + }, + ]; + + let mut bytes = [0u8; 32]; + bytes[0] = 0x7B; + bytes[1] = 0xFF; + let expected = Concrete::Bytes(2, H256::from_slice(&bytes)); + test_hex_literals(&hex_literals, expected) + } + + #[test] + fn test_hex_literals_empty() -> Result<()> { + let hex_literal = HexLiteral { + hex: "".to_string(), + loc: Loc::File(0, 0, 0), + }; + let expected = Concrete::Bytes(1, H256::default()); + test_hex_literals(&[hex_literal], expected) + } + + #[test] + fn test_hex_literals_large() -> Result<()> { + let hex_literal = HexLiteral { + hex: "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF".to_string(), // max H256 + loc: Loc::File(0, 0, 0), + }; + let expected = Concrete::Bytes(32, H256::from_slice(&[0xFF; 32])); + test_hex_literals(&[hex_literal], expected) + } + + fn test_address_literal(address: &str, expected: Concrete) -> Result<()> { + // setup + let mut analyzer = Analyzer { + debug_panic: true, + ..Default::default() + }; + let mut arena_base = RangeArena::default(); + let arena = &mut arena_base; + let ctx = make_context_node_for_analyzer(&mut analyzer); + let loc = Loc::File(0, 0, 0); + + // create an address literal + analyzer.address_literal(ctx, loc, address)?; + + // checks + let stack = &ctx.underlying(&analyzer)?.expr_ret_stack; + assert!( + stack.len() == 1, + "ret stack length should be 1, got {}", + stack.len() + ); + assert!( + stack[0].is_single(), + "ret stack[0] should be a single literal, got {:?}", + stack[0] + ); + assert!( + stack[0].has_literal(), + "ret stack[0] should have a literal, got {:?}", + stack[0] + ); + assert!( + stack[0].literals_list()?.len() == 1, + "ret stack[0] should have a single literal in the literal list" + ); + let cvar_node = ContextVarNode::from(stack[0].expect_single()?); + assert!(cvar_node.is_const(&analyzer, arena)?); + let min = cvar_node.evaled_range_min(&analyzer, arena)?.unwrap(); + let conc_value = min.maybe_concrete().unwrap().val; + assert!( + conc_value == expected, + "Values do not match: {:?} != {:?}", + conc_value, + expected + ); + Ok(()) + } + + #[test] + fn test_address_literal_valid() -> Result<()> { + let address = "0x0000000000000000000000000000000000000001"; + let expected = Concrete::Address(Address::from_str(address).unwrap()); + test_address_literal(address, expected) + } + + #[test] + fn test_address_literal_zero() -> Result<()> { + let address = "0x0000000000000000000000000000000000000000"; + let expected = Concrete::Address(Address::from_str(address).unwrap()); + test_address_literal(address, expected) + } + + #[test] + fn test_address_literal_max() -> Result<()> { + let address = "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"; + let expected = Concrete::Address(Address::from_str(address).unwrap()); + test_address_literal(address, expected) + } + + #[test] + fn test_address_literal_too_large() -> Result<()> { + let address = "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"; // 168 bits + let expected = Concrete::Address(Address::default()); // doesn't matter since we aren't using `expected` + assert!(test_address_literal(address, expected).is_err()); + Ok(()) + } + + fn test_string_literal(string_value: &str, expected: Concrete) -> Result<()> { + // setup + let mut analyzer = Analyzer { + debug_panic: true, + ..Default::default() + }; + let mut arena_base = RangeArena::default(); + let arena = &mut arena_base; + let ctx = make_context_node_for_analyzer(&mut analyzer); + let loc = Loc::File(0, 0, 0); + + // create a string literal + analyzer.string_literal(ctx, loc, string_value)?; + + // checks + let stack = &ctx.underlying(&analyzer)?.expr_ret_stack; + assert!( + stack.len() == 1, + "ret stack length should be 1, got {}", + stack.len() + ); + assert!( + stack[0].is_single(), + "ret stack[0] should be a single literal, got {:?}", + stack[0] + ); + assert!( + stack[0].has_literal(), + "ret stack[0] should have a literal, got {:?}", + stack[0] + ); + assert!( + stack[0].literals_list()?.len() == 1, + "ret stack[0] should have a single literal in the literal list" + ); + let cvar_node = ContextVarNode::from(stack[0].expect_single()?); + assert!(cvar_node.is_const(&analyzer, arena)?); + let min = cvar_node.evaled_range_min(&analyzer, arena)?.unwrap(); + println!("{min}"); + let conc_value = min.maybe_concrete().unwrap().val; + assert!( + conc_value == expected, + "Values do not match: {:?} != {:?}", + conc_value, + expected + ); + Ok(()) + } + + #[test] + fn test_string_literal_empty() -> Result<()> { + let string_value = ""; + let expected = Concrete::String(string_value.to_string()); + test_string_literal(string_value, expected) + } + + #[test] + fn test_string_literal_short() -> Result<()> { + let string_value = "hello"; + let expected = Concrete::String(string_value.to_string()); + test_string_literal(string_value, expected) + } + + #[test] + fn test_string_literal_long() -> Result<()> { + let string_value = "a".repeat(256); // 256 characters long + let expected = Concrete::String(string_value.clone()); + test_string_literal(&string_value, expected) + } + + #[test] + fn test_string_literal_special_chars() -> Result<()> { + let string_value = r#"!@#$%^&*()_+-=[]{}|;':,.<>/?"#; + let expected = Concrete::String(string_value.to_string()); + test_string_literal(string_value, expected) + } + + #[test] + fn test_string_literal_unicode() -> Result<()> { + let string_value = r#"🔥🔫"#; + // Chisel -> unicode"🔥🔫" returns: + // ├ Hex (Memory): + // ├─ Length ([0x00:0x20]): 0x0000000000000000000000000000000000000000000000000000000000000008 + // ├─ Contents ([0x20:..]): 0xf09f94a5f09f94ab000000000000000000000000000000000000000000000000 + + /* pyrometer analysis cuts off the contents + 21 │ │ string memory s = unicode"🔥🔫"; + │ │ ─────────────────┬─────────────────── + │ │ ╰───────────────────── Memory var "s" == {len: 8, indices: {0: 0xf0, 1: 0xf0}} + │ │ │ + │ │ ╰───────────────────── Memory var "s" ∈ [ {len: 0, indices: {0: 0xf0, 1: 0xf0}}, {len: 2**256 - 1, indices: {0: 0xf0, 1: 0xf0}} ] + │ │ │ + │ │ ╰───────────────────── Memory var "s" == {len: 8, indices: {0: 0xf0, 1: 0xf0}} + 22 │ │ return s; + │ │ ────┬─── + │ │ ╰───── returns: "s" == {len: 8, indices: {0: 0xf0, 1: 0xf0}} + */ + let expected = Concrete::String(string_value.to_string()); + test_string_literal(string_value, expected) + } + + fn test_bool_literal(bool_value: bool, expected: Concrete) -> Result<()> { + // setup + let mut analyzer = Analyzer { + debug_panic: true, + ..Default::default() + }; + let mut arena_base = RangeArena::default(); + let arena = &mut arena_base; + let ctx = make_context_node_for_analyzer(&mut analyzer); + let loc = Loc::File(0, 0, 0); + + // create a boolean literal + analyzer.bool_literal(ctx, loc, bool_value)?; + + // checks + let stack = &ctx.underlying(&analyzer)?.expr_ret_stack; + assert!( + stack.len() == 1, + "ret stack length should be 1, got {}", + stack.len() + ); + assert!( + stack[0].is_single(), + "ret stack[0] should be a single literal, got {:?}", + stack[0] + ); + assert!( + stack[0].has_literal(), + "ret stack[0] should have a literal, got {:?}", + stack[0] + ); + assert!( + stack[0].literals_list()?.len() == 1, + "ret stack[0] should have a single literal in the literal list" + ); + let cvar_node = ContextVarNode::from(stack[0].expect_single()?); + assert!(cvar_node.is_const(&analyzer, arena)?); + let min = cvar_node.evaled_range_min(&analyzer, arena)?.unwrap(); + let conc_value = min.maybe_concrete().unwrap().val; + assert!( + conc_value == expected, + "Values do not match: {:?} != {:?}", + conc_value, + expected + ); + Ok(()) + } + + #[test] + fn test_bool_literal_true() -> Result<()> { + let bool_value = true; + let expected = Concrete::Bool(true); + test_bool_literal(bool_value, expected) + } + + #[test] + fn test_bool_literal_false() -> Result<()> { + let bool_value = false; + let expected = Concrete::Bool(false); + test_bool_literal(bool_value, expected) + } +}