Hooks
Hooks and permissions
What a hook is called with, what it may answer, and how a pool's permission mask decides both.
A hook is a Solana program that Socket calls by cross-program invocation at fixed points of a pool's life. Each pool names at most one hook program and a six-bit permission mask when it is created. Neither ever changes.
Hook points#
There are four phases. Each is called only if the mask has its bit:
| Phase | Value | Bit | Called |
|---|---|---|---|
| Before swap | 0 | 1 | Before the swap's math, with the pool's price, tick and liquidity as they are. |
| After swap | 1 | 2 | After the math and before settlement, with the amounts and the new price. |
| Before liquidity | 2 | 4 | Before a deposit or withdrawal changes a position. |
| After liquidity | 3 | 8 | After the position has changed. |
The bit for a phase is 1 << phase. The liquidity phases cover both deposits and withdrawals: the call's liquidity_delta is positive for a deposit and negative for a withdrawal.
Powers#
Two more bits decide what a reply may change:
| Bit | Power | Allowed in | Bound |
|---|---|---|---|
| 16 | Fee override | Before swap | fee_ppm up to the pool's max fee |
| 32 | Input surcharge | After swap | input_delta up to amount_in × max_delta_bps / 10,000 |
Fee override only means something with before swap, and input surcharge only with after swap. Socket refuses to create a pool whose mask grants a power without its phase.
Try a mask:
Built-in hooks and the bits they need#
Socket checks the mask when a pool is created with a built-in hook. A mask may include more bits than the hook needs, but never fewer.
| Hook | Needs | Mask |
|---|---|---|
| No-op | nothing | 0 |
| Dynamic fee | before swap, after swap, fee override | 19 |
| TWAP oracle | before swap, after swap | 3 |
| Range order | both swap and both liquidity phases | 15 |
The call#
Each call is one instruction to the hook program, Callback(HookCall), Borsh-encoded, with these accounts:
- The pool account, read-only.
- The pool's hook authority, a Socket PDA (
["hook-authority", pool]) that signs the call. It proves the call came from Socket for this pool. - The hook's extra accounts, in the order and with the write access recorded at creation.
The hook never receives a vault, a position account, a token account or the user's signature. Everything it needs to know about the operation is in the call:
| Field | Type | Meaning |
|---|---|---|
version | u8 | ABI version, currently 1. |
phase | u8 | 0 to 3, as above. |
a_to_b | bool | Swap direction. |
exact_in | bool | Whether the swap fixes its input. |
amount | u64 | The amount the swap asked for. |
amount_in, amount_out | u64 | After swap: the swap's CLMM input and output. After liquidity: the token A and token B amounts deposited or withdrawn. Zero otherwise. |
sqrt_price | u128 | Q64.64 square root of the B/A price in atomic units. |
tick | i32 | The pool's current tick. |
liquidity | u128 | The pool's active liquidity. |
timestamp | i64 | The cluster Clock's Unix time. |
base_fee_ppm, max_fee_ppm | u32 | The pool's base fee and max fee. |
liquidity_delta | i128 | Liquidity being added (+) or removed (−). |
lower_tick, upper_tick | i32 | The position's range. |
position, position_owner | [u8; 32] | The position and its owner. |
position_liquidity | u128 | The position's liquidity before the change, or after it in the after phase. |
The reply#
The hook answers with Solana return data, a Borsh HookReply of at most 64 bytes:
pub struct HookReply {
pub version: u8, // 1
pub phase: u8, // must equal the call's phase
pub fee_ppm: Option<u32>, // before swap, with permission 16
pub input_delta: u64, // after swap, with permission 32
}Socket accepts the reply only if the return data was set by the pool's own hook program, decodes exactly with no trailing bytes, carries the right version and phase, and stays inside the pool's bounds. Anything else fails the transaction. A hook that wants to refuse an operation simply returns an error.
External hooks#
A pool may name any executable program as its hook. Socket enforces the same bounds on all of them, but bounds are not a review: within them, a hook can still charge its full surcharge, raise the fee to the cap, or refuse swaps. Routers and the Socket API route only through hook programs on their allowlist, and the app labels hooks outside the built-in program as External hook in mustard.
Next: Bounds lists every limit and the error it produces.