Skip to content

Commit

Permalink
feat(decompile): proper decompilation of constants
Browse files Browse the repository at this point in the history
  • Loading branch information
Jon-Becker committed Jun 24, 2024
1 parent b355c16 commit ab8aabc
Show file tree
Hide file tree
Showing 11 changed files with 65 additions and 22 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions crates/common/src/ether/http_or_ws_or_ipc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,7 @@ impl JsonRpcClient for HttpOrWsOrIpc {
async fn request<T, R>(&self, method: &str, params: T) -> Result<R, Self::Error>
where
T: Debug + Serialize + Send + Sync,
R: DeserializeOwned + Send,
{
R: DeserializeOwned + Send, {
let res = match self {
Self::Ws(ws) => JsonRpcClient::request(ws, method, params).await?,
Self::Ipc(ipc) => JsonRpcClient::request(ipc, method, params).await?,
Expand Down
2 changes: 1 addition & 1 deletion crates/common/src/ether/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use ethers::{
},
};
use heimdall_cache::{read_cache, store_cache};
use std::{io::Write, str::FromStr, time::Duration};
use std::{str::FromStr, time::Duration};
use tracing::{debug, error, trace};

/// Get the Provider object for RPC URL
Expand Down
2 changes: 1 addition & 1 deletion crates/common/src/ether/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ pub fn to_type(string: &str) -> ParamType {
string = string.replacen(&format!("[{}]", &size), "", 1);
}

let arg_type = match string.as_str() {
let arg_type = match string.as_str().replace("memory", "").trim() {
"address" => ParamType::Address,
"bool" => ParamType::Bool,
"string" => ParamType::String,
Expand Down
4 changes: 2 additions & 2 deletions crates/core/tests/test_decompile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,9 @@ mod integration_tests {
.expect("failed to decompile");

// assert that the output is correct
for line in &["function Unresolved_2fa61cd8(address arg0) public payable returns (uint16) {",
for line in &["function Unresolved_2fa61cd8(address arg0) public view returns (uint16) {",
"function Unresolved_41161b10(uint240 arg0, address arg1) public payable returns (bool) {",
"function Unresolved_06fdde03() public payable returns (bytes memory) {"] {
"constant Unresolved_06fdde03"] {
println!("{line}");
assert!(result.source.as_ref().expect("decompile source is empty").contains(line));
}
Expand Down
1 change: 1 addition & 0 deletions crates/decompile/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ exclude.workspace = true
heimdall-config = { workspace = true }
heimdall-common = { workspace = true }
heimdall-cache = { workspace = true }
heimdall-decoder = { workspace = true }
thiserror = "1.0.50"
clap = { workspace = true, features = ["derive"] }
derive_builder = "0.12.0"
Expand Down
32 changes: 29 additions & 3 deletions crates/decompile/src/core/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@ pub(crate) mod postprocess;
pub(crate) mod resolve;

use alloy_json_abi::JsonAbi;
use ethers::types::H160;
use ethers::{
abi::{decode as abi_decode, ParamType, Token},
types::H160,
};
use eyre::eyre;
use heimdall_common::{
ether::{
bytecode::get_bytecode_from_target,
compiler::detect_compiler,
signatures::{score_signature, ResolvedError, ResolvedFunction, ResolvedLog},
types::to_type,
},
utils::strings::{encode_hex, encode_hex_reduced, StringExt},
utils::strings::{decode_hex, encode_hex, encode_hex_reduced, StringExt},
};
use heimdall_disassembler::{disassemble, DisassemblerArgsBuilder};
use heimdall_vm::{
Expand Down Expand Up @@ -159,7 +163,29 @@ pub async fn decompile(args: DecompilerArgs) -> Result<DecompileResult, Error> {
);

// analyze the symbolic execution trace
let analyzed_function = analyzer.analyze(trace_root)?;
let mut analyzed_function = analyzer.analyze(trace_root)?;

// if the function is constant, we can get the exact val
if analyzed_function.is_constant() {
evm.reset();
let x = evm.call(&decode_hex(&selector).expect("invalid selector"), 0)?;

let returns_param_type = analyzed_function
.returns
.as_ref()
.map(|ret_type| to_type(ret_type.replace("memory", "").trim()))
.unwrap_or(ParamType::Bytes);

let decoded = abi_decode(&[returns_param_type], &x.returndata)
.map(|decoded| match decoded[0].clone() {
Token::String(s) => format!("\"{}\"", s),
Token::Uint(x) | Token::Int(x) => x.to_string(),
token => format!("0x{}", token),
})
.unwrap_or_else(|_| encode_hex(&x.returndata));

analyzed_function.constant_value = Some(decoded);
}

Ok::<_, Error>(analyzed_function)
})
Expand Down
13 changes: 7 additions & 6 deletions crates/decompile/src/core/out/source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@ pub fn build_source(
functions
.iter()
.filter(|f| {
!f.fallback
&& (analyzer_type == AnalyzerType::Yul
|| (f.maybe_getter_for.is_none() && !(f.pure && f.arguments.is_empty())))
!f.fallback &&
(analyzer_type == AnalyzerType::Yul ||
(f.maybe_getter_for.is_none() && !f.is_constant()))
})
.for_each(|f| {
let mut function_source = Vec::new();
Expand Down Expand Up @@ -229,9 +229,9 @@ fn get_constants(functions: &[AnalyzedFunction]) -> Vec<String> {
let mut output: Vec<String> = functions
.iter()
.filter_map(|f| {
if f.pure && f.arguments.is_empty() {
if f.is_constant() {
Some(format!(
"{} public constant {} = TMP;",
"{} public constant {} = {};",
f.returns
.as_ref()
.unwrap_or(&"bytes".to_string())
Expand All @@ -240,7 +240,8 @@ fn get_constants(functions: &[AnalyzedFunction]) -> Vec<String> {
f.resolved_function
.as_ref()
.map(|x| x.name.clone())
.unwrap_or("UNR".to_string())
.unwrap_or(format!("Unresolved_{}", f.selector)),
f.constant_value.as_ref().unwrap_or(&"0x".to_string())
))
} else {
None
Expand Down
9 changes: 9 additions & 0 deletions crates/decompile/src/interfaces/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ pub struct AnalyzedFunction {

/// the underlying storage variable, if this is a public getter
pub maybe_getter_for: Option<String>,

/// optional constant value for this function
pub constant_value: Option<String>,
}

#[derive(Clone, Debug)]
Expand Down Expand Up @@ -100,9 +103,15 @@ impl AnalyzedFunction {
analyzer_type: AnalyzerType::Abi,
fallback,
maybe_getter_for: None,
constant_value: None,
}
}

/// Whether this is a constant or not
pub fn is_constant(&self) -> bool {
self.pure && self.arguments.is_empty()
}

/// Gets the inputs for a range of memory
pub fn get_memory_range(&self, _offset: U256, _size: U256) -> Vec<StorageFrame> {
let mut memory_slice: Vec<StorageFrame> = Vec::new();
Expand Down
12 changes: 9 additions & 3 deletions crates/decompile/src/utils/heuristics/arguments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,16 @@ pub fn argument_heuristic(
{
function.returns = Some(String::from("address"));
}
// if the size of returndata is > 32, it must be a bytes memory return.
// it could be a struct, but we cant really determine that from the bytecode
// if the size of returndata is > 32, it must be a bytes or string return.
else if size > 32 {
function.returns = Some(String::from("bytes memory"));
// some hardcoded function selectors where the return type is known to be a string
if ["06fdde03", "95d89b41", "6a98de4c", "9d2b0822", "1a0d4bca"]
.contains(&function.selector.as_str())
{
function.returns = Some(String::from("string memory"));
} else {
function.returns = Some(String::from("bytes memory"));
}
} else {
// attempt to find a return type within the return memory operations
let byte_size = match AND_BITMASK_REGEX
Expand Down
8 changes: 4 additions & 4 deletions crates/decompile/src/utils/heuristics/solidity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,10 @@ pub fn solidity_heuristic(

// perform a series of checks to determine if the condition
// is added by the compiler and can be ignored
if (conditional.contains("msg.data.length") && conditional.contains("0x04"))
|| VARIABLE_SIZE_CHECK_REGEX.is_match(&conditional).unwrap_or(false)
|| (conditional.replace('!', "") == "success")
|| (conditional == "!msg.value")
if (conditional.contains("msg.data.length") && conditional.contains("0x04")) ||
VARIABLE_SIZE_CHECK_REGEX.is_match(&conditional).unwrap_or(false) ||
(conditional.replace('!', "") == "success") ||
(conditional == "!msg.value")
{
return Ok(());
}
Expand Down

0 comments on commit ab8aabc

Please sign in to comment.