Skip to content

Automation and cranks

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:

LayerWho triggers itWhat it doesReward
Explicit cranksAnyonefinalizeStrike, observe, settle, sweepFees, RollPolicy.rollkeeperRewardQuote / RollPolicy.bounty
Lazy executionWhoever is already interactingclaim, redeem, redeemShield, settle and refund first run any due strike or observationkeeperRewardQuote to msg.sender
Chainlink-compatible upkeepAny automation network or botNoteAutomation.checkUpkeep / performUpkeep batch the aboverewards 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).

fact None of the following require a role.

FunctionWhenReward
NoteCore.finalizeStrike(seriesId)block.timestamp >= observations[0] and status SubscriptionkeeperRewardQuote (default 2 USDG) on success
NoteCore.observe(seriesId)block.timestamp >= observations[nextObs] and status LivekeeperRewardQuote on success
NoteCore.settle(seriesId, account)After strike, for any account with an unsettled depositnone (but may trigger a lazy crank, see below)
NoteCore.sweepFees(seriesId)Any time accruedFees > 0none
RollPolicy.roll(underlying)Previous series of that underlying is no longer open, template enabled, feed fresh, cooldown elapsedmin(bounty, RollPolicy USDG balance)
Minter.mintWeekly()Once per epoch weeknone
Desk.harvest(seriesId)After a coupon or settlementnone
RevenueRouter.sellNoteForQuoteAny 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.

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.

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.

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.sol drives 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; dueWork lets front-ends warn about it.

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 fieldMeaning
feedChainlink proxy used as the series feed
autocallBps, barrierBps, couponFloorBps, couponCapBps, refBpsSeries economics, validated against NoteCore limits
notionalCap, minTicketSubscription limits
subscriptionTradingDaysSubscription ends at the official close of the N-th trading day after today
observationCount, observationSpacingTradingDaysObservations are official closes spaced N trading days apart (weekends, holidays and early closes come from MarketCalendar)
maxFeedAgeroll refuses a feed whose latest round is older than this
enabledGuardian 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.

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.

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

FeedProxyHeartbeatThresholdDecimals
AAPL / USD0x6B22A786bAa607d76728168703a39Ea9C99f2cD086400 s0.5%8
AMD / USD0x943A29E7ae51A4798823ca9eEd2ed533B2A22C7286400 s0.5%8
AMZN / USD0xD5a1508ceD74c084eBf3cBe853e2C968fB2a651C86400 s0.5%8
ASML / USD0xB4106147E8cce40b7d46124090d373A71b70f87D86400 s0.5%8
BABA / USD0x62Cc8F9b5f56a33c9C8A60c8B92779f523c4E98486400 s0.5%8
BTC / USD0xa2c5184bF03d373Dc9dE4876eb4Bce595B46025186400 s0.5%8
BTC.B / USD0x5BB5e6a17a477d5B6Fec77b4322daD4A66bFb73286400 s0.5%8
CBBTC / USD0x0009cD492adf8167f9eEBf1293556A673530a21a86400 s0.5%8
CLSK / USD0x810c12D3a554Bc47fd39597Fe3b3AAC4941F50eF86400 s0.5%8
COIN / USD0xA3a468A452940B7D6b69991207B508c609a98Ef286400 s0.5%8
CRCL / USD0x6652eDf64bA3731C4F2D3ce821A0Fb1f1f6b482a86400 s0.5%8
CRWV / USD0xe1b3aABCAFAd1c94708dc1367dcfF8Aa4407487C86400 s0.5%8
DELL-USD0x1C6c8cADBe02E19129c39dDB92281cE4c0bf206b86400 s0.5%8
ENA / USD0x2A291496b3aa19d8948e442Ef28Ee952f3Ee97E886400 s0.5%8
ETH / USD0x78F3556b67E17Df817D51Ef5a990cDaF09E8d3A986400 s0.5%8
EURC / USD0xfF2B10c1973eD10c841434f98e456d8f3a0D7DD886400 s0.5%8
EWY / USD0xEFdf54610B62A7753Ec30bDc380847c12D32e1D186400 s0.5%8
GME / USD0x27C71df6A64fB476468EdF256CF72c038baB5B6786400 s0.5%8
GOOGL / USD0xF6f373a037c30F0e5010d854385cA89185AE638b86400 s0.5%8
INTC / USD0x3f390C5C24628Ac7C489515402235FeAD71D191386400 s0.5%8
IONQ / USD0x22EfeC4919baf55F360E0EDee4AbEB26DE4971eb86400 s0.5%8
LBTC / USD0xa621344AdAEE699491597Fd8890E0C59a5BFBE5986400 s0.5%8
LINK / USD0xe86e3422Aa9B5e8ee9f3E41a63975bC387A8bce986400 s0.5%8
META / USD0x7C38C00C30BEe9378381E7B6135d7283356D71b186400 s0.5%8
MSFT / USD0x45C3C877C15E6BA2EBB19eA114Ea508d14C1Af2E86400 s0.5%8
MSTR / USD0x396118bdFB181e6240E74D243F266B061c0edc3D86400 s0.5%8
MU / USD0x425EEFdCf05ed6526C3cE61Af99429A228a6d59686400 s0.5%8
NBIS / USD0xE1D87B116Ba0fe898998f1D140339D1fA1E0970586400 s0.5%8
NVDA / USD0x379EC4f7C378F34a1B47E4F3cbeBCbAC3E8E9F1586400 s0.5%8
ORCL / USD0x0e6a64a2B58A6693a531E6c555f3A5d042eEA84486400 s0.5%8
PLTR / USD0x820ABedFF239034956B7A9d2F0a331f9F075eB4c86400 s0.5%8
QQQ / USD0x80901d846d5D7B030F26B480776EE3b29374C2ae86400 s0.5%8
RGTI / USD0x2A045cF1C49c61c166C036d2f06FA2D2d984f76586400 s0.5%8
RKLB / USD0x045477BF65Aef6f4F2386ad0164579e48381CC7486400 s0.5%8
SGOV-USD0xa0DF4ee0fFf975306345875E3548Fcc519577A1186400 s0.5%8
SLV / USD0x209b73908e92Ae021826eD79609845451Ecba2ce86400 s0.5%8
SNDK / USD0xfb133Fa4B7b385802B693a293606682Df47109A386400 s0.5%8
SPCX / USD0xB265810950ba6c5C0Ff821c9963014a56fD8Bffb86400 s0.5%8
SPY / USD0x319724394D3A0e3669269846abE664Cd621f9f6A86400 s0.5%8
SYRUPUSDC / USD0x8765c3B9Cda41d1029E780D0c1C37C8200DC467586400 s0.5%8
SYRUPUSDC / USDC Exchange Rate0x6317f016FA3e312C4625dee51d32b43a223011f886400 s0.05%18
SYRUPUSDT / USDT Exchange Rate0xBB688c0184Ce03fEdac89D71ccE752Ab21bC299986400 s0.05%18
TSLA / USD0x4A1166a659A55625345e9515b32adECea5547C3886400 s0.5%8
TSM / USD0x874cF94aa8eC88Fd9560094dD065f2fB3E41Fc2F86400 s0.5%8
USAR-USD0xA994d3684e8400A6c8078226925779FdeE682DD986400 s0.5%8
USDC / USD0x9e6f4605992a899eE2999999F3Ec80C41F45254686400 s0.5%8
USDE / USD0xb9fB4e65744E4178894f7C61CF80E8a48A5f224a86400 s0.5%8
USDG / USD0x61B7e5650328764B076A108EFF5fa7282a1B9aD286400 s0.5%8
USDS / USD0x2D88D75b625633dCcd65d9d53BfDD3Aea2d8e84f86400 s0.5%8
USDT / USD0xbf3550B6fAe1671da7C238Af12e03Ac586BEf3B186400 s0.5%8
USO / USD0x75a9c76Ef439e2C7c2E5a34Ab105EcFe3766431c86400 s0.5%8
WBTC / USD0x62107b0d3adA75fc1697fD342d99eed947a3aA5E86400 s0.5%8
WEETH / EETH Exchange Rate0xb63f44E40aA811Cc69Fc55da786a5F3834100B4A86400 s0.05%18
WEETH / USD0xf882e1D50352aecB0Ac85378378918BCf40511e786400 s0.5%8
WSTETH / STETH Exchange Rate0x8E3Eb706B170c8FD1DdcD402932D952887736f9A86400 s0.05%18
WSTETH / USD0x3F5040B50FB37934573B210fE54B53a6F1A792E886400 s0.5%8
syrupUSDG / USDG Exchange Rate0xDd194C66aDcb422F188a04434e4824D70c151cF086400 s0.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 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.

  • 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; NoteAutomation swallows it inside try/catch.
  • Paused core. observe, finalizeStrike and lazy cranks are inactive while paused (dueWork returns no due work). Observations are not lost; they run when unpaused, and the adapter still selects the same closing print as long as fewer than maxRoundWalk rounds were published since.
  • Deferred observations. Neither the lazy path nor NoteAutomation retries a Deferred observation blindly; dueWork reports it due again only when the oracle would accept it, or once governance has cancelled it (in which case the next observe records ObservationSkipped and pays the reward).
  • Paused RollPolicy / disabled template. No new series is created for that underlying; existing series are unaffected.