This article is one of a series where we present some implementations of "Bad Hooks" as part of our research supported by the Uniswap Foundation Grant.

The threat covered in this article is "Liquidity theft" using the following scenario: "Hook sets high withdrawal fee". The fee mechanism in Uniswap V4 has been changed during our research. We have prepared scenarios for both versions, the older one with fees explicitly specified and the newer one where fees are achieved with custom accounting.

The examples provided are drawn from the extensive threat modeling sessions conducted throughout our research. For a deeper understanding, refer to our previous article detailing the possible threats originating from various use cases.

Explicitly Defined Fees (prior version)

The Uniswap V4 encompasses a dual-fee structure, comprising a swap fee and a withdrawal fee. The swap fee, as delineated in the pool key (for instance, 0.3%), is allocated among the liquidity providers, the hook, and the protocol. Conversely, the withdrawal fee, as determined by the hook, is apportioned between the hook and the protocol.

In the prior iteration of Uniswap V4, the hook's swap fee configuration could either be static, embedded within the hook address, or dynamic, acquired from the hook contract during the swap operation. Upon calculating the total swap fee (such as 0.3%), the protocol's share is initially deducted (with the maximum proportion for the protocol being 25% of the total swap fee). Subsequently, the hook's portion is extracted, and the remainder is allocated to the liquidity providers.

The implementation of the withdrawal fee was contingent on its specification within the hook contracts . Analogous to the swap fee, this fee is distributed between the protocol and the hook contract. It is subject to modification at any given time, albeit indirectly, through the hook contract.

The problem

The swap fee adheres to an upper limit as determined by the maximum percentage value delineated in the liquidity pool key (for instance, a 0.3% cap as indicated).

In contrast, the withdrawal fee is not subject to such a cap.

Consequently, the hook possesses the discretion to impose a withdrawal fee of up to 100%, potentially leading to the appropriation of the entire liquidity pool.

IHookFeeManager interface

This aspect assumes critical significance, particularly considering the hook's capacity to modify the fees, as defined by the IHookFeeManager interface functions, post-deployment. Such a capability poses a substantial risk to liquidity providers, who may find themselves in an unfavorable situation. They might have initially contributed liquidity under the premise of lower fees, only to later encounter escalated fees instituted by the custom hook owner.

This scenario could also transpire through the practice of front-running, specifically targeting withdrawal transactions.

Attack scenario

The PoC of the attack has been implemented in the test_abuser_HookFeeOnWithdrawalStealsAllWithdrawnTokens test and can be found here: Fees.t.sol#L77

Note: This scenario has been also submitted as the issue #318 in Uniswap/v4-core repository.

The objective of this specific attack strategy is to expropriate funds from liquidity providers during the withdrawal process. The methodology unfolds as follows:

Step 1. Initial Setup: The initial step necessitates the deployment and configuration of the hook, accomplished through its constructor function. This setup process is executed within the setUp function.

Step 2. Adding Liquidity: Subsequently, a legitimate liquidity provider adds liquidity to the pool using the modifyPosition function. This function is accessed via the modifyPositionRouter (PoolOperator).

Adding liquidity to the Uniswap V4 pool

The quantities of tokens added to the liquidity pool are recorded in variables for subsequent verification, representing the amounts the liquidity provider has invested in the pool.

Step 3. Imposing Maximum Withdrawal Fees: In the ensuing step, the malevolent hook owner manipulates the withdrawal fees, raising them to the maximal limit of 100% for both tokens.

Setting high dynamic hook fees

The withdrawal fees are encoded within 4 bits per token and are accommodated in a uint8 variable. The _computeFee function is employed to designate the corresponding bits for each token.

The _computeFee function

Step 4. Withdrawal Process: The liquidity provider, intending to retrieve their investment, re-engages with the system, invoking the modifyPosition function (through the modifyPositionRouter intermediary contract) with a negative value that corresponds to the liquidity initially added in Step 2.

Withdrawing liquidity

The culmination of this process involves a verification test to ascertain whether the fees accumulated by the hook are equivalent to the total token amounts deposited by the liquidity provider, who, in a stark turn of events, is left devoid of their assets.

The attack flow

Custom Accounting Fees

In the latest iteration of the Uniswap V4 pool, there have been notable modifications to the fee structure. As of the time of this writing, the previous fee logic is still operational, but it can be substituted with a new system for dynamic fee management, as detailed subsequently.

The swap pool fee delineated within the pool key (for instance, 0.3%), remains integral for accruing fees designated for the protocol and liquidity providers. Nonetheless, for hooks to access these fees—both swap and withdrawal fees—they are required to activate the ACCESS_LOCK permission. This action enables them to independently mint pool tokens, effectively on behalf of the current locker.

Basically, when the hook mints the tokens on behalf of the locker, it decreases the locker's token balances kept in the currencyDelta mapping. The locker (e.g. PoolManager) will have to send those tokens to PoolManager to settle the balances. Most often, those tokens are taken from the users (swappers or liquidity providers).

The problem

The problem is similar to the previous version, but it gets worse. If the hook has ACCESS_LOCK permission, it can mint any tokens on behalf of the locker and steal all user’s tokens.

This process, wherein the hook mints tokens on behalf of the locker, results in a reduction of the locker's token balances, as recorded in the currencyDelta mapping. Consequently, the locker (for example, a PoolOperator) is obliged to transfer these tokens back to the PoolManager to reconcile the balances. In many instances, these tokens are sourced directly from the users, whether they are swappers or liquidity providers.

Attack scenario

The PoC of the attack has been implemented in the test_abuser_hookWithdrawalFee test and can be found here: BadWithdrawalFee.t.sol#L74

Note: This particular scenario has not been formally reported as an issue, it appears to function as an intended feature as mentioned by the Uniswap Labs team. However, it is imperative to exercise heightened vigilance regarding hooks that are granted ACCESS_LOCK permission.

The primary objective of this attack is to steal funds from liquidity providers during their withdrawal process. The steps involved are as follows.

Step 1. Deployment and Configuration of the Hook: Initially, the hook must be deployed and configured, which is achieved through its constructor within the setUp function.

Step 2. Adding Liquidity: During the execution of the setUp function, the user contributes some liquidity to the pool.

Adding liquidity to the Uniswap V4 pool

Step 3. Withdrawal Process: When the liquidity provider intends to withdraw a portion of their liquidity, they attempt to retrieve the first position (-60, 60, 10 ether).

Withdrawing liquidity to the Uniswap V4 pool

Prior to this action, there is a calculation of the token amounts that should be transferred to the liquidity provider upon withdrawal.

Getting balances

During the withdrawal phase, the beforeModifyPosition hook is activated. This hook assesses whether the transaction is a withdrawal (indicated by liquidity falling below zero) and, if so, mints the current balance of the liquidity provider in addition to the amounts being withdrawn.

That is why the current version is more dangerous!

The beforeModifyPosition hook

Note: It's crucial to note that for the hook to execute this action, it requires knowledge of the liquidity provider’s address, which is provided in the hookData. However, in the absence of this address, the hook retains the capability to steal the tokens being withdrawn, as illustrated in the commented-out lines of the code.

The attack flow

How to stay secure?

With the obsolescence of prior iteration, the ensuing best practices are tailored specifically for the contemporary implementation of the Uniswap V4 hooks.

For Builders constructing hooks:

  • Fee specification: It is essential to establish fixed fees, either as a set value or a predetermined percentage.
  • Restriction on minted amounts modification: Avoid incorporating functions that permit alterations to the minted amounts, such as fees.
  • Guidelines for PoolOperator hooks:
    • Seek approvals only for the necessary amounts, rather than requesting unlimited approvals.
    • Clearly define and validate the minimum quantities of tokens to be transferred to the liquidity provider upon withdrawal.
    • Conduct off-chain simulations of the operations and provide the results to the user for transparency and clarity.

For Users interacting with hooks that include ACCESS_LOCK permission:

  • Verification of minted amounts: Ensure that the amounts minted by the hook, such as fees, are explicitly defined (for example, as a fixed value or percentage) and are immune to manipulation.
  • Token approval limitations for PoolOperators: Avoid granting more extensive token approvals than necessary for PoolOperators.
  • Minimum output amounts: When engaging in swap transactions, specify and validate the minimum required amounts of tokens to ensure transaction integrity.

Prioritize quality and security to make your solution the best available on the market. We are happy to help you if you plan to develop or have already begun building the Uniswap V4 hook. Schedule a free consultation with us.

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

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)