Uniswap v4 dynamic-fee pools on Robinhood Chain (chain id 4663), market-made by
BandHook: hook-owned core band + wide backstop, oracle-guarded permissionless
recentering, divergence-driven dynamic fee. Pools: ETH/HOLLAR, HDX/HOLLAR,
ETH/HDX (native ETH).
Design + evidence: garden note wiki/note-robinhood-pools-proposal.
Audit + fixes: PR #1 (merged 2026-09-18).
src/BandHook.sol— the hook (owner-funded, open to third-party LPs)src/sources/—ChainlinkSource(single feed + invert + decimals),RatioSource(two-feed ratio; the ETH/HDX source)script/00–03— deploy hook (CREATE2 salt-mined to the flag bits), setup pool, fund, hand off ownershiptest/— unit tests (ERC20 + native pools, audit regression suites) and fork tests against the live PoolManager
Fresh clone: git clone --recurse-submodules (or git submodule update --init --recursive) before forge test.
Fill .env from .env.example. Blanks that must exist first:
- bridged HOLLAR + HDX addresses (and confirmed decimals),
- HDX/USD ManagedOracle (wormhole-direct receiver stack,
whmrepo; ETH/HDX usesRatioSource(HDX/USD, Chainlink ETH/USD)— no HDX/ETH oracle needed), - owner multisig on Robinhood Chain — must accept a plain ETH transfer
(
withdrawon the native pools pushes ETH via call and reverts otherwise).
Per pool, the initialize → fund sequence must run in immediate succession:
an empty pool cannot be arbitraged, so its price freezes at initialize and
drifts from market; fund refuses against a price outside guardTicks of the
oracle. If the pool sat idle after initialization, anchor its price back onto
the oracle first (tiny straddling position, one swap with sqrtPriceLimitX96
at the oracle price, withdraw and burn — costs gas only).
forge test # all green, incl. fork tests (uses the "robinhood" rpc alias)
forge script script/00_DeployBandHook.s.sol --rpc-url robinhood --broadcast # once
# per pool (three env files):
set -a; source .env.eth-hollar; set +a
forge script script/01_SetupPool.s.sol --rpc-url robinhood --broadcast # source + configure + initialize at oracle price
forge script script/02_Fund.s.sol --rpc-url robinhood --broadcast # POOL_ID + FUND_AMOUNT0/1, immediately after 01
# after all pools:
forge script script/03_HandOff.s.sol --rpc-url robinhood --broadcast # multisig then calls acceptOwnership()Handover verification: after acceptOwnership, assert owner is the multisig,
pendingOwner is zero, and the deploy key holds no role of any kind.
fund and recenter refuse rather than misplace capital. What each revert
means and what to do:
| Revert | Meaning | Response |
|---|---|---|
StaleOracle |
price source older than staleAfter (or unusable) |
fix the feed/pipeline; nothing moved. Swaps continue at the floor fee |
GuardTripped |
pool price > guardTicks from the oracle |
wait for arbitrage to align the pool, or anchor (empty pool). Never force capital against an unverified price |
EmptyBand |
the re-mint would place no liquidity (single-token inventory after a full band exit) | position is safe where it is; recenter becomes possible when price re-enters, or owner withdraws + re-funds two-sided |
fund(id, 0, 0) is the owner's forced recenter: it pulls nothing, skips
the drift trigger, but keeps the freshness and guard checks. Useful after
setParams changes band geometry.
setSource is the recovery path for a dead feed: owner-only, validates the
replacement against an expected tick, never calls the old source.
recenter(poolId) is permissionless (~350k gas) and safe to call blindly —
the guards decide. Watchdog principle: alert on work that was due, not on
quiet inactivity —
- recenter conditions held for > N minutes with no recenter executed,
- feed
updatedAtolder than its heartbeat + margin, - idle (unplaced) inventory above X% of position value,
- pool price pinned at
guardTicksdistance from the oracle for hours (fee dead-band or manipulation — investigate either way).
Not audited externally. Internal audit in PR #1; the hook custodies the LP capital — cap the pilot size until an external audit lands.