Ticks: WTF!?

Conversations exploring the latest Uniswap V4 protocols, hooks and industry news.

The Singleton Switch - How V4 Threw Out the Factory

By Twade

Uniswap V4's biggest architectural shift is invisible to swappers but underwrites everything else - flash accounting, ETH-native pools, hooks, and far cheaper pool creation. What changed, and what it unlocks.


Most V4 explainers lead with hooks. This one starts a layer beneath them. Hooks are the headline feature, but the architectural change that makes hooks possible - and that delivers most of V4's gas savings - is the move from "one contract per pool" to a single contract that holds them all. It's the kind of design decision that looks invisible from the outside but reshapes everything underneath.

By the end of this post you'll understand:

  • Why v3 deploying a new contract for every pool was an architectural ceiling, not just a cost
  • What changed in V4 - a single PoolManager contract with pools stored as libraries operating on state structs
  • Why this unlocked flash accounting, the unlock/callback pattern, ERC-6909 claim tokens, native ETH support, and ultimately hooks themselves
  • The trade-offs that come with the new design

Before V4: a contract per pool

Every pool in Uniswap v2 and v3 was its own deployed smart contract. The flow looked like this: someone calls createPool(tokenX, tokenY, fee) on the UniswapV3Factory. The factory uses CREATE2 to deploy a brand-new UniswapV3Pool contract, stores the resulting address in a nested mapping, and that contract from then on owns the pool's state - its slot0, its tick bitmap, its liquidity, its fee accumulators.

The Uniswap v3 high-level architecture: a factory contract deploys a separate pool contract per token pair and fee tier
From Uniswap Hook Incubator course materials

This design is clean and conceptually tidy: a pool is a thing, a thing is a contract, the EVM understands contracts. But three real problems compounded as the protocol grew.

Pool creation was expensive. Every new pool deployed bytecode to the chain. On Ethereum mainnet, that translated to hundreds of thousands of gas just to bring a new fee tier or token pair online. For long-tail tokens this was a meaningful barrier.

Multi-hop swaps had to physically move tokens between contracts. A swap from ETH → DAI routed through ETH/USDC and USDC/DAI was three external contract interactions plus two intermediate ERC-20 transfers. Every transfer() call meant entering and exiting external code, with all the gas overhead and reentrancy surface that implies.

Extending pool behaviour was all but impossible. Want a pool with a dynamic fee? A custom price curve? An on-chain limit order book? You'd need a new pool type, deployed as a new contract, integrated through new periphery. In v3, the protocol was the protocol.


The switch: one PoolManager, pools as libraries

V4 takes a different bet. There's a single PoolManager contract. All pools - every token pair, every fee tier, every hook configuration - live as data inside that one contract.

The Uniswap V4 high-level architecture: a single PoolManager contract holds the state for every pool
From Uniswap Hook Incubator course materials

The trick that makes this work is that pools in V4 aren't contracts - they're Solidity libraries that operate on a Pool.State struct. The PoolManager holds a mapping from PoolId to Pool.State, and when an action like a swap comes in, it dispatches into the matching state struct via a library call. Conceptually:

contract PoolManager {
  using Pool for *;
  mapping(PoolId id => Pool.State) internal pools;
 
  function swap(PoolKey memory key, ...) {
    PoolId id = key.toId();
    pools[id].swap(...);   // library call - no external contract hop
  }
}

The Pool.State struct itself holds roughly the same information a v3 pool contract held in its storage variables - sqrtPriceX96, tick bitmap, liquidity, fee accumulators, oracle observations. The data hasn't gone anywhere; it's just been relocated from one-contract-per-pool to one-mapping-entry-per-pool inside a shared host.

Insight #1

In V4, pools are data, not contracts. A pool's state lives as an entry in a mapping inside PoolManager. The pool's logic lives in a library that operates on that state. Creating a pool no longer deploys bytecode - it writes a struct.


What this unlocks

The singleton architecture is interesting in its own right, but it's mostly important because of what it enables. Once every pool lives inside one contract, a chain of downstream optimisations becomes possible.

Cheap pool creation

Creating a pool in V4 is a state write rather than a contract deployment. The gas savings here are roughly two orders of magnitude - pool creation moves from "expensive enough to budget for" to "cheap enough to do casually." This is what makes per-hook pools and long-tail markets economically viable in a way they weren't in v3.

Gas Savings

Creating a pool
createPool (deploy) vs initialize (state write)
Uniswap v3
~4,500,000 gas
Uniswap V4
~51,500 gas
Saving~99%

v3 deploys a fresh pool contract whose bytecode sits near the 24KB limit - most of the cost is paying ~200 gas per byte to store code on-chain. V4's initialize just writes a struct; the v4-core snapshot benches it at 51,532 gas. Uniswap's own headline figure is up to 99.99% cheaper.

Flash accounting

This is the big one, and the change that delivers most of V4's swap-side savings.

In v2 and v3, every swap moved tokens. A multi-hop swap from ETH → DAI via ETH/USDC and USDC/DAI moved USDC out of the first pool, into the second pool, then DAI out of the second pool to the user. Three ERC-20 transfers, two of them through pools that are just transient stops in the route.

Comparison of v3 and V4 swap flows for a multi-hop swap
From Uniswap Hook Incubator course materials

In V4, tokens don't move during intermediate hops. The PoolManager keeps an internal ledger of balance deltas - who is owed how much of which token - and only settles the net result at the end of the transaction. The intermediate USDC delta on a ETH → USDC → DAI swap is recorded as a positive balance for the route and an equal-and-opposite negative balance for the next hop, and they zero out without anyone ever calling transfer() on the USDC contract.

The end-state for the same multi-hop swap becomes:

  1. User sends ETH to the PoolManager (one transfer)
  2. PoolManager computes USDC output → tracked as a delta, no tokens moved
  3. PoolManager computes DAI output → tracked as a delta, no tokens moved
  4. PoolManager sends DAI to the user (one transfer)

Two real token movements for any number of hops. This is flash accounting.

Gas Savings

Single hop
ETH → USDC
Uniswap v3
~120,000 gas
Uniswap V4
~108,000 gas
Saving~10%

One hop still needs one transfer in and one out, so the win is modest - mostly from native ETH skipping the WETH wrap/unwrap.

Three hops
ETH → USDC → DAI → USDT
Uniswap v3
~360,000 gas
Uniswap V4
~190,000 gas
Saving~47%

v3 pays for a token transfer at every hop; V4 nets the intermediate deltas and settles just twice, so each extra hop only adds swap math, not transfers.

Estimates. V4 figures are anchored to the v4-core gas snapshots (a core single swap benches at ~123k, ~108k with native ETH); v3 figures are typical mainnet SwapRouter costs. Real numbers vary with tick crossings, hooks, and warm/cold storage.

Insight #2

Flash accounting only works because every pool shares one host contract. Intermediate balance deltas can be tracked as data inside PoolManager because the next hop's pool is already there with it. There's nothing to transfer() to - the next pool is the same contract.

Lock and unlock - the callback model

Flash accounting works because the PoolManager enforces an invariant: any unsettled deltas at the end of a transaction cause a revert. That invariant is implemented through a lock.

To do anything substantive with the PoolManager - swap, modify liquidity, donate - a caller must first call unlock(data). The PoolManager flips its lock to "unlocked," then immediately calls back into the caller via unlockCallback(data). The caller does whatever it needs to do inside that callback (swaps, multi-hop routes, hook-driven operations, position changes), and when the callback returns, the PoolManager checks that every balance delta has been settled to zero. If anything is still outstanding, the whole transaction reverts.

The caller in this flow is almost always a periphery contract - a SwapRouter, a PositionManager, a custom routing contract. End users never call unlock directly. But it's worth understanding the shape, because it's why you can't just call poolManager.swap(...) from outside: that function is gated by onlyWhenUnlocked, which is only true inside the unlockCallback execution context.

This callback-driven design is what lets V4 string together many actions inside a single atomic transaction (e.g. swap → hook fires another swap → settle once) while still guaranteeing the accounting closes cleanly at the end.

ERC-6909 claim tokens

For traders doing many swaps in quick succession - market-makers, arbitrage bots, search-and-execute routers - every "send tokens in, get tokens out" cycle has overhead, even with flash accounting reducing the per-hop cost.

V4 lets these users leave their tokens in the PoolManager and hold an ERC-6909 claim token representing the balance. Subsequent swaps mint/burn claim tokens internally instead of moving real ERC-20s. ERC-6909 is a multi-token standard (think ERC-1155 with less ceremony), so a single PoolManager can issue claim tokens for any underlying ERC-20 without needing a separate contract per token. The result is a per-swap gas profile that's nearly flat regardless of the underlying asset.

This is a power-user feature most retail swappers will never use directly - but it makes high-frequency strategies meaningfully cheaper, which feeds back into tighter spreads and better prices for everyone.

Native ETH support

In v2 and v3, all pools traded ERC-20s. ETH had to be wrapped to WETH before it could enter a pool, then unwrapped on the way out. Every ETH swap paid the wrap/unwrap tax.

V4 supports native ETH as a first-class asset. ETH is represented by the zero address inside PoolKey, and because zero sorts below every other address, ETH always becomes Token0 in any pool it's part of. This eliminates the wrapping round-trip for ETH swaps, which is a small but constant gas saving for one of the most common swap paths on the network.

Transient storage (EIP-1153)

The lock state and the per-transaction balance ledger both have a useful property: they're only meaningful within a single transaction. They don't need to persist after the transaction is done. EIP-1153 introduced two new EVM opcodes - TSTORE and TLOAD - for exactly this case: storage that lives for the duration of a transaction, then evaporates.

V4 uses transient storage extensively. The lock bit, the delta counters, the per-currency delta totals - all live in transient storage. The gas cost of reading and writing transient storage is dramatically lower than normal storage, so the per-action overhead of flash accounting stays small.

Fun fact. V4's mainnet launch was delayed until EIP-1153 shipped in the Cancun hard fork. Before Cancun, the design described here would have required hacky workarounds with custom Solidity compilers - interesting as an exercise but not something you'd ship.


And then there are hooks

Everything above is true regardless of whether you use hooks. The singleton, flash accounting, claim tokens, native ETH - all of it works for a vanilla pool with no extensions. Hooks ride on top of that infrastructure.

The singleton is also what makes hooks tractable in the first place. A hook is a smart contract that the PoolManager calls at specific points in a pool's lifecycle - before/after swaps, before/after liquidity changes, on pool initialization, on donations. Because pools share one host, hooks plug into the host's flow rather than having to be redeployed for every pool. A single hook contract can be attached to many different pools by including its address in the PoolKey.

And because flash accounting tracks deltas centrally, hooks can manipulate those deltas - taking tokens from a swap's output, contributing tokens to a swap's input, charging users extra on liquidity changes - all without needing direct custody of pool reserves. The hook just nudges the ledger, and the PoolManager's settlement step at the end of unlockCallback makes everything balance.

We'll cover hooks in depth in the next post. For now, the headline is: hooks exist because the singleton exists. Take the singleton away and hooks would be back to "redeploy a new pool contract per behaviour," which is what v3 in effect required for any non-default pool design.

Insight #3

The singleton turns Uniswap from a protocol into a platform. Pools share infrastructure, hooks share a host, and behavioural extensions become a function of which permissions a hook holds - not whether a new contract has been deployed. That's the entire reason the V4 design surface is bigger than v3's.


The trade-offs

No architecture is a free lunch. Three trade-offs stand out.

Hook-driven gas variability. Vanilla V4 swaps are cheaper than v3. But a swap on a pool with an expensive hook (think on-chain orderbook matching, complex MEV-mitigation logic) can be more expensive - sometimes meaningfully so. The mitigant is that popular tokens will continue to have hook-less reference pools for routine swaps; users who go through hook-driven pools are doing so because they want the hook's behaviour.

Liquidity fragmentation. A V4 pool is uniquely identified by five things: token0, token1, hook address, tick spacing, and fee. That means the same token pair can have many pools, one per (hook, spacing, fee) combination. This isn't new - v3 already had multiple pools per pair across fee tiers - but V4 widens it considerably. The mitigation is solver infrastructure (Uniswap X, third-party solvers) that finds the optimal route across fragmented liquidity automatically.

The callback model takes some getting used to. You can't call poolManager.swap(...) directly from a script or contract that isn't following the unlock pattern. This is rarely a problem in production (periphery contracts handle it), but it surprises developers approaching V4 from a v3 mental model.


Recap

The singleton switch sits behind almost every interesting property of V4. If you only take three things from this:

  1. Pools in V4 are data, not contracts. A single PoolManager holds the state for every pool; pool logic lives in libraries that operate on those state structs.
  2. The singleton makes flash accounting possible. Intermediate token movements collapse into ledger entries inside one contract. Multi-hop swaps go from O(n) transfers to O(1).
  3. The singleton makes hooks tractable. A shared host means hooks attach to many pools without deploying new contracts per behaviour - which is what makes V4 a platform rather than a protocol.

This piece is the architectural companion to a few other foundational posts. To continue:

  • Every Hook in V4 (and the four that secretly run the show) - the next post. Walks through all 14 hook permissions and dedicates the second half to return-delta hooks, which are where most of the novel V4 designs live.
  • Ticks - What the f**k!? - How prices are stored, how sqrtPriceX96 works, why 1.0001 shows up everywhere.
  • The PoolManager source - github.com/Uniswap/v4-core/blob/main/src/PoolManager.sol. The unlock function is the place to start.