For the complete documentation index, see llms.txt. This page is also available as Markdown.

Launching a token (ignite)

A launchpad token is created by a single call to FireLaunch.ignite(IgniteParams). That one transaction:

  1. Deploys a fresh FireToken ERC-20 (CREATE2, deterministic from salt)

  2. Creates the corresponding Uniswap V3 pool at the starting tick if it doesn't exist

  3. Transfers the bonding-curve allocation to FireGraduator

  4. Calls FireGraduator.createPositions() to seed the curve with overlapping concentrated positions plus a tail

  5. Splits the creator allocation across FuelVault (liquid bucket) and the on-token vesting schedule (which doesn't unlock until graduation)

The function returns the deployed token address.

Function

function ignite(IgniteParams calldata params)
    external
    nonReentrant
    whenNotPaused
    returns (address token);

⚠️ ignite() does not accept msg.value. It cannot bundle an ETH purchase. To buy tokens at launch, send a second transaction immediately after - see Buying at launch below.

IgniteParams

struct IgniteParams {
    string  name;                  // Token name (must be non-empty)
    string  symbol;                // Token symbol (must be non-empty)
    uint256 totalSupply;           // Total fixed supply, must fit in uint128
    address numeraire;             // Quote token (must be approved in registry)
    int24   tickLower;             // Bonding-curve start tick (lower price)
    int24   tickUpper;             // Graduation tick (target price)
    uint24  feeTier;               // V3 pool fee tier (must be approved)
    uint16  skimBps;               // % of buys redirected to FuelVault as buyer credits
    uint16  creatorAllocationBps;  // Creator's % of total supply
    uint16  maxShareToBeSoldBps;   // % of supply allocated to bonding curve
    uint16  numPositions;          // Bonding-curve positions; must be in [minPositions, maxPositions]
    uint256 vestingDuration;       // Creator vesting duration in seconds
    bytes32 salt;                  // CREATE2 salt for deterministic token address
}

Don't pass 0 for numPositions or vestingDuration expecting a "registry default." There is no fallback - numPositions == 0 reverts with ZeroPositions, and vestingDuration == 0 only passes when creatorAllocationBps == 0 (because the bounds check is skipped) and the registry's requiredVestingDuration is also 0. Always pass the values you want.

Validation: two phases

_validateParams runs two checks in sequence:

Phase 1 - bounds

Constraint
Implementation

name, symbol non-empty

EmptyName / EmptySymbol

0 < totalSupply ≤ type(uint128).max

ZeroSupply / SupplyExceedsUint128

Numeraire on the approved set

registry.approvedNumeraires(numeraire) (public mapping) → NumeraireNotApproved

Fee tier on the approved set

registry.approvedFeeTiers(feeTier) (public mapping) → FeeTierNotApproved

Fee tier has tick spacing in V3 factory

factory.feeAmountTickSpacing(feeTier) != 0InvalidFeeTier

tickLower < tickUpper, both aligned to fee tier spacing

InvalidTickRange / TickNotAligned

skimBps ≤ registry.maxSkimBps()

SkimTooHigh (max 1500 = 15%)

creatorAllocationBps ≤ registry.maxCreatorAllocationBps()

CreatorAllocTooHigh (max 2000 = 20%)

creatorAllocationBps + maxShareToBeSoldBps ≤ 9000

AllocationsAbove90Percent (≥10% tail)

maxShareToBeSoldBps ≥ registry.minMaxShareToBeSoldBps()

MaxShareToBeSoldTooLow

numPositions ≠ 0

ZeroPositions

numPositions ∈ [registry.minPositions(), registry.maxPositions()]

TooFewPositions / TooManyPositions (5 … 50)

vestingDuration ∈ [minVestingDuration(), maxVestingDuration()] only if creatorAllocationBps > 0 and not all of it is captured to FuelVault

VestingDurationTooShort / VestingDurationTooLong (90 … 730 days)

If creatorAllocationBps > 0 and registry hasn't set requiredVestingDuration and not all is captured to FuelVault

RequiredCreatorVestingUnset

Phase 2 - registry pin matching

If the registry has set any required* value to a non-zero value, the corresponding parameter must match exactly. The registry-required tick range also accepts the inverse (negated and swapped):

If registry has set…
…user param must equal
Mismatch revert

requiredTotalSupply

itself

TotalSupplyMismatch

requiredTickLower / requiredTickUpper

itself OR (-reqTickUpper, -reqTickLower)

TickRangeMismatch

requiredFeeTier

itself

FeeTierMismatch

requiredSkimBps

itself

SkimBpsMismatch

requiredCreatorAllocationBps / requiredMaxShareToBeSoldBps

both must match (set atomically)

CreatorAllocationMismatch / MaxShareToBeSoldMismatch

requiredNumPositions

itself

NumPositionsMismatch

requiredVestingDuration

itself

VestingDurationMismatch

The registry pinning means production mainnet may force values tighter than the absolute caps. Always read the registry's required* views before constructing IgniteParams - see Live registry config.

The contract-level inverse-tick acceptance (line bool reverseMatch = (params.tickLower == -reqTickUpper && params.tickUpper == -reqTickLower) in _validateRegistryRequirements) is separate from the indexer's (ticks, fireTokenPosition) check - see Token ordering and tick math below.

Production defaults (Kumbaya frontend)

The Kumbaya frontend uses these values for every standard launch on mainnet:

A few things worth calling out for integrators replicating the defaults:

  • creatorAllocationBps = 0. The standard frontend launch grants no on-token creator allocation. The remaining 10000 - 7200 = 2800 bps (28%) goes to the tail position. Creators earn from post-grad FireStream fees and FuelVault skim/gifts - there's no vesting schedule to claim because there's nothing vesting.

  • You can set creatorAllocationBps up to the registry cap (MAX_CREATOR_ALLOCATION_BPS = 2000, i.e. 20%) when calling ignite yourself - but make sure creatorAllocationBps + maxShareToBeSoldBps ≤ 9000 so there's still tail liquidity.

  • Always read LaunchState (returned by FireLaunch.getLaunchState(token)) to know the actual split for a deployed token - defaults can change.

⚠ Token ordering and tick math

This is the easiest way to break a launch. Read this section before you call ignite.

🚨 The Kumbaya indexer only indexes launches that match an allowed (tickLower, tickUpper, fireTokenPosition) configuration for the chain. A token launched with anything else still exists on-chain (the contract is permissionless), but it won't be picked up by the indexer - so it won't appear in the launchpad feed, search, or curated APIs, and Kumbaya pricing won't track it. Direct V3 trading against the pool still works for anyone with the contract address.

In Uniswap V3, every pool has a token0 and a token1, sorted by address: the lower-address token is token0. Ticks measure the price of token0 denominated in token1 - so swapping which side your token is on inverts the meaning of every tick value.

What the indexer actually accepts

The validator (isValidFirePoolConfig) accepts both the canonical config and its inverse:

Side

fireTokenPosition

tickLower

tickUpper

Canonical (Kumbaya frontend defaults)

token0

-219400

-174800

Inverse (token is token1)

token1

174800

219400

The inverse is the same curve from the pool's other side: ticks negated and swapped, position flipped. Fee tier, supply, skim, allocation, vesting duration, and position count are not checked by the indexer - only this (tickLower, tickUpper, fireTokenPosition) triple.

The Kumbaya frontend always launches with token as token0 and the canonical ticks above. Two reasons to follow that path:

  1. It matches every screenshot and creator-facing UI in kumbaya.xyz - no surprises for users.

  2. FireLaunch.ignite() deploys via CREATE2, so the token address is determined by the salt. Salt mining for tokenAddr < numeraire (i.e. token is token0) typically takes only a few thousand attempts.

The Kumbaya defaults (tickLower = -219400, tickUpper = -174800) assume the new token is token0:

  • The negative ticks place the launched token at a low price relative to the numeraire - i.e. early buyers get cheap tokens.

  • The graduation tick (tickUpper = -174800) is the price target the curve climbs up toward.

  • The tail position extends from tickUpper outward toward MAX_TICK.

If your launched token ends up as token1 (higher address than the numeraire), every one of those statements flips. Buys would walk price the wrong way, the "graduation" tick becomes a floor instead of a ceiling, and the tail position points the wrong direction.

Always mine the salt for token0

FireLaunch.ignite() deploys the new token via CREATE2, so the resulting address is fully determined by (deployer, salt, init code, constructor args). Pick salts in a loop until the resulting address is strictly less than the numeraire (WETH) address:

The Kumbaya frontend uses these defaults:

Setting
Value

Max attempts (no vanity)

10,000

Max attempts (with vanity, 3-hex suffix)

1,000,000

Vanity suffixes available

069, 420, 888

Mining without a vanity is fast - it's a 50/50 coin flip per salt, so you typically find one in a handful of tries.

Launching as token1 (inverse config)

If you can't (or don't want to) mine for token0, you can launch with the new token as token1 and the inverse ticks above. This is indexed correctly and will appear on the launchpad - the indexer treats canonical and inverse as equivalent.

To do it:

  1. Mine a salt where tokenAddr > numeraire.

  2. Pass tickLower = 174800 and tickUpper = 219400 (the negated and swapped values of the canonical config).

  3. Everything else stays the same.

In practice, mining for token0 is faster than the salt search needed to land on a memorable address while satisfying tokenAddr > numeraire, so the canonical config is what you'll see in production.

What the indexer rejects

Any tuple (tickLower, tickUpper, fireTokenPosition) that doesn't match either row above. The contract may still accept the call (because _validateRegistryRequirements only checks tick values, not the resulting token0/token1 ordering) - but the indexer skips the launch.

Examples and their outcomes:

Ticks passed

Token side after deploy

Contract ignite

Indexer

(-219400, -174800) (canonical)

token0

accepts

✓ indexed

(-219400, -174800) (canonical)

token1

accepts (matches required)

✗ skipped (unindexed)

(174800, 219400) (inverse)

token1

accepts

✓ indexed

(174800, 219400) (inverse)

token0

accepts (matches required inverse)

✗ skipped

Custom range, e.g. (-150000, -100000)

either

reverts (TickRangeMismatch) if registry pins ticks

n/a

If the launch is unindexed, the token is permanent and tradeable but invisible to Kumbaya. Re-deploying with the correct ordering means a fresh address.

Sanity check before submitting ignite

A failed launch costs gas and produces a stranded token. The 5-line check above prevents both.

Example: deploy with viem

After confirmation, parse the TokenIgnited(address indexed token, address indexed creator, address indexed pool, uint256 totalSupply, uint16 skimBps, int24 tickLower, int24 tickUpper) event to read the deployed token address, or compute it yourself from the salt + init code hash.

Salt mining

The CREATE2 salt is significant: many integrations want the new token to be token0 of the V3 pool (i.e. lower address than the numeraire). The Kumbaya frontend mines salts to satisfy that ordering, and additionally tries vanity suffixes like 069 and 420. Mining is fast - a few thousand attempts is typical.

Reference logic from the frontend (simplified):

Buying at launch

The Kumbaya frontend lets a creator buy from their own bonding curve in the same UX flow. The contract supports this only as a two-transaction batch, not a single call:

In practice both txs are signed up-front with sequential nonces and broadcast together via eth_sendRawTransactionBatch (or sequentially). The second tx executes against the fresh pool created by the first.

⚠️ Not atomic. If the second tx fails (e.g. slippage), the token is still deployed. Validate the bonding-curve price math client-side before submitting the buy.

Errors to handle

Most validation failures revert with these custom errors:

Error
Cause

FeeTierNotApproved / InvalidFeeTier

feeTier not in registry's accepted set / not a valid V3 fee tier

NumeraireNotApproved

numeraire not in registry's accepted set

ZeroSupply / SupplyExceedsUint128

Bad totalSupply

CreatorAllocTooHigh

creatorAllocationBps > MAX_CREATOR_ALLOCATION_BPS (2000)

SkimTooHigh

skimBps > MAX_SKIM_BPS (1500)

AllocationsAbove90Percent

creatorAllocationBps + maxShareToBeSoldBps > 9000

MaxShareToBeSoldTooLow

maxShareToBeSoldBps below registry minimum

InvalidTickRange / TickNotAligned

tickLower, tickUpper invalid or not aligned to fee tier's tick spacing

TooFewPositions / TooManyPositions / ZeroPositions

numPositions outside [5, 50]

VestingDurationTooShort / VestingDurationTooLong

vestingDuration outside registry-allowed range

EmptyName / EmptySymbol

Missing metadata fields

*Mismatch family (TotalSupplyMismatch, TickRangeMismatch, etc.)

An existing pool already exists for this pair but with different params than what you passed

PoolAlreadyInitialized

Pool exists and is already at a non-zero price

Where to next

Last updated