500 days of Security Summer
DeFi Security Summit 2024 · 8 Nov 2024 · 23:49
Sara Reynolds, a senior protocol engineer at Uniswap Labs, walks through the security process behind Uniswap v4 across design, building and testing, and auditing, along with the bugs uncovered at each stage. She covers design choices such as separate hook permissions and split liquidity flags so a reverting hook cannot lock liquidity, the rule that a hook cannot change a swap amount, and Foundry fuzzing and ffi-based math testing.
Speakers
Topics
From DeFi Security Summit 2024, hosted by DeFi Security Summit.
Transcript
okay great yeah perfect okay are we good okay hi guys um my name is Sarah I am a senior protocol engineer at Unis swap labs and this talk is called 500 Days of security summer um it's not actually 500 days we did not actually spend 500 Days on Audits and all of that but it's sort of a nod to I think this sort of robust process we went through in securing V4 and I'm going to talk a little bit about that today um but I'm also going to talk about the four I guess main
phases uh at least how I think us at Unis Swap and probably a lot of other protocols think about when we write a new protocol what are the four kind of big phases we think about and also how do we think about security at each of those phases so the design phase like what does the architecture look like what's going to be um a usable architecture but what is also um you know less vulnerable to uh security issues at the building and testing phase how do we Implement really good um testing practices in reviewing PRS and then finally at the auditing phase um and so we'll kind of see like little examples at least from my experience of where we've thought about security and brought it into building V
4 so first um I have been working on V4 for over a year now this was before um we really had hardened code for V4 and we had a vague idea of the architecture you know we knew we wanted a Singleton contract we had sort of this lock and call style architecture um that's really kind of the key like security mechanism in keeping um pools essentially solvent and then we also knew we had this like kind of flash accounting system we wanted to use transient storage there was no transient storage available yet in the evm um and so there was a lot of moving Parts with the architecture and
I'd say it's quite complex especially if you compare it to sort of V3 where we have the factory models and each pool is their own contract um right in V4 it's all pool storage is held in one contract so that was kind of complex and on top of that we had a huge feature set so like V3 it's a concentrated liquidity style amm um but you know it sort of Builds on the expressiveness of V3 with hooks right so that's already scary in itself we're basically allowing at predefined points in the life cycle of a transaction you to call to an external contract um any external contract defined by the pool Creator um we have
donate this is donating to range LPS that has some interesting implications that V3 didn't have um unlimited fee tiers Dynamic fees ERC 699 wrappers like this feature set is huge and on top of that there was one really big feature as well um that we wanted to get into V4 which was custom curves and I'll talk about that in a little bit um the first thing I want to touch on though is um hooks we were we had a few different design L for how um we were going to design sort of this this call out to various hooks so um for some context hooks are addresses defined by a pool Creator and
they basically allow the Creator to specify when at what point in the life cycle of a transaction can you call this external contract um and the key I think feature with hooks that is um really great is that you you have to predetermine what call sites those are so is it before swap after swap is it before initialize after initialize before modify after modify um and those are unchanged they're defined in the hook address you can't change um what call sites or what permissions a hook has um the other key thing to know is
that in creating these hook permissions we eventually realized that there were certain permissions that are quite scary so if you think about um modifying liquidity you can add liquidity you can remove liquidity you can do it in this at the same transaction let's say if you're rolling one position into another the scariest action is when you remove liquidity right you could build a hook that isn't intentionally malicious but reverts on accident with like a bad parameter what happens when the hook reverts when you're removing liquid worst case is now your liquidity is bricked in the pool and that was a really big kind of like scary situation and so one thing that we did in designing V4 is we actually added new
um Flags into instead of just having the before modify liquidity and after modify liquidity we actually split these flags out into four Flags this was really important because we had to distinguish between adding liquidity as an action and removing liquidity as an action and so really the key learning here in sort of going through some of the design for building Hooks and sort of defining some of these permissions is make these permissions as granular as possible um it has sort of implications when you think about building routers and um liquidity management contracts right of if I can feel like okay I'm calling a pool but I
know that I'm only caring about routing for a swap this pool doesn't have the before and after swap flag so I basically don't have to care what the other permissions are and you can kind of extend that when you think about the kind of plethora of permissions um that hooks can have um this was a really interesting principle that came up as well when we were designing custom curves so for some context here with custom curves um it's really uh essentially the ability to bypass or override the concentrated liquidity model that's like the V3 or or V V3 style or the X X Y equals K curve we wanted to basically allow hooks to be
able to write their own curves or Define their own kind of exchange rates on pools um this is a huge design space we probably had around four or five different designs and all of them kind of pulled and pushed at these different um kind of trade-offs so code complexity flexibility composability and in and sort of integratability and then security um and you can imagine like if you think about code complexity sometimes writing like uh a a ton of code actually might be worse for security um because you're you know potentially adding a lot of code to really hot paths in the code base um but
reducing that maybe maybe you know increases the flexibility of what what can happen at the protocol but then is a lot scarier um safety-wise for users and so there was all these various designs and it was really a matter of choosing one that we felt good hit a that could hit sort of a mixture of these tradeoffs um so at one point um on the left you'll see this diagram for this super easy Uh custom curve implementation um it was was a crazy design we essentially allowed a hook to be able to apply Deltas on behalf of a user if it had a certain permission
turned on and what that means is is essentially you could do things like a user has asked to swap um you know 100 usdc a hook in theory could then say okay well I'm going to give the user um 100 die but you know what I'm also gonna like give the user um you know I don't know Point 2 eth or something like as a reward so you could have these really interesting features this is like a tripool you could natively have a tripool um what was scary about this design though was that we were kind of um adjusting the locking mechanism um it was not a ton of code but it was also harder to test there was sort of
infinite possibilities a hook could do in this world um we iterated that that a little bit and we actually ended up making the constraints more defined so one of the key things um that I was talking about in the previous slide was sort of making these permissions more granular in the old design we had one flag that was any hook with a custom curve flag could have these um this permission in the current design um which I'm much happier with and I think really prioritizes user safety we have much more granular permissions you can see these four um permissions here outlined before swap returns Delta flag after swap returns
Delta flag and the design basically is really hinges on this invariant that a user's balance always changes by their specified amount so if a user specifies you know swapping 100 usdc they will swap 100 usdc a hook cannot say actually you know we want the user to swap 110 usdc on the pool um so was like a really important variant for us to keep and it meant that we actually had to increase some of the code complexity we have more checks more um you know kind of sanitization around our hook call sites um but it also meant that there's you know we could do more um specific testing around these kinds of behaviors
for Hooks and so ultimately we favored sort of security and safety in this regard when we were building um custom curve pools um and again it hinges back on the fact that we liked this design principle of separating out hook permissions and limiting where limiting them where possible okay into the testing phase of V4 um I put it kind of coupled with building because I think as we were building a lot of the features we were also heavily heavily heavily testing them um you know we had all the basics unit tests fuzz tests integration tests um but we actually
made a switch very early on in the codebase to use Foundry and we were able to leverage a lot of their really interesting and great kind of testing uh sweet properties um we have some you know High fuzz runs in CI we use ffi for some really interesting math testing like making sure the math makes sense we do we'll implement something in JavaScript and we'll test it against solidity um but there was one really interesting contract that we wrote with regards to testing that I want to call out um but first I'll go into some of the testing Frameworks that we used so when we went to go write like you know an
integration test for swap we'd have to go build these custom routers so we have this pool swap test. Soul contract and basically what that does is it unlocks locks the pool manager and then um processes the request and then settles all the Deltas on core and so you need this kind of like router this test router for any kind of swap test you want to do so we built all these routers for swap we built a router for modify liquidity we built one for donate they were all custom contracts for each of these paths this was really nice for you know simple-ish tests where we're just testing a swap or we're just testing a modify liquidity it's it's not so great when you think about um kind of
combining actions the more advanced flows um but those flows are really important because that's one of the huge I think benefits of V4 is you can couple all of these actions within one call to the pool um so if you want to say you know uh initialize a pool and then swap or then add liquidity and then swap um or if you want to add liquidity remove liquidity all in one go you can do these kinds of actions um and so what we built was um the actions router which I love I think it's um a really cool testing kind of infrastructure that we built it's sort of this General very generalized
contract um it allowed us to test all of these sorts of um interactions between swap modify liquidity sink settle take um mint burn all these things that you can do on core all in one um and it ends up it it's really quite extensible um one of our Auditors even you know built on top of this actions router and added new types of actions you could test through and the coolest thing I think about this is that it allowed us to check certain properties while we were testing um swaps or ad liquidity so if we wanted to do a balance check in core we could do a balance check in the middle of the test or um if we wanted to check the
transient storage variables we could do that in the middle of a test as well um and so this is kind of what it looks like um but you can see we have like all these actions defined like take sync we have like assert balance equals assert reserves equals and this ended up being a really important kind of contract that we built on um even in periphery so if you think about the periphery code this is all of our outing code it's our liquidity management code um we adopted this style of um actions routing in this like unlock call back for both of these contracts as well um which is kind of
pretty cool because it started as just something we needed for testing and now is something that we use in production and is something that I would also recommend for people if they're working on Integrations with core is a pretty good framework to think about how do you batch actions on the pool manager the last thing I'll call out for testing is I think this was a really cool thing we did we fuzzed certain um actions against V3 so I like to think about V4 as really like a super set of V3 we extend on all the math and everything that V3 uses and so if we could you know fuzz that swaps and modify liquidities are equivalent to V3 that's a pretty cool
benchmark viewing this was honestly probably a lot of my time in the last maybe six months is PR reviews um and this is like we I think myself and also the rest of the team spend most of our time just look reviewing um and I would just say like we spend a lot of time doing nits and we didn't really settle for finishing reviews quickly or trying to meet a deadline and the other thing that I think was really interesting is that we had to sometimes assume that Co is malicious we've had V4 open source for over a year and get community contributions into the code base and we always have to review those in mind with
you know the assumption that maybe maybe there's a bug being injected here um and so that's a really important thing I think to keep in mind as we're going and reviewing Community PRS but also internal PRS like we we have to you know not we have to basically doubt the code um when we review and then finally the audits this was sort of the last phase of securing V4 um we had nine total audits um a 2.2 maybe even greater than that um million doll competition and we spent around five months um doing sort of a waterf flow of audits and competitions this is what it looks like so we actually back in March
kind of scoped out what the audit process would look like and you can see it's sort of like a water flow uh sorry a waterfall of of audits where you know may we start the first two audits on core and then you know we start another audit a few weeks later and we incorporate those changes and then we do another Co like core code audit um same with periphery and then finally the competition to wrap it all up um and I think yeah the key thing here from this is that we kind of liked this idea of audit stacking where yes by the time the first audit started the code was Frozen um but as we went on through the audits we knew that the code would change and so being able to incorporate audits
later on in the process was really important for us and actually we had found mistakes from audit fixes previously found in later audits so it was like really key for us to have that audit stacking and then finally just like I think the last thing to call out was sort of having a diversity of expertise in the audit firms that we worked with so um and this is just a call out that there were some really great findings I think unique to each firm that we worked with um and you know some I think found you know maybe more focused on like math heavy pieces or some focused on um you know like longer term integration wide
bugs um we did formal verification so I think having all of these lenses on top top of also opening it up for the community to look at the code and um find something was really really important for us so where we are today V4 core is Frozen um what is next we are going to be announcing something on Monday uh uni day so um stay tuned and a bug Bounty so I like to say that yeah security definitely doesn't stop at um the audits we you know are still continually thinking about monitoring we're thinking
about hook security um I think there's a ton more we can do even once it's live um and so with that thank you all for listening I don't know if we have time for questions but if people want to ask questions hi uh yeah I've built out like a few hooks in the past and uh like some of the main issues I came across especially doing like more complex curves like you
described uh was trying to figure out like the logic for the unlocking the pool and like settling the Deltas um and I was just wondering when you were like kind of going through the process of of experimenting with like new curve designs and stuff like that like how did you go about sort of yeah handling Deltas like did you have any advice for like hook Builders who we trying to like grock that um and yeah just any advice in general about like that that part of hook building would be really really useful yeah it's super complicated um that's actually why we added a function this is Midway through the audits there's a functioning core that we added clear that basically just removes dust so like right now before that function
if you had like some dust left left over because you didn't exactly calculate it or there was some Precision loss or whatever um it's really annoying because you just revert or whatever you can't figure it out why clear um is something that we actually integrated in our peripheral code so if you go look at the position manager um we have a action called clear or take basically that says if there's a Delta open and it's like you can program it like in our test we're like okay if it's less than like 100 way like clear it otherwise we want to take it that's a really nice way to be like okay we don't actually really care about you know dust um but and we
want to make sure the transaction doesn't revert so we can use that that's really cool um the other thing we wrote is there's a contract in I think a lot of contracts in per free where we're writing integration code with core like a lot of those contracts hook developers can use and build off of so one like I mentioned is the actions router and we have a contract called base actions router that basically does nothing and so you can like inherit it and write your own actions and do your own like customize this actions router which is really great we also have this contract called Delta resolver so it handles like the
sync settle sync transfer settle process of like integrating with core which is really gnarly and definitely like could be a foot gun for a lot of users um and so I would recommend looking at that contract as well um and it handles a lot of the like yeah integration with take settles Etc sorry what was that last one called again Delta resolver perfect thank you thanks great
presentation um I'm curious among all the pool requests that your team reviewed like the external one you know over a year have you noticed any pool requests that you know looked a bit Shady or maybe you know are most malicious no um most of the well pretty much all of the community contributions were like really positive um so it's more just something we have to keep in mind when we look at reviews there's definitely PRS that we didn't accept um I think sometimes when we look at code and there's a lot of assembly or it touches really core critical paths we're a little bit um less likely to want to merge something like that with such big
changes um with that being said we still made some pretty interesting changes to to paths here that were written by the community thanks cool thank you [Applause]