diff --git a/README.md b/README.md index 36f1ced..60962e8 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,11 @@ Sierra-Analyzer is a security toolkit for analyzing Sierra files. 3) [Analyze a remote contract](#analyze-a-remote-contract) 4) [Print the contract's Control-Flow Graph](#print-the-contracts-control-flow-graph) 5) [Print the contract's Callgraph](#print-the-contracts-callgraph) -6) [Run the detectors](#print-the-contracts-callgraph) -7) [Use it as a library](#print-the-contracts-callgraph) +6) [Run the detectors](#run-the-detectors) +7) [Use the symbolic execution to generate unit tests](#use-the-symbolic-execution-to-generate-unit-tests) 8) [Improve the decompiler output using LLMs](#print-the-contracts-callgraph) +9) [Use it as a library](#print-the-contracts-callgraph) + #### Project structure @@ -96,16 +98,46 @@ cargo run -- -f ./examples/sierra/fib_array.sierra -d

-#### Use it as a library +#### Use the symbolic execution to generate unit tests -It is also possible to use the `sierra-analyzer-lib` library to decompile serialised or unserialised Sierra files. +##### 1) Using the Tests generator detector + +Symbolic execution can be used to generate unit tests for the functions that take `felt252` arguments as input. + +For example the file [symbolic_execution_test.sierra](https://github.com/FuzzingLabs/sierra-analyzer/blob/master/examples/sierra/symbolic_execution_test.sierra) contains a main function that takes four `felt252` arguments *v0*, *v1*, *v2* and *v3*. The function includes four conditions that check if `v0 == 102`, `v1 == 117`, `v2 == 122` and `v3 == 122` which correspond to the ASCII values for the letters *f*, *u*, *z*, and *z*, respectively. + +When running the detectors we can generate test cases for each path in the function with the **Tests generator detector**: + + +``` +cargo run -- -f ./examples/sierra/fib_array.sierra -d + +[...] + +[Informational] Tests generator + - symbolic::symbolic::symbolic_execution_test : + - v0: 102, v1: 0, v2: 0, v3: 0 + - v0: 103, v1: 0, v2: 0, v3: 0 + - v0: 102, v1: 117, v2: 0, v3: 0 + - v0: 0, v1: 118, v2: 0, v3: 0 + - v0: 102, v1: 117, v2: 122, v3: 0 + - v0: 0, v1: 0, v2: 123, v3: 0 + - v0: 102, v1: 117, v2: 122, v3: 122 + - v0: 0, v1: 0, v2: 0, v3: 123 +``` + +##### 2) Using the library -Examples can be found [here](/lib/examples/). +The tests generator can also be used [with the library](https://github.com/FuzzingLabs/sierra-analyzer/blob/master/lib/examples/tests_generator.rs). #### Improve the decompiler output using LLMs [Here](/doc/llm-decompilation.md) is a tutorial on how to improve the decompiler output using LLMs. +#### Use it as a library + +It is also possible to use the `sierra-analyzer-lib` library to decompile serialised or unserialised Sierra files. + #### Features - [x] Decompiler diff --git a/lib/src/detectors/mod.rs b/lib/src/detectors/mod.rs index e47a2ae..982160b 100644 --- a/lib/src/detectors/mod.rs +++ b/lib/src/detectors/mod.rs @@ -2,11 +2,13 @@ pub mod detector; pub mod functions_detector; pub mod statistics_detector; pub mod strings_detector; +pub mod tests_generator_detector; use crate::detectors::detector::Detector; use crate::detectors::functions_detector::FunctionsDetector; use crate::detectors::statistics_detector::StatisticsDetector; use crate::detectors::strings_detector::StringsDetector; +use crate::detectors::tests_generator_detector::TestsGeneratorDetector; /// Macro to create a vector of detectors macro_rules! create_detectors { @@ -21,5 +23,10 @@ macro_rules! create_detectors { /// Returns a vector of all the instantiated detectors pub fn get_detectors() -> Vec> { - create_detectors!(FunctionsDetector, StringsDetector, StatisticsDetector) + create_detectors!( + FunctionsDetector, + StringsDetector, + StatisticsDetector, + TestsGeneratorDetector + ) } diff --git a/lib/src/detectors/tests_generator_detector.rs b/lib/src/detectors/tests_generator_detector.rs new file mode 100644 index 0000000..017d0a0 --- /dev/null +++ b/lib/src/detectors/tests_generator_detector.rs @@ -0,0 +1,76 @@ +use crate::decompiler::decompiler::Decompiler; +use crate::detectors::detector::{Detector, DetectorType}; +use crate::sym_exec::sym_exec::generate_test_cases_for_function; + +#[derive(Debug)] +pub struct TestsGeneratorDetector; + +impl TestsGeneratorDetector { + /// Creates a new `TestsGeneratorDetector` instance + pub fn new() -> Self { + Self + } +} + +impl Detector for TestsGeneratorDetector { + /// Returns the id of the detector + #[inline] + fn id(&self) -> &'static str { + "tests" + } + + /// Returns the name of the detector + #[inline] + fn name(&self) -> &'static str { + "Tests generator" + } + + /// Returns the description of the detector + #[inline] + fn description(&self) -> &'static str { + "Returns the tests cases for the functions." + } + + /// Returns the type of the detector + #[inline] + fn detector_type(&self) -> DetectorType { + DetectorType::INFORMATIONAL + } + + /// Returns the generated unit tests for the function if they exist + fn detect(&mut self, decompiler: &mut Decompiler) -> String { + let mut result = String::new(); + + for function in &mut decompiler.functions { + // Determine the function name + let function_name = if let Some(prototype) = &function.prototype { + // Remove the "func " prefix and then split at the first parenthese + let stripped_prototype = &prototype[5..]; + if let Some(first_space_index) = stripped_prototype.find('(') { + Some(stripped_prototype[..first_space_index].trim().to_string()) + } else { + None + } + } else { + None + }; + + // If a function name was found, proceed with the mutable borrow + if let Some(function_name) = function_name { + + // Add the test cases to the result + let test_cases = generate_test_cases_for_function( + function, + decompiler.declared_libfuncs_names.clone(), + ); + + if !test_cases.is_empty() { + result += &format!("{} : \n", function_name); + result += &format!("{}\n", test_cases); + } + } + } + + result + } +} diff --git a/lib/src/sym_exec/sym_exec.rs b/lib/src/sym_exec/sym_exec.rs index d44ff62..57137fb 100644 --- a/lib/src/sym_exec/sym_exec.rs +++ b/lib/src/sym_exec/sym_exec.rs @@ -45,15 +45,18 @@ pub fn generate_test_cases_for_function( let cfg = Config::new(); let context = Context::new(&cfg); - // Create a solver - let solver = Solver::new(&context); - // Create Z3 variables for each felt252 argument let z3_variables: Vec = felt252_arguments .iter() .map(|(arg_name, _)| Int::new_const(&context, &**arg_name)) .collect(); + // Create a solver + let mut symbolic_execution = SymbolicExecution::new(&context); + + let mut zero_constraints = Vec::new(); + let mut other_constraints = Vec::new(); + // Convert Sierra statements to z3 constraints for basic_block in path { for statement in &basic_block.statements { @@ -63,16 +66,30 @@ pub fn generate_test_cases_for_function( &context, declared_libfuncs_names.clone(), ) { - solver.assert(&constraint); + symbolic_execution.add_constraint(&constraint); + + // Identify if it's a zero check and store the variable for non-zero testing + if let GenStatement::Invocation(invocation) = &statement.statement { + let libfunc_id_str = parse_element_name_with_fallback!( + invocation.libfunc_id, + declared_libfuncs_names + ); + + if IS_ZERO_REGEX.is_match(&libfunc_id_str) { + let operand_name = format!("v{}", invocation.args[0].id.to_string()); + let operand = Int::new_const(&context, operand_name.clone()); + zero_constraints.push((operand, constraint)); + } else { + // Store other constraints for reuse + other_constraints.push(constraint); + } + } } } - } - // Check if the constraints are satisfiable - match solver.check() { - SatResult::Sat => { - // If solvable, add the argument names and values to the result - if let Some(model) = solver.get_model() { + // Check if the constraints are satisfiable (value == 0) + if symbolic_execution.check() == SatResult::Sat { + if let Some(model) = symbolic_execution.solver.get_model() { let values: Vec = felt252_arguments .iter() .zip(z3_variables.iter()) @@ -84,16 +101,44 @@ pub fn generate_test_cases_for_function( ) }) .collect(); - let values_str = format!("{:?}\n", values); + let values_str = format!("{}", values.join(", ")); if unique_results.insert(values_str.clone()) { - result.push_str(&values_str); + result.push_str(&format!("{}\n", values_str)); } } } - SatResult::Unsat | SatResult::Unknown => { - let non_solvable_str = "non solvable\n".to_string(); - if unique_results.insert(non_solvable_str.clone()) { - result.push_str(&non_solvable_str); + + // Now generate test cases where the value is not equal to 0 + for (operand, _) in &zero_constraints { + // Create a fresh solver for the non-zero case + let non_zero_solver = Solver::new(&context); + + // Re-apply all other constraints except the zero-equality one + for constraint in &other_constraints { + non_zero_solver.assert(constraint); + } + + // Add a constraint to force the operand to be not equal to 0 + non_zero_solver.assert(&operand._eq(&Int::from_i64(&context, 0)).not()); + + if non_zero_solver.check() == SatResult::Sat { + if let Some(model) = non_zero_solver.get_model() { + let values: Vec = felt252_arguments + .iter() + .zip(z3_variables.iter()) + .map(|((arg_name, _), var)| { + format!( + "{}: {}", + arg_name, + model.eval(var, true).unwrap().to_string() + ) + }) + .collect(); + let values_str = format!("{}", values.join(", ")); + if unique_results.insert(values_str.clone()) { + result.push_str(&format!("{}\n", values_str)); + } + } } } }