
ClosingBell-Hook
Uniswap v4 hook that prices tokenized-stock swap fees off the market calendar and chainlink feed
Demo
About
ClosingBell is a Uniswap v4 dynamic-fee hook for tokenized-stock pools on Robinhood Chain. The problem: US stocks trade about 19% of the week, but the tokens trade 24/7. For the rest of the time a Uniswap pool is the only on-chain price for the stock, while the Chainlink reference sits frozen at the last close. When the market reopens the real price arrives in one swap, and whoever makes that swap takes the gap out of the LP. Every existing LP-protection signal (realized volatility, deviation from a live price) reads "calm" on a Sunday afternoon. ClosingBell sets the fee on every swap from three things at once: 1. The trading calendar. Regular hours, extended, overnight, or closed, including DST and NYSE holidays as rules in code. Closed sessions get a higher floor. 2. Staleness. While the calendar is closed the fee ramps with time since the close and caps. 3. Deviation, priced on where this swap would leave the pool relative to the live reference, so it's size-aware and can't be escaped by splitting. A swap that widens the gap pays more. A swap that closes a gap the pool itself created pays only the floor. But a swap that closes a gap the reference created (the weekend reopen arbitrage) is charged, because that's the trade that takes money from LPs. The pool never pauses. Uninformed weekend flow still fills and pays LPs; only the toxic direction and size is priced out. During regular hours with the pool tracking the market, the hook is invisible: 5 bps. Not deployed to mainnet; parameters are provisional. The fork tests and a local demo UI reproduce every number above.
How it's made
Solidity 0.8, Foundry, OpenZeppelin uniswap-hooks (BaseOverrideFee), Uniswap v4-core, Solady, Chainlink Data Feeds. Contracts (727 lines total): - ClosingBellHook (200 lines): BaseOverrideFee subclass implementing _getFee only. Stateless and view; one immutable instance per pool, bound in afterInitialize, which also refuses a start price more than 10x off the reference (catches a flipped token orientation). Reads pool state with StateLibrary and estimates the post-swap price with v4's own SqrtPriceMath, behind an external self-call in try/catch so an impossible trade size can never revert a swap. - FeeCurve: pure library, floor x staleness x deviation, round-to-nearest, cap at MAX_LP_FEE-2 (a protocol fee compounds 999,999 to 100%, learned from Uniswap's StablePair hook). - MarketHours: pure calendar, UTC to ET with DST, four sessions, NYSE holidays as rules with no table to expire. - ChainlinkEquityAdapter: stateless view over AggregatorV3 feeds with an optional quote feed for non-dollar pools, a 6-round window for the "did the reference move" rule, staleness and plausibility checks, and ERC-8056 oraclePaused(). The hacky parts worth mentioning: - The hook reads the adapter with a raw staticcall and a 256-byte length check, then decodes eight uint256 words by hand. try/catch cannot catch a decode failure in the caller, and a decode failure in beforeSwap would freeze every swap. A malformed or dead feed reads as "closed floor" instead. - Robinhood Chain's PoolManager is a non-canonical deployment. It was verified two ways: bytecode, and recomputing all 25,139 poolIds from their PoolKeys. - The demo forks Robinhood Chain at a block from Sunday of Labor Day weekend (the primary RPC has no archive state, one public RPC does) and deploys a ReplayFeed that answers latestRoundData with the real Chainlink rounds as of the chain clock, so the feed wakes up on Monday night exactly as it did on mainnet. Real AAPL and USDG balances are seeded by writing the token's balance storage slot on anvil (USDG is a proxy that packs balances into 64 bits). Testing: 175 unit tests at 10k fuzz runs, 7 fork tests against the live chain, six internal audit rounds with independent reviewer prompts; every fix has a regression test and a build note. AI (Claude Code) was used throughout and is attributed per file and per function in the README.