Skip to main content

Create Pool

In Uniswap V3, creating a pool usually requires deploying a new contract. However, Uniswap V4 introduces a different approach. It operates in singleton mode, managed by PoolManager.sol, eliminating the need to deploy a separate contract for each pool. Let's take a closer look at how this works.

Let's start with an example of how to create a pool in Uniswap V4. For more details, refer to test/learn/PoolInit.t.sol. (Depolyer.sol)

// 1. Create PoolKey
key = PoolKey({
currency0: currency0,
currency1: currency1,
fee: 3000, // 0.3% fee rate
tickSpacing: 60, // Standard tick spacing
hooks: IHooks(address(0)) // No hooks
});

// 2. Set initial price (using a valid price)
uint160 sqrtPriceX96 = 79228162514264337593543950336; // 1:1 price ratio

// 3. Initialize the pool
int24 initialTick = manager.initialize(key, sqrtPriceX96);

Simple, right? Now, let's dive deeper into PoolManager.sol to understand how it works.

// PoolManager.sol
mapping(PoolId id => Pool.State) internal _pools;

function initialize(PoolKey memory key, uint160 sqrtPriceX96) external noDelegateCall returns (int24 tick) {
...
uint24 lpFee = key.fee.getInitialLPFee();
...

PoolId id = key.toId();
tick = _pools[id].initialize(sqrtPriceX96, lpFee);
...
}
// libraries/Pool.sol
struct State {
Slot0 slot0;
...
}

// types/PoolId.sol
function toId(PoolKey memory poolKey) internal pure returns (PoolId poolId) {
assembly ("memory-safe") {
// 0xa0 represents the total size of the poolKey struct (5 slots of 32 bytes)
poolId := keccak256(poolKey, 0xa0)
}
}

// types/Slot0.sol
type Slot0 is bytes32;
// libraries/Pool.sol
function initialize(State storage self, uint160 sqrtPriceX96, uint24 lpFee) internal returns (int24 tick) {
if (self.slot0.sqrtPriceX96() != 0) PoolAlreadyInitialized.selector.revertWith();

tick = TickMath.getTickAtSqrtPrice(sqrtPriceX96);

// the initial protocolFee is 0 so doesn't need to be set
self.slot0 = Slot0.wrap(bytes32(0)).setSqrtPriceX96(sqrtPriceX96).setTick(tick).setLpFee(lpFee);
}

To ensure all data adjustments occur exclusively within PoolManager.sol, we apply the noDelegateCall modifier to enforce this constraint.

In PoolManager initialize, we see that lpFee represents the liquidity provider fee. The id is calculated using the keccak256 function. Since poolKey is a fixed 5 * 32 bytes structure, its length and layout are directly known, making abi.encode unnecessary. Instead, we explicitly declare its size as 0xa0 (160 bytes). This allows us to compute the pool's ID by directly passing poolKey to keccak256 function.

Next, let's examine the _pools[id].initialize() method. First, TickMath.getTickAtSqrtPrice(sqrtPriceX96) is used to convert the P\sqrt P value into a tick representation. After that, P\sqrt P , tick and fee data are written to the slot.

A slot is a compressed data structure that occupies only 32 bytes. Its design minimizes gas costs by efficiently packing data. The slot layout consists of the following: 24 bits empty | 24 bits lpFee | 12 bits protocolFee 1->0 | 12 bits protocolFee 0->1 | 24 bits tick | 160 bits sqrtPriceX96. Together, these fields add up to 32 bytes.