Hooks
Bounds
Every limit Socket puts on a hook, what happens when a hook crosses it, and the one it cannot enforce.
A pool's bounds are written at creation and checked by Socket on every call. They don't depend on the hook behaving well.
The limits#
| Bound | Set per pool | Range | Crossing it |
|---|---|---|---|
| Max fee | max_fee_ppm | base fee to 100,000 ppm (10%) | 6012 HookFeeCap |
| Surcharge cap | max_delta_bps | 0 to 1,000 bps (10%) of the swap's CLMM input | 6013 HookDeltaCap |
| Hook compute | hook_budget | any positive number of compute units | 6014 HookComputeCap |
| Extra accounts | the account list | at most 8, literal addresses | 6010 InvalidExtra, 6016 ForbiddenAlias |
| Reply shape | — | ≤ 64 bytes, exact Borsh, version 1, same phase, from the hook program | 6011 InvalidReply |
| Reentry | — | the pool is locked while its hook runs | 6003 Locked |
Every crossing fails the whole transaction. Program state, the hook's own writes and token transfers revert together.
Fees#
Before swap, a hook with fee override (16) may reply with fee_ppm. Socket doesn't clamp it: a value above the pool's max fee fails the swap with 6012. The built-in dynamic-fee hook reads max_fee_ppm from the call and never asks for more.
In a pool without fee override, a reply that sets any fee fails the swap with 6012 as well, so those pools always charge their base fee. The same goes for a surcharge from a pool without permission 32: 6013.
Surcharges#
After swap, a hook with input surcharge (32) may reply with input_delta, an extra amount of the input token. It is bounded by floor(amount_in × max_delta_bps / 10,000), where amount_in is the swap's CLMM input before the surcharge.
The swapper consents to it explicitly:
- Exact input: the swap carries
max_total_input, the most the user can be debited including the surcharge. - Exact output: the
thresholdis the most the user will pay, surcharge included.
A surcharge is recorded as owed to the hook (hook_owed_a or hook_owed_b) and stays in the vault, apart from LP principal and fees. Only the pool's creator can withdraw it, with CollectHookFees, and only up to the recorded amount. Negative surcharges (rebates) are not supported.
Compute#
Socket reads the remaining compute before and after each hook call and fails the transaction if the call used more than the pool's hook_budget. The measurement includes the cost of the call itself.
Accounts#
A hook's extra accounts are listed literally when the pool is created: address, owning program and write access, at most eight. Before any call, Socket checks that the accounts passed match the list exactly, that no extra account aliases a core account or another extra, and that none is a signer. A hook can't ask for a different account at run time; dynamic seeds are not supported.
Call depth#
Router → Socket → hook → anything the hook calls uses most of Solana's cross-program call depth. A hook that calls other programs leaves less room for a router above it, and it can't call back into the pool it serves. Keep hooks shallow.
Failure is the safe default#
A hook that panics, returns an error, overspends compute or replies out of bounds doesn't leave a half-done swap. Nothing is charged and nothing moves; the user loses only the network fee. A router's simulation fails on that pool and it can quote another.
That is also how hooks refuse things on purpose. The range order hook rejects swaps that would undo a filled order the same way.