 Buena Stardas, everyone. My name is Igor Jovoi. I'm a saluted developer, a CEO founder, and previously I worked at Open Zeppelin and co-founded a DeFi startup called Baby on Finance. Today I'm excited to present you a workshop titled Unlimited Size Contracts. Before we jump into action, you might want to download a GitHub repo for this workshop. Either use the QR code or just Google GitHub while we dash IO workshop and you'll get it. We also need a foundry for this workshop. If you have it, great. If you don't have it, just install it using the link in the slide. And then run two commands, foundry up and forge build. I know internet is pretty bad here and so for some reason you can't download it and install it. Don't worry, the repository will stay up and you have access to it and I'll post the slides tomorrow and I'll be doing some live demos so you can just watch and get the idea what's going on. Let's jump right into it. All right, who has ever deployed here and written a smart contract? Please raise your hand. Yes, the developers. Okay, who has ever run into the issue of a contract size limit here? Please raise your hand. Wow, it's way more problem than I thought. So yeah, this picture illustrates my personal pain, and why I work in Baby on Finance. We pretty quickly ran at the issue of a contract size limit and it's not easy issue to address and it caused us a lot of pain and one fellow developer I know in the Solidity Space called this limitation. I quote, bane of my existence. So this is how big this problem is and we are going to tackle it today. So there is a few solutions for this. Well, you can reduce byte code size using various techniques and essentially it's like alternative sport to guess golfing. I call it byte code golfing. I certainly do not recommend it because it's like a diminishing return sport with each optimization you have like less and less space left for each new function and it even made compromise the security of your project. So I would be very of using this technique and I suggest to try to design your architecture from day zero in the way it supports your contracts to be unlimited size. Other solutions are external libraries, static pre-compiled router and a dynamic router. We'll cover them in this workshop and hopefully have some time for questions and discussions. All right, we're gonna use a simple contract as a baseline contract for this workshop. It's called a counter. It has a few functions, a set function which sets a int variable to a new value, a get functions which retrieves this variable and finally a const functions which is just a constant. These are good functions for a baseline contract because they essentially utilize one S load operation, one S store operation and returning the constant value. And these examples are quite well-spread and real smart contracts. All right, let's jump into the code. Here we have our counter contract. Again, I'm not gonna spend any time on this. It's a pretty basic. What is important about it, it's rather small. So if we look at the byte code size, you can see it's under one Q byte, which is great and we have a lot of space left. But let's say a product team says, oh, actually we need to implement this new code feature called quote function. The quote function is a function which returns just like big string which takes all the space. And then if you look at this big modular, it takes almost all the space in the contract with few left. And if we try to implement it in very new way using Solid inheritance, so we just, okay, so we counter the big modular, like done, job done. And then we discover that no, the counter is over the size of the limit. We can't deploy it with nothing can be done. It can't be optimized. So how we can solve it? The first solution is external libraries. External libraries is a feature of Solidity. So how it works is that in your contract, in your methods, you just call a library instead of implementing it in the contract. In this case, we have two libraries. One is called storage lip, which contains our original counter methods such as set, get, const. And finally we have a quote lip which contains the only quote method which contains this big string. And if you look at the implementation details, the catch here is that you need to pass this parameter which is canonically called self and has a storage type. And this like a special syntax in Solidity allows you to modify storage in external contract. And essentially external libraries is Solidity is just abstraction layer over deligate call. And that's basically it, there is not much to this. And if we go back to our presentation, we can quickly sum up all the ups and downs with approach. So the good part about it, it comes with some security guarantees by Solidity. The main important one is library can't call self destruct. So library can't destroy your smart contract, which is important because it's not the case if you use deligate code directly essentially proxies where if a proxy contract calls self destruct, your contract is done. It's somewhat easy to add one more lip. You can always introduce one more lip and it's gas efficient to a degree but a bit later on this. And the main downside service is that it's not truly infinite because to define to add one more function, you need like external wrapper function which stack up quite quickly. And eventually you run out of space in your surface contract and it doesn't matter how many lips you can write more there will be just no more space. And the final downside maybe for some projects external libraries can be used, can support upgradable contracts. So once you deploy your external libraries, it's like set in stone and you can change it. Okay, the next approach is a static pre-compiled router. This work was pioneered by Alejandro Santander from Synthetics. He also previously worked at Open Zeppelin and he is known as a burnout on Twitter. And if you look at like, if you want to look at production ready like stable code I seriously check out hard hard router package at the Synthetics repo. What they are going to take a look at a simplified example. So the idea for the static router is pretty simple. It's a smart contract which has no external functions and it only has a fallback and receive functions. And what it does, it goes a forward function inside both of them. And the forward function consists of two parts. The lookup part for the implementation address and the assembly part for the legit call. I'm not going to spend time on this. I just going to say this is like a canonical implementation of a proxy dirijet call in assembly, which is used by all the proxies. And what it does, it basically sends your function call to whatever the implementation contract is and then returns its results. And the interesting part is here. So in Solidity, each function has a signature. And as you can see here, we have a switch for a signature method. And based on the signature of a code function, we pick up an implementation address which is later used for the legit call. And as you can see here, we have four functions registered for a counter modeler, which is a get set const and one function registered for a quote modeler, because it's like so big. And based on function signature, we do pick up a right implementation address and then use it for the legit call. And again, if you go to our contract sizes, why it works, because these are three separate contracts. We have a big modeler which is within the limits. Then we have a counter modeler, which is also within the limits. And finally, static router itself is pretty small because it's just with one function. Though the more functions you have, the more it's by its size grows, because you have to add each function here as a new line, though it grows way less than external libraries example. And if we go back to slides, we can see what are the benefits of using it. It's extremely gas efficient. Why? Because it's all pre-compiled in byte code, so you don't need to waste any expensive operations as it has load on the router itself. Your function code goes to the router and then immediately calls the legit call. So your most gas overhead comes from the legit call, which is about 2,600 gas. It's almost infinite. It will take really a lot of functions before you run out of space in this router in terms of adding with like constant signatures or contract sizes. It doesn't require external functions. Well, the downside's pretty expected because it's essentially a proxy. You can't use a constructors. You have to use initializer methods. And cross modular calls can be tricky and you have to use explicit storage slots. So you don't, your different model is going to write to the same storage. And then finally, develop and maintain so your head can be tricky because you need to pre-compute all these signatures and addresses upfront and you don't want to do it by hand, which is, I recommend check out again hard router. They built a plug-in which does that for you. Well, this like second approach, how you can achieve infinite size and compare libraries. It's like better in the way which gives you like more control using like assembly and low level language features. The next idea is iteration on a static router. It's not necessarily better. It is a different approach. It depends on your project. I call it dynamic router. If you're interested in product level battle-tested implementation of this idea, I highly recommend you to check out EEP 2535 by Nick Mudge. This EEP covers far more things than just unlimited contract size, but one thing it certainly does achieve is unlimited contract size through a dynamic router. I've written a simplified example of this dynamic router. So let's get to it so we can see how it works. It's quite similar to a static router in the same way it has a fallback receipt functions which just forward every function code to our forward function. But the difference comes in this line. If in a static router function, we have a switch case which just goes or have for this switch I have this address. In the dynamic router, we are using a smart contract storage mapping which is called modulus and as you can see here, we pick up an implementation address using a message signature. And then the rest of the relation functions the same, we just provide with implementation to the call and essentially it works in the same way. The essential difference between static router and dynamic is the static router has all the signatures and all the implementations and traces hard coded in its byte code where dynamic router use mapping on the storage of a smart contract to track them. And that adds pluses and minuses with approach. So the pluses is once you deploy your dynamic router you can add more modulus even it's already deployed or you can like fix bugs by updating it. Here I have this function update modulus and it's also pretty straightforward as you can see we just iterate and add selectors to implementations. The downside of this approach is you have to manage access rights. You can't allow anyone to update functions of your router that would be disastrous, a critical issue in security terms and there are different ways to use it. You can use open zeppelin access roles or some like other techniques, but that's like out of the scope of this workshop but you just have to keep this in mind. And if we go back to presentation dynamic router is the first router which is truly infinite because all the mappings between function signatures and implementations are inside the storage which is as we all know very big and solidity then you can literally add infinite amount of functions to this router. So it's very good for a complex system which would require upgrades and have like long expected lifetime. Then another option, another plus I just mentioned it can be updated anytime. Well, I mean it can be a minus if you're getting hacked but generally it's a plus, has no external functions. So the main downside always is it's like a gas expensive risk function is essentially here is rigging a storage which is extra S load which is about 2000 gas and the set part you have to pay it for every contract call. And then another downside is you have to manage access rights who can update this router. If you want to build a truly unstoppable contract maybe you don't want to have any upgrades. And then there can be many solutions like governments and multi-seq but you eventually have to figure it out for yourself. These are the three main approaches which you can use to tackle infinite size contracts. And then what's like important, what's the difference between infinite size contracts and normal contract use approach? It comes down to a gas overheads. Essentially here in this table we are campaigning our base contract with three approaches in terms of gas and why gas is important like a short story. When was like a bull run and gas prices were high we were seeing the function calls for the users in the price ranges of $100, $200 and even $300 for some projects. And a lot of these gas costs were coming from using proxies because proxies has a gas overhead at each function call and if you really have a complex defy system where protocols call each other and they all have their own proxies with amount of overhead stacks pretty heavily even within your own protocol. And it's like important to keep an eye on this gas overhead calls. So let's dig down a bit in these numbers the first base contract are the numbers for our base calls. So it's like no diligent code involved. And then if you look at the base call for a constant call it just returns a number, it's a query small and the first number in red is just what's the overhead on gas and the second number in red is in percentage. And you can see for like a simple return call of a constant value the overhead is actually around 2000% which is like insane. And you may say, oh, it's not practical to return a constant value but you'll be far from the truth because the famous UC20 token has a function called decimals which returns how many decimals a token has and you have to call this function if you're a defy developer because in order to handle a UC20 token you have to know how many decimals this token have and one of the most common UC20 tokens like USDC, they're approximate contracts. So every time you call decimals you overpaying roughly 2000% or probably more but anyway, let's take a high level look at these approaches. So as you can see indeed static router is the most gas efficient approach. So if you have to build gas efficient, infinite contract and you know what kind of functions you're gonna have and don't plan to upgrade then it's like a great solution. And then dynamic router is more expensive even more expensive than external library. The reason I wouldn't recommend external library as you can see it has unusually high overhead for quote, more than double than it should. And I think it has something to do with how solidity pass with like big amounts of data. But I haven't dipped the byte code yet but just be very if using external libraries and you pass or return a big amounts of data then your gas expenses may surprise you. And to take a look at this what will be the high level conclusions of this overview is that your gas overhead obviously is more significant for low functions which are low in gas. So essentially if your function has only a single S load then you would pay a lot of gas. So what it means in practical terms which I also encountered is that if you need to build a function which returns some like data around your protocol smart contract you may want to park them all this data in one function calls. So you don't need to call five functions in a row because you would pay a gas overhead on each call. Essentially batching view functions might be a good idea. Then again the bigger the baseline gas costs for your function the less you care about it. So let's say you have a function which is quite expensive let's say 300K then suddenly 5K overhead doesn't look that bad. There will be a catch to this in many protocols not all functions are equal. Essentially if you look at typical load functions such as deposit or withdraw which are mostly called by the user so you won't achieve the maximum gas efficiency on these functions. And some like admin functions which are rarely called you may want to skip optimizing gas for them. And because the gas is so important I spend some time thinking and trying to figure out a trick how we can improve this. And I came up with a solution which we use successfully in production to reduce the gas cost of our contracts. We never found anyone using it somewhere on the internet or on GitHub which is why I had to coin the term for this approach. So I'm calling it's like loaded router and so the approach is pretty simple because like if you go back to a static router to a static router in our case it doesn't take much space and even in growth we still a lot of space left. And what you can do essentially is again using salinity inheritance you can inherit some functions of your contract to a static router which allows them to be as gas efficient as a contract itself because it's essentially a contract itself. So in the other ways every some space in terms of byte code size left in your static router you can put the functions there which either people use the most in your system or the functions which has the highest gas overhead. Like calling decimals and this allows you to save quite a bit of gas because just to give better explanation of this if you combine static router with a counter modular we don't get to this forward function because static router now is counter modular and that results in a quite significant gas savings. And the same approach can be applied to dynamic router just gonna show you the code but it's pretty straightforward. You again can have counter modular but here this is the catch with dynamic router because in dynamic router we can update implementations of a function. If you place a function like get a set on the router itself you will never be able to update it. So that is why it's like rather risky and you should be very mindful which functions you put in dynamic router because you don't want to be in a situation where a critical bug was found in the function and you can't update it and it's just like a terrible situation to be in. And with this approach we can crunch some gas numbers. So now here we're using relatives themselves as a baseline and then they are loaded version essentially as improvement and as you would expect because we move a constant function from sitting behind the deligate code back on the router it results either to 95% improvement or 97% improvement for a constant function. It results around 54 or 68% improvement for a get function which is a single S load. A single S load function is not that uncommon as you may think because for example you see 20 dollars off function is just a single S load and again this function is called a lot and I don't really expect it to be changed. And then finally for a single S store operation the savings amount is about 11 or 18%. So it's like not that much but like still sizeable and what you could, like looking at these numbers my suggestion would be is to move low gas function on the router if they use a lot. So this is the most best saving or just the functions you expect to be user facing. So the functions people will use the most again as a transfer function the deposit of withdrawal or claim reward. And then finally the pure functions which is just a constant have the biggest impact on this. And just to give you some practical meaning let's take a look at the two most common contracts is essentially you see 20 talking and you see seven to one which is an NFT. We can see surprisingly many functions we'll proceed 20 function, 20 talking such as name and symbol and decimals. In most implementations they really return a constant just like name is like my great talking symbol is like three letters and decimals is like 18 number, right? And in most projects really these functions will never change. And literally if you write in you see 20 talking which has decimal functions which will return different values I might have a problem with you. Please let me know. And even going back even going later so total supply many you see 20 tokens has a fixed total supply, right? So we're again no reason for this function to be upgradable or to be sitting behind the daily trade call. And then things get tricky so I can see how transfer allowance or proof can be changed in the future or maybe standards changed but it's like up to every developer you have to make call on yourself whenever your project view encounter with issues in the future or not. And the same logic more or less applies to EC721 but these are just two common contracts I really wanted to show on them kind of logic I would fall if I would build a complex system where I would just sit down and look at my contracts and look at which methods are not going to change for sure in which are like safe to put on the router itself to save the gas and which makes the most sense because for example at Babylon finance we had this like admin model which had like a lot of admin functions but it's pretty much only us or governance we're calling them. So we really didn't care about the gas cost of this so I was like confident just like putting them behind the daily trade call and being upgradable. And let's jump back to the code. So here you would see once we put these functions on our routers they do grow in size. So a loaded static router is slightly bigger than just a static router. That's because we merged all these functions on this contract and eventually you hit the size of how many functions you can put on your router which we did and that is why you also should be like mindful that you have to put the functions on the router which like your absolute top priority in terms of gas so if it's like questionable don't do it and the same applies to dynamic router so it's higher but to repeat my security concern once again dynamic router is a different from static router in that sense that if you put a function on the router itself then it can be upgraded later and you should be like super careful doing this. The code, the repo is already public and it has always examples as well as the test which actually makes sure that all we discussed is worked. So here you can see that creating static router as a simple because it has everything encoded in it while to create dynamic router we need to build all these modules so we need to dynamically fetch all the selectors for the methods and then we create a model definition which is an array of selectors bind it to a certain implementation and finally we can use these modules to initialize our dynamic router with update modules and this is the same way you can update existing functions or add new even after the deployment and here we have tests to make sure that all these functions work and if you will be checking out this repo later you can run the following command test guest report to see all the guest numbers we just looked and speaking of these guest numbers these numbers are produced on 0.8 Solidity compiler with optimizations set on and about 200 optimizations run on. Let's get back to our presentation and that will be it. Thank you for your time. I hope you find it useful. I hope you built unlimited contracts successfully and never experienced the pain I did. Please follow my Twitter. I do post development stuff there.