ERC-6909 token id namespaces
Every Opshuns CLOB clone is an ERC-6909. Token ids use a fixed bit layout (not a hash): pool ids encode option terms bijectively; ephemeral NFTs use reserved meta prefixes with their own counters.
Source of truth in core: OpshunsClobTypes.sol / OpshunsClob/README.md. Book overview: CLOB (OpshunsClob). Premium ticks: Premium ratio.
Bit layout (256 bits)
| meta (8) | minNotionalBits (8) | expiry (32) | startExercise (32) | premiumTick (32) | strikeWad (144) |
255────248 247────────────240 239──────208 207────────────176 175───────────144 143────────────────────────────0
premiumTick(int32, packed as two’s complement):0= zero premium; elseR = ⌊1e18 × 1.0001^tick⌋.strikeWad(uint144): strike WAD ratio (must be non-zero for books that use strike).
Settlement uses WAD: premium_wei ≈ amount × R / 1e18, strikeCost ≈ amount × strikeWad / 1e18. See Premium ratio.
Meta byte
meta = ((IdType + 1) << 4) | optionType
| Region | Bits | Role |
|---|---|---|
| High nibble | 252..255 | Namespace (IdType + 1 for pool ids; 0xF0–0xF2 for ephemeral NFTs) |
| Low nibble | 248..251 | OptionType (0 = call, 1 = put) for pool ids |
Namespace table
| High byte / prefix | Constant | What it is | First id |
|---|---|---|---|
0x10–0x11 | pairKey.id() | Pair / book key in pairStates | Encoded from terms |
0x20–0x21 | buyId() | Fungible long inventory | pairKeyId + (1 << 252) |
0x30–0x31 | sellId() | Short terms id (fungible supply unused) | pairKeyId + (2 << 252) |
0xF0… | BUY_ORDER_ID_PREFIX = 0xF0 << 248 | Resting buy order NFT | BUY_ORDER_ID_BASE = 0xF0…01 |
0xF1… | SELL_ORDER_ID_PREFIX = 0xF1 << 248 | Resting sell order NFT | SELL_ORDER_ID_BASE = 0xF1…01 |
0xF2… | SHORT_LOT_ID_PREFIX = 0xF2 << 248 | Matched short-lot NFT | SHORT_LOT_ID_BASE = 0xF2…01 |
Valid pool metas are 0x10–0x31. Zero-premium and premium markets share these metas (zero premium is premiumTick == 0 in the payload, not a separate meta band). decodeTokenId rejects high nibble 0, high nibble past Sell, or meta >= 0xF0 (InvalidTokenPrefix).
Concrete pool meta bytes
| Id | Call (optionType = 0) | Put (optionType = 1) |
|---|---|---|
| Pair key | 0x10 | 0x11 |
| Buy | 0x20 | 0x21 |
| Sell | 0x30 | 0x31 |
Same lower 248 bits (strike, premium tick, timestamps, minNotionalBits); only the meta nibble differs:
pairKey.id() = 0x1_ | optionType | terms…
buyId() = 0x2_ | optionType | terms…
sellId() = 0x3_ | optionType | terms…
buyIdFromPairKeyId / sellIdFromPairKeyId add 1 << 252 / 2 << 252 to a cached pairKey.id() instead of re-packing terms.
Filled inventory shares one bucket
Resting book ids keep the full premiumTick in the lower 248 bits (each premium level is its own pairKeyId / FIFO queue).
Filled inventory does not:
- Fungible long
buyIdand short-lot terms clear premium → one series id per strike / window / type (optionId/optionKey). filledStatesis keyed by that series id.- Therefore a zero-premium fill (
premiumTick == 0) and a premium fill of the same strike / exercise window land in the same long / short inventory bucket on a given clone.
Indexers and portfolio UIs should not expect separate ZP pool metas (0x40–0x61 are gone). Treat ZP as premiumTick == 0 on the shared 0x10–0x31 metas.
Ephemeral NFT namespaces
Queued orders and matched shorts are not PairKey encodings. Each namespace has its own sequential counter:
BUY_ORDER_ID_PREFIX = 0xF0 << 248
BUY_ORDER_ID_BASE = BUY_ORDER_ID_PREFIX + 1 // sequentialBuyOrderId
SELL_ORDER_ID_PREFIX = 0xF1 << 248
SELL_ORDER_ID_BASE = SELL_ORDER_ID_PREFIX + 1 // sequentialSellOrderId
SHORT_LOT_ID_PREFIX = 0xF2 << 248
SHORT_LOT_ID_BASE = SHORT_LOT_ID_PREFIX + 1 // sequentialShortLotId
| Kind | Range helper | Terms live on |
|---|---|---|
| Buy order NFT | isBuyOrderNftId → [0xF0…, 0xF1…) | Order.tokenId (buy pool id) |
| Sell order NFT | isSellOrderNftId → [0xF1…, 0xF2…) | Order.tokenId (sell pool id) |
| Short lot NFT | isShortLotNftId → ≥ 0xF2… | ShortLot.tokenId (typically sellId()) |
isEphemeralNftId is true for any id ≥ 0xF0…. Counters are shared across premium and zero-premium books on a clone.
Short inventory
Matched sells mint a 0xF2… short-lot NFT, not a fungible sellId balance (totalSupply(sellId) stays 0). Settle is burn-settle via mothership settleShort(lotId):
- Pre-expiry: only when the lot is fully assigned → pay strike, burn, emit
StrikeClaimed - Post-expiry: pay strike for assigned + underlying for unassigned → burn, emit
SellWithdrawn
Mental model
PairKey (terms: strikeWad + premiumTick + window + minNotional)
│
├─ id() → book key in pairStates (premium tick kept for resting levels)
├─ buyId() → long ERC-6909 balance (fungible; filled inventory clears premium)
├─ sellId() → short terms id (fungible supply unused; shortLots.tokenId)
└─ ids() → (id, buyId, sellId) from one pack + meta nibble patch
Resting buy orders → Order NFT (`0xF0…`), sequentialBuyOrderId
Resting sell orders → Order NFT (`0xF1…`), sequentialSellOrderId
Matched short inventory → ShortLot NFT (`0xF2…`), sequentialShortLotId
decodeTokenId(pool id) → (IdType, PairKey) // lossless; reverts on 0xF0–0xF2
Premium books also maintain a tick bitmap ladder keyed by optionKey (PairKey with premium cleared) for cross-premium discovery. See CLOB.
Integrator tips
- Prefer mothership
resolvePairKey(clob, pairKey)so off-chain ids match on-chain packing (min-notional sentinels0/0xFFare materialized before the id is packed). - Indexers: classify holdings by high byte.
0x20/0x21longs,0xF0/0xF1resting orders,0xF2shorts to settle. Do not split inventory by premium tick after fill. - Writers settle lot NFTs (
settleShort), not fungiblesellId. - Do not invent ephemeral ids by hand; mint counters live on the clone.
Related
- CLOB (OpshunsClob): book vs bilateral
fillMatched, bitmap ladder - Call the CLOB directly. posting from your own stack
- Premium ratio:
premiumTickandstrikeWad - Portfolio: how the app surfaces holdings