Back to blogs
Written by
Giovanni Di Siena
Published on
November 6, 2024

Uniswap V4 vs V3: Architectural Changes and Technical Innovations with Code Examples

Discover Uniswap V4 architectural updates and technical innovations: new hook system, singleton pattern, flash accounting, fee tier flexibility, native token support.

Table of Contents

Uniswap V4 introduces a series of updates that build upon the Uniswap V3 concentrated liquidity model, focusing on improved efficiency and flexibility. The purpose of this article is to highlight the key architectural changes and technical innovations in Uniswap V4, including the new hook system, singleton pattern, flash accounting, fee tier flexibility, and native token support. These updates address various limitations of V3, such as gas inefficiencies and limited customizability in the AMM design. Code snippets/links have been included to contrast with V3 where appropriate.

Uniswap V4 core architectural changes

Singleton pattern & ERC-6909

One of the most significant changes in Uniswap V4 is the move to a singleton architecture where all assets are stored in and managed by a single PoolManager contract. This represents a fundamental shift from the V3 factory pattern where each pool required its own contract deployment and underscores the importance of a secure core protocol, as the singleton can be considered a honeypot for attackers.

The singleton approach dramatically reduces gas costs and improves capital efficiency by consolidating all token pairs into a single contract. This consolidation eliminates the need for routing token swaps between each individual pool, resulting in significant gas savings and improved system efficiency.

V4 introduces several other optimizations for state management in PoolManager, such as the base Extsload functionality that significantly reduces contract bytecode size by loading arbitrary storage slots; however, this does not come at the expense of developer experience as StateLibrary exposes familiar function names to wrap the abstract calculation of storage slots.

Another abstraction in V4 is the use of ERC-6909 tokens for the settlement of deltas (differences in token balances due to swaps or other pool interactions) and token management within the PoolManager. This is a gas-optimized alternative to ERC-1155, specifically designed for managing multiple tokens by their id in a single contract.

Uniswap V3 factory pattern:

contract UniswapV3Factory is UniswapV3PoolDeployer {
   mapping(address => mapping(address => mapping(uint24 => address))) public getPool;

   function createPool(
       address tokenA,
       address tokenB,
       uint24 fee
   ) external returns (address pool) {
       pool = deploy(address(this), token0, token1, fee, tickSpacing);
       getPool[token0][token1][fee] = pool;
   }
}

contract UniswapV3PoolDeployer {
   function deploy(
       address factory,
       address token0,
       address token1,
       uint24 fee,
       int24 tickSpacing
   ) internal returns (address pool) {
       parameters = Parameters({factory: factory, token0: token0, token1: token1, fee: fee, tickSpacing: tickSpacing});
       pool = address(new UniswapV3Pool{salt: keccak256(abi.encode(token0, token1, fee))}());
       delete parameters;
   }
}

Uniswap V4 singleton pattern:

contract PoolManager {
   mapping(PoolId id => Pool.State) internal _pools;

   function initialize(
       PoolKey memory key,
       uint160 sqrtPriceX96
   ) external noDelegateCall returns (int24 tick) {
       ...
       PoolId id = key.toId();
       tick = _pools[id].initialize(sqrtPriceX96, lpFee);
       ...
   }
}

library Pool {
   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);
   }
}

Flash accounting and transient storage

Building on the efficiency gains of the singleton pattern, Uniswap V4 introduces a “flash accounting” system that also helps to make routes between liquidity pools significantly more gas efficient. Unlike direct token transfers employed in V3, the V4 system tracks balance deltas throughout the lifetime of a single transaction and ensures all debts are settled at the end. It is imperative that this logic is correct; otherwise, the security of the protocol and its underlying assets could be compromised.

If there are unsettled positive deltas, users must deliberately call the clear() function to settle them or the execution reverts. This serves as a protection mechanism against unknowingly leaving unsettled deltas and prevents accidental state changes.

This design enables chaining multiple actions into a single transaction and reduces unnecessary token transfers, significantly improving gas efficiency in complex operations. Flash accounting is made possible in Uniswap V4 by the use of assembly-level transient storage for various operations, including:

Combined, these optimizations reduce gas costs by minimizing storage operations on both token transfers and internal state updates.

Uniswap V3 token management:

contract UniswapV3Pool {
   function swap(
       address recipient,
       bool zeroForOne,
       int256 amountSpecified,
       uint160 sqrtPriceLimitX96,
       bytes calldata data
   ) external override noDelegateCall returns (int256 amount0, int256 amount1) {
       ...
  
       // do the transfers and collect payment
       if (zeroForOne) {
           if (amount1 < 0) TransferHelper.safeTransfer(token1, recipient, uint256(-amount1));
      
           uint256 balance0Before = balance0();
           IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback(amount0, amount1, data);
           require(balance0Before.add(uint256(amount0)) <= balance0(), 'IIA');
       } else {
           if (amount0 < 0) TransferHelper.safeTransfer(token0, recipient, uint256(-amount0));
      
           uint256 balance1Before = balance1();
           IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback(amount0, amount1, data);
           require(balance1Before.add(uint256(amount1)) <= balance1(), 'IIA');
       }
   }
}

Uniswap V4 flash accounting:

contract PoolManager {
   function _settle(address recipient) internal returns (uint256 paid) {
       Currency currency = CurrencyReserves.getSyncedCurrency();

       // if not previously synced, or the syncedCurrency slot has been reset, expects native currency to be settled
       if (currency.isAddressZero()) {
           paid = msg.value;
       } else {
           if (msg.value > 0) NonzeroNativeValue.selector.revertWith();
           // Reserves are guaranteed to be set because currency and reserves are always set together
           uint256 reservesBefore = CurrencyReserves.getSyncedReserves();
           uint256 reservesNow = currency.balanceOfSelf();
           paid = reservesNow - reservesBefore;
           CurrencyReserves.resetCurrency();
       }

       _accountDelta(currency, paid.toInt128(), recipient);
   }

   function _accountDelta(Currency currency, int128 delta, address target) internal {
       if (delta == 0) return;
  
       (int256 previous, int256 next) = currency.applyDelta(target, delta);
  
       if (next == 0) {
           NonzeroDeltaCount.decrement();
       } else if (previous == 0) {
           NonzeroDeltaCount.increment();
       }
   }
}

Hooks and custom accounting

Another significant and highly anticipated change to the V4 protocol architecture is the introduction of hooks. Allowing integrators to “hook” into execution at specific pre-designated points unlocks massive potential for innovation and addresses a key design goal of enabling modifications to AMM mechanics without requiring full protocol forks (an endeavor often fraught with security vulnerabilities).

Uniswap V4 implements 14 distinct permissions across 8 hook types. Hook permissions are uniquely handled through address mining — hooks must be deployed to specific addresses to determine their permissions. This approach prevents permission changes without new contract deployments and optimizes gas usage by encoding permissions in the hook contract address itself, eliminating the need for external permission checks.

To get an idea of the custom accounting enabled by hooks, consider that V4 swaps support both positive and negative values for SwapParams.amountSpecified, meaning certain hook permissions (beforeSwap, afterSwap, beforeSwapReturnDelta, afterSwapReturnDelta) can return deltas to modify swap amounts. This can be used to implement custom trading curve logic (e.g. stable swap, dynamic fees, TWAMM, etc) and replace the vanilla V3 concentrated liquidity model,  creating entirely new AMM implementations. 

While hooks can alter swaps on concentrated liquidity, the amount specified by the caller remains constant (unless liquidity is depleted, as in both V3 and V4) and swaps succeed as expected if slippage requirements are met. Similar but more restrictive behavior is also possible in calls to Pool::modifyLiquidity.

In addition to customization, the implementation of hooks forms part of the wider Uniswap V4 security model through the unlock callback system, a key part of its security architecture. Any function that may apply a delta can be called when unlocked, with deltas validated before locking again. This makes any broken assumptions before calling _accountDelta() particularly critical for security:

contract PoolManager {
   modifier onlyWhenUnlocked() {
       if (!Lock.isUnlocked()) ManagerLocked.selector.revertWith();
       _;
   }

   function unlock(bytes calldata data) external override returns (bytes memory result) {
       if (Lock.isUnlocked()) AlreadyUnlocked.selector.revertWith();
  
       Lock.unlock();
  
       // the caller does everything in this callback, including paying what they owe via calls to settle
       result = IUnlockCallback(msg.sender).unlockCallback(data);
  
       if (NonzeroDeltaCount.read() != 0) CurrencyNotSettled.selector.revertWith();
       Lock.lock();
   }
}

Fee flexibility

To enable additional flexibility, Uniswap V4 removes V3’s restriction on fee tiers (0.05%, 0.30%, 1.00%). This allows pools to set any fee value, opening new possibilities for custom fee structures and allowing markets to find their optimal fee levels, even supporting dynamic LP fees where enabled.

With flexible fees and the integration of hooks in the PooolId struct, Uniswap V4 can support an infinite number of pools composed of the same currencies. The singleton architecture mitigates general token fragmentation because tokens are all stored in a single contract while market forces will address liquidity fragmentation as liquidity providers congregate in a set of generally recognized pools. The result is that any currency pair can have an infinite number of pools by specifying different fee and/or hook configurations. As such, V4 no longer supports on-chain enumeration of pools, so indexing of the Initialize event becomes more important.

Uniswap V3 fixed fees:

contract UniswapV3Factory {
   mapping(uint24 => int24) public override feeAmountTickSpacing;

   constructor() {
       ...
  
       feeAmountTickSpacing[500] = 10;
       feeAmountTickSpacing[3000] = 60;
       feeAmountTickSpacing[10000] = 200;
   }

   function createPool(
       address tokenA,
       address tokenB,
       uint24 fee
   ) external override noDelegateCall returns (address pool) {
       ...
  
       int24 tickSpacing = feeAmountTickSpacing[fee];
       require(tickSpacing != 0);
       require(getPool[token0][token1][fee] == address(0));
      
       ...
   }
}

Uniswap V4 flexible fees:

contract PoolManager {
   function initialize(
       PoolKey memory key,
       uint160 sqrtPriceX96
   ) external noDelegateCall returns (int24 tick) {
       ...
  
       if (!key.hooks.isValidHookAddress(key.fee)) Hooks.HookAddressNotValid.selector.revertWith(address(key.hooks));
  
       uint24 lpFee = key.fee.getInitialLPFee();
  
       ...
   }

   function updateDynamicLPFee(PoolKey memory key, uint24 newDynamicLPFee) external {
       if (!key.fee.isDynamicFee() || msg.sender != address(key.hooks)) {
           UnauthorizedDynamicLPFeeUpdate.selector.revertWith();
       }
       newDynamicLPFee.validate();
       PoolId id = key.toId();
       _pools[id].setLPFee(newDynamicLPFee);
   }
}

Native token support and custom types

In contrast to Uniswap V3, where native tokens are not supported without utilizing a wrapped ERC-20 alternative, Uniswap V4 has direct support for native tokens due, in part, to its extensive use of custom types. 

In addition to the Currency representation, which unifies ERC-20 and native token handling through a common transfer API, these custom types include:

One benefit here includes gas efficiency through in-memory packing of the Slot0 struct. Additionally, an interesting implementation detail around the return data size check in Currency stems from a need to accommodate some niche Curve v1 tokens that return 4096 bytes rather than strictly 32 bytes.

Note, certain tokens such as CELO have a native and ERC-20 representation that can make token handling through custom Currency type difficult to manage. Great care should be taken for such implementations to avoid introducing any security vulnerabilities, as documented in the Open Zeppelin audit report.

Periphery features

Position management

Uniswap V4 introduces an optimized position management system that leverages the core flash accounting system. 

The peripheral PositionManager contract has been redesigned to support a command-style interface similar to that of the UniversalRouter and V4Router contracts. While very different from the V3 NonFungiblePositionManager, this structure allows batched operations to both classic actions and delta-resolving functions, thus supporting complex multi-step actions within a single transaction.

contract PositionManager is BaseActionsRouter {
   function modifyLiquidities(bytes calldata unlockData, uint256 deadline)
       external
       payable
       isNotLocked
       checkDeadline(deadline)
   {
       _executeActions(unlockData);
   }
}

contract BaseActionsRouter {
   function _executeActions(bytes calldata unlockData) internal {
       poolManager.unlock(unlockData);
   }

   function _handleAction(uint256 action, bytes calldata params) internal virtual;
}

Unlike V3, Uniswap V4 positions are uniquely identified through the use of a salt parameter which makes each liquidity position distinct. Previously, two depositors within the same liquidity range shared pool state as a gas optimisation, but V4 isolates them for better customization and tracking. In V4, only the token id and underlying info are stored which can then be used to query the PoolManager directly.

The system enables users to specify maximum amounts when calling functions to increase positions and minimum amounts for those which decrease positions. Fees are automatically managed by flash accounting, so slippage validation is performed on the principal amount excluding fees.

Notifier/subscriber model

The Uniswap V4 periphery contracts introduce a new notifier/subscriber model to support staking actions without requiring NFT transfers. The design can be thought of as hooks for liquidity positions and allows subscribers to be registered for updates and monitor the status of positions, including any modifications to liquidity amounts and token transfers.

Operational costs for users are minimized while maintaining full ownership of their LP position. The implementation of gas limit validation ensures that the given subscriber is notified of an unsubscription and the try/catch block mitigates the risk of a malicious subscriber griefing its users by reverting. In this way, unsubscriptions are always successful.

UniversalRouter integration

Uniswap V4 swaps are now integrated into the UniversalRouter through the base Dispatcher contract. This enhances routing by allowing complex and optimized trade paths across Uniswap V2, V3, and V4 pools. This feature offers a seamless trading experience for users by accessing multiple pool types and routes.

The UniversalRouter also supports the migration of liquidity from V3 to V4 with the V3ToV4Migrator contract. Through nested actions, liquidity providers can seamlessly transfer their liquidity positions, utilizing flash accounting to ensure delta resolution without unnecessary token transfers.

Both of these features are enabled by the second of two entry points within the PositionManager which bypasses the unlock call on the PoolManager, allowing the UniversalRouter to manage positions in hooks.

Flexible action management

The V4 periphery contracts also allows users to settle deltas for swap amounts that may be unknown in advance. This is particularly helpful for fee-on-transfer tokens or complex routed trades. This feature is implemented through Actions constants, which handles flexible swap amounts and routes tokens based on delta resolutions.

This flexibility facilitates more robust trade execution and allows for better customization in the periphery. Note that ERC-6909 tokens do not currently have direct support from within the V4Router but this can be achieved using Actions constants and is likely to be added in future.

Conclusion

Uniswap V4 represents the next stage of evolution in AMM architecture. Through innovations such as flash accounting and the modular hook system, Uniswap V4 tackles many limitations present in V3 while iterating on the tried and tested concentrated liquidity model. 

These architectural changes also set the stage for more customizable liquidity models. This makes Uniswap V4 not just a DEX but a highly versatile framework for building the next-generation of decentralized finance applications, enabling developers to experiment, build, and innovate securely.

This is an exciting design space in which a focus on security, in particular that of custom hooks and by extension hook audits, is both challenging and incredibly important. To learn more about Uniswap V4 and go deeper into its security considerations, it is recommended to read the whitepaper, core protocol audits, periphery contract audits, and known effects of hook permissions.

Secure your protocol today

Join some of the biggest protocols and companies in creating a better internet. Our security researchers will help you throughout the whole process.
Stay on the bleeding edge of security
Carefully crafted, short smart contract security tips and news freshly delivered every week.