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/flagsand the SDK’sdecodeHookAddressdo. - 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.)
The 14 bits
Section titled “The 14 bits”| 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) !== 0Worked example
Section titled “Worked example”0x4e3468951D49f2EEa976eD0D6e75fFCb44a9a544 ends in ...a544.
0xa544 & 0x3fff = 0x2544, which is:
0x2000 beforeInitialize0x0400 afterAddLiquidity0x0100 afterRemoveLiquidity0x0040 afterSwap0x0004 afterSwapReturnDelta------0x2544In the SDK:
import { decodeHookAddress, flagsFor, FLAG_BITS } from "@hookkit/sdk"
decodeHookAddress("0x4e3468951D49f2EEa976eD0D6e75fFCb44a9a544").enabled// ["beforeInitialize", "afterAddLiquidity", "afterRemoveLiquidity", "afterSwap", "afterSwapReturnDelta"]
flagsFor(["beforeSwap", "afterSwap"]) // 0xc0FLAG_BITS.beforeInitialize // 0x2000The ReturnDelta rule
Section titled “The ReturnDelta rule”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).
Dynamic fees
Section titled “Dynamic fees”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
beforeSwapcan 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.
Mining an address
Section titled “Mining an address”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.
