Skip to content

Hook permissions

In Uniswap v4, a pool can attach one hook contract. The PoolManager decides which hook functions to call by looking at the hook’s address, not at any storage or registry: the lowest 14 bits of the address are permission flags. If a bit is set, the PoolManager calls the matching function; if not, it never does.

This has two consequences:

  • You can read a hook’s permissions from its address alone, offline. That is what GET /v1/hooks/:address/flags and the SDK’s decodeHookAddress do.
  • A hook’s permissions cannot change after deployment, because its address cannot. (Its logic can, if it is an upgradeable proxy. See Reading a hook’s risk profile.)
Bit Value Flag Called
13 0x2000 beforeInitialize Before a pool using the hook is created
12 0x1000 afterInitialize After a pool is created
11 0x0800 beforeAddLiquidity Before liquidity is added
10 0x0400 afterAddLiquidity After liquidity is added
9 0x0200 beforeRemoveLiquidity Before liquidity is removed
8 0x0100 afterRemoveLiquidity After liquidity is removed
7 0x0080 beforeSwap Before every swap
6 0x0040 afterSwap After every swap
5 0x0020 beforeDonate Before a donation to in-range LPs
4 0x0010 afterDonate After a donation
3 0x0008 beforeSwapReturnDelta beforeSwap may return a delta that changes the swap amounts
2 0x0004 afterSwapReturnDelta afterSwap may return a delta charged to or paid to the swapper
1 0x0002 afterAddLiquidityReturnDelta afterAddLiquidity may return a delta charged to the LP
0 0x0001 afterRemoveLiquidityReturnDelta afterRemoveLiquidity may return a delta charged to the LP

The mask over all 14 bits is 0x3fff:

const flags = Number(BigInt(hookAddress) & 0x3fffn)
const hasBeforeSwap = (flags & 0x0080) !== 0

0x4e3468951D49f2EEa976eD0D6e75fFCb44a9a544 ends in ...a544. 0xa544 & 0x3fff = 0x2544, which is:

0x2000 beforeInitialize
0x0400 afterAddLiquidity
0x0100 afterRemoveLiquidity
0x0040 afterSwap
0x0004 afterSwapReturnDelta
------
0x2544

In the SDK:

import { decodeHookAddress, flagsFor, FLAG_BITS } from "@hookkit/sdk"
decodeHookAddress("0x4e3468951D49f2EEa976eD0D6e75fFCb44a9a544").enabled
// ["beforeInitialize", "afterAddLiquidity", "afterRemoveLiquidity", "afterSwap", "afterSwapReturnDelta"]
flagsFor(["beforeSwap", "afterSwap"]) // 0xc0
FLAG_BITS.beforeInitialize // 0x2000

The four *ReturnDelta flags let a hook change token balances, not just observe. Each only makes sense together with the function that returns the delta, so the PoolManager (Hooks.isValidHookAddress) rejects a hook address that sets a *ReturnDelta flag without its base flag:

Flag Requires
beforeSwapReturnDelta beforeSwap
afterSwapReturnDelta afterSwap
afterAddLiquidityReturnDelta afterAddLiquidity
afterRemoveLiquidityReturnDelta afterRemoveLiquidity

A pool cannot be initialized with such a hook. HookKit reports these violations in the errors array of a decode, and the Hook Miner refuses to mine for them (422 invalid_flags).

A pool’s fee is normally a fixed LP fee in hundredths of a bip (3000 = 0.30%). If the key’s fee is the dynamic-fee flag 0x800000 (8388608), the pool’s LP fee is controlled by its hook instead:

  • the hook can set a stored fee with PoolManager.updateDynamicLPFee, and
  • a hook with beforeSwap can override the fee per swap by returning it with the override flag set.

The fee in effect right now is state.lpFee from the Pool API, and list results mark these pools with dynamicFee: true.

Two validity rules tie hooks and dynamic fees together:

  • A pool without a hook cannot use a dynamic fee.
  • A hook with no permission bits set is only valid on a dynamic-fee pool, where its only power is setting the fee. HookKit warns about such addresses in the hook profile.

Because permissions live in the address, deploying a hook means finding a CREATE2 salt that lands on an address with exactly the bits you want. See the Hook Miner.