Uniswap V4 Hooks
Yep one more article on uniV4 hooks, but this one is different (or maybe not)
Introduction
UniV3 vs UniV4 : Uniswap v3 introduced concentrated liquidity features that allow liquidity providers to choose price ranges to earn higher fees from their capital.
Singleton Contract Architecture: In earlier versions, every token pair needed its own contract. Uniswap V4, however, uses a Singleton contract model. This means that all pools are converged into one contract, cutting down on gas costs for trading and pool creation. It also makes multi-hop trades more efficient.
Hooks Feature (to choose best definitions 💡) :
With Uniswap v4, they're offering more choice to the users. Hooks are essentially smart contracts that can "hook" into the lifecycle of a trading pool on Uniswap. Pools on v4 can then choose to make their own tradeoffs; for example, accepting a higher gas cost in exchange for a feature set similar to Uniswap v4, or add entirely new functionality. With this new customizability possible, it is no longer necessary to "fork" Uniswap to create a derivative DEX, a slightly different modified version of Uniswap that makes the tradeoffs you want it to make. Since you can just plug-in to different actions in a pool's lifecycle, you can just deploy a pool with your custom hooks and it will work alongside all the other Uniswap v4 pools as normal. At most, perhaps you want to build a custom frontend that guarantees your users will always be going through the pool with your hooks, instead of some other pool for the same token pair.
By injecting these hooks, the developers are able to create pools that support such use cases as:
TWAMM (Time-Weighted Average Market Maker) for handling large swap orders without price impact
dynamic fees : liquidity pools can dynamically adjust fees based on market volatility or other input parameters to better adapt to market conditions.
custom oracle implementations
and many more.
However, there is a limitation for the hooks - they cannot be added or removed after the pool is initialized. Whenever the hook is turned on during the initialization, it cannot be turned off (and the opposite) because the hook activation flag is kept in the immutable key of the pool.
How Hooks work ?
It functions similarly to a plugin system, plugging different actions into a pool. This enables developers to deploy pools with custom Hooks that work alongside other Uniswap pools.
Hooks can be executed before and after major lifecycle actions, you can find these actions in the IHooks
interface in Uniswap 4 which provides you with an option to interact with different stages of transactions in your liquidity pools. You can consider hooks as operations that get triggered before and after key operations in your pool.
beforeInitialize
andafterInitialize
: When a new pool is initialized, e.g. if you want to add some additional setup logic when creating a new pool, this is the place.beforeModifyPosition
andafterModifyPosition
: When an LP position in a pool is being changed, in other words every time an LP adds/removes liquidity or changes parameters of his LP position.beforeSwap
andafterSwap
: Before and after a swap operation happens. You can see the flow on the right.beforeDonate
andafterDonate
: When liquidity is being added to the pool via the newdonate
functionbeforeAddLiquidity
andafterAddLiquidity
When liquidity is being added to the pool
By leveraging these hooks, developers can introduce unique functionalities and optimizations to the liquidity pools.
How they are executed ?
One hook can be assigned to multiple pools but one pool can only have one hook contract. The idea behind that is to simplify and avoid conflicts in logic
The Hook contract needs to explicitly specify which of the above stages to execute, and the pool needs to know whether the corresponding Hook needs to be executed at a certain stage. In order to save gas, the hook flags (you see above) are not stored in the contract but in the address. To deploy a Hook contract, we need to use CREATE2 for deploying at a pre-determined address and hook contracts must have specific flags encoded in the address.
It can be seen that the first 8 bits of the Hook address are used to mark whether the 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 the use of Create2 + calculation of random Salt to achieve.
The easier tool to use to deploy your hook is this repository : uniswapV4 Hook Mine And Sinker
Customization
The idea behind hooks is to customize hooks deployed so let’s have a look at one example and it’s security considerations :
Median Oracle Hook implementation :
How does it works ?
Swappers perform multiple swaps in the pool.
Hook saves current ticks before each swap in a limited buffer.
External contract asks for the median price within a given time period.
which hooks does it use :
beforeInitialize
: In thebeforeInitialize
hook it simply configures the buffer size (ringSizes
) and saves last update timestamp.beforeSwap
: the contract takes the tick from the previous swap and, if it is different from the current one, stores it in aringBuffers
array together with the duration. The duration here is a difference between the current timestamp and the last update. Finally, it caches the current tick for future use.
The main goal of the hook is to provide the tick to other smart contracts that integrate with it. It can be achieved by calling the
readOracle
function.Goals :
Provide the price value as an oracle
Protection from TWAP manipulation.
Multiple algorithms to generate the tick based on the historical data.
Possible threats :
Incorrect price
Use of short history period.
Incorrect calculations (e.g. omitting valid ticks).
Breaking assumptions of used source code (e.g. Euler’s).
Manipulated price
Malicious swap that increases the current tick and influences the resulting price.
Malicious multiple swaps that influence the median.
Data (ticks and duration) pollution by the hook owner.
Denial of Service
Overflow in oracle calculation.
Use of long history period that exceeds gas limit.
Low use
- Swappers do not want to pay (gas) for updating current values.
KYC hook : The smart contract is a KYC (Know Your Customer) hook. It is designed to enforce KYC checks on users, before they are allowed to conduct trades on a Uniswap liquidity pool.
How it works :
Owner of the hook sets the KYC verification smart contract.
Swapper passes the KYC procedure and is added to the KYC verification contract.
Swapper submits a swap.
Hook calls the KYC contract to check whether user has passed KYC procedure and performs the operation, or blocks it otherwise.
Which hooks are used :
beforeSwap
,beforeModifyPosition
The
beforeSwap
hook is triggered before a swap operation is performed. It ensures that the user trying to perform the swap operation has passed KYC procedure.The
beforeModifyPosition
hook is triggered before a user tries to modify their position in a pool. It ensures that the user trying to modify their position has passed KYC.Identified threats and threat scenarios:
Unauthorized usage
Using transaction originating from address (with KYC passed) and forwarding it to the pool via contract (without KYC passed) (KYC address → attacker address → pool)
Insufficient checks in the KYC validation contract
Centralization risk
Malicious owner can change the validation contract instantly to a malicious one that accepts anyone
Theft of owner’s key allows to take over the validation process and add change the validation contract instantly to a malicious one that accepts anyone
Malicious owner or thief of the owner’s key can bypass the timelock, to set the validation contract instantly to a malicious one that accepts anyone
Changing the validation contract without any information to users (e.g. events)
Owner of the validation contract changes its logic to accept anyone
Owner of the validation contract adds only selected addresses as validated
Lack of possibility to revoke an accepted address
- Impossibility to remove the access once user has passed KYC procedure
Ineffective access removal
- Revoked address front-runs the revocation and executes an operation
Denial of Service
Setting incorrect KYC validation contract that cannot handle the KYC requests
Destroying the KYC validation contract
Other examples of custom hooks :
Hooks Threat Modeling
Hooks seem to have a lot of power as they are injected in multiple places, specify fees and hold some of the funds (fees).
Potential threats:
Upgradeable hook
Hook Owner upgrades the hook and changes its logic to block selected operations.
A malicious user exploits an upgradeability vulnerability to upgrade the hook (destroy or change logic).
Flags protect from calling new hook function and changing fee type but stil there is a risk of a hooks doing a delegateCall to selfdestruct so upgradeability is a thing to consider
Denial of Service
- A hook starts to revert on calls after users have provided liquidity DoSing the protocol.
Fees
A hook sets huge fees (possibly front-running the swaps and withdrawals).
Hook Owner cannot collect fees due to the lack of that functionality.
A malicious user exploits an access control vulnerability to collect hook’s fees.
Sandwiching
A hook performs a sandwiching attack on the swap to MEV the user.
A hook performs a front-running attack during the modification of the position to change the withdrawable amounts.
Access control
- A malicious user bypasses access control and takes over the hook to update params.
External calls
The hook gets a price from the external oracle that can be manipulated.
The hook calls an external contract that can revert and DoS the hook (DoS forwarded to Uniswap v4 pool).
The hook calls an external contract that is upgradeable and can be self-destructed (DoS the hook and the pool).
Malicious Scenarios :
Scenario 1 : Liquidity Theft via Hook Fee : The swap pool fee is set within the pool key (for instance, 0.3%), it remains integral for accruing fees designated for the protocol and liquidity providers. Nonetheless, for hooks to access these fees—both swap and withdrawal fees—they are required to activate the
ACCESS_LOCK
permission. This action enables them to independently mint pool tokens, effectively on behalf of the current locker. Basically, when the hook mints the tokens on behalf of the locker, it decreases the locker's token balances kept in thecurrencyDelta
mapping. The locker (e.g.PoolManager
) will have to send those tokens toPoolManager
to settle the balances. Most often, those tokens are taken from the users (swappers or liquidity providers).The primary objective of this attack is to steal funds from liquidity providers during their withdrawal process. The steps involved are as follows.
Step 1. Deployment and Configuration of the Hook: Initially, the hook must be deployed and configured, which is achieved through its constructor within the
setUp
function.Step 2. Adding Liquidity: During the execution of the
setUp
function, the user contributes some liquidity to the pool.Step 3. Withdrawal Process: When the liquidity provider intends to withdraw a portion of their liquidity, they attempt to retrieve the first position (-60, 60, 10 ether).Prior to this action, there is a calculation of the token amounts that should be transferred to the liquidity provider upon withdrawal.
During the withdrawal phase, the
beforeModifyPosition
hook is activated. This hook assesses whether the transaction is a withdrawal (indicated by liquidity falling below zero) and, if so, mints the current balance of the liquidity provider in addition to the amounts being withdrawn.
That is why the current version is more dangerous!
For Users interacting with hooks that include ACCESS_LOCK
permission:
Verification of minted amounts: Ensure that the amounts minted by the hook, such as fees, are explicitly defined (for example, as a fixed value or percentage) and are immune to manipulation.
Token approval limitations for PoolOperators: Avoid granting more extensive token approvals than necessary for
PoolOperators
.Minimum output amounts: When engaging in swap transactions, specify and validate the minimum required amounts of tokens to ensure transaction integrity.
The FullRange
hook contract is a liquidity pool manager that allows a Uniswap V4 pool to provide liquidity for a range of prices. Anyone can rebalance the Uniswap liquidity pool added by the contract to align it with the current price in the pool. This can be used to create a market maker for a volatile asset or to provide more liquidity for a thin market.
The rebalancing of the hook’s liquidity is achieved with the _rebalance
function
This presents an existing vulnerability in the hook contract and the attacker’s scenario that exploits this vulnerability to lock tokens of other liquidity pools.
Function on the hooks has to be externally callable , anyone can call function on the hook so if you call it with the rights parameters you can overwrite the erc20 tokens emitted to represent liquidity , so if someone mints liquidity via erc20 of the hook and the hook erc20 is overwritten, the balances are all 0 so it doesn’t work anymore and all the liqudiity of previous user is no longer accessible
Step 1: The hook must be deployed and configured through the constructor. This is achieved in the setUp function.
Step 2: Create a new liquidity pool and initialize it. This is done by the owner of the pool (usually the hook's owner as well).
The hook saves the poolKey
or lastSqrtPriceX96
parameters of the initialized pool in local storage.
Step 3: The legitimate liquidity provider mints some liquidity (that will be later locked).
The hook becomes the locker in PoolManager (calls lock
function) and on callback (lockAcquired
) it modifies the position in the pool identified by the poolKey
variable.
Everything has worked as intended up until now…
Step 4: This is the moment when the attacker shows up. They simply create a new pool in the PoolManager and assign it to the same hook.
As you may have already noticed, the PoolManager will initialize the pool which will call the beforeInitialize
hook. The hook will override the poolKey
or lastSqrtPriceX96
variables with new values.
Step 5: The liquidity provider comes back to withdraw their liquidity. They simply call the burn
function. The hook becomes the locker in PoolManager (calls lock
function) and on callback (lockAcquired
) it tries to withdraw the liquidity by modifying the position in the pool identified by the poolKey
variable.
But... the poolKey
variable now points to a new pool, the fake one, created by the attacker, with no liquidity.
That simply means the liquidity provider is not able to withdraw the liquidity. Moreover, the original value of the poolKey
variable cannot be restored, because the hook cannot be re-initialized by the same pool.
Prevent it :
Do not assume that the hook will be used by one pool only.
Store pool parameters in mapping where the
poolKey.toId()
is the key.If you want the hook to be used and initialized only by one pool, make sure you require that in the
beforeInitialize
function.
Let’s imagine there is a lending protocol that uses this oracle to calculate the value of provided collateral when borrowing some other assets.
The goal of the attack is to manipulate the prices and make the collateral’s value higher, resulting in borrowing more assets and leaving the lending protocol with bad debt.
Step 1: The hook must be deployed and some legitimate swaps need to be executed to populate the oracle with some ticks. This is achieved with the createSwaps
function.
Step 2: The lending protocol gets the current price using the readOracle
function during a legitimate borrow operation. The price return is equal to 15 (the unit does not matter in this scenario).
Step 3: The malicious hook owner updates the ticks with arbitrary values leading to a higher price.
Step 4: The malicious hook owner adds the collateral covered by their to the lending protocol and borrows other assets. The protocol gets the current, manipulated price using the readOracle
function (the price is not over 10x higher) and allows the borrower to get more assets.
That simply means that the malicious hook owner can control the value of their collateral in the lending protocol.
Prevent it
Eliminate Backdoor Functionalities: Ensure that your hook does not include any functionalities that would permit the owner or any other entity to alter the values utilized by other protocols or users. This step is critical in maintaining the integrity and trustworthiness of the hook.
Ownership Strategy: It is advisable to either avoid the ownership design pattern altogether or to renounce ownership of the smart contract immediately after the hook's establishment. This measure reduces the risk of centralized control and potential manipulation.
Templates
These are templates for writing Uniswap V4 Hooks.
Uniswap Foundation's Template: This template repository provides a starting point for writing Uniswap V4 hooks. It includes the necessary files and contracts to get started. This template can be used to create a custom hook that can be used to execute arbitrary code on every swap. Previously built by saucepoint.
SolidityLabs' Template: Foundry-based template for developing custom pool in Uniswap v4 with hooks.
Arrakis' Playground: This playground is a web-based application that allows you to interact with hooks. You can use this playground to test your own hooks or to learn more about how hooks work. This playground can be used to test the functionality of your hooks by simulating swaps.
Lucas Martin Calderon's Template: This repository contains a template for a hook that was created for the ETHGlobal Hackathon. This template can be used to create a custom hook that can be used to provide liquidity to a particular pool.
Nick Addison's Template: This repository contains a template for a hook that includes a factory to mine addresses and trace diagrams. This template can be used to create a custom hook that can be used to mine addresses and generate trace diagrams.
Quantum3 Labs's Scaffold: A boilerplate to use Uniswap v4 hooks with scaffold eth.
Gnome101's Hardhat Template: A template and playground that uses hardhat. This can be used to create and test custom hooks.
Tools
A lot of tools are already available to help developers building their hooks
Scaffold Hook: Uniswap v4 Hook development stack, complete with testnet deployment and UI. This tool can help builder develop and test Uniswap v4 Hooks with minimal interfaces for the swap lifecycle (pool creation, liquidity provision, and swapping).
Hook Mine And Sinker: Mine addresses for UniswapV4 Hooks. This tool can be used to generate random addresses that are eligible to become hooks. This can be useful for testing or for deploying your own Hooks.
Hook Deployer: Hook create2 deployer. This tool can be used to deploy hooks to Ethereum.
Uniswap v4 Tests: A test suite for working with Uniswap v4 hooks. This test suite can be used to verify that your hooks are working correctly.
Uniswap v4 Hook Test Framework: A fuzz testing framework for Uniswap V4 Hooks, built during ETHGlobal 2023. This framework can be used to test the security of your hooks.
Hookmate: Experimental Solidity components and utilities for Uniswap v4 Hook development. This can help developers leverage extsload and access fee info for hooks.
Uniswap v4 Minimal: Minimal subgraph for Uniswap v4.
⇒ Conclusion : Be transparent about the hook’s functionality:
Build immutable hooks (no upgradeability).
Implement limits for the hook fees.
Make sure you have no unintended reverts in the hook.
Refs
https://github.com/ora-io/awesome-uniswap-hooks?tab=readme-ov-file
https://medium.com/buildbear/how-to-build-custom-hooks-in-uniswap-v4-79b158488ed2
https://composable-security.com/blog/uniswap-v-4-liquidity-theft-via-hook-fee/
https://composable-security.com/blog/uniswap-v-4-bad-hook-with-broken-access-control/