PoolManager
To understand the major parts of the PoolManager, let's look at the the main interface: IPoolManager.sol
.
https://github.com/Uniswap/v4-core/blob/main/src/interfaces/IPoolManager.sol
Lifecycle Functions
initialize: This function initializes a new pool with specified parameters such as pool key, initial price, and optional hook data, setting up the fundamental characteristics of the pool.
swap: Allows users to exchange one type of currency for another within a specified pool, adhering to set limits and conditions, and adjusting the pool's state accordingly.
modifyLiquidity: Enables users to change the amount of liquidity they've provided to a pool, either by adding or removing it, based on specified upper and lower tick limits.
Balance Functions
mint (related to ERC6909 claims): Used for creating new claim tokens (as per ERC6909 standards) for a user, denoting specific rights or entitlements, but not representing liquidity provider (LP) receipt tokens.
burn (related to ERC6909 claims): Allows users to destroy their claim tokens (compliant with ERC6909), effectively relinquishing the rights or entitlements those tokens represented.
take: This function allows users to withdraw or "net out" a specified amount of a currency, which could be seen as a mechanism for zero-cost flash loans under certain conditions.
settle: Used by users to pay off any outstanding amounts they owe, potentially in a different currency, with the function returning the amount paid.
The mint
and burn
functions are specifically related to handling ERC6909 claims, which are distinct from liquidity
provider receipt tokens. These functions deal with specific claims or entitlements rather than the representation of a
user's share in the liquidity pool.
Pool Initialization
The initialize
function sets up a new liquidity pool in Uniswap. It takes necessary information such as currencies
and pricing info, and hook information as inputs, checks various conditions to ensure that the pool is set up correctly,
and sets initial values for the pool.
https://github.com/Uniswap/v4-core/blob/main/src/PoolManager.sol
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.20;
// ... [other imports and contract definitions]
/// @notice Holds the state for all pools
contract PoolManager is IPoolManager, Fees, NoDelegateCall, ERC6909Claims {
// ... [other definitions and functions]
/// @inheritdoc IPoolManager
function initialize(PoolKey memory key, uint160 sqrtPriceX96, bytes calldata hookData)
external
override
onlyByLocker // Modifier ensures only the current locker can call this function
returns (int24 tick)
{
// Check if the fee specified in the key is too large
if (key.fee.isStaticFeeTooLarge()) revert FeeTooLarge();
// Validate tick spacing - it must be within defined min and max limits
if (key.tickSpacing > MAX_TICK_SPACING) revert TickSpacingTooLarge();
if (key.tickSpacing < MIN_TICK_SPACING) revert TickSpacingTooSmall();
// Ensure the currency order is correct (currency0 < currency1)
if (key.currency0 >= key.currency1) revert CurrenciesOutOfOrderOrEqual();
// Validate the hooks contract address
if (!key.hooks.isValidHookAddress(key.fee)) revert Hooks.HookAddressNotValid(address(key.hooks));
// Call before initialization hook with provided data
key.hooks.beforeInitialize(key, sqrtPriceX96, hookData);
// Convert the PoolKey to a PoolId
PoolId id = key.toId();
// Fetch protocol fee and dynamic swap fee if applicable
(, uint16 protocolFee) = _fetchProtocolFee(key);
uint24 swapFee = key.fee.isDynamicFee() ? _fetchDynamicSwapFee(key) : key.fee.getStaticFee();
// Initialize the pool with the given parameters and receive the current tick
tick = pools[id].initialize(sqrtPriceX96, protocolFee, swapFee);
// Call after initialization hook with the resulting data
key.hooks.afterInitialize(key, sqrtPriceX96, tick, hookData);
// Emit an event to signal the initialization of the pool with key details
emit Initialize(id, key.currency0, key.currency1, key.fee, key.tickSpacing, key.hooks);
}
// ... [other functions]
}
Upon success, the transaction announces a new pool was created by emitting an Initialize
event.
PoolKey
The PoolKey
is a structure that uniquely identifies a liquidity pool by storing its details -- the two
currencies involved (sorted numerically), the swap fee, tick spacing, and hooks (extra functionalities) of the pool.
It acts as a unique identifier, ensuring that each pool can be precisely specified and accessed within the code.
The liquidity for a PoolKey is unique to that pool alone
/// @notice Returns the key for identifying a pool
struct PoolKey {
/// @notice The lower currency of the pool, sorted numerically
Currency currency0;
/// @notice The higher currency of the pool, sorted numerically
Currency currency1;
/// @notice The pool swap fee, capped at 1_000_000. The upper 4 bits determine if the hook sets any fees.
uint24 fee;
/// @notice Ticks that involve positions must be a multiple of tick spacing
int24 tickSpacing;
/// @notice The hooks of the pool
IHooks hooks;
}
Since we create and pass the PoolKey
to the initialize
function, and as part of the PoolKey we pass the hooks
we
want to use for the pool.
We can use the hooks
to customize the pool to our liking.
Initialization Code
Here are the important parts of the initialization code from the PoolManagerInitializeTest
contract.
https://github.com/Uniswap/v4-core/blob/main/test/PoolManagerInitialize.t.sol
contract Deployers {
function deployFreshManager() internal {
manager = new PoolManager(500000);
}
function deployFreshManagerAndRouters() internal {
deployFreshManager();
// Initialize various routers with the deployed manager. These routers likely handle
// different aspects of the pool's functionality, such as swapping, liquidity modification, etc.
swapRouter = new PoolSwapTest(manager);
modifyLiquidityRouter = new PoolModifyLiquidityTest(manager);
donateRouter = new PoolDonateTest(manager);
takeRouter = new PoolTakeTest(manager);
initializeRouter = new PoolInitializeTest(manager); // This is the router that is used to initialize the pool
// ... [other routers]
}
}
contract PoolManagerInitializeTest is Test, Deployers, GasSnapshot {
function setUp() public {
deployFreshManagerAndRouters();
(currency0, currency1) = deployMintAndApprove2Currencies();
uninitializedKey = PoolKey({
currency0: currency0,
currency1: currency1,
fee: 3000,
hooks: IHooks(ADDRESS_ZERO),
tickSpacing: 60
});
}
function test_initialize_succeedsWithHooks(uint160 sqrtPriceX96) public {
// Assumptions tested in Pool.t.sol
sqrtPriceX96 = uint160(bound(sqrtPriceX96, TickMath.MIN_SQRT_RATIO, TickMath.MAX_SQRT_RATIO - 1));
address payable mockAddr = payable(address(uint160(Hooks.BEFORE_INITIALIZE_FLAG | Hooks.AFTER_INITIALIZE_FLAG)));
address payable hookAddr = payable(MOCK_HOOKS);
vm.etch(hookAddr, vm.getDeployedCode("EmptyTestHooks.sol:EmptyTestHooks"));
MockContract mockContract = new MockContract();
vm.etch(mockAddr, address(mockContract).code);
MockContract(mockAddr).setImplementation(hookAddr);
uninitializedKey.hooks = IHooks(mockAddr);
// Call initialize function with the uninitialized key and the specified sqrtPriceX96
int24 tick = initializeRouter.initialize(uninitializedKey, sqrtPriceX96, ZERO_BYTES);
(Pool.Slot0 memory slot0,,,) = manager.pools(uninitializedKey.toId());
assertEq(slot0.sqrtPriceX96, sqrtPriceX96, "sqrtPrice");
}
}