 a Solidity Auditor in open sepuline, 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 improve an experiment. And other than that, I consider myself a bit of an educator in the space. I just, it's not that I know a lot of things, I just love to empower other people with knowledge, right? So, what we're gonna do today is talk about proxies, and that like in general, and then we're gonna talk about a pretty sophisticated type of proxy that we're using in synthetics. So, I think that it's critical 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 that they're really easy, like in the end there's like no mystery. It's very easy to demystify it, right? So, I insist, besides the proxy I'm gonna show, I think the essence of this talk is to promote the awareness of how proxies work, and if you're gonna use one, like make sure that you understand how they work. So, to illustrate this, we're gonna play them, like we're gonna start 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 set the value, it measures sender and it emits an event. That's it. So, this contract is deployed at the 0x1 address, right? And then after deploying it, bot calls set value 42, right? Then 42 is set in storage slot zero because the variable is declared, it's the first variable declared, and 0xbob, which is the address of Bob, 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. An event is emitted from 0x1. Now, they decide to make their contract upgradable, all right, how does this work? They decide to deploy a proxy, which is just a contract that has a function, you can tell it what the current implementation is, which is gonna be the other contract, and then it has a magic assembly function which forwards anything using call to the implementation contract. So, this is deployed at 0x2, then bot calls set implementation 0x1, and in the proxy storage slot zero now holds 0x1 because the address implementation variable is declared in slot. It's the first variable declared, so it's saved in slot zero. Then, they're now connected, this is the proxy, and this is implementation, and bot calls value, right? It forwards the call to that implicit getter that Solidity generates, and it returns return number two, which is expected, right? Now, bot calls set value 1-3-3-7, let's see what happens. It gets forwarded using call to the set value function in the implementation, and it affects the storage of the implementation, not of the proxy, and it stores 1-3-3-7 in slot zero, and 0x2 in slot one, that's weird, right? That's message sender. So, the problem that we have here is that call makes the execution context to be the implementation, not the proxy, like the event is emitted from the implementation, which is also not a good thing, right? Because you don't wanna have a protocol and tell people 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 which storage space to use, who message sender is, and where it's come out from, right? There's more to it, but that's 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 runs 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 delegate call, right? It's deployed at zero three, zero x three. It's connected to the implementation, the same one as before, and now when Bob calls set value one three three seven, it uses delegate call, 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. 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 delegate call is awesome, but it is dangerous because you have storage collisions. So to solve this, Bob goes to the next level, and destructures the proxy storage. So what is destructuring? It's basically choosing where to put, to put to store something, that's it. It's not using Solidity's custom slots, but just choosing where you put it. So for Solidity, first variable is at zero, second at one, et cetera, and you have infinite slots. Destructuring is just picking a custom slot, right? So we have the third proxy here, it's called unstructured proxy, and the difference is that we are not using Solidity's like regular storage slots, but we are declaring where we store things, that's not 1,000, right? And using that, the code looks a little bit weirder, but that's it. So we deploy this proxy, 0x4, 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 1,000. 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 values, but they don't like step over, let's say the implementation address, right? And the event comes from the proxy, which is fine. So we have a proxy that works, right? And now we can upgrade the implementation because we know that it works. So we have value holder v2. The only difference is that we added a new variable called date, right? Just added it on top. And just whenever someone sets the value, we also record when that happened, right? So now we connect the implementation, we call value, right? Delegate calls. And value is whatever is stored at slot one, right? According to Soliti storage layout. And at one, we have surxpub, so another problem. This is another type of storage collision. We shifted the implementation storage and we have a collision between versions of the implementation, right? We have incompatible storage layout. So while it understands that to avoid this, in an implementation, you only append to the storage instead of like putting it anywhere, right? So he moves the date variable to the end of the previous storage layout. And that's it, that's pretty simple fix. That's another rule of like using proxies, always append to. And now this value is gonna get whatever stored at slot zero, which is one, three, three, seven, so it's fine. So we avoided that collision. So storage collisions, 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 can pretty much think about any type of collision. Things to consider. The execution context is always the proxy, so everything is stored in the proxy. There's two types of collisions that we just talked about. The first kind can be avoided by unstructuring, the storage layout, and the second one can be avoided by just making sure that the updates to the storage layout and implementations is valid. Always append, and something to consider, and this is critical. Multiple inheritance flattens your contracts, so you cannot predict the storage layout. So you can add a new inherited contract to your super contract, and it can add like five new variables in an unpredicted 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 to avoid the invalid mutation. Like if you have an inherited contract and it causes this collision, to avoid it 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 implementation storage definitions. So here we have value holder before, the implementation before, and the difference is that it uses the same technique that the proxy use and stores everything at 5,000, 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. We call set value, right? It makes it delegate call to the implementation, and we can see that we have the great storage we're not using anymore. We have the proxy storage namespace, let's say, there, and the new implementation namespace here, and they don't collide, right? So we've unstructured the implementation storage right now. We're pretty happy with this proxy configuration. 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 no storage collision that you don't notice, but the thing is that this custom or manual use of storage makes storage layouts much easier to control. So now that we understand these basic principles of proxies, let's talk about what a multi-contract system looks like in Solidity. So there's no ideal standard solution for multi-contract systems. People often use registries, which is basically a contract that knows every other contract, right? And whenever contract A needs to talk to contract B, it needs to go to the registry and ask, hey, I want to talk to B, who's B, here's B, and then it makes a call to contract B, right? And then B, 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 messy pretty fast. So let's try a pretty crazy solution, which is we're calling it the router proxy. 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 this storage namespace system instead of Solidity's like own storage layout thingy. But that's it, it just records a variable, right? Gets the store, sets the store's value, and then it's an event. So we deploy it at 0x8, and then, this is the tricky part, bear with me with this part. Bob uses tooling to build a router, right? So this is basically a table, right? It has the addresses of the two contracts, value holder and another contract, hard-coded, and it's fallback function basically has to do like this binary search algorithm to determine which implementation has that function, right? Is it value holder 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 regular delegate called proxy forwarding. So this is deployed at 0x9, and then we set the router as the proxy's 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 calls set value seven, you don't, I don't think you guys see it, but it's calling set value seven with the number seven. It makes a delegate call to the router, and then another delegate call to the another contract implementation because the router figures out that the function set value is in that contract. Let's keep in mind that the execution context is still the proxy. It doesn't matter how many delegates calls you make, it will always be the entry point. So that works. It sets the other contract's custom slot was 9000, so it stores seven right there, and the event is still limited from the execution context. So if Bob wants to upgrade the system, all he needs to do, like here we're making it like a silly change, right? We're just multiplying the incoming value just by seven. This new, another contract B2 is deployed at 0x10, and new router is generated by the tooling. It just has, the only difference is up there in another contract 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 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 namespaces 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 a particular behavior of your system. And then you have this thing, which is really cool, cool because, called mixed-ins, because it allows intermodular communication in a way that we're gonna see that's really efficient and really easy. So yeah, we don't use any storage other than the execution context storage. Gas efficiency, it's like a concern with this pattern because you're doing two delayed calls. Keep in mind that transparent proxies, the ones almost everyone uses, cost like 3,000 gas. Universal proxies about 1,600, and this system uses only 2,600 gas, which is all right. And then intermodular communications, how would a module talk to another module? You could cast your module as the other module and just call its function because every module is the system. But the problem with this is that message sender would be lost because it's a call. You break the delegate call chain, right? So you need to delay call to the other module, just the same like self-casting mechanism, but with delay call. It works, but there's something much better which is mixed-ins, 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 module A inherits a bit of the code of module B. 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 see an example. We have owner storage, which just declares a struct with a single variable. That mechanism to get custom storage slots. Then we have the owner mix-in, which knows that storage and only has an only owner modifier, right? That does the typical check, right? And then in owner module, we inherit the mix-in, 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 mix-in. 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 namespaces instead of regular variables. And yeah, you get used to it. Should Solidity do this under the hood? There's a proposal from MacSan4. So it is something that the Solidity team is considering. This could be a language-supported feature to have a contract hub. And yeah, while I use the router, you don't have contract size limitations anymore because you can just combine, like I think I tested it once with 800 functions, which is pretty crazy. 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 governance. If you wanna make an update to the system, you show your community, like, this is what we're gonna change. This is what the configuration of the system will look like. It's not hidden in some dynamic storage somewhere. It's right there. And yeah, that's why we're using it as a core component of Synthetix B3 because it's a complex system that needs to not have all the complications of intermodular communications and all that. And well, if you wanna try it out, it's a hard-hug plugin. It's Synthetix B3 hard-hug router. It generates the router source. It manages storage namespaces for you. It performs validations to ensure that there are no storage collisions. And that's pretty much it. That's what the plugin does. So thank you very much. Thank you. 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-faucet proxy, you know, the diamond proxy, but with hard-coded implementations. Yes, I couldn't hear perfectly, but I think you are asking about the diamond proxy, right? Yeah, yeah. Yeah, yeah. This is based on the diamond proxy. So it's the diamond proxy, but without getting the implementation from the storage, but instead hard-coding them, basically? Some people are calling the diamond proxy a dynamic router, and this one a static router. And we like this one for our project because it saves storage reads because the values are hard-coded and it's also more explicit. 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 used sort of ints and addresses and stuff. What happens with mappings and arrays in regards to storage collision? Sorry, can you repeat a bit louder? If you have an array, does that affect storage collision? Well, if you declare your array or any dynamic type inside of those storage namespace drugs, then Solidity's regular storage layout system is used, which uses unstructured storage under the hood. Like if you have a dynamic array, the position of that array, I think it's gonna be, it's lots, say if it's 9003 because it's the third variable in your struct, the hash of that, so it's gonna be some other random place. So probabilistically, even though they're in structs, some things are gonna be spread out, but the probability of a collision is very low. Insignificant. So you mentioned a couple of 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 can't hear really, really well from here. I'm just gonna stand there. Okay, so in your talk, you mentioned a couple of 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? Oh, sort of tools. Okay. Yeah, so if you're using open semblance proxies, you should use their tooling. If you're using the router, as you can see, we didn't just offer a solution to generate the code, but it checks the storage layout of your entire project. So in that case, you could 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 the storage collision thing on the EVM or compiler level? Like to sandbox each contact, for example? I don't think there's a need to. The way Solidity destructures arrays and mappings and all that, it's theoretically impossible 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? So I'm not aware of any attempt at that level to avoid collisions. Is it possible to migrate an open zippling 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 crazy thing about this is that the routing occurs in 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 storage, right? And make sure that new modules are declared like a new namespace or something. But yeah, sure, you can do it. Even if Solidity makes this a language feature, you just stop using generated routers and deploy a Solidity hub, right? So it's completely future-proof, I think. Ale, how standardized is this proxy? 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, do you think it will be useful to have something like a public function 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? Yep, 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. And your second question, you would just add a module that adopts ERC165, is it? That just replies, 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.