> For the complete documentation index, see [llms.txt](https://gitbook.hashpower.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://gitbook.hashpower.io/06.event-desing-spec.md).

# Futures Event Design Spec

This document is the single source of truth for the redesigned `Futures.sol` events, their emission order per scenario, and the corresponding indexer entity operations.

> **Cash settlement (contract `2.15.0`).** Futures are cash-settled at maturity against a single per-expiration **settlement price**, pinned the first time the expiration is settled (or via `recordSettlementPrice`) and broadcast by `SettlementPriceRecorded`. The canonical settlement event is `LotClosed(..., reason=SETTLED)`, emitted by the permissionless `settlePosition` / `settlePositions`. The `BREACH` / `EXPIRED` close-reason ordinals are kept in the enum (never emitted) only so the indexer ABI stays stable.

***

## Proposed Solidity events

> `contract` is a Solidity reserved keyword. Event names use the `Lot` prefix; the internal struct remains `Position`. The off-chain indexer entity is `Lot`.

```solidity
// ── Order ──────────────────────────────────────────────────────────────────
event OrderCreated(                          // unchanged
    bytes32 indexed orderId,
    address indexed participant,
    string  destURL,
    uint256 pricePerDay,
    uint256 deliveryAt,
    bool    isBuy
);

enum OrderCloseReason { MATCHED, CANCELLED, EXPIRED, LIQUIDATED, RESET }

event OrderClosed(
    bytes32          indexed orderId,
    address          indexed participant,
    OrderCloseReason         reason          // new — eliminates deferred promotion
);

// ── Lot (replaces Position) ────────────────────────────────────────────────
event LotCreated(
    bytes32 indexed lotId,
    address indexed seller,
    address indexed buyer,
    uint256 pricePerDay,
    uint256 deliveryAt,
    bytes32 makerOrderId,   // resting order that was matched
    bytes32 takerOrderId    // synthetic taker (OrderCreated+OrderClosed(MATCHED) already fired)
);

event LotTransferred(
    bytes32 indexed oldLotId,         // frozen with all accumulated state
    bytes32 indexed newLotId,         // fresh lot, clean state
    address exitingParticipant,       // exits; session netQty--
    address newParticipant,           // enters; session netQty++
    int256  exitPnl,
    uint256 newPricePerDay,
    bytes32 makerOrderId,
    bytes32 takerOrderId
    // remaining party's session is NEVER touched — no netQty flicker
);

enum LotCloseReason { MUTUAL_EXIT, LIQUIDATION, BREACH, SETTLED, RESET, EXPIRED }
// SETTLED (ordinal 3) is the cash-settlement reason emitted by `settlePosition`.
// BREACH (2) and EXPIRED (5) are legacy physical-delivery reasons; never emitted now.
// Ordinals are pinned for indexer ABI stability even though the legacy reasons are dead.

event LotClosed(
    bytes32        indexed lotId,
    address        indexed seller,
    address        indexed buyer,
    int256                 sellerPnl,
    int256                 buyerPnl,
    address                closedBy,  // for SETTLED: the address (e.g. keeper) that called settlePosition
    LotCloseReason         reason
);

// ── Liquidation (renamed) ──────────────────────────────────────────────────
event LotLiquidated(
    bytes32 indexed lotId,
    address indexed participant,
    address indexed liquidator,
    uint256 fee            // keeper incentives disabled — currently always 0
);
// OrderLiquidated and Liquidation kept unchanged (OrderLiquidated.fee is also 0 for now)

// ── Settlement price pinning ───────────────────────────────────────────────
event SettlementPriceRecorded(
    uint256 indexed deliveryAt,    // expiration whose settlement price is pinned
    uint256         price,         // pinned mark price (token decimals), reused by all settlements
    address         recordedBy     // address that pinned it (keeper, winner, or anyone)
);
// Emitted once per expiration, the first time its settlement price is pinned —
// either via recordSettlementPrice(deliveryAt) or lazily by the first settlePosition.
```

***

## State paths and events

### Path 1 — Resting order (no match)

Trigger: `createOrder` with no opposite orders.

```
OrderCreated(orderId, participant, destURL, price, deliveryAt, isBuy)
```

Indexer: `Order{status: OPEN}` upserted. `OrderEntry(orderId): ACTIVE`.

***

### Path 2 — Order cancelled

Trigger: `closeOrder(orderId)`.

```
OrderClosed(orderId, participant, CANCELLED)
```

Indexer: `OrderEntry.status = CANCELLED`. `Order.qty--`.

***

### Path 3 — Order expired

Trigger: `removeOutdatedOrder(orderId)`. Permissionless; not auto-invoked from `createOrder` / `createOrders`. Callers compose multiple cleanups (and an optional placement) via the inherited `multicall(bytes[])`.

```
OrderClosed(orderId, participant, EXPIRED)
```

Indexer: `OrderEntry.status = EXPIRED`. `Order.qty--`.

***

### Path 4 — Fresh match (no existing positions)

A places `createOrder(P, D, +2)` — resting:

```
OrderCreated(ord_A1, A, P, D, isBuy=true)   => OrderEntry(ord_A1): ACTIVE; Order(tx,A,P,D,buy): qty=1
OrderCreated(ord_A2, A, P, D, isBuy=true)   => OrderEntry(ord_A2): ACTIVE; Order(tx,A,P,D,buy): qty=2
```

B places `createOrder(P, D, -2)` — matches both. Per unit, no deferral:

Unit 1:

```
OrderCreated(taker_B1, B, P, D, isBuy=false) => OrderEntry(taker_B1): ACTIVE; Order(tx,B,P,D,sell): qty=1
OrderClosed(taker_B1, B, MATCHED)            => OrderEntry(taker_B1): MATCHED  ← immediate, no deferral
OrderClosed(ord_A1,   A, MATCHED)            => OrderEntry(ord_A1):   MATCHED  ← immediate, no deferral
LotCreated(lot_1, seller=B, buyer=A, P, D, makerOrderId=ord_A1, takerOrderId=taker_B1)
    => Lot(lot_1): OPEN
    => Fill(tx, B, A, D, P): qty=1
    => PositionSession(B, D): netQty=-1
    => PositionSession(A, D): netQty=+1
    => Trade(tx,B) / Trade(tx,A): upsert
```

Unit 2 (same tx):

```
OrderCreated(taker_B2, B, P, D, isBuy=false) => OrderEntry(taker_B2): ACTIVE; Order(tx,B): qty=2
OrderClosed(taker_B2, B, MATCHED)            => OrderEntry(taker_B2): MATCHED
OrderClosed(ord_A2,   A, MATCHED)            => OrderEntry(ord_A2):   MATCHED
LotCreated(lot_2, seller=B, buyer=A, P, D, ord_A2, taker_B2)
    => Lot(lot_2): OPEN
    => Fill(tx, B, A, D, P): qty=2  ← same Fill (same tx, seller, buyer, D, price)
    => PositionSession(B, D): netQty=-2
    => PositionSession(A, D): netQty=+2
```

***

### Path 5 — Mutual exit

Context: A net -1 (seller in lot\_1 with buyer B). B places sell (resting), A places buy (taker). Both parties exit; no new lot created.

```
OrderCreated(taker_A1, A, P, D, isBuy=true)
OrderClosed(taker_A1, A, MATCHED)
OrderClosed(ord_B1, B, MATCHED)
LotClosed(lot_1, seller=A, buyer=B, sellerPnl, buyerPnl, closedBy=0, MUTUAL_EXIT)
    => Lot(lot_1): CLOSED, pnl recorded
    => PositionSession(A, D): netQty+=1 → 0
    => PositionSession(B, D): netQty-=1 → 0
    => Trade(tx,A) / Trade(tx,B): upsert with pnl
```

Replaces `PositionClosed + PositionExited(seller) + PositionExited(buyer)`. PnL always inline.

***

### Path 6 — Counterparty transfer exit

Context: A net -1 (seller in lot\_1 with buyer B). C places resting buy at P'. B places sell at P' (taker, exits long). A's session is NEVER touched.

```
OrderCreated(taker_B1, B, P', D, isBuy=false)
OrderClosed(taker_B1, B, MATCHED)
OrderClosed(ord_C1, C, MATCHED)
LotTransferred(
    oldLotId=lot_1, newLotId=lot_2,
    exitingParticipant=B, newParticipant=C,
    exitPnl=<B_pnl>, newPricePerDay=P',
    makerOrderId=ord_C1, takerOrderId=taker_B1
)
    => Lot(lot_1): REPLACED (all accumulated state frozen)
    => Lot(lot_2): OPEN { seller=A, buyer=C, sellPricePerDay=P, buyPricePerDay=P' }
    => PositionSession(B, D): netQty-=1 → 0
    => PositionSession(C, D): netQty+=1
    => PositionSession(A, D): NO CHANGE  ← no flicker
    => Fill(tx, B, C, D, P'): qty=1
    => Trade(tx,B) / Trade(tx,C): upsert
```

***

### Path 7 — Order liquidated

Trigger: `liquidateOrder` / `liquidateOrders` (permissionless — anyone can call once the participant is undercollateralized). Per order:

```
OrderClosed(orderId, participant, LIQUIDATED)
OrderLiquidated(orderId, participant, liquidator, fee)   // fee currently 0
```

Indexer: `OrderEntry.status = LIQUIDATED`. `Order.qty--`. The first `OrderLiquidated`/`LotLiquidated` event in a transaction bumps `Futures.totalLiquidations` (deduped via a `LiquidationTx` sentinel keyed by tx hash).

***

### Path 8 — Lot liquidated

Trigger: `liquidatePosition` (single lot) or `liquidatePositions` (batched close-to-IM subset) — both permissionless. Per closed lot, `_forceLiquidatePosition` first runs `_createOrMatchSingleOrder` for the counterparty (may produce path-4/5/6 sub-events), then cash-settles at the hashprice oracle price. A batch emits the block below once per lot in the supplied set:

```
[optional path-4/5/6 events for counterparty's new lot]
LotClosed(lotId, seller, buyer, sellerPnl, buyerPnl, closedBy=0x0, LIQUIDATION)
LotLiquidated(lotId, participant, liquidator, fee)   // fee currently 0
```

`sellerPnl`/`buyerPnl` = MTM on full duration (pre-delivery) or remaining undelivered portion (during delivery window).

Indexer: `Lot.status = CLOSED, reason = LIQUIDATION`. `PositionSession` netQty updated. As with path 7, the first liquidation event in a tx bumps `Futures.totalLiquidations` via the `LiquidationTx` sentinel.

***

### Path — Cash settlement at maturity (current)

Trigger: `settlePosition(lotId)` / `settlePositions(lotId[])`. **Permissionless** — anyone (typically a keeper) can call once `block.timestamp >= deliveryAt`.

The first settlement (or an explicit `recordSettlementPrice(deliveryAt)`) pins the expiration's settlement price and emits `SettlementPriceRecorded` before any `LotClosed`:

```
SettlementPriceRecorded(deliveryAt, price, recordedBy=msg.sender)   // once per expiration
LotClosed(lotId, seller, buyer, sellerPnl, buyerPnl, closedBy=msg.sender, SETTLED)
```

`sellerPnl`/`buyerPnl` = MTM on the **full** notional (`deliveryDurationDays`) against the expiration's **pinned `settlementPrice`** (every position on the same `deliveryAt` uses the same price). PnL is routed through the insurance fund. No escrow, no breach penalty, no settlement window — a matured position can be settled any time after maturity, always at the pinned price, and its collateral is held until then.

Indexer: `Lot.status = CLOSED, reason = SETTLED`. `PositionSession` netQty updated. `SettlementPriceRecorded` upserts a `FuturesExpiration(id = deliveryAt)` carrying `settlementPrice`, `settledAt`, and `recordedBy`.

***

### Path 9 — Admin reset

Trigger: `resetState(participants[])` (owner only).

Per order:

```
OrderClosed(orderId, participant, RESET)
```

Per lot (seen once; counterparty deduplication handled on-chain):

```
LotClosed(lotId, seller, buyer, sellerPnl=0, buyerPnl=0, closedBy=0x0, RESET)
```

No cash settlement. Escrowed funds for paid lots are noted as out-of-band in the contract comment.

Indexer: `OrderEntry.status = RESET`. `Lot.status = CLOSED, reason = RESET`. `PositionSession` netQty zeroed.

***

## Indexer entity model

| Entity              | Key                                      | Description                                                                                                                                                                                                                                |
| ------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `Order`             | `(tx, user, price, deliveryAt, side)`    | Aggregate of `createOrder` units by same user in one tx.                                                                                                                                                                                   |
| `OrderEntry`        | `orderId`                                | One per on-chain orderId (resting OR synthetic taker). Status set immediately on `OrderClosed`.                                                                                                                                            |
| `Lot`               | `lotId`                                  | One matched unit (qty=1). Replaces `Position`. Queryable by seller or buyer. Carries `status`, `closeReason`, `sellerPnl`, `buyerPnl`.                                                                                                     |
| `Fill`              | `(tx, seller, buyer, deliveryAt, price)` | Shared seller+buyer aggregate of `Lot` units with the same pair+price in one tx. Replaces per-user `Fill`.                                                                                                                                 |
| `PositionSession`   | `(user, deliveryAt)`                     | User's open-to-flat cycle. Tracks `netQty`, `realizedPnl`.                                                                                                                                                                                 |
| `Trade`             | `(tx, user, session)`                    | Per-user, per-tx volume aggregate.                                                                                                                                                                                                         |
| `LiquidationTx`     | `txHash`                                 | Sentinel created on the first `OrderLiquidated`/`LotLiquidated` of a tx; dedupes `Futures.totalLiquidations` so one tx counts as one liquidation.                                                                                          |
| `FuturesExpiration` | `deliveryAt` (encoded as `Bytes`)        | One per expiration. Lazily created when first referenced; carries the pinned `settlementPrice`, `settledAt`, `recordedBy`. Related from `Order`/`Lot`/`Trade`/`PositionSession`/`PriceLevel` via a backward-compatible `expiration` field. |

`Position` entity dropped — replaced by `Lot`.\
`Fill` redesigned: shared (seller, buyer) view. Query "my fills" = `Fill where seller=me OR buyer=me`.

***

## Event → entity mapping

| Event                                                | Entity operation                                                                                                                                                                 |
| ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `OrderCreated`                                       | Create `OrderEntry(ACTIVE)`; upsert `Order` aggregate (qty++)                                                                                                                    |
| `OrderClosed(MATCHED)`                               | `OrderEntry.status = MATCHED` — immediate, no deferral needed                                                                                                                    |
| `OrderClosed(CANCELLED\|EXPIRED\|LIQUIDATED\|RESET)` | `OrderEntry.status = ...`; `Order.qty--`                                                                                                                                         |
| `LotCreated`                                         | Create `Lot(OPEN)`; upsert `Fill(tx,seller,buyer,D,price)` qty++; open/update `PositionSession` for both sides; upsert `Trade`                                                   |
| `LotTransferred`                                     | Freeze old `Lot`; create new `Lot`; decrement session for `exitingParticipant`; increment for `newParticipant`; remaining party session **untouched**; upsert `Fill` and `Trade` |
| `LotClosed`                                          | Close `Lot`, record pnl; update sessions for seller and buyer; update `User.realizedPnl`                                                                                         |
| `LotLiquidated`                                      | Metadata (fee detail beyond `LotClosed`); first per-tx event creates `LiquidationTx(txHash)` and `Futures.totalLiquidations++`                                                   |
| `OrderLiquidated`                                    | Metadata (fee detail beyond `OrderClosed`); first per-tx event creates `LiquidationTx(txHash)` and `Futures.totalLiquidations++`                                                 |
| `SettlementPriceRecorded`                            | Upsert `FuturesExpiration(deliveryAt)`: set `settlementPrice`, `settledAt`, `recordedBy` (idempotent — first write wins)                                                         |

***

## What is eliminated

* **Deferred promotion**: `OrderClosed(reason=MATCHED)` fires immediately — no waiting for `LotCreated`
* **Ordering ambiguity**: `LotClosed` always fires last; PnL always inline
* **netQty flicker**: `LotTransferred` is atomic; remaining party's session never touched
* **State corruption on transfer**: `LotTransferred` freezes the old lot and creates a fresh lot\_2
* **`handlePositionExited` no-op**: eliminated; PnL always in `LotTransferred.exitPnl` or `LotClosed`
* **`Position` entity**: replaced by `Lot`
* **Per-user `Fill`**: replaced by shared (seller, buyer) `Fill` per (tx, seller, buyer, D, price)
* **Delivery escrow double-charge**: `_closeAndCashSettleDelivery` always uses `address(this)` for delivery payment
