Smart contract design patterns | Noah Citron
46:46
a16z crypto engineering partner Noah Citron surveys common smart contract design patterns and explains why the generalized solutions used in web2 development, built for databases and APIs, don't translate directly to smart contracts. He walks through a taxonomy of patterns, the pitfalls facing smart contract developers today, and approaches for maximizing composability through standard interfaces.
Speakers
Topics
Transcript
[Music] thank you today I want to talk a little bit about design patterns and architecture for smart contracts so we're just going to jump right on into it so why are we here well start for those who don't know design patterns are these generalized solutions to common problems when designing systems uh but why do we want to talk about smart culture design patterns well you might recognize this guy his name is Uncle his name is Bob Martin affectionately referred to as Uncle Bob by software Engineers around the world and he talks a whole lot about how to build software he writes a lot of books uh his most famous one is called clean code and it's sort of the Bible of
design patterns but if you haven't read clean code you've probably still learned a ton about design patterns from blog posts work classes or just years of experience as engineers so then why are we here why why are we rehashing this if you know design patterns already permeate sort of the tech industry and I think that's because web2 design patterns differ pretty significantly from web3 um start with we don't use back ends um mostly because we have sort of some centralization concerns as they are centralized choke points instead we use Smart contracts and smart objects have these entirely different goals and constraints some people have tried to compare them with other Technologies like databases or apis but really the the design patterns we have from
databases apis don't fit in the context of smart contracts so because of this we don't have a book we don't have Uncle Bob writing about all the design patterns we can use instead though we have some protocols to look at so here we have sort of an image of maker dials protocol design we can look at those protocols and start to understand what the design patterns are and you know maybe eventually when we do that enough you know we've only been doing that for seven or eight years now we'll have enough to write a book but for now I think we have enough to do to do a talk um so we're going to frame the design patterns we talk about with two main metrics and those are upgradability and composability so upgradability is how a protocol can
change over time and what guarantees does it make for the future you know composability is sort of how protocols ensure that it's easy for you to build on top of them uh and there's other metrics we might want to talk about such as gas efficiency and organizational complexity and we will discuss those when a particular pattern really calls for it however we're really going to stick to these two metrics upgradability and composability and the reason for this is because this week we're talking about infrastructure and I think that protocols that use patterns that allow for limited reasonable upgradability and maximal composability are most likely to become these critical pieces of infrastructure on the chains that they're deployed on
so we're going to jump straight in into sort of this first metric upgradability and talk a whole lot about that so upgradability is often considered to be a frightening thing why is that well it's because if you have an upgradable smart contract system then someone some entity can do the upgrading and that means that that entity can in theory do a malicious upgrade and steal funds um so that's not all that true though because we we're going to split upgradability into sort of two polls that lie on the Spectrum we're gonna do unconstrained and constrained upgrade ability and then there's this large design space that we're going to focus a lot on that lies in the middle so with unconstrained upgradability
upgrades can create arbitrary changes to the system's Behavior they any rules enforced by the contracts can be overridden by an upgrade and some examples of this are Proxy Systems and this is what gets demonized a lot but I will say it's not all that bad they are used pretty pervasively throughout our industry and they can be super useful because it allows you to squash bugs quickly if a Old Co if your original contract has a critical bug in it a quick upgrade from the team can can alleviate those concerns but one of the big downsides you have is you can't make a pact with the user about that that the system is going to behave the same forever and into the future and this is particularly bad for systems that deal with high value assets
but fortunately we can sort of address some of these concerns even with fully unconstrained systems using things like Dow governance and long time locks which which helps to some extent but on the other side of the spectrum we have constrained upgradability and with this upgrades can make limited changes to the system's Behavior and the system can encode rules as to what parts can be upgraded and what components and invariants must remain constant um and some examples of this are module patterns and configurable parameters now this is a lot less scary because users have some degree depending on how constrained you are of ability to understand the system Behavior into the future
and yet if you allow a little bit of unconstrained then you can support limited upgrades and this is useful because it is incredibly hard to design protocols that work from day one and into perpetuity so we're going to start sort of with our first example of a bunch of patterns that allow for unconstrained upgradability and talk about proxies so here we have a picture of Ralph and he's a proxy user and he's realizing he might be in danger because he's interacting with proxy which we're going to use as a metaphor have be have a shotgun of a smiling face but a potentially dangerous and scary inner workings so let's start with like a quick example of how proxies work
um so we have Ralph here and he calls a proxy contract uh which is sort of the head of our our shagif and the proxy is this immutable contract at first it sort of looks to be somewhat safe meanwhile what the proxy then does is it delegate calls whatever Ralph did and forwards the call off to the implementation so what does this mean how does delegate call Work Well when the proxy delegate calls implementation What's Happening Here is that the proxy is handing all control off the implementation so when invitation then goes ahead and make some external calls those calls are going to look like they're coming from the proxy and when the implementation Updates this its own storage it's actually going to be updating and reading from the storage of the proxy contract instead
so in other words now when you do that delegate call you're handing over the keys and for the for the per the entire time that call happens the implementation sort of has the keys to the castle so why is this frightening why is this scary why are we using a shock if as our example well this is why um because the implementation can be upgraded so the old limitation may have been innocuous but a new one could be malicious on a Counterpoint though the older position could be incredibly scary it could have a critical bug allowing hackers to drain the protocol um so it can be useful proxies are useful but when you use them you do lose out on one of the core benefits of smart contracts which is the ability to reason about Its Behavior or in other words
it's immutability so we're gonna go a little bit deeper and actually understand how proxies work in different ways one might construct a proxy and our first example is a really really simple proxy system it's actually based on the first ever proxy to play it on ethereum designed by Nick from ens and we'll start by looking at that first contract the head of the shog if the proxy so it's a really really simple contract it's got one variable which is the implementation address or the address of the body of our shagath and then a single function called fallback now the fallback is a special function in salinity if you make a call to a Smart contract you generally are calling some function but if that call you make
is for a function that the smart contract doesn't recognize it goes to the fallback so in this example fallback's the only function here so it means that whenever this this smart contract receives a call it's going to execute the fallback a fallback is really simple all it does is it delegate calls the implementation and sort of forwards off the transaction data tube implementation and again hands the keys to the castle to the implementation now on the right this is our implementation contract and we can see it looks for the most part like a normal contract um the one weird thing about it is we have this first storage variable which is completely unused and it's exactly the same as that storage variable we had in our proxy and it's the address of the
implementation which in this context it doesn't really make sense right you are the implementation why do you need your own address and we certainly don't use it and we're going to talk about that on the next slide so bear with me but other than that this looks like a very normal smart contract it's got two storage variables A and B a function called setvars which updates those variables right very simple what's cool about these systems though is that those variables A and B when we're setting A and B in our set bars function it's actually since it's a delegate call it's going to be updating the storage of that proxy contract this means we can in theory swap out that implementation for new one and those stored variables A and B are still going to be there for the next contract and that's what really makes it powerful
because you've got the continuity of the state of the contract stays the same between upgrades okay now let's understand we need to really look under the hood of how storage Works in ethereum to properly understand why we needed to have this this random implementation address inside of our implementation contract and how not to make a whole class of critical mistakes that people have been making forever with proxies um so let's talk about storage or storage slots a smart contract when it stores things it's not just storing them in a variable but rather under the hood it stores them in slots and Slots are sort of can be thought of as these little slots each one holds 32 bytes of data and they're numbered the first slot is zero one two three and increments all
the way up to 2 to the power of 256 minus one there's a whole bunch of them you can use as many as you want however it's storage and in ethereum is quite expensive so you might want to not so because of delegate call though our proxy contract and representation contract are going to share these slots so if we look on sort of the top uh right we'll see what our original implementation looks like and we're going to see how those matte slots within our proxy so we have again our three storage variables implementation A and B invitation is going to be B slot zero a as we slot one and B is gonna be slot two this is because slot numbers are assigned in order the implementation uh variables first so it gets slot zero
and B is last so it gets slot two um when you look at this it's map remember since we're doing the delete call we're mapping it to the proxy this means well let's think about what the storage of the proxy looks like well the proxy has one variable and that's the implementation address and it goes in slot zero so you might be able to understand why we did this because we need to make sure that we don't accidentally overwrite the implementation address that the proxy expects to be in slot zero you can imagine a bad implementation sort of the naive invitation you might make would just have two variables A and B but then because a is the first variable it gets slot zero which means it will whenever you call set vars and you set a to something your accident not you're gonna be writing what the implementation does
think is a but you're going to be overwriting what your proxy thinks is the implementation address in other words if you were to do that you would you would lose control of your proxy system and you would never be able to upgrade it again and it would never work again um and this is a pretty common theme that we're gonna run into when we talk about proxies is that there are a lot of foot guns along the ways and you really need to constantly be looking under the hood and understanding how the proxy system works in order to use it safely and effectively um and if you don't you could very easily break your entire contract system and if you look at some of the largest mistakes that have happened to defies from the largest hacks or just breaking protocols a lot of them have to do with people not fully
understanding the communication implications of their Proxy Systems when they're working on it okay so I want to talk about we're going to show you like a modern proxy like what what they really look like and this is based off of eip1967 which like I said is sort of what most modern proxies base themselves off of and it's almost same but it's got sort of one kind of difference that is that we've removed our first storage variable the implementation address instead we put in a constant variable now constant variables don't take up storage slots they're hard-coded in our contract code so we don't need to worry about using that storage slot zero when we declare this variable implementation slot then we make this variable and it's set to one constant that's the ketchik hash
of some string here minus one in other words there's a reason why we do this but what this does is it gives us a very random slot value you could it's now you know impotation slot is going to be equal to like 10 gazillion and five or something like that now we modify our fallback function just slightly and we use a little bit of inline assembly and we directly load from that slot number and we load it and we expect the implementation to be there and then finally we act the same and we sort of delegate call the implementation and everything goes goes the same from there but let's look at our implementation contract and you'll notice it looks a lot simpler it's got just those two variables your setvar but no implementation address
and we don't even need to think about where it is the reason for this is that it's at this really big random number that we're never going to hit where you can't accidentally create a smart contract that's that's storage layout is ever going to map some variable to the value of implementation slot because of this we just get to use this almost as if it were a normal smart contract and not worry too much that's awesome and that's why most sort of modern proxies look a little bit like this there are still ways to make mistakes here you can imagine doing an upgrade and making a new implementation but in that upgrade you may have swapped the locations of variables A and B and when you do that now you've messed up that
slot mapping and now whenever you look inside B you're looking at was what was previously a in the in the previous version so you still need to be very careful you need to be you need to make sure that when you when you set your variables you don't you only append new variables you never delete them and you never transpose them but if you stick to those simple rules you for the most part can't make a mistake and that's why people really like these kinds of proxies but there's one last piece of complexity we do need to discuss to fully sort of understand all of the weird edges of proxies and and that's where the upgrade logic is so what the upgrade logic is is the the actual function that allows some owner to you know call upgrade and swap out the implementation for for a new one on
the proxy so there's two ways you could do this there's sort of the obvious approach based off of our simple proxy right would be to just put it inside the invitation that's where all the code goes anyway we already know where the implementation is stored and just have a function called upgrade that that swaps it out and this makes a lot of sense but it does have one pretty catastrophic risk and about it's that you could accidentally during an upgrade lose control of your proxy the way this would happen is that you may upgrade your proxy there's upgrade your implementation in the process of that you can accidentally modify in potentially subtle ways how that upgrade function works if you do that just once incorrectly you that upgrade function will stop working
and you no longer have sort of a a way to save yourself by upgrading because the upgrade function won't work anymore now because of this modern Proxy Systems put the upgrade logic inside of the proxy um and this prevents bad upgrades from bricking your your uh your upgradability logic because it lives in the proxy the proxy is immutable anyway it always knows where the implementation goes and you can always have that function swapping it out um it's also pretty nice organizationally it gives you a very clear separation of concerns between the proxy which sort of handles all of this delicate calling and storing the storing the implementation address and the implementation which for the most part is handles business logic that's where you know everyone really lives and spends their time look is working on the
implementations but I do want to spend one second to go over one last proxy pattern pattern and this is called the diamond pattern and diamond pattern gives you sort of this Dynamic function dispatch to different implementations so in our example here we again we still have our proxy but rather than having a single mutation we have multiple and here we have three and they're called facets in this this pattern and the proxy Maps certain functions to certain facets so here we have function for a and other function for a map to fast a we have function for B maps to facet B and function for cmaps is the fastest C this is an incredibly powerful pattern because it means it's great for organization you can sort of keep things uh segmented between different facets
you can build these really big systems you can even get around the limit on how big smart contracts are allowed to be in ethereum which is not that big using this um but there is for for all power there there are huge drawbacks using it the main one is that all the facets since they're being delicate called share storage all of their storage is the proxy storage this means when fast a updates storage it's updating the storage but fast B is using in facet C uh and this means it's very difficult to understand you know which facets own what piece of State and it's incredibly easy for one facet to overwrite storage but it shouldn't have and break your system unfortunately there are sort of patterns to make this better it's called the
diamond storage pattern um but when you use this those diamonds orange pattern you need to constantly now be remembering that you're using the diamond pattern you can't just for a moment even just consider that you're using a normal smart contract like you could in eip1967 style proxies so because of this big uh big drawback most people are pretty strongly against the diamond pattern and I tend to agree with them uh I have been told that there are proponents of it but I haven't actually met one yet so wrapping up in proxies uh proxies support unconstrained upgradability for the most part it's almost impossible to put limits on what an upgrade can do uh and it can make composability somewhat difficult the reason why
composability can be difficult is because systems that are integrating yours don't know how it will behave into the future so if they make your system invariance part of their core invariants they now have to extend their trust on to you know the team that can do the upgrading which you potentially might not want to do uh proxies also have this one big drawback that they can be difficult to reason about and this leads to very subtle bugs so we'll take a quick detour and talk about a set of patterns that don't really fit into our our original metrics of composability of upgradability but
are still really important because they are used in a lot of the patterns we're going to talk about later to to build up these more complex systems this is authorization and it answers a very simple problem which is how do we manage who can do what so we're gonna look at two examples of authorization patterns the first one is incredibly simple it's called roles based authorization and it's probably exactly how you would do it you probably didn't even give this a name so it's so simple um and in our example here we have you know an address that is the owner and then a mapping of uh address to booleans called cool guys and if that entry for your address is true uh then you're a cool guy if it's false which is the default then you're not a cool guy and then we have two modifiers only owner
which just make sure that the sender is in fact the owner and only cool guys which makes her that the sender is inside this cool guys mapping really simple but really powerful because it means now you could have different groups uh and different admins of the protocol of different powers it allows you to sort of constrain these actors in really nice ways but on the other side of authorization we're talking about a pattern that allows for a lot more complexity and can be a lot powerful it's called DS auth It's actually used by maker Dao to manage its sort of very complex authorization patterns it might not be worth using it unless you have a very complex protocol because it is sort of hard to use and harder to reason about but it actually does look somewhat
simple we have just one uh one address called The Authority and whenever we want to figure out someone's allowed to do something we simply say hey Authority like is this person authorized to do that so you can see we have is authorized message sender that that the caller or messages.sig which is sort of the function a unique identifier the function they're trying to call in more simpler terms what this pattern lets you do is it lets it lets your contract say hey Authority is Noah allowed to call Mint on my contract is Bob allowed to call upgrade and the authority is its own contract and can have sort of its own rules for making the decision and those rules can be very complex and
have sort of a lot of different subtleties so it also means that you can allow this authority to be upgraded it can be reconfigured you can sort of go crazy with it and you know you could have it so that things could only happen on on weekdays if you really wanted to it it lets you build these really powerful complicated setups so now that we understand how you would do authorization there's sort of this import an important facet we haven't discussed which is how do you change authorities like how do you say you know upgrade the owner for example in a safe and reliable way and there's sort of two ways to do it there's the obvious way which is called unchecked receive and a more interesting way called check to receive
so an unchecked receive it's exactly how you would think you got one function called set owner only owner is allowed to call it and when it's called it passes in the new owner and it overwrites it um really really simple and it just works um there is a risk here though you can in a rare case maybe set the wrong owner um and that would be really bad uh particularly if it's like the ownership of your entire protocol uh well goodbye to your protocol so how do you fix this well there's been this pattern called check receive which makes sure that we are actually properly setting the new owner and it makes a slight modification modification to the previous one we looked at where our set owner function doesn't just immediately set the owner but rather sets a new
variable called the pending owner and then we have another function called receive owner and it simply just checks like oh R is the caller of a pending owner and if it is then the pending the owner is set to the pending owner in other words what this does is like unchecked receive was just sort of throwing the ownership Into The Ether and hoping an address was there to catch it we've checked receive we're sort of very delicately handing ownership over and we're making sure that the recipient is actually there and is real and and can make a transaction now this is slightly more gas uh it's expensive from a gas perspective um so you might not want to do it for everything however if you're dealing with a high value role like the ownership of your
protocol it's it's worthwhile and in fact um compound governance for example the ownership of the entire protocol is owned by the thing called the time lock and the governance contract owns a time lock uh and it turns out from time to time they upgrade the government's contract to do new things and each time they've done it they've upgraded it twice now because there's been three versions of the government's contract and each time they do it they've used this check to receive pattern to do the upgrade because it's so so important that they do not make the mistake when handing over the governments of the entire compound system okay so now we're going to go back into some patterns that fit in our Framing and talk about modularity this is the set of patterns I think to be incredibly exciting because it lets us do constrained upgradability
and so what moduity does is it brings system down into several modules and generally each module has a particular job which is quite nice from an organizational perspective but modules will interact with each other to form complex systems so here we have an example we have module B and it's calling module a and model C and it's telling it to do some stuff but they can even have these complex authorization patterns between them so this is actually why we just talked about authorization because modules might want to decide what other modules have permissions to do what within the system and this provisioning can be based on on arbitrary rule sets so DS off for example could be very useful here uh and in our example we have you know module C who's approving module B but denying
module a from doing whatever it was it wanted to do and we could also finally swap out modules for new ones uh so here we have you know module C 2.0 which is newly improved because it is purple um and this is really useful because some modules are going to be immutable and unupgradable and some modules can be swapped out for new ones or could even be a proxy um and this is a really really great way to achieve constrained upgradability because what you can do is you can have these immutable modules basically gatekeep what the mutable ones are allowed to do all right so we're going to dive into an actual sort of example of a simple modularity pattern I'm going to call it simple composition
and we're going to motivate this by an example of nft project it's called verbsdao it's an imaginary nft project um not inspired by any other grammatically themed nft project and the verbs now has a couple of rules they really want to maintain uh one of them is that only one uh verb nft can can be minted per day one is maybe that some reserve price must be met and go back to the verbs Dow um and and as well as it needs to be minted by some authorized auction module who has some some auction system because they may want to play with with different mechanisms to auction off these nfts so in our example we sort of instantially this quite simply we have Mr Burns here who has lots of money and
wants to buy a verbsanity and he bids on the auction module the auction module has some arbitrary auction mechanics to decide who the winner is and Mr Burns of course wins because he's got that big thing of money so when was the burns wins the auction module is then going to call Mint on the verbs out of date now what's cool here is that doctors can say okay missed I swear Mr Burns one and you shouldn't have an ft but the virginity is still since the aux module might be mutable the verbs out NFC however is immutable and is going to make sure that these key constraints are maintained just gonna check as I say when was the last time we went to the nft because if it was less than a day ago I'm not going to let you mention everyone I said and what are you paying me if
it's less than the reserve price I'm not going to let you mint it and volume to check like are you a real auction module right and what's really cool about this pattern is it gives us this constrained upgradability because these two main invariants are always going to be maintained however there's one environment that isn't going to be maintained right the auction module is mutable it you can swap it out for a new auction module and that is really powerful because well you might want to change how you do your your auction design right we just learned all about auctions right from Michael and uh and Scott so you now know about all the different ways you can do options and you may change your mind at some point to a new auction mechanism and you can simply use this little bit of upgradability to make a new auction module but you can
still guarantee to the verbs Dao that the core invariant that that Community really cares about will be maintained all right so now we're going to look at uh another pattern called the invoker pattern now this is a little bit more flexible but it can be harder to constrain so in this example we have some core contract we call it the Vault here and it's got only one function on it called invoke and it takes a two and a call data so it basically what invoke does is when it receives an invoke call it goes ahead and makes a call so in our example here we're call it's being invoked and being told to call uniswap and to perform a swap on it right but it can be told to call any arbitrary contract and with any
arbitrary call it could be uniswap swap it can be to Mint some nft or what have you meanwhile we have these modules so the Vault will only allow sort of modules that are registered in its system to call invoke on it so in our example here we have we have a trade module and you might guess it's it's going to invoke The Vault to perform trades so like in our example we told the Vault to call unit Swap and to call swap on it right but these trade modules are pretty unconstrained in what they can do in theory you can for example have the trade module call the vault which maybe holds a bunch of tokens and tell it to transfer its tokens to some to me and that would obviously be bad but fortunately we can provide a little bit
of constraint on the system so for example we can maybe say to the trade module or have the Vault say well I'll let anyone inv or any valid module can call invoke however we're going to make sure that the call data is not the call data for transfer transfer from or approve and by doing just that we've now made it so the trade module can go ahead and do whatever it can make arbitrary trades through arbitrary dexes with weird mechanisms but fundamentally the vault is never going to let you is never going to allow a module to just transfer tokens out of its out of its own wallet so this is quite nice it allows us to constrain this to a certain degree not as well as we did the last time but it does allow us to create new modules that we never would have thought
about when we were originally instantiating it so you remember in our last pattern you need to think about the constraints and you need to think about the possibilities of what you your system might want to do from from the get-go but in this system you can just have modules that make weird other calls so for example we might make a new model called governance module and it might invoke The Vault and tell it hey like you have some governance tokens in your in your uh in your contract please go vote Yes on this governance proposal and you may have never thought about this that this possibility but because we've used this more flexible but yet still somewhat constrained pattern we're able to do it should I want to talk about one last sort of pattern for modularity and this is a sort of a delicate call based
module pattern and this for the most part supports unconstrained upgradability we can constrain it just a little bit but not really um so in our setup here we have again two contracts um and the main one is governance this is our immutable contract that cannot be changed and then we have the proposal payload which there can be many of them they can be swapped out um they're immutable and they can be changed and then our immutable governance contract is going to delegate call into proposal so this pattern the reason I'm using this framing is because this is super common in governance systems where you will app the action that will be performed when a vote passes is a governance contract might delegate call into a proposal payload and then the proposal can go on and
pretend it's governance it can reconfigure core system contracts because it it is now acting as if it's the protocol owner it can even update the internal state of the governance contract so it can for example modify the Quorum or the vote length or any other of these sort of internal pieces of State um now this this is nice because it's incredibly flexible but it does have some downsides um when that sounds it's hard to constrain it's hard to be able to for for governance to be able to prevent the proposal payload from doing a particular action there's one small way we can constrain it we can for example have the governance app contract after the delegate call finishes it can check some invariants it can say well make sure that the proposal payload didn't I don't know Mint new governance tokens if it
did revert the entire transaction sort of Undo It All so that is one way we can do constraint but it's hard to do and again it allows you to only constrain very limited sets of things the other downside of this is that you'll notice that if you have lots of modules lots of proposal payloads you'll notice this just looks a little bit like the diamond pattern but because it is um and for the same reason as why the diamond pattern is sort of frowned upon is this is often frowned upon in complex systems because all of these modules were shared in the same state and every time the main contract delegate calls into your module there's a chance you are going to corrupt the state of the entire system so it can it can be very very difficult to reason about um and it you know each of these models
can modify things in unpredictable ways so for that reason I I tend to shy away from it I do however think it's fantastic for the use case of governance lots of Provident governance systems do this maker for example does this the only difference is that they name their proposal payload that gets delegate called into are called spells but it's still basically this exact same setup as well as most Ave proposals use this mechanism all right so now I want to talk about like an actual module-based system that I think makes really good use case of somebody's patterns um and that's Zora V3 so Zora is an nft Marketplace and they have a pretty extreme focus on decentralization and because of this goal they've made heavy heavy use of the module pattern and
support only incredibly Limited upgradability so let's sort of lay out our scene here we have Bart and Bart has nft and the smart counterfeit is on the right uh and we'll see uh three sort of system contracts for Zora one is called the module manager one is called the transfer Helper but we also have this a bunch of modules but we're only going to show one here which is called the ask module which is sort of used for selling nfts at fixed prices so when Bart uses a system he starts off by telling his nft that he approves the transfer helper to spend his nft this is a really common action for tokens you can call on approval and what this means is that the transfer helper can sort of rip his nft out of his wallet whenever it wants to unfortunately the transfer helper is immutable uh contract so we
know exactly how it's going to behave and in what cases it will be allowed to sort of remove that that uh from Bart then Bart the second thing Bart's going to do is he's going to prove the ask module with the modulator so you can tell the module like hey that asked module I trusted it and this is actually sort of a core invariant of Zora is that the user always needs to approve a given module if they don't the modules are sort of powerless to act on that user I mean finally uh Bart's going to list the nft for sale on the ask module and the ask module has arbitrary rules for how it sells it and in this case I I believe it uh it sells it as at a fixed price
so now let's let's look at how all this comes together when someone Ned Flanders here comes to purchase uh Bart's nft so Ned calls purchase on the ask module and the ask module may be sort of probably take some tokens from from NED right and go to the BART but what happens inside the Zora system well the ass module is going to tell the transfer helper which is allowed to spend Barton of tea hey like uh we a sale just happened please transfer Bart's nft to net uh and the treasure helper though is not going to Simply blindly trust the ask module rather it's going to sort of ask the module manager say hey module manager does Bart trust ask module has he approved it and in our case because Bart had previously proved it the modern say yep we approve it I'm the transfer helper will only then
transfer the nfts to the buyer so this system is really great because it allows the modules to have these incredibly constrained behavior and user has full control over how the system is going to behave in the future because it has full control over what modules they want to trust now we can also imagine that this does support some upgradability because you can add new modules right but you can support you can imagine some another interesting type upgradability that Zora doesn't support but they could would be maybe allowing the admin to Blacklist certain modules from the system you can imagine that one module may have a critical bug in it that allows for example someone to steal nfts without paying the correct price and in theory you could have it set up such an admin can tell the module
manager hey this this model is no good anymore please unapprove it for all users now this would be great for safety but it wouldn't actually significantly change the trust assumptions that you have in this pretty trustless system so wrapping about modularity model patterns are really fantastic for constrained upgradability and they also really give us the Spectrum you can be as constrained or underneath as you want you can use these like delegate call-based patterns or invoke based patterns and have them be act in whatever way the modules uh please or you can do these highly highly constrained things like in Zora's case it also makes composability really really easy to slot in I think a reason
for this is because of the amount of constraint you can build into your systems if you can make core guarantees on a variants that will always be maintained in your module system now protocol is integrating with it can can understand how variant variants play with that and make sure that their systems will be safe when used in the context of your system forever and into perpetuity which is fantastic particularly for infrastructure so the last thing we're talking about is composability um and I think composability is sort of this essential aspect because it allows protocols to be easily integrated with other protocols and allows people to build these sort of like crazy things on top of your protocol that you may have
never anticipated and I think the key to building composable oil contracts is using standard interfaces and one of the easiest ways to do this is by handling many accounting tasks using tokenization when it makes sense so two notable examples of this are uniswap V2 LP tokens and compound C tokens so we're talking about Utah V2 if you've used it you might remember that when you deposit liquidity you know provide two tokens to facilitate swaps you receive sort of a receipt for accounting called an LP token that is sort of an IOU on the underlying uh LP position now because the lp token is just a normal token a normal erc20 it becomes
really easy for contracts to compose of this so way back in 2020 during defy summer um you this feature of uniswap was often used to build these farming contracts so we could have in our example Homer here with his LP token and he can simply deposit it inside the farm contract now the farm contract might lock it up for some period of time and maybe give homers some staking rewards for his troubles um but what's really cool about this is I don't think this is a really interesting use case proposability it's boring what I think is really cool about is the code it's not because the code's cool it's because the code's also super boring so in our code here we have just our um our LP token address which is in the rc20 uh and a mapping of deposits and we have two functions the positive
draw and all deposit does is it takes some of your token of that LP token from you and increments your deposit uh mapping entry and withdraw simply gives you the token back and decrements your entry what you'll notice here is that there is really like no mention of like the mechanics of uniswap V2 because uniswap made tokenize this asset it was incredibly easy to work with you didn't have to think about how what do I need to call to ask me to swap how much they have how do I make sure that person can't withdraw it all you need to know is that uniswap gives you an IOU and a token and you can take that token which makes it really easy because their smart contract Engineers are really good at dealing with tokens in the second example I'm going to do is a little bit more complicated and it's
how compound C tokens are used to build much more complex systems because of their sort of liberal use of tokenization so in the example I'm going to use is actually going to be example from my life when I worked at index Coupe and set protocol and we at index we built uh crypto index products tokens that hold of our tokens so we've got really good at building infrastructure for managing tokens so you can see here an exam a highly simplified example of what our contracts look like of some mapping that kept track of how many how many of each token we had as well as a trade function that allows us to swap between tokens and we can let's say that was about all we could do but we wanted to do something new right
all we had done is had tokens that held over tokens and that's interesting but not that interesting and we want to be able to leverage each product uh but we again only had infrastructure for dealing with tokens and if you think about what Levering up on Eve looks like it means you probably deposit some ether as collateral into some other complicated smart contract system you're gonna then borrow and maybe get something else like usdc and then you're going to trade that you see for ether and maybe redeposit your ether to up your quadril's age ratio it's complicated and the complicated part often deals with other protocols such as like compound but fortunately because of what compound does they were able to make it really easy and this is because compound represents their uh their collateral as a token they call them C tokens so in
the case of ether collateral if you deposit it into the compound system as collateral you get C ether as an IOU for your underlying ether what's really nice about this though is if you deposit ether and you get a token backseat there well that just looks like a trade right so our system built for managing tokens and doing trades was all of a sudden able to interact with compound and do deposits I've been through the rest it's very easy we call it a function that all of a sudden gave uscc and that's just the token we know we know what to deal with that trading it for ether another thing we were really good at and then finally one more because of compound being so great with its tokenization we were able to deposit back into combat ether and to us it just looked like a trade
what's really great about this is we built this really complex system for Levering up on tokens I mean but we didn't need to really think too much about compound's internal mechanics in fact the core smart contracts we used were completely unmodified we just had to add a couple very small small modules and because compound did this they were able to get a new user of the protocol that contributed something like 100 million in tvl so it's really fantastic uh use case of of composability and how we can build complicated things out of it so wrapping up uh we're building critical infrastructure that we want others to rely on to do that I think we need to minimize
upgradability with reasonable constraints and maximize composability using standard interfaces and if we do those two things I think we're building to be built on top of and you'll find people using your protocols and coming up with these sort of amazing use cases that you never would have considered before happy to take any questions thanks so much no I thought that was a really really interesting talk and one of the previous slides of the earlier slides it seemed like the core problem with modularity is this idea of memory management in the proxy and delegate delegate contract spaces the solution you presented using the kachak hash to kind of like dynamically allocate the implementation address it seems like you
could kind of extend that out to do dynamic memory allocation in general has anybody thought about like a separate contract that could be used to dynamically allocate slots instead of depending on the compiler yeah so this actually does get used so I didn't really go into it because the thing I wanted to convey on the diamond pattern is not to use it but the diamond pattern generally when you use it you use this thing called diamond storage and the whole point of that is it's sort of like this library that Maps uh your variables to like weird slots and make sure about the different facets don't conflict so people have thought a lot about this um in in the context of proxies and and in a lot of other pretty interesting Concepts um but it's uh because of it it's still really easy to make the mistake of like if you just
accidentally allocate a variable which is something that you would very often do like you can completely break the system so I still sort of shy away from from using these techniques yeah it makes a lot of sense it reminds me the early days of like Malik versus like using the stack for for variables exactly thank you uh I wanted to ask um this maybe isn't related to the design patterns themselves but um more like around around tooling if you've seen anything uh that developers use to like kind of track all the across all these proxies and these contracts they're deploying like you you end up with like a ton of contracts on chain how do you track like what did you actually deploy what belongs to your organization and like is there anything that's out there to solve
that um unfortunately like not a ton a hard hat does have like or sorry uh open Zeppelin has like this uh upgrade uh Library it's written in like JavaScript or typescript and you can use it to manage your upgrades a little bit so it's really good for working with proxies but there isn't I think a great like generalized like thing for keeping track of all your contracts you can build it yourself uh when I worked at next we built one ourselves uh it was really very complicated like deployment process that like loaded addresses like in this like big mapping and like when a new one was deployed it would it would like update in it so you can build it I can link you to that that uh that that repo but I haven't seen anyone building a good sort of pre-built one thank you
hi Lewis from Shield when you talked about the modularity and like being able to approve certain modules um that are you know related to that transaction are there like instances where that gets a bit complicated and you have to like approve a bunch of modules like individually and like thinking of ux like you know how how is it so that you can do that and as a user like I don't have to think about that yeah I I think so in Zora's case they they skew like crazy towards like being as un-upgradable as possible so they do require every user to constantly approve it right but like if you think about other modularity cases for example our simple composition one with the verbs Dao the user doesn't need to be in the loop they
they buy their nft by the auction module they call the auction module and then it sort of seems to just work so the Zora is kind of special in that regards but you are kind of right that like it does get complicated when you have lots of modules with different interactions and different rules for who can do what you need to like sit down and really map the the authorizations that are out there and make sure that there is in fact like no way to produce a complicated like series of calls between modules that or a complicated upgrade that can sort of get around your corn variants awesome well thank you so much Noah that was great [Applause] [Music]