Skip to content

Operating a perp dex: oracles, caps, halts and allowlists

Expert

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.

Each is a variant of {"type": "perpDeploy", <variant>: {...}}, lists of tuples sorted by key before signing.

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.
Deployer mark, round 1 markPxs[0] Deployer mark, round 2 markPxs[1], optional Local mark median of bid, ask, last trade median of the inputs present Mark price moves at most 1% per update within 10x the start-of-day value no move that lifts OI past 10x the cap NO UPDATE FOR 10 SECONDS: THE MARK FALLS BACK TO THE LOCAL MARK
The deployer supplies one or two of the three values in the median; the protocol clamps the result.

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.

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.

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.

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.

{ "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).

{ "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

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

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.

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.

  • 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: setOracle does not allow haltTrading. 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.