Crypto Training

Uniswap v4 Hooks: Secure Design Patterns for Adversarial Integrations

Hooks let you extend AMMs with custom code. They also move your security boundary into a callback. This is how to design hooks that survive reentrancy, MEV, and rounding edge cases.

Crypto Training2026-02-1013 min read

Uniswap hooks security boundary diagram

Uniswap v4 hooks are a power tool.

They let you inject logic into the swap and liquidity lifecycle. That means you can build things like dynamic fees, on-chain risk checks, and custom order flow.

It also means you have created a new attack surface that is:

  • permissionless (anyone can trigger hooks via swaps)
  • composable (hooks can call other contracts which call you back)
  • order-dependent (MEV can often choose whether your hook runs before or after some price movement)

If you take one thing from this post, take this:

In a hook, “I only changed a little logic” is not a small change. You moved invariants across an external-call boundary.

This article is a set of patterns for writing hooks that behave like protocol code, not like app code.

The hook threat model (in one paragraph)#

When your hook runs, assume:

  1. the caller is adversarial (including “honest” routers that can be used adversarially)
  2. the pool price can move in the same block (MEV)
  3. tokens can lie (fee-on-transfer, callbacks, rebasing, weird return values)
  4. any external call you make can reenter you
  5. rounding and precision are attack surfaces if a user can loop them

Hooks are not “just callbacks”. Hooks are stateful, externally-triggered entry points.

The core invariant: settle by deltas, not by vibes#

The most common integration failure pattern across DeFi is:

read something from an external contract and treat it as truth inside accounting.

In Uniswap v4-style designs, the antidote is to treat balance deltas as truth, and everything else as “claims”.

This matters because Uniswap v4 exposes swap outcomes as a BalanceDelta: a signed pair (amount0, amount1) that tells you what the pool gained and lost. If you ignore the delta and instead reconstruct “what should have happened” from return values or assumptions, you are choosing to be wrong in the exact place attackers optimize.

Swap semantics you must internalize (even if you never touch v4 core)#

Uniswap-style swaps have two orthogonal axes:

  1. direction: zeroForOne vs oneForZero (token0 to token1, or the reverse)
  2. exactness: exact input vs exact output (how much is fixed)

In v4-shaped APIs, exactness is often encoded by the sign of an integer amount. That design is elegant, but it is also a footgun if you treat “amount” as a normal uint256.

Parameter conceptWhat it meansSecurity relevance
zeroForOne = trueswap token0 to token1direction changes which side is paid vs received
amountSpecified > 0exact inputyou must enforce min-out and slippage
amountSpecified < 0exact outputyou must enforce max-in and consider MEV grief
sqrtPriceLimitX96price limit guardsetting it wrong can enable toxic execution ranges

Hooks are often given hookData as well. Treat it as untrusted calldata that can be crafted to trigger worst-case execution paths.

Examples of claims:

  • transfer() returns true
  • a token reports balanceOf() that can move due to rebasing
  • a hook “promises” it will transfer what it owes
  • a callback “promises” it already paid

If you can structure your hook so final settlement is derived from the canonical deltas (what actually moved), you shrink the space of “lies” you can be told.

What hooks actually change: your trust boundary moved#

Traditional AMM integrations have a simple boundary:

  • user calls router
  • router calls pool
  • pool calls token

Hooks insert user-defined logic inside the pool lifecycle.

That is not “an extension point”. It is a new boundary where:

  • liveness can be attacked (a revert bricks common paths)
  • order dependence becomes your problem (MEV can pick your execution context)
  • rounding becomes monetizable (hooks are called frequently)

If you publish a hook contract, you have effectively published a new protocol.

Pattern 1: design hooks as a finite-state machine#

Hooks often need transient state: “I’m in the middle of a swap; I expect callback X; only then do I finalize”.

Do not rely on “it happens in one call stack” as your state machine.

Adversaries can:

  • split across multiple calls (if you expose intermediate functions)
  • force revert paths that leave partial state
  • reenter at the most inconvenient point

Instead, encode phases.

Here is a minimal pattern:

SOLIDITY
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract HookFsm {
  enum Phase { Idle, InSwap, InLiquidity }
  Phase private phase;

  error Reentered();

  modifier nonReentrantPhase(Phase expected, Phase next) {
    if (phase != expected) revert Reentered();
    phase = next;
    _;
    phase = expected;
  }

  function beforeSwap() external nonReentrantPhase(Phase.Idle, Phase.InSwap) {
    // do checks, record expected deltas, etc.
  }

  function afterSwap() external nonReentrantPhase(Phase.InSwap, Phase.InSwap) {
    // finalize, settle, clear ephemeral state
    phase = Phase.Idle;
  }
}

This looks like a “reentrancy guard”, but the important property is semantic: you can reason about which functions are valid in which phase.

If you need nested phases, prefer explicit stacks (arrays) over “clever booleans”.

Pattern 2: isolate “dangerous calls” behind a single choke point#

The hard bugs in hooks are rarely in arithmetic. They live in edges:

  • token transfers
  • oracle reads
  • DEX swaps
  • delegatecall
  • callbacks into your hook

Instead of scattering external calls, build one “choke point” function that performs them and returns a structured result.

Benefits:

  • auditing is easier (one place to model trust boundaries)
  • fuzzing is easier (one place to add adversarial mocks)
  • invariants become clearer (you can assert pre/post around the choke point)

Example structure (pseudo):

SOLIDITY
struct ExternalOutcome {
  uint256 balanceBefore;
  uint256 balanceAfter;
  bytes32 oracleDigest;
}

function _callExternalStuff() internal returns (ExternalOutcome memory o) {
  o.balanceBefore = token.balanceOf(address(this));
  _safeTransferFrom(...);
  o.oracleDigest = _readOracle();
  o.balanceAfter = token.balanceOf(address(this));
}

Then everything else derives from ExternalOutcome.

Pattern 3: make MEV explicit, not accidental#

Many hook designs accidentally assume:

  • the price at beforeSwap is similar to the price at afterSwap
  • an oracle read is stable within a block
  • a “guard” function can run before a swap in the public mempool

That is not a safe assumption.

If your hook uses a price, decide which of these you want:

Design choiceWhat it buys youWhat it costs you
commit-reveal (two-step)removes mempool predictabilityworse UX, more state, more DoS surface
private order flow / bundlesresists sandwichingtrust/ops dependencies
in-protocol guardrails (min-out, TWAP)bounded lossnot full protection
“accept MEV, price it in”simpleusers pay the tax

If you leave MEV implicit, you often get the worst of both worlds: complexity and extractable value.

Pattern 4: reentrancy is not just swap() -> callback() -> swap()#

In hooks, reentrancy comes in multiple forms:

  • classic: you call token transfer, token calls you back
  • cross-function: beforeSwap reenters into an admin function you forgot to gate properly
  • cross-contract: you call an external module that calls your hook through a different path
  • read-only reentrancy: “view” calls that observe intermediate state and are used as oracles

The defense is not “sprinkle nonReentrant”.

The defense is:

  1. decide which functions are callable in which phases
  2. make admin paths uncallable from swap paths (separate contracts, or explicit phase checks)
  3. never expose intermediate accounting state via view functions unless you can prove it is safe under partial execution

Pattern 4.5: minimize hook permissions (reduce the callable surface)#

Hooks are configured with permissions: which lifecycle callbacks they implement.

Every enabled callback is a public entry point in practice, because anyone can trigger it through normal pool actions.

So the first “security pattern” is boring but high leverage:

Only enable the callbacks you actually need.

If your hook is “dynamic LP fee”, you likely do not need liquidity callbacks. If your hook is “whitelist swaps”, you likely do not need donation callbacks.

This reduces:

  • state you must keep consistent
  • phases you must model
  • edge cases that can revert

It also makes fuzzing dramatically more effective because the state machine is smaller.

Pattern 5: rounding is a security choice#

Hooks often implement:

  • fee logic
  • rebate logic
  • dynamic spreads
  • per-user accounting

Every one of these is fixed-point math.

In fixed-point math, “rounding direction” is equivalent to “who gets the dust”.

If a user can loop an action (swap small amounts repeatedly, mint/burn repeatedly, claim repeatedly), dust can become profit.

Rules that work well in practice:

  • round against the caller when minting value (shares, credits, rebates)
  • round in favor of the protocol when charging fees
  • if two parties are involved (LP vs trader), pick a rule and document it; inconsistency is where exploits live

I cover this deeply in a separate post on rounding, but hooks are where rounding bugs become monetizable fast because the hook is called often.

A concrete example: a dynamic fee hook that does not become an oracle#

Dynamic fees are a classic “hooks shine here” use case:

  • raise fees when volatility spikes
  • lower fees when liquidity is deep
  • enforce a minimum fee for certain routes

But dynamic fees are also a classic “hooks become an oracle” failure:

  • if the fee depends on spot price, MEV can manipulate the spot
  • if the fee depends on last swap size, attackers can grief with small swaps
  • if the fee depends on time, attackers can choose the block boundary

The safer posture is: compute fees from a bounded, slow-moving signal.

Signals that are typically safer than spot price:

  • a TWAP (windowed)
  • an external oracle (with explicit trust)
  • volatility estimates over time (not per-swap)

If you do use spot, bound the damage:

  • cap max fee per block
  • cap fee delta per update
  • apply hysteresis (do not oscillate on noise)

If you do not bound it, a “dynamic fee” quickly becomes “a manipulable fee oracle”, which is a free MEV primitive.

Using a battle-tested base instead of ad-hoc hooks#

OpenZeppelin publishes a hooks library with base implementations for things like dynamic fees. A minimal pattern looks like:

SOLIDITY
pragma solidity ^0.8.26;

import {BaseDynamicFee, IPoolManager, PoolKey} from "src/fee/BaseDynamicFee.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

contract DynamicLPFeeHook is BaseDynamicFee, Ownable {
  uint24 public fee; // hundredths of a bip

  constructor(IPoolManager pm) BaseDynamicFee(pm) Ownable(msg.sender) {}

  function _getFee(PoolKey calldata) internal view override returns (uint24) {
    return fee;
  }

  function setFee(uint24 _fee) external onlyOwner {
    fee = _fee;
  }
}

The exact API details evolve, but the security idea is stable:

  • isolate the sensitive policy behind a small interface
  • keep privileged updates explicit (onlyOwner, or governance)
  • do not blend fee policy with swap settlement logic in the same function

Hook liveness: "revert" is an attack vector#

Hooks sit on hot paths.

If a hook reverts, you may halt:

  • swaps (economic activity)
  • liquidity changes (risk management, rebalancing)
  • fees (protocol revenue)

Treat “can be forced to revert” as a vulnerability class.

Common revert-driven DoS patterns in hooks:

DoS patternHow it happensTypical fix
untrusted data triggers reverthook decodes user-provided hookData and reverts on shapereject unknown data early, version it, or default to safe behavior
“too much work” per swaphook loops over user positionscap work, use bounded queues, move heavy work to async settlement
rounding traprounding up causes value to exceed balance, revertingcap, clamp, or design math to avoid traps
oracle outagehook requires oracle update in same txaccept staleness with bounds, or fail open/closed explicitly

If you must fail closed, document the operational requirement. “Fail closed” without an ops plan is “brickable”.

Testing hooks like protocol code#

If your hook can be triggered by anyone, you must test it like permissionless protocol code.

Three test styles that consistently catch hook bugs:

1) scenario tests (one nasty path)#

Write one end-to-end test that simulates:

  • swap
  • reentrant callback attempt (malicious token / hook)
  • MEV ordering assumptions (two swaps back-to-back)

2) fuzz the entry points (sequence, not single-call)#

Hooks often fail on sequences:

  • swap then swap
  • add liquidity then swap then remove
  • swap then admin update then swap

Sequence fuzzing is where callback-state bugs show up.

3) invariants (the non-negotiables)#

Write invariants that match your threat model:

  • balances cannot be created from nothing
  • internal phase always returns to idle
  • hook cannot permanently lock the pool

Example invariant sketch:

SOLIDITY
function invariant_phaseReturnsToIdle() public {
  assert(uint(hook.phase()) == uint(Phase.Idle));
}

If you cannot write a simple invariant, your design is probably too implicit.

A hook audit checklist, without the “checklist vibe”#

Instead of a wall of bullets, here are five questions that, if you can answer honestly, usually means your design is mature.

1) What is the asset you are protecting?#

Examples:

  • pool invariant (no free liquidity, no free swaps)
  • fee growth (no fee theft via rounding)
  • risk module limits (no bypass via callback order)
  • liveness (no “anyone can brick the pool by reverting”)

Write the invariant as a sentence. Then write the counterexample.

2) Which external calls can lie to you, and how do you detect the lie?#

For each external call, write:

  • what it could return that is misleading
  • what you can measure instead (deltas, event-based reconciliation, bounded windows)

3) Can an attacker force you into a “stuck” state?#

Stuck states are a huge deal in hooks because they run in common paths.

Common stuck patterns:

  • a flag is set, then a downstream call reverts, and the flag is never cleared
  • a mapping entry is created per user and never deleted, leading to unbounded gas over time
  • you store “expected deltas” keyed by (sender, pool) and a reentrant call overwrites it

4) Where does your logic assume the current price is “fair”?#

If the answer is “anywhere”, ask how an attacker can:

  • move the price, call you, and move it back
  • call you in a bundle where your assumptions are invalid
  • exploit your logic as a per-block oracle

5) What breaks if a token is weird?#

Even in Uniswap-centric code, you end up touching tokens.

Weird token behaviors you should explicitly handle:

  • transfer returns no boolean (or returns garbage)
  • fee-on-transfer changes amounts
  • tokens with callbacks (ERC-777-like)
  • tokens that rebase between two reads

If your hook depends on a token being “normal”, encode that dependency (allowlist + explicit rationale), or redesign.

Watch: security considerations for Uniswap v4 hooks#

Hooks are callback-heavy code that runs inside the hottest paths of a DEX. This talk is useful because it frames hook design around attack surfaces: reentrancy, stuck-state bugs, and accounting that assumes “nice” tokens.

Further reading#