Protocol

How Whitespace works

Whitespace is a perpetual-futures exchange on Whitechain testnet 1874. You trade against a shared liquidity vault at a price signed by an off-chain oracle — there is no order book and no counterparty to find. This page explains what that means for your money, including the parts that are awkward. Every figure marked in bold below is read live from the deployed contracts as you load the page.

01

A vault, not an order book

On a normal exchange your counterparty is another trader, matched against you in a book. Here it is a shared LP vault. Depositors put USDW into the vault; the vault takes the other side of every position, and the price you open and close at comes from an oracle rather than from whoever happens to be quoting. When you win, the vault pays you. When you lose, the vault keeps your loss.

Why there is no book

A genuinely on-chain order book is not a matter of writing one. The only production example, Hyperliquid, needed a bespoke consensus protocol, a matching engine inside chain state, ~200,000 orders/sec, and a mempool that understands order-book semantics well enough to sort cancels ahead of aggressive orders. None of that is reachable as ordinary EVM contracts on a one-second-block chain with a public mempool — which is why even on Arbitrum, no perp team keeps the book on-chain.

The decisive argument is simpler than the technical one: a book with no market makers is an empty screen. This is an independent project with no market-making relationships, on a chain measured at 0.4 transactions per block. The vault model exists precisely because it produces tradeable liquidity with zero market makers — liquidity is bought with vault capital instead of negotiated with a desk.

What that costs you, stated plainly

  • No price discovery. The protocol is a price taker. It cannot disagree with its oracle, so oracle correctness is existential rather than merely important — see section 05.
  • Your size is bounded by the vault, not by demand. Open-interest caps exist because the vault has to be able to pay everyone who is winning at once. An order that would breach the cap is cancelled with EXPOSURE_LIMITS.
  • There is no depth to read. No bids, no asks, no book to infer intent from. The depth panel in the terminal says so rather than drawing a plausible-looking ladder.
  • LPs absorb trader PnL. If you are an LP, you are the house — fees, spread and funding are what have to cover traders’ winnings over time.

Source: design spec §1 (counterparty model), §3.2 (why not an order book), §3.3 (accepted costs).

02

Your order happens in two phases

This is the single most important mechanic on this exchange, and it is the one that most often surprises people: when your transaction confirms, you do not have a position. You have a request.

Phase 1
You request — wallet-signed, on-chain
openTrade validates your leverage against the market cap, checks the open-interest caps, takes your collateral, stores a pending order, and asks the price router for a price. That emits a PriceRequestedV2 event. No price has been applied to anything yet.
Phase 2
A keeper delivers a signed price report
An allowlisted keeper watches for that event, fetches a freshly signed report from the price publisher, and calls performUpkeep with it. The on-chain verifier checks the signatures; the trading callbacks then apply spread and price impact to arrive at your execution price.
Outcome
Filled, or cancelled and refunded
If the execution price is inside your slippage tolerance and every other check passes, the position opens. Otherwise the order is cancelled and your collateral is returned minus the oracle fee (currently ). Closing a position runs through the identical two phases — “close requested”, then closed.

Why it is built this way

You commit before the price is known, and the execution price comes from a report signed after your request. That ordering is the whole defence. Single-phase execution against a previously-stored oracle price is directly exploitable: watch for the oracle lagging spot, then open at the stale price and close into the correction. That is the GMX v1 AVAX exploit class, and it has cost real protocols real money.

If no keeper ever shows up

Your collateral is not stranded, but it is not instant either. After (~1 second per block on this chain) you can reclaim it yourself by calling openTradeMarketTimeout for that order.

Known gap — a dead window

A signed report is only deliverable for after its timestamp, but the timeout that lets you reclaim collateral is blocks. Between those two points there is a window — roughly 20 blocks — where an order can no longer be filled by anyone but is not yet refundable. It is a usability problem rather than a safety one: the collateral is recoverable, just not immediately.

Source: design spec §5.1; docs/decisions/phase-2-oracle-hardening.md §3, §5, §9; docs/decisions/phase-3-price-publisher.md §1–2; docs/decisions/phase-4-indexer-api.md §7.

03

Slippage is your only price protection

On a spot DEX, slippage tolerance is a convenience — you can already see the price. Here it is different in kind. Because you commit before the price exists, your slippage tolerance is the only thing standing between you and an execution price you would not have accepted. The terminal therefore shows it as a first-class control rather than hiding it in an advanced panel, and defaults it tight: 0.50%.

The check itself compares the execution price against the price you asked for, with a tolerance of slippageP basis points. If the report puts execution outside that band, the callback does not fill you at a worse price — it cancels:

CancelReason.SLIPPAGE -> position not opened -> collateral returned to you -> minus the oracle fee

Two things worth internalising. First, the execution price is not the raw reported price: spread and price impact are applied first, and it is the result that is measured against your tolerance. Second, a cancellation is a normal outcome, not a failure — it is the system doing exactly what you asked it to do when the price moved.

Source: design spec §5.1; docs/decisions/phase-5-frontend.md (slippage unit verified against OstiumTrading.sol PERCENT_BASE = 100e2 and the check in TradingCallbacksLib.sol); CancelReason from IOstiumTradingCallbacks.sol.

04

Isolated margin, and how liquidation actually decides

Margin is isolated, not cross. Each position carries its own collateral, and a losing position can take that collateral and nothing else. A second position in another market is not exposed to it, and neither is your wallet balance.

The trigger is a value test, not a price

Most exchanges give you a liquidation price. This one does not, and the reason is worth stating rather than papering over. The on-chain check is not a price comparison at all — it evaluates your position’s current value against a margin floor:

tradeValue = collateral + collateral * percentProfit / 1e6 / 100 - rolloverFee - fundingFee (floored at 0) liqMarginValue = collateral * liqMarginThresholdP * leverage / maxLeverage / 100 liquidated when tradeValue < liqMarginValue (strictly less)

liqMarginThresholdP is currently , and it is governance-mutable — it can change without your position changing. Note the strict <: a position sitting exactly at the threshold is not liquidatable.

Both rolloverFee and fundingFee accrue continuously against your position, which means your liquidation level moves even when the price does not. That is the deeper reason a single “liquidation price” is misleading here: it is a snapshot of a moving quantity.

Why the Liq. column is a dash

The contract does expose a getTradeLiquidationPrice view — but the liquidation path never calls it, and it disagrees with the real predicate exactly at the boundary, where it matters. Showing its output as “your liquidation price” would be presenting an approximation as a guarantee, so the terminal and the portfolio both show an explained dash instead.

What protects you from a single bad tick

The mark price used for risk is an EMA of the index, not the latest tick, specifically so that one outlier print cannot cascade a book of positions into liquidation. Liquidations also run through the same two-phase signed-price flow as your own close — there is no faster path for the liquidator than there is for you — and the liquidator suppresses all new submissions whenever the oracle is degraded (see section 05).

Not currently operational on testnet 1874

The repo’s phase-6 record (docs/decisions/phase-6-liquidator.md §2.6) states that the automation upkeep liquidations execute through, OstiumTradesUpKeep, is not deployed on this chain — and that its entry point is gated to an allowlisted forwarder rather than being permissionless, so no third party can substitute for it. Treat automatic liquidation as not yet proven on this deployment. This has not been independently re-probed by this page.

Source: docs/decisions/phase-6-liquidator.md §2.1–2.6, §4; docs/decisions/phase-5-frontend.md (isolated margin ruling); design spec §5.2 (mark = EMA of index).

05

The price is signed by k of N

Everything above rests on the price being right, so this is the part of the system with the most defence built into it. A price only becomes usable on-chain if enough independent signers have signed it and the number itself survives a set of contract-level sanity rails. Those are two different checks on two different assumptions, and neither substitutes for the other.

Layer 1 — threshold signatures

Signatures required (k)
Authorised signers (N)
Report max age
Max deviation vs last price

The signers sign one hash over a fixed payload: the chain id, the verifier’s own address, the feed id, a timestamp, and the price, bid and ask at 18 decimals. Putting the chain id and verifier address inside the signed bytes is what makes a report from one chain worthless on another — testnet and mainnet run the same contracts with the same signer set, and without this a testnet report would be a mainnet report.

The verifier requires the recovered signer addresses to be strictly ascending. That is not tidiness: it is what stops one compromised key from reaching the threshold by submitting the same signature k times. And a single unauthorised signature rejects the entire report — it is not skipped and counted as one fewer.

Layer 2 — rails that do not care who signed

Threshold signatures defend against key compromise. They are powerless against the case where every signer is honest and they all receive the same wrong input — all N will faithfully sign a falsehood. The deviation rail catches exactly that, because it does not ask who signed, it asks whether the number is plausible against the last accepted one. Alongside it sit a staleness bound, a per-feed circuit breaker, and a global pause. The pause is a hot-key emergency stop: a guardian can stop the system immediately, but only governance can restart it.

A pause stops closes too

The emergency stop halts new orders, closes and liquidations alike. That is intended behaviour for a circuit breaker, but it means “you can always exit” is not a promise this system makes.

Where the price comes from before it is signed

The publisher ingests four venues — Binance, Bybit, OKX and WhiteBIT — and takes each one’s mid of best bid and ask, never last trade, because a last trade can be moved with one cheap order. A venue is dropped if its data is stale, if its spread is too wide, or if it disagrees with the median of the others by too much. The surviving venues produce the index; the mark is an EMA of it.

Below 3 healthy venues the market goes degraded, and the policy is asymmetric on purpose: closes are allowed, opens are blocked. If the price cannot be trusted, letting people out is the lesser risk; letting new positions in against an unreliable index is not.

The degraded signal has a blind spot

GET /price/:pairIndex answers from one of two sources and says which. When the publisher answers, it carries the healthy-venue list and the degraded flag, and the trading form blocks opening on it. When the publisher is unreachable the API falls back to the chain’s last settled price, which carries no venue health at all — healthyVenues and degraded come back null rather than being guessed. The publisher’s own refusal to sign an open still holds in that mode, but the front-end gate cannot fire on a signal it is not receiving. Check source if it matters to you.

Source: design spec §5.2, §6.2–6.4; docs/decisions/phase-2-oracle-hardening.md §1–3; docs/decisions/phase-3-price-publisher.md §1, §3–4. Live values read from WhitespaceVerifier and WhitespacePriceUpKeep at the addresses in section 07.

06

What you are charged

FeeLive valueWhen
Oracle feeFlat, per price request. Kept even when your order is cancelled — this is the “minus the oracle fee” in every refund.
Opening fee · makerCharged on the notional when a position opens.
Opening fee · takerSame field group. The landing mockup’s 0.035% is a design figure and is not what this market is configured with.
RolloverAccrues continuously on an open position and is deducted from tradeValue at settlement. The accrual is real; no endpoint publishes the rate, so none is shown.
FundingSame treatment. The terminal’s FUNDING readout is a dash for the same reason, not because funding is zero.

Separately from fees, spread and price impact move your execution price away from the reported one. They are applied on-chain before your slippage tolerance is checked, and this app does not reproduce that arithmetic — which is why the unrealised PnL shown on your positions is labelled an estimate rather than a payout.

Source: IOstiumPairsStorage.pairOracleFee and IOstiumPairInfos.pairOpeningFees (both PRECISION_6), read live; docs/decisions/phase-6-liquidator.md §2.3 (rollover/funding accrual); docs/decisions/phase-5-frontend.md (fee-honesty ruling).

07

The contracts you are actually trading against

Deployed to whitechain-testnet-op (chain 1874) on 2026-09-08 from commit 22e1a5cbac. These addresses are read from deployments/1874.json at build time, so this table cannot drift from what the app itself calls.

RPC: https://rpc.testnet.whitechain.io. Each address links to explorer.testnet.whitechain.io, confirmed to index this chain by querying it for the vault’s own creation transaction — nothing in this repo configures an explorer, so it was checked rather than assumed. Collateral is USDW, 6 decimals; the vault issues 6-decimal shares against it.

08

What is not live

This is a testnet deployment and it is not finished. The list below is the honest inventory, not a roadmap.

  • The collateral is not money. USDW is a faucet token with no supply cap — anyone can mint as much as they like. Nothing traded here has value.
  • Markets are still loading. The markets list is read from the API and is never padded out with placeholder rows.
  • Market orders only. Limit and stop orders exist as on-chain order types, but the UI to place, list and cancel resting orders was not built. TWAP does not exist in the contracts at all.
  • No liquidation price is shown, anywhere — see section 04 for why that is a decision rather than an omission.
  • No funding rate is published by any endpoint, so the terminal shows a dash rather than a number.
  • Partial closes are missing from history. The indexer only writes a history row when a position closes fully, so a partially-closed position stays in your open table until the remainder is closed.
  • Live updates are polled, not pushed. The WebSocket is backed by a poll, so there is no latency floor below the poll interval.
  • There is no points programme and no referral programme. See Points for the full statement; the referral share in the landing mockup describes something that was cut from scope.
  • Nothing here has been audited. No third-party audit has been performed, and static and fuzz analysis are not yet wired into CI.

Source: docs/decisions/phase-0-1.md §5–6; phase-2-oracle-hardening.md §7, §10; phase-4-indexer-api.md §6; phase-5-frontend.md (design-honesty rulings).