Co-authors: Xaler & Bartosz Wodziński
The latest innovation in Uniswap v4's PoolManager introduces immense potential for custom extension tailored to individual pools. These extensions come in the form of hook contracts, which can be inserted into the liquidity action flow to customize pool behavior. This guide outlines some key considerations when designing a hook to suit your specific needs.
When writing a Uniswap v4 hook, it’s crucial to consider the relationship between pools and hooks. By default, when a pool is created in the PoolManager, any eligible hook contract can be added to the PoolKey
without the hook’s consent. This means that a pool can always determine which hook it is calling by examining the PoolKey
, but the hook has no inherent awareness of which pool is calling it, unless explicitly designed to track that information.
One-Pool Only
If your hook is designed for a single pool, make sure to implement a mechanism to prevent other pools from calling it. A simple way to achieve this is by allowing the hook to be initialized only once via the afterInitialize
callback.
Non-Explicit Multiple-Pool Support
If your hook is not explicitly restricted to a single pool, it can be used by multiple pools by default. In such a case, you must consider the implications for the hook’s state. For example, if the hook’s state is modified via pool callbacks:
To ensure correct accounting, each pool should have its own separate storage space in the hook.
The non-explicit multiple-pool support also impacts the logic within your callback functions. If your hook takes amounts from the PoolManager, you’ll need to account for which pool those amounts are coming from. Make sure that your callback functions properly handle these cases.
If your hook initiates calls to the PoolManager (i.e., it’s not just responding to callbacks), you’ll need to carefully manage the implementation of the unlockCallback
function and any callbacks that may follow.
On unlockCallback
Data
Any contract that calls the PoolManager will need to unlock it first, which requires your hook to implement the unlockCallback
function and create appropriate calldata. This calldata can be used to call any function on the hook, so you need to limit the ability to craft this calldata. Make sure users cannot craft arbitrary unlockCallback
data, as it may expose unintended functions. Pay extra attention to encoding and decoding the callback data, especially when using assembly code.
Skipped Callbacks
Permissioned callback functions will not trigger if the caller is the hook itself. However, if any other caller interacts with the PoolManager, the hook’s permissioned callbacks will trigger. Ensure that your callback logic accounts for the case when the hook is the caller.
If your hook calls the PoolManager.modifyLiquidity
function, it will own the liquidity it manages. You’ll need to consider how the hook manages ownership and the fees accrued from the liquidity it owns on behalf of its users. The Uniswap/v4-periphery repository can be a helpful reference. Specifically, make sure you are mindful of the following:
modifyLiquidity
return values purposes and definitions.Managing accrued fees is critical, especially for hooks that simulate complex financial instruments like limit orders by creating and managing out-of-range liquidity positions. The logic for tracking, attributing, and distributing these fees must be flawless to prevent scenarios where fees could be misdirected or become inaccessible. It must be noted also that fees accruals on liquidity position can be triggered by anyone anytimime, this means that just-in-time liquidity modifications can have conflicts with the hook's custom fees accrual logic.
Furthermore, if your hook implements economic incentives or penalties to discourage transient liquidity strategies that might disadvantage long-term providers (Just-In-Time liquidity attacks), the integrity of this mechanism is paramount. The conditions for triggering penalties and the calculation logic must be robust against sophisticated bypass attempts. In such cases, care should be taken by acknowledging how fees arre distributed across positions and how ticks are crossed and managed.
Hooks That Mint Shares
If your hook manages positions in the PoolManager, you may need to mint shares for your users. This introduces the need to differentiate between Uniswap v4 liquidity and the shares minted by the hook.
Naming conventions are key here. To avoid confusion, reserve the term "liquidity" exclusively for Uniswap v4 "liquidity" and refer to any hook-issued share tokens as "shares". Be mindful of how user input (amount
) is converted into Uniswap v4 liquidity
and then to hook-issued shares
. These three terms have different units and may introduce rounding issues.
When designing your hook, it is important to account for the symmetry in swap logic. Since a swap can be executed in multiple ways, you must ensure your hook can trigger the swap logic for all cases.
If your hook alters the return delta for a swap (e.g., adjusting fees, rewards, or custom calculations), it may modify either the specifiedAmount
(set by the user) or the unspecifiedAmount
(calculated based on the specifiedAmount
). Both amounts can be either positive or negative, depending on whether it is specified for exact-output
or exact-input
in relation to the zeroForOne
boolean. Symmetry in the swap logic is necessary to handle all cases.
With the implementation of the PoolManager, the specifiedAmount
can only be altered in the beforeSwap
hook, while the unspecifiedAmount
can only be altered in the afterSwap
hook. You’ll need to ensure your hook can handle both the before and after swap hooks to preserve symmetry.
The logic should also account for the exact-output vs exact-input swap specifications. Be sure to check the sign of the specifiedAmount
to handle both scenarios.
Custom Swapping Logic
If your hook introduces custom swap logic, this might involve calculating the unspecifiedAmount
based on the specifiedAmount
and additional pool or hook state. Custom logic like this introduces the risk of price manipulation, particularly if the returned amounts depend on underlying balances that could be manipulated or rounding errors that could be exploited. Therefore, custom swap logic should be carefully reviewed to avoid unintended consequences.
For hooks that introduce custom swap logic, such as those designed to mitigate front-running or sandwich attacks by adjusting swap outputs based on a reference price (one which is more difficult to manipulate, like the price at the start of a block), meticulous attention to detail is required in handling the distinction between specified and unspecified swap amounts. Flaws in the conditional logic that determines when and how adjustments are made, can render the protective measures ineffective or, worse, introduce new avenues for exploitation.
If your hook is designed to support native token pools, it should be capable of:
msg.value
from users and returning any excess msg.value
to them.Interacting with native tokens introduces the potential for reentrancy risks, either on the PoolManager or on the hook itself. This could inadvertently alter the pool or hook state, especially if custom accounting logic depends on underlying token balances. Be cautious about this vulnerability, as it may expose the system to price manipulation risks.
Access control mechanisms are vital for ensuring that your hook operates as intended and is secure against unauthorized interactions. Beyond the PoolManager
's interactions, consider who or what should be permitted to call specific functions or modify the hook's state.
Caller Verification and Hook Permissions
The recent $10M+ Cork protocol hack was due to a lack of access control in one of the hook functions and it shows to be a hard lesson for developers to always carefully review the access control structure within your codebase. While any pool can technically call a hook, your hook contract might need to verify the caller for certain operations. For instance, is msg.sender
the PoolManager
? or is it an authorized pool ? Or worse, can it be called my malicious actors or manually crafted contracts ?
Does your hook require an administrative role (e.g., an owner)? This role could be responsible for:
Protecting Sensitive Functions
Ensure that functions are declared with the correct visibility (public, external, internal, private). Functions that don't need to be called externally should be restricted to prevent misuse. Additionally, as highlighted in Question 2, the data passed to unlockCallback
can call any function on the hook. Scrutinize how this data is formed and what functions it can target, effectively acting as an access control layer for operations initiated via unlock.
Configuration and Upgradability
If your hook has configurable parameters, who is authorized to change them? Are changes immediate, or do they go through a timelock or governance process?
If your hook is designed to be upgradeable (e.g., via a proxy pattern), clearly define who has the authority to perform upgrades. This is a powerful capability and should be strictly controlled.
Careful consideration of access control helps prevent unauthorized state changes, fund misappropriation, and other potential exploits, ensuring the integrity and reliability of your hook contract.
While some of the potential use cases for hooks are known, many novel designs can appear over time. With that, past assumptions on how the code used to behave in Uniswap v3 might not be valid anymore under specific hook logics. Novel designs can come with novel attack vectors, and for this, special care should be taken to ensure that the theoretical protocol incentive system still holds even with the added custom logic.
We hope these 5 questions can help forming a basic framework in thinking about your hook design. These are just the beginning and by no means comprehensive. Each individual hook may need to consider further specific trade-offs in functionality, security, and efficiency during its implementation. As the ecosystem around Uniswap v4 evolves, new best practices and strategies will emerge. In the meantime, keep iterating, testing, and refining your hooks!