Chapter 04 · Swaps

Swaps

The Router path, exact-input only, gas in USDC, and the beforeSwap / afterSwap fee pipeline.

Trades go through the Uniswap v4 PoolManager like any other pool, but the hook runs before and after every swap. Only exact-input swaps are allowed. On a buy the hook skims its fees from the quote input before the AMM sees it; on a sell it skims from the quote output. Either way the fee is on the quote side and the event that describes the trade is HookSwap. Gas is paid in native USDC and a swap costs a few thousandths of a dollar; with 0.5 s blocks the trade is final before the wallet's spinner stops.

The Router

Router.buy(key, quoteIn, minOut, recipient, deadline) and Router.sell(key, tokensIn, minOut, recipient, deadline) are the two entry points the app uses. The router:

  • refuses any key whose hook is not the Hookarc hook or whose pool is not live (UnknownPool), and any expired deadline or zero recipient;
  • pulls the input with transferFrom: USDC or cirBTC on a buy, the token on a sell (approve the router first). The native-value path exists for an address(0) quote, but no such quote is registered, so msg.value is never needed for a trade;
  • calls poolManager.unlock and, inside the callback, swap with a negative amountSpecified (exact input) and the widest possible price limit;
  • settles the consumed input, takes the output to the recipient, and refunds any input the pool did not consume (a partial fill against the price limit);
  • stamps abi.encode(account, recipient) into hookData. The hook honours that only from a trusted router; otherwise it labels the trade with tx.origin.

You can swap without the router. The hook enforces the same rules on any caller; the router only adds slippage protection, refunds and attribution.

Swap pipelineA buy pays its fees before the AMM sees the input; a sell pays on the quote it receives. Both end in one HookSwap event.
hook.beforeSwaphook.afterSwapyessellbuybuysellRouter.buy / Router.sell_validate(key)key.hooks == hook · hook.isLive(id)UnknownPool · Expired · ZeroRecipientpull the inputUSDC / cirBTC / token via transferFrompoolManager.unlock → swap()exact input · hookData(account, recipient)live · unlockedexact input?PoolNotLiveLaunchLockedExactOutputNotSupportedbuy?zeroForOne == quoteIs0sell → zero deltaHaltGuardregistry.quoteHaltedQuoteHaltedAntiSnipe capboughtInBlock + in > capOverSnipeCap_buyFees · mint claimsprotocol · creator · potNthBuyPotone advance per blockpool swap math (v4 core)input net of the hook deltabuy: AutoBurn share of tokensOuttake from PoolManager → LaunchToken.burnsell: 30 + 50 bps of quote outrounded up · capped · mint claimsHookSwap(id, trader, recipient, …)settle / take → refund unconsumed input

beforeSwap

The hook runs _beforeSwap for every swap on a Hookarc pool:

  1. The pool must be registered and seeded, the launch lock must be clear, and amountSpecified must be negative (exact input). Otherwise PoolNotLive, LaunchLocked or ExactOutputNotSupported.
  2. A buy is a swap in the direction that pays quote for token: zeroForOne == quoteIsCurrency0. A sell returns immediately with a zero delta; its fee is taken in afterSwap.
  3. For a buy, the blocks run in a fixed order: HaltGuard (may revert QuoteHalted), then AntiSnipe (may revert OverSnipeCap), then the fee split, then the Nth-buy pot. See Blocks.
  4. The fee total is minted to the hook as ERC-6909 claims on the quote and booked into protocolFees[quote], creatorFees[poolId] and pot[poolId]. The hook returns a BeforeSwapDelta equal to the fee, so the AMM swaps quoteIn − fee.
Buy-side fee splitFees are skimmed from the quote input in beforeSwap and minted to the hook as ERC-6909 claims. Only the remainder reaches the AMM.
quoteIn (exact input)protocol · 30 bpsrounded upcreator · 50 bpsrounded up+ snipe taxwindow only · to creatorpot share · potBpsNthBuyPot onlyremainder → AMMthe swap inputprotocolFees[quote]claims on PoolManagercreatorFees[poolId]claims on PoolManagerpot[poolId]claims on PoolManagertokensOutfrom the poolAutoBurn burnBpsof tokensOut · afterSwapDust branchIf the fees would reach or exceed the input,the whole input splits 30 : 50 into protocoland creator, and no pot share is taken.

_buyFees is a view. _beforeSwap books it and _afterSwap reports it, and because the same function is used for both with the same inputs, the ledgers and the event cannot disagree within one transaction. Rounding is up for the two base fees. If the fees would reach or exceed the input (dust buys), the entire input becomes fee, split 30 : 50 between protocol and creator, and no pot share is taken.

afterSwap

  • Buy: the token output is known now. AutoBurn, if enabled, takes burnBps of it from the PoolManager and burns it through LaunchToken.burn; the burned amount is returned as the hook's positive delta so accounting nets to zero. The event reports quoteAmount = quote actually consumed by the pool + fee (not the requested input, which a partial fill would overstate), tokenAmount = tokensOut − burned, and feeQuote.
  • Sell: the quote output is known now. The hook takes 30 bps and 50 bps of it, rounded up and capped at the output, mints them as claims and returns the total as its delta. The event reports the gross quote out, the tokens in and the fee.

Fee size in practice

SituationBuy drag on the quote sideSell drag on the quote side
No blocks0.80 %0.80 %
Nth-buy pot at 0.5 %1.30 %0.80 %
AntiSnipe window open, 40 % tax40.80 % (+ pot if on)0.80 %

Price impact comes on top and depends on the pool's depth. previewBuyFees(id, quoteIn) returns the exact split for the current block, which is what the swap box uses to show the drag before you sign. The swap box quotes the net output through the canonical V4Quoter (0x8Dc178eFB8111BB0973Dd9d722ebeFF267c98F94), so Halt Guard, Anti-Snipe and Auto Burn are all reflected in the number it shows.

read the code
  • contracts/src/Router.sol buy(), sell(), unlockCallback, refunds
  • contracts/src/HookarcHook.sol _beforeSwap, _afterSwap, _buyFees, _traderAndRecipient
  • contracts/test/HookFees.t.sol fee math in both orientations and the dust branch
  • contracts/test/HookSwapEvent.t.sol event amounts on full and partial fills
  • contracts/test/RouterRefund.t.sol unconsumed input is always returned