Session — Uniswap v4 hooks
Closed 18 Sep 2026. Every launch now carries a permanent hook that charges, and a creator
picks mechanics — modules — that run inside its trades. Live on Arbitrum Sepolia. Next thread:
notes/sessions/security.md.
What was done
LaunchHook(11 flags, all four return-delta) andILaunchModule; the decision is D32.- Three seams: curve (
onCurveTrade,onCurveFeeTaken), pool (beforeSwap/afterSwap), token (afterTokenTransfer). One module per launch, named in the launch transaction. - Five modules — tax, anti-snipe, graduation protection, trade size, wallet cap — and
ModuleSetto run 2 to 10 at once (24 before S2 corrected the arithmetic). Factory keeps an enable switch per module. - Web: mechanic picker, module card, readable refusals (unwraps v4's
WrappedError). - Supabase metadata (image, description, socials), writable only by a wallet signature from the launch's creator. Convenience only.
- Sepolia redeployed with all of it, then two cohort upgrades (
script/Upgrade.s.sol,ops/sepolia.sh upgrade): new curve beacon and new modules for future launches.
Decided in passing — do not relitigate
- Nobody is exempt from a wallet cap or anti-snipe, the creator least of all. A teammate found a creator holding 380x the cap. Structural holders and registered venues (the launch router) are exempt; no list is configurable.
- Old launches keep their old modules (D12). The first
ModuleSetholds 0.0049 WETH it cannot attribute; left there by choice. Launches before the fixes stay as they were; holders can exit. - Store an image's path, not its URL — Supabase's
getPublicUrlreturns the internal host. - Scale defaults to the launch — a flat trade cap never binds on a 0.01-WETH threshold.
Still open
- S1, and the security pass generally —
notes/sessions/security.md. - Capped tokens will not sell through custody routers we have not registered as venues.
- A capped launch needs several buyers to graduate. Inherent, shown in the interface.
- Modules 6–9 below.
Module backlog — not built
Written down 17 Sep 2026 so they survive this transcript. Each is a ILaunchModule
implementation unless noted.
-
6. Lottery. A buy pays a fee, the fee buys a spin, the outcome is decided by an L2 block hash a few seconds later and pays from a pot the fees fill.
../888is the working version — read it before designing. Needs anArbSysmock (a precompile; a fork does not inherit it), a pot the module holds, and settlement piggybacked on later swaps. Accept that a spin nobody settles inside ~64 seconds expires as a loss. -
7. Holder reward tracker. A fee paid to holders in proportion to what they hold. No longer blocked: the token's observer is now the launch's own module, so it sees every holder. Three traps already paid for: exclude the PoolManager from the denominator, skip same-block positions so a flash-loaned balance earns nothing (
../888'slastBalanceChange), and never distribute by iterating holders on chain (../concept). -
8. Gacha packs. Fees go to a vault and the buyer is minted an NFT that is a pack. The mechanic to elaborate when we reach it; what it needs from the seam is already visible — a module that mints to the trader needs to know who the trader is, and a hook only sees
sender, which is the router.../888's three-level attribution (hookData, then an EOAsender, thentx.origin) is the known answer and it is a guess, not a fact. -
9. Holder analytics. Time-weighted holdings and whatever else describes a holder base, kept on chain because D1 forbids a payout that reads an indexer. Pairs with 7 rather than duplicating it: 9 measures, 7 pays. Cheap enough for the token seam if accrual is lazy — on each transfer, credit
balance x (now - lastSeen)for the two addresses actually touched, which is O(1) and well insideLaunchToken.OBSERVER_GAS_LIMIT.Worth noting why this is the better substrate for 7: time weighting dissolves the flash-loaned leaderboard exploit
../888had to patch, because a balance held for zero seconds accrues zero. The fix stops being a special case and becomes the shape of the measurement.
What the modules told us about the seam
- A pool-only module could not express anti-snipe or a wallet cap. Anti-snipe protects the curve (O12); a cap has to cover plain transfers. That is why there are three seams.
- There are three places logic can live, and they see different things: the curve (
buy/sell), the hook (v4 swaps), and the token (_update, every transfer in both phases). Only the token spans a launch's whole life. - A swallowed check fails open. Anything enforcing a rule must be incapable of reverting, not
merely unlikely to —
TradeSizeModulehad this bug via-amountSpecifiedpanicking ontype(int256).min.
Stage 5: several mechanics on one launch
ModuleSet is a module that is a list of modules, so the hook, the curve and the token each still
talk to one address. Three things about it were forced rather than chosen:
- The budget is a launch's, and members divide it. The callers cannot know how many mechanics
are behind the address they hold, so they hand over a fixed allowance sized for a launch.
600,000 is what graduation leaves room for — a crossing buy with a module already costs
1,425,000 against a
GRADUATION_GAS_FLOORof 2,000,000, and the curve must hold the module's budget before asking and still open a pool afterwards. Raise one and the other stops fitting. - 10 members, and that number is arithmetic — the most for which each share, after the set's
own work and the 64th every call keeps back, is still
MIN_MEMBER_GAS. It was 24 until S2 (notes/sessions/security.md) showed that division ignored both. An over-long list is refused at launch, and a starved member refuses the trade rather than being skipped. - Fees are collected and settled, not pushed. One callback carries one recipient; two members charging to two wallets need two. The set records each share and anyone can settle it.
Also closed here: a member answering with a megabyte would have charged the set for copying it,
outside its own gas cap, and could have made every launch it belonged to untradeable. The set now
reads back exactly the 96 bytes an Action occupies and discards the rest unread.