# Foci — complete integration documentation Network arc-mainnet (chain 5042). Generated from the deployed contracts. 12 contracts · 232 functions · 78 events · 166 errors. --- # Foci Foci is a permissionless launchpad. Anyone deploys a token against a **bonding curve**; the curve is the only venue until a fixed amount has been raised, at which point the launch **graduates** — reserves are swept out of the curve and seeded as a full-range Uniswap V4 position whose liquidity is locked forever. Everything is on-chain and permissionless. There is no allowlist for trading, no admin who can seize a position, and no path by which locked liquidity comes back out. ## The two venues A token is only ever tradeable in one place, and which one depends on its **phase**: | Phase | Name | Where it trades | |---|---|---| | `0` | NotGraduated | the bonding curve | | `1` | Swept | **nowhere** — reserves are out of the curve, the pool is not yet created | | `2` | PoolCreated | the Uniswap V4 pool | | `3` | Rescued | nowhere; terminal | Phase 1 is transient and usually invisible — the crossing buy normally sweeps *and* seeds in one transaction. But it can persist if the seed runs out of gas, and a token sitting there has no tradeable venue at all. See [Graduation](/documentation/graduation). Read the phase from `factory.getLaunchedToken(token).phase` and branch your UI on it. Do not infer it from whether a pool exists. ## What you actually call | To | Call | |---|---| | launch with no opening buy | `factory.launchToken` | | launch and buy atomically | `launchAndBuy.launchAndBuy` | | trade before graduation | `curve.buy` / `curve.sell` on the launch's own curve | | trade after graduation | Uniswap's UniversalRouter | | collect fees you are owed | `feeEscrow.claimToken` | The curve is **per launch** — it has no fixed address. Get it from `factory.getLaunchedToken(token).curve`. ## Before you write anything Three pages will save you the most time, in this order: 1. **[Approvals](/documentation/approvals)** — which contract pulls your funds. Two of the five entrypoints approve something other than the contract you are calling. 2. **[Decimals](/documentation/decimals)** — a 6-, 8- or 18-decimal quote asset against an 18-decimal token. 3. **[Graduation](/documentation/graduation)** — the gas trap that leaves a launch half-migrated. ## Building with an AI There is a **Use with AI** button at the top of every page. It will copy the page you are on, open a Claude conversation already pointed at the full reference, or install a Claude Code skill. Directly, if you prefer: | | | |---|---| | [`/llms-full.txt`](/llms-full.txt) | The entire documentation as one document — every address, signature, selector and gotcha. Paste this into an agent. | | [`/llms.txt`](/llms.txt) | A short index, following the [llms.txt](https://llmstxt.org) convention. | | [`/skill.md`](/skill.md) | A Claude Code skill. Save it as `.claude/skills/foci/SKILL.md` and it loads itself when relevant. | | `/documentation/.md` | Any single page as raw markdown. | The skill is short on purpose. It teaches the handful of things that are counter-intuitive — approval targets, the two decimal scales, the graduation gas trap — and points at the full reference for everything else, so it costs little context to keep loaded. --- # Quickstart Launch a token with an opening buy, in one transaction, with viem. Every address below is Arc mainnet (chain `5042`); see [Addresses](/documentation/addresses). The snippets are written against a `QUOTE` — any approved pair token — rather than USDC in particular. Pick one from `GET /v1/protocol` → `approvedPairTokens`, and take its decimals from the same record; see [Decimals](/documentation/decimals) for the five and their scales. ```ts const QUOTE = USDC; // or WETH, cirBTC, EURC, XAUM const QUOTE_DECIMALS = 6; // 18 for WETH/XAUM, 8 for cirBTC ``` ## 1 · Check you are allowed to launch Launching is gated and **disabled by default on a fresh deployment** — on Arc mainnet it stays closed until the owner opens it. Always check first: ```ts const allowed = await client.readContract({ address: FACTORY, abi: factoryAbi, functionName: "canLaunch", args: [account], }); if (!allowed) throw new Error("launching is not open for this address"); ``` ## 2 · Pin the economics `expectedEconomics` freezes the terms your user was shown. If the owner re-pegs the curve or changes the launch fee between your quote and your signature, the launch reverts instead of landing on terms nobody agreed to. ```ts const expectedEconomics = await client.readContract({ address: FACTORY, abi: factoryAbi, functionName: "previewLaunchEconomics", args: [0n, QUOTE], }); ``` Fetch it in the same flow as the submit. Do not cache it. Passing `bytes32(0)` waives the check, which means accepting whatever terms are live when the transaction lands. ## 3 · Approve the router The atomic path pulls the fee **and** the opening buy, and the spender is the router, not the factory. They are two different assets unless the launch quotes in USDC: the fee is always `launchFee()` of `launchFeeToken()` (5 USDC), the opening buy is `quoteIn` of `QUOTE`. ```ts const fee = await client.readContract({ address: FACTORY, abi: factoryAbi, functionName: "launchFee" }); const feeToken = await client.readContract({ address: FACTORY, abi: factoryAbi, functionName: "launchFeeToken" }); const openingBuy = parseUnits("1", QUOTE_DECIMALS); // 1 unit of the quote, in ITS decimals if (feeToken.toLowerCase() === QUOTE.toLowerCase()) { // Same token: one approval for the sum covers both pulls. await walletClient.writeContract({ address: QUOTE, abi: erc20Abi, functionName: "approve", args: [LAUNCH_AND_BUY, fee + openingBuy], }); } else { // Different tokens: the fee in USDC, the buy in the quote — two approvals, same spender. await walletClient.writeContract({ address: feeToken, abi: erc20Abi, functionName: "approve", args: [LAUNCH_AND_BUY, fee], }); await walletClient.writeContract({ address: QUOTE, abi: erc20Abi, functionName: "approve", args: [LAUNCH_AND_BUY, openingBuy], }); } ``` ## 4 · Launch ```ts const { result } = await client.simulateContract({ address: LAUNCH_AND_BUY, abi: launchAndBuyAbi, functionName: "launchAndBuy", args: [ { name: "My Token", symbol: "MINE", logo: "", description: "", socials: { twitter: "", telegram: "", discord: "", website: "", farcaster: "" }, creatorFeeRecipient: account, // must NOT be zero on this path creatorTaxBps: 0, expectedEconomics, salt: crypto.getRandomValues(new Uint8Array(32)), }, 0n, // launchConfigId QUOTE, // pairToken openingBuy, // quoteIn — must be non-zero here, in the quote's decimals 0n, // minTokensOut account, // recipient of the bought tokens ], account, }); const [token, curve, tokensOut] = result; ``` Then send it with an **explicit gas limit** — see the warning below. > [!WARNING] > If the opening buy is large enough to cross the graduation threshold, the curve tries to > seed the Uniswap pool inside this same transaction. `eth_estimateGas` cannot size that: > the seed is a best-effort `try/catch`, so a simulation in which it fails still *succeeds* > overall and returns a limit too small for it to work — every time, deterministically. Send > something generous (8,000,000 is ample). EIP-1559 charges for gas used, not the limit, so > over-providing costs nothing. ## 5 · Launch without an opening buy `launchAndBuy` reverts on a zero `quoteIn` — it exists to buy and refuses to act as a plain deployer. Use the factory directly, and note the approval target moves with it: ```ts await walletClient.writeContract({ address: feeToken, abi: erc20Abi, functionName: "approve", // the fee token (USDC), whatever the quote args: [FACTORY, fee], // the FACTORY, not the router }); const { result } = await client.simulateContract({ address: FACTORY, abi: factoryAbi, functionName: "launchToken", args: [params, 0n, QUOTE], account, }); ``` A token launched this way starts with the creator holding **none** of its supply. ## 6 · Buy on the curve The curve has no fixed address — read it from the launch record. ```ts const launch = await client.readContract({ address: FACTORY, abi: factoryAbi, functionName: "getLaunchedToken", args: [token], }); await walletClient.writeContract({ address: launch.pairToken, abi: erc20Abi, functionName: "approve", // the launch's OWN quote args: [launch.curve, amountIn], // approve the CURVE, amount in the quote's decimals }); await walletClient.writeContract({ address: launch.curve, abi: curveAbi, functionName: "buy", args: [amountIn, minTokensOut, account], }); ``` --- # Addresses Everything below is **Arc mainnet**, chain `5042`. The live table on this page is read from the API at request time, so it reflects what is actually deployed rather than what was true when this page was written. Arc testnet (chain `5042002`) still carries an older deployment. None of its addresses are listed here, and they are not interchangeable with the mainnet ones. ## Per-launch contracts have no fixed address Three of the contracts in the reference are deployed **once per launch**: - `FociBondingCurve` — by the factory - `FociLauncherToken` — by the factory - `FociRewardsDistributor` — by the rewards factory, only for launches that chose **Fees go to: Holders**; read it with `rewardsFactory.distributorOf(token)` (zero otherwise) There is no address to configure. Read them from the launch record: ```ts const launch = await client.readContract({ address: FACTORY, abi: factoryAbi, functionName: "getLaunchedToken", args: [token], }); launch.curve; // the bonding curve for this launch launch.exists; // false for an unknown token — check this first ``` `getLaunchedToken` returns a **zeroed struct** rather than reverting for an address it does not know, so an unchecked read makes an unknown token look like a live curve at phase 0. ## Indexing `FACTORY_START_BLOCK` is the block of the deploy broadcast — there is nothing to index before it. Note that the public Arc RPC and a dedicated provider disagree on `eth_getLogs` limits: the public node allows a 25,000-block span, QuickNode caps it at 10,000. Pin your range to your provider, and re-measure it if you switch — a range above the cap fails every request rather than degrading. ## Deployed contracts Network **arc-mainnet**, chain `5042`. Explorer: https://explorer.arc.io Indexing starts at block `20883999`. | Contract | Address | Kind | |---|---|---| | FociLaunchFactory | `0xa392D6eca5242715517eeCd43406aeD19424FAC0` | singleton | | FociLaunchAndBuy | `0x5C5c202271E1300bD5Ce43A4F5C1cEA8efd57B63` | singleton | | FociMemeHook | `0xF847790B6fA5DA300BB3f56f10d743e71E98e044` | singleton | | FociFeeEscrow | `0x5a76a44B49ca0f7c4dB181f289C1eCA91d928406` | singleton | | FociReferralRegistry | `0xe047D0F0ce0dD600732793762B1f1929Adc5015d` | singleton | | FociLaunchLocker | `0x539fD9e6a6316B65bEd9dDb9A570959e0bc8C31A` | singleton | | FociLaunchDeployer | `0xa93f9CeFD92A77e1EAffa3246B6F4DB91a5c5659` | singleton | | FociGraduationExecutor | `0xEB286974C35d2741B0fe9b2a1Cd41E53d06aE406` | singleton | | FociRewardsDistributorFactory | `0xdac447110867954F00638125bbd5c66D8E0a7195` | singleton | | FociBondingCurve | — | per launch, read from the launch record | | FociLauncherToken | — | per launch, read from the launch record | | FociRewardsDistributor | — | per launch, read from the launch record | ## External contracts | Key | Address | Purpose | |---|---|---| | poolManager | `0x8366a39CC670B4001A1121B8F6A443A643e40951` | Uniswap V4 PoolManager | | positionManager | `0x6049c9a0e26405c0985f9e3685c87d0ae917f82b` | Uniswap V4 PositionManager | | universalRouter | `0x4fcA4a51Ab4F23A7447b3284fBd7D73289A89Fb1` | Uniswap UniversalRouter — pool swaps | | v4Quoter | `0x8Dc178eFB8111BB0973Dd9d722ebeFF267c98F94` | Uniswap V4 Quoter — pool-phase estimates | | stateView | `0xF3334192D15450CdD385c8B70e03f9A6bD9E673b` | Uniswap V4 StateView — pool state reads | | permit2 | `0x000000000022D473030F116dDEE9F6B43aC78BA3` | Permit2 — required for UniversalRouter | | usdc | `0x3600000000000000000000000000000000000000` | Quote asset (6 decimals) | --- # Lifecycle A launch moves through four states. Everything an integration does depends on which one it is in, so read the phase rather than inferring it. ``` launchToken / launchAndBuy | v [0] NotGraduated ── curve.buy / curve.sell ──┐ | | | sellable allocation hits zero | v | [1] Swept NO TRADEABLE VENUE | | | | createGraduatedPool | v | [2] PoolCreated ── UniversalRouter ───────────┘ ``` ## 0 · Launch The factory deploys a curve and a token via CREATE2, mints the whole supply to the curve, takes the launch fee, and writes a launch record. Trading is live immediately. The curve reserves a fraction of supply that it will never sell — that reserve is what seeds the pool at graduation, and it is why the curve has a hard stop rather than an asymptote. ## 1 · Curve trading Constant product against a **virtual** reserve: the curve behaves as though it already held `phantomQuote` of the quote asset, which sets the opening price. Every fee comes off the quote leg. The launch is finished when the sellable allocation reaches zero, which is by construction the same moment the real reserve reaches the graduation threshold. ## 2 · Sweep `factory.graduate(token)` sets the curve's `graduated` flag, sweeps fees, and moves the reserves into the factory. Phase becomes **1**. **In this phase the token has no venue.** The curve is closed and the pool does not exist. Normally it lasts one instruction, because the crossing buy performs both steps — but it can persist. See [Graduation](/documentation/graduation). ## 3 · Pool seed `factory.createGraduatedPool(token)` initialises the V4 pool at the curve's closing price, registers it with the hook, mints a full-range position **directly to the locker**, and permanently locks the leftover supply. Phase becomes **2**. The locker has no withdrawal function of any kind. Liquidity and the burned supply are unrecoverable by anyone, including the contract owner. That is the point. ## 4 · Pool trading Ordinary V4 swaps through UniversalRouter, with the hook taking its fee on the unspecified leg of each swap. The curve is never used again. ## Rescued (phase 3) A terminal state reached only by owner intervention, after a delay, when a swept launch cannot be seeded because the quote asset can no longer deliver an exact transfer. Reserves are released to a single recipient. It exists so that a broken quote asset cannot strand funds forever; you will not see it in normal operation. --- # Launching a token Two entrypoints, and the right one depends on whether there is an opening buy. | | `factory.launchToken` | `launchAndBuy.launchAndBuy` | |---|---|---| | opening buy | none | required, non-zero | | approve | **the factory**, `launchFee()` of `launchFeeToken()` | **the router**: `launchFee()` of `launchFeeToken()` **and** `quoteIn` of the quote asset (one approval when they are the same token) | | `creatorFeeRecipient` zero | defaults to the caller | **rejected** | | creator's starting balance | nothing | the opening buy | `launchAndBuy` reverts with `ZeroAmount` on a zero `quoteIn` — it exists to make deploy-and-buy atomic and refuses to be used as a plain deployer. ## TokenParams ```solidity struct TokenParams { string name; // required, <= 64 bytes string symbol; // required, <= 16 bytes string logo; // <= 512 bytes — a URI, never the image string description; // <= 2048 bytes Socials socials; // five strings, each <= 256 bytes address creatorFeeRecipient; // earns the creator split and the whole creator tax uint16 creatorTaxBps; // extra tax on top of the curve fee, capped by maxCreatorTaxBps bytes32 expectedEconomics; // terms pin; bytes32(0) waives it bytes32 salt; // CREATE2 salt, namespaced per account } ``` Three of these deserve attention. ### logo is a reference, not an image It is stored **on-chain**, so it must be short. Putting a base64 data URI here does not merely cost gas — the node rejects the transaction outright and the launch never reaches the contract. Upload the image first and store the resulting URL. ### expectedEconomics A digest of the exact terms this launch will lock in. Get it from `previewLaunchEconomics(launchConfigId, pairToken)` in the same flow as the submit. It covers the phantom reserve, graduation threshold, supply, curve fee, pool fee, tick spacing, the protocol fee shares — **and the launch fee**. So an owner calling `setLaunchFee` between your quote and your signature invalidates it, and the launch reverts with `LaunchEconomicsMismatch(expected, actual)` rather than landing on terms your user never saw. Do not cache it. `bytes32(0)` waives the check entirely, which means accepting whatever is live when the transaction lands. ### salt A raw CREATE2 salt, namespaced by the factory as `keccak256(deployer, salt)` — so it only needs to be unique among **your own** launches. Two creators may use the same value. Reusing one on otherwise identical terms reverts with `FailedDeployment`, which says nothing about salts. Call `launchDeployer.predictLaunchAddresses(...)` first to check for existing code, and to mine a vanity address if you want one. ## Preconditions worth checking before you show a form ```ts const canLaunch = await read("canLaunch", [account]); // false by default on a new deployment const fee = await read("launchFee"); // may be zero -> no fee approval needed const feeToken = await read("launchFeeToken"); // USDC, whatever the launch quotes in const economics = await read("pairTokenEconomics", [pairToken]); // phantom + threshold, in the QUOTE's decimals ``` `launchEnabled` is left **false** by deployment, so a fresh environment rejects every launch until the owner opens it or whitelists an address. Surfacing that as a disabled button beats a revert. ## Common reverts | Error | Cause | |---|---| | `NotWhitelisted` / `NotApprovedLauncher` | launching is closed for this address | | `LaunchEconomicsMismatch` | terms moved between quote and submit | | `FailedDeployment` | this account already used that salt | | `CreatorTaxTooHigh` | `creatorTaxBps` above `maxCreatorTaxBps()` | | `PairTokenNotApproved` | the quote asset is not approved by the owner | | `ZeroAmount` | zero `quoteIn` on the router path — use `launchToken` | | ERC-20 allowance revert | approved the wrong contract; see [Approvals](/documentation/approvals) | --- # Trading the curve Before graduation the only venue is the launch's own bonding curve. It has no fixed address — read it from `factory.getLaunchedToken(token).curve`. Approve **the curve** for both legs: the quote asset to buy, the memecoin to sell. ## The two slippage bounds are not the same kind of bound This surprises people, and it is deliberate. **On a buy, `minTokensOut` is a price bound.** A buy that would take more than the remaining allocation is *clamped* rather than reverted — you get the remaining tokens and the unspent quote is refunded to `msg.sender` in the same transaction. The bound is then enforced as an implied price rather than a quantity, so a partial fill at an acceptable price succeeds. That means **you can safely overshoot the graduation threshold**. Sending more than the curve can absorb is normal and costs nothing. **On a sell, `minQuoteOut` is a strict quantity bound.** You get at least that much or the call reverts. ## The sell side closes early A sell reverts with `CurveGraduated` as soon as `readyToGraduate()` becomes true — which is *before* the `graduated` flag is set and before anyone has called `graduate`. So there is a window where the curve looks live, `graduated` is still false, and every sell reverts. If your UI reads `graduated` to decide whether selling is possible, it will offer a button that cannot work. Read `readyToGraduate()` too. ## Referrals The four-argument `buy` overload takes a referrer: ```solidity function buy(uint256 quoteIn, uint256 minTokensOut, address recipient, address referrer) ``` Three things to know: - The binding is written for **`recipient`**, not `msg.sender`. - It is consulted only on that address's **first** referred trade, and is permanent after that. Passing a fresh referrer later cannot poach an existing relationship. - **A bad referrer reverts the whole buy.** `SelfReferral` and `ReciprocalReferral` are not swallowed. Validate before you put an address in the call — particularly if it came from a URL parameter. `sell` has no referrer argument; only an existing binding applies. Users can also bind themselves ahead of any trade with `referralRegistry.setReferrer(address)`. ## Quoting Off-chain, the curve is constant product against a virtual reserve: ``` netIn = quoteIn - fee - creatorTax // fees come off the QUOTE leg tokensOut = netIn * tokenReserve / (quoteReserve + netIn) ``` where `quoteReserve = phantomQuote + trackedQuote - quoteFeeBalance - creatorTaxBalance`, available together from `getReserves()`. Note `getReserves()` returns the **virtual** quote reserve, which includes the phantom reserve. For the amount actually raised — what a progress bar should show against the threshold — use `realQuoteReserve()`. Once a launch graduates the curve is closed and estimates must come from the Uniswap V4 Quoter instead. See [Trading the pool](/documentation/pool). --- # Graduation When the curve's sellable allocation reaches zero — the same moment its real reserve reaches the graduation threshold — the launch graduates. That is two steps, and understanding why they are separate is the difference between a working integration and a stuck token. ## The two steps ``` factory.graduate(token) phase 0 -> 1 sweeps the curve into the factory factory.createGraduatedPool(token) phase 1 -> 2 seeds the V4 pool, locks the position ``` Both are **permissionless** — anyone may call either. Both normally run automatically inside the buy that crosses the threshold, wrapped in `try/catch` so that a failure to graduate never fails the trade. ## The gas trap That `try/catch` is also the trap. Seeding the pool costs roughly **900,000 gas** on its own. Under EIP-150's 63/64 rule the nested call only receives 63/64 of whatever the buy has left. If the buy was sized by a naive `eth_estimateGas`, there is not enough left and the seed dies. And estimation **cannot** discover this. The estimator simulates the whole transaction including the `try/catch`; the seed fails inside it; the catch swallows the failure; the transaction *succeeds*. So the estimator returns a limit sized for the **seed-fails** path — which is by construction never enough for the seed-succeeds path. This is deterministic, not flaky. With naive estimation the auto-seed fails **every time**, and reports success while doing it. Measured on Arc testnet (same contract code as mainnet), two launches identical except the gas limit: | Gas limit | Result | |---|---| | 1,154,267 (from `estimateGas`) | `AutoSeedFailed(token, 27828)` — stuck in phase 1 | | 3,000,000 (explicit) | seeded in the same transaction | The shortfall was about 150,000 gas. > [!WARNING] > On any buy that might cross the threshold, send an explicit gas limit — roughly > `estimate + 1,200,000`, or simply 3,000,000. You cannot control third-party routers' > gas limits, so run the keeper below regardless. ## Watch for the failure events The curve tells you when it happened: ```solidity event AutoGraduationFailed(address indexed token, uint256 gasRemaining); event AutoSeedFailed(address indexed token, uint256 gasRemaining); ``` Both are the signal to finish the job manually. Alert on them, and have a keeper call the matching entrypoint: ```ts // finish a launch stuck in phase 1 const gas = await client.estimateContractGas({ address: FACTORY, abi: factoryAbi, functionName: "createGraduatedPool", args: [token], account, }); await walletClient.writeContract({ address: FACTORY, abi: factoryAbi, functionName: "createGraduatedPool", args: [token], gas: gas * 2n, }); ``` Estimation *is* reliable here — called directly there is no `try/catch` hiding the cost. A launch in phase 1 is not broken and nothing is lost: the swept reserves sit safely in the factory and the seed is retryable by anyone, indefinitely. It simply has no venue until somebody calls it. ## What the seed does 1. Computes the token side as `sweptTokens · sweptQuote / (sweptQuote + phantomQuote)`. 2. **Permanently locks the remainder** in the launch locker — that supply is burned in every sense that matters; the locker has no withdrawal path for anyone, including its owner. 3. Initialises the V4 pool at the curve's closing price, with the Foci hook attached. 4. Mints a full-range position **directly to the locker**, which is why the liquidity can never be pulled. At the shipped curve shape, 71.43% of supply is sold on the curve, 20.41% is seeded into the pool, and **8.16% is burned forever**. ## Checking the phase ```ts const launch = await client.readContract({ address: FACTORY, abi: factoryAbi, functionName: "getLaunchedToken", args: [token], }); // 0 curve · 1 swept (no venue) · 2 pool · 3 rescued ``` `getLaunchedToken` returns a **zeroed struct** for an unknown token rather than reverting — check `.exists` before trusting `.phase`, or an unknown address looks like a live curve. --- # Trading the pool Once a launch reaches phase 2 the curve is closed and the token trades as an ordinary Uniswap V4 pool — with the Foci hook attached, which takes the protocol and creator fee out of every swap. ## You cannot call PoolManager directly V4 swaps only work inside an `unlock` callback, so you need a router. Foci does not ship one; use Uniswap's **UniversalRouter**. Two consequences: - **Approvals go through Permit2**, not a plain ERC-20 allowance. See [Approvals](/documentation/approvals) — this is where most pool integrations fail first. - **Referral attribution depends on the router.** The hook identifies the trader by calling `msgSender()` on whoever called it. UniversalRouter implements that (via `BaseActionsRouter`), so referrals work. A router that does not implement `IMsgSender` leaves the trader unattributed and the swap is charged the **undiscounted** fee — it does not revert, it just quietly costs the user more. ## Building the pool key ```ts const memecoinIsCurrency0 = token.toLowerCase() < usdc.toLowerCase(); const poolKey = { currency0: memecoinIsCurrency0 ? token : usdc, currency1: memecoinIsCurrency0 ? usdc : token, fee: 0, // ALWAYS zero — the hook charges, not V4's LP fee tickSpacing: 200, hooks: MEME_HOOK, }; const zeroForOne = spendToken.toLowerCase() === poolKey.currency0.toLowerCase(); ``` `fee` is zero by construction: the factory rejects any launch config with a non-zero pool fee, because the hook takes the fee instead. Do not copy a 3000 from a V4 example. ## Quoting The Foci API refuses to quote a graduated launch by design. Use Uniswap's **V4Quoter**, which simulates the real swap through the real hook — so the hook fee is included by construction, rather than reimplemented off-chain and drifting. ```ts const { result } = await client.simulateContract({ address: V4_QUOTER, abi: quoterAbi, functionName: "quoteExactInputSingle", args: [{ poolKey, zeroForOne, exactAmount: amountIn, hookData: "0x" }], }); const [amountOut] = result; ``` `quoteExactInputSingle` is state-mutating (it reverts internally to unwind), so simulate it — a plain `readContract` will not work. ## Swapping ```ts const swap = encodeAbiParameters( parseAbiParameters("((address,address,uint24,int24,address),bool,uint128,uint128,uint256,bytes)"), [[[poolKey.currency0, poolKey.currency1, 0, 200, MEME_HOOK], zeroForOne, amountIn, minOut, 0n, "0x"]], ); const settle = encodeAbiParameters(parseAbiParameters("address, uint256"), [spendToken, amountIn]); const take = encodeAbiParameters(parseAbiParameters("address, uint256"), [takeToken, minOut]); // SWAP_EXACT_IN_SINGLE, SETTLE_ALL, TAKE_ALL const actions = "0x060c0f"; const input = encodeAbiParameters(parseAbiParameters("bytes, bytes[]"), [actions, [swap, settle, take]]); await walletClient.writeContract({ address: UNIVERSAL_ROUTER, abi: universalRouterAbi, functionName: "execute", args: ["0x10", [input], BigInt(Math.floor(Date.now() / 1000) + 600)], // 0x10 = V4_SWAP }); ``` Slippage belongs in the `TAKE_ALL` minimum — that is the router's own bound and it reverts rather than delivering less, so there is no need to re-check the output afterwards. ## The fee comes off the unspecified leg The hook charges on whichever side of the swap you did *not* specify. On an exact-input sell that is the output, so the amount you receive is net while the V4 `Swap` event reports the **gross** figure. Anything reading `Swap` alone will overstate proceeds by the fee. If you are indexing, subtract the hook's take. --- # Approvals Every entrypoint that moves your money pulls it with `transferFrom`, which means you must `approve` the contract that does the pulling — and that contract is **not the same one you are calling** in two of the five cases. Getting this wrong is the most common integration failure, and the error message does not help: you get a bare ERC-20 allowance revert naming neither the expected spender nor the amount. ## The matrix | You are calling | Approve | Asset | Amount | |---|---|---|---| | `factory.launchToken` | **the factory** | `launchFeeToken()` (USDC) | `launchFee()` | | `launchAndBuy.launchAndBuy` | **the router** | `launchFeeToken()` (USDC) **and** the launch's quote asset | `launchFee()` in USDC, `quoteIn` in the quote — two approvals unless the quote is USDC, when one for the sum covers both | | `curve.buy` | **the curve** | the launch's quote asset | `quoteIn` | | `curve.sell` | **the curve** | the **memecoin** (18 dp) | `tokensIn` | | a graduated-pool swap | **Permit2, then the router** | whichever asset you spend | see below | | `escrow.claim` / `claimToken` | nothing | — | claims are pull-only | Two things people get wrong reading that table: - **The two launch paths approve different contracts.** The factory pulls the fee from `msg.sender` itself, so a direct launch approves the *factory*. The router pulls the fee *and* the opening buy, so the atomic path approves the *router* — and approving the factory there does nothing. - **Both curve legs approve the curve**, but for different assets. A buy spends the quote asset; a sell spends the memecoin. The spender is the same, the token is not. - **The launch fee and the opening buy are different assets** unless the launch quotes in USDC. The fee is always `launchFee()` of `launchFeeToken()` (5 USDC); the opening buy is `quoteIn` of whatever `pairToken` the launch chose. A WETH-quoted launch with an opening buy therefore approves the router **twice** — USDC for the fee, WETH for the buy — and approving WETH for `launchFee() + quoteIn` leaves the fee unpaid and the launch reverting. ```ts const feeToken = await read("launchFeeToken"); // USDC const fee = await read("launchFee"); // 5e6 await approve(feeToken, LAUNCH_AND_BUY, fee); if (pairToken.toLowerCase() !== feeToken.toLowerCase()) { await approve(pairToken, LAUNCH_AND_BUY, quoteIn); // the quote asset, its own decimals } else { await approve(feeToken, LAUNCH_AND_BUY, fee + quoteIn); // one approval when they coincide } ``` ## Pool swaps need two approvals, not one UniversalRouter does not pull with a plain ERC-20 allowance. It pulls through **Permit2**, so a pool swap needs two transactions before the swap itself: ```ts // 1. one-time, per token: let Permit2 move this asset on your behalf await writeContract({ address: token, abi: erc20Abi, functionName: "approve", args: [PERMIT2, MAX_UINT160], }); // 2. per spender, with an expiry: let the router draw from Permit2 await writeContract({ address: PERMIT2, abi: permit2Abi, functionName: "approve", args: [token, UNIVERSAL_ROUTER, MAX_UINT160, Math.floor(Date.now() / 1000) + 30 * 86400], }); ``` Skipping the second step reverts with **`AllowanceExpired(uint256)`** — a name that reads like a grant lapsed when the truth is that one was never made. If you see that selector (`0xd81b2f2e`), you are missing the Permit2 approval, not the ERC-20 one. > [!TIP] > Check both allowances before offering a swap button. `permit2.allowance(owner, token, > spender)` returns `(amount, expiration, nonce)` — treat an expiry in the past as > unapproved, not as approved-with-zero. ## Zero launch fee needs no approval If `factory.launchFee()` returns zero there is nothing to pull, and a direct launch needs no approval at all. Read the fee rather than assuming it — it is owner-settable and it is part of the economics digest, so it can change between your quote and your submit. --- # Decimals Every launch quotes in one of the approved pair tokens, and every quote-side amount is in **that asset's own decimals**. Every launched memecoin is **18 decimals**. Almost every arithmetic bug in an integration comes from mixing the two — or from assuming the quote is always the 6-decimal one. ## The quote assets | Symbol | Address | Decimals | |---|---|---| | USDC | `0x3600000000000000000000000000000000000000` | **6** | | EURC | `0xbef5f6d51cb62b58e6a8f77868681825c6fe21c1` | **6** | | cirBTC | `0x171a4217b86a807a64eb94757db6849fb4bdbaa0` | **8** | | WETH | `0x128cc466b61f542da60c70e3aa11c10e19b84edb` | **18** | | XAUM | `0x178b01f61cbea1d2a5581fe1621be607835ec349` | **18** | The live list — with each asset's phantom reserve and graduation threshold — is `GET /v1/protocol` → `approvedPairTokens[]`, and on chain `factory.pairTokenEconomics(token)` plus the token's own `decimals()`. Read it per launch: `getLaunchedToken(token).pairToken` says which asset a given curve trades in. ## Which side is which | Quantity | Decimals | |---|---| | `quoteIn`, `minQuoteOut`, `phantomQuote`, `graduationThreshold`, escrow balances, distributor epochs | **the launch's quote asset** (6, 8 or 18 — table above) | | `launchFee` | **the fee token's** — `launchFeeToken()` is USDC, so 6, whatever the launch quotes in | | `minTokensOut`, `tokensIn`, `supply`, token balances, curve token reserves | **18** | So on a USDC launch `5e6` is five dollars and `5e18` is five tokens; on a WETH launch a `quoteIn` of `5e6` is 0.000000000005 ETH — dust. The 5 USDC launch fee reads back from `launchFee()` as `5000000` for every launch. ```ts const { pairToken } = await client.readContract({ address: FACTORY, abi: factoryAbi, functionName: "getLaunchedToken", args: [token] }); const quoteDecimals = await client.readContract({ address: pairToken, abi: erc20Abi, functionName: "decimals" }); const quoteIn = parseUnits("0.05", quoteDecimals); // never a hard-coded 6 ``` ## The native asset is a different thing Arc uses USDC for gas, and viem's chain definition describes that native asset as `{ symbol: "USDC", decimals: 18 }`. That is **not** the ERC-20 you trade with. They report the same balance through two different interfaces at two different scales. Reading the native balance and passing it as `quoteIn` is a 10^12 error. Always read the traded balance from the ERC-20 at `0x3600…0000` — and, for any other quote, from that quote's own contract. ## Never round-trip through a float `Number(raw) / 1e18` loses precision above about 2^53. On a real balance the loss is large enough to matter: ``` balance 714275814275814275814275815 via a float 714275814275814175605773926 lost 100208501889 wei ``` That is a hundred billion wei of dust that a "sell max" can never reach, because the amount submitted was never quite the amount held. Use `formatUnits` and `parseUnits`, which are exact inverses, and keep bigints end to end. ## sqrtPriceX96 carries the skew The graduated pool's `sqrtPriceX96` is derived from **raw amount ratios**, so it embeds the decimal gap between the quote side and the 18-decimal memecoin — 10^12 against USDC or EURC, 10^10 against cirBTC, none against WETH or XAUM. It is not a human price. Adjust for the launch's quote decimals before displaying anything derived from it. ## Currency ordering is by address, not by role A V4 `PoolKey` sorts its two currencies by raw address. The quote asset is `currency0` only when the memecoin's address happens to sort above it — for USDC (`0x36…`) that is most of the time, for WETH (`0x12…`) less often, and never reliably, because launch addresses are CREATE2 and effectively random. **Compute the ordering per launch.** Assuming the quote is always `currency0` will silently invert `zeroForOne` on some tokens and swap the wrong direction. ```ts const memecoinIsCurrency0 = token.toLowerCase() < pairToken.toLowerCase(); const currency0 = memecoinIsCurrency0 ? token : pairToken; const currency1 = memecoinIsCurrency0 ? pairToken : token; ``` --- # Fees and referrals ## Where fees come from Every trade pays a fee on the **quote leg**, in the quote asset, from the first trade onward. There are two components: | Component | Goes to | Set by | |---|---|---| | curve / hook fee | split between the protocol and the creator | the launch config and fee policy | | creator tax | entirely to the creator, never split | `TokenParams.creatorTaxBps` at launch | Both are frozen into the launch at creation. A later policy change by the owner affects only launches created after it, so an existing token's economics cannot be altered underneath its holders. On Arc mainnet the curve fee is **1%** of the quote leg, and after graduation the hook charges the same **1%** (the V4 pool's own LP fee is zero). The protocol keeps **30%** of that fee and the creator receives **70%**. Creator tax is **0–10%**, chosen at launch, all of it to the creator. Launching costs **5 USDC**. ## Referrals A referred trade is cheaper for the trader **and** pays the referrer: - the trader pays a discount on the standard fee — **0.9%** instead of 1%, - the referrer receives a share of what is paid — **20%** of that fee, taken off the top, - the pool books the remainder. Bindings are **permanent and never rewritten**. A user can bind themselves ahead of time with `referralRegistry.setReferrer(address)`, or a referrer can be passed to the four-argument `curve.buy` on their first trade. On the curve, an invalid referrer **reverts the buy**. In the pool, the hook is far more forgiving — a rejected binding just charges the standard fee rather than failing the swap. One security property worth knowing if you build a router: pool-phase `hookData` can only ever name the **referrer**, never the trader. The trader always comes from `msgSender()`. So nobody can bind a stranger's referrer with a dust swap. ## Claiming Everything owed to anyone accumulates in the **fee escrow**, and every claim is **pull-only** by `msg.sender` — you cannot claim on someone else's behalf. That is deliberate: it means a recipient who cannot receive a transfer (a blocklisted address, a reverting contract) can never block a sweep for everybody else. ```ts // One balance per asset: a wallet that earned on a USDC launch and a WETH launch claims twice. for (const currency of [USDC, WETH /* every quote your launches used */]) { const owed = await client.readContract({ address: FEE_ESCROW, abi: escrowAbi, functionName: "balanceOfToken", args: [account, currency], }); if (owed > 0n) { await walletClient.writeContract({ address: FEE_ESCROW, abi: escrowAbi, functionName: "claimToken", args: [currency], }); } } ``` `GET /v1/users/:address` → `claimable.creator.byCurrency` and `claimable.referral.byCurrency` list exactly which assets hold something for you, each with its decimals. > [!WARNING] > The escrow stores **one balance per (recipient, token)**. It does not distinguish creator > fees from referral fees — that split is attribution derived off-chain from events. So > `claimToken` withdraws **both at once**, and a UI showing them as two separately claimable > pots is lying about what the button does. Show the split as attribution; make one claim. Fees are paid in the launch's quote asset — an ERC-20 for every approved pair token — so `claimToken` is the path, with amounts in that asset's own decimals. The native `claim()` is unreachable unless a launch quotes in the native asset, which no approved pair token does. ## Pool-phase fees need a sweep first Curve fees accrue in the curve and are swept to the escrow by `curve.sweepFees()`. Pool fees accrue in the hook and are swept by `hook.sweepPoolFees(poolId, minOut)` — which may need to convert memecoin-denominated inventory back to the quote asset against the pool's own liquidity, which is why it takes a slippage bound. Referral accruals in the pool are settled with `hook.claimReferralFees(referrer, currency)`, which is **permissionless** — anyone can settle anyone's accrual into the escrow. The referrer then claims from the escrow as above. Two steps, not one. ## Holder rewards A launch can send its **creator side** of the fees to its holders instead of to the creator. On the create page this is **Fees go to: Holders**. Nothing else about the economics changes: the protocol still keeps its 30% of the 1% curve/hook fee, referrals still work the same way, and the creator tax is still charged — it just lands with the holders too. | | Fees go to: Creator | Fees go to: Holders | |---|---|---| | protocol share (30% of the fee) | protocol | protocol | | creator share (70% of the fee) | creator | **token holders** | | creator tax (0–10%, if set) | creator | **token holders** | | referral share | referrer | referrer | ### How it works The launchpad already lets any address be a launch's `creatorFeeRecipient` and claim from the escrow. Holder rewards point that field at a **FociRewardsDistributor** — a small contract per token, deployed from the rewards factory at an address that is predictable before the launch exists. So the launch transaction names the distributor as its fee recipient from block one, and the escrow credits it by address from the very first trade, whether or not the contract has been deployed yet. From there the flow is: 1. **Sweep.** Curve fees are swept from the curve into the escrow (the keeper does this for every live launch; pool fees are swept from the hook after graduation). This is the same step every launch goes through. 2. **Harvest.** `distributor.harvest()` pulls the distributor's escrow balance into its `unallocated` pot. Permissionless; the keeper runs it once at least about $1 of the quote asset is waiting. 3. **Snapshot.** Once a week, holder balances are snapshotted at the indexer's latest block and each holder's share is `balance / eligible supply`, rounded down. Protocol addresses that hold supply but can never claim are excluded from the supply and get no share: the launch's bonding curve, the Uniswap V4 PoolManager and the locker (the pool's liquidity is locked there forever — paying it would burn the money), the hook, the escrow, the factory and graduation executor, the launch-and-buy router, and the distributor itself. 4. **Publish.** The shares become a Merkle tree and the distributor **owner** publishes its root with `publishEpoch(root, total)`. This is the one step that is not automated: publishing a root is the only way to decide who gets paid, so it is signed from a hardware wallet by a person, not by the always-on keeper key. Epochs below about $1 of the quote asset are skipped and roll into the next week. 5. **Claim.** Holders claim their share against the root for **90 days**. Whatever is not claimed by then is rolled back into `unallocated` by `rollOver(epochId)` and funds a later epoch. Everything is checkable: the tree is built from public `Transfer` events, the exclusion list is on chain (`isExcluded`), and `leafFor(index, account, amount)` returns the exact leaf encoding, so anyone can rebuild an epoch from chain data and compare roots. > [!WARNING] > Choosing **Holders** is a one-way door. `creatorFeeRecipient` can only be moved by the > current recipient, and a distributor has no function that could ever call > `transferCreatorFeeRecipient` — so once a launch pays its holders, it pays them for the > life of the token. There is no way for the creator to reclaim the stream later. Holder rewards need an ERC-20 quote asset (every approved pair token on Arc is one) and the rewards factory deployed on the network; the create page only offers the option when both are true. ### Predicting the recipient The distributor's address is known **before the launch exists**, so the launch transaction can name it as `creatorFeeRecipient` directly — no second transaction, no window in which fees go to the wrong place. It is a CREATE2 address on the rewards factory, keyed by the creator, the launch's salt and the quote asset: ```ts // The salt you will pass as TokenParams.salt. Reuse it — the prediction is a function of it. const salt = keccak256(toBytes(crypto.randomUUID())); const distributor = await client.readContract({ address: REWARDS_FACTORY, abi: rewardsFactoryAbi, functionName: "predict", args: [creator, salt, pairToken], // creator = the account that will sign launchToken }); await walletClient.writeContract({ address: FACTORY, abi: factoryAbi, functionName: "launchToken", args: [{ ...params, salt, creatorFeeRecipient: distributor }, launchConfigId, pairToken], }); ``` Or let the API do it — `POST /v1/launch/predict` with `feesTo: "holders"` returns the same address as `distributor` alongside the predicted token and curve, and the create page uses exactly this: ```ts const p = await fetch(`${API}/v1/launch/predict`, { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${siwe}` }, body: JSON.stringify({ ...params, salt, feesTo: "holders" }), }).then((r) => r.json()); p.distributor; // → TokenParams.creatorFeeRecipient p.token; // the token address this launch will get ``` Three things have to line up, or `deploy` refuses and the fee stream sits unclaimed at the predicted address until they do: the **creator** must be the launch's `deployer` (the account that signs, or the account `launchAndBuy` launches for), the **salt** must be the launch's own `TokenParams.salt`, and the **quote asset** must be the launch's `pairToken`. Salts are namespaced per creator (`keccak256(abi.encode(creator, salt))`), so nobody can pre-empt your address with a launch of their own. Once the launch is indexed, the keeper calls `rewardsFactory.deploy(token, salt)`, which checks all three against the launchpad's record and puts the contract in place. `distributorOf(token)` is zero until then. The full surface — `predict`, `deploy`, `claim`, `harvest`, `rollOver` and the views — is in the [contracts reference](/documentation/contracts) under FociRewardsDistributorFactory and FociRewardsDistributor, with ABIs on the [ABI page](/documentation/abi). ### Claiming as a holder The portfolio page shows a **Holder fees** tile with what is claimable now and an estimate of what is still accruing toward the next epoch, and claims everything open in one transaction. If you are building your own client, the API hands you the proofs: ```ts const { claimable } = await fetch(`${API}/v1/users/${account}`).then((r) => r.json()); for (const t of claimable.holders.byToken) { // One open epoch per weekly distribution — batchClaim settles them all at once. const open = t.epochs.map((e) => e.calldata); await walletClient.writeContract({ address: t.distributor, abi: distributorAbi, functionName: "batchClaim", args: [ open.map((c) => BigInt(c.epochId)), account, open.map((c) => BigInt(c.amountRaw)), open.map((c) => BigInt(c.index)), open.map((c) => c.proof), ], }); } ``` `claim(epochId, account, amount, index, proof)` does the same for a single epoch. Anyone may submit a claim, but the proof fixes the recipient — a claim always pays `account`, never `msg.sender`. An address on the exclusion list cannot claim even with a valid proof, and each `(epoch, account)` pair claims once (`hasClaimed`). Claimed amounts arrive as the launch's quote asset (`claimable.holders.byToken[].pairToken` names it, decimals included), straight from the distributor — not through the escrow, so they are separate from any creator or referral balance you also hold. ### What the numbers mean - **Claimable** is the sum of your leaves in epochs whose 90-day window is still open. - **Pending** is an estimate: your share of what the token has accrued since the last epoch (curve fees not yet swept, pool fees still in the hook, harvested-but-unpublished `unallocated`), computed from your current balance. It changes with every trade and every transfer until the snapshot, so treat it as a preview, not a promise. --- # Contracts Every deployed contract: functions, events and errors. ## FociLaunchFactory `0xa392D6eca5242715517eeCd43406aeD19424FAC0` ### Functions an application calls ### `canLaunch(address)` `0x58373f04` · `view` · view Whether `launcher` may launch right now: true while the public gate is open, and true for whitelisted addresses while it is closed. The same predicate `launchToken` enforces on its caller, exposed so routers like FociLaunchAndBuy can hold their own callers to this single list instead of maintaining a second one. `launchEnabled` is left FALSE by deployment. Always check this before showing a launch form. **Parameters** | Name | Type | Description | |---|---|---| | `launcher` | `address` | | **Returns** | Name | Type | Description | |---|---|---| | `arg0` | `bool` | | ### `createGraduatedPool(address)` `0x2f53ef2f` · `nonpayable` · Permissionless and retryable. Initializes the V4 pool with the swept reserves, mints a full-range position directly to the locker, and registers the pool with the meme hook. The curve already holds the pool's quote asset, so this seeds with exactly what it swept and needs no slippage bound. Permissionless and retryable: a launch stays in Swept until a seed succeeds, so a transient failure can never strand reserves. Seeds the V4 pool and locks the position. Like `graduate` this normally runs inside the crossing buy — but it costs roughly 900k gas on its own, and under EIP-150's 63/64 rule a buy sized by a naive `estimateGas` starves it. When that happens the curve emits `AutoSeedFailed` and the launch sits in phase 1 with no tradeable venue until someone calls this. Run a keeper on that event. Send an explicit gas limit; estimation is reliable here because there is no try/catch to hide the cost. **Parameters** | Name | Type | Description | |---|---|---| | `token` | `address` | | **Returns** | Name | Type | Description | |---|---|---| | `positionId` | `uint256` | | **Reverts** - `WrongGraduationPhase` — not in the Swept phase - `GraduationSeedNotViable` ### `getLaunchConfig(uint256)` `0x1cad862d` · `view` · view Returns one token launch configuration. **Parameters** | Name | Type | Description | |---|---|---| | `id` | `uint256` | | **Returns** | Name | Type | Description | |---|---|---| | `arg0` | `(uint256,uint256,uint256,uint256,uint24,int24,bool)` | | ### `getLaunchedToken(address)` `0x3cf28b5a` · `view` · view Returns the immutable record for a token created by this factory. Returns a ZEROED struct for an unknown token rather than reverting — check `.exists`. This is also where you get the per-launch `curve` address, since curves have no fixed deployment. **Parameters** | Name | Type | Description | |---|---|---| | `token` | `address` | | **Returns** | Name | Type | Description | |---|---|---| | `arg0` | `(address,address,address,address,address,uint256,uint24,int24,uint16,uint8,uint256,uint256,uint256,bool)` | | ### `graduate(address)` `0xff6d8d05` · `nonpayable` · Permissionless — anyone may call it. Sweeps the curve's remaining quote and token reserves into this factory and halts curve trading. Purely internal to the curve's own balances, so it is safe for the curve to call this automatically the instant a buy crosses the graduation threshold. Normally runs automatically inside the buy that crosses the threshold. Call it manually only when that inner attempt failed, which the curve reports by emitting `AutoGraduationFailed`. **Parameters** | Name | Type | Description | |---|---|---| | `token` | `address` | | **Reverts** - `WrongGraduationPhase` — already swept - `NotReadyToGraduate` — the curve is not finished ### `launchFee()` `0xcf3cf573` · `view` · view **Returns** | Name | Type | Description | |---|---|---| | `arg0` | `uint256` | | ### `launchFeeToken()` `0xbd03b5fc` · `view` · view A flat charge on creating a launch, in `launchFeeToken`. Spam friction rather than revenue. Zero disables it outright: the payment path is skipped entirely, so nothing is pulled and no approval is needed while it is off. ERC-20 rather than native even where a chain's native asset is the same asset: the atomic launch router funds itself through `transferFrom`, and a native fee would make one call carry both an approval and attached value for what is economically one token. **Returns** | Name | Type | Description | |---|---|---| | `arg0` | `address` | | ### `launchToken((string,string,string,string,(string,string,string,string,string),address,uint16,bytes32,bytes32),uint256,address)` `0xbc9bc035` · `nonpayable` · Permissionless, but gated by `canLaunch(msg.sender)` — check it before offering a launch UI. Deploys a bonding curve and its launch token, wires them together, and records the launch. Trading starts immediately on the curve; the graduation pool's pairToken is fixed here, chosen by the caller. **Approve first:** `factory` for launchFee() of launchFeeToken() (USDC, whatever the launch quotes in). Use this when there is NO opening buy. The atomic router reverts on a zero `quoteIn`, so it cannot be used as a plain deployer. Note the approval target differs from the router path: the factory pulls the fee from `msg.sender` itself. If `launchFee()` is zero, no approval is needed at all. A token launched this way starts with the creator holding none of its supply. **Parameters** | Name | Type | Description | |---|---|---| | `params` | `(string,string,string,string,(string,string,string,string,string),address,uint16,bytes32,bytes32)` | | | `launchConfigId` | `uint256` | | | `pairToken` | `address` | | **Returns** | Name | Type | Description | |---|---|---| | `token` | `address` | | | `curve` | `address` | | **Reverts** - `NotWhitelisted` — `launchEnabled` is false and you are not a whitelisted launcher - `InvalidLaunchConfigId` — no such config - `InvalidTokenParams` — empty name or symbol - `CreatorTaxTooHigh` — `creatorTaxBps` above `maxCreatorTaxBps()` - `PairTokenNotApproved` — quote asset not approved by the owner - `LaunchEconomicsMismatch(expected,actual)` — terms moved since you read `previewLaunchEconomics` - `FailedDeployment` — you already used this `salt`; salts are namespaced per account - ERC-20 revert on the fee transfer — insufficient allowance **to the factory** ### `pairTokenEconomics(address)` `0x31082134` · `view` · view Phantom reserve and graduation threshold in the QUOTE ASSET'S OWN DECIMALS (6 for USDC/EURC, 8 for cirBTC, 18 for WETH/XAUM). **Parameters** | Name | Type | Description | |---|---|---| | `pairToken` | `address` | | **Returns** | Name | Type | Description | |---|---|---| | `phantomQuote` | `uint256` | | | `graduationThreshold` | `uint256` | | | `decimals` | `uint8` | | ### `previewLaunchEconomics(uint256,address)` `0xf718b78c` · `view` · view Returns the economics digest a launch of `launchConfigId` in `pairToken` would produce right now, for a creator to pass back as TokenParams.expectedEconomics. Reading the digest and launching in separate transactions still leaves the terms free to move in between; the pin is what makes that movement revert instead of silently repricing the launch. Returns the digest to put in `TokenParams.expectedEconomics`. Fetch it in the same flow as the submit — it covers the launch fee, so an owner changing `setLaunchFee` between your quote and your signature invalidates it. Passing `bytes32(0)` waives the check entirely, which means accepting whatever terms are live when the transaction lands. **Parameters** | Name | Type | Description | |---|---|---| | `launchConfigId` | `uint256` | | | `pairToken` | `address` | | **Returns** | Name | Type | Description | |---|---|---| | `arg0` | `bytes32` | | ### Other functions | Signature | Selector | Mutability | |---|---|---| | `acceptOwnership()` | `0x79ba5097` | nonpayable | | `addLaunchConfig((uint256,uint256,uint256,uint256,uint24,int24,bool))` | `0x0e5b0aae` | nonpayable | | `approvedPairTokens(address)` | `0x9831705e` | view | | `cancelCreatorFeeRecipientChange(address)` | `0x6e47a188` | nonpayable | | `CREATOR_FEE_RECIPIENT_EXECUTION_WINDOW()` | `0x02d4753d` | view | | `CREATOR_FEE_RECIPIENT_TIMELOCK()` | `0x5a83b00a` | view | | `executeCreatorFeeRecipientChange(address)` | `0x3d3d2d58` | nonpayable | | `feeEscrow()` | `0xc4b7de97` | view | | `forceSweptGraduation(address)` | `0x7aed273e` | nonpayable | | `getLaunchFeePolicy(address)` | `0x470ef5fc` | view | | `GRADUATION_RESCUE_DELAY()` | `0x2d1250b8` | view | | `graduationExecutor()` | `0xcc6d7a39` | view | | `graduationGuard()` | `0x496aa100` | view | | `launchConfigCount()` | `0xae72d871` | view | | `launchDeployer()` | `0x858f5964` | view | | `launchEnabled()` | `0x236a4afb` | view | | `launchForwarder()` | `0x9b924452` | view | | `launchTokenFor((string,string,string,string,(string,string,string,string,string),address,uint16,bytes32,bytes32),uint256,address,address)` | `0x266101cb` | nonpayable | | `locker()` | `0xd7b96d4e` | view | | `maxCreatorTaxBps()` | `0xf325a5fb` | view | | `memeHook()` | `0x6651812c` | view | | `owner()` | `0x8da5cb5b` | view | | `pendingCreatorFeeRecipient(address)` | `0x9beacf4a` | view | | `pendingOwner()` | `0xe30c3978` | view | | `permit2()` | `0x12261ee7` | view | | `poolManager()` | `0xdc4c90d3` | view | | `positionManager()` | `0x791b98bc` | view | | `renounceOwnership()` | `0x715018a6` | pure | | `rescueCurveFees(address)` | `0x189eb0f5` | nonpayable | | `rescueSweptGraduation(address,address)` | `0xdbcb9c76` | nonpayable | | `setCreatorFeeRecipient(address,address)` | `0xe102c9aa` | nonpayable | | `setGraduationExecutor(address)` | `0xfbec2d8b` | nonpayable | | `setLaunchDeployer(address)` | `0x3a9391e8` | nonpayable | | `setLaunchEnabled(bool)` | `0xf56f05b2` | nonpayable | | `setLaunchFee(address,uint256)` | `0x6e51833f` | nonpayable | | `setLaunchForwarder(address)` | `0x767b7c16` | nonpayable | | `setMaxCreatorTaxBps(uint256)` | `0x2260aead` | nonpayable | | `setPairTokenApproved(address,bool)` | `0x8763e3dc` | nonpayable | | `setPairTokenEconomics(address,uint256,uint256,uint8)` | `0x092c08bd` | nonpayable | | `setWhitelistedLauncher(address,bool)` | `0x366f0f3e` | nonpayable | | `transferCreatorFeeRecipient(address,address)` | `0x2931861b` | nonpayable | | `transferOwnership(address)` | `0xf2fde38b` | nonpayable | | `updateLaunchConfig(uint256,(uint256,uint256,uint256,uint256,uint24,int24,bool))` | `0xe73e334a` | nonpayable | | `whitelistedLaunchers(address)` | `0xda3eda65` | view | ### Events | Event | topic0 | |---|---| | `CreatorFeeRecipientChangeCancelled(address,address)` | `0xbe2de91c1cbef653c760573fff8355c0c851d35ed2a898342b4db556301cccf4` | | `CreatorFeeRecipientChangeProposed(address,address,address,uint256,uint256)` | `0x7f119e44c84a715429bee60d30ad2e14afdef6c60bb1a7eaa01290ecf6d1b2e5` | | `CreatorFeeRecipientUpdated(address,address,address)` | `0x308c390ed1ab5873392818e036cabdf408bc8ad042fbaead3108954ff75ba980` | | `GraduationExecutorSet(address)` | `0xac04674474e93058fae25e6df5dd94f57cdcacfe560a182a2eefc8c6006fbf6f` | | `GraduationTokensPermanentlyLocked(address,uint256)` | `0xa0a18f5bf205becee8b268d7cf69addab8548ae8ef361791464cf0e0e17c1361` | | `LaunchConfigAdded(uint256)` | `0xedd96c570c6e5ef9add0378e59df53579a283889dc5dab6440ef6eca2ee6c8ce` | | `LaunchConfigUpdated(uint256)` | `0x2f8ba78ae68cfd0c82c7756c540eaf4eead3341aef9ccebcb91d546bff10d62b` | | `LaunchDeployerSet(address)` | `0xd5ea7aa3e328a0594dcf6914cd9e5369779efaa194ee4dd4c5afcad4f4ebbb0c` | | `LaunchEnabledUpdated(bool)` | `0x4f1ea5016c51c2f82324e00e9b8a4a95ee5aeaa10c653dabaec5f1bc9047ba0b` | | `LaunchFeeUpdated(address,uint256)` | `0xd0766d3f1431146228fe8edef25f27842a1669c91d46e4af1b73405b354489a5` | | `LaunchForceSwept(address)` | `0x52c1a28345695afc7f6b7629133124dec5d61ee745affd65e4fd2a776bc05840` | | `LaunchForwarderSet(address)` | `0x56b32d3633fed72f97c4df44a78b5fa04f1d662d4bddebcd8a9b216d26d093ad` | | `LaunchGraduationRescued(address,address,uint256,uint256)` | `0x7017304fdd491394686dce984eac721f0be1a22228346210f16694772bde44ca` | | `LaunchSwept(address,uint256,uint256)` | `0xcdb72f157fd3666758a6ce201387ffb52038c7562e4fff352828da1096c4b6b4` | | `MaxCreatorTaxUpdated(uint256)` | `0x3e99ceb3e222d2214d53dacca902810db845f156f78152fdc076be628c4e9a40` | | `OwnershipTransferred(address,address)` | `0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0` | | `OwnershipTransferStarted(address,address)` | `0x38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e22700` | | `PairTokenApprovalUpdated(address,bool)` | `0x060d1992d069dc524985f328329aae36102a017c59733c5c91fc0691ee0703b6` | | `PairTokenEconomicsUpdated(address,uint256,uint256,uint8)` | `0x67d517ee0e305d608b8410ddef27bbd2ed964d843d9b936e84ea2ad1bd65e5d1` | | `PoolGraduated(address,uint256,uint256,uint256)` | `0x0a44ef75df69c534f43cd6c1aa3ef8983065fe5fe79ef9e79f6494e6f258c259` | | `TokenLaunched(address,address,address,address,uint256,uint256)` | `0x8d4aad4953d0ca700d468f3753aa14432d1b35b43ec6409f051fb6aa43a89607` | | `WhitelistedLauncherUpdated(address,bool)` | `0xef2b562a67f01ed4b7c4265ec09b539039c6d5dd7e752191d3940508c3dc0068` | ### Errors | Error | Selector | |---|---| | `AlreadySet()` | `0xa741a045` | | `CombinedFeeTooHigh()` | `0x49e55bcb` | | `CoreLpFeeMustBeZero()` | `0x85258712` | | `CreatorTaxTooHigh()` | `0x9ad465dc` | | `CurveFeeTooHigh()` | `0x4e222a24` | | `CurveNotQuotable()` | `0x95e32dab` | | `FeeTransferFailed()` | `0x4033e4e3` | | `GraduationExecutorNotSet()` | `0xd43cabc3` | | `GraduationRescueTooEarly(uint256)` | `0xbdcd75af` | | `GraduationSeedNotViable()` | `0x2c37d0eb` | | `GraduationStillViable()` | `0x6d3bcfe5` | | `InexactTransfer(address,uint256,uint256)` | `0x495a9962` | | `InvalidBasisPoints()` | `0x800c7e91` | | `InvalidGraduationThreshold()` | `0x2bb8bdd6` | | `InvalidLaunchConfigId()` | `0x68b42c59` | | `InvalidPhantomQuote()` | `0x2b7ad4f8` | | `InvalidTickSpacing()` | `0x270815a0` | | `InvalidTokenParams()` | `0x374852ca` | | `LaunchConfigDisabled()` | `0xa8b63076` | | `LaunchDependenciesNotWired()` | `0x1de25df3` | | `LaunchDeployerNotSet()` | `0x57332dcf` | | `LaunchEconomicsMismatch(bytes32,bytes32)` | `0xecb27319` | | `LaunchFeeTokenNotSet()` | `0x52660db0` | | `NoPendingChange()` | `0xa3fef2f8` | | `NotCreatorFeeRecipient()` | `0xb9f93944` | | `NothingToGraduate()` | `0xc2074c46` | | `NotLaunchForwarder()` | `0xea9eaa96` | | `NotReadyToGraduate()` | `0xffa32558` | | `NotWhitelisted()` | `0x584a7938` | | `OwnableInvalidOwner(address)` | `0x1e4fbdf7` | | `OwnableUnauthorizedAccount(address)` | `0x118cdaa7` | | `OwnershipCannotBeRenounced()` | `0x2fab92ca` | | `PairTokenDecimalsMismatch(uint8,uint8)` | `0x4e3de34f` | | `PairTokenDecimalsUnavailable()` | `0xe43c14ca` | | `PairTokenEconomicsInvalid()` | `0x764c63c8` | | `PairTokenNotApproved()` | `0x49285dfb` | | `PairTokenValidationFailed()` | `0x26fbfa60` | | `ReentrancyGuardReentrantCall()` | `0x3ee5aeb5` | | `SafeERC20FailedOperation(address)` | `0x5274afe7` | | `SqrtPriceOutOfBounds()` | `0x582157bb` | | `SupplyTooHigh()` | `0xacb9fa2b` | | `SupplyTooLow()` | `0xc0b4e373` | | `TimelockExpired(uint256)` | `0xb79d40e8` | | `TimelockNotElapsed(uint256)` | `0x810c4f2a` | | `TokenNotFound()` | `0xcbdb7b30` | | `UnsupportedPrice()` | `0xdd737e7c` | | `WrongGraduationPhase()` | `0x9465dbd4` | | `ZeroAddress()` | `0xd92e233d` | | `ZeroAmount()` | `0x1f2a2005` | --- ## FociBondingCurve Deployed **once per launch** — no fixed address. Read it from `factory.getLaunchedToken(token)`. ### Functions an application calls ### `buy(uint256,uint256,address)` `0x59a87bc1` · `payable` · Permissionless. Reverts once the launch has graduated. Buys the launch token with this launch's quote asset. The fee is always taken from the quote leg, so this curve never holds a memecoin-denominated fee. `quoteIn` must equal `msg.value` for a native launch, and must be accompanied by no value at all for an ERC-20 launch. The credited amount for an ERC-20 is the observed balance delta rather than the requested amount, so a fee-on-transfer quote asset cannot make the curve promise reserves it never received. A buy that would take the curve past its reserved allocation is filled only up to that allocation, charged for what it actually received, and refunded the difference. It is deliberately not rejected: the last buy of a launch is the one most likely to be sized against a state someone else has already moved, and reverting would let anyone grief it by slipping a small buy in ahead. Partial fills reinterpret `minTokensOut` as a bound on price rather than on quantity, since a caller who spends less than they offered cannot expect the whole quantity they asked for. The requirement is that the price paid is no worse than the price implied by the caller's own arguments, and when nothing is clamped it reduces exactly to `tokensOut >= minTokensOut`. **Approve first:** `curve` for quoteIn of the launch's quote asset (getLaunchedToken(token).pairToken), in its own decimals. `minTokensOut` is a PRICE bound, not a quantity bound. A buy that would exceed the remaining allocation is clamped rather than reverted, and the remainder is refunded to `msg.sender` in the same transaction — so you can safely overshoot the graduation threshold. **Parameters** | Name | Type | Description | |---|---|---| | `quoteIn` | `uint256` | | | `minTokensOut` | `uint256` | | | `recipient` | `address` | | **Returns** | Name | Type | Description | |---|---|---| | `tokensOut` | `uint256` | | **Reverts** - `CurveGraduated` — the curve is closed; trade the V4 pool instead - `SlippageExceeded(tokensOut,minTokensOut)` — the effective price broke your bound - `UnexpectedNativeValue` — sent ETH on an ERC-20-quoted launch; `msg.value` must be 0 - `ZeroAmount` — the transfer delivered nothing ### `buy(uint256,uint256,address,address)` `0x82b2a559` · `payable` · Permissionless. Reverts once the launch has graduated. Buys with a referrer attached. The referrer is only consulted the first time `recipient` trades with one; after that the binding in the registry stands and this argument is ignored, so passing a fresh referrer cannot poach an existing relationship. An unusable referrer (the recipient themselves, or someone the recipient already refers) reverts rather than being dropped, because it is an argument the caller chose to supply and silently charging them the undiscounted fee would be worse. **Approve first:** `curve` for quoteIn of the launch's quote asset (getLaunchedToken(token).pairToken), in its own decimals. As above, with a referrer bound to `recipient` (not to `msg.sender`). The binding is written only on that address's first referred trade and is permanent thereafter. An unusable referrer REVERTS THE WHOLE BUY — `SelfReferral` and `ReciprocalReferral` are not swallowed — so resolve and validate a referrer before putting it in the call. **Parameters** | Name | Type | Description | |---|---|---| | `quoteIn` | `uint256` | | | `minTokensOut` | `uint256` | | | `recipient` | `address` | | | `referrer` | `address` | The account to credit for this recipient's trades, or the zero address to trade under whatever binding already exists. | **Returns** | Name | Type | Description | |---|---|---| | `tokensOut` | `uint256` | | **Reverts** - `SelfReferral` — referrer is the recipient - `ReciprocalReferral` — the recipient already refers that address - everything the 3-argument overload throws ### `getReserves()` `0x0902f1ac` · `view` · view Returns the curve's current tradeable reserves, excluding fees pending sweep. Returns the VIRTUAL reserves — `quoteReserve` includes the phantom reserve. For the amount actually raised use `realQuoteReserve()`. **Returns** | Name | Type | Description | |---|---|---| | `quoteReserve_` | `uint256` | | | `tokenReserve_` | `uint256` | | ### `graduated()` `0xe7c2b772` · `view` · view **Returns** | Name | Type | Description | |---|---|---| | `arg0` | `bool` | | ### `quoteReserve()` `0x9da771f4` · `view` · view Tradeable quote reserve only, matching IFociBondingCurve. **Returns** | Name | Type | Description | |---|---|---| | `quoteReserve_` | `uint256` | | ### `readyToGraduate()` `0xc68360a5` · `view` · view True once the curve's sellable allocation has been bought out. Equivalent to the real quote reserve reaching `graduationThreshold`, since the reserved balance is derived from that same point. Expressed against the token side because that is the one a buy cannot overshoot: the quote side is a floor that a large trade could sail past, while the token side is a hard stop the curve refuses to cross. True when the sellable allocation reaches zero, which is by construction the same point as the real quote reserve reaching the graduation threshold. **Returns** | Name | Type | Description | |---|---|---| | `arg0` | `bool` | | ### `realQuoteReserve()` `0x4f1f58fd` · `view` · view Returns physically held tradeable quote asset, excluding virtual liquidity and balances already earmarked as fees or creator tax. **Returns** | Name | Type | Description | |---|---|---| | `arg0` | `uint256` | | ### `sell(uint256,uint256,address)` `0xd04c6983` · `nonpayable` · Permissionless. Closes the instant `readyToGraduate()` is true. Sells the launch token back to the curve for the quote asset. The fee is taken from the quote output, so it is always quote-denominated here too. Closed once the sellable allocation is exhausted, not merely once `graduated` is set. `_tryAutoGraduate` swallows a failed graduation so a problem there cannot take the crossing buy down with it, which leaves a window where the curve is ready but the flag is still false. `buy` already refuses that state through its own `sellable == 0` check, and `sell` has to match: `graduate` hands the pool whatever `trackedTokens` holds, so a sell landing in the window would put tokens back on the curve and take quote off it, and the pool would then be seeded deeper and cheaper than the reserved allocation fixes it at. The deterministic graduation price only holds if the window is closed on both sides. This cannot strand a holder. `graduate` is permissionless, so anyone blocked here can settle the launch themselves in the same transaction and trade the V4 pool instead. **Approve first:** `curve` for tokensIn of the MEMECOIN (18 decimals). Unlike buy, `minQuoteOut` is a strict QUANTITY bound. The sell side shuts the moment the allocation is exhausted — before the `graduated` flag is even set — so a sell can start reverting with `CurveGraduated` while the UI still shows a live curve. Approve the CURVE for the memecoin, not the factory. **Parameters** | Name | Type | Description | |---|---|---| | `tokensIn` | `uint256` | | | `minQuoteOut` | `uint256` | | | `recipient` | `address` | | **Returns** | Name | Type | Description | |---|---|---| | `quoteOut` | `uint256` | | **Reverts** - `CurveGraduated` — the allocation is exhausted or the curve has graduated - `SlippageExceeded(quoteOut,minQuoteOut)` - `ZeroAmount` ### `sellableTokens()` `0x808bcddc` · `view` · view Tokens still available to buy before the curve graduates. **Returns** | Name | Type | Description | |---|---|---| | `arg0` | `uint256` | | ### Other functions | Signature | Selector | Mutability | |---|---|---| | `creatorTaxBalance()` | `0xdb2bd533` | view | | `creatorTaxBps()` | `0xc1bb8901` | view | | `deployer()` | `0xd5f39488` | view | | `factory()` | `0xc45a0155` | view | | `feeBps()` | `0x24a9d853` | view | | `feeEscrow()` | `0xc4b7de97` | view | | `feePolicy()` | `0x82589038` | view | | `graduate(address)` | `0xff6d8d05` | nonpayable | | `graduationThreshold()` | `0x8b0bc501` | view | | `initialize(address)` | `0xc4d66de8` | nonpayable | | `isNativeQuote()` | `0xdc08e094` | view | | `launchedAt()` | `0xbf56b371` | view | | `launchSupply()` | `0x3f7ed6b7` | view | | `maxInternalPriceImpactBps()` | `0x90addc1e` | view | | `pairToken()` | `0x3de35b79` | view | | `phantomQuote()` | `0xc57eadfc` | view | | `protocolFeeRecipient()` | `0x64df049e` | view | | `protocolFeeShareBps()` | `0x9040f866` | view | | `quoteFeeBalance()` | `0xed479c47` | view | | `referralDiscountBps()` | `0x30ab6943` | view | | `referralRegistry()` | `0x4e627e62` | view | | `referralShareBps()` | `0x47c9bc2d` | view | | `rescueFees()` | `0x52920587` | nonpayable | | `reservedTokens()` | `0x15a55347` | view | | `setCreatorFeeRecipient(address)` | `0x7b04ea62` | nonpayable | | `sweepFees()` | `0xd113b95c` | nonpayable | | `token()` | `0xfc0c546a` | view | | `tokenReserve()` | `0xcbcb3171` | view | | `trackedQuote()` | `0xca52b0b7` | view | | `trackedTokens()` | `0x4c37ef23` | view | ### Events | Event | topic0 | |---|---| | `AutoGraduationFailed(address,uint256)` | `0xe2cd2f31ebc05ec28640102987f4c8fc5f20e269e1b3aa82577f3f2f0e35c7c6` | | `AutoSeedFailed(address,uint256)` | `0x2cbe77dadc7f8418071409bebfd71778263eecb998af52aa5c9e27b995a71676` | | `CreatorFeeRecipientUpdated(address,address)` | `0x2cc664e1ac1e2d05c0d4637bb63ec8189113b6ac39276be8977e26216a8cdd19` | | `CurveBuy(address,address,uint256,uint256,uint256,uint256)` | `0xec36bf571f136799e8dc0b0b8bea4b04d8bd3d43de838aab0d5fc21d4cbfc455` | | `CurveBuyRefunded(address,uint256)` | `0xa69e8258ccc7b9bbb70ab953fc2d1062b4ee28b8ca827534097e1732e87b0262` | | `CurveCompleted(address,uint256,uint256)` | `0xf8d37a90738ae063b8b8058b66f5880cf3cf7ab0c5d4fa78219696591dfbfb67` | | `CurveSell(address,address,uint256,uint256,uint256,uint256)` | `0x8113d738abdcb6b38357e9d53a54a7157861a09031b453651f0fe7fe151f59df` | | `FeesRescued(address,address,uint256,uint256)` | `0x6460dc5c867a0678a8bcc5e64f629fae539901c53a4a8b42fe21d7a6c5e6437d` | | `FeesSwept(uint256,uint256)` | `0xaf739f46ca7a23c9f259838ec2c5249acf4e1cf9fe68a46f77c3dfa452eda605` | | `Initialized(address)` | `0x908408e307fc569b417f6cbec5d5a06f44a0a505ac0479b47d421a4b2fd6a1e6` | | `ReferralFeePaid(address,address,uint256)` | `0xde9bddf476dde28b26de9d0b38bb9811ebb9d4945cd0c7feadd215c28fe09717` | ### Errors | Error | Selector | |---|---| | `AlreadyGraduated()` | `0xe6a0d45f` | | `AlreadyInitialized()` | `0x0dc149f0` | | `CurveGraduated()` | `0x025ac17e` | | `InsufficientInputAmount()` | `0x098fb561` | | `InsufficientLiquidity()` | `0xbb55fd27` | | `InsufficientOutputAmount()` | `0x42301c23` | | `InvalidFeePolicy()` | `0x7a34030f` | | `InvalidLaunchEconomics()` | `0xbc0ecfe3` | | `NativeValueMismatch(uint256,uint256)` | `0xbc760cfe` | | `NotFactory()` | `0x32cc7236` | | `NotFeeSweepOperator()` | `0x8d42130c` | | `NotInitialized()` | `0x87138d5c` | | `NotReadyToGraduate()` | `0xffa32558` | | `ReentrancyGuardReentrantCall()` | `0x3ee5aeb5` | | `SafeERC20FailedOperation(address)` | `0x5274afe7` | | `SlippageExceeded(uint256,uint256)` | `0x71c4efed` | | `TransferFailed()` | `0x90b8ec18` | | `UnexpectedNativeValue()` | `0xe0aeda7d` | | `ZeroAddress()` | `0xd92e233d` | | `ZeroAmount()` | `0x1f2a2005` | --- ## FociLauncherToken Deployed **once per launch** — no fixed address. Read it from `factory.getLaunchedToken(token)`. ### Other functions | Signature | Selector | Mutability | |---|---|---| | `allowance(address,address)` | `0xdd62ed3e` | view | | `approve(address,uint256)` | `0x095ea7b3` | nonpayable | | `balanceOf(address)` | `0x70a08231` | view | | `burn(uint256)` | `0x42966c68` | nonpayable | | `burnFrom(address,uint256)` | `0x79cc6790` | nonpayable | | `curve()` | `0x7165485d` | view | | `decimals()` | `0x313ce567` | view | | `deployer()` | `0xd5f39488` | view | | `description()` | `0x7284e416` | view | | `getTokenInfo()` | `0xabb1dc44` | view | | `launchFactory()` | `0x536dac9b` | view | | `logo()` | `0xfb7f21eb` | view | | `name()` | `0x06fdde03` | view | | `socials()` | `0x53cd512a` | view | | `symbol()` | `0x95d89b41` | view | | `totalSupply()` | `0x18160ddd` | view | | `transfer(address,uint256)` | `0xa9059cbb` | nonpayable | | `transferFrom(address,address,uint256)` | `0x23b872dd` | nonpayable | ### Events | Event | topic0 | |---|---| | `Approval(address,address,uint256)` | `0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925` | | `Transfer(address,address,uint256)` | `0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef` | ### Errors | Error | Selector | |---|---| | `ERC20InsufficientAllowance(address,uint256,uint256)` | `0xfb8f41b2` | | `ERC20InsufficientBalance(address,uint256,uint256)` | `0xe450d38c` | | `ERC20InvalidApprover(address)` | `0xe602df05` | | `ERC20InvalidReceiver(address)` | `0xec442f05` | | `ERC20InvalidSender(address)` | `0x96c6fd1e` | | `ERC20InvalidSpender(address)` | `0x94280d62` | | `ZeroAddress()` | `0xd92e233d` | --- ## FociLaunchAndBuy `0x5C5c202271E1300bD5Ce43A4F5C1cEA8efd57B63` ### Functions an application calls ### `launchAndBuy((string,string,string,string,(string,string,string,string,string),address,uint16,bytes32,bytes32),uint256,address,uint256,uint256,address)` `0x32b6091a` · `payable` · Permissionless, but `factory.canLaunch(msg.sender)` must hold — routing through the router does not widen the gate. Launches a token and immediately buys `quoteIn` of its curve for `recipient`, both in this transaction. A native launch carries the opening buy as `msg.value`. An ERC-20 launch carries no value at all and the buy is pulled from the caller, who must have approved this contract for `quoteIn` first. **Approve first:** `launchAndBuy` for launchFee() in the fee token + quoteIn in the quote — one approval for the sum only when both are USDC, otherwise one per asset of launchFeeToken() (USDC) for the fee AND the launch's pairToken for the opening buy. Deploys the token and performs the creator's opening buy in one transaction. Two things differ from the direct path: `creatorFeeRecipient` may NOT be zero here (the direct path defaults it to the caller), and `quoteIn` may not be zero — the router exists to buy. GAS: if the opening buy crosses the graduation threshold the curve tries to seed the V4 pool inside this same transaction, and `eth_estimateGas` cannot size that — the seed is a best-effort try/catch, so a simulation in which it fails still succeeds overall and returns a limit too small for it to work. Send an explicit generous gas limit. **Parameters** | Name | Type | Description | |---|---|---| | `params` | `(string,string,string,string,(string,string,string,string,string),address,uint16,bytes32,bytes32)` | Launch parameters, forwarded to the factory untouched. Set `creatorFeeRecipient` to the wallet that should earn the launch's fees, and `expectedEconomics` to the value `previewLaunchEconomics` returned, which still pins the terms as it would on a direct launch. | | `launchConfigId` | `uint256` | Factory launch config to launch against. | | `pairToken` | `address` | Quote asset, or the zero address for a native launch. | | `quoteIn` | `uint256` | Amount of the quote asset to spend on the opening buy. An amount past what the curve can sell is clamped by the curve and the remainder comes back to the caller. | | `minTokensOut` | `uint256` | Slippage bound on the opening buy. The curve prices a clamped fill against this too, so a buy sized to take the whole allocation can still set a meaningful floor. | | `recipient` | `address` | Receives the purchased tokens. | **Returns** | Name | Type | Description | |---|---|---| | `token` | `address` | | | `curve` | `address` | | | `tokensOut` | `uint256` | | **Reverts** - `NotApprovedLauncher` — the factory's launch gate is closed for you - `ZeroAddress` — `recipient` or `params.creatorFeeRecipient` is zero - `ZeroAmount` — `quoteIn` is zero; use `factory.launchToken` instead - everything `launchToken` throws, plus an allowance failure against **the router** ### Other functions | Signature | Selector | Mutability | |---|---|---| | `acceptOwnership()` | `0x79ba5097` | nonpayable | | `factory()` | `0xc45a0155` | view | | `owner()` | `0x8da5cb5b` | view | | `pendingOwner()` | `0xe30c3978` | view | | `renounceOwnership()` | `0x715018a6` | nonpayable | | `rescue(address,address)` | `0x4fdf5d1d` | nonpayable | | `transferOwnership(address)` | `0xf2fde38b` | nonpayable | ### Events | Event | topic0 | |---|---| | `Launched(address,address,address,address,uint256,uint256)` | `0xdcacba5e347ae7abd91cb519eb877af8fa7774e347b85dd3ddcd24a2ba8cdf37` | | `OwnershipTransferred(address,address)` | `0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0` | | `OwnershipTransferStarted(address,address)` | `0x38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e22700` | | `Rescued(address,address,uint256)` | `0x3af790fafda720819b2fc6e15090606e81154e0ac9a92d38ecad006d99d20ecc` | ### Errors | Error | Selector | |---|---| | `NativeValueMismatch(uint256,uint256)` | `0xbc760cfe` | | `NotApprovedLauncher()` | `0x502ba015` | | `OwnableInvalidOwner(address)` | `0x1e4fbdf7` | | `OwnableUnauthorizedAccount(address)` | `0x118cdaa7` | | `ReentrancyGuardReentrantCall()` | `0x3ee5aeb5` | | `RefundFailed()` | `0xf0c49d44` | | `SafeERC20FailedOperation(address)` | `0x5274afe7` | | `ZeroAddress()` | `0xd92e233d` | | `ZeroAmount()` | `0x1f2a2005` | --- ## FociMemeHook `0xF847790B6fA5DA300BB3f56f10d743e71E98e044` ### Functions an application calls ### `claimReferralFees(address,address)` `0xcf893acc` · `nonpayable` · Permissionless — anyone may settle anyone's accrual into the escrow. Pays a referrer their accrued fees for one currency into the escrow. Permissionless: the amount and destination are fixed by the ledger, so who triggers the settlement does not matter. Deliberately not settled inside `afterSwap`. An escrow credit is an external call plus an approval, and putting it on the swap path would charge every trader for it. This mirrors how the pool's own fees are batched into `sweepPoolFees` rather than distributed per swap. Two steps, not one: this moves the accrual into the escrow, then the referrer calls `feeEscrow.claimToken` to withdraw. Returns 0 and does nothing when the ledger is empty. **Parameters** | Name | Type | Description | |---|---|---| | `referrer` | `address` | | | `currency` | `address` | | **Returns** | Name | Type | Description | |---|---|---| | `amount` | `uint256` | | ### `currentFeePolicy()` `0x89a69bd8` · `view` · view Returns the policy terms new launches snapshot immutably. Live policy. A launch freezes a copy of this at creation, so an existing launch is unaffected by later changes. **Returns** | Name | Type | Description | |---|---|---| | `arg0` | `(address,uint16,uint16,uint16,uint16,uint16)` | | ### `pendingReferral(address,address)` `0xd85b2777` · `view` · view **Parameters** | Name | Type | Description | |---|---|---| | `referrer` | `address` | | | `currency` | `address` | | **Returns** | Name | Type | Description | |---|---|---| | `amount` | `uint256` | | ### `sweepPoolFees(bytes32,uint256)` `0xebe51768` · `nonpayable` · The fee-sweep operator, or the pool's creator. Converts any pending memecoin-denominated fee into the pool's quote currency against the pool's own liquidity, then splits the combined quote-currency total between protocol and creator using the live policy, exactly mirroring the bonding curve's own sweep. The trusted sweep operator is required whenever the sweep would execute an internal conversion. The creator may still distribute already-quoted fees when no internal swap is needed. If any memecoin-denominated fee is pending, only the operator may call and `minConversionQuoteOut` must be non-zero — the sweep converts inventory against the pool's own liquidity and needs a slippage bound. **Parameters** | Name | Type | Description | |---|---|---| | `poolId` | `bytes32` | | | `minConversionQuoteOut` | `uint256` | | ### Other functions | Signature | Selector | Mutability | |---|---|---| | `acceptOwnership()` | `0x79ba5097` | nonpayable | | `afterAddLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),int256,int256,bytes)` | `0x9f063efc` | nonpayable | | `afterDonate(address,(address,address,uint24,int24,address),uint256,uint256,bytes)` | `0xe1b4af69` | nonpayable | | `afterInitialize(address,(address,address,uint24,int24,address),uint160,int24)` | `0x6fe7e6eb` | nonpayable | | `afterRemoveLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),int256,int256,bytes)` | `0x6c2bbe7e` | nonpayable | | `afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)` | `0xb47b2fb1` | nonpayable | | `beforeAddLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),bytes)` | `0x259982e5` | nonpayable | | `beforeDonate(address,(address,address,uint24,int24,address),uint256,uint256,bytes)` | `0xb6a8b0fa` | nonpayable | | `beforeInitialize(address,(address,address,uint24,int24,address),uint160)` | `0xdc98354e` | nonpayable | | `beforeRemoveLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),bytes)` | `0x21d0ee70` | nonpayable | | `beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)` | `0x575e24b4` | nonpayable | | `factory()` | `0xc45a0155` | view | | `feeEscrow()` | `0xc4b7de97` | view | | `feeSweepOperator()` | `0x8a36a6bb` | view | | `getHookPermissions()` | `0xc4e833ce` | pure | | `hookFeeBps()` | `0xea26abcf` | view | | `launches(bytes32)` | `0xad091230` | view | | `maxInternalPriceImpactBps()` | `0x90addc1e` | view | | `owner()` | `0x8da5cb5b` | view | | `pendingCreatorTax(bytes32,address)` | `0xc8eaa792` | view | | `pendingFees(bytes32,address)` | `0x359b4f30` | view | | `pendingOwner()` | `0xe30c3978` | view | | `poolManager()` | `0xdc4c90d3` | view | | `protocolFeeRecipient()` | `0x64df049e` | view | | `protocolFeeShareBps()` | `0x9040f866` | view | | `referralDiscountBps()` | `0x30ab6943` | view | | `referralRegistry()` | `0x4e627e62` | view | | `referralShareBps()` | `0x47c9bc2d` | view | | `registerPool((address,address,uint24,int24,address),address,address,uint16,(address,uint16,uint16,uint16,uint16,uint16))` | `0x302511dd` | nonpayable | | `renounceOwnership()` | `0x715018a6` | pure | | `rescuePoolFees(bytes32)` | `0x5cbe8117` | nonpayable | | `setCreatorFeeRecipient(bytes32,address)` | `0xed8ef7a3` | nonpayable | | `setFactory(address)` | `0x5bb47808` | nonpayable | | `setFeeSweepOperator(address)` | `0x54faf9c3` | nonpayable | | `setHookFeeBps(uint256)` | `0xbfe7af83` | nonpayable | | `setMaxInternalPriceImpactBps(uint256)` | `0xb89eddab` | nonpayable | | `setProtocolFeeRecipient(address)` | `0xe521cb92` | nonpayable | | `setProtocolFeeShareBps(uint256)` | `0xfc75e481` | nonpayable | | `setReferralDiscountBps(uint256)` | `0x98da62d5` | nonpayable | | `setReferralRegistry(address)` | `0x6a79115f` | nonpayable | | `setReferralShareBps(uint256)` | `0xd07e995b` | nonpayable | | `transferOwnership(address)` | `0xf2fde38b` | nonpayable | | `unlockCallback(bytes)` | `0x91dd7346` | nonpayable | ### Events | Event | topic0 | |---|---| | `CreatorFeeRecipientUpdated(bytes32,address,address)` | `0xb45e6b72a7de9a2077babe9717744436f3880e114099956ca85f91a77469a532` | | `FactorySet(address)` | `0x1edf3afd4ac789736e00d216cd88be164ddcef26a6eedcc30cdb0cb62f3741b1` | | `FeeSweepOperatorUpdated(address)` | `0xae994ca926e252e299c3df7516cb609272a57bf80b0e0715297e55939f873420` | | `HookFeeBpsUpdated(uint256)` | `0xaea8b8d37d8110dd00c418d9c1c268f0fbadacb802c284b71a1777e411cd965a` | | `HookFeeCollected(bytes32,address,uint256,uint256)` | `0xc532c43b3423e14ef72748f1c8291238829ca0af8ba9b67975ad1483485a4b4d` | | `MaxInternalPriceImpactUpdated(uint256)` | `0x6968b68c1fb468c8b257b012290bf803a6a6d7e79468e0326050724f7573cf01` | | `OwnershipTransferred(address,address)` | `0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0` | | `OwnershipTransferStarted(address,address)` | `0x38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e22700` | | `PoolConversionSkipped(bytes32,uint256)` | `0xeed2d18eb96f3c2cb8c7b6993512a506c170e17d29355f2d7a0d5961f338de09` | | `PoolFeesRescued(bytes32,address,uint256,uint256)` | `0x0fbb28f9c335f55dcc5cc19e595ab55f9e6a0fd1b58ad77be3a98f99901daaff` | | `PoolFeesSwept(bytes32,uint256,uint256)` | `0x2b33b68d948eb789fd57906bde3dc24d9f748c1df9b98d7359910f6aa1c06e2f` | | `PoolRegistered(bytes32,address,address,address)` | `0x01bf263a1db1652580721573296e1a1fa70b3d4c87f61d02a69c4e1109d2d573` | | `ProtocolFeeRecipientUpdated(address)` | `0xc1b5345cce283376356748dc57f2dfa7120431d016fc7ca9ba641bc65f91411d` | | `ProtocolFeeShareUpdated(uint256)` | `0x4d1fc9430e27afb14db15169fd1c79e8b51773302919ac8c049f1c41995e380b` | | `ReferralDiscountUpdated(uint256)` | `0xe0f45d08835a6839e8d2327d73ee817a6da7276c4776e77abe76eaa524bc92ef` | | `ReferralFeeAccrued(bytes32,address,address,uint256)` | `0x7ca75a36687fd0a9628cbb8d737989015c0e5236d128a986b078ed14a030fe81` | | `ReferralFeeClaimed(address,address,uint256)` | `0x646dbd2d0dbd68fc66a49d8c448dd995f308238033d47b3c6122f637b331bfdb` | | `ReferralRegistrySet(address)` | `0xcf7381fd801bfc0e3e6a57a711e8165131a80c69051919ee96c9896cd87c0c11` | | `ReferralShareUpdated(uint256)` | `0x7c13f976b8efb8331f00ce07146b8270d065e3789f2eabeab837c72ca942ad61` | ### Errors | Error | Selector | |---|---| | `AlreadyRegistered()` | `0x3a81d6fc` | | `AlreadySet()` | `0xa741a045` | | `HookNotImplemented()` | `0x0a85dc29` | | `InexactQuoteTransfer(address,uint256,uint256)` | `0x197001d6` | | `InternalSwapRequiresOperator()` | `0x31cdb504` | | `InvalidBps()` | `0xc6cc5d7f` | | `InvalidPoolKey()` | `0xc256622b` | | `MinimumOutputRequired()` | `0x3672d25f` | | `NotFactory()` | `0x32cc7236` | | `NotFeeSweepOperator()` | `0x8d42130c` | | `NothingToRescue()` | `0x00f6b210` | | `NotPoolManager()` | `0xae18210a` | | `OwnableInvalidOwner(address)` | `0x1e4fbdf7` | | `OwnableUnauthorizedAccount(address)` | `0x118cdaa7` | | `OwnershipCannotBeRenounced()` | `0x2fab92ca` | | `ReentrancyGuardReentrantCall()` | `0x3ee5aeb5` | | `SafeCastOverflowedIntToUint(int256)` | `0xa8ce4432` | | `SafeCastOverflowedUintToInt(uint256)` | `0x24775e06` | | `SafeERC20FailedOperation(address)` | `0x5274afe7` | | `SlippageExceeded(uint256,uint256)` | `0x71c4efed` | | `UnknownPool()` | `0xf7139e33` | | `ZeroAddress()` | `0xd92e233d` | --- ## FociFeeEscrow `0x5a76a44B49ca0f7c4dB181f289C1eCA91d928406` ### Functions an application calls ### `balanceOf(address)` `0x70a08231` · `view` · view Returns the claimable native ETH balance for `recipient`. **Parameters** | Name | Type | Description | |---|---|---| | `recipient` | `address` | | **Returns** | Name | Type | Description | |---|---|---| | `arg0` | `uint256` | | ### `balanceOfToken(address,address)` `0xf59e38b7` · `view` · view Returns the claimable balance of `token` for `recipient`. `(account, token)`. Read this rather than an indexer if you want on-chain truth for a claim button. **Parameters** | Name | Type | Description | |---|---|---| | `recipient` | `address` | | | `token` | `address` | | **Returns** | Name | Type | Description | |---|---|---| | `arg0` | `uint256` | | ### `claim()` `0x4e71d92d` · `nonpayable` · Pull-only, `msg.sender`. Pays out the caller's entire claimable native ETH balance. Native-asset balance. Unreachable unless a launch quotes in the native asset, which no approved pair token does. **Returns** | Name | Type | Description | |---|---|---| | `amount` | `uint256` | | ### `claimToken(address)` `0x32f289cf` · `nonpayable` · Pull-only — claims the balance of `msg.sender`. You cannot claim for someone else. Pays out the caller's entire claimable balance of `token`. The escrow holds ONE balance per (recipient, token). It does not distinguish creator fees from referral fees — that split is attribution derived off-chain from events — so this withdraws both at once. Fees are in each launch's quote asset — an ERC-20 for every approved pair token — so this is the path, once per asset, not the native `claim()`. **Parameters** | Name | Type | Description | |---|---|---| | `token` | `address` | | **Returns** | Name | Type | Description | |---|---|---| | `amount` | `uint256` | | **Reverts** - `NoBalance` — nothing accrued for you in that token ### Other functions | Signature | Selector | Mutability | |---|---|---| | `claim(uint256)` | `0x379607f5` | nonpayable | | `claimToken(address,uint256)` | `0x1698755f` | nonpayable | | `credit(address)` | `0xd5d44d80` | payable | | `creditToken(address,address,uint256)` | `0x09ad4dd9` | nonpayable | ### Events | Event | topic0 | |---|---| | `Claimed(address,uint256)` | `0xd8138f8a3f377c5259ca548e70e4c2de94f129f5a11036a15b69513cba2b426a` | | `ClaimedToken(address,address,uint256)` | `0xdbc1ea3a8459e4c7e11fb385b52bbb5cc8c8ab85eec5d883ac9aa78c171f5141` | | `Credited(address,address,uint256)` | `0x4e45da441832cf53bdaa69235704fc0575e68210f459ee1562911024b12967d5` | | `CreditedToken(address,address,address,uint256)` | `0x5d104c62f50449fadfe6f4013c8f36588d32737f94b5ac9b83ddad33b3e1ffdf` | ### Errors | Error | Selector | |---|---| | `InsufficientBalance(uint256,uint256)` | `0xcf479181` | | `NoBalance()` | `0xc2caa2a6` | | `ReentrancyGuardReentrantCall()` | `0x3ee5aeb5` | | `SafeERC20FailedOperation(address)` | `0x5274afe7` | | `TransferFailed()` | `0x90b8ec18` | | `ZeroAddress()` | `0xd92e233d` | --- ## FociReferralRegistry `0xe047D0F0ce0dD600732793762B1f1929Adc5015d` ### Functions an application calls ### `referrerOf(address)` `0xd21cacdf` · `view` · view The permanent referrer of each user, or the zero address if they have never been referred. Read by every curve on every trade. **Parameters** | Name | Type | Description | |---|---|---| | `user` | `address` | | **Returns** | Name | Type | Description | |---|---|---| | `referrer` | `address` | | ### `setReferrer(address)` `0xa18a7bfc` · `nonpayable` · Self-service — binds `msg.sender`. Claims a referrer for the caller before their first trade. Reverts rather than no-ops on an existing binding: someone calling this directly asked for a specific outcome and should be told it did not happen, where a trade merely carrying a stale referrer should still settle. Permanent and never rewritten. Bind before trading, or pass the referrer to `buy` instead. **Parameters** | Name | Type | Description | |---|---|---| | `referrer` | `address` | | **Reverts** - `AlreadyReferred` - `SelfReferral` - `ReciprocalReferral` - `ZeroAddress` ### Other functions | Signature | Selector | Mutability | |---|---|---| | `bindFor(address,address)` | `0x620e206c` | nonpayable | | `factory()` | `0xc45a0155` | view | | `memeHook()` | `0x6651812c` | view | ### Events | Event | topic0 | |---|---| | `ReferrerBound(address,address,address)` | `0x5b6dcb011725a9616ecced5408efb270f5e20477283b63e06c8b4eb0b4da4296` | ### Errors | Error | Selector | |---|---| | `AlreadyReferred()` | `0x7aabdfe3` | | `NotAuthorizedBinder()` | `0xbe447ef2` | | `ReciprocalReferral()` | `0xb6ea0b01` | | `SelfReferral()` | `0x55e8f70e` | | `ZeroAddress()` | `0xd92e233d` | --- ## FociLaunchLocker `0x539fD9e6a6316B65bEd9dDb9A570959e0bc8C31A` ### Other functions | Signature | Selector | Mutability | |---|---|---| | `acceptOwnership()` | `0x79ba5097` | nonpayable | | `factory()` | `0xc45a0155` | view | | `isLocked(address)` | `0x4a4fbeec` | view | | `lockedPositions(address)` | `0xfa22143d` | view | | `lockedTokenSupply(address)` | `0x732e78e4` | view | | `lockPosition(address,uint256)` | `0x292d5732` | nonpayable | | `lockTokenSupply(address,uint256)` | `0xb8a0d7ab` | nonpayable | | `onERC721Received(address,address,uint256,bytes)` | `0x150b7a02` | view | | `owner()` | `0x8da5cb5b` | view | | `pendingOwner()` | `0xe30c3978` | view | | `positionManager()` | `0x791b98bc` | view | | `renounceOwnership()` | `0x715018a6` | pure | | `setFactory(address)` | `0x5bb47808` | nonpayable | | `transferOwnership(address)` | `0xf2fde38b` | nonpayable | ### Events | Event | topic0 | |---|---| | `FactorySet(address)` | `0x1edf3afd4ac789736e00d216cd88be164ddcef26a6eedcc30cdb0cb62f3741b1` | | `OwnershipTransferred(address,address)` | `0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0` | | `OwnershipTransferStarted(address,address)` | `0x38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e22700` | | `PositionLocked(address,uint256)` | `0x2cabb2a2973327d5863ceb4707e9441851243897e86d587ee35943599752eb54` | | `TokenSupplyLocked(address,uint256)` | `0xaf33c4aba92959b3e7ddc83ab728938262da159a6c05ca836f6c46f9bcb2c740` | ### Errors | Error | Selector | |---|---| | `AlreadyInitialized()` | `0x0dc149f0` | | `NotFactory()` | `0x32cc7236` | | `NotPositionManager()` | `0x20fdc658` | | `OwnableInvalidOwner(address)` | `0x1e4fbdf7` | | `OwnableUnauthorizedAccount(address)` | `0x118cdaa7` | | `OwnershipCannotBeRenounced()` | `0x2fab92ca` | | `PositionAlreadyLocked()` | `0xfe3099b6` | | `PositionNotHeld()` | `0x6b49c94a` | | `SafeERC20FailedOperation(address)` | `0x5274afe7` | | `ZeroAddress()` | `0xd92e233d` | --- ## FociLaunchDeployer `0xa93f9CeFD92A77e1EAffa3246B6F4DB91a5c5659` ### Functions an application calls ### `predictLaunchAddresses((address,address,address,address,(address,uint16,uint16,uint16,uint16,uint16),address,uint256,uint256,uint256,uint256,uint256,bytes32,string,string,string,string,(string,string,string,string,string)))` `0xe6a900b5` · `view` · view Returns the addresses `deployLaunch` would produce for `params`, without deploying anything. Lets a caller confirm that a launch it has not seen confirmed yet will land where it expects, and lets the launch path be checked for a salt the creator has already used. The token is derived from the curve because the curve's address is one of the token's constructor arguments, so the pair has to be computed in deployment order. Computes the CREATE2 token and curve addresses before you send. Use it to mine a vanity address, and to check for an existing deployment — a reused salt reverts with the unhelpful `FailedDeployment`. **Parameters** | Name | Type | Description | |---|---|---| | `params` | `(address,address,address,address,(address,uint16,uint16,uint16,uint16,uint16),address,uint256,uint256,uint256,uint256,uint256,bytes32,string,string,string,string,(string,string,string,string,string))` | | **Returns** | Name | Type | Description | |---|---|---| | `token` | `address` | | | `curve` | `address` | | ### Other functions | Signature | Selector | Mutability | |---|---|---| | `deployLaunch((address,address,address,address,(address,uint16,uint16,uint16,uint16,uint16),address,uint256,uint256,uint256,uint256,uint256,bytes32,string,string,string,string,(string,string,string,string,string)))` | `0x84b2d5c6` | nonpayable | | `factory()` | `0xc45a0155` | view | | `referralRegistry()` | `0x4e627e62` | view | ### Errors | Error | Selector | |---|---| | `Create2EmptyBytecode()` | `0x4ca249dc` | | `FailedDeployment()` | `0xb06ebf3d` | | `InsufficientBalance(uint256,uint256)` | `0xcf479181` | | `MetadataTooLong()` | `0x85b8e2f4` | | `NotFactory()` | `0x32cc7236` | | `ZeroAddress()` | `0xd92e233d` | --- ## FociGraduationExecutor `0xEB286974C35d2741B0fe9b2a1Cd41E53d06aE406` ### Other functions | Signature | Selector | Mutability | |---|---|---| | `factory()` | `0xc45a0155` | view | | `locker()` | `0xd7b96d4e` | view | | `mintFullRangePosition(address,(address,address,uint24,int24,address),int24,int24,uint160,uint256,uint256,address,address,address)` | `0xcbba1910` | payable | | `permit2()` | `0x12261ee7` | view | | `positionManager()` | `0x791b98bc` | view | ### Events | Event | topic0 | |---|---| | `GraduationDustRetained(address,address,uint256)` | `0x667636bce2491e3f246c8b4ec1f4ca0be227dfa611d0575c59f5949283b433c1` | | `GraduationDustSwept(address,address,uint256)` | `0x80a5a2ff8b8c5533e5862e4e161bbcade9af6fd9d67bef56a590b062107f027f` | ### Errors | Error | Selector | |---|---| | `FeeTransferFailed()` | `0x4033e4e3` | | `MintAmountOverflow()` | `0xeee66814` | | `NotFactory()` | `0x32cc7236` | | `SafeERC20FailedOperation(address)` | `0x5274afe7` | | `SlippageExceeded(uint256,uint256)` | `0x71c4efed` | | `ZeroAddress()` | `0xd92e233d` | --- ## FociRewardsDistributorFactory `0xdac447110867954F00638125bbd5c66D8E0a7195` ### Functions an application calls ### `deploy(address,bytes32)` `0x32c02a14` · `nonpayable` · Permissionless — every argument is checked against the launchpad's own record, so the caller cannot change the outcome. The keeper runs it minutes after a holder-rewards launch. Deploys the distributor `token` already names as its creator fee recipient, and records the pair. Permissionless: every argument is checked against the launchpad's own record, so who sends it cannot change the outcome. That matters because a creator's setup is only finished once this has run, and nothing should depend on them coming back to do it. Rejects a native quote because `harvest` only drains the escrow's ERC-20 ledger and a distributor has no `receive()` — such a launch could never be funded. This is a backstop, not the gate: by the time it runs the launch has already pointed its fees at an address that cannot use them. The real gate is refusing to offer holder fees on a native quote at all. Deploys the distributor the launch already names as its `creatorFeeRecipient`, and records the pair in `distributorOf` / `tokenOf`. Re-attempting under the same salt is safe: the address is fixed, so the check either passes once or reverts. **Parameters** | Name | Type | Description | |---|---|---| | `token` | `address` | | | `salt` | `bytes32` | | **Returns** | Name | Type | Description | |---|---|---| | `distributor` | `address` | | **Reverts** - `TokenNotFound` — no such launch on the factory - `NativeQuoteUnsupported` — the launch quotes in the native asset, which a distributor cannot harvest - `AlreadyRegistered` — this token already has a distributor, or the address is already bound to another token - `NotCreatorFeeRecipient(expected, actual)` — the launch's fee recipient is not `predict(deployer, salt, pairToken)`; the launch was made with a different salt, creator, or recipient ### `distributorOf(address)` `0x3f20b9b4` · `view` · View, anyone. Token → its deployed distributor, or zero. Zero for a launch that pays its creator, and for a holder-rewards launch whose `deploy` has not run yet. **Parameters** | Name | Type | Description | |---|---|---| | `token` | `address` | | **Returns** | Name | Type | Description | |---|---|---| | `arg0` | `address` | | ### `initCodeHash(address)` `0x75fd9f28` · `view` · View, anyone. Exposed so a prediction can be verified without this contract. The creation-code hash `predict` uses, exposed so the prediction can be checked without this contract. **Parameters** | Name | Type | Description | |---|---|---| | `pairToken` | `address` | | **Returns** | Name | Type | Description | |---|---|---| | `arg0` | `bytes32` | | ### `predict(address,bytes32,address)` `0x6339c853` · `view` · View, anyone. The address `deploy` will produce for `creator` and `salt` on a launch quoted in `pairToken`. Callers should read this rather than recomputing the salt rule, so the namespacing lives in exactly one place. Where the distributor for `(creator, salt, pairToken)` will live. `creator` is the launch's `deployer` (the account that signs `launchToken`, or the account the router launches for) and `salt` is the same `TokenParams.salt` the launch uses — the address is a CREATE2 of `keccak256(abi.encode(creator, salt))`, namespaced per creator so nobody else's salt can land on it. Send the result as `TokenParams.creatorFeeRecipient` to make the launch pay its holders. Nothing exists there yet and nothing needs to: the escrow credits by address. **Parameters** | Name | Type | Description | |---|---|---| | `creator` | `address` | | | `salt` | `bytes32` | | | `pairToken` | `address` | | **Returns** | Name | Type | Description | |---|---|---| | `arg0` | `address` | | ### `tokenOf(address)` `0x42ec38e2` · `view` · View, anyone. Distributor → the token it was registered for. Informational only: the authoritative direction is the launch record's `creatorFeeRecipient`. **Parameters** | Name | Type | Description | |---|---|---| | `distributor` | `address` | | **Returns** | Name | Type | Description | |---|---|---| | `arg0` | `address` | | ### Other functions | Signature | Selector | Mutability | |---|---|---| | `distributorOwner()` | `0x2127911d` | view | | `escrow()` | `0xe2fdcc17` | view | | `launchFactory()` | `0x536dac9b` | view | ### Events | Event | topic0 | |---|---| | `DistributorDeployed(address,address,address,address,bytes32)` | `0x3ef9707063aece3a59b74cf57d0c1ed7a69cceb8630a1b4d42bd1a3f6743f560` | ### Errors | Error | Selector | |---|---| | `AlreadyRegistered()` | `0x3a81d6fc` | | `Create2EmptyBytecode()` | `0x4ca249dc` | | `FailedDeployment()` | `0xb06ebf3d` | | `InsufficientBalance(uint256,uint256)` | `0xcf479181` | | `NativeQuoteUnsupported()` | `0x07609e34` | | `NotCreatorFeeRecipient(address,address)` | `0x02f5a580` | | `TokenNotFound()` | `0xcbdb7b30` | | `ZeroAddress()` | `0xd92e233d` | --- ## FociRewardsDistributor Deployed **once per launch** — no fixed address. Read it from `factory.getLaunchedToken(token)`. ### Functions an application calls ### `batchClaim(uint256[],address,uint256[],uint256[],bytes32[][])` `0xa12e01d4` · `nonpayable` · Permissionless to submit; the proof fixes the recipient. Settles several epochs for one account in a single transaction. A holder accumulates one open epoch per distribution, so by the time they get around to claiming there are usually several. Sending N transactions for what is one decision is a bad enough experience that dust goes unclaimed and rolls over. Deliberately all-or-nothing: a bad proof reverts the batch rather than being skipped. Silently dropping one entry would let a caller believe an epoch was settled when it was not, and `hasClaimed` is the only record. `claim` for several epochs in one transaction — a holder accumulates one open epoch per weekly distribution. Same checks per element; the whole batch reverts if any fails. **Parameters** | Name | Type | Description | |---|---|---| | `epochIds` | `uint256[]` | | | `account` | `address` | | | `amounts` | `uint256[]` | | | `indexes` | `uint256[]` | | | `proofs` | `bytes32[][]` | | **Reverts** - `LengthMismatch` — the four arrays differ in length - …then everything `claim` throws, per epoch ### `claim(uint256,address,uint256,uint256,bytes32[])` `0x3e4fcb21` · `nonpayable` · Permissionless to submit; the proof fixes the recipient. Pays `account`, never `msg.sender`. Claims `amount` from `epochId` for `account`. Permissionless in who submits it: the proof fixes the recipient, so a third party can settle on a holder's behalf without being able to redirect anything. Claims one epoch. `amount`, `index` and `proof` come from the published tree — the API returns them under `claimable.holders.byToken[].epochs[].calldata`, or rebuild the tree from chain data. Leaves are `keccak256(abi.encode(index, account, amount))` (`leafFor` returns the exact encoding); internal nodes hash sorted pairs. **Parameters** | Name | Type | Description | |---|---|---| | `epochId` | `uint256` | | | `account` | `address` | | | `amount` | `uint256` | | | `index` | `uint256` | | | `proof` | `bytes32[]` | | **Reverts** - `NoEpoch` — no such epoch id - `ClaimWindowClosed` — more than 90 days since the epoch was published; the funds have rolled over - `ExcludedAccount` — `account` is on the exclusion list (the curve, the pool, the locker, the distributor itself, …) - `AlreadyClaimed` — this `(epoch, account)` already claimed - `InvalidProof` — leaf or proof does not match the epoch's root - `EpochOverdrawn(total, claimed, amount)` — the tree over-allocated this epoch; cannot happen for a correctly built tree ### `epochCount()` `0x829965cc` · `view` · View, anyone. Epoch ids are `0 … epochCount - 1`. **Returns** | Name | Type | Description | |---|---|---| | `arg0` | `uint256` | | ### `getEpoch(uint256)` `0xbc0bc6ba` · `view` · View, anyone. Root, total, claimed so far, publication time and expiry for an epoch. **Parameters** | Name | Type | Description | |---|---|---| | `epochId` | `uint256` | | **Returns** | Name | Type | Description | |---|---|---| | `arg0` | `(bytes32,uint256,uint256,uint64,bool)` | | ### `harvest()` `0x4641257d` · `nonpayable` · Permissionless. The keeper calls it once at least about $1 of the launch's quote asset is waiting in the escrow. Pulls this contract's accrued fees out of the launchpad's escrow. Permissionless: the destination and amount are fixed by the escrow's ledger, so who triggers it does not matter. Credits the balance delta rather than the escrow's reported figure, so a quote asset that under- delivers can never make this contract believe it holds more than it does. Pulls the distributor's escrow balance into `unallocated`, from where epochs are published. Returns the amount moved. **Returns** | Name | Type | Description | |---|---|---| | `amount` | `uint256` | | **Reverts** - `NothingToHarvest` — the escrow holds nothing for this distributor ### `hasClaimed(uint256,address)` `0x873f6f9e` · `view` · View, anyone. **Parameters** | Name | Type | Description | |---|---|---| | `epochId` | `uint256` | | | `account` | `address` | | **Returns** | Name | Type | Description | |---|---|---| | `arg0` | `bool` | | ### `isExcluded(address)` `0xcba0e996` · `view` · View, anyone. Addresses that can never receive a distribution, whatever a published root says. The pool above all: its liquidity is locked forever, so anything sent there is destroyed rather than distributed. True for the distributor itself from construction (it self-excludes), and for anything the owner has excluded since. **Parameters** | Name | Type | Description | |---|---|---| | `account` | `address` | | **Returns** | Name | Type | Description | |---|---|---| | `arg0` | `bool` | | ### `leafFor(uint256,address,uint256)` `0x98cd4d42` · `pure` · Pure, anyone. The leaf a claim must prove, so callers and tree builders agree on the encoding rather than each guessing it. The exact leaf encoding, `keccak256(abi.encode(index, account, amount))`, so a tree builder and the contract cannot disagree. Not `encodePacked`, not double-hashed. **Parameters** | Name | Type | Description | |---|---|---| | `index` | `uint256` | | | `account` | `address` | | | `amount` | `uint256` | | **Returns** | Name | Type | Description | |---|---|---| | `arg0` | `bytes32` | | ### `outstanding(uint256)` `0x874a0e73` · `view` · View, anyone. What an epoch still owes, whether or not its window has closed. `total - claimed` for an epoch — what `rollOver` will return once the window closes. **Parameters** | Name | Type | Description | |---|---|---| | `epochId` | `uint256` | | **Returns** | Name | Type | Description | |---|---|---| | `arg0` | `uint256` | | ### `rollOver(uint256)` `0xd27411ce` · `nonpayable` · Permissionless. The keeper calls it once an epoch's window has closed. Returns an expired epoch's unclaimed remainder to the unallocated pool, so it funds a later distribution instead of being stranded. Permissionless once the window has closed. There is no path that sends it anywhere but back into the next epoch, so nobody needs to be trusted to run it. Returns an expired epoch's unclaimed remainder to `unallocated`, so a later epoch distributes it. Emits `RolledOver` even when the remainder is zero. **Parameters** | Name | Type | Description | |---|---|---| | `epochId` | `uint256` | | **Returns** | Name | Type | Description | |---|---|---| | `amount` | `uint256` | | **Reverts** - `NoEpoch` — no such epoch id - `ClaimWindowOpen` — the 90-day window has not closed - `AlreadyRolledOver` — already rolled ### `unallocated()` `0xdf1c455c` · `view` · View, anyone. Harvested but not yet committed to an epoch. Publishing draws from this, and a rolled-over epoch returns to it. Harvested funds not yet committed to an epoch — what the next epoch can distribute. **Returns** | Name | Type | Description | |---|---|---| | `arg0` | `uint256` | | ### Other functions | Signature | Selector | Mutability | |---|---|---| | `acceptOwnership()` | `0x79ba5097` | nonpayable | | `CLAIM_WINDOW()` | `0x9f34fc80` | view | | `escrow()` | `0xe2fdcc17` | view | | `owner()` | `0x8da5cb5b` | view | | `pendingOwner()` | `0xe30c3978` | view | | `publishEpoch(bytes32,uint256)` | `0xc16f10ee` | nonpayable | | `quoteToken()` | `0x217a4b70` | view | | `renounceOwnership()` | `0x715018a6` | pure | | `setExcluded(address,bool)` | `0x2836be24` | nonpayable | | `setExcludedBatch(address[],bool)` | `0x8018135d` | nonpayable | | `transferOwnership(address)` | `0xf2fde38b` | nonpayable | ### Events | Event | topic0 | |---|---| | `Claimed(uint256,address,uint256)` | `0x4ec90e965519d92681267467f775ada5bd214aa92c0dc93d90a5e880ce9ed026` | | `EpochPublished(uint256,bytes32,uint256,uint64)` | `0x92c85369eead3d8555a7298769ddafde22d0f2f5f1ec05b2aa4059520533f003` | | `ExclusionUpdated(address,bool)` | `0x83f2b279b6151af5a15cdbe8471d34fe8b34aed9bd9514ffeadb57c9ea366e61` | | `Harvested(uint256,uint256)` | `0xfa07446fad45314351eb89109a154880278451332bb87f1824d435fe58da5939` | | `OwnershipTransferred(address,address)` | `0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0` | | `OwnershipTransferStarted(address,address)` | `0x38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e22700` | | `RolledOver(uint256,uint256)` | `0xf52162a5e697efbc1377904d60df6a1b984cc85ca7964998e201cbea0b8e7549` | ### Errors | Error | Selector | |---|---| | `AlreadyClaimed()` | `0x646cf558` | | `AlreadyRolledOver()` | `0xd4b3db41` | | `ClaimWindowClosed()` | `0xf0f25a33` | | `ClaimWindowOpen()` | `0x29dfa3ce` | | `EpochOverdrawn(uint256,uint256,uint256)` | `0x1e8a6f77` | | `ExcludedAccount()` | `0xb7594bec` | | `InsufficientUnallocated(uint256,uint256)` | `0x30e44a1d` | | `InvalidProof()` | `0x09bde339` | | `LengthMismatch()` | `0xff633a38` | | `NoEpoch()` | `0x6a0b56a8` | | `NothingToHarvest()` | `0x3f29331a` | | `OwnableInvalidOwner(address)` | `0x1e4fbdf7` | | `OwnableUnauthorizedAccount(address)` | `0x118cdaa7` | | `OwnershipCannotBeRenounced()` | `0x2fab92ca` | | `ReentrancyGuardReentrantCall()` | `0x3ee5aeb5` | | `SafeERC20FailedOperation(address)` | `0x5274afe7` | | `ZeroAddress()` | `0xd92e233d` | | `ZeroAmount()` | `0x1f2a2005` | --- # ABI Every ABI below is emitted straight from the compiler by `foci/backend/ops/scripts/emit-ui-docs.ts`, so it matches the deployed bytecode on **arc-mainnet** (chain `5042`). Each file is the bare ABI array — exactly what viem, ethers, wagmi and `cast` expect. ## Everything at once **[foci.json](/abi/foci.json)** — every contract with its address, the chain id, the factory's start block and the third-party addresses (Uniswap, Permit2, USDC) you need alongside them. One download is enough to build a client. ```bash curl -O https://foci.family/abi/foci.json ``` ```ts import foci from "./foci.json"; const factory = foci.contracts.find((c) => c.name === "FociLaunchFactory")!; await client.readContract({ address: factory.address as `0x${string}`, abi: factory.abi, functionName: "launchFee", }); ``` ## Individual contracts | File | Surface | Address | | --- | --- | --- | | [`FociLaunchFactory.json`](/abi/FociLaunchFactory.json) | 54 fn · 22 ev · 49 err | `0xa392D6eca5242715517eeCd43406aeD19424FAC0` | | [`FociBondingCurve.json`](/abi/FociBondingCurve.json) | 39 fn · 11 ev · 20 err | per launch — no fixed address | | [`FociLauncherToken.json`](/abi/FociLauncherToken.json) | 18 fn · 2 ev · 7 err | per launch — no fixed address | | [`FociLaunchAndBuy.json`](/abi/FociLaunchAndBuy.json) | 8 fn · 4 ev · 9 err | `0x5C5c202271E1300bD5Ce43A4F5C1cEA8efd57B63` | | [`FociMemeHook.json`](/abi/FociMemeHook.json) | 47 fn · 19 ev · 22 err | `0xF847790B6fA5DA300BB3f56f10d743e71E98e044` | | [`FociFeeEscrow.json`](/abi/FociFeeEscrow.json) | 8 fn · 4 ev · 6 err | `0x5a76a44B49ca0f7c4dB181f289C1eCA91d928406` | | [`FociReferralRegistry.json`](/abi/FociReferralRegistry.json) | 5 fn · 1 ev · 5 err | `0xe047D0F0ce0dD600732793762B1f1929Adc5015d` | | [`FociLaunchLocker.json`](/abi/FociLaunchLocker.json) | 14 fn · 5 ev · 10 err | `0x539fD9e6a6316B65bEd9dDb9A570959e0bc8C31A` | | [`FociLaunchDeployer.json`](/abi/FociLaunchDeployer.json) | 4 fn · 0 ev · 6 err | `0xa93f9CeFD92A77e1EAffa3246B6F4DB91a5c5659` | | [`FociGraduationExecutor.json`](/abi/FociGraduationExecutor.json) | 5 fn · 2 ev · 6 err | `0xEB286974C35d2741B0fe9b2a1Cd41E53d06aE406` | | [`FociRewardsDistributorFactory.json`](/abi/FociRewardsDistributorFactory.json) | 8 fn · 1 ev · 8 err | `0xdac447110867954F00638125bbd5c66D8E0a7195` | | [`FociRewardsDistributor.json`](/abi/FociRewardsDistributor.json) | 22 fn · 7 ev · 18 err | per launch — no fixed address | > [!NOTE] > `FociBondingCurve` and `FociLauncherToken` are deployed **once per launch**, so they > have no fixed address. Read the pair from the factory with `getLaunchedToken(token)` — see > [Addresses](/documentation/addresses). ## Using one file ```bash curl -O https://foci.family/abi/FociLaunchFactory.json ``` ```ts import factoryAbi from "./FociLaunchFactory.json"; ``` The JSON is the array itself, not an object wrapping one, so it drops straight into `abi:` with no unwrapping. --- # Errors Every custom error across every contract. When a transaction reverts with a bare selector, look it up here. | Selector | Error | Declared by | |---|---|---| | `0x646cf558` | `AlreadyClaimed()` | FociRewardsDistributor | | `0xe6a0d45f` | `AlreadyGraduated()` | FociBondingCurve | | `0x0dc149f0` | `AlreadyInitialized()` | FociBondingCurve, FociLaunchLocker | | `0x7aabdfe3` | `AlreadyReferred()` | FociReferralRegistry | | `0x3a81d6fc` | `AlreadyRegistered()` | FociMemeHook, FociRewardsDistributorFactory | | `0xd4b3db41` | `AlreadyRolledOver()` | FociRewardsDistributor | | `0xa741a045` | `AlreadySet()` | FociLaunchFactory, FociMemeHook | | `0xf0f25a33` | `ClaimWindowClosed()` | FociRewardsDistributor | | `0x29dfa3ce` | `ClaimWindowOpen()` | FociRewardsDistributor | | `0x49e55bcb` | `CombinedFeeTooHigh()` | FociLaunchFactory | | `0x85258712` | `CoreLpFeeMustBeZero()` | FociLaunchFactory | | `0x4ca249dc` | `Create2EmptyBytecode()` | FociLaunchDeployer, FociRewardsDistributorFactory | | `0x9ad465dc` | `CreatorTaxTooHigh()` | FociLaunchFactory | | `0x4e222a24` | `CurveFeeTooHigh()` | FociLaunchFactory | | `0x025ac17e` | `CurveGraduated()` | FociBondingCurve | | `0x95e32dab` | `CurveNotQuotable()` | FociLaunchFactory | | `0x1e8a6f77` | `EpochOverdrawn(uint256,uint256,uint256)` | FociRewardsDistributor | | `0xfb8f41b2` | `ERC20InsufficientAllowance(address,uint256,uint256)` | FociLauncherToken | | `0xe450d38c` | `ERC20InsufficientBalance(address,uint256,uint256)` | FociLauncherToken | | `0xe602df05` | `ERC20InvalidApprover(address)` | FociLauncherToken | | `0xec442f05` | `ERC20InvalidReceiver(address)` | FociLauncherToken | | `0x96c6fd1e` | `ERC20InvalidSender(address)` | FociLauncherToken | | `0x94280d62` | `ERC20InvalidSpender(address)` | FociLauncherToken | | `0xb7594bec` | `ExcludedAccount()` | FociRewardsDistributor | | `0xb06ebf3d` | `FailedDeployment()` | FociLaunchDeployer, FociRewardsDistributorFactory | | `0x4033e4e3` | `FeeTransferFailed()` | FociLaunchFactory, FociGraduationExecutor | | `0xd43cabc3` | `GraduationExecutorNotSet()` | FociLaunchFactory | | `0xbdcd75af` | `GraduationRescueTooEarly(uint256)` | FociLaunchFactory | | `0x2c37d0eb` | `GraduationSeedNotViable()` | FociLaunchFactory | | `0x6d3bcfe5` | `GraduationStillViable()` | FociLaunchFactory | | `0x0a85dc29` | `HookNotImplemented()` | FociMemeHook | | `0x197001d6` | `InexactQuoteTransfer(address,uint256,uint256)` | FociMemeHook | | `0x495a9962` | `InexactTransfer(address,uint256,uint256)` | FociLaunchFactory | | `0xcf479181` | `InsufficientBalance(uint256,uint256)` | FociFeeEscrow, FociLaunchDeployer, FociRewardsDistributorFactory | | `0x098fb561` | `InsufficientInputAmount()` | FociBondingCurve | | `0xbb55fd27` | `InsufficientLiquidity()` | FociBondingCurve | | `0x42301c23` | `InsufficientOutputAmount()` | FociBondingCurve | | `0x30e44a1d` | `InsufficientUnallocated(uint256,uint256)` | FociRewardsDistributor | | `0x31cdb504` | `InternalSwapRequiresOperator()` | FociMemeHook | | `0x800c7e91` | `InvalidBasisPoints()` | FociLaunchFactory | | `0xc6cc5d7f` | `InvalidBps()` | FociMemeHook | | `0x7a34030f` | `InvalidFeePolicy()` | FociBondingCurve | | `0x2bb8bdd6` | `InvalidGraduationThreshold()` | FociLaunchFactory | | `0x68b42c59` | `InvalidLaunchConfigId()` | FociLaunchFactory | | `0xbc0ecfe3` | `InvalidLaunchEconomics()` | FociBondingCurve | | `0x2b7ad4f8` | `InvalidPhantomQuote()` | FociLaunchFactory | | `0xc256622b` | `InvalidPoolKey()` | FociMemeHook | | `0x09bde339` | `InvalidProof()` | FociRewardsDistributor | | `0x270815a0` | `InvalidTickSpacing()` | FociLaunchFactory | | `0x374852ca` | `InvalidTokenParams()` | FociLaunchFactory | | `0xa8b63076` | `LaunchConfigDisabled()` | FociLaunchFactory | | `0x1de25df3` | `LaunchDependenciesNotWired()` | FociLaunchFactory | | `0x57332dcf` | `LaunchDeployerNotSet()` | FociLaunchFactory | | `0xecb27319` | `LaunchEconomicsMismatch(bytes32,bytes32)` | FociLaunchFactory | | `0x52660db0` | `LaunchFeeTokenNotSet()` | FociLaunchFactory | | `0xff633a38` | `LengthMismatch()` | FociRewardsDistributor | | `0x85b8e2f4` | `MetadataTooLong()` | FociLaunchDeployer | | `0x3672d25f` | `MinimumOutputRequired()` | FociMemeHook | | `0xeee66814` | `MintAmountOverflow()` | FociGraduationExecutor | | `0x07609e34` | `NativeQuoteUnsupported()` | FociRewardsDistributorFactory | | `0xbc760cfe` | `NativeValueMismatch(uint256,uint256)` | FociBondingCurve, FociLaunchAndBuy | | `0xc2caa2a6` | `NoBalance()` | FociFeeEscrow | | `0x6a0b56a8` | `NoEpoch()` | FociRewardsDistributor | | `0xa3fef2f8` | `NoPendingChange()` | FociLaunchFactory | | `0x502ba015` | `NotApprovedLauncher()` | FociLaunchAndBuy | | `0xbe447ef2` | `NotAuthorizedBinder()` | FociReferralRegistry | | `0xb9f93944` | `NotCreatorFeeRecipient()` | FociLaunchFactory | | `0x02f5a580` | `NotCreatorFeeRecipient(address,address)` | FociRewardsDistributorFactory | | `0x32cc7236` | `NotFactory()` | FociBondingCurve, FociMemeHook, FociLaunchLocker, FociLaunchDeployer, FociGraduationExecutor | | `0x8d42130c` | `NotFeeSweepOperator()` | FociBondingCurve, FociMemeHook | | `0xc2074c46` | `NothingToGraduate()` | FociLaunchFactory | | `0x3f29331a` | `NothingToHarvest()` | FociRewardsDistributor | | `0x00f6b210` | `NothingToRescue()` | FociMemeHook | | `0x87138d5c` | `NotInitialized()` | FociBondingCurve | | `0xea9eaa96` | `NotLaunchForwarder()` | FociLaunchFactory | | `0xae18210a` | `NotPoolManager()` | FociMemeHook | | `0x20fdc658` | `NotPositionManager()` | FociLaunchLocker | | `0xffa32558` | `NotReadyToGraduate()` | FociLaunchFactory, FociBondingCurve | | `0x584a7938` | `NotWhitelisted()` | FociLaunchFactory | | `0x1e4fbdf7` | `OwnableInvalidOwner(address)` | FociLaunchFactory, FociLaunchAndBuy, FociMemeHook, FociLaunchLocker, FociRewardsDistributor | | `0x118cdaa7` | `OwnableUnauthorizedAccount(address)` | FociLaunchFactory, FociLaunchAndBuy, FociMemeHook, FociLaunchLocker, FociRewardsDistributor | | `0x2fab92ca` | `OwnershipCannotBeRenounced()` | FociLaunchFactory, FociMemeHook, FociLaunchLocker, FociRewardsDistributor | | `0x4e3de34f` | `PairTokenDecimalsMismatch(uint8,uint8)` | FociLaunchFactory | | `0xe43c14ca` | `PairTokenDecimalsUnavailable()` | FociLaunchFactory | | `0x764c63c8` | `PairTokenEconomicsInvalid()` | FociLaunchFactory | | `0x49285dfb` | `PairTokenNotApproved()` | FociLaunchFactory | | `0x26fbfa60` | `PairTokenValidationFailed()` | FociLaunchFactory | | `0xfe3099b6` | `PositionAlreadyLocked()` | FociLaunchLocker | | `0x6b49c94a` | `PositionNotHeld()` | FociLaunchLocker | | `0xb6ea0b01` | `ReciprocalReferral()` | FociReferralRegistry | | `0x3ee5aeb5` | `ReentrancyGuardReentrantCall()` | FociLaunchFactory, FociBondingCurve, FociLaunchAndBuy, FociMemeHook, FociFeeEscrow, FociRewardsDistributor | | `0xf0c49d44` | `RefundFailed()` | FociLaunchAndBuy | | `0xa8ce4432` | `SafeCastOverflowedIntToUint(int256)` | FociMemeHook | | `0x24775e06` | `SafeCastOverflowedUintToInt(uint256)` | FociMemeHook | | `0x5274afe7` | `SafeERC20FailedOperation(address)` | FociLaunchFactory, FociBondingCurve, FociLaunchAndBuy, FociMemeHook, FociFeeEscrow, FociLaunchLocker, FociGraduationExecutor, FociRewardsDistributor | | `0x55e8f70e` | `SelfReferral()` | FociReferralRegistry | | `0x71c4efed` | `SlippageExceeded(uint256,uint256)` | FociBondingCurve, FociMemeHook, FociGraduationExecutor | | `0x582157bb` | `SqrtPriceOutOfBounds()` | FociLaunchFactory | | `0xacb9fa2b` | `SupplyTooHigh()` | FociLaunchFactory | | `0xc0b4e373` | `SupplyTooLow()` | FociLaunchFactory | | `0xb79d40e8` | `TimelockExpired(uint256)` | FociLaunchFactory | | `0x810c4f2a` | `TimelockNotElapsed(uint256)` | FociLaunchFactory | | `0xcbdb7b30` | `TokenNotFound()` | FociLaunchFactory, FociRewardsDistributorFactory | | `0x90b8ec18` | `TransferFailed()` | FociBondingCurve, FociFeeEscrow | | `0xe0aeda7d` | `UnexpectedNativeValue()` | FociBondingCurve | | `0xf7139e33` | `UnknownPool()` | FociMemeHook | | `0xdd737e7c` | `UnsupportedPrice()` | FociLaunchFactory | | `0x9465dbd4` | `WrongGraduationPhase()` | FociLaunchFactory | | `0xd92e233d` | `ZeroAddress()` | FociLaunchFactory, FociBondingCurve, FociLauncherToken, FociLaunchAndBuy, FociMemeHook, FociFeeEscrow, FociReferralRegistry, FociLaunchLocker, FociLaunchDeployer, FociGraduationExecutor, FociRewardsDistributorFactory, FociRewardsDistributor | | `0x1f2a2005` | `ZeroAmount()` | FociLaunchFactory, FociBondingCurve, FociLaunchAndBuy, FociRewardsDistributor | --- # Events Every event with its `topic0`, for indexers. | topic0 | Event | Contract | |---|---|---| | `0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925` | `Approval(address,address,uint256)` | FociLauncherToken | | `0xe2cd2f31ebc05ec28640102987f4c8fc5f20e269e1b3aa82577f3f2f0e35c7c6` | `AutoGraduationFailed(address,uint256)` | FociBondingCurve | | `0x2cbe77dadc7f8418071409bebfd71778263eecb998af52aa5c9e27b995a71676` | `AutoSeedFailed(address,uint256)` | FociBondingCurve | | `0xd8138f8a3f377c5259ca548e70e4c2de94f129f5a11036a15b69513cba2b426a` | `Claimed(address,uint256)` | FociFeeEscrow | | `0x4ec90e965519d92681267467f775ada5bd214aa92c0dc93d90a5e880ce9ed026` | `Claimed(uint256,address,uint256)` | FociRewardsDistributor | | `0xdbc1ea3a8459e4c7e11fb385b52bbb5cc8c8ab85eec5d883ac9aa78c171f5141` | `ClaimedToken(address,address,uint256)` | FociFeeEscrow | | `0xbe2de91c1cbef653c760573fff8355c0c851d35ed2a898342b4db556301cccf4` | `CreatorFeeRecipientChangeCancelled(address,address)` | FociLaunchFactory | | `0x7f119e44c84a715429bee60d30ad2e14afdef6c60bb1a7eaa01290ecf6d1b2e5` | `CreatorFeeRecipientChangeProposed(address,address,address,uint256,uint256)` | FociLaunchFactory | | `0x308c390ed1ab5873392818e036cabdf408bc8ad042fbaead3108954ff75ba980` | `CreatorFeeRecipientUpdated(address,address,address)` | FociLaunchFactory | | `0x2cc664e1ac1e2d05c0d4637bb63ec8189113b6ac39276be8977e26216a8cdd19` | `CreatorFeeRecipientUpdated(address,address)` | FociBondingCurve | | `0xb45e6b72a7de9a2077babe9717744436f3880e114099956ca85f91a77469a532` | `CreatorFeeRecipientUpdated(bytes32,address,address)` | FociMemeHook | | `0x4e45da441832cf53bdaa69235704fc0575e68210f459ee1562911024b12967d5` | `Credited(address,address,uint256)` | FociFeeEscrow | | `0x5d104c62f50449fadfe6f4013c8f36588d32737f94b5ac9b83ddad33b3e1ffdf` | `CreditedToken(address,address,address,uint256)` | FociFeeEscrow | | `0xec36bf571f136799e8dc0b0b8bea4b04d8bd3d43de838aab0d5fc21d4cbfc455` | `CurveBuy(address,address,uint256,uint256,uint256,uint256)` | FociBondingCurve | | `0xa69e8258ccc7b9bbb70ab953fc2d1062b4ee28b8ca827534097e1732e87b0262` | `CurveBuyRefunded(address,uint256)` | FociBondingCurve | | `0xf8d37a90738ae063b8b8058b66f5880cf3cf7ab0c5d4fa78219696591dfbfb67` | `CurveCompleted(address,uint256,uint256)` | FociBondingCurve | | `0x8113d738abdcb6b38357e9d53a54a7157861a09031b453651f0fe7fe151f59df` | `CurveSell(address,address,uint256,uint256,uint256,uint256)` | FociBondingCurve | | `0x3ef9707063aece3a59b74cf57d0c1ed7a69cceb8630a1b4d42bd1a3f6743f560` | `DistributorDeployed(address,address,address,address,bytes32)` | FociRewardsDistributorFactory | | `0x92c85369eead3d8555a7298769ddafde22d0f2f5f1ec05b2aa4059520533f003` | `EpochPublished(uint256,bytes32,uint256,uint64)` | FociRewardsDistributor | | `0x83f2b279b6151af5a15cdbe8471d34fe8b34aed9bd9514ffeadb57c9ea366e61` | `ExclusionUpdated(address,bool)` | FociRewardsDistributor | | `0x1edf3afd4ac789736e00d216cd88be164ddcef26a6eedcc30cdb0cb62f3741b1` | `FactorySet(address)` | FociMemeHook | | `0x1edf3afd4ac789736e00d216cd88be164ddcef26a6eedcc30cdb0cb62f3741b1` | `FactorySet(address)` | FociLaunchLocker | | `0x6460dc5c867a0678a8bcc5e64f629fae539901c53a4a8b42fe21d7a6c5e6437d` | `FeesRescued(address,address,uint256,uint256)` | FociBondingCurve | | `0xaf739f46ca7a23c9f259838ec2c5249acf4e1cf9fe68a46f77c3dfa452eda605` | `FeesSwept(uint256,uint256)` | FociBondingCurve | | `0xae994ca926e252e299c3df7516cb609272a57bf80b0e0715297e55939f873420` | `FeeSweepOperatorUpdated(address)` | FociMemeHook | | `0x667636bce2491e3f246c8b4ec1f4ca0be227dfa611d0575c59f5949283b433c1` | `GraduationDustRetained(address,address,uint256)` | FociGraduationExecutor | | `0x80a5a2ff8b8c5533e5862e4e161bbcade9af6fd9d67bef56a590b062107f027f` | `GraduationDustSwept(address,address,uint256)` | FociGraduationExecutor | | `0xac04674474e93058fae25e6df5dd94f57cdcacfe560a182a2eefc8c6006fbf6f` | `GraduationExecutorSet(address)` | FociLaunchFactory | | `0xa0a18f5bf205becee8b268d7cf69addab8548ae8ef361791464cf0e0e17c1361` | `GraduationTokensPermanentlyLocked(address,uint256)` | FociLaunchFactory | | `0xfa07446fad45314351eb89109a154880278451332bb87f1824d435fe58da5939` | `Harvested(uint256,uint256)` | FociRewardsDistributor | | `0xaea8b8d37d8110dd00c418d9c1c268f0fbadacb802c284b71a1777e411cd965a` | `HookFeeBpsUpdated(uint256)` | FociMemeHook | | `0xc532c43b3423e14ef72748f1c8291238829ca0af8ba9b67975ad1483485a4b4d` | `HookFeeCollected(bytes32,address,uint256,uint256)` | FociMemeHook | | `0x908408e307fc569b417f6cbec5d5a06f44a0a505ac0479b47d421a4b2fd6a1e6` | `Initialized(address)` | FociBondingCurve | | `0xedd96c570c6e5ef9add0378e59df53579a283889dc5dab6440ef6eca2ee6c8ce` | `LaunchConfigAdded(uint256)` | FociLaunchFactory | | `0x2f8ba78ae68cfd0c82c7756c540eaf4eead3341aef9ccebcb91d546bff10d62b` | `LaunchConfigUpdated(uint256)` | FociLaunchFactory | | `0xd5ea7aa3e328a0594dcf6914cd9e5369779efaa194ee4dd4c5afcad4f4ebbb0c` | `LaunchDeployerSet(address)` | FociLaunchFactory | | `0xdcacba5e347ae7abd91cb519eb877af8fa7774e347b85dd3ddcd24a2ba8cdf37` | `Launched(address,address,address,address,uint256,uint256)` | FociLaunchAndBuy | | `0x4f1ea5016c51c2f82324e00e9b8a4a95ee5aeaa10c653dabaec5f1bc9047ba0b` | `LaunchEnabledUpdated(bool)` | FociLaunchFactory | | `0xd0766d3f1431146228fe8edef25f27842a1669c91d46e4af1b73405b354489a5` | `LaunchFeeUpdated(address,uint256)` | FociLaunchFactory | | `0x52c1a28345695afc7f6b7629133124dec5d61ee745affd65e4fd2a776bc05840` | `LaunchForceSwept(address)` | FociLaunchFactory | | `0x56b32d3633fed72f97c4df44a78b5fa04f1d662d4bddebcd8a9b216d26d093ad` | `LaunchForwarderSet(address)` | FociLaunchFactory | | `0x7017304fdd491394686dce984eac721f0be1a22228346210f16694772bde44ca` | `LaunchGraduationRescued(address,address,uint256,uint256)` | FociLaunchFactory | | `0xcdb72f157fd3666758a6ce201387ffb52038c7562e4fff352828da1096c4b6b4` | `LaunchSwept(address,uint256,uint256)` | FociLaunchFactory | | `0x3e99ceb3e222d2214d53dacca902810db845f156f78152fdc076be628c4e9a40` | `MaxCreatorTaxUpdated(uint256)` | FociLaunchFactory | | `0x6968b68c1fb468c8b257b012290bf803a6a6d7e79468e0326050724f7573cf01` | `MaxInternalPriceImpactUpdated(uint256)` | FociMemeHook | | `0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0` | `OwnershipTransferred(address,address)` | FociLaunchFactory | | `0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0` | `OwnershipTransferred(address,address)` | FociLaunchAndBuy | | `0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0` | `OwnershipTransferred(address,address)` | FociMemeHook | | `0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0` | `OwnershipTransferred(address,address)` | FociLaunchLocker | | `0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0` | `OwnershipTransferred(address,address)` | FociRewardsDistributor | | `0x38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e22700` | `OwnershipTransferStarted(address,address)` | FociLaunchFactory | | `0x38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e22700` | `OwnershipTransferStarted(address,address)` | FociLaunchAndBuy | | `0x38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e22700` | `OwnershipTransferStarted(address,address)` | FociMemeHook | | `0x38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e22700` | `OwnershipTransferStarted(address,address)` | FociLaunchLocker | | `0x38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e22700` | `OwnershipTransferStarted(address,address)` | FociRewardsDistributor | | `0x060d1992d069dc524985f328329aae36102a017c59733c5c91fc0691ee0703b6` | `PairTokenApprovalUpdated(address,bool)` | FociLaunchFactory | | `0x67d517ee0e305d608b8410ddef27bbd2ed964d843d9b936e84ea2ad1bd65e5d1` | `PairTokenEconomicsUpdated(address,uint256,uint256,uint8)` | FociLaunchFactory | | `0xeed2d18eb96f3c2cb8c7b6993512a506c170e17d29355f2d7a0d5961f338de09` | `PoolConversionSkipped(bytes32,uint256)` | FociMemeHook | | `0x0fbb28f9c335f55dcc5cc19e595ab55f9e6a0fd1b58ad77be3a98f99901daaff` | `PoolFeesRescued(bytes32,address,uint256,uint256)` | FociMemeHook | | `0x2b33b68d948eb789fd57906bde3dc24d9f748c1df9b98d7359910f6aa1c06e2f` | `PoolFeesSwept(bytes32,uint256,uint256)` | FociMemeHook | | `0x0a44ef75df69c534f43cd6c1aa3ef8983065fe5fe79ef9e79f6494e6f258c259` | `PoolGraduated(address,uint256,uint256,uint256)` | FociLaunchFactory | | `0x01bf263a1db1652580721573296e1a1fa70b3d4c87f61d02a69c4e1109d2d573` | `PoolRegistered(bytes32,address,address,address)` | FociMemeHook | | `0x2cabb2a2973327d5863ceb4707e9441851243897e86d587ee35943599752eb54` | `PositionLocked(address,uint256)` | FociLaunchLocker | | `0xc1b5345cce283376356748dc57f2dfa7120431d016fc7ca9ba641bc65f91411d` | `ProtocolFeeRecipientUpdated(address)` | FociMemeHook | | `0x4d1fc9430e27afb14db15169fd1c79e8b51773302919ac8c049f1c41995e380b` | `ProtocolFeeShareUpdated(uint256)` | FociMemeHook | | `0xe0f45d08835a6839e8d2327d73ee817a6da7276c4776e77abe76eaa524bc92ef` | `ReferralDiscountUpdated(uint256)` | FociMemeHook | | `0x7ca75a36687fd0a9628cbb8d737989015c0e5236d128a986b078ed14a030fe81` | `ReferralFeeAccrued(bytes32,address,address,uint256)` | FociMemeHook | | `0x646dbd2d0dbd68fc66a49d8c448dd995f308238033d47b3c6122f637b331bfdb` | `ReferralFeeClaimed(address,address,uint256)` | FociMemeHook | | `0xde9bddf476dde28b26de9d0b38bb9811ebb9d4945cd0c7feadd215c28fe09717` | `ReferralFeePaid(address,address,uint256)` | FociBondingCurve | | `0xcf7381fd801bfc0e3e6a57a711e8165131a80c69051919ee96c9896cd87c0c11` | `ReferralRegistrySet(address)` | FociMemeHook | | `0x7c13f976b8efb8331f00ce07146b8270d065e3789f2eabeab837c72ca942ad61` | `ReferralShareUpdated(uint256)` | FociMemeHook | | `0x5b6dcb011725a9616ecced5408efb270f5e20477283b63e06c8b4eb0b4da4296` | `ReferrerBound(address,address,address)` | FociReferralRegistry | | `0x3af790fafda720819b2fc6e15090606e81154e0ac9a92d38ecad006d99d20ecc` | `Rescued(address,address,uint256)` | FociLaunchAndBuy | | `0xf52162a5e697efbc1377904d60df6a1b984cc85ca7964998e201cbea0b8e7549` | `RolledOver(uint256,uint256)` | FociRewardsDistributor | | `0x8d4aad4953d0ca700d468f3753aa14432d1b35b43ec6409f051fb6aa43a89607` | `TokenLaunched(address,address,address,address,uint256,uint256)` | FociLaunchFactory | | `0xaf33c4aba92959b3e7ddc83ab728938262da159a6c05ca836f6c46f9bcb2c740` | `TokenSupplyLocked(address,uint256)` | FociLaunchLocker | | `0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef` | `Transfer(address,address,uint256)` | FociLauncherToken | | `0xef2b562a67f01ed4b7c4265ec09b539039c6d5dd7e752191d3940508c3dc0068` | `WhitelistedLauncherUpdated(address,bool)` | FociLaunchFactory |