The introduction of ERC-6551, also known as Token Bound Accounts (TBAs), has significantly enhanced the capabilities of NFTs within the Ethereum and EVM ecosystem blockchains. This innovation marks a departure from the static JPEG era, as ERC-6551 opens up a whole new realm of possibilities for NFTs. Now, NFTs can take ownership of tokens, possess other NFTs, facilitate token transfers, and even authorize transactions on the blockchain. In this tutorial, we will guide you through the process of creating and deploying an ERC-721 NFT with a customized Token Bound Account. If you prefer a no-code approach for creating a TBA for your NFT, you can follow the instructions in this guide.
- A fundamental understanding of ERC-721 NFTs.
- Access to your wallet’s private key.
- Goerli testnet ETH.
- A fundamental understanding of Hardhat.
- Alchemy API Key for the Goerli testnet.
What You'll Learn:
Throughout this tutorial you will learn how to:
- Deploy an ERC-721, Token Bound Account, and Registry contract to the Goerli testnet.
- Create and mint an ERC-721 NFT.
- Implement custom functions on your Token Bound Account (TBA).
- Deploy and bind a custom TBA with your ERC-721 NFT.
- Interact with your custom TBA functions.
Overview of ERC-6551 Standard Implementation
Before we delve into the code, let's break down how an ERC-6551 is implemented and the relationships that exist between NFTs, NFT holders, token-bound accounts, and the Registry.
In the diagram above, you can see that a User Account (such as a Metamask wallet) owns an ERC-721 NFT (EIP-721 Contract A). This ERC-721 NFT, in turn, owns a token-bound account (i.e., Account A). A token bound account essentially functions as a smart contract wallet capable of holding funds and other NFTs. Token-bound accounts are constrained by the implementation logic defined in a separate smart contract (i.e., Implementation A) which the TBA will proxy.
In this tutorial, we will be adding custom logic to that implementation contract. Token bound accounts (i.e., Account A) are created by a contract called the Registry. The Registry serves as the single entry point for creating token-bound accounts and functions as a database, tracking the linkage between NFTs and their associated TBAs.
Now that we (hopefully!) have a clearer understanding of the implementation logic, let’s get to coding. You can view the full source code to this tutorial here.
Open a new terminal window and run these commands to create a new project folder and navigate into it.
Initialize an npm project with the following command. You'll be prompted to answer some questions.
Install hardhat as a development dependency.
In the same directory where you installed Hardhat, run the following command.
We will also need to install the following npm packages:
We almost have our hardhat project setup complete, but there are a few last things to do! This includes adding the correct hardhat configurations, environment variables and adjusting our file structure.
To update our hardhat configurations, go into your directory and locate <span class="code-inline">hardhat.config.js</span>. Replace the contents of this file with the code below.
Create a file called <span class="code-inline">.env</span> in your root directory and add the following values. Make sure you have a <span class="code-inline">.gitignore</span> set up in your project that includes the <span class="code-inline">.env</span> value.
Setting up our file structure. The hardhat project initialization will populate example files, we will not be using this code. Instead, go ahead and delete the following files (keep the folders) and add a new folder called interfaces to your project.
<div><span class="code-inline">- contracts/Lock.sol</span></div>
<div><span class="code-inline">- scripts/deploy.js</span></div>
<div><span class="code-inline">- test/Lock.js</span></div>
<div><span class="code-inline is--green">+ interfaces</span></div>
Great, now we can move on to the fun stuff…contracts!
Creating our Contracts
If you remember the implementation diagram from above, you may not be surprised to learn that our project will be comprised of three main contracts. To start, let’s generate the contract and interface files that we will need. Don’t worry too much about them now, as I will go into more detail throughout the tutorial.
Under the contracts folder, create the three following files:
<div><span class="code-inline is--green">+ ERC6551Registry.sol</span></div>
<div><span class="code-inline is--green">+ ERC6551Account.sol</span></div>
<div><span class="code-inline is--green">+ Pinnie.sol</span></div>
Under the interfaces folder, create the three following files:
<div><span class="code-inline is--green">+ IERC6551Registry.sol</span></div>
<div><span class="code-inline is--green">+ IERC6551Account.sol</span></div>
<div><span class="code-inline is--green">+ IERC6551Executable.sol</span></div>
Your file structure should now look something like this:
We’re going to breakdown the contract implementation into three parts: ERC721, Registry and Account. Let’s start with the ERC-721 token!
Before we can create a token bound account, we need something to bind it too. That is where the ERC-721 NFT comes into play, we’re going to create a Pinnie NFT! Navigate to the Pinnie.sol file and paste the following code:
This is a rather standard implementation of an ERC-721 token that extends the <span class="code-inline">ERC721URIStorage</span> contract. If you want to learn more about ERC-721 NFTs, I suggest checking out this tutorial. The extended <span class="code-inline">ERC721URIStorage</span> functionality allows you customize metadata for each minted token.
Now that we have our ERC-721 contract sorted out, let’s move on to the Registry contract.
The Registry acts similarly to a contract Factory with a few differences. According to the EIP-6551, the term Registry was selected to “emphasize the act of querying account addresses (which occurs regularly) over the creation of accounts (which occurs only once per account)”.
In most instances, you would not need deploy your own registry. The ERC-6551 Registry has already been deployed across several chains and their deployed addresses can be found here. However, you may want to deploy the Registry to an unsupported chain or interact with a TBA on a local node. Therefore, for the purpose of this tutorial, we will be deploying our own Registry 🚀.
The Registry contract must extend the <span class="code-inline">IERC6551Registry</span> declared in the EIP-6551, so head back to your project and locate the empty file <span class="code-inline">IERC6551Registry.sol</span> inside the interfaces folder. Insert the following code inside:
Now we want to create the Registry contract itself. Locate the <span class="code-inline">IERC6551Registry.sol</span> file and paste the following code inside:
There are a few things to point out in this contract.
First, notice that the contract extends the <span class="code-inline">IERC6551Registry</span> interface we added into our project. Ensure your file directory is correct or it will not be imported properly. Secondly, it implements two very important functions.
<span class="code-inline">createAccount</span>: Creates a token bound account for your ERC-721 NFT. If the account has already been created, it will return the token bound account address.
<span class="code-inline">account</span>: Retrieves the address of a previously created token-bound account for an NFT. This function bestows querying capabilities upon the contract, effectively transforming it into a database.
This contract is the main ERC-6551 account implementation. In the contract code featured below, we have the standard ERC-6551 implementation that was described in the EIP-6551, which has been extended to add our own custom logic to the token bound account. However, first we need to add the interfaces that the contract will inherit.
The Account contract will extend two interfaces, if you would like to learn more about the interface functionality, I suggest referencing the official EIP-6551. But for now, locate <span class="code-inline">IERC6551Executable.sol</span> and <span class="code-inline">IERC6551Account.sol</span> in the interfaces folder and insert the following code.
Finally, let’s add the token bound account contract logic. Locate <span class="code-inline">ERC6551Account.sol</span> in the contracts folder and insert the following code.
Customizing our Account contract
Part of the magic of smart contract wallets is that you can write your own custom logic into the code. So, that is what we are going to do! We’re going to keep our custom logic brief for simplicity purposes, but you can extend this contract however you see fit.
Let's customize our TBA by giving it a unique account name, like 'Pinnie's Savings Account'. In the <span class="code-inline">ERC6551Account.sol</span> contract, we've introduced a new variable named <span class="code-inline">accountName</span> to capture this customized state. Go ahead and add the following variable to your <span class="code-inline">ERC6551Account.sol</span> contract.
But, we also want to be able to set and modify this account name. To achieve this, we're implementing both getter and setter functions. With these additions, you have the flexibility to personalize your TBA's account name as you see fit. The possibilities are endless! Go ahead and add the following functions to your <span class="code-inline">ERC6551Account.sol</span> contract.
The above customization is simple, but you have the freedom to further enhance and customize your smart contract wallet as you see fit. By adding new functions or modifying existing ones, you can tailor its behavior to your specific needs. The versatility of smart contract wallets empowers you to implement any custom logic or functionality, making your TBA truly unique and adaptable to your requirements.
Compile your contracts
Before we can deploy our contracts to the goerli testnet, we must first compile them. Run the following command.
Deploying contracts to Goerli
It's time to level up and deploy our contracts to the Goerli testnet! 🎉 Although we'll be focusing on testnet deployment for the sake of brevity in this tutorial, it's worth noting that you can also deploy these contracts in Remix or within a local Hardhat environment.
Locate the scripts folder and create a script name <span class="code-inline">deploy.js</span>, insert the following code.
This script deploys three smart contracts, "Pinnie," "ERC6551Account," and "ERC6551Registry," to the Ethereum blockchain on the goerli testnet, and then logs their respective deployment addresses.
Ready to deploy? Make sure your private key and wallet address are set in the .env file and you have sufficient Goerli testnet ETH. Run the following command.
If all goes well, you should see similar output in your terminal.
We will need these values for our upcoming scripts, so go ahead and store these in your .env file with the following variable names.
Minting your ERC-721 NFT
Before we can create a token bound account, we must first mint an ERC-721 NFT, aka Pinnie 💜. Please ensure that your <span class="code-inline">PINNIE_ADDRESS</span> environment variable is set before trying to move forward with this tutorial.
Minting the NFT
In your scripts folder, create another file named <span class="code-inline">mint.js</span> and insert the following code. If you want to customize your NFT metadata, use your Pinata account, or create a free one, to pin custom content to IPFS and replace the baseURI in the script below.
Run the script.
If all goes as expected you should see an output similar to this:
Create the Token Bound Account
We have our ERC-721 minted and we are finally ready to create a token bound account for it! We’re going to do this by create yet another script. Locate your scripts folder and create a new file named <span class="code-inline">createAccount.js</span> Insert the following code.
Note: make sure to adjust the token id to match the token id that was minted through your <span class="code-inline">mint.js</span>.
In this script we are fetching the <span class="code-inline">Registry.sol</span> contract and calling the <span class="code-inline">createAccount</span> function, this is creating the token bound account and binding it to our ERC-721. We wait for a complete transaction before calling <span class="code-inline">account</span>, the second function on <span class="code-inline">Registry.sol</span>. This function is not necessary to create the account, but we are calling it to ensure an address is being returned properly.
Run the script
If all goes well you should see an output similar to this:
Congrats! You’ve now created an ERC-721 NFT with a custom token bound account 🎉
Interacting with your TBA
The last step of this tutorial is to interact with our token bound account and call our custom function <span class="code-inline">setAccountName</span>.
Once again, locate your scripts folder and create a file named <span class="code-inline">interactAccount.js</span>. Insert the following code and replace the <span class="code-inline">tokenBoundAccount</span> variable with the token bound account address generated in the <span class="code-inline">createAccount.js</span> script.
This script gets the token bound account contract using ether.Contract and passes in the deployed address, abi (produced from hardhat compiler), and a signer. Once we have the token bound account contract we can call the setAccountName function to adjust our account name to whatever we would like.
After adjusting the token bound account address, go ahead and try running the script.
If all goes well you should see an output similar to this:
This tutorial has equipped you with the knowledge and tools necessary to create and deploy an ERC-721 with a custom ERC-6551 token bound account, offering you the ability to explore and harness the full potential of this groundbreaking standard. ERC-6551 opens the door to a new era of functionality and creativity within the NFT space. Stay tuned for more tutorials to come!