Protocol
Four contracts, one state of truth.
How Elysta divides authority between its vault, registry, order book and risk engine; how an order moves through all four in one transaction; and what nothing off-chain is allowed to decide.
The four contracts
Each contract has one responsibility and an explicit boundary. Nothing is accounted twice: a balance lives in the vault, a position in the risk engine, an order in the book, a parameter in the registry.
ElystaCollateralVault.solCollateral Vault
Holds the settlement collateral and keeps the one canonical ledger: available, reserved and locked per account, the insurance fund and the PnL pool.
Authoritative for every balance.
ElystaMarketRegistryOracle.solMarket Registry & Oracle
Lists markets with their execution, risk, fee and session parameters, and turns adapter observations into one validated index price with freshness and deviation rules.
Authoritative for configuration and the index.
ElystaOrderBookEngine.solOrder Book & Matching
The on-chain limit order book: tick-indexed levels, FIFO queues and deterministic price-time matching inside a bounded gas budget.
Authoritative for which orders fill, and at what price.
ElystaPerpRiskEngine.solPerpetual Risk Engine
Positions, isolated margin, leverage limits, mark price, funding, permissionless liquidation and the loss waterfall into the insurance fund.
Authoritative for positions and settlement.
Supporting code is libraries and one interface: ElystaTypes (shared enums, structs and the WAD convention), FixedPointMath (18-decimal arithmetic with explicit rounding), TickBitmap (price-level discovery), and IOracleAdapter, behind which the keeper adapter sits. None of them holds protocol state.
State-authority boundaries
Who may write each contract’s state, and what each one reads from the others.
| Contract | Authoritative for | Who writes it | What it reads |
|---|---|---|---|
| Collateral Vault | Every balance: available, reserved and locked per account; the insurance fund; protocol fees; bad debt; the PnL pool. | Traders (deposit, withdraw, fundInsurance). The risk engine, for every ledger movement. PAUSER_ROLE and TREASURY_ROLE for the bounded administration below. | Nothing: it holds the token and the ledger and knows no market. |
| Market Registry & Oracle | Which markets exist, every parameter of each, their status and session, and the one validated index price. | MARKET_MANAGER_ROLE and ORACLE_MANAGER_ROLE for configuration; anyone for refreshIndex; PAUSER_ROLE to pause; the risk engine to halt on bad debt. | Oracle adapters, through IOracleAdapter. |
| Order Book & Matching | Which orders exist, which fill, against whom, at what price and in what order. | Traders (placeOrder, cancelOrder, cancelOrders, replaceOrder); anyone for cancelExpired; PAUSER_ROLE and DEFAULT_ADMIN_ROLE for pause and matching limits. | The registry (isTradingOpen, execParams, indexPrice) and the risk engine (markPrice). Calls the risk engine’s three fill hooks. |
| Perpetual Risk Engine | Positions, isolated margin, open interest, the mark price, funding indices, liquidation and the loss waterfall. | The order book, through reserveForOrder, releaseReservation and applyFill. Anyone for liquidate, settleFunding and accruePremium. A trader for addMargin and removeMargin on their own position. | The registry for parameters and the index, the order book for the best bid and ask. Writes the vault’s ledger and may halt a market in the registry. |
The binding between the two engines is permanent: setOrderEngine can be called once, grants ORDER_ENGINE_ROLE to the order book, and reverts afterwards. The vault accepts ledger instructions only from RISK_ENGINE_ROLE, held by the risk engine contract. Every ledger primitive moves an amount between two terms of one equation, which is why the whole vault can be checked at any block:
token.balanceOf(vault) × scale == totalAvailable + totalReserved + totalLocked + insuranceFund + protocolFees + pnlPoolThe status page reads solvency() and reports whether the equation holds. The PnL pool is a signed accumulator, always equal to minus the sum of unrealized PnL over every open position: a gain paid out subtracts from it, a loss taken adds to it, and the dust left by rounding lands in it.
The order lifecycle, in one transaction
Placement, risk reservation, matching and settlement happen inside a single call to placeOrder. Either all of it is in the block or none of it is.
Placement
Order bookplaceOrder(PlaceOrderParams) checks the market with the registry (isTradingOpen: status Active or ReduceOnly, session open, valid index), refuses non-reduce-only orders on a ReduceOnly market, enforces the open-order cap, the expiry, and the quantity (a multiple of the lot between the minimum and maximum). A limit price must be a tick multiple inside priceBandBps of the index; a market order is turned into an immediate limit at the mark moved by maxImpactBps, tightened by the trader’s protectionPrice.
Risk reservation
Risk enginereserveForOrder moves notional × initialMarginFraction + notional × takerFee from the trader’s available to reserved collateral, for the full quantity at the limit price, so a later fill can never fail for lack of collateral. A reduce-only order reserves nothing; it must shrink a position that already holds margin, on the opposite side and within its size.
Matching
Order bookThe incoming order walks the opposing side from the best level while that level crosses its limit, oldest order first. Each fill is settled before the next is attempted. If the risk engine rejects the resting (maker) side, that order is cancelled with reason RiskRejected and matching moves on; if it rejects the taker, the whole transaction reverts and the book never shows a trade that did not settle. A resting order of the taker’s own is cancelled (SelfTrade); an expired maker is cancelled on contact. The walk stops at 24 fills or 12 levels per transaction.
Settlement
Risk engine and vaultapplyFill computes a plan for each side before writing anything: the share of the reservation to release, the quantity that closes existing exposure and the quantity that opens new exposure, the PnL realized at the fill price, the margin to lock, the fee. Then it writes the vault: release the reservation, pay a gain to available or take a loss from margin (and, past the margin, from the insurance fund), lock new margin, charge the fee split between insurance fund and protocol fees, and update the position and open interest. Every step is an event.
Remainder
Order bookA fully filled order emits OrderFilled. A GTC or post-only limit remainder rests (OrderRested) with its reservation. An IOC or market remainder is cancelled (Remainder). A FOK that could not fill entirely reverts (FillOrKillNotFilled), as does a post-only order that would have crossed (PostOnlyWouldCross). A GTC remainder that still crosses the book when the budget runs out is cancelled rather than rested, because resting it would leave the book crossed; the trader resubmits.
What a filled order leaves in the log
In order: OrderPlaced; for each fill, Fill from the book, two PositionChanged from the risk engine and the vault’s ledger events (Released, MarginLocked, FeeCharged, and AvailableCredited or LockedDebited when PnL is realized); then OrderFilled, OrderRested or OrderCancelled. A trader can reconstruct every figure the terminal shows from these events alone, and the indexer does exactly that.
Position lifecycle
A position is a signed quantity per account and market with an average entry price and its own margin. A fill on the same side increases it and raises the margin by the new quantity’s initial margin, averaging the entry price. A fill on the opposite side closes quantity first, releasing margin in proportion and realizing PnL at the fill price; any quantity beyond the position opens the other way at the fill price. Funding is settled whenever a position is touched. Margin can be added or removed by its owner while the position stays above its initial requirement. Liquidation is described on the risk methodology page.
Event model
The chain is the record. Every state change emits an event with the identifiers an indexer needs, and no figure the application shows lacks an event to derive it from.
- One sequence per market.
OrderPlaced.seqandFill.seqshare a counter, so placements and fills are totally ordered without relying on log position.sequence()returns the next order id and the last sequence number for reconciliation. - Reasons travel with changes.
OrderCancelledcarries aCancelReason,MarketStatusChangedaStatusReason,PositionChangeda numeric reason (fill, liquidated, liquidator), and every vault settlement aSettlementKind. - Prices are recorded on acceptance and on rejection.
IndexPriceUpdatednames the adapter and whether the secondary was used;PriceRejectednames the reason: no price, stale, or deviation. - Indexed arguments are the query keys. Account, market id and order id are indexed wherever they appear, so a wallet’s whole history is a handful of topic filters.
The event specification lists every event of the four contracts with its parameters, when it is emitted and which indexer table it lands in.
Off-chain services, and what they may not decide
Three services run beside the contracts. They make the exchange fast to read and keep its clocks ticking; none of them is authoritative for anything.
Indexer
Follows confirmed blocks, decodes every event of the five contracts into an archive (protocol_events) and into query-efficient projections: orders, fills, positions, balances, candles, funding, liquidations, market configuration and status. Detects reorgs and replays the archive, so the projections are a pure function of the chain. Periodically reads balances from the vault and marks whether the replay reconciles.
Never: Creates a balance, a fill or a price. A projection that disagrees with the chain is a bug in the indexer, and the reconciliation flag says so.
API and WebSocket
Serve the projections over REST and push them over WebSocket, labelling every response with its source (chain, indexed, computed, estimated), the block it describes and its freshness.
Never: Accepts an order, moves collateral or signs anything. There is no authenticated endpoint because there is nothing to authorise: every write is a wallet transaction to a contract.
Oracle keeper
Reads HyperCore’s published oracle prices for each feed, posts them to the keeper adapter (postAndRefresh) on a heartbeat or when a price moves, and asks the registry to validate them in the same transaction. Also calls settleFunding when an interval boundary has passed and accruePremium to keep the premium accumulator current.
Never: Decides a price: the registry applies the heartbeat, deviation and reopening rules to every observation and halts the market when none passes. Decides a funding rate: settleFunding computes it from the on-chain premium accumulator. Anyone may call both functions; the keeper merely pays the gas on time.
If every off-chain service stopped, the exchange would not.
Deployed addresses
From the deployment record this build was compiled with. Explorer links appear on networks that have one.
No deployment is recorded for chain 99901 in this build.
Addresses, explorer links and role holders appear here once the contracts are deployed to the configured network and the deployment record is generated (pnpm abi). Nothing is shown in their place.
Permission matrix
Every role in the five contracts, read from the Solidity: what it may do, and the lines no role can cross.
| Role | Grant roles | List markets, set params | Oracle params, clear halt | Pause deposits, orders or a market | Pause withdrawals (≤ 3 days) | Set Halted | Vault ledger writes | Fill hooks | Sweep protocol fees | Post prices |
|---|---|---|---|---|---|---|---|---|---|---|
DEFAULT_ADMIN_ROLE | yes | no | no | no | no | no | no | no | no | no |
MARKET_MANAGER_ROLE | no | yes | no | yes | no | no | no | no | no | no |
ORACLE_MANAGER_ROLE | no | no | yes | no | no | no | no | no | no | no |
PAUSER_ROLE | no | no | no | yes | yes | no | no | no | no | no |
RISK_ENGINE_ROLE (contract) | no | no | no | no | no | yes | yes | no | no | no |
ORDER_ENGINE_ROLE (contract) | no | no | no | no | no | no | no | yes | no | no |
TREASURY_ROLE | no | no | no | no | no | no | no | no | yes | no |
UPDATER_ROLE (keeper) | no | no | no | no | no | no | no | no | no | yes |
| Edit a balance, force a fill, write a price | No role. No function exists for any of them. | |||||||||
DEFAULT_ADMIN_ROLE
admin- Grant and revoke every other role (OpenZeppelin AccessControl).
- Order book: setMatchingLimits (fills and levels per transaction, open orders per account).
- Risk engine: setOrderEngine, once; the binding is permanent.
- Keeper adapter: setRegistry and configureFeed (declare a feed and its source string).
- Change an account's available, reserved or locked collateral.
- Withdraw from the insurance fund: no function exists.
- Place, fill or cancel another account's orders.
- Write an index price: prices enter only through refreshIndex reading an adapter.
MARKET_MANAGER_ROLE
admin- addMarket: list a market, which starts Inactive.
- setExecutionParams, setRiskParams, setFeeParams, setSession, setHoliday.
- setMarketStatus to Inactive, Active, ReduceOnly or Paused (also lifts a bad-debt halt after review).
- Set a market to Halted: only the risk engine may.
- Set oracle parameters or clear an oracle halt.
- Exceed the parameter bounds the registry validates (fees ≤ 1%, funding cap ≤ 5%, liquidation fee < initial margin, interval 1 min to 1 day).
ORACLE_MANAGER_ROLE
admin- setOracleParams: adapters, feed ids, heartbeat, deviation and reopening thresholds.
- clearOracleHalt: lift an oracle halt after review, optionally adopting the pending (deviated) adapter price.
- Type in a price: clearOracleHalt(acceptPending) can only adopt a price an adapter already reported.
- Change a position, a balance or a settled funding index.
PAUSER_ROLE
admin- Vault: pause and unpause deposits; pauseWithdrawals(until), at most MAX_WITHDRAWAL_PAUSE (3 days) ahead.
- Registry: setMarketStatus to Paused only.
- Order book: pause and unpause new orders and matching. Cancellation keeps working while paused.
- Pause withdrawals for longer than 3 days in one call.
- Halt or activate a market.
- Stop a trader cancelling an order or (outside a withdrawal pause) withdrawing free collateral.
RISK_ENGINE_ROLE
risk engine contract- Vault ledger: reserve, release, lockFromReserved, lockFromAvailable, unlockToAvailable, creditAvailable, creditLocked, debitLocked, lockedToInsurance, lockedToAvailable, chargeFee, coverFromInsurance.
- Registry: setMarketStatus to Halted only (on bad debt).
- Move value anywhere but between the terms of the vault equation: every primitive debits one bucket and credits another.
- Transfer tokens out of the vault.
ORDER_ENGINE_ROLE
order book contract- reserveForOrder, releaseReservation and applyFill: the three hooks a fill needs.
- Be granted twice: setOrderEngine reverts once an engine is bound.
- Liquidate, settle funding or adjust margin: those are permissionless or the trader's own.
TREASURY_ROLE
admin- sweepProtocolFees(to): transfer the accrued protocol share of trading fees.
- Touch user collateral or the insurance fund: the function reads only protocolFees.
UPDATER_ROLE
keeper- post and postAndRefresh: store observations for configured feeds and ask the registry to validate them.
- Post a zero price, an observation from the future (beyond 60 s) or one older than the last.
- Bypass the registry's validation: a posted price becomes the index only if it passes heartbeat and deviation rules.
MINTER_ROLE
admin- mint test collateral, used to seed the insurance fund at deployment.
- Mint anything of value: tUSD has none, and a deployment that settles in a real token does not include this contract.
What no role can do
- Edit a balance
- The vault's ledger functions are callable only by the risk engine contract, and each one moves an amount between two terms of the vault equation (available, reserved, locked, insurance fund, protocol fees, PnL pool). There is no function that sets a balance.
- Force a fill
- placeOrder, cancelOrder and replaceOrder act on msg.sender's orders. applyFill is callable only by the order book, which only calls it while matching a trader's own order against resting orders.
- Override the price a position settles at
- Fills settle at the resting order's price; liquidation and funding settle at the mark price, which the risk engine derives from the validated index and the live book. An administrator can change oracle parameters and clear a halt, but cannot write a price.
- Withdraw the insurance fund
- fundInsurance adds to it; coverFromInsurance pays a shortfall into the PnL pool; lockedToInsurance and chargeFee add penalties and fee shares. No function pays it out in this version.
- Hold withdrawals indefinitely
- pauseWithdrawals accepts an end time at most three days ahead. Deposits can be paused outright; withdrawals cannot.
The one power that is a policy, not a proof
The governance page lists who holds each role today and the path to a multisig and timelock before mainnet.