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 “Re-initialization of the hook using a new pool” which was identified in the Dynamic Fee hook created by the ArrakisFinance team during the EthCC Paris Hookathon.

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.

Dynamic Fee hook

The Dynamic Fee 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. The change is proportional to the swap size.

Additionally, the hook allows liquidity providers to 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 from the pool. The hook contract is the share token itself (it inherits from ERC20).

Here you can find the implementation: https://github.com/ArrakisFinance/uni-v4-playground/blob/main/contracts/ArrakisHookV1.sol

The problem

We observed that one of the hooks that the contract is called on is the beforeInitialize hook. This function is usually used to store some information when a pool is created and assigned to the hook.

The beforeInitialize hook

However, hooks should not assume that only one pool would be assigned to them and should not store shared params (like poolKey or lastSqrtPriceX96 in example above). Especially, when those parameters are later used in other functions.

In this case, the poolKey variable is later used to mint and burn liquidity while it can be easily overridden by any new pool.

Attack scenario

The PoC of the attack has been implemented in the test_abuser_burn test and can be found here: BadArrakisHookV1.t.sol#L180

The goal of the attack is to lock the liquidity provider’s funds, but there are also other possible ways to exploit this bug as you will have a chance to explore.

Step 1: The hook must be deployed and configured through the constructor. This is achieved in the setUp function.

Step 2: Create a new liquidity pool and initialize it. This is done by the owner of the pool (usually the hook's owner as well).

Legitimate initialization of the Uniswap V4 pool

The hook saves the poolKey or lastSqrtPriceX96 parameters of the initialized pool in local storage.

Step 3: The legitimate liquidity provider mints some liquidity (that will be later locked).

Minting the liquidity

The hook becomes the locker in PoolManager (calls lock function) and on callback (lockAcquired) it modifies the position in the pool identified by the poolKey variable.

Minting callback

Everything has worked as intended up until now…

Step 4: This is the moment when the attacker shows up. They simply create a new pool in the PoolManager and assign it to the same hook.

Malicious re-initialization of the Uniswap V4 pool

As you may have already noticed, the PoolManager will initialize the pool which will call the beforeInitialize hook. The hook will override the poolKey or lastSqrtPriceX96 variables with new values.

Step 5: The liquidity provider comes back to withdraw their liquidity. They simply call the burn function. The hook becomes the locker in PoolManager (calls lock function) and on callback (lockAcquired) it tries to withdraw the liquidity by modifying the position in the pool identified by the poolKey variable.

But... the poolKey variable now points to a new pool, the fake one, created by the attacker, with no liquidity.

That simply means the liquidity provider is not able to withdraw the liquidity. Moreover, the original value of the poolKey variable cannot be restored, because the hook cannot be re-initialized by the same pool.

The attack flow

How to stay secure?

Note down the following points and revisit them when building a hook.

  • Do not assume that the hook will be used by one pool only.
  • Store pool parameters in mapping where the poolKey.toId() is the key.
  • If you want the hook to be used and initialized only by one pool, make sure you require that in the beforeInitialize function.

Challenge

Quite similar vulnerability (using a different pool to attack the hook) exists in another hook created during EthCC Paris Hookathon - the Hedge hook.

If you find it, tweet it and tag us and we'll be happy to share it!

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