Albert.Lin

Posted on Oct 08, 2023Read on Mirror.xyz

Exploring the Core Mechanism of UniswapV4

Since the announcement of UniswapV4, this swapping platform has undergone a significant transformation, evolving from a simple swapping platform into an infrastructure service provider. Especially noteworthy is the Hooks feature in V4, which has garnered widespread attention. After conducting in-depth research over a period of time, I have compiled some content to help everyone better understand this transformation and its implementation.

The innovation in UniswapV4 doesn't focus on improving AMM technology per se but rather on expanding the ecosystem. Specifically, this innovation includes several key features:

  1. Flash Accounting

  2. Singleton Contract

  3. Hooks Architecture

In the following sections, I will provide a detailed explanation of the significance of these features and how they are implemented.

source: https://twitter.com/jermywkh/status/1670779830621851650


Flash Accounting

Double Entry Bookkeeping

UniswapV4 adopts a recording method similar to Double Entry Bookkeeping to track the changes in token balances corresponding to each operation. This double-entry bookkeeping recording method requires that each transaction must be recorded in multiple accounts simultaneously, ensuring that the asset values between these accounts remain balanced. For example, suppose a user exchanges 100 TokenA for 50 TokenB in the pool. The ledger would record it as follows:

  • USER: TokenA decreases by 100 units (-100), and TokenB increases by 50 units (+50).

  • POOL: TokenA increases by 100 units (+100), and TokenB decreases by 50 units (-50).

Operations Related to Token Delta

In UniswapV4, the primary operations use this accounting method and employ a storage variable called lockState.currencyDelta[currency] in the code to record the change in token balances. The numerical value of this change, if positive, represents the expected increase in the token within the pool, while if negative, it signifies the expected decrease in the token within the pool. From another perspective, a positive value indicates the shortage of tokens in the pool (the expected amount to be received), whereas a negative value indicates the surplus of tokens in the pool (the expected amount for users to withdraw). The following lists the impact of various operations on Token Delta:

Here is the translation of the descriptions of various operations on Token Delta in UniswapV4:

  • modifyPosition: Represents the execution of Add/Remove liquidity operations. For Add liquidity, it uses addition to update Token Delta (indicating the expected addition of TokenA to the pool). For Remove liquidity, subtraction is used to update Token Delta (indicating the expected withdrawal of TokenB from the pool).

  • swap: Represents the execution of a Swap operation. Taking the example of swapping TokenA for TokenB, addition is used to update TokenADelta, while subtraction is used to update TokenBDelta.

  • settle: Accompanies the transfer of tokens to the Pool. The Pool calculates the increase in tokens before and after the operation and uses subtraction to update Token Delta. If the Pool receives the exact expected amount of tokens, the subtraction here precisely sets TokenDelta to zero.

  • take: Accompanies the withdrawal of tokens from the Pool. The Pool uses addition to update Token Delta, indicating that tokens have been removed from this Pool.

  • mint: The behavior of updating Token Delta is similar to "take," but minting does not actually withdraw tokens from the pool. Instead, it issues corresponding ERC1155 Tokens as proof of withdrawal, while the tokens remain in the pool. Later, users can retrieve tokens from the pool by burning the ERC1155 Tokens. There are two purposes for this: 1. To save gas costs associated with ERC20 token transfers (contract call + one less storage write). In the future, TokenDelta can be updated using the burn method of ERC1155 tokens for trading purposes. 2. To keep liquidity in the pool, maintaining liquidity depth for users to have a better Swap Token experience.

  • donate: Indicates the intention to donate tokens to the Pool, but in practice, you still need to use "settle" to send the tokens into the Pool. Therefore, addition is used here to update Token Delta.

The operations described above only involve actual token transfers in the cases of "settle" and "take." For the other operations, their primary purpose is to update the TokenDelta values without physically transferring tokens.

Token Delta Example

Let's illustrate how TokenDelta is updated with a simple example. Suppose today we exchange 100 TokenA for 50 TokenB:

  1. Before the transaction starts, both TokenADelta and TokenBDelta are 0.

  2. Swap: Calculate how much TokenA the Pool needs to receive and how much TokenB the user will receive. At this point, TokenADelta = 100, and TokenBDelta = -50.

  3. Settle: Send 100 TokenA into the Pool and update TokenADelta = 100 - 100 = 0.

  4. Take: Withdraw 50 TokenB from the Pool to the user's account, and update TokenBDelta = -50 + 50 = 0.

Absolutely, when TokenADelta and TokenBDelta are both reset to 0 after the entire exchange operation is completed, it signifies that the operation is fully balanced, ensuring consistency in account balances. This meticulous tracking and resetting mechanism are essential for maintaining accurate and reliable accounting within the system.

EIP-1153: Transient storage opcodes

Mentioned that UniswapV4 uses Storage Variables to record TokenDelta, but within smart contracts, reading and writing to Storage Variables can be quite costly. This is where another EIP introduced by Uniswap comes into play: EIP1153 - Transient Storage Opcodes.

UniswapV4 plans to utilize the TSTORE and TLOAD opcodes provided by EIP1153 to update TokenDelta. Storage Variables using Transient Storage Opcodes will be discarded after the transaction ends (similar to Memory Variables), which means they do not need to be written to disk, thus reducing gas costs.

EIP1153 has been confirmed to be included in the upcoming Dencun Upgrade, and UniswapV4 has indicated that they will go live with this upgrade after Constantinople. This approach should help reduce the gas costs associated with these operations while still maintaining accurate accounting within the protocol.

source: https://etherworld.co/2022/12/13/transient-storage-for-beginners/

Flash Accounting — Lock

UniswapV4 has introduced a locking mechanism, which means that before performing any Pool operations, you must first call PoolManager.lock() to acquire a lock. Before the execution of lock() is complete, it checks whether the TokenDelta value is 0; otherwise, it will trigger a revert. After calling PoolManager.lock() and successfully acquiring the lock, the lockAcquired() function of msg.sender will be called. In the lockAcquired() function, the operations related to the Pool (such as swap, modifyPosition, etc.) are executed.

Let's illustrate this process with a diagram. When a user needs to perform a Token Swap operation, they must call a Smart Contract with a lockAcquired() function (referred to as the Callback Contract). The Callback Contract will first call PoolManager.lock(), and then PoolManager will call the lockAcquired() function of the Callback Contract. In the lockAcquired() function, the logic related to Pool operations, such as swap, settle, and take, is defined. Finally, just before the entire lock() is about to end, PoolManager checks whether the TokenDelta related to this operation has been reset to 0 to ensure that the assets in the Pool remain balanced.

This locking mechanism helps ensure the integrity and consistency of operations involving the Pool, preventing any discrepancies or unintended changes in token balances.


Singleton Contract

The introduction of the Singleton Contract in UniswapV4 signifies the abandonment of the previous Factory-Pool model. Each Pool is no longer an independent Smart Contract; instead, all Pools share a single singleton contract. This design, combined with the Flash Accounting mechanism, only requires the updating of necessary Storage Variables, further reducing the complexity and cost of operations.

Let's illustrate this with a diagram. In the case of UniswapV3, converting ETH to DAI required at least four Token transfers (Storage write operations). This involved multiple changes recorded for USDC, USDT, and DAI Tokens. However, with the improvements in UniswapV4, coupled with the Flash Accounting mechanism, only one Token transfer is needed (transferring DAI from the Pool to the user), significantly reducing the number of operations and costs.

This streamlined approach simplifies transactions and makes them more cost-efficient, benefiting users of the UniswapV4 platform.

source: https://twitter.com/Uniswap/status/1671208668304486404


Hooks Architecture

UniswapV4's most notable update in this release is the Hooks Architecture. This update provides significant flexibility around Pool usability. Hooks refer to additional actions called when specific operations are performed on the Pool. These actions are categorized into different classes, including initialize (create pool), modifyPosition (add/remove liquidity), swap, and donate, with each class having pre-execution and post-execution actions:

  • beforeInitialize / afterInitialize

  • beforeModifyPosition / afterModifyPosition

  • beforeSwap / afterSwap

  • beforeDonate / afterDonate

This design allows users to execute custom logic before and after specific operations, providing greater flexibility in extending the functionality of UniswapV4.

source: https://github.com/Uniswap/v4-core/blob/main/whitepaper-v4-draft.pdf

Hook Example — Limit Order Hook

Next, we will use an example of a Limit Order to explain the actual operational process of Hooks. Before we begin, let's briefly explain the principle of implementing Limit Orders in UniswapV4.

The principle behind implementing limit orders in UniswapV4 involves adding liquidity to a specific price range and then executing the removal of liquidity if that range of liquidity is traded.

For example, let's say we added liquidity in the price range of 1900–2000 for ETH, and then the price of ETH rises from 1800 to 2100. At this point, all the ETH liquidity we previously added in the 1900–2000 price range has been exchanged for USDC (assuming it's an ETH-USDC Pool). Removing liquidity at this moment is equivalent to executing a market order for ETH at the current price range of 1900–2000.

Limit Order Hook Contract

This example is provided by UniswapV4 on its GitHub platform. In this example, the Limit Order Hook contract offers two hooks, namely afterInitialize and afterSwap. The afterInitialize hook is used to record the price range (tick) when creating the Pool, allowing identification of which limit orders have been matched after someone performs a swap.

Place Order

When a user needs to place an order, the Hook contract executes the operation of adding liquidity based on the price range and quantity specified by the user. In the Limit Order Hook contract, you can observe the presence of the place() function. The primary logic involves calling the lockAcquiredPlace() function after acquiring the lock, which executes the operation of adding liquidity. This part is equivalent to placing a limit order.

source: https://github.com/Uniswap/v4-periphery/blob/main/contracts/hooks/examples/LimitOrder.sol#L246

afterSwap Hook

After a user completes a token swap within this Pool, the Pool calls the afterSwap() function of the Hook contract. The main logic behind afterSwap() is to remove liquidity for the previously executed order operations between the previous price range and the current price range. This behavior is equivalent to the order being filled, indicating that the order has been executed.

source: https://github.com/Uniswap/v4-periphery/blob/main/contracts/hooks/examples/LimitOrder.sol#L192

Limit Order Flow

The entire process of implementing a Limit Order using the Hook mechanism is as follows:

  1. The order placer sends the order to the Hook contract.

  2. The Hook contract executes the liquidity addition operation based on the order information.

  3. Regular users perform token swaps within the Pool.

  4. After the token swap is completed, the Pool calls the afterSwap() function of the Hook contract.

  5. The Hook contract, based on the price range changes from the token swap, executes the removal of liquidity for the executed limit orders.

This sequence illustrates how the Hook mechanism is used to implement the entire process of Limit Orders within the UniswapV4 framework.


Hook: Other features

Hooks Contract Address Bit

Whether to execute the before/after specific operations is determined by the leftmost 1 byte of the Hook contract address. 1 byte consists of 8 bits, which precisely corresponds to 8 additional actions. The Pool checks whether the bit of that action is set to 1 to determine whether it should call the corresponding hook function in the Hook contract. This also means that Hook contract addresses need to be designed in a specific way and cannot be arbitrarily chosen as Hook contracts. This design primarily aims to reduce gas consumption by shifting the cost to contract deployment, thereby achieving more efficient operations. (PS: In practice, you can use different CREATE2 salts to calculate contract addresses that meet the criteria through brute force.)

Dynamic Fee

In addition to allowing additional actions to be executed before and after each operation, Hooks also support the implementation of dynamic fees. When creating a Pool, you can specify whether to enable dynamic fees. If dynamic fees are enabled, the getFee() function of the Hook contract is called during token swaps. The Hook contract can then determine how much fee should be charged based on the current state of the Pool. This design allows for fee calculations to be adjusted based on real-time conditions, enhancing the flexibility of the system.

Pool Creation

Each Pool needs to determine its Hook contract at the time of creation, and this cannot be changed afterward (though different Pools can share the same Hook contract). This is primarily because Hooks are considered part of the PoolKey, and PoolManager uses the PoolKey to identify which Pool to operate on. Even if the assets are the same, having different Hook contracts would classify them as different Pools. This design ensures that the states and operations of different Pools can be independently managed and maintains the consistency of each Pool.

However, this design can also lead to increased complexity in routing as the number of Pools grows. Solutions like UniswapX might be designed to address this issue.


TL;DR

  • Flash Accounting is used to track the quantity changes of each Token, ensuring that all changes are reset to zero after a transaction. To save on gas fees, Flash Accounting utilizes a special storage method provided by EIP1153.

  • The Singleton Contract design helps reduce gas consumption by avoiding updates to multiple storage variables.

  • The Hooks architecture provides additional operations divided into "pre-execution" and "post-execution" phases, making each Pool operation more flexible but also complicating Pool routing.

UniswapV4 clearly emphasizes expanding the entire Uniswap ecosystem and transforming it into infrastructure for more services to be built on Uniswap Pools. This helps enhance Uniswap's competitiveness and reduces the risk of other services replacing it, but its success remains to be observed. Some highlights include the combination of Flash Accounting and EIP1153, which we believe will lead to more services adopting these features and various application scenarios emerging in the future. This is the core concept of UniswapV4, and we hope it provides a deeper understanding of how UniswapV4 operates. If there are any errors in the article, please feel free to correct them, and we welcome discussions and feedback.


Reference