> ## Documentation Index
> Fetch the complete documentation index at: https://docs.windrose.market/llms.txt
> Use this file to discover all available pages before exploring further.

# LaunchFactory

> Creates launches: a fixed-supply LaunchToken minted to a fresh BondingCurve clone sized in dollars at the oracle rate, with an optional first buy by the creator.

`LaunchFactory.createLaunch` is the one call that brings a token into existence. It reads the currency from the [registry](/protocol/contracts/currency-registry), converts the configured dollar depth into the launch currency at the oracle rate, deploys the token and a minimal-proxy [curve](/protocol/contracts/bonding-curve), seeds the curve with the whole supply and, when asked, executes the creator's first buy in the same transaction.

Source: `contracts/src/launch/LaunchFactory.sol` (`Ownable2Step`, `ReentrancyGuard`) and `contracts/src/launch/LaunchToken.sol`. Addresses: [Addresses](/protocol/addresses).

## Structs

```solidity theme={"system"}
struct LaunchConfig {
    uint256 totalSupply;        // minted once to the curve
    uint256 curveSupply;        // sold on the curve
    uint256 lpSupply;           // reserved for the Uniswap v4 position; curveSupply + lpSupply == totalSupply
    uint256 virtualToken;       // virtual token reserve of every new curve
    uint256 virtualQuoteUsd;    // virtual quote reserve in USD, 1e18; converted at the oracle rate at creation
    uint16  tradeFeeBps;        // fee on every curve buy and sell
    uint16  creatorFeeShareBps; // share of the trade fee that accrues to the creator
    uint16  graduationFeeBps;   // fee on the reserve at graduation
}

struct CreateParams {
    string  name;
    string  symbol;
    string  metadataURI;   // stored on the factory and emitted; the app stores a data: URI with JSON
    bytes32 code;          // currency code, e.g. "INR"
    uint256 initialIn;     // creator's first buy: dollars (raw) when payInUsdc, else the quote asset (raw); 0 skips it
    uint256 minTokensOut;  // slippage floor for that first buy
    bool    payInUsdc;     // pay the first buy in dollars and mint the synthetic through the vault (tier 2 only)
}

struct Launch {
    address token;
    address curve;
    address creator;
    bytes32 code;
    uint64  createdAt;
    string  metadataURI;
}
```

### Deployed configuration

| Field                     | Value         | Meaning                                                  |
| ------------------------- | ------------- | -------------------------------------------------------- |
| `totalSupply`             | `1e27`        | 1,000,000,000 tokens, 18 decimals                        |
| `curveSupply`             | `750e24`      | 750,000,000 sold on the curve                            |
| `lpSupply`                | `250e24`      | 250,000,000 for the pool                                 |
| `virtualToken`            | `375e24`      | 375,000,000 virtual tokens                               |
| `virtualQuoteUsd`         | `6000e18`     | 6,000 USD of virtual quote depth                         |
| `tradeFeeBps`             | `100`         | 1% per curve trade                                       |
| `creatorFeeShareBps`      | `3000`        | 30% of the trade fee to the creator, 70% to the protocol |
| `graduationFeeBps`        | `200`         | 2% of the reserve at graduation                          |
| `poolFee` / `tickSpacing` | `3000` / `60` | Uniswap v4 pool parameters (0.3%)                        |

With these numbers every curve starts at a 6,000 USD virtual depth and graduates after raising 12,000 USD in its currency (`virtualQuoteUsd * curveSupply / virtualToken`). See [Units](/protocol/units) for a worked example.

## `createLaunch`

```solidity theme={"system"}
function createLaunch(CreateParams calldata p, bytes[] calldata priceUpdate)
    external payable nonReentrant returns (address token, address curve);
```

<Steps>
  <Step title="Resolve the currency">
    `registry.requireEnabled(p.code)` returns the `CurrencyConfig` or reverts (`UnknownCurrency`, `CurrencyDisabled`).
  </Step>

  <Step title="Push the price and size the curve">
    `priceUpdate` is forwarded to the registry's oracle with exactly `oracle.updateFee(priceUpdate)` of `msg.value`; less reverts `InsufficientFee`, more is refunded (`RefundFailed` if the refund fails). With the keeper oracle pass `[]` and no value. Then `oracle.peekRate(p.code)` (no age check, so a launch can be created while the FX market is closed) gives `r`, and `virtualQuote = virtualQuoteUsd * r / 1e18` in 18-decimal quote units.
  </Step>

  <Step title="Deploy">
    `Clones.clone(curveImplementation)` creates the curve; `new LaunchToken(name, symbol, curve, totalSupply)` mints the whole supply to it; `curve.initialize(Init{...})` copies the config, the quote asset, the code and `msg.sender` as creator. The curve computes its own `graduationQuote`.
  </Step>

  <Step title="Record and emit">
    The `Launch` is appended (index = `launchCount()` before the call), `indexOfToken[token]` and `indexOfCurve[curve]` are set to `index + 1`, and `LaunchCreated` then `LaunchMetadata` are emitted.
  </Step>

  <Step title="Optional first buy">
    When `p.initialIn > 0`: if `payInUsdc` and the quote asset is not the dollar, the currency must be tier 2 (`UseQuoteAsset` otherwise); the factory pulls `initialIn` dollars, mints the synthetic through the vault with `minSynthOut = 0` and an empty price update, and buys with the minted amount. Otherwise it pulls `initialIn` of the quote asset directly. The buy goes to `msg.sender` with `p.minTokensOut` as the floor (`Slippage` from the curve); any quote the curve did not take (the last buy is capped at graduation) is returned to the creator in the quote asset.
  </Step>
</Steps>

Approvals before calling: `initialIn` of the dollar to the factory when `payInUsdc`, else `initialIn` of the quote asset (EURC for a EUR launch, the synthetic for a tier-2 launch paid in kind). USD launches always pay in the dollar; `payInUsdc` is irrelevant for them.

A launch can be created by any account; there is no allowlist and no creation fee beyond gas. Names and symbols are not validated on chain (the app limits them to 64 and 11 characters).

## Views

```solidity theme={"system"}
function launchCount() external view returns (uint256);
function getLaunch(uint256 index) external view returns (Launch memory);
function curveOfToken(address token) external view returns (address);   // 0x0 for unknown tokens
function indexOfToken(address token) external view returns (uint256);   // index + 1, 0 for unknown
function indexOfCurve(address curve) external view returns (uint256);   // index + 1, 0 for unknown
function config() external view returns (LaunchConfig memory);
function registry() external view returns (ICurrencyRegistry);
function curveImplementation() external view returns (address);
function poolManager() / positionManager() / permit2() / locker() / feeCollector() external view returns (address);
function poolFee() external view returns (uint24);
function tickSpacing() external view returns (int24);
```

Curves read `poolManager`, `positionManager`, `permit2`, `locker`, `feeCollector`, `poolFee` and `tickSpacing` from the factory at graduation and fee-sweep time (`ILaunchFactoryView`), so those values apply to every launch, including ones created earlier, at the moment it graduates.

## Owner functions

```solidity theme={"system"}
function setConfig(LaunchConfig calldata cfg) external onlyOwner;
function setAddresses(address pm, address posm, address p2, address locker_, address collector) external onlyOwner;
function setPoolParams(uint24 fee, int24 spacing) external onlyOwner;
function setRegistry(ICurrencyRegistry r) external onlyOwner;
```

| Function        | Validation (`InvalidConfig` / `ZeroAddress`)                                                                                                                             | Affects                                                                |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------- |
| `setConfig`     | every supply and reserve non-zero, `curveSupply + lpSupply == totalSupply`, `tradeFeeBps <= 1000` (10%), `creatorFeeShareBps <= 10000`, `graduationFeeBps <= 2000` (20%) | launches created afterwards only (existing curves copied their values) |
| `setAddresses`  | no zero address                                                                                                                                                          | future graduations and fee sweeps of every launch                      |
| `setPoolParams` | `spacing > 0`                                                                                                                                                            | future graduations of every launch                                     |
| `setRegistry`   | non-zero                                                                                                                                                                 | future launches                                                        |

The same bounds are enforced again by `BondingCurve.initialize`. The owner cannot touch a live curve's fees, supply, price or reserve.

## `LaunchToken`

```solidity theme={"system"}
contract LaunchToken is ERC20 {
    constructor(string memory name_, string memory symbol_, address to, uint256 supply);
}
```

A plain OpenZeppelin ERC-20 with 18 decimals whose entire supply is minted to the curve in the constructor. No owner, no mint, no burn hook, no transfer tax, no blocklist, no pause. Holdings of the curve, the Uniswap `PoolManager` and the burn address are excluded from the indexer's holder counts.

## Events

```solidity theme={"system"}
event LaunchCreated(uint256 indexed index, address indexed token, address indexed curve,
                    address creator, bytes32 code, address quoteAsset, uint256 virtualQuote);
event LaunchMetadata(address indexed token, string name, string symbol, string metadataURI);
event ConfigSet(LaunchConfig config);
event AddressesSet(address poolManager, address positionManager, address permit2, address locker, address feeCollector);
event PoolParamsSet(uint24 poolFee, int24 tickSpacing);
event RegistrySet(address registry);
```

`virtualQuote` in `LaunchCreated` is in 18-decimal quote units. The indexer creates its `launch` row from these two events and starts following the new curve and token addresses (see [Launches query](/api/queries/launches)).

## Errors

| Error               | When                                                                        |
| ------------------- | --------------------------------------------------------------------------- |
| `InvalidConfig()`   | `setConfig` / constructor bounds, `setPoolParams` with non-positive spacing |
| `ZeroAddress()`     | zero registry, curve implementation or any address in `setAddresses`        |
| `UseQuoteAsset()`   | `payInUsdc` with a tier-1 quote asset that is not the dollar (EURC)         |
| `InsufficientFee()` | `msg.value` below the oracle's update fee                                   |
| `RefundFailed()`    | the caller rejected the excess value                                        |

Reverts from the registry (`UnknownCurrency`, `CurrencyDisabled`), the vault (`Slippage`, `LiabilityCapExceeded`, `StalePrice`, paused) and the curve (`Slippage`) bubble up unchanged.

## Metadata

`metadataURI` is opaque to the contracts. The app encodes `{ name, description, image }` as `data:application/json;base64,...`, with the image itself a small WebP data URL (at most 20 KB after client-side cropping), so a launch's artwork lives on chain with no external host. Integrators may store any URI (IPFS, HTTPS); the app resolves `ipfs://` through a public gateway and treats anything it cannot parse as absent.
