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 resting bids or asks, no book to infer intent from. The order panel instead shows the vault’s two-sided quote for the size you enter — Buy at the ask, Sell at the bid, after size-dependent impact — which is exactly the price the contract will fill at.
  • 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.

Limit and stop orders, take-profit and stop-loss

Besides market orders, the ticket places limit and stop entries, and any position can carry a take-profit and a stop-loss — set when you open it or later from the positions table. Resting orders live on-chain and are listed under Open orders, where you can re-price or cancel them.

Nothing on-chain executes them by itself. Two automation bots watch the price against every resting order and open position, and when a level is reached they request a freshly signed price through WhitespaceTradesUpKeep, which only registered forwarders may call. Those fills therefore run through the same two phases as a market order:

  • A limit order fills at your price or better. If the price after spread and impact is worse than your limit when the report arrives, that attempt is cancelled and the order keeps resting. A fast move can fill you better than the limit — it is checked every few seconds, not matched in a book.
  • A stop order fills at the market once triggered. It triggers when the oracle price crosses your level, and then fills at the price of that moment, which in a fast move can be worse than the stop.
  • Take-profit and stop-loss close the position once the price reaches their level. A stop-loss, like a stop entry, can close beyond its level in a fast move.

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) the order gets a Reclaim collateral button under Open orders, which calls openTradeMarketTimeout for it and returns the collateral.

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 WhitespaceTrading.sol PERCENT_BASE = 100e2 and the check in TradingCallbacksLib.sol); CancelReason from IWhitespaceTradingCallbacks.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

The terminal shows a liquidation price — in the order ticket before you open, and on every position — read from the contract’s own liquidation-price views (getTradeLiquidationPrice for an open position) rather than recomputed in the browser. It is a useful number, but it is not what the contract checks. The on-chain test 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.

Treat the liquidation price as a snapshot

The liquidation path never calls getTradeLiquidationPrice; it runs the value test above, and the two can disagree right at the boundary. Funding and rollover keep moving the real level while you hold. Keep a margin of safety rather than trading to the last tick of the number shown.

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).

Who liquidates

Two independent automation bots evaluate every open position against the value test. One that fails it is submitted through WhitespaceTradesUpKeep and settles at a freshly signed price, like any close. That entry point is gated to registered forwarders rather than being permissionless, so no third party can liquidate — or step in if both bots stop.

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 its healthy-source minimum — 3 for a market the major exchanges all quote — 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 minimum is per market, not global. A market that only one exchange lists carries a lower one, met by that exchange’s separate spot and perpetual books so the two still cross-check each other. Such a market is listed with a lower leverage ceiling and a smaller open-interest cap to match the narrower base its price rests on. The banner and the order ticket always name the requirement the market you are looking at is actually judged by.

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 fee—Flat, per price request. Taken from your margin when a position opens, and kept even when an order is cancelled — this is the “minus the oracle fee” in every refund.
Opening fee · maker—Charged instead of the taker rate on the part of a trade that narrows the gap between long and short open interest, up to the market’s maker leverage limit.
Opening fee · taker—Charged on the rest of the notional when a position opens.
Rollover—Accrues every block on an open position and is settled when it closes. No rate is published, but what each position has accrued is shown in its Funding column, and what was charged is listed in Funding history.
FundingPer market, liveFlows between longs and shorts every block; a positive rate means longs pay shorts. The terminal header shows each market’s current rate per hour, read from the contract; positions show what they have accrued, and Funding history what was charged.

Fees come out of your margin

The opening fee and the oracle fee are deducted from the collateral you send before the position opens, and the leverage applies to what is left. So the position is (collateral − fees) × leverage: at 100× a 0.03% fee is 3% of the margin, and a ticket for 1 BTC opens about 0.97 BTC. The ticket shows the size that opens after fees next to the size you typed.

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. The ticket’s quote applies the same formula, so the Buy and Sell prices it shows for your size are what the contract would fill at that moment; the unrealised PnL on open positions is still an estimate at the live mark, before closing fees, funding and rollover.

Source: IWhitespacePairsStorage.pairOracleFee and IWhitespacePairInfos.pairOpeningFees (both PRECISION_6), read live; maker/taker split in WhitespacePairInfos._getBaseOpeningFee; fee deduction in TradingCallbacksLib.executeRegisterTrade; docs/decisions/phase-6-liquidator.md §2.3 (rollover/funding accrual).

07

The contracts you are actually trading against

Deployed to whitechain-testnet-op (chain 1874) on 2026-09-28 from commit 8429c02d30. 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

Limits of this testnet

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, limit and stop orders, with TP/SL — nothing more. TWAP and other algorithmic orders do not exist in the contracts, and margin is isolated only: there is no cross margin.
  • Nothing here has been audited. No third-party audit has been performed. The contract tests, fuzz tests included, run in CI on every push and the invariant suites on their own profile, but no static analysis runs in CI.
  • The contracts are run with plain admin keys. Testnet roles are not behind a multisig or a timelock: the team can change parameters, pause, upgrade or redeploy at any time.
  • Live updates are polled, not pushed. The WebSocket is backed by a poll, so there is no latency floor below the poll interval.
  • Points are a testnet score. Points Season One counts your activity; points have no monetary value and may count toward a possible future airdrop, with nothing guaranteed — see the Terms. There is no referral programme.

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).

09

Community & support

Announcements, the community chats and bug reports all live outside this site. If something here is wrong or broken, the bug form is the fastest way to reach the team.