Skip to content

Oracle and MarketCalendar

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 is a governed contract answering one question: is a timestamp exactly the official close of a US equity trading day?

RuleValue
Regular close20:00 UTC during US daylight saving time; 21:00 UTC otherwise (16:00 New York)
DaysMonday to Friday
HolidaysRegistered by governance per year: setHoliday(dayIndex, bool)
Early closesRegistered per year: setEarlyClose(dayIndex, closeTs), typically 17:00 UTC (DST) or 18:00 UTC (13:00 New York)
DST startSecond Sunday in March, computed on-chain by a pure function
DST endFirst Sunday in November
isOfficialClose(ts) = officialCloseOf(ts / 86400) == ts officialCloseOf(day) = 0 if weekend or isHoliday(day) = earlyClose(day) if registered = day × 86400 + 20h if isDST(day) = day × 86400 + 21h otherwise

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.

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.

  1. The call is accepted only once the close grace has elapsed: observe reverts TooEarly before closeTs and GraceNotElapsed until closeTs + closeGrace has passed (resolvableAt(closeTs) = closeTs + closeGrace + 1 is the first accepted second). At that point every round that can qualify already exists.
  2. Call latestRoundData. Walk back rounds with getRoundData(roundId − k) while updatedAt > closeTs + closeGrace, for at most maxRoundWalk rounds. The first round at or before the grace end — the last round published no later than closeTs + closeGrace — is the candidate.
  3. Require updatedAt >= closeTs − maxPreCloseLag, answer > 0, answeredInRound >= roundId. Rounds published after the grace are never selected, so two keepers calling at closeTs + 6 min and closeTs + 2 h observe the same round even if the feed printed many times in between.
  4. If a backupFeed is registered, repeat for it. If exactly one succeeds it is used. If both succeed and exactly one is fresh (published within freshLag of the close) the fresh one is used. If both are fresh (or both stale) and they differ by more than maxDeviationBps, the observation is Deferred.
  5. 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.
  6. After deferralWindow, governance may proposeForceObserve(feed, closeTs, price) (event ForceObserveProposed), executable by anyone via executeForceObserve after forceDelay, revocable by owner or guardian in the meantime (revokeForceObserve); or cancelObservation(feed, closeTs). A successful read or force emits ObservationResolved.
ParameterDefaultRoleLabel
closeGrace5 minutes (hard maximum 1 hour, must be below settlementWindow)Rounds up to closeTs + closeGrace count as the closing printfact
maxPreCloseLag30 minutes (1 second to 24 hours)Oldest acceptable closing print relative to the closefact
settlementWindow2 hoursKeeper deadline for off-chain operations; does not affect which round is selectedfact
maxLookback26 hoursStaleness bound for the provisional latestPrice onlyfact
maxDeviationBps300 (3%)Primary/backup disagreement thresholdfact
deferralWindow72 hoursEarliest governance force or cancel after a deferralfact
forceDelay24 hours (hard minimum 24 hours)Delay between a force proposal and its executionfact
maxRoundWalk64 rounds (hard maximum MAX_ROUND_WALK_LIMIT = 500)Bound on the walk back from the latest roundfact
freshLag6 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 occursfact

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 − maxPreCloseLag (30 min) closeTs (official close) closeTs + closeGrace (5 min) closing print (selected) later rounds never selected The last round updated no later than closeTs + closeGrace, and no earlier than closeTs − maxPreCloseLag, is the observed price.
Round selection. The observed price is the last round at or before 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.

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 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.

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.

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.

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.

FailureDetectionResponse
Feed did not update around the closeNo round in [closeTs − maxPreCloseLag, closeTs + closeGrace]Deferred; later rounds cannot qualify, so governance forces or cancels after 72 h
Feed unreachable when the keeper callslatestRoundData/getRoundData revertDeferred; retry — the closing print still qualifies once the feed is readable again
Keeper calls before the grace has elapsedGraceNotElapsedCall rejected without state change; retry from resolvableAt(closeTs)
More than maxRoundWalk rounds after the closing print before the callWalk boundDeferred; governance resolves (keepers should call at resolvableAt)
Feed stale but a backup existsPrimary fails, backup passesBackup used
Feeds disagree by more than 3%Deviation checkDeferred; retried; governance resolves
Negative or zero answeranswer > 0 checkRound rejected
Governance cancels an intermediate observationisCancelledNo coupon, no autocall, series continues (ObservationSkipped)
Governance cancels the maturity observationisCancelledSettles at lastPrice (the last good observation)
Governance cancels the strikeisCancelledSeries Cancelled, all deposits refundable

See Oracle risk for the residual risks this design does not remove.