
In recent news, Uniswap Lab has announced the development progress of the next generation AMM Uniswap V4, and has publicly released the whitepaper and code repository. The V4 whitepaper is only 3 pages long because V4 did not make many modifications to the core algorithm of AMM. Instead, it added some new features on top of V3 to meet more scenario requirements. SharkTeam will analyze the new features brought by V4 based on the currently open-sourced code, and explore the best practices for the important feature introduced in V4, Hook.
1. Differences between V4 and V3
1.1 AMM
At the AMM algorithm level, Uniswap V4 did not make any modifications to V3 and still uses the constant product liquidity algorithm based on x*y=k.
In Uniswap V3, each trading pair can have 4 pools (originally 3, but later added a new 1 bp pool), representing pool fees of 0.01%, 0.05%, 0.3%, and 1%. These pools also have different tick spaces, and when creating a pool, you can only choose one of these 4 types.
In Uniswap V4, theoretically, each trading pair can have any number of pools, and each pool's fee rate can be any value. These pools can also have any tick space.
This also brings up a problem: the liquidity of trading pairs in Uniswap V4 will be fragmented, so a more efficient router/aggregator is needed to help users find the optimal trading path.
1.2 Hooks
Hooks are a group of contracts developed by third parties or Uniswap official that can be bound to a pool when creating it. Afterwards, at specific stages of the transaction, the pool will automatically invoke the Hook contract it is bound to. Uniswap V4 defines the following stages where hook contracts can be executed:
beforeInitialize
afterInitialize
beforeModifyPosition
afterModifyPosition
beforeSwap
afterSwap
beforeDonate
afterDonate
Represent respectively the before and after calls of the actions such as initializing the pool, adding/removing liquidity, trading, and donation.
The Hook contract needs to explicitly specify in which stages the execution should take place, and the pool needs to know whether the corresponding Hook needs to be executed at a certain stage. In order to save gas, these flags are not stored in the contract but require the Hook to use a specific address to indicate. The specific judgment code is as follows:
As can be seen, the first 8 bits of the Hook address are used to mark the flags indicating whether this Hook needs to be executed at a specific stage.
Therefore, the developer of the Hook needs to generate an address that meets the requirements of the Pool when deploying the contract, which usually requires using Create2 + calculating a random Salt to achieve.
The following is an example of Hook execution in the white paper:
You can see that before and after executing swap, the pool will first check whether the corresponding Hook for the pool has enabled the corresponding flag. If enabled, it will automatically call the corresponding function of the Hook contract.
1.3 Dynamic fee ratio
In addition to executing code at specific stages, Hooks can also determine the swap fee rate and withdraw fee rate for a specific pool. The withdraw fee rate refers to the fee that users need to pay to the Hook when removing liquidity. In addition, Hooks can also specify a portion of the swap fee to be taken for themselves.
When creating a pool, the fee parameter (uint 24) needs to be used to mark whether this pool uses a dynamic fee, and whether to enable the hook swap fee and withdraw fee:
If dynamic fee is enabled, the pool will call the Hook contract before each swap to obtain the current swap fee ratio. The Hook contract needs to implement the getFee() function to return the current swap fee ratio.
Hooks make Uniswap V4 a developer platform, giving AMMs more possibilities. Some functionalities that can be implemented using Hooks will be introduced in detail in subsequent chapters, including TWAMM (Time-Weighted Automated Market Maker), Limit Order, LP reinvestment, etc.
1.4 Singleton Contract
In Uniswap V3, each time a new pool is created, a new contract needs to be deployed, which consumes a large amount of gas. However, in fact, the code used by these pools is the same, only the initialization parameters are different. Uniswap V4 introduces the Singleton contract to manage all pools, so creating a new pool no longer requires deploying a new contract, saving gas for deploying contracts.
In addition, the advantage of using the Singleton contract is that it can reduce the transfer of tokens during the transaction process, because all pools are in the same contract, so the swap across pools can be directly completed within the contract. In V3, swapping across pools requires transferring tokens between different pools, which increases gas consumption.
Meanwhile, in V4, all pools use the same contract, and the token accounting within the contract is simplified to account for each token individually, rather than by pool. This makes it more convenient to borrow a large amount of tokens through flash loans.
1.5 extload
To facilitate integration with hooks and other contracts, the V4 contract has added the extload function, which makes all internal contract states readable externally, making the status of all pools completely transparent to the external world.
1.6 Flash Accounting
To reduce token transfers during cross-pool swaps, V4 also uses a method called Flash Accounting, which standardizes the process of swaps, add/remove liquidity, and flash loans, similar to flash loans:
(1) Users acquire a lock.
(2) Users perform any operation, such as swapping in multiple pools, adding/removing liquidity, or borrowing tokens from pools through flash loans.
(3) All token transfers generated by the user's operations are recorded in the lock.
(4) After all operations are completed, users can withdraw the tokens they obtained, while also needing to pay the tokens recorded in the lock.
These processes need to occur within a single transaction.
In this way, if a transaction needs to swap across multiple pools, only two transfers are needed for settlement. For example, in a swap like ETH->USDC-BTC, USDC as an intermediate token does not require any transfers.
1.7 ERC 1155 mint/burn
Flash Accounting reduces token transfers in the same transaction by using ERC 1155 tokens, further reducing token transfers in multiple transactions.
V4 allows you to store your tokens in the V4 contract through ERC 1155 mint, so you can use these tokens in multiple transactions without transferring the tokens to the V4 contract every time.
ERC 1155 burn can be used to withdraw tokens stored in the V4 contract.
ERC 1155 is suitable for users who frequently swap or add/remove liquidity. These users can store commonly used tokens directly in the V4 contract to reduce gas costs for token transfers.
2. Best Practices for Hooks
2.1 TWAMM (Time-Weighted Automated Market Maker)
Alice wants to buy $100 million worth of Ether on the blockchain. Executing such a large order on existing Automated Market Maker (AMM) platforms like Uniswap would be expensive as they may charge Alice high fees to prevent her from taking advantage of insider information to get better prices.
To get a better price, Alice's best option is to manually split the order into several smaller sub-orders and execute them gradually over several hours. The purpose of doing this is to give the market enough time to realize that she doesn't have insider information and thus give her better prices. However, even if she sends several larger sub-orders, each sub-order will still have a significant impact on the price and are vulnerable to "sandwich attacks" from adversarial traders.
TWAMM solves this problem by trading on behalf of Alice. It decomposes her orders into an infinite number of tiny virtual orders to ensure smooth execution over time. At the same time, TWAMM utilizes the special mathematical relationship of the embedded AMM protocol to share the Gas cost among these virtual orders. Since TWAMM processes transactions between blocks, it is also less vulnerable to "sandwich attacks".
Overall, TWAMM provides Alice with a more efficient way to conduct large-scale trades, avoiding high fees and potential market manipulation.
2.1.1 Principle
TWAMM has a built-in AMM, which is no different from other AMMs. Users can directly trade spot on this AMM and add liquidity to it. However, TWAMM also has two TWAP order pools for executing TWAP orders in two directions. When a user submits an order, specifying the token input amount and duration of the trade, TWAMM will place the order in the corresponding pool for the same direction and automatically execute the trade at the specified rate. Once the user's order is fully executed, they can withdraw the traded tokens. Of course, users can also cancel or modify orders before they are fully executed.
In Ethereum, smart contracts can only be triggered and executed by EOA addresses actively sending transactions. They cannot execute automatically. Therefore, TWAMM needs an EOA account to regularly send transactions to settle the pending token trades in its order pool. This requires a keeper account to execute these transactions.
Alternatively, TWAMM can automatically settle the order pool whenever it interacts with a user, eliminating the need for a keeper, which is a common approach for DeFi protocols dealing with streaming data.
2.1.2 Why is this trading model difficult to be sandwich attacked?
This type of attack is difficult to carry out. Since the timestamp of a block does not change, an attacker must manipulate the pool's price in the last transaction of a block for the settlement of TWAMM in the next block to be affected. This requires the sandwich attack to occur across multiple blocks, which undoubtedly exposes the attacker to significant risks, as other arbitrageurs may intervene in between and cause the attacker to incur losses.
Furthermore, due to the presence of arbitrageurs, such price manipulation is unlikely to be sustainable. Because of the characteristics of TWAP orders, it does not trade a large number of tokens in a short period of time, resulting in limited losses in most cases.
2.1.3 TWAMM Workflow in V4
(1) This Hook maintains two TWAP order pools, representing TWAP orders in two trading directions.
(2) Users can submit TWAP orders through this Hook, specifying the token, quantity, and duration of the trade.
(3) This Hook registers beforeSwap and beforeModifyPosition, which will be triggered whenever users trade or adjust their positions.
(4) When triggered, the Hook is responsible for settling the two TWAP order pools.
(5) Users can also manually trigger settlement at any time.
(6) Users can cancel or modify the token quantity in a TWAP order.
2.1.4 Example Explanation
TWAMM registers three stages to make logical calls to hooks. It initializes TWAMM before pool initialization and triggers this hook whenever users trade or adjust their positions.
Users can manually call the submitOrder function in TWAMM to submit their desired orders to the contract.
After users add their desired orders to the contract, the orders will be executed automatically whenever the pool performs swap and modifyPosition operations.
Each time a user calls the swap function in v4 for transactions or the modifyPosition function to change positions, it will trigger the execution function in TWAMM. The function will call the internal function _executeTWAMMOrders to continue the execution of orders that have not been completed before.
The _executeTWAMMOrders function
Above is the execution process for updating orders. After the execution is completed, the execution time of the current twamm orders will be updated.
2.2 Limit Order
Unlike market orders that are executed immediately at the last market price, limit orders are executed once the specified price is reached. Most DEXs based on Automated Market Makers (AMMs) default to market order systems, which are simple and easy to understand for beginners. Market orders are either executed or failed due to parameters such as maximum price impact. In limit orders, the order will only be executed when the asset price reaches the limit price; otherwise, the order will remain open.
For example, suppose ETH is currently traded at 1 ETH = 1500 DAI in the ETH/DAI pool. A user can place a take-profit order with the main content "Sell all my ETH if 1 ETH = 2000 DAI". If this price is reached, the user's ETH will be automatically exchanged for DAI in a decentralized manner on the chain.
In previous versions of Uniswap, limit orders were actually not possible. Most AMMs only allow market buy and sell orders. However, in version 4, due to the powerful features and scalability of hooks, the foundation for implementing limit orders was created.
2.2.1 Principle
The design principle of limit orders is simpler compared to TWAMM. Currently, limit orders related to adding liquidity have been implemented, making it relatively easier to implement limit orders for trading.
In version 4, with the existence of tickLower and tickUpper, which change based on the trading situation of the pool, the lower and upper values will also change. When users want to add liquidity but not at the current price, they can use the limit order hook to fulfill this requirement. In the hook, they can set the corresponding price. After each swap is completed, the hook will check the current pool price. If the set price is reached, the corresponding liquidity will be added to gain profits.
2.2.2 Workflow of Limit Orders in V4
1. limit maintains multiple epochs as limit orders for different lower values and trade directions.
2. Users submit their lower values and trade directions through this hook and add their limit orders to the contract.
3. This hook registers afterSwap and is triggered only when the price changes after each swap.
4. When triggered, the hook verifies the current price range and checks if there are any limit orders in the epoch that need liquidity added.
5. Users can withdraw or add liquidity at any time.
2.2.3 Detailed Example
The hook is triggered in two stages after contract registration. It is initialized after Pool initialization and its logic is triggered after each exchange.
The user calls the place function to pass in the quantity, price, and transaction direction they want to add liquidity to. The hook will first add the liquidity the user wants to the pool and then create corresponding limit orders for the user.
This hook is triggered after each swap. It completes the liquidity addition operation for limit orders within the current price range.
Users can use the kill function to cancel the limit orders and obtain the earnings from adding liquidity before the limit orders are completed.
Users can call the withdraw function when they want to remove liquidity to extract the desired liquidity.
In general, this limit order hook provides users with a more convenient way. Users can set the price they want to add liquidity at. When the price range in the pool reaches that price, the hook will automatically perform the operation of adding liquidity to the pool for the user. Users can also cancel and withdraw liquidity at any time.
In addition to TWAMM and Limit Order, hooks can also be used to implement functionalities such as LP reinvestment and dynamic fee changes. Due to the length, we will cover these topics in the following analysis:
LP reinvestment: Users can use hooks to add, modify, and remove liquidity. Hooks can register afterSwap and afterModifyPosition to make the calls. Since users use hooks uniformly to add liquidity, the address for adding liquidity in poolManager is only the hook. The hook can check the current time each time it is triggered, and after a certain time interval, choose to extract liquidity rewards and add the obtained LP tokens back to the pool to optimize users' earnings automatically.
Dynamic fee changes: Hooks can register interfaces like beforeSwap to modify dynamic fees before the swap. Dynamic fees can be changed in a non-linear way, for example, quantifying volatility based on the number of tick jumps generated by a single swap, dynamically adjusting fees, and hedging against impermanent loss for LP providers. This reduces the impact of impermanent loss caused by trading on liquidity providers.
III. Hooks Security Best Practices
Reduce the use of require and revert
In the logic of functions related to hooks calling on pools, try to minimize the use of fallback statements. As the pool contract is related to the hooks contract, when a transaction rollback occurs in the hooks, the transactions in the pool will also be rolled back. If there are fallback statements in the hooks that are unrelated to the normal transactions in the pool, it may cause users to be unable to use the functionality in the pool properly.
Avoid using self-destructive functions
Avoid using the selfdestruct function in hooks. If the self-destructive function is called in the hook, it will not only cause issues in the hook's logic, but also prevent the functionality in the pool from working properly, resulting in the loss of assets in the entire pool and the inability to use the functionality.
Strictly control permissions
Strictly control the permissions in the hook contract to avoid roles with excessive permissions. Implement multi-signature management for privileged roles to prevent single point attacks. Avoid situations where privileged roles can modify contract state variables arbitrarily, as it may result in logic errors causing the entire transaction to be rolled back, affecting the normal use of the pool. Follow the principle of least privilege and use the AccessControl contract from openzeppelin to achieve more granular access control, as this practice limits each system component to follow the principle of least privilege.
Implement reentrancy protection
As an external extension code of Pool, hooks should also pay attention to the possible reentrancy attack in the contract, such as the reentrancy that may occur when transferring native tokens in a limit order, which may cause loss of contract assets. Check and attempt to update all states before calling external contracts or the so-called "check-effect-interact" pattern. This way, even if reentrancy occurs, it will not have any impact because all states have been updated.
Contract Upgrade Control
Some developers may use proxy contracts to allow future changes and upgrades to hooks' logic, but this also requires attention to potential issues with contract upgrades. First, even if the proxy mode is adopted in hooks, the corresponding stages need to be declared in the proxy contract, such as beforeSwap, otherwise the Pool will not be able to verify the correct return values. Secondly, check whether the target contract exists before invoking delegatecall. Solidity will not perform this check for us, and ignoring this check may lead to unexpected behavior and security issues. Carefully consider the order of variable declarations, as there may be variable packing that stores multiple variables in the same slot, affecting gas costs, memory layout, delegate call results, and other issues.
About Us
SharkTeam's vision is to comprehensively protect the security of the Web3 world. The team consists of experienced security professionals and senior researchers from around the world, who are proficient in the underlying theory of blockchain and smart contracts, and provide services including smart contract audits, on-chain analysis, emergency response, etc. They have established long-term relationships with key participants in various fields of the blockchain ecosystem, such as Polkadot, Moonbeam, Polygon, OKC, Huobi Global, imToken, ChainIDE, etc.
Official Website: https://www.sharkteam.org
Twitter: https://twitter.com/sharkteamorg
Discord: https://discord.gg/jGH9xXCjDZ
Telegram: https://t.me/sharkteamorg
