Blog home

How to Deploy a Custom Implementation of ERC-6551

Paige Jones

Published on

20 min read

How to Deploy a Custom Implementation of ERC-6551

Dive into the creation and deployment of ERC-721 NFTs with custom ERC-6551 token bound accounts, exploring the synergy between IPFS pinning and NFT storage.

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.

Prerequisites:

  • 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

ℹ️Find the source of the documentation right here

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.

Getting Started

Open a new terminal window and run these commands to create a new project folder and navigate into it.

mkdir erc6551-tutorial && cd erc6551-tutorial

Initialize an npm project with the following command. You'll be prompted to answer some questions.

npm init

Install hardhat as a development dependency.

npm install --save-dev hardhat

In the same directory where you installed Hardhat, run the following command.

npx hardhat

ℹ️Note: You’ll be presented with some template options for the project. For the purposes of this tutorial, select “Create a Javascript project”. You may hit enter through the remaining questions and accept the defaults.

We will also need to install the following npm packages:

npm i @openzeppelin/contracts dotenv ethers

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 hardhat.config.js. Replace the contents of this file with the code below.

require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config();


module.exports = {
  solidity: "0.8.20",
  networks: {
		//Add extra chains as needed 
    hardhat: {
      chainId: 1337,
    },
      goerli: {
        url: `${process.env.ALCHEMY_GOERLI_URL}`,
        accounts: [`0x${process.env.PRIVATE_KEY}`],
      }, 
  }, 
  defaultNetwork: "hardhat",
};

hardhat.config.js

Create a file called .env in your root directory and add the following values. Make sure you have a .gitignore set up in your project that includes the .env value.

# Ethereum Wallet and Private Key Configuration
WALLET_ADDRESS=<YOUR_WALLET_ADDRESS>
PRIVATE_KEY=<YOUR_PRIVATE_KEY>

# Alchemy Goerli API Configuration
ALCHEMY_GOERLI_URL="https://eth-goerli.g.alchemy.com/v2/<YOUR_API_KEY>"
ALCHEMY_GOERLI_API_KEY=<YOUR_API_Key>

.envℹ️Note: It is recommended to use a private key and wallet address from an account where you do not hold your primary assets.

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.

<span class="code-inline__wrp">

<div>- contracts/Lock.sol</div>
<div>- scripts/deploy.js</div>
<div>- test/Lock.js</div>
<div><span class="code-inline is--green">+ interfaces</span></div>

</span>

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:

<span class="code-inline__wrp">

<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>

</span>

Under the interfaces folder, create the three following files:

<span class="code-inline__wrp">

<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>

</span>

Your file structure should now look something like this:

ERC6551-tutorial
├── contracts
│   ├── ERC6551Account.sol
│   ├── ERC6551Registry.sol
│   └── Pinnie.sol
├── interfaces
│   ├── IERC6551Account.sol
│   ├── IERC6551Executable.sol
│   └── IERC6551Registry.sol
├── node_modules
├── scripts 
├── test
├── .env
├── .gitignore
├── hardhat.config.js
├── package-lock.json
└── package.json

We’re going to breakdown the contract implementation into three parts: ERC721, Registry and Account. Let’s start with the ERC-721 token!

ERC-721 NFT

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:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract Pinnie is ERC721, ERC721URIStorage, Ownable {
    uint256 public _tokenId;
    string private _baseTokenURI;

    constructor(string memory baseURI)
        ERC721("Pinnie", "PIN")
        Ownable(msg.sender)
    {
        _baseTokenURI = baseURI;
    }

    function safeMint(address to, string memory uri) public onlyOwner {
        uint256 newTokenId = _tokenId++;
        _safeMint(to, newTokenId);
        _setTokenURI(newTokenId, uri);
    }

  //Returns the id of the next token without having to mint one.
    function nextId() external view returns(uint256) {
        return _tokenId;
    }

    function _baseURI() internal view override returns (string memory) {
        return _baseTokenURI;
    }

    function setBaseURI(string memory baseURI) public onlyOwner {
        _baseTokenURI = baseURI;
    }

    // The following functions are overrides required by Solidity.
    function tokenURI(uint256 tokenId)
        public
        view
        override(ERC721, ERC721URIStorage)
        returns (string memory)
    {
        return super.tokenURI(tokenId);
    }

    function supportsInterface(bytes4 interfaceId)
        public
        view
        override(ERC721, ERC721URIStorage)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }
}

Pinnie.sol

This is a rather standard implementation of an ERC-721 token that extends the ERC721URIStorage contract. If you want to learn more about ERC-721 NFTs, I suggest checking out this tutorial. The extended ERC721URIStorage functionality allows you customize metadata for each minted token.

Registry

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 🚀.


Interfaces

The Registry contract must extend the IERC6551Registry declared in the EIP-6551, so head back to your project and locate the empty file IERC6551Registry.sol inside the interfaces folder. Insert the following code inside:

// SPDX-License-Identifier: UNLICENSED

pragma solidity ^0.8.9;

interface IERC6551Registry {
    /**
     * @dev The registry SHALL emit the AccountCreated event upon successful account creation
     */
    event AccountCreated(
        address account,
        address indexed implementation,
        uint256 chainId,
        address indexed tokenContract,
        uint256 indexed tokenId,
        uint256 salt
    );

    /**
     * @dev Creates a token bound account for a non-fungible token.
     *
     * If account has already been created, returns the account address without calling create2.
     *
     * If initData is not empty and account has not yet been created, calls account with
     * provided initData after creation.
     *
     * Emits AccountCreated event.
     *
     * @return the address of the account
     */
    function createAccount(
        address implementation,
        uint256 chainId,
        address tokenContract,
        uint256 tokenId,
        uint256 salt,
        bytes calldata initData
    ) external returns (address);

    /**
     * @dev Returns the computed token bound account address for a non-fungible token
     *
     * @return The computed address of the token bound account
     */
    function account(
        address implementation,
        uint256 chainId,
        address tokenContract,
        uint256 tokenId,
        uint256 salt
    ) external view returns (address);
}

IERC6551Registry.sol


Contract

Now we want to create the Registry contract itself.  Locate the IERC6551Registry.sol file and paste the following code inside:

// SPDX-License-Identifier: UNLICENSED

pragma solidity ^0.8.9;

import "@openzeppelin/contracts/utils/Create2.sol";
import "../interfaces/IERC6551Registry.sol";

library ERC6551BytecodeLib {
    function getCreationCode(
        address implementation_,
        uint256 chainId_,
        address tokenContract_,
        uint256 tokenId_,
        uint256 salt_
    ) internal pure returns (bytes memory) {
        return
            abi.encodePacked(
                hex"3d60ad80600a3d3981f3363d3d373d3d3d363d73",
                implementation_,
                hex"5af43d82803e903d91602b57fd5bf3",
                abi.encode(salt_, chainId_, tokenContract_, tokenId_)
            );
    }
}

contract ERC6551Registry is IERC6551Registry {
    error AccountCreationFailed();

    function createAccount(
        address implementation,
        uint256 chainId,
        address tokenContract,
        uint256 tokenId,
        uint256 salt,
        bytes calldata initData
    ) external returns (address) {
        bytes memory code = ERC6551BytecodeLib.getCreationCode(
            implementation,
            chainId,
            tokenContract,
            tokenId,
            salt
        );

        address _account = Create2.computeAddress(bytes32(salt), keccak256(code));

        if (_account.code.length != 0) return _account;

        emit AccountCreated(_account, implementation, chainId, tokenContract, tokenId, salt);

        assembly {
            _account := create2(0, add(code, 0x20), mload(code), salt)
        }

        if (_account == address(0)) revert AccountCreationFailed();

        if (initData.length != 0) {
            (bool success, bytes memory result) = _account.call(initData);

            if (!success) {
                assembly {
                    revert(add(result, 32), mload(result))
                }
            }
        }

        return _account;
    }

    function account(
        address implementation,
        uint256 chainId,
        address tokenContract,
        uint256 tokenId,
        uint256 salt
    ) external view returns (address) {
        bytes32 bytecodeHash = keccak256(
            ERC6551BytecodeLib.getCreationCode(
                implementation,
                chainId,
                tokenContract,
                tokenId,
                salt
            )
        );

        return Create2.computeAddress(bytes32(salt), bytecodeHash);
    }
}

ERC6551Registry.sol

There are a few things to point out in this contract.

First, notice that the contract extends the IERC6551Registry 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.

createAccount: 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.

account: 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.

ERC6551 Account

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.

Interfaces

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 IERC6551Executable.sol and IERC6551Account.sol in the interfaces folder and insert the following code.

// SPDX-License-Identifier: UNLICENSED

pragma solidity ^0.8.9;

/// @dev the ERC-165 identifier for this interface is `0x74420f4c`
interface IERC6551Executable {
    /**
     * @dev Executes a low-level operation if the caller is a valid signer on the account
     *
     * Reverts and bubbles up error if operation fails
     *
     * @param to        The target address of the operation
     * @param value     The Ether value to be sent to the target
     * @param data      The encoded operation calldata
     * @param operation A value indicating the type of operation to perform
     *
     * Accounts implementing this interface MUST accept the following operation parameter values:
     * - 0 = CALL
     * - 1 = DELEGATECALL
     * - 2 = CREATE
     * - 3 = CREATE2
     *
     * Accounts implementing this interface MAY support additional operations or restrict a signer's
     * ability to execute certain operations
     *
     * @return The result of the operation
     */
    function execute(
        address to,
        uint256 value,
        bytes calldata data,
        uint256 operation
    ) external payable returns (bytes memory);
}

IERC6551Executable.sol

// SPDX-License-Identifier: UNLICENSED

pragma solidity ^0.8.9;

/// @dev the ERC-165 identifier for this interface is `0x6faff5f1`
interface IERC6551Account {
    /**
     * @dev Allows the account to receive Ether
     *
     * Accounts MUST implement a `receive` function.
     *
     * Accounts MAY perform arbitrary logic to restrict conditions
     * under which Ether can be received.
     */
    receive() external payable;

    /**
     * @dev Returns the identifier of the non-fungible token which owns the account
     *
     * The return value of this function MUST be constant - it MUST NOT change
     * over time
     *
     * @return chainId       The EIP-155 ID of the chain the token exists on
     * @return tokenContract The contract address of the token
     * @return tokenId       The ID of the token
     */
    function token()
        external
        view
        returns (
            uint256 chainId,
            address tokenContract,
            uint256 tokenId
        );

    /**
     * @dev Returns a value that SHOULD be modified each time the account changes state
     *
     * @return The current account state
     */
    function state() external view returns (uint256);

    /**
     * @dev Returns a magic value indicating whether a given signer is authorized to act on behalf of the account
     *
     * MUST return the bytes4 magic value 0x523e3260 if the given signer is valid
     *
     * By default, the holder of the non-fungible token the account is bound to MUST be considered a valid
     * signer
     *
     * Accounts MAY implement additional authorization logic which invalidates the holder as a
     * signer or grants signing permissions to other non-holder accounts
     *
     * @param  signer     The address to check signing authorization for
     * @param  context    Additional data used to determine whether the signer is valid
     * @return magicValue Magic value indicating whether the signer is valid
     */
    function isValidSigner(address signer, bytes calldata context)
        external
        view
        returns (bytes4 magicValue);
}

IERC6551Account.sol


Contract

Finally, let’s add the token bound account contract logic. Locate ERC6551Account.sol in the contracts folder and insert the following code.

// SPDX-License-Identifier: UNLICENSED

pragma solidity ^0.8.9;

import "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/interfaces/IERC1271.sol";
import "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";
import  "../interfaces/IERC6551Account.sol";
import  "../interfaces/IERC6551Executable.sol";

contract ERC6551Account is IERC165, IERC1271, IERC6551Account, IERC6551Executable {
    uint256 public state;

    receive() external payable {}

    function execute(
        address to,
        uint256 value,
        bytes calldata data,
        uint256 operation
    ) external payable returns (bytes memory result) {
        require(_isValidSigner(msg.sender), "Invalid signer");
        require(operation == 0, "Only call operations are supported");

        ++state;

        bool success;
        (success, result) = to.call{value: value}(data);

        if (!success) {
            assembly {
                revert(add(result, 32), mload(result))
            }
        }
    }



    function isValidSigner(address signer, bytes calldata) external view returns (bytes4) {
        if (_isValidSigner(signer)) {
            return IERC6551Account.isValidSigner.selector;
        }

        return bytes4(0);
    }

    function isValidSignature(bytes32 hash, bytes memory signature)
        external
        view
        returns (bytes4 magicValue)
    {
        bool isValid = SignatureChecker.isValidSignatureNow(owner(), hash, signature);

        if (isValid) {
            return IERC1271.isValidSignature.selector;
        }

        return "";
    }

    function supportsInterface(bytes4 interfaceId) external pure returns (bool) {
        return (interfaceId == type(IERC165).interfaceId ||
            interfaceId == type(IERC6551Account).interfaceId ||
            interfaceId == type(IERC6551Executable).interfaceId);
    }

    function token()
        public
        view
        returns (
            uint256,
            address,
            uint256
        )
    {
        bytes memory footer = new bytes(0x60);

        assembly {
            extcodecopy(address(), add(footer, 0x20), 0x4d, 0x60)
        }

        return abi.decode(footer, (uint256, address, uint256));
    }

    function owner() public view returns (address) {
        (uint256 chainId, address tokenContract, uint256 tokenId) = token();
        if (chainId != block.chainid) return address(0);

        return IERC721(tokenContract).ownerOf(tokenId);
    }

    function _isValidSigner(address signer) internal view returns (bool) {
        return signer == owner();
    }
}

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 ERC6551Account.sol contract, we've introduced a new variable named accountName to capture this customized state. Go ahead and add the following variable to your ERC6551Account.sol contract.

contract ERC6551Account is IERC165, IERC1271, IERC6551Account, IERC6551Executable {
    uint256 public state;
    bytes32 public accountName;

    receive() external payable {}

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 ERC6551Account.sol contract.

contract ERC6551Account is IERC165, IERC1271, IERC6551Account, IERC6551Executable {
    uint256 public state;
    bytes32 public accountName;

    receive() external payable {}

    function setAccountName(bytes32 newName_) public {
        require(_isValidSigner(msg.sender), "Invalid signer");
        accountName = newName_;
    }

    function getAccountName() view public returns (bytes32) {
        return accountName;
    }

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.

npx hardhat compile

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 deploy.js, insert the following code.

require("hardhat");

async function main() {
  const Pinnie = await ethers.deployContract("Pinnie");
  const pinnie = await Pinnie.waitForDeployment();
 

  const Account = await ethers.deployContract("ERC6551Account");
  const account = await Account.waitForDeployment();

  const Registry = await ethers.deployContract("ERC6551Registry");
  const registry = await Registry.waitForDeployment();

  console.log("Pinnie contract deployed at:", pinnie.target);
  console.log("Account contract deployed at:", account.target);
  console.log("Registry contract deployed at:", registry.target);
}

// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

deploy.js

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.

npx hardhat run --network goerli scripts/deploy.js

If all goes well, you should see similar output in your terminal.

Pinnie contract deployed at: 0x56068cB8f2FEC5C69801b70f0ACC95E2692FE357
Account contract deployed at: 0x69C3f2D30ceC7EeBD618D2141A0dA4255b9DdC2a
Registry contract deployed at: 0x003D0748F31C37Dba236C10A41705dF8a2916F59

Note: these are sample addresses, the addresses of your deployed contracts will be different.

We will need these values for our upcoming scripts, so go ahead and store these in your .env file with the following variable names.

PINNIE_ADDRESS="your address here"
ERC6551ACOUNT_ADDRESS="your address here"
ERC6551REGISTRY_ADDRESS="your address here"

.env

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 PINNIE_ADDRESS environment variable is set before trying to move forward with this tutorial.

Minting the NFT

In your scripts folder, create another file named mint.js 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.

require("hardhat");
require("dotenv").config();

async function main() {
  // Get the contract instance
  const Pinnie = await ethers.getContractFactory("Pinnie");
  const pinnie = await Pinnie.attach(process.env.PINNIE_ADDRESS);
  tokenId = await pinnie.nextId()
  //Default IPFS hash for Pinnie json metadata. Replace with your own if desired. 
  const baseURI = "ipfs://QmTRxBoLapSUgAiaz2FxvQYW2ektgJnhoomzaQ8Q76puvA"
 

  //Address you want to mint your NFT to
  const to = process.env.WALLET_ADDRESS
  // Mint token
  const tx = await pinnie.safeMint(to, baseURI);

  // Wait for the transaction to be mined
  const receipt = await tx.wait();

  // Log the transaction details
  console.log("Transaction hash:", receipt.hash);
  console.log("Gas used:", receipt.cumulativeGasUsed);

  // Check if the transaction was successful (status 1)
  if (receipt.status === 1) {
    console.log(`Transaction was successful. Token ${tokenId} minted to ${to}`);
  } else {
    console.log("Transaction failed.");
  }

}

main().catch((error) => {
  console.error(error);
  process.exit(1);
});

mint.js

Run the script.

npx hardhat run --network goerli scripts/mint.js

If all goes as expected you should see an output similar to this:

Transaction hash: 0xd76990171883cb946e5061e156921868b9975391a5dde6767cc04adfa6683e1c
Gas used: 7938813n
Transaction was successful. Token 0 minted to 0x9B7c18a71a98acD2f1271e2D1fe63750A70bC52B

ℹ️Note: Keep this token id on hand, we will be using it in our next script!

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 createAccount.js Insert the following code.

Note: make sure to adjust the token id to match the token id that was minted through your mint.js.

require("hardhat");
require("dotenv").config();


async function main() {
  const Registry = await ethers.getContractFactory("ERC6551Registry");
  const registry = await Registry.attach(process.env.ERC6551REGISTRY_ADDRESS);
  //update salt for a more secure hash
  const salt = 0;  
  const implementation = process.env.ERC6551ACOUNT_ADDRESS
  const tokenAddress = process.env.PINNIE_ADDRESS;
  //replace with tokenId minted in scripts/mint.js, logged on the CLI
  const tokenId = 0
  const chainID = 5 //goerli
  const initData = "0x";

  const tx = await registry.createAccount(implementation, chainID, tokenAddress, tokenId, salt, initData);
  const receipt = await tx.wait();
  const address = await registry.account(implementation, chainID, tokenAddress, tokenId, salt)
  
  if(receipt.status == 1 && address){
   console.log("Account created successfully at address: ", address);
  }
   else{
    console.log("Account creation failed");
  }

}

main().catch((error) => {
  console.error(error);
  process.exit(1);
});

createAccount.js

In this script we are fetching the Registry.sol contract and calling the createAccount function, this is creating the token bound account and binding it to our ERC-721. We wait for a complete transaction before calling account, the second function on Registry.sol. 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

npx hardhat run --network goerli scripts/createAccount.js

If all goes well you should see an output similar to this:

Account created successfully at address:  0x6e2f2D13E8021af92df8B65394094ae2848f3d2e

ℹ️Note: keep this deployed token bound account address on hand, we will be using it in our next script!

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 setAccountName.

Once again, locate your scripts folder and create a file named interactAccount.js. Insert the following code and replace the tokenBoundAccount variable with the token bound account address generated in the createAccount.js script.

const {ethers} = require("hardhat");
require("dotenv").config();
const artifact = require("../artifacts/contracts/ERC6551Account.sol/ERC6551Account.json")

async function main() {
    const provider = new ethers.AlchemyProvider(
        "goerli",
        process.env.ALCHEMY_GOERLI_API_KEY
      );
    const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider);
    //insert your TBA address here from createAccount.js
    const tokenBoundAccount = "your tba address here"
    const tba = new ethers.Contract(tokenBoundAccount, artifact.abi, signer)


    const newName = ethers.encodeBytes32String("Pinnie's Savings Account")
    const tx =  await tba.setAccountName(newName);
    await tx.wait();
    const accountName =  await tba.getAccountName();
    console.log("New Account Name: ", ethers.decodeBytes32String(accountName))

  }
  
  main().catch((error) => {
    console.error(error);
    process.exit(1);
  });

interactAccount.js

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.

npx hardhat run --network goerli scripts/interactAccount.js

If all goes well you should see an output similar to this:

New Account Name:  Pinnie's Savings Account

Conclusion

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!

Stay up to date

Join our newsletter for the latest stories & product updates from the Pinata community.

No spam, notifications only about new products, updates and freebies. You can always unsubscribe.