Architecture
One immutable contract per basket; prices touch the chain only where they must, and never the exits.
Robinwood Stocks's core is intentionally small: one non-upgradeable basket contract per basket (built on OpenZeppelin v5), plus a stateless zap router beside it and a display-only frontend above it. Two product lines share that core:
- Frozen baskets (
BasketToken): the recipe never changes and no oracle exists on-chain at all. - Agentic baskets (
BasketToken2): the same vault plus exactly one power — an agent mayrebalanceinside immutable ceilings — which requires pricing, added in the narrowest shape that works and only on the rebalance path.
┌─────────────────────────────┐
Chainlink feeds ──┤ scripts/computeUnits.ts │ deploy-time only
(off-chain read) │ equal weights → raw units │
└──────────────┬──────────────┘
│ baskets/*.json
▼
┌─────────────────────────────┐
user ──────► │ BasketToken (per basket) │ ◄────── guardian (Safe)
mint / redeem │ immutable, no oracles │ pause-mint / cap /
(in-kind, ERC-20) │ holds constituents 1:1 │ fee recipient ONLY
└──────────────┬──────────────┘
│ reads (units, supply, events)
▼
┌─────────────────────────────┐
Chainlink feeds ──┤ frontend (Next.js, edge) │ display-time only
(client read) │ NAV, drift, badges, geoblk │
└─────────────────────────────┘The agentic line adds one path to that picture — and only one:
┌─────────────────────────────┐
AssetRegistry ────┤ BasketToken2 (per basket) │ ◄────── agent key
(frozen feeds: │ same vault + rebalance() │ rebalance ONLY,
Chainlink / TWAP) │ policy ceilings immutable │ inside the policy
└──────┬───────────────┬──────┘
│ trades via │ surplus USDG
▼ ▼
MakerRegistry → BasketDistributor
whitelisted push payouts to
settlement venues holders on a fixed cycleMint and redeem on a BasketToken2 are byte-for-byte the same in-kind,
price-free operations as on the frozen line. The oracle layer
(details) exists solely to check the agent's
trades against the policy.
Design invariants
- Full backing. For every constituent i:
balanceOf(vault) ≥ ceil(totalSupply × units[i] / 1e18)after any call sequence. Enforced by ceil-on-mint, balance-delta checks and floor-on-redeem; proven by the invariant test suite. Details: Full backing & rounding. - Redeem is ungated. No state (pause, cap, guardian action) can make
redeemrevert for a solvent holder. The only external failure mode is a constituent token itself reverting transfers (issuer freeze). - No oracle can ever touch an exit. On frozen baskets, prices exist only (a) off-chain at deploy to pick units, (b) in the frontend for display with staleness rules. On agentic baskets, prices additionally gate the agent's rebalance — frozen feeds and TWAPs, fail-safe by construction — but never mint or redeem. A manipulated price can't mint underbacked tokens on any basket, because prices never enter the mint path.
- Raw amounts only. The ERC-8056
uiMultiplieris never read on-chain, so corporate actions cannot desync the vault (why).
What is deliberately absent
- No proxy, no upgradeability, no
selfdestruct, nodelegatecall. The only external calls are ERC-20 transfers of the constituents. - No rebalancing on the frozen line. Rebalancing needs trading
authority over vault funds and an oracle in the core; the frozen
BasketTokenrefuses both, and its weights drift like an unrebalanced ETF. The agentic line grants exactly that authority — to one key, bounded by immutable ceilings, with backing re-proven after every trade. The two lines are separate contracts; choosing a frozen basket still buys you the zero-oracle, zero-trading-authority core. - No admin over funds. The guardian's complete power is: pause minting, move the supply cap under an immutable ceiling, change the fee recipient. See Guardian & governance.
- No rescue paths. If a basket were misconfigured, the remedy is pausing its mints and deploying a corrected one, never mutating the old, whose redemption keeps working forever.
Units: computed once, immutable forever
scripts/computeUnits.ts reads live Chainlink prices during US market
hours (it refuses to run on stale feeds), converts equal USD weights into
raw constituent wei per 1e18 basket wei targeting ≈$100 per basket token,
and writes the deploy config. From that moment on, the units are constructor
arguments to an immutable contract; nothing on-chain can ever change the
recipe.
The chain underneath
Robinhood Chain is an Arbitrum Orbit/Nitro L2: standard EVM, ~100ms blocks with preconfirmations, a single Robinhood-operated sequencer, settlement on Ethereum. Finality is soft on sequencer confirmation and hard on Ethereum. Stock Tokens are ERC-20s with 18 decimals, freely transferable by contracts; "indices & baskets" is an explicitly invited use case in the official docs.
Fake tokens with identical tickers exist on the chain. Robinwood Stocks's tooling
re-verifies every address against the official docs page and on-chain
symbol()/decimals() before any deploy. Do the same in your own
integrations, and take addresses only from
Reference → Addresses.