# foci — complete integration documentation Network arc-testnet (chain 5042002). Generated from the deployed contracts. 10 contracts · 202 functions · 70 events · 140 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-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 testnet; see [Addresses](/documentation/addresses). ## 1 · Check you are allowed to launch Launching is gated and **disabled by default on a fresh deployment**. 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, USDC], }); ``` 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, so one approval covers both — and the spender is the router, not the factory. ```ts const fee = await client.readContract({ address: FACTORY, abi: factoryAbi, functionName: "launchFee" }); const openingBuy = 1_000_000n; // 1 USDC — 6 decimals await walletClient.writeContract({ address: USDC, abi: erc20Abi, functionName: "approve", args: [LAUNCH_AND_BUY, fee + 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 USDC, // pairToken openingBuy, // quoteIn — must be non-zero here 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: USDC, abi: erc20Abi, functionName: "approve", args: [FACTORY, fee], // the FACTORY, not the router }); const { result } = await client.simulateContract({ address: FACTORY, abi: factoryAbi, functionName: "launchToken", args: [params, 0n, USDC], 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: USDC, abi: erc20Abi, functionName: "approve", args: [launch.curve, amountIn], // approve the CURVE }); await walletClient.writeContract({ address: launch.curve, abi: curveAbi, functionName: "buy", args: [amountIn, minTokensOut, account], }); ``` --- # Addresses Everything below is **Arc testnet**, chain `5042002`. 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. ## Per-launch contracts have no fixed address Two of the contracts in the reference are deployed **once per launch** by the factory: - `FociV2BondingCurve` - `FociV2LauncherToken` 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-testnet**, chain `5042002`. Explorer: https://testnet.arcscan.app Indexing starts at block `61068004`. | Contract | Address | Kind | |---|---|---| | FociV2LaunchFactory | `0xa93f9CeFD92A77e1EAffa3246B6F4DB91a5c5659` | singleton | | FociV2LaunchAndBuy | `0xE4A165a52A6033ee0C1b3B6dA83f72AbA1707349` | singleton | | FociV2MemeHook | `0x527129aB10Fa3163629A32c4E3Ab7a98ba28e044` | singleton | | FociV2FeeEscrow | `0xcecCFebAcaDCd9404Ae70b160305Af07D9760A46` | singleton | | FociV2ReferralRegistry | `0xEB286974C35d2741B0fe9b2a1Cd41E53d06aE406` | singleton | | FociV2LaunchLocker | `0xe047D0F0ce0dD600732793762B1f1929Adc5015d` | singleton | | FociV2LaunchDeployer | `0x5C5c202271E1300bD5Ce43A4F5C1cEA8efd57B63` | singleton | | FociV2GraduationExecutor | `0x02e7d818080bf85EEB7191536A768B0CFdB38D5D` | singleton | | FociV2BondingCurve | — | per launch, read from the launch record | | FociV2LauncherToken | — | per launch, read from the launch record | ## External contracts | Key | Address | Purpose | |---|---|---| | poolManager | `0xa4aD72b5C7F72528Ce0baE359A13BD5A93a4574F` | Uniswap V4 PoolManager | | positionManager | `0x0f42e91f2cd13E6f03CC7650e72ceBed6031A125` | Uniswap V4 PositionManager | | universalRouter | `0x52EF879333a56E94d62113cC7a9440d0550f8d32` | Uniswap UniversalRouter — pool swaps | | v4Quoter | `0x719A77E47B14F262143399Cbdc49D1510087A148` | Uniswap V4 Quoter — pool-phase estimates | | stateView | `0x270CE77c308C181018F1168903739F400bF9B4FA` | 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()` | **the router**, `launchFee() + quoteIn` | | `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 approval needed const economics = await read("pairTokenEconomics", [USDC]); // phantom + threshold, in 6 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, 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** | quote asset (USDC) | `launchFee()` | | `launchAndBuy.launchAndBuy` | **the router** | quote asset (USDC) | `launchFee() + quoteIn` — one approval covers both | | `curve.buy` | **the curve** | quote asset (USDC) | `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. ## 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 Arc's quote asset is a **6-decimal** ERC-20 USDC at `0x3600000000000000000000000000000000000000`. Every launched memecoin is **18 decimals**. Almost every arithmetic bug in an integration comes from mixing the two. ## Which side is which | Quantity | Decimals | |---|---| | `quoteIn`, `minQuoteOut`, `launchFee`, `phantomQuote`, `graduationThreshold`, escrow balances | **6** | | `minTokensOut`, `tokensIn`, `supply`, token balances, curve token reserves | **18** | So `5e6` is five dollars, and `5e18` is five tokens. A launch fee of `500000` is 0.50 USDC. ## 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`. ## 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 10^12 difference between a 6-decimal and an 18-decimal side. It is not a human price. Adjust for 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. USDC (`0x36…`) is `currency0` only when the memecoin's address happens to sort above it — which is most of the time, but not always, because launch addresses are CREATE2 and effectively random. **Compute the ordering per launch.** Assuming USDC is always `currency0` will silently invert `zeroForOne` on some tokens and swap the wrong direction. ```ts const memecoinIsCurrency0 = token.toLowerCase() < usdc.toLowerCase(); const currency0 = memecoinIsCurrency0 ? token : usdc; const currency1 = memecoinIsCurrency0 ? usdc : 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. ## Referrals A referred trade is cheaper for the trader **and** pays the referrer: - the trader pays a discount on the standard fee, - the referrer receives a share of what is paid, - 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 const owed = await client.readContract({ address: FEE_ESCROW, abi: escrowAbi, functionName: "balanceOfToken", args: [account, USDC], }); if (owed > 0n) { await walletClient.writeContract({ address: FEE_ESCROW, abi: escrowAbi, functionName: "claimToken", args: [USDC], }); } ``` > [!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 on Arc are the 6-decimal USDC ERC-20, so `claimToken` is the path. 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. --- # Contracts Every deployed contract: functions, events and errors. ## FociV2LaunchFactory `0xa93f9CeFD92A77e1EAffa3246B6F4DB91a5c5659` ### 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 FociV2LaunchAndBuy 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). 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). **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` | --- ## FociV2BondingCurve 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 quote asset (USDC). `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 quote asset (USDC). 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 IFociV2BondingCurve. **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` | --- ## FociV2LauncherToken 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` | --- ## FociV2LaunchAndBuy `0xE4A165a52A6033ee0C1b3B6dA83f72AbA1707349` ### 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() + quoteIn — ONE approval covers both of launchFeeToken() (USDC). 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` | --- ## FociV2MemeHook `0x527129aB10Fa3163629A32c4E3Ab7a98ba28e044` ### 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` | --- ## FociV2FeeEscrow `0xcecCFebAcaDCd9404Ae70b160305Af07D9760A46` ### 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 on Arc are the 6-decimal USDC ERC-20, so this is the path, 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` | --- ## FociV2ReferralRegistry `0xEB286974C35d2741B0fe9b2a1Cd41E53d06aE406` ### 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` | --- ## FociV2LaunchLocker `0xe047D0F0ce0dD600732793762B1f1929Adc5015d` ### 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` | --- ## FociV2LaunchDeployer `0x5C5c202271E1300bD5Ce43A4F5C1cEA8efd57B63` ### 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` | --- ## FociV2GraduationExecutor `0x02e7d818080bf85EEB7191536A768B0CFdB38D5D` ### 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` | --- # Errors Every custom error across every contract. When a transaction reverts with a bare selector, look it up here. | Selector | Error | Declared by | |---|---|---| | `0xe6a0d45f` | `AlreadyGraduated()` | FociV2BondingCurve | | `0x0dc149f0` | `AlreadyInitialized()` | FociV2BondingCurve, FociV2LaunchLocker | | `0x7aabdfe3` | `AlreadyReferred()` | FociV2ReferralRegistry | | `0x3a81d6fc` | `AlreadyRegistered()` | FociV2MemeHook | | `0xa741a045` | `AlreadySet()` | FociV2LaunchFactory, FociV2MemeHook | | `0x49e55bcb` | `CombinedFeeTooHigh()` | FociV2LaunchFactory | | `0x85258712` | `CoreLpFeeMustBeZero()` | FociV2LaunchFactory | | `0x4ca249dc` | `Create2EmptyBytecode()` | FociV2LaunchDeployer | | `0x9ad465dc` | `CreatorTaxTooHigh()` | FociV2LaunchFactory | | `0x4e222a24` | `CurveFeeTooHigh()` | FociV2LaunchFactory | | `0x025ac17e` | `CurveGraduated()` | FociV2BondingCurve | | `0x95e32dab` | `CurveNotQuotable()` | FociV2LaunchFactory | | `0xfb8f41b2` | `ERC20InsufficientAllowance(address,uint256,uint256)` | FociV2LauncherToken | | `0xe450d38c` | `ERC20InsufficientBalance(address,uint256,uint256)` | FociV2LauncherToken | | `0xe602df05` | `ERC20InvalidApprover(address)` | FociV2LauncherToken | | `0xec442f05` | `ERC20InvalidReceiver(address)` | FociV2LauncherToken | | `0x96c6fd1e` | `ERC20InvalidSender(address)` | FociV2LauncherToken | | `0x94280d62` | `ERC20InvalidSpender(address)` | FociV2LauncherToken | | `0xb06ebf3d` | `FailedDeployment()` | FociV2LaunchDeployer | | `0x4033e4e3` | `FeeTransferFailed()` | FociV2LaunchFactory, FociV2GraduationExecutor | | `0xd43cabc3` | `GraduationExecutorNotSet()` | FociV2LaunchFactory | | `0xbdcd75af` | `GraduationRescueTooEarly(uint256)` | FociV2LaunchFactory | | `0x2c37d0eb` | `GraduationSeedNotViable()` | FociV2LaunchFactory | | `0x6d3bcfe5` | `GraduationStillViable()` | FociV2LaunchFactory | | `0x0a85dc29` | `HookNotImplemented()` | FociV2MemeHook | | `0x197001d6` | `InexactQuoteTransfer(address,uint256,uint256)` | FociV2MemeHook | | `0x495a9962` | `InexactTransfer(address,uint256,uint256)` | FociV2LaunchFactory | | `0xcf479181` | `InsufficientBalance(uint256,uint256)` | FociV2FeeEscrow, FociV2LaunchDeployer | | `0x098fb561` | `InsufficientInputAmount()` | FociV2BondingCurve | | `0xbb55fd27` | `InsufficientLiquidity()` | FociV2BondingCurve | | `0x42301c23` | `InsufficientOutputAmount()` | FociV2BondingCurve | | `0x31cdb504` | `InternalSwapRequiresOperator()` | FociV2MemeHook | | `0x800c7e91` | `InvalidBasisPoints()` | FociV2LaunchFactory | | `0xc6cc5d7f` | `InvalidBps()` | FociV2MemeHook | | `0x7a34030f` | `InvalidFeePolicy()` | FociV2BondingCurve | | `0x2bb8bdd6` | `InvalidGraduationThreshold()` | FociV2LaunchFactory | | `0x68b42c59` | `InvalidLaunchConfigId()` | FociV2LaunchFactory | | `0xbc0ecfe3` | `InvalidLaunchEconomics()` | FociV2BondingCurve | | `0x2b7ad4f8` | `InvalidPhantomQuote()` | FociV2LaunchFactory | | `0xc256622b` | `InvalidPoolKey()` | FociV2MemeHook | | `0x270815a0` | `InvalidTickSpacing()` | FociV2LaunchFactory | | `0x374852ca` | `InvalidTokenParams()` | FociV2LaunchFactory | | `0xa8b63076` | `LaunchConfigDisabled()` | FociV2LaunchFactory | | `0x1de25df3` | `LaunchDependenciesNotWired()` | FociV2LaunchFactory | | `0x57332dcf` | `LaunchDeployerNotSet()` | FociV2LaunchFactory | | `0xecb27319` | `LaunchEconomicsMismatch(bytes32,bytes32)` | FociV2LaunchFactory | | `0x52660db0` | `LaunchFeeTokenNotSet()` | FociV2LaunchFactory | | `0x85b8e2f4` | `MetadataTooLong()` | FociV2LaunchDeployer | | `0x3672d25f` | `MinimumOutputRequired()` | FociV2MemeHook | | `0xeee66814` | `MintAmountOverflow()` | FociV2GraduationExecutor | | `0xbc760cfe` | `NativeValueMismatch(uint256,uint256)` | FociV2BondingCurve, FociV2LaunchAndBuy | | `0xc2caa2a6` | `NoBalance()` | FociV2FeeEscrow | | `0xa3fef2f8` | `NoPendingChange()` | FociV2LaunchFactory | | `0x502ba015` | `NotApprovedLauncher()` | FociV2LaunchAndBuy | | `0xbe447ef2` | `NotAuthorizedBinder()` | FociV2ReferralRegistry | | `0xb9f93944` | `NotCreatorFeeRecipient()` | FociV2LaunchFactory | | `0x32cc7236` | `NotFactory()` | FociV2BondingCurve, FociV2MemeHook, FociV2LaunchLocker, FociV2LaunchDeployer, FociV2GraduationExecutor | | `0x8d42130c` | `NotFeeSweepOperator()` | FociV2BondingCurve, FociV2MemeHook | | `0xc2074c46` | `NothingToGraduate()` | FociV2LaunchFactory | | `0x00f6b210` | `NothingToRescue()` | FociV2MemeHook | | `0x87138d5c` | `NotInitialized()` | FociV2BondingCurve | | `0xea9eaa96` | `NotLaunchForwarder()` | FociV2LaunchFactory | | `0xae18210a` | `NotPoolManager()` | FociV2MemeHook | | `0x20fdc658` | `NotPositionManager()` | FociV2LaunchLocker | | `0xffa32558` | `NotReadyToGraduate()` | FociV2LaunchFactory, FociV2BondingCurve | | `0x584a7938` | `NotWhitelisted()` | FociV2LaunchFactory | | `0x1e4fbdf7` | `OwnableInvalidOwner(address)` | FociV2LaunchFactory, FociV2LaunchAndBuy, FociV2MemeHook, FociV2LaunchLocker | | `0x118cdaa7` | `OwnableUnauthorizedAccount(address)` | FociV2LaunchFactory, FociV2LaunchAndBuy, FociV2MemeHook, FociV2LaunchLocker | | `0x2fab92ca` | `OwnershipCannotBeRenounced()` | FociV2LaunchFactory, FociV2MemeHook, FociV2LaunchLocker | | `0x4e3de34f` | `PairTokenDecimalsMismatch(uint8,uint8)` | FociV2LaunchFactory | | `0xe43c14ca` | `PairTokenDecimalsUnavailable()` | FociV2LaunchFactory | | `0x764c63c8` | `PairTokenEconomicsInvalid()` | FociV2LaunchFactory | | `0x49285dfb` | `PairTokenNotApproved()` | FociV2LaunchFactory | | `0x26fbfa60` | `PairTokenValidationFailed()` | FociV2LaunchFactory | | `0xfe3099b6` | `PositionAlreadyLocked()` | FociV2LaunchLocker | | `0x6b49c94a` | `PositionNotHeld()` | FociV2LaunchLocker | | `0xb6ea0b01` | `ReciprocalReferral()` | FociV2ReferralRegistry | | `0x3ee5aeb5` | `ReentrancyGuardReentrantCall()` | FociV2LaunchFactory, FociV2BondingCurve, FociV2LaunchAndBuy, FociV2MemeHook, FociV2FeeEscrow | | `0xf0c49d44` | `RefundFailed()` | FociV2LaunchAndBuy | | `0xa8ce4432` | `SafeCastOverflowedIntToUint(int256)` | FociV2MemeHook | | `0x24775e06` | `SafeCastOverflowedUintToInt(uint256)` | FociV2MemeHook | | `0x5274afe7` | `SafeERC20FailedOperation(address)` | FociV2LaunchFactory, FociV2BondingCurve, FociV2LaunchAndBuy, FociV2MemeHook, FociV2FeeEscrow, FociV2LaunchLocker, FociV2GraduationExecutor | | `0x55e8f70e` | `SelfReferral()` | FociV2ReferralRegistry | | `0x71c4efed` | `SlippageExceeded(uint256,uint256)` | FociV2BondingCurve, FociV2MemeHook, FociV2GraduationExecutor | | `0x582157bb` | `SqrtPriceOutOfBounds()` | FociV2LaunchFactory | | `0xacb9fa2b` | `SupplyTooHigh()` | FociV2LaunchFactory | | `0xc0b4e373` | `SupplyTooLow()` | FociV2LaunchFactory | | `0xb79d40e8` | `TimelockExpired(uint256)` | FociV2LaunchFactory | | `0x810c4f2a` | `TimelockNotElapsed(uint256)` | FociV2LaunchFactory | | `0xcbdb7b30` | `TokenNotFound()` | FociV2LaunchFactory | | `0x90b8ec18` | `TransferFailed()` | FociV2BondingCurve, FociV2FeeEscrow | | `0xe0aeda7d` | `UnexpectedNativeValue()` | FociV2BondingCurve | | `0xf7139e33` | `UnknownPool()` | FociV2MemeHook | | `0xdd737e7c` | `UnsupportedPrice()` | FociV2LaunchFactory | | `0x9465dbd4` | `WrongGraduationPhase()` | FociV2LaunchFactory | | `0xd92e233d` | `ZeroAddress()` | FociV2LaunchFactory, FociV2BondingCurve, FociV2LauncherToken, FociV2LaunchAndBuy, FociV2MemeHook, FociV2FeeEscrow, FociV2ReferralRegistry, FociV2LaunchLocker, FociV2LaunchDeployer, FociV2GraduationExecutor | | `0x1f2a2005` | `ZeroAmount()` | FociV2LaunchFactory, FociV2BondingCurve, FociV2LaunchAndBuy | --- # Events Every event with its `topic0`, for indexers. | topic0 | Event | Contract | |---|---|---| | `0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925` | `Approval(address,address,uint256)` | FociV2LauncherToken | | `0xe2cd2f31ebc05ec28640102987f4c8fc5f20e269e1b3aa82577f3f2f0e35c7c6` | `AutoGraduationFailed(address,uint256)` | FociV2BondingCurve | | `0x2cbe77dadc7f8418071409bebfd71778263eecb998af52aa5c9e27b995a71676` | `AutoSeedFailed(address,uint256)` | FociV2BondingCurve | | `0xd8138f8a3f377c5259ca548e70e4c2de94f129f5a11036a15b69513cba2b426a` | `Claimed(address,uint256)` | FociV2FeeEscrow | | `0xdbc1ea3a8459e4c7e11fb385b52bbb5cc8c8ab85eec5d883ac9aa78c171f5141` | `ClaimedToken(address,address,uint256)` | FociV2FeeEscrow | | `0xbe2de91c1cbef653c760573fff8355c0c851d35ed2a898342b4db556301cccf4` | `CreatorFeeRecipientChangeCancelled(address,address)` | FociV2LaunchFactory | | `0x7f119e44c84a715429bee60d30ad2e14afdef6c60bb1a7eaa01290ecf6d1b2e5` | `CreatorFeeRecipientChangeProposed(address,address,address,uint256,uint256)` | FociV2LaunchFactory | | `0x308c390ed1ab5873392818e036cabdf408bc8ad042fbaead3108954ff75ba980` | `CreatorFeeRecipientUpdated(address,address,address)` | FociV2LaunchFactory | | `0x2cc664e1ac1e2d05c0d4637bb63ec8189113b6ac39276be8977e26216a8cdd19` | `CreatorFeeRecipientUpdated(address,address)` | FociV2BondingCurve | | `0xb45e6b72a7de9a2077babe9717744436f3880e114099956ca85f91a77469a532` | `CreatorFeeRecipientUpdated(bytes32,address,address)` | FociV2MemeHook | | `0x4e45da441832cf53bdaa69235704fc0575e68210f459ee1562911024b12967d5` | `Credited(address,address,uint256)` | FociV2FeeEscrow | | `0x5d104c62f50449fadfe6f4013c8f36588d32737f94b5ac9b83ddad33b3e1ffdf` | `CreditedToken(address,address,address,uint256)` | FociV2FeeEscrow | | `0xec36bf571f136799e8dc0b0b8bea4b04d8bd3d43de838aab0d5fc21d4cbfc455` | `CurveBuy(address,address,uint256,uint256,uint256,uint256)` | FociV2BondingCurve | | `0xa69e8258ccc7b9bbb70ab953fc2d1062b4ee28b8ca827534097e1732e87b0262` | `CurveBuyRefunded(address,uint256)` | FociV2BondingCurve | | `0xf8d37a90738ae063b8b8058b66f5880cf3cf7ab0c5d4fa78219696591dfbfb67` | `CurveCompleted(address,uint256,uint256)` | FociV2BondingCurve | | `0x8113d738abdcb6b38357e9d53a54a7157861a09031b453651f0fe7fe151f59df` | `CurveSell(address,address,uint256,uint256,uint256,uint256)` | FociV2BondingCurve | | `0x1edf3afd4ac789736e00d216cd88be164ddcef26a6eedcc30cdb0cb62f3741b1` | `FactorySet(address)` | FociV2MemeHook | | `0x1edf3afd4ac789736e00d216cd88be164ddcef26a6eedcc30cdb0cb62f3741b1` | `FactorySet(address)` | FociV2LaunchLocker | | `0x6460dc5c867a0678a8bcc5e64f629fae539901c53a4a8b42fe21d7a6c5e6437d` | `FeesRescued(address,address,uint256,uint256)` | FociV2BondingCurve | | `0xaf739f46ca7a23c9f259838ec2c5249acf4e1cf9fe68a46f77c3dfa452eda605` | `FeesSwept(uint256,uint256)` | FociV2BondingCurve | | `0xae994ca926e252e299c3df7516cb609272a57bf80b0e0715297e55939f873420` | `FeeSweepOperatorUpdated(address)` | FociV2MemeHook | | `0x667636bce2491e3f246c8b4ec1f4ca0be227dfa611d0575c59f5949283b433c1` | `GraduationDustRetained(address,address,uint256)` | FociV2GraduationExecutor | | `0x80a5a2ff8b8c5533e5862e4e161bbcade9af6fd9d67bef56a590b062107f027f` | `GraduationDustSwept(address,address,uint256)` | FociV2GraduationExecutor | | `0xac04674474e93058fae25e6df5dd94f57cdcacfe560a182a2eefc8c6006fbf6f` | `GraduationExecutorSet(address)` | FociV2LaunchFactory | | `0xa0a18f5bf205becee8b268d7cf69addab8548ae8ef361791464cf0e0e17c1361` | `GraduationTokensPermanentlyLocked(address,uint256)` | FociV2LaunchFactory | | `0xaea8b8d37d8110dd00c418d9c1c268f0fbadacb802c284b71a1777e411cd965a` | `HookFeeBpsUpdated(uint256)` | FociV2MemeHook | | `0xc532c43b3423e14ef72748f1c8291238829ca0af8ba9b67975ad1483485a4b4d` | `HookFeeCollected(bytes32,address,uint256,uint256)` | FociV2MemeHook | | `0x908408e307fc569b417f6cbec5d5a06f44a0a505ac0479b47d421a4b2fd6a1e6` | `Initialized(address)` | FociV2BondingCurve | | `0xedd96c570c6e5ef9add0378e59df53579a283889dc5dab6440ef6eca2ee6c8ce` | `LaunchConfigAdded(uint256)` | FociV2LaunchFactory | | `0x2f8ba78ae68cfd0c82c7756c540eaf4eead3341aef9ccebcb91d546bff10d62b` | `LaunchConfigUpdated(uint256)` | FociV2LaunchFactory | | `0xd5ea7aa3e328a0594dcf6914cd9e5369779efaa194ee4dd4c5afcad4f4ebbb0c` | `LaunchDeployerSet(address)` | FociV2LaunchFactory | | `0xdcacba5e347ae7abd91cb519eb877af8fa7774e347b85dd3ddcd24a2ba8cdf37` | `Launched(address,address,address,address,uint256,uint256)` | FociV2LaunchAndBuy | | `0x4f1ea5016c51c2f82324e00e9b8a4a95ee5aeaa10c653dabaec5f1bc9047ba0b` | `LaunchEnabledUpdated(bool)` | FociV2LaunchFactory | | `0xd0766d3f1431146228fe8edef25f27842a1669c91d46e4af1b73405b354489a5` | `LaunchFeeUpdated(address,uint256)` | FociV2LaunchFactory | | `0x52c1a28345695afc7f6b7629133124dec5d61ee745affd65e4fd2a776bc05840` | `LaunchForceSwept(address)` | FociV2LaunchFactory | | `0x56b32d3633fed72f97c4df44a78b5fa04f1d662d4bddebcd8a9b216d26d093ad` | `LaunchForwarderSet(address)` | FociV2LaunchFactory | | `0x7017304fdd491394686dce984eac721f0be1a22228346210f16694772bde44ca` | `LaunchGraduationRescued(address,address,uint256,uint256)` | FociV2LaunchFactory | | `0xcdb72f157fd3666758a6ce201387ffb52038c7562e4fff352828da1096c4b6b4` | `LaunchSwept(address,uint256,uint256)` | FociV2LaunchFactory | | `0x3e99ceb3e222d2214d53dacca902810db845f156f78152fdc076be628c4e9a40` | `MaxCreatorTaxUpdated(uint256)` | FociV2LaunchFactory | | `0x6968b68c1fb468c8b257b012290bf803a6a6d7e79468e0326050724f7573cf01` | `MaxInternalPriceImpactUpdated(uint256)` | FociV2MemeHook | | `0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0` | `OwnershipTransferred(address,address)` | FociV2LaunchFactory | | `0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0` | `OwnershipTransferred(address,address)` | FociV2LaunchAndBuy | | `0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0` | `OwnershipTransferred(address,address)` | FociV2MemeHook | | `0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0` | `OwnershipTransferred(address,address)` | FociV2LaunchLocker | | `0x38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e22700` | `OwnershipTransferStarted(address,address)` | FociV2LaunchFactory | | `0x38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e22700` | `OwnershipTransferStarted(address,address)` | FociV2LaunchAndBuy | | `0x38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e22700` | `OwnershipTransferStarted(address,address)` | FociV2MemeHook | | `0x38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e22700` | `OwnershipTransferStarted(address,address)` | FociV2LaunchLocker | | `0x060d1992d069dc524985f328329aae36102a017c59733c5c91fc0691ee0703b6` | `PairTokenApprovalUpdated(address,bool)` | FociV2LaunchFactory | | `0x67d517ee0e305d608b8410ddef27bbd2ed964d843d9b936e84ea2ad1bd65e5d1` | `PairTokenEconomicsUpdated(address,uint256,uint256,uint8)` | FociV2LaunchFactory | | `0xeed2d18eb96f3c2cb8c7b6993512a506c170e17d29355f2d7a0d5961f338de09` | `PoolConversionSkipped(bytes32,uint256)` | FociV2MemeHook | | `0x0fbb28f9c335f55dcc5cc19e595ab55f9e6a0fd1b58ad77be3a98f99901daaff` | `PoolFeesRescued(bytes32,address,uint256,uint256)` | FociV2MemeHook | | `0x2b33b68d948eb789fd57906bde3dc24d9f748c1df9b98d7359910f6aa1c06e2f` | `PoolFeesSwept(bytes32,uint256,uint256)` | FociV2MemeHook | | `0x0a44ef75df69c534f43cd6c1aa3ef8983065fe5fe79ef9e79f6494e6f258c259` | `PoolGraduated(address,uint256,uint256,uint256)` | FociV2LaunchFactory | | `0x01bf263a1db1652580721573296e1a1fa70b3d4c87f61d02a69c4e1109d2d573` | `PoolRegistered(bytes32,address,address,address)` | FociV2MemeHook | | `0x2cabb2a2973327d5863ceb4707e9441851243897e86d587ee35943599752eb54` | `PositionLocked(address,uint256)` | FociV2LaunchLocker | | `0xc1b5345cce283376356748dc57f2dfa7120431d016fc7ca9ba641bc65f91411d` | `ProtocolFeeRecipientUpdated(address)` | FociV2MemeHook | | `0x4d1fc9430e27afb14db15169fd1c79e8b51773302919ac8c049f1c41995e380b` | `ProtocolFeeShareUpdated(uint256)` | FociV2MemeHook | | `0xe0f45d08835a6839e8d2327d73ee817a6da7276c4776e77abe76eaa524bc92ef` | `ReferralDiscountUpdated(uint256)` | FociV2MemeHook | | `0x7ca75a36687fd0a9628cbb8d737989015c0e5236d128a986b078ed14a030fe81` | `ReferralFeeAccrued(bytes32,address,address,uint256)` | FociV2MemeHook | | `0x646dbd2d0dbd68fc66a49d8c448dd995f308238033d47b3c6122f637b331bfdb` | `ReferralFeeClaimed(address,address,uint256)` | FociV2MemeHook | | `0xde9bddf476dde28b26de9d0b38bb9811ebb9d4945cd0c7feadd215c28fe09717` | `ReferralFeePaid(address,address,uint256)` | FociV2BondingCurve | | `0xcf7381fd801bfc0e3e6a57a711e8165131a80c69051919ee96c9896cd87c0c11` | `ReferralRegistrySet(address)` | FociV2MemeHook | | `0x7c13f976b8efb8331f00ce07146b8270d065e3789f2eabeab837c72ca942ad61` | `ReferralShareUpdated(uint256)` | FociV2MemeHook | | `0x5b6dcb011725a9616ecced5408efb270f5e20477283b63e06c8b4eb0b4da4296` | `ReferrerBound(address,address,address)` | FociV2ReferralRegistry | | `0x3af790fafda720819b2fc6e15090606e81154e0ac9a92d38ecad006d99d20ecc` | `Rescued(address,address,uint256)` | FociV2LaunchAndBuy | | `0x8d4aad4953d0ca700d468f3753aa14432d1b35b43ec6409f051fb6aa43a89607` | `TokenLaunched(address,address,address,address,uint256,uint256)` | FociV2LaunchFactory | | `0xaf33c4aba92959b3e7ddc83ab728938262da159a6c05ca836f6c46f9bcb2c740` | `TokenSupplyLocked(address,uint256)` | FociV2LaunchLocker | | `0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef` | `Transfer(address,address,uint256)` | FociV2LauncherToken | | `0xef2b562a67f01ed4b7c4265ec09b539039c6d5dd7e752191d3940508c3dc0068` | `WhitelistedLauncherUpdated(address,bool)` | FociV2LaunchFactory |