Smart Contract Families in Solidity
EthCC[8] · 2 Jul 2025 · 18:14
Jan Gorzny presents a systematic way to express families of similar Solidity contracts. The presentation discusses reusing code across potentially incompatible chains to simplify multichain development and audits.
Speakers
Topics
From EthCC[8], hosted by Ethereum France.
Transcript
Okay. Hello. Hi everyone. Uh welcome to this talk uh smart contract families in solidity. My name is Yan Gors and I'll be presenting on behalf of um Casper Beck who was originally supposed to be giving this talk um but couldn't make it due some travel concerns. Uh, nonetheless, I'm a co-author on this work. Uh, and so I'm happy to present this. Uh, although this delivery might be a little bit rusty because I didn't get quite the chance to practice that I normally would have. Uh, and um, you know, you might see the branding is is not the company I work for. I work for Zirket. Um, Casper and others uh, who worked on this did work uh, do work at Quasamp. And so um, you know, the branding might be a little off. It might be a little jarring if you try to associate me with my current affiliation
rather than this. But I didn't have time to remake the slides, so we're just going to roll with it. And um we're gonna, you know, talk about smart contract families. And so what are we talking about? Well, smart contracts are software, and you can have a notion of um software families. So, if you're a one-off app developer, maybe this isn't something that you're super familiar with. But if you spend a lot of time in open source software, it might be a little bit more common because you have essentially um a set of a codebase which can be configured into multiple different versions of that depending on what features you want included or custom modifications you want. So, essentially you have sort of a standard codebase where you can configure some features and functionality and then you know people fork it and build different things. maybe some features get merged
back and you want to use them uh in some deployments and not others. So some common examples of this um include like the Linux kernel, right? The core kernel functionality is is pretty standard across all distributions, but some distributions may uh you know come with by default some functionality that others may not. Uh and similarly uh stuff that is in a lot of hardware like cars. Uh so you have a sort of a standard probably UI or UX4 um you know setting your your car's uh maybe uh radio or you know even the performance functionality under the hood. Uh but if you don't buy the performance package maybe the functionality to go into sport mode isn't available or something like this but other than that specific functionality you you know have the same
software experience more or less based on this common set of code base. So this is what software families are. This is well studied in software engineering pra u academia and in practice. People build this so that they can maintain the core parts and you know make that improvements or make improvements to that as soon as possible for all the good common stuff but also gives people flexibility to change that version for what they need. And no surprise that we're starting to see something like this in um a lot of Solidity based applications, right? Um this is not 10 years ago or 8 years ago or whenever Solidity started to become sort of popular or prominent on Ethereum. Uh we we've matured as an ecosystem and there's a lot of um similar but different pieces of code out there. And so I think it's time that we
start talking about uh a lot of this as a smart contract family. So if you're building a DAP, you know, actually start to think about what it means to have a single version that you might want to redeploy on multiple networks or in multiple different contexts and and you know, ideally you don't maybe want to do this, but the reality is this is something that's pretty valuable and often overlooked and not discussed, right? So there's a lot of chains out there um that will say, you know, you're you're um EVM compatible or EVM equivalent. And so you take your code and you want to actually do it on the same, you know, use the same code for multiple different versions of the network, which is not ideal. Obviously, we'd all love if there was a single, you know, liquidity source or single implementation of a tool. Um, just so
it's simpler and easier to maintain and you don't have to worry about liquidity fragmentation. Uh, but you know, the reality is that's probably not the case right now in in a lot of situations. You take the same code, you fork it, you put it on your specific network, you get your users there, and then you might have different users using a slightly different version of the chain. um on a different one, then you know that's that's life. U so maybe if we get new tech, new interop, this will disappear. But I I don't think that'll be truly the case um ever. I think there's always going to be a reason why you want to do something on different chains. And so in this case, you know, it might be very clear that you'd have, you know, multiple roll-ups. Um there's like 100 in development according to LGBT right
now. So there's a ton of roll-ups. Uh and most of them say they're EVM compatible. Not all of them. Some of them by design, others less so. And actually um it's not clear that they're exactly EVM compatible or or sorry not EVM equivalent. Um most of them are compatible but there's actually a lot of really subtle changes. And so if you actually take a look at under the hood at the EVM implementations of these rollups often they will change slight behavior not you know major changes for the most part but you might start to see that some things do different things on different blockchains. So some roll-ups were will for example change the operation or change the result of calling you know the block time stamp.
It might give you the L1 time stamp for that L2 E block or it might give you a time track by the L2 or you might support specific features that either are ahead of or behind roll-ups. Um so you know you might really want revert maybe your rollup doesn't uh implement that because Ethereum sort of wants to deprecate it and so maybe they've gone ahead and done that on their version of the network. Nonetheless, this means if you want to have an exactly equivalent situation for your codebase, it's very very difficult. You either have to restrict yourself to some very specific situations in chains where you can guarantee that this is the case or you have to deal with it. And maybe your app just doesn't care about it, which case is also pretty easy. Um, but you know, the actual feature set that changes from
chain to chain um is maybe not well defined and you have to go investigate. You have to figure it out for yourself. So if you can build all of this with a software uh smart uh sorry software family in mind maybe we can make this simpler. So let's see how we can do that and there are a couple ways we can do that. Um but first let's define the goals. Uh so what do we want? We want to be able to develop manage and deploy similar products across chains. So ideally not ideally not too different among different chains but we want to be sort of having a core common code base where we can change specific parts that relate to the specific chain we care about. Uh and what we also want is a really large common codebase if we can because we want to reduce the costs of
audit. A lot of uh smart contracts obviously get audited so that other people can take a look at the security issues and hopefully there are none but if there are any they fix them and then they go to deploy. And so you want to reduce all of this because you don't want to be saying okay I had a version on on optimism I got it audited. I had a different version on zirket yeah I got it audited. What you want to say is I have you know one nice thing that's audited and maybe I changed the line or two and maybe you still need a diff audit for that. probably you should but um you know you don't need to say it's totally different. You don't need to start from scratch to an auditor. You can say look I I have just this part changed. Ideally if you can actually
give them the whole family at once and say look these are the different versions that I plan to compile into or you know otherwise use um there's you know a need to only get one audit maybe and that's that's kind of nice because audits can be expensive. So let's go into um some specifics with an example. And here my example will be that of a retireable contract. So something we can retire basically we want to stop using it at some point. And now if we look at this contract um it's very small contract it takes some stuff uh it has some money that's the important part uh and then maybe at some point you want to retire it. Uh you don't want to maintain it anymore or you want your users to upgrade. I don't know you have you have an excuse that you've convinced yourself it's worthwhile and
you don't want this contract on chain anymore. So you have this thing. So let's retire. You force this function or this thing to be retired. Probably this should not allow you know anyone to be calling it. No. Perfect. It's got a message sender guard. That's good. And you have implemented using it as self-destruct. Okay. So, what's this going to do? Well, on Ethereum, it's deprecated. So, that's not good. Um, in other places, you know, it might revert only. Is that good? Is that good enough? I don't know. Uh, maybe your funds will be trapped in there, right? You want self-destruct in the old behavior at least it would self-destruct and send the money somewhere. Um, if it just reverts, maybe the funny the the money is trapped there. uh or maybe you know that reason that you originally had to um ensure that this contract is retired
actually disappears and that's not good either. Um or in other situations maybe it just won't compile right if this is long enough and the solidity libraries moved on this is no longer valid code and so when you go to redeploy this on a modern chain with a modern compiler maybe doesn't do what you want. So this is annoying right we want to see if we can fix this. How can we fix this? Well there's a couple ways we can do we can do what we call clone and own. So this is the idea that you take a smart contract from somewhere else that exists. You clone it, copy it, you know, make a copy, get fork, whatever you want to do. Uh, and then you make it your own. You put in your own repo. Uh, that's the own part. And then you change it because it's yours. You can do
whatever you want with it now, right? Subject to licensing and all that, but like you you essentially have your own version of the code there. You have to maintain separately from the original version. And then what you do is you change, okay, uh, if I know this call is going to be problematic on some chain, I'll replace it with a call that's more friendly, in this case of receiver.f. And it'll it'll do that. So, sure, we can do that. this is probably the most straightforward one. This is probably the one that everyone will think about if they're not sort of uh familiar with other options or if they're doing it quick. Um in a lot of cases even, you know, we saw um other projects that weren't necessarily optimizing for development time still do this because, you know, they just want their own copy of the code. But there's
other ways you can do it. Um you could build for this, right? At the very beginning, you can say, I know that this code, this line of code is going to be a problem. Um and I'm not too concerned about the size of my contract. I'll talk about the downsides of these approaches later. Uh and if it's on Ethereum, so if I have a specific chain ID, I'll do this behavior in the contract. And if I have a different behavior or a different chain ID, I will do a different behavior. And so, uh in this case, you know, we fork on where we are deployed with this contract execution and it does the appropriate thing that we think it should do. And that's that's also neat, but uh kind of a mess to start reading your code. You start looking at this contract. Now, you got to figure out
multiple paths at once. If you're a security auditor, not exactly the most convenient thing to do. you're always trying to keep a mental model of what you know the system should be doing and if you have these everywhere in your code uh it's going to be you know really chaotic to figure it out so you're going to have to consider it in different chains uh and there might be nested ones I I don't know right it could be a lot of stuff that goes crazy what I think is the best option although it's the perhaps the biggest engineering lift is what we call conditional compilation um this is a little bit more akin to traditional software families this is why I'm talking about this uh and um what we can do is actually have specify different sources um for parts of our codebase at a given
time and then at compilation we say I'm going to compile with these features enabled or for this chain or for whatever the flags are and what happens is you build the common core out the way you want and you put in some sort of indication as to where the specific functionality that will change will be changed depending on the compilation options and then when you go to compile you you specify the flags and everything works and so that's what we wanted to do um in this work we were just trying to see if if we could do that with ease because um what I just described to you actually doesn't sound that hard, but you know, if it takes 3 hours or 3 days to do, no one's going to do this. So, we want to see if we could do it in 20 minutes or something short, something
convenient developer experience-wise. And uh that's what we talk about. Um and so here's an example here uh of of that motivating example with a conditional compilation. You see, it's still kind of an annoyance to read to be totally honest. That part's probably not optimized entirely. But when you do get a compilation that's complete given the specific flags that version is very easy to read. So now when you're trying to understand this the behavior of this contract on a specific chain you can remove all the considerations for other chains in this example. And so on the left we would indicate that you know in this um retire function we have uh this area where you know if your compilation flag is set to mainet do this line of code otherwise do something else. And then you take an extra step before you
compile uh using a macro and some uh pre-processing stuff that actually exists. Um we repurposed a lot of the stuff from the Linux library I believe for this work. And uh when you hit the compile with those flags, you do get um just one version of that that function which is very nice. So now if you're an auditor, you know, you can say okay, I got to look at this code. That's great. I'll compile it, but also tell me what chain you're on and now I don't need to worry about the rest. And you know hopefully if you're a good auditor you also do the other ones if there's time and you want to you think there's a concern but at least you know for the version you were supposed to do you can figure it out very easily which I think is really really nice.
Okay. And so how do we actually do that? Um we do that with a C pre-processor. Um C is not something you probably hear too much at an Ethereum CC conference but uh yeah they have that that syntax looked like C. There's a reason that was the case. Um the C pre-processor that was used here I think was the one like I said mentioned uh that I already mentioned I think was used for the uh Linux compiler kernel compiler itself. So this is you know not some script that could break. I mean obviously it could break but uh it's one that's mature and battle tested uh and it does this job very very well. So then you start writing these macros in your solidity contract which then stops being sort of pure functional solidity in the sense that the compiler will complain if you skip this step. But if you don't skip
this step, you do get the option to use this um using sort of top tier battle tested scripts for compilation. And it's very easy to specify. There's a bunch of little flags there that I don't know off the top of my head. I'm not the only author for a reason. Uh and you know, you get the code that you want, which is really really cool. Um and then you uh can also, you know, do this programmatically. So those flags like I said really annoying but if you have a really large um software family or you have a lot of configuration options you can do this using you know some sort of a config file where you can specify different versions of this across different things and all of this will be picked up by the script and at the end of the day you'll get something
that's you know solidity that will work on your EVM compatible chain and uh also you can say you borrowed ideas from some really good software engineering stuff um and it does scale uh so it was a scientific paper. So, we have to at least justify is this approach going to be viable or useful? Um, I don't have a lot of stats on this, unfortunately. It's not my slide deck, but uh, you know, this works for hundreds of features for the Linux kernel. It's definitely going to work for your little uh, solidity contract, which generally speaking are much much smaller than things like the Linux kernel. So, it works. It works well.
Um, we provide scripts to do this. Uh, basically just the stuff you need to get it done. So I don't know if um actually there's a link to the um specific GitHub repo but if not come ask me I can definitely find it for you or look up the paper uh and we can definitely do it and then once you have the scripts and all the compilation op options are done you just compile so let it do the way you want in this case we use hard hat and it works okay so all I've been trying to argue is that you know maybe there's a situation where you want to do slightly different versions of your solidity for slightly different things uh might be different chains might be different use cases You don't actually have to restrict it to only modifying behaviors of dangerous op codes. You can
be totally different in your feature set of course, right? You can say on one chain do something or in one deployment do something in a different deployment do something entirely different. Uh you know you could have the function not retire for example. Um we could send money somewhere else or you know upgrade it whatever. Anyways there's a lot of things you can do with the smart contract families. Um but to sum up this is the comparison of these approaches right? If you're looking for bite code optimization, clone and own will work, but then you're mult you're dealing with multiple versions at once. Just as effective to use this um this conditional compilation method because at the end of the day, what you put onto a chain is optimized by by the compiler without additional conditionals. So that's nice because obviously when you compile you don't specify what chain it
is generally. Um so that's also true of execution cost and compilation cost. Um, I guess there's one extra pre-processing step, so maybe there's a thumb up with a little asterisk over there, but still pretty good. And you're still going to get the standard compiled. I remember any specific version you get. So, as a developer, it's still pretty good. Um, the clone and own is a little harder for uh the auditing team themselves if they want to do something like this because they have to go figure out if there's any version of the code that contains bugs that you didn't clone, right? You might have cloned a a version that was pre-bug fix and that's a problem. Um, in this case, uh, you get to see everything all at once. So, I think it is a little bit nicer. Uh, I will say I would maybe
have put this as a slightly different symbol because again, if you have to do multiple versions of it, maybe the auditing is not the best, but it's still pretty nice. Uh, and certainly the conditional compilation has very good readability, right? So, you're only looking at the single version if you want, but also you can look at different versions uh, and you can consider them in different ways, but that's probably unavoidable. If you have different versions of code, you have to look at different versions to understand what they're doing differently, right? So, that makes sense. Um the downside is with this approach because um although we use some great stuff from the Linux Foundation all of that um I don't think there's any testing and tooling around you know that supporting the different versions at once and testing each possible configuration of which there may be many if you have a lot of
features. Um but if you're interested in this and you think there's a good use case or I you know just convinced you that this is a valuable approach build it go build it into you know your favorite testing framework and and they'll probably be happy and you'll be happy and everyone will be happy. Um, so that's basically it. That's what we did. Um, I hope you found it, you know, informative and and maybe you consider using it. We do have code available. I guess it's not linked here. Um, but essentially, if you are going to do multiple things, clone works sometimes, but if you're really deploying a lot of versions, mix it up, do something better. A lot of, you know, better software engineering has existed over the last, you know, 30 years, 50 years, not just the past four or five, eight years of solidity. Learn from it. Do
something better. And in this case, we showed that you can actually do conditional compilation if that is what you want. And it's very valuable. It's very quick. It's very easy. So that's it. That's my last slide. So thank you. Do I have time for questions? Yes, I do have time for questions. I have two minutes. Okay. No questions and I'm okay to be done. Thank you. [Music]