Uniswap V4 is scheduled to be deployed on the Ethereum Mainnet in January 2025, and marks a significant evolution in DeFi. One of the most notable innovations in Uniswap V4 is the introduction of hooks, which allow developers to build custom logic on top of Uniswap. This enables the introduction of a wide range of applications without needing to modify the Uniswap V4 core contracts. However, the flexibility of the hook design also introduces new surface area for potential security vulnerabilities.
This article discusses some of the new features of Uniswap V4, and explores the security considerations related to Uniswap V4 hooks.
Uniswap V4 introduces several new features:
V4 hooks can be placed before/after initializing a liquidity pool, before/after modifying liquidity, before/after swapping, and before/after donating. Using swapping as an example, the following simplified diagram shows where hooks fit into the workflow.
When a swapping transaction is initiated, the swap function in the PoolManager
contract first checks if the beforeSwap
flag is true
, and if it is, the PoolManager
calls the beforeSwap
function of the corresponding hook contract. Then, the PoolManager
executes the core swapping logic. Subsequently, the afterSwap
flag is checked, and if it is true
, the PoolManager
calls the afterSwap
function of the same hook contract.
The two hook calls have return values that potentially impact the logic of the swap. For example, the beforeSwap
function call returns a lpFeeOverride
that can affect swap fees, and the afterSwap
function returns a hookDelta
that can affect the distribution of currency deltas between the msg.sender
and the hook address. If the beforeSwap
and afterSwap
flags are false
, then the hook is never called and the lines of code in PoolManager
with the hook logic are no-ops. The mechanism that Uniswap V4 employs to determine the permission of the hooks involves examining the least significant bits of the deployed hook address. Once a hook is deployed, the permissions cannot be changed.
Before the deployment of Uniswap V4, various ETH communities and the Uniswap Foundation funded incubators and organized hackathons specifically for the development of Uniswap V4 hooks. As a part of our security research process, our security engineers have reviewed multiple hooks submissions from the incubators and hackathons.
The following chart illustrates a simplified mental model for the security considerations of hooks.
Users can interact with Uniswap V4 PoolManager
either via a Router (or Universal Router) for swapping, or a Position Manager for liquidity modification, and the PoolManager
calls the corresponding hook contract. Users can also interact with a Hook contract directly which, in turn, calls the PoolManager
contract. For the User-Hook interface, a user can be a victim (if the hook is malicious) or a potential exploiter of the hook (if the hook is vulnerable). For the Hook - V4 PoolManager
interface, the security measures in PoolManager
are designed to protect themselves against a malicious hook. However, the integration of a hook can still be incorrect which can cause unintended consequences.
The V4-core PoolManager contract enforces several requirements on the integration of hooks. Hook developers should be aware of these requirements, to ensure that their hooks would function as intended. Potential issues of incorrectly configured hooks include the following:
Each hook is designed to operate with specific permissions in Uniswap V4. To determine whether a hook has certain permissions, V4 compares the least significant bits of the hook’s deployed address with a flag that corresponds to the different permission.
Source: Github
Source: Github
An incorrect implementation may occur when a hook function is deployed at an address that lacks the necessary permissions, or conversely, when the deployed address has the permission, but the function itself is not implemented. If a hook does not have the permission, the hook call is essentially a no-op. If the hook has the permission but the relevant function is not implemented, transactions could revert. Either case can lead to the hooks not functioning as intended and should be addressed.
Fortunately, the BaseHook
contract in V4-periphery already checks for potential permission mismatch in the constructor. If a hook developer does not find the right address when deploying the hook, the hook contract will not deploy at all. This is one of the many reasons that hook developers should always inherit the BaseHook
contract in their custom hooks.
In order to pass the validateHookAddress()
check, the getHookPermissions()
function also needs to be implemented in the hook contract, and the specified permissions should have their corresponding hook callback functions implemented.
Another potential integration issue with hooks is returning unexpected or incorrectly typed data. All hooks should return either bytes4 or (bytes4, 32-byte-delta) or (bytes4, 32-byte-delta, uint24). Hooks that return data in the wrong format or fail to handle return values properly can lead to failed transactions due to invalid hook response.
Source: Github
One common mistake we have observed from reviewing hooks contracts from Hackathon submissions is missing access control. Specifically, certain functions in the hook contract should only be called by the singleton PoolManager contract. This includes 1) the unlockCallback()
function, and 2) the other hook callback functions for which the hook has permissions.
Generally, if a hook inherits the BaseHook
contract, unlockCallback()
should already have the safeguard. The BaseHook
contract inherits the SafeCallback
contract, in which the unlockCallback()
function already has the onlyPoolManager
modifier. When hook developers implement their own internal _unlockCallback()
function, access control is already incorporated.
Source: Github
However, the same is not true for the other hook callback functions. In the example below, anyone can call the beforeSwap()
function directly with arbitrary function arguments, including the swapper
address which is encoded in the data
field. Given that users need to approve the hook, anyone can force another user to transfer their tokens to the hook contract. The intended design should be to only let users trigger the transaction when they intend to do so, and only the PoolManager contract should have the ability to call the beforeSwap()
function.
Source: Github
Uniswap V4 has robust security mechanisms to protect a malicious hook from tampering with the logic in the PoolManager contract; however, the hook-user interface can contain custom logic and thus have a broader attack surface. Depending on the design and intended use cases, the hook can act as a custodian of user funds, or contain execution logic only and never take possession of user funds.
When a hook acts as a custodian of user funds, vulnerabilities in the hook contract can cause direct loss of user funds, and thus requires close scrutiny. The following are a few categories of potential areas of concern in the hook-user interface.
Similar to any smart contract, there can be legitimate reasons for a hook contract to include privileged roles. However, if a privileged address is compromised, there could be significant risks. For example, if an upgradeable hook holds user funds, the address with the upgrade authority is capable of injecting a malicious withdrawal function via contract upgrade. As such, the usage of upgradeable contracts should be limited for hook contracts, and risk mitigation mechanisms such as multisig / DAO + timelock are also applicable to hook contracts.
The privileged role does not necessarily apply to the hook contract only. In the following example, the hook contract utilizes a custom ERC-20 contact to represent LP positions. If the custom ERC-20 contract allows a privileged address other than the hook contract to mint and burn tokens, then the privileged address could mint tokens and withdraw liquidity from the corresponding liquidity pool.
Source: Github
Uniswap V4 does not restrict who can create new liquidity pools, nor which hook address to use in a new liquidity pool. If a hook is not restricted to a specific pool or set of pools, an attacker could deploy a malicious pool with fake tokens using the hook. This can potentially compromise the hook contract.
In the BaseCustomAcounting
abstract contract below, the _beforeInitialize()
function allows poolKey
to be set more than once. If a poolKey
has already been set in the hook contract, a subsequent call to the _beforeInitialize()
function would overwrite the previous poolKey
. This abstract contract contains a note stating, “This base hook is designed to work with a single pool key. If you want to use the same custom accounting hook for multiple pools, you must have multiple storage instances of this contract and initialize them via the PoolManager
with their respective pool keys.” However, there is no enforcement of such a mechanism in this base contract, and if a hook developer does not pay special attention to enforcing the single pool key requirement when extending this abstract contract, a malicious actor could still overwrite the pool key. Given that this base hook contract contains custom accounting and enables hook-owned liquidity, the impact of overwriting the pool key could be very serious, including the loss of user funds.
Source: Github
In the following example, the hook also does not restrict which pool can use the hook, and holds user funds. Anyone can register a pool with fake tokens and use the hook to perform swaps. Due to the external call to an untrusted address (fake token), a hacker can sandwich legit transactions (e.g. with a large swap or Just-In-Time liquidity), or reenter the handleSwap() function with arbitrary parameters.
Source: Github
To prevent situations like the above, hooks that are designed for specific pools should validate token pairs / PoolKey during initialization. This can be done in the beforeInitialize()
or afterInitialize()
functions. If a hook is designed to support any token pairs, developers should pay special attention to how a fake or malicious token pair could potentially compromise the hook contract. Robust access control and input validation could mitigate some of the risks associated with supporting arbitrary token pairs. Additionally, it is essential to conduct rigorous security reviews by working with trusted security partners.
The PoolManager in Uniswap V4 allows hooks to return a hookDelta
in the modifyLiquidity()
and swap()
functions, provided that the hook has the necessary permissions when it is deployed. The hookDelta
essentially modifies how delta is attributed between the msg.sender
and the hook
contract address. Given that all deltas need to be settled in order for a transaction to succeed, such attribution potentially dictates which party is responsible for settling which portions of the total delta.
This can introduce serious risks if the accounting is incorrect or intentionally manipulated to benefit the hook operator at the expense of users, or to benefit one group of users at the expense of another group.
A hook could contain different revert conditions that are necessary, but it is important to ensure that key user flows, such as liquidity withdrawals, are not subject to unwarranted reverts. By deliberately reverting during the beforeRemoveLiquidity()
or afterRemoveLiquidity()
functions, a hook could permanently trap user funds in the protocol. There might be special situations where these revert conditions are the intended design, but these should be exceptions, and the behavior should be well-documented and disclosed to users.
In the following example, the beforeRemoveLiquidity()
function always reverts, which could cause users’ liquidity positions to be permanently trapped.
Uniswap V4 sets the stage for more community-driven development, encouraging developers to create novel DeFi applications on top of its infrastructure. The introduction of hooks offers virtually limitless possibilities for customization, facilitating innovation in areas like capital-efficient LP management, custom trading strategies, and complex Oracle design. However, with great flexibility comes the potential of a broader attack surface.
In this article, we discussed how hooks integrate with Uniswap V4, as well as the security considerations of the Users - Hook and Hook - PoolManager interfaces. Like any smart contract development, performing rigorous security review should be an indispensable step in the development lifecycle.
CertiK has deep technical expertise in securing DeFi protocols. With ongoing research about Uniswap V4 hooks, CertiK stands ready to work with the Uniswap V4 hook developer community to help secure the V4 ecosystem and promote continuous innovation among developers.