The Robinhood rail

The tokenized stocks vaults hold live on Robinhood Chain, an Arbitrum Orbit L2. The protocol reaches it through LayerZero, with Paxos's USDG OFT as the cash leg.

Why this rail

The choice was settled on 2026-09-11, after ruling out the previous rail.

On a primary-mint rail, a vault contract must be eligible to hold the asset with the issuer: KYB, registration of the contract address, and a written confirmation the issuer has never documented publicly. That was the project's only genuinely unavoidable blocker.

Robinhood Chain's secondary pools require none of that: they are open to everyone. StockFun has no relationship with the issuer and needs none.

The price paid for that freedom is a cross-chain dependency: a bridge, two hubs, and a stablecoin issued by a third party that can freeze it. That is an accepted, documented risk, not an avoided one.

The circuit

flowchart LR V[TreasuryVault

Ethereum] -->|ETH → USDC

Uniswap v4| U[USDC] U -->|USDC → USDG

Curve| G[USDG] G --> BH[BridgeHub] BH -->|Paxos OFT

LayerZero| RH[RemoteHub

Robinhood Chain] RH --> MV[Mirror vault

CREATE2, one per market] MV -->|v3 / v4 pools| S[Stock Tokens] S -->|stock adapters, wrapped| AD[AirdropDistributor

Ethereum] AD -->|claims| HO[Token holders

on Ethereum]

The USDG return path, from Robinhood Chain to Ethereum, served only the creator buyback; both were removed from the code on 2026-09-28.

Since 2026-10-04 the mirror vault sends its stocks to the airdrop contract on Ethereum: each stock is locked in its LayerZero adapter on Robinhood Chain and minted wrapped on Ethereum, where the holders claim it. That path carries only stocks. The adapters, one per stock, are not in the repository yet for mainnet; the tests use mocks, and the LayerZero testnet run of 2026-10-06 used test adapters (below).

What is permissionless and what is not

Component Status
LayerZero endpoint Permissionless
Stock Token secondary pools Permissionless — what the protocol uses
Robinhood primary mint / burn KYB — not used
USDG Paxos controls mint and burn, and can pause or freeze

The last row is the rail's hardest dependency, and it is real.

The mirror vault

Every market has a mirror vault on Robinhood Chain, deployed via CREATE2 at an address predictable from Ethereum. The keeper can therefore have the remote hub pre-deploy it before the USDC arrives, so a transfer never lands on an address with no code. Since 2026-10-05 only the keeper and StockFun's owner can (predeploy): until then anyone could, even for a market that did not exist yet, tying its mirror vault to that day's wiring. The remote hub learns the keeper from a bridge batch, which carries it: before the first batch it knows no keeper, so StockFun's owner pre-deploys the first market's mirror vault. Since the second audit loop of 2026-10-05, a market whose mirror vault the keeper cannot pre-deploy (before the first batch, or after a change of keeper the remote hub has not learnt yet) is left out of the batch, with an alert, instead of being sent with too little gas to deploy its vault; the other markets cross, and their batch carries the new keeper.

Since 2026-10-02 each mirror vault is a proxy, a MirrorVaultProxy, in front of the vault implementation the remote hub names when the vault is deployed. The proxy's constructor takes no argument, so its creation code, and with it every mirror vault's address, stays predictable from Ethereum whatever the implementation. The vault's basket arrives with the first batch, which since 2026-10-05 also brings the vault's stock router and oracle up to those the remote hub names then: a vault deployed before a stock was listed accepts a basket with that stock. StockFun's owner, as the remote hub mirrors it, upgrades the mirror vaults one by one, market by market, with immediate effect.

Swap routes are not hardcoded: they arrive in calldata, encoded as abi.encode(uint8 version, bytes payload) — version 3 for a packed Uniswap v3 path, version 4 for an array of v4 PathKeys. The vault receives several candidates per leg and fills on the first that clears its bound. Since 2026-10-01 a route that fills only part of the amount fails, and the next candidate is tried; before, a partial fill on a v3 route left the unspent USDG stuck in the router.

Since the security pipeline of 2026-10-01, a v3 route runs one pool at a time. Each pool must take its whole input, or the route fails; its output comes back to the stock router and is the next pool's input, and the last pool pays the vault, under the leg's minimum. A failed pool undoes the earlier pools of the same attempt, and the next candidate is tried. Until then the check saw only a route's first pool: a partial fill further along left the intermediate token in Uniswap's v3 router, SwapRouter02, where anyone could take it, while the leg succeeded. A multi-hop v3 leg now costs one swap and one approval per pool.

Each leg spends only the USDG reserved for its stock, at the basket's weights: see The TreasuryVault. Its price bound against the stock's feed, 200 basis points by default, is a setting the remote hub holds and every mirror vault reads live (setStockMaxSlippageBps). Since 2026-10-05 the vault holds the keeper's own minimum, never looser than that bound, on what actually arrived.

When the oracle holds a price back

Since 2026-10-06 the oracle of the mirror vaults has two guards, which Robinhood's own documentation recommends; each can only hold a price back, never change one.

  • A corporate action. While a stock is going through one (a split, say), its token says its oracle is paused (oraclePaused()), and its Chainlink feed holds its last value, which can still look fresh while the token's multiplier changes. The oracle then holds that stock's price back: its purchase leg fails alone (StockOraclePaused), its USDG stays reserved for it, and the basket's other stocks are bought. The check is on for every stock; StockFun's owner can turn it off for one stock (setOraclePauseCheck), its price then falling back on its feed's own checks. A token that does not answer counts as not paused.
  • The sequencer. Robinhood Chain is an Arbitrum chain with a single sequencer. With Chainlink's L2 sequencer uptime feed set (setSequencerUptimeFeed), the oracle holds every price back while the feed says the sequencer is down, came back up no more than the grace period ago (an hour by default), or cannot be read: no leg buys until then. No such feed exists for Robinhood Chain today, so the rail is deployed with this check off; StockFun's owner sets the feed if Chainlink publishes one.

The keeper sees both before it quotes anything (see The keeper), the Worker says why a stock's price is missing, and the dapp shows it (see The dapp).

Sending the stocks to the airdrop

Since 2026-10-04, once bought, the stocks leave the mirror vault one way: sendToAirdrop. The keeper calls it with the list of stocks to send and pays the LayerZero fees; what it pays in excess is refunded to it. For each stock, the vault sends its whole balance through that stock's adapter to the airdrop contract on Ethereum, with the market's id as payload. The keeper names neither the amount nor the destination: the adapter of each stock (setStockAdapter, which checks that the adapter carries that very token) and the airdrop contract (setAirdrop) are named on the remote hub by its admin, StockFun's owner. A stock listed twice or outside the basket makes the call fail.

Since 2026-10-06 the keeper names the gas each delivery gets on Ethereum: sendToAirdrop(stocks, receiveGas, composeGas), the stock OFT's lzReceive on top of what that OFT enforces, and the airdrop contract's compose. The remote hub clamps each value into the floor and the ceiling its admin sets, zero taking the default (airdropGas), and the vault builds the send and its quote with what the hub grants; the call with the stocks alone takes both defaults. The keeper chooses the values from simulations of the delivery on Ethereum (see The keeper): Ethereum's Glamsterdam upgrade, active on Sepolia since 2026-10-06, made a new storage slot cost about five times as much, and the single compose figure of before, 600,000 gas, left every delivery of the LayerZero testnet run's first cycle short of gas. A remote hub upgraded from a version without the policies refuses every send and quote (AirdropGasNotSet) until both are set.

Since 2026-10-05 each stock goes on its own. A stock without an adapter, one whose balance cannot be read, and one whose send fails — an issuer freeze, a missing peer, a fee the keeper's payment does not cover — stays in the vault with an event (AirdropSendFailed), and the others go; when nothing goes, the call fails and says why. The quote, quoteSendToAirdrop, leaves out what it cannot price instead of failing.

LayerZero carries six decimals: under 10^12 units of an 18-decimal stock, a millionth of a token, cannot cross and stays in the vault for a later send.

On Ethereum, the stock's OFT mints the wrapped stock to the airdrop contract, then LayerZero's endpoint calls it with the payload. The contract credits the delivery only if it comes from its endpoint, from a stock OFT the owner registered, from Robinhood Chain, and was sent by the mirror vault the bridge hub derives for the market the payload names. That delivery runs on the gas the send carried (above); a delivery short of gas fails without losing anything, stored on LayerZero's endpoint, the wrapped stocks already on the airdrop contract when only the compose failed, and anyone can run it again with more gas. Since 2026-10-06 the keeper does, from its own key, within a bound, and alerts a second failure. Detail and measurements in Deployment and The airdrop.

Upgrades and wiring

Since 2026-10-02 every StockFun contract of the rail is upgradeable except the remote vault deployer, from whose address every mirror vault's address is derived. On Ethereum, the bridge hub and its adapters answer to the factory's owner. On Robinhood Chain, the remote hub is the upgrade authority: it answers with its emergency admin, the Ethereum owner as the last batch carried it, so the remote hub, the mirror vaults, the stock router and the oracle are upgraded by StockFun's owner, with immediate effect.

The remote hub is deployed first on its chain, with an initial admin, who then names the stock router, the oracle and the implementation of the mirror vaults to come, and, when they are given, the airdrop route and the stock adapters. The first batch replaces that admin with StockFun's owner.

The same admin holds the remote hub's settings, since 2026-10-05: the records one sweep pays, 64 by default (setMaxRecordsPerSweep, never zero), the price bound of the mirror vaults' purchases, and the gas of each airdrop delivery (setAirdrop); and, since 2026-10-06, the oracle's two guards and the airdrop deliveries' two gas policies on Ethereum, each a default, a floor and a ceiling (setAirdropReceiveGas, 650,000 between 200,000 and 1,500,000; setAirdropComposeGas, 1,250,000 between 600,000 and 4,000,000; a zero floor, a floor above the ceiling and a default outside them refused). The remote hub sets both at initialization, and DeployRemote sets them again from its environment. A replacement oracle the admin names (setOracle) starts with both guards off, so the admin turns them on again for it; the mirror vaults already initialized keep the oracle they were initialized with.

On Ethereum the bridge hub no longer deploys its adapter: the adapter is deployed against the hub, then named once by StockFun's owner (setAdapter), which checks its pins. A later replacement, changeAdapter, is immediate since 2026-10-05. It only accepts an adapter whose pins are exactly the current one's: the same hub, factory, cash tokens, remote hub, destination chain and transport. It can change gas figures, options or the Curve pool, never a destination; the remote hub still accepts batches only from the adapter it was built for (M-2, see Emergency mode). So an adapter is changed by upgrading it in place, at the same address; a new address would first need the remote hub upgraded to accept it. On 2026-10-05 the owner decided to keep it that way.

The adapters' own numbers are settings of StockFun's owner too: on the USDG rail, the worst USDG-per-USDC rate accepted on Curve, 30 basis points by default, and the gas of the delivery's last step on Robinhood Chain (setSettings); since the tenth audit loop, on 2026-10-06, that gas is a base every batch needs, 200,000 by default, plus a part for each market of the batch, 400,000 by default, and a batch carries at most 17 markets (setBatchGas; see "A batch's compose gas" below). Until then one figure, 1,200,000 by default, paid every batch whatever it carried. On the canonical rail, the gas of both tickets (setTicketGas). Since 2026-10-05 the canonical rail's accounting ticket pays what the bridge's inbox asks for the batch's size at the current base fee, the setting being its floor. A quote read off chain, without a gas price, saw a zero base fee and priced that ticket at the floor alone, so a long batch failed at the real base fee; since the second audit loop of that day the keeper asks for the quote at a base fee it names (quoteBridgeAt), twice the latest one, and gets the excess back. Since the fourth loop the deposit ticket is priced the same way: what the inbox asks for depositCalldataLength() bytes at the base fee, the tokenSubmissionCost setting being its floor. That length is a setting of the owner too (setDepositCalldataLength, never zero): 1,024 bytes by default, above the 740 bytes of the gateway's deposit for USDC. Until then the deposit's submission cost was a fixed setting, and a base fee that outgrew it made every batch fail.

Faults that stay contained

Since 2026-10-01, after the security audit of 2026-09-29:

  • A refused basket blocks only its own market. If Robinhood Chain refuses a market's basket, for instance a stock its router or oracle does not support, that market's mirror vault stays uninitialized and keeps its cash, which emergency mode can recover. The other markets of the same bridge batch go through. Before, the whole batch failed, and anyone could bundle healthy markets with a refused one.
  • One remote token, one identifier. The bridge hub refuses to map a second basket identifier to a Robinhood Chain token that is already mapped.
  • A paused remote hub still applies the roles. Each batch carries the current keeper and emergency admin. A paused hub applies them all the same, so a change of StockFun's owner always reaches Robinhood Chain. The cash is recorded per market and paid to the mirror vaults by a sweep after the unpause.
  • Canonical records are paid in order. On the Orbit canonical bridge, used on testnet and as a fallback, tokens and accounting arrive as two separate tickets. The hub pays waiting records whole, in the order their messages landed, whoever triggers the sweep. Since the security pipeline of 2026-10-01, an accounting ticket pays at most as many records as it added to the queue, oldest first, and they can belong to earlier batches; the sweep pays the rest, up to 64 records per call by default. Before, a backlog left by a pause or a late deposit could push a ticket past the fixed gas of its automatic execution, and the batch stayed unrecorded unless someone replayed the ticket by hand within seven days.

One fault was only partly contained. The second round of the audit, on 2026-10-01, found that if the cash token refuses one mirror vault, for instance because its issuer froze that address, the canonical queue halts and, on both rails, every batch bundled with that market reverts (R2H-2). The second audit loop of 2026-10-05 mitigated it, and the fourth closed it the same day: such a delivery now waits on its own, and the batch goes on (below).

Since 2026-10-05, after the audit loop of that day:

  • Only the keeper bridges. The bridge batch, bridgeReady, is the keeper's alone. Until then anyone could send one: a third party could slip a batch in at the adapter's loosest minimum between two trades of its own on the Curve pool, or make the keeper's batch fail by bridging one of its vaults first.
  • An emergency on the remote hub is settled, not paid by another market. On the USDG rail the hub refuses to pay out what it owes while the cash that backs it falls short, a count it keeps itself since the second audit loop of that day, never its balance, which also holds the cash of batches still on their way; cash comes back through restore. On the canonical rail StockFun's owner pauses the hub before the transfer, drops the record whose cash the emergency took, then unpauses. See Emergency mode.

Since the fourth audit loop of 2026-10-05, which applies the founder's rule that one failure never blocks the rest (see Architecture):

  • A refused delivery waits on its own. On the USDG rail, the share of a mirror vault the cash token refuses stays on the remote hub, owed to that market and backed (DeliveryRefused), and the other markets of the batch are paid. On the canonical rail, such a record leaves the queue and is held apart for its market (undeliverable), so the records behind it and the next tickets are paid. On both, anyone pays it with sweep(marketId) once the vault takes the cash again. Since 2026-10-06 the keeper, on the canonical rail, also closes the transfers whose refused records one sweep paid at once, or StockFun's owner charged to the market, so that none stays followed for good
  • Each market of a batch goes on its own. bridgeReady leaves out a listed market whose vault cannot release its cash — paused, empty, refused by the issuer, or, since the fifth loop, a vault with no code yet, like the protocol market's before it is named — an unknown market, or one whose payload the adapter cannot build (MarketSkipped), and the others go. The keeper's minimum covers the batch as listed, and is scaled down in proportion to what actually left; Bridged lists only the markets that left. A batch from which nothing leaves fails, with the first market's reason.
  • Each purchase leg goes on its own, on the mirror vault as on the Ethereum vault (LegFailed): see The TreasuryVault.
  • An emergency transfer can be charged to one market. StockFun's owner moves one market's cash and settles its books in the same call, so that no other market waits: emergencyTransferFromPending takes what the hub owes that market on the USDG rail, or its refused share on the canonical rail, and emergencyTransferRecord a queued canonical record, whole. Since the fifth loop the latter is for a record whose deposit has landed: the queue's cash is pooled, so it refuses an amount beyond the cash not held for refused shares (RecordNotCovered). A record whose deposit is lost is written off (writeOffRecord). On the USDG rail, a transfer charged to no market keeps its general wait, by design: the books show a shortfall until the cash is restored or written off.
  • A held price holds back its own legs only (since 2026-10-06): a stock in a corporate action fails its own leg alone, in every mirror vault; with the sequencer check set, a sequencer outage holds back every leg until it has been back up for the grace period, and the cash waits in the vaults (above, "When the oracle holds a price back").

A batch's compose gas

Since the tenth audit loop, on 2026-10-06. A bridge batch's last step on Robinhood Chain, the remote hub's compose, initializes and funds each market's mirror vault, so its gas grows with the batch. It used to get one fixed figure, 1,200,000 by default, whatever the batch held: four new markets on five-stock baskets ran it out of gas. Their USDG then sat on the remote hub under no record, every later batch with the same markets failed the same way, and only a run by hand on Robinhood Chain's endpoint moved it. Nothing was lost, but no market of the batch was bought or airdropped meanwhile.

  • A base and a part per market. The USDG adapter gives a batch of n markets composeGas + n × composeGasPerMarket of compose gas, 200,000 plus 400,000 a market by default, for the send and for every quote alike. A five-stock market's first batch needs about 294,000 gas (extrapolated from the one to three stocks measured), and a market already set up 21,000 to 39,000, on a fork of Robinhood Chain's testnet through LayerZero's own endpoint
  • At most 17 markets a batch. LayerZero refuses a message above the pathway's size limit, 10,000 bytes by default, and each five-stock market adds at most 544 bytes to the batch's: 17 markets fit, 18 do not. A larger batch is refused whole before anything moves, every vault keeping its USDC. The keeper sends at most that many, the markets waiting longest first, and the others go at its next pass, so none waits for good. The canonical rail has no such cap
  • Within Robinhood Chain's limits. The largest batch's compose gas is at most 24,000,000, below the 32,000,000 gas Robinhood Chain executes in one transaction; the owner's settings cannot go past it. Before mainnet the owner reads the size limit of the pathway Paxos's USDG uses and sets the cap to match if it differs
  • It costs little. LayerZero's executor charges the extra gas at Robinhood Chain's gas price: on the testnet a batch's fee grows by 0.00001 ETH per million gas
  • An adapter from before refuses every batch until the owner sets the two new settings, which its upgrade does in the same transaction. The testnet's adapter was upgraded this way on 2026-10-06, and its next batch was composed by LayerZero's executor at its new option, 600,000 gas for one market, 115,990 used
  • A stalled compose says so. The keeper's alert for a transfer not credited in time now reads the batch's message on Robinhood Chain's endpoint: not delivered there yet; composed, the credit following; or stored, its USDG on the remote hub, waiting to be run again by hand with more gas, which anyone may do, the alert giving the stored message's hash. The keeper still does not run a bridge compose itself

What has been verified, and what has not

The current state first. No mainnet deployment and no transfer of real funds has taken place. Local tests simulate delivery; they do not prove a real LayerZero delivery. The airdrop's sends, coded on 2026-10-04, are tested against mocks of LayerZero and of the stock adapters.

The LayerZero testnet run, 2026-10-06. The protocol was deployed on Sepolia and on Robinhood Chain's testnet (167 deployment transactions, all successful) and run end to end on LayerZero's real testnet endpoints, DVN and executor, from 13:50 to 19:07 UTC, by the keeper, in seven hourly windows: each window's ETH converted once, bridged in one LayerZero batch, composed on the remote hub into the mirror vault and spent on the three test stocks. Six airdrop cycles were opened, sent back over LayerZero and composed on the airdrop contract; the first four were claimed in full by the five holders, the fifth in part, the sixth left to claim, every payout exactly the share computed independently from the holding recorder. The incident drills (pauses, emergency transfers and restore, rescue, a keeper killed with a transaction pending, a compose run by hand, a stale feed, a paused stock, the sequencer guard, a delivery the cash token refuses) were played on live messages and recovered as documented, but for two halves: the Sepolia bridge hub's pause, and a bridge compose run by hand. Sepolia activated Ethereum's Glamsterdam upgrade during the run: the first airdrop deliveries ran out of gas at LayerZero's executor and were run by hand, and the fix of the delivery gas (above) was applied by upgrade and carried the following cycles. The keeper was restarted at 18:25 on the ninth audit loop's code and ran the last window cleanly, with the gas it chose from its simulations.

What that run does not prove: Paxos's USDG, whose testnet token has no OFT function on these testnets, so a test USDG under LayerZero's MintBurnOFTAdapter, shaped like the mainnet pair, stood in for it, and neither the pair's DVNs, freeze nor pause were exercised; Robinhood's stocks and their adapters, for which test stocks under LayerZero's OFTAdapter stood in; real price feeds and real liquidity; mainnet's gas, fees and finality. A mainnet trial remains, a first small cycle on a verification vault.

The older Sepolia scripts use the canonical Orbit bridge and do not validate LayerZero. A fork success, or a testnet one, is not evidence of a live delivery on mainnet.