Deployment runbook
Deploy order matters: contracts → indexer → frontend (each needs the previous one's output). There is no keeper to deploy — the fee split runs inline in the hook.
0. Prerequisites
- A funded deployer key on the target chain (testnet faucet).
pnpm installat the repo root.- The chain's Uniswap V4 PoolManager address (testnet:
0x8366a39CC670B4001A1121B8F6A443A643e40951).
1. Contracts (Foundry)
cd contracts
cp ../.env.example .env # DEPLOYER_PRIVATE_KEY, PROTOCOL_TREASURY, BLOCKSCOUT_API_KEY
forge test # full suite should pass
# Testnet 46630 — deploys the protocol (registry-first, hook mined to its permission bits),
# plus mock stocks + price feeds + hookless USDG/stock reserve pools
forge script script/DeployTestnet.s.sol \
--rpc-url robinhood_testnet --broadcast --verify \
--gas-estimate-multiplier 600 --slow
Robinhood testnet's L1-data-gas component is under-weighted by the node's eth_estimateGas — small transactions out-of-gas at default multipliers. Always pass --gas-estimate-multiplier 600 --slow.
The script writes contracts/deployments/<chainId>.json with every address. The IndexFeeHook address must satisfy Uniswap V4's hook-permission bits — the deploy script CREATE2-mines it (the deterministic proxy 0x4e59b448… is present on testnet).
End-to-end validation: script/LaunchTradeE2E.s.sol launches a single-sided index, runs buys, and asserts the inline split grows the USDG reserve + in-kind buyback + basket every swap (and that the market cap walks up the curve from ~$20k).
Register basket assets / feeds on an existing deployment (owner-gated):
cast send <FEE_REGISTRY> "setAssets(address[],bool)" "[0x…,0x…]" true \
--rpc-url robinhood_testnet --private-key 0x…
cast send <FEE_REGISTRY> "setPriceFeed(address,address)" <STOCK> <FEED> \
--rpc-url robinhood_testnet --private-key 0x…
2. Indexer (Ponder on Railway)
railway login
railway init # or connect the GitHub repo, root = indexer/
railway add --database postgres # provisions DATABASE_URL
Service variables (from deployments/<chainId>.json):
CHAIN_ID=46630
PONDER_RPC_URL=https://rpc.testnet.chain.robinhood.com
INDEX_FACTORY_ADDRESS=<indexFactory>
FEE_REGISTRY_ADDRESS=<feeRegistry>
FEE_HOOK_ADDRESS=<feeHook> # the per-swap FeeSplit source
START_BLOCK=<deploy block>
Deploy with railway up (config in indexer/railway.json: start command pnpm start --schema $RAILWAY_DEPLOYMENT_ID, healthcheck /ready).
railway up uploads source — always run it after any indexer changeThe Railway indexer is deployed by railway up (a CLI source upload), not by a git push or a variable change. Changing a variable and redeploying reuses the stale code. After any indexer code change (e.g. a changed event signature — which otherwise silently stops matching), you must re-run railway up. Reindex from START_BLOCK on every factory/hook change.
The testnet RPC is a pruned node — a historical eth_call from a Ponder handler during backfill fails missing trie node. Handlers must not do historical contract reads; resolve on-chain state (e.g. bondOf, token metadata) from the frontend at latest instead.
3. Frontend (Next.js on Vercel)
The app lives in web/ and is self-contained. Either import the repo in Vercel with Root Directory = web, or:
cd web && vercel --prod
After a fresh contract deploy, sync web/lib/contracts.ts (DEFAULTS) and web/lib/shared.ts (ASSETS[<chainId>]) from the new deployments/<chainId>.json. The app reads addresses from these DEFAULTS when the NEXT_PUBLIC_* env vars are unset.
4. Mainnet promotion gate
Promote to 4663 only after all of: the tokenized-equity custody spike passes, a professional audit of the high-severity areas, US-person geo-gating + legal review for stock baskets, confirmation of the real Universal Router vs the custom V4SwapRouter, and confirming mainnet stock-buy routing (the inline basket leg assumes hookless USDG/stock V4 reserve pools — Robinhood equities trade via RFQ/0x, so this routing needs validating). See Security.