Skip to content

Repository files navigation

SLAB — An order book for every grade

SLAB is a fully on-chain central limit order book for tokenized graded collectibles, built on Monad. Not one book — hundreds of thousands, executing concurrently, because that is what the asset class actually requires.

Bids are placed against a grade, not a token. Asks are placed against a specific card. Orders rest, cross, and cancel entirely on-chain — no sequencer, no off-chain matching engine, no operator who can reorder your fill.


Deployed — Monad Testnet (chain 10143)

Contract Address
OrderBook 0x515f7291d570abF63aB33CF7aB51384b3c155866
SeriesRegistry 0x8Fde7cA9D0F5e7dA335d8ACC33EE81a013658A8B
MockUSDC 0xf59101519884967EC2F430DA2B1EFb93d4aA3E49
MockCard 0x95EcE2347BC771378EB38aa5d43819CBDaaD9AB8

All verified on MonadVision and Monadscan. Nothing here is real money.


Getting started

Prerequisites

Frontend

git clone https://github.com/CodeBlocker52/slab.git
cd slab/web
npm install
cp .env.example .env.local     # fill in the values below
npm run dev

web/.env.example

NEXT_PUBLIC_PRIVY_APP_ID = YOUR_PRIVY_APP_ID
NEXT_PUBLIC_INDEXER_URL  = https://slab-indexer.onrender.com
NEXT_PUBLIC_MONAD_RPC    = https://testnet-rpc.monad.xyz

The hosted indexer is public, so the app runs against live testnet state without running one yourself. Point NEXT_PUBLIC_INDEXER_URL at http://localhost:PORT if you'd rather run indexer/ locally.

Contracts

cd slab/contracts
forge build
forge test

163 tests, 20 fuzz properties at 5,000 runs each. Everything should pass against a clean checkout with no network access.

contracts/.env.example — only needed to deploy or verify, not to build or test:

# Deployer key. Fund at https://faucet.monad.xyz
PRIVATE_KEY=0x0000000000000000000000000000000000000000000000000000000000000000
MONAD_TESTNET_RPC=https://testnet-rpc.monad.xyz

# Optional, only for `forge verify-contract`
MONADVISION_API_KEY=

Repository layout

contracts/   Solidity — OrderBook, SeriesRegistry, mocks, tests, deploy scripts
web/         Next.js app — market grid, depth ladder, terminal
indexer/     Rebuilds the ladder from three events; reconciles against chain state
maker/       Reference market maker — quotes, reprices, cancels across every book

The scale problem

Every order-book venue that exists runs a small number of markets. dYdX and Hyperliquid list a couple hundred perpetuals. Serum had a few hundred pairs. The NYSE and NASDAQ together list under six thousand securities.

Graded cards do not work like that.

Pokémon alone          ~30,000 distinct printed cards
× graders              PSA · BGS · CGC · SGC
× liquid grades        8 · 9 · 10
─────────────────────────────────────────────
                       ~270,000 tradable instruments

Add Magic, Yu-Gi-Oh, and sports and the number keeps going. That is roughly fifty times the combined listings of the NYSE and NASDAQ, and every one of them is a genuine instrument with its own supply, its own price, and its own book.

No venue on earth runs this. Not because order books are hard — they were solved decades ago — but because a hundred thousand simultaneous books on a serial execution environment all contend on the same state, and the entire exchange runs at the speed of its busiest market.

That is the problem SLAB exists to solve, and it is a Monad problem.


Why the instruments are real

The scale argument only holds if these are genuinely separate instruments rather than a hundred thousand unique items. They are, and this is the design insight the whole project rests on.

Graded cards are fungible within their grade. Grading is standardisation — the same function assaying performs for gold or grading performs for wheat. A PSA 10 Base Set Charizard is interchangeable with any other PSA 10 Base Set Charizard: same set, same card number, same grader, same numeric grade, thousands of units in circulation.

seriesId = keccak256(set, cardNumber, grader, grade)

  Base Set · #4 · PSA · 10
  Base Set · #4 · PSA · 9
  Evolving Skies · #215 · CGC · 10

A series is an instrument. An instrument gets a book. There are a hundred thousand of them.

Every existing platform treats these as unique NFTs, because that is what the token standard suggests. It is the wrong abstraction, and it is why a market doing hundreds of millions a month tolerates a spread nobody competes on.


The market as it exists

Tokenized collectibles moved roughly $290M in July 2026, up from $124.5M across the four main marketplaces in August 2025. Courtyard vaults graded cards with Brink's and mints on Polygon; Collector Crypt and Phygitals run comparable businesses on Solana; RIP.FUN is on Base.

Demand is proven. Market structure is not.

Every one of these is a listing board, and every one of them prices the same thing: immediacy.

Fee
RIP.FUN marketplace 2.5%
RIP.FUN instant buyback 85% of FMV, 48h
Courtyard buyback ~90% of FMV
eBay final value fee ~13%

Note the shape of it. A low fee if you will wait indefinitely for a listing to sell, a much worse one if you want out now. That gap is a bid-ask spread — set unilaterally by the platform, identical whether the asset has four thousand siblings or is a genuine one-of-one.

You do not fix that by charging less. You fix it by giving the instrument a book.


The asymmetry

Bids and asks are not symmetric here, and they should not be.

Bids are generic. A buyer wants a PSA 10 and does not care which, so a bid carries a seriesId and a quantity, and fills against whichever unit is cheapest.

Asks are specific. A seller owns card #8821 and can only sell that one, so an ask carries a tokenId and quantity is always 1.

        BIDS (aggregated by tick)              ASKS (individual cards)
        ─────────────────────────              ────────────────────────
        4,210.00   ×7    ████                  4,255.00   #8821
        4,205.50   ×3    ██                    4,260.00   #1174
        4,199.00   ×12   ███████               4,260.00   #9302
        4,180.00   ×4    ███                   4,299.00   #4417

                    ▲ spread 45.00 · 1.06%

The bid side collapses into price levels with size; the ask side is a queue of distinct assets. One percent, discovered by competition, against a 10–15% haircut set by a platform. That number is an outcome of the structure, not a fee schedule anyone promises to keep low.


Why Monad

Stated precisely, because the imprecise version does not survive scrutiny.

The argument that matters: a hundred thousand disjoint partitions

Two traders working different series touch entirely separate storage. Under Monad's optimistic parallel execution those transactions run concurrently. The venue scales with the number of instruments rather than serialising behind one contract.

The contracts are written for this deliberately:

  • Order IDs are per-series counters, never one global sequence — a global counter would serialise the whole exchange behind a single slot
  • No protocol-wide volume or fee accumulator anywhere in the matching path
  • Bitmaps and price levels are stored per series, so no two books share a tick structure

This is checkable, not asserted: a test records every storage slot written by two different books and asserts zero overlap.

What this is not a claim about

Being straight about this is worth more than overreaching:

  • Cheap on-chain cancels are not new. Serum did it on Solana in 2021. Sei, Injective and Hyperliquid are built around it. Cancel economics are necessary for SLAB and they are not a Monad discovery.
  • Sub-second blocks are not unique. Solana and Sei are both in the same range.
  • Storage-level parallelism is not unique either. Sealevel has done account-level parallelism since Solana launched.

So the honest form is a conjunction, not an impossibility

The asset is EVM-native. Vaulted-card NFTs are ERC-721s, and they are permissionless — anyone can build on them — but only from an EVM chain. Bridging to Solana would mean trading a wrapper of a redemption claim, which breaks the one guarantee that gives the asset value: burn the token, receive the physical card.

So SLAB needs EVM equivalence, and parallel execution across a hundred thousand partitions, and sub-cent cancels, and sub-second blocks, simultaneously.

Monad is the only chain at that intersection. That is a narrower claim than "only Monad can do this," and it has the advantage of being true.


Architecture

graph TB
    subgraph off["Off-chain"]
        MK["🤖 Makers<br/>quote across many series"]
        BY["👤 Buyers"]
        SL["👤 Sellers"]
    end

    subgraph on["Monad — one partition per series"]
        SR["SeriesRegistry<br/>tokenId → seriesId · tick size"]
        B1["Book · series A"]
        B2["Book · series B"]
        B3["Book · series C"]
        BN["… ~10⁵ more"]
    end

    VN["Vaulted card NFTs<br/>permissionless ERC-721"]

    VN --> SR
    SL --> B1
    BY --> B1
    MK --> B1
    MK --> B2
    MK --> B3
    B1 --> SR

    style B1 fill:#836EF9,color:#fff
    style B2 fill:#836EF9,color:#fff
    style B3 fill:#836EF9,color:#fff
    style BN fill:#B4A5FF,color:#000
Loading

Books never touch each other. That is the entire point — the diagram is the thesis.

Order lifecycle

sequenceDiagram
    autonumber
    participant B as Buyer
    participant OB as OrderBook (series N)
    participant SR as SeriesRegistry

    B->>OB: placeBid(seriesId, tick, qty, maxFills)
    OB->>SR: series registered and active?
    OB->>OB: escrow the FULL limit up front

    loop while askTick <= tick, up to maxFills
        OB->>OB: bitmap → lowest ask tick
        OB->>OB: step over cancelled orders
        OB->>OB: NFT → buyer, USDC → seller
        Note over OB: fill at the RESTING ask's price
    end

    alt quantity remains
        OB->>OB: set bit, push to FIFO queue
    end
    OB->>B: refund what fills and remainder did not need
Loading

Escrowing the full limit before matching means the contract never discovers mid-match that it cannot pay, so no fill can partially settle.

The tick bitmap book

Prices quantise to a per-series tick. Occupancy is tracked in a two-level bitmap: 256 words of 256 bits, plus one summary word marking which words are non-empty. Finding the best price is "find the lowest set bit".

The summary word is not an optimisation, it is a requirement:

Path Gas
Flat scan of 256 words — 256 cold SLOADs 2,073,600
With the summary word 2,488

That is the cold-start path, paid by the first bid into every series with no asks. At two million gas per empty book, seeding thousands of markets is impossible.

Operation Cost How
Insert O(1) Set bit, push to queue. No hint, no pointers.
Cancel O(1) One flag, refund, return.
Match Bounded Bitmap scan, fills up to caller-supplied maxFills.

Lazy cancellation

cancel sets one flag, refunds, and returns. It does not unlink, compact the queue, or clear the bitmap. Matching steps over dead orders and clears bits lazily as it walks past them, with the head pointer making that cost amortised.

Two reasons. Cancel is the most-executed write in the system, so it must be the cheapest. And Monad charges on gas_limit with no storage-clearing refund, so zeroing state to reclaim gas reclaims nothing and costs an extra slot write.

Contracts

Contract Responsibility
OrderBook.sol Per-series bitmaps, FIFO queues, matching, custody. No shared state between series.
SeriesRegistry.sol tokenId → seriesId, tick size, active flag. Owner-attested.

One custody contract, so escrow solvency is a single property over a single balance.

Events

Three events. The indexer rebuilds the entire ladder from them and nothing else.

OrderPlaced(seriesId, orderId, maker, tick, qty, isBid, tokenId)
OrderCancelled(seriesId, orderId, maker)
Fill(seriesId, bidId, askId, buyer, seller, tokenId, tick, price, fee)

Fill does not say which side was the taker — it does not need to. The contract emits every fill before its own OrderPlaced, so a fill always names one order already seen (the resting one) and one not yet seen (the taker). npm run reconcile proves the reconstruction matches on-chain state, and exits non-zero if it ever does not.

Errors

Custom errors only, no revert strings. The UI maps every one to human copy from a single shared source.

SeriesNotRegistered   SeriesInactive        TickOutOfRange
PriceNotOnTick        ZeroQuantity          ZeroMaxFills
NotOrderMaker         OrderAlreadyCancelled OrderFullyFilled
OrderNotFound         TokenNotAttested      TokenSeriesMismatch
FeeTooHigh            TransferFailed        SelfTrade*

Built with ❤️ for Monad Blitz New Delhi V4 Hackathon


About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages