Revm investigation-harness architectureΒΆ
How to assemble an in-process EVM investigation harness (deploy a contract, drive a call, read the result) against a real contract, without re-deriving the revm plumbing every time.
The tl;dr: the only part that was ever hard β deploying pinned bytecode into a
revm CacheDB, seeding storage, driving a call, and telling a Solidity Revert
apart from a verbless Halt β is now a single deep, contract-agnostic module in
the simulation crate. Everything else is a thin per-contract driver you scaffold
at the user layer.
LayeringΒΆ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β USER LAYER β your contract, your investigation β
β Β· hand-written per-contract driver β investigations/<name>/ (standalone)β
β Β· degenbot::investigation β degenbot's OWN path-fixture tool β
βββββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββ
β builds on (thin, per-contract)
βββββββββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββ
β DEEP REUSABLE β degenbot_simulation::oracle β
β deploy β seed slots β drive call β classify (Revert vs Halt) β read β
β Contract-agnostic. No pools, no executor, no backrun. β
βββββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββ
β also the landing zone for the tier-3 oracles
βββββββββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββ
β degenbot-simulation (revm owner) β in-process EVM engine β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Deep reusable lives in the simulation crate: one EVM spine shared by the tier-3 on-chain oracles and any investigation harness.
Scaffolding lives at the user layer: a generator that emits a fresh, standalone per-contract project for your specific use case. Nothing user-specific is baked into the core.
Layer 1 β degenbot_simulation::oracle (the deep driver)ΒΆ
The genuinely reusable primitive. It knows nothing about pools, executors, or
backruns β it only sequences EVM transactions against a fresh self-contained
CacheDB. Add it with cargo add degenbot-simulation.
APIΒΆ
Function |
What it does |
|---|---|
|
Build a pristine revm EVM over an empty |
|
Turn off the nonce check for arbitrary staged txs. |
|
Raise/clear contract+initcode size caps (deploying oversized harnesses, e.g. EIP-170). |
|
Deploy creation bytecode, return its address. |
|
Run a |
|
A call that must succeed; returns the raw output. |
|
Read an address out of a getter that returns a 32-byte word. |
|
Seed raw storage slots ( |
|
|
|
Decode a Solidity |
|
Load a foundry artifactβs creation bytecode. |
The Revert-vs-Halt distinction (write this down)ΒΆ
transact returns a [Verdict] with three cases:
Verdict::Accepted { output, logs }β the call succeeded.Verdict::Reverted(Bytes)β a SolidityREVERT: a math-level verdict you are expected to match against (e.g. a pool rejecting a swap).Verdict::Halted(String)β the EVM halted (OOG / invalid opcode) with no verdict. For oracle work this is a legitimate βstate not computableβ, not a failure of your harness.
Conflating these two is exactly the kind of silent bug this module exists to prevent (ADR-020 H1).
Worked exampleΒΆ
use alloy::primitives::Bytes;
use degenbot_simulation::oracle::{
deploy, new_fixture_evm, selector, set_disable_nonce_check, transact, TxSpec, Verdict,
};
fn probe() -> Result<(), String> {
// Load your contract's creation bytecode (or paste hex).
let init = degenbot_simulation::oracle::load_foundry_creation_bytecode(
std::path::Path::new("tier3-oracle/artifacts"),
"MyContract.sol",
"MyContract",
)?;
let mut evm = new_fixture_evm();
set_disable_nonce_check(&mut evm, true);
let contract = deploy(&mut evm, Bytes::from(init), 16_700_000)?;
println!("deployed @ {contract}");
// Seed raw storage (your layout).
degenbot_simulation::oracle::seed_slots(&mut evm, contract, &[]);
// Drive a call and classify the verdict.
match transact(&mut evm, TxSpec::Call {
to: contract,
data: Bytes::from(selector("swap(bool,int256,uint160)").to_vec()),
gas: 16_700_000,
}) {
Verdict::Accepted { output, .. } => println!("accepted: {output:?}"),
Verdict::Reverted(r) => println!("REVERTED (verdict): {r:?}"),
Verdict::Halted(h) => println!("HALTED (no verdict): {h}"),
}
Ok(())
}
It is the tier-3 oracle landing zoneΒΆ
The driver is not speculative surface β the tier-3 V3 on-chain oracle
(rust/crates/foundation/degenbot-pools/tests/tier3_v3_common/mod.rs::run_onchain_swap) now
drives the real UniswapV3Pool/PancakeSwap bytecode through it, and all 9
byte-exact tests still pass. When you add a new concentrated liquidity math
capability, extend the tier-3 oracle slice per
ADR-020.
Layer 2 β Scaffold your own contract (user layer)ΒΆ
Scaffold it as a thin per-contract standalone project at
investigations/my_executor/ building on degenbot_simulation::oracle: bake in
the contractβs real creation bytecode, seed the storage slots it reads, drive
your call, and classify Revert-vs-Halt. Then:
cd investigations/my_executor
cargo run # after editing src/main.rs for YOUR contract
The scaffold is contract-agnostic by construction: it depends only on
degenbot-simulation + alloy, and contains zero degenbot pool/executor logic.
Use it for your executor, a lending market, a new pool family β anything.
Layer 3 β degenbotβs own path investigationsΒΆ
For reproducing degenbotβs captured failing backrun paths (the
tests/fixtures/path<N>_β¦_block<B>.json files), use the
degenbot::investigation toolkit:
use degenbot::investigation::{PathFixture, register_v2, register_v3, build_v3_state, v3_hop_output};
let fx = PathFixture::load("tests/fixtures/path5000_v2v4v3_block25704509.json")?;
let pid0 = register_v2(&mut engine.core().write(), &fx.pools["v2_0"])?;
let st = build_v3_state(&fx.pools["v3_2"]);
v3_hop_output(&st, fee, spacing, zero_for_one, input); // vs the tier-3-validated twin
This is degenbotβs own pool-level scaffold (its capture format, its V2/V3/V4
reconstruction). It contains no executor logic. Its per-hop oracle checks use the
fast Rust twins (v2_get_amount_out / v3_simulate_swap / v4_simulate_swap),
which the tier-3 oracles prove byte-exact to the real contracts; for a deep
bytecode-level probe, drive the same state through degenbot_simulation::oracle
instead.
Choosing a layerΒΆ
You want to⦠|
Use |
|---|---|
Sequence raw EVM transactions against a contract (any contract) |
|
Investigate a specific new contract you donβt want in the core |
a standalone |
Reproduce a degenbot captured backrun path (V2/V3/V4 hops) |
|
Prove a pool-swap math change against real bytecode |
tier-3 oracle slice (ADR-020) |
Migration notesΒΆ
Donβt hand-roll
CacheDB::new(EmptyDB::default())+Context::mainnet()+transact+commit+ Revert/Halt matching in a new harness β call the driver.Donβt add executor-specific driver logic to the core crates. If a new use case canβt be expressed with the existing driver, extend
degenbot_simulation::oraclewith a generic primitive, and let the user-layer scaffold consume it.When you move a harness onto the driver, the tier-3 tests are the guard: they stay byte-exact or they fail loudly (
bucket=β¦), so a driver change never silently corrupts an oracle.Retired one-off capture scripts (2026-08):
capture_path13822_full_snapshot.pyandcapture_path205_v2v4v3_fixture.pyplus their fixtures (path13822_v3v3v3_block25696004_onchain.json,path205_v2v4v3_block25695845.json) were deleted as dead weight β no live test/example/doc consumed them (verified by whole-treerg+ git history).capture_path13822_v3v3v3_fixture.py(the parameterizedFIX_PATH_IDgenerator for thepath13827fixture) was deleted in the 2026-08-19 sweep (HAVRUW/SEG2PS) together with that fixtureβs last consumer (a run-once example).Deleted one-shot path-debug examples (2026-08-19, HAVRUW/SEG2PS): the 19
rust/crates/facade/degenbot/examples/run-oncepath*/fee1/desync/probe harnesses plus their fixtures and one-off capture/verify/watch scripts β no live test, example, or doc consumed them (verified by whole-treerg; per GLOSSARY.md, ad-hoc path fixtures are weak cross-checks to DELETE once the revm harnesses cover them). Kept:standalone_consumer.rs, thepath5000/path73385fixtures + capture scripts (consumed by committed tier-3 regression tests). The Layer-3 worked example now points at the keptpath5000fixture, and theEXECUTE_CONFIGconstant whose calldata-dump examples went is deleted.Retired the path capture scripts (2026-09-30):
capture_path5000_v2v4v3_fixture.pyandcapture_path73385_v4_fixture.pywere replaced by thecapture_path_fixtureexample (degenbot::investigation::capture), which recaptures every fixtureβs recorded state (DB tick snapshot + on-chain scalars attarget_block) through the typed config β the retired scripts hardcoded the since-removed~/.config/degenbot/degenbot.db. The committed fixtures remain the tier-3 regression input, and their_docstamps keep the recorder names as provenance of what the file records.