// SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// The one thing this feed asks of the oracle: how long a reader has paid for. The same seat serves /// HarmonicOracleV2's rows, HarmonicWeekendMark's marks and this feed's records: one payment covers all three. interface IRiskFeedSeat { function paidUntil(address reader) external view returns (uint64); } /// The issuer's own two views on every Robinhood stock token, read by this contract and never taken on trust. interface IRiskFeedStockToken { function uiMultiplier() external view returns (uint256); function oraclePaused() external view returns (bool); } /// A Chainlink Data Feed proxy, read only as a second, independent price beside the writer's mark. interface IRiskFeedAggregator { function decimals() external view returns (uint8); function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80); } /// The calls an adapter makes on the feed that made it. interface IRiskFeedForAdapter { function statusOf(address token) external view returns (uint8 status, uint128 mark, uint40 markAt, uint40 checkedAt, uint32 round); function seat() external view returns (address); function symbolOf(address token) external view returns (bytes32); } /// What a lender, a venue or an agent calls. uint8 everywhere, so an integrator needs no enum. interface IHarmonicRiskFeed { function statusOf(address token) external view returns (uint8 status, uint128 mark, uint40 markAt, uint40 checkedAt, uint32 round); function markOf(address token, uint8 minStatus) external view returns (uint128 mark, uint8 status, uint40 checkedAt); function sessionNow() external view returns (uint8 session, uint40 endsAt); function canRead(address reader) external view returns (bool); function adapterOf(address token, uint8 minStatus) external view returns (address); } /** * HARMONIC RISK FEED: one answer per Robinhood stock token, inside a * transaction. Can it be priced right now, at what mark, and is anything * unusual happening to it. * * THE SEAM. A stock token halts when the issuer halts, goes still every * weekend and every exchange holiday, steps its multiplier when a dividend * lands and when a stock splits, and the chain has none of that in it. A * lender, a perps venue or an agent's vault that values one inside its own * transaction needs four things at once: the session, the halt, the corporate * action and a mark with its time. This contract keeps one record per token * and answers all four in one call. * * WHAT A RECORD IS. Written by the house's operations wallet from desks it * already runs: the session oracle (the issuer's print, its halt flag and its * verdict), the Weekend Mark (what the token traded at on chain while its * market was shut), Halt Watch (trades through a halt) and the Dividend Ledger * (every step of the token's multiplier). A record carries the mark in 1e8 * dollars per token, where it came from, when it was observed, the issuer's * verdict and since when, and a set of flags for anything unusual, each with * one source. THE MULTIPLIER IS NEVER THE WRITER'S: every whole write reads * the token's own uiMultiplier() inside the write, and when it differs from * the one on record the contract records the step itself, from the two * numbers, with the Dividend Ledger's own five percent line. * * WHAT IS READ LIVE, AT EVERY ASSESSMENT, AND NEVER TAKEN ON TRUST: * the token's own oraclePaused() and uiMultiplier(): a pause raised since * the last write reads Paused at once, and a multiplier that moved since the * record reads Rebasing at once; a token that will not answer either reads * Unknown, never priced; * the session, from the exchange calendar this contract holds (New York * time, daylight time per day, holidays and early closes), so a weekend or * a holiday is known here at the second it begins, with no write; * the writer's liveness: every cycle the writer vouches for each * record it read and found within its stated tolerance, and naming the ones * it could not; a record nobody has vouched for inside quietAfter reads * Quiet, whatever it last said; * for a token with a Chainlink feed registered, that feed's latest round, * and an issuer mark the feed contradicts by more than disputeBps reads * Disputed. * * THE STATUSES, numbered so that a higher number is more priceable * (Unknown 0, Quiet 1, Halted 2, Paused 3, Rebasing 4, Disputed 5, Stale 6, * ClosedHeld 7, ClosedMarked 8, Extended 9, Live 10), and decided in this * order, the first that holds being the answer: * 1. never written Unknown * 2. the token's oraclePaused() is true now Paused * 3. its uiMultiplier() now differs from the record's Rebasing * 4. either of those two reads did not answer Unknown * 5. nobody has vouched for the record inside quietAfter Quiet * 6. the writer's verdict is Halted Halted * 7. the writer's verdict is Paused Paused * 8. the latest split or the latest unexplained fall of * the multiplier (lastSplitAt, lastAnomalyAt) was * recorded at or after the time the mark was observed, * whatever step was recorded after it Rebasing * 9. the verdict is Unknown, there is no mark, or the * calendar holds no entry for now Unknown * 10. the market is shut (weekend or holiday): a weekend * mark observed inside closedMaxAge ClosedMarked * anything else (the last print before it shut, or an * older weekend mark) ClosedHeld * 11. the session is open and the mark is not an issuer * print with an Open verdict Stale * 12. a current Chainlink round differs from the issuer * mark by more than disputeBps Disputed * 13. the extended session (pre, post, overnight) Extended * the regular session, or a token that prints * around the clock Live * So a status of ClosedHeld or above is never given on a read that did not * answer, a quiet record, a halt, a pause, a moved multiplier, a split since * the mark, an unknown verdict, a missing mark or a day off the calendar. * * WHO PAYS. Nobody pays this contract. A read from off the chain (a wallet, a * script: any call whose sender is the transaction's origin) is free. A * contract reading a record inside its own transaction must be a paid reader * of the HarmonicOracle named as the seat, the same subscription that serves * its rows and the weekend marks; this contract holds and moves nothing. An * adapter this contract deployed passes the gate here and applies it to its * own caller, so an adapter is never a way around it. * * WHAT IT KEEPS ITSELF. A mark has a source and a time, and no source has no * mark; within one source a mark never steps back in time; nothing is dated * more than FUTURE_SLACK ahead of the block; an Open verdict is a statement * about an issuer print and needs one; a verdict's start time is kept while * the verdict holds; the multiplier, every step's kind and size, and the * latest split and anomaly (which never move back) are the contract's own; * the write time, the vouching and the round are the contract's and never the * writer's. The rules a reader integrates against (moveBps, quietAfter, * closedMaxAge, feedMaxAge, disputeBps) are fixed at deployment. * * A STATUS IS A MEASUREMENT OF A RECORD AT A BLOCK. It is not a view on a * stock, not a price the house trades at and not a recommendation to lend. */ contract HarmonicRiskFeed is IHarmonicRiskFeed { /* ---------------------------------------------------------- vocabulary */ enum Status { Unknown, Quiet, Halted, Paused, Rebasing, Disputed, Stale, ClosedHeld, ClosedMarked, Extended, Live } enum Session { Unknown, Regular, Extended, Weekend, Holiday, Continuous } enum Source { None, Issuer, WeekendMark, HeldPrint } /// HarmonicOracleV2's own order, so a verdict reads the same on both contracts. enum Verdict { Unknown, Halted, Paused, Held, Stale, Open } enum Step { None, Dividend, Split, Anomaly } /// What the writer sends for one token. The multiplier, the step, the write time, the vouching and the round are the contract's. struct Input { uint128 mark; // 1e8 dollars per token (share times multiplier); 0 means no mark uint40 markAt; // unix seconds the mark was observed uint8 source; // a Source uint8 verdict; // a Verdict: the session oracle's verdict on the issuer's quote, as the writer last read it uint16 flags; // the writer's flags, F_* below uint16 bandBps; // half-width in bps of what the writer measured around the mark; 0 means not stated uint40 verdictSince; // when the writer first saw this verdict; kept from the record while the verdict is unchanged uint8 clock; // 0: the 24/5 calendar governs; 1: the token prints around the clock } /// One token's record as stored: three slots. struct Record { // slot 0: the mark uint128 mark; uint40 markAt; uint40 writtenAt; // block time of the last whole write or move; the contract's uint32 round; // changes so far; the contract's; an adapter's round id uint16 bandBps; // slot 1: the multiplier, read from the token inside the write, and where its last step came from uint128 multiplier; // 1e18 is one share per token uint128 stepFrom; // the multiplier before the last step this contract recorded; 0 means none // slot 2: the clocks and the issuer's state uint40 verdictSince; uint40 unvouchedSince; // nonzero while the writer's vouch has named it; the contract's uint40 stepAt; // block time of the write that recorded the last step; the contract's uint40 lastSplitAt; // the latest split this contract recorded; never moves back uint40 lastAnomalyAt; // the latest unexplained fall this contract recorded; never moves back uint8 stepKind; // a Step; the contract's uint8 verdict; uint8 clock; uint8 source; uint16 flags; } /// The whole answer, in one call. struct Risk { uint8 status; // a Status uint8 session; // a Session, at this block uint40 sessionEndsAt; // when the calendar next changes the session; 0 when it cannot say uint128 mark; // 1e8 dollars per token; 0 means no mark uint8 source; // a Source uint40 markAt; // when the mark was observed uint40 checkedAt; // the last time the writer vouched for this record uint16 bandBps; uint8 verdict; // a Verdict, as the writer last saw it uint40 verdictSince; uint128 multiplier; // the multiplier on record uint128 liveMultiplier; // uiMultiplier() read in this call; 0 when it did not answer uint128 stepFrom; uint8 stepKind; // a Step int32 stepPpm; // the last step in parts per million of the multiplier before it, worked out here uint40 stepAt; uint40 lastSplitAt; uint40 lastAnomalyAt; uint16 flags; // the writer's flags, F_* below uint16 liveFlags; // read in this call, L_* below int256 feedAnswer; // the Chainlink round's answer, 8 decimals; 0 when none is registered or it did not answer uint40 feedAt; // its updatedAt uint32 round; } /* ----------------------------------------------------------- the flags */ // THE WRITER'S FLAGS, each from one desk. A flag is a measurement; none of them changes the status. uint16 public constant F_DIVIDEND_RECENT = 1; // the Dividend Ledger saw a dividend step in the last 7 days uint16 public constant F_SPLIT_RECENT = 2; // ... a split-scale step in the last 30 days uint16 public constant F_ANOMALY_RECENT = 4; // ... a fall of at most five percent in the last 30 days uint16 public constant F_ACTION_OPEN = 8; // the issuer lists a corporate action on it, not completed and not yet matched to a step uint16 public constant F_CALENDAR_UNREAD = 16; // the issuer's corporate-action calendar did not read, so a missing F_ACTION_OPEN is not a none uint16 public constant F_HALT_RECENT = 32; // the writer read it halted in the last 24 hours uint16 public constant F_TRADED_THROUGH_HALT = 64; // Halt Watch saw a hooked pool trade it while the oracle read halted or paused, last 24 hours uint16 public constant F_PRINT_FLAT = 128; // in an open session the issuer's mid has not changed for longer than the writer allows uint16 public constant F_SESSION_MISMATCH = 256; // the calendar and the issuer disagree on whether the market is shut uint16 public constant F_LEDGER_LAGGING = 512; // the Dividend Ledger is behind the token's multiplier, or has not read in 25 minutes uint16 public constant F_WEEKEND_MARK_WITHHELD = 1024; // a weekend mark exists for the hold and stands on too little, so the held print is carried uint16 public constant F_CHAIN_GAP = 2048; // the chain's own pool price and the issuer's print, both fresh, differ by more than 300 bps uint16 public constant F_ACTION_NO_STEP = 4096; // a cash distribution's window closed with no multiplier step while the ledger watched uint16 private constant F_ALL = 8191; // READ LIVE IN EVERY ASSESSMENT. uint16 public constant L_PAUSED = 1; // oraclePaused() returned true uint16 public constant L_PAUSE_UNREAD = 2; // oraclePaused() did not answer a boolean uint16 public constant L_MULT_MOVED = 4; // uiMultiplier() differs from the multiplier on record uint16 public constant L_MULT_UNREAD = 8; // uiMultiplier() did not answer uint16 public constant L_FEED_UNREAD = 16; // a Chainlink feed is registered and did not answer a positive, dated round uint16 public constant L_FEED_QUIET = 32; // the feed's last round is older than feedMaxAge uint16 public constant L_FEED_DISAGREES = 64; // the feed's current round and the issuer mark differ by more than disputeBps uint16 public constant L_UNVOUCHED = 128; // the writer's last vouch named this record as one it could not vouch for uint16 public constant L_CALENDAR_ENDED = 256; // the calendar holds no entry for now uint16 public constant L_STEP_AFTER_MARK = 512; // the last recorded step, of any kind, is at or after the time the mark was observed; a flag only: the status reads lastSplitAt and lastAnomalyAt /* ------------------------------------------------------- the constants */ uint256 public constant FUTURE_SLACK = 300; // seconds a time may run ahead of the block: two clocks, never a forecast uint256 public constant SPLIT_DENOM = 20; // a step past one twentieth (five percent) either way is a split: the Dividend Ledger's line uint256 public constant READ_GAS = 60_000; // the most one read of a token or a feed may spend uint8 public constant FEED_DECIMALS = 8; uint256 private constant OPEN = 34_200; // 09:30 New York uint256 private constant CLOSE = 57_600; // 16:00 uint256 private constant EARLY_CLOSE = 46_800; // 13:00 uint256 private constant EVENING = 72_000; // 20:00: the weekend hold and the overnight session turn here uint8 private constant DAY_KNOWN = 1; uint8 private constant DAY_DST = 2; uint8 private constant DAY_HOLIDAY = 4; uint8 private constant DAY_EARLY = 8; /* ----------------------------------------------------------- the rules */ IRiskFeedSeat public immutable seat; uint16 public immutable moveBps; // the writer's tolerance: at checkedAt its own fresh read was within this of the mark uint32 public immutable quietAfter; // seconds after checkedAt a record reads Quiet uint32 public immutable closedMaxAge; // seconds a weekend mark stays ClosedMarked after the time it was observed uint32 public immutable feedMaxAge; // seconds a Chainlink round counts as current uint16 public immutable disputeBps; // a current feed round this far from an issuer mark reads Disputed /* ----------------------------------------------------------- the state */ address public owner; address public writer; uint40 public vouchedAt; // the writer's last vouch uint32 public calendarFrom; // the first New York day (days since 1970-01-01) the calendar holds uint32 public calendarThrough; // the last; 0 until the first day is written mapping(address => Record) private _rec; mapping(address => bytes32) public symbolOf; address[] private _tokens; mapping(address => bool) private _listed; mapping(address => address) public feedOf; // token -> Chainlink proxy; zero means none mapping(address => bool) public isAdapter; mapping(uint256 => uint256) private _days; // 32 day codes a word, one byte each /* --------------------------------------------------------- the events */ event RecordSet(address indexed token, bytes32 indexed symbol, uint8 source, uint8 verdict, uint128 mark, uint40 markAt, uint16 flags, uint128 multiplier, uint32 round, uint40 writtenAt); event MarkMoved(address indexed token, uint128 mark, uint40 markAt, uint16 bandBps, uint32 round, uint40 writtenAt); event StepSeen(address indexed token, uint8 indexed kind, uint128 from, uint128 to, uint40 at); event HaltSeen(address indexed token, uint40 since, uint32 round); event Vouched(uint40 at, uint32 except); event Unvouched(address indexed token, uint40 since); event DaysSet(uint32 firstDay, uint32 lastDay); event FeedSet(address indexed token, address feed); event AdapterDeployed(address indexed token, uint8 minStatus, address adapter); event WriterSet(address indexed writer); event OwnerSet(address indexed owner); error NotOwner(); error NotWriter(); error NotSubscribed(); error ZeroAddress(); error LengthMismatch(); error BadRule(); error BadEnum(); error BadFlags(); error MarkNeedsSource(); error MarkNeedsTime(); error FromTheFuture(); error OpenNeedsIssuerPrint(); error VerdictNeedsSince(); error MultiplierUnread(address token); error OlderMark(address token); error NotWritten(address token); error BadDays(); error BadFeed(address token, address feed); error BadMinStatus(); error NotPriceable(address token, uint8 status); modifier onlyOwner() { if (msg.sender != owner) revert NotOwner(); _; } modifier onlyWriter() { if (msg.sender != writer && msg.sender != owner) revert NotWriter(); _; } /** * THE GATE, the oracle's own. A call from off the chain has msg.sender == * tx.origin and is free. A call from inside a contract must come from a * paid reader of the seat, or from an adapter this contract deployed, * which applies the same gate to its own caller. A read routed through an * aggregator contract is a read from inside a contract. */ modifier paidReader() { if (msg.sender != tx.origin && !isAdapter[msg.sender] && seat.paidUntil(msg.sender) < block.timestamp) revert NotSubscribed(); _; } constructor(address seat_, address writer_, uint16 moveBps_, uint32 quietAfter_, uint32 closedMaxAge_, uint32 feedMaxAge_, uint16 disputeBps_) { if (seat_ == address(0)) revert ZeroAddress(); if (moveBps_ == 0 || moveBps_ > 1_000) revert BadRule(); if (quietAfter_ < 60 || quietAfter_ > 1 days) revert BadRule(); if (closedMaxAge_ < 1 hours || closedMaxAge_ > 3 days) revert BadRule(); if (feedMaxAge_ < 1 hours || feedMaxAge_ > 7 days) revert BadRule(); if (disputeBps_ < 10 || disputeBps_ > 5_000) revert BadRule(); seat = IRiskFeedSeat(seat_); moveBps = moveBps_; quietAfter = quietAfter_; closedMaxAge = closedMaxAge_; feedMaxAge = feedMaxAge_; disputeBps = disputeBps_; owner = msg.sender; writer = writer_ == address(0) ? msg.sender : writer_; emit OwnerSet(owner); emit WriterSet(writer); } /* ------------------------------------------------------------- roles */ function setOwner(address owner_) external onlyOwner { if (owner_ == address(0)) revert ZeroAddress(); owner = owner_; emit OwnerSet(owner_); } function setWriter(address writer_) external onlyOwner { if (writer_ == address(0)) revert ZeroAddress(); writer = writer_; emit WriterSet(writer_); } /// The Chainlink proxy read beside a token's mark, or zero to read none. Each must answer decimals() == 8. function setFeeds(address[] calldata tokens_, address[] calldata feeds) external onlyOwner { uint256 n = tokens_.length; if (feeds.length != n) revert LengthMismatch(); for (uint256 i = 0; i < n; i++) { if (tokens_[i] == address(0)) revert ZeroAddress(); if (feeds[i] != address(0)) { (bool ok, uint256 d) = _word(feeds[i], IRiskFeedAggregator.decimals.selector); if (!ok || d != FEED_DECIMALS) revert BadFeed(tokens_[i], feeds[i]); } feedOf[tokens_[i]] = feeds[i]; emit FeedSet(tokens_[i], feeds[i]); } } /* ------------------------------------------------------------ writes */ /// Whole records, so a cycle's changes land together. Each reads the token's own multiplier inside this write. function setRecords(address[] calldata tokens_, bytes32[] calldata symbols, Input[] calldata recs) external onlyWriter { uint256 n = tokens_.length; if (symbols.length != n || recs.length != n) revert LengthMismatch(); for (uint256 i = 0; i < n; i++) _set(tokens_[i], symbols[i], recs[i]); } /// The mark alone, for a record whose every other field still holds: the cheap write a price move needs. function moveMarks(address[] calldata tokens_, uint128[] calldata marks, uint40[] calldata markAts, uint16[] calldata bands) external onlyWriter { uint256 n = tokens_.length; if (marks.length != n || markAts.length != n || bands.length != n) revert LengthMismatch(); uint40 now_ = uint40(block.timestamp); for (uint256 i = 0; i < n; i++) { Record storage r = _rec[tokens_[i]]; if (r.writtenAt == 0 || r.mark == 0) revert NotWritten(tokens_[i]); if (marks[i] == 0) revert MarkNeedsSource(); if (markAts[i] < r.markAt) revert OlderMark(tokens_[i]); if (markAts[i] > block.timestamp + FUTURE_SLACK) revert FromTheFuture(); r.mark = marks[i]; r.markAt = markAts[i]; r.bandBps = bands[i]; r.writtenAt = now_; r.round += 1; if (r.unvouchedSince != 0) r.unvouchedSince = 0; emit MarkMoved(tokens_[i], marks[i], markAts[i], bands[i], r.round, now_); } } /** * THE HALT, FAST. The writer read the issuer's halt flag up for these * tokens: each written record that does not already read Halted reads * Halted from this block, with the time the flag was first read. Only a * whole write takes a halt off, and only the writer's next pass on a * fresh quote with the flag down does that. Nothing else in the record * moves, and nothing here vouches for it. */ function haltMany(address[] calldata tokens_, uint40[] calldata since) external onlyWriter { uint256 n = tokens_.length; if (since.length != n) revert LengthMismatch(); for (uint256 i = 0; i < n; i++) { Record storage r = _rec[tokens_[i]]; if (r.writtenAt == 0) revert NotWritten(tokens_[i]); if (since[i] == 0) revert VerdictNeedsSince(); if (since[i] > block.timestamp + FUTURE_SLACK) revert FromTheFuture(); if (r.verdict == uint8(Verdict.Halted)) continue; r.verdict = uint8(Verdict.Halted); r.verdictSince = since[i]; r.round += 1; emit HaltSeen(tokens_[i], since[i], r.round); } } /** * THE VOUCH. Once a cycle the writer says it read every listed token and * found each one either rewritten or within its tolerance, except those it * names here. A named record stops being vouched for until it is written * again; every other record counts as checked at this block. */ function vouch(address[] calldata except) external onlyWriter { uint40 now_ = uint40(block.timestamp); uint256 n = except.length; for (uint256 i = 0; i < n; i++) { Record storage r = _rec[except[i]]; if (r.writtenAt != 0 && r.unvouchedSince == 0) { r.unvouchedSince = now_; emit Unvouched(except[i], now_); } } vouchedAt = now_; emit Vouched(now_, uint32(n)); } /** * THE EXCHANGE CALENDAR, one byte per New York day: 1 known, 2 daylight * time, 4 a full holiday, 8 an early close at 13:00. Days are written * contiguously, so the calendar never has a gap; a day outside it reads * Unknown. Writing a day again corrects it. */ function setDays(uint32 firstDay, uint8[] calldata codes) external onlyWriter { uint256 n = codes.length; if (n == 0 || n > 400) revert BadDays(); if (calendarThrough != 0 && (firstDay < calendarFrom || uint256(firstDay) > uint256(calendarThrough) + 1)) revert BadDays(); for (uint256 i = 0; i < n; i++) { uint8 c = codes[i]; if (c & DAY_KNOWN == 0 || c > 15) revert BadDays(); uint256 d = uint256(firstDay) + i; uint256 shift = (d & 31) << 3; _days[d >> 5] = (_days[d >> 5] & ~(uint256(0xff) << shift)) | (uint256(c) << shift); } if (calendarThrough == 0) calendarFrom = firstDay; uint32 last = uint32(uint256(firstDay) + n - 1); if (last > calendarThrough) calendarThrough = last; emit DaysSet(firstDay, last); } /// Several of the writes above in one transaction: the sender is kept, so each call's own check still applies. function batch(bytes[] calldata calls) external onlyWriter { for (uint256 i = 0; i < calls.length; i++) { (bool ok, bytes memory ret) = address(this).delegatecall(calls[i]); if (!ok) { assembly { revert(add(ret, 0x20), mload(ret)) } } } } function _set(address token, bytes32 symbol, Input calldata x) private { if (token == address(0)) revert ZeroAddress(); _check(x); Record storage r = _rec[token]; // WITHIN ONE SOURCE A MARK NEVER STEPS BACK IN TIME. if (r.mark != 0 && x.mark != 0 && x.source == r.source && x.markAt < r.markAt) revert OlderMark(token); // THE MULTIPLIER IS THE TOKEN'S OWN, read inside this write; a token that will not answer is not written. (bool ok, uint256 m) = _word(token, IRiskFeedStockToken.uiMultiplier.selector); if (!ok || m == 0 || m > type(uint128).max) revert MultiplierUnread(token); uint40 now_ = uint40(block.timestamp); uint128 prev = r.multiplier; if (prev != 0 && m != prev) { // A STEP, RECORDED HERE FROM THE TWO NUMBERS: its kind and size are the contract's, never the writer's. uint8 kind = _classify(prev, m); r.stepFrom = prev; r.stepAt = now_; r.stepKind = kind; if (kind == uint8(Step.Split)) r.lastSplitAt = now_; else if (kind == uint8(Step.Anomaly)) r.lastAnomalyAt = now_; emit StepSeen(token, kind, prev, uint128(m), now_); } r.multiplier = uint128(m); // A VERDICT'S START IS KEPT WHILE THE VERDICT HOLDS: only a change of verdict takes a new one. if (r.writtenAt == 0 || x.verdict != r.verdict) { if (x.verdictSince == 0) revert VerdictNeedsSince(); r.verdictSince = x.verdictSince; r.verdict = x.verdict; } r.mark = x.mark; r.markAt = x.markAt; r.writtenAt = now_; r.round += 1; r.bandBps = x.bandBps; r.unvouchedSince = 0; r.source = x.source; r.clock = x.clock; r.flags = x.flags; symbolOf[token] = symbol; if (!_listed[token]) { _listed[token] = true; _tokens.push(token); } emit RecordSet(token, symbol, x.source, x.verdict, x.mark, x.markAt, x.flags, uint128(m), r.round, now_); } function _check(Input calldata x) private view { if (x.source > uint8(Source.HeldPrint) || x.verdict > uint8(Verdict.Open) || x.clock > 1) revert BadEnum(); if (x.flags & ~F_ALL != 0) revert BadFlags(); if ((x.mark == 0) != (x.source == uint8(Source.None))) revert MarkNeedsSource(); if (x.mark != 0 && x.markAt == 0) revert MarkNeedsTime(); uint256 limit = block.timestamp + FUTURE_SLACK; if (x.markAt > limit || x.verdictSince > limit) revert FromTheFuture(); if (x.verdict == uint8(Verdict.Open) && x.source != uint8(Source.Issuer)) revert OpenNeedsIssuerPrint(); } /* ------------------------------------------------- reads, gated: the answer */ /// Everything about one token, assessed at this block. function riskOf(address token) external view paidReader returns (Risk memory) { return _assess(token); } /// The same for several tokens. function risksOf(address[] calldata tokens_) external view paidReader returns (Risk[] memory out) { out = new Risk[](tokens_.length); for (uint256 i = 0; i < tokens_.length; i++) out[i] = _assess(tokens_[i]); } /// The short answer: the status, the mark, when it was observed, when it was last vouched for, and the round. function statusOf(address token) external view paidReader returns (uint8 status, uint128 mark, uint40 markAt, uint40 checkedAt, uint32 round) { Risk memory r = _assess(token); return (r.status, r.mark, r.markAt, r.checkedAt, r.round); } /// The mark, only when the status is at least minStatus; otherwise NotPriceable with the status it read. /// checkedAt is the writer's last vouch for the record, not the time the price was observed: statusOf and riskOf /// carry that as markAt. ClosedHeld carries no limit on the mark's age. function markOf(address token, uint8 minStatus) external view paidReader returns (uint128 mark, uint8 status, uint40 checkedAt) { Risk memory r = _assess(token); if (r.status < minStatus || r.mark == 0) revert NotPriceable(token, r.status); return (r.mark, r.status, r.checkedAt); } /// The stored record, unassessed. writtenAt is zero for a token never written. function recordOf(address token) external view paidReader returns (Record memory) { return _rec[token]; } /// Stored records for several tokens: one eth_call for the writer, free from off the chain. function recordsOf(address[] calldata tokens_) external view paidReader returns (Record[] memory out) { out = new Record[](tokens_.length); for (uint256 i = 0; i < tokens_.length; i++) out[i] = _rec[tokens_[i]]; } /* ------------------------------------------------- reads, free to everyone */ function tokens() external view returns (address[] memory) { return _tokens; } function count() external view returns (uint256) { return _tokens.length; } /// Whether a contract would be served a record right now. function canRead(address reader) external view returns (bool) { return isAdapter[reader] || seat.paidUntil(reader) >= block.timestamp; } /// The session at any instant the calendar holds, and when it next changes. function sessionAt(uint256 t) external view returns (uint8 session, uint40 endsAt) { (Session s, uint40 e) = _sessionAt(t); return (uint8(s), e); } function sessionNow() external view returns (uint8 session, uint40 endsAt) { (Session s, uint40 e) = _sessionAt(block.timestamp); return (uint8(s), e); } /// The rules a reader integrates against, fixed at deployment. function rules() external view returns (uint16 moveBps_, uint32 quietAfter_, uint32 closedMaxAge_, uint32 feedMaxAge_, uint16 disputeBps_, address seat_) { return (moveBps, quietAfter, closedMaxAge, feedMaxAge, disputeBps, address(seat)); } /* --------------------------------------------------------- the adapters */ /** * An AggregatorV3-shaped adapter for one token, answering only while the * token's status is at least minStatus, which the deployer chooses among * the four statuses that carry a mark meant to be read as a price: * ClosedHeld, ClosedMarked, Extended, Live. Anyone may deploy one; it is * made here, at an address fixed by the token and minStatus, so the feed * knows its own adapters. */ function deployAdapter(address token, uint8 minStatus) external returns (address adapter) { if (token == address(0)) revert ZeroAddress(); if (minStatus < uint8(Status.ClosedHeld) || minStatus > uint8(Status.Live)) revert BadMinStatus(); adapter = adapterOf(token, minStatus); if (adapter.code.length == 0) { new HarmonicRiskFeedAdapter{salt: keccak256(abi.encode(token, minStatus))}(token, minStatus); isAdapter[adapter] = true; emit AdapterDeployed(token, minStatus, adapter); } } /// Where the adapter for a token and a minimum status is, deployed or not. function adapterOf(address token, uint8 minStatus) public view returns (address) { bytes32 init = keccak256(abi.encodePacked(type(HarmonicRiskFeedAdapter).creationCode, abi.encode(token, minStatus))); return address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), keccak256(abi.encode(token, minStatus)), init))))); } /* ---------------------------------------------------------- assessment */ function _assess(address token) private view returns (Risk memory r) { Record storage rec = _rec[token]; r.mark = rec.mark; r.source = rec.source; r.markAt = rec.markAt; r.bandBps = rec.bandBps; r.verdict = rec.verdict; r.verdictSince = rec.verdictSince; r.multiplier = rec.multiplier; r.stepFrom = rec.stepFrom; r.stepKind = rec.stepKind; r.stepAt = rec.stepAt; r.lastSplitAt = rec.lastSplitAt; r.lastAnomalyAt = rec.lastAnomalyAt; r.flags = rec.flags; r.round = rec.round; if (rec.stepFrom != 0) { int256 ppm = (int256(uint256(rec.multiplier)) - int256(uint256(rec.stepFrom))) * 1e6 / int256(uint256(rec.stepFrom)); r.stepPpm = ppm > type(int32).max ? type(int32).max : int32(ppm); } if (rec.unvouchedSince != 0) { r.checkedAt = rec.writtenAt; r.liveFlags = L_UNVOUCHED; } else { r.checkedAt = rec.writtenAt > vouchedAt ? rec.writtenAt : vouchedAt; } if (rec.writtenAt == 0) r.checkedAt = 0; if (rec.stepAt != 0 && rec.stepAt >= rec.markAt) r.liveFlags |= L_STEP_AFTER_MARK; Session s; if (rec.writtenAt != 0 && rec.clock == 1) s = Session.Continuous; else (s, r.sessionEndsAt) = _sessionAt(block.timestamp); r.session = uint8(s); if (s == Session.Unknown) r.liveFlags |= L_CALENDAR_ENDED; if (rec.writtenAt == 0) return r; // Unknown: never written _live(token, r); r.status = uint8(_status(r)); } /// The token's two flags and the feed's round, read now, into the answer. function _live(address token, Risk memory r) private view { (bool pok, uint256 p) = _word(token, IRiskFeedStockToken.oraclePaused.selector); if (!pok || p > 1) r.liveFlags |= L_PAUSE_UNREAD; else if (p == 1) r.liveFlags |= L_PAUSED; (bool mok, uint256 m) = _word(token, IRiskFeedStockToken.uiMultiplier.selector); if (!mok || m == 0 || m > type(uint128).max) r.liveFlags |= L_MULT_UNREAD; else { r.liveMultiplier = uint128(m); if (m != r.multiplier) r.liveFlags |= L_MULT_MOVED; } address f = feedOf[token]; if (f == address(0)) return; (bool fok, int256 ans, uint256 upd) = _round(f); if (!fok) { r.liveFlags |= L_FEED_UNREAD; return; } r.feedAnswer = ans; r.feedAt = uint40(upd); if (block.timestamp > upd + feedMaxAge) { r.liveFlags |= L_FEED_QUIET; return; } Session s = Session(r.session); if ((s == Session.Regular || s == Session.Extended || s == Session.Continuous) && r.source == uint8(Source.Issuer) && r.mark != 0) { uint256 a = uint256(ans); uint256 d = a > r.mark ? a - r.mark : uint256(r.mark) - a; if (d * 10_000 > a * disputeBps) r.liveFlags |= L_FEED_DISAGREES; } } /// THE ORDER, first match wins. Every path that cannot see a fresh, open, undisputed price ends below ClosedHeld. function _status(Risk memory r) private view returns (Status) { uint16 lf = r.liveFlags; if (lf & L_PAUSED != 0) return Status.Paused; if (lf & L_MULT_MOVED != 0) return Status.Rebasing; if (lf & (L_PAUSE_UNREAD | L_MULT_UNREAD) != 0) return Status.Unknown; if (block.timestamp > uint256(r.checkedAt) + quietAfter) return Status.Quiet; if (r.verdict == uint8(Verdict.Halted)) return Status.Halted; if (r.verdict == uint8(Verdict.Paused)) return Status.Paused; // A SPLIT OR A FALL AT OR AFTER THE MARK HOLDS IT, read from the latest of each and never from the last step // alone, so a dividend recorded after a split cannot hide the split from a mark observed before it. uint40 hard = r.lastSplitAt > r.lastAnomalyAt ? r.lastSplitAt : r.lastAnomalyAt; if (hard != 0 && hard >= r.markAt) return Status.Rebasing; Session s = Session(r.session); if (r.verdict == uint8(Verdict.Unknown) || r.mark == 0 || s == Session.Unknown) return Status.Unknown; if (s == Session.Weekend || s == Session.Holiday) { if (r.source == uint8(Source.WeekendMark) && block.timestamp <= uint256(r.markAt) + closedMaxAge) return Status.ClosedMarked; return Status.ClosedHeld; } if (r.source != uint8(Source.Issuer) || r.verdict != uint8(Verdict.Open)) return Status.Stale; if (lf & L_FEED_DISAGREES != 0) return Status.Disputed; return s == Session.Extended ? Status.Extended : Status.Live; } /// A step's kind from two multipliers: the Dividend Ledger's rule, drawn in integers. function _classify(uint256 from, uint256 to) private pure returns (uint8) { uint256 diff = to > from ? to - from : from - to; if (diff * SPLIT_DENOM > from) return uint8(Step.Split); if (to < from) return uint8(Step.Anomaly); return uint8(Step.Dividend); } /* ------------------------------------------------------------ the session */ function _code(uint256 day) private view returns (uint8) { if (calendarThrough == 0 || day < calendarFrom || day > calendarThrough) return 0; return uint8(_days[day >> 5] >> ((day & 31) << 3)); } /// A New York wall-clock second on a New York day, as unix seconds, by that day's own daylight bit; 0 off the calendar. function _at(uint256 day, uint256 sec) private view returns (uint40) { uint8 c = _code(day); if (c & DAY_KNOWN == 0) return 0; return uint40(day * 1 days + sec + (c & DAY_DST != 0 ? 4 hours : 5 hours)); } /** * THE SESSION. New York time is unix time less five hours, or four on a * day the calendar marks as daylight time; the clocks change at 02:00 on a * Sunday, inside the weekend hold, so no session boundary ever sits on a * change. Friday 20:00 to Sunday 20:00 is the weekend. A full holiday runs * from 20:00 the evening before to 20:00 on the day. The regular session is * 09:30 to 16:00, or to 13:00 on an early close, on a trading weekday. * Every other hour from Sunday 20:00 to Friday 20:00 is the extended * session: pre-market, after-hours and overnight. */ function _sessionAt(uint256 t) private view returns (Session, uint40) { if (t < 1 days) return (Session.Unknown, 0); uint256 local = t - 5 hours; uint8 c = _code(local / 1 days); if (c & DAY_DST != 0) { local = t - 4 hours; c = _code(local / 1 days); } if (c & DAY_KNOWN == 0) return (Session.Unknown, 0); uint256 day = local / 1 days; uint256 sec = local % 1 days; uint256 wd = (day + 3) % 7; // 0 Monday ... 4 Friday, 5 Saturday, 6 Sunday if ((wd == 4 && sec >= EVENING) || wd == 5 || (wd == 6 && sec < EVENING)) { return (Session.Weekend, _at(day + 6 - wd, EVENING)); } if (sec >= EVENING) { uint8 next = _code(day + 1); if (next & DAY_KNOWN == 0) return (Session.Unknown, 0); if (next & DAY_HOLIDAY != 0) return (Session.Holiday, _at(day + 1, EVENING)); return (Session.Extended, _at(day + 1, OPEN)); } if (c & DAY_HOLIDAY != 0) return (Session.Holiday, _at(day, EVENING)); uint256 close = c & DAY_EARLY != 0 ? EARLY_CLOSE : CLOSE; if (sec < OPEN) return (Session.Extended, _at(day, OPEN)); if (sec < close) return (Session.Regular, _at(day, close)); uint8 tomorrow = _code(day + 1); if (wd == 4 || tomorrow & DAY_KNOWN == 0 || tomorrow & DAY_HOLIDAY != 0) return (Session.Extended, _at(day, EVENING)); return (Session.Extended, _at(day + 1, OPEN)); } /* ------------------------------------------------------------- live reads */ /// One word from a no-argument view, bounded in gas and in return data; ok is false on a revert, no code or a short answer. function _word(address target, bytes4 sel) private view returns (bool ok, uint256 w) { assembly ("memory-safe") { mstore(0x00, sel) let success := staticcall(READ_GAS, target, 0x00, 0x04, 0x00, 0x20) ok := and(success, gt(returndatasize(), 0x1f)) w := mload(0x00) } if (!ok) w = 0; } /// A Chainlink round: ok only for a positive answer that fits the mark's width, dated no later than FUTURE_SLACK ahead of the block. function _round(address f) private view returns (bool ok, int256 answer, uint256 updatedAt) { bytes4 sel = IRiskFeedAggregator.latestRoundData.selector; assembly ("memory-safe") { let p := mload(0x40) mstore(p, sel) let success := staticcall(READ_GAS, f, p, 0x04, p, 0xa0) ok := and(success, gt(returndatasize(), 0x9f)) answer := mload(add(p, 0x20)) updatedAt := mload(add(p, 0x60)) } if (!ok || answer <= 0 || updatedAt == 0 || updatedAt > block.timestamp + FUTURE_SLACK || uint256(answer) > type(uint128).max) return (false, 0, 0); } } /** * HARMONIC RISK FEED ADAPTER: one stock token's mark in the shape existing * lending code already reads (AggregatorV3Interface, 8 decimals), answering * only while the feed's status for the token is at least minStatus, and * reverting NotPriceable with the status otherwise, so code that never checks * a status cannot read a halted, paused, rebasing, quiet, stale, disputed or * unknown token as a price. * * startedAt is when the mark was observed; updatedAt is the last time the * writer vouched for it, which is not when the price was observed. Code that * checks block.timestamp - updatedAt against a heartbeat bounds how recently * the record was vouched for, not how old the price is; the price's age is * block.timestamp - startedAt. A Closed status vouches for the last price * before the market shut, and its startedAt says how old that price is. * ClosedHeld carries no limit on the mark's age: through a weekend or a * holiday it is the last print before the shut, however old, and a record * that read Stale while the market was open reads ClosedHeld from the second * the market shuts, with no write. roundId is the record's change count; only * the latest round is kept, and getRoundData answers it alone. * * Deployed only by HarmonicRiskFeed.deployAdapter. It applies the oracle's * gate to its own caller: free from off the chain, a paid reader of the seat * from inside a contract. It holds nothing and has no owner. */ contract HarmonicRiskFeedAdapter { IRiskFeedForAdapter public immutable feed; address public immutable token; uint8 public immutable minStatus; IRiskFeedSeat public immutable seat; error NotSubscribed(); error NotPriceable(address token, uint8 status); error NoDataPresent(); constructor(address token_, uint8 minStatus_) { feed = IRiskFeedForAdapter(msg.sender); token = token_; minStatus = minStatus_; seat = IRiskFeedSeat(IRiskFeedForAdapter(msg.sender).seat()); } function decimals() external pure returns (uint8) { return 8; } function version() external pure returns (uint256) { return 1; } function description() external view returns (string memory) { bytes32 s = feed.symbolOf(token); uint256 n = 0; while (n < 32 && s[n] != 0) n++; bytes memory out = new bytes(n); for (uint256 i = 0; i < n; i++) out[i] = s[i]; return string(abi.encodePacked(out, " / USD, Harmonic risk feed")); } function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) { return _latest(); } function getRoundData(uint80 roundId) external view returns (uint80, int256, uint256, uint256, uint80) { (uint80 id, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredIn) = _latest(); if (roundId != id) revert NoDataPresent(); return (id, answer, startedAt, updatedAt, answeredIn); } function latestAnswer() external view returns (int256 answer) { (, answer,,,) = _latest(); } function latestTimestamp() external view returns (uint256 updatedAt) { (,,, updatedAt,) = _latest(); } function latestRound() external view returns (uint256 id) { (uint80 r,,,,) = _latest(); id = r; } /// The status without the revert, gated the same way, so a frontend or a keeper can read why. function statusNow() external view returns (uint8 status, uint128 mark, uint40 checkedAt) { _gate(); (status, mark,, checkedAt,) = feed.statusOf(token); } function _gate() private view { if (msg.sender != tx.origin && seat.paidUntil(msg.sender) < block.timestamp) revert NotSubscribed(); } function _latest() private view returns (uint80, int256, uint256, uint256, uint80) { _gate(); (uint8 status, uint128 mark, uint40 markAt, uint40 checkedAt, uint32 round) = feed.statusOf(token); if (status < minStatus || mark == 0) revert NotPriceable(token, status); return (uint80(round), int256(uint256(mark)), uint256(markAt), uint256(checkedAt), uint80(round)); } }