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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ dist/
target/
.aider*
.gradle

# Added by cargo-zigbuild
.intentionally-empty-file.o
6 changes: 4 additions & 2 deletions Cargo.lock

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

12 changes: 10 additions & 2 deletions 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", "/benchmark", "/.github", "/go"]

[dependencies]
alloy-primitives = "0.8"
Expand All @@ -19,11 +19,19 @@ pyo3 = { version = "0.23", 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 }

# TODO: When we build for a windows target on an ubuntu runner, crunchy tries to
# get the wrong path, update this when the workflow has been updated
#
# See: https://github.com/eira-fransham/crunchy/issues/13
crunchy = "=0.2.2"

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

# for dev
trace_selectors = []
Expand All @@ -33,7 +41,7 @@ trace_storage = []
trace = ["trace_selectors", "trace_arguments", "trace_mutability", "trace_storage"]

[lib]
crate-type = ["cdylib", "lib"]
crate-type = ["cdylib", "lib", "staticlib"]

[profile.release]
lto = true
38 changes: 38 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
.PHONY: staticbuild
staticbuild: staticbuild-darwin staticbuild-windows staticbuild-linux

.PHONY: staticbuild-darwin
staticbuild-darwin:
rustup target add x86_64-apple-darwin
cargo zigbuild --release --target x86_64-apple-darwin --features c_api

rustup target add aarch64-apple-darwin
cargo zigbuild --release --target aarch64-apple-darwin --features c_api

.PHONY: staticbuild-windows
staticbuild-windows:
rustup target add x86_64-pc-windows-gnu
cargo zigbuild --release --target x86_64-pc-windows-gnu --features c_api

.PHONY: staticbuild-linux
staticbuild-linux:
rustup target add x86_64-unknown-linux-musl
cargo zigbuild --release --target x86_64-unknown-linux-musl --features c_api

rustup target add aarch64-unknown-linux-musl
cargo zigbuild --release --target aarch64-unknown-linux-musl --features c_api

.PHONY: gobuild
gobuild: staticbuild
mkdir -p go/staticlibs/linux-amd64
mkdir -p go/staticlibs/linux-arm64
cp target/x86_64-unknown-linux-musl/release/libevmole.a go/staticlibs/linux-amd64/libevmole.a
cp target/aarch64-unknown-linux-musl/release/libevmole.a go/staticlibs/linux-arm64/libevmole.a

mkdir -p go/staticlibs/darwin-amd64
mkdir -p go/staticlibs/darwin-arm64
cp target/x86_64-apple-darwin/release/libevmole.a go/staticlibs/darwin-amd64/libevmole.a
cp target/aarch64-apple-darwin/release/libevmole.a go/staticlibs/darwin-arm64/libevmole.a

mkdir -p go/staticlibs/windows-amd64
cp target/x86_64-pc-windows-gnu/release/libevmole.a go/staticlibs/windows-amd64/libevmole.a
47 changes: 47 additions & 0 deletions evmole.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#ifndef EVMOLE_H
#define EVMOLE_H
#ifdef __cplusplus
extern "C" {
#endif

#include <stdint.h>

/**
* Configuration options for contract analysis.
*/
typedef struct {
int selectors; /* Include function selectors */
int arguments; /* Include function arguments */
int state_mutability; /* Include state mutability */
int storage; /* Include storage layout */
int disassemble; /* Include disassembled bytecode */
int basic_blocks; /* Include basic block analysis */
int control_flow_graph; /* Include control flow graph */
} EvmoleContractInfoOptions;

/**
* Free memory allocated by this library.
*
* @param ptr Pointer to memory allocated by evmole
*/
void evmole_free(char* ptr);

/**
* Analyzes contract bytecode and returns contract information in JSON format.
*
* @param code Runtime bytecode as a hex string
* @param options Configuration options for the analysis
* @param error_msg Pointer to store error message in case of failure
* @return JSON string containing analysis results or NULL on error
* (memory must be freed with evmole_free)
*/
char* evmole_contract_info(
const char* code,
EvmoleContractInfoOptions options,
char** error_msg
);

#ifdef __cplusplus
} /* extern \"C\" */
#endif
#endif /* EVMOLE_H */
89 changes: 89 additions & 0 deletions go/evmole.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package evmole

/*
#cgo linux,amd64 LDFLAGS: ${SRCDIR}/staticlibs/linux-amd64/libevmole.a -ldl
#cgo linux,arm64 LDFLAGS: ${SRCDIR}/staticlibs/linux-arm64/libevmole.a -ldl
#cgo darwin,amd64 LDFLAGS: ${SRCDIR}/staticlibs/darwin-amd64/libevmole.a -ldl
#cgo darwin,arm64 LDFLAGS: ${SRCDIR}/staticlibs/darwin-arm64/libevmole.a -ldl
#cgo windows,amd64 LDFLAGS: ${SRCDIR}/staticlibs/windows-amd64/libevmole.a -ldl
#include <stdlib.h>
#include "../evmole.h"
*/
import "C"

import (
"encoding/hex"
"encoding/json"
"fmt"
"unsafe"
)

// ContractInfoOptions represents the configuration options for contract analysis
type ContractInfoOptions struct {
Selectors bool // Include function selectors
Arguments bool // Include function arguments
StateMutability bool // Include state mutability
Storage bool // Include storage layout
Disassemble bool // Include disassembled bytecode
BasicBlocks bool // Include basic block analysis
ControlFlowGraph bool // Include control flow graph
}

// ContractInfo analyzes the provided contract bytecode with custom options
func ContractInfo(bytecode []byte, options ContractInfoOptions) (*Contract, error) {
cBytecode := C.CString(hex.EncodeToString(bytecode))
defer C.free(unsafe.Pointer(cBytecode))

var cOptions C.EvmoleContractInfoOptions

// Convert Go bools to C ints (0 or 1)
cOptions.selectors = boolToInt(options.Selectors)
cOptions.arguments = boolToInt(options.Arguments)
cOptions.state_mutability = boolToInt(options.StateMutability)
cOptions.storage = boolToInt(options.Storage)
cOptions.disassemble = boolToInt(options.Disassemble)
cOptions.basic_blocks = boolToInt(options.BasicBlocks)
cOptions.control_flow_graph = boolToInt(options.ControlFlowGraph)

var cError *C.char
result := C.evmole_contract_info(cBytecode, cOptions, &cError)

if result == nil {
errMsg := C.GoString(cError)
C.evmole_free(cError)
return nil, fmt.Errorf("evmole analysis failed: %s", errMsg)
}

jsonResult := C.GoString(result)
C.evmole_free(result)

// Parse JSON into Contract struct
var contract Contract
err := json.Unmarshal([]byte(jsonResult), &contract)
if err != nil {
return nil, fmt.Errorf("failed to parse contract info: %w", err)
}

return &contract, nil
}

// Analyze is a convenience function that analyzes bytecode with all options enabled
func Analyze(bytecode []byte) (*Contract, error) {
return ContractInfo(bytecode, ContractInfoOptions{
Selectors: true,
Arguments: true,
StateMutability: true,
Storage: true,
Disassemble: true,
BasicBlocks: true,
ControlFlowGraph: true,
})
}

// Helper function to convert a bool to a C int (0 or 1)
func boolToInt(b bool) C.int {
if b {
return 1
}
return 0
}
3 changes: 3 additions & 0 deletions go/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/cdump/evmole/go

go 1.23.6
Binary file added go/staticlibs/darwin-amd64/libevmole.a
Binary file not shown.
Binary file added go/staticlibs/darwin-arm64/libevmole.a
Binary file not shown.
Binary file added go/staticlibs/linux-amd64/libevmole.a
Binary file not shown.
Binary file added go/staticlibs/linux-arm64/libevmole.a
Binary file not shown.
Binary file added go/staticlibs/windows-amd64/libevmole.a
Binary file not shown.
70 changes: 70 additions & 0 deletions go/test/evmole_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package evmole_test

import (
_ "embed"
"encoding/hex"
"slices"
"testing"

evmole "github.com/cdump/evmole/go"
)

// This is the bytecode of a Uniswapv3 pool
// @see https://etherscan.io/address/0x5777d92f208679DB4b9778590Fa3CAB3aC9e2168#code
//
//go:embed uniswapv3.bytecode
var uniswapv3Bytecode string

// TestContractInfo tests the evmole.Analyze function on the Uniswapv3 pool contract
// It ensures all the read methods are present in the result
func TestContractInfo(t *testing.T) {
bytecode, err := hex.DecodeString(uniswapv3Bytecode)
if err != nil {
t.Fatalf("Error: %v", err)
}

result, err := evmole.Analyze(bytecode)
if err != nil {
t.Fatalf("Error: %v", err)
}

expectedMethods := []string{
"c45a0155", // factory
"ddca3f43", // fee
"f3058399", // feeGrowthGlobal0X128
"46141319", // feeGrowthGlobal1X128
"1a686502", // liquidity
"70cf754a", // maxLiquidityPerTick
"252c09d7", // observations
"883bdbfd", // observe
"514ea4bf", // positions
"1ad8b03b", // protocolFees
"3850c7bd", // slot0
"a38807f2", // snapshotCumulativesInside
"5339c296", // tickBitmap
"d0c93a7c", // tickSpacing
"f30dba93", // ticks
"0dfe1681", // token0
"d21220a7", // token1
}
for _, method := range expectedMethods {
found := false
for _, f := range result.Functions {
if f.Selector == method {
found = true
break
}
}
if !found {
t.Errorf("Expected method %s not found", method)
}
}

for _, method := range expectedMethods {
for _, s := range result.Storage {
if slices.Contains(s.Reads, method) {
t.Logf("Storage %s reads %s (offset: %d, type: %s)", s.Slot, method, s.Offset, s.Type)
}
}
}
}
1 change: 1 addition & 0 deletions go/test/uniswapv3.bytecode

Large diffs are not rendered by default.

Loading