Oracle and MarketCalendar
Dependencies
Section titled “Dependencies”fact The protocol depends on exactly three external contract types: the Stock Token ERC-20s, one Chainlink-style AggregatorV3Interface per underlying (optionally a backup), and USDG. There are no other protocol integrations in v1. Two oracle sources exist on Robinhood Chain and both are supported: Chainlink Data Feeds (push, the primary per underlying) and Pyth Pro (pull, wrapped by PythProFeed into the same aggregator interface and used as the backup).
MarketCalendar
Section titled “MarketCalendar”MarketCalendar is a governed contract answering one question: is a timestamp exactly the official close of a US equity trading day?
| Rule | Value |
|---|---|
| Regular close | 20:00 UTC during US daylight saving time; 21:00 UTC otherwise (16:00 New York) |
| Days | Monday to Friday |
| Holidays | Registered by governance per year: setHoliday(dayIndex, bool) |
| Early closes | Registered per year: setEarlyClose(dayIndex, closeTs), typically 17:00 UTC (DST) or 18:00 UTC (13:00 New York) |
| DST start | Second Sunday in March, computed on-chain by a pure function |
| DST end | First Sunday in November |
DST boundaries for reference (US rule): 2026: 8 Mar to 1 Nov. 2027: 14 Mar to 7 Nov. 2028: 12 Mar to 5 Nov. 2029: 11 Mar to 4 Nov. 2030: 10 Mar to 3 Nov. Unit tests pin these dates.
Holiday and early-close registrations are governance actions and must be made before a series that spans them is created; createSeries rejects timestamps that fail isOfficialClose. If a holiday is added after a series exists and one of its observations now falls on it, the observation still executes at its recorded timestamp; the feed is normally idle on a holiday, so no round lands inside [closeTs − maxPreCloseLag, closeTs + closeGrace], the observation is Deferred and governance must force or cancel it after deferralWindow (a round published later can never qualify). Governance should avoid this by registering the calendar well ahead.
OracleAdapter
Section titled “OracleAdapter”observe(feed, closeTs) -> (price, ok) implements the read. The rule is deterministic and independent of when the keeper calls (audit finding M-ORACLE-WINDOW): the observed price is the closing print, defined purely by the feed’s round history.
- The call is accepted only once the close grace has elapsed:
observerevertsTooEarlybeforecloseTsandGraceNotElapseduntilcloseTs + closeGracehas passed (resolvableAt(closeTs) = closeTs + closeGrace + 1is the first accepted second). At that point every round that can qualify already exists. - Call
latestRoundData. Walk back rounds withgetRoundData(roundId − k)whileupdatedAt > closeTs + closeGrace, for at mostmaxRoundWalkrounds. The first round at or before the grace end — the last round published no later thancloseTs + closeGrace— is the candidate. - Require
updatedAt >= closeTs − maxPreCloseLag,answer > 0,answeredInRound >= roundId. Rounds published after the grace are never selected, so two keepers calling atcloseTs + 6 minandcloseTs + 2 hobserve the same round even if the feed printed many times in between. - If a
backupFeedis registered, repeat for it. If exactly one succeeds it is used. If both succeed and exactly one is fresh (published withinfreshLagof the close) the fresh one is used. If both are fresh (or both stale) and they differ by more thanmaxDeviationBps, the observation is Deferred. - If neither succeeds, Deferred. Anyone may retry; a feed that was merely unreachable recovers naturally because its closing print still qualifies. A feed that never published inside the window cannot recover on its own.
- After
deferralWindow, governance mayproposeForceObserve(feed, closeTs, price)(eventForceObserveProposed), executable by anyone viaexecuteForceObserveafterforceDelay, revocable by owner or guardian in the meantime (revokeForceObserve); orcancelObservation(feed, closeTs). A successful read or force emitsObservationResolved.
| Parameter | Default | Role | Label |
|---|---|---|---|
closeGrace | 5 minutes (hard maximum 1 hour, must be below settlementWindow) | Rounds up to closeTs + closeGrace count as the closing print | fact |
maxPreCloseLag | 30 minutes (1 second to 24 hours) | Oldest acceptable closing print relative to the close | fact |
settlementWindow | 2 hours | Keeper deadline for off-chain operations; does not affect which round is selected | fact |
maxLookback | 26 hours | Staleness bound for the provisional latestPrice only | fact |
maxDeviationBps | 300 (3%) | Primary/backup disagreement threshold | fact |
deferralWindow | 72 hours | Earliest governance force or cancel after a deferral | fact |
forceDelay | 24 hours (hard minimum 24 hours) | Delay between a force proposal and its execution | fact |
maxRoundWalk | 64 rounds (hard maximum MAX_ROUND_WALK_LIMIT = 500) | Bound on the walk back from the latest round | fact |
freshLag | 6 hours (1 second to 24 hours) | A closing print younger than this is fresh; when exactly one of primary/backup is fresh it is used and no deferral occurs | fact |
closeGrace and maxPreCloseLag are set together by setCloseWindow(closeGrace, maxPreCloseLag) (owner only, CloseWindowUpdated); the other seven by setParams(settlementWindow, maxLookback, maxDeviationBps, deferralWindow, forceDelay, maxRoundWalk, freshLag) (owner only, ParamsUpdated). Backup feeds are registered with setBackupFeed(primary, backup); a backup equal to the primary reverts SameFeed.
closeTs + closeGrace; rounds after the grace are never used, so the result is the same whenever the keeper calls and a post-close print cannot leak into an observation. The pre-close lag tolerates feeds whose last print before the close is a few minutes old.Data sources on Robinhood Chain mainnet
Section titled “Data sources on Robinhood Chain mainnet”fact Chainlink publishes 57 push feeds for Robinhood Chain mainnet (chain id 4663), of which 35 track Robinhood stock tokens; all are 8-decimal proxies with a 24 h heartbeat and a 0.5 % deviation threshold, and the stock feeds already include the ERC-8056 uiMultiplier. Every proxy and every paired stock token was verified on chain on 2026-09-05 (description, decimals, fresh latestRoundData, uiMultiplier, oraclePaused == false); the joined registry is contracts/deploy/oracle/feeds.mainnet.json and the address table is on the Automation page. Robinhood lists 194 stock tokens on mainnet; only these 35 have a Chainlink feed, and there is no HOOD / USD Chainlink feed.
fact Pyth serves Robinhood Chain through Pyth Pro (pull oracle, verifier 0xACeA761c27A909d4D3895128EBe6370FDE2dF481 on mainnet and testnet, verification fee 1 wei). Pyth Core / Hermes is not deployed on the chain. Pyth Pro covers every one of the 35 Chainlink-listed stocks plus HOOD, USDG, ETH and BTC. Signed updates are fetched off chain by whoever wants to post them (Pyth Pro stream access is credentialed) and verified on chain permissionlessly; the protocol itself holds no credentials and runs no poster.
PythProFeed
Section titled “PythProFeed”PythProFeed is an ownerless, pause-less contract that implements AggregatorV3Interface (8 decimals) on top of the Pyth Pro verifier. post(bytes update) is permissionless: it pays the verifier fee, verifies the Pyth signature on chain, parses the payload, and appends a round if the update is for the configured feed id, carries a positive price and an exponent in [−18, 0], is not flagged Closed by Pyth’s market-session field (equity feeds), has a confidence interval within maxConfidenceBps, is no older than maxAge (60 s), is not from the future and is newer than the previous round. Prices are scaled to 8 decimals and multiplied by the stock token’s uiMultiplier, matching the Chainlink convention. Malformed payloads, unknown property kinds, replays and untrusted signers all revert; excess fee is refunded.
Registered with OracleAdapter.setBackupFeed(chainlinkProxy, pythProFeed), it lets any party place an exact Pyth closing print inside the observation window, which the adapter cross-checks against the Chainlink primary within maxDeviationBps. If nobody posts, the primary is used alone.
Close-window coverage of the Chainlink push feeds
Section titled “Close-window coverage of the Chainlink push feeds”fact Deviation-triggered feeds do not print at the close by construction. Sampling the last 16 rounds of all 35 equity feeds around the 2026-09-04 close (20:00 UTC), 19 had a round inside the default window [closeTs − 30 min, closeTs + 5 min] and 16 did not (NVDA, TSLA, MSFT, META, GOOGL, SPY, QQQ, BABA, GME, SLV, USO, RKLB, PLTR, CRCL, TSM, SGOV). The production configuration is therefore maxPreCloseLag = 24 h (the contract maximum and the feed heartbeat) and maxRoundWalk = 500, applied at deployment on testnet and by the first governance proposal on mainnet (the adapter is Timelock-owned from its first block): a live feed then always has a qualifying round, and the selected price is the last print before the close, which the feed’s own deviation rule keeps within 0.5 % of the price at the close. Sample data: contracts/deploy/oracle/close-window-sample.2026-09-04.json.
estimate With a Pyth Pro poster active at the close, the observed price is the Pyth closing print cross-checked against the Chainlink primary; without one, it is the Chainlink pre-close print with a 0.5 % bound. Either way the observation is deterministic and independent of who calls observe.
Provisional price
Section titled “Provisional price”latestPrice(feed) returns the most recent positive answer and reverts StalePrice(feed) if it is older than maxLookback, non-positive, or answeredInRound < roundId. It is used only to value SHIELD stock during Subscription for the prefund requirement and pro-rata bookkeeping. It never affects settlement.
ERC-8056 multiplier
Section titled “ERC-8056 multiplier”fact Stock Token feeds already include the uiMultiplier (Robinhood building guide). The adapter reads the feed as-is and the protocol never multiplies by uiMultiplier. Test mocks expose uiMultiplier and balanceOfUI while keeping raw ERC-20 balances, so tests would catch any accidental double application.
Failure modes and responses
Section titled “Failure modes and responses”| Failure | Detection | Response |
|---|---|---|
| Feed did not update around the close | No round in [closeTs − maxPreCloseLag, closeTs + closeGrace] | Deferred; later rounds cannot qualify, so governance forces or cancels after 72 h |
| Feed unreachable when the keeper calls | latestRoundData/getRoundData revert | Deferred; retry — the closing print still qualifies once the feed is readable again |
| Keeper calls before the grace has elapsed | GraceNotElapsed | Call rejected without state change; retry from resolvableAt(closeTs) |
More than maxRoundWalk rounds after the closing print before the call | Walk bound | Deferred; governance resolves (keepers should call at resolvableAt) |
| Feed stale but a backup exists | Primary fails, backup passes | Backup used |
| Feeds disagree by more than 3% | Deviation check | Deferred; retried; governance resolves |
| Negative or zero answer | answer > 0 check | Round rejected |
| Governance cancels an intermediate observation | isCancelled | No coupon, no autocall, series continues (ObservationSkipped) |
| Governance cancels the maturity observation | isCancelled | Settles at lastPrice (the last good observation) |
| Governance cancels the strike | isCancelled | Series Cancelled, all deposits refundable |
See Oracle risk for the residual risks this design does not remove.