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