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
, anddonate
) 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:
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.
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.
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:
- The attacker registers a malicious pool with fake tokens, specifying the Take Profits Hook as the pool's hook.
- The attacker places a stop-profit order in the malicious pool via the hook.
- The attacker performs a swap in the malicious pool, triggering the
fillOrder
in theafterSwap
callback to fill the attacker's stop-profit order. - The hook invokes the
PoolManager
'slock
function to request a lock and calls the_handleSwap
function in thelockAcquired
callback. - 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.
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
[2] Stop Loss Order
[3] DiamondHookPoC
[4] v4-periphery
[5] Take Profits