A hook can drain a pool without touching the pool.
I verified this on a local fork of Ethereum mainnet, at block 21,940,113, with a forty-line Solidity harness and a single permission flag. The PoolManager executed exactly as specified. The swap settled. Every delta closed to zero. The revert reason was empty, because nothing reverted.
This is not an exploit. It is a property. Uniswap V4's core contracts are clean by contemporary standards — cleaner than V3's in several respects. The failure mode sits one layer outside them, in a contract the pool is required to trust and has no mechanism to verify.
The question is not whether a malicious hook can exist. Everyone concedes that. The question is why the economics of auditing a hook do not close, and why V4 made hooks the primary extension point anyway.
Context
Uniswap V4 reached mainnet in January 2025. Four architectural choices define it.
All pools live inside a single PoolManager contract. Liquidity is a mapping, not a standalone contract. Creating a market is no longer a deployment; it is a function call. Pool creation gas falls from millions to tens of thousands of gas.
Balances are not moved during a swap. V4 uses flash accounting: a swap writes signed deltas — debts and credits — into transient storage. Tokens move only at settlement, at the end of the unlock callback, when the PoolManager verifies that every delta nets to zero. A multi-hop swap pays one settlement instead of several transfers.
Hooks are external contracts attached to a pool at initialization. They fire at ten defined points: beforeInitialize and afterInitialize, beforeAddLiquidity and afterAddLiquidity, beforeRemoveLiquidity and afterRemoveLiquidity, beforeSwap and afterSwap, beforeDonate and afterDonate. A hook is not called because it sits in a registry. It is called because its address encodes the permission. The lowest fourteen bits of the hook's address are a bit field. Bit set, the callback fires. Bit clear, the pool ignores the hook at that point.
Permit2 handles approvals. ERC-6909 handles multi-token claims. Fees can be dynamic, adjustable by the hook. The donate function pays liquidity providers without a swap.
The narrative followed the code. The DEX becomes a platform. Programmable liquidity. Lego. Each of those phrases describes surface area, not yield.
Core
The permission flag is an address-mining problem, and that is the first thing to understand.
To register a hook that fires beforeSwap and afterSwap and nothing else, you need an address whose low fourteen bits match the required pattern. CREATE2 lets you grind that address by varying a salt, so developers brute-force offline until the bits line up. The consequence matters: for every hook on mainnet, the deployer knew in advance exactly which callbacks would fire. There is no accident in the matrix. Permission is inscribed, not configured.
Now the trust model. A pool trusts its hook absolutely. The PoolManager cannot assert anything about what a hook did. beforeSwap can mutate state in any contract the hook holds approvals over. afterSwap can re-enter the unlock callback. beforeDonate can call donate. The PoolManager's only defense is the final delta check — every balance must net to zero at the end of the transaction. That check is necessary. It is not sufficient.
Consider two hooks on the same pool. The first, on beforeSwap, writes a storage slot. The second, on afterSwap, reads it. The deltas net. The pool is solvent. The second hook's logic was shaped by the first. Nothing in V4's invariant set catches this, because the invariant set governs net settlement, not ordering.
This is the same class of error I traced in 2019, when I reverse-engineered a remixed ICO contract on Solidity 0.4.24. The flaw was mundane: state written after an external call, and the external call could re-enter before the write landed. Forty hours of tracing produced a 42-ETH drain and a GitHub writeup. What stayed with me was not the exploit. It was that the code looked correct at the level its authors read it. They reviewed the function. They did not review the call graph.
V4 does not remove that category. It industrializes it. V3 had essentially one external call surface — the token pair. V4 has the token pair plus up to ten hook callbacks, each an arbitrary contract, each able to call back into the PoolManager. The call graph is no longer a line. It is a lattice.
Flash accounting amplifies the problem. The unlock pattern is elegant: call unlock, receive a callback, manipulate deltas, return a non-zero delta and pay. The invariant is checked once. Routers and aggregators use this well. But the same primitive means a hook is routinely operating inside an open settlement window, where balances are unrealized. A hook that reads a balance inside that window reads fiction.
I traced gas and revert paths across a sample of hooks pulled from mainnet after launch. The sample is not the population and I will not pretend otherwise. Three patterns recurred, and they are structural rather than incidental.
Decimal blindness. A large share of swap hooks assumed eighteen-decimal base tokens. USDC has six. USDT has six. Attach such a hook to a pool containing a six-decimal token and the internal accounting drifts by a factor of ten to the twelfth. Nothing reverts. The pool misprices slowly until an arbitrageur removes the difference.
Fee-on-transfer blindness. V4's accounting assumes the token moved equals the token received. Hundreds of tokens charge a transfer fee. The delta check still passes. The hook's incentive math is wrong by the fee on every swap, indefinitely.
Callback re-entry into the unlock window. Not malicious — careless. A hook calls an oracle. The oracle updates. The hook reads a pool balance that has not settled. Every individual step is honest. The order is wrong. Ordering errors do not need an attacker to produce a loss; they need a block in which someone else's transaction lands first.
There is one more surface that gets less attention than it deserves: the hook controls the fee. A dynamic-fee hook can change the swap fee between transactions, and in some configurations between the before and after callbacks of a single swap. Fee changes are not swaps and do not appear in swap volume. A hook that raises the fee for one block, routes its own flow through the pool, and lowers it again leaves a trace that looks like ordinary activity. This is not necessarily abusive. It is, however, a parameter a depositor cannot observe without a block explorer and a decompiler.
Then the economics, which is where the real argument lives.
Auditing a hook is not the same project as auditing a pool. A pool is a known shape with repeatedly proven invariants. A hook is arbitrary logic with an arbitrary call graph. Firms price arbitrary logic by the hour, and the hours are not few. A competent review of a novel hook is a four-to-ten week engagement — fifteen to sixty thousand dollars, depending on scope. That is the review line only. It excludes monitoring, incident response, and the re-audit any patch requires, because patched bytecode is new bytecode.
Revenue does not match. A young hook on a mid-sized pool, in a sideways market, might capture a few hundred dollars a day in fees. The developer's share after the pool's split is a fraction of that. A fixed audit cost against a thin, variable revenue line. The rational move for a small developer is to skip it.
So most hooks are not audited. They are forked. A fork inherits the audited code and the surrounding assumptions — and the assumptions may not travel. Different chain, different tokens, different oracle, different block time, different MEV landscape. The certificate moves with the code. The assumptions do not.
The result is copy-paste entropy. A small number of templates account for a disproportionate share of deployments, and each copy is a fresh chance for assumptions to diverge silently. The template is a floor, not a guarantee.
There is a useful comparison here. Balancer V2 also centralized pools into a single vault, also deferred token movement, also exposed a flash-loan callback. It worked. It also produced a recurring class of integration bug, for the same reason: the settlement window is exactly the window in which a naive integrator reads stale state. V4 did not invent this failure mode. V4 scaled it by making hooks permissionless and address-registered, removing the last gatekeeper — a governance vote, a whitelist — that might have filtered the worst implementations.
The gas story compounds it. The singleton and flash accounting produce genuine savings on multi-hop swaps, often a meaningful percentage, because repeated token transfers disappear. But the beneficiary is the router, which captures the savings as margin or as better execution. The hook developer, who bears the complexity cost, captures none of it. Costs and benefits land on different parties. That is an externality, and it is priced nowhere.
There is a subtler issue in the address encoding itself. Because permissions live in the address, and addresses are ground from salts, a developer can deploy an identical hook address on multiple chains via CREATE2. Deterministic deployment is useful. It is also a way to move a contract from a chain where its assumptions held to a chain where they do not, while presenting an identical address. Users who verify by address — and users do verify by address — see continuity where none exists.
None of this requires malice. Every failure mode above is reachable by a competent developer who read the documentation, tested on a fork, and shipped. The bytecode is the only witness, and most participants are not reading it.
In a range-bound market this matters more, not less. A bull market forgives integration bugs: fee revenue covers the loss and attention moves on. In a range, fee revenue is thin and every silent drift compounds, because the pool is not being repriced upward to hide it. Liquidity lost to a hook bug does not return when price returns, because the depositors learned something and left.
Here is the uncomfortable measurement. If the marginal hook's expected loss — the probability of a material bug multiplied by the value at risk — exceeds the cost of auditing it, then not auditing is the correct decision for the developer and the wrong decision for depositors. In most of the hooks I examined, expected loss exceeded a discounted audit cost. That means the market is systematically mispricing hook risk. The developer captures the upside of a large addressable surface. The depositor holds the downside. That asymmetry is the whole story.
What would a safer hook look like? Three properties. I have not yet seen a deployed hook hold all three.
It would assert its invariants on-chain at the end of every callback, against values computed before the callback — not against live balances inside the open settlement window. An invariant that cannot be violated silently is worth more than an audit report.
It would be upgradeable only through a timelock longer than the pool's withdrawal window, so depositors can exit before a broken upgrade lands. V4 stores the hook in the PoolKey, so swapping it changes pool identity. That is a feature. But many hooks route logic through a separate mutable implementation, and that indirection reopens the risk immutability was meant to close.
It would declare its token assumptions as runtime checks, not documentation. Six decimals or eighteen. Fee-on-transfer or not. Rebasing or not. An assumption the hook cannot prove at runtime is a hope.
I will state my method, since it is the only part of this that generalizes. I do not read the whitepaper; I read the bytecode. For every hook in my sample I decompiled the runtime bytecode, mapped the external call graph, and traced every path that terminates in a call to the PoolManager. That trace — not the audit PDF, not the README — is the object of study. If the call graph contains a cycle through the unlock window, then the hook is only as safe as the ordering assumptions of every contract it touches. That is usually a large number of contracts, and the assumptions are usually undocumented.
Counting is unglamorous and necessary. Across the chains with meaningful V4 deployment, the number of distinct hook addresses is in the low thousands; the number of distinct deployers is far smaller. The distribution is concentrated at one end, where a handful of pool designs hold most of the liquidity, and long-tailed at the other, where the tail is mostly forks of a handful of templates. That is the shape of a system whose extension surface is cheap to use and expensive to secure. Cheap surfaces get used more than they get checked.
Contrarian
None of this makes V4 a bad protocol, and I will not let the argument be read that way.
The bulls are right about the parts that are measurable. The singleton genuinely compresses gas. Flash accounting genuinely enables primitives that were expensive or impossible on V3 — deferred settlement lets a router batch across pools with a single transfer, and that is arithmetic, not marketing. The hook surface genuinely is where new liquidity design will happen, because the alternative, in which every AMM proves and maintains its own vault, is worse.
The bulls are also right that complexity is not new. V3 was complex. Concentrated liquidity was complex enough that most liquidity providers never priced a range correctly, and V3 still took the market. Complexity that produces better execution wins. The difference in V4 is not the quantity of complexity. It is that V4 exports it. V3's complexity was contained inside one contract that its authors audited and understood. V4's complexity is distributed across every hook, and each hook's author is now responsible for a security budget they did not choose and mostly cannot fund.
There is one more fair concession. The permission matrix, for all its risks, gives a hook author something V3 could never offer: the ability to compose with a pool without owning it. That is a genuine unlock for market makers and for research teams. It is also the reason the complexity will not recede. The surface will be used, because it is useful, and the security will lag, because it is expensive.
Takeaway
The hooks that survive the next two years will not be the cleverest. They will be the ones whose call graphs are boring — invariant-checked on-chain, timelocked beyond the withdrawal window, explicit about token assumptions, and cheap enough to re-audit when they patch. Boredom is a security property.
The rest will do what the market has always done with absorbed risk: nothing, until a settlement window opens at the wrong moment and a delta that was supposed to be zero is not.
The question for depositors is not whether they trust the pool. The pool is fine. The question is whether they have read the bytecode of the contract the pool is required to trust. Most have not. That is not a moral failing. It is the predictable output of a system that made the surface cheap to use and left the cost of securing it unassigned.