Status: testnet-bound. The suite compiles and the invariant + unit tests pass. The graduation settlement, POL and payout seams are now authored (Section 7), and the lifecycle trades bond to graduated in-harness against a real v4 PoolManager (
test/integration/LaunchLifecycle.t.sol). A formal security review precedes mainnet, and one known attribution defect (H1, bucket-3 activity is keyed to the swap submitter, in practice a router, not the end trader) is open and documented in Section 7. Testnet is not mainnet. No value should sit behind the hook until an external review of the graduate-in-place core is clean. No affiliation with or endorsement by Robinhood (the chain).
This document maps the contracts. It complements the invariant index (test/Invariants.t.sol),
the invariants overview (docs/INVARIANTS.md), and the testing guide (docs/TESTING.md).
| File | Purpose | Assembled or Written |
|---|---|---|
src/interfaces/ILaunchHook.sol |
External surface + shared types (Phase, LaunchParams, ActivityKind) |
Written (canonical types) |
src/interfaces/IFeeSink.sol |
FeeSink, TreasurySink, BuyAndBurnSink (canonical names) |
Written (interfaces) |
src/interfaces/IHookToken.sol |
Model A "no token" read behind an interface | Written (interface) |
src/interfaces/Events.sol |
Canonical event schema (subgraph/UI read this) | Written |
src/interfaces/Errors.sol |
Canonical custom errors | Written |
src/curve/BondingCurve.sol |
Pure curve integral + graduation crossing (partial fill + refund) | Written (custom, fuzzable) |
src/fees/FeeRouter.sol |
Four-bucket accrue-only split + two MasterChef pull indexes | Written (custom, review-critical) |
src/LaunchHook.sol |
THE hook: phase machine, phase-switched beforeSwap, the flip, anti-sniper, splitter | Written (custom, review-critical) + extends BaseCustomCurve (assembled) |
src/token/LaunchToken.sol |
Minimal ERC20 clone target, mint gated to hook, renounceable | Assembled (OZ ERC20 + Initializable) + written gate |
src/token/TokenFactory.sol |
CREATE2 one-call: clone token, init pool, register launch, collect fee | Assembled (OZ Clones) + written glue |
script/Deploy.s.sol |
Provisional testnet deploy order | Written (thin) |
test/Invariants.t.sol |
Scaffold naming all 25 invariants as failing stubs | Written (scaffold) |
| Concern | Source | Verdict |
|---|---|---|
Bonding-phase swap-override plumbing (_beforeSwap, take/settle, BeforeSwapDelta) |
OZ BaseCustomCurve (MIT, uniswap-hooks) |
EXTEND, do not write |
| ERC20 token behavior | OZ ERC20Upgradeable |
ASSEMBLE |
| EIP-1167 minimal-proxy clone | OZ Clones |
ASSEMBLE |
| Hook permission bitmap type, PoolId/PoolKey/Currency types | Uniswap v4-core | ASSEMBLE (use the types) |
| Integer sqrt, checked math | solc 0.8.26 built-in + a boring Babylonian sqrt | ASSEMBLE / one-liner |
| The curve integral + crossing | nothing gives us this | WRITE |
| The three-state phase machine + the flip | nothing gives us this | WRITE (deepest review) |
| The four-bucket accrue-only router + two pull indexes | nothing gives us this | WRITE (review-critical) |
| The always-on post-graduation splitter | nothing gives us this | WRITE (review-critical) |
| The anti-sniper module (decaying fee + per-sender cap, tax to POL) | nothing gives us this | WRITE |
Three states, keyed by PoolId, a machine not a bool so the transient state is testable:
Bonding ──(crossing buy reaches graduationTarget)──► Graduating ──(atomic flip completes)──► Graduated
│ curve ON, anti-sniper ON, external LPing LOCKED │ ALL entry points revert except │ curve OFF,
│ sole-minter fair launch │ the flip's own insideFlip POL add │ splitter ON,
│ │ (all-entry-point fail-closed guard) │ external LPing UNLOCKED
Bonding:beforeSwapprices against the curve, applies the decaying anti-sniper fee, mints on buy / burns on sell, advancestokensSold/quoteRaised. External LP add/remove reverts.Graduating(TRANSIENT): the flip runs inside the crossing swap's unlock callback. Every hook entry point reverts for any caller except the flip's own POL-seeding add, whitelisted by the transient_insideFlipflag (never by caller address). (invariant 3, M1.)Graduated: the curve declines to override (normal pool pricing); the hook stays attached andafterSwapruns the accrue-only four-bucket splitter forever. External LPing is normal v4.
Every fee event (bonding beforeSwap skim AND graduated afterSwap) calls _accrueSplit:
fee ── split by immutable-per-launch bucketBps ──► bucket 1 treasury accrual (+ all rounding dust)
│ ACCRUE-ONLY: no external call on the swap path bucket 2 creatorAccrued (high-precision index)
│ (inv.15) bucket 3 rewardPool + activity score (multiplier-applied)
│ bucket 4 sinkAccrued (0 under Model A)
▼
recipients PULL later (never pushed on the swap):
creator ─► claimCreatorFee participant ─► claimRewards anyone ─► sweepTreasury (Model B) sink ─► deposit
- Conservation (inv.14):
bucket1 + bucket2 + bucket3 + bucket4 == fee; dust to bucket 1. - Accrue-only (inv.15): a reverting/blocklisting/gas-griefing recipient can NEVER brick a swap, because the swap path touches no external contract. This collapses the reentrancy, delta-settlement, and gas-griefing classes on the always-on path by construction.
- Anti-self-wash (NF2): a creator's OWN-pool trade earns the bucket-2 creator cut but accrues ZERO bucket-3 activity score (the reward pool still grows for other participants).
- Micro-swap anti-grief (inv.18): the creator index is high-precision (
ACC_PRECISION = 1e18) so dust accumulates rather than truncating to zero. - Distribution never iterates participants (inv.20): each participant PULLS their pro-rata share, O(1) per claim; unclaimed rolls forward.
- No withdraw/sweep/rescue path reaches POL, curve reserves, user funds, the reward pool, or the creator accrual (inv.6, inv.24). POL is owned by the hook, not held as pullable LP tokens; there is no admin function that decreases it. This is the anti-rug core.
- Params immutable post-init (inv.9):
registerLaunchis the ONLY write of a launch's curve, fees, anti-sniper params, target, and bucket split. There is no setter. - Mint gated + renounced (inv.2, inv.11): only the hook mints; mint is renounced inside the atomic flip after POL is seeded, verifiable on-chain.
- Pause is pause-NEW-launches only (spec 3.3): bounded / auto-expiring, no fund authority; the guardian set must be pairwise-disjoint from treasury and timelock (M4, an operational deploy constraint, enforced by key management not by this contract).
- Model A: fees recycle in the fee asset (ETH/USDC);
IHookTokenreturns "no token" so no discount / multiplier / gate reads against a nonexistent token;BuyAndBurnSink(bucket 4) is authored but not wired live.
LaunchHook._graduate (the flip). It is the one place funds move at graduation and the only state
transition that (a) seeds POL, (b) renounces mint, and (c) arms the perpetual splitter, atomically,
inside the crossing swap. A bug here is the moat and the honeypot in one function. Deepest review
focus: flip atomicity (all-or-nothing), the transient all-entry-point guard, AMM-vs-curve price
continuity, and the POL-seed delta settlement. Closely followed by the always-on splitter
(_afterSwap + FeeRouter._accrueSplit), which runs on every graduated swap forever.
LaunchHook._graduate(the flip) and_seedPOLLaunchHook._getUnspecifiedAmount/_bondingQuote(the delta accounting + phase switch)LaunchHook._afterSwap(the always-on splitter path) + the_splitLockguardLaunchHook._beforeAddLiquidity(the fail-closed all-entry-point flip guard +_insideFlip)LaunchHookanti-sniper (_currentFeeBps,_enforceSnipeCap)FeeRouter._accrueSplit(four-bucket conservation, accrue-only)FeeRouter._settleCreatorClaim(the creator MasterChef index)FeeRouter._settleRewardClaim(the participation index, O(1) pull)BondingCurve.fillWithCrossing(the no-straddle partial fill)LaunchToken.mint/burnFrom/renounceMint(the sole-minter + renounce gate)TokenFactory.launch(registration atomicity)
7. Known provisional seams (flagged, not hidden)
The swap-context refactor resolved the swap-context seams by overriding _beforeSwap
directly, which gives the hook the PoolKey and the resolved end-user sender in hand (verified
against OpenZeppelin/uniswap-hooks via deepwiki). So _currentPoolId and _swapper are
GONE (the pool comes from key.toId(), the user is the base-passed sender), and _isBuy /
_absSpecified are now authored against SwapParams (direction from zeroForOne + currency
orientation; amount from amountSpecified).
The two graduation POL seams are now authored against the vendored v4 checkout.
_polLiquidityDelta: AUTHORED. CallsLiquidityAmounts.getLiquidityForAmounts(...)via the deployedPOLMathlibrary (factored out to keep LaunchHook under EIP-170; the inline version pushed the hook 3 bytes over the 24,576 limit). Full-range, min-of-both-sides, conservation-safe._settlePOLDeltas: AUTHORED. Settles the POL add's owed currency deltas viaCurrencySettler(token side = real transfer of the minted inventory; quote side = burn of the escrowed ERC-6909 claims), taking any positive delta as claims. Correct terminal shape._graduatedFee: AUTHORED. The realized per-swap fee read on a graduated swap (defined atLaunchHook.soland called on the graduated branch of_beforeSwap). This is the always-on SPLITTER's input, carried to_afterSwapthrough a transient slot.
BONDING-SWAP CURRENCY SETTLEMENT: RESOLVED. LaunchHook overrides _beforeSwap fully and calls
super._beforeSwap for the base's take/settle plumbing, then applies its own CurrencySettler
.take / .settle on the bonding path (quote escrowed as ERC-6909 claims, token side taken from
minted inventory). A real swap through the PoolManager settles and reaches graduation: the
integration suite launches a pool the production way and trades bond, cross, flip and a graduated
swap through the stock PoolSwapTest router with no try/catch, and the invariant handlers ran the
buy/sell/graduate path with 0 reverts. The _settlePOLDeltas quote-burn relies on this settlement
holding the quote as claims.
_seedPOL mints the token side, computes the curve-terminal sqrtPriceX96, builds
ModifyLiquidityParams, calls poolManager.modifyLiquidity, and settles via _settlePOLDeltas.
_payout is authored: it transfers the fee asset by opening a poolManager.unlock whose callback
settles the owed currency to the recipient (the CEI ordering around it is correct, the watermark is
advanced before payout).
- End-user identity (H1, OPEN attribution defect, stated honestly): the hook keys anti-sniper
caps and bucket-3 activity off the
senderthatBaseCustomCurve._beforeSwappasses. In v4 every swap arrives through a contract (PoolManager.swapisonlyWhenUnlocked), so thatsenderis the swap SUBMITTER, in practice the router, NOT the end trader. This is an attribution defect, not a collection one: fees, POL and the creator cut meter correctly regardless of caller, but bucket-3 participation credit accrues to the submitter. The resolution under evaluation is an UNTOLL router that passes attributedhookDatatrusted only from an allowlist, scoping the claim to "traders who come through the front door", never "traders get paid". The NF2 creator-own-pool exclusion compares the samesenderto the immutable creator. - Referral bind:
bindReferrer(address)bindsmsg.sender(the user's own signed tx) to a first-touch, non-rebindable referrer; self-referral reverts. Because the binder ismsg.sender, no third party can front-run the bind against another account (invariant 25 / NF6, H3 fix).
The bonding fee is now charged on the QUOTE the buyer pays (not token-out): fee is taken BEFORE the
curve fill, the NET quote funds the curve, so grossQuoteIn == quoteSpent + fee + refund holds. The
PUNITIVE portion (the premium above tradeFeeBps inside the snipe window) routes to polAccrued
(deepening POL, spec 1.6), and the graduation flip seeds POL from quoteRaised + polAccrued.
BaseCustomAccounting binds to a SINGLE pool at init, while LaunchHook is one-hook-serves-many-pools
(spec 1.1). The _beforeSwap override reads the correct pool from key and takes the hybrid path:
it calls super._beforeSwap for the base's take/settle plumbing and layers the pool-scoped bonding
delta accounting on top via CurrencySettler. Whether that base plumbing multiplexes cleanly across
pools under adversarial interleaving is the review focus, exercised by the invariant handlers but
flagged for external review, not assumed.