Introduction
Uniswap V4 represents a fundamental reimagining of automated market maker (AMM) architecture. Built on the foundation of concentrated liquidity, V4 introduces a modular, extensible framework that transforms how developers interact with decentralized exchanges. This guide explores the technical innovations that make V4 a powerful platform for building custom DeFi applications.
Core Architecture: The Unified PoolManager
Understanding the PoolManager
At the heart of Uniswap V4 lies a single, unified contract called PoolManager. This contract serves as the central repository for all pool state and operations across the entire protocol. Instead of deploying individual contracts for each trading pair, V4 maintains all pools within this singleton structure.

The architectural shift brings several profound implications:
State Consolidation: Every pool’s data: liquidity positions, price ticks, fee information, resides in the PoolManager’s storage. This creates a unified state space where pools are identified by unique PoolId values rather than contract addresses.
Operational Efficiency: When you create a new pool, you’re not deploying a new contract. Instead, you’re simply initializing a new entry in the PoolManager’s internal mapping. This transforms pool creation from a contract deployment operation into a state update operation.
Cross-Pool Operations: The singleton design enables seamless interactions between pools. When routing a swap through multiple pools, tokens don’t need to be transferred between separate contracts, they exist within the same storage context, tracked as balance deltas.
Pool Identification and State Management
Each pool in V4 is uniquely identified by a PoolId, which is derived from the pool’s key parameters:

struct PoolKey {
Currency currency0;
Currency currency1;
uint24 fee;
int24 tickSpacing;
IHooks hooks;
}
// PoolId is computed as a hash of the PoolKey
PoolId id = keccak256(abi.encode(key));
The PoolManager maintains a mapping from PoolId to Pool.State, where each state object contains:
- Current price and tick information
- Liquidity distribution across price ranges
- Fee accumulator data
- Tick-specific liquidity data
- Observation data for price history
This structure allows the protocol to manage thousands of pools within a single contract while maintaining clear separation of state.
Transaction Execution: Unlock Pattern and Flash Accounting
Understanding the Unlock Mechanism
All pool operations in V4 must occur within an “unlock” context. The unlock mechanism provides a controlled environment where multiple operations can execute while maintaining proper delta tracking and settlement.

interface IUnlockCallback {
function unlockCallback(bytes calldata data)
external
returns (bytes memory);
}
// Usage pattern
function executeOperations() external {
bytes memory callbackData = abi.encode(operations);
poolManager.unlock(callbackData, this.unlockCallback.selector);
}
function unlockCallback(bytes calldata data)
external
returns (bytes memory)
{
require(msg.sender == address(poolManager), "Unauthorized");
// Decode and execute operations
Operations memory ops = abi.decode(data, (Operations));
// Perform swaps, liquidity modifications, etc.
// All deltas are tracked automatically
// Must settle all positive deltas before returning
return "";
}
Why This Pattern?
The unlock pattern provides several critical benefits:
- Atomicity: All operations within an unlock either succeed or fail together
- Delta Management: The system can track and net all deltas across operations
- Security: Callbacks ensure only authorized code can perform operations
- Flexibility: Developers can compose complex multi-step operations
This pattern is fundamental to V4’s architecture, enabling both the flash accounting system and the hook extensibility.
Flash Accounting: The Delta System
Conceptual Foundation
Flash accounting is perhaps V4’s most elegant innovation. Instead of immediately transferring tokens for every operation, the system tracks balance changes, called “deltas” throughout the lifetime of a transaction. Only at the end does it settle the net balance.
Think of it like a tab at a restaurant: instead of paying after each item, you accumulate charges and settle the total bill at the end. This eliminates unnecessary intermediate transfers and dramatically reduces gas costs.

How Deltas Work
When you perform an operation in V4, the system calculates how many tokens you owe to the protocol (positive delta) or how many the protocol owes you (negative delta). These deltas are stored in transient storage, which exists only during the transaction execution.
// Simplified delta tracking concept
struct BalanceDelta {
int256 amount0;
int256 amount1;
}
// During a swap:
// - You might owe 100 tokens (positive delta)
// - Protocol owes you 95 tokens (negative delta)
// Net: You pay 5 tokens at settlement
Transient Storage Implementation
V4 leverages EIP-1153 transient storage, which provides temporary storage that automatically clears at the end of a transaction. This is perfect for tracking deltas because:
- No Persistence Needed: Deltas only matter during transaction execution
- Gas Efficiency: Transient storage is cheaper than regular storage
- Automatic Cleanup: No need to manually clear deltas after settlement
The flash accounting system tracks deltas per currency across all operations in a transaction. If you swap through three pools in sequence, the system nets all the intermediate deltas and you only settle the final difference.
Settlement Mechanism
At the end of your transaction, you must settle all deltas. The system enforces this through the unlock mechanism:
// Conceptual flow
function executeOperation() {
poolManager.unlock(abi.encode(operations), callback);
}
function unlockCallback(bytes calldata data) external {
// Perform multiple operations
swap(pool1, ...); // Creates deltas
swap(pool2, ...); // Creates more deltas
swap(pool3, ...); // Creates final deltas
// All deltas are netted
// Must settle before unlock completes
settle(currency0);
settle(currency1);
}
If you have positive deltas (you owe tokens), you must call settle() to transfer them. Negative deltas (protocol owes you) are automatically transferred. The transaction reverts if any positive deltas remain unsettled, preventing accidental state inconsistencies.
Extensibility: The Hook System
What Are Hooks?
Hooks are the mechanism that transforms Uniswap V4 from a fixed protocol into a programmable platform. A hook is a smart contract that can execute custom logic at specific points in a pool’s lifecycle. Think of hooks as plugins that extend pool functionality without modifying the core protocol.
Hook Lifecycle Points
Hooks can implement callbacks at eight different lifecycle points:

Initialization Hooks:
beforeInitialize: Execute logic before pool creationafterInitialize: Execute logic after pool creation
Liquidity Hooks:
beforeAddLiquidity: Logic before adding liquidityafterAddLiquidity: Logic after adding liquiditybeforeRemoveLiquidity: Logic before removing liquidityafterRemoveLiquidity: Logic after removing liquidity
Swap Hooks:
beforeSwap: Logic before executing a swapafterSwap: Logic after executing a swap
Donation Hooks:
beforeDonate: Logic before accepting donationsafterDonate: Logic after accepting donations
Each hook callback is optional, you implement only what you need The hook address is stored in the `PoolKey.hooks` field, and the address itself encodes which hook functions are implemented through specific bit flags. This creates an immutable association between a pool and its hook.
Hook Address Encoding
Hook contracts in Uniswap V4 use a unique encoding mechanism: hook permissions are encoded directly in the contract address itself, not in the fee parameter. Each hook function corresponds to a specific bit flag in the hook contract’s address.
How Hook Flags Work: Each hook function has an associated flag that represents a specific bit position in the address. The PoolManager reads these bits to determine which hook callbacks to invoke:
BEFORE_SWAP_FLAG = 1 << 7 (7th bit from right)
AFTER_SWAP_FLAG = 1 << 7 (7th bit)
BEFORE_ADD_LIQUIDITY_FLAG = 1 << 6 (6th bit)
AFTER_ADD_LIQUIDITY_FLAG = 1 << 5 (5th bit)
BEFORE_REMOVE_LIQUIDITY_FLAG = 1 << 4 (4th bit)
AFTER_REMOVE_LIQUIDITY_FLAG = 1 << 3 (3rd bit)
BEFORE_DONATE_FLAG = 1 << 2 (2nd bit)
AFTER_DONATE_FLAG = 1 << 1 (1st bit)
Example:
Consider a hook contract deployed at address 0x00000000000000000000000000000000000000C0. In binary, the trailing 8 bits are 1100 0000:
Bit 7 is set (1) → AFTER_SWAP hook is implemented
Bit 8 is set (1) → BEFORE_SWAP hook is implemented
When the PoolManager executes a swap, it checks these bits in the hook address to determine which hook functions to call. If the bit is set, the corresponding hook function is invoked.
Hook Address Mining: Since addresses must have specific bits set to indicate which hooks are implemented, hook developers must “mine” an address that matches their desired hook permissions. This is done using the HookMiner library:
import {HookMiner} from "v4-periphery/src/utils/HookMiner.sol"; import {Hooks} from "v4-core/src/libraries/Hooks.sol";
// Specify which hooks you want to implement uint160 flags = uint160( Hooks.AFTER_SWAP_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG );
// Mine for a salt that produces an address with these flags bytes memory constructorArgs = abi.encode(poolManager); (address hookAddress, bytes32 salt) = HookMiner.find( CREATE2_DEPLOYER, // 0x4e59b44847b379578588920cA78FbF26c0B4956C flags, type(MyHook).creationCode, constructorArgs );
// Deploy using CREATE2 with the mined salt MyHook hook = new MyHook{salt: salt}(poolManager); require(address(hook) == hookAddress, "Address mismatch");
Key Points:
- The hook address is stored in PoolKey.hooks, completely separate from the fee parameter
- The fee parameter (uint24 fee) only contains the fee value (typically stored in bits 0-15)
- Hook permissions are immutable once deployed (encoded in the address)
- The PoolManager validates hook addresses by checking that the bits match the implemented functions
- Invalid hook addresses (with incorrect bit patterns) are rejected by the PoolManager
This design ensures that:
- Hook permissions are validated at deployment time (address must match flags)
- The hook cannot be changed after pool creation (address is part of PoolKey hash)
- Gas efficiency: no need to store permission flags separately
- Type safety: invalid hook addresses are rejected by the PoolManager
Hook Permission Flags
Hooks declare which callbacks they implement through permission flags. The PoolManager checks these flags to determine which callbacks to execute, avoiding unnecessary external calls for unimplemented hooks.
The permission system uses bit flags:
- Bit 0: beforeInitialize
- Bit 1: afterInitialize
- Bit 2: beforeAddLiquidity
- And so on…
This efficient encoding allows the system to quickly determine hook capabilities without additional storage or complex logic.
Advanced Hook Capabilities
Dynamic Fee Structures
Moving Beyond Fixed Fees
Uniswap V4 introduces dynamic fees, allowing pools to adjust their liquidity provider fees based on market conditions or custom logic. Unlike previous versions with fixed fee tiers, V4 gives developers complete control over fee calculation and update frequency.
How Dynamic Fees Work
A pool can be initialized with dynamic fee capability. When enabled, the hook contract controls fee updates through a dedicated function:

contract VolatilityBasedFeeHook is BaseHook {
function beforeSwap(
address,
PoolKey calldata key,
IPoolManager.SwapParams calldata params,
bytes calldata
) external override returns (bytes4) {
// Calculate current volatility
uint256 volatility = calculateVolatility(key.toId());
// Adjust fee based on volatility
uint24 newFee;
if (volatility > HIGH_VOLATILITY_THRESHOLD) {
newFee = HIGH_FEE; // 1%
} else if (volatility > MEDIUM_VOLATILITY_THRESHOLD) {
newFee = MEDIUM_FEE; // 0.5%
} else {
newFee = LOW_FEE; // 0.05%
}
// Update the pool's fee
poolManager.updateDynamicLPFee(key, newFee);
return this.beforeSwap.selector;
}
}
The fee can be updated:
- On every swap
- Every N blocks
- Based on time intervals
- According to any custom logic
This flexibility enables sophisticated fee strategies that respond to market conditions in real-time.
Fee Update Constraints
While hooks have control over fee updates, the system enforces important constraints:
- Only Hook Can Update: Only the hook associated with a pool can update its dynamic fee
- Fee Validation: Updated fees must fall within acceptable ranges
- No Retroactive Changes: Fee changes apply to future operations, not past ones
These constraints ensure fee updates are controlled and predictable while maintaining the flexibility that makes dynamic fees powerful.
Token Handling and Currency Abstraction
Native Token Support
Eliminating the WETH Wrapper
Previous Uniswap versions required wrapping native ETH into WETH (Wrapped Ether) before trading. V4 eliminates this requirement by introducing native support for ETH through a unified Currency type.
The Currency Abstraction
V4 introduces a custom type called Currency that represents either an ERC-20 token address or native ETH (represented by the zero address). This abstraction allows the protocol to handle both token types uniformly:

type Currency is address;
library CurrencyLibrary {
Currency constant NATIVE = Currency.wrap(address(0));
function transfer(
Currency currency,
address to,
uint256 amount
) internal {
if (Currency.unwrap(currency) == address(0)) {
// Native ETH transfer
(bool success, ) = to.call{value: amount}("");
require(success, "ETH transfer failed");
} else {
// ERC-20 transfer
IERC20(Currency.unwrap(currency)).transfer(to, amount);
}
}
function balanceOf(Currency currency, address account)
internal
view
returns (uint256)
{
if (Currency.unwrap(currency) == address(0)) {
return account.balance;
} else {
return IERC20(Currency.unwrap(currency)).balanceOf(account);
}
}
}
Benefits of Native Support
Native ETH support provides several advantages:
Reduced Gas Costs: Eliminates the wrap/unwrap operations that add gas overhead
Simplified User Experience: Users can trade ETH directly without understanding WETH
Unified Interface: The same code paths handle both ERC-20 tokens and ETH
Flash Accounting Integration: Native ETH deltas are tracked just like token deltas
This design makes ETH a first-class citizen in the V4 ecosystem, reducing friction for the most common trading pair.
Custom Accounting Mechanisms
Beyond Standard Swaps
V4’s custom accounting feature allows hooks to modify token amounts during swaps and liquidity operations. This opens possibilities that go far beyond traditional AMM behavior.
How Custom Accounting Works
Custom accounting allows hooks to modify token amounts during swaps. Hooks cannot directly reassign BalanceDelta. Instead, they use return deltas and poolManager.take() to collect fees.
Important: When using afterSwap for custom accounting, you must:
Enable afterSwapReturnDelta: true in hook permissions
Return (bytes4, int128) instead of just bytes4
Use poolManager.take() to collect fees
Return the fee amount as int128 to adjust user balance
contract FeeCollectingHook is BaseHook { uint256 public constant HOOK_FEE_PERCENTAGE = 10; // 0.01% uint256 public constant FEE_DENOMINATOR = 100_000;
function getHookPermissions() public pure override returns (Hooks.Permissions memory) {
return Hooks.Permissions({
// ... other permissions ...
afterSwap: true,
afterSwapReturnDelta: true, // REQUIRED for return delta
// ...
});
}
function _afterSwap(
address,
PoolKey calldata key,
SwapParams calldata params,
BalanceDelta delta,
bytes calldata
) internal override returns (bytes4, int128) {
// Get output amount based on swap direction
bool outputIsToken0 = params.zeroForOne ? false : true;
int256 outputAmount = outputIsToken0 ? delta.amount0() : delta.amount1();
if (outputAmount <= 0) {
return (BaseHook.afterSwap.selector, 0);
}
// Calculate and take fee
uint256 feeAmount = (uint256(outputAmount) * HOOK_FEE_PERCENTAGE) / FEE_DENOMINATOR;
Currency feeCurrency = outputIsToken0 ? key.currency0 : key.currency1;
poolManager.take(feeCurrency, address(this), feeAmount);
// Return delta adjusts user's final balance
return (BaseHook.afterSwap.selector, int128(int256(feeAmount)));
}
}
How It Works:
delta contains actual token amounts after swap
Calculate fee on output amount
poolManager.take() transfers fee to hook
Return int128(feeAmount) modifies user's balance automatically
The return delta ensures users pay the fee while hooks collect it atomically within the swap transaction
Use Cases for Custom Accounting
Custom Pricing Curves: Hooks can implement entirely different pricing mechanisms, effectively replacing the concentrated liquidity curve with custom mathematics.
Fee Structures: Hooks can charge additional fees on swaps or liquidity operations, redistributing value to stakeholders.
Liquidity Incentives: Hooks can modify liquidity amounts to implement reward or penalty mechanisms.
Protocol Integration: Hooks can integrate with external protocols, modifying amounts to account for cross-protocol interactions.
The flexibility of custom accounting transforms V4 from a fixed AMM into a framework for building custom exchange mechanisms.
User Operations and Position Management
Position Management in V4
Unique Position Identification
In V4, each liquidity position is uniquely identified by a salt parameter, making every position distinct even if it occupies the same price range as another position. This isolation enables:
- Individual position tracking
- Custom position-level logic through hooks
- Better accounting and fee distribution
- Enhanced position management capabilities
PositionManager Architecture
The PositionManager contract provides a user-friendly interface for managing liquidity positions. It uses a command-style architecture that enables batching multiple operations:
// Conceptual command structure
bytes memory commands = abi.encodePacked(
ADD_LIQUIDITY_COMMAND,
abi.encode(poolKey, tickLower, tickUpper, liquidityDelta),
SWAP_COMMAND,
abi.encode(poolKey, swapParams),
REMOVE_LIQUIDITY_COMMAND,
abi.encode(poolKey, positionId, liquidityDelta)
);
positionManager.modifyLiquidities(commands, deadline);
This design allows complex, multi-step operations within a single transaction, all benefiting from flash accounting’s gas efficiency.
Slippage Protection
V4 implements sophisticated slippage protection that accounts for fees separately from principal amounts:
- Maximum Amounts: For operations that increase your position (deposits), specify the maximum tokens you’re willing to provide
- Minimum Amounts: For operations that decrease your position (withdrawals), specify the minimum tokens you must receive
- Fee Exclusion: Slippage calculations exclude fees, which are handled automatically by flash accounting
This approach provides more accurate slippage protection since fees are managed separately from the core operation amounts.
Performance Optimization
Gas Optimization Techniques
Storage Optimization
V4 employs several techniques to minimize gas costs:
Packed Storage: The Slot0 struct packs multiple values (price, tick, fees) into a single storage slot, reducing storage operations.
Transient Storage: Delta tracking uses transient storage (EIP-1153), which is cheaper than regular storage and automatically clears.
Extsload: The protocol uses base extload functionality to reduce bytecode size while maintaining familiar function interfaces through libraries.
Operation Batching
Flash accounting enables batching multiple operations without intermediate transfers:
// Single transaction can:
// 1. Swap tokenA -> tokenB in pool1
// 2. Swap tokenB -> tokenC in pool2
// 3. Swap tokenC -> tokenD in pool3
// 4. Add liquidity to pool4
// All with a single final settlement
This batching capability reduces gas costs by eliminating redundant token transfers and storage operations.
Singleton Benefits
The singleton architecture provides gas savings at multiple levels:
- Pool Creation: State update vs. contract deployment (estimated 99% reduction)
- Multi-Pool Swaps: No token transfers between pool contracts
- Shared Storage: Common data structures reduce overall storage footprint
Security and Best Practices
Security Considerations
Singleton as a High-Value Target
The PoolManager contract holds all protocol assets, making it an attractive target for attackers. V4 addresses this through:
- Comprehensive security audits
- Formal verification of critical components
- Extensive testing including fuzzing
- Careful access control on all state-modifying functions
Hook Security
Hooks introduce additional attack vectors that developers must consider:
Reentrancy: Hooks can call back into PoolManager, requiring careful reentrancy protection
Access Control: Hooks must properly restrict who can call their functions
State Validation: Hooks must ensure their modifications don’t break pool invariants
Gas Limits: Malicious hooks could consume excessive gas; the system includes protections against this
Delta Settlement Enforcement
The requirement to settle all positive deltas prevents several attack vectors:
- Prevents leaving tokens in an unsettled state
- Ensures atomicity of operations
- Protects against accounting errors
Developers must always ensure their code properly settles all deltas before unlock completes.
Conclusion
Uniswap V4 is more than an upgrade, it’s a reimagining of what an AMM can be. By introducing hooks, flash accounting, dynamic fees, and native token support, V4 transforms from a fixed protocol into a flexible framework for building the next generation of DeFi applications.
The technical innovations in V4, the singleton architecture, transient storage-based accounting, and modular hook system, work together to create a platform that is simultaneously more efficient, more flexible, and more powerful than its predecessors.
As the ecosystem evolves, V4 provides the foundation for unprecedented innovation in decentralized finance, enabling developers to experiment with new models while maintaining the security and reliability that users expect.
Additional Resources
- Official Documentation: Uniswap V4 Docs
- Core Repository: v4-core on GitHub
This guide provides a comprehensive overview of Uniswap V4’s architecture and features. For implementation details and latest updates, refer to the official Uniswap documentation and codebase.
