Uniswap v4 Hooks: A Security Guide for Builders and Breakers

EthCC[9] · 2 Apr 2026 · 20:46

Nisedo examines the attack surfaces introduced by Uniswap v4 hooks. The presentation considers vulnerabilities in customizable pool behavior and lessons from costly exploits, then discusses how builders can design hooks with those security risks in mind rather than treating customization as risk-free.

Speakers

Topics

From EthCC[9], hosted by Ethereum France.

Transcript

[music] >> Hi everyone. Um so, today we'll be talking about the Uniswap before hooks and with a focus for developer how to make your hooks safe. And for breakers and auditors how to find the most bug possible in the following audits. Um We'll be going over a few of the architectural changes that I have I've been happened in from V3 to V4. Um with a focus on the one interesting for this um topic because we won't be reviewing all of them. And then I will be

presenting the five most common um issues that I've been finding in audits and in other um analysis that we have done from other reports and from Solidified. And then you will have a takeaway uh two checklists, one as a developer and one as auditor for uh hopefully not making the same mistakes in your next uh engagement. As a quick intro, um I'm Isidore. I'm a blockchain security engineer at 12 bits. Um I'm also the founder of Solidified the French Web3 security community. So, in case you are French and not part of it please uh reach out at the end of the presentation and I will send you an invite.

Um and basically I spend my days reviewing code, uh smart contracts, uh and finding bugs. And lately it has been a lot of Uniswap before hooks. That's why the topic of the today's talk. So, to give a quick um um catch-up on the changes from V3 to V4 with a focus on the architecture. The main uh change has been um evolving from a factory to independent contract pattern that uh we had in V3 where each pool was an independent contract that you have to deploy through the factory.

In V4, they opted for a singleton contract deploy manager. And now pools instead of being contract, they are structs basically. They are part of the storage of the deploy manager. And they implemented something that is very innovative, the hooks, uh which are independent contracts that can implement logic uh throughout the uh flows of the pool manager. So, around the swaps, around the liquidity provision, the liquidity removal. Um and something to keep in mind is that one hook one pool can have at most one hook, but it's optional. It's not obligatory. And one hook can be attached to multiple pools. And you will see why it's important for the rest of the talk.

And so, this is now a pool in Uni V4. Uh as you can see, there are five values. Uh something to keep in mind is that these five values make up one pool key. And each pool key is unique. And if you change any of these values, you will be talking to a completely different pool. So, trusting the pool key mean trusting the pool. And trusting the wrong pool key mean trusting the wrong pool, of course. And this is also some innovation that I really like in Uni V4. Um in Uni V4 compared to Uni V3, everything happened in what they call a session. So, a session starts with your hook or your contract calling the unlock contract on the the unlock function, sorry, on the pool manager. Uh which will trigger a unlock callback calling back into your

hook. And this is where your data will um happen. And this is where all the um flows of the user will be happening. So, you will be swapping, you will be adding liquidity, removing liquidity. Um it kind of works like a flash loan. And what's important is that at the end of this session, to end the transaction, you will have to call lock on the pool manager. And this is where the pool manager um trust boundary, uh as we could say, uh come in handy because it will ensure that the settlement, so all the balances, all the deltas are zeroed out, kind of how a flash swap works.

Um And so, this is also why, as you can see, there are two different and distinct component. And as a developer and auditor, you are focusing on the smart contract. And this is not part of Uniswap before uh V3 like it was before. So, it has completely a different threat model. And this is a hook swap lifecycle. Uh something to keep in mind is that there is 14 different functions in a hook. You are not obliged to uh implement them all. You can choose which one. Uh you will set different type of bits in your address when deploying the hook to say to the pool manager, "Okay, I want to activate the before swap flag or I don't want to activate it."

And so, as you can see in this lifecycle, the pool manager will basically check if the hook is activating the before swap flag. If if it is, it will trigger it. Otherwise, it will execute the flow. And the same thing for the after swap flag. Something that is important here is to keep in mind that both of your um flag or function in that specific example won't be seeing the same state in the pool manager. Of course, um the before swap will be seeing the pre-swap state and the after swap will be seeing the after swap state. And so, this is something also that I've been um the sources of so many issues that we found in audit. We often find the right code but in the wrong place.

And so, this is the threat model I was talking to talking about uh earlier. The PM, so the pool manager, will protect you from run thing. There is one invariant is that at the end of a session, the deltas and the balances are zeroed out. And up until now, it has done this perfectly. So, you can make this on the side and you will have to focus as a developer or as a reviewer on all the left uh right side of this slide uh being the access control, the trust boundary with boundaries with the pool, the the custom accounting if you were to implement any in your hooks, and your external integration. Something as well to keep in mind is that a hook can be almost seen as a pool on top of another pool.

So, some hooks can be uh stateless, but it's pretty hard. Most of the hooks are stateful. And so, this is something uh that you have to keep in mind while implementing all of these um um the left side um dots here that are your own responsibilities. And so, this is how um session is uh encoded on the pool manager. You will be calling the unlock function. Uh then there is this very important line here that you can see. This is where the pool manager will be doing its job making sure that the deltas and the balances are zeroed out. And you will then be calling the lock function at the end. Otherwise, if everything is not zeroed out, it will revert.

And so, now the five most common um foot guns or pitfalls or issues that we've been finding in audits, in all public reports that I could be uh that I could put my hands on, and on 50-ish uh findings from Solidified. Something that was very interesting for me is that when preparing this talk, I was uh expecting to be finding some very complex path or attack path or issues because hooks are pretty complex by themselves. But I think we are kind of where reentrancy was four years ago. Um right now, every developer knows that we should be implementing CI and um non-reentrant modifier, but it was not the case four years ago. And so, what I've seen from analyzing close to 100 findings is that

the five uh main common issues I will present you now are kind of simple. So, we also hope at 12 bits that by educating more the developers on something that is really new because Uniswap before is like one year, something like that. They won't be having the same issues and will be able to be focusing on much more complex um attack path in our uh further audits. So, the first one, really simple, access control. Um but the threat model of Uniswap before being different from V3 as I said earlier, the hooks are not part of the core protocol right now. So, you have as a developer to implement uh safe uh access control on every entry point of your hook.

Um If you not do do not do it, there is an entry door to your logic, to your storage on your hook. And this is something that we saw we found so many times on audit. Uh there is a very quick fix uh this is that is provided by Uni Uni V4, which is to inherit base base hook, sorry, and safe callback uh in order to make sure all your entry points are um safe and access controlled and that the safe callback can only be called by the pool manager. Otherwise, anyone can basically uh do whatever they want with the storage or balances and any funds or value that is stored in your hook.

And so, while the exploit was a um very complex chain of uh multiple steps, uh a lack of access control was one of them. So, this could have been prevented by um a safe access control. And so, this is what I've been what is provided by Uni V4 in the Uni immutable state dot sol contract. It's a very simple modifier that ensure that your callback will only be ever called by the only pool manager. So, please implement this one. It's a very simple one, but it will save you from a lot of uh issues. The second one, and I touched upon it very briefly at the beginning, a pool key equals a pool. If you change any of the value in the pool key uh in the struct, you change the pool you are interacting with. And so, you have to make sure that you are interacting

with the pool that you want to be interacting with in order for the tokens to be the one you want to have in your hook. And um for to do this, a very simple way is to bind at initialization of your hook the pool key you want to be interacting with if you want to be interacting with only one pool. This is uh for example, what Coinbase has done in their uh whitelist pool um hooks. Uh otherwise, you should be implementing a whitelist in order to make sure that only the hooks the pool, sorry, you you want to be interacting with are the one you will be um uh interacting with your hook.

And this is a very simple pseudo code kind of way to do it. At initialization, you make sure you um store the right pool and every single other call, of course, if you are in a one pool setup, you will make sure that it's the right pool, otherwise you revert. Custom accounting, this is something that we've seen a lot because developer are very creative on way to implement logics in their hooks. And so, you have to make sure that every single value, every type of value, sorry, the fees, the rewards, the LP funds are very distinct in your hook and are properly labeled because otherwise everything can get can get messed up very easily.

And I always like to ask myself this question on the right and so as a developer, you be you should you should be sure to test them all. Can a user get free output? Can a user profit from a round trip? Does internal accounting match the real token balances? All these questions are very are obligatory in my opinion to go through in order to make sure that all the accounting, if you're implementing custom accounting in your hooks, is safe. And the Bunny exploit is unfortunately one example of how everything can go but quite quickly and in a huge loss when the accounting isn't perfectly safe. It was due to a rounding error and unfortunately everything wasn't properly

implemented in the hook. Last one as well, fourth one. Right logic in the wrong hook. There is two main topic on this one. The first one being the fact that the before actions and the after actions won't be seeing the same state and so you have to make sure that for example, one common case that I've seen is that you will have some kind of tick initialization logic, but if this tick initialization logic is in the after swap, then after adding liquidity, the tick will already be initialized and so of course this doesn't work. And once again, it sounds kind of silly, but given that it's a very novel protocol and system, I think it's some mistake that developer are not

just aware of and we've seen quite a few times in our audit. >> [snorts] >> The second one is very critical in my opinion. There are lots of peripheral path that could be implemented. Some kind of dust removal logic, some kind of reward logic. But if you implement any kind of potential revert in this peripheral path, make sure that they are not blocking the withdrawal path for example. Otherwise, something that should be a secondary could be completely DOSing your hooks and could block any user to remove from removing their liquidity from your hooks.

And the last one, which is one of the most innovative feature that they implemented in V4 in my opinion. The way you tell the pool manager which feature you want to have enabled in your hook is to enable some of the bits in the address when develop when deploying the hook contract. And so as the title said, address bits are part of the API and so um you have to make sure that everything correspond nicely and that if you have enabled a bit in the address, you will have the corresponding function. Otherwise, the call from the pool manager will revert. In the other way around, if there is a function but the bit is not initialized or not checked, sorry, in the address, the pool

manager will never call it. So we saw some cases for example the after swap return delta, which is a flag that will tell the pool manager, "Hey, we are doing things with the delta." If the bit is missing, then the pool manager will never call it. Unfortunately, it means that at the end when the pool manager should be doing his settlement calculation, it will always be off because there will be some delta that is left. Some yeah, return delta that is left. Um And so one simple fix, as I said earlier, this is why you should definitely always be using base hook is to implement it because it will validate the bit the match at code the code at deploy time.

And this is how it's done. Very nice design pattern in my opinion. Every single bit, no, not every single bit, the last uh I don't know, 20-ish bits of the address um are dedicated to letting know the pool manager which feature implemented or not and they will be doing a bitwise to ensure which one are called by the pool manager or not. And this is also provided by UniV4 in the hooks test all contract. And so as a takeaway, you can take screenshot for the developer the auditor's checklist will be following up nicely.

This is the main thing you should be answering when developing your hooks. Access control, trust boundaries, making your accounting internal accounting right. Keep LP funds, fees and incentives separated as what I was being telling you. Make sure any peripheral path are not reverting or not potentially DOSing the main user flows like the withdrawal path. And as always at Trail of Bits, we always recommend to fuzz any single entry point that you can be thinking of in order to make sure they are not missing on edge cases.

As for the auditor checklist, kind of the same thing but with an attacker mindset. Can I call any callback or call entry point directly and if so, what can I do with it? Can I route some custom logic into the hook and mess up with the internal accounting? Can I mess up with the internal balances and deltas and if so, can I make the settlement always revert at the end? Can I make a peripheral path revert and so DOS the the hook entirely for for example the remove liquidity, which is of course the main user flow that should never revert under a

wrong assumption, I would say. Do the permission bits are nicely implemented? Do they have the right function the right return values? And what breaks if I can make the hook interact with pool that is not trusted and with for example some malicious token? And so this is the takeaway. Um Pool manager does one thing and it does it very well. Up until now, it secures the settlement. At the end of the session, the deltas will be zero, the balances will be zero, otherwise it will revert. But all the rest is under your own responsibilities as developer and as auditor to ensure safe. Meaning the access control, the trust boundaries and the accounting, the internal accounting.

Thanks. >> [applause] >> Anyone have a question? No? No question? Okay, thanks all. Yes. I don't know what kind of data you got.

Sorry, could you repeat the question? >> um if we use the factory contract and this is not a related question to your um to your slides here just but if we use the function get all pools, then we got way over 1.6 million liquidity pools. We wonder is that a correct figure or is that data that we got from on chain? No, it sounds right. I mean anyone can basically deploy pools so there are so many pools that are not used, of course, but it sounds right, yeah. It wouldn't surprise me that this number is right.

Okay, thank you. Question, you said you already like around 100 use cases for V4? No, we I've analyzed close to 100 findings that are coming from our audit at Trail of Bits, from some public reports that have been publicized by other firms and close to 50 findings from Solid which is a online database with findings on from reports, contest and bug bounties.

Nice. So the follow up question is like could you give an overview of what are the the most commonly the most common use cases? Oh, sure. Um The most common, wouldn't be sure because there is Coinbase as I said earlier are implementing hooks to have some kind of white list to be able to allow only certain people to be interacting with their pools. There was some nice stop loss feature that have been developed in order to basically remove all your liquidity when the ticks is at the right price you want to be buying or selling for example, which is also very nice use case.

Um Lately I've been auditing component that was turning UniV4 because UniV4, if you not know is using concentrated liquidity like UniV3 is also and they were turning the concentrated liquidity pool into like a full range pool basically and so they were implementing their own vault on top of the UniV4 and kind of adding another layer of shares and and vault accounting. Um Mhm. Yeah, clients have been quite innovative So, I think we'll be seeing a lot more use cases in the future. But, for now, the Y series is one of the few I've uh one of the main one I've seen up until now.

Thank you. Anyone have a another question? No? Thank you all. >> [music and applause] >> THANKS. THANKS, EVERYONE.