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.
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.
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:
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.
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.
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.
| Role | Can do | Key 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:
- ✕Forge the oracle price.The oracle only updates from a signature-verified Chainlink data-streams report (
_updateOraclePrice→_verifyLinkReport). The price key relays reports; it cannot choose the value inside one. - ✕Widen the tolerance band.Needs the tolerance-administrator key — and even that is capped at 20%.
- ✕Turn the oracle off, or halt trading.Needs the owner multisig.
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.
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.