Understanding Clones With Immutable Args
ETHCluj Meetup·Wed, Oct 9, 2024, 12:00 AM
This presentation is focused around the design pattern for clones with immutable args for creating efficient smart contracts or loans. Ionut Gingu elaborates on the use of proxies to make contracts upgradeable, the importance of immutable variables and the process of deploying a contract with immutable parameters. The discussion also covers potential issues and mitigation strategies, as well as optimal storage of the total supply in an ERC20 clone.
Transcript
hi everyone and thank you for joining this is our 13th Meetup and uh we have y with us uh he prepared a presentation about how to create more gas cost efficient smart contracts uh or smart contract clones that is clones that are uh more efficient to create and to call okay so um your notes whenever you are ready I'll stop sharing my screen and uh you can share yours uh sure thank you Alex I'll go right away just one second uh yes can you see it yep yes okay cool uh yes so I'll be talking about this uh kind of design pattern called uh clones with immutable arcs um I have this remix open with the code here is the repository that you can look at the at the code so it's this person who is very active in the community they developed this clones with immutable arcs library and inside you can see uh basically the boiler dat files and then just a sec yes you can see uh the Clone with mutable arcs Library which is their library then a clone file which are supposed to inherit and then an example clone an example clone uh Factory so I'm going to just describe what these are probably start directly from the proxy and what it is and why you would need these clones with immutable ARs and then build up from there so uh in the proxy pattern basically one caveat that you have is you want to have your contract upgradeable so you have an implementation and then you have your proxy contract which points to the implementation and then it uses this uh evm op code called uh delegate call uh what that does is it runs the implementation code in the context of the proxy so for example uh yeah that that's basically what it does so whenever you interact with the implementation in order to interact with the storage you need to go through the proxy all the time but if you go directly to the implementation you're not going to have the same storage which you have worked with previously so now one caveat to that is uh you cannot use Constructors with uh proxies and implementations and I'm going to just write a simple example here so if you have like a contract proxy like this and it has an uh an implementation and then you have like your fullback function which delegates to this code so you have something like this uh delegate call to implementation and now you have your implementation contract which has a Constructor and a u in to 65 uh variable called V so in the Constructor you might want to uh you have a parameter and you assign it at construction time so if you use the proxy pattern and you plan on using the proxy patter with this implementation contract The Constructor will not work uh the reason it will not work is because the Constructor sets uh a variable inside the storage of the implementation but when you work through a proxy you want to storage this value inside the proxy's implementation so that's why you would use something called an initializer which is you would have a separate function uh let's just call it initialize n Li yes I'm not going to put like all the solidity syntax uh and in here you would have also U into 56 V and you would set V to V the reason why this is different is because you can call the initialized function through the proxy so you would call the initialized function through proxy do fallback and in this way you would actually work with the with the proxy uh storage does that make sense so far um yes I have a question um for the implementation smart contract um I think um I I think it's Tor on the implementation because the Constructor is um in the bites of the code when you deployed is it right so when when you deploy the implementation you deployed The Constructor that then it's um um deleted from the bite code if I'm right uh yes it's it's deleted afterwards and only the runtime code remains but everything that happens in the deployment is inside the implementation storage exactly and that that's why actually you can use a Constructor right uh yeah the The Constructor is kind of a special function which only runs a deployment so when you deploy the implementation the Constructor will run it will update the implementation's storage and then it will get uh thrown out it will no longer be on the blockchain but even if it was I don't think it would be callable through the proxy okay thank you um okay yeah so if it's clear so far this is why the Constructor would not work now comes uh a question could I do something like this like imagine if V was a a constant or an immutable variable and I'm again working in this proxy implementation pattern and let's say V doesn't need to be configurable I just know what I want the value to be and I would have my implementation be something like this you want 256 constant P equals 5 would this work so what would I see if I'm calling a proxy do uh let's say v through the f back would it be zero or would it be five in this case oh it probably it would be zero because the constant is on implementation B code the I think the storage and the pro uh the implementation and the proxy don't have the same storage order yeah and it's over uh yeah let's um sorry my bad with that let's just say uh here you have an implementation as well just so that they don't overlap uh so we just discard this to not have to mess with uh storage uh layout just saying that I'm setting this V at the time of construction to a constant and uh can I see this from the proxy storage like what what would this function call return I think Z that's that would be the intuitive answer but it would actually return five and this is because it's like a gas optimization that people do sometimes and the way this works is when you have constant or immutable uh values uh they get save to the in the bite code of the contract uh yes they they get initialized and saved in the contract by code not inside the storage so in this case v equals 5 is kind of like an abstraction of solidity but underne in the bite code you would have this hardcoded value somewhere so through the proxy because you are working with the code of the implementation you are actually able to see that the V is five so in this case this would work and you would correctly see that it's five and now the question comes is is everything clear so far yes okay perfect so now the the question comes how can I put uh note that this also works with immutable so even if I had something like this and then I'd have a Constructor and I'd say V equals 5 a proxy do V would still return five just because the immutable value also gets stored inside the B code so now comes the question how can I make it so that I can use this constant or immutable variable from the proxy but this uh immutable variable is uh parameterizable so basically let's assume the implementation is actually like an erc20 or something like that and I just have a and this is like a clone contract so this is a proxy that is going to point to the erc20 and I can just uh deploy multiple of these clones let's say each one of us wants to have an rc20 so I cannot set like a a specific value here I need to somehow let the user pick the name of their erc20 like this so now the question becomes pretty complicated like how can I deploy this clone and not have to mess with it a lot but also be able to set immutable uh storage or immutable variables and save on gas that way um inside and and this these immutable variables would be uh kind of like parameters like I could CH choose one name Alex could choose another name the the reason why this is a bit complicated is because I can't use an ini izer function so if I call an initializer function on my proxy I will set the variables to whatever I want but it's impossible that they would be constant or immutable because constant and immutable only works at deployment time so this is kind of like the Clones with immutable this is what the Clones with immutable arguments pattern uh solves it allows you to deploy a clone that points to an implementation and when you deploy that clone you can specify some arguments specific to that implementation which are going to be mutable or constant and it does that using some assembly magic basically uh is that clear so far yes yes yes okay but perfect um let me just look through my yes and now let's take a look through the code a bit to see how it does that so the the main idea is this I'm just going to have some other small note here you have your clone Factory which creates clones this is clone one and then this is clone two and then this is clone three and they all point to an erc20 implementation which is the the same erc20 implementation but inside clone one you have uh name one inside clone two you have name two and inside clone three you have name three and these are immutable and you save gas this way so how does the Clone with immutable arguments work uh in at a very at a very high level what it does is that the Clone Factory deploys a clone which in its bite code has the immutable parameters saved at the end so just as you would have for example for a smart contract uh the bite code when you deploy the layout is a creation code plus a runtime code plus some I guess Constructor arguments yeah and this is it so basically when the Clone Factory is going to deploy something it's going to deploy creation code plus runtime code plus immutable arguments and the the Clones are written in such a way that whenever you go through the fallback it not only delegate calls to the implementation with the message do data that you intended the call data but it also so loads its own code into memory gets the last part of the code which is the immutable arguments and puts them at the end so the Clone actually forwards uh execution to the implementation and passes the immutable arguments as call data so inside the implementation you're going to see them as call data inside the proxy you took it from the bite code which is just like you would X is like a constant or immutable variable it's actually like an immutable variable at the core does that make sense so far yep okay uh so now we can just look a bit through the code so here is the Clone this is from the uh GitHub library and it has like this specific parameters so in our case the Clone would be um would be what the erc20 would inherit so it would be the implementation basically it has kind of a confusing name but uh our our erc20 would be is clone so this is not the proxy this is the implementation actually and it has all these functions to be able to load uh arguments from the end of the call data let me just um yes so all these all these functions get a u in8 from arguments get u in 64 get a u in 256 array and get un 256 and address use this get immutable arcs offset function so what does the immutable arcs offset function do it actually have some notes on it on this uh U stuff so it gets the call data size it subtracts two from it because the last two bytes are reserved to uh store the length of the immutable arguments in in all this Clon with immutable arguments pattern so then it shifts it and basically what it does is it goes to the end of the call data from the last two by it figures out how big the immutable arguments are and then it jumps back by that length so it arrives right at the end of the message do data that was supposed to be and right at the beginning of the immutable arguments um I also had yes okay so now this function if you imagine the call data call data is going to be something like this uh function selector plus function arguments plus immutable arguments plus 2 by length of immutable arguments and the get immutable ARX offset basically gives us this point so from here to the right and you can see you can specify an offset to this uh function so basically what you would say let's say in your immutable arguments you know that the first one is supposed to be an address so you call get AR address with offset zero it's the relative offset zero added to the absolute offset of starting from immutable arguments now if you know your second argument is u in 256 you would call get AR u in 256 with argument offset 20 because you know the first thing thing in the immutable argument is an address that is 20 byes you need to jump to the right of it and this is what is shown in the example clone as well so um this is taken from the from the repository and here it just shows like how you would use such a contract this contract has four parameters which are supposed to be immutable but it's hard to do in this proxy pattern so uh first one is an address you would get from uh offset zero second one is a u in 256 you add 0 plus address which is 20 and you'll get the 256 from offset 20 third one is uh u in 64 so you add address plus zero or you just 20 plus u into 56 which is 30 byes so you starts from 32 bytes 52 bytes sorry and this is 8 bytes so you need to go from the 60 bite does that make sense so far yes yeah it does okay cool uh and now I just wanted to give a little bit of detail on how the deployment actually happens uh so you also have these uh two classes which is clones with immutable ARS which is part of the library and then an example of a clone Factory and you can see how to deploy such uh clone with immutable arcs so basically you know for your contract that you're going to have four immutable parameters you encode pack them together and then to the library you specify I want to deploy um this implementation like a proxy that points to this implementation and with these um immutable arguments and then the pr with mutable ARX library is going to take care of the rest and this just exemplifies you have many options to deploy which is create create two and then add all the types of creates basically deterministic and all that uh solidity stuff and now in the Clone with immutable arcs it's a lot of assembly stuff um but maybe some more important parts are for example the Clone function if you look at what it does it takes the implementation that is supposed to uh point to it takes the data that we said and it takes a message. value it's a parameter to the create function in assembly and then it does this get creation bite code uh function so this is the bite code of the proxy or of the Clone that is going to be deployed and uh yes let's just take a look inside it because it has uh a couple of interesting parts so this function is going to return to us the bite code that is going to be deployed as a clone and if you look through it there's several important parts um let's see okay so as you can see uh data is the immutable arguments and the extra length is two it also tells us uh two bytes for telling how much data there is to be appended to the call so the length of the immutable arguments basically and then it does some computation how big should be the creation size how big is the Run size this is like just some hardcoded numbers because the creator of the library just wrote down this very optimized way of deploying a clone like this so he knows EX exactly that the creation size is going to be 10 bytes and these are the 10 bytes that is going to be and then the runtime size is going to be 55 bytes plus the immutable arguments and this is the 55 bytes that it's it's going to be so maybe just some uh implementation detail here uh just to see a bit How It Was Written uh I thought it would be interesting to present where the delegate call happens and how the immutable arguments are loaded from the Clones bite code in put inside the call data because that's kind of like the most interesting part uh yes okay and this is actually the Chun that is responsible to it so all this assembly here what it does this is the important part the code copy so what the code copy does when you have this is the stack you have the uh I think this is the Cod data size 0x 37 this is a hardcoded value because of how the all the bytes are set up and then these are like the length of your extra immutable arguments so what it does is it puts in memory from uh the beginning uh yes so inside the bite code inside the bite code of the proxy the immutable arguments with will start at um the 37th bite in hexad decimal which 37 is like 55 if you remember correctly the runtime had 55 bytes so exactly after the runtime code is going to be the immutable arguments as we said so that's one what's going to be copied to memory so this chunk goes to the 55th bite in the uh bite code of the Clone and gets all the bites of the immutable arguments and puts it inside memory so after this is done some other steps happen and then at some point it delegate calls and uh the argument to delegate call is not only the call data but also the arguments that are loaded from memory and this is how this uh happens under the hood now this is actually this method is actually one of two or three methods that uh can be there is another article and I can share this uh with the group after the presentation about uh some more less optimal ways to implement clone with immutable arcs and these these two are some very good uh examples so here this is an article about how uh people started thinking about it going from the list optimal approach to the most optimal approach and then this page does a comparison in um gas so it actually compares the method that I presented which is aend to call data and another method which is the implementation loads the code of the proxy using X code copy and uh gets the immutable parameters from there so that's in in practice that's actually sometimes more efficient depending on how many IM mutable arguments you have uh is everything clear so far yes I do have a question it's um I'm I'm not sure I'm understanding uh where is happening the gas optimization so for example if we have rc20 that have a function that um called the total Supply like um the optimization is um how how it happens in simple terms so we have when we create the Clone uh it pass to through the bite code um it pass the immutable Arc that it will be wrote on the Clone right uh yes so so the optimization is if you have like an erc20 with a total Supply which you can make constant you could just do the first thing that we did which was total Supply uh equals 10 let's say and then you have un 256 total Supply uh constant or sorry immutable so this would be like the most optimal gas way to store the total Supply uh where clones with immutable arguments comes in is because of the fact that you can't do this in a way in which you can configure the total Supply so that if you want to deploy an erc20 uh a clone of an erc20 where total Supply is not 10 you would have to change the implementation code CU there's no way to give this as a as a parameter okay so that's why you have to send it call data uh yes yes the the with mutable arguments does exactly kind of what happens here but makes the makes everything to be to be able to specify as a parameter okay and after the deployment um is it optimized in any way or it's just like normal clones after the deployment uh yes after the deployment is just like normal clones you interact with the proxy just as you interacted before it's just that the arguments that would have been immutable or constant inside the implementation are actually in the proxy bite code and are loaded automatically when you delegate call so if you had this assembly written in solidity it would be a function which is more complicated than your usual fallback function of a proxy just because it looks at the B code and it loads the last parts and it adds them to the call data so it does this um these things okay so this it doesn't increase the gas in uh it it it does but probably by a very uh small amount because code copy is not very expensive and then you need to just append these to the call data at the end uh it shouldn't increase the gas more than just having like a constant variable because just normal constant variables those are all stored inside the contracts code so when you access a constant variable it's probably loading the the code somehow as well and needs knows where the offset is to to get that value from okay thank you um okay cool thank you for the the questions uh I wanted to present one more thing which is just uh a vulnerability that can come with this when you use this pattern and the vulnerability goes something like this I'm going to use another example file uh the vulnerability kind of is in the sense that the arguments are immutable only if you call them through the proxy so the proxy has like some hardcoded values inside the bite code which is going to forward to the implementation but if you call the implementation directly it's still going to load the parameters from the end of the call data but you can specify any parameters you want you're not going through the proxy anymore so what can happen is you have your clone and then you have your implementation and uh I'm just going to write us simple implementation contract and this is going to have uh just a function X like this and it's going to load just as we seen before uh get Arc address of zero so we have this we have like this uh implementation where the immutable variable is just one address so I get the immutable variable yes so when I'm calling this through the proxy this is always going to be the same thing because it's taken from the proxy uh bite code and loaded sent in the call data through the delegate call but if I'm calling the implementation directly I can specify here anything I want because the get AR address looks at the call data at the end and gets the last uh 20 bytes or or yeah the last 20 bytes more or less so if my implementation in by some reason does a further delegate call let's say I have this pattern where I'm cloning an implementation which is actually like a bon proxy uh and now I would do something like x. delegate call uh blah blah blah this is very dangerous because a malicious person would call the implementation directly and put the address of a malicious contract at the end the implementation will delegate call to that malicious contract and that malicious contract would self-destruct therefore self-destructing the implementation because of the delegate call we are inside the context of the implementation so it will destroy all the proxies that point to it does that make sense yes so and it it can also affect the logic of whatever the implementation probably need to Kin go ahead yeah sorry I have a bad Network so probably you have to write a require right to go through the proxy each time inside of the function I'm not sure I I haven't thought of the of a mitigation but maybe just like the uh safest thing would be don't delegate call to anything that is specified by anyone other than the proxy so maybe maybe you could do in it in a way that the implementation figures out if it's called by the proxy or not and require that if not you could just think of another design where you're not delegate calling to something from the outside and you actually store this x value in in another way and that's probably how you would be safe first but don't quote me on that sorry my internet is very bad I think if you um solve the problem with who can call the functions then you will solve the problem with what you can do in the functions right so if you are going through the proxy it's okay to have the delegate call inside of implementation right uh I think so yes I haven't thought very deeply about it but the first glance maybe the require would work yeah MH I've uh we I've deployed smart contracts with this type of require inside of each functions and as I saw this is the only solutions only solution that um people are using right now to mitigate this problem yeah yeah I think I've seen it somewhere as well I just didn't think very much about the mitigation but uh yeah I think I've seen this as well in in some source code okay okay and I have one more question if it's okay um if I'm um using the IM mutable arcs on the proxy like like on the storage would be a diff uh would be the same gas cost for immutable for this type of clones with immutable arcs that are immutable ARS on the clones and the that um proxy contract that have immutable ARS directly on it I see so you mean in your clone you would here specify the immutable Arc one all imut okay the the gas cost is probably going to be the same maybe even slightly lower but this approach comes with several disadvantages which are uh in the Clon with utable arguments you don't need to alter the Clone at all regardless of what you deploy that's one of the one of the uh advantages because in the Clone Factory here is where you actually specify which immutable parameters you are encoding and it's like a very simple interface to it the chrone code remains exactly the same just the way you call the create Chrome function is different so uh the way you mention would be uh it might be actually a bit guess cheaper but it would come with the course that it would be harder to uh change between other implementations depending on which arguments you're going to need you're going to have to alter the proxy code okay um and I'm thinking on the uh the diamond prox is working with that type of structure if you if you know about the diamond storage I I think um actually at that point no in in the in the diamond um storage you can you can't use immutable arcs so no it's it's not a good approach what I'm saying uh oh by by diamond storage you mean the name space uh storage where you uh put uh values at certain random locations in in stage in a struct in a struct you use a struct for stage yeah I I haven't thought about it but um I'm not sure if that would work in this case but also that would be like a very complicated architecture like the diamond proxy is already very complicated I think if you add this on top it's just going to be very hard to debug maybe it's better to just leave it non optimal yeah definitely just I Tred to challenge you and all the guys from here to to think of the most optimized version just as concept no one want to get there down at assembly Point yeah if you want and are curious as well you can go through the um clone with mutable ARX uh Library it just uh it has here all the bite code that the uh proxy has and this is actually pretty good insight into the minimal proxy pattern as well because there you also have like the simplest smallest code possible retain assembly to have like a working proxy and it's pretty interesting how it works but I try to present the most important part which is how the delegate call happens and how the data is taken from the B code and added to the call data thank you for info yeah thank you as well for listening and asking all the questions uh I don't have anything else for the presentation so if maybe there are other questions or if not this was everything uh I have a question regarding um maybe you know what's like the most often uh looked up use case for uh the CL with IM mutable [Music] arguments um yeah I think probably the I'm I don't know for sure but people usually use clones regarding uh vaults or nois saves or things like this so where you have like some kind of Vault or multi architecture that you want to deploy and there's one implementation and you clone it so if you wanted to make that a bit cheaper I think it would like the Clones with immutable ARX would pretty much apply to anything that the Clones apply to the only caveat is because of the way these immutable arguments are uh transferred in the call data you couldn't just take like a multisig and deploy it with the Clones with immutable arcs you'd have to change the implementation to fit this pattern just like we saw in the uh example clone you would have to implement these functions where the immutable parameters are taken from the call data but otherwise you should be able to use it just with the most uh used uh use case in the Chrome's world as well right understood thank you great I do have one more question so yeah if you um can you do a com between clone with imut immutable and minimal proxy comic info um it's not working can you hear me uh can you barely I I didn't understand the second part could you repeat the whole question sorry no better internet so uh I was and the ones that are the simple clones with the minimal of hold some storage on the pro see and also what can be mutable directly oh direct sorry K can you if you hear me can you write the question into the chat please it would be you're breaking up and uh okay if you can write it in the chat please thanks uh okay so the question is if you can use a combination between close with immutable arcs and normal CLS um you you probably could but I don't really see what the use case would be because in the Clones with immutable ARS you have the whole uh clone functionality already so if you use clones with mutable ARS for one of your implementation contracts it would make sense you use it for all your all your clones basically and there's no need to mix them oh I see I see what you mean so you mean could you use CL with immutable args pattern and also on the implementation store some constant variable which is common for all the Clones is that the question yes yes okay um I'm not uh let me think about it yeah probably you should be able to do that because the immutable arguments inside the proxy would be stored at the end of the proxy bite codee and then the constant variables inside the implementation would be stored at the end of the implementation bite code so if you had immutable arguments that could be different between each uh proxy those would be in the on S immutable arguments part and then if you had some argument which would be common for all the proxies and constant that would sit as a constant or immutable inside the implementation this is just my first thought but probably test it uh in Forge or something for just to make sure okay this this was what I'm thinking to so I wanted to validate my idea thank you cool uh great question yeah okay so if there are no more other questions or are there any questions okay then y not thanks a lot for um the presentation uh you explained very very clearly and thanks a lot for answering all the questions and the different hypothetical situations uh it was uh interesting to follow every everyone thanks for joining and thanks for um asking questions and see you at the uh our next meet up yeah thank you as well and thanks for having me thank you
Automatic transcript — names and jargon may be misspelled.