The main architectural change in Uniswap V4 is the shift from the factory to the singleton pattern. In all previous versions, new pools were deployed using a pool factory; now new pools can be created and managed using a single contract. Let's start from the
PoolManager.sol.
The main operational structure of the
PoolManager is the
PoolKey struct, which contains the following elements: two addresses of swappable tokens (
currency0 and
currency1), the amount and type of fees (static or dynamic), hooks addresses for this pool, and the ticks spacing parameter (refer to the
article about concentrated liquidity). The
id of the pool is constructed from the
PoolKey structure by
hashing all the struct contents.
The deployment of the pool is performed by the
initialize() function, which accepts the
PoolKey struct (containing token addresses), the initial sqrtPrice, and initial parameters for pool hooks (set in the
PoolKey struct).
After the first checks, the
beforeInitialize hook is
called, allowing hooks to setup the initial state. Then the pool is
initialized to its original state, returning the tick number, corresponding to the initial price. Then the
afterInitialize hook is called, and the function is finished.
The
PoolKey described above is a unique identifier in all Uniswap V4 pool operations (like
swap() or
modifyLiquidity()). By outsourcing pool address calculations to external parties, Uniswap reduces gas costs and improves efficiency.
Now, it's time to look at the main security mechanic of Uniswap V4 - the "unlock" pattern and token deltas. Every operation in Uniswap V4 requires the
unlocking of the
PoolManager. The locking is made
using transient storage, which keeps the lock status persistent until the end of the transaction. It is necessary because memory cannot be used in reentrancy scenarios, while using storage for reentrancy locks is too expensive.
Any operations with liquidity include the
onlyWhenUnlocked() modifier. The "lock/unlock" mechanic works like a reentrancy lock, denying reentry into the function from external calls. However, in addition, at the end of the
unlock() function, there is an important
check for non-zero token deltas. At the end of the swap() function (
here), liquidity management (
here), and the
donate() function (
here), we see the
_accountPoolBalanceDelta() function, which
calculates the amount of "non-zero" token balance changes after the operations. This counter must be equal to zero when
unlock() finishes its work, meaning that any operations inside swaps and liquidity providing must end with a zero credit/debit balance.
This pattern, along with the usage of transient storage, is very powerful and should be studied by DeFi developers to keep the main protocol consistent while allowing external protocols to perform multiple arbitrary actions. In Uniswap V4, it's the pools that can be flexibly customized by the usage of hooks. Let's look at the Uniswap V4 Pools.