The Guardian of the Chain: Under the Hood (Part 2/3)

TL;DR

  • Stack: Rust workspace with crates for invariant spec, Ethereum state, evaluation engine, case-study replay, and run storage.
  • CLI: exploit-analyzer—seed, list, show, analyze, replay—drives replay and writes Markdown + JSON artifacts.
  • Reliability: Provider pooling and reorg-safe indexing (finality depth) so replays stay correct when RPC or chain forks.
  • Config: JSON for invariants (no custom DSL); supports types like reserve_ratio_min, health_factor_min, and others.

Table of Contents

Introduction

Hook: Replaying historical exploits and evaluating invariants block-by-block means dealing with flaky RPCs, reorgs, and large state. The design has to be reliable and reproducible.

Context: In Part 1 we introduced the exploit-analyzer CLI and why invariant-based replay matters. Here we open the hood: the Rust crates, how config flows into evaluation, and how the system handles provider failures and chain reorganizations.

Preview: This post walks through the architecture (invariant spec, eth-state, invariant-eval, case-studies, run-store), the choice of JSON for config, and runnable commands so you can try the workflow yourself.

Background

Replay-based analysis has to:

  • Fetch state at specific blocks via RPC (reserves, liabilities, prices, etc.).
  • Evaluate invariants consistently and record the first violation block.
  • Handle real-world chaos: RPC rate limits, node outages, and chain reorgs.

The project uses Rust for performance, type safety, and async RPC handling. The architecture doc explicitly favors no auto-pause in the core tool—reducing liability and keeping the CLI focused on analysis; optional Guardian contracts exist separately for teams that want on-chain pause capability.

Architecture: Crates and Data Flow

The repo is a Rust workspace. The exploit-analyzer binary and supporting crates are under monitor/. Key components (from the architecture docs):

Core Crates

CrateRole
invariant-specJSON schema and parsing for invariant configs. Supports types such as reserve_ratio_min, health_factor_min, price_deviation_max, erc20_supply_matches_sum_balances.
eth-stateRPC layer: block-pinned state, multicall batching, provider pooling (multi-provider failover, health checks, retries), reorg-safe indexing, and finality depth.
invariant-evalEvaluation engine: load specs, fetch contract state, evaluate invariants, emit violation events.
case-studiesHistorical exploit replay: fork at given blocks, block-by-block evaluation, report generation.
run-storePersistence for runs and results (e.g. SQLite).

Data Flow

JSON config → invariant-spec (parse) → invariant-eval (evaluate per block)

Ethereum RPC ← eth-state (fetch state, handle reorgs / finality)

            Violation events → Reports (Markdown) + JSON artifacts

The exploit-analyzer CLI wires these together: it loads config, runs the replay (or case-study analysis), and writes the output directory (report, violations, run metadata).

Reliability: Provider Pooling

A single RPC URL is a single point of failure: outages, rate limits, or lag can break a long replay.

What the Repo Does

eth-state implements provider pooling:

  • Multiple RPC endpoints (e.g. Alchemy, Infura, or your own node).
  • Health tracking and automatic failover.
  • Retries with backoff and rotation.

So when you run analyze or replay, the same design that would power an always-on daemon makes your CLI runs resilient to one provider failing.

Example: Passing RPC URL

You supply at least one RPC URL; more can be configured where supported:

export RPC_URL="https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY"

cargo run --bin exploit-analyzer -- analyze \
  --exploit-id "<EXPLOIT_ID>" \
  --config config/case-studies/euler.json \
  --rpc-url "$RPC_URL" \
  --output output/euler-analysis

Use a reliable mainnet endpoint (e.g. Alchemy or Infura) to avoid mid-run failures.

Reorg-Safe Replay and Finality Depth

Blockchains fork and reorg. A block that looks canonical now might be replaced later. For replay, you want to either run over already-final blocks or rely on logic that tracks the canonical chain and finality.

What the Repo Does

The architecture describes:

  • Reorg-safe indexing: Canonical chain tracking and rollback so that reorgs don’t produce wrong violation timelines.
  • Finality depth: Only treat blocks as “final” after a configurable number of confirmations (e.g. 12), which avoids false violations on short-lived forks.

So when you analyze a historical exploit, the system is built to respect chain reorganizations and finality rather than naively trusting the first block you see.

Config Snippet (Finality)

Where applicable, finality can be configured so that only blocks beyond a certain depth are considered:

{
  "finality_depth": 12
}

(Exact key names and location depend on the config schema in the repo; see config/case-studies/euler.json or the docs.)

JSON over DSL

Invariants are defined in JSON, not a custom DSL.

Why JSON?

  • Universal: Easy to edit, version, and share.
  • Tooling: Standard parsers and validation.
  • No new syntax: Anyone who can read a contract ABI can adjust configs.

Trade-off: less expressive than a full DSL, but sufficient for the supported invariant types.

Example Invariant Config

Conceptually, a reserve-ratio invariant looks like this (align with actual schema in the repo):

{
  "type": "reserve_ratio_min",
  "params": {
    "contract": "0xProtocol...",
    "min_ratio": 1.0,
    "reserves_method": "getReserves()",
    "liabilities_method": "getLiabilities()"
  },
  "severity": "CRITICAL"
}

The exploit-analyzer reads such configs from files like config/case-studies/euler.json and uses them during analyze and replay.

Runnable Workflow Examples

All commands assume you’re in the repo root and have built the project (cd monitor && cargo build --release).

1. Seed and list exploits

cd monitor

# Seed the exploit database (includes Euler Finance)
cargo run --bin exploit-analyzer -- seed

# List available exploits and note the exploit ID for Euler
cargo run --bin exploit-analyzer -- list

2. Run Euler analysis

# Set your RPC URL (required)
export RPC_URL="https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY"

# Replace <EXPLOIT_ID> with the ID from `list`
cargo run --bin exploit-analyzer -- analyze \
  --exploit-id "<EXPLOIT_ID>" \
  --config config/case-studies/euler.json \
  --rpc-url "$RPC_URL" \
  --blocks-before 100 \
  --blocks-after 10 \
  --output output/euler-analysis

3. Replay a custom block range

cargo run --bin exploit-analyzer -- replay \
  --config config/invariants.json \
  --rpc-url "$RPC_URL" \
  --start-block 18000000 \
  --end-block 18000100 \
  --output output/custom-replay

4. Inspect outputs

ls output/euler-analysis/
cat output/euler-analysis/Euler-Finance-*/report.md
cat output/euler-analysis/Euler-Finance-*/violations.json
cat output/euler-analysis/Euler-Finance-*/run-metadata.json

Common Pitfalls to Avoid

Pitfall 1: Single RPC provider

What goes wrong: One endpoint goes down or gets rate-limited mid-replay; the run fails or stalls.

How to avoid it: Use a stable RPC URL; prefer providers with high rate limits. The codebase is designed for multiple providers and failover—configure them where the tool supports it.

Pitfall 2: Ignoring finality and reorgs

What goes wrong: You treat violations on very recent blocks as definitive, but the chain can reorg.

How to avoid it: For historical analysis, use block ranges that are already final. Rely on the built-in finality depth and reorg handling when running the analyzer.

Pitfall 3: Wrong or missing exploit ID

What goes wrong: analyze fails or analyzes the wrong exploit because the ID is missing or incorrect.

How to avoid it: Always run cargo run --bin exploit-analyzer -- list first and use the exact exploit ID (e.g. for Euler) in --exploit-id.

Conclusion

Summary: The Guardian project’s under-the-hood story is a Rust workspace built for reliable, replay-based invariant analysis: clear crates (invariant-spec, eth-state, invariant-eval, case-studies, run-store), JSON config, provider pooling, and reorg-safe behavior with finality depth. The exploit-analyzer CLI ties this together for seed/list/analyze/replay and produces Markdown reports and JSON artifacts.

Key takeaways:

  • Crates separate spec parsing, RPC/state, evaluation, case-study replay, and persistence.
  • Provider pooling and reorg-safe indexing make replays robust to RPC and chain chaos.
  • JSON config keeps invariants easy to define and share without a custom DSL.

Call to action:

Additional Resources


Tags: rust, architecture, blockchain, defi, systems-design