Operating a perp dex: oracles, caps, halts and allowlists
Running a HIP-3 dex means pushing prices every 3 seconds, never faster than every 2.5, and managing caps, funding, margin, fees, halts and sub-deployers.
On a HIP-3 dex the deployer is the oracle. Funding, margin, liquidation and take-profit and stop-loss triggers run on its prices, which the protocol only clamps. If it stops, the mark quietly falls back to the dex’s book; irregular prices are slashable.
Every control is one perpDeploy action
Section titled “Every control is one perpDeploy action”Each is a variant of {"type": "perpDeploy", <variant>: {...}}, lists of tuples sorted by key before signing.
The oracle loop: setOracle
Section titled “The oracle loop: setOracle”Push every 3 seconds, even unchanged, and never less than 2.5 seconds apart:
type SetOracle = { dex: string; oraclePxs: Array<[string, string]>; // sorted by asset markPxs: Array<Array<[string, string]>>; // 0, 1 or 2 rounds, each sorted by asset externalPerpPxs: Array<[string, string]>; // sorted by asset, must include every asset};oraclePxs: oracle prices, for funding and as a mark component.markPxs: up to 2 rounds of mark inputs, medianed with the local mark (median of best bid, best ask and last trade).externalPerpPxs: reference prices, for every asset, that stop the mark jumping; from an external source, falling back to an EMA of recent marks. A move over 50 per cent from start of day triggers a validator manipulation review.
Say your source jumps 5 per cent after a gap. At 1 per cent per update, the mark needs at least 5 updates to catch up: 12 seconds on a 3-second grid.
The 10-second fallback counts as an update for the rate limit and “should not be relied upon” (docs). Only the deployer or its oracleUpdater may push, unless setOracle is granted to a sub-deployer. Use a fixed 3-second grid, not a sleep after each call, so latency cannot accumulate past the fallback.
Open interest caps
Section titled “Open interest caps”Notional open interest (absolute size times mark) is capped per asset and across the dex; size open interest per asset only. The deployer sets notional caps:
{ "type": "perpDeploy", "setOpenInterestCaps": [["<asset>", 5000000], ["<asset>", null]] }null removes a cap. Read perpDexLimits (totalOiCap, oiSzCapPerPerp, maxTransferNtl, coinToOiCap) and, for assets at their cap, perpsAtOpenInterestCap with dex.
Funding
Section titled “Funding”| Action | Value per asset | Range |
|---|---|---|
setFundingMultipliers |
multiplier on the funding rate | 0 to 10 |
setFundingInterestRates |
8-hour interest rate component | -0.01 to 0.01 |
setFundingClamps |
8-hour clamp on the move from average premium towards the interest rate | 0 to 0.01, default 0.0003 |
Multipliers read back in perpDexs as assetToFundingMultiplier; the computation is on funding.
Margin
Section titled “Margin”| Action | Shape | Rule |
|---|---|---|
insertMarginTable |
{dex, marginTable: {description, marginTiers}}, tiers {lowerBound, maxLeverage} |
tiers by increasing lower bound and decreasing leverage |
setMarginTableIds |
[[coin, id]] |
ids non-zero |
setMarginModes |
[[coin, mode]] |
strictIsolated, noCross or normal |
Validators permit cross margin only with observable liquidity, a reliable external oracle and manipulation resistance.
Halting and dated contracts
Section titled “Halting and dated contracts”{ "type": "perpDeploy", "haltTrading": { "coin": "abc:BTC", "isHalted": true } }Halting cancels the asset’s orders and settles all positions at the current mark; isHalted: false resumes the same asset id. So a dated contract settles at expiry and reopens as the next, with no new auction. New assets start halted.
setFeeRecipient {dex, feeRecipient} directs the deployer’s share; setDeployerFees [[coin, {scale, growthMode}]] sets it (scale a decimal string). Growth mode has a 30-day cooldown per asset and needs markets disjoint from validator-operated perps (deployer economics).
Sub-deployers
Section titled “Sub-deployers”{ "type": "perpDeploy", "setSubDeployers": { "dex": "abc", "subDeployers": [ { "variant": "setOracle", "user": "0x...", "allowed": true }, { "variant": "haltTrading", "user": "0x...", "allowed": true } ] } }variant names a perpDeploy variant. Grants carry permission, not stake: none of the 37 mainnet sub-deployer and fee recipient addresses held staked HYPE (2026-09-20).
Annotations, disabling, liquidator transfers
Section titled “Annotations, disabling, liquidator transfers”| Control | Detail |
|---|---|
setPerpAnnotation {coin, category, description, displayName, keywords} |
category at most 15 characters, description 400, display name 9; at most 2 keywords of 10 |
disableDex: "<dex>" |
disables the dex |
hip3LiquidatorTransfer {dex, ntl, isDeposit} |
user-signed; moves collateral between a user’s dex account and the backstop liquidator at 0x4000...0000 + dex_index; ntl in millionths of the collateral token, a multiple of 1,000,000,000 (1,000 tokens) |
node flag --write-user-account-summaries <dex> |
writes each user’s account value on oracle updates: a full snapshot, then changes only, with a new snapshot on the first update more than 10 minutes after the last |
Allowlisted (“star”) dexes
Section titled “Allowlisted (“star”) dexes”Undocumented, seen on testnet in July 2026: 2 starred dexes among 243 swept (62,112 deployer transactions; 29 dexes not replayable).
{"star": {"dex": "abc", "operation": "activate"}}made a dex allowlist-only: one 2 seconds after creation, one 5 months after launch, no redeploy.{"star": {"dex": "abc", "operation": {"modifyApprovals": [["0x...", true]]}}}edits the list in a batch.- Only approved addresses could then open positions; the deployer’s own order, identical to an approved one, neither traded nor locked margin. Anyone could still deposit.
- Another dex’s deployer was refused:
Invalid perp deployer. - Reducing by an unapproved holder was never tested.
The numbers
Section titled “The numbers”| Control | Value |
|---|---|
setOracle spacing |
at least 2.5 s between calls |
setOracle expected cadence |
every 3 s, even unchanged |
| Stale mark fallback | local mark after 10 s without an update |
| Mark move clamp | 1% from the previous mark per update |
| Price bound | 10x the start-of-day value |
| Open interest bound on mark moves | cannot push OI to 10x the cap |
| Notional OI cap floor | larger of 1,000,000 and half current OI |
| Size OI cap | 1B units per asset |
| Funding multiplier | 0 to 10 |
| Funding interest rate | -0.01 to 0.01 per 8 hours |
| Funding clamp | 0 to 0.01, default 0.0003 |
| Margin tiers | at most 3; leverage 1 to 50 |
| Fee scale | 0 to 3; 0 to below 10 in growth mode |
| Fee scale changes (mainnet) | once per 30 days |
| Backstop liquidator transfer | multiples of 1,000 collateral tokens |
What traders are exposed to
Section titled “What traders are exposed to”Within the clamps, the operator can at any time halt and settle positions at the mark, lower an open interest cap, and change funding multipliers, interest rates and (once per 30 days on mainnet) the fee. Up to 2 of 3 mark inputs are its own; a testnet starred dex can shut out anyone unlisted.
Building the oracle loop
Section titled “Building the oracle loop”A minimal builder; signing is on signing:
const TICK_MS = 3000;
function setOracleAction(dex: string, px: Map<string, string>, ext: Map<string, string>) { const sorted = (m: Map<string, string>) => [...m.entries()].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)); return { type: "perpDeploy", setOracle: { dex, oraclePxs: sorted(px), markPxs: [sorted(px)], externalPerpPxs: sorted(ext), }, };}
// Schedule on a fixed grid: next = start + k * TICK_MS, never less than 2.5 s after the last send.Prices are decimal strings without trailing zeros. Whether per-asset keys in setOracle, setOpenInterestCaps and the funding and margin actions carry the dex prefix is undocumented; registration and haltTrading use the full dex:COIN. Confirm on testnet.
Failures to watch for
Section titled “Failures to watch for”- The fallback hides outages: a dead loop still shows a moving mark. Monitor your last successful
setOracle, not the mark. - Halting settles at the mark, not the oracle, even one a thin book has dragged.
- Grants are per variant:
setOracledoes not allowhaltTrading. Revoking is a deployer action. - Cross margin is a one-way door: once enabled on an asset, it stays.
To create the dex, see deploying a perp dex; for the slashing rules, HIP-3; for how prices feed margin, oracles.