How CycleLabs works
Create token markets with supported stock-token pairs and transparent, on-chain fee distribution. This page is generated from the same configuration the app runs on; addresses and dates below are the ones in config/networks/robinhood.json.
Overview
Three modules, one honest boundary:
- Cycle Launch creates a fixed-supply ERC-20, pairs it with a verified quote asset (a Robinhood Stock Token or USDG) in a Uniswap v3 pool, and fixes the fee recipients — all in one transaction.
- Cycle Explorer lists real pools from indexed events with liquidity, volume, price, collected fees and distribution status.
- Cycle Fees follows fees from the pool through collection, allocation and claims, per asset and per recipient.
A pool paired with a Stock Token is not a token backed by shares. Launching a token does not give its holders ownership of the paired stock, a claim on its issuer, or any guarantee of value. A pool paired with a Stock Token holds Stock Tokens as one side of its liquidity; the launched token confers no rights over them.
Fee mechanism
The one mechanism CycleLabs uses is the fee accounting of Uniswap v3 itself:
- Each launch seeds a full-range Uniswap v3 position. The position NFT is minted to
CyclePositionVault, which can never transfer it. - Swaps pay the pool's fee tier. Uniswap credits a position's share as
tokensOwed, separately from its liquidity (principal). collectFees(launchId)— callable by anyone — calls the position manager'scollectwith the launch'sCycleFeeDistributoras recipient, thensyncs the distributor. Only fees can take this path.- The distributor recognises what arrived (
Received) and lets each immutable recipient pullfloor(accounted × share ÷ 10,000) − claimed. Rounding dust stays inside and is bounded by recipients − 1 wei per asset for the contract's lifetime. - Principal leaves only through
withdrawLiquidity(creator, after the lock): it collects pending fees to the distributor first, then decreases liquidity and collects principal to the creator's address in the same transaction, asserting that the two amounts match. Nothing can interleave.
What is not counted: fees earned by other liquidity providers in the same pool, and fees the position has earned but nobody has collected (shown as an estimate from a static call, never as revenue). There is no protocol fee in this version.
Contracts & permissions
| Contract | Role | Privileged functions |
|---|---|---|
| CycleToken | Fixed-supply ERC-20 (+ permit) | none — no mint, burn hook, tax, blocklist, pause or owner |
| CycleLaunchFactory | Creates launches, emits LaunchCreated / LaunchTerms | owner (2-step): setQuoteAsset (future launches only), setPaused (new launches only). No power over existing launches or funds. |
| CyclePositionVault | Owns every position NFT; collects fees; withdraws principal | withdrawLiquidity: launch creator, after unlockAt, never with a permanent lock. Creator role hand-over is two-step. No admin. |
| CycleFeeDistributor | Per-launch minimal proxy; pull claims | none — recipients and shares are immutable; anyone may sync or claimFor (funds always go to the recipient). |
Build: solc 0.8.26+commit.8a97fa7a, EVM paris, optimizer runs 800, via-IR false. Libraries: OpenZeppelin Contracts 5.4 (ERC20, ERC20Permit, Ownable2Step, Clones, SafeERC20, ReentrancyGuard, Math).
Known limitations: the token address is predictable, so a pool could in theory be pre-initialised at another price before a launch; the vault then reverts with PoolPriceMismatch and no funds move. Quote assets that rebase or take transfer fees are rejected or unsupported by design. Contracts are unaudited.
Not deployed on Robinhood Chain. Addresses appear here only from a recorded broadcast (see docs/DEPLOYMENT.md).
Integration registry
Verified 2026-09-27 at block 74296929. Re-run pnpm verify:config to re-check every fact live.
- https://docs.robinhood.com/chain/connecting — chain id 4663 (testnet 46630), ETH gas, explorer robinhoodchain.blockscout.com, public RPC rpc.mainnet.chain.robinhood.com (rate-limited; providers recommended for production)
- https://docs.robinhood.com/chain/protocol-contracts — L2 WETH 0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73
- https://developers.uniswap.org/deployments.json — records with chainId 4663, tier labs-supported, status active (sourceRef 56928a9)
- https://sourcify.dev/server/v2/contract/4663/<address> — UniswapV3Factory, NonfungiblePositionManager and QuoterV2 are Sourcify matches of solc 0.7.6 builds; SwapRouter02 has no Sourcify record but is in Uniswap's registry and its factory() returns the v3 factory
- eth_call on 4663: factory.feeAmountTickSpacing = {100:1, 500:10, 3000:60, 10000:200}; positionManager.factory() and quoter.factory() return the factory; positionManager.WETH9() returns the documented L2 WETH
| Uniswap v3 | Address |
|---|---|
| Factory | 0x1f7d7550B1b028f7571E69A784071F0205FD2EfA |
| NonfungiblePositionManager | 0x73991a25C818Bf1f1128dEAaB1492D45638DE0D3 |
| QuoterV2 | 0x33e885eD0Ec9bF04EcfB19341582aADCb4c8A9E7 |
| SwapRouter02 | 0xCaf681a66D020601342297493863E78C959E5cb2 |
| WETH9 | 0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73 |
| Fee tiers | 100 (spacing 1), 500 (spacing 10), 3000 (spacing 60), 10000 (spacing 200) |
Every official Stock Token is a BeaconProxy whose beacon slot holds the issuer's AccessControlsRegistry (also the beacon). The implementation `Stock` (solc 0.8.33, Sourcify exact match) is a scaled-UI ERC-20: balanceOf/transfer are raw units and never rebase (uiMultiplier is display-only), there is no transfer fee, transfers revert with Blocked() for blocklisted from/to/spender and with IsPaused() while the token or the registry is paused, and the issuer holds adminBurn, pause and upgrade roles. Holding a Stock Token or an LP position over it is not ownership of the underlying share: see the issuer's terms.
Chainlink Data Feeds, directory https://reference-data-directory.vercel.app/feeds-robinhood-mainnet.json (58 feeds, fetched 2026-09-27); official page https://docs.robinhood.com/chain/oracles-and-price-feeds. AggregatorV3Interface proxies, 8 decimals, 24 h heartbeat. Stock feeds price ONE token (the display multiplier is already applied). Equity feeds follow market hours, so on weekends the answer can be many hours old: the UI shows the feed's updatedAt next to every USD estimate.
Data & indexing
- Source of truth: events of the factory, the vault, each launch's distributor and each launch's pool (Swap, Mint, Burn, Collect). Token transfers are never counted as swaps.
- Backfill from the deployment block; adaptive batch size; retries with backoff on rate limits and truncated batches.
- Idempotent rows keyed by (tx hash, log index); checkpoint advanced only after a range commits.
- Reorg recovery from a stored block-hash trail; finality depth 64 blocks separates "indexed" from "final".
- Pool and position state (price, liquidity, balances, uncollected estimate) is read from the chain after each sync, never derived from events.
- Indexing progress (indexed block, last sync, worker heartbeat) is reported separately from live chain status on
/api/health. - Creator metadata is sanitised, content-addressed and served from this origin; the server never fetches creator-supplied URLs.
- Chain-direct fallback: when the index database is unavailable, the API serves the current on-chain state instead (launches from the factory, pool and position state, distributor ledgers, claimable amounts) and every page says so. Volume, charts and history are then shown as unavailable, never as zero.
Environments
- local — Anvil + a throwaway Uniswap v3 + dev quote fixtures (clearly labelled) + PGlite. The whole flow runs here:
pnpm chain:local,pnpm dev:local,pnpm worker:local,pnpm e2e:local. - robinhood-testnet (46630) — no Uniswap v3, no Stock Tokens, no feeds: on-chain actions are disabled and the UI says why.
- robinhood (4663) — official Uniswap v3 deployments and Stock Tokens; CycleLabs contracts only after a real broadcast is recorded.
Local fixtures live in their own database schema and can never populate production analytics.
Token
$CYCLELABS is the project token on Robinhood Chain. Its contract address is published only on the home page token card and the footer, from a single configuration value that is verified on chain before it goes live (pnpm token:ca 0x…). Current status: CA soon — no address has been published. A plain token. It gives no fee share, discount, allow-list access or governance in CycleLabs. Only the address published here is official.
Operations
- Deploying the contracts:
/deploylets the operator sign the deployment from their own wallet (factory creation + one allow-list transaction per quote asset, optional ownership hand-over). The page produces a manifest;pnpm contracts:ca <manifest> --deployre-verifies every address on chain, recordscontracts/deployments/4663.jsonand redeploys the site. No server key is involved. - Publishing the token address:
pnpm token:ca 0x…checks the address is a live ERC-20 with symbol CYCLELABS on Robinhood Chain and not a quote asset or protocol contract, then sets it for production and redeploys;pnpm token:ca SOONreverts to "CA soon". - Registry re-verification:
pnpm verify:config robinhood --writere-checks chain id, Uniswap addresses, fee tiers, Stock Token beacons, issuer registry entries and price feeds, and stamps the date shown above.
Readiness report
Working end to end on the local chain: launch creation, pool initialisation at the contribution ratio, swaps through SwapRouter02, permissionless fee collection, exact pro-rata claims, principal withdrawal with fee separation, indexing with idempotency and reorg recovery, explore / pool / fee read models, metadata and logo handling, CSV export. Verified by 47 Foundry tests (unit, fuzz, invariant), unit tests, and the e2e:local script.
Not yet true for Robinhood Chain: no CycleLabs contracts are deployed, so launching, collecting and claiming are unavailable until an authorised deployment is recorded. Uniswap v3, Stock Tokens and Chainlink feeds on this network are verified and would be used as-is. The contracts are unaudited. Passing local tests is not production readiness.