Skip to content

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.

  1. 01ElystaCollateralVault.sol

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

  2. 02ElystaMarketRegistryOracle.sol

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

  3. 03ElystaOrderBookEngine.sol

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

  4. 04ElystaPerpRiskEngine.sol

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

ContractAuthoritative forWho writes itWhat it reads
Collateral VaultEvery 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 & OracleWhich 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 & MatchingWhich 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 EnginePositions, 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 + pnlPool

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

The stages of one placeOrder call, grouped by the contract that owns each. Deposit happens earlier, in its own transaction.

  1. 01

    Placement

    Order book

    placeOrder(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.

  2. 02

    Risk reservation

    Risk engine

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

  3. 03

    Matching

    Order book

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

  4. 04

    Settlement

    Risk engine and vault

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

  5. 05

    Remainder

    Order book

    A 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.seq and Fill.seq share 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. OrderCancelled carries a CancelReason, MarketStatusChanged a StatusReason, PositionChanged a numeric reason (fill, liquidated, liquidator), and every vault settlement a SettlementKind.
  • Prices are recorded on acceptance and on rejection. IndexPriceUpdated names the adapter and whether the secondary was used; PriceRejected names 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.

Orders, cancellations, deposits, withdrawals, margin changes, liquidations and funding settlement are public contract calls that any wallet or script can make directly. The terminal would lose its charts and its speed; traders would lose nothing they own. The only thing a stopped keeper costs is fresh prices, and the registry answers that by halting markets rather than trading on a stale index.

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.

RoleGrant rolesList markets, set paramsOracle params, clear haltPause deposits, orders or a marketPause withdrawals (≤ 3 days)Set HaltedVault ledger writesFill hooksSweep protocol feesPost prices
DEFAULT_ADMIN_ROLEyesnonononononononono
MARKET_MANAGER_ROLEnoyesnoyesnononononono
ORACLE_MANAGER_ROLEnonoyesnonononononono
PAUSER_ROLEnononoyesyesnonononono
RISK_ENGINE_ROLE (contract)nononononoyesyesnonono
ORDER_ENGINE_ROLE (contract)nononononononoyesnono
TREASURY_ROLEnonononononononoyesno
UPDATER_ROLE (keeper)nononononononononoyes
Edit a balance, force a fill, write a priceNo role. No function exists for any of them.

DEFAULT_ADMIN_ROLE

admin

all five contracts and the test token

May

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

May not

  • 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

ElystaMarketRegistryOracle

May

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

May not

  • 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

ElystaMarketRegistryOracle

May

  • setOracleParams: adapters, feed ids, heartbeat, deviation and reopening thresholds.
  • clearOracleHalt: lift an oracle halt after review, optionally adopting the pending (deviated) adapter price.

May not

  • 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

ElystaCollateralVault, ElystaMarketRegistryOracle, ElystaOrderBookEngine

May

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

May not

  • 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

ElystaCollateralVault, ElystaMarketRegistryOracle

May

  • Vault ledger: reserve, release, lockFromReserved, lockFromAvailable, unlockToAvailable, creditAvailable, creditLocked, debitLocked, lockedToInsurance, lockedToAvailable, chargeFee, coverFromInsurance.
  • Registry: setMarketStatus to Halted only (on bad debt).

May not

  • 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

ElystaPerpRiskEngine

May

  • reserveForOrder, releaseReservation and applyFill: the three hooks a fill needs.

May not

  • 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

ElystaCollateralVault

May

  • sweepProtocolFees(to): transfer the accrued protocol share of trading fees.

May not

  • Touch user collateral or the insurance fund: the function reads only protocolFees.

UPDATER_ROLE

keeper

ElystaKeeperOracleAdapter (keeper price adapter)

May

  • post and postAndRefresh: store observations for configured feeds and ask the registry to validate them.

May not

  • 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

ElystaTestUSD (test collateral)

May

  • mint test collateral, used to seed the insurance fund at deployment.

May not

  • 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

DEFAULT_ADMIN_ROLE can grant any role, including RISK_ENGINE_ROLE on the vault. A compromised admin key could therefore install a new ledger writer. The code does not prevent this; the operating policy (a single administrator key today, a multisig with a timelock before any real collateral) is what limits it, and it is stated here rather than hidden.

The governance page lists who holds each role today and the path to a multisig and timelock before mainnet.