Alice in Proxyland
Devcon·Mon, Oct 9, 2023, 12:00 AM
Visit the https://archive.devcon.org/ to gain access to the entire library of Devcon talks with the ease of filtering, playlists, personalized suggestions, decentralized access on IPFS and more. https://archive.devcon.org/archive/watch/6/alice-in-proxyland/index The story of Alice in her journey of making her smart contract upgradeable. She starts with a very naive approach, and quickly bumps into the fundamental problems of proxies: using CALL vs DELEGATECALL, the execution context, storage collisions, the importance of tooling, etc. As her system grows in complexity, her proxy architecture iterates and becomes more and more sophisticated, finally arriving to a very powerful, elegant and easy to use architecture: The Router Proxy. Speaker(s): Alejandro Santander Skill level: Intermediate Track: Developer Infrastructure Keywords: proxies,upgradeability Follow us: https://twitter.com/efdevcon, https://twitter.com/ethereum Learn more about devcon: https://www.devcon.org/ Learn more about ethereum: https://ethereum.org/ Devcon is the Ethereum conference for developers, researchers, thinkers, and makers. Devcon 6 was held in Bogotá, Colombia on Oct 11 - 14, 2022. Devcon is organized and presented by the Ethereum Foundation, with the support of our sponsors. To find out more, please visit https://ethereum.foundation/
Transcript
foreign [Music] then I worked with Aragon for a bit and now I'm working with synthetics mainly addressing the problem of how do you build a super complex smart contract system in a way that you can iterate through it and fix bugs and and improve an experiment um and other than that I I consider myself a bit of a an educator in the space I just it's not that I know a lot of things I I just love to empower other people with with knowledge right so what we're gonna do today is talk about proxies uh and then like in general and then we're gonna talk about a pretty sophisticated uh type of proxy that we're using in synthetics so I think that it's critical for for everyone to understand how proxies work under the hood I think that it's not okay for a Dev to use proxies and not know how they work but the good news is it's that they're really easy like in the end there's like no mystery it's very easy to demystify it right so I I insist uh besides the proxy I'm going to show I think the the essence of this talk is to to promote the awareness of how proxies work and if you're going to use one like make sure you understand how they work so to illustrate this we're going to play them like we're going to start with a with a very simple contract and make it upgradable and see what happens so this is the contract I don't know if it's a good idea to bring in code to a talk but I don't know if you can see it but it's just a contract that sets a value right it has a function set value you can set the value and it records who said the value it's message sender and an image an event that's it so this car this contract is deployed at the 0x1 address right and then after deploying it Bob calls uh set value 42 right then 42 is set in storage slot zero because the variable is declared it's the first variable declared and uh zero X Bob which is the address of Bob uh gets stored in the second slot which is slot one okay that's how solidity lays out storage automatically when you declare variables in a contract and an event is emitted from 0x1 now they decide to make their contact upgradable all right how does this work they they decide to deploy a proxy which is just a function a contract that has a function you can tell it what the current implementation is which is going to be the other contract and then it has a like a magic assembly function which forwards anything using call to the implementation contract so this is deployed at 0x2 then Bob calls set implementation 0x1 and in the proxy storage it's not zero now holds 0x1 because the address implementation variable is declared in slot it's the first one declared so it's saved in slot zero then they're now connected this is the proxy and this is implementation and block calls value right it forwards the call to that uh implicit getter that solid generates and ever since return 32 which is expected right yeah now uh Bob called set value one three three seven let's see what happens uh it gets forward using call to the set value function in the implementation and it affects the storage of the implementation not of the proxy and its stores one three three seven in slot zero and 0x2 installed uh one that's weird right that's message sender so the the problem that we have here is that call makes the execution context to be the implementation of the proxy like the event is submitted from the from the implementation which is also not a good thing right because you don't want to have a protocol and and tell people like to be changing addresses every time you update the implementation right so the problem that we have with this particular proxy that uses call is that the execution context is here right and we don't want that so what is an execution context is when you run code basically what determines uh which storage uh space to use uh who message sender is and where emits come out from right um there's more to it but that's like pretty much it so how can we take the proxy the execution context to the proxy we just need to use delegate call so call wants the code that we're running in the current context and delegate call runs the code in the context of the caller right so here we have a second proxy right which is the only difference is that it uses delicate call right it's deployed at zero three zero X3 it's connected to the implementation the same one as before and now when Bob calls set value one three three seven it uses delay core so the execution context is this right we are using the storage space of the proxy which is good the event is coming from the proxy which is good uh now Bob calls value like the getter right the execution context is still that it's fine now this is gonna delegate call to whatever is stored in the implementation and the implementation is in slot zero and the value now holds one three three seven right so what are we delegating call to to some contract at one three three seven which there's probably nothing there right so we just we have a storage Collision right we overwrote the address of the implementation with a number right so we basically bricked this proxy right so daily call is awesome but it is dangerous because you have storage collisions so to solve this Bob goes to the next level and and these structures the proxy storage so what is this structuring it's basically choosing where to put to put to store something that's it it's not using solitaire's custom slots but just choosing where you put it um so for solidity first variable is it zero second at one Etc and you have infinite slots destruction is just picking a custom slot right so we have the third proxy here is called unstructured proxy and the difference is that we are not using solidities like regular storage slots but we are declaring where we store things at slot 1000 right and using that the the code looks a little bit weirder but that's it so we deploy this proxies he works for we connect it to the implementation oh this is important can you see the storage it's the implementation address is stored at the custom slot of 1000. so that's destructured right now so now we call set value it makes a delegate call the execution context is that we write the new value values but they don't like step over let's say the implementation address right and the event comes from from the proxy which is fine so we have a proxy that works right and now we can upgrade the implementation because we we know that it works so we have value holder V2 the only difference is that we added a new variable called dates right just added it on top uh and just whenever someone sets the value we also record record when that happened right so now we connect the implementation we call Value right a delegate calls and value is whatever is stored at slot one right according to solidity storage layout and at one we have coax pop so another problem um this is another type of storage Collision we shifted the the implementation storage uh and we have a collision between versions of the implementation right we have income incompatible storage layout so Paul understands that to to avoid this you in an implementation you you only append to the storage instead of like putting it anywhere right so keep uh moves the date variable to the end of the previous storage layout um and that's it that's pretty a pretty simple fix that's another rule of like using proxies always a pen to um and now this value is gonna get whatever is stored at slot zero which is one three three seven so it's fine so we we avoided that Collision so storage collisions it's it's critical to understand when they occur and it's basically the two types of collisions that I just showed you that's it if you get that you you can pretty much think about any type of coalition things to consider the execution context is always the proxy so everything is stored in the proxy um there's two type of collisions that we just talked about um the first kind can be avoided by unstructuring the storage layout and the the second one can be avoided by just making sure that the uh updates to the storage layout and implementations is valid always append and something to consider and this is critical multiple inheritance um flattens your contracts so you cannot protect the storage layout so you can add a new inherited contract to you to your like super contract and it can add like five new variables in an unpredicted uh part of the layout uh part of the layout so it becomes hard to detect when you have invalid storage mutations and you need to use tooling if you use proxies I see a lot of people using proxies and not using tooling and it's not a good idea and even if you detect them and this is not something that tooling fixes it can be very hard hard to avoid the invalid mutation like if you have a inherited contract and it it causes this Collision to avoid it you you have to do some weird stuff right so why not use that technique of unstructuring on everything not just the proxy storage but the the implementation storage definitions so here we have value holder before the implementation B4 and the difference is that it uses the same technique that the proxy use and stores everything at slot 5000 right everything else is the same so we deploy this we connect it to our proxy which we're not changing anymore because it works um we call set value right it makes it delegate call to the implementation and we can see that we have the gray storage we're not using anymore we have the proxy name storage name space let's say there and the new implementation namespace uh here and they don't Collide right so we've unstructured the implementation storage right now we have we're pretty happy with this proxy like configuration we the the context is kept at the proxy and collisions are avoided using unstructured storage or storage namespace absolutely everywhere tooling should still be used to guarantee that there's like no uh storage Collision that you don't notice but the thing is that uh this like custom or manual use of storage uh makes um storage layouts much easier to control so now that we understand these like basic principles of of proxies let's talk about like what a multi-contract system looks like in in solidity so there's no ideal standard solution for multi-contract systems people often use registry which is basically a contract that knows every other contract right and whenever a contract a needs to talk to contract B it needs to go to the registry and ask hey I want to talk to business B here's B and then it makes a call to contract B right and then B needs if it's a sensitive operation needs to say okay who's calling me a is a from the system it asks the registry the registry goes yes and then okay then you can perform this it's complicated and it gets it gets uh messy pretty fast so let's let's try a pretty crazy solution which is uh we're calling it the router proxy um so we basically have a new contract which is another contract that has one variable it's called cool value and it's also using these uh this storage name space system instead of solidities like own a storage layout thingy but that's it it just records a variable right gets the store sets the the stores value and then it's a an event so we deploy it at 0x8 and then this is the tricky part bear with me with this part uh Bob uses tooling to build a router right so this is basically a table right it has the addresses of the two contracts uh value holder and another contract hard-coded and it's a fallback function basically has to do like this binary search algorithm to determine which implementation has that function right is it valueholder or is it another contract right and it just checks the incoming selector and forwards it to the appropriate implementation and that's it and then it just makes the the regular delegate call uh proxy forwarding so this is deployed at 2x9 and then we set the router as the proxies implementation right 0x9 is the new implementation and now we have this we have the proxy over here we have the router over here and we have the implementations over here right so when Bob called said value seven you don't I don't think you guys see it but it's it's calling set value seven with the number seven it makes it really a call to the to the router and then another daily call to the to the another contact implementation because the router figures out that the the function set values in that contract let's keep in mind that the execution context is still the proxy it doesn't matter how many deleg calls you make it will always be the the the entry point so that works like it sets uh the the other contracts Uh custom slot what was 9 000 so it's store seven right there and the event is still emitted from the the execution context so if Bob wants to upgrade the system all he needs to do like here we're making a like a silly change right we're just multiplying the incoming value just by seven um this new another contract B2 is deployed at 2x10 a new router is generated by the tooling it just has the only difference is up there in in another contracts has a new address everything else is the same and Bob sets the implementation of the proxy to that new router so that's how you upgrade the any contract in your system so what would a more complex system look like maybe like this you have the main proxy right you have the different storage name spaces of that proxy and then you have the router which you keep changing every time you upgrade the system and you have the different modules that specify uh a particular behavior of your system and then you have this thing which is really cool cool because uh called mixense because it allows intermodular Communication in a way that we we're going to see that's really efficient and really easy um so yeah we don't use any storage other than the execution context storage gas efficiency uh it's like a concern with this pattern because you're used doing two delay calls um keep in mind that transparent proxy is the ones almost everyone uses uh costs like 3 000 gas Universal proxies about 1600 and this system uh uses only uh 2 600 gas which is all right and then intermodial intermodular Communications how would a module talk to another module you could you could cast your module as the other module and just call its function because every module is this the system but the problem with this is that message sender would be lost because it's it's a call you you break the delegate call chain right so you need to delay call to the other module just the the same like self-casting mechanism but with delay code uh it works but there's something much better which is mixins which are pieces of code that know how to interact with another module storage they're like delegates for that particular part of the logic right and they're not deployed they're just inherited it's like modular inherits a bit of the code of more UB and the nice thing is that you can tell the mix in to interact with the other module without even making a call so communication becomes super cheap let's let's see an example we have owner storage which just declares a struct with what single variable uh that mechanism to to get custom storage slots then we have the owner mixing which knows that storage and only has and only only owner modifier right that does the typical check right and then in owner module we inherit the mixing which gives us the only owner modifier access right and we have a getter for the owner and now we have a new version of value holder B5 right that has a single change which is we're using the only owner modifier here from another module using the owner mixing if you want to use this you only have to change your code style a little bit it's kind of weird but you get used to it fast because it's simple you just need to use storage name spaces instead instead of like regular variables and yeah you get used to it should Solitaire do this under the hood there's a proposal from maxim4 um so it is something that the solidity team is considering uh this could be a language supported feature to have like a contract hub and yeah why use the router you don't have a contract size limitations anymore because you can just combine like I think I I tested it once with like 800 functions which is pretty crazy um it's like the router merges all the contracts into a single contract so then as we just saw we have good easy Communications between the models without having to use a registry or authentication or anything it's ideal for complex experimental systems and the other nice thing is that the router is since the addresses are hard coded it's very explicit so it's good for for governance if you want to make an update to the system you show your community like this is what we're going to change this is what the configuration of the system will look like it's not hidden in some uh Dynamic storage somewhere it's right there uh and yeah that's why we're using it as a core component of synthetics B3 because it's it's a complex system that needs to not have all the complications of intermodular communications and all that and well if you want to try it out it's a heart health plugin it's uh synthetics B3 uh hard hat router it generates the router source it manages storage namespaces for you it performs validations to ensure that there's no there are no storage collisions and that's pretty much it that's what the plugin does so thank you very much thank you um are there any questions I just want to know if I'm missing something but this proxy router could be the same as the multi-facet proxy you know the diamond proxy but with the hard-codile uh implementations uh yes I I couldn't hear it perfectly but I think you are asking about the diamond proxy right yeah yeah yeah yeah this is this is based on the diamond proxy so is that that the diamond proxy but without getting the implementation from the storage yeah but instead hardcoding them yeah basically okay it's it's some people are calling the diamond proxy a dynamic router and this one is static router and we like this one because uh for our project because it it saves storage reads because the the values are hard-coded and it's also more explicit like that what we don't like about diamonds is that you don't like if you're a Community member or whatever and you want to know what's the current composition of the system you need to query it a lot right but yeah it's the same otherwise just one question so you only use the sort of ins and addresses and stuff what happens with mappings and arrays in regards to storage Collision so can you repeat a bit louder so if you have an array yeah does that affect storage Collision well if if you declare your array or any Dynamic type inside of those storage namespace drugs then solidity is like regular storage layouts um system is used which uh uses unstructured storage under the hood like if you have a dynamic array the position of that array I think it's going to be it's slot say if it's 9003 because it's the third variable instruct uh the hash of that so it's going to be some other random place so it probabilistically the even though they're in in struts so some things are going to be spread out but the the probability of a collision is uh very low like insignificant so you mentioned a couple times that we should use Tooling in order to check for storage collisions what sort of tooling do you recommend contract developers use sorry I I can't hear really really well from here I'm just going to stand there okay so in your talk you mentioned a couple times that you should be using Tooling in order to detect storage collisions what sort of tooling should developers use in order to do these checks all sort of tools okay um yeah so if you're using open settlements proxies you should use their tooling if you're using uh the router as you can see we just we didn't just offer like a solution to generate the code but we have we have a it checks your the storage layout of your entire project so in that case you can use our code right so I would say always use the tooling of whoever is providing you the code the smart contract code of the proxy were there any attempts to solve this garage Collision thing on the evm or compiler level like to sandbox each contact for example I don't think there's a need to um the way uh solidity like these structures arrays and mappings and all that it's it's theoretically impossible to to get a collision so there's no need to sandbox it the problem with collisions is when people use like a design that's not supported at a language or at a protocol layer like the evm right and they get collisions between two contracts right um so so I don't I'm not aware of any attempt at that level to avoid coalitions is it possible to migrate and open Zeppelin proxy to this one the router proxy or would we have to start from scratch code wise it's pretty much the same like you can use a new Universal proxy as your entry point so you could probably just use that and the migration is the the crazy thing about this is that the the router routing occurs at the implementation right so code wise you're okay then storage wise right you just need to probably choose new namespaces and populate the data right or accept that your modules are going to use existing search right and make sure that new modules declare like a new namespace or something but yeah sure you can do it even if so if if solidity like makes this a language feature you just stop using generate routers and deploy a solution Hub right so it's completely future proof I think how standardized it is is proxy are you are you the only ones using it or is someone else using it in production doesn't mean first question and the second question is uh do you think it will be useful to have something in the uh like a public function in the in the proxy to share the signatures that are being used so let's say a user or someone that wants to check what is being used doesn't have to dive into the source code yeah so standards not many right now if you deploy this you won't see anything on etherscan for example etherscan doesn't know how to interpret a proxy that has multiple implementations which is unfortunate but we're trying to solve that pretty fast it shouldn't be hard um and your second question um you would just add a module that adopts a ERC 165 is it that uh just like replies it it has that function I don't remember the name of the function that gives you the entire interface of the whole system okay that's it thank you very much
Automatic transcript — names and jargon may be misspelled.