One launch, one contract
The launchpad is two contract shapes:LunyaLaunchFactory— the singleton, and the only launchpad address you should ever hardcode. It creates launches, keeps the registry, holds the protocol’s accrued fees per quote token, and lists each graduating launch’s pool through the DEX lister it is configured with.- One clone per launch (EIP-1167 with immutable args) of the implementation registered for its launch type — for type
1,LunyaLaunchCP, the constant-product curve. Each launch holds its own unsold supply and its own raise, so nothing that happens to one can reach another. The launched ERC-20 (LunyaLaunchToken) is deployed by its launch.
ILunyaLaunch is the interface every launch implements, whatever runs underneath — the surface written to outlive any one launch type.
Discovery
Creating a launch
msg.value is left becomes the opening buy. The opening buy is atomic with creation, so nobody can front-run a creator between the two — and it is made to the creator, who is exempt from the anti-snipe surcharge.
creatorFeeRecipientreceives the creator’s share of the curve’s fees and of the graduated pool’s fees. A contract can hold that role — see Receiving fees in a contract.launchParamsare the creator’s own parameters. For type1they are empty, orabi.encode(address[] exempt)— up to 32 non-zero addresses exempt from the anti-snipe surcharge.
Token metadata
contractURI is the token’s whole off-chain identity, under ERC-7572’s name for it. Read it from the LaunchCreated log — the event carries the same string the token stores, so indexing needs no eth_call — or from token.contractURI().
It is written once. The token sets it in its constructor, bounded to 2,048 bytes (MAX_CONTRACT_URI_LENGTH, ContractURITooLong() past it), and has no setter. ERC-7572’s ContractURIUpdated is deliberately never emitted: what the creation log carries is final for the life of the token.
The contract stores whatever the creator passed, so a reader handles three cases:
The document’s keys, all optional and omitted rather than emptied:
namerepeats the ERC-20name()so the document stands on its own. The two cannot drift: both are set in the same call, and neither has a setter.imageisipfs://<cid>when the creator uploaded through the app, anhttps://URL otherwise. Resolveipfs://through your own gateway.external_linkis the project’s website — ERC-7572’s name for it. Acceptwebsiteas a synonym; some documents use that key.- Socials are full URLs as the app writes them, but a creator calling the factory directly can leave a bare handle. Normalise before rendering.
- Split at the first comma and check that the media type before it contains
json. - The body is not percent-encoded —
;utf8,means the bytes are the document. AttemptdecodeURIComponentonly if the body matches%[0-9a-f]{2}, and keep the raw bytes if it throws: a description containing a literal%otherwise costs you the whole document. - Require a leading
{, and treat a parse failure as “no metadata” rather than as an error on the token.
Deterministic addresses
Launch addresses are known before creation. Clones deploy viaCREATE2 with a salt namespaced per creator — keccak256(abi.encode(creator, salt)) — so a salt only has to be unique among one creator’s launches, and nobody can take an address someone else was going to use.
predictLaunch returns both addresses: a fresh clone’s first CREATE is at nonce 1, so fixing the launch fixes the token. creatorFeeRecipient and launchParams are clone arguments, so they are part of the address — decide them first, then choose the salt.
The salt is mined, not chosen
A creation whose token would sort above its quote reverts withTokenAboveQuote(). Tooling that creates launches searches for an accepted salt — predictLaunch is a view, so the search is free:
token0 in its graduated pool, always, so the pool’s own price reads as quote per token. Mining is also how a creator picks a vanity address — add your condition to the same loop.
Terms can move the address
The clone’s arguments include the governance-set terms a launch is created under — the quote’s configuration, the fee defaults, the graduation pool type and the type’s parameters — so a retune moves the predicted address. ReadtermsHash(launchType, quoteToken) beside the prediction and pass it back as expectedTermsHash: a change then reverts TermsChanged instead of deploying somewhere you did not publish. The hash covers governance’s terms only; creatorFeeRecipient and launchParams are your own arguments.
What a launch promises
Views onILunyaLaunch, stable across launch types:
progress() rather than supply counters, deliberately: every launch that graduates has a completion condition, but not every launch type has a token counter that means the same thing. Build progress bars on it and they keep working across types.
ReadyToGraduate is in the shared enum for launch types that graduate in two steps. Type 1 never reports it: the buy that fills its curve graduates it, so phase() goes from Trading to Graduated in one transaction. Watch phase(), read pool().
config(), sold(), openedAt(), launchParams(), quoteBuyFor(), currentSnipeTaxBps(), isExempt(), protocolFees(), creatorFees() and collectFees() exist on the type-1 implementation but are its vocabulary, not the cross-type promise. Use them to quote type-1 launches off-chain; use progress() for anything meant to outlive one type.Units and quote tokens
Every amount is in the quote token’s own decimals — reserve, fees, quotes, event fields. On a 6-decimal quote,2e18 is not “two units”. The exceptions are the values that exist only in the native coin: sellForNative’s minimum-out, quoteSellForNative’s nativeOut, and quoteBuyWithNative’s nativeRefund.
Which tokens a launch may be quoted in is quoteConfig(token), and the set grows by governance call rather than by new factory. An approved quote is not yet a pair: each launch type carries its own parameters per quote (typeParams), because they are denominated in that quote’s units — a type with no parameters on a quote refuses to launch on it (PairNotOpen).
Launch types
LaunchType is a uint8 naming an implementation in an append-only registry; type 1 is the constant-product curve. A type’s implementation is fixed the moment it is registered, so a type-1 launch today runs the same code as one made a year from now; a different curve is the next number. A type can be retired — new launches on it stop, existing ones are untouched.
For type 1, the parameters are (totalSupply, virtualQuote, virtualToken, curveSupply, lpSupply, snipeTaxBps, snipeWindow, snipeDecay) — the supply split, virtual reserves and anti-snipe window are set per quote token, not constants.
A launch’s terms are fixed at creation. Two things are read live instead: the DEX lister, at graduation, and the protocol fee recipient, whenever protocol fees are paid. Read launchType from LaunchCreated and treat a launch’s ABI as a function of it.