Appearance
Oracle and safety
Pyth pull oracle
- Every price is a Pyth
PriceUpdateV2account owned by the Pyth Solana receiverrec2HHDDnjLfj4kE7VyEtFA1HPGQLK33259532cRyHp. The receiver address is hard-coded in the program, so governance can't substitute another price source. - Only fully Wormhole-verified updates are accepted; partially verified ones are rejected. The update's feed id must match the market's.
- Prices are normalized to 8 decimals: price and EMA round down, confidence rounds up. Zero or negative prices are rejected.
- The price used is Pyth's aggregate across many publishers, not a single venue's book.
Admissibility (every price read)
| Check | Current limit | Parameter |
|---|---|---|
conf × 10,000 ≤ price × max_conf_bps | 50 bps (0.5%) | max_conf_bps (1–1,000) |
|price − ema| × 10,000 ≤ ema × max_ema_dev_bps | 500 bps (5%) | max_ema_dev_bps (1–5,000) |
prev_publish_time ≤ publish_time | — | — |
Both ratios are compared exactly, by cross-multiplying, with no rounding to whole basis points: a 50 bps limit admits 50.000 bps but not 50.0001 bps.
Fills and heartbeats need all three checks. A fill or void uses the confidence and EMA limits recorded in its request when the request was made, so a parameter change executed while it is pending can't turn its fill into a void, or back. Liquidations skip the confidence check: they need the EMA and timing checks only, and confidence enters solely through the band rule (Liquidation), so a wide band can't keep an underwater position alive. A liquidation price that fails the confidence check advances last_oracle_ts but isn't recorded as last_oracle_price, which therefore always comes from an update that passed every check.
EMA lag. Every fill, liquidation and heartbeat needs the price within 5% of Pyth's EMA. During a move of more than 5% away from the EMA, fills (closes included), liquidations and heartbeats stop until the EMA catches up, and new requests fail with StaleHeartbeat after about 10 s.
Freshness and timing limits
| Use | Rule | Current value |
|---|---|---|
| Fill (open/close) | The unique update with prev_publish_time < pin_ts ≤ publish_time, published ≤ 60 s ahead of the Solana clock, filled while pin_ts ≤ clock ≤ expires_ts | Pin delay 6 s, fill window 30 s |
| New request | Market watermark last_oracle_ts within [clock − max_obs_age_s, clock + max_clock_lag_s] | 60 s / 5 s |
| Liquidation | Published after entry, ≤ liq_max_age_s old by max(clock, last_oracle_ts) | 10 s |
Heartbeat (observe_price) | ≤ liq_max_age_s old by the Solana clock | 10 s |
| Retired-market settlement | The unique update with prev_publish_time < settle_pin_ts ≤ publish_time, executed after settle_pin_ts + 120 s | Pin = the settlement proposal's eta |
Watermark. last_oracle_ts is the newest verified publish time the program has recorded for a market. Fills, liquidations and permissionless heartbeats (observe_price) all update it, and it only moves forward. Pins are set after it. That blocks fills at an already-published price when Solana's clock lags, as long as the watermark itself is recent in real time. Like other Solana perps, Papersol trusts the Solana clock: between heartbeats (every 30 s on a quiet market) the watermark can be older than the pin delay, and pins then rest on the clock alone. A lagging clock combined with an old watermark can produce a pin on a price that has already been published. The keeper watches for that: it compares the Solana clock with real time every 2 s and alerts the team if the clock falls more than 2.5 s behind, so new opens can be paused or heartbeats sped up (How trading works).
Clock-lag detector. Pyth timestamps are signed real time. A verified update more than 5 s ahead of the Solana clock proves the clock is lagging, so new requests stop (ClockLag) until the clock catches up.
Heartbeats. Requests need a watermark no older than 60 s. Fills record prices too, so busy markets need fewer heartbeats. On a quiet market the Papersol keeper records one every 30 s: Pyth's sponsored push feed when it is fresh (mainnet BTC and SOL update about every 5 s), otherwise a verified update it posts itself (ETH's push feed steps about every 52 s). If heartbeats stop, new requests fail with StaleHeartbeat within about 60 s.
Keepers and permissionless actions
There is no privileged execution keeper: every execution instruction below accepts any signer. A separate risk-keeper role, a single key in Config, sets the trailing OI limits (Pause and emergency controls).
| Action | Instruction | Reward |
|---|---|---|
| Fill an open or close | execute_open, execute_close | The execution fee goes to the poster of the pinned update; the filler is reimbursed for any rent it fronts |
| Liquidate | liquidate | None |
| Post a heartbeat | observe_price | None |
| Void an unfillable close | void_close | The execution fee goes to the poster of the inadmissible update |
| Cancel an expired request | cancel_request | ¼ of the execution fee |
| Pay the queue | harvest, harvest_one | None |
| Sweep LP excess to stakers | sweep | None |
| Settle a position in a settled market | settle_retired | None; claim rent fronted is refunded on payment |
| Execute a ready proposal | execute_proposal, execute_add_market | None |
Anyone can post a Pyth update through the Pyth receiver. In practice the Papersol keeper (a primary plus a standby) fills, voids, cancels expired requests, liquidates, posts heartbeats, harvests the queue and maintains the trailing OI limits. It also executes due SettleMarket proposals with the pinned update as soon as the program allows, keeps filling closes committed before a settlement pin, and settles every position in a settled market that has no pending close with settle_retired, checking about every 10 s. It alerts if a due settlement still hasn't executed 10 minutes after its pin. It does not execute other proposals; those need another caller. It has no separate sweep loop, but every close fill, liquidation, harvest and staking instruction sweeps. It fills every request it can, including ones that are unfavourable to the house: since nobody can cancel a live request, leaving unfavourable ones unfilled would give traders a free option.
If keepers stop:
- New requests fail after about 60 s (
StaleHeartbeat). That includes new close requests: you can't start a close until someone records a fresh oracle observation. - Close requests already pending don't need a fresh watermark. Anyone who posts the pinned update can fill them until
pin + 30 s. - After
pin + 30 s, anyone (including you in the app) can cancel a pending request. For an open, that refunds the locked margin. For a close, the position stays open. The rent and execution fee are refunded, less the ¼ cancel bounty if someone else cancels. - Open positions stay open. They can't be liquidated without a fresh price. Positions in a settled market can be closed with
settle_retired, which needs no oracle. A due settlement can be executed by anyone who posts the update pinned at its settlement pin. - All of these steps are permissionless: anyone can post heartbeats and Pyth updates and send fills in place of the team keeper. The Papersol app doesn't offer self-filling.
- Deposits, withdrawals, staking, unstaking and reward claims keep working. None of them reads the oracle.
- Queue payments wait until someone calls
harvest.
Pause and emergency controls
| Control | Who | Effect |
|---|---|---|
set_paused(true) | Guardian or admin, instantly | request_open and execute_open fail with Paused. Pending opens can't fill while paused. If the pause is lifted before their deadline they can still fill; otherwise they expire and cancelling them refunds the margin. |
set_paused(false) | Admin only | Resumes opens. |
| OI brake, lower | Guardian or admin, instantly | Caps a side's live + pending notional for new requests and fills. |
| OI brake, raise or release | Admin only | — |
| Trailing OI limits | Risk keeper (a hot key), instantly | Any value between current usage and the governance ceiling; blocks new requests above it. |
| Cancel a pending proposal | Guardian or admin | A settlement proposal (like re-opening its market) only while both the Solana clock and the market's last_oracle_ts are more than 1 h before its pin, or after the settlement lapses (more than 7 days after the pin) |
Never paused: closes (request and fill), liquidations, withdrawals, deposits, staking and claims, queue payments, cancels and settlements.
Governance
Before launch the admin sets parameters directly. Deposits and requests are refused until launch, and
launchcan't be undone.After launch, changes to economics, execution parameters, market parameters, markets, settlement prices, the trader cap, the timelock and roles are proposals. The exceptions are the pause, the OI brake and the trailing OI limits, which change instantly through their role keys (above). The admin proposes; it takes effect no earlier than
timelock_delay_slater, and anyone can then execute it. A ready proposal lapses if nobody executes it within 7 days of itseta. The guardian or admin can cancel a proposal before it is executed, except that a settlement proposal can be cancelled only while both the Solana clock and the market'slast_oracle_tsare more than 1 h before its pin, or after the settlement lapses (more than 7 days after the pin). Actions are validated both when proposed and when executed.Timelock: 60 s on devnet; mainnet: TBD (the runbook calls for at least 48 h). The delay is at most 30 days. Production builds refuse to
launchwith a delay under 24 h. In every build, aSetTimelockDelayproposal can't set the delay below the lesser of the current delay and 24 h: a delay of 24 h or more can never drop below 24 h, and a shorter one can never be shortened. Devnet's 60 s delay was set before the launch floor existed; the rule only stops it from being lowered. Test anddevnet-feature builds skip the launch floor.What a proposal can change: economics (fees, fee split, mint curve, staker cap, bust buffer, reward stream window), execution parameters, any market's parameters (haircut, max leverage, min margin, max position, OI ceilings, retirement), adding a market, a retired market's final settlement price, the per-trader notional cap, the timelock delay, the guardian, the risk keeper, and the admin (a two-step handover; the new admin must accept).
Changes apply to open positions. Bust buffer, haircut parameters and fee rate are read when a position is closed or liquidated, not stored on the position. A queued win pays the fee in force when it is paid.
Hard bounds, enforced by the program: fee ≤ 10%; bust buffer < 0.1%; max leverage ≤ 1000; haircut base rate < 100%; pin delay 4–60 s; fill window 5–600 s; execution fee ≤ 0.01 SOL; reward stream 1 h–7 days; timelock ≤ 30 days, at least 24 h at launch, and never lowered below the lesser of the current delay and 24 h (full list). Test and
devnet-feature builds relax the pin-delay floor to 1 s, the reward-stream floor to 0 and the launch timelock floor; mainnet is built without them.Market settlement. Settlement only applies to a market that is already retired, so it takes no new opens. Neither whoever executes it nor a trader holding positions can choose its price or which price a position gets.
- Settlement pin. A
SettleMarketproposal names a fallback price, which must be within 10% of the market'slast_oracle_pricewhen proposed. Proposing it records itsetaon the market assettle_pin_ts, the settlement pin. A market has one pending settlement at a time; cancelling the proposal clears the pin, and so does re-opening the market, which voids any settlement proposed before. Both are allowed only while both the Solana clock and the market'slast_oracle_tsare more than 1 h before its pin, or after the settlement lapses (more than 7 days after the pin); otherwise they fail withSettlementPending. - Nothing prices a position at or after the pin any other way. From the pin on, close requests must be pinned (every sample) before it (
SettlementPending), and liquidations must use a price published before it. A close committed before the pin keeps precedence: it still fills at its own pin, even after settlement, andsettle_retiredrefuses a position with a pending close until that close is filled, voided, or expired and cancelled. - Execution waits until
pin + 120 s(theliq_max_age_sceiling), when every pre-pin price is too old to liquidate with, so executing early can't save a position that busted before the pin and executing late can't doom one. Anyone can then execute it with the unique Pyth update pinned at the pin (prev_publish_time < pin ≤ publish_time), posted any time later. If that update passes the confidence and EMA limits recorded in the proposal (snapshotted when proposed, so another proposal executed before or after can't pick the outcome), the market settles at its price. If it is authentic but inadmissible, or can't even be normalized, the proposed price applies. - Dead feed (nothing published at or after the pin): only the admin, only 24 h after the pin, and only if no update at or after the pin was ever recorded, at the proposed price, re-checked against the 10% band. This is not permissionless, because a missing observation doesn't prove the feed is dead (keepers may simply be down).
- Veto window. A settlement can be vetoed (the proposal cancelled, or its market re-opened with
SetMarket,retired = false) only while both the Solana clock and the market'slast_oracle_tsare earlier thanpin − 3,600 s(SETTLE_VETO_MARGIN_S; 2 s in test anddevnet-feature builds). Pyth time can run ahead of the Solana clock, so a clock still before the pin doesn't prove the pinned price is unknown, and a veto once it may be known would be a choice of outcome. With a timelock under 1 h, such as the current deployment's 60 s, the pin is already less than 1 h away when proposed, so the settlement can't be vetoed before it lapses. If it lapses unexecuted (7 days after the pin), cancelling it clears the pin for a new proposal.
The price is set once and bound to that retirement. After that, anyone can settle its positions at that price using the normal PnL rules, and the Papersol keeper does so automatically.
- Settlement pin. A
What the admin cannot do: move user balances, the treasury or staker cash; substitute ordinary oracle prices (the Pyth receiver is hard-coded); skip the configured timelock after launch. The admin can, however, settle a retired market at the proposed price, within the 10% band above, on the dead-feed path (the proposed price also applies, deterministically, when the pinned update is inadmissible), and can shorten the timelock through a proposal, but not below the lesser of the current delay and 24 h. The admin can withdraw accrued
dev_fees, up to the amount accrued.
Upgrade authority
The program is upgradeable. The upgrade authority can deploy new code, and an upgrade bypasses every rule on this page, including the timelock. On devnet, one key is upgrade authority, admin and guardian. For mainnet the runbook calls for a Squads multisig as upgrade authority and admin, and a published verifiable build (solana-verify). Mainnet authority setup: TBD.
The runbook's upgrade procedure: re-run the full test suites, get the diff re-reviewed (audited if it touches funds or timing), rebuild verifiably, and propose it in Squads with the executable hash. Account layouts are append-only; accounts created before a field was appended are zero-extended by the permissionless migrate_market and migrate_request.
Risks
- Not audited. The program has had internal and automated reviews only.
- Wins can queue indefinitely. The house is funded only by losses and starts at $0. While the queue is empty, treasury above $5M is swept to stakers. In a sustained one-way move, wins become IOUs. They are paid from whatever backing cash exists, and any shortfall waits for future losses (The house).
- Hard-bust liquidation. Any authentic Pyth update within the 10 s window that passes the EMA check and meets the liquidation rule (bust after the confidence band, or underwater at the midpoint; Liquidation) can be used to liquidate the whole margin, wicks included, whatever its confidence width. A close that fills past the bust price also loses the whole margin.
- Oracle trust. Pyth and the Wormhole guardians are trusted. A compromised or manipulated feed could liquidate positions or pay false wins. The EMA check (and, except for liquidations, the confidence check) limits this risk but doesn't remove it.
- Keeper dependence. New requests need fresh oracle observations, and every request needs a filler. If keepers stop and nobody else steps in, you can't start a new close; your available funds stay withdrawable.
- Fill price is set by the pin. You learn the price only after the pin, and orders have no limit price or minimum payout. Fast markets can fill well away from the price you saw. Whether the pinned price was still unpublished when you sent the order depends on the Solana clock and on recent observations (see Watermark above).
- Governance changes apply to open positions, after the timelock.
- Upgrade risk. The upgrade authority can change anything.
- Session-key risk. A leaked session key can trade, and lose, your whole available balance until it expires or you revoke it. It can't withdraw. Requests it already placed can still fill after you revoke it.
- Throughput. Close fills, liquidations, harvests and staking all write one global account, and requests and open fills read it. Heavy load can therefore delay transactions. Requests stay fillable until
pin + 30 s; after that, anyone can cancel them and the applicable margin and escrow are refunded. - $NOTE's price is set by the market, not the protocol. $NOTE is minted on every close, including hedged trades, so continuing emissions can weigh on its price. The protocol doesn't support, buy back or guarantee any price. Its only protocol-backed use is earning USDC by staking it.
- Retired-market settlement. From a settlement pin until execution, a retired market's positions are frozen: only closes committed before the pin can still fill. A committed close must be filled before it expires; an unfilled one is cancelled and the position then settles at the settlement price. The admin's dead-feed path is discretionary: during a 24 h observation outage a compromised admin could use the proposed price even though a pinned update exists, bounded by the 10% band, which at high leverage can still be a whole margin.