Skip to main content

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 ExitA 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 blockingA module that reverts, runs out of gas or answers garbage never stops a trade
I3 No skipNobody can turn a module's check into a silent pass: not by gas, not by route, not by settlement mode
I4 IsolationOne 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 feesEvery contract that holds fees for others (hook, curve, set) owes exactly what it holds, and every debt reaches its payee or stays claimable
I6 GraduationNobody can block, pre-empt or mis-price the pool a launch graduates into
I7 No exemptionA cap or anti-snipe binds everyone but structural holders (curve, migrator, PoolManager) and registered venues

Who can try

ActorControlsTrusted?
TraderAmounts, direction, exact-in or out, recipient, gas limit, route, v4 settlement mode (tokens or ERC-6909 claims), hookData, a contract as senderNo
CreatorWhich modules, their parameters and recipients; a recipient that is a contract or blocklistedNo
ModuleAnything inside its gas cap; its answer; calls into the PoolManager while a swap holds the lockNo. Any creator may attach any contract (D26); the catalogue only gates ours. Same for a set's members
Quote assetTransfer behaviour: blocklists, no bool return, callbacksRegistered by the owner; callbacks already bit once (S1)
Outside LPAdd and remove liquidity on a launch's poolNo
Owner, PoolManagerEverything / v4 itselfYes. Owner powers are not findings

Per seam

Curve — BondingCurve.buy / sellmodule.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.
  • Check the 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 payTo while 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._updatemodule.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. TaxModule allows 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.sol does it in that order.