Smart Security Practices From The Best
What do Lido, Red Stone, YieldNest, and Braintrust have in common? They’ve developed effective methods for improving security without drastically increasing costs. Top-tier protocol […]
Uniswap is undoubtedly the OG and trendsetter in the crypto ecosystem. The new Uniswap update is the largest yet and will create a completely new landscape full of possibilities for creative applications. Yet, in these times of change, remember: with great power comes great responsibility.

This article is the first one in a security research series about the “malicious design space of hooks” supported by the Uniswap Foundation, focusing on the potential threats, risks and vulnerabilities associated with bad hooks in UniswapV4.
We are very grateful for the opportunity to conduct this research.
Young Jedi, to benefit fully from this article’s value, quickly introduce you to the basics, we must.
Uniswap V4 introduces hooks as a key feature in its architecture. Of course this is not the only change introduced in the new version. The brief summary of the most important changes looks like this:
| Feature | Uniswap v3 | Uniswap v4 |
|---|---|---|
| Architecture | pools deployed by factory | singleton-style architecture |
| Gas efficiency | lower | higher |
| Pool Creation Cost | higher due to factory model | lower due to singleton model |
| Swap and Withdrawal Fees | fixed | fixed plus managed by hook contracts |
| Upgradeabilitiy | non-upgradeable | non-upgradeable |
| Access control | permissionless | permissionless |
| Custody | non-custodial | non-custodial |
| Native ETH support | no | yes |
| Custom Functionality via Hooks | no | yes |
| Accounting | standard ERC20 | ERC1155 & flash accounting |
| Interaction | direct or indirect through routers | indirect through locker |
We wrote more about them in this blog post.
Hooks allow developers to inject custom logic before and after the core functionalities of Uniswap liquidity pool, providing flexibility and extensibility. This architecture empowers developers to customize and enhance the behavior of UniswapV4 pools to meet specific requirements.

Jedi, as you navigate the Uniswap V4 architecture, think of it as gazing out from the Millennium Falcon’s window. Peer through the cockpit window for a clearer view. This panoramic perspective offers a deeper understanding of how each component synergistically operates.

What you may notice is that we changed the name of the contract that users interact with to PoolOperator. That is because in V4 it is no longer as simple router. We came up with this name as this contract is needed to execute the user’s operation on PoolManager.
Here’s a clearer breakdown of the interaction flow:
lockAcquired function in the PoolOperator. After this callback, the PoolOperator gains the ability to execute specific functions on the PoolManager, like swap or modifyPosition.Essentially, in UniswapV4, the transaction flow is more dynamic. It facilitates a more complex and efficient system.
Face problems and ensure safe expansion, we must. Know the potential threats and dangers lurking in the dark side of the Force, we should.

We analyzed the potential threats for 7 different hook applications to illustrate that each extended functionality requires a personalized approach and a thorough understanding of the goal being pursued.
The list of analyzed hooks:
Before we move on to analyzing individual hook examples, we want to emphasize that this article focuses on possible threats for particular usage, rather than a specific hooks contract implementation. Not all threats need to materialize and not all need to be relevant every time – this depends on the use case and should be analyzed individually.
NOTE: Some custom hooks were written for previous versions of Uniswap V4 and might not be compatible with its current version.
The code examples have not been audited and may contain vulnerabilities.
An alternative to TWAP oracle that saves the current tick on each swap. This approach mitigates some risk as when using TWAP, “lower-liquidity pools are prone to manipulation”. The hook has a function that allows to get the median price within a given time period.
Typical usage consists of the following steps:
The following description is based on the specific hook that implements the method from Euler protocol, and its name is EulerMedianOracle. The contract is executed in the following hooks:
beforeInitialize,beforeSwap.In the beforeInitializehook it simply configures the buffer size (ringSizes) and saves last update timestamp.

Then, in beforeSwap hook, the contract takes the tick from the previous swap and, if it is different from the current one, stores it in a ringBuffers 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.

This function goes through the ticks saved in ringBuffers and populates the array arr that is later used to calculate the weighted median. The loop adds the tick and its duration to arr, but it updates the tick and duration only when the loop iteration is the multiple of 8 to optimize storage usage (one 32 bytes slot fits 8 pairs of tick and duration, both 2 bytes long).
Besides the Euler median, there is also the implementation of Frugal median.
The MultiSigSwapHook contract leverages hooks functionality to require multiple signatures from authorized signers before executing a swap. It provides an additional layer of security and control.
Typical usage consists of the following steps:
The hook contract is executed in the following hooks:
beforeSwap ,afterSwap .In the constructor the hooks sets the owner, initial list of signers (authorizedSigners) and the threshold (requiredSignatures).

In the beforeSwap hook, it calculates the hash of the swap params and checks whether the number of approvals in swapApprovals mapping is greater than requiredSignatures.

In the afterSwap hook, the contract deletes the hash of swap from swapApprovals mapping to protect from replay attacks.

Apart from the hooks, the contract implements the approveSwap function that allows the authorized signers to add approvals for specific swap params.

There are also administrative functions, callable only by the owner:
addSigner,removeSigner,setRequiredSignatures,and one view function:
getApprovalDetails, that allows to get the number of approvals for specific hash and whether the sender has approved it.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.
https://github.com/jdubpark/Uniswap-Hooks/blob/main/contracts/KYCHook.sol
Typical usage consists of the following steps:
The contract uses two custom hooks:
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.

Both hooks have onlyPermitKYC modifier that calls hasValidToken on the external kycValidity contract and requires it to return true. The parameter passed to the function is tx.origin (assumed to be the initial swapper or LP). If the user has not passed KYC procedures, the function will revert and stop the execution of the transaction.

The contract has a function setKYCVerifier which is used to set or change the address of the KYC verifier contract. This function can only be called by the owner of the contract and has a 7-day period before the new smart contract can be set.

The primary purpose of the Hedge contract is to allow users to set up triggers that perform hedging actions based on currency prices. When the price crosses a user-defined limit, the contract can automatically swap.
Typical usage consists of the following steps:
The hook contract is executed in the following hooks:
afterSwap1. Setting up a Hedge
Alice, fearing a drop in the TestCoin price, decides to set up a hedge:

setTrigger function._tokenAddress (TestCoin’s address) is stored in trigger.currency0._priceLimit (50 USD) is stored in trigger.minPriceLimit._maxAmountSwap (1000 TestCoin) is stored in trigger.maxAmountSwap.msg.sender (Alice’s address) is stored in trigger.owner._findIndex function is then used to determine where in the orderedPriceByCurrency list Alice’s price limit should be inserted to keep the list ordered.2. Price Drop and Hedge Activation
After a swap in the Uniswap pool, the afterSwap function is automatically called:

currency0 (TestCoin) is calculated. Let’s assume it’s 48 USD._findIndex function determines the position of this price in the orderedPriceByCurrency list._performHedge function is called.Inside _performHedge:

trigger.fired is false, it proceeds to execute the hedge.poolManager.lock function is called, which likely locks the tokens and performs the swap.trigger.fired is set to true to indicate the hedge has been executed.3. Price Recovery and Unwind
If the TestCoin price rebounds, the afterSwap function is triggered again:

_performUnwind for Alice’s trigger since her price limit is now less than the current price.Inside _performUnwind:

trigger.fired is true, it proceeds to execute the unwind.poolManager.lock function is called again, likely to reverse the previous swap.trigger.fired is set to false to indicate the hedge has been unwound.swap to activate hedge.swap operation with the new trigger that will be called as part of this swap so that the swapper pays for it.A hook that allows users to place limit orders. This means that they can specify the price at which they are willing to buy or sell an asset. If the market price reaches the limit price, the order will be executed and liquidity is withdrawn automatically.
Typical usage consists of the following steps:
The hook contract is executed in the following hooks:
afterInitialize,afterSwap.In the afterInitialize hook the contract saves the current tick in the pool as the last lower tick.

1. Placing a limit order
When user wants to make a swap for a specific price, they can add liquidity for a one tickSpacing size and the specific price. Then, whenever the pool reaches this price any swap would change that liquidity from one token to the other.
However, if the price goes back the liquidity is also going to be swapped back to the initial token. This is where this hook comes in and solves this problem by automatically withdrawing the liquidity as the other token after it is swapped.
The hook has the place function that allows to specify the order that you would like to submit. The user specifies the pool (with key parameter), the price (as tickLower parameter), whether they want to swap token0 to token1 or otherwise (with zeroForOne parameter) and liquidity amount.
The place function starts with calling the lock function on PoolManager and the flow is moved to the lockAcquiredPlace function. There, the hook adds liquidity for one tickSpacing size and parameters provided by the user. Next, the proper assets are pulled from the owner, transferred to the PoolManager and the lock is settled.

2. Filling the limit order
The filling of the order is done automatically in the afterSwap hook. First, it checks the range of ticks that were crossed within the swap. If the price increased, the last tick would be lower than the current, and the opposite, if the price dropped, the lower tick would be current one and upper would the the last one.
Next, all epochs that fit the crossed ticks are filled. When the tick is crossed, all the liquidity added for that tick (assuming the spacing is one tickSpacing size) was swapped for the other asset. This is the moment, where _fillEpoch function withdraws the liquidity for that tick, but only if the order maker specified the correct direction of swap (only epochs with the opposite zeroForOne are withdrawn).
The withdrawal must be executed through lock function and thus the hook calls that function and the flow is moved to lockAcquiredFill function. There, the hook withdraws liquidity via position modification. However, it does not pull the asset tokens from PoolManager, but mints PoolManager’s ERC1155-like that will later be taken (via take function on withdrawal by user).

3. Withdrawing tokens
After the limit order is filled and its liquidity withdrawn automatically by the hook, the user is able to take out his swapped tokens using withdraw function.
The function at the beginning calculates how many tokens within the specific epoch belong to user. Then, it calls a lock function on PoolManager and redirects the flow to lockAcquiredWithdraw function. There, transfers PoolManager’s ERC1155-like tokens (representing assets) from the hook to PoolManager to update accounting and transfer out asset tokens from PoolManager to the user (using take function).

4. Cancelling an order
Before the limit order is executed the user is able to cancel it using kill function. The hook makes sure that the selected epoch (identified with passed parameters) has not been filled yet. Then, it takes user’s liquidity from a given epoch and passes that as data to the PoolManager’s lock function that redirects the flow to lockAcquiredKill function.
The first operation in the lockAcquiredKill function, if not the whole epoch is cancelled, is the retrieval of fees from PoolManager that disallows malicious users to steal fees from legitimate liquidity providers within the same epoch.
Then, user’s liquidity is removed from the shared position and the asset tokens are transferred out from PoolManager to the user.
In the end, back in lock function, the amount of PoolManager’s ERC1155-like tokens is increased with the fees collected.

zeroForOne).Locking liquidity hook enables to manage and stabilize liquidity levels. It allows users to lock liquidity for set periods, offering rewards and penalizing early withdrawals with fees to stabilize liquidity levels. This tool incentivizes long-term liquidity provision and enhances LP benefits.
https://github.com/BrokkrFinance/hooks-poc/blob/main/src/LiquidityLocking.sol
Typical usage consists of the following steps:
The hook contract is executed in the following hooks:
beforeInitialize,beforeModifyPosition,beforeSwap.The contract is designed to manage liquidity in a Uniswap pool, allowing users to lock liquidity for a certain period, earn rewards, and face penalties for early withdrawal. In addition to the standard hook functions that are overridden, two additional functions have been added that can be called by any user: addLiquidity and removeLiquidity.
Let’s break down this hook’s basic flow:
1. Initialization
Just before a pool is initialized, the beforeInitialize function is called. This function sets up the basic parameters of the pool, including reward token generation and early withdrawal penalties.

2. Adding Liquidity
Users can add liquidity to the pool using the addLiquidity function in the Hook. This function requires users to specify how much liquidity they want to add and the duration of the lock.

After a few checks, it calculates the amount of liquidity to be added based on the desired amounts. Then, modifies the position by passing the calculated liquidity amount and finally adjust user LiquidityShares:

3. Reward Calculation
When users add liquidity to custom pools, the contract calculates the rewards based on the amount of liquidity and the duration of the lock. This is done within addLiquidity using the pool’s rewardGenerationRate.

This calculation takes into account the current time (block.timestamp), the time when the liquidity was locked (lockingInfo.lockingTime), and the time until which it is locked (lockingInfo.lockedUntil). The reward is proportional to the duration the liquidity is locked and the amount of liquidity provided.
4. Removing Liquidity
Users can remove their liquidity (part or all of it) using the removeLiquidity function. If they remove it before the lockedUntil period ends, they incur a penalty.

5. Updating Liquidity Shares
Both addLiquidity and removeLiquidity functions update the user’s liquidity shares in the pool. This is crucial for calculating the user’s share of rewards and potential penalties.

lockedUntil period
lockedUntil period.The fees are adjusted with the goal of cutting LP losses to arbitrageurs, and an implementation of a shared fungible liquidity position (similar to a vault).
The hook implements dynamic swap fee that is based on the swap directions. If the swap is made in the opposite direction than swaps from previous blocks (aggregated) the fee is decreased and it’s increased otherwise.
https://github.com/ArrakisFinance/uni-v4-playground/blob/main/contracts/ArrakisHookV1.sol
Typical usage consists of the following steps:
getFee function).The hook contract is executed in the following hooks:
beforeInitialize,beforeSwapprovides a function to get the dynamic fee:
getFee.In the beforeInitialize hook the contract stores the pool key, its last price and the current block number as lastly used.

The beforeSwap hook does not change the state for the swap but simply gets some information from the swap to be made.
The first information it gets is taken only on the first swap in a given block. The information includes the difference between current and previous price divided by the square root of the last price (and multiplied by some constant c) and is stored in delta. Additional information stored on the first swap in the block is impactDirection which is true when the price went up from the last swap.
The second information is stored before every swap and is the swap direction – zeroForOne.

The getFee function is called by the PoolManager to ask the hook for the swap fee. In this case the fee is based on the relative price change (delta).
The hook firstly checks whether the current swap is the same direction like the last swap from previous blocks (impactDirection != zeroForOne). If so, it increases the referenceFee by delta. Otherwise, user is doing a healthy swap for the pool, so the referenceFee is decrease by delta, but cannot be lower than 0.

Additionally, the hook inherits from ERC20 which makes it a token. Liquidity providers can add liquidity to hook’s pool and get the token representing their shares in hook’s position. Similarly, they can burn their token and withdraw liquidity.
poolKey variable to change the pool on which mint and burn operations are donedelta and impactDirection by executing the first swap in the block from different pool (assigned to this hook) and lowering the fee to 0After examining various sample hooks, it is evident that the threats they face vary, and require individual approach. However, there exists a subset of common threats, which presents an opportunity for some generalization.
In different they may seem, yet similar their weakness is.

That’s what we’ll do now to help you ensure the security of the hooks you create
The key assets are critical part of the threat model and represent the elements and functions of the system that must be protected against potential threats and vulnerabilities.
In the Uniswap V4 Threat Model, the key assets are:
As you might have guessed, users’ funds are the main and actually the only material key asset we want to protect. However, there are also such attributes like availability of the system or its parts (hooks, PoolOperators, the protocol), the fairness of the system for all users and the reputation of the creators.
Not to forget, one conclusion that arises here is that we have to include also PoolOperators in the research, and thus the threat model, and not only the simple hooks. Especially, because some hooks can be also the PoolOperators.
Once we know what we need to protect, we need to consider the entities that may try to access it or open access to it by mistake. At this point we can think of them in two categories: malicious and vulnerable.
Let’s recall the diagram to better illustrate who it could be:

Now it’s clear that the list of threat actors should consist of the following:
All of the threat actors that represent a contract are identified not only by the contract itself, but also by the owner that controls it. That is, when we talk about the hook as the threat actor, we also include its deployer and owner who can execute privileged operations on it.
Moreover, as pointed out above, one must remember that such components like a hook or PoolOperator can be vulnerable or intentionally vulnerable (malicious). We are going to cover both cases.
Next step is to think of possible threats that may harm our key assets. These would be:
Now, if we think of the actors and the threats, we have to come up with abuser stories (scenarios) where a particular threat actor materializes some threat. Here is the list of threat scenarios we have identified and common for all hooks.
slot0 as price oracle and bases some business logic and calculations on it.lockAcquired directly on the PoolOperator.To sum up, custom hook developers could and should, include generalized threats in their internal threat models. This approach will minimize the number of issues in a given hook, increase its security, and minimize the cost of its audit and the time required to make corrections after it.
When building the hooks it is particularly important to remember that:
We believe that our research will bring many good practices and help you explore the hooks landscape in a safe way. Stay tuned for a new SCSVS category, which will contain specific checks that are worth checking and a series of bad hooks that will clearly illustrate the problems we discuss here.
Composable Security 🇵🇱⛓️ is a Polish company specializing in increasing the security of projects based on smart contracts written in Solidity. Examples of projects that have trusted us are market leaders such as FujiDAO, Enjin, Volmex Finance, DIVA Protocol or Tellor. We are creators of the Smart Contract Security Verification Standard. Speakers at various conferences such as EthCC, ETHWarsaw, or OWASP AppSec EU. Authors of numerous publications on DeFi security. Experienced auditors operating in the IT Security space since 2016.
If you need support in the field of security or auditing smart contracts do not hesitate to contact us.
Meet Composable Security
Get throughly tested by the creators of Smart Contract Security Verification Standard
Let us help
Get throughly tested by the creators of Smart Contract Security Verification Standard