Mark Price Oracle Guard

How Perpl bounds every mark-price update to the live spot oracle — the tolerance band, oracle freshness, role separation, and why a stolen price key still can't move the market.

The mark price is the number the exchange settles everything against. Unrealized profit and loss, margin, the liquidation trigger, and funding all key off it. If the mark could be set to any value, an attacker who controlled it could manufacture liquidations, drain margin from healthy positions, or mint funding payments out of nothing. It is the single most safety-critical price in the system.

Perpl computes the mark off-chain — a pricing bot blends the order book, external venue feeds, and a Chainlink spot price into a single number — and then submits that number on-chain through an admin key. That key signs continuously and lives on a server, not in a vault, so it is exposed by design. The question the guard has to answer is blunt: what is the worst that happens if that key is stolen, or if someone simply submits a wrong price? The answer is that the damage is bounded to a few percent, for a few seconds — and the code that bounds it is one tolerance check.


How the mark price is made

The mark is produced by an off-chain pricing bot running as two active-active replicas. Each tick it reads three independent inputs, blends them, applies a basis clamp and a funding clamp, and proposes the result on-chain.

Figure 1: How the mark price is produced
Three inputs — the order book, external venue feeds, and a Chainlink spot price — feed a pricing bot, which submits the mark price on-chain through the price administrator's admin key. Order book impact-mid / book price Exchange price feeds external venue references Chainlink spot price signed data-streams report Pricing bot blend + basis / funding clamps · 2 replicas proposes Price key admin key · signs + submits Nothing above is trusted on-chain — the contract sees only the final proposed number, then checks it.
The off-chain pipeline is where the mark comes from, but none of it is enforced on-chain. The contract cannot see the order book the bot read or the feeds it blended — it sees one number arriving from the price key. Every safety property below is therefore a property of the on-chain check, not of the bot.

The on-chain guard

Every proposed mark passes through one function — _updateMarkPricePNS — before it is written. The proposal only becomes the live mark if it clears an ordered series of gates. Any failure rejects the update and leaves the previous mark in place.

The gates, in order: the caller holds the PriceAdministrator role and the exchange is not halted; the perpetual is not mid-unwind; the on-chain oracle price is fresh; and the proposed mark is within tolerance of that oracle. Only after all pass does the contract call setMark and emit MarkUpdated.

Figure 2: What happens to a proposed mark price
A proposed mark price is checked against oracle freshness and then against the tolerance band. Failing either rejects the update; passing both accepts it. The tolerance check uses the live signed oracle price as its reference. Proposed mark from the price key Oracle fresh? yes Within tolerance? yes Mark accepted setMark · MarkUpdated Signed spot oracle price Chainlink data-streams the reference Rejected previous mark stays in place no · stale no · too far
The two gates that stop a bad price are freshness and tolerance. A stale oracle rejects the update — it does not wave it through (see "A stale oracle freezes the mark", below). A proposal too far from the oracle reverts with MarkExceedsTol. The reference for "too far" is the live signed oracle price, not the previous mark.

The tolerance band

The tolerance gate is a single inequality. A proposed mark passes only if its distance from the oracle price, as a fraction of the oracle price, is under the configured tolerance:

| mark − oracle | × 100,000 < priceTolPer100K × oracle Rearranged from |mark − oracle| / oracle < priceTolPer100K / 100,000 — multiplication only, to avoid on-chain division.

The tolerance is stored per-market as priceTolPer100K, a figure out of 100,000. The default is 5,000 — a 5% band. The contract caps it at _MAX_PRICE_TOL = 20,000, so the band can never exceed 20%, no matter who sets it.

oracle = $100.00, priceTolPer100K = 5,000 (5%) Accepted range: $95.00 … $105.00. Propose $130.00 → |30| × 100,000 = 3,000,000 ≥ 5,000 × 100 = 500,000 → revert MarkExceedsTol.

The check measures absolute distance, so it is symmetric: a mark too far above the oracle and one too far below are rejected identically. A long profits when the mark is marked up and a short when it is marked down — the band closes both exits, so no position can force a favorable settlement past it.

Figure 3: The accept band around the oracle price
A price axis centered on the oracle price. A green accept band spans plus or minus the tolerance around the oracle; anything outside is rejected. A proposal inside the band is accepted; one outside reverts. oracle $100 $95 $105 −5% accept band +5% rejected rejected $103 · accepted $130 · rejected
The band moves with the oracle. Because the reference is the live signed oracle price and not the previous mark, an attacker cannot walk the mark away one small step at a time — every step is re-measured against the real spot, so the mark is always pinned to within the band of the true price.
▶ Try the interactive guard

Separation of powers

The price key is powerful but narrow. It can propose a mark and relay oracle reports — nothing else. The two things that would actually let someone move the market freely — widening the band and turning the oracle off — live behind different keys entirely.

RoleCan doKey type
Price administrator Submit the proposed mark; relay signed oracle reports on-chain. Admin key
Tolerance administrator Set the band width priceTolPer100K, capped at 20%. Separate admin key
Owner Disable the oracle (setIgnOracle), halt the exchange, unwind, and set tolerance. Multisig

That separation is what turns a stolen price key from a catastrophe into a bounded, transient nuisance. With the price key alone, an attacker is stuck inside the band around a price they cannot forge:

Why this matters — Ostium, July 2026. An attacker compromised Ostium's oracle signer and, in one looped transaction, opened positions at market then printed a favorable close price — marked up to cash a long, or down to cash a short — booking fake profit the vault paid out. Around ten loops compounded roughly $18M before trading was paused. The failure was not a contract bug; it was a single key that was the price source. Perpl differs in exactly the place that mattered: the oracle price is an independently-signed Chainlink report the price key relays but cannot forge, and every mark is then bounded to that oracle by a symmetric tolerance gate — too far above or too far below is rejected the same way. CoinDesk
The whole design in one line: the mark can only ever land within a few percent of a price that is signed by an independent oracle and that the price key cannot forge — and the two levers that could remove that constraint are held by different keys.

A stale oracle freezes the mark

The freshness gate has a property worth stating plainly, because the safe behavior is the counterintuitive one. The oracle price must be newer than refPriceMaxAgeSec for a mark update to be accepted. If the oracle stops updating and goes stale, the tolerance check has no trustworthy reference — so the contract rejects the mark update. It does not fall back to accepting whatever was proposed.

Freezing is the safe failure. A stale oracle makes the mark un-updatable, not free. An attacker cannot unlock arbitrary mark control by starving the oracle feed — the worst they achieve is a frozen mark, which stops fresh liquidations rather than enabling fake ones. The mark can go at most refPriceMaxAgeSec without an update; the oracle at most twice that.

This is the mechanism a trader sees as a rejected update or a settlement guard when an oracle feed lags: the protocol chooses to stop rather than to trust a price it can no longer verify is current.


What a bad price can still do

The guard bounds the damage; it does not reduce it to zero. Inside the band, a compromised price key can still nudge the mark by up to the tolerance — up to 5% off the true spot at the default setting. That is real, but it is small, and it is transient: the very next honest update snaps the mark back to the oracle, because every update is re-checked against the live reference.

The tolerance width is the trade-off dial. A tighter band contains a compromise more strictly but rejects more legitimate updates during fast, real moves between oracle ticks; a wider band absorbs volatility but gives a bad key more room. The tolerance administrator can tighten it in calm conditions, and the owner multisig can respond to an incident by halting or disabling the affected market outright.

Key property: the guard cannot stop a wrong price from being proposed — it can only cap how wrong the accepted price is, and for how long. Its value is the ceiling it puts on damage: a few percent, until the next valid update, with no path for a single stolen key to widen that ceiling.
▶ Try the interactive guard