Skip to content

Repository files navigation

Morpho Midnight Tools

Tooling for the Morpho Midnight market — an EVM event indexer and liquidation bot that tracks on-chain market and position state and acts on liquidatable positions.

What it does

The system is split into two independently-deployable services that communicate over a RabbitMQ topic exchange:

  • Indexer — polls an EVM chain for logs emitted by the Midnight contract, filters them to known event signatures, persists the raw logs to Postgres, and publishes each decoded event to a RabbitMQ topic exchange (one routing key per event type, e.g. event.repay, event.liquidate).

  • Liquidator — declares a durable queue bound to the exchange and consumes the decoded event stream, folding it into an in-memory projection of markets and positions (market → user → position) while persisting the state to the markets, collateral_params, positions, and user_collaterals tables. On startup it loads the state, then continues consuming new events off the queue, scans for liquidatable positions, and submits liquidate transactions on-chain with its own signer. Each submission is recorded in the liquidation_transactions table (tx hash, sender, nonce, repaid units, pending/confirmed/reverted status) so in-flight positions are excluded from re-selection.

The raw events table plus per-row history tables (history_markets, history_positions) provide durability and reorg recovery: every state-row update copies the prior version into history via a trigger, so on a chain reorg the state can be rolled back to a known block and the new fork's events re-applied. RabbitMQ carries the live delta and fans it out.

Architecture

flowchart LR
    chain[(EVM chain<br/>Anvil / RPC)]

    subgraph indexer[Indexer service]
        idx[poll · filter · decode]
    end

    subgraph broker[RabbitMQ]
        ex{{topic exchange<br/>morpho.events}}
        q[[queue<br/>liquidator.events]]
    end

    subgraph liquidator[Liquidator service]
        liq[apply · scan for<br/>liquidatable positions]
    end

    db[(Postgres<br/>events · block_headers · indexer_status<br/>markets · positions · collateral_params · user_collaterals<br/>history_* · liquidator_status · liquidation_transactions)]

    chain -->|get_logs / get_block| idx
    idx -->|persist raw logs| db
    idx -->|publish event.*| ex
    ex -->|bind event.#| q
    q -->|consume| liq
    liq -->|persist reduced state| db
    db -.->|hydrate on startup| liq
    liq -->|liquidate tx| chain
    liq -->|record tx status| db

    ex -.->|future: event.liquidate| future[Other subscribers]
Loading

Repository layout

A Cargo workspace with a shared common crate and two service binaries:

.
├── docker-compose.yml            # Postgres + RabbitMQ + Anvil + services
├── anvil/                        # Dockerfile for a local Anvil EVM node
├── Cargo.toml                    # [workspace] + shared dependency versions
├── migrations/                   # sqlx SQL migrations (events, positions, markets, …)
└── crates/
    ├── common/                   # shared crate, no service-specific logic
    │   └── src/
    │       ├── contracts/        # sol! event types + decoding + EventEnvelope (wire format)
    │       ├── messaging.rs      # AmqpConfig + RabbitPublisher / RabbitSubscriber
    │       ├── db.rs             # Postgres/sqlx Store trait + impl
    │       ├── rpc.rs            # alloy ChainReader trait + impl
    │       └── price.rs          # PriceProvider trait + math
    ├── services/
    │   ├── indexer/              # binary: poll chain, persist, publish
    │   │   └── src/{main,indexer,configuration}.rs
    │   └── liquidator/           # binary: consume, fold state, persist, scan
    │       └── src/{main,liquidator,protocol_state,market,position,selector,price,math,configuration}.rs
    │           └── db/           # LiquidatorDb: state persistence, hydration, reorg rollback
    └── test_utils/               # mock chain / store / publisher / price for unit tests

Tech stack

  • Rust (Cargo workspace, two binaries + shared crate)
  • alloy for EVM types, RPC, and sol! event decoding
  • amqprs for the RabbitMQ (AMQP 0-9-1) publisher/subscriber
  • sqlx with Postgres (compile-time-checked queries)
  • tracing for structured logging (rolling file + stdout)
  • Anvil (Foundry) for a local dev chain

Getting started

1. Start dependencies

docker compose up rabbitmq anvil postgres

anvil deploys Midnight and runs the liquidation-opportunity loop (see anvil/); its deploy grants the liquidator (anvil account 1) a one-time loan-token approval.

2. Configure

Each service reads configuration from the environment. The two services use separate variable sets (the liquidator prefixes its own with LIQUIDATOR_); the AMQP_* set is shared. See indexer/configuration.rs and liquidator/configuration.rs. Create a .env at the repo root:

Indexer

Variable Description
DATABASE_URL Postgres connection string
RPC_URL EVM JSON-RPC endpoint
CONTRACT_ADDRESS Midnight contract address to index
START_BLOCK Block to begin indexing from
BLOCK_RANGE Max blocks per get_logs batch
INTERVAL Seconds between indexer polls
LOG_FILE Log filename (written under logs/)
LOG_LEVEL trace/debug/info/warn/error (default info)

Liquidator

Variable Description
LIQUIDATOR_DATABASE_URL Postgres connection string
LIQUIDATOR_SUPPORTED_CHAINIDS Comma-separated chain IDs to service (e.g. 31337)
LIQUIDATOR_RPC_URLS Comma-separated chainId=url map, one entry per supported chain (e.g. 31337=http://localhost:48545)
LIQUIDATOR_PRIVATE_KEY Signer key that submits liquidate txs (anvil account 1, the liquidator)
LIQUIDATOR_LOG_FILE Log filename (written under logs/)
LIQUIDATOR_LOG_LEVEL trace/debug/info/warn/error (default info)

AMQP (shared) — indexer publishes, liquidator consumes; queue/binding/prefetch are liquidator-only.

Variable Description
AMQP_HOST RabbitMQ hostname — bare host, no http:// or port (e.g. localhost)
AMQP_PORT AMQP port (default 5672; use the docker-compose-mapped port for local)
AMQP_USER RabbitMQ user (default guest)
AMQP_PASS RabbitMQ password (default guest)
AMQP_EXCHANGE Topic exchange name (default morpho.events)
AMQP_QUEUE Durable queue name, liquidator (e.g. liquidator.events)
AMQP_BINDING_KEY Routing-key pattern, liquidator (default events.<chainId>.#)
AMQP_PREFETCH Consumer QoS prefetch for back-pressure, liquidator (default 32)

3. Run migrations and start

In a second terminal (from the repo root), apply migrations and start the indexer:

sqlx migrate run                     # applies migrations/ against DATABASE_URL
cargo run --bin indexer              # publisher

In a third terminal, start the liquidator:

cargo run --bin liquidator           # subscriber + on-chain executor

The indexer logs rabbitmq publisher ready; the liquidator logs rabbitmq subscriber ready after declaring and binding its queue.

Per-service .env.sample files live next to each binary: indexer/.env.sample and liquidator/.env.sample.

Logging

Each service writes structured logs to stdout and to a daily-rotated file under logs/. The filename comes from that service's LOG_FILE, and the on-disk file is suffixed with the date: logs/<LOG_FILE>.YYYY-MM-DD.

To keep the two services in separate log files, give each a distinct LOG_FILE (the samples already do this):

# indexer .env
LOG_FILE=indexer.log       # -> logs/indexer.log.YYYY-MM-DD

# liquidator .env
LOG_FILE=liquidator.log    # -> logs/liquidator.log.YYYY-MM-DD

Development

# from the repo root
cargo build --workspace
cargo test  --workspace              # DB-backed tests need DATABASE_URL set

License

See LICENSE.

About

Indexer and liquidation bot for Morpho's Midnight protocol

Topics

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages