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.
Uniswap V4: Basics and Nomenclature
Young Jedi, to benefit fully from this article’s value, quickly introduce you to the basics, we must.
General idea of hooks
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:
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.
Architecture and nomenclature
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:
User Interaction with PoolOperator: Users now interact with the PoolOperator contract to initiate transactions.
PoolOperator with PoolManager Interaction: The PoolOperator facilitates operations on the PoolManager. This interaction is a bit more complex than before.
Callback Requirement: For the process to complete, the PoolManager requires a callback to the lockAcquired function in the PoolOperator. After this callback, the PoolOperator gains the ability to execute specific functions on the PoolManager, like swap or modifyPosition.
Role of Hooks: Additionally, the architecture includes “hooks.” These hooks can have user-invoked functions, allowing them to act as PoolOperators too.
Dual Functionality of Hooks: Hooks are versatile. They can be called back by the PoolManager, and they can also initiate transactions with the PoolManager.
Essentially, in UniswapV4, the transaction flow is more dynamic. It facilitates a more complex and efficient system.
Threat landscape for Specific Hooks
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:
Median Oracle
Multi-sig
KYC
Hedge
Limit order
Locking liquidity
Dynamic fees
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.
Median Oracle hook
Overview
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.
Hook saves current ticks before each swap in a limited buffer.
External contract asks for the median price within a given time period.
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.
Realized goals and opportunities:
Provide the price value as an oracle.
Protection from TWAP manipulation.
Multiple algorithms to generate the tick based on the historical data.
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.
Multi-sig hook
Overview
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.
Authorized signers sign particular swaps in order to allow to process them.
Swapper submits a swap.
Hook checks whether the swap is authorized by signers and executes it or reverts if there is no enough signatures.
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.
Realized goals and opportunities:
Control over the swaps, allowing only selected ones
Identified threats and threat scenarios:
Bypassing signatures verification
Using an invalid signature that bypasses verification
Using multiple signatures by the same signer to bypass threshold verification
Submitting a swap with the same hash, but different parameters
Replaying operations
Reusing signatures with a different pool
Reusing the same signature to bypass verification (replay attack)
Denial of Service (swapping)
Absence of signers because there are no incentives for them
Absence of signers because each swap has to be approved independently
Inability to approve the same swap for multiple users (only one user can use the approval)
Removing signers below the required threshold
Setting a threshold over the number of signers
Unauthorized signers
Malicious owner can add new signers instantly
Theft of owner’s key allows to take over the multi-sig and add new owners instantly
Lack of use
Users do not use it because of too strict params (e.g. price limit)
KYC hook
Overview
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.
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.
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.
Realized goals and opportunities:
Allows to KYC swappers and LPs
Allows to swap and provide liquidity only by users that 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
Hedge hook
Overview
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.
Swap activates the hedge trigger after price drop.
The hook swaps the set amount of token to protect against unfavorable price.
The hook contract is executed in the following hooks:
afterSwap
1. Setting up a Hedge
Alice, fearing a drop in the TestCoin price, decides to set up a hedge:
Alice calls the setTrigger function.
The struct is populated with the provided parameters:
_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.
The _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:
The current price of currency0 (TestCoin) is calculated. Let’s assume it’s 48 USD.
The _findIndex function determines the position of this price in the orderedPriceByCurrency list.
The function then checks all price triggers that are greater than or equal to the current price.
Since Alice’s set limit (50 USD) is greater than the current price (48 USD), the _performHedge function is called.
Inside _performHedge:
The function loops through all triggers.
For Alice’s trigger, since trigger.fired is false, it proceeds to execute the hedge.
The poolManager.lock function is called, which likely locks the tokens and performs the swap.
After execution, Alice’s 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:
With a rebounded price (say 52 USD), the function now calls _performUnwind for Alice’s trigger since her price limit is now less than the current price.
Inside _performUnwind:
The function loops through all triggers.
For Alice’s trigger, since trigger.fired is true, it proceeds to execute the unwind.
The poolManager.lock function is called again, likely to reverse the previous swap.
After execution, Alice’s trigger.fired is set to false to indicate the hedge has been unwound.
Realized goals and opportunities:
Protection against unfavorable price movement.
Minimizing the risk of the hedge user.
Identified threats and threat scenarios:
Loss of tokens by Hedge user
Setting a high amount for small price fluctuations.
Executing hedge via malicious pool (assigned to the hook) with the manipulated price.
Token price manipulation within pools via swap to activate hedge.
Reliance on flawed price data.
Loss of tokens by swapper
Front-running swap operation with the new trigger that will be called as part of this swap so that the swapper pays for it.
Denial of Service
Excessive trigger creation.
Infinite swap loop via hedging and unwinding.
Insufficient users’ balances when handling triggers.
Lowering the attractiveness of the pool
Significant increase in gas consumption by adding multiple triggers.
Wrong hedge activation
The hook does not cover all operations that affect the price.
Lack of use
High cost of operation (many triggers executed during swap).
Limit order hook
Overview
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.
Limit order hook implementation example
How it works?
Typical usage consists of the following steps:
User place a limit order,
User cancel an order,
User withdraw tokens after the order is filled.
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’slock 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.
Realized goals and opportunities:
Executing a limit order (one-way swap at a given price)
Identified threats and threat scenarios:
Losing tokens
Adding liquidity to the invalid pool that is non handled by withdrawal function.
Placing an order with both assets transferred.
Lack of functionality to withdraw assets.
No accounting fees earned by LPs on withdrawal .
Placing not the limit order
Placing an order results in a swap (wrong price tick, mismatch of tick and zeroForOne).
Stealing tokens
Attacker adds liquidity on other user’s behalf that approved the hook.
Calling directly functions that are to be called by liquidity pool manager (lock callbacks).
Transferring less or no transferring at all tokens when placing an order (e.g. non-reverting assets).
Malicious LP steals fees generated by other LPs from shared position in the pool by adding and removing liquidity for specific position.
Malicious hook places an order with invalid price tick.
Withdrawing LP fees by the hook owner.
Malicious user cancels their order twice to get double-liquidity.
Malicious user cancels somebody else’s order.
Malicious user cancels executed order.
Denial of service
Big list of limit orders that reach the gas limit when handling the hook.
Lack of use
High gas price for handling multiple limit orders.
Locking liquidity hook
Overview
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.
PoolManager initializes the pool and sets the parameters.
Users add and lock their liquidity.
Users remove liquidity and receive rewards after a certain period.
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.
Realized goals and opportunities:
Enhanced Pool Stability: Contributes to the predictability and stability of liquidity pools.
Reward System: Offers rewards based on locked liquidity duration and amount.
Early Withdrawal Penalties: Applies penalties for early withdrawal, benefiting remaining liquidity providers.
Identified threats and threat scenarios:
Denial of Service
The user cannot withdraw their funds because the penalty is greater than the liquidity they provided.
Bad rewards calculation
Before collecting the rewards, the user adds a lot of liquidity, which increases the final amount of the reward.
Significant disproportions between locking small amounts for the long term and large amounts for the short term.
Early withdrawals without consequences
The user withdraws their assets before the time they locked them for had passed without paying a fine.
The user modifies their position by adding a new one with a shorter locked period, which when combining together with previous one, shortens the final locked period while maintaining higher rewards.
The user collects rewards earlier to reduce the amount on which the penalty will be charged.
Changing the lockedUntil period
Adding/removing new liquidity may shorten the lockedUntil period.
Bad fee distribution
The user front-run withdrawal with big penalty to add liquidity and become one of the LP’s to steal the fee distributed to LP’s.
Unfairness for LPs
The user locks big amount of assets for very short period of time (e.g. 1 second) to steal distributed fees.
Lack of pool stability
The user withdraws the funds at any time because the penalty is very small, making the pools unstable.
Mass withdrawals because of sudden market movements.
Lack of use
The reward token has no value.
The reward value is too small.
Dynamic Fee hook
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).
Overview
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.
Liqudity provider adds the liquidity to the pool via the hook and mints the share token.
Swapper submits a swap in the PoolManager.
Hook calculates the swap fee depending on the swap direction and size.
Swapper pays the dynamic fee (retrieved with getFee function).
The hook contract is executed in the following hooks:
beforeInitialize,
beforeSwap
provides 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.
Realized goals and opportunities:
Adjusting the swap fee according to the liquidity changes. If the swap is the opposite of the ones in previous blocks (aggregated), the fee gets lowered.
Identified threats and threat scenarios:
Funds stolen
The user imbalances the pool and LP gets tokens worth less on withdrawal (lack of minimum tokens outputs)
Withdrawing more liquidity than deposited due to a bug in arithmetic operations on share tokens
Withdrawing multiple times due to the lack of burning the share tokens
Funds locked
Reinitialization of the hook using a new pool
The user overrides the poolKey variable to change the pool on which mint and burn operations are done
Burning the share token directly (without withdrawal)
Invalid fee
Hook does not set dynamic swap fee flag in the hook.
The user overrides the attributes delta and impactDirection by executing the first swap in the block from different pool (assigned to this hook) and lowering the fee to 0
Invalid calculations of price
Denial of Service
Resulting fee gets higher than maximum dynamic swap fee
Uniswap V4 Threat Model
After 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
Key Assets
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:
User funds
in form of liquidity: The tokens and assets held by users in Uniswap V4 pools and hooks.
in form of swapped tokens: The tokens and assets sent by users and temporarly controlled by Uniswap V4 pools, hooks and PoolOperators.
in form of fees: accumulated by the pools, hooks and PoolOperators.
Availability
Fairness
Reputation
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.
Threat Actors
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:
User
Pool Operator, specifically acting as:
Router
Liquidity Manager
Hook, both:
the simple one, and
the one that also plays the role of PoolOperator,
External Contract
UniV4 Protocol
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.
Threats and scenarios
Next step is to think of possible threats that may harm our key assets. These would be:
Funds theft
Funds locked or lost
Denial of Service
Invalid results from external contracts
Unfair operations
Insufficient limitations and access control
Lack of usage
Reputation damage
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.
Funds theft
Hook which is a proxy that holds user funds gets upgraded and a withdrawal function is added that transfers out all funds.
Hook sets high dynamic fee via front-running the user.
Hook sets high withdrawal fee via front-running the withdrawal.
Hook uses the slot0 as price oracle and bases some business logic and calculations on it.
Hook (as PoolOperator) that holds user funds has a backdoor that allows to withdraw funds.
Hook (as PoolOperator) accepts users’ operations executed on pools that do not have this hook assigned.
Hook (as PoolOperator) incorrectly stores the balances of users shared in the same pool position (NFT).
Hook (as PoolOperator) incorrectly calculates the fees earned by liquidity providers, not ensuring that given provider’s liquidity was in fact used.
Protocol sets high fees.
PoolOperator uses malicious route during swap resulting in swapping legitimate tokens for a fake token.
PoolOperator allows high slippage (in terms of price limit) during swap.
User transfers others’ funds abusing approvals in PoolOperator.
User creates a fake pool, assigns it to the hook and make hook do the operations on the original pool (identified by the key) leading to funds theft.
User (as Liquidity Provider) adds and withdraws liquidity in the same transaction to steal a portion of previously earned fees.
User (as Liquidity Provider) withdraws liquidity of other LP.
Funds locked or lost
All threat scenarios resulting in Denial of Service.
Hook that holds user funds does not implement withdrawal function.
Hook does not validate initialization params that influence further operations and locks some funds due to invalid parameters.
User calls pool functions via PoolOperator in a pool that is not managed by this hook.
User (as Liquidity Provider) adds liquidity directly to the pool with a hook that manages whole pool’s liquidity (Hook is not able to manage it anymore).
Denial of Service
Hook starts to revert on selected hooks after some time or state changes.
Hook which is a proxy gets upgraded to non-working implementation and reverts on each call.
Hook reaches the gas consumption limit due to the number of iterated data kept on it (unbound loops).
Hook reaches overflows or underflows in calculations.
Hook does not validate initialization params that influence further operations and reverts on them.
Hook’s owner looses control over hook params and functions (e.g. withdrawal of fees) by loosing the keys.
User initializes the fake pool and overrides the data stored in the hook.
External contract consumes most of the gas and the hook contract reverts.
Invalid results from external contracts
External contract returns malicious result that makes the hook do a swap with big slippage exploited by the attacker.
External contract returns malicious result that is used as price for some asset by the hook.
External contract returns a value that is easily manipulatable by users with direct calls on external contracts (e.g. external oracle with easily manipulated price).
Unfair operations
Hook gives different results for the same swaps depending on the caller.
Hook makes a lot of (a different number of) gas consuming operations depending on the caller.
Insufficient limitations and access control
User bypasses the business limits (e.g. LP withdrawal time locks).
User calls lockAcquired directly on the PoolOperator.
User calls hooks directly on the hook contract.
Lack of usage
Hook does not configure all used hooks.
Hook consumes too much gas and disincentives users.
Reputation damage
All threat scenarios resulting in Denial of Service, Unfair operations and Funds theft or lock influence the author’s reputation negatively.
Things to remember about hooks in the context of security
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:
Hooks cannot influence the PoolManager’s storage directly, but can change the state for users’ operations.
Hooks are shared by multiple pools by default and those can abuse (e.g. reuse or change) their storage and logic.
Hooks can depend on external contracts and those must be taken into consideration when designing a hook, because hooks inherit their risk.
Threats and threat scenarios are highly dependent on the specific hook, some of them overlap, but many are specific.
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.
Did you like this article? Share it on social media!
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