Threat model
What the launchpad must never let happen, who can try, and where. Written 19 Sep 2026 for the
security pass (notes/sessions/security.md); the manual pass works through the Check items.
What must never happen
| I1 Exit | A holder can always sell: on the curve before graduation, in the pool after. The only exception is a refusal the creator chose, visible at launch |
| I2 Breaking is not blocking | A module that reverts, runs out of gas or answers garbage never stops a trade |
| I3 No skip | Nobody can turn a module's check into a silent pass: not by gas, not by route, not by settlement mode |
| I4 Isolation | One launch cannot touch another's funds, fees or trading, though they share the hook, the set, the modules, the router and the vault |
| I5 Solvent fees | Every contract that holds fees for others (hook, curve, set) owes exactly what it holds, and every debt reaches its payee or stays claimable |
| I6 Graduation | Nobody can block, pre-empt or mis-price the pool a launch graduates into |
| I7 No exemption | A cap or anti-snipe binds everyone but structural holders (curve, migrator, PoolManager) and registered venues |
Who can try
| Actor | Controls | Trusted? |
|---|---|---|
| Trader | Amounts, direction, exact-in or out, recipient, gas limit, route, v4 settlement mode (tokens or ERC-6909 claims), hookData, a contract as sender | No |
| Creator | Which modules, their parameters and recipients; a recipient that is a contract or blocklisted | No |
| Module | Anything inside its gas cap; its answer; calls into the PoolManager while a swap holds the lock | No. Any creator may attach any contract (D26); the catalogue only gates ours. Same for a set's members |
| Quote asset | Transfer behaviour: blocklists, no bool return, callbacks | Registered by the owner; callbacks already bit once (S1) |
| Outside LP | Add and remove liquidity on a launch's pool | No |
| Owner, PoolManager | Everything / v4 itself | Yes. Owner powers are not findings |
Per seam
Curve — BondingCurve.buy / sell → module.onCurveTrade, then onCurveFeeTaken
Guards: nonReentrant; a 640k gas floor before the module; revert swallowed, reject honoured; rate
clamped to 99%; fee charged on what was accepted; payments fall back to owed plus a permissionless
retry.
- Sell rounding at 99%: the fee rounds up, so a sell worth under ~100 wei of quote nets zero and reverts. Dust only; holds.
- Graduation gas, measured 19 Sep: all five mechanics in a set graduate from 1.55M, inside the 2M floor with the 25% margin barely (now asserted). A full set of ten members that each burn their whole share needs 2.56M — and still never defers: every gas limit reverts or graduates, swept in 5k steps around the floor.
- Held module fees: graduation hands over
quoteCollected, not the balance, so a fee held for a blocked payee survives graduation and is flushed whole afterwards. Now tested.
Pool — LaunchHook.beforeSwap / afterSwap → module
Guards: onlyPoolManager; initialize gated to initializers; revert swallowed, reject honoured;
rate clamped to 99%; fee taken then pushed, deferred per pool on failure. The hook is permanent:
anything wrong here is fixed only by a new hook for new launches.
- No gas floor, unlike the other two seams — S3, fixed for new hooks. The 63/64 rule protects only
against a module that fails by running out of gas. One that gives up cheaply when short hands
its gas back, and a claims-only swap (no ERC20 moves, so no token floor) then finishes without it.
Confirmed 19 Sep against the live
ModuleSet. The fixed hook checks a 640k floor; for pools on the old hook the module rule stands: starved, refuse, never revert. Checkthe exact-in/exact-out × direction matrix at a 99% rate: holds. Exact-in spends exactly what it names, exact-out receives exactly what it names, every leg is charged.- H1, fixed for new hooks: a deferred debt was held per pool and currency with one payee, so a
module changing
payTowhile payments failed sent earlier debts to the latest payee. Now per payee. - Outside LPs: the liquidity callbacks return zero deltas and call no module, and module fees are charged on swaps only, so liquidity cannot route around them. An LP removing more than a cap is refused like anyone else. Holds.
- A hostile module can trade other pools, or its own, from inside the lock; its deltas are its own and must net to zero before the swap ends. The worst it does is what a 99% fee already does to its own launch's traders.
Token — LaunchToken._update → module.afterTokenTransfer
Guards: a 640k gas floor on every transfer; revert swallowed, reject honoured.
- Known limit — a cap binds wallets, not claims. Tokens held as PoolManager claims never transfer, and claims move between accounts as ERC-6909 without touching the token. So a whale can hold and dump beyond the cap as claims; only withdrawing to a wallet is capped. Inherent to v4.
- Known limit — 640k per transfer. Anything that forwards a fixed, smaller gas amount (some bridges, exchange sweeps, relayers) cannot move a launch's token at all. Usability, and I1 for whoever holds through one. Accepted for testnet; revisit before mainnet.
Cross-cutting
- Shared contracts (I4). All module and set state is keyed by launch, bar
ModuleSet.owed, which is per currency and payee — correctly, a payee's debt is theirs whichever launch paid it. One of our modules listed in someone else's set refuses to attach: only its own set may drive it. - Creator's rope.
TaxModuleallows a 99% sell tax for life, paid to the creator. The code keeps I1 technically; the holder keeps 1%. Decide: cap sell taxes, or accept it as visible at launch. - Upgrades (D12) — an ordering rule. A set after S1 credits curve fees only when the curve calls
onCurveFeeTaken. Enabled while the factory still points at an older curve, its curve fees arrive and are owed to nobody. Switch the curve beacon first, then enable the set;Upgrade.s.soldoes it in that order.