Modern Smart Contract Design and Security Without Compromise
DeFi Security Summit 2023 · 16 Jul 2023 · 12:27
Mark Toda, a protocol engineer at Uniswap Labs, gives an overview of Uniswap v4's architecture, focusing on the Singleton pool manager and flash accounting and how they affect security. Earlier versions enforced invariants contract by contract, which forced intermediate settlements when a trade crossed pools. In v4 one contract holds all pools and balance changes are tracked as deltas that must settle by the end of a transaction, so invariants stay tight while multi-step operations get cheaper. The Q&A touches on read-only reentrancy and unusual tokens.
Speakers
Topics
From DeFi Security Summit 2023, hosted by DeFi Security Summit.
Transcript
Mark Tota he'll be speaking about modern secure or modern smart contract design and security without compromise hello good morning uh yeah I'm Mark Toda I'm a protocol engineer at Union swap Labs I'm sure many of you saw that we released our uniswap V4 protocol a few weeks ago I'm going to be talking about it today I only have about 10 minutes I'm going to be giving you a brief overview of kind of the architecture of unisaw V4 specifically talking about our Singleton and Flash accounting pattern and how it sort of affects the security of the system okay so kind of a standard design principle for smart contracts and one
that you know swap really cares about is this idea of sort of defining your invariance and enforcing them explicitly and tightly on your smart contracts and sort of a famous example of this is the XY equals K curve from unisot V1 and V2 you know there's like a single end of code where we're enforcing the invariance and it sort of ensures the security of the unit swap pools and sort of one important thing to note about the way that we like sort of traditionally have handled these invariants is sort of that they use the smart contract as like the base unit for invariant checking right like one pool in unisoft V one and V2 and V3 is one smart contract and the invariants sort of apply to one pool and so any
operations over that pool you know the invariants are enforced and like each pool is sort of its own set of invariants and its own checks and this is a really nice abstraction for smart contracts because it makes them quite easy to audit right like every smart contract has these things that you can verify and things that you can test it makes them makes it quite a bit easier to reason about changes to the protocol as well and sort of the asset positions specifically for uniswap are segregated by pool which allows for some nice checks but it does have a few downsides which I'll try to explain here one is that there's sort of if you're performing operations over the unusual protocol that affect multiple pools then there's some intermediate checks between
the different pools that have to be done which can cause some gas inefficiency it's also hard to do more than one operation at a time and I'll give examples for both of these so here's an example where someone is doing a swap across two pools on uniswap this could be unusual V2 or V3 so for example here they're swapping weave to die but there's this usdc intermediate step and so the first thing a router will do was would be to you know swap with for usdc on the with usdc pool and because the invariance and the logic is handled contract by contract pool by pool the with usdc pool you know checks the curve and sees how much usdc the user should get for a certain amount of Wheat and sends that back to the router
and then the router has to do the second stage separately where it goes and talks to the uscc dipole sends its uscc there and it swaps for the die secondary pool checks for its invariants sort of separately right and so the key thing to note here is that this usdc is kind of like an intermediate operation in terms of the protocol itself but because each pool is handled separately it has to be sort of fully settled uh in the middle right so this usdc is like transferred back from the protocol to the router and then back to the protocol so you can see how that's kind of a potentially inefficient like extra step another example here would be if we're trying to like Swap and add liquidity to one single pool um like if we just had one side if we
had just we think we wanted to add liquidity on wheat and usdc at the same time um it's one pool but it's two operations on one pool and the invariants are checked operation by operation as well and so the router would have to send the wheat in uh swap it for you know half of it for usdc and then send them both back to the pool to add and so again you see this intermediate step with the usdc that potentially like is uh is unnecessary and so as we were sort of thinking about designing a new protocol we were thinking how can we improve this trade-off while also you know making you know making it easier to interact with the pools easier to build Integrations and also maintaining this sort of uh invariant checking system and so the first thing that we did which seems kind of like the the main way to
do this is to make uh Unison a single tint as opposed to like a multi-contract system and so in Minnesota V4 there's one single contract that holds all the pools we call that the Singleton or the pool manager and so all pools live on this contract instead of being separate contracts with separate asset balances all the asset balances are shared and the pools are handled by uh struct and the library construct that handles all of the curve math so each pool is still able to maintain its invariance and maintain the balances of its curve using that Library construct construct but we have the ability here as you can see to kind of perform multiple operations in the same smart contract and the way that we actually do this is using an interaction
pattern called flash accounting which is specifically enabled by a new EIP called EIP 1153 or transient storage which is not out yet but it should be out in the next hard Fork Cancun and so the basic pattern of flash accounting and again I'll go through an example of this is that the router can still make calls one by one operations swaps and add liquidities across different pools each pool still asserts its invariance on each operation but instead of settling them right then as the operation occurs they sort of store the the delta or they store like what needs to happen for these operations to be valid and as all these operations happen those Deltas kind of aggregate and only at the very end when the router
or the integration is done operating on the pool is everything fully settled so I'll explain an example here so here we have like a router and we have the V4 Singleton pool manager which has many pools so it's going to be the same thing where we're swapping wheat for die with usdc in the middle so the first thing the router will do is call a function called lock on the Singleton and lock is sort of the way that you start this context or you start this like multi-operation transaction and so calling lock does two things it sets up a lock accounting context on the Singleton which is a transient storage mapping basically of two sides like debits and credits where one is tokens that the pool is owed for the operations to be valid and the other one is tokens
that the pool owes to the user that they can take and then the Singleton calls a callback on the router which lets them perform operations under this context so first operation here might be let's swap one week for usdc and what happens in the Singleton when that operation is called is it talks to the pool construct which is just like it operates pretty similarly to a unusual V3 pool and the construct will say okay if I get one with I can give the user 1800 usdc based on the curves and the liquidity and so on and then the Singleton will add that sort of Delta to the lock accounting so in this case it says I need one with because that's what the user was swapping and I owe the user 1800 usdc
and then if we do the second swap here where we're swapping 1800 usdc for die we'll talk to another Pool and again not pool operates independently using the library and struct construct and it says okay if I get 1800 usdc I can give the user 1800 die and importantly here those Deltas cancel out so when the 1800 usdc is sort of given to this usdc die pool it cancels out from the lock accounting because it was an intermediate step and now the lock accounting says okay I need one with and I can give 1800 die and now the router can call a function called take which literally just takes from this o side of the lock accounting and it pulls the tokens into the router and then there's another operation
called subtle which is basically like you send in funds and then it settles the Need side of the lock accounting so the router might send in one week and call subtle which would clear out the lock accounting and then once the full lock call is done the Singleton essentially needs to just check like is everything settled right are all of the are all of the Delta zero and if so then we know that every pool was satisfied and the full swap the full operation was was done properly and so yeah again transient storage is really important here because as you can imagine all of these operations and all of this accounting is happening um on every call like separate calls and so if we had to do this before transient
storage it would be full s stores for every Delta um and so with transient storage because these are sort of only temporary State it's only needed for the context of this operation we can throw it away afterwards and transient storage is is much cheaper for that operation also this is quite flexible in terms of operations because or in terms of Integrations because you can you know call many operations on many pools in whatever order you want integrate with other external protocols as well makes it makes it quite easy also yeah the invariants are so managed tightly by the pool so we can like buzz test and formally verify those pool constructs which is which is a nice security principle okay obviously it's very short there's
many more talks on unisa V4 at ECC I put a few of them here um yeah go see them thank you thank you so much Mark so who has questions about univ4 oh there's a lot of questions regular mic hi thanks for the talk um in terms of invariant check usually it's encouraged at the end of the operations
and it's great for the protocol itself but do you think it opens other attack vectors because encourage you know it basically ignore the middle steps in a largely sense for example you know read only reagency and what's your take on that and how should we deal with that yeah that that's a good question I think from my perspective kind of the uh if I'm not sure if you've like read the code but it's quite similar actually to the the code of Unison V3 it's just like instead of being the whole smart contract it's a library inside of it and so basically the only change to the ways that the invariants are handled is that instead of checking the required tokens
in and tokens out after a swap they just get added to the Deltas and so like from the perspective of a pool as long as the Deltas are being satisfied properly throughout the entire transaction all of the invariants are satisfied uh if that if that makes sense I think the sort of re-entrancy and multi-operation flow is uh actually pretty unchanged in this case yeah hello hi so how do you handle uh tokens that do weird things in this bottle of only one single pool yeah that is a good question um again it's actually pretty much the same as unisotv3 like you can make a
unusual V3 pull with any random tokens you want and like maybe the sort of behavior of that pool is is undefined in that case and that's the case on Unison V4 as well but but because these sort of logic and state and checks of every pool is segregated within the Singleton uh it still actually doesn't affect the solvency of the pool as a whole or up pool might have less tokens what do you mean by that if somebody uh you know if weird behaving coin actually remove your balance or increase your balance you know the pools accounting you know the total accounting might be off right oh right I see I see what you mean yeah
um that definitely would be true but it would only affect other pools that also have this weird token which affects balances right okay cool any other or sorry is um Jeremiah here yeah awesome awesome well thank you so much Mark let's give it up for Mark again [Applause]