Reading Pool State
Introductionβ
Unlike previous versions, v4 uses a different approach for storing and accessing pool data, which requires understanding the use of StateLibrary
and extsload
.
Understanding the PoolManager Architectureβ
The Singleton Designβ
In Uniswap v4, all pools are managed by a single PoolManager
contract, unlike v3 where each pool was a separate contract. This design offers simplified management since all pools are now accessible through a single contract.
This approach significantly reduces deployment costs, simplifies protocol upgrades, and enables more efficient cross-pool interactions. It also allows for easier implementation of new features across all pools simultaneously.
Pools as Library Callsβ
In v4, pools are stored as complex structs, with Solidity libraries handling state changes. The PoolManager
contract uses these libraries to perform operations on the pool state:
contract PoolManager {
using Pool for Pool.State;
mapping(PoolId => Pool.State) internal pools;
function swap(PoolId id, ...) external {
pools[id].swap(...); // Library call
}
}
This design allows all AMM logic to be encapsulated within the PoolManager
contract.
Reading Pool State in v4β
In Uniswap v4, reading pool state involves a few key concepts and mechanisms that differ from previous versions. At the core of this new structure is a complex mapping within the PoolManager contract:
mapping(PoolId id => Pool.State) internal _pools;
This mapping represents a fundamental shift in pool data storage:
- Each pool is identified by a unique
PoolId
. - The
Pool.State
is a struct that contains all the state variables for a single pool. - This struct itself contains several nested mappings and complex data structures.
For example, the Pool.State
struct might look something like this (simplified for illustration):
struct State {
uint160 sqrtPriceX96;
int24 tick;
uint128 liquidity;
uint256 feeGrowthGlobal0X128;
uint256 feeGrowthGlobal1X128;
mapping(int24 => TickInfo) ticks;
mapping(bytes32 => Position.Info) positions;
// ... other fields
}
This complex structure allows for efficient storage of multiple pools and their associated data within a single contract. However, it also means that traditional getter functions would be inefficient or impractical for accessing this data, especially for nested mappings like ticks
and positions
.
To address this, Uniswap V4 introduces the StateLibrary and the concept of using extsload
for reading pool state. These mechanisms provide efficient ways to access the data stored in this complex structure.
The StateLibrary and extsload
β
abstract contract Extsload is IExtsload {
/// @inheritdoc IExtsload
function extsload(bytes32 slot) external view returns (bytes32) {
assembly ("memory-safe") {
mstore(0, sload(slot))
return(0, 0x20)
}
}
// [...]
}
The StateLibrary
is a crucial component in Uniswap v4 for reading pool state. It utilizes the extsload
function, which is an external wrapper for the SLOAD
opcode. This allows for efficient reading of arbitrary storage slots.
How extsload
works:
- It takes a storage slot as input.
- It reads the value stored in that slot directly, using
SLOAD
, from the contract's storage. - It returns the value as a
bytes32
.
This method is more gas-efficient than traditional getter functions, especially when reading multiple storage slots.
Moreover, using extsload
instead of hand-written Solidity view functions lowers the contract bytecode size. This optimization is particularly important for Uniswap v4, as the core contracts are nearly at Ethereum's contract size limit.
TransientStateLibrary and exttload
β
abstract contract Extsload is IExtsload {
/// @inheritdoc IExtsload
function extsload(bytes32 slot) external view returns (bytes32) {
assembly ("memory-safe") {
mstore(0, sload(slot))
return(0, 0x20)
}
}
// [...]
}
While StateLibrary
deals with persistent storage, TransientStateLibrary
is used for handling transient storage. Transient storage, introduced in EIP-1153, is a way to store data that is only needed for the duration of a transaction, making it ideal for temporary data.
It uses the exttload
function, which is similar to extsload
, but for transient storage; it is an external wrapper for the TLOAD
opcode.
Implementing a PoolStateReader
Contractβ
Let's create a PoolStateReader
contract that showcases different methods for reading pool state. For each function, we'll explain its purpose, how it works, and provide an example use case.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol";
import {PoolKey} from "v4-core/types/PoolKey.sol";
import {PoolId, PoolIdLibrary} from "v4-core/types/PoolId.sol";
contract PoolStateReader {
using PoolIdLibrary for PoolKey;
IPoolManager public immutable poolManager;
constructor(IPoolManager _poolManager) {
poolManager = _poolManager;
}
// Functions will be implemented here
}
Before we start, we need to import StateLibrary
from the libraries available in v4-core.
import {StateLibrary} from "v4-core/libraries/StateLibrary.sol";
Let's focus on this important line that we should add:
using StateLibrary for IPoolManager;
This line is crucial for our PoolStateReader contract because it allows us to call StateLibrary functions as if they were methods of the IPoolManager interface, like for instance now we will be able to do poolManager.getSlot0()
.
Now weβre up for breaking down each wrapper function that we're going to be adding to our helper contract, explain the purpose of the pool manager function to read the state, and provide use cases to make sure we understand its utility:
getSlot0()
β
function getPoolState(PoolKey calldata key) external view returns (
uint160 sqrtPriceX96,
int24 tick,
uint24 protocolFee,
uint24 lpFee
) {
return poolManager.getSlot0(key.toId());
}
Explanation:
This function retrieves the current state of the pool, including its price, tick, and fee settings. It uses the getSlot0()
function from StateLibrary
, which efficiently reads these values from a single storage slot.
sqrtPriceX96
: The current price, encoded as a square root and scaled by 2^96. This encoding allows for efficient price calculations in the Uniswap algorithm.tick
: The current tick, representing the quantized price. Ticks are used to efficiently track and update liquidity positions.protocolFee
: The current protocol fee, represented in hundredths of a bip (i.e., units of 0.0001%).lpFee
: The current liquidity provider fee, also represented in hundredths of a bip.
Use Case:
This function is essential for any application that needs to know the current state of a Uniswap v4 pool. For example:
- A price oracle could use this to get the current price of the pool.
- A trading bot could use this to determine if a trade is profitable given the current price and fees.
- A liquidity management system could use the
tick
to decide where to place new liquidity.
getLiquidity()
β
function getPoolLiquidity(PoolKey calldata key) external view returns (uint128 liquidity) {
return poolManager.getLiquidity(key.toId());
}
Explanation:
This function retrieves the current total liquidity in the pool. Liquidity in Uniswap v4 represents the amount of assets available for trading within the current price range.
Use Case:
Understanding the total liquidity is crucial for several scenarios:
- Assessing the depth of the market and potential slippage for large trades.
- Monitoring the depth and growth of a pool over time.
getPositionInfo
β
function getPositionInfo(
PoolKey calldata key,
address owner,
int24 tickLower,
int24 tickUpper,
bytes32 salt
) external view returns (
uint128 liquidity,
uint256 feeGrowthInside0LastX128,
uint256 feeGrowthInside1LastX128
) {
return poolManager.getPositionInfo(key.toId(), owner, tickLower, tickUpper, bytes32(salt));
}
Explanation:
This function retrieves information about a specific liquidity position. It returns:
liquidity
: The amount of liquidity in the position.feeGrowthInside0LastX128
andfeeGrowthInside1LastX128
: The last recorded cumulative fees earned per unit of liquidity for each token.
Use Case:
This function is crucial for applications that need to manage or analyze individual liquidity positions:
- A liquidity management dashboard could use this to display a user's current positions and earned fees.
- An automated liquidity provision system could use this to decide when to rebalance or compound rewards.
- An analytics tool could use this to calculate the performance of different liquidity provision strategies.
getFeeGrowthGlobal
β
function getFeeGrowthGlobal(PoolKey calldata key) external view returns (
uint256 feeGrowthGlobal0X128,
uint256 feeGrowthGlobal1X128
) {
return poolManager.getFeeGrowthGlobal(key.toId());
}
Explanation:
This function retrieves the global fee growth for both tokens in the pool. These values represent the cumulative fees per unit of liquidity since the pool's inception.
Use Case:
Global fee growth is essential for several advanced operations:
- Calculating the fees earned by a position that has been held for a long time.
- Analyzing the overall fee generation of a pool over its lifetime.
- Comparing the performance of different pools or fee tiers.
For additional reference, see StateLibrary
and Extsload