Evidence-backed EVM transaction forensics with honest uncertainty.
EVMX takes a transaction hash and returns a verdict you can check — not a score, not a confidence percentage, but a named pattern, the five pieces of evidence that establish it, and their exact positions in the execution trace. And when the evidence does not support a verdict, it says so — and says why.
- What it is
- What it detects, exactly
- Three outcomes, never two
- What it does not claim
- Networks
- Getting started
- Usage
- Architecture
- Project structure
- Reproducibility
- Status
- Documentation
- Roadmap
- Contributing
- Security
- License
- Credits
EVMX is a forensic evidence engine for EVM transactions. It reconstructs the relevant execution evidence from a trace, separates what was observed from what was inferred, and refuses to manufacture certainty when the trace cannot support a verdict.
transaction hash ──► execution trace ──► typed evidence ──► verdict ──► report
It is not a vulnerability scanner, not a "risk score" generator, and not another blockchain explorer. It is a tool that turns a transaction hash into a checked, reproducible, human- and machine-readable report grounded in the execution trace itself.
Two detectors today, both for reentrancy, both defined in terms of trace structure rather than heuristics:
| Detector | Pairing rule |
|---|---|
reentrancy.classic |
the same contract re-entered through the same function |
reentrancy.cross_function |
the same contract and code, re-entered through a different function |
A finding requires six conditions (R1..R6) and produces five pieces of
evidence (E1..E5) — all five or nothing. A finding missing any one of
them is not constructed at all; that is enforced by the type system, not by a
runtime check.
| Evidence | Meaning |
|---|---|
E1 |
the external call — who called whom, with what value |
E2 |
the storage read that preceded it — contract, slot, value |
E3 |
the write that followed — before and after |
E4 |
the re-entry pair, with the full frame lineage between them |
E5 |
the stale read inside the inner frame |
Most tools answer vulnerable or not vulnerable. EVMX has a third answer, and treats it as a first-class result rather than a failure:
| Verdict | Meaning |
|---|---|
PATTERN_CANDIDATE |
the structure is present, with its evidence |
NO_FINDING |
the pattern did not hold — on named grounds |
INCONCLUSIVE |
the evidence does not support a verdict — and here is precisely why |
INCONCLUSIVE is not hedging. It carries a named cause
(SELECTOR_UNAVAILABLE, STALE_READ_UNDETERMINED, …) and, where one exists,
what would lift it — for example: re-capture the trace with memory enabled,
and the selector becomes readable.
This follows one rule applied everywhere in the engine:
"We do not know" is never collapsed into "there is nothing."
These limits are enforced in the output, not buried in a footnote.
NO_FINDINGis not a clean bill of health. One pattern was examined. Other patterns, and vulnerabilities of entirely different classes, are outside what was checked.- A finding is a structure, not a loss. EVMX reports that a read → call → write chain with a stale read occurred. It does not establish that funds moved, or that the contract is exploitable.
- No aggregate verdict exists. Detectors are reported independently. Where they disagree, that disagreement is information; the report type has no field in which a combined score could be stored.
- Provenance is part of the result. Every report states the chain, the endpoint host (never the URL, never a key), and whether the trace came from a live query or recorded evidence.
| Network | Status |
|---|---|
| Ethereum | supported |
| Arbitrum One | supported — after a documented compatibility measurement |
| Base | supported — after a documented compatibility measurement |
Each network is admitted only after its trace shape, storage semantics, frame
semantics, precompile behaviour and payload limits are measured, not assumed.
The Arbitrum measurement is written up in
docs/24, and the Base measurement in
docs/25 — each including what was not
measured, so the boundary stays honest.
Chain identity is verified, never declared: live endpoints are checked with
eth_chainId before a single trace is fetched, and recorded evidence carries its
chain in its manifest so a fixture cannot be opened under the wrong network.
- Rust
1.97.1(seerust-toolchain.toml); the minimum supported Rust version is1.91.0.
The demo ships real, hash-verified mainnet evidence and runs entirely offline — no API key, no live node:
cargo run -p evmx-api --features demo --bin evmx-demo
# then open http://127.0.0.1:8080You can pick the network the demo serves recorded evidence for:
cargo run -p evmx-api --features demo --bin evmx-demo -- arbitrum-one # or: ethereum (default)Running a live analysis requires an endpoint whose RPC exposes the per-step
structLogs this engine consumes — a capability that varies by provider and is
measured by the engine rather than assumed.
The analysis is served over HTTP, and answers the question "give me a tx hash → tell me what the trace shows."
POST /v1/analyze {"chain":"ethereum","tx_hash":"0x…"} ──► application/jsonPOST /v1/report {"chain":"ethereum","tx_hash":"0x…"} ──► text/html
POST /v1/report/ar ╰─► text/html (Arabic)Both routes are one analysis in two representations — a test asserts that every detector name and every reason code present in the JSON also appears in the rendered page. The renderer may add words; it can never drop a verdict.
The HTML report is self-contained: no scripts, no fonts, no external assets. It opens identically a year later, offline.
Note. The current interface is the HTTP demo/report layer plus the engine crates as a library. A dedicated
evmxCLI (analyze,compare,explain,batch) is an explicit roadmap item — see Roadmap.
The engine is a Rust workspace of eight crates, each with a single responsibility and a typed interface:
| Crate | Responsibility |
|---|---|
evmx-core |
decoding, error model, frame identity, canonical JSON |
evmx-trace |
struct-logs → access timeline, frame indexing, labels, frame kinds |
evmx-state |
storage/account state, diffs, provenance, pre-state cross-check |
evmx-detectors |
pattern detectors (reentrancy.classic, reentrancy.cross_function) |
evmx-analysis |
orchestration: trace + state → analysis → report summary |
evmx-report |
rendering: narrative + self-contained HTML (EN/AR) |
evmx-rpc |
chain identity, provider capability/refusal model, live + recorded I/O |
evmx-api |
HTTP server (evmx-demo) exposing /v1/analyze, /v1/report, /v1/report/ar |
A central idea: a detector returns a typed verdict with evidence, and the
verdict type has no field in which an unreachable or unsupported conclusion
could be stored (proven by construction via Rust's type system).
.
├── Cargo.toml # workspace manifest (8 crates)
├── rust-toolchain.toml # pinned, reproducible toolchain
├── crates/
│ ├── evmx-core/ # types, decoding, canonical JSON
│ ├── evmx-trace/ # struct logs → timeline
│ ├── evmx-state/ # storage/account state & provenance
│ ├── evmx-detectors/ # pattern detectors
│ ├── evmx-analysis/ # orchestration + results
│ ├── evmx-report/ # narrative + HTML rendering
│ ├── evmx-rpc/ # provider capability + evidence I/O
│ └── evmx-api/ # HTTP server (evmx-demo)
├── tests/
│ └── fixtures/ # frozen, hash-verified evidence corpus
├── docs/ # design reviews, measurements, decisions
├── tools/ # capture / verify / build-evidence scripts
└── provider-check/ # measured provider capability results
Findings are not reproducible in principle — they are reproducible in fact.
- 19 frozen fixtures, each a real mainnet transaction with its trace hashed and its provenance recorded.
- 382 tests, no network required. Live-network tests exist and are
#[ignore]d by default: a test whose result depends on network conditions tests the network, not the code. - Guards are proven by mutation. Each guard is deliberately broken, the failing tests are recorded, and the file is restored and byte-compared. A guard no test can break is not a guard.
# build
cargo build --workspace
# run the offline test suite (live-network tests are ignore-d by default)
cargo test --workspace
# static analysis
cargo clippy --workspace --all-targetsThe core is closed and tagged. The detector_known_positive gate stands at
2 of 3: two historical incidents are anchored to independent published
sources at the level of the specific contract, variable and values. No third case
in the current corpus meets that bar, and none was manufactured to reach the
number — see docs/23.
That gap is a corpus limit, and it is published rather than papered over. It is also the shortest honest summary of how this project is built.
The repository's internal design log lives in docs/, led by
PROJECT_MAP.md. Highlights:
| Document | Scope |
|---|---|
01-tech-stack |
dependency matrix and rationale |
03-architecture |
crate boundaries, contracts, data models |
05-proof-format |
canonicalization and .evmxproof scheme |
07-threat-model |
trust boundaries and invariants INV-1..INV-11 |
08-fixtures-registry |
the corpus registry as a formal entity |
20-b5-historical-corpus |
the historical-incident corpus |
23-ff-gate-closure |
the detector_known_positive gate |
24-multi-network-arbitrum |
the Arbitrum One measurement |
25-multi-network-base |
the Base measurement |
The project is deliberately sequenced around evidence and market signal, not feature-count:
- Close the detector gate — a genuinely independent third positive case against the published record (never manufactured).
- A dedicated CLI —
evmx analyze,evmx report,evmx batchas a first-class utility that fits inside a researcher's daily workflow. - Provider-aware compatibility layer — publish the measured provider capability model so live tracing is predictable across endpoints.
- Network expansion on demand — Base joined on a measured capability; any further network is added only when there is a commercial request or a measured capability, never for logo count.
Contributions are welcome and are expected to meet the same bar as the code they
touch. Please read CONTRIBUTING.md first — in particular
the rule that "we do not know" is never collapsed into "there is nothing",
which applies to pull requests as much as to verdicts.
The default branch is protected: all changes land through a pull request that requires at least one approving review.
Please report vulnerabilities privately and only after responsible
disclosure — never in a public issue. See SECURITY.md for
the reporting process and supported versions.
Dual-licensed under Apache-2.0 or MIT — see
LICENSE, LICENSE-APACHE, and
LICENSE-MIT.
Author & maintainer: EslaM-X