Verify a Solidity smart contract from EVM runtime bytecode alone — reconstruct source + compiler settings that recompile to byte-identical bytecode, to the Etherscan/Sourcify partial-match standard (every byte matches after stripping the trailing CBOR metadata, which encodes a hash of the original source file and is unrecoverable from bytecode).
Demo: a contract reconstructed and verified with verifyoor —
0xbdd0…9516
(published on Etherscan and Sourcify).
Two layers:
- Deterministic toolkit (
verifyoor …) — all the mechanical work, as a pipeline:- Parse & analyze — CBOR metadata; dispatcher walk with trampoline body-offset tracing; evmole-primary selector / argument-type / state-mutability / storage-layout extraction (the dispatcher walk is the fallback); Sourcify 4byte name resolution (rehash-verified, verified-contract names ranked first).
- Lift — an intra-block IR lift: per-basic-block symbolic stack execution
into Yul-style statements, deterministic and complete for EVM bytecode, with
inter-block edges (including context-sensitive dynamic jumps) resolved from
evmole's CFG. Surfaced standalone via
liftand inline in every diff region. - Compile & compare — pinned-solc compilation with a settings sweep, masked byte-exact comparison.
- Diff — an offset-stable normalized-disassembly diff, run per function:
both images are partitioned by the owning function's selector (via
context-sensitive CFG analysis) and diffed bucket-by-bucket, so a length
change in one function can't desync or misattribute regions in another. A
divergence in genuinely shared codegen reads
shared helper; the dispatcher is its own bucket. (Falls back to a single global pass when a CFG isn't available for both sides.) A small pattern recognizer attaches ahintto regions matching a known source→bytecode signature (selector-encoding cast, unchecked-vs-checked increment, custom-error vsrequire), naming the fix. - Brute-force the last mile — a source-variant sweep (
sweep: expand a templated candidate's<<< a ||| b >>>markers across all variants × settings, ranked by diff-region count, until byte-exact) and a library-version sweep (identify-library: for contracts built on a known OSS library like OpenZeppelin, sweep its published versions × settings scored by basic-block fingerprint overlap against the target, to recover large verbatim chunks and pin the compiler settings before hand-authoring the rest). - Selector minting — mint a function name for an exact selector when the real name is unrecoverable (a collision or a custom name).
- Creation-code check — the loop matches runtime bytecode;
analyze --creation/verify --creationalso fetch the deploy tx and diff the constructor init-code (which never appears in runtime) so a runtime match doesn't fail Etherscan's creation-code verification (see Publishing below). - Inspect —
disasm(a pc-range disassembly) anddiff-asm(the same offset-stable diff over an arbitrary candidate hex) for pinpointing a single divergent instruction by hand.
- Claude Code skill (
.claude/skills/verify/SKILL.md, invoke as/verify <network> <address>) — Claude is the reconstruction engine, authoring the Solidity and refining it against the toolkit's diff feedback in an author→compile→diff loop until the toolkit reports a match.
The loop converges because solc is deterministic and its codegen maps source structure to bytecode directly; the normalized diff makes each iteration's "what still differs" legible even when a one-line change shifts every jump target.
- uv (manages the Python environment).
- foundry (
cast) onPATH. - solc binaries under
~/.svm/<version>/solc-<version>(orsolc-select) for the versions you target;verifyoorauto-installs a missing one viasolc-selectwhen possible.
evmole is a core Python dependency (installed by
uv sync) — it's the primary source for selectors, argument types, and state
mutability in analyze. If it errors on a given input, analysis falls back to the
built-in dispatcher walk.
uv sync # create the .venv and install deps (pytest, pycryptodome, evmole)Everything then runs through uv run (no manual venv activation needed). The
one Python dependency is a keccak-256 backend (pycryptodome) — Python's stdlib
ships NIST SHA3, which is not Ethereum's keccak.
The bytecode source is <network> <address> (fetched via eth_getCode) or a
single local hex file / hex string:
uv run verifyoor analyze ethereum 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
uv run verifyoor lift <network> <address> --out runs/<name>/lift.txt
uv run verifyoor mine-selector 0x2247831f "(address[],uint256[])"
uv run verifyoor verify <src.sol> <network> <address> --sweep --out runs/<name>
uv run verifyoor sweep <template.sol> <network> <address> --out runs/<name>
uv run verifyoor identify-library <probe.sol> <network> <address> \
--package @openzeppelin/contracts --path access/Ownable2Step.sol --contract Probe
uv run verifyoor disasm <network> <address> --range 0x5ae-0x8f2
uv run verifyoor diff-asm <network> <address> --candidate mine.hex --function "transfer(address,uint256)"
# local bytecode works in place of <network> <address>:
uv run verifyoor analyze tests/fixtures/sample.hex
uv run verifyoor verify tests/fixtures/sample.sol tests/fixtures/sample.hex --sweepdisasm prints pc: OPCODE imm lines (optionally a --range), and diff-asm
runs the same offset-stable, function-attributed diff verify uses but over an
arbitrary candidate hex — the quick way to localize a single divergent instruction
between two bytecodes (scope with --function/--range).
<network> is an alias (ethereum, base, arbitrum, optimism, polygon,
bsc, sepolia, …) or a full RPC URL; --rpc-url overrides it, and
VERIFYOOR_RPC_<NETWORK> is honored. Fetched code is cached under
~/.cache/verifyoor/bytecode.
verify exits 0 on match (writing source.sol, settings.json,
standard-input.json, report.{json,md} to the run dir), 1 on mismatch (printing
a function-attributed diff for the next iteration).
On a match, verifyoor writes standard-input.json — the solc Standard-JSON-Input
with the evmVersion (and optimizer/viaIR) embedded. Use standard-json, not
single-file/flatten: the flatten path lets the verifier default the evmVersion,
which silently fails for any contract built for a non-default target (e.g. a
paris build on a 0.8.26 contract whose default is cancun).
uv run verifyoor submit runs/<name> <network> <address> --verifier bothSubmits that standard-json to Etherscan ($ETHERSCAN_API_KEY or --api-key)
and/or Sourcify (--verifier etherscan|sourcify|both) and polls to completion.
verify matches the runtime bytecode (eth_getCode). Sourcify verifies on
runtime, so a runtime match publishes there directly. Etherscan verifies the
creation bytecode — init ++ runtime ++ constructor_args — whose constructor
init segment is not present in runtime at all. So a byte-perfect runtime match
can still be rejected by Etherscan if the reconstructed constructor differs (a
missing storage init, an event, a different msg.sender write) — none of which a
runtime-only analysis can see.
Two commands close that gap (both fetch the deploy tx via Etherscan's
getcontractcreation, so they need a key):
# report what the real constructor does (storage writes never seen in runtime):
uv run verifyoor analyze <network> <address> --creation
# constructor: init 99 bytes, 0 byte(s) of constructor args
# storage writes: slot 0 = caller(), slot 1 = 0x1 <- e.g. a reentrancy-guard init
# on a runtime match, also diff the constructor init-code against the deploy tx:
uv run verifyoor verify <src.sol> <network> <address> --creation --out runs/<name>
# == creation: ❌ constructor init-code DIFFERS ==
# → add to your constructor: slot 1 = 0x1submit also self-diagnoses: when Etherscan rejects with "deployment bytecode does
NOT match", it recompiles, fetches the creation code, and prints the constructor
divergence instead of dead-ending.
Pass --constructor-args <hex> if the contract has a constructor with arguments.
A function's name never appears in runtime bytecode — solc's dispatcher carries
only the 4-byte selector keccak(name+argtypes)[:4]. So when a function's real
name is unrecoverable (a custom name absent from signature DBs, or a selector
collision where the DB resolves the wrong preimage — e.g. a 2-array batch
function that happens to share transfer's 0xa9059cbb), you don't need the
name. Take the arg types from analyze (evmole; they drive the body's codegen
and must be right — cross-check the lift IR when in doubt), then mint any name
that hashes to the selector:
uv run verifyoor mine-selector 0x2247831f "(address[],uint256[])"
# -> func_2247831f_<n>(address[],uint256[]) (selector == 0x2247831f, re-verified)The minted func_<selector>_<n> name is cosmetic; only the selector lands in
bytecode, so the compiled runtime is byte-identical. Mining is a ~2³² keccak
search with two backends (auto-selected; every result re-verified with keccak):
- external (fast) — Vectorized's
function-selector-miner(MIT; Rust, AVX2 + multithread), sub-minute on an x86+AVX2 host. Build it (cargo build --release) and either putfunction-selector-mineronPATHor pointVERIFYOOR_SELECTOR_MINERat the binary. On non-AVX2 hosts (e.g. Apple Silicon) it falls back to a scalar path, ~on par with the Python backend. - python (fallback) — pure-Python multiprocessing, no build step; always
present. Force it with
--python.
Byte-identical after: stripping trailing CBOR metadata from both sides; masking immutable value slots (recovered and reported); masking library link placeholders (recovered); masking embedded child-contract metadata (factory contracts).
uv run python tests/fixtures/generate_fixtures.py # (re)generate fixtures
uv run pytest # 96 tests (2 gate on the external miner)The suite covers metadata parsing, the dispatcher walk (EQ/SUB forms, trampoline
body-offset tracing), string extraction across all three solc encodings, the
offset-stable diff, the intra-block IR lift (stack semantics, let/temp policy,
block splitting) with CFG edge resolution, context-sensitive CFG attribution,
template expansion for the variant sweep, library import resolution and
basic-block fingerprinting (network calls injected, not live in tests), selector
minting (both backends), evmole enrichment (arg types, mutability, storage
layout), immutable masking, viaIR detection, and round-trips
of fixtures exercising structs, mappings, events, custom errors, immutables,
optimizer-on, viaIR, and solc 0.7.6. Compile-dependent tests need the matching
solc binaries; network-dependent name resolution is cached (--offline to skip).
MIT — see LICENSE.