V4 PoolManager storage layout — on-chain-truth verification slots¶
Source of truth:
contract_reference/uniswap/V4/PoolManager.sol(canonical Uniswap V4-corePoolManager, the verified deployed source at0x000000000004444c5dc75cB358380D2e3dE08A90). This note records the storage slots the V4 on-chain-truth verifier reads viaPoolManager.extsload.
Why V4 differs from V3¶
V3 exposes ticks(int24) + tickBitmap(int16) as public view functions on
each UniswapV3Pool — the verifier calls them directly (selectors
0xf30dba93 / 0x5339c296).
V4’s PoolManager is a singleton whose tick state lives inside the
per-pool Pool.State struct value, reachable only via raw storage reads
(extsload). It exposes extsload(bytes32), extsload(bytes32,uint256),
and the batched extsload(bytes32[]) (selector 0xdbd035ff). There is no
public ticks()/tickBitmap() view.
Interface ownership and identity¶
The contract-facing full liquidity-map verifier in
degenbot-rpc::liquidity_verifier owns these slot reads for all callers. A
V4 target is the canonical PoolManager address plus its PoolId; the
StateView address is optional scalar/bootstrap configuration and is not a
full-map verification target. The private V4 tick-window reader is retired.
PoolIngress supplies the complete map to this shared verifier, and the
storage slot derivation is an internal read adapter rather than a second
verification implementation.
Top-level PoolManager storage (slot assignment via forge inspect)¶
contract PoolManager is IPoolManager, ProtocolFees, NoDelegateCall, ERC6909Claims, Extsload, Exttload.
Storage-bearing ancestors (C3-linearized, base→derived): Exttload (transient,
no permanent storage) → Extsload (no storage) → ERC6909 (3 mappings) →
ERC6909Claims (none added) → NoDelegateCall (none) → Owned (owner) →
ProtocolFees (protocolFeesAccrued, protocolFeeController) → PoolManager.
Slot |
Variable |
Source contract |
|---|---|---|
0 |
|
|
1 |
|
|
2 |
|
|
3 |
|
|
4 |
|
|
5 |
|
|
6 |
|
|
So _pools (mapping PoolId => Pool.State) is at top-level base slot 6.
Pool.State struct — nested-mapping slot offsets¶
struct State {
Slot0 slot0; // `type Slot0 is bytes32` — 1 slot
uint256 feeGrowthGlobal0X128; // +1
uint256 feeGrowthGlobal1X128; // +2
uint128 liquidity; // +3 (mappings always start a new slot)
mapping(int24 tick => TickInfo) ticks; // +4
mapping(int16 wordPos => uint256) tickBitmap; // +5
mapping(bytes32 => Position.State) positions; // +6
}
forge inspect reports State occupies 224 bytes = 7 slots (4 value + 3
mapping), confirming the relative offsets.
Slot-derivation math¶
For a pool poolId (the bytes32 PoolId, derived from the PoolKey via
keccak256(abi.encode(currency0, currency1, fee, tickSpacing, hooks))):
S_state = keccak256(abi.encode(poolId, uint256(6)))— thePool.Statevalue base.ticksmapping base within State =S_state + 4.tickBitmapmapping base within State =S_state + 5.
Per-tick / per-word (nested-mapping slot math):
TickInfo[tick] = keccak256(abi.encode(int256(tick), S_state + 4))BitmapWord[word] = keccak256(abi.encode(int256(word), S_state + 5))
abi.encode of an int24/int16 key sign-extends to 256 bits (two’s-complement
left-padding) — the slot-derive helper MUST sign-extend negative ticks/words
before hashing, or the slot diverges for the negative half of the tick range.
TickInfo field layout (first slot)¶
struct TickInfo {
uint128 liquidityGross; // bits 0..127 of slot+0
int128 liquidityNet; // bits 128..255 of slot+0
uint256 feeGrowthOutside0X128; // slot+1 (not read by the verifier)
uint256 feeGrowthOutside1X128; // slot+2 (not read by the verifier)
}
Reading the TickInfo’s first storage slot yields a packed bytes32:
liquidityGross = uint128(uint256(word) & ((1<<128)-1))liquidityNet = int128(int256(word >> 128))(sign-extend the high half)
Read strategy (batched)¶
One extsload(bytes32[]) call — selector 0xdbd035ff, calldata
selector || abi.encode(bytes32[] slots) — covers the supplied tick and bitmap
slots for the pool in a single round trip. If bitmap discovery finds
on-chain-only ticks, the shared verifier performs one additional batched read
for those rows. Decode the returned bytes32[] in order; tick slots yield the
packed (gross, net) per the layout above, bitmap slots yield the raw
uint256 word.
On-chain cross-check status¶
The deployed PoolManager storage layout is exactly what forge inspect
computes from the verified deployed source (the vendored
contract_reference/uniswap/V4/PoolManager.sol). A runtime extsload
round-trip against a live V4 pool (RPC_URL available) is a nice-to-have
sanity check, not a layout-correctness gate — the layout is canonical by
construction.