The Factory and Pool pattern of Uniswap V3 is familiar, and we’ve become used to calling “getter” functions to retrieve state data for a given token pair.
However Uniswap V4’s PoolManager contract takes a different approach in the name of efficiency, adding a lot of complexity to gain flexibility and lower gas costs.
The primary place this has occurred is the pool initialization step. The PoolManager, playing the role of factory, no longer maintains a mapping of token pairs. Instead, it emits an Initialized
event with the relevant pool parameters, and places the responsibility for recording and providing these parameters on the user.
As a direct consequence, PoolManager has no equivalent to V2’s getPair or V3’s getPool functions. And it’s no surprise either, because V4 allows pools of any fee and tick spacing, plus the option to customize the behavior of state changing functions using hooks. Multiple pools can be created with the same token pair, but different sets of fee/spacing/hook options. The only true restriction on pool creation is that two pools cannot be created with the same Pool ID, which is deterministic based on the options already described. So we must live with the fact that multiple pools will exist inside the PoolManager at a given time, and devise methods to track them.
Please read Part II if you need more information on the design of PoolManager.
Pool State
PoolManager.sol defines an internal mapping of PoolId
s to the Pool.State
struct:
/// @title PoolManager
/// @notice Holds the state for all pools
contract PoolManager is IPoolManager, ProtocolFees, NoDelegateCall, ERC6909Claims, Extsload, Exttload {
using SafeCast for *;
using Pool for *;
using Hooks for IHooks;
using CurrencyDelta for Currency;
using LPFeeLibrary for uint24;
using CurrencyReserves for Currency;
using CustomRevert for bytes4;
int24 private constant MAX_TICK_SPACING = TickMath.MAX_TICK_SPACING;
int24 private constant MIN_TICK_SPACING = TickMath.MIN_TICK_SPACING;
mapping(PoolId id => Pool.State) internal _pools;
[...]
}
The Pool.State
struct is defined in Pool.sol:
/// @notice The state of a pool
/// @dev Note that feeGrowthGlobal can be artificially inflated
/// For pools with a single liquidity position, actors can donate to
/// themselves to freely inflate feeGrowthGlobal
/// atomically donating and collecting fees in the same unlockCallback
/// may make the inflated value more extreme
struct State {
Slot0 slot0;
uint256 feeGrowthGlobal0X128;
uint256 feeGrowthGlobal1X128;
uint128 liquidity;
mapping(int24 tick => TickInfo) ticks;
mapping(int16 wordPos => uint256) tickBitmap;
mapping(bytes32 positionKey => Position.State) positions;
}
The Uniswap V3 Pool Contract has dedicated getters for retrieving slot0
and liquidity
values.
V3 also includes a special TickLens library for retrieving tick mappings and liquidity positions at those ticks.
External Storage Access
PoolManager throws all that out the window, instead leaning heavily on a more generic approach that allows generic external storage access. From the list of inherited features, covered in Part II:
An implementation in the spirit of EIP-2330 that would allow external contracts to retrieve storage values from a given slot using a lightweight external method, from Extsload.sol
The same EIP-2330 access implementation described above but for transient storage, from Exttload.sol
Storage state on EVM is not private, and you can retrieve the storage state at a given 32-byte address at any time. I covered this in an article on eth_call
, going into great detail on hashing and retrieving storage values for balances and approvals held in mappings.
However generalized storage access is not yet available on EVM, and all storage reads/writes are specific to the contract executing the calls.
You can read more about EVM storage in Part IV in the Low Level EVM series:
The Extsload and Exttload contracts expose the read-only opcodes (SLOAD
, TLOAD
) to external callers. The functionality of these contracts are available at PoolManager, which inherits from them.
State Library
Having a generic interface to look up arbitrary storage values is useful, but not exactly friendly.
Luckily for us, several useful functions in StateLibrary.sol facilitate direct lookup of the appropriate values for a given pool. These functions perform the necessary translation of PoolId to a given storage slot, retrieve the value, decode it, and return it to the caller. Nice!
Keep reading with a 7-day free trial
Subscribe to Degen Code to keep reading this post and get 7 days of free access to the full post archives.