Paboxo

Oracles and market hours

The order the adapter checks things in, the freshness rules, the calendar arithmetic, and what a weekend does to a position.

The narrative is on oracles and market hours. This page is the decision procedure and the clock arithmetic behind them.

Adapter status

Each price feed sits behind a ChainlinkFeedAdapter. Its status() checks, in this order, and returns at the first match (ChainlinkFeedAdapter):

#ConditionStatus
1The Stock Token's issuer has paused the oracle (oraclePaused())OraclePaused (4)
2Answer ≤0\le 0, or updatedAt =0= 0InvalidAnswer (5)
3age>maxAgeHard\text{age} > \text{maxAgeHard}HardStale (3)
4The calendar says the market is closed nowMarketClosed (1)
5Within the reopen graceMarketClosed (1)
6age≤maxAgeInSession\text{age} \le \text{maxAgeInSession}Fresh (0)
7OtherwiseStaleInSession (2)

Where the age of the answer is

age=max⁡(tnow−updatedAt, 0)\text{age} = \max(t_{\text{now}} - \text{updatedAt},\ 0)

(ChainlinkFeedAdapter), and the reopen grace holds when

calendar set  ∧  tnow>maxAgeInSession  ∧  ∃ t∈[max⁡(updatedAt, tnow−maxAgeInSession), tnow]:¬ isOpen(t)\text{calendar set} \;\wedge\; t_{\text{now}} > \text{maxAgeInSession} \;\wedge\; \exists\, t \in \big[\max(\text{updatedAt},\ t_{\text{now}} - \text{maxAgeInSession}),\ t_{\text{now}}\big] : \neg\,\text{isOpen}(t)

— that is, the market was closed at some instant since the answer was published, within the last in-session window, so the answer is a price from before the closure and the feed has not yet published in the new session (ChainlinkFeedAdapter). The interval is checked as a whole: against each holiday range, and against the weekly window's start offset (as configured and one hour earlier, for daylight saving) (ChainlinkFeedAdapter). So a closure shorter than the window, such as a one-day holiday, counts as fully as a weekend.

Three consequences of the order:

  • The hard cap is checked before the calendar and is never softened. A feed that has stopped is unusable at maxAgeHard, weekend or not.
  • The calendar is checked before the age. A price that is one minute old is still MarketClosed while the market is shut.
  • The grace is checked before the age, too. An answer published before the last closure is MarketClosed during the grace even when it is younger than maxAgeInSession — after a one-day holiday, the pre-holiday answer is not Fresh. A round published after the reopen starts the interval after the closure, so it is Fresh immediately. The grace ends on its own: once a new round lands, or once the clock is maxAgeInSession into the session.

WETH and USDG adapters have no calendar, so for them steps 4 and 5 never apply.

What serves a price, and what needs Fresh

Two different questions, answered by two different functions.

Can a price be read? latestRoundData on the adapter reverts for OraclePaused, InvalidAnswer, HardStale and StaleInSession, and serves the last answer for Fresh and MarketClosed (ChainlinkFeedAdapter). The TokenDataStream in front of it adds its own age cap and a positivity check (TokenDataStream · 0xCa6c…3045). So over a weekend, health checks and valuations keep working on Friday's price.

Is the price Fresh? Actions that open or raise risk require priceStatus to be exactly Fresh (0) for both the collateral and the borrow token (LendingPoolRouter):

ActionNeeds Fresh on both tokensSource
Borrowyes, when requireFreshForBorrowLendingPoolRouter
Liquidateyes, when requireFreshForLiquidationLendingPoolRouter
Withdraw collateral, with debtyes, when requireFreshForBorrowLendingPoolRouter
Withdraw collateral, no debtno
Supply collateral, repayno — reads no price
Earn vault deposit, withdrawno — reads no price

Both flags are on for every market (read 12 October 2026, again at block 92690105, on all five markets at block 94915008, on MSFT/USDG and AAPL/USDG at block 99393260, and on QQQ/USDG, GOOGL/USDG and AMD/USDG at block 113204091). A stale USDG feed therefore stops borrowing and liquidation in every market at once.

Freshness windows

Mainnet (block 86868761)
maxAgeInSession, all four launch adapters93,600 s (26 h)
maxAgeHard, all four launch adapters349,200 s (97 h)
TokenDataStream defaultMaxPriceAge349,200 s (97 h)

Read on 12 October 2026, and unchanged at block 92690105. The SPCX and TSLA adapters, added on 22 October 2026, have the same two windows, and TokenDataStream's effective cap for both tokens is 349,200 s (block 94915008). So do the MSFT and AAPL adapters, and the QQQ, GOOGL and AMD adapters added on 13 November 2026, with the same 349,200 s cap for their tokens (block 113204091). The TokenDataStream cap is a backstop, set equal to the adapters' hard cap so the adapter decides.

Every feed has a 24-hour heartbeat, so 26 hours in session leaves two hours of slack between rounds. A weekend is longer than 26 hours, which is why the calendar matters: from Friday's close to the Sunday reopen the status is MarketClosed, not StaleInSession. After the reopen, Friday's price is older than 26 hours, so it is not Fresh; the reopen grace keeps it MarketClosed rather than StaleInSession until the first new round makes it Fresh. Borrowing and liquidation wait for that round. After a one-day holiday the pre-holiday price can still be under 26 hours old at the reopen; the grace keeps it MarketClosed all the same, and the first new round — about a minute after the reopen, as measured after Labor Day 2026 — makes it Fresh. The hard cap, 97 hours, has to outlast the longest closure, a weekend joined to a holiday (about 81 hours measured).

The calendar

Whether the market is open is computed on-chain by MarketCalendar (MarketCalendar · 0x9181…aD04).

Position in the week. Seconds since Monday 00:00 UTC:

o=(t+3×86,400) mod 604,800o = (t + 3 \times 86{,}400) \bmod 604{,}800

The Unix epoch fell on a Thursday; adding three days moves the origin to Monday.

The weekly window [s,e)[s, e) is stored in those units and may wrap past Monday 00:00. It is closed when

{s≤o<es<eo≥s  ∨  o<es>e (wraps)\begin{cases} s \le o < e & s < e \\ o \ge s \;\vee\; o < e & s > e \text{ (wraps)} \end{cases}

Daylight saving. With autoDst on, both ends move one hour earlier while US daylight saving is in effect: from the second Sunday of March at 07:00 UTC to the first Sunday of November at 06:00 UTC, computed on-chain from the date.

(s,e)←((s+604,800−3,600) mod 604,800, (e+604,800−3,600) mod 604,800)(s, e) \leftarrow \big((s + 604{,}800 - 3{,}600) \bmod 604{,}800,\ (e + 604{,}800 - 3{,}600) \bmod 604{,}800\big)

Holidays are absolute ranges [start,end)[\text{start}, \text{end}); the market is closed inside any of them.

Mainnet: s=435,600s = 435{,}600 (Saturday 01:00 UTC), e=3,600e = 3{,}600 (Monday 01:00 UTC) — Friday 20:00 to Sunday 20:00 Eastern Standard Time. autoDst is on and 21 holiday closures are configured (closureCount() at block 92690105).

Worked: at t=1792420603t = 1792420603 (Monday 19 October 2026, 14:36:43 UTC, block 92690105):

o=(1792420603+259200) mod 604800=52603  (Monday 14:36:43)o = (1792420603 + 259200) \bmod 604800 = 52603 \;(\text{Monday } 14{:}36{:}43)

Daylight saving is in effect, so the window is (432000,0)(432000, 0): Saturday 00:00 UTC to Monday 00:00 UTC, still Friday 20:00 to Sunday 20:00 Eastern. It wraps (s>es > e), and 52603≥43200052603 \ge 432000 is false, 52603<052603 < 0 is false: open. Five days later, o=484603≥432000o = 484603 \ge 432000: closed. The calendar agrees: at that block weeklyCloseWindowAt returned (432000,0)(432000, 0), and isOpen returned true for tt and false for t+432000t + 432000.

A weekend

A weekend, Friday close to Monday
In-session window
UTC offset

Scroll sideways to see Monday →

Weekend timeline. The calendar closes Friday 20:00 ET and reopens Sunday 20:00 ET. While closed the adapter reports MarketClosed and serves the last price; borrowing, liquidation and withdrawing collateral with debt pause; repaying, adding collateral and Earn deposits and withdrawals stay open; interest keeps accruing.Market calendar (every Stock Token)OpenClosed — weekly windowOpenAdapter statusFreshMarketClosed — last price still servedMarketClosed (grace) until the first new round, then FreshBorrow · liquidate · withdraw collateral with debtAllowedPaused (needs Fresh)Paused until the first new roundRepay · add collateral · withdraw with no debt · Earn deposit and withdrawAllowed — reads no priceInterest on debtAccrues every second; booked at the next accrualFri 20:00 ETSat 00:00 UTCSat 20:00 ETSun 00:00 UTCSun 20:00 ETMon 00:00 UTC
The weekly window is stored in UTC and shifted one hour earlier during US daylight saving, so in Eastern time it is always Friday 20:00 to Sunday 20:00. Holiday closures add further closed ranges. The in-session window is 26 hours, shorter than the weekend, so a Friday price is no longer Fresh at the reopen: the reopen grace holds it at MarketClosed until a new round lands, for up to 26 hours. The one-hour window is illustrative only, to show what happens once the grace runs out; it is not a configured value.

What a weekend does to a position

The price is frozen; the debt is not. Interest keeps accruing every second the market is closed, and is booked at the next accrual. Over a closed window of Δt\Delta t seconds, at an unchanged rate, a position's debt grows by

Δd≈d⋅r ΔtY\Delta d \approx d \cdot \frac{r\,\Delta t}{Y}

and its health factor falls by the same proportion, on a price nobody can react to.

Worked, the illustrative position from position health over the 48-hour weekly window at the illustrative market's 2.3759375% APR:

Δd=2402.294128×0.023759375×17280031536000≈0.3128 USDG\Delta d = 2402.294128 \times 0.023759375 \times \frac{172800}{31536000} \approx 0.3128 \text{ USDG}

HF moves from 1.22533 to 1.22517 — negligible at that rate. At 90% utilisation on the same launch curve (54% APR) the same weekend adds about 7.11 USDG and HF falls to 1.22171. The real weekend risk is the price gap at the reopen, not the interest; see risks.

On this page