Q1 2025 Web3 Security ReportAccess control failures led to $1.63 billion in losses
Discover report insights
  • Hacken
  • Blog
  • Discover
  • Auditing Uniswap V4 Hooks: Risks, Exploits, and Secure Implementation

Auditing Uniswap V4 Hooks: Risks, Exploits, and Secure Implementation

22 minutes

Uniswap V4 Hooks offer unprecedented flexibility for developers, enabling custom logic within liquidity pools and swaps. This opens the door to innovative features like dynamic fees, custom AMM strategies, and tighter integrations with other DeFi protocols. However, this flexibility dramatically expands the potential attack surface. A single misconfigured or malicious Hook can lead to significant financial losses, denial-of-service conditions, or manipulation of the pool.

In this article, we will analyze how Hooks work, where they introduce risks, and how to audit them effectively. We’ll walk through a structured process, from initial configuration checks to advanced threats like front-running and centralization risks, equipping you with the knowledge to build secure and robust Hooks.

Through code analysis, diagrams, and practical examples, we will explore how Uniswap V4 Hooks can be both a powerful tool and a potential liability — depending on how they are implemented.

Understanding Uniswap V4 Hook Architecture

Uniswap V4 introduces Hooks, external smart contracts that can modify liquidity management and swap behavior at different execution points.

Hooks open up many possibilities: custom fees, dynamic liquidity adjustments, and even complex trading strategies that run entirely on-chain. However, this power comes with significant security risks. These risks need to be carefully considered before you start coding, planned for during development, and thoroughly audited after the code is written.

In Uniswap V4, each liquidity pool can have a Hook contract attached to it. Uniswap will call this Hook at specific moments during a swap or when someone adds or removes liquidity. Anyone can create a pool, and the person who creates it chooses which Hook (if any) to use. You can have many pools for the same pair of tokens (like ETH/USDC), each with different settings, including different Hooks. Each unique pool gets a unique ID. Crucially, once a pool is created, you cannot change the Hook.

function initialize(PoolKey memory key, uint160 sqrtPriceX96) external noDelegateCall returns (int24 tick) {
    …

    uint24 lpFee = key.fee.getInitialLPFee();

    key.Hooks.beforeInitialize(key, sqrtPriceX96);

    PoolId id = key.toId();

    tick = _pools[id].initialize(sqrtPriceX96, lpFee);

    …
            key.Hooks.afterInitialize(key, sqrtPriceX96, tick);
}

Source: PoolManager.sol

function toId(PoolKey memory poolKey) internal pure returns (PoolId poolId) {
    assembly ("memory-safe") {
    // 0xa0 represents the total size of the poolKey struct (5 slots of 32 bytes)
    poolId := keccak256(poolKey, 0xa0)
    }
}

Source: PoolId.sol 

Hooks can be triggered at certain moments before or after key actions execution:

beforeInitialize – called before pool initialization, when initial parameters (such as price range, coefficients, etc.) are set.

afterInitialize – called after initialization, when the pool has been created and is ready for use.

beforeAddLiquidity – called before adding liquidity, when parameters can still be checked or modified.

afterAddLiquidity – called after liquidity has been added, when assets are deposited into the pool and its state is updated.

beforeRemoveLiquidity – called before removing liquidity, to check whether the operation can be executed.

afterRemoveLiquidity – called after liquidity has been removed, when assets have been returned to the user and the pool’s reserves are updated.

beforeSwap – called before a swap occurs, allowing validation of trade parameters, such as price limits or available liquidity.

afterSwap – called after the swap is completed when the trade has taken place and the pool’s balance has been updated.

beforeDonate – called before donating to the pool, when transaction parameters can still be adjusted.

afterDonate – called after the donation has been made when the pool’s balance is updated and liquidity has received a bonus.

General Flow of Hook Execution

To ensure the security of your Uniswap V4 Hook, we’ll follow a structured approach, examining key areas of potential vulnerability. These steps cover everything from initial configuration to ongoing operational risks.

Step 1: Is the Hook Configured Correctly?

A misconfigured Hook may cause transactions to revert due to invalid function implementation or return values of unexpected type or size. The flowchart below illustrates how PoolManager interacts with a Hook before a swap operation. Any misconfiguration in these areas can lead to unexpected behavior or transaction failures.

Hook Configuration and Execution Flow

To illustrate common misconfigurations, сonsider the following custom VulnerableConfigurationHook implementation that has several configuration problems:

contract VulnerableConfigurationHook is Ownable, UUPSUpgradeable {
    function getHookPermissions() public pure returns (HookPermissions memory) {
        return HookPermissions({
            beforeSwap: true, 
            afterSwap: false,
            beforeModifyLiquidity: false,
            afterModifyLiquidity: false
        });
    }

    function beforeSwap(...) external returns (uint256 lpFeeOverride) {
        return 5000;
    }

    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}

    // Future plan: Implement afterSwap() in next upgrade...
}

Uniswap V4 does not expect to fetch permissions from the contract itself — instead, it derives permissions directly from the Hook’s address using bitwise operations. 

function hasPermission(IHooks self, uint160 flag) internal pure returns (bool) {
    return uint160(address(self)) & flag != 0;
}

Source: Hooks.sol

VulnerableConfigurationHook contract claims to support beforeSwap(), but if it is deployed at an address that does not encode BEFORE_SWAP_FLAG, PoolManager will not recognize it, and beforeSwap() will never be called.

Checking the Hook's Address Encoding
Hook Address (binary):    0b1011010101101101...0000  (Ends in `0` → does NOT have beforeSwap ❌)
BEFORE_SWAP_FLAG (0x1):   0b0000000000000000...0001
--------------------------------------------------
Result (Bitwise AND `&`): 0b0000000000000000...0000  ❌ No permission!

📌 Therefore, the first issue arises from the mismatch between the Hook’s declared permissions and its encoded address permissions. If the Hook claims to support a specific function but its address does not encode the corresponding permission bits, PoolManager will never call it, rendering the function ineffective. Conversely, if the Hook’s address includes extra permission bits, PoolManager may attempt to execute non-existent functions, resulting in transaction reverts (DoS).

When deploying Hooks, address mining is required to find the proper deployment address. Below is presented an example of address mining that identifies a salt value that produces a Hook address with the desired flags set on.

uint160 flags = uint160(
            Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG
                | Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG
        );

        // Mine a salt that will produce a Hook address with the correct flags
        bytes memory constructorArgs = abi.encode(POOLMANAGER);
        (address HookAddress, bytes32 salt) =
            HookMiner.find(CREATE2_DEPLOYER, flags, type(PointsHook).creationCode, constructorArgs);

        // Deploy the Hook using CREATE2
        vm.broadcast();
        PointsHook pointsHook = new PointsHook{salt: salt}(IPoolManager(POOLMANAGER));

📌The next issue in VulnerableConfigurationHook is an incorrect return type in beforeSwap():

function beforeSwap(...) external returns (uint256 lpFeeOverride) {
        return 5000;
}

Uniswap processes return values using the Hook library, which expects beforeSwap to return a tuple of type:

function beforeSwap(...) internal returns (int256 amountToSwap, BeforeSwapDelta HookReturn, uint24 lpFeeOverride)

Source: Hooks.sol

An incorrect return type can cause:

  • Overflow risks due to incorrect type casting.
  • Transaction reverts, preventing executions.

📌The final issue in VulnerableConfigurationHook that concerns is upgradeability. The contract is designed to support future upgrades via UUPSUpgradeable:

function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} 

A comment within the contract indicates plans to implement afterSwap() in a future upgrade.

Although the Hook may implement new functions in future upgrades, its deployment address does not encode permissions for these functions. If afterSwap() is added in an upgrade, PoolManager will not recognize it because the contract’s address lacks the required permission bits.

To prevent this issue, the Hook’s address must encode all intended permissions at deployment. One way to achieve this is to define placeholder functions for future Hooks.

Ultimately, it is recommended to use BaseHook from Uniswap when implementing Hooks that follow the IHook interface. As mentioned above, writing custom implementations can lead to configuration issues, potentially causing inconsistencies or unexpected behavior in the system.

Step 2: How the Hook Affects PoolManager’s Logic?

Uniswap V4 introduces a custom accounting mechanism that allows Hooks to return deltas, which impact swap execution and liquidity modification. It allows Hooks to take fees from swaps and liquidity modifications, providing fine-grained control over balance adjustments and even overriding default swap behavior. These modifications require specific permission flags, encoded in the Hook’s address:

uint160 internal constant BEFORE_SWAP_RETURNS_DELTA_FLAG = 1 << 3;
uint160 internal constant AFTER_SWAP_RETURNS_DELTA_FLAG = 1 << 2;
uint160 internal constant AFTER_ADD_LIQUIDITY_RETURNS_DELTA_FLAG = 1 << 1;
uint160 internal constant AFTER_REMOVE_LIQUIDITY_RETURNS_DELTA_FLAG = 1 << 0;

One of the parameters returned by beforeSwap() Hook is a BeforeSwapDelta, which is an alias type for int256, where upper 128 bits are for delta in specified tokens (e.g., token0 or token1) and lower 128 bits are for delta in unspecified tokens (fee adjustments or additional charges).

type BeforeSwapDelta is int256;
…
function getSpecifiedDelta(BeforeSwapDelta delta) internal pure returns (int128 deltaSpecified) {
        assembly ("memory-safe") {
            deltaSpecified := sar(128, delta)
        }
    }

    /// extracts int128 from the lower 128 bits of the BeforeSwapDelta
    /// returned by beforeSwap and afterSwap
    function getUnspecifiedDelta(BeforeSwapDelta delta) internal pure returns (int128 deltaUnspecified) {
        assembly ("memory-safe") {
            deltaUnspecified := signextend(15, delta)
        }
    }

Source: BeforeSwapDelta.sol

The key point to remember is that BeforeSwapDelta is from the perspective of the Hook:

  • If the Hook takes a fee, it must pass the value as a negative delta (-a).
  • If the Hook grants a rebate, it must pass the value as a positive delta (+a).

Other Hooks use BalanceDelta, which is also an alias for int256 type. It shares the same design as BeforeSwapDelta, but the order is different. Upper 128 bits are for amount0, representing the delta in token0, and lower 128 bits are for amount1, representing the delta in token1.

type BalanceDelta is int256;
function amount0(BalanceDelta balanceDelta) internal pure returns (int128 _amount0) {
        assembly ("memory-safe") {
            _amount0 := sar(128, balanceDelta)
        }
    }

    function amount1(BalanceDelta balanceDelta) internal pure returns (int128 _amount1) {
        assembly ("memory-safe") {
            _amount1 := signextend(15, balanceDelta)
        }
    }

Source: BalanceDelta.sol

The Uniswap processes the delta in the following way: the swap amount (amountToSwap) is initially set to params.amountSpecified. The specified token delta (HookDeltaSpecified) is added to amountToSwap, modifying the swap execution.

if (self.hasPermission(BEFORE_SWAP_RETURNS_DELTA_FLAG)) {
    HookReturn = BeforeSwapDelta.wrap(result.parseReturnDelta());

    // any return in unspecified is passed to the afterSwap Hook for handling
    int128 HookDeltaSpecified = HookReturn.getSpecifiedDelta();

    // Update the swap amount according to the Hook's return, and check that the swap type doesn't change (exact input/output)
    if (HookDeltaSpecified != 0) {
        bool exactInput = amountToSwap < 0;
        amountToSwap += HookDeltaSpecified;
        if (exactInput ? amountToSwap > 0 : amountToSwap < 0) {
            HookDeltaExceedsSwapAmount.selector.revertWith();
        }
    }
}

Source: Hooks.sol

To illustrate common misconfigurations, сonsider the following VulnerableDeltaAdjustmentHook implementation:

contract VulnerableDeltaAdjustmentHook is BaseHook {

    function beforeSwap(
        address sender,
        PoolKey calldata key,
        SwapParams calldata params,
        bytes calldata HookData
    ) external override returns (bytes4 selector, int256 HookDelta, uint24 lpFeeOverride) {
        int128 token0Delta = 500 * 10**18; // fee
        int128 token1Delta = 0;

      HookDelta = BeforeSwapDelta.toBeforeSwapDelta(token0Delta, token1Delta);

        selector = bytes4(keccak256("beforeSwap()"));
        return (selector, HookDelta, lpFeeOverride);
    }
}

📌  The first issue in the code is that token0Delta is intended to represent the Hook fee. However, since BeforeSwapDelta is considered from the perspective of the Hook, the fee should be negative when the Hook deducts an amount.

📌 The second issue is the order in which parameters are passed to BeforeSwapDelta.toBeforeSwapDelta(). The order of deltas in BeforeSwapDelta is not fixed and must be aligned with the swap direction as determined by the SwapParams.zeroForOne parameter.

In Uniswap V4, PoolManager ensures that all deltas (token balance changes) are settled to zero before the transaction is finalized. If deltas are not properly handled, it can lead to transaction failures due to unsettled token balances.

To understand how Hook deltas work, we need to analyze how Uniswap tracks and enforces balance settlements within PoolManager.

The delta balancing process starts with unlock(), which is called before executing swaps or liquidity modifications.

function unlock(bytes calldata data) external override returns (bytes memory result) {

    if (Lock.isUnlocked()) AlreadyUnlocked.selector.revertWith();

    Lock.unlock();

    // the caller does everything in this callback, including paying what they owe via calls to settle
    result = IUnlockCallback(msg.sender).unlockCallback(data);

    if (NonzeroDeltaCount.read() != 0) CurrencyNotSettled.selector.revertWith();
    Lock.lock();
}

Source: PoolManager.sol

The unlock() allows contract logic execution via unlockCallback(). After execution, NonzeroDeltaCount is checked to ensure all balances are settled. This means that neither the protocol nor the user should owe tokens to each other at the end of execution. The same applies to Hooks — if a Hook modifies a balance, it must ensure that deltas sum to zero before the transaction finalizes.

To understand how delta adjustments affect transactions, we will analyze how Uniswap modifies deltas in the presence of Hooks.

BalanceDelta HookDelta;
(swapDelta, HookDelta) = key.Hooks.afterSwap(key, params, swapDelta, HookData, beforeSwapDelta);

// If the Hook returns a nonzero delta, update the pool balance for the Hook contract
if (HookDelta != BalanceDeltaLibrary.ZERO_DELTA) {
    _accountPoolBalanceDelta(key, HookDelta, address(key.Hooks));
}

Source: PoolManager.sol

Hooks return HookDelta in afterSwap(), which modifies the final balance changes. The HookDelta that affects the PoolManager’s final settlement is calculated based on beforeSwap() and afterSwap() modifications.

if (msg.sender == address(self)) return (swapDelta, BalanceDeltaLibrary.ZERO_DELTA);

int128 HookDeltaSpecified = beforeSwapHookReturn.getSpecifiedDelta();
int128 HookDeltaUnspecified = beforeSwapHookReturn.getUnspecifiedDelta();

// If the Hook has AFTER_SWAP permissions, modify HookDeltaUnspecified
if (self.hasPermission(AFTER_SWAP_FLAG)) {
    HookDeltaUnspecified += self.callHookWithReturnDelta(
        abi.encodeCall(IHooks.afterSwap, (msg.sender, key, params, swapDelta, HookData)),
        self.hasPermission(AFTER_SWAP_RETURNS_DELTA_FLAG)
    ).toInt128();
}

// Apply Hook deltas
if (HookDeltaUnspecified != 0 || HookDeltaSpecified != 0) {
    HookDelta = (params.amountSpecified < 0 == params.zeroForOne)
        ? toBalanceDelta(HookDeltaSpecified, HookDeltaUnspecified)
        : toBalanceDelta(HookDeltaUnspecified, HookDeltaSpecified);

    // Ensure the swap amount accounts for Hook delta changes
    swapDelta = swapDelta - HookDelta;
}
return (swapDelta, HookDelta);

Source: Hooks.sol

The HookDeltaUnspecified is updated if the Hook modifies it in afterSwap().

To ensure all balances are correctly adjusted, Uniswap tracks Hook deltas using _accountPoolBalanceDelta() (it is called in the swap function which we analyzed above).

function _accountPoolBalanceDelta(PoolKey memory key, BalanceDelta delta, address target) internal {
    _accountDelta(key.currency0, delta.amount0(), target);
    _accountDelta(key.currency1, delta.amount1(), target);
}
function _accountDelta(Currency currency, int128 delta, address target) internal {
    if (delta == 0) return;

    (int256 previous, int256 next) = currency.applyDelta(target, delta);

    if (next == 0) {
        NonzeroDeltaCount.decrement();
    } else if (previous == 0) {
        NonzeroDeltaCount.increment();
    }
}

Source: PoolManager.sol

Deltas are updated for each currency held by the Hook or user. If the delta reaches zero, NonzeroDeltaCount is decremented. Since NonzeroDeltaCount is checked in unlock(), an unsettled Hooks delta will cause a transaction revert.

This VulnerableDeltaAdjustmentHook incorrectly handles delta adjustments, causing unsettled balances that will make the transaction revert.

contract VulnerableDeltaAdjustmentHook is BaseHook {
    function beforeSwap(
        address sender,
        PoolKey calldata key,
        SwapParams calldata params,
        bytes calldata HookData
    ) external override returns (bytes4 selector, int256 HookDelta, uint24 lpFeeOverride) {
        int128 specifiedTokenDelta = 5;  // Hook adds 5 tokens to swap
        int128 unspecifiedTokenDelta = 0;

        if (params.zeroForOne) {
            HookDelta = BeforeSwapDelta.toBeforeSwapDelta(specifiedTokenDelta, unspecifiedTokenDelta);
        } else {
            HookDelta = BeforeSwapDelta.toBeforeSwapDelta(unspecifiedTokenDelta, specifiedTokenDelta);
            }
        return (BaseHook.beforeSwap.selector, HookDelta, lpFeeOverride);
    }

    function afterSwap(
        address sender,
        PoolKey calldata key,
        SwapParams calldata params,
        BalanceDelta swapDelta,
        bytes calldata HookData,
        BeforeSwapDelta beforeSwapHookReturn
    ) external override returns (bytes4 selector, int256 HookDelta, uint24 feeDelta) {
        int128 HookDeltaSpecified = beforeSwapHookReturn.getSpecifiedDelta();
        int128 HookDeltaUnspecified = beforeSwapHookReturn.getUnspecifiedDelta();

        HookDeltaUnspecified += -10; // Hook takes 5 tokens of fee from swap result

        HookDelta = BalanceDeltaLibrary.toBalanceDelta(HookDeltaSpecified, HookDeltaUnspecified);

        return (BaseHook.afterSwap.selector, HookDelta, feeDelta);
    }
}

📌 The beforeSwap Hooks adds 5 specified tokens to the swap, afterSwap takes 10 tokens from the swap result. Therefore, the two deltas will be +5 and -10 for specified and unspecified accordingly. Finally, the NonzeroDeltaCount will be equal to 2 which will make unlock revert.

To settle the deltas, Hooks should transfer 5 specified tokens to the PoolManager and call settle() and take() for 10 unspecified tokens. (There is also a settleFor function which allows to transfer tokens to the PoolManager instead of Hook, therefore, each security review should take into account the particular context).

Swap Execution Flow with Unlock and Hooks Deltas Settlement

📌 Parameters returned by Hooks can affect more than just unsettled deltas; if they violate Uniswap’s constraints, they can lead to other issues. For example, a key validation inside swap() ensures that Hooks do not change the swap type (exact input or exact output).

if (HookDeltaSpecified != 0) {
    bool exactInput = amountToSwap < 0;
    amountToSwap += HookDeltaSpecified;
    if (exactInput ? amountToSwap > 0 : amountToSwap < 0) {
        HookDeltaExceedsSwapAmount.selector.revertWith();
    }
}

📌 When examining previous Hooks, we observed that the last parameter they return is lpFeeOverride, which allows Uniswap V4 Hooks to dynamically override LP fees. Uniswap V4 provides flexibility in setting fees, but this also introduces potential risks. If a Hook returns an excessive or invalid fee value, it can prevent a call from successful execution.

Step 3: The Risks of the Async Hook

While standard Hooks in Uniswap V4 modify swap parameters while keeping Uniswap’s core execution logic intact, Async Hooks go a step further by entirely replacing the swap execution process. Async Hooks introduce a unique security concern in Uniswap V4 overriding Uniswap’s native swap logic. Unlike standard Hooks that modify swap parameters, Async Hooks replace the core swapping mechanism by taking full control over the user’s sent tokens and executing their own logic.

Async Hooks replace Uniswap’s swap logic by reversing amountToSwap in the delta calculations, meaning that instead of Uniswap executing the swap, the Hook takes full custody of the swapped amount.

function beforeSwap(
    address sender,
    PoolKey calldata key,
    SwapParams calldata params,
    bytes calldata HookData
) external override returns (bytes4 selector, int256 HookDelta, uint24 lpFeeOverride) {
   
    // Async behavior: Hook completely takes the swap amount
    int128 specifiedTokenDelta = -int128(params.amountSpecified);
    int128 unspecifiedTokenDelta = 0;
    // main logic

    HookDelta = BeforeSwapDelta.toBeforeSwapDelta(specifiedTokenDelta, unspecifiedTokenDelta);

    selector = bytes4(keccak256("beforeSwap()"));
    return (selector, HookDelta, lpFeeOverride);
}

📌 This allows full flexibility, but also shifts all responsibility for swap execution from Uniswap to the Hook, creating severe risks if the Hook is malicious or misconfigured.

The ability of Async Hooks to assume full custody over swapped funds creates significant risks. A malicious or misconfigured Hook can:

  • Steal Funds – Since Uniswap does not enforce execution rules, an attacker can create a Hook that directly transfers swapped tokens to an unauthorized address.
  • Block Execution – A faulty Hook could fail to return swapped assets, resulting in locked user funds.
  • Price Manipulation – If a Hook front-runs or delays swap execution, it can take advantage of price fluctuations at the user’s expense.

Below is a malicious Async Hook that steals all swapped funds by sending them to the deployer of the contract instead of processing the swap:

contract VulnerableAsyncHook is BaseHook {

    constructor() {        owner = msg.sender;    }
    function beforeSwap(
        address sender,
        PoolKey calldata key,
        SwapParams calldata params,
        bytes calldata HookData
    ) external override returns (bytes4 selector, int256 HookDelta, uint24 lpFeeOverride) {
   
        // Async behavior: Hook completely takes the swap amount
        int128 specifiedTokenDelta = -int128(params.amountSpecified);
        int128 unspecifiedTokenDelta = 0;
          HookDelta = BeforeSwapDelta.toBeforeSwapDelta(specifiedTokenDelta, unspecifiedTokenDelta);
        IERC20(key.token0).transfer(owner, uint256(params.amountSpecified));

        selector = bytes4(keccak256("beforeSwap()"));
        return (selector, HookDelta, lpFeeOverride);
    }
}

📌In this attack, the Hook is explicitly coded to transfer tokens to the attacker’s wallet instead of executing a valid swap. Since Uniswap does not enforce swap logic in Async Hooks, users relying on such a Hook could permanently lose their funds.

Swap Execution Flow with Async Hook Handling

Step 4: Who Can Call the Hook and Modify Its Behavior?

If a Hook can be called by unauthorized contracts or users, it becomes vulnerable to unexpected state changes, or manipulation by external actors.

Below is a faulty Hook implementation that exhibits several authorization issues.

contract VulnerableAuthorizationHook is BaseHook {
    function afterSwap(...) external override returns (bytes4 selector, int256 HookDelta, uint24 feeDelta) {
        token.transfer(msg.sender, contractBalance);        …
    }

    function updatePool(address newPool) external {
        attachedPool = newPool;
    }
}

📌 It is expected that only PoolManager will call Hook functions. However, in this Hook, there is no restriction on the caller. Therefore, the absence of access control allows anyone to execute the Hook’s logic, potentially leading to critical vulnerabilities, the severity of which depends on the specific implementation of each Hook.

To prevent this vulnerability, all Hooks` access should be restricted to the PoolManager.

Direct Hook Access and Interaction 

Each pool and each event can trigger only one Hook at a time. However, Uniswap V4’s PoolManager allows multiple liquidity pools to reference the same Hook, as there are no built-in restrictions preventing a Hook from being attached to multiple pools.

The initialize() function in PoolManager.sol registers a Hook for a given liquidity pool, but it does not enforce exclusivity, meaning the same Hook can be attached to multiple pools.

function initialize(PoolKey memory key, uint160 sqrtPriceX96) external noDelegateCall returns (int24 tick) {
    if (key.tickSpacing > MAX_TICK_SPACING) TickSpacingTooLarge.selector.revertWith(key.tickSpacing);
    if (key.tickSpacing < MIN_TICK_SPACING) TickSpacingTooSmall.selector.revertWith(key.tickSpacing);
    if (key.currency0 >= key.currency1) {
        CurrenciesOutOfOrderOrEqual.selector.revertWith(
            Currency.unwrap(key.currency0), Currency.unwrap(key.currency1)
        );
    }
    if (!key.Hooks.isValidHookAddress(key.fee)) Hooks.HookAddressNotValid.selector.revertWith(address(key.Hooks));

    uint24 lpFee = key.fee.getInitialLPFee();

    key.Hooks.beforeInitialize(key, sqrtPriceX96); // Hook is executed but not validated for exclusivity

    PoolId id = key.toId();

    tick = _pools[id].initialize(sqrtPriceX96, lpFee);

    emit Initialize(id, key.currency0, key.currency1, key.fee, key.tickSpacing, key.Hooks, sqrtPriceX96, tick);

    key.Hooks.afterInitialize(key, sqrtPriceX96, tick);
}

Source: PoolManager.sol

📌 The VulnerableAuthorizationHook relies only on onlyPoolManager, assuming it will always be called by a trusted pool. Since PoolManager does not validate pool exclusivity, an attacker can attach this Hook to an arbitrary pool and trigger its logic.

function afterSwap(...) external override onlyPoolManager returns (bytes4 selector, int256 HookDelta, uint24 feeDelta) {
        token.transfer(msg.sender, contractBalance);
        ...
    }

To prevent unauthorized pools from using a Hook, the contract should explicitly verify the PoolKey against a predefined trusted pool. An alternative approach is to implement beforeInitialize() in the Hook itself, ensuring that it can only be initialized once for a single pool.

Unauthorized Hook Attachment

📌 Finally, we are going to check all the other functionality which may be called by external users which can impact the Hook execution. A misconfigured Hook introduces vulnerabilities that can affect liquidity pools, swap execution, and fee calculations.

function updatePool(address newPool) external {
        attachedPool = newPool;
    }

Unauthorized Hook Parameter Manipulation

Step 5: Centralization and Governance Risk

In Uniswap V4, Hooks introduced additional governance considerations that do not exist in previous versions of the protocol. Since Hooks are external contracts, they may be upgradable, centrally controlled, or have privileged roles that can modify key protocol parameters. These factors introduce risks that could allow a single entity to manipulate swap fees, pause trading, or extract liquidity from users.

📌If a Hook inherits from an upgradeable proxy pattern (e.g., UUPSUpgradeable), its logic can be modified after deployment, potentially introducing vulnerabilities or backdoors that did not exist at the time of audit.

contract UpgradeableHook is BaseHook, Ownable, UUPSUpgradeable {
    function beforeSwap(...) external override returns (bytes4 selector, int256 HookDelta, uint24 lpFeeOverride) {
        int128 specifiedTokenDelta = -3000;
        int128 unspecifiedTokenDelta = 0;

        HookDelta = BeforeSwapDelta.toBeforeSwapDelta(specifiedTokenDelta, unspecifiedTokenDelta);
        selector = bytes4(keccak256("beforeSwap()"));
        lpFeeOverride = dynamicSwapFee;

        return (selector, HookDelta, lpFeeOverride);
    }

The Hook can be arbitrarily modified after deployment, allowing the owner to introduce new security vulnerabilities.

A malicious upgrade could modify beforeSwap() to drain user funds, increase swap fees, or bypass permission checks.

Malicious Upgrade of Hook 

Hooks may include configuration parameters such as:

  • Custom swap fees.
  • Liquidity withdrawal restrictions.
  • Contract pause mechanisms.

📌If these parameters can be arbitrarily modified by a single entity, the Hook introduces centralized control over key protocol mechanics.

contract CentralizedHook is BaseHook, Ownable {
    uint24 public swapFee;

    function setSwapFee(uint24 newFee) external onlyOwner {
        swapFee = newFee;
    }

    function beforeSwap(...) external override returns (bytes4 selector, int256 HookDelta, uint24 lpFeeOverride) {
        int128 specifiedTokenDelta = -swapFee
        int128 unspecifiedTokenDelta = 0;

        HookDelta = BeforeSwapDelta.toBeforeSwapDelta(specifiedTokenDelta, unspecifiedTokenDelta);
        selector = bytes4(keccak256("beforeSwap()"));
        lpFeeOverride = dynamicSwapFee;

        return (selector, HookDelta, lpFeeOverride);
    }
}

A single entity can modify swap fees at will, potentially front-running users or extracting excessive fees.

If an attacker gains control of the owner address, they could set swap fees to 100%, effectively locking user funds in the pool.

Centralized Hook Control And Owner Wallet Loss Risks 

Step 6: Can the Hook Be Front-Run?

Hooks in Uniswap V4 introduce custom execution logic at key points in swaps and liquidity management. If improperly designed, Hooks can become vulnerable to front-running and MEV (Maximal Extractable Value) attacks, where malicious actors manipulate transactions order for profit.

The following VulnerablePriceOracleHook is vulnerable to front-running attack.

contract VulnerablePriceOracleHook is BaseHook {
    PriceOracle public oracle;
    uint24 public dynamicSwapFee = 3000;
    mapping(address => uint256) public lastRecordedPrice;

    constructor(address _oracle) {
        oracle = PriceOracle(_oracle);
    }

    function beforeSwap(
        address sender,
        PoolKey calldata key,
        SwapParams calldata params,
        bytes calldata HookData
    ) external override returns (bytes4 selector, int256 HookDelta, uint24 lpFeeOverride) {
       
        uint256 currentPrice = oracle.getPrice(key.token0);
        uint256 previousPrice = lastRecordedPrice[key.token0];

        if (previousPrice == 0) {
            lastRecordedPrice[key.token0] = currentPrice;
            return (bytes4(keccak256("beforeSwap()")), 0, dynamicSwapFee);
        }

        // Adjust fees based on actual price movement over time
        if (currentPrice > 1.1 * previousPrice) { 
            dynamicSwapFee = 500; // Lower fee
        }
        else if (currentPrice < 0.9 * previousPrice) {
            dynamicSwapFee = 10000; // High penalty fee
        }

        lastRecordedPrice[key.token0] = currentPrice; // Update the price

        HookDelta = BeforeSwapDelta.toBeforeSwapDelta(-int128(dynamicSwapFee), 0);
        selector = bytes4(keccak256("beforeSwap()"));
        lpFeeOverride = dynamicSwapFee;

        return (selector, HookDelta, lpFeeOverride);
    }
}

MEV bot can observe a large swap in the mempool that would affect the price, then front-run the trade to get a preferable fee.

Common front-run vulnerabilities include:

📌 Price-dependent logic – If a Hook adjusts swap fees or liquidity based on external market data, MEV bots can anticipate changes and exploit them.

📌 Time-sensitive execution – Hooks that enable delayed swaps or rely on block timestamps can be manipulated.

📌Oracle-based pricing – Hooks that fetch on-chain prices may be vulnerable to oracle manipulation attacks.

Front-Running and MEV Exploitation in Vulnerable Hooks

Step 7: Denial of Service (DoS) Risks

If improperly implemented, Hooks can increase gas costs, introduce infinite loops, or cause unnecessary reverts, ultimately leading to denial-of-service (DoS) attacks. These issues can make pools unusable by forcing transactions to fail consistently, preventing liquidity providers from adding or removing liquidity, or even blocking swaps.

When auditing Hooks for DoS risks, consider whether the Hook can cause excessive gas usage, leading to failed transactions?

contract VulnerableDoSHook is BaseHook {
    address[] public authorizedUsers;
    ExternalContract public externalContract;

    constructor(address _externalContract) {
        externalContract = ExternalContract(_externalContract);
    }

    function beforeSwap(
        address sender,
        PoolKey calldata key,
        SwapParams calldata params,
        bytes calldata HookData
    ) external override returns (bytes4 selector, int256 HookDelta, uint24 lpFeeOverride) {
       
        // Loop through all authorized users
        for (uint256 i = 0; i < authorizedUsers.length; i++) { 
            require(externalContract.checkAccess(authorizedUsers[i]), "Access check failed");
        }

        selector = bytes4(keccak256("beforeSwap()"));

        return (selector, HookDelta, lpFeeOverride);
    }
}

📌If authorizedUsers grows too large, the beforeSwap() function will consume excessive gas, potentially exceeding block gas limits. This prevents swaps from executing, leading to a denial-of-service condition.

To prevent gas exhaustion, the Hook should limit the size of arrays and use more efficient data structures like mappings to ensure that swaps remain executable under all conditions.

📌Incorrectly implemented require() or revert() statements can block transactions even when they should succeed.

DoS Risk in Hook

Conclusion

Uniswap V4 Hooks offer unprecedented flexibility, unlocking capabilities like customized liquidity incentives, automated trading strategies, enhanced risk mitigation, and improved DeFi composability. 

However, this flexibility significantly expands the attack surface, requiring thorough security assessments. Key vulnerabilities include:

  1. Improper Configuration: Misconfiguring Hooks can lead to failed swaps, DoS, or unexpected behavior.
  2. Misconfigured Delta Handling: Incorrect delta calculations can cause fund misallocation, DoS, or unfair pricing.
  3. Risky Async Hooks: Async Hooks take full custody of assets, creating risks of theft, execution failures, or manipulation.
  4. Authorization Vulnerabilities: Weak permissions allow unauthorized Hook modification, risking fund loss or privilege escalation.
  5. Centralization and Governance Risks: Centralized control introduces risks of abuse, censorship, or malicious actions.
  6. Front-Running Risks: Hooks adjusting fees based on market conditions can be exploited by MEV bots.
  7. Denial of Service (DoS) Risks: Hooks with external dependencies or unoptimized logic can lead to stalled pools.

Uniswap V4 Hooks offer immense potential but demand rigorous security practices. Proper design and security audits are essential for DeFi ecosystem functionality and security.

Resources:

Docs:

Code:

v4-periphery: https://github.com/Uniswap/v4-periphery

Subscribe
to our newsletter

Be the first to receive our latest company updates, Web3 security insights, and exclusive content curated for the blockchain enthusiasts.

Speaker Img

Table of contents

Tell us about your project

Follow Us

Read next:

More related

Trusted Web3 Security Partner