IndexFeeHook
contracts/src/index/IndexFeeHook.sol — the single, shared Uniswap V4 hook on every index pool. It replaces the keeper, the FeeSwapEngine, and the permissionless "convert" of earlier designs: on every swap it takes the whole fee and runs the creator's 3-way split inline, in the same PoolManager lock, with no off-chain trigger.
Each index pool is USDG (currency0) / index (currency1) — the index clone is CREATE2-mined above USDG. The pool is a dynamic-fee pool and the hook overrides the LP fee to 0, taking the entire fee itself via the V4 delta mechanism.
The fee, per swap
The fee is computed deterministically within a block (so beforeSwap and afterSwap agree):
- Protocol base fee: a flat
PROTOCOL_BASE_FEE_BPS= 0.30% of swap volume, in USDG. - Index fee: volatility-scaled between the base and the cap —
- base 1.0% (buy) / 1.5% (sell) (
BASE_BUY_FEE_BPS/BASE_SELL_FEE_BPS), - plus a decayed-volatility term, clamped to 3% (
MAX_DYNAMIC_FEE_BPS), - except the first
LAUNCH_GUARD_BLOCKS(2) blocks after launch, when it's forced toLAUNCH_GUARD_FEE_BPS= 5% (a Clanker-style snipe guard).
- base 1.0% (buy) / 1.5% (sell) (
Specified vs unspecified side
Because USDG and the index are the pool's two currencies, every swap has exactly one specified and one unspecified side. The hook splits the work across both callbacks:
beforeSwap— takes the fee on the specified side (its amount is known here), returning aBeforeSwapDelta.afterSwap— takes the fee on the unspecified side (now known from the balance delta), returning anafterSwapReturnDelta, then records the price/volatility observation.
Whichever side is USDG gets the USDG split (_splitUsdgFee); whichever is the index gets the in-kind buyback (_takeBuyback).
The 3-way split (_splitUsdgFee)
From the USDG fee (base + index fee), in one lock:
- Protocol —
base + 10% of the index fee(PROTOCOL_SHARE_BPS) →poolManager.take(usdg, protocolTreasury, …). - Basket — the basket share (
NON_PROTOCOL_BPS − rfvShareBps − buybackShareBps) buys each stock with an even USDG slice on its hooklessUSDG/stockreserve pool (_buyBasket→swapUsdgForStock), taken into the treasury. - Reserve —
rfvShareBpsplus any basket USDG that didn't fill →poolManager.take(usdg, treasury, …).
Each basket leg is an isolated self-call under try/catch: a dead or thin stock pool reverts only that leg, and its USDG falls through to the reserve. A bad stock can never brick a user's trade.
The in-kind buyback (_takeBuyback)
The buyback share is taken directly in the index token — poolManager.take(indexToken, treasury, buyback) — straight from the swap into the treasury as bond inventory. No swap, no reentrancy, and supply is untouched (this is float the treasury now holds). This is the inventory that future bond markets sell.
Price & volatility oracle
Every swap records a Uniswap-style cumulative-tick observation plus a decaying volatility accumulator, keyed by PoolId:
twapTick(id) → (int24 tick, bool ok)— arithmetic-mean tick over the most recent window of at leastMIN_TWAP_WINDOW(30 min).okis false until the pool has that much history, so a freshly-launched pool can't be priced off an instantaneous spot. This feeds the bond market-price leg.- The volatility accumulator (bumped by each swap's
|tick move|, decaying ~1%/sec of itself) drives the dynamic index fee.
Two rotating window anchors guarantee the TWAP always spans ≥ MIN_TWAP_WINDOW of real time, so a single transaction cannot move it.
Wiring
configure(PoolKey)— factory-only, once per pool: validatescurrency0 == USDGandcurrency1 == index, caches the treasury, and stamps the launch block for the snipe guard.setFactory(address)— owner-only, one-shot.- Emits
FeeSplit(indexToken, protocolUsdg, rfvUsdg, basketUsdg, buybackIndex)per swap — the indexer's accumulation source.
V4SwapRouter
contracts/src/periphery/V4SwapRouter.sol — a minimal exact-input, single-hop router for the USDG/index pools. Robinhood Chain ships a forked Universal Router with ABI quirks, so in-app trading routes through this purpose-built router instead.
function swapExactIn(
address indexToken,
bool buy, // true: USDG → index; false: index → USDG
uint256 amountIn,
uint256 minOut,
address to,
uint256 deadline
) external returns (uint256 amountOut);
Approve amountIn of the input token first (USDG on a buy, the index token on a sell). Correctness is by construction: after poolManager.swap, the router settles the currency it owes and takes the one it's owed. The hook's fee is already folded into those deltas, so the router just honors the net delta and enforces minOut. Any unspent input is refunded, and it emits Swapped(indexToken, trader, buy, amountIn, amountOut).