├── .github └── workflows │ └── build.yml ├── .gitignore ├── .gitmodules ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── benchmark ├── Makefile ├── README.md ├── compare.py ├── providers │ ├── etherscan │ │ ├── Dockerfile │ │ └── main.py │ ├── ethersolve │ │ ├── Dockerfile │ │ ├── build.gradle │ │ └── src │ │ │ └── main │ │ │ └── java │ │ │ └── HelloEtherSolve.java │ ├── evm-cfg-builder │ │ ├── Dockerfile │ │ └── main.py │ ├── evm-cfg │ │ ├── Dockerfile │ │ └── main.py │ ├── evm-hound-rs │ │ ├── .dockerignore │ │ ├── Cargo.lock │ │ ├── Cargo.toml │ │ ├── Dockerfile │ │ └── src │ │ │ └── main.rs │ ├── evmlisa │ │ ├── Dockerfile │ │ ├── build.gradle │ │ └── src │ │ │ └── main │ │ │ └── java │ │ │ └── Main.java │ ├── evmole-js │ │ ├── Dockerfile │ │ └── main.mjs │ ├── evmole-py │ │ ├── Dockerfile │ │ └── main.py │ ├── evmole-rs │ │ ├── .dockerignore │ │ ├── Cargo.lock │ │ ├── Cargo.toml │ │ ├── Dockerfile │ │ └── src │ │ │ └── main.rs │ ├── heimdall-rs │ │ ├── .dockerignore │ │ ├── Cargo.lock │ │ ├── Cargo.toml │ │ ├── Dockerfile │ │ └── src │ │ │ └── main.rs │ ├── sevm │ │ ├── Dockerfile │ │ └── main.mjs │ ├── simple │ │ ├── Dockerfile │ │ └── main.py │ ├── smlxl │ │ ├── Cargo.lock │ │ ├── Cargo.toml │ │ ├── Dockerfile │ │ └── src │ │ │ └── main.rs │ └── whatsabi │ │ ├── Dockerfile │ │ └── main.mjs └── results │ └── .gitkeep ├── dev.sh ├── evmole.pyi ├── javascript ├── .npmignore ├── Makefile ├── README.md ├── examples │ ├── contract.sol │ ├── esbuild │ │ ├── index.html │ │ ├── main.js │ │ ├── package-lock.json │ │ └── package.json │ ├── node │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── with_import.mjs │ │ └── with_require.cjs │ ├── parcel │ │ ├── package-lock.json │ │ ├── package.json │ │ └── src │ │ │ ├── app.js │ │ │ └── index.html │ ├── parcel_packageExports │ │ ├── package-lock.json │ │ ├── package.json │ │ └── src │ │ │ ├── app.js │ │ │ └── index.html │ ├── vite │ │ ├── index.html │ │ ├── main.js │ │ ├── package-lock.json │ │ ├── package.json │ │ └── vite.config.js │ └── webpack │ │ ├── index.js │ │ ├── package-lock.json │ │ ├── package.json │ │ └── webpack.config.js ├── package-lock.json ├── package.json ├── src │ ├── evmole_esm.js │ ├── evmole_node.cjs │ ├── evmole_node.mjs │ └── evmole_wasm_import.js └── tests │ ├── main.test.js │ ├── package-lock.json │ ├── package.json │ ├── playwright.config.ts │ └── webserver.py ├── pyproject.toml ├── python ├── README.md └── test_python.py └── src ├── arguments ├── calldata.rs └── mod.rs ├── collections.rs ├── contract_info.rs ├── control_flow_graph ├── initial.rs ├── mod.rs ├── reachable.rs ├── resolver.rs └── state.rs ├── evm ├── calldata.rs ├── code_iterator.rs ├── element.rs ├── memory.rs ├── mod.rs ├── op.rs ├── stack.rs └── vm.rs ├── interface_js.rs ├── interface_py.rs ├── lib.rs ├── selectors ├── calldata.rs └── mod.rs ├── serialize.rs ├── state_mutability ├── calldata.rs └── mod.rs ├── storage ├── calldata.rs ├── keccak_precalc.rs └── mod.rs └── utils.rs /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | env: 8 | MATURIN_VERSION: 1.8.6 9 | 10 | jobs: 11 | rust-test: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Test and Clippy 16 | run: | 17 | cargo test 18 | cargo clippy --all-features -- -D warnings 19 | 20 | javascript: 21 | runs-on: ubuntu-latest 22 | needs: rust-test 23 | steps: 24 | - uses: actions/checkout@v4 25 | - name: Install Node 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: 20 29 | registry-url: 'https://registry.npmjs.org/' 30 | - name: Install Rust 31 | uses: dtolnay/rust-toolchain@stable 32 | - name: Build 33 | id: build 34 | working-directory: javascript 35 | run: | 36 | cp ../README.md ./ 37 | npm ci 38 | npm run build 39 | npx tsc ./dist/evmole.d.ts 40 | npm pack 41 | - name: Install test dependencies 42 | working-directory: javascript/tests 43 | run: npm ci && npx playwright install --with-deps 44 | - name: Run tests 45 | working-directory: javascript 46 | run: make test 47 | - name: Upload artifacts 48 | uses: actions/upload-artifact@v4 49 | with: 50 | name: javascript 51 | path: javascript/evmole-*.tgz 52 | 53 | python-wheel: 54 | needs: rust-test 55 | strategy: 56 | matrix: 57 | python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] 58 | platform: 59 | - runner: ubuntu-latest 60 | os: linux 61 | target: x86_64 62 | - runner: windows-latest 63 | os: windows 64 | target: x64 65 | - runner: macos-latest 66 | os: macos 67 | target: universal2-apple-darwin 68 | runs-on: ${{ matrix.platform.runner }} 69 | steps: 70 | - uses: actions/checkout@v4 71 | - name: Install python 72 | uses: actions/setup-python@v5 73 | with: 74 | python-version: ${{ matrix.python-version }} 75 | - name: Build 76 | uses: PyO3/maturin-action@v1 77 | with: 78 | maturin-version: ${{ env.MATURIN_VERSION }} 79 | target: ${{ matrix.platform.target }} 80 | manylinux: auto 81 | args: -i ${{ matrix.python-version }} --release --out dist 82 | - name: Install wheel 83 | run: pip3 install ${{ matrix.platform.os == 'windows' && '(get-item .\dist\*.whl)' || 'dist/*.whl' }} 84 | - name: Test 85 | run: python3 python/test_python.py 86 | - name: Upload artifacts 87 | uses: actions/upload-artifact@v4 88 | with: 89 | name: python-${{ matrix.platform.os}}-${{ matrix.python-version }} 90 | path: dist/*.whl 91 | compression-level: 0 92 | 93 | python-sdist: 94 | runs-on: ubuntu-latest 95 | needs: rust-test 96 | steps: 97 | - uses: actions/checkout@v4 98 | - name: Install python 99 | uses: actions/setup-python@v5 100 | with: 101 | python-version: 3.x 102 | - name: Build sdist 103 | uses: PyO3/maturin-action@v1 104 | with: 105 | maturin-version: ${{ env.MATURIN_VERSION }} 106 | command: sdist 107 | args: --out dist 108 | - name: Upload artifacts 109 | uses: actions/upload-artifact@v4 110 | with: 111 | name: python-sdist 112 | path: dist 113 | compression-level: 0 114 | 115 | python-sdist-test: 116 | # ubuntu-latest image (https://github.com/actions/runner-images/blob/main/images/ubuntu/Ubuntu2404-Readme.md) have Rust installed 117 | runs-on: ubuntu-latest 118 | needs: python-sdist 119 | steps: 120 | - uses: actions/checkout@v4 121 | - name: Install python 122 | uses: actions/setup-python@v5 123 | with: 124 | python-version: 3.13 125 | allow-prereleases: true 126 | - uses: actions/download-artifact@v4 127 | with: 128 | name: python-sdist 129 | - name: Build and install wheel 130 | run: pip3 install *.tar.gz 131 | - name: Test 132 | run: python3 python/test_python.py 133 | 134 | release: 135 | runs-on: ubuntu-latest 136 | if: startsWith(github.ref, 'refs/tags/') # only on tagged releases 137 | needs: [javascript, python-wheel, python-sdist, python-sdist-test] 138 | permissions: 139 | contents: write 140 | id-token: write 141 | steps: 142 | - uses: actions/checkout@v4 143 | with: 144 | fetch-depth: 0 # need tags to generate release notes 145 | 146 | - name: Publish rust 147 | env: 148 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_TOKEN }} 149 | run: cargo publish --allow-dirty 150 | 151 | - name: Install Node 152 | uses: actions/setup-node@v4 153 | with: 154 | node-version: 20 155 | registry-url: 'https://registry.npmjs.org/' 156 | 157 | - name: Release Notes 158 | run: | 159 | echo '## Changes since previous release:' > changelog.md 160 | git log --oneline $(git describe --tags --abbrev=0 HEAD^)..HEAD --pretty=format:"- [%h](https://github.com/cdump/evmole/commit/%H) %s" >> changelog.md 161 | cat changelog.md 162 | 163 | - name: Download Python Artifacts 164 | uses: actions/download-artifact@v4 165 | with: 166 | pattern: python-* 167 | path: dist-py 168 | merge-multiple: true 169 | 170 | - name: Download Javascript Artifacts 171 | uses: actions/download-artifact@v4 172 | with: 173 | name: javascript 174 | path: ./javascript/ 175 | 176 | - name: Github Release 177 | uses: softprops/action-gh-release@v2 178 | with: 179 | name: Release ${{ github.ref_name }} 180 | draft: false 181 | prerelease: false 182 | body_path: changelog.md 183 | files: | 184 | javascript/evmole-*.tgz 185 | dist-py/* 186 | 187 | - name: Publish javascript 188 | env: 189 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 190 | working-directory: javascript 191 | run: npm publish --provenance *.tgz 192 | 193 | - name: Publish python 194 | uses: pypa/gh-action-pypi-publish@unstable/v1 # unstable tmp for issue #309 195 | with: 196 | attestations: true 197 | packages-dir: dist-py/ 198 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | benchmark/results/* 2 | !benchmark/results/.gitkeep 3 | 4 | __pycache__/ 5 | node_modules/ 6 | .parcel-cache/ 7 | test-results/ 8 | *.tgz 9 | 10 | dist/ 11 | 12 | target/ 13 | .aider* 14 | .gradle 15 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "benchmark/datasets"] 2 | path = benchmark/datasets 3 | url = https://github.com/cdump/evmole-datasets.git 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "evmole" 3 | version = "0.8.0" 4 | edition = "2024" 5 | description = "Extracts function selectors and arguments from EVM bytecode" 6 | authors = ["Maxim Andreev "] 7 | license = "MIT" 8 | readme = "README.md" 9 | repository = "https://github.com/cdump/evmole" 10 | exclude = ["/javascript", "/python", "/benchmark", "/.github"] 11 | 12 | [dependencies] 13 | alloy-primitives = { version = "1", default-features = false, features = [ 14 | "std", 15 | "map-foldhash", 16 | ] } 17 | alloy-dyn-abi = { version = "1", default-features = false } 18 | indexmap = "2.9" 19 | 20 | pyo3 = { version = "0.25", features = ["extension-module"], optional = true } 21 | wasm-bindgen = { version = "0.2", optional = true } 22 | serde-wasm-bindgen = { version = "0.6", optional = true } 23 | serde = { version = "1.0", features = ["derive"], optional = true } 24 | 25 | [features] 26 | serde = ["dep:serde"] 27 | python = ["dep:pyo3"] 28 | javascript = ["dep:wasm-bindgen", "dep:serde-wasm-bindgen", "serde"] 29 | 30 | # for dev 31 | trace_selectors = [] 32 | trace_arguments = [] 33 | trace_mutability = [] 34 | trace_storage = [] 35 | trace = [ 36 | "trace_selectors", 37 | "trace_arguments", 38 | "trace_mutability", 39 | "trace_storage", 40 | ] 41 | 42 | [lib] 43 | crate-type = ["cdylib", "lib"] 44 | 45 | [profile.release] 46 | lto = true 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Maxim Andreev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /benchmark/Makefile: -------------------------------------------------------------------------------- 1 | PROVIDER_BASE = etherscan 2 | PROVIDERS_SELECTORS ?= simple whatsabi evm-hound-rs sevm evmole-rs evmole-js evmole-py 3 | PROVIDERS_ARGUMENTS ?= simple evmole-rs evmole-js evmole-py 4 | PROVIDERS_MUTABILITY ?= simple whatsabi sevm evmole-rs evmole-js evmole-py 5 | PROVIDERS_STORAGE ?= evmole-rs smlxl 6 | PROVIDERS_BLOCKS ?= evmole-rs 7 | PROVIDERS_FLOW ?= evmole-rs evm-cfg ethersolve sevm evm-cfg-builder heimdall-rs 8 | 9 | DATASETS ?= largest1k random50k vyper 10 | DATASETS_STORAGE ?= storage3k 11 | DOCKER ?= docker 12 | DOCKER_CPUS ?= 1 13 | DOCKER_PREFIX ?= evmole-bench 14 | 15 | PROVIDERS_SELECTORS := $(PROVIDER_BASE) $(PROVIDERS_SELECTORS) 16 | PROVIDERS_ARGUMENTS := $(PROVIDER_BASE) $(PROVIDERS_ARGUMENTS) 17 | PROVIDERS_MUTABILITY := $(PROVIDER_BASE) $(PROVIDERS_MUTABILITY) 18 | PROVIDERS_STORAGE := $(PROVIDER_BASE) $(PROVIDERS_STORAGE) 19 | PROVIDERS_UNIQ := $(sort $(PROVIDERS_SELECTORS) $(PROVIDERS_ARGUMENTS) $(PROVIDERS_MUTABILITY) $(PROVIDERS_STORAGE) $(PROVIDERS_BLOCKS) $(PROVIDERS_FLOW)) 20 | 21 | DATASET := $(shell pwd)/datasets 22 | RES := $(shell pwd)/results 23 | 24 | BUILD_TARGETS := $(addsuffix .build, $(PROVIDERS_UNIQ)) 25 | UNPACK_TARGETS := $(foreach d,$(DATASETS) $(DATASETS_STORAGE),$(addprefix datasets/, $(d))) 26 | RUN_SELECTORS_TARGETS := $(foreach p,$(PROVIDERS_SELECTORS),$(addprefix $(p).selectors/, $(DATASETS))) 27 | RUN_ARGUMENTS_TARGETS := $(foreach p,$(PROVIDERS_ARGUMENTS),$(addprefix $(p).arguments/, $(DATASETS))) 28 | RUN_MUTABILITY_TARGETS := $(foreach p,$(PROVIDERS_MUTABILITY),$(addprefix $(p).mutability/, $(DATASETS))) 29 | RUN_STORAGE_TARGETS := $(foreach p,$(PROVIDERS_STORAGE),$(addprefix $(p).storage/, $(DATASETS_STORAGE))) 30 | RUN_BLOCKS_TARGETS := $(foreach p,$(PROVIDERS_BLOCKS),$(addprefix $(p).blocks/, $(DATASETS))) 31 | RUN_FLOW_TARGETS := $(foreach p,$(PROVIDERS_FLOW),$(addprefix $(p).flow/, $(DATASETS))) 32 | 33 | RUN_TARGETS := $(RUN_SELECTORS_TARGETS) $(RUN_ARGUMENTS_TARGETS) $(RUN_MUTABILITY_TARGETS) $(RUN_STORAGE_TARGETS) $(RUN_BLOCKS_TARGETS) $(RUN_FLOW_TARGETS) 34 | 35 | benchmark-selectors: $(addsuffix .build, $(PROVIDERS_SELECTORS)) run-selectors 36 | benchmark-arguments: $(addsuffix .build, $(PROVIDERS_ARGUMENTS)) run-arguments 37 | benchmark-mutability: $(addsuffix .build, $(PROVIDERS_MUTABILITY)) run-mutability 38 | benchmark-storage: $(addsuffix .build, $(PROVIDERS_STORAGE)) run-storage 39 | benchmark-flow: $(addsuffix .build, $(PROVIDERS_FLOW)) run-blocks run-flow 40 | 41 | build: $(BUILD_TARGETS) 42 | run-selectors: $(RUN_SELECTORS_TARGETS) 43 | run-arguments: $(RUN_ARGUMENTS_TARGETS) 44 | run-mutability: $(RUN_MUTABILITY_TARGETS) 45 | run-storage: $(RUN_STORAGE_TARGETS) 46 | run-blocks: $(RUN_BLOCKS_TARGETS) 47 | run-flow: $(RUN_FLOW_TARGETS) 48 | 49 | $(BUILD_TARGETS): 50 | $(info [*] Building $(basename $@)...) 51 | # for evmole - use latest version, from current git branch 52 | if [ "$@" = "evmole-py.build" ] || [ "$@" = "evmole-js.build" ] || [ "$@" = "evmole-rs.build" ]; then \ 53 | rsync -av ../ ./providers/$(basename $@)/rust --exclude benchmark --exclude target --exclude dist --exclude javascript/examples --exclude javascript/tests --exclude javascript/node_modules --exclude .git; \ 54 | fi; 55 | DOCKER_BUILDKIT=1 $(DOCKER) build -t $(DOCKER_PREFIX)-$(basename $@) providers/$(basename $@) 56 | if [ "$@" = "evmole-py.build" ] || [ "$@" = "evmole-js.build" ] || [ "$@" = "evmole-rs.build" ]; then \ 57 | rm -rf ./providers/$(basename $@)/rust; \ 58 | fi; 59 | 60 | $(UNPACK_TARGETS): 61 | $(info [*] Unpacking $@...) 62 | tar -C datasets/ -zxf $@.tar.gz 63 | 64 | .SECONDEXPANSION: 65 | $(RUN_TARGETS): datasets/$$(notdir $$@) 66 | $(info [*] Running $@...) 67 | $(DOCKER) run --network=none --cpus=$(DOCKER_CPUS) --rm \ 68 | -v $(DATASET)/$(notdir $@):/dataset \ 69 | -v $(RES):/mnt \ 70 | -it $(DOCKER_PREFIX)-$(basename $(subst /,,$(dir $@))) \ 71 | $(subst .,,$(suffix $(subst /,,$(dir $@)))) \ 72 | /dataset \ 73 | /mnt/$(subst /,_,$@).json \ 74 | /mnt/$(PROVIDER_BASE).selectors_$(notdir $@).json 75 | 76 | .PHONY: benchmark-selectors benchmark-arguments benchmark-mutability build run-selectors run-arguments run-mutability $(BUILD_TARGETS) $(RUN_TARGETS) 77 | -------------------------------------------------------------------------------- /benchmark/README.md: -------------------------------------------------------------------------------- 1 | # Benchmarks 2 | 3 | Test accuracy and speed of different EVM bytecode analysis tools 4 | 5 | For results, refer to the [main README.md](../README.md#Benchmark). 6 | 7 | ## Methodology 8 | 1. Get N Etherscan-verified contracts, save the bytecode and ABI to `datasets/NAME/ADDR.json`. 9 | 2. Extract information from the bytecode using different tools. Each tool runs inside a Docker container and is limited to 1 CPU (see `providers/NAME` and `Makefile`). 10 | 3. Assume Etherscan's ABI as ground truth. 11 | 4. Compare the results: 12 | - For selectors: Count [False Positives and False Negatives](https://en.wikipedia.org/wiki/False_positives_and_false_negatives) 13 | - For arguments/mutability: Count exact matches 14 | 15 | ## Reproduce 16 | Set the performance mode using `sudo cpupower frequency-set -g performance` and run benchmarks ([GNU Make](https://www.gnu.org/software/make/)) inside the `benchmark/` directory: 17 | 18 | ```sh 19 | make benchmark-selectors # Run function selector tests 20 | make benchmark-arguments # Run argument extraction tests 21 | make benchmark-mutability # Run state mutability tests 22 | ``` 23 | 24 | To use [Podman](https://podman.io/) instead of Docker: 25 | ```sh 26 | DOCKER=podman make benchmark-selectors 27 | ``` 28 | 29 | You can run specific steps; for example: 30 | ```sh 31 | # Only build docker-images 32 | $ make build 33 | 34 | # Only run tests for selectors (assume docker-images are built) 35 | $ make run-selectors 36 | 37 | # Build specific provider 38 | $ make etherscan.build 39 | 40 | # Run specific provider/mode/dataset 41 | $ make etherscan.selectors/largest1k 42 | $ make etherscan.arguments/largest1k 43 | ``` 44 | 45 | ## Process Results 46 | Use `compare.py` to analyze results: 47 | 48 | ```sh 49 | # Default mode (selectors) 50 | $ python3 compare.py 51 | 52 | # Compare specific mode 53 | $ python3 compare.py --mode=arguments 54 | $ python3 compare.py --mode=mutability 55 | $ python3 compare.py --mode=flow 56 | 57 | # Filter by dataset/provider and show errors 58 | python3 compare.py --mode=arguments --datasets largest1k --providers etherscan evmole-py --show-errors 59 | 60 | # Normalize argument comparisons 61 | python3 compare.py --mode=arguments --normalize-args fixed-size-array tuples string-bytes 62 | 63 | # Output markdown tables 64 | python3 compare.py --mode=selectors --markdown 65 | ``` 66 | 67 | ## Control Flow Graph Analysis 68 | The CFG analysis methodology consists of the following steps: 69 | 70 | 1. Constructing Basic Blocks 71 | - A basic block is a contiguous subsequence of EVM opcodes with: 72 | - One entry point (first instruction) 73 | - Ends at: JUMP, JUMPI, STOP, REVERT, RETURN, INVALID, unknown opcode, or end of code 74 | - JUMPDEST cannot appear inside a block - it marks the start of a new block 75 | 76 | 2. Filtering Out Definitely Unreachable Blocks 77 | A block is definitely unreachable if: 78 | - It does not begin at pc = 0 (contract start), AND 79 | - First instruction is not JUMPDEST, AND 80 | - Previous block does not end with JUMPI whose "false" branch falls through 81 | 82 | 3. Set Definitions 83 | - SET_BB: Set of all basic blocks after initial partitioning and removal of invalid blocks 84 | - SET_CFG: Set of blocks reachable from pc = 0 per CFG algorithm 85 | 86 | 4. Error Metrics 87 | - False Positives = (SET_CFG - SET_BB) 88 | - Blocks CFG claims reachable but not valid basic blocks 89 | - Should be empty in correct analysis 90 | - False Negatives = (SET_BB - SET_CFG) 91 | - Valid blocks not marked reachable by CFG 92 | - May include legitimate dead code 93 | - Fewer indicates more precise analysis 94 | 95 | ## Datasets 96 | See [datasets/README.md](datasets/README.md) for information about how the test datasets were constructed. 97 | -------------------------------------------------------------------------------- /benchmark/providers/etherscan/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | FROM python:3.12-slim 4 | WORKDIR /app 5 | RUN pip3 install pycryptodome==3.19 6 | COPY main.py /app 7 | ENTRYPOINT ["python3", "/app/main.py"] 8 | -------------------------------------------------------------------------------- /benchmark/providers/etherscan/main.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import re 4 | import sys 5 | import time 6 | 7 | from Crypto.Hash import keccak 8 | 9 | 10 | def sign(inp: bytes) -> str: 11 | return keccak.new(digest_bits=256, data=inp).digest()[:4].hex() 12 | 13 | def join_inputs(inputs) -> str: 14 | if len(inputs) == 0: 15 | return '' 16 | n = '' 17 | for v in inputs: 18 | if v['type'].startswith('tuple'): 19 | n += '(' + join_inputs(v['components']) + ')' + v['type'][5:] 20 | else: 21 | n += v['type'] 22 | n += ',' 23 | return n[:-1] 24 | 25 | def process_storage_mapping(types, k, v) -> str: 26 | kt = types[k] 27 | vt = types[v] 28 | if isinstance(vt, str): 29 | return f'mapping({kt} => {vt})' 30 | 31 | if isinstance(vt, dict): 32 | assert len(vt) == 1 33 | val = process_storage_mapping(types, *list(vt.items())[0]) 34 | return f'mapping({kt} => {val})' 35 | 36 | if isinstance(vt, tuple): 37 | if len(vt) == 1: 38 | # struct with only 1 field: 39 | return process_storage_mapping(types, k, vt[0]['type']) 40 | else: 41 | return f'mapping({kt} => struct_{len(vt)}_fields)' 42 | 43 | if isinstance(vt, list): 44 | val = process_storage_dynarray(types, types[vt[0]]) 45 | return f'mapping({kt} => {val})' 46 | 47 | raise Exception(f'Unsupported map type {kt} / {vt}') 48 | 49 | def process_storage_dynarray(types, base) -> str: 50 | if isinstance(base, str): 51 | return f'{base}[]' 52 | if isinstance(base, tuple): 53 | if len(base) == 1: 54 | return process_storage_dynarray(types, base[0]) + '[]' 55 | else: 56 | return f'struct_{len(base)}_fields[]' 57 | 58 | if isinstance(base, list): 59 | return process_storage_dynarray(types, types[base[0]]) + '[]' 60 | 61 | raise Exception(f'Unsupported dynamic array base type {base}') 62 | 63 | def process_storage_value(types, base_slot: int, offset, value) -> dict[str, str]: 64 | key = f'{base_slot:064x}_{offset}' 65 | if isinstance(value, str): 66 | return {key: value} 67 | elif isinstance(value, tuple): 68 | assert offset == 0 69 | ret: dict[str, str] = {} 70 | for y in value: 71 | r = process_storage_value(types, base_slot + int(y['slot']), y['offset'], types[ y['type'] ]) 72 | ret.update(r) 73 | return ret 74 | elif isinstance(value, dict): 75 | assert len(value) == 1 76 | k, v = list(value.items())[0] 77 | v = process_storage_mapping(types, k, v) 78 | return {key: v} 79 | elif isinstance(value, list): 80 | base = types[ value[0] ] 81 | v = process_storage_dynarray(types, base) 82 | return {key: v} 83 | else: 84 | raise Exception(f'Unsupported value type {value}') 85 | 86 | def process_storage(sl): 87 | """ 88 | Experimental code, not 100% accurate benchmark 89 | """ 90 | types = {} 91 | for (tname, tinfo) in (sl['types'] or {}).items(): 92 | tvalue = None 93 | match tinfo['encoding']: 94 | case 'inplace': 95 | if 'members' in tinfo: 96 | assert tinfo['label'].startswith('struct') 97 | tvalue = tuple(tinfo['members']) 98 | else: 99 | tvalue = tinfo['label'] 100 | case 'mapping': 101 | tvalue = {tinfo['key']: tinfo['value']} 102 | case 'bytes': 103 | tvalue = tinfo['label'] 104 | case 'dynamic_array': 105 | tvalue = [ tinfo['base'] ] 106 | case _: 107 | raise Exception(f'Unsupported type {tinfo}') 108 | 109 | if isinstance(tvalue, str): 110 | tvalue = tvalue.replace('address payable', 'address') 111 | tvalue = re.sub(r'contract \w+', 'address', tvalue) 112 | types[tname] = tvalue 113 | 114 | ret = {} 115 | for x in sl['storage']: 116 | r = process_storage_value(types, int(x['slot']), x['offset'], types[ x['type'] ]) 117 | ret.update(r) 118 | 119 | return ret 120 | 121 | def process(data, mode): 122 | if mode == 'storage': 123 | return process_storage(data['storageLayout']) 124 | ret = {} 125 | for x in data['abi']: 126 | if x['type'] != 'function': 127 | continue 128 | args = join_inputs(x['inputs']) 129 | n = f'{x["name"]}({args})' 130 | sg = sign(n.encode('ascii')) 131 | if mode == 'arguments' or mode == 'selectors': 132 | ret[sg] = args 133 | elif mode == 'mutability': 134 | ret[sg] = x.get('stateMutability', '') 135 | else: 136 | raise Exception(f'Unknown mode {mode}') 137 | 138 | if mode == 'selectors': 139 | return list(ret.keys()) 140 | else: 141 | return ret 142 | 143 | if len(sys.argv) < 4: 144 | print('Usage: python3 main.py MODE INPUT_DIR OUTPUT_FILE') 145 | sys.exit(1) 146 | 147 | 148 | ret = {} 149 | mode = sys.argv[1] 150 | indir = sys.argv[2] 151 | outfile = sys.argv[3] 152 | 153 | for fname in os.listdir(indir): 154 | with open(f'{indir}/{fname}', 'r') as fh: 155 | d = json.load(fh) 156 | t0 = time.perf_counter_ns() 157 | r = process(d, mode) 158 | duration_us = int((time.perf_counter_ns() - t0) / 1000) 159 | ret[fname] = [duration_us, r] 160 | 161 | with open(outfile, 'w') as fh: 162 | json.dump(ret, fh) 163 | -------------------------------------------------------------------------------- /benchmark/providers/ethersolve/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.io/gradle:jdk23 2 | 3 | WORKDIR /app 4 | COPY ./build.gradle /app/ 5 | COPY ./src/main/java/HelloEtherSolve.java /app/src/main/java/ 6 | RUN gradle build 7 | ENTRYPOINT ["java", "-jar", "/app/build/libs/app.jar"] 8 | -------------------------------------------------------------------------------- /benchmark/providers/ethersolve/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'application' 4 | } 5 | 6 | repositories { 7 | mavenCentral() 8 | } 9 | 10 | def etherSolveJar = layout.buildDirectory.file('libs/EtherSolve.jar') 11 | 12 | tasks.register('downloadEtherSolve') { 13 | outputs.file etherSolveJar 14 | doLast { 15 | def f = etherSolveJar.get().asFile 16 | f.parentFile.mkdirs() 17 | new URL('https://github.com/SeUniVr/EtherSolve/raw/main/artifact/EtherSolve.jar') 18 | .withInputStream { inputStream -> 19 | f.withOutputStream { it << inputStream } 20 | } 21 | } 22 | } 23 | 24 | dependencies { 25 | implementation files(etherSolveJar) 26 | implementation 'com.google.code.gson:gson:2.10.1' 27 | } 28 | 29 | tasks.named('compileJava') { 30 | dependsOn 'downloadEtherSolve' 31 | } 32 | 33 | application { 34 | mainClass = 'HelloEtherSolve' 35 | } 36 | 37 | jar { 38 | manifest { 39 | attributes 'Main-Class': application.mainClass 40 | } 41 | from { 42 | configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } 43 | } 44 | duplicatesStrategy = DuplicatesStrategy.EXCLUDE 45 | } 46 | -------------------------------------------------------------------------------- /benchmark/providers/ethersolve/src/main/java/HelloEtherSolve.java: -------------------------------------------------------------------------------- 1 | import parseTree.Contract; 2 | import parseTree.cfg.Cfg; 3 | import parseTree.cfg.BasicBlock; 4 | import parseTree.cfg.BasicBlockType; 5 | import com.google.gson.Gson; 6 | import com.google.gson.JsonObject; 7 | 8 | import java.nio.file.*; 9 | import java.util.*; 10 | import java.util.concurrent.*; 11 | import java.util.stream.Collectors; 12 | 13 | public class HelloEtherSolve { 14 | private static final long PROCESS_TIMEOUT_SECONDS = 90; 15 | private final Gson gson = new Gson(); 16 | 17 | private List processContract(String bytecode) { 18 | try { 19 | Cfg cfg = new Contract("Sample", bytecode, true) 20 | .getRuntimeCfg(); 21 | 22 | List ret = new ArrayList(); 23 | for (BasicBlock block : cfg) { 24 | if (block.getType() == BasicBlockType.EXIT) { 25 | continue; 26 | } 27 | long start = block.getOffset(); 28 | for (BasicBlock successor : block.getSuccessors()) { 29 | if (successor.getType() == BasicBlockType.EXIT) { 30 | continue; 31 | } 32 | long off = successor.getOffset(); 33 | ret.add(new Long[]{start, off}); 34 | } 35 | } 36 | return ret; 37 | } catch (Exception e) { 38 | e.printStackTrace(); 39 | return List.of(); 40 | } 41 | } 42 | 43 | private List executeWithTimeout(String bytecode) { 44 | ExecutorService executor = Executors.newSingleThreadExecutor(); 45 | try { 46 | Future> future = executor.submit(() -> processContract(bytecode)); 47 | return future.get(PROCESS_TIMEOUT_SECONDS, TimeUnit.SECONDS); 48 | } catch (TimeoutException e) { 49 | return List.of(); 50 | } catch (Exception e) { 51 | e.printStackTrace(); 52 | return List.of(); 53 | } finally { 54 | executor.shutdownNow(); 55 | } 56 | } 57 | 58 | private void processFile(Path file, Map results) throws Exception { 59 | if (!Files.isRegularFile(file)) return; 60 | 61 | String content = Files.readString(file); 62 | JsonObject json = gson.fromJson(content, JsonObject.class); 63 | String bytecode = json.get("code").getAsString().substring(2); 64 | 65 | long startTime = System.nanoTime(); 66 | List processResults = executeWithTimeout(bytecode); 67 | long timeUs = TimeUnit.NANOSECONDS.toMicros(System.nanoTime() - startTime); 68 | 69 | results.put(file.getFileName().toString(), new Object[]{timeUs, processResults}); 70 | } 71 | 72 | public void execute(String[] args) throws Exception { 73 | if (args.length < 3 || !"flow".equals(args[0])) { 74 | System.out.println("Usage: self flow INPUT_DIR OUTPUT_FILE"); 75 | System.exit(1); 76 | } 77 | 78 | Map results = new HashMap<>(); 79 | Path inputDir = Paths.get(args[1]); 80 | Path outputFile = Paths.get(args[2]); 81 | 82 | try (DirectoryStream stream = Files.newDirectoryStream(inputDir)) { 83 | for (Path file : stream) { 84 | processFile(file, results); 85 | } 86 | } 87 | 88 | Files.writeString(outputFile, gson.toJson(results)); 89 | } 90 | 91 | public static void main(String[] args) { 92 | try { 93 | new HelloEtherSolve().execute(args); 94 | } catch (Exception e) { 95 | e.printStackTrace(); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /benchmark/providers/evm-cfg-builder/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | FROM python:3.12-slim 4 | RUN pip install evm-cfg-builder==0.3.1 5 | WORKDIR /app 6 | COPY main.py /app 7 | ENTRYPOINT ["python3", "/app/main.py"] 8 | -------------------------------------------------------------------------------- /benchmark/providers/evm-cfg-builder/main.py: -------------------------------------------------------------------------------- 1 | import json 2 | import time 3 | import os 4 | import sys 5 | 6 | from evm_cfg_builder import CFG 7 | 8 | 9 | def extract_cfg(code_hex: str): 10 | start_ts = time.perf_counter_ns() 11 | result = [] 12 | try: 13 | cfg = CFG(code_hex) 14 | except Exception as e: 15 | print(e) 16 | duration_us = int((time.perf_counter_ns() - start_ts) / 1000) 17 | return [duration_us, []] 18 | 19 | duration_us = int((time.perf_counter_ns() - start_ts) / 1000) 20 | for x in cfg.basic_blocks: 21 | assert all(ins.mnemonic != 'JUMPDEST' for ins in x.instructions[1:]), x.instructions 22 | result = [(basic_block.start.pc, out.start.pc) for basic_block in cfg.basic_blocks for out in basic_block.all_outgoing_basic_blocks] 23 | 24 | return [duration_us, sorted(result)] 25 | 26 | 27 | if len(sys.argv) < 4: 28 | print('Usage: python3 main.py MODE INPUT_DIR OUTPUT_FILE [SELECTORS_FILE]') 29 | sys.exit(1) 30 | 31 | ret = {} 32 | mode = sys.argv[1] 33 | indir = sys.argv[2] 34 | outfile = sys.argv[3] 35 | 36 | assert mode == 'flow' 37 | 38 | for fname in os.listdir(indir): 39 | with open(f'{indir}/{fname}', 'r') as fh: 40 | d = json.load(fh) 41 | ret[fname] = extract_cfg(d['code']) 42 | 43 | with open(outfile, 'w') as fh: 44 | json.dump(ret, fh) 45 | -------------------------------------------------------------------------------- /benchmark/providers/evm-cfg/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | FROM rust:1.87 as build 4 | RUN cargo install --git https://github.com/plotchy/evm-cfg --root /installed 5 | 6 | FROM python:3.12-slim 7 | WORKDIR /app 8 | RUN pip3 install pydot==3.0.4 9 | COPY --from=build /installed/bin/evm-cfg /usr/bin/evm-cfg 10 | COPY main.py /app 11 | ENTRYPOINT ["python3", "/app/main.py"] 12 | -------------------------------------------------------------------------------- /benchmark/providers/evm-cfg/main.py: -------------------------------------------------------------------------------- 1 | import json 2 | import time 3 | import os 4 | import re 5 | import subprocess 6 | import sys 7 | 8 | import pydot 9 | 10 | # pydot is VERY slow, 1ms for evm-cfg and 900ms for pydot 11 | def process_slow(output: str) -> list: 12 | index = output.find("digraph G {") 13 | assert index != -1 14 | output = output[index:] 15 | 16 | start_ts = time.monotonic() 17 | graph = pydot.graph_from_dot_data(output)[0] 18 | xduration_ms = int((time.monotonic() - start_ts) * 1000) 19 | print(xduration_ms) 20 | 21 | node2pc = {} 22 | for node in graph.get_nodes(): 23 | name = node.get_name() 24 | label = node.get_label() 25 | if label is not None: 26 | label = label.strip('"') 27 | code = [line.split(' ', 1) for line in label.split('\n') if line.startswith('[')] 28 | assert all(x[1] not in 'JUMPDEST' for x in code[1:]) 29 | node2pc[name] = int(code[0][0][1:-1], 16) 30 | 31 | ret = [] 32 | for edge in graph.get_edges(): 33 | src = edge.get_source() 34 | dst = edge.get_destination() 35 | fr = node2pc[src] 36 | to = node2pc[dst] 37 | ret.append((fr, to)) 38 | return ret 39 | 40 | def process_fast(output: str) -> list: 41 | ret = [] 42 | addrs = {} 43 | for line in output.split('\n'): 44 | node_match = re.match(r'^ (\d+) \[ label = "\[([a-f0-9]+)\]', line) 45 | if node_match: 46 | addrs[node_match.group(1)] = int(node_match.group(2), 16) 47 | else: 48 | edge_match = re.match(r'^ (\d+) -> (\d+) \[', line) 49 | if edge_match is not None: 50 | fr = addrs[edge_match.group(1)] 51 | to = addrs[edge_match.group(2)] 52 | ret.append((fr, to)) 53 | return ret 54 | 55 | 56 | def extract_cfg(code_hex: str): 57 | start_ts = time.perf_counter_ns() 58 | try: 59 | output = subprocess.check_output(['evm-cfg', code_hex], timeout=15, text=True) 60 | except Exception as e: 61 | print('Err') 62 | duration_us = int((time.perf_counter_ns() - start_ts) / 1000) 63 | return (duration_us, []) 64 | duration_us = int((time.perf_counter_ns() - start_ts) / 1000) 65 | 66 | # ret = process_slow(output) 67 | ret = process_fast(output) 68 | 69 | return [duration_us, sorted(ret)] 70 | 71 | 72 | if len(sys.argv) < 4: 73 | print('Usage: python3 main.py MODE INPUT_DIR OUTPUT_FILE [SELECTORS_FILE]') 74 | sys.exit(1) 75 | 76 | ret = {} 77 | mode = sys.argv[1] 78 | indir = sys.argv[2] 79 | outfile = sys.argv[3] 80 | 81 | assert mode == 'flow' 82 | 83 | for fname in os.listdir(indir): 84 | with open(f'{indir}/{fname}', 'r') as fh: 85 | d = json.load(fh) 86 | ret[fname] = extract_cfg(d['code']) 87 | 88 | with open(outfile, 'w') as fh: 89 | json.dump(ret, fh) 90 | -------------------------------------------------------------------------------- /benchmark/providers/evm-hound-rs/.dockerignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /benchmark/providers/evm-hound-rs/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "evm_hound" 7 | version = "0.1.4" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "df599e1209f8f80b9b260c05cb65f2cbbc8154f0dfad23d80c007bdbfb98fa4d" 10 | 11 | [[package]] 12 | name = "hex" 13 | version = "0.4.3" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 16 | 17 | [[package]] 18 | name = "itoa" 19 | version = "1.0.11" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 22 | 23 | [[package]] 24 | name = "main" 25 | version = "0.1.0" 26 | dependencies = [ 27 | "evm_hound", 28 | "hex", 29 | "serde", 30 | "serde_json", 31 | ] 32 | 33 | [[package]] 34 | name = "memchr" 35 | version = "2.7.4" 36 | source = "registry+https://github.com/rust-lang/crates.io-index" 37 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 38 | 39 | [[package]] 40 | name = "proc-macro2" 41 | version = "1.0.86" 42 | source = "registry+https://github.com/rust-lang/crates.io-index" 43 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 44 | dependencies = [ 45 | "unicode-ident", 46 | ] 47 | 48 | [[package]] 49 | name = "quote" 50 | version = "1.0.37" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 53 | dependencies = [ 54 | "proc-macro2", 55 | ] 56 | 57 | [[package]] 58 | name = "ryu" 59 | version = "1.0.18" 60 | source = "registry+https://github.com/rust-lang/crates.io-index" 61 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 62 | 63 | [[package]] 64 | name = "serde" 65 | version = "1.0.210" 66 | source = "registry+https://github.com/rust-lang/crates.io-index" 67 | checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" 68 | dependencies = [ 69 | "serde_derive", 70 | ] 71 | 72 | [[package]] 73 | name = "serde_derive" 74 | version = "1.0.210" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" 77 | dependencies = [ 78 | "proc-macro2", 79 | "quote", 80 | "syn", 81 | ] 82 | 83 | [[package]] 84 | name = "serde_json" 85 | version = "1.0.128" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" 88 | dependencies = [ 89 | "itoa", 90 | "memchr", 91 | "ryu", 92 | "serde", 93 | ] 94 | 95 | [[package]] 96 | name = "syn" 97 | version = "2.0.77" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" 100 | dependencies = [ 101 | "proc-macro2", 102 | "quote", 103 | "unicode-ident", 104 | ] 105 | 106 | [[package]] 107 | name = "unicode-ident" 108 | version = "1.0.13" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" 111 | -------------------------------------------------------------------------------- /benchmark/providers/evm-hound-rs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "main" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | evm_hound = "0.1.4" 10 | serde = { version = "1.0", features = ["derive"] } 11 | serde_json = "1.0" 12 | hex = "0.4.3" 13 | -------------------------------------------------------------------------------- /benchmark/providers/evm-hound-rs/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | FROM rust:1.87 4 | WORKDIR /app 5 | COPY . . 6 | RUN --mount=type=cache,target=./.cargo \ 7 | --mount=type=cache,target=./target \ 8 | CARGO_HOME=/app/.cargo \ 9 | cargo install --locked --root=. --path . 10 | ENTRYPOINT ["./bin/main"] 11 | -------------------------------------------------------------------------------- /benchmark/providers/evm-hound-rs/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::io::{BufWriter, Write}; 3 | use std::time::Instant; 4 | use std::{env, fs}; 5 | 6 | #[derive(Debug, serde::Deserialize)] 7 | struct Input { 8 | code: String, 9 | } 10 | 11 | fn main() -> std::io::Result<()> { 12 | let args: Vec = env::args().collect(); 13 | if args.len() < 4 { 14 | eprintln!("Usage: ./main MODE INPUT_DIR OUTPUT_FILE"); 15 | std::process::exit(1); 16 | } 17 | let mode = &args[1]; 18 | let indir = &args[2]; 19 | let outfile = &args[3]; 20 | 21 | if mode != "selectors" { 22 | eprintln!("Only 'selectors' mode supported"); 23 | std::process::exit(1); 24 | } 25 | 26 | type Meta = u64; // duration in us 27 | let mut ret: HashMap)> = HashMap::new(); 28 | 29 | for entry in fs::read_dir(indir)? { 30 | let entry = entry?; 31 | let path = entry.path(); 32 | let fname = entry.file_name().to_str().unwrap().to_string(); 33 | 34 | let code: Vec = { 35 | let file_content = fs::read_to_string(path)?; 36 | let v: Input = serde_json::from_str(&file_content)?; 37 | let x = v.code.strip_prefix("0x").unwrap(); 38 | hex::decode(x).unwrap() 39 | }; 40 | 41 | let now = Instant::now(); 42 | let r = evm_hound::selectors_from_bytecode(&code); 43 | let duration_us = now.elapsed().as_micros() as u64; 44 | let string_selectors: Vec<_> = r.into_iter().map(hex::encode).collect(); 45 | 46 | ret.insert(fname, (duration_us, string_selectors)); 47 | } 48 | 49 | let file = fs::File::create(outfile)?; 50 | let mut bw = BufWriter::new(file); 51 | let _ = serde_json::to_writer(&mut bw, &ret); 52 | bw.flush()?; 53 | 54 | Ok(()) 55 | } 56 | -------------------------------------------------------------------------------- /benchmark/providers/evmlisa/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.io/gradle:jdk23 2 | 3 | WORKDIR /app 4 | RUN apt-get update && apt-get install -y git 5 | 6 | # 24 feb 2025 commit 7 | RUN git clone https://github.com/lisa-analyzer/evm-lisa && cd evm-lisa && git checkout f12cc46d6a87de6c5d553273d841c6d35564b4cd && gradle shadowJar 8 | 9 | # COPY evm-lisa-all.jar /app/ 10 | RUN mv ./evm-lisa/build/libs/evm-lisa-all.jar ./ 11 | 12 | COPY ./build.gradle /app/ 13 | COPY ./src/main/java/Main.java /app/src/main/java/ 14 | RUN gradle jar 15 | ENTRYPOINT ["java", "-jar", "/app/build/libs/app.jar"] 16 | -------------------------------------------------------------------------------- /benchmark/providers/evmlisa/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'application' 4 | } 5 | 6 | repositories { 7 | mavenCentral() 8 | } 9 | 10 | // def etherSolveJar = layout.buildDirectory.file('libs/EtherSolve.jar') 11 | // 12 | // tasks.register('downloadEtherSolve') { 13 | // outputs.file etherSolveJar 14 | // doLast { 15 | // def f = etherSolveJar.get().asFile 16 | // f.parentFile.mkdirs() 17 | // new URL('https://github.com/SeUniVr/EtherSolve/raw/main/artifact/EtherSolve.jar') 18 | // .withInputStream { inputStream -> 19 | // f.withOutputStream { it << inputStream } 20 | // } 21 | // } 22 | // } 23 | 24 | dependencies { 25 | // implementation files(etherSolveJar) 26 | implementation 'com.google.code.gson:gson:2.10.1' 27 | 28 | implementation files('./evm-lisa-all.jar') 29 | } 30 | 31 | // tasks.named('compileJava') { 32 | // dependsOn 'downloadEtherSolve' 33 | // } 34 | 35 | application { 36 | mainClass = 'Main' 37 | } 38 | 39 | jar { 40 | manifest { 41 | attributes 'Main-Class': application.mainClass 42 | } 43 | from { 44 | configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } 45 | } 46 | duplicatesStrategy = DuplicatesStrategy.EXCLUDE 47 | } 48 | -------------------------------------------------------------------------------- /benchmark/providers/evmlisa/src/main/java/Main.java: -------------------------------------------------------------------------------- 1 | import com.google.gson.Gson; 2 | import com.google.gson.JsonObject; 3 | 4 | import java.io.*; 5 | 6 | import java.nio.file.*; 7 | import java.util.*; 8 | import java.util.concurrent.*; 9 | import java.util.stream.Collectors; 10 | import java.util.List; 11 | 12 | import it.unipr.EVMLiSA; 13 | 14 | class ProcessTimeoutExecutor { 15 | private static final long TIMEOUT_SECONDS = 90; 16 | 17 | public List executeWithTimeout(String input) throws Exception { 18 | Process process = null; 19 | ExecutorService executor = null; 20 | 21 | try { 22 | ProcessBuilder pb = new ProcessBuilder( 23 | "java", 24 | "-cp", System.getProperty("java.class.path"), 25 | getClass().getName(), 26 | "subprocess" 27 | ); 28 | 29 | process = pb.start(); 30 | Process finalProcess = process; 31 | 32 | executor = Executors.newFixedThreadPool(2); 33 | 34 | Future inputFuture = executor.submit(() -> { 35 | try (OutputStreamWriter writer = new OutputStreamWriter(finalProcess.getOutputStream())) { 36 | writer.write(input); 37 | writer.flush(); 38 | } catch (IOException e) { 39 | throw new RuntimeException("Failed to write input to process", e); 40 | } 41 | }); 42 | 43 | Future> outputFuture = executor.submit(() -> { 44 | try (ObjectInputStream ois = new ObjectInputStream(finalProcess.getErrorStream())) { 45 | @SuppressWarnings("unchecked") 46 | List result = (List) ois.readObject(); 47 | return result; 48 | } catch (Exception e) { 49 | throw new RuntimeException("Failed to read process output", e); 50 | } 51 | }); 52 | 53 | inputFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); 54 | return outputFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); 55 | 56 | } catch (TimeoutException e) { 57 | System.out.println("Process execution timed out"); 58 | return null; 59 | } catch (Exception e) { 60 | System.out.println("Process execution failed: " + e.getMessage()); 61 | return null; 62 | } finally { 63 | if (executor != null) { 64 | executor.shutdownNow(); 65 | } 66 | if (process != null) { 67 | process.destroyForcibly(); 68 | process.waitFor(1, TimeUnit.SECONDS); 69 | } 70 | } 71 | } 72 | 73 | public static void main(String[] args) { 74 | if (args.length > 0 && args[0].equals("subprocess")) { 75 | try { 76 | BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); 77 | String input = reader.readLine(); 78 | 79 | List result = processContract(input); 80 | 81 | ObjectOutputStream oos = new ObjectOutputStream(System.err); 82 | oos.writeObject(result); 83 | oos.flush(); 84 | } catch (Exception e) { 85 | System.err.println("Subprocess error: " + e.getMessage()); 86 | System.exit(1); 87 | } 88 | System.exit(0); 89 | } 90 | } 91 | 92 | private static List processContract(String bytecode) { 93 | try { 94 | return new EVMLiSA().computeBasicBlocks(bytecode); 95 | } catch (Exception e) { 96 | e.printStackTrace(); 97 | return List.of(); 98 | } 99 | } 100 | } 101 | 102 | public class Main { 103 | private static final long PROCESS_TIMEOUT_SECONDS = 90; 104 | private final Gson gson = new Gson(); 105 | 106 | private List executeWithTimeout(String bytecode) { 107 | ProcessTimeoutExecutor executor = new ProcessTimeoutExecutor(); 108 | try { 109 | List result = executor.executeWithTimeout(bytecode); 110 | if (result == null) { 111 | return List.of(); 112 | } else { 113 | return result; 114 | } 115 | } catch (Exception e) { 116 | return List.of(); 117 | } 118 | } 119 | 120 | private void processFile(Path file, Map results) throws Exception { 121 | if (!Files.isRegularFile(file)) return; 122 | 123 | String content = Files.readString(file); 124 | JsonObject json = gson.fromJson(content, JsonObject.class); 125 | String bytecode = json.get("code").getAsString(); // NEED 0x prefix for evmlisa 126 | 127 | long startTime = System.nanoTime(); 128 | List processResults = executeWithTimeout(bytecode); 129 | long timeUs = TimeUnit.NANOSECONDS.toMicros(System.nanoTime() - startTime); 130 | 131 | results.put(file.getFileName().toString(), new Object[]{timeUs, processResults}); 132 | } 133 | 134 | public void execute(String[] args) throws Exception { 135 | if (args.length < 3 || !"flow".equals(args[0])) { 136 | System.out.println("Usage: self flow INPUT_DIR OUTPUT_FILE"); 137 | System.exit(1); 138 | } 139 | 140 | Map results = new HashMap<>(); 141 | Path inputDir = Paths.get(args[1]); 142 | Path outputFile = Paths.get(args[2]); 143 | 144 | try (DirectoryStream stream = Files.newDirectoryStream(inputDir)) { 145 | int a = 0; 146 | for (Path file : stream) { 147 | a += 1; 148 | System.out.println(a); 149 | System.out.println(file); 150 | processFile(file, results); 151 | } 152 | } 153 | 154 | Files.writeString(outputFile, gson.toJson(results)); 155 | } 156 | 157 | public static void main(String[] args) { 158 | try { 159 | new Main().execute(args); 160 | } catch (Exception e) { 161 | e.printStackTrace(); 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /benchmark/providers/evmole-js/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | FROM node:22 AS build 4 | RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain 1.84 5 | COPY ./rust /workdir 6 | WORKDIR /workdir/javascript 7 | ENV PATH=/root/.cargo/bin:$PATH 8 | RUN npm ci && npm run build && npm pack 9 | 10 | FROM node:22 11 | WORKDIR /app 12 | COPY --from=build /workdir/javascript/evmole-*.tgz ./ 13 | RUN npm install ./evmole-*.tgz 14 | COPY main.mjs /app 15 | ENTRYPOINT ["node", "/app/main.mjs"] 16 | -------------------------------------------------------------------------------- /benchmark/providers/evmole-js/main.mjs: -------------------------------------------------------------------------------- 1 | import { readdirSync, readFileSync, writeFileSync } from 'fs' 2 | import { hrtime } from 'process' 3 | 4 | import { contractInfo } from 'evmole' 5 | 6 | const argv = process.argv; 7 | if (argv.length < 5) { 8 | console.log('Usage: node main.js MODE INPUT_DIR OUTPUT_FILE [SELECTORS_FILE]') 9 | process.exit(1) 10 | } 11 | 12 | const mode = argv[2]; 13 | const indir = argv[3]; 14 | const outfile = argv[4]; 15 | 16 | const selectors = mode === 'mutability' || mode === 'arguments' ? JSON.parse(readFileSync(argv[5])) : {}; 17 | 18 | function timeit(fn) { 19 | const start_ts = hrtime.bigint(); 20 | const r = fn(); 21 | const duration_us = Number((hrtime.bigint() - start_ts) / 1000n); 22 | return [duration_us, r] 23 | } 24 | 25 | function extract(code, mode, fname) { 26 | if (mode === 'selectors') { 27 | let [duration_us, r] = timeit(() => contractInfo(code, {selectors: true})); 28 | return [duration_us, r.functions.map((f) => f.selector)]; 29 | } else if (mode === 'arguments') { 30 | let [duration_us, r] = timeit(() => contractInfo(code, {arguments: true})); 31 | const by_sel = new Map(r.functions.map((f) => [f.selector, f.arguments])); 32 | return [duration_us, Object.fromEntries( 33 | selectors[fname][1].map((s) => [s, by_sel.get(s) ?? 'notfound']) 34 | )]; 35 | } else if (mode === 'mutability') { 36 | let [duration_us, r] = timeit(() => contractInfo(code, {stateMutability: true})); 37 | const by_sel = new Map(r.functions.map((f) => [f.selector, f.stateMutability])); 38 | return [duration_us, Object.fromEntries( 39 | selectors[fname][1].map((s) => [s, by_sel.get(s) ?? 'notfound']) 40 | )]; 41 | } else if (mode === 'flow') { 42 | let [duration_us, r] = timeit(() => contractInfo(code, {controlFlowGraph: true})); 43 | let ret = [] 44 | for (const b of r.controlFlowGraph.blocks) { 45 | let bt = b.get('type'); 46 | let start = b.get('start'); 47 | let data = b.get('data'); 48 | if (bt === 'Jump') { 49 | ret.push([start, data.to]) 50 | } else if (bt === 'Jumpi') { 51 | ret.push([start, data.true_to]) 52 | ret.push([start, data.false_to]) 53 | } else if (bt === 'DynamicJump') { 54 | for (let v of data.to) { 55 | if(v.to) { 56 | ret.push([start, v.to]) 57 | } 58 | } 59 | } else if (bt === 'DynamicJumpi') { 60 | for (let v of data.true_to) { 61 | if(v.to) { 62 | ret.push([start, v.to]) 63 | } 64 | } 65 | ret.push([start, data.false_to]) 66 | } else if (bt === 'Terminate') { 67 | // do nothing 68 | } else { 69 | throw `unknown block type ${bt}`; 70 | } 71 | } 72 | return [duration_us, ret]; 73 | } else { 74 | throw 'unsupported mode'; 75 | } 76 | } 77 | 78 | const res = Object.fromEntries( 79 | readdirSync(indir).map( 80 | file => [ 81 | file, 82 | extract(JSON.parse(readFileSync(`${indir}/${file}`))['code'], mode, file) 83 | ] 84 | ) 85 | ); 86 | writeFileSync(outfile, JSON.stringify(res), 'utf8'); 87 | -------------------------------------------------------------------------------- /benchmark/providers/evmole-py/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | FROM rust:1.87 AS build 4 | RUN wget https://github.com/PyO3/maturin/releases/download/v1.8.6/maturin-x86_64-unknown-linux-musl.tar.gz \ 5 | && tar xf maturin-*.tar.gz && mv maturin /usr/local/bin/ 6 | COPY ./rust /workdir 7 | WORKDIR /workdir 8 | RUN maturin build --release --out wheel/ 9 | 10 | FROM python:3.11-slim 11 | WORKDIR /app 12 | COPY --from=build /workdir/wheel ./wheel 13 | RUN pip3 install ./wheel/*.whl 14 | COPY main.py /app 15 | ENTRYPOINT ["python3", "/app/main.py"] 16 | -------------------------------------------------------------------------------- /benchmark/providers/evmole-py/main.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import json 3 | import os 4 | import time 5 | 6 | from evmole import contract_info, BlockType 7 | 8 | parser = argparse.ArgumentParser() 9 | parser.add_argument('mode', choices=['selectors', 'arguments', 'mutability', 'flow']) 10 | parser.add_argument('input_dir') 11 | parser.add_argument('output_file') 12 | parser.add_argument('selectors_file', nargs='*') 13 | cfg = parser.parse_args() 14 | 15 | selectors = {} 16 | if cfg.mode != 'selectors': 17 | with open(cfg.selectors_file[0], 'r') as fh: 18 | selectors = json.load(fh) 19 | 20 | ret = {} 21 | for fname in os.listdir(cfg.input_dir): 22 | with open(f'{cfg.input_dir}/{fname}', 'r') as fh: 23 | d = json.load(fh) 24 | code = d['code'] 25 | t0 = time.perf_counter_ns() 26 | if cfg.mode == 'selectors': 27 | info = contract_info(code, selectors=True) 28 | elif cfg.mode == 'arguments': 29 | info = contract_info(code, arguments=True) 30 | elif cfg.mode == 'mutability': 31 | info = contract_info(code, state_mutability=True) 32 | elif cfg.mode == 'flow': 33 | info = contract_info(code, control_flow_graph=True) 34 | else: 35 | raise Exception(f'Unknown mode {cfg.mode}') 36 | duration_us = int((time.perf_counter_ns() - t0) / 1000) 37 | 38 | if cfg.mode == 'selectors': 39 | r = [f.selector for f in info.functions] 40 | elif cfg.mode == 'arguments': 41 | by_sel = {f.selector: f.arguments for f in info.functions} 42 | r = {s: by_sel.get(s, 'notfound') for s in selectors[fname][1]} 43 | elif cfg.mode == 'mutability': 44 | by_sel = {f.selector: f.state_mutability for f in info.functions} 45 | r = {s: by_sel.get(s, 'notfound') for s in selectors[fname][1]} 46 | elif cfg.mode == 'flow': 47 | r = [] 48 | for bl in info.control_flow_graph.blocks: 49 | match bl.btype: 50 | case BlockType.Jump(to): 51 | r.append((bl.start, to)) 52 | case BlockType.Jumpi(true_to, false_to): 53 | r.append((bl.start, true_to)) 54 | r.append((bl.start, false_to)) 55 | case BlockType.DynamicJump(to): 56 | for v in to: 57 | if v.to is not None: 58 | r.append((bl.start, v.to)) 59 | case BlockType.DynamicJumpi(true_to, false_to): 60 | for v in true_to: 61 | if v.to is not None: 62 | r.append((bl.start, v.to)) 63 | r.append((bl.start, false_to)) 64 | case BlockType.Terminate: 65 | pass 66 | else: 67 | raise Exception(f'Unknown mode {cfg.mode}') 68 | 69 | ret[fname] = [duration_us, r] 70 | 71 | with open(cfg.output_file, 'w') as fh: 72 | json.dump(ret, fh) 73 | -------------------------------------------------------------------------------- /benchmark/providers/evmole-rs/.dockerignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /benchmark/providers/evmole-rs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "main" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | serde = { version = "1.0", features = ["derive"] } 10 | serde_json = "1.0" 11 | hex = "0.4.3" 12 | clap = { version = "4.5.1", features = ["derive"] } 13 | 14 | evmole = { path = "./rust" } 15 | -------------------------------------------------------------------------------- /benchmark/providers/evmole-rs/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | FROM rust:1.87 4 | WORKDIR /app 5 | COPY . . 6 | RUN --mount=type=cache,target=./.cargo \ 7 | --mount=type=cache,target=./target \ 8 | CARGO_HOME=/app/.cargo \ 9 | cargo install --locked --root=. --path . 10 | ENTRYPOINT ["./bin/main"] 11 | -------------------------------------------------------------------------------- /benchmark/providers/evmole-rs/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{BTreeSet, HashMap}; 2 | use std::fs; 3 | use std::io::{BufWriter, Write}; 4 | use std::time::Instant; 5 | 6 | use evmole::control_flow_graph::BlockType; 7 | 8 | use clap::{Parser, ValueEnum}; 9 | 10 | #[derive(serde::Deserialize)] 11 | struct Input { 12 | code: Option, 13 | runtimeBytecode: Option, 14 | } 15 | 16 | #[derive(ValueEnum, Clone, PartialEq)] 17 | enum Mode { 18 | Selectors, 19 | Arguments, 20 | Mutability, 21 | Storage, 22 | Blocks, 23 | Flow, 24 | } 25 | 26 | 27 | #[derive(Parser)] 28 | struct Args { 29 | mode: Mode, 30 | 31 | input_dir: String, 32 | 33 | output_file: String, 34 | 35 | selectors_file: Option, 36 | 37 | #[arg(long)] 38 | filter_filename: Option, 39 | 40 | #[arg(long)] 41 | filter_selector: Option, 42 | } 43 | 44 | fn timeit(args: evmole::ContractInfoArgs) -> (evmole::Contract, u64) 45 | { 46 | let now = Instant::now(); 47 | let result = evmole::contract_info(args); 48 | let duration_us = now.elapsed().as_micros() as u64; 49 | (result, duration_us) 50 | } 51 | 52 | fn main() -> Result<(), Box> { 53 | let cfg = Args::parse(); 54 | 55 | type Meta = u64; // duration in ms 56 | 57 | let selectors: HashMap)> = match cfg.mode { 58 | Mode::Selectors | Mode::Storage | Mode::Blocks | Mode::Flow => HashMap::new(), 59 | Mode::Arguments | Mode::Mutability => { 60 | let file_content = fs::read_to_string(cfg.selectors_file.unwrap())?; 61 | serde_json::from_str(&file_content)? 62 | } 63 | }; 64 | 65 | let only_selector = if let Some(s) = cfg.filter_selector { 66 | vec![s.strip_prefix("0x").unwrap_or(&s).to_string()] 67 | } else { 68 | vec![] 69 | }; 70 | 71 | let mut ret_selectors: HashMap)> = HashMap::new(); 72 | let mut ret_other: HashMap)> = HashMap::new(); 73 | let mut ret_flow: HashMap)> = HashMap::new(); 74 | 75 | for entry in fs::read_dir(cfg.input_dir)? { 76 | let entry = entry?; 77 | let path = entry.path(); 78 | let fname = entry.file_name().to_str().unwrap().to_string(); 79 | 80 | if let Some(ref v) = cfg.filter_filename { 81 | if !fname.contains(v) { 82 | continue; 83 | } 84 | } 85 | 86 | let code = { 87 | let file_content = fs::read_to_string(path)?; 88 | let v: Input = serde_json::from_str(&file_content)?; 89 | let code = if v.runtimeBytecode.is_some() { 90 | v.runtimeBytecode.unwrap() 91 | } else { 92 | v.code.unwrap() 93 | }; 94 | hex::decode(code.strip_prefix("0x").expect("0x prefix expected"))? 95 | }; 96 | 97 | // eprintln!("processing {}", fname); 98 | 99 | match cfg.mode { 100 | Mode::Selectors => { 101 | let (info, dur) = timeit(evmole::ContractInfoArgs::new(&code).with_selectors()); 102 | ret_selectors.insert( 103 | fname, 104 | ( 105 | dur, 106 | info.functions 107 | .unwrap() 108 | .iter() 109 | .map(|f| hex::encode(f.selector)) 110 | .collect(), 111 | ), 112 | ); 113 | } 114 | Mode::Arguments => { 115 | let fsel = if !only_selector.is_empty() { 116 | &only_selector 117 | } else { 118 | &selectors[&fname].1 119 | }; 120 | 121 | let (info, dur) = timeit(evmole::ContractInfoArgs::new(&code).with_arguments()); 122 | let args: HashMap = info 123 | .functions 124 | .unwrap() 125 | .into_iter() 126 | .map(|f| { 127 | ( 128 | hex::encode(f.selector), 129 | f.arguments 130 | .unwrap() 131 | .iter() 132 | .map(|t| t.sol_type_name().to_string()) 133 | .collect::>() 134 | .join(","), 135 | ) 136 | }) 137 | .collect(); 138 | 139 | let res = fsel 140 | .iter() 141 | .map(|s| { 142 | ( 143 | s.to_string(), 144 | match args.get(s) { 145 | Some(v) => v.to_string(), 146 | None => "not_found".to_string(), 147 | }, 148 | ) 149 | }) 150 | .collect(); 151 | 152 | ret_other.insert(fname, (dur, res)); 153 | } 154 | Mode::Mutability => { 155 | let fsel = if !only_selector.is_empty() { 156 | &only_selector 157 | } else { 158 | &selectors[&fname].1 159 | }; 160 | 161 | let (info, dur) = timeit(evmole::ContractInfoArgs::new(&code).with_state_mutability()); 162 | let smut: HashMap = info 163 | .functions 164 | .unwrap() 165 | .into_iter() 166 | .map(|f| { 167 | ( 168 | hex::encode(f.selector), 169 | f.state_mutability.unwrap().as_json_str().to_string(), 170 | ) 171 | }) 172 | .collect(); 173 | 174 | let res = fsel 175 | .iter() 176 | .map(|s| { 177 | ( 178 | s.to_string(), 179 | match smut.get(s) { 180 | Some(v) => v.to_string(), 181 | None => "not_found".to_string(), 182 | }, 183 | ) 184 | }) 185 | .collect(); 186 | 187 | ret_other.insert(fname, (dur, res)); 188 | } 189 | 190 | Mode::Storage => { 191 | let (info, dur) = timeit(evmole::ContractInfoArgs::new(&code).with_storage()); 192 | ret_other.insert( 193 | fname, 194 | ( 195 | dur, 196 | info.storage 197 | .unwrap() 198 | .into_iter() 199 | .map(|sr| { 200 | (format!("{}_{}", hex::encode(sr.slot), sr.offset), sr.r#type) 201 | }) 202 | .collect(), 203 | ), 204 | ); 205 | } 206 | 207 | Mode::Blocks => { 208 | let (info, dur) = timeit(evmole::ContractInfoArgs::new(&code).with_basic_blocks()); 209 | ret_flow.insert(fname, (dur, info.basic_blocks.unwrap().into_iter().collect())); 210 | } 211 | 212 | Mode::Flow => { 213 | let (info, dur) = timeit(evmole::ContractInfoArgs::new(&code).with_control_flow_graph()); 214 | let mut flow: BTreeSet<(usize, usize)> = BTreeSet::new(); 215 | for block in info.control_flow_graph.unwrap().blocks.values() { 216 | match block.btype { 217 | BlockType::Jump{to} => { 218 | flow.insert((block.start, to)); 219 | }, 220 | BlockType::Jumpi{true_to, false_to} => { 221 | flow.insert((block.start, true_to)); 222 | flow.insert((block.start, false_to)); 223 | }, 224 | BlockType::DynamicJump { ref to } => { 225 | for x in to { 226 | if let Some(v) = x.to { 227 | flow.insert((block.start, v)); 228 | } 229 | } 230 | }, 231 | BlockType::DynamicJumpi { ref true_to, false_to } => { 232 | for x in true_to { 233 | if let Some(v) = x.to { 234 | flow.insert((block.start, v)); 235 | } 236 | } 237 | flow.insert((block.start, false_to)); 238 | }, 239 | BlockType::Terminate{..} => {}, 240 | } 241 | } 242 | ret_flow.insert(fname, (dur, flow)); 243 | } 244 | } 245 | } 246 | 247 | let file = fs::File::create(cfg.output_file)?; 248 | let mut bw = BufWriter::new(file); 249 | if cfg.mode == Mode::Selectors { 250 | let _ = serde_json::to_writer(&mut bw, &ret_selectors); 251 | } else if cfg.mode == Mode::Blocks || cfg.mode == Mode::Flow { 252 | let _ = serde_json::to_writer(&mut bw, &ret_flow); 253 | } else { 254 | let _ = serde_json::to_writer(&mut bw, &ret_other); 255 | } 256 | bw.flush()?; 257 | 258 | Ok(()) 259 | } 260 | -------------------------------------------------------------------------------- /benchmark/providers/heimdall-rs/.dockerignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /benchmark/providers/heimdall-rs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "main" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | heimdall-core = { git = "https://github.com/Jon-Becker/heimdall-rs.git", tag = "0.8.6" } 10 | tokio = {version = "1", features = ["full"]} 11 | serde = { version = "1.0", features = ["derive"] } 12 | serde_json = "1.0" 13 | clap = { version = "4.5.1", features = ["derive"] } 14 | -------------------------------------------------------------------------------- /benchmark/providers/heimdall-rs/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | FROM rust:1.87 4 | WORKDIR /app 5 | COPY . . 6 | RUN --mount=type=cache,target=./.cargo \ 7 | --mount=type=cache,target=./target \ 8 | CARGO_HOME=/app/.cargo \ 9 | cargo install --locked --root=. --path . 10 | ENTRYPOINT ["./bin/main"] 11 | -------------------------------------------------------------------------------- /benchmark/providers/heimdall-rs/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{BTreeSet, HashMap}; 2 | use std::fs; 3 | use std::io::{BufWriter, Write}; 4 | use std::time::Instant; 5 | 6 | use clap::{Parser, ValueEnum}; 7 | 8 | use heimdall_core::heimdall_decompiler::DecompilerArgsBuilder; 9 | 10 | #[derive(Debug, serde::Deserialize)] 11 | struct Input { 12 | code: String, 13 | } 14 | 15 | #[derive(ValueEnum, Clone, PartialEq)] 16 | enum Mode { 17 | Selectors, 18 | Arguments, 19 | Mutability, 20 | Flow, 21 | } 22 | 23 | #[derive(Parser)] 24 | struct Args { 25 | mode: Mode, 26 | 27 | input_dir: String, 28 | 29 | output_file: String, 30 | 31 | selectors_file: Option, 32 | } 33 | 34 | async fn measure_time(f: F) -> (T, u64) 35 | where 36 | F: std::future::Future, 37 | { 38 | let start = Instant::now(); 39 | let result = f.await; 40 | let duration_us = start.elapsed().as_micros() as u64; 41 | (result, duration_us) 42 | } 43 | 44 | #[tokio::main] 45 | async fn main() -> Result<(), Box> { 46 | let cfg = Args::parse(); 47 | 48 | type Meta = u64; // duration in us 49 | 50 | let selectors: HashMap)> = match cfg.mode { 51 | Mode::Selectors | Mode::Flow => HashMap::new(), 52 | Mode::Arguments | Mode::Mutability => { 53 | let file_content = fs::read_to_string(cfg.selectors_file.unwrap())?; 54 | serde_json::from_str(&file_content)? 55 | } 56 | }; 57 | 58 | let mut ret_selectors: HashMap)> = HashMap::new(); 59 | let mut ret_other: HashMap)> = HashMap::new(); 60 | let mut ret_flow: HashMap)> = HashMap::new(); 61 | 62 | for entry in fs::read_dir(cfg.input_dir)? { 63 | let entry = entry?; 64 | let path = entry.path(); 65 | let fname = entry.file_name().to_str().unwrap().to_string(); 66 | // eprintln!("{}", fname); 67 | let hex_code: String = { 68 | let file_content = fs::read_to_string(path)?; 69 | let v: Input = serde_json::from_str(&file_content)?; 70 | v.code 71 | }; 72 | match cfg.mode { 73 | Mode::Selectors => { 74 | let dargs = DecompilerArgsBuilder::new() 75 | .target(hex_code) 76 | .skip_resolving(true) 77 | .build()?; 78 | 79 | let (result, duration_us) = measure_time(heimdall_core::heimdall_decompiler::decompile(dargs)).await; 80 | 81 | let r = match result { 82 | Err(e) => { 83 | println!("got error for {}: {}", fname, e); 84 | vec![] 85 | } 86 | Ok(v) => v 87 | .abi 88 | .functions 89 | .keys() 90 | .map(|x| x.strip_prefix("Unresolved_").unwrap().to_string()) 91 | .collect(), 92 | }; 93 | ret_selectors.insert(fname, (duration_us, r)); 94 | } 95 | Mode::Arguments => { 96 | let dargs = DecompilerArgsBuilder::new() 97 | .target(hex_code) 98 | .skip_resolving(true) 99 | .build()?; 100 | 101 | let (result, duration_us) = measure_time(heimdall_core::heimdall_decompiler::decompile(dargs)).await; 102 | 103 | let r = match result { 104 | Err(e) => { 105 | println!("got error for {}: {}", fname, e); 106 | selectors[&fname] 107 | .1 108 | .iter() 109 | .map(|s| (s.to_string(), "not_found".to_string())) 110 | .collect() 111 | } 112 | Ok(v) => { 113 | let args: HashMap = v 114 | .abi 115 | .functions 116 | .iter() 117 | .map(|(name, v)| { 118 | let selector = 119 | name.strip_prefix("Unresolved_").unwrap().to_string(); 120 | let arguments: Vec<_> = 121 | v[0].inputs.iter().map(|v| v.ty.to_string()).collect(); 122 | (selector, arguments.join(",")) 123 | }) 124 | .collect(); 125 | 126 | selectors[&fname] 127 | .1 128 | .iter() 129 | .map(|s| { 130 | ( 131 | s.to_string(), 132 | match args.get(s) { 133 | Some(v) => v.to_string(), 134 | None => "not_found".to_string(), 135 | }, 136 | ) 137 | }) 138 | .collect() 139 | } 140 | }; 141 | ret_other.insert(fname, (duration_us, r)); 142 | } 143 | Mode::Mutability => { 144 | let dargs = DecompilerArgsBuilder::new() 145 | .target(hex_code) 146 | .skip_resolving(true) 147 | .build()?; 148 | 149 | let (result, duration_us) = measure_time(heimdall_core::heimdall_decompiler::decompile(dargs)).await; 150 | 151 | let r = match result { 152 | Err(e) => { 153 | println!("got error for {}: {}", fname, e); 154 | selectors[&fname] 155 | .1 156 | .iter() 157 | .map(|s| (s.to_string(), "not_found".to_string())) 158 | .collect() 159 | } 160 | Ok(v) => { 161 | let args: HashMap = v 162 | .abi 163 | .functions 164 | .iter() 165 | .map(|(name, v)| { 166 | let selector = 167 | name.strip_prefix("Unresolved_").unwrap().to_string(); 168 | let mutability = v[0].state_mutability.as_json_str().to_string(); 169 | (selector, mutability) 170 | }) 171 | .collect(); 172 | 173 | selectors[&fname] 174 | .1 175 | .iter() 176 | .map(|s| { 177 | ( 178 | s.to_string(), 179 | match args.get(s) { 180 | Some(v) => v.to_string(), 181 | None => "not_found".to_string(), 182 | }, 183 | ) 184 | }) 185 | .collect() 186 | } 187 | }; 188 | ret_other.insert(fname, (duration_us, r)); 189 | } 190 | Mode::Flow => { 191 | let cfg_args = heimdall_core::heimdall_cfg::CfgArgsBuilder::new() 192 | .target(hex_code) 193 | .build()?; 194 | 195 | let (result, duration_us) = measure_time(heimdall_core::heimdall_cfg::cfg(cfg_args)).await; 196 | let cfg = result?; 197 | 198 | let mut jump_dest_mapping: HashMap = HashMap::new(); 199 | let mut control_flow: BTreeSet<(usize, usize)> = BTreeSet::new(); 200 | 201 | // Helper function to parse hex addresses 202 | fn parse_hex_address(hex_str: &str) -> usize { 203 | let s = hex_str.strip_prefix("0x").unwrap_or(hex_str); 204 | usize::from_str_radix(s, 16).unwrap() 205 | } 206 | 207 | // Split blocks by JUMPDEST 208 | for node in cfg.graph.raw_nodes() { 209 | let instructions: Vec<_> = node.weight 210 | .lines() 211 | .filter_map(|line| { 212 | let (pc_hex, op) = line.trim_end().split_once(" ")?; 213 | let pc = parse_hex_address(pc_hex); 214 | Some((pc, op)) 215 | }) 216 | .collect(); 217 | 218 | let mut current_pc = instructions[0].0; 219 | for (pc, op) in instructions.iter().skip(1) { 220 | if *op == "JUMPDEST" { 221 | jump_dest_mapping.insert(instructions[0].0, *pc); 222 | control_flow.insert((current_pc, *pc)); 223 | current_pc = *pc; 224 | } 225 | } 226 | } 227 | 228 | // Process edges 229 | control_flow.extend(cfg.graph.raw_edges().iter().map(|edge| { 230 | let source = cfg.graph.node_weight(edge.source()) 231 | .and_then(|s| s.split_once(" ")) 232 | .map(|(hex, _)| parse_hex_address(hex)) 233 | .unwrap(); 234 | 235 | let target = cfg.graph.node_weight(edge.target()) 236 | .and_then(|s| s.split_once(" ")) 237 | .map(|(hex, _)| parse_hex_address(hex)) 238 | .unwrap(); 239 | 240 | let from = jump_dest_mapping.get(&source).copied().unwrap_or(source); 241 | (from, target) 242 | })); 243 | 244 | ret_flow.insert(fname, (duration_us, control_flow)); 245 | } 246 | } 247 | } 248 | 249 | let file = fs::File::create(cfg.output_file)?; 250 | let mut bw = BufWriter::new(file); 251 | if cfg.mode == Mode::Selectors { 252 | let _ = serde_json::to_writer(&mut bw, &ret_selectors); 253 | } else if cfg.mode == Mode::Flow { 254 | let _ = serde_json::to_writer(&mut bw, &ret_flow); 255 | } else { 256 | let _ = serde_json::to_writer(&mut bw, &ret_other); 257 | } 258 | bw.flush()?; 259 | 260 | Ok(()) 261 | } 262 | -------------------------------------------------------------------------------- /benchmark/providers/sevm/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | FROM node:22 4 | WORKDIR /app 5 | RUN npm install sevm@0.7.4 6 | COPY main.mjs /app 7 | ENTRYPOINT ["node", "/app/main.mjs"] 8 | -------------------------------------------------------------------------------- /benchmark/providers/sevm/main.mjs: -------------------------------------------------------------------------------- 1 | import { readdirSync, readFileSync, writeFileSync } from 'fs' 2 | import { hrtime } from 'process' 3 | 4 | import { Contract } from 'sevm'; 5 | 6 | const argv = process.argv; 7 | if (argv.length < 5) { 8 | console.log('Usage: node main.js MODE INPUT_DIR OUTPUT_FILE [SELECTORS_FILE]') 9 | process.exit(1) 10 | } 11 | 12 | const mode = argv[2]; 13 | const indir = argv[3]; 14 | const outfile = argv[4]; 15 | 16 | const selectors = mode === 'mutability' ? JSON.parse(readFileSync(argv[5])) : {}; 17 | 18 | function timeit(fn) { 19 | const start_ts = hrtime.bigint(); 20 | const r = fn(); 21 | const duration_us = Number((hrtime.bigint() - start_ts) / 1000n); 22 | return [duration_us, r] 23 | } 24 | 25 | function extract(code, mode, fname) { 26 | const [duration_us, contract] = timeit(() => { 27 | try { 28 | return new Contract(code); 29 | } catch (e) { 30 | // console.log(e); 31 | } 32 | }); 33 | if (mode === 'selectors') { 34 | return [duration_us, Object.keys(contract ? contract.functions : {})] 35 | } else if (mode === 'mutability') { 36 | return [duration_us, Object.fromEntries(selectors[fname][1].map((s) => { 37 | const fn = contract ? contract.functions[s] : undefined; 38 | if (fn === undefined) { 39 | return [s, 'selnotfound']; 40 | } else { 41 | return [s, fn.constant ? 'view' : (fn.payable ? 'payable' : 'nonpayable')]; 42 | } 43 | }))]; 44 | } else if (mode === 'flow') { 45 | let res = new Map(); 46 | const add = (from, to) => { 47 | res.set(`${from}|${to}`, [from, to]); 48 | }; 49 | for (const [pc, block] of (contract ? contract.blocks : [])) { 50 | for (const {opcode} of block.opcodes.slice(1)) { // skip first 51 | if (opcode.opcode === 91) { 52 | throw 'JUMPDEST inside block'; 53 | } 54 | } 55 | for (const state of block.states) { 56 | // console.log(pc, state.last); 57 | switch (state.last?.name) { 58 | case 'Jumpi': 59 | add(pc, state.last.destBranch.pc); 60 | add(pc, state.last.fallBranch.pc); 61 | break; 62 | case 'SigCase': 63 | add(pc, state.last.offset.jumpDest); 64 | add(pc, state.last.fallBranch.pc); 65 | break; 66 | case 'Jump': 67 | add(pc, state.last.destBranch.pc); 68 | break; 69 | case 'JumpDest': 70 | add(pc, state.last.fallBranch.pc); 71 | break; 72 | default: 73 | } 74 | } 75 | } 76 | const ret = [...res.values()].sort((a, b) => a[0] === b[0] ? a[1] - b[1] : a[0] - b[0]) 77 | return [duration_us, ret] 78 | } else { 79 | throw 'unsupported mode'; 80 | } 81 | } 82 | 83 | const res = Object.fromEntries( 84 | readdirSync(indir).map( 85 | file => [ 86 | file, 87 | extract(JSON.parse(readFileSync(`${indir}/${file}`))['code'], mode, file) 88 | ] 89 | ) 90 | ); 91 | writeFileSync(outfile, JSON.stringify(res), 'utf8'); 92 | -------------------------------------------------------------------------------- /benchmark/providers/simple/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | FROM python:3.12-slim 4 | WORKDIR /app 5 | COPY main.py /app 6 | ENTRYPOINT ["python3", "/app/main.py"] 7 | -------------------------------------------------------------------------------- /benchmark/providers/simple/main.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import sys 4 | import time 5 | 6 | 7 | def extract_selectors(code: bytes) -> list[str]: 8 | ret = [] 9 | for i in range(len(code) - 5): 10 | # PUSH3/PUSH4 11 | if (code[i] == 0x62 or code[i] == 0x63): 12 | off = code[i] - 0x62 13 | 14 | # EQ or (DUP2 + EQ) 15 | if (code[i+off+4] == 0x14) or (code[i+off+4] == 0x81 and code[i+off+5] == 0x14): 16 | ret.append(code[i+1:i+4+off]) 17 | 18 | return [s.hex().zfill(8) for s in ret] 19 | 20 | if len(sys.argv) < 4: 21 | print('Usage: python3 main.py MODE INPUT_DIR OUTPUT_FILE [SELECTORS_FILE]') 22 | sys.exit(1) 23 | 24 | ret = {} 25 | mode = sys.argv[1] 26 | indir = sys.argv[2] 27 | outfile = sys.argv[3] 28 | 29 | selectors = {} 30 | if mode != 'selectors': 31 | selectors_file = sys.argv[4] 32 | with open(selectors_file, 'r') as fh: 33 | selectors = json.load(fh) 34 | 35 | for fname in os.listdir(indir): 36 | with open(f'{indir}/{fname}', 'r') as fh: 37 | d = json.load(fh) 38 | code = bytes.fromhex(d['code'][2:]) 39 | t0 = time.perf_counter_ns() 40 | if mode == 'arguments': 41 | r = {s: '' for s in selectors[fname][1]} 42 | elif mode == 'mutability': 43 | r = {s: 'nonpayable' for s in selectors[fname][1]} 44 | elif mode == 'selectors': 45 | r = extract_selectors(code) 46 | else: 47 | raise Exception(f'Unknown mode {mode}') 48 | duration_us = int((time.perf_counter_ns() - t0) / 1000) 49 | ret[fname] = [duration_us, r] 50 | 51 | with open(outfile, 'w') as fh: 52 | json.dump(ret, fh) 53 | -------------------------------------------------------------------------------- /benchmark/providers/smlxl/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "main" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | serde = { version = "1.0", features = ["derive"] } 8 | serde_json = "1.0" 9 | hex = "0.4.3" 10 | clap = { version = "4.5.1", features = ["derive"] } 11 | 12 | storage-layout-extractor = { git = "https://github.com/smlxl/storage-layout-extractor", branch = "main" } 13 | -------------------------------------------------------------------------------- /benchmark/providers/smlxl/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | FROM rust:1.87 4 | WORKDIR /app 5 | COPY . . 6 | RUN --mount=type=cache,target=./.cargo \ 7 | --mount=type=cache,target=./target \ 8 | CARGO_HOME=/app/.cargo \ 9 | cargo install --locked --root=. --path . 10 | ENTRYPOINT ["./bin/main"] 11 | -------------------------------------------------------------------------------- /benchmark/providers/smlxl/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::fs; 3 | use std::io::{BufWriter, Write}; 4 | use std::time::{Duration, Instant}; 5 | use clap::{Parser, ValueEnum}; 6 | 7 | use storage_layout_extractor::{ 8 | self, 9 | watchdog::Watchdog, 10 | extractor::{ 11 | chain::{ 12 | version::{ChainVersion, EthereumVersion}, 13 | Chain, 14 | }, 15 | contract::Contract, 16 | }, 17 | tc, 18 | vm, 19 | }; 20 | 21 | #[derive(serde::Deserialize)] 22 | struct Input { 23 | #[serde(rename = "runtimeBytecode")] 24 | code: String, 25 | } 26 | 27 | #[derive(ValueEnum, Clone, PartialEq)] 28 | enum Mode { 29 | Storage, 30 | } 31 | 32 | #[derive(Parser)] 33 | struct Args { 34 | mode: Mode, 35 | 36 | input_dir: String, 37 | 38 | output_file: String, 39 | 40 | selectors_file: Option, 41 | 42 | #[arg(long)] 43 | filter_filename: Option, 44 | 45 | #[arg(long)] 46 | filter_selector: Option, 47 | } 48 | 49 | // TODO: improve this, validate with github upstream (new issue) 50 | fn type_str(tp: &tc::abi::AbiType) -> String { 51 | use tc::abi::AbiType::*; 52 | // eprintln!("{:?}", tp); 53 | match tp { 54 | Any => "uint256".to_string(), 55 | Number { size } | UInt { size } => format!("uint{}", size.unwrap_or(256)), 56 | Int { size } => format!("int{}", size.unwrap_or(256)), 57 | Address => "address".to_string(), 58 | Selector => "err_selector".to_string(), 59 | Function => "err_function".to_string(), 60 | Bool => "bool".to_string(), 61 | Array { size, tp } => format!("{}[{}]", type_str(tp), u16::try_from(size.0).unwrap_or(1)), 62 | Bytes { length } => format!("bytes{}", length.unwrap_or(32)), 63 | Bits { length } => "errbits".to_string(), 64 | DynArray { tp } => format!("{}[]", type_str(tp)), 65 | DynBytes => "bytes".to_string(), 66 | Mapping { key_type, value_type } => format!("mapping({} => {})", type_str(key_type), type_str(value_type)), 67 | Struct { elements } => { 68 | "struct".to_string() 69 | }, 70 | InfiniteType => "uint256".to_string(), 71 | ConflictedType { conflicts, reasons } => "err_conflict".to_string(), 72 | } 73 | } 74 | 75 | #[derive(Debug)] 76 | struct MyWatchDog { 77 | pub end: Instant, 78 | } 79 | 80 | impl MyWatchDog { 81 | fn new(time_limit: Duration) -> Self { 82 | MyWatchDog { 83 | end: Instant::now() + time_limit, 84 | } 85 | } 86 | } 87 | 88 | impl Watchdog for MyWatchDog { 89 | fn should_stop(&self) -> bool { 90 | let now = Instant::now(); 91 | now >= self.end 92 | } 93 | 94 | fn poll_every(&self) -> usize { 95 | storage_layout_extractor::constant::DEFAULT_WATCHDOG_POLL_LOOP_ITERATIONS 96 | } 97 | } 98 | 99 | fn main() -> Result<(), Box> { 100 | let cfg = Args::parse(); 101 | type Meta = u64; // duration in us 102 | let mut ret_other: HashMap)> = HashMap::new(); 103 | for entry in fs::read_dir(cfg.input_dir)? { 104 | let entry = entry?; 105 | let path = entry.path(); 106 | let fname = entry.file_name().to_str().unwrap().to_string(); 107 | let code = { 108 | let file_content = fs::read_to_string(path)?; 109 | let v: Input = serde_json::from_str(&file_content)?; 110 | hex::decode(v.code.strip_prefix("0x").expect("0x prefix expected"))? 111 | }; 112 | eprintln!("processing {}", fname); 113 | 114 | let contract = Contract::new( 115 | code, 116 | Chain::Ethereum { 117 | version: EthereumVersion::latest(), 118 | }, 119 | ); 120 | 121 | let vm_config = vm::Config::default(); 122 | let unifier_config = tc::Config::default(); 123 | // let watchdog = LazyWatchdog.in_rc(); 124 | let watchdog = std::rc::Rc::new(MyWatchDog::new(Duration::from_secs(3))); 125 | let extractor = storage_layout_extractor::new(contract, vm_config, unifier_config, watchdog); 126 | let now = Instant::now(); 127 | let r = extractor.analyze(); 128 | let dur = now.elapsed().as_micros() as u64; 129 | 130 | ret_other.insert( 131 | fname, 132 | ( 133 | dur, 134 | match r { 135 | Ok(layout) => { 136 | layout.slots().iter().map(|s| 137 | ( 138 | format!( 139 | "{}_{}", 140 | hex::encode(s.index.0.to_be_bytes()), 141 | s.offset / 8, 142 | ), 143 | type_str(&s.typ), 144 | )).collect() 145 | }, 146 | Err(_err) => { 147 | HashMap::new() 148 | // "err".to_string() 149 | }, 150 | } 151 | ) 152 | ); 153 | } 154 | 155 | let file = fs::File::create(cfg.output_file)?; 156 | let mut bw = BufWriter::new(file); 157 | let _ = serde_json::to_writer(&mut bw, &ret_other); 158 | bw.flush()?; 159 | 160 | Ok(()) 161 | } 162 | -------------------------------------------------------------------------------- /benchmark/providers/whatsabi/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | FROM node:22 4 | WORKDIR /app 5 | RUN npm install @shazow/whatsabi@0.19.0 6 | COPY main.mjs /app 7 | ENTRYPOINT ["node", "/app/main.mjs"] 8 | -------------------------------------------------------------------------------- /benchmark/providers/whatsabi/main.mjs: -------------------------------------------------------------------------------- 1 | import { readdirSync, readFileSync, writeFileSync } from 'fs' 2 | import { hrtime } from 'process' 3 | 4 | import { whatsabi } from "@shazow/whatsabi"; 5 | 6 | const argv = process.argv; 7 | if (argv.length < 5) { 8 | console.log('Usage: node main.js MODE INPUT_DIR OUTPUT_FILE [SELECTORS_FILE]') 9 | process.exit(1) 10 | } 11 | 12 | const mode = argv[2]; 13 | const indir = argv[3]; 14 | const outfile = argv[4]; 15 | 16 | const selectors = mode === 'mutability' ? JSON.parse(readFileSync(argv[5])) : {}; 17 | 18 | function timeit(fn) { 19 | const start_ts = hrtime.bigint(); 20 | const r = fn(); 21 | const duration_us = Number((hrtime.bigint() - start_ts) / 1000n); 22 | return [duration_us, r] 23 | } 24 | 25 | function extract(code, mode, fname) { 26 | if (mode === 'selectors') { 27 | const [duration_us, r] = timeit(() => whatsabi.selectorsFromBytecode(code)) 28 | return [duration_us, r.map(x => x.slice(2))]; // remove '0x' prefix 29 | } else if (mode === 'mutability') { 30 | const [duration_us, abi] = timeit(() => whatsabi.abiFromBytecode(code)); 31 | const smut = Object.fromEntries(abi.filter((v) => v.type == 'function').map((v) => [v.selector, v.stateMutability])); 32 | return [duration_us, Object.fromEntries(selectors[fname][1].map((s) => [s, smut[`0x${s}`] || 'selnotfound']))]; 33 | } else { 34 | throw 'unsupported mode'; 35 | } 36 | } 37 | 38 | const res = Object.fromEntries( 39 | readdirSync(indir).map( 40 | file => [ 41 | file, 42 | extract(JSON.parse(readFileSync(`${indir}/${file}`))['code'], mode, file) 43 | ] 44 | ) 45 | ); 46 | writeFileSync(outfile, JSON.stringify(res), 'utf8'); 47 | -------------------------------------------------------------------------------- /benchmark/results/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cdump/evmole/9919ba418bb07ed13ffe990ee33b245184edb43e/benchmark/results/.gitkeep -------------------------------------------------------------------------------- /dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | MODE=$1 4 | DS=$2 5 | 6 | BDIR=`pwd`/benchmark 7 | 8 | ln -s `pwd` ${BDIR}/providers/evmole-rs/rust 2>/dev/null || true 9 | 10 | case ${NOTRACE+x} in 11 | x) FEAT='' ;; 12 | *) FEAT="--features evmole/trace_${MODE}" ;; 13 | esac 14 | 15 | cargo run \ 16 | --manifest-path benchmark/providers/evmole-rs/Cargo.toml \ 17 | ${FEAT} \ 18 | ${MODE} \ 19 | ${BDIR}/datasets/${2} \ 20 | out.json \ 21 | ${BDIR}/results/etherscan.selectors_${2}.json \ 22 | --filter-filename ${3} \ 23 | --filter-selector ${4} 24 | 25 | rm -rf ${BDIR}/providers/evmole-rs/rust 26 | -------------------------------------------------------------------------------- /evmole.pyi: -------------------------------------------------------------------------------- 1 | from typing import List, Optional, Tuple, Union 2 | 3 | class Function: 4 | """ 5 | Represents a public smart contract function. 6 | 7 | Attributes: 8 | selector (str): Function selector as a 4-byte hex string without '0x' prefix (e.g., 'aabbccdd'). 9 | bytecode_offset (int): Starting byte offset within the EVM bytecode for the function body. 10 | arguments (Optional[str]): Function argument types in canonical format (e.g., 'uint256,address[]'). 11 | None if arguments were not extracted 12 | state_mutability (Optional[str]): Function's state mutability ('pure', 'view', 'payable', or 'nonpayable'). 13 | None if state mutability was not extracted 14 | """ 15 | 16 | selector: str 17 | bytecode_offset: int 18 | arguments: Optional[str] 19 | state_mutability: Optional[str] 20 | 21 | class StorageRecord: 22 | """ 23 | Represents a storage variable record in a smart contract's storage layout. 24 | 25 | Attributes: 26 | slot (str): Storage slot number as a hex string (e.g., '0', '1b'). 27 | offset (int): Byte offset within the storage slot (0-31). 28 | type (str): Variable type (e.g., 'uint256', 'mapping(address => uint256)', 'bytes32'). 29 | reads (List[str]): List of function selectors that read from this storage location. 30 | writes (List[str]): List of function selectors that write to this storage location. 31 | """ 32 | 33 | slot: str 34 | offset: int 35 | type: str 36 | reads: List[str] 37 | writes: List[str] 38 | 39 | class DynamicJump: 40 | """ 41 | Represents a dynamic jump destination in the control flow. 42 | 43 | Attributes: 44 | path (List[int]): Path of basic blocks leading to this jump. 45 | to (Optional[int]): Target basic block offset if known, None otherwise. 46 | """ 47 | path: List[int] 48 | to: Optional[int] 49 | 50 | class BlockType: 51 | """ 52 | Represents the type of a basic block and its control flow. 53 | This is an enum-like class, all child classes are derived from BlockType class 54 | """ 55 | class Terminate: 56 | """Block terminates execution""" 57 | success: bool # True for normal termination (STOP/RETURN), False for REVERT/INVALID 58 | 59 | class Jump: 60 | """Block ends with unconditional jump""" 61 | to: int # Destination basic block offset 62 | 63 | class Jumpi: 64 | """Block ends with conditional jump""" 65 | true_to: int # Destination if condition is true 66 | false_to: int # Destination if condition is false (fall-through) 67 | 68 | class DynamicJump: 69 | """Block ends with jump to computed destination""" 70 | to: List[DynamicJump] # Possible computed jump destinations 71 | 72 | class DynamicJumpi: 73 | """Block ends with conditional jump to computed destination""" 74 | true_to: List[DynamicJump] # Possible computed jump destinations if true 75 | false_to: int # Destination if condition is false (fall-through) 76 | 77 | class Block: 78 | """ 79 | Represents a basic block in the control flow graph. 80 | 81 | Attributes: 82 | start (int): Byte offset where the block's first opcode begins 83 | end (int): Byte offset where the block's last opcode begins 84 | btype (BlockType): Type of the block and its control flow. 85 | """ 86 | start: int 87 | end: int 88 | btype: BlockType 89 | 90 | class ControlFlowGraph: 91 | """ 92 | Represents the control flow graph of the contract bytecode. 93 | 94 | Attributes: 95 | blocks (List[Block]): List of basic blocks in the control flow graph. 96 | """ 97 | blocks: List[Block] 98 | 99 | class Contract: 100 | """ 101 | Contains analyzed information about a smart contract. 102 | 103 | Attributes: 104 | functions (Optional[List[Function]]): List of detected contract functions. 105 | None if no functions were extracted 106 | storage (Optional[List[StorageRecord]]): List of contract storage records. 107 | None if storage layout was not extracted 108 | disassembled (Optional[List[Tuple[int, str]]]): List of bytecode instructions, where each element is [offset, instruction]. 109 | None if disassembly was not requested 110 | basic_blocks (Optional[List[Tuple[int, int]]]): List of basic block ranges as (first_op, last_op) offsets. 111 | None if basic blocks were not requested 112 | control_flow_graph (Optional[ControlFlowGraph]): Control flow graph of the contract. 113 | None if control flow analysis was not requested 114 | """ 115 | 116 | functions: Optional[List[Function]] 117 | storage: Optional[List[StorageRecord]] 118 | disassembled: Optional[List[Tuple[int, str]]] 119 | basic_blocks: Optional[List[Tuple[int, int]]] 120 | control_flow_graph: Optional[ControlFlowGraph] 121 | 122 | def contract_info( 123 | code: Union[bytes, str], 124 | *, 125 | selectors: bool = False, 126 | arguments: bool = False, 127 | state_mutability: bool = False, 128 | storage: bool = False, 129 | disassemble: bool = False, 130 | basic_blocks: bool = False, 131 | control_flow_graph: bool = False, 132 | ) -> Contract: 133 | """ 134 | Extracts information about a smart contract from its EVM bytecode. 135 | 136 | Args: 137 | code (Union[bytes, str]): Runtime bytecode as a hex string (with or without '0x' prefix) 138 | or raw bytes. 139 | selectors (bool, optional): When True, extracts function selectors. Defaults to False. 140 | arguments (bool, optional): When True, extracts function arguments. Defaults to False. 141 | state_mutability (bool, optional): When True, extracts function state mutability. 142 | Defaults to False. 143 | storage (bool, optional): When True, extracts the contract's storage layout. 144 | Defaults to False. 145 | disassemble (bool, optional): When True, includes disassembled bytecode. 146 | Defaults to False. 147 | basic_blocks (bool, optional): When True, extracts basic block ranges. 148 | Defaults to False. 149 | control_flow_graph (bool, optional): When True, builds control flow graph. 150 | Defaults to False. 151 | 152 | Returns: 153 | Contract: Object containing the requested smart contract information. Fields that 154 | weren't requested to be extracted will be None. 155 | """ 156 | ... 157 | -------------------------------------------------------------------------------- /javascript/.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cdump/evmole/9919ba418bb07ed13ffe990ee33b245184edb43e/javascript/.npmignore -------------------------------------------------------------------------------- /javascript/Makefile: -------------------------------------------------------------------------------- 1 | DOCKER ?= docker 2 | 3 | EXAMPLES = node esbuild parcel parcel_packageExports vite webpack 4 | 5 | EXAMPLES_DIR = $(addprefix examples/,$(EXAMPLES)) 6 | NODE_MODULES_TARGETS = $(addsuffix /node_modules,$(EXAMPLES_DIR)) 7 | 8 | %/node_modules: evmole.latest.tgz 9 | @echo "Installing dependencies in $(@D) $@" 10 | npm --prefix $(@D) install --no-save 11 | touch $@ 12 | 13 | evmole-%.tgz: 14 | npm run build 15 | npm pack 16 | 17 | evmole.latest.tgz: evmole-%.tgz 18 | ln -f -s evmole-*.tgz evmole.latest.tgz 19 | 20 | test: evmole.latest.tgz $(NODE_MODULES_TARGETS) 21 | npx tsc ./dist/evmole.d.ts 22 | 23 | for dir in $(EXAMPLES_DIR); do \ 24 | npm --prefix $$dir install --force --no-save ./evmole.latest.tgz; \ 25 | done 26 | 27 | node examples/node/with_import.mjs 28 | 29 | node examples/node/with_require.cjs 30 | 31 | ifeq ($(GITHUB_ACTIONS),true) 32 | cd tests && npx playwright test 33 | else 34 | npm install 35 | $(DOCKER) run --network=none --rm \ 36 | -v $(shell pwd)/:/mnt \ 37 | -it mcr.microsoft.com/playwright:v1.50.1-noble \ 38 | /bin/bash -c \ 39 | 'cd /mnt/tests && npx playwright test' 40 | endif 41 | 42 | clean: 43 | rm -f *.tgz 44 | for dir in $(EXAMPLES_DIR); do \ 45 | rm -rf $$dir/node_modules; \ 46 | rm -rf $$dir/dist; \ 47 | rm -rf $$dir/.parcel-cache; \ 48 | done 49 | -------------------------------------------------------------------------------- /javascript/examples/contract.sol: -------------------------------------------------------------------------------- 1 | contract Contract { 2 | function a(uint32 v) public pure returns (uint32) { 3 | return v + 1; 4 | } 5 | } 6 | 7 | // Runtime binary: 8 | // $ solc --no-cbor-metadata --optimize --hashes --bin-runtime ./contract.sol 9 | -------------------------------------------------------------------------------- /javascript/examples/esbuild/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ESBuild App 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /javascript/examples/esbuild/main.js: -------------------------------------------------------------------------------- 1 | import { contractInfo } from 'evmole/wasm_import' 2 | 3 | document.body.innerHTML = ` 4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | ` 12 | 13 | const code = '6080604052348015600e575f80fd5b50600436106026575f3560e01c8063fae7ab8214602a575b5f80fd5b603960353660046062565b6052565b60405163ffffffff909116815260200160405180910390f35b5f605c826001608a565b92915050565b5f602082840312156071575f80fd5b813563ffffffff811681146083575f80fd5b9392505050565b63ffffffff8181168382160190811115605c57634e487b7160e01b5f52601160045260245ffd'; 14 | 15 | const info = contractInfo(code, { selectors: false, arguments: true, stateMutability: true, disassemble: true }); 16 | 17 | document.getElementById('selectors').innerText = info.functions[0].selector; 18 | 19 | document.getElementById('arguments').innerText = info.functions[0].arguments; 20 | 21 | document.getElementById('state_mutability').innerText = info.functions[0].stateMutability; 22 | 23 | document.getElementById('disassembled').innerText = info.disassembled.map(([pc, val]) => `${pc} ${val}`).join('\n') 24 | -------------------------------------------------------------------------------- /javascript/examples/esbuild/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-esbuild", 3 | "version": "0.0.1", 4 | "scripts": { 5 | "build": "esbuild main.js --format=esm --loader:.wasm=file --bundle --outdir=dist && cp index.html dist/" 6 | }, 7 | "devDependencies": { 8 | "esbuild": "0.24" 9 | }, 10 | "dependencies": { 11 | "evmole": "file:../../evmole.latest.tgz" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /javascript/examples/node/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-node", 3 | "version": "0.0.1", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "example-node", 9 | "version": "0.0.1", 10 | "dependencies": { 11 | "evmole": "file:../../evmole.latest.tgz" 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /javascript/examples/node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-node", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "evmole": "file:../../evmole.latest.tgz" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /javascript/examples/node/with_import.mjs: -------------------------------------------------------------------------------- 1 | import { contractInfo } from 'evmole'; 2 | import { equal, deepEqual } from 'node:assert'; 3 | 4 | const code = '6080604052348015600e575f80fd5b50600436106026575f3560e01c8063fae7ab8214602a575b5f80fd5b603960353660046062565b6052565b60405163ffffffff909116815260200160405180910390f35b5f605c826001608a565b92915050565b5f602082840312156071575f80fd5b813563ffffffff811681146083575f80fd5b9392505050565b63ffffffff8181168382160190811115605c57634e487b7160e01b5f52601160045260245ffd'; 5 | 6 | const info = contractInfo(code, { selectors: false, arguments: true, stateMutability: true, disassemble: true }); 7 | equal(info.functions.length, 1); 8 | equal(info.functions[0].selector, 'fae7ab82'); 9 | equal(info.functions[0].arguments, 'uint32'); 10 | equal(info.functions[0].stateMutability, 'pure'); 11 | equal(info.storage, undefined); 12 | deepEqual(info.disassembled[1], [2, 'PUSH1 40']); 13 | -------------------------------------------------------------------------------- /javascript/examples/node/with_require.cjs: -------------------------------------------------------------------------------- 1 | const assert = require('node:assert'); 2 | const evmole = require('evmole'); 3 | 4 | const code = '6080604052348015600e575f80fd5b50600436106026575f3560e01c8063fae7ab8214602a575b5f80fd5b603960353660046062565b6052565b60405163ffffffff909116815260200160405180910390f35b5f605c826001608a565b92915050565b5f602082840312156071575f80fd5b813563ffffffff811681146083575f80fd5b9392505050565b63ffffffff8181168382160190811115605c57634e487b7160e01b5f52601160045260245ffd'; 5 | 6 | const info = evmole.contractInfo(code, { selectors: false, arguments: true, stateMutability: true, disassemble: true }); 7 | assert.equal(info.functions.length, 1); 8 | assert.equal(info.functions[0].selector, 'fae7ab82'); 9 | assert.equal(info.functions[0].arguments, 'uint32'); 10 | assert.equal(info.functions[0].stateMutability, 'pure'); 11 | assert.equal(info.storage, undefined); 12 | assert.deepEqual(info.disassembled[1], [2, 'PUSH1 40']); 13 | -------------------------------------------------------------------------------- /javascript/examples/parcel/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-parcel", 3 | "version": "0.0.1", 4 | "source": "src/index.html", 5 | "scripts": { 6 | "start": "parcel serve", 7 | "build": "parcel build" 8 | }, 9 | "devDependencies": { 10 | "parcel": "^2.13" 11 | }, 12 | "dependencies": { 13 | "evmole": "file:../../evmole.latest.tgz" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /javascript/examples/parcel/src/app.js: -------------------------------------------------------------------------------- 1 | import init, { contractInfo } from 'evmole/dist/evmole.js' 2 | // https://parceljs.org/blog/v2-9-0/#new-resolver 3 | 4 | document.body.innerHTML = ` 5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | ` 13 | 14 | const code = '6080604052348015600e575f80fd5b50600436106026575f3560e01c8063fae7ab8214602a575b5f80fd5b603960353660046062565b6052565b60405163ffffffff909116815260200160405180910390f35b5f605c826001608a565b92915050565b5f602082840312156071575f80fd5b813563ffffffff811681146083575f80fd5b9392505050565b63ffffffff8181168382160190811115605c57634e487b7160e01b5f52601160045260245ffd'; 15 | 16 | // or: `init().then() => { }` 17 | async function main() { 18 | await init(); 19 | 20 | const info = contractInfo(code, { selectors: false, arguments: true, stateMutability: true, disassemble: true }); 21 | 22 | document.getElementById('selectors').innerText = info.functions[0].selector; 23 | document.getElementById('arguments').innerText = info.functions[0].arguments; 24 | document.getElementById('state_mutability').innerText = info.functions[0].stateMutability; 25 | document.getElementById('disassembled').innerText = info.disassembled.map(([pc, val]) => `${pc} ${val}`).join('\n') 26 | } 27 | 28 | main(); 29 | -------------------------------------------------------------------------------- /javascript/examples/parcel/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Parcel App 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /javascript/examples/parcel_packageExports/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-parcel", 3 | "version": "0.0.1", 4 | "source": "src/index.html", 5 | "scripts": { 6 | "start": "parcel serve", 7 | "build": "parcel build" 8 | }, 9 | "devDependencies": { 10 | "parcel": "^2.13" 11 | }, 12 | "@parcel/resolver-default": { 13 | "packageExports": true 14 | }, 15 | "dependencies": { 16 | "evmole": "file:../../evmole.latest.tgz" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /javascript/examples/parcel_packageExports/src/app.js: -------------------------------------------------------------------------------- 1 | import init, { contractInfo } from 'evmole/no_tla' 2 | // https://parceljs.org/blog/v2-9-0/#new-resolver 3 | 4 | document.body.innerHTML = ` 5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | ` 13 | 14 | const code = '6080604052348015600e575f80fd5b50600436106026575f3560e01c8063fae7ab8214602a575b5f80fd5b603960353660046062565b6052565b60405163ffffffff909116815260200160405180910390f35b5f605c826001608a565b92915050565b5f602082840312156071575f80fd5b813563ffffffff811681146083575f80fd5b9392505050565b63ffffffff8181168382160190811115605c57634e487b7160e01b5f52601160045260245ffd'; 15 | 16 | // or: `init().then() => { }` 17 | async function main() { 18 | await init(); 19 | 20 | const info = contractInfo(code, { selectors: false, arguments: true, stateMutability: true, disassemble: true }); 21 | 22 | document.getElementById('selectors').innerText = info.functions[0].selector; 23 | document.getElementById('arguments').innerText = info.functions[0].arguments; 24 | document.getElementById('state_mutability').innerText = info.functions[0].stateMutability; 25 | document.getElementById('disassembled').innerText = info.disassembled.map(([pc, val]) => `${pc} ${val}`).join('\n') 26 | } 27 | 28 | main(); 29 | -------------------------------------------------------------------------------- /javascript/examples/parcel_packageExports/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | My First Parcel App 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /javascript/examples/vite/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Vite App 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /javascript/examples/vite/main.js: -------------------------------------------------------------------------------- 1 | import { contractInfo } from 'evmole' 2 | 3 | document.getElementById('app').innerHTML = ` 4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | ` 12 | 13 | const code = '6080604052348015600e575f80fd5b50600436106026575f3560e01c8063fae7ab8214602a575b5f80fd5b603960353660046062565b6052565b60405163ffffffff909116815260200160405180910390f35b5f605c826001608a565b92915050565b5f602082840312156071575f80fd5b813563ffffffff811681146083575f80fd5b9392505050565b63ffffffff8181168382160190811115605c57634e487b7160e01b5f52601160045260245ffd'; 14 | 15 | const info = contractInfo(code, { selectors: false, arguments: true, stateMutability: true, disassemble: true }); 16 | 17 | document.getElementById('selectors').innerText = info.functions[0].selector; 18 | 19 | document.getElementById('arguments').innerText = info.functions[0].arguments; 20 | 21 | document.getElementById('state_mutability').innerText = info.functions[0].stateMutability; 22 | 23 | document.getElementById('disassembled').innerText = info.disassembled.map(([pc, val]) => `${pc} ${val}`).join('\n') 24 | -------------------------------------------------------------------------------- /javascript/examples/vite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-vite", 3 | "version": "0.0.1", 4 | "type": "module", 5 | "scripts": { 6 | "build": "vite build", 7 | "preview": "vite preview" 8 | }, 9 | "devDependencies": { 10 | "vite": "^6.1" 11 | }, 12 | "dependencies": { 13 | "evmole": "file:../../evmole.latest.tgz" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /javascript/examples/vite/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | export default defineConfig({ 3 | build: { 4 | target: 'esnext', 5 | }, 6 | optimizeDeps: { // https://github.com/vitejs/vite/issues/13756 7 | exclude: ['evmole'], 8 | }, 9 | }) 10 | -------------------------------------------------------------------------------- /javascript/examples/webpack/index.js: -------------------------------------------------------------------------------- 1 | import { contractInfo } from 'evmole' 2 | 3 | document.body.innerHTML = ` 4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | ` 12 | 13 | const code = '6080604052348015600e575f80fd5b50600436106026575f3560e01c8063fae7ab8214602a575b5f80fd5b603960353660046062565b6052565b60405163ffffffff909116815260200160405180910390f35b5f605c826001608a565b92915050565b5f602082840312156071575f80fd5b813563ffffffff811681146083575f80fd5b9392505050565b63ffffffff8181168382160190811115605c57634e487b7160e01b5f52601160045260245ffd'; 14 | 15 | const info = contractInfo(code, { selectors: false, arguments: true, stateMutability: true, disassemble: true }); 16 | 17 | document.getElementById('selectors').innerText = info.functions[0].selector; 18 | 19 | document.getElementById('arguments').innerText = info.functions[0].arguments; 20 | 21 | document.getElementById('state_mutability').innerText = info.functions[0].stateMutability; 22 | 23 | document.getElementById('disassembled').innerText = info.disassembled.map(([pc, val]) => `${pc} ${val}`).join('\n') 24 | -------------------------------------------------------------------------------- /javascript/examples/webpack/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-webpack", 3 | "version": "0.0.1", 4 | "scripts": { 5 | "build": "webpack" 6 | }, 7 | "devDependencies": { 8 | "html-webpack-plugin": "^5.6", 9 | "webpack": "^5.97", 10 | "webpack-cli": "^6.0" 11 | }, 12 | "dependencies": { 13 | "evmole": "file:../../evmole.latest.tgz" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /javascript/examples/webpack/webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 2 | 3 | module.exports = { 4 | experiments: { 5 | asyncWebAssembly: true, 6 | }, 7 | 8 | entry: "./index.js", 9 | output: { 10 | path: __dirname + "/dist", 11 | filename: "index_bundle.js", 12 | }, 13 | plugins: [new HtmlWebpackPlugin()], 14 | }; 15 | -------------------------------------------------------------------------------- /javascript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "evmole", 3 | "description": "Extracts function selectors and arguments from EVM bytecode", 4 | "version": "0.8.0", 5 | "license": "MIT", 6 | "collaborators": [ 7 | "Maxim Andreev " 8 | ], 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/cdump/evmole.git" 12 | }, 13 | "type": "module", 14 | "main": "dist/evmole_node.cjs", 15 | "module": "dist/evmole.mjs", 16 | "types": "dist/evmole.d.ts", 17 | "files": [ 18 | "README.md", 19 | "dist/evmole_bg.wasm", 20 | "dist/evmole_bg.wasm.d.ts", 21 | "dist/evmole.js", 22 | "dist/evmole.d.ts", 23 | "dist/evmole.mjs", 24 | "dist/evmole_node.cjs", 25 | "dist/evmole_node.mjs", 26 | "dist/evmole_wasm_import.js" 27 | ], 28 | "exports": { 29 | ".": { 30 | "require": "./dist/evmole_node.cjs", 31 | "node": "./dist/evmole_node.mjs", 32 | "default": "./dist/evmole.mjs" 33 | }, 34 | "./evmole_bg.wasm": "./dist/evmole_bg.wasm", 35 | "./wasm_import": "./dist/evmole_wasm_import.js", 36 | "./no_tla": "./dist/evmole.js", 37 | "./dist/*": "./dist/*" 38 | }, 39 | "scripts": { 40 | "build": "rm -rf dist && npm run build-wasm && npm run build-node && npm run build-esm && npm run build-cp", 41 | "build-wasm": "wasm-pack build --no-pack --out-dir ./javascript/dist --target web --features javascript", 42 | "build-node": "rollup ./src/evmole_node.cjs --file ./dist/evmole_node.cjs --format cjs", 43 | "build-esm": "rollup ./src/evmole_esm.js --file ./dist/evmole.mjs --format esm", 44 | "build-cp": "cp ./src/evmole_node.mjs ./src/evmole_wasm_import.js dist/", 45 | "doc": "jsdoc2md --files ./dist/evmole.js --heading-depth 3" 46 | }, 47 | "devDependencies": { 48 | "jsdoc-to-markdown": "^9.1", 49 | "rollup": "^4.28", 50 | "typescript": "^5.7.3", 51 | "wasm-pack": "^0.13" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /javascript/src/evmole_esm.js: -------------------------------------------------------------------------------- 1 | export { contractInfo } from "../dist/evmole.js"; 2 | import initEvmole from "../dist/evmole.js"; 3 | 4 | await initEvmole({ module_or_path: new URL('evmole_bg.wasm', import.meta.url) }) 5 | -------------------------------------------------------------------------------- /javascript/src/evmole_node.cjs: -------------------------------------------------------------------------------- 1 | import { initSync } from "../dist/evmole.js"; 2 | 3 | const path = require("path").join(__dirname, "evmole_bg.wasm"); 4 | const bytes = require("fs").readFileSync(path); 5 | 6 | initSync({ module: bytes }); 7 | 8 | export { contractInfo } from "../dist/evmole.js"; 9 | -------------------------------------------------------------------------------- /javascript/src/evmole_node.mjs: -------------------------------------------------------------------------------- 1 | import { initSync } from "../dist/evmole.js"; 2 | import fs from "node:fs"; 3 | 4 | const path = new URL("../dist/evmole_bg.wasm", import.meta.url); 5 | const bytes = fs.readFileSync(path); 6 | 7 | initSync({ module: bytes }); 8 | 9 | export { contractInfo } from "../dist/evmole.js"; 10 | -------------------------------------------------------------------------------- /javascript/src/evmole_wasm_import.js: -------------------------------------------------------------------------------- 1 | export { contractInfo } from "../dist/evmole.js"; 2 | import initEvmole from "../dist/evmole.js"; 3 | 4 | import wasmUrl from "../dist/evmole_bg.wasm"; 5 | 6 | await initEvmole({ module_or_path: new URL(wasmUrl, import.meta.url) }); 7 | -------------------------------------------------------------------------------- /javascript/tests/main.test.js: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | [ 4 | { name: 'Vite packed', port: 4151 }, 5 | 6 | { name: 'Webpack packed', port: 4152 }, 7 | 8 | { name: 'Parcel preview', port: 4153 }, 9 | { name: 'Parcel packed', port: 4154 }, 10 | 11 | { name: 'Parcel_packageExports preview', port: 4155 }, 12 | { name: 'Parcel_packageExports packed', port: 4156 }, 13 | 14 | { name: 'ESBuild packed', port: 4157 }, 15 | ].forEach(({ name, port }) => { 16 | test(name, async ({ page }) => { 17 | await page.goto(`http://localhost:${port}/`); 18 | 19 | const s = await page.locator('#selectors') 20 | await expect(s).toHaveText('fae7ab82'); 21 | 22 | const a = await page.locator('#arguments') 23 | await expect(a).toHaveText('uint32'); 24 | 25 | const m = await page.locator('#state_mutability') 26 | await expect(m).toHaveText('pure'); 27 | 28 | const d = await page.locator('#disassembled') 29 | await expect(d).toHaveText(/0 PUSH1 80.*/); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /javascript/tests/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "examples-tests", 3 | "version": "0.0.1", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "examples-tests", 9 | "version": "0.0.1", 10 | "devDependencies": { 11 | "@playwright/test": "^1.50" 12 | } 13 | }, 14 | "node_modules/@playwright/test": { 15 | "version": "1.50.1", 16 | "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.50.1.tgz", 17 | "integrity": "sha512-Jii3aBg+CEDpgnuDxEp/h7BimHcUTDlpEtce89xEumlJ5ef2hqepZ+PWp1DDpYC/VO9fmWVI1IlEaoI5fK9FXQ==", 18 | "dev": true, 19 | "license": "Apache-2.0", 20 | "dependencies": { 21 | "playwright": "1.50.1" 22 | }, 23 | "bin": { 24 | "playwright": "cli.js" 25 | }, 26 | "engines": { 27 | "node": ">=18" 28 | } 29 | }, 30 | "node_modules/fsevents": { 31 | "version": "2.3.2", 32 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 33 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 34 | "dev": true, 35 | "hasInstallScript": true, 36 | "license": "MIT", 37 | "optional": true, 38 | "os": [ 39 | "darwin" 40 | ], 41 | "engines": { 42 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 43 | } 44 | }, 45 | "node_modules/playwright": { 46 | "version": "1.50.1", 47 | "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.50.1.tgz", 48 | "integrity": "sha512-G8rwsOQJ63XG6BbKj2w5rHeavFjy5zynBA9zsJMMtBoe/Uf757oG12NXz6e6OirF7RCrTVAKFXbLmn1RbL7Qaw==", 49 | "dev": true, 50 | "license": "Apache-2.0", 51 | "dependencies": { 52 | "playwright-core": "1.50.1" 53 | }, 54 | "bin": { 55 | "playwright": "cli.js" 56 | }, 57 | "engines": { 58 | "node": ">=18" 59 | }, 60 | "optionalDependencies": { 61 | "fsevents": "2.3.2" 62 | } 63 | }, 64 | "node_modules/playwright-core": { 65 | "version": "1.50.1", 66 | "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.50.1.tgz", 67 | "integrity": "sha512-ra9fsNWayuYumt+NiM069M6OkcRb1FZSK8bgi66AtpFoWkg2+y0bJSNmkFrWhMbEBbVKC/EruAHH3g0zmtwGmQ==", 68 | "dev": true, 69 | "license": "Apache-2.0", 70 | "bin": { 71 | "playwright-core": "cli.js" 72 | }, 73 | "engines": { 74 | "node": ">=18" 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /javascript/tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "examples-tests", 3 | "version": "0.0.1", 4 | "main": "main.test.js", 5 | "devDependencies": { 6 | "@playwright/test": "^1.50" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /javascript/tests/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from '@playwright/test'; 2 | 3 | export default defineConfig({ 4 | projects: [ 5 | { 6 | name: 'chromium', 7 | use: { ...devices['Desktop Chrome'] }, 8 | }, 9 | { 10 | name: 'firefox', 11 | use: { ...devices['Desktop Firefox'] }, 12 | }, 13 | { 14 | name: 'webkit', 15 | use: { ...devices['Desktop Safari'] }, 16 | }, 17 | ], 18 | webServer: [ 19 | // vite packed 20 | { 21 | command: 'npm --prefix ../examples/vite run build && python3 webserver.py 4151 ../examples/vite/dist', 22 | url: 'http://localhost:4151', 23 | }, 24 | 25 | // webpack packed 26 | { 27 | command: 'npm --prefix ../examples/webpack run build && python3 webserver.py 4152 ../examples/webpack/dist', 28 | url: 'http://localhost:4152', 29 | }, 30 | 31 | // parcel preview 32 | { 33 | command: 'npm --prefix ../examples/parcel run start -- --port 4153', 34 | url: 'http://localhost:4153', 35 | }, 36 | 37 | // parcel packed 38 | { 39 | command: 'npm --prefix ../examples/parcel run build && python3 webserver.py 4154 ../examples/parcel/dist', 40 | url: 'http://localhost:4154', 41 | }, 42 | 43 | // parcel_packageExports preview 44 | { 45 | command: 'npm --prefix ../examples/parcel_packageExports run start -- --port 4155', 46 | url: 'http://localhost:4155', 47 | }, 48 | 49 | // parcel_packageExports packed 50 | { 51 | command: 'npm --prefix ../examples/parcel_packageExports run build && python3 webserver.py 4156 ../examples/parcel_packageExports/dist', 52 | url: 'http://localhost:4156', 53 | }, 54 | 55 | // esbuild packed 56 | { 57 | command: 'npm --prefix ../examples/esbuild run build && python3 webserver.py 4157 ../examples/esbuild/dist', 58 | url: 'http://localhost:4157', 59 | }, 60 | ] 61 | }); 62 | -------------------------------------------------------------------------------- /javascript/tests/webserver.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import http.server 4 | import os 5 | import socketserver 6 | import sys 7 | 8 | port = int(sys.argv[1]) 9 | directory = sys.argv[2] 10 | os.chdir(directory) 11 | Handler = http.server.SimpleHTTPRequestHandler 12 | Handler.extensions_map.update({ 13 | '.wasm': 'application/wasm', 14 | }) 15 | 16 | 17 | socketserver.TCPServer.allow_reuse_address = True 18 | # with socketserver.TCPServer(('127.0.0.1', port), Handler) as httpd: 19 | with socketserver.TCPServer(('0.0.0.0', port), Handler) as httpd: 20 | httpd.allow_reuse_address = True 21 | print('serving at port', port) 22 | httpd.serve_forever() 23 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["maturin>=1.0,<2.0"] 3 | build-backend = "maturin" 4 | 5 | [tool.maturin] 6 | features = ["pyo3/extension-module", "python"] 7 | -------------------------------------------------------------------------------- /python/README.md: -------------------------------------------------------------------------------- 1 | # EVMole Python 2 | 3 | EVMole Python is built with [PyO3](https://pyo3.rs/) for different operating systems and Python versions. In most cases, pip will install a pre-built wheel. However, the source distribution (sdist) is also published, allowing installation on other architectures and Python versions (this is automatically tested). 4 | 5 | ## Installation 6 | To install or upgrade EVMole, use pip: 7 | ```bash 8 | $ pip install evmole --upgrade 9 | ``` 10 | 11 | 12 | ## API 13 | 14 | ### contract\_info 15 | 16 | ```python 17 | def contract_info(code: Union[bytes, str], 18 | *, 19 | selectors: bool = False, 20 | arguments: bool = False, 21 | state_mutability: bool = False, 22 | storage: bool = False, 23 | disassemble: bool = False, 24 | basic_blocks: bool = False, 25 | control_flow_graph: bool = False) -> Contract 26 | ``` 27 | 28 | Extracts information about a smart contract from its EVM bytecode. 29 | 30 | **Arguments**: 31 | 32 | - `code` - Runtime bytecode as a hex string (with or without '0x' prefix) 33 | or raw bytes. 34 | - `selectors` - When True, extracts function selectors. 35 | - `arguments` - When True, extracts function arguments. 36 | - `state_mutability` - When True, extracts function state mutability. 37 | - `storage` - When True, extracts the contract's storage layout. 38 | - `disassemble` - When True, includes disassembled bytecode. 39 | - `basic_blocks` - When True, extracts basic block ranges. 40 | - `control_flow_graph` - When True, builds control flow graph. 41 | 42 | **Returns**: 43 | 44 | - `Contract` - Object containing the requested smart contract information. Fields that 45 | weren't requested to be extracted will be None. 46 | 47 | ### Contract 48 | 49 | ```python 50 | class Contract(): 51 | functions: Optional[List[Function]] 52 | storage: Optional[List[StorageRecord]] 53 | disassembled: Optional[List[Tuple[int, str]]] 54 | basic_blocks: Optional[List[Tuple[int, int]]] 55 | control_flow_graph: Optional[ControlFlowGraph] 56 | ``` 57 | 58 | Contains analyzed information about a smart contract. 59 | 60 | **Attributes**: 61 | 62 | - `functions` - List of detected contract functions. None if no functions were extracted 63 | - `storage` - List of contract storage records. None if storage layout was not extracted 64 | - `disassembled` - List of bytecode instructions, where each element is [offset, instruction]. None if disassembly was not requested 65 | - `basic_blocks` - List of basic block ranges as (first_op, last_op) offsets. None if basic blocks were not requested 66 | - `control_flow_graph` - Control flow graph of the contract. None if control flow analysis was not requested 67 | 68 | ### Function 69 | 70 | ```python 71 | class Function(): 72 | selector: str 73 | bytecode_offset: int 74 | arguments: Optional[str] 75 | state_mutability: Optional[str] 76 | ``` 77 | 78 | Represents a public smart contract function. 79 | 80 | **Attributes**: 81 | 82 | - `selector` - Function selector as a 4-byte hex string without '0x' prefix (e.g., 'aabbccdd'). 83 | - `bytecode_offset` - Starting byte offset within the EVM bytecode for the function body. 84 | - `arguments` - Function argument types in canonical format (e.g., 'uint256,address[]'). 85 | None if arguments were not extracted 86 | - `state_mutability` - Function's state mutability ('pure', 'view', 'payable', or 'nonpayable'). 87 | None if state mutability was not extracted 88 | 89 | ### StorageRecord 90 | 91 | ```python 92 | class StorageRecord(): 93 | slot: str 94 | offset: int 95 | type: str 96 | reads: List[str] 97 | writes: List[str] 98 | ``` 99 | 100 | Represents a storage variable record in a smart contract's storage layout. 101 | 102 | **Attributes**: 103 | 104 | - `slot` - Storage slot number as a hex string (e.g., '0', '1b'). 105 | - `offset` - Byte offset within the storage slot (0-31). 106 | - `type` - Variable type (e.g., 'uint256', 'mapping(address => uint256)', 'bytes32'). 107 | - `reads` - List of function selectors that read from this storage location. 108 | - `writes` - List of function selectors that write to this storage location. 109 | 110 | ### ControlFlowGraph 111 | 112 | ```python 113 | class ControlFlowGraph(): 114 | blocks: List[Block] 115 | ``` 116 | 117 | Represents the control flow graph of the contract bytecode. 118 | 119 | **Attributes**: 120 | 121 | - `blocks` - List of basic blocks in the control flow graph 122 | 123 | ### Block 124 | 125 | ```python 126 | class Block(): 127 | start: int 128 | end: int 129 | btype: BlockType 130 | ``` 131 | 132 | Represents a basic block in the control flow graph. 133 | 134 | **Attributes**: 135 | 136 | - `start` - Byte offset where the block's first opcode begins 137 | - `end` - Byte offset where the block's last opcode begins 138 | - `btype` - Type of the block and its control flow 139 | 140 | 141 | ### BlockType 142 | 143 | ```python 144 | class BlockType(): 145 | class Terminate: 146 | success: bool 147 | 148 | class Jump: 149 | to: int 150 | 151 | class Jumpi: 152 | true_to: int 153 | false_to: int 154 | 155 | class DynamicJump: 156 | to: List[DynamicJump] 157 | 158 | class DynamicJumpi: 159 | true_to: List[DynamicJump] 160 | false_to: int 161 | ``` 162 | 163 | Represents the type of a basic block and its control flow. 164 | 165 | This is an enum-like class, all child classes are derived from `BlockType` class 166 | 167 | #### Terminate 168 | Block terminates execution 169 | - `success` - True for normal termination (STOP/RETURN), False for REVERT/INVALID 170 | 171 | 172 | #### Jump 173 | Block ends with unconditional jump 174 | - `to` - Destination basic block offset 175 | 176 | #### Jumpi 177 | Block ends with conditional jump 178 | - `true_to` - Destination if condition is true 179 | - `false_to` - Destination if condition is false (fall-through) 180 | 181 | #### DynamicJump 182 | Block ends with jump to computed destination 183 | - `to` - Possible computed jump destinations 184 | 185 | #### DynamicJumpi 186 | Block ends with conditional jump to computed destination 187 | - `true_to` - Possible computed jump destinations if true 188 | - `false_to` - Destination if condition is false (fall-through) 189 | 190 | 191 | ### DynamicJump 192 | 193 | ```python 194 | class DynamicJump(): 195 | path: List[int] 196 | to: Optional[int] 197 | ``` 198 | 199 | Represents a dynamic jump destination in the control flow. 200 | 201 | **Attributes**: 202 | 203 | - `path` - Path of basic blocks leading to this jump 204 | - `to` - Target basic block offset if known, None otherwise 205 | 206 | -------------------------------------------------------------------------------- /python/test_python.py: -------------------------------------------------------------------------------- 1 | from evmole import contract_info 2 | from evmole import Contract, Function, StorageRecord 3 | 4 | code = '6080604052348015600e575f80fd5b50600436106026575f3560e01c8063fae7ab8214602a575b5f80fd5b603960353660046062565b6052565b60405163ffffffff909116815260200160405180910390f35b5f605c826001608a565b92915050565b5f602082840312156071575f80fd5b813563ffffffff811681146083575f80fd5b9392505050565b63ffffffff8181168382160190811115605c57634e487b7160e01b5f52601160045260245ffd' 5 | 6 | info = contract_info(code, selectors=True, arguments=True, state_mutability=True, disassemble=True) 7 | assert isinstance(info, Contract) 8 | assert info.functions is not None 9 | assert len(info.functions) == 1 10 | assert isinstance(info.functions[0], Function) 11 | assert info.functions[0].selector == 'fae7ab82' 12 | assert info.functions[0].arguments == 'uint32' 13 | assert info.functions[0].state_mutability == 'pure' 14 | 15 | assert info.disassembled is not None 16 | assert info.disassembled[0] == (0, 'PUSH1 80') 17 | 18 | print(f'Success #1, {info}') 19 | 20 | from evmole import ControlFlowGraph, Block, BlockType, DynamicJump 21 | info = contract_info(code, basic_blocks=True, control_flow_graph=True, selectors=True) 22 | assert isinstance(info.basic_blocks, list) 23 | assert isinstance(info.basic_blocks[0], tuple) 24 | 25 | assert isinstance(info.control_flow_graph, ControlFlowGraph) 26 | assert isinstance(info.control_flow_graph.blocks, list) 27 | assert isinstance(info.control_flow_graph.blocks[0], Block) 28 | 29 | b = info.control_flow_graph.blocks[0] 30 | assert isinstance(b.btype, BlockType) 31 | assert isinstance(b.btype, BlockType.Jumpi) 32 | assert isinstance(b.btype.true_to, int) 33 | 34 | print(f'Success #2, {info}') 35 | -------------------------------------------------------------------------------- /src/arguments/calldata.rs: -------------------------------------------------------------------------------- 1 | use super::Label; 2 | use crate::evm::{calldata::CallData, element::Element, U256, VAL_4, VAL_131072}; 3 | use std::error; 4 | 5 | pub(super) struct CallDataImpl { 6 | pub selector: [u8; 4], 7 | } 8 | 9 | impl CallData