Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions Cargo.lock

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

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ authors = ["Maxim Andreev <andreevmaxim@gmail.com>"]
license = "MIT"
readme = "README.md"
repository = "https://github.com/cdump/evmole"
exclude = ["/javascript", "/python", "/benchmark", "/.github"]
exclude = ["/javascript", "/python", "/go", "/benchmark", "/.github"]

[dependencies]
alloy-primitives = { version = "1", default-features = false, features = [
Expand All @@ -21,11 +21,13 @@ pyo3 = { version = "0.27", features = ["extension-module"], optional = true }
wasm-bindgen = { version = "0.2", optional = true }
serde-wasm-bindgen = { version = "0.6", optional = true }
serde = { version = "1.0", features = ["derive"], optional = true }
serde_json = { version = "1.0", optional = true }

[features]
serde = ["dep:serde"]
python = ["dep:pyo3"]
javascript = ["dep:wasm-bindgen", "dep:serde-wasm-bindgen", "serde"]
wasm = ["serde", "dep:serde_json"]

# for dev
trace_selectors = []
Expand Down
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.PHONY: wasm
wasm:
cargo build --target wasm32-unknown-unknown --features wasm --release
cp target/wasm32-unknown-unknown/release/evmole.wasm go/evmole.wasm
Binary file added evmole.wasm
Binary file not shown.
210 changes: 210 additions & 0 deletions go/evmole.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
package evmole

import (
"context"
_ "embed"
"encoding/hex"
"encoding/json"
"fmt"

"github.com/tetratelabs/wazero"
)

//go:embed evmole.wasm
var evmoleWASM []byte

// Selector is a 4-byte function selector
type Selector [4]byte

// String returns the selector as a hex string
func (s Selector) String() string {
return hex.EncodeToString(s[:])
}

// Bytes returns the selector as a byte slice
func (s Selector) Bytes() []byte {
return s[:]
}

// UnmarshalJSON unmarshals a JSON string into a Selector
func (s *Selector) UnmarshalText(data []byte) error {
_, err := hex.Decode(s[:], data)
return err
}

// MarshalText marshals the selector as a hex string
func (s Selector) MarshalText() ([]byte, error) {
return []byte(s.String()), nil
}

// ContractInfoOptions configures what information to extract from contract bytecode
type ContractInfoOptions struct {
// Selectors enables extraction of function selectors
Selectors bool
// Arguments enables extraction of function arguments
Arguments bool
// StateMutability enables extraction of function state mutability
StateMutability bool
// Storage enables extraction of contract storage layout
Storage bool
}

// Function represents a public smart contract function
type Function struct {
// Selector is the 4-byte function selector as hex string (e.g., "aabbccdd")
Selector Selector `json:"selector"`
// BytecodeOffset is the starting byte offset within the EVM bytecode for the function body
BytecodeOffset int `json:"bytecodeOffset"`
// Arguments are the function argument types in canonical format (e.g., "uint256,address[]")
// nil if arguments were not extracted
Arguments *string `json:"arguments,omitempty"`
// StateMutability is the function's state mutability ("pure", "view", "payable", or "nonpayable")
// nil if state mutability was not extracted
StateMutability *string `json:"stateMutability,omitempty"`
}

// StorageRecord represents a storage variable record in a smart contract's storage layout
type StorageRecord struct {
// Slot is the storage slot number as hex string (e.g., "0", "1b")
Slot string `json:"slot"`
// Offset is the byte offset within the storage slot (0-31)
Offset uint8 `json:"offset"`
// Type is the variable type (e.g., "uint256", "mapping(address => uint256)", "bytes32")
Type string `json:"type"`
// Reads is a list of function selectors that read from this storage location
Reads []Selector `json:"reads"`
// Writes is a list of function selectors that write to this storage location
Writes []Selector `json:"writes"`
}

// Contract contains analyzed information about a smart contract
type Contract struct {
// Functions is the list of detected contract functions
// nil if functions were not extracted
Functions []Function `json:"functions,omitempty"`
// Storage is the list of contract storage records
// nil if storage layout was not extracted
Storage []StorageRecord `json:"storage,omitempty"`
}

// ContractInfo extracts information about a smart contract from its EVM bytecode.
//
// Parameters:
// - ctx: Context for cancellation and timeout
// - code: Runtime bytecode as raw bytes
// - options: Optional configuration specifying what data to extract. If nil, extracts selectors only.
//
// Returns:
// - Contract object containing the requested smart contract information
// - Error if extraction fails
//
// Example:
//
// code, _ := hex.DecodeString("6080604052...")
// contract, err := ContractInfo(ctx, code, &ContractInfoOptions{
// Selectors: true,
// StateMutability: true,
// })
func ContractInfo(ctx context.Context, code []byte, options *ContractInfoOptions) (*Contract, error) {
if options == nil {
options = &ContractInfoOptions{Selectors: true}
}

// Create a new WebAssembly runtime
runtime := wazero.NewRuntime(ctx)
defer func() { _ = runtime.Close(ctx) }()

instance, err := runtime.Instantiate(ctx, evmoleWASM)
if err != nil {
return nil, fmt.Errorf("failed to instantiate WASM module: %w", err)
}
defer func() { _ = instance.Close(ctx) }()

memory := instance.Memory()
contractInfoFunc := instance.ExportedFunction("contract_info")
if contractInfoFunc == nil {
return nil, fmt.Errorf("could not find exported function: contract_info")
}

// Prepare options byte (bitflags)
var optionsByte uint8
if options.Selectors {
optionsByte |= 1 << 0
}
if options.Arguments {
optionsByte |= 1 << 1
}
if options.StateMutability {
optionsByte |= 1 << 2
}
if options.Storage {
optionsByte |= 1 << 3
}

// Memory layout:
// [0..len(code)]: input code
// [len(code)]: options byte
// [len(code)+1..len(code)+5]: result length (u32)
// [len(code)+5..]: result JSON
codeOffset := uint32(0)
optionsOffset := uint32(len(code))
resultLenOffset := optionsOffset + 1
resultOffset := resultLenOffset + 4
resultCapacity := uint32(1024 * 1024) // 1MB buffer for result

// Write input to memory
if !memory.Write(codeOffset, code) {
return nil, fmt.Errorf("failed to write code to memory")
}
if !memory.Write(optionsOffset, []byte{optionsByte}) {
return nil, fmt.Errorf("failed to write options to memory")
}

// Call the WASM function
results, err := contractInfoFunc.Call(ctx,
uint64(codeOffset), uint64(len(code)),
uint64(optionsOffset),
uint64(resultLenOffset), uint64(resultOffset), uint64(resultCapacity))
if err != nil {
return nil, fmt.Errorf("failed to call contract_info: %w", err)
}

if status := uint32(results[0]); status != 0 {
return nil, fmt.Errorf("contract_info error: status=%d", status)
}

// Read the result length
rawResultLen, ok := memory.Read(resultLenOffset, 4)
if !ok {
return nil, fmt.Errorf("failed to read result length")
}
resultLen := uint32(rawResultLen[0]) | uint32(rawResultLen[1])<<8 |
uint32(rawResultLen[2])<<16 | uint32(rawResultLen[3])<<24

// Read the result JSON
resultJSON, ok := memory.Read(resultOffset, resultLen)
if !ok {
return nil, fmt.Errorf("failed to read result from memory")
}

// Parse JSON result
var contract Contract
if err := json.Unmarshal(resultJSON, &contract); err != nil {
return nil, fmt.Errorf("failed to parse result JSON: %w", err)
}

return &contract, nil
}

func FunctionSelectors(ctx context.Context, code []byte) ([]Selector, error) {
contract, err := ContractInfo(ctx, code, &ContractInfoOptions{Selectors: true})
if err != nil {
return nil, err
}

selectors := make([]Selector, len(contract.Functions))
for i, fn := range contract.Functions {
selectors[i] = fn.Selector
}
return selectors, nil
}
Binary file added go/evmole.wasm
Binary file not shown.
163 changes: 163 additions & 0 deletions go/evmole_test.go

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions go/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/cdump/evmole/go

go 1.22.5

require github.com/tetratelabs/wazero v1.7.3
2 changes: 2 additions & 0 deletions go/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github.com/tetratelabs/wazero v1.7.3 h1:PBH5KVahrt3S2AHgEjKu4u+LlDbbk+nsGE3KLucy6Rw=
github.com/tetratelabs/wazero v1.7.3/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
87 changes: 87 additions & 0 deletions src/interface_wasm.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
//! WASM interface for EVMole contract analysis

use crate::ContractInfoArgs;

/// Extracts contract information from EVM bytecode and returns JSON-encoded result.
///
/// # Options Flags (bitfield at `options_ptr`)
///
/// * Bit 0 (0x01): Function selectors
/// * Bit 1 (0x02): Function arguments
/// * Bit 2 (0x04): State mutability
/// * Bit 3 (0x08): Storage layout
/// * Bit 4 (0x10): Disassemble bytecode
/// * Bit 5 (0x20): Basic blocks
///
/// # Returns
///
/// * `0` - Success
/// * `1` - Buffer too small (check `result_len_ptr` for required size)
/// * `2` - Serialization error
#[unsafe(no_mangle)]
pub extern "C" fn contract_info(
code_ptr: *const u8,
code_len: usize,
options_ptr: *const u8,
result_len_ptr: *mut u32,
result_ptr: *mut u8,
result_capacity: usize,
) -> u32 {
let code = unsafe { std::slice::from_raw_parts(code_ptr, code_len) };
let options = unsafe { *options_ptr };

// Parse option flags (bitfield)
let selectors = (options & (1 << 0)) != 0; // Bit 0: selectors
let arguments = (options & (1 << 1)) != 0; // Bit 1: arguments
let state_mutability = (options & (1 << 2)) != 0; // Bit 2: state mutability
let storage = (options & (1 << 3)) != 0; // Bit 3: storage layout
let disassemble = (options & (1 << 4)) != 0; // Bit 4: disassembly
let basic_blocks = (options & (1 << 5)) != 0; // Bit 5: basic blocks

// Build contract info args
let mut args = ContractInfoArgs::new(code);
if selectors {
args = args.with_selectors();
}
if arguments {
args = args.with_arguments();
}
if state_mutability {
args = args.with_state_mutability();
}
if storage {
args = args.with_storage();
}
if disassemble {
args = args.with_disassemble();
}
if basic_blocks {
args = args.with_basic_blocks();
}

// Get contract info
let contract = crate::contract_info(args);

// Serialize to JSON
let json = match serde_json::to_vec(&contract) {
Ok(j) => j,
Err(_) => return 2, // Serialization error
};

let result_len = json.len();

unsafe {
*result_len_ptr = result_len as u32;
}

if result_len > result_capacity {
// Buffer too small - caller should allocate larger buffer and retry
return 1;
}

unsafe {
std::ptr::copy_nonoverlapping(json.as_ptr(), result_ptr, result_len);
}

0 // Success
}
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,6 @@ mod interface_py;

#[cfg(feature = "javascript")]
mod interface_js;

#[cfg(feature = "wasm")]
mod interface_wasm;