Oracle and observations
An observation is one oracle price of MON or ETH that a market accepts. Each accepted observation settles one funding interval. This page gives the acceptance rules and the limits of the test oracle.
Source: contracts/src/Market.sol (_readOracle, _observe), contracts/src/TestOracle.sol, and packages/shared/src/oracle.ts.
Test oracle
One operator controls the oracle prices. The prices are not live MON or ETH prices. The oracle does not verify Pyth signatures, a confidence value, or the source sequence. Market.testOracle() always returns true. A production adapter and an independent review are necessary before a funded deployment.
The observation model is for testnet only
On testnet, one operator signs the price of each observation and sends it with a maker quote, at a fixed interval. This model keeps the markets active without an external feed. It is not the design for a market with real funds, where an independent price feed must supply the observations.
The observation schedule
A market has a fixed observation interval. The boundaries are multiples of that interval in Unix time. nextBoundary() returns the first boundary after the last accepted observation.
| Deployment | Interval | Set by |
|---|---|---|
| Local chain, default | 60 s | LOCAL_OBSERVATION_SECONDS, default 60 |
| Local chain, Hedging demo | 10 s | LOCAL_OBSERVATION_SECONDS=10 |
| Monad testnet | 300 s | A constant in scripts/monad-testnet.ts |
The interval is a constructor input of the market. A deployed market cannot change it. The contract accepts an interval from 1 to 600 seconds.
How a price reaches the market
The market accepts update data in two forms:
| Form | Data | Use |
|---|---|---|
| Signed price | Price, timestamp, and a signature of the oracle signer | All current scripts and the keeper. No publish transaction is necessary. |
| Stored record | A timestamp of 32 bytes. The market reads the price that the owner stored with TestOracle.publish. | An older path. The contract still supports it. |
The oracle signer is the owner of the TestOracle contract, which is the deployer. The signature covers the chain ID, the oracle address, the feed ID, the price, and the timestamp.
Any address can submit a signed observation with submitObservation(updateData). The quoter can also attach it to a quote call. The demo loops use the second method, so the observation and the new quote arrive in one transaction.
Acceptance rules
The market accepts an observation only if all of these conditions are true:
- The signature is from the oracle signer, or a stored record exists.
- The price is from
1e6to1e30in 18-decimal units. - The timestamp is later than the last accepted timestamp.
- The timestamp is at or after the next boundary.
- The timestamp is not in the future, and it is at most 5 seconds before the block time.
Data that fails a rule reverts without a state change.
What an accepted observation does
- The contract updates the mark from the book.
- It calculates the squared log return between the new price and the last price, and applies the cap of 0.0025.
- It adds the net payment for one contract to the funding index. See Funding.
- It saves the new price and time, and sets the next boundary.
- It saves the current mark as the charge of the next interval.
- It emits an
Observationevent with the raw return, the capped return, the payment, the old charge, and the new charge.
An observation does not loop over accounts. Each account takes its payment from the funding index when it is next used.
Late and missing observations
| Gap since the last accepted observation | Result |
|---|---|
| Up to the gap limit (3 intervals) | One capped return between the two prices, and the fixed charge for the actual elapsed time |
| Above the gap limit | Both funding legs are skipped. The contract rebases to the new price. The mark stays frozen until the book gives a valid mark. |
The oracle halts when the block time is later than the last accepted time plus the gap limit plus 5 seconds. During a halt, new orders, withdrawals, and liquidations wait. Cancellation stays available. A new observation clears the halt.
A permitted gap does not recover the missing price path. For two log returns, the square of the sum is not the sum of the squares. Thus an omitted observation can increase or decrease the payment.
Recovery is a test tool
The recover commands accept a current price after a long pause. They do not restore missing returns. A long gap skips funding. Do not treat a recovered test path as evidence of continuous hedging.
Price moves in the demo loops
| Loop | Price at each observation |
|---|---|
Local pnpm demo serve | The last price times a pending shock factor, or else calm noise |
Testnet pnpm testnet serve | The last price, unchanged |
Testnet pnpm testnet shock and rebound | The last price times exp(-0.04) or exp(0.03) |
The calm noise of the local loop is a random log return with an annual volatility of 110% for MON and 60% for ETH. The loop limits it to 1% for each observation, so calm noise stays far below a shock. See Run the local exchange.
Open problems
- The test oracle does not prove that the selected price is the first eligible source update. A caller could omit a price.
- The source time and the receipt time have different cut-off rules.
- The target design uses Pyth with one valid price for each boundary. This is not implemented, and its availability on Monad is not confirmed. See Target architecture.