New Ethereum talks, every Monday. The week's conference uploads by event, in your inbox.

Loading player…

Powerful On-Chain Extensions for Tokens and NFTs with Edward Penta

Ethereum DenverMon, Oct 9, 2023, 12:00 AM

The Universal Token is a smart-contract framework for creating customisable tokens. Tokens created following the framework are composed of a Token contract to which one or multiple Extension contracts can be connected. The workshop will go through how a developer can deploy a NFT token with these Extension contracts today, and how the developer can go about making their own extension contract. Edward Penta Blockchain Engineer Consensys https://twitter.com/eddikek

Transcript

immutability. You know, you want sometimes you want immutability in your smart contract, sometimes you want them to be upgradeable and it's sort of a black and white. And complexity, you want to add lots of functionality to your smart contract. And you can really only choose two of these, right? So, if you add a lot of complexity and make it, you know, mutable, then you kind of have this like size limit where if you make it complex, then you reach the size limit.

You kind of have this like weird problem. You see is limited by 24 kilobytes in size. Um contract sometimes smart contracts want to be immutable and you there should be some fine grain control of like what's mutable and what's immutable, right? And futurerich smart contracts that are complex oftent times have like big big size contracts. So you want to spread them out but now you have this complex smart contract architecture of like different smart contracts talking to each other and it gets really really complex.

Right? So the solution that we designed is called universal token and the way it works is very opinionated towards tokens such as NFTs and ESC20 tokens and it allows both upgradability and token extendability. What that means is that I can upgrade the token logic as if it's an upgradeable contract. We can also add plugins to reuse uh functionality that I might have had in other tokens or NFTs. For example, I can add custom functionality.

I can deploy the extension once and use it in all my tokens. Um, and I can listen and the plugin can listen to token events on chain. And best of all, these plugins are upgradeable as well. Um, so this is sort of a diagram to show you like what I'm kind of talking about. So in the on the left here, this is kind of like what we have now.

So we have a token A, token B. They both are upgradeable. So they have a token logic contract as well, but they both have an allow and blockless feature that's isolated inside of the logic contract, right? And we wanted If we were managing both of these tokens and we wanted to like add a feature, we'd have to add it to both tokens and we would have to like manage the code for both because they're going to be slightly different. Whereas with upgradeable and extensions, we have token A, token B, they might have their own independent logic contracts, but they're sharing the same functionality for the plugin.

So like the allow list is going to be the same between token A and B and block listing is going to be the same between token A and B. This makes it more reusable, more maintainable and allows for smaller logic contracts because now the logic contract to just like the core token functionality rather than just bloating it with all this additional functionality that we want to have for our token. So how does this work? So the way this works is that it sort of uses the proxy pattern but on steroids, right? So the way it works is we deploy the deployer registers plugins or extensions to the universal token.

A new extension proxy is deployed for that extension instance. The function selectors for that extension are added to the token prox token contract. That way the user can interact with the token proxy uh as if the extension is part of the token and the roles are granted to the extension. Uh if the extension needs specific roles like minting rights or admin rights or allow list rights to give a little better diagram this is how this is how it looks like. So you have token contract A and B token A and B.

They both have an extension proxy that's independent to themselves, but they're both delegate calling to the same smart contract extension, and that allows us to reuse this code a lot more easily. Now, the user can interact directly with the token and call the extension function, or they can interact directly with the extension proxy as well. Both options are perfectly valid in this architecture. Um, it it's smart enough to know that where the call is coming from. Um, the RO system, the reason why we did this because we wanted to have a permission role in the system.

We don't want to have extensions have like unlimited access to storage. We have this I guess separation here between the token and the extension because we don't want to give untethered access to token storage to plugins, right? We want to have them be granted roles. Um so some default roles are like mentor roles, manager role, control transfers, admin roles and before a extension is registered you can see on chain what these roles are. So that way you're aware of like okay this plugin requires me to writes I and that's it.

I'm confident that I I know what this plugin is doing. So yeah um all required roles are visible on train and the there are some function modifiers in the extension API that allow you to benefit from these roles. So I'm going to show you how to build an extension super quickly. So first step is we have write some boiler plate. Um so we're going to write us a new smart contract.

We're going to make some imports for token extension and the transfer data. We're going to inherit from token extension and then we'll make a initialize function. So the token extension base contract offers is the base of all token extensions. It offers a extension API to do things like require ro set metadata listen for events and things like that. Um, and once an extension is registered to a new token, the initialize function will be called just as if it's an upgradeable contract.

Like when you deploy an upgradeable contract, the initialization function is called the same cases here as well. Next, we define the logic for our extension. So in this case, I'm making a soulbound extension. I want an NFT and the NFT has the soulbound option. So it can't be transferred if you mark it as soulbound.

Um, so this extension looks pretty simple. We have a mapping for soulbounded tokens. We have a function for if a fun if a token is soulbound we can query for that. We have a function that allows you to soul bind your own tokens so you can't transfer them. And then we have this on transfer function that will be that will check if a token is uh soulbound or not.

If it is then it will revert the whole transaction. Um the initialize function I kind of hit here just for the sake of just showing the logic here. This is extensions can define their own state because they're isolated inside of their own proxy. Um and yeah and in this case um the the key thing change here is that we have to use underscore message sender instead of message.cender because underscore message sender is doing some magic behind the scenes to tell view who the real caller of this um of the function is.

So like right here we have the require message sender equals this. This is we're just making sure they the the person who is sbounding the token is the owner of that token. Uh this is like a big bigger view. I don't know how Yeah. Next, we'll define some event listeners.

We want to know when a token transfer happens. So, we can call our callback function. So, in our initialize function, we just simply invoke listen for token transfers. We give it the function um solidity 0.8 added function uh types.

So now we can just specify this on transfer. We don't have to do anything like weird or anything. It automatically knows that this function right here at the bottom is the one to call when token transfers occur. Extension can do whatever they want. inside of a callback they can modify state.

They can check for state and do reverts. The only thing they can't do is do a transfer again because then it becomes recursive, right? Unless it's like unless you plan it out, right? But that's just the only limitation is if you do a transfer without really thinking about it, then you can end up in this recursive uh callback. And then lastly, we have to define the extensions metadata inside of the constructor.

We do it in the constructor because then it becomes visible on chain, right? So we can define what function selectors that this plugin has. We can define what token standards this plugin supports. We set a package name and version for upgradeable upgradability sake and for tracking. And we can set an interface label for introspection into the extension.

Now this is also a place where you can define what roles are required for the extension. So I can say require underscore require ro and then put the mentor role or the admin role. for this extension we don't need a role because we're just doing so bound stuff. Um yeah and all these all this metadata is visible on chain because we do it in the constructor and finally the extension is complete. So this is what the uh oh this is not my laptop but um I guess I can go back.

So this is what the full extension looks like but just imagine that the initialize is visible. Um, and then I was going to show you a video, but I don't have my laptop working. But I can try. You guys want to come close? I can show you a video of this working live.

I was going to plug it into this, but it's not working. But, uh, yeah. Um, you all want to come close and I can show you this as best I can. So, what I'm going to show you is this. This is the demo app that comes with the Universal token repo.

Um, I'm going to go play. Um, so this demo app is part of the repo. So, you can go ahead and get started on this right away. It allows you to deploy tokens and manage those tokens and those extensions. So right here I'm just connecting to the the demo app.

So right now I'm going to deploy a new NFT. I'll call it a soulbound extent soulbound NFT. I forgot what I call it. SBN. Yep.

Set the options and I'm going to deploy a new logic contract. Um this is required because you know it's a it's a upgradeable contract as well. Um so we're going to go ahead and deploy one. Um, and then once it deploys, it deployed. So now we're going to deploy the token.

So once we have the token all set up and configured, we're going to go ahead and deploy the token. Once that's done, sorry, I'm just trying to skip forward because it takes forever. Once that's done, we can see it on Etherscan. Um, Etherscan shows it as a proxy contract. So, we can specify and tell Ether Scan, oh, this is a proxy.

You should, you know, look at the proxy implementation. That way, we can see all the functions. So, I'm going to go ahead and do that real quick. So we did that. So now Etherscan recognizes it as a proxy contract and we can see all the functions.

So we have all the normal NFT functions like balance of things like that. We can have all the normal right functions as well. So now let's go ahead and deploy or register our extension. Um I think at some point I'm going to go So, we're going to go back. We're going to go to manage extent, manage token.

We're going to put in the new token we just deployed into this uh little text box here. And then you can see now we can add register extensions. We can upgrade the token logic. And then here you can see I have the extension already deployed on chain. You can see the code.

Um you could see the functions if you want and query them. But right now we're just going to register. So, I'm going to put it in this little box, but I just do extension. It's going to come up here and it's going to say, "Hey, you want to confirm." We're going to hit confirm.

And it's going to take a little bit. And then once it's registered, we'll be able to see the extension proxy on chain and then we can interact with the extension directly on Ethere. I'm going to fast forward a little bit if I can. Yep. So, this is the extension on chains.

Now, this is also proxy. So we have to let Etherscan know, hey, this is a proxy contract. Go ahead and look at the implementation. So that way you can see what's in there. Etherscan's like, yep, that is indeed in proxy.

And then now we can see our extension functions. Now we can see we have is soulbound I think is in here. Yep. Is soulbound. Yep.

And then we can see right as proxy and we can see now we have the functions soulbind and the other ones as well. So now let's go to function a token. Let's go ahead and do a transfer just to make sure that you know it works right at this point. Nothing is cellbound. So we should be able to mint and transfer tokens normally.

So I think right now I'm going to mint a token because there are no tokens minted. I have to connect. That's right. Let's go ahead and admit that. We'll fast forward a little bit just so we're not waiting for ecan to test net to mine.

So now that we've minted the token, let's go ahead and We'll bind it. Oh, well before we still bind it, we should probably check to see if transfers work, right? So, we should be able to transfer this MetaMask should an error. So, let's do a transfer first just to for sanity check, you know. So, let's get a different address here that we're going to transfer to.

Rolling. Rolling.

Automatic transcript — names and jargon may be misspelled.