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