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.
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.
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 OstiumTrading.sol PERCENT_BASE = 100e2 and the check in TradingCallbacksLib.sol); CancelReason from IOstiumTradingCallbacks.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.
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:
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 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.
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).
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).
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 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.
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. Kept even when your order is cancelled — this is the “minus the oracle fee” in every refund. |
| Opening fee · maker | — | Charged on the notional when a position opens. |
| Opening fee · taker | — | Same field group. The landing mockup’s 0.035% is a design figure and is not what this market is configured with. |
| Rollover | — | Accrues 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. |
| Funding | — | Same 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).
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.
| Contract | Address |
|---|---|
| registry | 0xD6Cc323BF2736B121586B2929E0E27a80DDCC98A |
| collateral | 0x34A2945DBFe45e86dB968ff7E6Bb7d8394711f89 |
| tradingStorage | 0xb51217a43BF812B354E18977a4781909C4d33341 |
| pairsStorage | 0xc5B68AfA8f64288f6d06DEfC5c07555Ef5323397 |
| pairInfos | 0xF87205EbCf03513D24B998925109FcB84Ce84ED1 |
| trading | 0x9f7F9be7731E805B0b9174465257f0C1Ab589f27 |
| callbacks | 0x4084cd63dB84c88c98418e33b88449b3b006da9c |
| vault | 0x7B9147B4b8b05e7e3c3F86694d8EA7a639c2D516 |
| openPnl | 0x9287F73ac606F85657010F458c0CF5efBb22d880 |
| priceRouter | 0xB14c4cf81007D60afDfd2Cc48b94cFCf861134a4 |
| verifier | 0xF0F7Abe953Ac6448258AEc4d0b6ddB6b60750814 |
| priceUpKeep | 0x6d8fa4DE0DD0aF71F044266aA630B560733efC96 |
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).