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 anaddress(0)quote, but no such quote is registered, somsg.valueis never needed for a trade; - calls
poolManager.unlockand, inside the callback,swapwith a negativeamountSpecified(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)intohookData. The hook honours that only from a trusted router; otherwise it labels the trade withtx.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.
beforeSwap
The hook runs _beforeSwap for every swap on a Hookarc pool:
- The pool must be registered and seeded, the launch lock must be clear, and
amountSpecifiedmust be negative (exact input). OtherwisePoolNotLive,LaunchLockedorExactOutputNotSupported. - 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 inafterSwap. - For a buy, the blocks run in a fixed order: HaltGuard (may revert
QuoteHalted), then AntiSnipe (may revertOverSnipeCap), then the fee split, then the Nth-buy pot. See Blocks. - The fee total is minted to the hook as ERC-6909 claims on the quote and booked into
protocolFees[quote],creatorFees[poolId]andpot[poolId]. The hook returns aBeforeSwapDeltaequal to the fee, so the AMM swapsquoteIn − fee.
_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
burnBpsof it from the PoolManager and burns it throughLaunchToken.burn; the burned amount is returned as the hook's positive delta so accounting nets to zero. The event reportsquoteAmount = quote actually consumed by the pool + fee(not the requested input, which a partial fill would overstate),tokenAmount = tokensOut − burned, andfeeQuote. - 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
| Situation | Buy drag on the quote side | Sell drag on the quote side |
|---|---|---|
| No blocks | 0.80 % | 0.80 % |
| Nth-buy pot at 0.5 % | 1.30 % | 0.80 % |
| AntiSnipe window open, 40 % tax | 40.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.
contracts/src/Router.solbuy(), sell(), unlockCallback, refundscontracts/src/HookarcHook.sol_beforeSwap, _afterSwap, _buyFees, _traderAndRecipientcontracts/test/HookFees.t.solfee math in both orientations and the dust branchcontracts/test/HookSwapEvent.t.solevent amounts on full and partial fillscontracts/test/RouterRefund.t.solunconsumed input is always returned