# Hashpower market semantics

---



---

> Regenerated 2026-09-16T15:08:43.258Z from `Lumerin-protocol/*@dev`. Individual docs also live at /semantics/<slug>.md.

---



---

# Hashprice Oracle

Trustless on-chain Bitcoin hashprice oracle powered by Bitcoin SPV (Simplified Payment Verification) proofs. No trusted intermediary — anyone can permissionlessly submit Bitcoin block headers and coinbase merkle proofs directly to the contract.

## Overview

This repository contains:

- **Smart Contracts** (`/contracts`) — `HashpriceBTC` (BTC-denominated hashprice via SPV) and `HashpriceUSD` (USD-denominated, combines `HashpriceBTC` with a Chainlink BTC/USD feed). Both implement `AggregatorV3Interface`.
- **Keeper** (`/keeper`) — Automated bot that relays Bitcoin block headers and coinbase merkle proofs to the on-chain oracle, keeping the hashprice feed current.
- **Subgraph Indexer** (`/indexer`) — A Graph Protocol subgraph that indexes oracle updates and provides historical hashprice data with hourly/daily aggregations.

## Architecture

```
Bitcoin network
      │  block headers + coinbase merkle proofs
      ▼
  [ keeper ]  ──────────────────────────────────►  [ HashpriceBTC ]
                                                        │
                                          latestRoundData() → hashprice in BTC
                                                        │
                                                        ▼
                                                  [ HashpriceUSD ]
                                                        │  ▲
                                          latestRoundData() │
                                                        │  └─── [ Chainlink BTC/USD ]
                                                        │         (any AggregatorV3Interface)
                                                        ▼
                                          latestRoundData() → hashprice in USD
```

### HashpriceBTC

`HashpriceBTC` is a Bitcoin SPV contract — it stores Bitcoin block headers on-chain and verifies coinbase transactions via merkle proofs, without trusting any off-chain data source.

Each submitted block provides:

- An **80-byte block header**, from which the contract extracts difficulty target, timestamp, and previous block hash, and verifies proof-of-work
- A **coinbase merkle proof**, which proves the coinbase transaction is the first transaction in that block

From the verified coinbase (block subsidy + fees) and the on-chain difficulty, the contract computes the hashprice — the expected revenue per petahash per day — using a 144-block Simple Moving Average over fees, matching the [Luxor hashprice index](https://luxor.tech/hashprice).

Key properties:

- **Permissionless** — any address can submit headers and proofs; no owner, no admin key
- **Trustless** — the contract rejects any submission that fails PoW or merkle verification; a malicious keeper cannot corrupt the feed
- **Reorg-aware** — `submitBlocks()` handles chain reorganizations by accepting a new chain of headers that replaces the current tip
- **Chainlink-compatible** — implements `AggregatorV3Interface`; output is 1 PH/s per day priced in BTC (16 decimals)

**Why SPV?** SPV verification requires only 80-byte block headers and a merkle path — no full node, no trusted oracle, no multisig. The same primitive underpins Bitcoin light clients and cross-chain bridges like BTC Relay.

For a detailed breakdown of every validation check — what is included, what is deliberately excluded, and why — see [docs/BLOCK_VALIDATION.md](docs/BLOCK_VALIDATION.md).

### HashpriceUSD

`HashpriceUSD` is a thin aggregator contract that combines two `AggregatorV3Interface` feeds to produce a USD-denominated hashprice:

```
hashprice (USD) = hashprice (BTC)  ×  BTC/USD price
```

It accepts any `AggregatorV3Interface`-compatible BTC/USD feed at deploy time (e.g. Chainlink on mainnet, a custom feed on other networks), making it chain-agnostic. Staleness semantics are conservative: `updatedAt` reflects the older of the two upstream feeds, so consumers checking staleness always see the bottleneck.

Both contracts implement `AggregatorV3Interface`, so they are drop-in compatible with any protocol that reads Chainlink price feeds.

## Keeper

The keeper monitors the on-chain oracle height against the Bitcoin tip and submits missing blocks as a batch. It is runtime-agnostic and ships adapters for Node.js, AWS Lambda, and Cloudflare Workers. See [`keeper/README.md`](keeper/README.md) for setup and configuration.

## Quick Start

### Contracts

```bash
cd contracts
pnpm install
pnpm test
pnpm compile
```

### Keeper

```bash
cd keeper
pnpm install
cp .env.example .env
# fill in .env
pnpm dev
```

### Indexer

```bash
cd indexer
yarn install
# Configure .env from .env.example
yarn codegen
yarn build
```

### Seed JSON (local history dump)

Regenerate `contracts/seed/` with one command — see [`indexer/SEED.md`](indexer/SEED.md):

```bash
cd indexer
SEED_DAYS=30 pnpm seed:generate
```

## Gas Costs

Current gas numbers are tracked in [`gas-benchmark.md`](./contracts/gas-benchmark.md) and regenerated by running the gas benchmark test:

```bash
cd contracts
npx hardhat test tests/HashpriceBTC/gas-benchmark.test.ts
```

## License

MIT


---

# Collateral & Accounts

Everything you trade on Hash Power Perps is backed by collateral held in a
shared **CollateralVault**. This page explains how collateral gets in and
out, how your balance is used, and why it is shared with the Futures market.

---

## The shared collateral vault

Perps does **not** custody tokens itself. Collateral lives in a separate
`CollateralVault` contract that is shared across the Hash Power venues
(Perps and Futures). The DEX only asks the vault to move balances internally
when trades, fees, funding, or liquidations settle.

```mermaid
flowchart LR
    Wallet["Your wallet<br/>ERC-20 collateral"]
    Vault["CollateralVault<br/>receipt balance"]
    Markets["Perps / Futures"]

    Wallet -->|"deposit(amount)"| Vault
    Vault -->|"withdraw(amount)"| Wallet
    Vault <-->|"internalTransfer<br/>PnL · fees · funding"| Markets
```

When you `deposit`, the vault pulls your ERC-20 collateral and credits you an
equal **receipt balance**. `HashPowerPerpsDEX.balanceOf(you)` simply reads
that vault balance.

## Depositing

Call the vault directly:

| Function                                          | Use                                                        |
| ------------------------------------------------- | ---------------------------------------------------------- |
| `deposit(amount)`                                 | Pull `amount` of collateral from you, credit your balance  |
| `depositForPermit(recipient, amount, deadline, v, r, s)` | Approve + deposit in one transaction (ERC-2612 permit) |
| `depositFor(recipient, amount)`                   | Deposit on behalf of another account                       |

Before `deposit`, approve the vault to spend your collateral (or use
`depositForPermit` to combine approval and deposit in a single transaction).

## Withdrawing

```
withdraw(amount)
```

Withdrawals burn your receipt balance and return the underlying token. A
withdrawal **reverts if it would breach your portfolio margin** — you can
only take out collateral that is not backing an open position or resting
order (your *free* / excess margin).

## One balance, two venues (portfolio margin)

Your vault balance is a **single pool of margin** that backs positions on
both Perps and Futures at the same time. Margin requirements are computed at
the **portfolio** level by the `PortfolioMarginEngine`, so offsetting
exposure across venues can reduce the total margin you need, and a
withdrawal is checked against your combined requirement.

Practical consequences:

- Depositing once funds trading on both venues.
- A loss (or owed funding) on one venue reduces the collateral available to
  the other.
- Your Perps liquidation threshold depends on your **whole** portfolio, not
  just your Perps position.

## Your balance components

At any time your vault balance is conceptually split into:

| Component            | Meaning                                                         |
| -------------------- | -------------------------------------------------------------- |
| Initial Margin (IM)  | Locked to **open / increase** exposure                         |
| Maintenance Margin (MM) | The floor below which you become liquidatable               |
| Order margin         | Extra IM your resting (unmatched) orders add, after netting them against your positions |
| Free / excess margin | Withdrawable collateral above all requirements                 |

Useful views:

- `balanceOf(user)` — total vault balance.
- `computePortfolioIM(user)` / `computePortfolioMM(user)` on the margin engine — current IM / MM across all products.
- `orderMarginOf(user)` on the margin engine — collateral reserved by resting orders. Portfolio-wide, not per-venue: the engine nets each market's resting-order delta into your total before stressing it, so an order that only moves you toward flat reserves nothing. The figure moves with the price; it is not a fixed amount set when you placed the order.
- `getUnrealizedPnl(user)` — mark-to-market PnL on the open position.
- `getPendingFunding(user)` — unsettled funding (positive = you owe).

## The insurance fund

The vault holds a protocol-owned **insurance fund** account
(`INSURANCE_FUND_ADDR`). It is the counterparty ledger for:

- **Fees** — taker/maker fees are paid into it (see [Fees](./06.Fees.md)).
- **Funding** — funding payments flow to/from it.
- **PnL & bad debt** — realized PnL settles against it; if a liquidated
  account can't cover its loss, the shortfall is **absorbed by the insurance
  fund** (never taken from other users). See
  [Margin & Liquidation](./05.Margin-and-Liquidation.md).

---

## Read next

- [Trading Guide](./03.Trading-Guide.md) — place your first order.
- [Margin & Liquidation](./05.Margin-and-Liquidation.md) — keep your account healthy.


---

# Trading Guide

This guide covers how to trade on Hash Power Perps: the order book model,
placing and matching orders, and managing their lifecycle.

---

## Prerequisites

1. **Collateral deposited** in the shared vault — see
   [Collateral & Accounts](./02.Collateral-and-Accounts.md).
2. **Wallet** connected to the correct network.
3. Enough **free margin** to cover the Initial Margin of the order you place.

---

## The order book model

Hash Power Perps is a fully on-chain **central limit order book (CLOB)**:

- Orders rest on a **fixed price grid**. Every price must be a multiple of
  `minimumPriceIncrement` (the tick size); off-grid prices revert
  `InvalidPrice`.
- Each side keeps **sorted price levels** — bids high-to-low, asks
  low-to-high — and within a level orders queue **FIFO** (price-time
  priority).
- Quantities use `QUANTITY_DECIMALS = 6` and are **signed**: positive = long
  (buy), negative = short (sell).

There is a single entry point for trading — `createOrder` — which behaves as
both a marketable (aggressive) order and a resting (passive) order depending
on the book.

## Placing an order

```solidity
createOrder(uint256 price, int256 quantity, TimeInForce tif)
```

- `price` — your **limit** price (must be on the tick grid).
- `quantity` — signed size. Positive buys/longs, negative sells/shorts.
- `tif` — how long the order lives: `GTC` rests the unfilled remainder, `IOC`
  cancels it, `FOK` requires the whole size to fill at once.

What happens, in order:

1. Global funding is refreshed and your funding is settled.
2. The order **matches immediately** against the opposite side, walking
   prices from best toward your limit:
   - A **buy** fills against asks priced **at or below** your limit.
   - A **sell** fills against bids priced **at or above** your limit.
3. Any quantity left after matching **rests** on the book at your limit price
   (subject to the limits below).
4. Unless the order is *reduce-only*, your Initial Margin is checked
   (`_ensureInitialMargin`); insufficient free margin reverts.

To trade like a **market order**, submit an aggressive limit price (e.g. far
through the book); it fills against everything up to that price and rests the
remainder. To trade **passively**, price it away from the top of book so it
rests as a maker order.

### Simulating first

`simulateOrder(price, quantity)` is a read-only preview that returns the
`filledQuantity`, the volume-weighted `averageFillPrice`, and the
`remainingQuantity` that would rest — without sending a transaction.

## How matching works

Orders match when they cross:

1. **Crossing price** — buy limit ≥ ask, or sell limit ≤ bid.
2. **Opposite direction** — buys take from the ask queue, sells from the bid
   queue.
3. **FIFO within a level** — the oldest maker order at a price fills first.

Trades execute at the **maker's price**, so an aggressive taker gets price
improvement when the resting order is better than its limit.

```mermaid
flowchart TD
    A["createOrder(price, quantity, tif)"] --> B["Refresh and settle funding"]
    B --> C{"Crosses the book?<br/>buy: ask at or below limit<br/>sell: bid at or above limit"}
    C -->|Yes| D["Fill best levels first, FIFO within a level,<br/>at the maker's price"]
    D --> E{"Quantity left?"}
    C -->|No| E
    E -->|Yes| F["Rest remainder on the book at your limit"]
    E -->|No| G["Fully filled"]
    F --> H{"Reduce-only?"}
    G --> H
    H -->|No| I["Check Initial Margin (revert if short)"]
    H -->|Yes| J["Skip margin check"]
```

Example book depth (asks high→low, bids high→low; each level is a FIFO queue):

| Side | Price | Resting queue (oldest first) |
| ---- | ----- | ---------------------------- |
| Ask  | 4.15  | Seller A                     |
| Ask  | 4.14  | Seller B                     |
| Ask  | 4.12  | Seller C                     |
| Bid  | 4.11  | Buyer Y, Buyer Z             |
| Bid  | 4.10  | Buyer W                      |

- New **buy @ 4.12** → fills Seller C at 4.12.
- New **sell @ 4.11** → fills Buyer Y first (oldest in the queue).

`getBestBidPrice()` / `getBestAskPrice()` return the top of book, and
`getQuantityAtPrice(price, isBid)` the resting size at a level.

## Positions from fills

Each fill updates your single **net position** rather than creating separate
lots:

- Adding in the same direction updates your **weighted-average entry price**.
- Trading the opposite direction **reduces** the position (realizing PnL on
  the closed part), and if it exceeds your size, **flips** you to the other
  side at the new price.

See [Positions & Funding](./04.Positions-and-Funding.md) for the details.

## Reduce-only fills skip the margin check

If an order is on the opposite side of your current position and does not
exceed its size, it is treated as **reduce-only** and skips the Initial
Margin check — you can always de-risk, even when close to your margin
limits.

## Closing or reducing a position

There is no expiry and no settlement step. To close, submit an opposite-side
order:

```
Open:  +1.0 long  @ 4.10
Close: -1.0 sell  @ 4.14

Realized PnL = (4.14 − 4.10) × 1.0 = 0.04 per unit (before fees/funding)
```

Fully offsetting flattens the position; partially offsetting leaves a smaller
position at the **same** entry price.

## Cancelling resting orders

```solidity
cancelOrder(bytes32 orderId)
```

Removes your unmatched order from the book and releases the margin it
reserved. Only the order's owner can cancel it (`OrderNotBelongToSender`).

## Order limits

| Limit                       | Value / rule                                                    |
| --------------------------- | -------------------------------------------------------------- |
| Price grid                  | Multiple of `minimumPriceIncrement` (else `InvalidPrice`)      |
| Max resting orders per user | `MAX_ORDERS_PER_PARTICIPANT = 100` (`MaxOrdersPerParticipantReached`) |
| Max price levels per side   | `MAX_PRICE_LEVELS_PER_SIDE = 200` (`MaxPriceLevelsReached`)     |
| Margin                      | Portfolio IM computed by the PME                               |
| Quantity                    | Non-zero, `QUANTITY_DECIMALS = 6` (`InvalidSize`)              |

`minimumMarginPerOrder`, its setter/event, and `OrderMarginTooLow` are deprecated compatibility ABI only; the configured value is not enforced.

## Useful views

| View                                | Returns                                              |
| ----------------------------------- | ---------------------------------------------------- |
| `simulateOrder(price, qty)`         | Preview fill / average price / resting remainder     |
| `getBestBidPrice()` / `getBestAskPrice()` | Top of book                                    |
| `getQuantityAtPrice(price, isBid)`  | Resting quantity at a price level                    |
| `getOrder(orderId)`                 | A single order                                       |
| `getUserOrders(user)`               | A user's open order ids                              |

---

## Read next

- [Positions & Funding](./04.Positions-and-Funding.md) — what happens after a fill.
- [Fees](./06.Fees.md) — what each trade costs.


---

# Positions & Funding

Once your orders fill you hold a **net position**. This page explains how
that position is tracked, how PnL works, and how **funding** keeps the
perpetual tethered to the oracle hashprice.

---

## Net-position accounting

Each account has exactly **one** position per market, not a list of lots:

```solidity
struct Position {
    int256  netQuantity;         // + = long, − = short (QUANTITY_DECIMALS = 6)
    uint256 aggregatedEntryPrice; // weighted-average entry price
}
```

Every fill folds into this single position.

### Adding to a position (same direction)

The entry price becomes the **notional-weighted average** of the old and new
fills:

```
newEntry = (|oldQty|·oldEntry + |fillQty|·fillPrice) / |oldQty + fillQty|
```

Example:

```
Have:  +2 long @ 4.00
Buy:   +1 long @ 4.30

netQuantity        = +3
aggregatedEntry    = (2·4.00 + 1·4.30) / 3 = 4.10
```

### Reducing a position (opposite direction)

Trading against your position realizes PnL on the closed slice at the trade
price; the entry price of the remainder is **unchanged**:

```
Have:  +3 long @ 4.10
Sell:  −1      @ 4.25

Realized PnL = (4.25 − 4.10) × 1 = 0.15
Remaining    = +2 long @ 4.10  (entry unchanged)
```

### Flipping

If an opposite order is **larger** than your position, the position closes
fully (realizing PnL on the old size) and a **new** position opens on the
other side, sized by the excess, at the trade price.

## Unrealized PnL

Marked continuously against the oracle **index price**:

```
Unrealized PnL = (mark price − aggregatedEntryPrice) × netQuantity / 10^QUANTITY_DECIMALS
```

Because `netQuantity` is signed, longs profit when price rises and shorts
profit when price falls. Read it with `getUnrealizedPnl(user)`.

> Realized PnL settles against the insurance fund. A winning close is paid
> from the fund; a losing close pays into it. If the fund cannot cover a
> winning partial close, the close reverts `InsufficientReservePool`.

---

## Funding

A perpetual has no expiry to force its price back to fair value, so a
periodic **funding** payment does it instead. When the order book trades
**above** the oracle, longs pay shorts; when it trades **below**, shorts pay
longs. This incentivizes traders to push the book back toward the index.

### The two prices

| Leg          | Source                                          |
| ------------ | ----------------------------------------------- |
| Mark price   | Order-book mid — `(getBestBidPrice() + getBestAskPrice()) / 2` |
| Index price  | Hashprice oracle — `getMarketPrice()`           |

If either side of the book is empty, no new funding accrues.

### The funding rate

```
fundingRate = (markPrice − indexPrice) / indexPrice        (per funding period)
```

clamped to ±`fundingRateMaxBps` per `fundingPeriod` (both owner-configured).
A positive rate means the book is rich → longs pay shorts.

```mermaid
flowchart LR
    Mark["Mark price<br/>order-book mid"] --> Cmp{"mark vs. index"}
    Index["Index price<br/>oracle hashprice"] --> Cmp
    Cmp -->|"mark above index"| L["Longs pay shorts"]
    Cmp -->|"mark below index"| S["Shorts pay longs"]
```

### How it accrues and settles

- A **global cumulative funding index** (`cumulativeFundingPerUnit`) grows
  over time at the current rate. Anyone can advance it with `updateFunding()`,
  and it is refreshed automatically before every order, cancel, and
  liquidation.
- Each account stores a **snapshot** of that index taken when its position
  was last touched. Your owed/received funding is:

```
funding = netQuantity × (cumulativeIndexNow − yourSnapshot) / (10^QUANTITY_DECIMALS · 10^FUNDING_DECIMALS)
```

- Funding is **settled into your vault balance** (against the insurance
  fund) whenever your position is touched — before any size change so it is
  charged on the old size. Positive = you pay, negative = you receive.
- If you owe funding but can't cover it, the shortfall is recorded as
  `BadDebt` and absorbed by the insurance fund.

### Checking funding

- `getPendingFunding(user)` — unsettled funding accrued so far (positive =
  you owe, negative = you receive).
- The `FundingUpdated` and `FundingSettled` events track the global index and
  per-user settlements.

> **Funding affects your margin.** Owed funding is settled out of your
> balance, moving you closer to your maintenance threshold. Account for it
> when sizing collateral.

---

## Position views

| View                      | Returns                                         |
| ------------------------- | ----------------------------------------------- |
| `getUserPosition(user)`   | `{ netQuantity, aggregatedEntryPrice }`         |
| `getUnrealizedPnl(user)`  | Mark-to-market PnL                              |
| `getPendingFunding(user)` | Unsettled funding                               |

Position-holder lists are maintained off chain from indexed events; the contract
does not expose global participant enumeration.

---

## Read next

- [Margin & Liquidation](./05.Margin-and-Liquidation.md) — staying solvent.
- [Fees](./06.Fees.md) — trading costs.


---

# Margin & Liquidation

The Hash Power Perps margin system keeps every open position collateralized
so the protocol and its counterparties are protected from default. Each user
holds a single **net position** (an aggregated `netQuantity` at an
`aggregatedEntryPrice`); margin is evaluated at the **portfolio** level and
undercollateralized accounts are liquidated back toward their Initial Margin
buffer.

---

## Overview

Margin is collateral deposited into the shared vault to guarantee a
position's obligations. The contract continuously values each account against
two thresholds computed by the `PortfolioMarginEngine`:

```mermaid
flowchart TD
    Bal["Balance (vault collateral)"] --> Excess["Excess margin — withdrawable"]
    Excess --> IM["Initial Margin (IM) — required to OPEN"]
    IM --> Buffer["IM buffer — liquidation target band"]
    Buffer --> MM["Maintenance Margin (MM) — breach triggers liquidation"]
    MM --> Risk["Unrealized loss / owed funding"]

    Check{"Balance below MM?"}
    Check -->|Yes| Liq["LIQUIDATABLE"]
    Check -->|No| Ok["Healthy"]
```

- **`computePortfolioMM(user)`** — maintenance margin (lower). Falling
  below it makes the account liquidatable.
- **`computePortfolioIM(user)`** — initial margin (higher). Required to
  open or increase a position, and the target that liquidation restores an
  account to.

Both are derived from the account's net exposure and the engine's spot
shocks (`mmSpotShock` for MM, `imSpotShock` for IM), so `IM ≥ MM` whenever
a real buffer is configured. Because margin is portfolio-wide, your Perps
threshold depends on your combined Perps + Futures exposure (see
[Collateral & Accounts](./02.Collateral-and-Accounts.md)).

---

## Margin Types

### 1. Deposited Collateral (Balance)

The account's vault balance (`balanceOf(user)`), in collateral-token
decimals. Funding payments and realized PnL move in and out of this
balance.

### 2. Maintenance Margin (MM)

The minimum collateral required to keep the net position open. Conceptually:

```
MM ≈ |netQuantity| × mark price × mmSpotShock
```

### 3. Initial Margin (IM)

The higher requirement enforced when opening or increasing exposure, and
the level liquidation aims to restore:

```
IM ≈ |netQuantity| × mark price × imSpotShock      (imSpotShock ≥ mmSpotShock)
```

### 4. Unrealized PnL & Funding

Both reduce the effective health of an account before the thresholds are
checked:

```
Unrealized PnL = (mark price − aggregatedEntryPrice) × netQuantity
```

(`netQuantity` is signed, so its sign handles long vs short.) Perpetual
**funding** is accrued continuously and is settled into the balance whenever
the position is touched (see [Positions & Funding](./04.Positions-and-Funding.md)),
so an account that owes funding is closer to its MM than PnL alone suggests.

The margin engine charges each of these once: an unrealized *loss* and any
funding *owed* are added as separate terms. Funding you are owed does not
reduce the requirement until it settles.

Unrealized gains depend on which threshold is being checked. **MM** sums PnL
across every venue before taking the loss, so a gain on your futures book
offsets a loss on your perps book and a hedge that is flat overall is not
liquidated merely because one leg is underwater. **IM** clamps each venue
separately and ignores gains entirely, because IM is what gates opening new
positions and withdrawing collateral — an unrealized gain should keep you
alive, but it should not let you take out cash or add leverage before it
settles. In neither case does a net gain reduce the requirement below the
stress term; profit can cancel a loss you actually carry, and no more.

---

## Liquidations

### What Triggers a Liquidation?

An account becomes liquidatable when:

```
Balance < Maintenance Margin
```

Liquidation is **permissionless**: any address can submit a liquidation
transaction once the condition holds.

> **Keeper incentives are currently disabled.** No `liquidationFee` is
> transferred to `msg.sender`; the liquidation events carry `0`. The
> `liquidationFee` state variable and its setter are **retained** — note it
> also serves as the *minimum taker fee* on trades, so it is not removed,
> only its keeper-payout role is switched off. The protocol runs the sole
> keeper for now; the payout hook is kept for a future incentive iteration.

### Liquidation Process

The **trigger** is a Maintenance Margin breach, but the **goal** is to
restore the account to its Initial Margin buffer — liquidation closes only
as much of the net position as needed to bring the balance back to (at
most) IM, not necessarily the whole position. The keeper sizes the partial
amount off-chain.

```mermaid
flowchart TD
    D["Keeper detects: Balance below Maintenance Margin"] --> O["liquidateOrder(user, orderId)<br/>FIFO-sweep resting orders"]
    O --> Q{"Still below MM?"}
    Q -->|No| Done["Healthy — stop"]
    Q -->|Yes| P["liquidatePosition(user, closeQty)<br/>settle funding, realize PnL on closed slice"]
    P --> F{"Full close?<br/>closeQty at or above size"}
    F -->|Yes| Full["Delete position<br/>bad-debt path, guard skipped"]
    F -->|No| Part["Reduce position toward zero"]
    Part --> G{"Leftover balance at or below IM?"}
    G -->|Yes| OK2["Lands in the [MM, IM] band"]
    G -->|No| Rev["revert OverLiquidation<br/>(closed too much)"]
    Full --> BD["Bad debt (if any) absorbed by the insurance fund"]
```

There is no on-chain multi-user batch entry point. Keepers process accounts
individually: call `liquidateOrders(user, ids)` to clear that user's resting
orders, re-snapshot portfolio health, then call
`liquidatePosition(user, closeQty)` when the account remains underwater and
all venues are order-free. A race or recoverable revert affects only that
user and does not hide partial execution inside a successful batch receipt.

### Partial vs. full close

`liquidatePosition(user, closeQty)` settles owed funding, then:

- **Full close** (`closeQty ≥ |netQuantity|`, or `type(uint256).max`):
  realizes the entire PnL against the insurance fund, deletes the position,
  and **skips** the over-liquidation guard — this is the deep-underwater /
  bad-debt path where the keeper deliberately deleverages everything.
- **Partial close** (`closeQty < |netQuantity|`): realizes PnL only on the
  closed slice at the mark, reduces `netQuantity` toward zero (the
  `aggregatedEntryPrice` is unchanged), then applies the guard — with a
  real `IM > MM` buffer, the **leftover balance must be ≤ IM**, otherwise it
  reverts `OverLiquidation` (the keeper closed *more* than needed and should
  supply a smaller `closeQty`).

Preconditions checked at entry (reverting otherwise):

| Condition                              | Revert                 |
| -------------------------------------- | ---------------------- |
| `netQuantity == 0`                     | `NotLiquidatable`      |
| `Balance ≥ MM` (account is healthy)    | `NotLiquidatable`      |
| User still has resting orders          | `OrdersStillOpen`      |
| `closeQty == 0`                        | `InvalidSize`          |
| Partial close overshoots the IM buffer | `OverLiquidation`      |

### Bad debt

If a losing account cannot cover its realized loss, the shortfall is
**absorbed by the protocol insurance fund** — the fund receives less than
it is owed, and never draws from other users' collateral. The uncovered
amount is surfaced as `BadDebt(user, amount)` for off-chain observers.
Winning liquidations are paid out of the insurance fund; a partial close
that cannot be funded reverts `InsufficientReservePool`.

---

## Off-chain Keepers

A keeper is any off-chain service that:

1. **Monitors** account health (`balanceOf` vs `computePortfolioMM`) via
   the indexer / on-chain reads.
2. **Alerts** users approaching their maintenance threshold.
3. **Submits** `liquidateOrder` / `liquidatePosition` transactions for
   undercollateralized accounts, sizing the partial `closeQty` so the
   account lands back within the `[MM, IM]` band.

Because liquidation is permissionless, there is no privileged liquidator
role — anyone can run a keeper. Keeper incentives (`liquidationFee` payout)
are currently disabled.

---

## Events

```solidity
// Emitted per force-cancelled resting order (fee currently 0).
event OrderLiquidated(bytes32 indexed orderId, address indexed user, address indexed liquidator, uint256 fee);

// Emitted per position close (partial or full). positionSize is the SIGNED
// closed quantity; liquidatorFee is currently 0.
event PositionLiquidated(address indexed user, address indexed liquidator, int256 positionSize, int256 pnl, uint256 liquidatorFee);

// Emitted when an account's loss exceeds its collateral; `amount` is the
// shortfall absorbed by the insurance fund.
event BadDebt(address indexed user, uint256 amount);
```

---

## Read next

- [Collateral & Accounts](./02.Collateral-and-Accounts.md) — the shared vault and portfolio margin.
- [Fees](./06.Fees.md) — trading costs and the minimum taker fee.


---

# Contract Specifications

This document describes the technical specifications and parameters of HPDX Hashprice Futures contracts. Current values can be found in the Contract Specs section of the [HPDX Futures application](https://hashpower.exchange/futures).

---

## Understanding Contract Specifications

These are the key values that define how each futures contract works. You'll see these values displayed in the trading interface.

### Contract Specifications

| Parameter              | What It Means                                                                                                                                                            |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Contract Unit**      | The amount of hashrate each contract represents (measured in hashes per second).                                                                                         |
| **Margin Requirement** | The percentage of contract value you must deposit as collateral. This protects both parties if prices move against you. Higher requirements mean more capital locked up. |
| **Maturity Time**      | The time at which the contract matures and becomes settleable. Stored on-chain as `expirationAt`.                                                                           |

### Contract frequency

| Parameter                 | What It Means                                                                                                 |
| ------------------------- | ------------------------------------------------------------------------------------------------------------- |
| **Available Expirations** | How far into the future you can trade. For example, if set to 4 contracts, you can trade up to 4 weeks ahead. |
| **Expiration Interval**   | Interval between two closest expiration dates in days.                                                          |

### Pricing & Settlement

| Parameter               | What It Means                                                                                                                                               |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Settlement Currency** | The currency used for all payments (e.g., USDC). This is what you deposit as margin and receive during settlement.                                          |
| **Tick Size**           | The smallest price movement allowed. Orders must be placed at prices that are multiples of this step (e.g., if the step is \$0.01, you cant bid at \$0.015) |
| **Tick Value**          | The value of a one-tick move for a single contract.                                                                                                         |
| **Contract Size**       | One contract settles one unit of mark price (no duration multiplier). PnL = `mark × netQuantity − netEntryValue`.                                           |

### Fees & Limits

| Parameter           | What It Means                                                                                           |
| ------------------- | ------------------------------------------------------------------------------------------------------- |
| **Maker / Taker Fee** | Charged on fills. Maker fee may be zero by default.                                                   |
| **Max Open Orders** | Maximum resting orders per address and delivery date (`MAX_ORDERS_PER_PARTICIPANT_PER_EXPIRATION` = 100). Quantity is signed `int256` (no 127 cap). |

---

## Settlement Functions, Events & Errors

As of contract version `3.0.0`, futures are **cash-settled** unilateral aggregates. The table
below summarizes the settlement-related surface. See [Settlement](./05.Delivery-Settlement.md)
for the full lifecycle.

### Functions

| Function                                         | Status | What It Does |
| ------------------------------------------------ | ------ | ------------ |
| `settlePosition(address user, uint256 expirationAt)` | Active | Permissionless. Pins the expiry settlement price on first use, marks that user's aggregate to the pin, routes PnL via the insurance fund, clears the aggregate, emits `PositionSettled`. |
| `settlePositions(address[], uint256[])`          | Active | Batch of `(user, expirationAt)` pairs; lengths must match. |
| `recordSettlementPrice(uint256)`                 | Active | Permissionless pin of `getMarketPrice()` at/after maturity; idempotent; emits `SettlementPriceRecorded`. |
| `settlementPrice(uint256)`                       | Active | Pinned settlement price (`0` until recorded). |
| `getUserPosition(address, uint256)`              | Active | `{ netQuantity, netEntryValue }` for that user+expiry. |
| `getActiveExpirationDates(address)`                | Active | Expiries where the user still has a non-zero aggregate. |
| `getMarketPrice()`                               | Active | Live oracle mark used for pinning and margin. |

### Events

| Event | Status | Meaning |
| ----- | ------ | ------- |
| `PositionSettled(user, expirationAt, closedQuantity, pnl, settlementPrice, settledBy)` | Active | User's aggregate cash-settled at the pinned price. |
| `SettlementPriceRecorded(expirationAt, price, recordedBy)` | Active | Expiry settlement price pinned (once). |

### Errors

`settlePosition` reverts with `PositionNotExists()` (flat at that expiry) or
`PositionExpirationNotStartedYet()` (before `expirationAt`).
`recordSettlementPrice` reverts with `SettlementDateNotReached()` before `expirationAt`.

---

## Read next

- [Margin System](./03.Margin-System.md) - How collateral and liquidation work
- [Trading Guide](./04.Trading-Guide.md) - Creating and managing orders
- [Settlement](./05.Delivery-Settlement.md) - Cash settlement of positions at maturity


---

# Margin System

The HPDX Futures margin system ensures all positions are properly collateralized, protecting both parties from counterparty default risk.

> **Contract `3.0.0`.** Positions are unilateral aggregates per `(user, expirationAt)`.
> Portfolio IM/MM is computed by the Portfolio Margin Engine (PME). There is no
> `deliveryDurationDays` multiplier — each whole contract settles one unit of price.

---

## Overview

Margin is collateral deposited into the Collateral Vault used by Futures. The system
continuously monitors margin levels and automatically liquidates undercollateralized accounts.

```mermaid
flowchart TB
  B[Vault balance]
  B --> E[Excess margin<br/>withdrawable]
  E --> UP[Unrealized profit]
  UP --> MM[Maintenance margin<br/>minimum to avoid liquidation]
  MM --> UL[Unrealized loss if any]
  B -.->|Balance &lt; MM| L[Liquidatable]
```

---

## Margin Types

### 1. Deposited Collateral (Balance)

The actual token balance held in the vault for the participant.

### 2. Maintenance Margin (MM)

The **minimum collateral** required to hold open exposure without triggering liquidation.
Futures contributes order margin and position stress through the PME; per-contract notional
is price × quantity (no duration factor).

### 3. Initial Margin (IM)

The higher buffer the account should sit at after a partial liquidation. Keepers close
only enough exposure to restore the account into the `[MM, IM]` band when possible.

### 4. Unrealized PnL

For each aggregate:

```
pnl = mark × netQuantity − netEntryValue
```

Profitable exposure reduces effective margin pressure; losses increase it.

---

## Margin Examples

### Example 1: Opening a Long Aggregate

```
Balance:        100.00 USDC
Mark / entry:   4.10 USDC
netQuantity:    +1
netEntryValue:  4.10

Unrealized PnL = 4.10 × 1 − 4.10 = 0
```

Portfolio IM/MM come from the PME shocks applied to the account's net exposure.

### Example 2: Price Moves Against a Short

```
Entry:          4.10   (netQuantity = −1, netEntryValue = −4.10)
Mark:           4.12

pnl = 4.12 × (−1) − (−4.10) = −0.02 USDC   (loss)
```

If vault balance falls below MM, the account is liquidatable.

### Example 3: Price Moves In Favor of a Short

```
Entry:          4.10
Mark:           4.08

pnl = 4.08 × (−1) − (−4.10) = +0.02 USDC   (profit)
```

---

## Liquidations

### What Triggers a Liquidation?

A participant becomes liquidatable when portfolio margin reports:

```
Balance < Maintenance Margin
```

Liquidation is **permissionless**: any address can submit a liquidation transaction once
that condition holds.

> **Keeper incentives are currently disabled.** The `liquidationFee` parameter is retained
> in storage and still appears on liquidation events, but payouts are fixed at `0`.

### Liquidation Process

The **trigger** is an MM breach, but the **goal** is to restore the account to its IM buffer:
liquidation closes only as many contracts (across expiries) as needed to bring the balance
back to (at most) IM — not necessarily the whole book. The keeper selects the worst-first
subset off-chain.

```mermaid
flowchart TD
  A[Keeper: Balance &lt; MM] --> B[liquidateOrders user]
  B --> C{Still under MM?}
  C -->|no| D[Done]
  C -->|yes| E[liquidatePositions user<br/>expirationAts + closeQtys]
  E --> F[Close worst-first subset at mark]
  F --> G{Positions remain and IM &gt; MM?}
  G -->|balance &gt; IM| H[Revert OverLiquidation]
  G -->|balance ≤ IM or full close| I[Accept]
  I --> J[Bad debt if any → insurance fund]
```

`liquidatePositions(user, expirationAts[], closeQtys[])` closes the keeper-supplied set in
**one transaction** and reads margin **once** at the end: if any exposure remains and a real
`IM > MM` buffer exists, it reverts `OverLiquidation` when leftover balance ends up **above**
IM. A fully-closed account skips that guard (deep-underwater / bad-debt path). Zero-net or
unknown expiries in the batch are skipped. The single-expiry entry point
`liquidatePosition(user, expirationAt, closeQty)` remains available.

All entry points revert with `NotLiquidatable` when the target's `Balance ≥ MM`, and position
entry points revert `OrdersStillOpen` while resting orders remain.

---

## Off-chain Keepers

### Role of a Keeper

A keeper is any off-chain service that:

1. **Monitors** participant margin utilization
2. **Alerts** users when utilization exceeds warning thresholds
3. **Submits** `liquidateOrders` / `liquidatePositions` for undercollateralized accounts,
   sizing the worst-first close so the account lands back within the `[MM, IM]` band

Because liquidation is permissionless, there is no privileged validator role for this flow.

### Margin Utilization Calculation

```
Utilization = Min Margin / Balance × 100%

< 80%:    Safe
80-100%:  Warning (notifications sent)
≥ 100%:   Liquidatable by any keeper
```

---

## Best Practices

### For Traders

1. **Monitor Utilization**: Keep utilization below 80% to avoid warnings
2. **Add Buffer**: Deposit more than the minimum required
3. **Set Alerts**: Use the notification service for margin warnings
4. **Act Quickly**: Top up collateral immediately when warned

### For Miners (Sellers)

1. **Account for Volatility**: Hash prices can move significantly
2. **Conservative Sizing**: Don't over-commit production capacity
3. **Settlement Reserves**: Keep extra margin to cover an adverse mark at maturity settlement

### Utilization Guidelines

| Utilization | Status      | Action                    |
| ----------- | ----------- | ------------------------- |
| 0-50%       | Safe        | Normal operation          |
| 50-80%      | Moderate    | Monitor closely           |
| 80-95%      | Warning     | Consider adding margin    |
| 95-100%     | Critical    | Immediate action required |
| >100%       | Liquidation | Liquidatable by any keeper |

---

## Read next

- [Trading Guide](./04.Trading-Guide.md) - Managing orders and positions
- [Settlement](./05.Delivery-Settlement.md) - Cash settlement of positions at maturity


---

# Trading Guide

This guide explains how to trade on HPDX Hashprice Futures, from placing your first order to managing positions.

> **Contract `3.3.1`.** Orders carry signed whole-contract quantity. Positions are unilateral
> aggregates per `(user, expirationAt)`. Matching is a per-maturity limit order book (walk to
> limit, fill at maker price). `createOrder` / `createOrders` take an explicit time in force
> (GTC / IOC / FOK). Each side's net updates independently — no bilateral lots.

---

## Prerequisites

Before trading, ensure you have:

1. **Settlement Tokens**: USDC or the configured token in your wallet
2. **Wallet Connection**: Connected to the correct network (Base)
3. **Margin Deposit**: Collateral deposited in the Collateral Vault used by Futures

---

## Understanding Order Types

### Long Orders (Buy)

```
createOrder(price, expirationAt, +qty, tif)   // positive = buy / long
```

- Long exposure to hashprice, cash-settled at maturity
- Profit if settlement (or exit) is above entry

### Short Orders (Sell)

```
createOrder(price, expirationAt, -qty, tif)   // negative = sell / short
```

- Short exposure to hashprice, cash-settled at maturity
- Profit if settlement (or exit) is below entry

One placement creates **one** FIFO order node with `|qty|` contracts. Later placements at the
same price do **not** merge into the earlier node.

---

## Order Matching

Each maturity has its own book. An incoming limit order walks the opposite side from best
price toward its limit:

- **Buy**: match asks while `askPrice <= limit`; fill at each ask's price
- **Sell**: match bids while `bidPrice >= limit`; fill at each bid's price
- Unfilled size rests at the taker's limit (FIFO at that level)

```mermaid
flowchart LR
  subgraph book [Order book at one maturity]
    direction TB
    A1[Ask 0.15 Seller A]
    A2[Ask 0.14 Seller B]
    A3[Ask 0.12 Seller C]
    B1[Bid 0.12 Buyer X]
    B2[Bid 0.11 Buyer Y then Z]
    B3[Bid 0.10 Buyer W]
  end
  N1[New buy @ 0.14] -->|walks asks| A3
  N1 -->|then| A2
  N2[New sell @ 0.11] -->|FIFO at bid| B2
```

### FIFO Priority

```mermaid
flowchart LR
  Q1[Buyer A<br/>10:00] --> Q2[Buyer B<br/>10:05] --> Q3[Buyer C<br/>10:10]
  S[New sell @ 0.10] -->|fills first| Q1
```

### Self-trade (net-out)

If the next maker is yourself, quantities cancel against each other — no `OrderMatched`, no
fees. Any leftover on either side stays (or rests) as usual.

### Time in force

Every placement carries one: `createOrder` takes it as the last argument, and each leg of a
`createOrders` / `updateOrders` batch carries its own.

| TIF | Behavior |
| --- | -------- |
| **GTC** | Unfilled size rests on the book |
| **IOC** | Fill what is available now; cancel remainder (never rests); revert `TimeInForceNotFilled` if nothing fills |
| **FOK** | Fill entire size now or revert `TimeInForceNotFilled` |

GTD is not supported.

---

## Positions (aggregates)

After fills, each user holds at most one aggregate per maturity:

| Field | Meaning |
| ----- | ------- |
| `netQuantity` | Signed whole contracts (+long / −short) |
| `netEntryValue` | Sum of `price × signedFillQty` for open exposure |

```
unrealized / settlement pnl = mark × netQuantity − netEntryValue
```

There is **no** duration multiplier — one contract settles one unit of price.

---

## Closing Positions

### Method 1: Offset Before Maturity

Place an opposite order to reduce `netQuantity` toward zero:

```
Open:  +1 @ 4.10
Close: −1 @ 4.12

pnl = 4.12 − 4.10 = +0.02 USDC per contract
```

### Method 2: Cash Settlement at Maturity

Hold until `expirationAt`. Anyone may call `settlePosition(user, expirationAt)` (typically a keeper).
See [Settlement](./05.Delivery-Settlement.md).

---

## Order Limits & Fees

### Maximum Orders

Each address may have up to `MAX_ORDERS_PER_PARTICIPANT_PER_EXPIRATION` (100)
resting orders for each delivery date. Expired orders are outside this cap and
the active-order views without requiring cleanup. There is no per-order max qty
of 127 — quantity is a signed `int256` of whole contracts.

### Fees

Maker/taker fees are charged on fills (not on resting placements). Defaults may set maker fee to 0.

---

## Read next

- [Settlement](./05.Delivery-Settlement.md) — cash settlement at maturity
- [Event Design Spec](./06.Event-Desing-Spec.md) — `OrderMatched` / `PositionSettled`


---

# Settlement

This document explains how futures positions are cash-settled at maturity in HPDX Hashprice Futures.

> **Cash settlement model (contract `3.0.0`).** Positions are **unilateral aggregates** per
> `(user, expirationAt)`. There is no physical hashrate delivery, no escrow, no validator role,
> and no destination URL. At maturity, a single **settlement price** is pinned per expiration;
> each user's aggregate on that expiration is marked to that price independently, and PnL is
> routed through the insurance fund.

---

## Settlement Lifecycle

```mermaid
flowchart LR
  A[Order placed<br/>±qty, price, maturity] --> B[Matched FIFO<br/>updates both nets]
  B --> C[Aggregate held<br/>offset / liquidate]
  C --> D[Cash settled<br/>at / after maturity]
```

| Phase | When | What happens |
| ----- | ---- | ------------ |
| Order | Before maturity | Place signed-qty orders |
| Match | Before maturity | FIFO fills update each side's `(netQuantity, netEntryValue)` |
| Hold | Before maturity | Offset via opposite orders, or liquidate if underwater |
| Settle | At/after `expirationAt` | Permissionless mark-to-pinned-price |

---

## Before Maturity

Before an aggregate matures, the holder can:

- **Offset**: Place opposite orders to reduce `netQuantity` toward zero and realize PnL
- **Be liquidated**: Permissionlessly reduced via `liquidatePosition(user, expirationAt, closeQty)`
  if undercollateralized (orders must be cleared first)

No payment, escrow, or destination endpoint is required. The aggregate waits until
`expirationAt` and is then cash-settled.

---

## Settlement at Maturity

Once `block.timestamp >= expirationAt`, a user's non-zero aggregate at that expiry can be settled
at the expiration's **pinned settlement price**.

### Who Can Settle

Settlement is **permissionless**. Anyone — typically an off-chain keeper — can call:

```solidity
function settlePosition(address user, uint256 expirationAt) public;
```

To settle several user+expiry pairs in one transaction:

```solidity
function settlePositions(address[] calldata users, uint256[] calldata expirationAts) external;
```

Lengths must match (`ArrayLengthMismatch` otherwise). Keepers should pre-filter to pairs where
`block.timestamp >= expirationAt` and `getUserPosition(user, expirationAt).netQuantity != 0`.

### Settlement Price Pinning

All aggregates sharing the same `expirationAt` settle against **one** price, recorded the first
time anyone settles (or explicitly pins) that expiration at/after maturity.

```mermaid
sequenceDiagram
  participant Caller
  participant Futures
  participant Oracle

  Caller->>Futures: recordSettlementPrice(expirationAt)<br/>or first settlePosition
  alt price not yet pinned
    Futures->>Oracle: getMarketPrice()
    Futures->>Futures: settlementPrice[expirationAt] = mark
    Futures-->>Caller: SettlementPriceRecorded
  else already pinned
    Futures-->>Caller: reuse stored price
  end
  Caller->>Futures: settlePosition(user, expirationAt)
  Futures-->>Caller: PositionSettled(..., settlementPrice, ...)
```

Anyone can pin without settling:

```solidity
function recordSettlementPrice(uint256 expirationAt) external;
```

`recordSettlementPrice` reverts with `SettlementDateNotReached` before maturity and is
idempotent once set. Readable via `settlementPrice(expirationAt)` (`0` until recorded).

```solidity
event SettlementPriceRecorded(uint256 indexed expirationAt, uint256 price, address recordedBy);
```

### Collateral Is Held Until Settlement

A matured-but-unsettled aggregate still carries margin until settled. This prevents a losing
party from withdrawing collateral after maturity but before settlement. Once pinned, market risk
is frozen; on settlement the held collateral funds realized PnL.

### How PnL Is Computed

Each whole contract settles one unit of price (no duration multiplier):

```
pnl = settlementPrice × netQuantity − netEntryValue
```

Example — long 1 contract, entry 4.10, pinned settlement 4.30:

```
netQuantity   = +1
netEntryValue = 4.10
pnl = 4.30 × 1 − 4.10 = +0.20 USDC
```

Short of 1 at the same entry realizes −0.20. PnL routes through the **insurance fund**.
After settlement the user's aggregate at that `expirationAt` is cleared
(`netQuantity = 0`, removed from active dates).

### Settlement Event

```solidity
PositionSettled(user, expirationAt, closedQuantity, pnl, settlementPrice, settledBy)
```

Each call settles **one** user's aggregate. Settling Alice does not settle Bob — call once
per `(user, expirationAt)`.

### There Is No Settlement Window

A matured aggregate can be settled **any time** after `expirationAt`. There is no expiry window
or late-settlement penalty; it always settles at the pinned price.

---

## Complete Settlement Flow

```mermaid
sequenceDiagram
  participant Alice
  participant Bob
  participant Keeper
  participant Futures

  Note over Alice,Bob: Pre-maturity: Alice long +2, Bob short −2<br/>(independent aggregates, same expirationAt)
  Keeper->>Futures: recordSettlementPrice(expirationAt)
  Futures-->>Keeper: SettlementPriceRecorded
  Keeper->>Futures: settlePosition(Alice, expirationAt)
  Futures-->>Keeper: PositionSettled(Alice, ...)
  Keeper->>Futures: settlePosition(Bob, expirationAt)
  Futures-->>Keeper: PositionSettled(Bob, ...)<br/>same pinned settlementPrice
```

---

## Read next

- [Event Design Spec](./06.Event-Desing-Spec.md) - `PositionSettled` / `SettlementPriceRecorded`
- [Trading Guide](./04.Trading-Guide.md) - Offsetting before maturity

