Target class: token-launchpads built as Uniswap-V4 hooks - a bonding-curve/Dutch-auction hook that GRADUATES (migrates raised proceeds + remaining tokens) to a standard pool when the curve completes. Examples: Doppler (Airlock), and the proliferating "pump-style" V4 launchpads. This class pairs with the cycle230 v4-singleton-hook-amm lens - use BOTH on any V4 launchpad.
The curve math (tickAccumulator, slug placement, Dutch-auction rebalance) is heavily modeled and audited. The HIGH-YIELD surface is the graduation handoff: when the auction ends, who decides where proceeds + leftover tokens go, and is that recipient bound? Three things move value at once: principal proceeds, accrued LP fees, and remaining unsold tokens.
Airlock.migrate(asset) is PERMISSIONLESS, but the hook's migrate() enforces
msg.sender == initializer AND earlyExit || (proceeds >= minimum && now >= endTime).
- Check: can it be triggered EARLY (before the curve completes) to grab partial liquidity?
Can it be triggered LATE/never (stuck funds)? Is the "insufficient proceeds" refund path
distinct from a real brick?UniswapV4Initializer.exitLiquidity passes recipient = address(airlock) (bound),
onlyAirlock. The hook never lets the migrate caller name the recipient. GOOD.
- RED FLAG if migrate(recipient) takes an arbitrary recipient reachable by an untrusted caller.unlockPool() (onlyOwner) reverts because the orchestrator lost ownership.
- Check explicitly: many launchpads DON'T delete the asset record after migrate - confirm the
state machine (ownership transfer, isInitialized, a migrated flag) blocks re-entry.At graduation a protocol commonly splits fees from proceeds: proceeds = balance - fees, then
routes fees to protocol/integrator and balance - fees to the new pool.
- If fees > balance is EVER produced, balance - fees underflows (solc 0.8) and migrate()
reverts FOREVER => permanently stuck liquidity. (Doppler: Airlock._handleFees L240.)
- The defense is structural: fees must be a proven SUBSET of the migrated physical balance.
In Doppler, balance = final-slug withdrawal (includes its fee growth) + extraBalance (the hook's
self-held accumulated prior-epoch fees+proceeds taken during each _update). Fee tokens are
physically inside balance, so balance >= fees. PASS.
- AUDIT ACTION on any launchpad: trace whether the fees figure is computed from the SAME
withdrawal that produced balance, or from a SEPARATE accumulator that could drift above it.
A separate fee accumulator + a capped/clamped balance (e.g. balance = min(x, uint128.max))
is the danger combo - if balance is clamped but fees aren't, fees can exceed clamped balance.
(Doppler clamps balance to uint128.max on overflow but that path also needs fees-clamping to be
safe; in practice unreachable with realistic supplies, but it's the exact spot to fuzz.)
poolManager.swap to reset an out-of-range tick. It re-enters
before/afterSwap, but: the epoch guard (_getCurrentEpoch() <= lastEpoch) skips re-rebalance,
and the reset targets an in-range tick so the nested afterSwap does no further reset =>
recursion terminates at depth 1, no user-amount double-count. Design-correct.insufficientProceeds refund phase
(asset->numeraire sell-back at avg clearing price), NOT a stuck-funds bug. The migrate gate
intentionally blocks migrating an under-subscribed auction.V4-launchpad hook bears no net return-delta if: afterSwap returns (selector, 0) and beforeSwap
returns ZERO_DELTA. Doppler does both; all its currency movement is balanced inside its own
unlock callbacks (take positive deltas / sync+settle negative deltas / take net to bound sender).
No swapper/LP can mint un-settled value from the PoolManager.
depth-1 clone leaves submodules (v4-core, v4-periphery, solady...) empty; a load-constrained box
can't run the full make install + via-ir build. PATTERN: isolate the arithmetic under test
(here: Airlock._handleFees + the migrate balance/fees derivation) into a fresh forge-std-only
project and fuzz it directly. Caveat: this proves the LOCAL arithmetic, not the upstream V4
modifyLiquidity delta returns - state that limit explicitly in the verdict.
Canonical, multiply-audited V4 launchpads are saturation-likely. WALK CLEAN with this lens +
the cycle230 lens IS the win. Only stage a finding you can PoC at the PoolManager level or as a
provable migration brick / recipient-rebind / double-graduate. Banked isolated harness:
~/bounty/cycles/cycle-231-doppler/iso/test/HandleFees.t.sol.