Protecting Contract Upgradeability Through Sound Proxy Pattern Usage

DeFi Security Summit 2023 · 14 Jul 2023 · 23:55

Dimitri Kamenski and Richard Skinner of Sigma Prime explain how proxy contracts make Ethereum contracts upgradeable by using delegate calls to run a logic contract's code against the proxy's own storage. They compare transparent proxies, which handle upgrades in the proxy and check the admin on every call, with UUPS proxies, which cost less gas but keep upgrade access control in the implementation. Best practices cover initializer functions in place of constructors, disabling initializers on the logic contract, initializing in the same transaction as deployment to prevent front-running, and avoiding an accidental mix of both patterns.

Speakers

Topics

From DeFi Security Summit 2023, hosted by DeFi Security Summit.

Transcript

so if you have another exciting talk now it's by Dimitri and Richard from Sigma Prime they're going to be talking about protecting contract upgradability through sound proxy pattern usage so please give a warm welcome to Dmitry and Richard uh Hey guys my name is Dimitri I'm a blockchain security engineer at Sigma Prime this is my colleague Richard we work in the same area basically looking at security issues in DPI protocols and evm compatibles smart contracts as well as non-evm stuff like client implementations

So today we're going to talk about sound proxy usage uh and uh yeah basically we'll cover what is a proxy transparent versus uups proxies and some best practices when it comes to implementation okay thanks so um what is a proxy well one of the key attributes of an ethereum or ethereum style blockchain is that basically once you've deployed your code your core your code doesn't change there is one quite one specific exception but for the most part this holds true once you've deployed your contract your code isn't going to be modified it's not going to change you can't change it even if you want to and this has some upsides and some

downsides the main upside of course is that because your code isn't going to change people can trust your code not to change which is useful if they're going to put a lot of money or valuable tokens into your code um but it has the downside that if your code has a problem maybe a bug maybe a security issue is discovered then you can't modify it and you can't also upgrade to add new features so proxying is a system which gets around this it doesn't directly change your code it works by sort of borrowing code from other contracts which we will get into so let's imagine we've got a proxy contract here this is contract P or proxy and the proxy doesn't have much uh code of its own it has a variable telling us

where we're going where the code we're interested in actually exists and in my example here that is inside contract a um here is contract a full of lovely code so this is the code that you would write or the project developer would write um that's the code we're interested in and proxy contract p is going to sort of borrow it P um does contain code as well of course but it's just the code to make a proxy work uh so to have an example we've just got a very simple set of code in a here we've got a variable called number and then we've got one function which gets it and returns it and another function which changes it and

it works like this proxies work like this you call the function you're interested in on the proxy and then like I say it sort of borrows the code from the other contracts as in this example so that the function there only exists in contract a and contract P sort of borrows the code from a and then it runs it using contract P's own contract address and it sets it in its own storage right if you notice inside contract P there we've got number equal three so we've set the variable inside P's own storage even though the code was from a and we can imagine you can sort of duplicate this out so imagine we had another proxy contract just the same as P but we've called it Q it's also using

the code at a and you could then call the function on queue as well um and as you can see because it's using its own storage we've got a different value of number in the um storage of Q and the storage of a remains untrained unchanged we could if we wanted call it directly on a like that and we're then modifying the storage of a this is a different type of operations the sort of more basic operation right and this is called a standard call when we're talking about proxy is one of the key Concepts to know about is that there are these two types of calls going on a standard call and a delegate call now a

standard call as the name implies is the sort of call that you're used to the calls that funk that contracts generally make to each other if nothing else is going on the calls that uh users make into contracts are standard calls and they use the everything of the contract so that's called contracts code the called contract storage the call contracts address the other type though that proxies use and and other code can use as well it's a delegate call so this uses so this is the sort of borrowing step that I'm talking about this is um using the code of the contract that you're calling but your own storage your own address the callers storage and the caller's own address and so you get this

sort of what I'm describing is borrowing the code and if we go back this is just a diagram of everything that we've already seen um and I've just put the different call types onto the diagram so contract a the last thing we did is at the top there that red arrow that's just a standard call into contract area modifying its own storage then at the bottom left the you've got the proxy flow going up that diagonal on P and it starts with a standard call into p and then sort of behind the scenes if you will contract P makes a delegate call into contract a um to use its code and running with its own storage and address um so if we go back to where we were

before we've got rid of Q let's move a over and now let's say that we want to upgrade right so if we want to upgrade what we do is we create a new contract um but no down in contract P note where that red ring is we changed our variable we're no longer using the code at a we're using the code at B and if we look at the code of b b is just the same as a but we've added this very simple function which is just returning double of that stored variable and just getting rid of a there just to tidy up and then if we call that function our new function on P again it goes over it borrows the code

from B just as it did before it's borrowing the new code that we've added note that it's running though again with P storage so it's setting that using the value of number that we set previously actually using the code of a completely different contract right it was contract A's code that set that variable in P storage and now we're you're doing operations on it using the code of B and that's the basic idea of upgrading cool um yeah so uh rich is basically talked you through how uh the logic of most uh proxy contracts work uh I'll just quickly discuss two very common implementations uh so we have transparent and uh Universal upgradable proxy standard uh just abbreviated to

uups and uh yeah basically uh we'll just do one like small unofficial like kind of bit of advice uh we'd kind of prefer you to be a bit skeptical of other variants of these proxy contracts just because they kind of add complexities uh only use them if you are absolutely certain you need to like based on some specific use case uh so transparent versus UPS so talking about what the similarities are first uh essentially they like Richard said they deal with storage in the proxy contract they deal with delegate call based logic and they refer to a logic contract in

order to actually update storage and actually process kind of function logic uh so in terms of the differences well it really is dependent on how the upgrade occurs so in transparent proxies the upgrade logic is handled within the proxy contract uh using a standard call so just a regular function call whereas on on the UPS side uh contracts basically uh are forwarding all calls using delegate call so it's the actual implementation contract that handles the upgrade process so if we kind of zoom in a little bit more so in transparent proxies we need an admin user or a

contract that's whitelisted the admin user will call this particular fallback function and in doing so the transparent proxy knows that this should be something that is an upgrade process because the admin would only ever call it during upgrade on the uups side it's boarding all calls to the logic contract so Access Control needs to be implemented within the actual implementation contract so basically since the uh so on the transparent side this is kind of a diagram kind of depicting this uh basically we have the admin user calls

the proxy contract uh the proxy contract doesn't need to forward any calls to the logic contract instead it just updates its internal storage slot for where the implementation contract is and it will just change it from logic one to logic two any subsequent user calls will be forwarded to the next logic contract so that kind of basically solves your upgradability problems so if you needed to change your logic one contract for something else uh future calls will get forwarded to logic 2. uh so on the UPS side we have something similar but uh because all uh calls get forwarded the logic contract needs to decide if this is supposed to be a valid user calling

the upgrade process uh then it uh will trigger a storage slot change in the proxy contract and then the next logic two will be what other users are interacting with when all the calls that they are making get boarded to the implementation side so um I was like initially kind of super confused about proxies when I first started uh understanding where the upgrade process actually really helps differentiate between the two so it helps you kind of determine uh which proxy you should use now it is worth mentioning that there is some gas overhead in the transparent side and that's because they need to account for the uh the logic changes uh or deciding whether the admin is

available user or not to be calling the upgrade logic so yeah your UPS contracts are typically a little bit uh have a little bit less gas overhead and kind of zooming in to that for the transparent side we can see this is actually the gas overhead that I'm talking about so it's quite like a bit of a complex logic in terms of whether a user is able to upgrade or not the key line really is just under the function call just checks that the message sender is the admin or not and yeah basically uh all other calls would just get forwarded to the fallback function if the user isn't the admin it's worth mentioning

here that this call this logic this small gas overhead is actually called on every single interaction with a proxy so let's have a look at the UPS side and see how that compares so it's much leaner this is only called when an upgrade is happening so it minimizes the gas usage uh yeah and without any extra on UPS side of things we will talk about some best practices with implementation so when it comes to best practices proxy contracts might not seem complicated but it's because a lot of the logic's

already been kind of abstracted away and um it's easy that to like change a few things and kind of end up in a more complex situation again uh so we kind of recommend you basically to basically don't use upgrade logic if you don't need it don't use complex unnecessary proxy patterns when they aren't required and don't create your own version of a proxy when you can use the eips that have already been built and tested so often proxy patterns are kind of neglected like a developer will build their Suite of smart contracts uh kind of implement proxy patterns without

kind of maybe considering whether it's necessary or not but just because it's on Trend and yeah sometimes that can have some genuine security issues as a result yeah okay so then we need to talk about Constructors so um Constructors kind of don't really work with proxies um and the reason for this is because of how Constructors work Constructors although in in a bit of solidity code they do look a lot like a function they're not really a normal function at all they just run when you deploy the contracts and then the code is basically just thrown away so the Constructor doesn't hang around as a callable function the way that all

the other functions do which means that you can't write a Constructor in your logic contract and then call it with the from the proxy contract and you also can't really do that sort of thing in the proxy very easily because of course all of your the stuff that you use Constructors for which is sort of starting configuration all that stuff tends to be kind of defined in the logic contracts all of your complicated variables and your mappings and structs and all this sort of stuff it's all over there in the logic contract and so not very accessible from the proxy

so the solution that we have is this thing called an initializer function and then an initializer is just a normal function and that you write in the logic contract and it's designed to be called by the proxy to do the job of a Constructor um it's they're often called initialize or have names like that they don't have to be called that sort of thing and but that's how that's how they work they're called during the proxies deployment usually um to do that initial configuration using all the advantages of being able to code it within the logic contract but it they create dangers and complications of their own one of the

biggest issues with an initializer is that it's still sitting there on your logic contract and it's possible for somebody to attack you by just initializing your logic contract directly right no proxy business just they just directly call the initializer and initialize it and you know often they can't do that much harm this way but you still don't want people poking around in your logic contracts anyway it's it's not a good situation generally and there are some dangers like catastrophically they could potentially self-destructure a logic contract which is obviously not good so the um main defense against this in solidity if you're using the open Zeppelin proxy

libraries is this you call this function here this internal function disable initializers disable initializers um tells your logic contract that it's been maximally initialized and you might be thinking hold on didn't you just say that we don't use Constructors here and now here's a Constructor that's true um but the reason why we're using construction here is we're actually not dealing with proxies this this is protecting the logic contract from being directly called itself and interacted with as a contract normally which is why this very limited Constructor that just shuts down the initializer is useful uh yeah so uh another thing worth

mentioning is uh basically setting up and initializing in one transaction so uh in proxy deployment well in proxy deployment and maintenance usually we have one action which is the actual deployment where we release the proxy into the real world uh and then the second action is initializing it uh like Richard mentioned uh so the thing is if this is handled in two separate transactions uh as some of you may be aware transactions can get reorganized or malicious users can insert their own transaction in the middle so this basically allows for someone to front run initialization by initializing your contract for you so not ideal

to avoid this uh basically we recommend you just do deployment and initialization in one transaction sometimes there's complexities around this where you may run out of gas doing both in one transaction then you need to think about how you're going to implement Access Control in order to split them across transactions to make sure a user can't just come in and front run one of your initialization transactions uh kind of talking about the risk of not doing this basically a malicious user could kind of take over your contract or potentially yeah mess around with the values that you would pass into the

initialization right yeah and we also have one little Pitfall here which is that it's possible to use both transparent and a uups pattern together probably by accident they're not actually mutually exclusive and they are actually sort of technically compatible they don't break each other um but it's probably not what was intended most likely it would happen because maybe you start with transparent and then without realizing sort of install a UPS logic contract or perhaps as misuse of a sort of automated installation tool somebody might accidentally set up both and they will seem to work well they

will work the danger really is just because it's probably not done intentionally there's an a much higher risk of human error here maybe the transparent proxies admin gets forgotten about um and maybe get sort of comes into the hands of somebody else in the future and doesn't an unwanted upgrade something like that yeah I'd like to just add a quick point on this um without mentioning who it was this uh was a quirky misconfiguration we found in the real world on one of our client engagements um so it's kind of easy to not consider what kind of framework you're using for proxy deployment and what the default

values as to which proxy it's using um so it's worth checking that depends on what framework you use uh so yeah basically another thing to mention is uh as Richard kind of mentioned with delegate core logic uh your forwarding kind of control of a proxy contract to some other contract so uh basically this sensitive op code called delegate call is basically directly acting on the proxy contract so it can take over certain storage slots and it can even self-destruct the proxy contract so it's really important to only pass in a contract that has been vetted uh so code

that you actually trust uh yeah and without uh further Ado I think that's uh if best practices uh any questions just about actually let me just give you the microphone recorded yeah so thanks so much for the talk um just add one question the transparent versus the UPS so I remember seeing on like the open zap plane library and the docs and they had this like official recommendation to switch over to UPS so I'm kind of curious what might have

prompted it to be like so opinionated um is it just a gas overhead is it just you know one less contract to worry about um like because you don't need to proxy admin for UPS or if there's any other reason yeah those are good those are good reasons I mean the fact that you've got a slight gas overhead on every single call is a perfectly good reason to prefer it the other reason I've heard put us an advantage of eups is that you can shut down the um upgrade ability if you want to if you ever want to you can modify upgrade to a uups but you can sorry upgrade a uups contract to one that doesn't have upgradeability at all but with a transparent proxy because the upgrade ability is baked into the proxy itself you can never remove the

code you could try to make the admin unaccessible but you can't remove that code entirely and sort of lock it in Forever in a way that everybody can see is locked okay I think that makes sense thanks so much okay all right we have time for one other question sorry who was it I didn't quite catch the name of your company of Sigma Prime Sigma Prime okay thank you okay that was an easy one okay I think

we're no we got one okay uh thanks for the talk uh what are your thoughts about the diamond proxy standard I kind of see that a lot now so uh depending on these two proxies would you recommend going for that I think there are advantages but happy to hear your thoughts um yeah that's a good question uh without kind of uh kind of adding any negative negativity towards some certain particular Dev decisions I do see that as an extremely complex pattern and not necessary if you don't need it for the most part it really solves uh being able to split contracts across different like basically circumventing size limitations

and there's other ways we can do this so uh yeah I think unless absolutely necessary probably you could just stick to your UPS or transparent okay all right okay one last question thank you what's the use case where one would use both transparent and ups at the same time that would be a mistake okay an easy mistake to do though yeah yeah okay thank you very much okay thanks very much great let's thank the speakers Dimitri and Richard [Applause]