Automation and cranks
Design
Section titled “Design”fact Note Systems has no keeper daemon, cron job or privileged bot. Every state transition of a series is reachable in one of three ways, all of which are on-chain and permissionless:
| Layer | Who triggers it | What it does | Reward |
|---|---|---|---|
| Explicit cranks | Anyone | finalizeStrike, observe, settle, sweepFees, RollPolicy.roll | keeperRewardQuote / RollPolicy.bounty |
| Lazy execution | Whoever is already interacting | claim, redeem, redeemShield, settle and refund first run any due strike or observation | keeperRewardQuote to msg.sender |
| Chainlink-compatible upkeep | Any automation network or bot | NoteAutomation.checkUpkeep / performUpkeep batch the above | rewards forwarded to msg.sender |
If nobody cranks, the next user who touches the series does; the price observed is the same either way, because OracleAdapter selects the closing print deterministically (see Oracle and MarketCalendar).
Permissionless cranks
Section titled “Permissionless cranks”fact None of the following require a role.
| Function | When | Reward |
|---|---|---|
NoteCore.finalizeStrike(seriesId) | block.timestamp >= observations[0] and status Subscription | keeperRewardQuote (default 2 USDG) on success |
NoteCore.observe(seriesId) | block.timestamp >= observations[nextObs] and status Live | keeperRewardQuote on success |
NoteCore.settle(seriesId, account) | After strike, for any account with an unsettled deposit | none (but may trigger a lazy crank, see below) |
NoteCore.sweepFees(seriesId) | Any time accruedFees > 0 | none |
RollPolicy.roll(underlying) | Previous series of that underlying is no longer open, template enabled, feed fresh, cooldown elapsed | min(bounty, RollPolicy USDG balance) |
Minter.mintWeekly() | Once per epoch week | none |
Desk.harvest(seriesId) | After a coupon or settlement | none |
RevenueRouter.sellNoteForQuote | Any time (the buyback auction) | the auction price itself |
The strike/observation reward is paid from the series’ accruedFees and capped by them: min(keeperRewardQuote, accruedFees). A Deferred observation pays nothing until it succeeds. The reward is credited to keeperOwed[keeper] and withdrawn with NoteCore.claimKeeperReward(); NoteAutomation.performUpkeep claims it before forwarding, and a direct cranker claims it in a separate call. This keeps a keeper address that rejects transfers from blocking an observation.
Timing
Section titled “Timing”Observation timestamps are official US closes, so every crank time is known in advance. observe reverts with TooEarly(closeTs) before the timestamp and with InvalidStatus if the series is not Live. The OracleAdapter rejects observe (GraceNotElapsed) until closeTs + closeGrace (5 min) has passed, then selects the last round published no later than that instant, whoever calls and whenever. Cranking at OracleAdapter.resolvableAt(closeTs) or shortly after is ideal; calling later changes nothing except the reward race, unless more than maxRoundWalk rounds have been published since the close.
Reading the queue
Section titled “Reading the queue”uint256 n = core.seriesCount();for (uint256 id = 0; id < n; id++) { (bool strikeDue, bool observationDue, ) = core.dueWork(id); if (strikeDue) core.finalizeStrike(id); else if (observationDue) core.observe(id);}dueWork(seriesId) -> (strikeDue, observationDue, settleable) is readiness-based: it returns true only when the corresponding crank would succeed right now (close reached, oracle grace elapsed, oracle not paused, observation not deferred-and-pending). It returns (false, false, settleable) while NoteCore is paused.
Lazy execution
Section titled “Lazy execution”fact claim, redeem, redeemShield, settle and refund call an internal _crankIfDue first. It finalises a due strike or processes up to MAX_LAZY_OBSERVATIONS = 4 due observations, paying keeperRewardQuote per successful step to msg.sender exactly as an explicit crank would, then continues with the user’s action against the updated state.
- A lazy crank never makes the user’s action revert: if the strike/observation is not ready (grace not elapsed, oracle paused, observation deferred) it is simply skipped.
- If the user’s action itself reverts (for example
NothingToClaim), the whole transaction reverts, including the crank. Nothing is left half-done. - The result is byte-identical to the explicit path:
test/fuzz/core/LazyCrank.fuzz.t.soldrives a lazy and an explicit twin series through random price paths and asserts equal balances, buckets and statuses. - Users who are not interested in cranking pay the extra gas only when work is actually due;
dueWorklets front-ends warn about it.
RollPolicy
Section titled “RollPolicy”fact RollPolicy is the only address besides the owner allowed to call NoteCore.createSeries (NoteCore.seriesCreator, set by governance via setSeriesCreator). It holds one template per underlying and turns it into a concrete series on demand:
| Template field | Meaning |
|---|---|
feed | Chainlink proxy used as the series feed |
autocallBps, barrierBps, couponFloorBps, couponCapBps, refBps | Series economics, validated against NoteCore limits |
notionalCap, minTicket | Subscription limits |
subscriptionTradingDays | Subscription ends at the official close of the N-th trading day after today |
observationCount, observationSpacingTradingDays | Observations are official closes spaced N trading days apart (weekends, holidays and early closes come from MarketCalendar) |
maxFeedAge | roll refuses a feed whose latest round is older than this |
enabled | Guardian can disable an underlying instantly |
roll(underlying) (nonReentrant, whenNotPaused) checks TemplateDisabled, SeriesStillOpen (the last series of that underlying must be past Subscription/Live, i.e. cancelled, autocalled or matured), FeedStale and RollCooldown (minRollInterval, default 1 hour, max 30 days), computes the dates from the calendar, calls createSeries, emits Rolled(underlying, seriesId, caller, bountyPaid) and pays min(bounty, balance) USDG to the caller. previewRoll(underlying) returns (params, canRoll, reason) (reason one of TemplateDisabled, SeriesStillOpen, FeedStale, RollCooldown, Paused) so bots can simulate before sending. Governance funds the bounty pot with fund(amount) and can sweep it back.
Rolled series have no SeriesGauge by default; governance adds one when desired.
Chainlink-compatible upkeep
Section titled “Chainlink-compatible upkeep”fact NoteAutomation implements the Chainlink AutomationCompatibleInterface (checkUpkeep(bytes) -> (bool, bytes) / performUpkeep(bytes)). checkUpkeep scans up to maxSeriesPerCheck (default 256) series via dueWork (all series when checkData is empty, or the range abi.encode(from, to) for sharded upkeeps), the Minter week and every registered underlying’s previewRoll, and encodes a list of actions {Strike | Observe | MintWeekly | Roll}. performUpkeep executes at most maxActionsPerPerform (default 24) actions inside try/catch (one failing action never blocks the others, UpkeepAction(kind, id, success, reason) is emitted per action) and forwards its entire USDG balance (crank rewards and roll bounties) to msg.sender (RewardsForwarded).
status Chainlink Automation and Gelato are not yet live on Robinhood Chain. Until they are, anyone can run checkUpkeep/performUpkeep from a wallet, a Safe module, a Defender/OpenZeppelin relayer or a simple bot; the interface is unchanged when a network becomes available. Nothing in the protocol depends on it: the lazy path guarantees liveness on its own.
Mainnet Chainlink Data Feeds (Robinhood Chain)
Section titled “Mainnet Chainlink Data Feeds (Robinhood Chain)”fact 57 feeds are published for Robinhood Chain mainnet (chain id 4663). All are 8-decimal proxies with a 24 h heartbeat and 0.5 % deviation threshold; the equity feeds follow us_equities_24/5 market hours. Source: contracts/deploy/chainlink/feeds-robinhood-mainnet.json (mirrors the Chainlink feed directory).
| Feed | Proxy | Heartbeat | Threshold | Decimals |
|---|---|---|---|---|
| AAPL / USD | 0x6B22A786bAa607d76728168703a39Ea9C99f2cD0 | 86400 s | 0.5% | 8 |
| AMD / USD | 0x943A29E7ae51A4798823ca9eEd2ed533B2A22C72 | 86400 s | 0.5% | 8 |
| AMZN / USD | 0xD5a1508ceD74c084eBf3cBe853e2C968fB2a651C | 86400 s | 0.5% | 8 |
| ASML / USD | 0xB4106147E8cce40b7d46124090d373A71b70f87D | 86400 s | 0.5% | 8 |
| BABA / USD | 0x62Cc8F9b5f56a33c9C8A60c8B92779f523c4E984 | 86400 s | 0.5% | 8 |
| BTC / USD | 0xa2c5184bF03d373Dc9dE4876eb4Bce595B460251 | 86400 s | 0.5% | 8 |
| BTC.B / USD | 0x5BB5e6a17a477d5B6Fec77b4322daD4A66bFb732 | 86400 s | 0.5% | 8 |
| CBBTC / USD | 0x0009cD492adf8167f9eEBf1293556A673530a21a | 86400 s | 0.5% | 8 |
| CLSK / USD | 0x810c12D3a554Bc47fd39597Fe3b3AAC4941F50eF | 86400 s | 0.5% | 8 |
| COIN / USD | 0xA3a468A452940B7D6b69991207B508c609a98Ef2 | 86400 s | 0.5% | 8 |
| CRCL / USD | 0x6652eDf64bA3731C4F2D3ce821A0Fb1f1f6b482a | 86400 s | 0.5% | 8 |
| CRWV / USD | 0xe1b3aABCAFAd1c94708dc1367dcfF8Aa4407487C | 86400 s | 0.5% | 8 |
| DELL-USD | 0x1C6c8cADBe02E19129c39dDB92281cE4c0bf206b | 86400 s | 0.5% | 8 |
| ENA / USD | 0x2A291496b3aa19d8948e442Ef28Ee952f3Ee97E8 | 86400 s | 0.5% | 8 |
| ETH / USD | 0x78F3556b67E17Df817D51Ef5a990cDaF09E8d3A9 | 86400 s | 0.5% | 8 |
| EURC / USD | 0xfF2B10c1973eD10c841434f98e456d8f3a0D7DD8 | 86400 s | 0.5% | 8 |
| EWY / USD | 0xEFdf54610B62A7753Ec30bDc380847c12D32e1D1 | 86400 s | 0.5% | 8 |
| GME / USD | 0x27C71df6A64fB476468EdF256CF72c038baB5B67 | 86400 s | 0.5% | 8 |
| GOOGL / USD | 0xF6f373a037c30F0e5010d854385cA89185AE638b | 86400 s | 0.5% | 8 |
| INTC / USD | 0x3f390C5C24628Ac7C489515402235FeAD71D1913 | 86400 s | 0.5% | 8 |
| IONQ / USD | 0x22EfeC4919baf55F360E0EDee4AbEB26DE4971eb | 86400 s | 0.5% | 8 |
| LBTC / USD | 0xa621344AdAEE699491597Fd8890E0C59a5BFBE59 | 86400 s | 0.5% | 8 |
| LINK / USD | 0xe86e3422Aa9B5e8ee9f3E41a63975bC387A8bce9 | 86400 s | 0.5% | 8 |
| META / USD | 0x7C38C00C30BEe9378381E7B6135d7283356D71b1 | 86400 s | 0.5% | 8 |
| MSFT / USD | 0x45C3C877C15E6BA2EBB19eA114Ea508d14C1Af2E | 86400 s | 0.5% | 8 |
| MSTR / USD | 0x396118bdFB181e6240E74D243F266B061c0edc3D | 86400 s | 0.5% | 8 |
| MU / USD | 0x425EEFdCf05ed6526C3cE61Af99429A228a6d596 | 86400 s | 0.5% | 8 |
| NBIS / USD | 0xE1D87B116Ba0fe898998f1D140339D1fA1E09705 | 86400 s | 0.5% | 8 |
| NVDA / USD | 0x379EC4f7C378F34a1B47E4F3cbeBCbAC3E8E9F15 | 86400 s | 0.5% | 8 |
| ORCL / USD | 0x0e6a64a2B58A6693a531E6c555f3A5d042eEA844 | 86400 s | 0.5% | 8 |
| PLTR / USD | 0x820ABedFF239034956B7A9d2F0a331f9F075eB4c | 86400 s | 0.5% | 8 |
| QQQ / USD | 0x80901d846d5D7B030F26B480776EE3b29374C2ae | 86400 s | 0.5% | 8 |
| RGTI / USD | 0x2A045cF1C49c61c166C036d2f06FA2D2d984f765 | 86400 s | 0.5% | 8 |
| RKLB / USD | 0x045477BF65Aef6f4F2386ad0164579e48381CC74 | 86400 s | 0.5% | 8 |
| SGOV-USD | 0xa0DF4ee0fFf975306345875E3548Fcc519577A11 | 86400 s | 0.5% | 8 |
| SLV / USD | 0x209b73908e92Ae021826eD79609845451Ecba2ce | 86400 s | 0.5% | 8 |
| SNDK / USD | 0xfb133Fa4B7b385802B693a293606682Df47109A3 | 86400 s | 0.5% | 8 |
| SPCX / USD | 0xB265810950ba6c5C0Ff821c9963014a56fD8Bffb | 86400 s | 0.5% | 8 |
| SPY / USD | 0x319724394D3A0e3669269846abE664Cd621f9f6A | 86400 s | 0.5% | 8 |
| SYRUPUSDC / USD | 0x8765c3B9Cda41d1029E780D0c1C37C8200DC4675 | 86400 s | 0.5% | 8 |
| SYRUPUSDC / USDC Exchange Rate | 0x6317f016FA3e312C4625dee51d32b43a223011f8 | 86400 s | 0.05% | 18 |
| SYRUPUSDT / USDT Exchange Rate | 0xBB688c0184Ce03fEdac89D71ccE752Ab21bC2999 | 86400 s | 0.05% | 18 |
| TSLA / USD | 0x4A1166a659A55625345e9515b32adECea5547C38 | 86400 s | 0.5% | 8 |
| TSM / USD | 0x874cF94aa8eC88Fd9560094dD065f2fB3E41Fc2F | 86400 s | 0.5% | 8 |
| USAR-USD | 0xA994d3684e8400A6c8078226925779FdeE682DD9 | 86400 s | 0.5% | 8 |
| USDC / USD | 0x9e6f4605992a899eE2999999F3Ec80C41F452546 | 86400 s | 0.5% | 8 |
| USDE / USD | 0xb9fB4e65744E4178894f7C61CF80E8a48A5f224a | 86400 s | 0.5% | 8 |
| USDG / USD | 0x61B7e5650328764B076A108EFF5fa7282a1B9aD2 | 86400 s | 0.5% | 8 |
| USDS / USD | 0x2D88D75b625633dCcd65d9d53BfDD3Aea2d8e84f | 86400 s | 0.5% | 8 |
| USDT / USD | 0xbf3550B6fAe1671da7C238Af12e03Ac586BEf3B1 | 86400 s | 0.5% | 8 |
| USO / USD | 0x75a9c76Ef439e2C7c2E5a34Ab105EcFe3766431c | 86400 s | 0.5% | 8 |
| WBTC / USD | 0x62107b0d3adA75fc1697fD342d99eed947a3aA5E | 86400 s | 0.5% | 8 |
| WEETH / EETH Exchange Rate | 0xb63f44E40aA811Cc69Fc55da786a5F3834100B4A | 86400 s | 0.05% | 18 |
| WEETH / USD | 0xf882e1D50352aecB0Ac85378378918BCf40511e7 | 86400 s | 0.5% | 8 |
| WSTETH / STETH Exchange Rate | 0x8E3Eb706B170c8FD1DdcD402932D952887736f9A | 86400 s | 0.05% | 18 |
| WSTETH / USD | 0x3F5040B50FB37934573B210fE54B53a6F1A792E8 | 86400 s | 0.5% | 8 |
| syrupUSDG / USDG Exchange Rate | 0xDd194C66aDcb422F188a04434e4824D70c151cF0 | 86400 s | 0.05% | 18 |
Note: there is currently no HOOD / USD feed in the directory. Pyth Pro does publish HOOD (feed id 1182), so a HOOD series on mainnet would need governance to accept a PythProFeed as the primary; until then it is not templated. Every one of the 35 stock feeds above has a matching Pyth Pro feed id and a live stock token contract; the joined and on-chain-verified registry is contracts/deploy/oracle/feeds.mainnet.json, and the Pyth side is described on the Oracle page.
Testnet caveat
Section titled “Testnet caveat”testnet only Robinhood Chain testnet (chain id 46630) has no Chainlink stock feeds. The testnet deployment uses eight MockChainlinkFeed contracts whose prices are pushed by the feed admin (deployer, KEEPER_ROLE on the mock). This is a test fixture, not part of the protocol: on mainnet the series feed is the Chainlink proxy from the table above and nobody can write to it. Testnet series also run in SHORT mode (subscription 3 trading days, 4 observations spaced 5 trading days) so a full cycle completes in about four weeks.
Failure handling
Section titled “Failure handling”- Competition. Two callers submitting for the same observation: the first succeeds, the second reverts (the index has advanced). Submit with a modest gas price and accept the occasional revert;
NoteAutomationswallows it insidetry/catch. - Paused core.
observe,finalizeStrikeand lazy cranks are inactive while paused (dueWorkreturns no due work). Observations are not lost; they run when unpaused, and the adapter still selects the same closing print as long as fewer thanmaxRoundWalkrounds were published since. - Deferred observations. Neither the lazy path nor
NoteAutomationretries a Deferred observation blindly;dueWorkreports it due again only when the oracle would accept it, or once governance has cancelled it (in which case the nextobserverecordsObservationSkippedand pays the reward). - Paused RollPolicy / disabled template. No new series is created for that underlying; existing series are unaffected.