Programmable downside protection for Stock Tokens on Robinhood Chain.
Live app · Setup · Try it (no wallet) · Proof · How we tried to break it · Docs
Sherwood lets users protect their stock positions against downside risk while keeping all their upside. Buy a Protection Note, define your floor, and settle with verified prices. The web app is SherwoodNotes.
Built for the Arbitrum Open House Singapore: Online Buildathon — deployed on Robinhood Chain testnet (chain ID 46630).
No wallet needed for any of these — every page reads live on-chain state:
- Open the live app — the dashboard lists the five real Robinhood Chain testnet stocks (TSLA, AMZN, PLTR, AMD, NFLX) with live feed prices.
- Open Protect — pick a stock, type an amount, and see the on-chain premium quote computed by the ProtectionNote contract itself (the UI never estimates prices locally).
- Open Vault — the backer pool's deposits, reserved collateral, and remaining capacity are live contract reads.
- Open Notes — settled demo notes and their payout receipts, straight from event logs.
With a wallet: connect on chain 46630, open Vault, claim 1,000 test USDG from the faucet, approve, and buy protection on stock you hold. Setup below if you want to run it locally.
Stock Tokens make equities programmable on-chain. But holding one still exposes you to market downside without native, straightforward protection.
Options are complex: you need to understand strikes, expiration, Greeks, and manage multiple positions. Centralized derivatives platforms take fees, have custody risk, and don't compose with on-chain workflows.
Sherwood fixes this: hold stock tokens, buy protection, keep upside, define downside.
- You hold 5 TSLA at entry price $100 = $500 position
- You buy 80% protection for 7 days = pay $12.50 USDG premium
- Your protection floor is $400 (80% of entry value)
- At expiry:
- TSLA rises to $120 → you get 100% of upside ($600), no payout needed
- TSLA falls to $60 → you get $400 floor, Sherwood pays you $100 USDG
- TSLA falls to $90 → you get $450, no payout (still above floor)
eligibleAmount = min(note.amount, stock tokens the holder still owns at settlement)
protectedValue = eligibleAmount × entryPrice × protectionLevel
currentValue = eligibleAmount × settlementPrice
payout = max(0, protectedValue - currentValue)
Key insight: You keep all upside. Sherwood only covers the gap below your floor. The maximum possible payout is the floor value itself (protectedValue) — that's what the vault reserves per note. And because a note protects a real position, the payout covers only the shares you still hold when it settles: sell half, get half the payout; sell all, get nothing. The reserved collateral releases either way.
Four bounds around that basis:
- One position, one stack. You can't buy protection on shares that already back active notes — the aggregate of your active notes on an asset can never exceed the stock you actually hold.
- One stock, one slice. Across everyone combined, a single stock's active liability is capped at 30% of vault deposits — one crash can only ever eat its slice.
- Claim deadline. Payouts are collectible from expiry until 30 days after it (the longest priced term); miss the window and the payout is forfeited — the reserve still releases, so the vault never strands.
- Your note settles on its own feed. The price source is bound when you buy; later feed rotations only affect notes that don't exist yet.
Known limitation: eligibility is checked at settlement — it's a snapshot, not proof you held through the whole term. A buyer who sells and re-buys before settling passes it. Closing that would require taking custody of the stock, which the protocol deliberately doesn't do.
A Protection Note is an immutable on-chain record — a plain struct addressed by noteId in the ProtectionNote contract:
- Owner: The buyer, fixed at creation. Notes are deliberately not transferable: protection is priced for the buyer, so it settles to the account that bought it.
- Asset: Which stock token (TSLA, AMZN, NFLX, PLTR, AMD)
- Amount: How many tokens to protect
- Entry Price: Verified Chainlink price at creation time
- Protection Level: 70%, 80%, or 90% floor
- Duration: 1, 7, 14, or 30 days
- Premium: Paid upfront in USDG
- Status: ACTIVE → SETTLED (with or without payout)
Once created, terms are immutable. No modifications, no surprises. To buy protection you must hold the stock token you're protecting — Sherwood insures a real position, it isn't a naked bet. The token is never taken into custody: you keep the shares and all the upside, and only the downside is settled in USDG against the price difference.
V1 supports three predefined tiers:
| Tier | Floor | Tier risk | Use Case |
|---|---|---|---|
| 70% | Covers 30% drop | +1.0% | Moderate risk tolerance |
| 80% | Covers 20% drop | +1.0% | Balanced hedge |
| 90% | Covers 10% drop | +2.0% | Strong downside fear |
70% and 80% carry the same tier risk — the rate table prices only the 90% tail higher — so notes of the same duration price identically at 70% and 80%. Every tier pays the same base and duration components.
Example: $1,000 position
- 70% protection → floor is $700
- 80% protection → floor is $800
- 90% protection → floor is $900
Transparent, protocol-defined pricing (no black box):
Premium Rate = Base Rate (1%) + Protection Tier Risk + Duration Risk
Premium = Position Value × Premium Rate
Example for 80% protection, 7 days:
- Base rate: 1.0%
- 80% tier: +1.0%
- 7-day duration: +0.5%
- Total: 2.5%
- On $500 position = $12.50 USDG
V1 deliberately does not use Black-Scholes, implied volatility, or options pricing models. That's V2.
Sherwood holds collateral in a Vault. The protocol never sells more protection than it can cover.
Example:
- Vault has $1,000 USDG collateral
- 20% reserve buffer required
- Maximum active liability: $800
- If you request $200 protection and $700 is already reserved, Sherwood rejects your request
This guarantee is checked at creation time, before your premium is collected. If anything in the creation flow reverts, nothing is collected.
Anyone can deposit USDG and back the protection pool. Backer claims are tracked in shares: your share of the vault rises with every premium collected (90% of each premium flows to backers pro rata; Sherwood keeps 10%, hard-capped at 25% and configurable down to zero) and falls with every payout. Deposits and withdrawals are open to anyone — but the withdrawal gate is the safety story: a backer can only withdraw from free capacity, so money reserved for active notes, and the unencumbered buffer behind them, are never withdrawable by anyone. The fee share lives outside the backing pool entirely, so sweeping it can never strand a reserve.
Three bounds cap what the system can owe, checked before any premium moves:
- Per holder, per stock: your stack of active notes on one asset can never exceed the stock you actually hold (
activeProtected). - Per stock, all holders combined: the active liability of any one stock is capped at a fixed share of the vault's deposits (30% at deploy), so a single stock's crash can only ever eat its slice.
- Total: the global capacity check with the 20% buffer bounds everything together.
Protection Notes settle using verified price data from Chainlink:
- At expiry, Chainlink oracle returns settlement price (freshness- and sequencer-uptime-checked)
- Contract calculates:
payout = max(0, protectedValue - currentValue)oneligibleAmount— the shares the holder still owns at settlement - Note status → SETTLED
- Vault executes USDG transfer (zero if the price stayed above the floor, or if the position was closed) and releases the note's full reserved liability
Prices are never user-supplied — they always come from Chainlink. A stale price is not silently used: the oracle checks each feed's round freshness and reverts if it's too old, so settlement simply waits for a fresh round rather than settling on frozen data. No contract can promise a price source is never stale; Sherwood guarantees it never acts on a stale one.
Settlement is permissionless: anyone can settle a note once it passes expiry, and within the claim window the payout is the first fresh price the oracle accepts. Waiting out a worse price costs the buyer nothing — the payout can never exceed the liability already reserved for that note — but the window is hard: past expiry + 30 days the payout is forfeited and the reserve releases with a zero payout, like an insurance claim past its filing deadline.
USER
│
▼
SHERWOODNOTES WEB APP
│
▼
PROTECTION NOTE (noteId)
│
┌────────┴────────┐
▼ ▼
STOCK TOKEN USDG VAULT
(Holdings) (Collateral)
│ │
▼ ▼
CHAINLINK SHERWOOD
PRICE DATA SETTLEMENT
│ │
└────────┬────────┘
▼
USDG PAYOUT
SherwoodVault.sol — Holds the settlement token (USDG), reserves capacity, executes payouts, and keeps the backer share ledger
deposit()— Deposit the settlement token and mint backer shares pro rata (1:1 into an empty vault)withdraw(assets)/redeem(shares)— Backer exits, capped atavailableCapacity(): funds reserved for active notes, and the unencumbered buffer behind them, are never withdrawable by anyone. Wind-down is settle →setBufferBps(0)→ withdrawreserveFor()— Reserve capacity and collect premium for a new note (called only by ProtectionNote; capacity checked before any token movement, and the reserve is recorded before the premium is pulled, so a hooked token can't re-enter past the check). The premium splits: the backer share joinstotalDeposits, Sherwood's fee share parks in a bucket outside the backing poolsettlePayout()— Pay the settlement token to the buyer and release the reserved liability (payout ≤ liability, re-checked on-chain; the release is booked before the transfer)claimProtocolFees()— Owner sweep of accrued fees to the treasury; the fee bucket never backed a reserve, so sweeping it can never strand one- State:
totalDeposits,totalShares/sharesOf,reserved,pendingProtocolFees,availableCapacity(),bufferBps,protocolFeeBps/treasury
ProtectionNote.sol — Creates and settles Protection Notes as structs keyed by noteId (non-transferable; no ERC-721 surface)
create()— Read verified entry price, check vault capacity, collect premium, record the notesettle()— Settle an expired note; pays the recorded buyerquote()— Live on-chain quote so the UI never recomputes prices client-sidecalculatePayout()— Spec payout formula for a hypothetical settlement price- State:
notes(noteId),nextId, per-note status,activeProtected/assetExposure(the two create-time guard ledgers), and the feed each note settles against (noteFeeds)
ProtectionOracle.sol — Retrieves and validates prices
getPrice()— ChainlinklatestRoundData()with round-completeness and freshness checks- L2 sequencer-uptime gate — Robinhood Chain is an Arbitrum-based L2, and during a sequencer outage a feed's
updatedAtcan look fresh while the round still carries a pre-outage price. When an uptime feed is configured, every price read first checks Chainlink's standard aggregator (0 = up, 1 = down) and a post-restart grace window (default 3600 s, owner-capped at 1 day), rejectingSequencerDown/SequencerGracePeriodNotOver. It fails closed: a malformed uptime round is treated as down. Unset (address(0)) means the chain publishes no feed and the gate is off — the state of testnet 46630 today. - Rejects stale, invalid, or unsupported prices; no caching, no fallbacks
AssetRegistry.sol — Registers supported stock tokens and their Chainlink feeds. Feeds must report 8 decimals — the scale every price formula in ProtectionMath is written at — and a feed that doesn't is rejected at registration and at rotation (UnsupportedFeedDecimals), because a 6- or 18-decimal feed would silently mis-price every note against it. Owner-gated: registerAsset, setAssetActive, setAssetFeed are admin-only; read views are permissionless.
Production architecture and what the testnet demo actually runs are different, and the difference is one of addresses, not code paths:
| Layer | Production intent | Robinhood Chain testnet (46630) demo |
|---|---|---|
| Settlement currency | USDG (Paxos Global Dollar) 0x5fc5...d168 (4663) |
MockUSDG 0x8c4a...f006, 6 dec, public faucet() 1,000/address/24h |
| Price oracle | Chainlink AggregatorV3 tokenized-equity feeds |
DemoFeed stand-ins, owner-set — Chainlink publishes no equity feeds on 46630; prices there are test fixtures, NOT market data |
| Stock tokens | Robinhood tokenized stocks | the same real Robinhood testnet tokens (TSLA/AMZN/PLTR/AMD/NFLX, 18 dec) |
- Chain: Robinhood Chain — mainnet (4663) and testnet (46630), an Arbitrum-based L2 with ETH gas
- Smart Contracts: Solidity (Foundry for compile/test/deploy)
- Frontend: SherwoodNotes — Next.js + React + TypeScript, wagmi / viem, Tailwind CSS
The contracts read any AggregatorV3 feed and any 6-decimal ERC-20 unchanged, so the
swap to production is registering real feeds and pointing SETTLEMENT_TOKEN at canonical
USDG — script/Config.s.sol selects by chain ID and refuses MockUSDG on mainnet
outright. No contract has a testnet branch. DemoFeed prices must never be presented as
live market data, and the frontend labels its feed type wherever a price is quoted.
These were verified against official docs (September 2026):
| Fact | Value | Source |
|---|---|---|
| Testnet chain ID | 46630 |
docs.robinhood.com/chain/connecting |
| Mainnet chain ID | 4663 |
docs.robinhood.com/chain/connecting |
| Testnet RPC (public, rate-limited) | https://rpc.testnet.chain.robinhood.com |
docs.robinhood.com/chain/connecting |
| Testnet explorer | https://explorer.testnet.chain.robinhood.com |
docs.robinhood.com/chain/connecting |
| Mainnet explorer | https://robinhoodchain.blockscout.com |
docs.robinhood.com/chain/connecting |
| Gas token | ETH | docs.robinhood.com/chain |
| USDG on mainnet | 0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168 |
docs.robinhood.com/chain/contracts |
| USDG on testnet | 0x7E955252E15c84f5768B83c41a71F9eba181802F |
testnet explorer, verified 2026-09-12: ERC-1967 proxy to verified Paxos contracts/stablecoins/USDG.sol (0xF0863D7A29a55d0c4263c11bFac754312ff078DF); implementation bytecode is byte-identical to the mainnet USDG implementation except self-references and one chain constant |
| Testnet USDG faucet | 100 USDG per claim, dripped continuously by ops wallet 0xcc9644EC26A647de0B9b86f1560d5180232f70a3; claim site https://testnet.robinhoodchain.com — no claim ever reached protocol wallets (balance still 0 as of 2026-09-13; escalated to Robinhood dev support), which is why the testnet stack settles on MockUSDG below |
testnet explorer transfer history, observed live 2026-09-12; re-checked 2026-09-13 |
| Settlement token on testnet (protocol default) | MockUSDG 0x8c4aa106a0A0d9ECAeD5C87e1AE766aa8Efbf006 — "Mock USDG", symbol USDG, 6 decimals, public faucet() = 1,000 tokens per address per 24h (FAUCET_AMOUNT = 1000e6, FAUCET_COOLDOWN = 1 days), mintLocked = true so the faucet is the only open supply; testnet-only, admin 0x0b64B35c6Dd23944D6D4029864D2cA2AA1B66422 can still mint/burn |
source-verified on the testnet explorer as src/mocks/MockUSDG.sol (solc 0.8.28, creation tx 0x78a713a5d9b5d897f47809ead0f8afd540c0a0dae34c47903a028d471756e089); constants read on-chain and one live claim proven 2026-09-13: tx 0x6e8c867dfb0f9b2b057bac9cbe6fc82979dac7ec701f1b7cdb0af1bc8a4122e6 minted exactly 1_000_000_000, immediate retry reverted FaucetCooldown(1789363365) = claim timestamp + 86400 |
Stock tokens on testnet (18 dec, BeaconProxy → verified Stock impl) |
TSLA 0xC9f9c86933092BbbfFF3CCb4b105A4A94bf3Bd4E, AMZN 0x5884aD2f920c162CFBbACc88C9C51AA75eC09E02, NFLX 0x3b8262A63d25f0477c4DDE23F83cfe22Cb768C93, PLTR 0x1FBE1a0e43594b3455993B5dE5Fd0A7A266298d0, AMD 0x71178BAc73cBeb415514eB542a8995b82669778d |
testnet explorer token list (same deployer infra as the verified Beacon/Stock contracts) |
| Price feeds | Every Stock Token has a Chainlink feed (AggregatorV3, latestRoundData()); USD feeds are 8 decimals; updates are 24/5 with no heartbeats off-hours |
docs.robinhood.com/chain/oracles-and-price-feeds |
Open items — verify at deploy time, not assumed:
- Chainlink feed availability on testnet — Chainlink's tokenized-equity feed list currently covers Robinhood Chain mainnet; if testnet lacks feeds, register demo feeds and disclose it.
- Stock Token API (
/rhj/assets,/rhj/prices/{SYM}) — investigated 2026-09-12 as a settlement source, not integrated: official and public (HTTP 200, no auth; 194 assets), but every deployment is mainnet chain 4663 (nothing for testnet), responses are unsigned, andbid/askare raw underlying prices — explicitly not multiplier-adjusted, unlike the on-chain Chainlink feeds. Feeding it on-chain would require a trusted updater, breaking the invariant that settlement prices are publicly verifiable from chain data. It is a good display-only source (market context, halt status) and a future mainnet registry discovery path. - Feed addresses — per Chainlink's guidance, never hardcode: read them from the Chainlink Robinhood feeds page at deploy and pass via
FEED_<SYMBOL>env. - Sequencer uptime — resolved: no such feed exists on Robinhood Chain (checked 2026-09-14). Robinhood's oracle docs require the check and publish no address to satisfy it, Chainlink lists uptime feeds for only 11 networks and says it is "no longer expanding" them, and Chainlink's own address-book data for this chain (57 feeds) contains no sequencer or uptime entry.
ProtectionOracleimplements the gate anyway — correct code, waiting on a feed that may never arrive — so it ships disabled on both networks and the staleness guard (default 72h, per-feed capped at 7 days) is the only price-freshness protection here. SupplyingSEQUENCER_UPTIME_FEEDat deploy is the one-line switch if Chainlink ever publishes one. Project documentation carries the full evidence and the reasoning for why the gap costs Sherwood little: at 24/5 with no off-hours heartbeats, a closed-market price and an outage-frozen price are already indistinguishable inside a 72h bound. - Mainnet equity feeds — read live from
rpc.mainnet.chain.robinhood.com(chainId 4663) on 2026-09-14, alldecimals() == 8, all returning current rounds: TSLA0x4A1166a659A55625345e9515b32adECea5547C38, AMZN0xD5a1508ceD74c084eBf3cBe853e2C968fB2a651C, PLTR0x820ABedFF239034956B7A9d2F0a331f9F075eB4c, AMD0x943A29E7ae51A4798823ca9eEd2ed533B2A22C72. NFLX has no feed on this chain — the 35 equity feeds skip it — so the initial asset list needs one drop or one substitution before any mainnet deploy. Addresses are documented as deploy-time evidence, not pasted into config: they rotate with feed migrations and must be re-read, not copied.
Every claim below is a link — a transaction, a contract, or a test you can run with one command. Nothing here is an assertion.
| Claim | Evidence |
|---|---|
| All five real Robinhood Chain testnet stocks are registered and active — including NFLX | AssetRegistry 0xb7Dbaa2b…49A7 — isSupported() returns true for TSLA, AMZN, PLTR, AMD, NFLX · NFLX reactivation tx 0x792a1a77…ee6b6 (owner setAssetActive, no redeploy) |
| The vault is funded and every active note is fully backed | Live contract reads via the Vault page: deposits 16,017.23 USDG · reserved 1,223.50 · available capacity 11,590.28 · invariant pinned by test_E2E_TwoBuyers_ReservedTracksLiability_VaultSolventAndSurplusWithdrawable |
| Premium pull refuses an underfunded wallet — the vault cannot collect what isn't there | Real failed tx 0x9ef87fb6…603cd — status 0 (failed), reverted ERC20InsufficientBalance while creating a 0.01 TSLA note with zero USDG |
| You cannot protect stock you don't hold | Real failed tx 0x38adec28…343e3 — status 0 (failed), reverted InsufficientPosition(TSLA, held=10e18, required=11e18) |
| The faucet pays exactly 1,000 USDG, once per 24h per address | Claim tx 0x6e8c867d…122e6 minted exactly 1_000_000_000 (6 dec) · the immediate retry reverted FaucetCooldown(claim + 86400) · economics pinned by test/MockUSDG.t.sol |
| Stale prices are rejected, never quoted | test_GetPrice_RevertsWhenTooStale — the oracle reverts StalePrice past the per-asset bound (72h); the same guard is why the demo carries a cron + freshness monitor |
| Capacity is checked before a single premium is taken | test_ReserveFor_RevertsWhenCapacityExceeded_NothingCollected |
| Contracts are source-verified | All four v7 contracts (registry, oracle, vault, note) verified on the testnet explorer — addresses in deploy/deployments.json |
| The suite passes with one command | forge test → 195 passed, 0 failed · cd frontend && npm test → 33 passed, 0 failed · both run on every push by CI |
Adversarial evidence beats feature lists. Each row is a refusal that actually happened — on chain or in the suite — and where to see it.
| Attack | Outcome | Proof |
|---|---|---|
| Buy protection with zero USDG in the wallet | Reverted — no note created, nothing collected | Failed tx 0x9ef87fb6… above (status 0) |
| Protect more stock than the wallet holds | Reverted InsufficientPosition |
Failed tx 0x38adec28… above (status 0) |
| Back a stack of notes with one position | Reverted — aggregate exposure capped at holdings | test_Create_AggregateCap_RevertsOnStackingBeyondPosition |
| Concentrate one stock past 30% of the vault | Reverted — per-stock cap, immutable at deploy | test_Create_ConcentrationCap_BlocksStackingOneStockPastItsSlice |
| Overcommit the vault past its buffer | Reverted InsufficientCapacity — capacity checked before the premium transfer |
test_ReserveFor_RevertsWhenCapacityExceeded_NothingCollected |
| Quote or settle against a stale price | Reverted StalePrice |
test_GetPrice_RevertsWhenTooStale |
| Double-claim the faucet inside 24h | Reverted FaucetCooldown |
Live retry, 2026-09-13 — recorded in deploy/deployments.json |
| Write a demo-feed price with the wrong signer | Reverted NotOwner — feed ownership is immutable |
The real GitHub Actions failures of 2026-09-29/30 (caught, secret fixed, monitor added) |
| Point mainnet at the mock token | Reverts MockTokenOnMainnet at deploy time |
test_MockOnMainnetReverts |
| Modify a note's terms after creation | Impossible — no setter exists; terms are immutable | ProtectionNote has no mutating path on an issued note; settle is the only terminal transition |
- This is a research prototype, not a product offering. A protection note on testnet demonstrates the mechanism — it is not insurance, not a financial service, and not investment advice. Anything beyond testnet starts with legal review.
- Contracts are unaudited. 195 tests pin the math and the guards; no third party has reviewed them.
- Testnet prices are owner-set fixtures, not market data. Robinhood Chain testnet publishes no stock feeds, so the registry points at
DemoFeedstand-ins behind the sameAggregatorV3interface. Mainnet is an address swap, not a code path — but until then, every price on the demo is a test fixture and is labelled as one. - The settlement token on testnet is
MockUSDG. The official testnet USDG drip never funded a protocol wallet (observed live, escalated to Robinhood support), so the stack settles on a source-verified mock with a public faucet. Mainnet uses canonical USDG;Config.s.solrefuses the mock outright (MockTokenOnMainnet). - The L2 sequencer-uptime gate ships disabled. No uptime feed exists on this chain (checked against Chainlink's address book and Robinhood's docs). The 72h staleness bound is the only price-freshness protection — full evidence in Verified Environment Facts.
- NFLX has no mainnet Chainlink feed yet. The 35 mainnet equity feeds skip it; the initial mainnet asset list needs one drop or one substitution.
- A demo video is pending. The live app is the demo until the recording is made.
# Contracts (Foundry)
forge build
forge test
# Deploy to Robinhood Chain testnet
export RPC_URL=https://rpc.testnet.chain.robinhood.com
export PRIVATE_KEY=your-testnet-key
export SETTLEMENT_TOKEN=0x8c4aa106a0A0d9ECAeD5C87e1AE766aa8Efbf006 # optional: MockUSDG is already the 46630 default; point at 0x7E95... when real testnet USDG becomes claimable
export TOKEN_TSLA=0xC9f9c86933092BbbfFF3CCb4b105A4A94bf3Bd4E FEED_TSLA=0x... # repeat per asset (AMZN, NFLX, PLTR, AMD); feeds must report 8 decimals
export SEQUENCER_UPTIME_FEED=0x... SEQUENCER_GRACE_PERIOD=3600 # optional: L2 sequencer gate; unset = off, which is correct for a chain with no feed
forge script script/Deploy.s.sol --rpc-url $RPC_URL --broadcast
# Source verification (Blockscout):
forge verify-contract --chain-id 46630 --verifier blockscout \
--verifier-url https://explorer.testnet.chain.robinhood.com/api <ADDRESS> <SRC>:<CONTRACT> [--constructor-args <abi-encoded>]
# Fund a wallet for the demo (testnet only): claims the settlement token, then
# DemoCreate.s.sol deposits into the vault and buys a 1-day note.
forge script script/Faucet.s.sol --rpc-url $RPC_URL --broadcast # 1,000 per address per 24h
forge script script/DemoCreate.s.sol --rpc-url $RPC_URL --broadcast # after expiry: DemoSettle.s.sol
# Frontend (SherwoodNotes)
cd frontend
npm install
npm test # node:test suites — canonical asset set, USDG funding policy, deployments metadata
npm run devThen open http://localhost:3000:
- Connect wallet on Robinhood Chain testnet (chain ID 46630)
- Open Vault and claim test USDG — Robinhood's testnet faucet pays ETH and stock
tokens but not MockUSDG, so the app carries its own "Need test USDG?" card that calls
faucet()from your wallet (never a backend signer). 1,000 USDG per address per 24h - View your stock tokens (portfolio shows the five real testnet stocks only)
- Select asset and create a Protection Note — the app blocks create until your USDG balance covers the premium and the vault allowance is approved
- Monitor on-chain settlement
- View payout receipt (if triggered)
Deployable with zero configuration. The live testnet addresses and the settlement token are
built into frontend/lib/addresses.ts (source of truth: deploy/deployments.json), fonts load
at runtime rather than at build time, and every page prerenders as static content — so the
build needs no secrets and no network beyond npm ci from the committed lockfile.
- Import the repo on Vercel, set Root Directory to
frontend(Next.js is auto-detected;outputFileTracingRootinnext.config.mjspins the workspace root there so nothing above the checkout can pull the build graph sideways) - Deploy — no env vars required
- Optional env vars:
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID— with it set, the Connect wallet button opens the Reown AppKit chooser: installed browser extensions (MetaMask, OKX, …, detected via EIP-6963) connect directly on desktop, phone wallets deep-link exactly as before, and everything else gets a QR; get one free at cloud.walletconnect.com. With it unset the chooser is unavailable and connect relies on the browser's injected wallet. In the WalletConnect dashboard allowlisthttp://localhost:3000for local dev and your deployed domain, or leave allowed domains empty to permit all.NEXT_PUBLIC_RPC_ROBINHOOD_TESTNET(a rate-limit-free RPC instead of the public one) - To buy protection on the deployed app you need the testnet settlement token: MockUSDG
(
0x8c4aa106a0A0d9ECAeD5C87e1AE766aa8Efbf006), 1,000 per address per 24h. The simplest path is the Vault page's "Need test USDG?" card, which callsfaucet()from the connected wallet and refreshes the balance on confirmation. For scripts and CI,forge script script/Faucet.s.sol --rpc-url $RPC_URL --broadcastdoes the same thing from a shell. The official testnet USDG at0x7E95...802Fis claimable from the Robinhood site faucet (testnet.robinhoodchain.com), but its drip never reached protocol wallets — see Verified Environment Facts — which is why MockUSDG is the testnet default.NEXT_PUBLIC_USDG_ROBINHOOD_TESTNETrepoints the frontend at the official token the day its faucet is reliable; the mock's faucet UI never renders against it (it is gated on chain 46630 and the MockUSDG address).
All contract addresses fall back to the deployed testnet values and can be overridden per
environment with NEXT_PUBLIC_* without touching code. frontend/.env.local does the same
thing locally and is gitignored, so a deploy can never inherit a local-only address.
sherwood/
├── README.md # This file
├── foundry.toml # Foundry configuration
│
├── src/ # Solidity contracts
│ ├── SherwoodVault.sol # Collateral, capacity checks, payouts
│ ├── ProtectionNote.sol # Note creation, quoting, settlement
│ ├── ProtectionOracle.sol # Chainlink feed validation
│ ├── AssetRegistry.sol # Owner-gated asset + feed registry
│ ├── ProtectionMath.sol # Pure premium/payout/conversion math
│ ├── Ownable.sol # Minimal ownership control
│ └── interfaces/ # IERC20, IAggregatorV3 (vendored)
│
├── test/ # Foundry tests (dependency-free TestBase)
├── script/ # Deploy.s.sol, Config.s.sol (chain config), Faucet.s.sol, demo scripts
├── assets/ # Logo, opengraph card, brand art
└── frontend/ # SherwoodNotes (Next.js App Router)
├── app/ # Dashboard, Protect, Notes, Vault pages
├── components/ # Header, shared UI
└── lib/ # ABIs, addresses, chain definition, formatting
Step 1: Connect wallet to Robinhood Chain testnet
Step 2: App displays your stock token balances
Your Holdings
5 TSLA
3 AMZN
10 AMD
Step 3: Select an asset to protect
Protect TSLA
Step 4: Configure protection
Amount: 5 TSLA
Protection: 80%
Duration: 7 Days
Step 5: Review terms
Position Value: $500
Protection Floor: $400
Premium: $12.50 USDG
Max Payout: $400
Expires: Sep 29, 2026
Step 6: Approve USDG and create note
- Chainlink reads entry price
- Vault reserves capacity
- Premium collected
- Protection Note recorded on-chain
Step 7: Note is now ACTIVE
- Monitor your position
- Watch price movement
- View on-chain settlement record
Step 8: At expiry
- Chainlink reads settlement price
- Contract calculates payout
- USDG transferred (if triggered) or liability released
- Note status → SETTLED
CREATE
│
▼
ACTIVE (waiting for expiry)
│
├── Price > Floor
│ ↓
│ SETTLED (no payout, you kept upside)
│
└── Price < Floor
↓
SETTLED (payout = floor - current, on the shares still held)
Phase 1 (Core MVP) — Must Ship
- Wallet connection to Robinhood Chain testnet
- Read real stock token balances
- Deploy SherwoodVault with collateral
- Create Protection Notes with verified entry price
- Reserve vault capacity and prevent over-commitment
- Calculate premiums transparently (base + tier + duration)
- Settle notes using Chainlink prices
- Execute USDG payouts (or zero if price stays above floor)
- Display on-chain settlement history and proof of payout
Phase 2 (Improvements) — If Time
- Multiple coverage tiers (configurable per asset)
- Vault analytics dashboard (utilization, reserves, active liability)
- Protection provider deposits (for liquidity)
- Risk monitoring and capacity alerts
- Premium utilization curves
Phase 3 (Nice-to-Have) — Only If Remaining Time
- Session-based permissions for automation
- Additional chains, if a deploy target justifies itself
Shipped beyond the phases above: the L2 sequencer-uptime gate in ProtectionOracle (2026-09-14), 8-decimal feed validation in AssetRegistry, and checks-effects-interactions ordering across the vault and note (pinned by test/Reentrancy.t.sol).
No Mocked Flows — Every primary flow runs against real testnet infrastructure: real stock token balances, real wallet connections, real on-chain Protection Notes, real ERC-20 settlement transfers. Two substitutes exist on testnet, both disclosed, and both are addresses rather than code paths: prices come from owner-set DemoFeed stand-ins (Chainlink publishes no tokenized-equity feeds on 46630) and the settlement token is MockUSDG, a source-verified 6-decimal faucet token, because Robinhood's testnet USDG drip never funded a protocol wallet. Mainnet uses real Chainlink feeds and canonical USDG with no protocol change — only script/Config.s.sol differs, and it refuses the mock on mainnet outright.
No Overbuild — Sherwood's core demo is: Real Stock Token → User Creates Protection Note → Verified Price → On-Chain Terms → Real Settlement. That alone is a complete financial primitive.
Prices from Chainlink Only — Never accept user-supplied, estimated, or cached prices. Always verify freshness. Always reject stale, invalid, or unsupported assets. Prices always come from an on-chain AggregatorV3 read — on mainnet that is the real Chainlink tokenized-equity feed; on Robinhood testnet (46630), where Chainlink publishes no equity feeds, the same read hits an owner-set DemoFeed stand-in, which is test fixture data and labelled as such (the swap to production is a registry address change, not a code path). On an L2, freshness is not enough: while the sequencer is down a round can look current and carry a pre-outage price, so no price is used unless the chain's uptime feed reports the sequencer up and clear of its restart grace window (and the gate fails closed when the uptime data is malformed).
No TODOs — Write complete implementations. If something is blocked, log it as a known limitation in the commit message, not as a TODO marker.
- Simple UX: Users think in natural terms (floor price, duration, premium). No options jargon.
- Transparent Pricing: No Black-Scholes, no hidden margin. Premium = Base + Tier + Duration.
- Minimal surface: Notes are plain structs keyed by
noteId— no token-standard attack surface, no approval flows. The contract is readable top to bottom. - Verifiable: Chainlink prices are public. Payouts are auditable. No surprises.
- Immutable Terms: Once created, a Protection Note cannot be changed. Terms are guaranteed.
- Robinhood Chain Docs: https://docs.robinhood.com/chain/
- Chainlink Robinhood feeds: https://docs.chain.link/data-feeds/tokenized-equity-feeds/robinhood
- Foundry (build/test): https://book.getfoundry.sh/
- viem (TypeScript): https://viem.sh/
- wagmi (React): https://wagmi.sh/
- Arbitrum Docs (Robinhood Chain is Arbitrum-based): https://docs.arbitrum.io/
See the project documentation for the complete technical specification and invariants list.
Keep the upside. Define the downside.