Create
Documentation
Guides

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 — 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.