Skip to content

Apostlex0/Axios

Repository files navigation

OEV Liquidation Pipeline

A Chainlink CRE-powered, auction-based Oracle Extractable Value capture system for DeFi lending protocols.

This system replaces chaotic MEV gas wars with a deterministic, two-phase auction that fairly allocates liquidation rights, ensures protocol solvency, and distributes captured OEV between competitive liquidators and the protocol treasury. The architecture spans on-chain smart contracts, a multi-service offchain coordination layer, a Python agent fallback network, and a Rust terminal interface — all wired together through a commit-reveal oracle feed and a public bid endpoint.


Demo Proof (Base Sepolia)

Liquidation Transactions (Shown in Demo)

  1. Auction winner execution (testnet-sim-liquidator path): https://sepolia.basescan.org/tx/0x1db3b8faeacdf0ae6a731b5c6c2e21fc1f41cc3cd496cb241b4bab7d284f5afe
  2. Fallback execution via ERC-8004 registered fallback agent: https://sepolia.basescan.org/tx/0x41f607daef6d3624952659cd732843f03c88576ed1dde7bba2d828ba3255c594

TUI Screenshots from Demo

TUI Demo 1 TUI Demo 2 TUI Demo 3 TUI Demo 4 TUI Demo 5

Deployment Addresses (from deployments.base-sepolia.json)

Network: base-sepolia
Chain ID: 84532

Contract Address
SourcePriceOracle 0x3E092C1D36cC779ABAcD40Df0e52b5d4fFbA4Abd
SVRFeed 0x950eb14c49d2fa4fb7d146f2886704b8e3f02b82
LendingMarket 0xb40e2240335e2423a33b0e4067f6144266ab1d7f
LiquidationExecutor 0xbb07962898a13587af84cbbfcb25c6808bac19ca
SwapRouter 0x5b8dc9759c8c128951cc7cdbf9ee469c48607858
Treasury 0x04dce57655a0ced6f5f356be684fbb8daa02e973
AgentIdentityRegistry 0x8004A818BFB912233c491871b3d84c89A494BD9e
CollateralToken 0x7f7a2f064309a783e8f4e7894bd3a53fd86d1cb0
DebtToken 0xd6162b3bac2873eb8fb5b40cc513f3d907aba4db
EOA Role Address
deployer 0x8a94fdF8D895B0DA01c8649559e2E5e7c03d2CCf
updater 0x8a94fdF8D895B0DA01c8649559e2E5e7c03d2CCf

Table of Contents


1. What is OEV and Why It Matters

Oracle Extractable Value is a subset of MEV that arises specifically from oracle price updates. In a lending protocol, the moment an oracle publishes a price that makes a borrower's health factor drop below 1.0, a liquidation becomes profitable. In the absence of any coordination layer, every bot on the network sees this opportunity simultaneously and competes for it through priority gas auctions — a zero-sum game where most of the extracted value flows to block producers rather than back to the protocol.

This system captures that value at the source. By wrapping the oracle update itself inside a commit-reveal round, and auctioning off the exclusive right to execute the liquidation before the price is revealed, it converts an unstructured race into a sealed-bid auction. The winning bid represents the liquidator's willingness to pay for exclusivity. That payment flows to the protocol treasury. The liquidator still profits from the liquidation bonus. The borrower's position is resolved before it can accumulate bad debt. Every participant in the protocol benefits compared to the MEV alternative.

The auction is public. Any participant that can construct a valid signed liquidation transaction and submit it to the /bid endpoint before the bid window closes is eligible to win. The liquidators in this repository participate automatically via server-sent events, but they hold no structural advantage over external bidders who satisfy the same validation rules.


2. The Core Design: Two-Phase Price Architecture

The single most important design decision in this system is the deliberate separation between the price used for candidate detection and the price used for liquidation execution. Understanding this separation is necessary to understand every other component.

Timeline ──────────────────────────────────────────────────────────────────►

Block N                          Block N+K (Reveal Block)
  │                                    │
  │  SourcePriceOracle.updatePrice()   │  SVRFeed.reveal(price, salt)
  │  emits SourcePriceUpdated          │  LendingMarket price updated
  │                                    │
  ▼                                    ▼
╔══════════════════════════════╗    ╔══════════════════════════════════════╗
║  PHASE 1: DETECTION          ║    ║  PHASE 2: EXECUTION                  ║
║  ─────────────────────────── ║    ║  ──────────────────────────────────  ║
║  Price: Source trigger price ║    ║  Price: SVRFeed revealed price       ║
║                              ║ ── ║                                      ║
║  CRE reads source event      ║    ║  LendingMarket.liquidate() runs      ║
║  Indexer screens candidates  ║    ║  Health factor checked at THIS price ║
║  Coordinator opens round     ║    ║  Winner tx broadcast                 ║
║  priceHash committed onchain ║    ║  Profit settled atomically           ║
║  Auction bid window open     ║    ║                                      ║
╚══════════════════════════════╝    ╚══════════════════════════════════════╝
         │                                    │
         │  Exclusivity window active         │
         │  Only LiquidationExecutor          │
         │  can liquidate                     │
         └────────────────────────────────────┘

Phase 1 uses the source oracle event as an early signal. The lending market price has not changed yet — SVRFeed still holds the previous value. CRE and the indexer use the source price purely for candidate screening: would this borrower become liquidatable if the feed updates to this price? This pre-selection determines whether a round is worth opening.

Phase 2 is where economic reality settles. At the reveal block, SVRFeed.reveal() pushes the new price into the lending market. All liquidation health-factor checks run against this revealed price. A liquidator who won the auction may find that the revealed price produced slightly different economics than the source price suggested — this is expected and is factored into rational bid computation.

This design means the system never speculatively executes liquidations. It only acts when an oracle update makes positions genuinely unhealthy, and the auction ensures that when it does act, the right participant executes at the right price.


3. System Architecture

The system is composed of five layers: on-chain contracts, the offchain coordination stack, the CRE trigger service, the agent fallback network, and the Rust TUI. The diagram below shows how these layers interconnect at runtime.

graph TD
    subgraph Blockchain["On-Chain"]
        SPO["SourcePriceOracle\n─────────────────\nupdatePrice()\nemits SourcePriceUpdated"]
        SVR["SVRFeed\n─────────────────\ncommit(priceHash)\nreveal(price, salt)\nlatestRoundData()"]
        LM["LendingMarket\n─────────────────\nonPriceCommit()\nliquidate()\nhealth factor checks"]
        LE["LiquidationExecutor\n─────────────────\npull debt\ncall market liquidate\nswap collateral"]
        SR["SwapRouter\n─────────────────\nroute collateral swap\nreturn proceeds"]
        AIR["AgentIdentityRegistry\n─────────────────\nERC-8004\nRegistered / URIUpdated"]
    end

    subgraph Offchain["Offchain Coordination Stack"]
        CRE["CRE\n─────────────────\nonSourcePriceLog()\nstale-position cron\nguardian emergency mode"]
        COORD["Coordinator\n─────────────────\nround state machine\nauction controller\nbid validator\nbackstop activator"]
        IDX["Indexer\n─────────────────\nevent sourcing\ncandidate math\nposition tracking\nstate persistence"]
        LIQ1["Liquidator A\n─────────────────\nSSE consumer\nprofit solver\nbid submitter"]
        LIQ2["Liquidator B"]
        LIQ3["Liquidator N"]
        RW["Registry Watcher\n─────────────────\nERC-8004 events\nagentDirectory.json\nhealth prober"]
        BS["Backstop\n─────────────────\nfilter active agents\nrank by priority\nstandby + activate"]
    end

    subgraph Agents["Agent Fallback Network"]
        AG1["Liquidation Agent\n─────────────────\n/standby  /activate\nliquidate_borrower tool"]
        AG2["Liquidation Agent"]
        AGN["Liquidation Agent N"]
    end

    subgraph TUI["TUI Monitor (Rust)"]
        DASH["Dashboard\nservice health\nlatest round"]
        ROUNDS["Rounds\ncoordinator state"]
        POS["Positions\nborrower states"]
    end

    SPO -->|"SourcePriceUpdated event"| CRE
    CRE -->|"/candidates?price1e8=..."| IDX
    CRE -->|"/cre/start-round"| COORD
    COORD -->|"SVRFeed.commit(priceHash)"| SVR
    COORD -->|"LendingMarket.onPriceCommit()"| LM
    SVR -->|"latestRoundData()"| LM
    COORD -->|"SSE /auction/stream"| LIQ1
    COORD -->|"SSE /auction/stream"| LIQ2
    COORD -->|"SSE /auction/stream"| LIQ3
    LIQ1 -->|"/bid"| COORD
    LIQ2 -->|"/bid"| COORD
    LIQ3 -->|"/bid"| COORD
    COORD -->|"SVRFeed.reveal()"| SVR
    COORD -->|"broadcast winner tx"| LE
    LE -->|"LendingMarket.liquidate()"| LM
    LE -->|"swap collateral"| SR
    COORD -->|"activate backstop"| BS
    AIR -->|"Registered / URIUpdated"| RW
    RW -->|"agentDirectory.json"| BS
    BS -->|"/standby + /activate"| AG1
    BS -->|"/standby + /activate"| AG2
    BS -->|"/standby + /activate"| AGN
    AG1 -->|"LendingMarket.liquidate()"| LM
    COORD -->|"rounds API"| ROUNDS
    IDX -->|"/positions"| POS
    COORD -->|"health API"| DASH

    style Blockchain fill:#0d1117,stroke:#30363d,color:#e6edf3
    style Offchain fill:#0d1117,stroke:#30363d,color:#e6edf3
    style Agents fill:#0d1117,stroke:#30363d,color:#e6edf3
    style TUI fill:#0d1117,stroke:#30363d,color:#e6edf3
Loading

Service composition and default ports are defined in offchain_stack.sh. Each service is independently deployable and communicates exclusively over HTTP — no shared in-process state between the coordinator, indexer, liquidators, and registry watcher.


4. End-to-End Liquidation Flow

The following sequence diagram traces a complete liquidation from oracle price event to profit settlement, covering the nominal path (auction winner succeeds) and indicating where the fallback branch begins.

sequenceDiagram
    autonumber
    participant Oracle as SourcePriceOracle
    participant CRE as CRE
    participant IDX as Indexer
    participant COORD as Coordinator
    participant SVR as SVRFeed
    participant LM as LendingMarket
    participant LIQA as Liquidator A
    participant LIQB as Liquidator B
    participant LE as LiquidationExecutor
    participant SR as SwapRouter
    participant BS as Backstop

    Oracle->>CRE: SourcePriceUpdated(price1e8)
    CRE->>IDX: GET /candidates?price1e8=...
    IDX-->>CRE: [candidate borrowers] or []
    Note over CRE: Empty candidates → stop, no round

    CRE->>COORD: POST /cre/start-round {price1e8, sourceRoundKey}
    Note over COORD: Dedupe by sourceRoundKey<br/>assertNoActiveRound (local + onchain)<br/>Create salt + priceHash<br/>Classify risk: normal/stress/emergency<br/>Warm backstop standby

    COORD->>IDX: POST /pending-price
    COORD->>SVR: commit(priceHash)
    COORD->>LM: onPriceCommit()
    Note over LM: Exclusivity armed<br/>Only LiquidationExecutor<br/>can liquidate

    COORD-->>LIQA: SSE: auction frame {roundId, candidates, revealBlock}
    COORD-->>LIQB: SSE: auction frame {roundId, candidates, revealBlock}

    Note over LIQA,LIQB: Each liquidator:<br/>compute profit per candidate<br/>build signed LiquidationExecutor tx<br/>sign bid hash binding round+reveal+tx

    LIQA->>COORD: POST /bid {signedTx, bidHash, bidAmount, roundId}
    LIQB->>COORD: POST /bid {signedTx, bidHash, bidAmount, roundId}

    Note over COORD: Validate: tx targets LiquidationExecutor<br/>chainId matches, signature binds<br/>round params match

    Note over COORD: BID_WINDOW_MS expires<br/>Pick highest valid bid → Liquidator A wins

    Note over COORD: Wait for reveal block N+K

    COORD->>SVR: reveal(price, salt)
    SVR->>LM: update latestRoundData()

    COORD->>LE: broadcast winner signed tx

    LE->>LM: liquidate(borrower, debtAmount, collateral)
    Note over LM: Check exclusivity window<br/>Check health factor at revealed price<br/>Apply close factor + liquidation bonus<br/>Protocol cut → treasury

    LM->>LE: seize collateral tokens
    LE->>SR: swap(collateral → debtAsset)
    SR-->>LE: proceeds
    LE-->>LIQA: repay debt + profit

    Note over COORD: Winner path complete → round marked done

    rect rgb(40, 20, 20)
        Note over COORD,BS: FALLBACK PATH (winner fails or no bids)
        COORD->>BS: activate backstop
        BS->>IDX: GET /positions (fresh state)
        BS->>BS: filter active+fresh agents<br/>rank by locality/priority
        BS->>BS: parallel race: /standby then /activate
        Note over BS: First agent to complete wins
    end
Loading

5. Service Layer — Component Reference

5.1 CRE Orchestrator

The CRE (Chainlink Runtime Environment) is the entry point for all liquidation activity. It operates two modes concurrently: an event-driven path triggered by oracle price updates, and a cron-driven path that sweeps for stale positions that may have become liquidatable without a corresponding price event.

Event-driven trigger pipeline (workflow.ts:65):

SourcePriceOracle.updatePrice()
        │
        ▼ emits SourcePriceUpdated(price1e8)
onSourcePriceLog() decodes log
        │
        ▼
GET /candidates?price1e8={source_price}
        │
   ┌────┴────┐
   │ empty   │ candidates exist
   ▼         ▼
  stop    POST /cre/start-round
          { price1e8, sourceRoundKey }

The sourceRoundKey is a deterministic identifier derived from the source round, used by the coordinator for deduplication. If a network partition causes the same oracle event to trigger CRE twice, the coordinator will reject the second start-round call.

Stale-position cron (workflow.ts:339): CRE independently polls the indexer for positions that have drifted into liquidatable territory without a price event — for example, due to interest accrual increasing a borrower's debt. This ensures the system catches liquidations that the event-driven path would miss.

Guardian emergency mode (workflow.ts:176): CRE can be configured to escalate to a guardian contract under systemic stress conditions, providing a circuit-breaker for cascading liquidation scenarios.

Local simulation entrypoint (cre/source-oracle-oev/main.ts): A stripped-down runner that executes only initSourceWorkflow for development and testing without the full workflow handler chain.


5.2 Coordinator and Round State Machine

The coordinator owns the entire auction lifecycle. It is the single source of truth for round state and enforces ordering invariants both locally (in-memory + coordinator-state.json) and onchain (via pendingRoundId and pendingExclusiveUntilBlock).

Round state transitions:

5.2 Coordinator and Round State Machine

Single active round enforcement: The coordinator rejects any /cre/start-round call if pendingRoundId is set either locally or onchain. This prevents two simultaneous auction rounds from competing for the same lending market state.

Risk classification (coordinator.ts:65): Rounds are classified as normal, stress, or emergency based on the number of candidates and the magnitude of the price move. Higher risk classification triggers earlier backstop warming and tighter bid window handling.

Bid window: After BID_WINDOW_MS elapses post-commit, the coordinator locks the bid pool. It selects the highest bid whose validation passes all checks (see section 5.4). It then waits for the reveal block before proceeding to execution.

Persistence: All round state and bids are written to data/coordinator-state.json atomically after each state transition. On restart, the coordinator restores in-progress rounds and resumes from the correct state.


5.3 Indexer — Position Intelligence

The indexer is an event-sourced position database. It replays all LendingMarket events from genesis (or its last checkpoint) to maintain an accurate map of every borrower's health factor at any given price.

Event sources (indexer.ts:232):

Event Effect
Supplied Increase collateral for user
Withdrawn Decrease collateral for user
Borrowed Increase debt for user, add to knownUsers
Repaid Decrease debt for user
Liquidated Reduce both debt and collateral, update firstSeenLiquidatable

Candidate computation (indexer.ts:179, liquidation.ts:41):

The /candidates endpoint takes a price1e8 parameter (the source trigger price) and returns all borrowers who would have a health factor below 1.0 at that price, along with the estimated liquidation parameters:

For each known user:
  Compute HF at price1e8
  If HF < 1.0:
    Apply close factor → max repayable debt
    Apply liquidation bonus → seizable collateral
    Estimate net profit
    Include in candidate list if profit > threshold

This computation uses the pending/source price before the SVRFeed has been revealed, so it is intentionally approximate. The liquidator's profit solver performs a more precise calculation before bidding.

Pending price tracking (indexer.ts:299): When the coordinator opens a round, it pushes the pending price to /pending-price. The indexer stores this alongside the round ID so that subsequent candidate queries during an open round are tied to the correct price context.

State persistence (indexer.ts:49): Full position state is written to data/indexer-state.json on each update. On restart, the indexer loads this checkpoint and replays only the events it missed while offline, rather than re-indexing from genesis.

Key endpoints:

Endpoint Description
GET /candidates?price1e8= Borrowers liquidatable at given price
GET /positions All tracked borrower positions
GET /stale Positions not updated within staleness threshold
POST /pending-price Set pending round price context
POST /refresh-health Force health factor recomputation

5.4 Public Auction Mechanics

The auction is a sealed-bid, first-price mechanism with a public submission endpoint. Any participant with a valid signed liquidation transaction can compete.

sequenceDiagram
    participant COORD as Coordinator
    participant LA as Liquidator A
    participant LB as Liquidator B
    participant LC as External Bidder

    COORD->>LA: SSE /auction/stream — roundId, candidates, revealBlock, bidDeadline
    COORD->>LB: SSE /auction/stream — roundId, candidates, revealBlock, bidDeadline
    Note over LC: polls coordinator API independently

    Note over LA: Step 1 — score each candidate<br/>by expected profit at reveal price<br/>select highest-yield borrower
    Note over LA: Step 2 — build LiquidationExecutor calldata<br/>sign tx with liquidator EOA key
    Note over LA: Step 3 — compute bid hash<br/>keccak256(roundId, revealBlock, bidAmount, txHash)<br/>sign bid hash with EOA

    Note over LB: same three steps in parallel
    Note over LC: same three steps in parallel

    LA->>COORD: POST /bid — signedTx, sig, bidAmount, roundId, revealBlock
    LB->>COORD: POST /bid — signedTx, sig, bidAmount, roundId, revealBlock
    LC->>COORD: POST /bid — signedTx, sig, bidAmount, roundId, revealBlock

    Note over COORD: Validate — recover signer from sig<br/>tx must target LiquidationExecutor<br/>chainId must match config<br/>bid hash binds round + reveal + tx<br/>roundId + revealBlock match active round

    Note over COORD: BID_WINDOW_MS expires<br/>sort valid bids descending by amount<br/>winner = highest valid bid

    COORD-->>LA: selected as winner — proceed to reveal block
Loading

Bid validation (coordinator.ts:233) checks four properties:

  1. The ECDSA signature recovers to the claimed bidder address
  2. The decoded transaction targets LiquidationExecutor (not an arbitrary contract)
  3. The chainId in the signed transaction matches the coordinator's configured chain
  4. The bid hash binds roundId, revealBlock, bidAmount, and the transaction hash — preventing a winning bid from being replayed in a different round or with a different transaction

These validation rules make it impossible to win an auction with a bid that doesn't correspond to a real, executable liquidation transaction on the correct chain and round.


5.5 Execution Path

Once the reveal block is reached, execution happens in a tight sequence. The coordinator sends SVRFeed.reveal() first, updating the lending market price, then immediately broadcasts the winner's pre-signed transaction.

5.5 Execution Path

Exclusivity window: LendingMarket.onPriceCommit() arms a flag that restricts liquidate() calls to the LiquidationExecutor address until the window expires. This gives the auction winner guaranteed exclusivity — no other liquidator can front-run the winner's transaction during this period. After the window, public liquidation is permitted, allowing anyone to liquidate if the winner fails and the backstop also fails.

Treasury cut: The protocol fee is collected inside LendingMarket.liquidate() as a percentage of the seized collateral, before the remainder is passed to LiquidationExecutor. This happens atomically in the same transaction as the liquidation, making it impossible to extract value without paying the fee.


5.6 ERC-8004 Registry Watcher

ERC-8004 is an agent identity registry standard. The registry watcher maintains a live directory of all registered liquidation agents, continuously updated as agents register, update their metadata URI, or go offline.

Event consumption (registry-watcher.ts:270):

The watcher subscribes to two events on AgentIdentityRegistry:

  • Registered(agentId, metadataURI) — a new agent has registered
  • URIUpdated(agentId, newURI) — an existing agent has changed its metadata endpoint

On each event, the watcher fetches the /metadata endpoint at the agent's URI to retrieve capability declarations, supported chains, and contact information.

Health probing (registry-watcher.ts:124, registry-watcher.ts:287): The watcher periodically re-probes all known agents to keep lastSeen timestamps fresh. An agent that fails probing is marked inactive and will not be selected by the backstop until it recovers and is re-probed successfully.

Output: All discovered and probed agents are written to data/agentDirectory.json with the following fields per entry:

  • agentId — registry identity
  • metadataURI — current endpoint
  • capabilities — declared agent capabilities (must include liquidation)
  • lastSeen — timestamp of last successful probe
  • active — boolean health status
  • priority — declared execution priority

5.7 Backstop and Agent Fallback

The backstop is the system's last line of defense against bad debt. It activates when the primary auction winner fails — either because their transaction reverted (e.g., insufficient DEX liquidity at the swap router) or because no valid bids were submitted before the bid window closed.

5.7 Backstop and Agent Fallback

Agent execution (liquidation_agent.py:159, liquidation.py:48):

Each Python agent implements two HTTP endpoints: /standby (prepare resources, validate position is still liquidatable) and /activate (execute). The liquidate_borrower action tool performs three steps atomically:

  1. Compute safe repay amount based on current on-chain health factor
  2. Approve the debt token for LendingMarket
  3. Call LendingMarket.liquidate() directly

Agents interact with the lending market directly, without going through LiquidationExecutor. This bypasses the exclusivity window restriction (which has expired by the time backstop activates) and does not route through the swap router, trading some capital efficiency for simplicity and reliability.


6. Smart Contract Layer

The five contracts in this system form a clean dependency graph. No contract calls upstream in the flow — information and value move in one direction from oracle to settlement.

6 Smart Contract Layer

Key invariants enforced on-chain:

  • SVRFeed.reveal() verifies that keccak256(price, salt) == storedHash before updating the price feed. A coordinator that commits a hash cannot later reveal a different price.
  • LendingMarket.liquidate() checks msg.sender == liquidationExecutor during the exclusivity window. The exclusivity window is set when onPriceCommit() is called and expires at pendingExclusiveUntilBlock.
  • LendingMarket.liquidate() reads SVRFeed.latestRoundData() at call time — not at commit time, not at bid time. The revealed price is the authoritative price for all liquidation math.
  • LiquidationExecutor requires the caller to have pre-approved sufficient debt token. The winning liquidator's signed transaction includes this approval, enforcing that the liquidator bears the debt repayment.

7. Resilience, Persistence, and Recovery

Every service in the offchain stack is designed to survive restarts without data loss or state inconsistency.

Coordinator recovery (coordinator.ts:56, round-store.ts:135):

The coordinator writes all round state transitions and received bids to data/coordinator-state.json atomically (write-to-temp then rename). On startup, it reads this file and classifies each round:

  • committed rounds with an active reveal block in the future: resume waiting for reveal
  • committed rounds whose reveal block has passed: immediately attempt reveal + execution
  • revealed rounds with a pending winner tx: re-broadcast the winner tx
  • failed rounds: activate backstop if backstop has not yet been attempted

This means a coordinator restart in the middle of a live round does not lose the round — it picks up from the last persisted state and continues.

Indexer recovery (indexer.ts:49): The indexer checkpoints its full position state to data/indexer-state.json. On restart, it reads the checkpoint, determines the last processed block number, and replays only the events it missed. For a service that processes all lending market events from genesis, this catch-up mechanism is essential for fast restart times.

Registry watcher recovery (registry-watcher.ts:44): The agent directory is written to data/agentDirectory.json continuously. On restart, the watcher loads the existing directory and re-probes all agents to refresh health status before making the directory available to the backstop.

Atomic writes (persist.ts:12): All three services use the same atomic persistence pattern: write to a .tmp file, then rename() to the target path. This prevents partial writes from corrupting state files on crash.


8. Execution Modes

The coordinator supports two execution modes, selected at startup via the EXECUTION_MODE environment variable.

Mode Description Reveal mechanism Winner tx broadcast
testnet-sim Local development and testing Coordinator calls SVRFeed.reveal() directly Coordinator broadcasts via standard RPC
flashbots Mainnet with MEV protection Reveal + winner tx bundled together Submitted to Flashbots bundle relay

Flashbots mode (coordinator.ts:883, Flashbots-mainnet/server.ts:14): In production, the reveal transaction and the winner's liquidation transaction are submitted as an atomic Flashbots bundle. This provides two guarantees: the two transactions land in the same block (preventing any gap where the price is revealed but the liquidation hasn't executed), and the bundle is not visible in the public mempool until inclusion (preventing front-running of the reveal).

The Flashbots-mainnet/server.ts component manages bundle construction, signing with the Flashbots relay authentication key, and submission with retry logic. Failed bundle submissions trigger an immediate fallback to the backstop path rather than attempting a second non-private submission.


9. TUI Monitor

The terminal interface is a three-page Rust application that provides live observability into the running system. It communicates exclusively with the coordinator and indexer HTTP APIs.

┌─────────────────────────────────────────────────────────────────────────┐
│  Page 1: Dashboard                                                       │
│  ─────────────────────────────────────────────────────────────────────  │
│  Service health status for CRE, Coordinator, Indexer, Registry Watcher  │
│  Latest round snapshot: ID, state, bid count, winner, reveal block       │
│  Activity feed: recent round outcomes                                    │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│  Page 2: Rounds                                                          │
│  ─────────────────────────────────────────────────────────────────────  │
│  Loads from coordinator rounds API                                       │
│  Frontend filters: open/pending/failed rows shown separately             │
│  Columns: round ID, state, candidates, bid count, winner, profit        │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│  Page 3: Positions                                                       │
│  ─────────────────────────────────────────────────────────────────────  │
│  Reads indexer /positions                                                │
│  Columns: borrower, collateral value, debt, health factor, status       │
│  Status: healthy / at-risk / liquidatable / being-liquidated            │
└─────────────────────────────────────────────────────────────────────────┘

Navigation: Number keys 1, 2, 3 switch between pages. ENTER on the dashboard opens the full round view.

Scenario hotkeys (main.rs:88):

Key Action
h Trigger healthy scenario script
s Trigger stress scenario script
c Trigger crash scenario script

These hotkeys call external scripts that manipulate the local testnet state — adjusting prices, borrower positions, and market parameters — to simulate different market conditions against the live running services without requiring manual contract interactions.


10. Getting Started

Prerequisites

  • Node.js 18+ and npm
  • Python 3.10+
  • Rust 1.75+ and Cargo
  • A local EVM node (Hardhat, Anvil, or Ganache)
  • Access to a Chainlink CRE environment or the local simulation entrypoint

Installation

git clone <repository>
cd OEV

# Install offchain service dependencies
cd backend
npm install

# Install Python agent dependencies
cd ../agents
pip install -r requirements.txt

# Build the Rust TUI
cd ../tui
cargo build --release

Starting the Offchain Stack

The offchain_stack.sh script starts all services with their default ports and inter-service URLs preconfigured:

cd backend/scripts
./offchain_stack.sh

To start services individually for development:

# Indexer (default: port 3001)
cd backend && npx ts-node offchain/indexer/indexer.ts

# Coordinator (default: port 3000)
cd backend && npx ts-node offchain/coordinator/coordinator.ts

# Registry Watcher (default: port 3002)
cd backend && npx ts-node offchain/agents/registry-watcher.ts

# CRE source simulation (local testnet only)
cd backend && npx ts-node cre/source-oracle-oev/main.ts

# TUI
cd tui && ./target/release/tui

Running with the Local Source Oracle Simulation

For local development, the full Chainlink CRE runtime is replaced by the source oracle simulation entrypoint, which directly calls SourcePriceOracle.updatePrice() on a local testnet and feeds events into the CRE workflow:

# Start local testnet (Anvil example)
anvil --block-time 2

# Deploy contracts
cd backend && npx hardhat run scripts/deploy.ts --network localhost

# Set environment variables
export RPC_URL=http://localhost:8545
export COORDINATOR_URL=http://localhost:3000
export INDEXER_URL=http://localhost:3001
export CHAIN_ID=31337

# Start offchain stack
./scripts/offchain_stack.sh

# Trigger a price update (will fire the full pipeline)
npx ts-node cre/source-oracle-oev/main.ts

11. Configuration Reference

All services are configured via environment variables. The following table covers the primary configuration surface.

Variable Service Description Default
RPC_URL All EVM node WebSocket or HTTP endpoint
CHAIN_ID All Target chain ID for tx validation
COORDINATOR_URL CRE, Liquidators, TUI Coordinator service base URL http://localhost:3000
INDEXER_URL CRE, Coordinator, TUI Indexer service base URL http://localhost:3001
BID_WINDOW_MS Coordinator Milliseconds bids are accepted after round open 5000
EXECUTION_MODE Coordinator testnet-sim or flashbots testnet-sim
FLASHBOTS_RELAY_URL Coordinator Flashbots bundle relay endpoint
LENDING_MARKET_ADDRESS All Deployed LendingMarket contract address
SVR_FEED_ADDRESS Coordinator Deployed SVRFeed contract address
LIQUIDATION_EXECUTOR_ADDRESS Coordinator, Liquidators Deployed LiquidationExecutor address
SOURCE_ORACLE_ADDRESS CRE Deployed SourcePriceOracle address
AGENT_REGISTRY_ADDRESS Registry Watcher Deployed AgentIdentityRegistry address
AGENT_FRESHNESS_MS Backstop Max age of lastSeen before agent excluded 60000
COORDINATOR_PRIVATE_KEY Coordinator Key for signing reveal transactions
LIQUIDATOR_PRIVATE_KEY Liquidators Key for signing bid transactions

12. API Reference

Coordinator

Method Path Description
POST /cre/start-round Open a new auction round (CRE only)
GET /auction/stream SSE stream of auction frames for liquidators
POST /bid Submit a signed bid for the active round
GET /rounds List all rounds with state and outcome
GET /health Service health check

Indexer

Method Path Description
GET /candidates Borrowers liquidatable at ?price1e8=
GET /positions All tracked borrower positions
GET /stale Positions not refreshed within staleness window
POST /pending-price Set pending round price context
POST /refresh-health Force health factor recomputation for all positions

Agent (per instance)

Method Path Description
GET /metadata ERC-8004 capability declaration
POST /standby Prepare to execute a specific liquidation
POST /activate Execute the prepared liquidation

13. Contributing

The codebase is split across four languages and five distinct service boundaries. Changes to one service rarely require changes to others, since all inter-service communication is HTTP. The most likely areas for contribution are:

New liquidation strategies: The liquidator service computes a profit estimate per candidate and bids accordingly. More sophisticated strategies — multi-hop collateral swaps, flash loan integration, partial liquidations across multiple borrowers in one bid — can be implemented by extending liquidator.ts and the corresponding LiquidationExecutor calldata construction.

Additional agent implementations: Any service that implements the three endpoints /metadata, /standby, and /activate and registers with AgentIdentityRegistry is automatically discovered by the registry watcher and made available to the backstop. New agent implementations in any language are valid.

Extended indexer math: The candidate computation in liquidation.ts uses a simplified close factor and bonus model. More accurate models that account for variable rates, multi-asset collateral, and partial liquidation incentives would improve candidate quality and reduce spurious round opens.

Onchain improvements: SVRFeed.sol and LendingMarket.sol are the most sensitive contracts in the system. Any changes require comprehensive test coverage and should be accompanied by a threat model analysis covering the commit-reveal scheme's assumptions.

To contribute:

  1. Fork the repository and create a branch from main
  2. Make changes with accompanying tests
  3. Ensure all existing tests pass: npm test for backend services, cargo test for the TUI
  4. Submit a pull request with a description of the change and its motivation

14. License

This project is licensed under the MIT License.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors