Skip to main content

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; else R = ⌊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
RegionBitsRole
High nibble252..255Namespace (IdType + 1 for pool ids; 0xF0–0xF2 for ephemeral NFTs)
Low nibble248..251OptionType (0 = call, 1 = put) for pool ids

Namespace table​

High byte / prefixConstantWhat it isFirst id
0x10–0x11pairKey.id()Pair / book key in pairStatesEncoded from terms
0x20–0x21buyId()Fungible long inventorypairKeyId + (1 << 252)
0x30–0x31sellId()Short terms id (fungible supply unused)pairKeyId + (2 << 252)
0xF0…BUY_ORDER_ID_PREFIX = 0xF0 << 248Resting buy order NFTBUY_ORDER_ID_BASE = 0xF0…01
0xF1…SELL_ORDER_ID_PREFIX = 0xF1 << 248Resting sell order NFTSELL_ORDER_ID_BASE = 0xF1…01
0xF2…SHORT_LOT_ID_PREFIX = 0xF2 << 248Matched short-lot NFTSHORT_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​

IdCall (optionType = 0)Put (optionType = 1)
Pair key0x100x11
Buy0x200x21
Sell0x300x31

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 buyId and short-lot terms clear premium → one series id per strike / window / type (optionId / optionKey).
  • filledStates is 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
KindRange helperTerms live on
Buy order NFTisBuyOrderNftId → [0xF0…, 0xF1…)Order.tokenId (buy pool id)
Sell order NFTisSellOrderNftId → [0xF1…, 0xF2…)Order.tokenId (sell pool id)
Short lot NFTisShortLotNftId → ≥ 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​

  1. Prefer mothership resolvePairKey(clob, pairKey) so off-chain ids match on-chain packing (min-notional sentinels 0 / 0xFF are materialized before the id is packed).
  2. Indexers: classify holdings by high byte. 0x20/0x21 longs, 0xF0/0xF1 resting orders, 0xF2 shorts to settle. Do not split inventory by premium tick after fill.
  3. Writers settle lot NFTs (settleShort), not fungible sellId.
  4. Do not invent ephemeral ids by hand; mint counters live on the clone.