Lethal Integration: Vulnerabilities in Hooks Due to Risky Interactions

In this article, we explore the vulnerabilities that arise during hook interaction logic, specifically concentrating on two scenarios: flawed access control and improper input validation.

Lethal Integration: Vulnerabilities in Hooks Due to Risky Interactions

As highlighted in our previous article, over 30% of the projects in the Awesome Uniswap v4 Hooks repository[1] exhibit vulnerabilities. It is worth noting that the vulnerabilities we refer to here are specific to the Uniswap v4 interactions. Accordingly, in this article, we will scrutinize secure hook interaction logic from the following two perspectives:

  • Flawed Access control
  • Improper Input Validation

For each category, we'll begin with an analysis of the vulnerability and demonstrate its potential exploitation by providing the corresponding Proof-of-Concept (PoC). This will be followed by a discussion on potential mitigation strategies.

Flawed Access Control

Generally, interactions related to Uniswap v4's hooks can be classified based on whether the hook acts as a locker, acquiring a lock in the PoolManager to perform operations in pools. Two primary interaction scenarios require proper access controls:

  • Hook-PoolManager Interaction: This involves interactions between the official callback functions and the PoolManager. The callback functions include eight pool action callbacks (i.e., initialize, modifyPosition, swap, and donate) and the lock callback (i.e., lockAcquired).
  • Hook-Internal Interaction: This pertains to interactions occurring within the hook contract (acting as the locker).

Hook-PoolManager interactions are relatively straightforward. Here, the hook acts purely as a hook, accepting the eight pool action callbacks. The logic in the hook does not affect related pools, meaning there are no fund flows between the hook and the pools. The parameters provided by the callback functions are used to modify necessary storages or as important function parameters. The key consideration is whether the callback parameters can be manipulated.

Hook-Internal interactions are somewhat more complex. In practice, many hook prototypes do more than act as pure hooks. Some developers allow the hooks to provide fund management functions for their users. These functions may not be implemented in the hook contracts, but we can still regard them collectively as hooks in this context. In these cases, a hook accepts user funds and performs pool operations such as liquidity management or swaps. This means the contract must acquire a lock from the PoolManager, turning the hook into a locker. The Uniswap Foundation has considered this situation and integrated a function into their hook template. Specifically, the BaseHook template provides the lockAcquired function as the lock callback, as follows:

    function lockAcquired(bytes calldata data) external virtual poolManagerOnly
returns (bytes memory) {
        (bool success, bytes memory returnData) = address(this).call(data);
        if (success) return returnData;
        if (returnData.length == 0) revert LockFailure();
        // if the call failed, bubble up the reason
        /// @solidity memory-safe-assembly
        assembly {
            revert(add(returnData, 32), mload(returnData))
        }
    }

To execute custom logic, lockAcquired accepts data bytes and conducts a low-level call to itself using that data. The data depends on the business logic of the hook and can be manipulated by users, potentially leading to security issues due to Hook-Internal interactions triggered by lockAcquired. Note that hook design is so flexible that we can't cover all possible scenarios in this situation. Our primary focus here is on the hook acquiring a lock and its subsequent internal interactions. Delving into other potential business logic would make the situation too complex for this discussion.

In both scenarios, the priority is to address any flawed access controls that could potentially lead to exploitation, given that these functions have clear interaction entities. In the subsequent sub-sections, we will sequentially examine each scenario and discuss the necessary access controls to ensure safer interaction logic.

Vulnerability Analysis

Access controls serve as highly efficient and straightforward security solutions for many projects. If a function is designed to be called by specific entities, it should incorporate access control. The most well-known example of access control is the Ownable contract of the OpenZeppelin library, which requires privileged functions to be called only by the contract owner. It's clear that the two scenarios we discussed above are appropriate cases for this type of control.

Hook-PoolManager Interaction: For secure interactions with the PoolManager, hooks should enforce necessary access control on these callback functions. Specifically, these callbacks should be exclusively callable by the PoolManager and not any other accounts. Failure to establish such controls may leave these sensitive interfaces exposed to potential exploitation by malicious actors.

Beyond the eight pool action callbacks, the lock (i.e, lockAcquired) callback, which executes custom logic after obtaining the lock from the PoolManager, also needs to address this issue.

Hook-Internal Interaction: The functions involved in the hook-internal interactions are also designed to be invoked by specific callers. As we stated before, this scenario contains two phases. First, the locker's lockAcquired function is called by the PoolManager, which indicates the function should require the msg.sender to be the PoolManager. Second, the hook dispatches the function call accordingly. Based on the BaseHook's design, it is implemented by low-level calls to the hook itself. This indicates those functions must be defined as external and limit the caller must be the hook's address.

Take one of the examples listed by the Awesome Uniswap v4 Hooks repository, i.e., Stop Loss Order as an example[2]:

Integrated directly into the Uniswap V4 pools, stop loss orders are posted onchain and executed via the afterSwap() hook. No external bots or actors are required to guarantee execution.

Let's examine its afterSwap callback function:

Figure 1: The afterSwap function of Stop Loss Order[2]

Clearly, the above function is designed to perform sensitive operations. However, due to flawed access control, it could be exploited by malicious actors manipulating the arguments (e.g., the key and params), resulting in unexpected behaviors. For instance, the afterSwap callback may operate under the assumption that the swap has already taken place in the PoolManager. Following this, it could initiate actions to record essential state information, such as the current price or collected swap fees. However, if afterSwap does not limit its invocations strictly from the PoolManager, malicious actors could falsify the params parameter, leading to skewed recorded states.

Exploit & PoC

For the sake of simplicity, we'll use a basic PoC to illustrate this access control issue. Generally, the hook's beforeInitialize accepts a PoolKey type parameter, which must contain this hook address in its hooks field (as the PoolManager will use this field to determine the hook address to call).

The screenshot provides a PoC that demonstrates the exploitation of a hook with flawed access control, as seen in DiamondHookPoC [3]. In the absence of access restrictions on the beforeInitialize callback function, malicious actors can feed an arbitrary poolKey to this function. The hook does not verify whether the hook of this poolKey matches the current hook address.

Figure 2: PoolKey.hooks can be set to a zero address

While it's important to note that the exploit in this scenario may not cause financial losses to the hook, it nonetheless dramatically highlights how the state of the hook can be manipulated through unprotected callback functions.

How to Mitigate

To ensure the security of the Hook-PoolManager interactions, both the hook callbacks and the lock callback should restrict their accessibility exclusively to the PoolManager.

Fortunately, Uniswap v4 provides best practices via the BaseHook in its v4-periphery repository[4]. The BaseHook provides the poolManagerOnly modifier to constrain invocations strictly from the PoolManager:

    /// @dev Only the pool manager may call this function
    modifier poolManagerOnly() {
        if (msg.sender != address(poolManager)) revert NotPoolManager();
        _;
    }

This modifier can be effectively employed to enforce proper access control on the sensitive hook and lock callbacks.

On the other hand, the presence of the Hook-Internal interactions requires that any significant state-changing functions invoked via the lockAcquired callback, as specified by the BaseHook, should not be arbitrarily callable.

To meet this requirement, the BaseHook offers a selfOnly modifier. This modifier confines the declared function's accessibility to the hook itself, prohibiting external contracts from directly invoking these sensitive functions for malicious purposes.

    /// @dev Only this address may call this function
    modifier selfOnly() {
        if (msg.sender != address(this)) revert NotSelf();
        _;
    }

In summary, by inheriting from BaseHook, custom hooks can leverage these built-in access control modifiers and callbacks to enforce proper access control.

Improper Input Validation

The BaseHook in v4-periphery[4] offers a solution for safer interaction logic, which developers of hook can leverage. However, we continue to observe instances of improper usage that open up new possibilities for attack vectors in existing hooks.

By default, hooks permit any pool to register via the initialize function in PoolManager. However, if a hook fails to validate the underlying assets in the registering pool, malicious users could register a pool containing counterfeit tokens, enabling them to reenter the hook via the tokens' transfer function.

This vulnerability is subtle since the hook itself may not execute malicious logic. However, when the hook calls the PoolManager, the interactions between the PoolManager and the underlying assets of a malicious pool could potentially hand over the control flow to an attacker via the take function in the PoolManager.

    /// @inheritdoc IPoolManager
    function take(Currency currency, address to, uint256 amount) external override 
noDelegateCall onlyByLocker {
        _accountDelta(currency, amount.toInt128());
        reservesOf[currency] -= amount;
        currency.transfer(to, amount);
    }

In essence, the vulnerability stems from improper validations on the registered pool that hook users plan to interact with. We will delve into this vulnerability using a concrete example and discuss potential mitigation strategies.

Vulnerability Analysis

Take Profits Hook[5] is a hook listed by Awesome Uniswap v4 Hooks:

In this example, we build a hook that allows users to place 'take-profit' positions. For example, in an ETH/DAI pool if currently 1 ETH = 1500 DAI, you could place a take-profit order as "sell all my ETH when 1 ETH = 2000 DAI" which will be executed automatically.

Let's take a look at the _handleSwap function in this hook. This function carries out a swap to fill take-profit orders after obtaining a lock.

Figure 3: The _handleSwap function of Take Profits Hook[5]

You may notice that this function is not protected by any access control modifier. However, line 250 effectively restricts access so that this function can only be invoked after a lock has been acquired from the PoolManager. Otherwise, the poolManager.swap would fail, as the operator wouldn't be the most recent locker. In other words, _handleSwap must be invoked in a specific order, provided the registered pools are validated. Unfortunately, the hook doesn't implement such validation.

Due to this flawed implementation, the hook is susceptible to a reentrancy attack. This vulnerability could allow attackers to force arbitrary swaps using funds deposited by users.

Exploit & PoC

Specifically, the attack can be launched through the following steps:

  1. The attacker registers a malicious pool with fake tokens, specifying the Take Profits Hook as the pool's hook.
  2. The attacker places a stop-profit order in the malicious pool via the hook.
  3. The attacker performs a swap in the malicious pool, triggering the fillOrder in the afterSwap callback to fill the attacker's stop-profit order.
  4. The hook invokes the PoolManager's lock function to request a lock and calls the _handleSwap function in the lockAcquired callback.
  5. In the _handleSwap fuction, the transfers of tokens trigger malicious logic in the fake token contract, which re-enters the _handleSwap function. This is possible _handleSwap is an external function without any accessibility restrictions. Since the lock has already been obtained, the attacker can force the hook to execute arbitrary swaps on any pool, as long as the hook holds sufficient underlying assets. The attacker can then sandwich the swaps to make profits at the expense of other users.

The following detailed diagram illustrates the flow of the attack.

Figure 4: The Attack Flow

As previously mentioned, the hook itself doesn't invoke malicious logic. The only mistake is the hook doesn't prevent untrusted token pools from registering in the PoolManager contract. Indirectly, the malicious logic in the fake token contract is invoked via token transfer operations, which is also a kind of untrusted external call.

How to Mitigate

There are three feasible approaches to mitigate potential attacks due to improper input validation:

  • Proper Access Control. By leveraging build blocks from the BaseHook, a hook can strictly manage function accessibility. This prevents arbitrary accounts from invoking sensitive functions.

  • Reentrancy Lock. In the above attack scenario, this approach can undoubtedly prevent the malicious token logic from re-entering the sensitive functions. However, in some cases, the hook design requires the hook itself to be re-enterable. Specifically, when a hook needs to execute some pool actions, it should allow the PoolManager to re-enter its callbacks to complete these actions. A reentrancy lock may break this intended functionality.

  • Whitelisting Approach. This would require a privileged admin to whitelist approved pools in the hooks. The admin ensures that whitelisted pools do not introduce potential risks. However, the limitation is that hook users could only execute operations on a limited number of admin-approved pools via the hook. While the whitelisting approach improves security, it severely restricts the hook's functionality.

It's challenging to find a perfect solution that balances security and usability for hooks. While we discuss several mitigation approaches, developers will need to thoughtfully consider trade-offs in their hook design. The goal should be to mitigate potential risks as much as possible while retaining the intended functionality. Additionally, our discussion only covers vulnerabilities that may lie in the interactions specifically related to Uniswap v4 features. Practical applications will undoubtedly be more comprehensive. Always ensure that you understand every line of your contracts, and stay SAFU!

Conclusion

In this article, we explore the vulnerabilities that arise during hook interaction logic, specifically concentrating on two scenarios: flawed access control and improper input validation. We present a detailed vulnerability analysis, illustrate potential exploitations along with their PoC, and discuss potential mitigation strategies. We believe these insights can contribute to the secure development and usage of the hooks, and guide future efforts in vulnerability detection.

Reference

[1] Awesome Uniswap v4 Hooks

[2] Stop Loss Order

[3] DiamondHookPoC

[4] v4-periphery

[5] Take Profits

Read the Other Article in This Series

Sign up for the latest updates