Architecture

The protocol lives on two chains. Ethereum carries the launchpad, the pools and the vaults; Robinhood Chain carries the tokenized stocks.

graph TD subgraph ETH[Ethereum] F[StockFunFactory] -->|deploys| TK[StockFunToken] F -->|deploys| V[TreasuryVault] F --> LL[LiquidityLock] TK -->|every transfer| HR[HoldingRecorder] LL -->|two locked positions| PM[Uniswap v4 PoolManager] PM --> H[StockFunHook] H -->|2 %| V H -->|2 %| CR[Creator] H -->|0.5 %| TW[Team wallet] H -->|0.5 %| BB[BuybackBurner] BB -->|buy + burn| SF[$STOCKFUN] V -->|ETH → USDC → USDG| BH[BridgeHub] V -->|sendToAirdrop, local rail| AD[AirdropDistributor] AD -.reads.-> HR AD -->|claims, pro rata| HO[Token holders] L[StockFunLens] -.reads.-> F O[TreasuryOracle] -.bounds.-> V end subgraph RH[Robinhood Chain] RH2[RemoteHub] --> MV[Mirror vault per market] MV -->|secondary pools| ST[Stock Tokens] end BH -->|LayerZero, USDG OFT| RH2 ST -->|sendToAirdrop, stock OFTs| AD K[Offchain keeper] -.triggers.-> V K -.triggers.-> BH

The contracts

This table describes the protocol as decided. The launch part is in the code since 2026-09-28, and the airdrop distribution since 2026-10-04, not deployed; the LayerZero adapters of the stocks are not in the repository. Routers and adapters other than the official swap router are left out: UniswapV4StockRouter on Ethereum, RobinhoodStockRouter on Robinhood Chain and the UsdgOftAdapter. See Status. Since 2026-10-02 every contract in it is upgradeable, except the tokens, the liquidity lock and the deployers: see below. The percentages in the diagram are the default settings of the tax.

Contract Role Cardinality
StockFunFactory Creates markets, takes the creation fee, keeps the registry and the owner's settings for new markets and for the vaults; the upgrade authority of every Ethereum module One
MarketDeployer · VaultDeployer Work around EIP-170 by carrying the token's creation code and the vault proxy's; hold no state, replaced through the factory rather than upgraded One each
StockFunToken Plain ERC-20, not upgradeable, with no mint and no burn; no logic of its own beyond one setter, setRecorder, and its rescues, for the protocol owner; reports every balance change to the holding recorder One per market
HoldingRecorder Records, for every token, each address's holdings over time and the supply outside the Uniswap PoolManager; a transfer fails if its record fails, the one deliberate exception to the isolation rule below One
TreasuryVault Holds ETH, USDC then stocks; conversion, each purchase leg on its own, hand-over of its stocks to the airdrop on the local rail (sendToAirdrop), each stock on its own, and immediate emergency recovery One proxy per market
LiquidityLock Creates the pool and deposits both positions, locked, and keeps each pool's LP fee and tick spacing; keeps a refused share of a fee collection for its recipient; not upgradeable; its only exit is the end mode, 30 days after it is announced One
StockFunHook Takes the fee on every swap, applies the anti-snipe, keeps the tax settings and each pool's whitelist, refuses liquidity from anyone but the lock, holds the creator, team and buyback balances until claimed, and what a vault that refused its share is owed One
StockFunSwapRouter The official router for trades, the only one through which a whitelisted address is exempt from the anti-snipe One, replaceable by the owner
StockFunLens Read-only; aggregates a market's state for the dapp, reading each vault on its own; since 2026-10-06 its page also carries the vault's rail, pause and bridged USDC, the pool's lock state, the token's holding recorder, and what the hook and the lock owe the vault One
TreasuryOracle Chainlink feeds, written once; their heartbeats are settings, and since 2026-10-06 its two guards on Robinhood Chain: the sequencer check, off until Chainlink publishes an uptime feed for that chain, and each stock's oracle pause, which holds that stock's price back during a corporate action One per chain
BuybackBurner Buys $STOCKFUN and sends it to the burn. While the $STOCKFUN pool is locked, no other path exists One
BridgeHub · RemoteHub · RemoteTreasuryVault The cross-chain rail; the remote hub is the upgrade authority on Robinhood Chain and names the airdrop route and each stock's adapter; the mirror vault sends its stocks to the airdrop (sendToAirdrop) One, one, one proxy per market
StockFunProtocolToken $STOCKFUN, not upgradeable like a market token; its whole supply goes into its locked position One
AirdropDistributor Distributes, on Ethereum, the stocks each treasury bought to its token's holders, by daily cycle; credits only the market's own vaults; each holder claims, and a claim pays every stock it can; bound to the factory, which names it One

Proxies and upgrades

Since 2026-10-02 every module is an ERC-1967 proxy in front of an implementation, upgraded through UUPS. The proxy holds the address and the state; the implementation holds the code, and an upgrade replaces it. An implementation can never be initialized: each proxy is initialized inside its own constructor.

Every module asks one contract who may upgrade it, its upgrade authority, fixed in its implementation: the factory on Ethereum, which answers with its owner, and the remote hub on Robinhood Chain, which answers with its emergency admin, the Ethereum owner as the last bridge batch carried it. A transfer of the factory's ownership therefore moves the upgrade power of every Ethereum module at once, and that of the Robinhood Chain modules with the next batch. An upgrade is refused if the new implementation names another authority; the hook's and the holding recorder's must also keep the same PoolManager.

Not upgradeable: the tokens, the liquidity lock and, on Robinhood Chain, the remote vault deployer, from whose address every mirror vault's address is derived. MarketDeployer and VaultDeployer hold no state: the factory replaces them (setDeployers) rather than upgrading them.

The storage is appended to, never reordered: each module's layout is recorded in contracts/storage-layouts/, and contracts/script/check-storage-layouts.sh compares it before every upgrade. Since 2026-10-05 it compares every depth of every struct, sizes included, and refuses any change to a struct that is the element of a storage array: only a struct that is a mapping's value, or the last state variable, may grow, at its end.

An implementation contract is never used directly, but whatever is sent to its own address by mistake has a lever too. Most implementations read their authority from an immutable, so the protocol owner moves it out; the factory's and the remote hub's keep their admin in the proxy's storage, so on their implementations the lever is the address that deployed them.

Who can upgrade, and how fast: see Trust model.

The airdrop contract

Since 2026-10-04, AirdropDistributor distributes each treasury's stocks on Ethereum. It is an upgradeable module like the others, a proxy whose upgrade authority is the factory. The factory names it (setAirdropDistributor) and the vaults read it live; a replaced distributor keeps every cycle claimable where it is. It reads the holdings from the holding recorder, and its rules are on The airdrop.

Two paths reach it, and nothing else credits a cycle:

  • The bridge rail. The mirror vault's sendToAirdrop sends each listed stock through that stock's LayerZero adapter, which the remote hub names (setStockAdapter), to the distributor the remote hub names (setAirdrop), with the market's id as payload. On Ethereum the stock's OFT mints the wrapped stock to the distributor, and LayerZero's endpoint calls it. It credits the delivery only from a stock OFT the owner registered, from Robinhood Chain, sent by the mirror vault the bridge hub derives for that market.
  • The local rail. The Ethereum vault's sendToAirdrop approves the exact amounts, the distributor pulls them, from the market's own vault only, and credits what it received; the approvals are closed again. A vault wired to the bridge hub refuses this call.

The keeper triggers both, and decides only when; on the bridge rail, since 2026-10-06, also the gas each delivery gets on Ethereum, which the remote hub holds within the bounds StockFun's owner sets there.

Structural properties

The hook is a singleton. One contract serves every pool, which avoids mining an address per market — a v4 hook's address encodes its permissions in its low bits, and finding one costs compute. Since 2026-10-02 it is a proxy whose address carries all 14 v4 permissions: an upgrade keeps that address, and a later implementation can use any callback.

Liquidity is not an NFT. It is held directly in the PoolManager, keyed by the lock's address. There is no position to transfer, no approve to revoke, no tokenId to lose. The only liquidity operations the contract can perform are a modifyLiquidity with a delta of exactly zero, to collect fees, and, through the end mode, the removal of every position of a pool, 30 days after the end is announced.

Only the lock adds liquidity. Since 2026-10-01 the hook refuses any other position on a StockFun pool, so every trade is a swap against the locked positions, and pays the tax.

Vaults have no withdrawal. No function lets anyone send a vault's assets to an address of their choosing. The two exits are the airdrop, whose only destination is the airdrop contract the protocol names, which pays the token's holders pro rata under a rule nobody chooses, and emergency mode, by which StockFun's owner moves an asset to any address, at once. Those are the rules of the current implementation: StockFun's owner can upgrade a vault, with immediate effect.

The bounds are settings, the feeds are not. The vaults' price bounds, 50 and 200 basis points by default, are settings of StockFun's owner, read live by every vault; the keeper can only tighten them. The feed registry writes each feed once; the feeds' heartbeats are settings, and so, since 2026-10-06, are its two guards, which can only hold a price back, never change one: the sequencer check (off on Robinhood Chain until Chainlink publishes an uptime feed for it) and each stock's oracle pause (on for every Robinhood Chain stock; see The Robinhood rail). Emergency mode touches neither: it moves assets, it does not change execution rules. The registry, like the vaults, can be upgraded by StockFun's owner.

Isolation and levers

On 2026-10-05 the founder set a design rule: when something makes one function fail, the other functions must not pay for it; everything must be able to keep working, and everything must have a lever to recover lost funds and take the fix. The fourth audit loop of that day put it in the code, and since then the audit loops count any breach of its two parts as a defect.

Isolation. A failure in one function, market, stock, cycle, record or recipient never blocks the others: the item is skipped, kept as owed or deferred, with an event, and the rest goes on.

  • A bridge batch leaves out a market whose vault cannot release its cash (MarketSkipped), and the others cross. A delivery the cash token refuses to one mirror vault stays on the remote hub, owed to that market (DeliveryRefused), and the other markets are paid
  • A purchase runs each stock's leg on its own (LegFailed), and a send to the airdrop each stock on its own (AirdropSendFailed)
  • A claim pays every stock it can and defers the others (ClaimDeferred)
  • A vault that refuses ETH no longer halts its market's trading: the hook keeps what it could not pay as owed to that vault (treasuryOwed), and the lock keeps a refused share of a fee collection for its recipient (vaultOwed, creatorOwed). Anyone pays them once the recipient takes ETH again (payTreasury, payOwed), and the keeper does so every cycle
  • The Lens reads each vault in its own call, so a vault that cannot answer leaves the others readable (vaultReadable). Since 2026-10-06 that call also carries the vault's rail, pause and bridged USDC, and the page what the hook and the lock owe the vault: the app's data service reads nothing else per market, so one vault that burns its gas fails only its own figures. Since the seventh audit loop it also reads each upgradeable module (the hook, the oracle, the airdrop contract, the two bridge hubs) and the protocol token in a group of its own, so one module that burns its gas, after a faulty upgrade, fails only its own figures

Levers. Every contract that can hold ETH or tokens, even in passing or by mistake, has a lever to move out what is stuck, and every module can take a fix: an upgrade, or a setter that swaps the module.

  • The modules that keep nothing of anyone's between transactions — the factory, the Lens, the oracles, the holding recorder, the stock routers, the official swap router and the bridge adapters — have the protocol owner's rescue(asset, amount, to)
  • The modules that keep books of their own — the vaults, the two hubs and the airdrop contract — have emergency mode
  • The hook's rescue takes only what was sent to it by mistake, never what it owes. The lock's never takes the shares it keeps, nor the positions, which only the end mode reaches. The BuybackBurner's takes its ETH only once no burn could spend it. The tokens, which cannot be upgraded, can move out what was sent to their own address
  • Without a lever, by design: the two deployers on Ethereum and the remote vault deployer, which hold no state, accept no ETH and have no owner

Detail in Emergency mode.

The one deliberate exception. A token's report to its holding recorder stays blocking: if the report fails, the transfer fails. A report allowed to fail would let a holder starve it of gas on their own transfer, so that the record skips the move and their airdrop share grows. The lever is immediate, one transaction each: setRecorder(0) on the token, which stops its record, or an upgrade of the recorder in place. See The airdrop.

Offchain packages

Package Role
shared/ Generated ABIs, protocol constants, formatting. One source shared by everything else
backend/ Price service. Isolates the dapp's only external dependency
keeper/ Conversion, bridge batches, remote routing, delivery monitoring and, since 2026-10-05, the airdrop step, the payment of what the hook and the lock keep for a recipient, and the collection of LP fees
cairn-app/ The app, a Next.js front end, beside projet/ since 2026-10-02, when it replaced the React + Vite dapp: see The dapp
cairn-worker/ The app's data edge: reads the protocol once for every open page and pushes the changes