Ticks: WTF!?

Conversations exploring the latest Uniswap V4 protocols, hooks and industry news.

Every Hook in V4 (and the four that secretly run the show)

By Twade

Hooks turn Uniswap V4 from a protocol into a platform. There are 14 of them - but four are doing most of the interesting work. What each hook does, when it fires, and why the return-delta hooks deserve their own conversation.


A hook in Uniswap V4 is a smart contract that the PoolManager calls at specific points in a pool's lifecycle - before or after swaps, before or after liquidity changes, when a pool is initialised, when donations happen. By attaching a hook to a pool, you change what that pool does without changing the core Uniswap code.

There are 14 hook functions in total. Ten of them are conventional lifecycle hooks - they observe and influence what's happening inside a standard pool. The other four are the return-delta hooks, sometimes called NoOp hooks, and they're where the novel V4 designs live: custom pricing curves, just-in-time liquidity, internal order matching, and any "pool that isn't an AMM" pattern.

This piece walks every hook function: when it fires, what it sees, and what kind of thing you'd build with it. The second half then slows down on the return-delta hooks, because they're the most powerful and the most opaque, and the brief callouts you usually see don't do them justice.

This article assumes you've read The Singleton Switch. Hooks make a lot more sense once you understand the PoolManager and flash accounting underneath them. If you haven't, that's the prerequisite - come back when you're ready.


What a hook actually is

If you've used React, you already have the right mental model. React hooks plug into a component's lifecycle - useEffect runs after render, useState participates in re-renders, and so on. Uniswap hooks are conceptually the same: they plug into a pool's lifecycle at well-defined points and run custom logic alongside the core protocol behaviour.

There's also a Solidity precedent. Older versions of OpenZeppelin's ERC-20 and ERC-721 contracts exposed beforeTransfer / afterTransfer hooks for the same reason - to let you customise behaviour without forking the core contract. That pattern has since moved out of OpenZeppelin's standards, but the design idea is exactly the one V4 uses for pools.

In V4, a single hook contract can implement any subset of the 14 hook functions. It declares which ones it implements through a permissions struct, and the PoolManager calls only the implemented ones at the appropriate moments during pool operations. A pool that has no hook attached behaves exactly like a stock V4 pool with no extensions.

Insight #1

Hooks are permissioned extensions to the standard pool lifecycle. The protocol still runs the show - the hook just gets invited to participate at specific moments. This is the design choice that makes V4 extensible without making it unsafe.


The 14 hooks, grouped

The full list first, grouped by what they hook into:

Initialization - fires once when a pool is created.

  • beforeInitialize
  • afterInitialize

Liquidity changes - fires when LPs add or remove liquidity.

  • beforeAddLiquidity
  • afterAddLiquidity
  • beforeRemoveLiquidity
  • afterRemoveLiquidity

Swaps - fires when someone trades against the pool.

  • beforeSwap
  • afterSwap

Donations - fires when someone donates tokens directly to LPs.

  • beforeDonate
  • afterDonate

Return-delta hooks - modify or override the protocol's accounting. Always used in combination with their lifecycle counterparts.

  • beforeSwapReturnDelta
  • afterSwapReturnDelta
  • afterAddLiquidityReturnDelta
  • afterRemoveLiquidityReturnDelta

Ten lifecycle hooks. Four return-delta hooks. Every interesting V4 design is a specific combination of these.


The lifecycle hooks

We'll walk each one with the same pattern: when it fires, what it sees, what it returns, and a real use case.

beforeInitialize and afterInitialize

These fire when a pool is created - specifically, when initialize is called on the PoolManager for a PoolKey that includes this hook. They run once in a pool's lifetime.

Sees: the PoolKey (token pair, fee, tick spacing, hook address) and the initial sqrtPriceX96.

Use cases:

  • Gate which pools can be created with this hook attached. A launchpad hook might require Token0 to be ETH and Token1 to be the launched token, and revert otherwise.
  • Register the new pool in an external system - for example, a hook that tracks all pools it's attached to in a mapping(PoolId => Metadata).
  • Enforce a specific tick spacing or fee value. Useful when your hook's invariants assume particular pool parameters.

beforeAddLiquidity and afterAddLiquidity

These fire when an LP adds liquidity to a pool. The before variant fires before the protocol updates state; the after variant fires once the position has been written.

Sees: the PoolKey, the ModifyLiquidityParams (tick range, liquidity delta, salt), the sender, and (in after) the BalanceDelta showing how much of each token was pulled from the LP.

Use cases:

  • Whitelist or KYC-gated liquidity provision. Hooks targeting compliance-sensitive markets often check the LP against an allowlist in before.
  • Anti-frontrun protection: detect liquidity additions immediately before known trades.
  • Mint a custom receipt token to the LP based on their position size, for use in external reward systems.
  • Track aggregate liquidity from a specific group of LPs.

beforeRemoveLiquidity and afterRemoveLiquidity

Symmetric to the add hooks. Fire on liquidity withdrawal.

Sees: same parameters as the add hooks, but the liquidityDelta will be negative.

Use cases:

  • Enforce a time-lock on withdrawals - a vesting hook might revert removal attempts before the lock expires.
  • Burn the receipt token minted in afterAddLiquidity.
  • Apply an exit fee or settle external accounting before the LP exits.

beforeSwap and afterSwap

The hooks you'll see most often in hook tutorials. Fire before and after a swap is executed against the pool.

Sees: the PoolKey, the SwapParams (direction, amount, price limit), the sender. afterSwap additionally receives the BalanceDelta from the swap.

Use cases:

  • Dynamic fees. A beforeSwap hook can compute and return a custom LP fee for that specific swap based on volatility, time of day, oracle state, anything.
  • MEV protection. Detect sandwich patterns by inspecting tx.origin history, or apply per-block volume limits.
  • Custom analytics or external state updates that should fire alongside a swap.
  • Reward emissions. afterSwap can mint a points token to the user based on swap volume.

beforeDonate and afterDonate

Donate is V4's "tip the in-range LPs" function. Anyone can call donate on a pool to send tokens directly to LPs whose positions cover the current price, with no swap or liquidity action involved. The donate hooks fire around this.

Sees: the PoolKey, the donation amounts, the sender.

Use cases:

  • Anti-spam: validate the donor against an allowlist.
  • Hook-driven LP reward distributions - a hook accumulating fees internally can call donate and use these hooks to track the distribution event.

Insight #2

The 10 lifecycle hooks let you observe and influence a pool's behaviour, but they can't bypass the protocol. A beforeSwap hook can reject a swap, charge a custom fee, or do its own work alongside the swap - but the core swap maths still runs. To replace the protocol's behaviour, you need the return-delta hooks.


Hook permissions live in the address

Before we get to return-delta hooks, one detail: how the PoolManager knows which of the 14 hook functions a given hook contract implements.

A naive design would store a mapping(address => Permissions) somewhere and look it up on every call. V4 does something more elegant: the permissions are encoded in the hook contract's address itself.

Ethereum addresses are 160 bits (20 bytes) long. V4 reserves the trailing 14 bits - one bit per hook function - to act as the permission flags. A 1 at a given bit position means "this hook implements the corresponding function"; a 0 means "skip the call." The bit assignments are:

BitHook function
13beforeInitialize
12afterInitialize
11beforeAddLiquidity
10afterAddLiquidity
9beforeRemoveLiquidity
8afterRemoveLiquidity
7beforeSwap
6afterSwap
5beforeDonate
4afterDonate
3beforeSwapReturnDelta
2afterSwapReturnDelta
1afterAddLiquidityReturnDelta
0afterRemoveLiquidityReturnDelta

When the PoolManager is about to call a hook function, it checks the appropriate bit in the hook's address. If the bit is 1, it makes the call. If 0, it skips it. No mapping lookup, no extra storage read - just a bitmask check.

The consequence for hook developers: when deploying a hook, you need to land on an address whose trailing bits match the permissions your contract implements. This is typically done by mining a CREATE2 salt - try different salts until one produces an address with the correct trailing bits. The HookMiner utility in v4-hooks-public handles this for you.

An address can lie about what's implemented, in the sense that nothing stops you from deploying to an address with permission bits set but with no actual implementation behind them. If the PoolManager then tries to call one of those phantom functions, the transaction will revert. So while addresses can be cosmetically wrong, in practice this just means broken hooks fail loudly the first time they're used - not a security risk in itself.


The four that secretly run the show

The return-delta hooks are the four functions that modify the protocol's accounting rather than just observing it. They're also called NoOp hooks, because their core power is the ability to make PoolManager do "nothing" (or less than it would have) for part of an operation, while the hook handles things instead.

The four are:

  • beforeSwapReturnDelta - partially or completely bypass the core swap maths
  • afterSwapReturnDelta - extract tokens from a swap's output, or contribute extra
  • afterAddLiquidityReturnDelta - charge or rebate users when they add liquidity
  • afterRemoveLiquidityReturnDelta - charge or rebate users when they remove liquidity

Each must be paired with its lifecycle counterpart. You can't have beforeSwapReturnDelta without beforeSwap. The lifecycle hook is the entry point; the return-delta flag enables the powerful behaviour inside it.

Before we dig into specific examples, we need to introduce two data types that carry the return-delta information.

BalanceDelta vs BeforeSwapDelta

You're probably already familiar with BalanceDelta. It's the type returned from swap and modifyLiquidity on the PoolManager. Its shape is:

BalanceDelta = (amount0, amount1)

A positive value means the user is owed that amount; a negative value means the user owes that amount. The periphery contract that initiated the unlock is responsible for settling these deltas before the transaction can complete.

BeforeSwapDelta is the type returned from beforeSwap when the beforeSwapReturnDelta permission is set. Its shape is different:

BeforeSwapDelta = (amountSpecified, amountUnspecified)

It's expressed in terms of the swap's specified and unspecified tokens, not Token0 and Token1 directly. Which token is "specified" depends on the swap parameters, which brings us to the next thing.

The four swap configurations

A V4 swap is described by two booleans-flavoured parameters:

  • zeroForOne - true if swapping Token0 → Token1, false for Token1 → Token0
  • amountSpecified - negative for "exact input" swaps, positive for "exact output" swaps

These combine into four configurations:

ConfigurationzeroForOneamountSpecifiedSpecified tokenUnspecified token
Exact Input Zero-for-OnetruenegativeToken0 (input)Token1 (output)
Exact Output Zero-for-OnetruepositiveToken1 (output)Token0 (input)
Exact Input One-for-ZerofalsenegativeToken1 (input)Token0 (output)
Exact Output One-for-ZerofalsepositiveToken0 (output)Token1 (input)

The "specified" token is always the one the user is fixing exactly - either "I'm spending exactly this much" (exact input) or "I want exactly this much" (exact output). Everything else is the unspecified side, where the protocol calculates the matching amount.

BeforeSwapDelta carries deltas in this specified/unspecified frame, which is what lets the return-delta logic work cleanly regardless of swap direction.


beforeSwapReturnDelta - the powerful one

Of the four return-delta hooks, beforeSwapReturnDelta is the one that does the most. It lets a hook consume some or all of the swap's input before the core swap maths even runs.

The mechanism, in plain terms: normally, when a swap is initiated, PoolManager calls the pool's internal _swap function with amountSpecified = params.amountSpecified - i.e. the full amount the user wanted to swap. If a hook has beforeSwapReturnDelta enabled, the hook returns a BeforeSwapDelta from its beforeSwap function, and PoolManager adjusts amountToSwap based on what the hook consumed:

  • If the hook returns a BeforeSwapDelta that absorbs the full input, amountToSwap becomes zero, and the internal _swap function exits early without touching the AMM curve. The hook handled the swap entirely.
  • If the hook returns a BeforeSwapDelta that absorbs some of the input, amountToSwap is reduced by that amount, and the remaining input flows through the standard swap logic. Partial bypass.
  • If the hook returns no delta, the swap runs normally.

The hook is responsible for honouring the deltas it returns - if it says "I consumed 1 ETH from the user," then 1 ETH had better end up with the hook, and the user had better receive whatever output the hook promised in exchange. This is settled through the standard BalanceDelta accounting at the end of the unlock cycle.

Insight #3

beforeSwapReturnDelta turns the pool into an optional fallback. The hook gets first refusal on every swap. If it can fill the trade out of its own reserves - at its own price, on its own terms - the AMM curve doesn't even run. This is what makes custom pricing, internal matching, and orderbook-style hooks possible.

A worked example: the internal swap pool

A clean illustration of beforeSwapReturnDelta is what UHI calls the internal swap pool - a hook that accumulates the non-ETH side of swap fees and uses them to fill incoming opposite-direction swaps before they hit the AMM.

The scenario: a launchpad pool for ETH ↔ TOKEN. Most swaps are ETH → TOKEN (people buying), so the LP fees accumulate as TOKEN. Distributing those TOKEN fees to LPs as TOKEN is undesirable - LPs would just sell them, hurting the launch price. The hook would prefer to distribute all fees as ETH.

The hook solves this without an external swap by matching trades against its accumulated TOKEN reserves directly. When a TOKEN → ETH swap comes in:

  1. beforeSwap checks if the hook has accumulated TOKEN fees.
  2. If it does, the hook fills as much of the user's TOKEN → ETH swap as it can from its internal reserves, at the pool's current price.
  3. The hook returns a BeforeSwapDelta that absorbs the consumed input and provides the matching output.
  4. PoolManager reduces amountToSwap accordingly. If the entire trade was filled internally, the core swap doesn't run at all. If only part of it was filled, the rest flows through the AMM normally.
  5. The TOKEN the hook took from the user is converted to ETH on the hook's internal ledger, which the hook can later distribute to LPs as a clean ETH donation.

The net effect: users get the same execution price, LPs receive their fees in ETH only, and the pool isn't impacted by the hook continuously selling TOKEN for ETH on it. It's an elegant pattern that wasn't possible in v3.


afterSwapReturnDelta - adjusting the output

afterSwapReturnDelta is less radical than beforeSwap's version. By the time it fires, the core swap has already run. What afterSwapReturnDelta lets the hook do is adjust the unspecified-token delta that the user will ultimately settle.

Concretely, it can:

  • Take some of the swap's output for itself. A hook can withhold a portion of what would have gone to the user - for example, taking a custom fee in a specific token, regardless of swap direction.
  • Charge the user extra input. Returning a delta that increases what the user owes lets the hook capture additional fees on the way in.
  • Add to the user's output. Less common, but useful for hook-driven rewards or rebates.

A typical use case is custom fee structures. The standard LP fee is charged on the swap and accrues to liquidity providers. A hook with afterSwapReturnDelta can take an additional cut from the unspecified token - for example, always collecting its fee in ETH regardless of which way the user is swapping. That's a small but powerful change for hooks that have downstream constraints on which token they want to handle.


The liquidity-side return deltas

afterAddLiquidityReturnDelta and afterRemoveLiquidityReturnDelta are the symmetric pair to the swap-side return deltas, applied to LP actions instead of swaps.

afterAddLiquidityReturnDelta lets a hook:

  • Charge an extra amount when an LP adds liquidity. This could be a hook-specific deposit fee, a tax that funds a reward pool, or a contribution to a public-goods treasury.
  • Rebate the LP by giving back some of what they deposited.

afterRemoveLiquidityReturnDelta is the mirror - applied to withdrawals. Most often used for:

  • Exit fees that disincentivise short-term LPing in pools where stability matters.
  • Rebates as a reward for long-term LPs.

These two are less commonly used than their swap-side counterparts, but they're the right primitives if your hook design needs custom economics around position lifecycle.


What to be careful about

Return-delta hooks are powerful, which means they have a sharper edge.

Reentrancy and state assumptions. A beforeSwapReturnDelta hook is running inside PoolManager's unlocked state. If the hook itself calls back into PoolManager (for another swap, or to modify liquidity), it's now running nested operations against the same lock. This is supported - the unlock model is built for it - but the hook author needs to be very careful about what state assumptions they're making between the call out and the call back in.

Settlement responsibility. If your BeforeSwapDelta says you absorbed 1 ETH, 1 ETH had better be available to settle. The PoolManager will check at the end of unlockCallback that all deltas net to zero, and if your hook's claimed deltas aren't covered by real token movements (or claim-token mints/burns), the transaction reverts. This is good (the protocol's safety net catches accounting mistakes), but you need to move the tokens, not just claim to.

Gas profile shifts. A hook-driven pool's gas cost depends entirely on what the hook does. A simple beforeSwap that checks an oracle adds a few thousand gas. A beforeSwapReturnDelta that runs its own internal matching loop can add tens of thousands. Always benchmark your hook's worst-case path against the swap baseline.

Hook address mining is non-trivial. As permission bits accumulate, finding a deployment address with the right trailing bits gets slower. Hooks with many permissions can take a meaningful amount of CPU time to mine a CREATE2 salt for. Plan for this in your deployment flow.


Try it yourself

The hook permissions explorer below lets you flip each of the 14 flags and see what your hook can do with that combination, what the resulting address bitmap looks like, and which real V4 patterns use that permission set.

14 Permissions · 1 Address · Many Patterns

The trailing 14 bits of your hook's contract address encode which functions it implements.

Initialization

bit 13
bit 12

Liquidity

bit 11
bit 10
bit 9
bit 8

Swaps

bit 7
bit 6

Donate

bit 5
bit 4

Return Deltas - the powerful ones

bit 3
bit 2
bit 1
bit 0

Address bitmap · trailing 14 bits

0x…0000

What this hook can do

  • Nothing - a stock pool with no extensions.

Matching patterns

    A few combinations to try:

    1. Flip only beforeSwap and watch the bitmap - it's a single 1 in bit 7. This is the simplest swap-influencing hook.
    2. Flip beforeSwap + beforeSwapReturnDelta. Now you've enabled an internal-swap-pool-style hook. The bitmap shows bits 7 and 3 set.
    3. Try the full launchpad pattern: beforeInitialize, beforeSwap, afterSwap, beforeSwapReturnDelta, afterSwapReturnDelta. A handful of bits, a meaningful amount of power.
    4. Flip everything on. Take a second to absorb the address mining problem you've just signed up for.

    Recap

    If you take away three things:

    1. The 10 lifecycle hooks let you observe and influence the pool - they don't replace it. Use them for dynamic fees, analytics, gating, rewards, and any "alongside the protocol" behaviour.
    2. The 4 return-delta hooks let you replace or modify the protocol's accounting. They're how custom pricing curves, JIT liquidity, internal matching, and orderbook-style hooks are built.
    3. Permissions are encoded in the hook contract's address. The trailing 14 bits are the bitmap; the PoolManager reads them directly. You mine a CREATE2 salt to land on the right address at deployment.

    This piece is the catalogue. To see hooks in real use:

    • Hooks in the Wild - a Field Guide to Real V4 Patterns (coming next) - a survey of actual hook patterns being deployed: JIT rebalancing, limit orders, COW, dynamic fees, Nezlobin loss mitigation. Each gets a proper treatment.
    • Hooks.sol in v4-core - github.com/Uniswap/v4-core/blob/main/src/libraries/Hooks.sol. The canonical reference for permission flags and call dispatch.
    • v4-hooks-public - github.com/Uniswap/v4-hooks-public. BaseHook (src/base/) and HookMiner (src/utils/) live here. If you're going to write a hook, start by reading these.

    If a particular hook combination has you stuck, reach out on X and let me know.