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.
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.
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.
EXPOSURE_LIMITS.Source: design spec §1 (counterparty model), §3.2 (why not an order book), §3.3 (accepted costs).
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.
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.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.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.
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:
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.
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.
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:
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.
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 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:
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.
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.
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).
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).
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.
| 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.
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.
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.
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.
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.
| Fee | Live value | When |
|---|---|---|
| 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. |
| Funding | Per market, live | Flows 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. |
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).
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.
| Contract | Address |
|---|---|
| registry | 0x86d4EC32F8ed6e07D6176b71De05c9866A30c357 |
| tradingStorage | 0x05122cf5B6B3C44e2Ffb4B20C376498E4776FC47 |
| pairsStorage | 0x2e5CcD43667fCE72e3EDE9Af5E3E1A10DBBA985A |
| pairInfos | 0x688C4166B54Cc1c138D322e8bEaD15dd915baAa5 |
| trading | 0x2379Bc62d7213604175fEfA21b584Ce51A8a01c6 |
| callbacks | 0x22253F52D87CfaBef8f1685ECcEFD4dE4Bd4Aa23 |
| vault | 0x0aC538e15e4C7a27E6E38025Bed911B8975044F0 |
| openPnl | 0x3911c9d929B776CcA45F0023bfe7128A75dA682D |
| priceRouter | 0x3fbF4775821413288BC2Df740E5d4Cf97C856C39 |
| tradesUpKeep | 0x97162dD1FE0e34Cf6ca712a227f4cE4284B2fe65 |
| collateral | 0x2E652a62314c4b51480E54fC55954f51bf20D2B7 |
| verifier | 0x233A5F2d8c66dBd88eB43E4A9C7f7A9BF2F6b1Cf |
| priceUpKeep | 0x1E0DBa1e028eE40A0096dD63eaBadB5b4A7c2c99 |
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.
This is a testnet deployment and it is not finished. The list below is the honest inventory, not a roadmap.
USDW is a faucet token with no supply cap — anyone can mint as much as they like. Nothing traded here has value.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).
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.