Skip to content

Commit

Permalink
Merge pull request #12 from FuzzingLabs/feat/rpc-provider
Browse files Browse the repository at this point in the history
Add RPC client support for Starknet contract analysis
  • Loading branch information
Rog3rSm1th authored May 28, 2024
2 parents e266316 + 691e947 commit 491bcb0
Show file tree
Hide file tree
Showing 11 changed files with 407 additions and 77 deletions.
34 changes: 22 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Sierra analyzer is a security toolkit for analyzing Sierra files.
#### Decompile a Sierra file

```
cargo run --bin sierra-decompiler <sierra file>
cargo run -- -f <sierra file>
```

<p align="center">
Expand All @@ -27,25 +27,34 @@ cargo run --bin sierra-decompiler <sierra file>
For a colourless output :

```
cargo run --bin sierra-decompiler <sierra file> --no-color
cargo run -- -f <sierra file> --no-color
```

It it also possible to get a verbose output with more informations :

```
cargo run --bin sierra-decompiler <sierra file> --verbose
cargo run -- -f <sierra file> --verbose
```

#### Analyze a remote contract

Contracts can be fetched directly from Starknet (Mainnet & Sepolia) by specifying the contract class to analyze :

```
# Fetch & decompile a contract from starknet mainnet
cargo run -- --remote 0x07c43d18d37d66d7855dab8f21ebf9d554dd213c6307aacecaf2d595a53b3bbb
# Fetch & decompile a contract from Sepolia network
cargo run -- --network sepolia --remote 0x068377a89d64c0b16dc97c66933777bf4e9b050652c4fde2c59c8c4d755a163b
```

#### Print the contract's Control-Flow Graph

```
cargo run ./examples/sierra/fib_array.sierra --cfg
cargo run -- -f ./examples/sierra/fib_array.sierra --cfg
# Output the Control-Flow Graph to a custom folder (default is ./output_cfg)
cargo run ./tests/sierra_files/fib_array.sierra --cfg --cfg-output ./test
# Get the CFG of a specific function
cargo run ./examples/sierra/fib_unary.sierra --cfg --function 'examples::fib_unary::fib'
cargo run -- -f ./examples/sierra/fib_array.sierra --cfg --cfg-output ./test
```

<p align="center">
Expand All @@ -55,13 +64,13 @@ cargo run ./examples/sierra/fib_unary.sierra --cfg --function 'examples::fib_una
#### Print the contract's Callgraph

```
cargo run ./examples/sierra/fib_array.sierra --callgraph
cargo run -- -f ./examples/sierra/fib_array.sierra --callgraph
# Output the Callgraph to a custom folder (default is ./output_callgraph)
cargo run ./tests/sierra_files/fib_array.sierra --callgraph --callgraph-output ./test
cargo run -- -f ./examples/sierra/fib_array.sierra --callgraph --callgraph-output ./test
# Get the Callgraph of a specific function
cargo run ./examples/sierra/fib_unary.sierra --callgraph --function 'examples::fib_unary::fib'
cargo run -- -f ./examples/sierra/fib_unary.sierra --callgraph --function 'examples::fib_unary::fib'
```

<p align="center">
Expand All @@ -71,7 +80,7 @@ cargo run ./examples/sierra/fib_unary.sierra --callgraph --function 'examples::f
#### Run the detectors

```
cargo run ./examples/sierra/fib_array.sierra -d
cargo run -- -f ./examples/sierra/fib_array.sierra -d
```

<p align="center">
Expand All @@ -90,4 +99,5 @@ Examples can be found [here](/lib/examples/).
- [x] Control-Flow Graph
- [x] Call Graph
- [X] Informational & Security detectors
- [x] Fetching contracts from Starknet
- [ ] Symbolic execution
4 changes: 4 additions & 0 deletions lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ hex = "0.4.3"
lazy_static = "1.4.0"
num-bigint = "0.4.4"
regex = "1.10.4"
reqwest = { version = "0.12.4", features = ["json"] }
serde = "1.0.198"
serde_json = "1.0.116"
tokio = { version="^1.37.0", features = ["full"] }

[dev-dependencies]
serde_json = "1.0.116"
Expand Down
40 changes: 40 additions & 0 deletions lib/examples/parse_starknet_contract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use sierra_analyzer_lib::provider::NetworkConfig;
use sierra_analyzer_lib::provider::RpcClient;
use sierra_analyzer_lib::sierra_program::SierraProgram;

use cairo_lang_starknet_classes::contract_class::ContractClass;

use tokio;

#[tokio::main]
async fn main() {
let client = RpcClient::new(NetworkConfig::MAINNET_API_URL);
let contract_class = "0x01c0bb51e2ce73dc007601a1e7725453627254016c28f118251a71bbb0507fcb";
match client.get_class(contract_class).await {
Ok(response) => {
// Convert RpcClient response to JSON content
let content = response.to_json();

// Deserialize JSON into a ContractClass
let program_string = serde_json::from_str::<ContractClass>(&content)
.ok()
.and_then(|prog| prog.extract_sierra_program().ok())
.map_or_else(|| content.clone(), |prog_sierra| prog_sierra.to_string());
let program = SierraProgram::new(program_string);

// Don't use the verbose output
let verbose_output = false;

// Decompile the Sierra program
let mut decompiler = program.decompiler(verbose_output);

// Print the decompiled program with use_color=true parameter
// You can disable colored output by passing use_color=false
let use_color = true;
println!("{}", decompiler.decompile(use_color));
}
Err(e) => {
eprintln!("Error calling RPC: {}", e);
}
}
}
7 changes: 6 additions & 1 deletion lib/src/decompiler/cfg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ impl<'a> ControlFlowGraph {

/// Generates the CFG basic blocks
pub fn generate_basic_blocks(&mut self) {
// Check if there are no statements and return early
if self.statements.is_empty() {
return;
}

// Retrieve basic blocks delimitations
let (basic_blocks_starts, basic_blocks_ends) = self.get_basic_blocks_delimitations();

Expand Down Expand Up @@ -107,7 +112,7 @@ impl<'a> ControlFlowGraph {
}

// Handle conditional branches
if let Some(conditional_branch) = statement.as_conditional_branch() {
if let Some(conditional_branch) = statement.as_conditional_branch(vec![]) {
if let Some(edge_2_offset) = conditional_branch.edge_2_offset {
// Conditional branch with 2 edges (JNZ)
current_basic_block.edges.push(Edge {
Expand Down
114 changes: 84 additions & 30 deletions lib/src/decompiler/decompiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use crate::decompiler::function::Function;
use crate::decompiler::function::SierraStatement;
use crate::decompiler::libfuncs_patterns::{IS_ZERO_REGEX, USER_DEFINED_FUNCTION_REGEX};
use crate::parse_element_name;
use crate::parse_element_name_with_fallback;
use crate::sierra_program::SierraProgram;

/// A struct that represents a decompiler for a Sierra program
Expand All @@ -28,6 +29,10 @@ pub struct Decompiler<'a> {
printed_blocks: Vec<BasicBlock>,
/// The function we are currently working on
current_function: Option<Function<'a>>,
/// Names of all declared types (in order)
declared_types_names: Vec<String>,
/// Names of all declared libfuncs (in order)
declared_libfuncs_names: Vec<String>,
/// Enable / disable the verbose output
/// Some statements are not included in the regular output to improve the readability
verbose: bool,
Expand All @@ -41,6 +46,8 @@ impl<'a> Decompiler<'a> {
indentation: 1,
printed_blocks: Vec::new(),
current_function: None,
declared_types_names: Vec::new(),
declared_libfuncs_names: Vec::new(),
verbose: verbose,
}
}
Expand Down Expand Up @@ -76,7 +83,7 @@ impl<'a> Decompiler<'a> {
}

/// Decompiles the type declarations
fn decompile_types(&self) -> String {
fn decompile_types(&mut self) -> String {
self.sierra_program
.program()
.type_declarations
Expand All @@ -87,7 +94,7 @@ impl<'a> Decompiler<'a> {
}

/// Decompiles the libfunc declarations
fn decompile_libfuncs(&self) -> String {
fn decompile_libfuncs(&mut self) -> String {
self.sierra_program
.program()
.libfunc_declarations
Expand Down Expand Up @@ -126,7 +133,7 @@ impl<'a> Decompiler<'a> {
}

/// Decompiles a single type declaration
fn decompile_type(&self, type_declaration: &TypeDeclaration) -> String {
fn decompile_type(&mut self, type_declaration: &TypeDeclaration) -> String {
// Get the debug name of the type's ID
let id = format!(
"{}",
Expand All @@ -135,10 +142,9 @@ impl<'a> Decompiler<'a> {
.debug_name
.as_ref()
.unwrap_or(&"".into())
)
.yellow();
);

// Get the long ID of the type, which consists of the generic ID and any generic arguments
// Get the long ID of the type
let long_id = &type_declaration.long_id;
let generic_id = long_id.generic_id.to_string();

Expand All @@ -152,8 +158,14 @@ impl<'a> Decompiler<'a> {
generic_id.clone()
};

// Retrieve the declared type information for the type, if it exists
// We don't use it in the decompiler output because it might not be readable enough
// Conditionally format id and long_id_repr
let (id_colored, long_id_repr_colored) = if id.is_empty() {
(id.yellow(), long_id_repr.yellow().to_string())
} else {
(id.white(), long_id_repr.clone())
};

// Retrieve declared type information
let _declared_type_info_str = type_declaration.declared_type_info.as_ref().map_or_else(
String::new,
|declared_type_info| {
Expand All @@ -168,18 +180,33 @@ impl<'a> Decompiler<'a> {
},
);

// Conditionally append long_id_repr in parentheses if it is different from id
let type_definition = if *long_id_repr != *id {
format!("type {} ({})", id, long_id_repr)
} else {
format!("type {}", id)
// Construct the type definition string
// If the id is not empty, format the type definition with the id and optionally the long ID representation
let type_definition = if !id.is_empty() {
let id_string = id.clone().to_string();
self.declared_types_names.push(id_string.clone());
format!(
"type {}{}",
id.yellow(),
if long_id_repr_colored != id_colored.to_string() {
format!(" ({})", long_id_repr_colored)
} else {
"".to_string()
}
)
}
// If the id is empty, format the type definition with only the long ID representation
else {
let long_id_repr_string = long_id_repr.clone().to_string();
self.declared_types_names.push(long_id_repr_string.clone());
format!("type {}{}", long_id_repr_colored, "")
};

type_definition
}

/// Decompile an single libfunc
fn decompile_libfunc(&self, libfunc_declaration: &LibfuncDeclaration) -> String {
/// Decompiles an individual libfunc declaration
fn decompile_libfunc(&mut self, libfunc_declaration: &LibfuncDeclaration) -> String {
// Get the debug name of the libfunc's ID
let id = format!(
"{}",
Expand All @@ -188,10 +215,25 @@ impl<'a> Decompiler<'a> {
.debug_name
.as_ref()
.unwrap_or(&"".into())
)
.blue();
);

// Get the long ID of the libfunc
let long_id = &libfunc_declaration.long_id;

// Parse kgeneric arguments
let _arguments = self.parse_arguments(&libfunc_declaration.long_id.generic_args);

// Construct the libfunc definition string
let libfunc_definition = if id.is_empty() {
long_id.to_string() // Use long_id if id is empty
} else {
id.to_string()
};

self.declared_libfuncs_names
.push(libfunc_definition.clone()); // Push non-colored version to declared_libfuncs_names

format!("libfunc {}", id)
format!("libfunc {}", libfunc_definition.blue())
}

/// Decompiles the functions prototypes
Expand All @@ -217,24 +259,22 @@ impl<'a> Decompiler<'a> {
&self,
function_declaration: &GenFunction<StatementIdx>,
) -> String {
// Get the debug name of the function's ID and format it in bold
let id = format!("{}", function_declaration.id.debug_name.as_ref().unwrap()).bold();
// Parse the function name
let id = format!("{}", parse_element_name!(function_declaration.id)).bold();

// Get the function signature, which consists of the parameter types and return types
let signature = &function_declaration.signature;
let param_types: Vec<String> = signature
.param_types
.iter()
.map(|param_type| {
param_type
.debug_name
.as_ref()
.unwrap_or(&format!("[{}]", param_type.id).into())
.to_string()
// We use `parse_element_name_with_fallback` and not `parse_element_name` because
// we try to match the type id with it's corresponding name if it's a remote contract
parse_element_name_with_fallback!(param_type, self.declared_types_names)
})
.collect();

// Create a list of strings representing the function parameters,
// Create a list of strings representing the function parameters
// with each string formatted as "<param_name>: <param_type>"
let param_strings: Vec<String> = param_types
.iter()
Expand Down Expand Up @@ -262,7 +302,8 @@ impl<'a> Decompiler<'a> {
let ret_type_string = if let Some(debug_name) = &ret_type.debug_name {
debug_name.to_string()
} else {
format!("[{}]", ret_type.id)
// Replace id with the corresponding type name
format!("[{}]", self.declared_types_names[ret_type.id as usize])
};
let ret_type_colored = ret_type_string.purple(); // Color ret_type_string in purple
ret_type_colored.to_string()
Expand Down Expand Up @@ -482,7 +523,11 @@ impl<'a> Decompiler<'a> {
// Append each statement to the string block
for statement in &block.statements {
// If condition
if let Some(conditional_branch) = statement.as_conditional_branch() {
if let Some(conditional_branch) =
// We pass it the declared libfunc names to allow the method to reconstruct function calls
// For remote contracts
statement.as_conditional_branch(self.declared_libfuncs_names.clone())
{
if block.edges.len() == 2 {
let function_name = &conditional_branch.function;
let function_arguments = conditional_branch.parameters.join(", ");
Expand All @@ -494,15 +539,24 @@ impl<'a> Decompiler<'a> {
}
}
// Unconditional jump
else if let Some(_unconditional_branch) = statement.as_conditional_branch() {
else if let Some(_unconditional_branch) =
// We pass it the declared libfunc names to allow the method to reconstruct function calls
// For remote contracts
statement.as_conditional_branch(self.declared_libfuncs_names.clone())
{
// Handle unconditional branch logic
todo!()
}
// Default case
else {
// Add the formatted statements to the block
// Some statements are only included in the verbose output
if let Some(formatted_statement) = statement.formatted_statement(self.verbose) {
//
// We pass it the declared libfunc names to allow the method to reconstruct function calls
// For remote contracts
if let Some(formatted_statement) = statement
.formatted_statement(self.verbose, self.declared_libfuncs_names.clone())
{
decompiled_basic_block += &format!("{}{}\n", indentation, formatted_statement);
}
}
Expand Down
Loading

0 comments on commit 491bcb0

Please sign in to comment.