Uniswap Foundation Grant

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.

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:

FeatureUniswap v3Uniswap v4
Architecturepools deployed by factorysingleton-style architecture
Gas efficiencylowerhigher
Pool Creation Costhigher due to factory modellower due to singleton model
Swap and Withdrawal Feesfixedfixed plus managed by hook contracts
Upgradeabilitiynon-upgradeablenon-upgradeable
Access controlpermissionlesspermissionless
Custodynon-custodialnon-custodial
Native ETH supportnoyes
Custom Functionality via Hooksnoyes
Accountingstandard ERC20ERC1155 & flash accounting
Interactiondirect or indirect through routersindirect 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.

Architecture and nomenclature

UniswapV4 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.

UniswapV4 Architecture Diagram

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:

  1. User Interaction with PoolOperator: Users now interact with the PoolOperator contract to initiate transactions.
  2. PoolOperator with PoolManager Interaction: The PoolOperator facilitates operations on the PoolManager. This interaction is a bit more complex than before.
  3. 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.
  4. Role of Hooks: Additionally, the architecture includes "hooks." These hooks can have user-invoked functions, allowing them to act as PoolOperators too.
  5. 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.

Threat landscape for hooks

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.

Median oracle hook implementation example

https://github.com/saucepoint/median-oracles

How it works?

Typical usage consists of the following steps:

  1. Swappers perform multiple swaps in the pool.
  2. Hook saves current ticks before each swap in a limited buffer.
  3. 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.

c1

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.

c2

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.

c3

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.

Identified threats and threat scenarios:

  • 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.

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.

Multi-sig hook implementation example

https://github.com/atj3097/mfa-multisig-hook-v4

How it works?

Typical usage consists of the following steps:

  1. Swappers submit swap params off-chain.
  2. Authorized signers sign particular swaps in order to allow to process them.
  3. Swapper submits a swap.
  4. 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).

c4

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.

c5

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

c6

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

c7

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.

KYC hook implementation example

https://github.com/jdubpark/Uniswap-Hooks/blob/main/contracts/KYCHook.sol

How it works?

Typical usage consists of the following steps:

  1. Owner of the hook sets the KYC verification smart contract.
  2. Swapper passes the KYC procedure and is added to the KYC verification contract.
  3. Swapper submits a swap.
  4. 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.

c8

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.

c9

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.

c10

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.

c11

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.

Hedge hook implementation example

https://github.com/vanillaHill/hedge

How it works?

Typical usage consists of the following steps:

  1. User sets up the hedge.
  2. Swap activates the hedge trigger after price drop.
  3. 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:

c12

  • 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:

c13

  • 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:

c14

  • 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:

c15

  • 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:

c16

  • 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:

  1. User place a limit order,
  2. User cancel an order,
  3. 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.

c17

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.

c18

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).

c19

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).

c20

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.

c21

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.

Locking liquidity hook implementation example

https://github.com/BrokkrFinance/hooks-poc/blob/main/src/LiquidityLocking.sol

How it works?

Typical usage consists of the following steps:

  1. PoolManager initializes the pool and sets the parameters.
  2. Users add and lock their liquidity.
  3. 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.

c22

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.

c23

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:

c24

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.

c25

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.

c26

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.

c27

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.

Dynamic fee hook implementation example

https://github.com/ArrakisFinance/uni-v4-playground/blob/main/contracts/ArrakisHookV1.sol

How it works?

Typical usage consists of the following steps:

  1. Liqudity provider adds the liquidity to the pool via the hook and mints the share token.
  2. Swapper submits a swap in the PoolManager.
  3. Hook calculates the swap fee depending on the swap direction and size.
  4. 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.

c28

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.

c29

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.

c30

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.

UniswapV4 Threat Model

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:

UniswapV4 Architecture Diagram

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.

Damian Rusinek

Damian Rusinek

Managing Partner & Smart Contract Security Auditor

About the author

PhD, Speaker, Co-Author of SCSVS and White Hat. Professionally dealing with security since 2009, contributing to the crypto space since 2017. Smart contract security research lead.

View all posts (13)