Auditing Uniswap v4 Hooks: Essential Best Practices

Auditing Uniswap v4 Hooks: Essential Best Practices

Uniswap v4 introduces hooks: contracts that can run logic before and after core pool actions. Hooks enable fees, oracles, dynamic controls, and new market mechanics. They also widen the attack surface. An audit must prove that a hook is safe under adversarial order flow, quirky tokens, and network-level stress.

This guide gives a structured way to review hook code. It focuses on lifecycle correctness, permission surfaces, state safety, gas griefing, token quirks, and rigorous testing.

Understand the hook lifecycle and call context

Hooks in v4 are called by the PoolManager during onSwap/onMint/onBurn and their after-callbacks. The PoolManager controls the entrypoint and sets constraints with a lock. The hook address encodes which callbacks it claims to implement via specific address bits. The call context shapes trust assumptions and reentrancy risk.

Confirm that the hook never assumes msg.sender is a user or router. The PoolManager will be the caller. If the hook branches on msg.sender, it should treat only the PoolManager as trusted and treat user-provided bytes as untrusted input.

Verify hook flag bits and address correctness

Uniswap v4 uses address-encoded hook flags. A hook must be deployed to an address whose low bits match the callbacks it advertises. Many teams use CREATE2 with salt grinding to find a valid address. A mismatch breaks pools or silently disables logic.

Check that the deployed address has the correct bitmask for the intended callbacks. Review the CREATE2 salt derivation and ensure salts are unique and recorded. A trivial example: a hook that claims beforeSwap/afterSwap must set those bits and must actually implement the functions; stubs that revert will DoS the pool.

Reentrancy, locks, and PoolManager interactions

Uniswap v4 uses a lock pattern to serialize state-sensitive flows. Hooks can call back into PoolManager within the same lock for controlled operations. Unsafe external calls or reentrancy back into the hook can corrupt accounting or drain funds.

Require these checks:

  • Use the provided lock context correctly; do not implement a second reentrancy guard that conflicts with PoolManager locks.
  • Avoid external calls during before-callbacks that could reenter via tokens with hooks or ERC-777 like behavior.
  • Never rely on storage that assumes single-threaded execution; reentrancy can reorder after-callbacks.

If the hook must call arbitrary contracts, prefer pulling state first, performing effects, then interacting, and limit external call targets. A hook that calls a price feed in beforeSwap can be reentered if the feed triggers token transfers or callbacks.

Input validation and data shaping

Hooks often parse bytes from swap/mint calls to control behavior. Treat these bytes as attacker-controlled. Decode with bounds checks. Use fixed-size structs where possible, and validate enums and flags at parse time.

Assert invariants early: token addresses in data must match the pool, fee values must be within a safe range, and slippage or price bounds must be sane. Keep per-transaction storage minimal; do not append to unbounded arrays keyed by msg.sender.

State integrity, storage layout, and packing

State bugs are common. Hooks accumulate fees, track TWAPs, or record positions. Pack storage carefully to minimize gas but avoid collisions and unintended overwrites. If using upgradeable patterns, lock down the storage gap.

Two small scenarios:

1) A fee hook stores feeAccrued[token] in a mapping. If token is the zero address, writes collide with a sentinel key. Add an explicit token != address(0) check and reject non-standard tokens if unsupported.

2) A rate limiter stores lastTimestamp and bucket in a single slot. If arithmetic uses unchecked underflows, a large time jump resets the limiter and allows a surge; cap deltas and test timestamp edges.

Gas griefing and denial-of-service vectors

Hooks that loop over dynamic sets (e.g., whitelists, buckets, or observations) can be griefed. Adversaries can trigger worst-case branches by sending minimal swaps that hit the path repeatedly.

Before adding any loop, confirm it has a strict cap or amortization. Avoid storage writes on no-op flows. Make refunds and distributions lazy and bounded per call. If your hook inspects N items, keep N small and enforced on-chain.

Token quirks and transfer safety

Uniswap pools face ERC-20 variants: fee-on-transfer, rebasing, tokens that do not return booleans, and tokens with callbacks. A hook that assumes standard semantics will miscount balances.

Use safe transfer helpers that handle non-standard returns. For accounting, measure deltas with pre/post balance reads instead of assuming amountIn equals amountReceived. If the design does not support fee-on-transfer, block such tokens explicitly and emit a clear error.

Economic and MEV-aware checks

Hooks that set dynamic fees, change tick spacing, or alter swap routes can be gamed by MEV. If a fee depends on recent flow, a searcher can push state in one transaction and profit in the next. Snapshot state at entry and charge based on pre-state, not post-state. If the hook relies on oracles, prefer pull-based, bounded-latency data or TWAPs with enforced minimum windows.

Simulate sandwiches: attacker does a small swap to change the fee, victim trades, attacker reverts the state with an after-swap. Ensure after-callbacks cannot roll back accounting or rebate more than collected.

Access control and administration

Many hooks have admins for parameters and pauses. Keep admin scope minimal. Use a single, explicit owner or role module. Every privileged action must emit an event with the new value. On deploy, verify the owner is a secured address (ideally a multisig or timelock).

A pause function should block only risky paths and allow withdrawals. Test that paused state does not brick the pool or trap funds in the hook.

Create2, salts, and deployment hygiene

Because hook addresses must match flag bits, teams grind salts. Auditors should confirm the script records the salt, bytecode hash, and expected address. Check that salts are not reused across environments in ways that cause collisions or confusion. Verify that the final deployed address exactly matches the address registered in pool creation transactions.

Invariants to assert during review

An invariant list keeps the audit concrete. The set below covers most hooks, and each item should map to a test.

  1. Only the PoolManager can call hook callbacks; external calls revert.
  2. Hook flags match implemented callbacks and deployed address bits.
  3. No unbounded loops over user-controlled sets.
  4. Accounting cannot go negative; amounts credited never exceed amounts collected.
  5. State changes in before-callbacks are consistent with after-callback cleanup.
  6. External calls are minimized and cannot reenter dangerous paths.
  7. Admin functions are restricted and emit events.

Each invariant should have at least one fuzzing property and one edge-case unit test. Link properties to code lines in the audit report for traceability.

Testing strategy that catches real bugs

Tests should blend unit checks with adversarial fuzzing and invariants. Focus on the hook’s public surface, not just happy paths.

Use this plan as a baseline, then extend with hook-specific cases:

  • Deterministic address tests: assert flag bits in the deployed address.
  • Fuzz on bytes data and token pairs; ensure no reverts or invariant breaks on random inputs.
  • Differential tests: compare a “no-hook” pool to the hook pool for conservation of value.
  • MEV scenarios: backrun and sandwich simulations with gas constraints.
  • Non-standard ERC-20 harness: fee-on-transfer, no-return, rebase.
  • DoS checks: repeated minimal swaps, gas-throttled calls, and revert bombing in after-callbacks.

Record coverage for both callbacks and branch edges. Aim for high coverage on parsing, permissions, and any math that affects balances.

Common risks and mitigations

The table lists recurring issues seen in hook designs and a direct mitigation. Use it as a checklist during review and as a template for audit notes.

Frequent Hook Issues and Fixes
Risk Why it Matters Mitigation
Flag/address mismatch Callbacks skip or revert Assert bitmask off-chain and on-chain at init
msg.sender misuse Unauthorized paths open Gate callbacks to PoolManager only
Unbounded loops DoS via gas griefing Cap list sizes; amortize work
External reentrancy State corruption, drains Limit external calls; use lock context
Token quirks Incorrect balances Safe transfers; delta-based accounting
Oracle manipulation Profitable MEV loops TWAPs; min windows; snapshot pre-state
Admin overreach Rug or accidental freeze Minimal roles; events; pause only risky paths

Prioritize fixes that remove entire classes of bugs: strict gating, bounded loops, and clear math checks reduce review time and runtime risk.

Documentation and operational readiness

Good docs reduce misuse and speed audits. Provide a spec that maps each callback to its intent, inputs, and outputs. Include a threat model that lists adversaries, from retail users to MEV searchers, and how the hook resists them. Ship a runbook with emergency steps: how to pause, how to withdraw stranded funds, and how to verify state after an incident.

Finally, archive the exact compiler version, optimizer settings, bytecode hash, CREATE2 salt, and deployed address. These details allow independent reproduction and cut down guesswork during incidents.