Since the announcement of UniswapV4, this swap platform has undergone a significant transformation, evolving from a simple swap platform to an infrastructure service provider. In particular, the Hooks feature of V4 has gained widespread attention. After in-depth research, I have compiled some content to help everyone better understand this transformation and its implementation.
The focus of UniswapV4’s innovation is not just about improving AMM technology but also about expanding the ecosystem. Specifically, this innovation includes the following key features:
In the following sections, I will explain in detail the significance of these features and their implementation principles.
source: https://twitter.com/jermywkh/status/1670779830621851650
UniswapV4 adopts a record-keeping method similar to Double Entry Bookkeeping to track the balance changes of tokens corresponding to each operation. This Double Entry Bookkeeping method requires recording each transaction in multiple accounts simultaneously and ensuring the balance of assets between these accounts remains balanced. For example, suppose a user exchanges 100 TokenA for 50 TokenB from the pool. The record in the ledger would be as follows:
In UniswapV4, this record-keeping method is primarily used for major operations, and a storage variable named lockState.currencyDelta[currency] is used in the code to record the amount of token balance changes. If the value of this delta is positive, it represents the expected increase in the token amount in the pool, while a negative value represents the expected decrease in the token amount. Alternatively, if the value is positive, it indicates the amount of token shortage in the pool (the expected amount to be received), while a negative value indicates the excess token in the pool (the expected amount for users to withdraw). The following list shows the effects of various operations on Token Delta:
Among these operations, only “settle” and “take” involve the actual transfer of tokens, while other operations are solely responsible for updating the TokenDelta value.
Here we use a simple example to illustrate how to update TokenDelta. Let’s assume that today we exchange 100 TokenA for 50 TokenB:
When the entire exchange operation is completed, both TokenADelta and TokenBDelta are reset to 0. This means that the operation has been completely balanced, thus ensuring the consistency of account balances.
Previously, it was mentioned that UniswapV4 utilizes Storage Variables to record TokenDelta. However, within the contract, reading and writing to Storage Variables are quite expensive. This brings us to another EIP introduced by Uniswap: EIP1153 - Transient Storage Opcodes.
UniswapV4 plans to use the TSTORE and TLOAD opcodes provided by EIP1153 to update TokenDelta. Storage Variables that adopt Transient Storage Opcodes will be discarded after the end of the transaction (similar to Memory Variables), thus reducing gas fees.
EIP1153 has been confirmed to be included in the upcoming Cancun upgrade, and UniswapV4 has also stated that it will go live after the Cancun upgrade, as reported here.
source: https://etherworld.co/2022/12/13/transient-storage-for-beginners/
UniswapV4 introduces a lock mechanism, which means that before performing any Pool operation, you must first call PoolManager.lock() to acquire a lock. During the execution of lock(), it checks if the TokenDelta value is 0, otherwise it will revert. Once PoolManager.lock() is successfully acquired, it calls the lockAcquired() function of msg.sender. Inside the lockAcquired() function, the operations related to the Pool, such as swap and modifyPosition, are performed.
The process is illustrated below. When a user needs to perform a Token Swap operation, they must call a Smart Contract with the lockAcquired() function (referred to as the Callback Contract). The Callback Contract first calls PoolManager.lock(), and then PoolManager calls the lockAcquired() function of the Callback Contract. Inside the lockAcquired() function, the logic related to Pool operations, such as swap, settle, and take, is defined. Finally, when the lock() is about to end, PoolManager checks if the TokenDelta associated with this operation has been reset to 0, ensuring the balance of assets in the Pool remains intact.
Singleton Contract means that UniswapV4 has abandoned the previous Factory-Pool model. Each Pool is no longer an independent Smart Contract, but all Pools share a single singleton contract. This design, combined with the Flash Accounting mechanism, only requires updating the necessary Storage Variables, further reducing the complexity and cost of operations.
In the example below, using UniswapV3 as an example, exchanging ETH for DAI would require at least four Token transfers (Storage write operations). This includes 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 (moving DAI from the Pool to the user), significantly reducing the number of operations and costs.
source: https://twitter.com/Uniswap/status/1671208668304486404
In the latest update of UniswapV4, the most notable feature is the Hooks Architecture. This update brings great flexibility in terms of Pool availability. Hooks are additional actions that are triggered through the Hooks Contract when performing specific operations on the Pool. These actions are categorized into initialize (create pool), modifyPosition (add/remove liquidity), swap, and donate. Each category has pre-execution and post-execution actions.
This design allows users to execute custom logic before and after specific operations, making it more flexible and expanding the functionality of UniswapV4.
source: https://github.com/Uniswap/v4-core/blob/main/whitepaper-v4-draft.pdf
Next, we will use an example of a Limit Order to explain the actual operation process of Hooks in UniswapV4. Before we begin, let’s briefly explain the principle of implementing Limit Orders in UniswapV4.
The UniswapV4 implementation of the limit order works by adding liquidity to a specific price range and then executing the remove liquidity operation if the liquidity in that range is swapped.
For example, let’s say we add 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 swapped for USDC (assuming in the ETH-USDC pool). By removing the liquidity at this moment, we can achieve a similar effect to executing an ETH market order at the current price range of 1900-2000.
This example is taken from GitHub of UniswapV4. In this example, the Limit Order Hook contract provides two hooks, namely afterInitialize and afterSwap. The afterInitialize hook is used to record the price range (tick) when creating a pool, in order to determine which limit orders have been matched after someone swaps.
When the user needs to place an order, the Hook contract executes the liquidity addition operation based on the user-specified price range and quantity. In the Hook contract for limit orders, you can see the place() function. The main logic is to call the lockAcquiredPlace() function after acquiring the lock to execute the liquidity addition operation, which is equivalent to placing a limit order.
source: https://github.com/Uniswap/v4-periphery/blob/main/contracts/hooks/examples/LimitOrder.sol#L246
After the user completes a Swap Token within this Pool, the Pool will invoke the afterSwap() function of the Hook contract. The main logic of afterSwap is to remove the liquidity of previously placed orders that have been executed between the previous price range and the current price range. This behavior is equivalent to the order being filled.
source: https://github.com/Uniswap/v4-periphery/blob/main/contracts/hooks/examples/LimitOrder.sol#L192
Here is a flowchart illustrating the process of executing a limit order:
The above is the entire process of implementing Limit-Order using the Hook mechanism.
Hooks have several interesting points that I find worth sharing.
The decision to perform specific operations before/after is determined by the leftmost 1 byte of the Hook contract address. 1 byte is equal to 8 bits, which corresponds to 8 additional actions. The Pool will check if the bit of that action is 1 to determine whether to invoke the corresponding hook function of the Hook contract. This also means that the address of the Hook contract needs to be designed in a specific way and cannot be arbitrarily chosen as the Hook contract. This design is mainly aimed at reducing gas consumption and shifting the cost to contract deployment to achieve more efficient operations. (PS: In practice, different CREATE2 salts can be used to brute force calculate contract addresses that meet the conditions)
In addition to being able to perform additional operations before and after each action, 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 when swapping tokens. The Hook contract can determine the amount of fees to be charged based on the current state of the Pool. This design allows for flexible fee calculation based on the actual circumstances, increasing the system’s flexibility.
Each Pool needs to determine the Hook contract during its creation, and it cannot be changed afterward (although different Pools can share the same Hook contract). This is mainly because Hooks are considered part of the PoolKey, and the PoolManager uses the PoolKey to identify which Pool to operate on. Even if the assets are the same, if the Hook contract is different, it will be considered as a different Pool. This design ensures that the state and operations of different Pools can be managed independently, ensuring the consistency of Pools. However, it also increases the complexity of routing as the number of Pools increases (perhaps UniswapX is designed to solve this problem).
UniswapV4 clearly emphasizes expanding the entire Uniswap ecosystem, turning it into infrastructure to enable more services to be built on the foundation of Uniswap Pools. This helps enhance Uniswap’s competitiveness and reduces the risk of alternative services. However, whether it will achieve the expected success remains to be seen. Some highlights include the combination of Flash Accounting and EIP1153, and we believe that more services will adopt these features in the future, leading to various application scenarios. 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 point them out. We also welcome discussions and feedback.
Finally, we would like to thank Anton Cheng and Ping Chen for reviewing the article and providing valuable feedback!
Since the announcement of UniswapV4, this swap platform has undergone a significant transformation, evolving from a simple swap platform to an infrastructure service provider. In particular, the Hooks feature of V4 has gained widespread attention. After in-depth research, I have compiled some content to help everyone better understand this transformation and its implementation.
The focus of UniswapV4’s innovation is not just about improving AMM technology but also about expanding the ecosystem. Specifically, this innovation includes the following key features:
In the following sections, I will explain in detail the significance of these features and their implementation principles.
source: https://twitter.com/jermywkh/status/1670779830621851650
UniswapV4 adopts a record-keeping method similar to Double Entry Bookkeeping to track the balance changes of tokens corresponding to each operation. This Double Entry Bookkeeping method requires recording each transaction in multiple accounts simultaneously and ensuring the balance of assets between these accounts remains balanced. For example, suppose a user exchanges 100 TokenA for 50 TokenB from the pool. The record in the ledger would be as follows:
In UniswapV4, this record-keeping method is primarily used for major operations, and a storage variable named lockState.currencyDelta[currency] is used in the code to record the amount of token balance changes. If the value of this delta is positive, it represents the expected increase in the token amount in the pool, while a negative value represents the expected decrease in the token amount. Alternatively, if the value is positive, it indicates the amount of token shortage in the pool (the expected amount to be received), while a negative value indicates the excess token in the pool (the expected amount for users to withdraw). The following list shows the effects of various operations on Token Delta:
Among these operations, only “settle” and “take” involve the actual transfer of tokens, while other operations are solely responsible for updating the TokenDelta value.
Here we use a simple example to illustrate how to update TokenDelta. Let’s assume that today we exchange 100 TokenA for 50 TokenB:
When the entire exchange operation is completed, both TokenADelta and TokenBDelta are reset to 0. This means that the operation has been completely balanced, thus ensuring the consistency of account balances.
Previously, it was mentioned that UniswapV4 utilizes Storage Variables to record TokenDelta. However, within the contract, reading and writing to Storage Variables are quite expensive. This brings us to another EIP introduced by Uniswap: EIP1153 - Transient Storage Opcodes.
UniswapV4 plans to use the TSTORE and TLOAD opcodes provided by EIP1153 to update TokenDelta. Storage Variables that adopt Transient Storage Opcodes will be discarded after the end of the transaction (similar to Memory Variables), thus reducing gas fees.
EIP1153 has been confirmed to be included in the upcoming Cancun upgrade, and UniswapV4 has also stated that it will go live after the Cancun upgrade, as reported here.
source: https://etherworld.co/2022/12/13/transient-storage-for-beginners/
UniswapV4 introduces a lock mechanism, which means that before performing any Pool operation, you must first call PoolManager.lock() to acquire a lock. During the execution of lock(), it checks if the TokenDelta value is 0, otherwise it will revert. Once PoolManager.lock() is successfully acquired, it calls the lockAcquired() function of msg.sender. Inside the lockAcquired() function, the operations related to the Pool, such as swap and modifyPosition, are performed.
The process is illustrated below. When a user needs to perform a Token Swap operation, they must call a Smart Contract with the lockAcquired() function (referred to as the Callback Contract). The Callback Contract first calls PoolManager.lock(), and then PoolManager calls the lockAcquired() function of the Callback Contract. Inside the lockAcquired() function, the logic related to Pool operations, such as swap, settle, and take, is defined. Finally, when the lock() is about to end, PoolManager checks if the TokenDelta associated with this operation has been reset to 0, ensuring the balance of assets in the Pool remains intact.
Singleton Contract means that UniswapV4 has abandoned the previous Factory-Pool model. Each Pool is no longer an independent Smart Contract, but all Pools share a single singleton contract. This design, combined with the Flash Accounting mechanism, only requires updating the necessary Storage Variables, further reducing the complexity and cost of operations.
In the example below, using UniswapV3 as an example, exchanging ETH for DAI would require at least four Token transfers (Storage write operations). This includes 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 (moving DAI from the Pool to the user), significantly reducing the number of operations and costs.
source: https://twitter.com/Uniswap/status/1671208668304486404
In the latest update of UniswapV4, the most notable feature is the Hooks Architecture. This update brings great flexibility in terms of Pool availability. Hooks are additional actions that are triggered through the Hooks Contract when performing specific operations on the Pool. These actions are categorized into initialize (create pool), modifyPosition (add/remove liquidity), swap, and donate. Each category has pre-execution and post-execution actions.
This design allows users to execute custom logic before and after specific operations, making it more flexible and expanding the functionality of UniswapV4.
source: https://github.com/Uniswap/v4-core/blob/main/whitepaper-v4-draft.pdf
Next, we will use an example of a Limit Order to explain the actual operation process of Hooks in UniswapV4. Before we begin, let’s briefly explain the principle of implementing Limit Orders in UniswapV4.
The UniswapV4 implementation of the limit order works by adding liquidity to a specific price range and then executing the remove liquidity operation if the liquidity in that range is swapped.
For example, let’s say we add 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 swapped for USDC (assuming in the ETH-USDC pool). By removing the liquidity at this moment, we can achieve a similar effect to executing an ETH market order at the current price range of 1900-2000.
This example is taken from GitHub of UniswapV4. In this example, the Limit Order Hook contract provides two hooks, namely afterInitialize and afterSwap. The afterInitialize hook is used to record the price range (tick) when creating a pool, in order to determine which limit orders have been matched after someone swaps.
When the user needs to place an order, the Hook contract executes the liquidity addition operation based on the user-specified price range and quantity. In the Hook contract for limit orders, you can see the place() function. The main logic is to call the lockAcquiredPlace() function after acquiring the lock to execute the liquidity addition operation, which is equivalent to placing a limit order.
source: https://github.com/Uniswap/v4-periphery/blob/main/contracts/hooks/examples/LimitOrder.sol#L246
After the user completes a Swap Token within this Pool, the Pool will invoke the afterSwap() function of the Hook contract. The main logic of afterSwap is to remove the liquidity of previously placed orders that have been executed between the previous price range and the current price range. This behavior is equivalent to the order being filled.
source: https://github.com/Uniswap/v4-periphery/blob/main/contracts/hooks/examples/LimitOrder.sol#L192
Here is a flowchart illustrating the process of executing a limit order:
The above is the entire process of implementing Limit-Order using the Hook mechanism.
Hooks have several interesting points that I find worth sharing.
The decision to perform specific operations before/after is determined by the leftmost 1 byte of the Hook contract address. 1 byte is equal to 8 bits, which corresponds to 8 additional actions. The Pool will check if the bit of that action is 1 to determine whether to invoke the corresponding hook function of the Hook contract. This also means that the address of the Hook contract needs to be designed in a specific way and cannot be arbitrarily chosen as the Hook contract. This design is mainly aimed at reducing gas consumption and shifting the cost to contract deployment to achieve more efficient operations. (PS: In practice, different CREATE2 salts can be used to brute force calculate contract addresses that meet the conditions)
In addition to being able to perform additional operations before and after each action, 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 when swapping tokens. The Hook contract can determine the amount of fees to be charged based on the current state of the Pool. This design allows for flexible fee calculation based on the actual circumstances, increasing the system’s flexibility.
Each Pool needs to determine the Hook contract during its creation, and it cannot be changed afterward (although different Pools can share the same Hook contract). This is mainly because Hooks are considered part of the PoolKey, and the PoolManager uses the PoolKey to identify which Pool to operate on. Even if the assets are the same, if the Hook contract is different, it will be considered as a different Pool. This design ensures that the state and operations of different Pools can be managed independently, ensuring the consistency of Pools. However, it also increases the complexity of routing as the number of Pools increases (perhaps UniswapX is designed to solve this problem).
UniswapV4 clearly emphasizes expanding the entire Uniswap ecosystem, turning it into infrastructure to enable more services to be built on the foundation of Uniswap Pools. This helps enhance Uniswap’s competitiveness and reduces the risk of alternative services. However, whether it will achieve the expected success remains to be seen. Some highlights include the combination of Flash Accounting and EIP1153, and we believe that more services will adopt these features in the future, leading to various application scenarios. 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 point them out. We also welcome discussions and feedback.
Finally, we would like to thank Anton Cheng and Ping Chen for reviewing the article and providing valuable feedback!