Build Your Own ERC-404 Token

Discover the new token standard ERC-404 as it redefines the NFT ecosystem. Learn how to create your own ERC-404 token and explore its innovative features.
Build Your Own ERC-404 Token

On this page

What is ERC 404?

In February, Pandora team unveiled ERC 404, a new experimental token standard introduced to redefine the landscape of digital assets. This new standard combines the best of both ERC-20 and ERC-721, creating a hybrid token with characteristics of fungible and non-fungible tokens.

ERC 404 tokens have a distinct identity as non-fungible tokens, aka NFTs, enabling ownership of assets with unique characteristics. At the same time, they preserve the divisible property of ERC-20 tokens, ensuring seamless trading and interoperability within DeFi ecosystems. This dual nature of ERC 404 broadens the utility of this type of asset.

Importance of ERC 404

Presently, the NFT ecosystem has a high barrier to entry for users and investors. This challenge primarily arises from requiring investors to acquire the complete asset, a necessity because of the ERC-721 characteristics. This stops users from investing in high-priced NFTs, consequently decreasing the asset's liquidity. 

ERC-404 represents a significant advancement in the blockchain and NFT ecosystem, effectively bridging the gap for users by offering them the opportunity to invest in premier NFTs without needing to acquire the entire asset. Since the fractions of the NFT are represented by the respective ERC-20 token, it increases the liquidity for the NFTs and opens the door for more exciting DeFi opportunities. 

How does ERC 404 work?

ERC 404, at its core, utilizes the ERC-20's divisibility to collectively represent the NFT. Let's say we have holder A, who starts with 0 coins, and 1 NFT equals 1 whole coin. If holder A gets 1 or more whole coins after a transfer, the contract first checks the contract bank. If the bank is empty, meaning no NFTs were burned, a new NFT is minted for the user. However, if NFT IDs are in the bank(representing burned NFTs), the contract transfers 1 NFT from the bank to the user.

The contract bank serves as a queue where the IDs of burned NFTs are stored. If a user has 1 whole coin but ends up with less than 1 whole coin after a transfer, the contract burns the associated NFT and adds its ID to the contract bank queue for future use. This mechanism ensures efficient management of NFTs and maintains the balance of coins and NFTs within the system. In the event of a token transfer, three distinct scenarios are considered.

  1. If both transferor and recipient don’t have enough fractions to constitute the NFT after transfer, only ERC-20 tokens are transferred to the recipient. 
  2. If the transferor doesn’t have enough fractions for NFT, but after the transfer, the recipient will have enough fractions to constitute the NFT, then an NFT is minted for the recipient along with the transferred ERC-20 tokens.
  3. When the transferor already holds an NFT but the combined fractions post-transfer fall short of constituting a complete NFT for either party, only ERC-20 tokens are transferred to the recipient. Also, the NFT held by the transferor is burned, and the NFT ID is pushed to the contract’s bank, where it can be minted in the future.

These scenarios ensure that the NFT ownership and ERC-20 transfers are carefully handled by the contract.

Build your own ERC-404 Token

ERC-404 is currently generating significant buzz within the ecosystem. Let's dive into the process of building and deploying your own ERC-404 token. While we'll demonstrate using Hardhat, feel free to utilize any development setup that suits your preferences.

We will use the abstract contract provided by the Pandora team as a base to create the ERC-404 token. 

Overview of Contract

Before we use the abstract contract, let’s overview the contract and discuss some important components.


Name Description
balanceOf Represents the user’s ERC-20 balance, i.e., fractions of NFT. If the fractions are enough to constitute an NFT, users can mint one.
allowance Represents the user’s ERC-20 token allowance for the spender contract address.
getApproved Represents the approval of NFT ID to the spender contract address.
isApprovedForAll Represents the map of the owner to the operator to manage all the NFTs on behalf of the owner.
_owned Represents the NFTs owned by the user.
_erc721TransferExempt Represents the addresses exempt from transferring or burning.


Name Description
name As it self explanatory, it represents the name of the token.
symbol Represents the symbol/ticker for the token.
decimals Represents the decimals for the corresponding ERC-20 token.
units Represents the units/fractions for an NFT.
totalSupply Represents the total supply of the ERC-20 token.
minted Represents the counter to keep track of highest minted id for ERC-721.


Name Description
ownerOf Returns the owner of the given ERC-721 id.
owned Returns the list of ids owned by the given address.
erc721BalanceOf Returns the total ids owned by the given address.
erc20BalanceOf Returns the balance of the corresponding ERC-20 token for the given address.
erc20TotalSupply Returns the total supply of corresponding ERC-20 tokens.
erc721TotalSupply Returns the count of total NFTs minted.
getERC721QueueLength Returns the total ERC-721 tokens stored in contracts, which can be minted in future if requirements are matched.
tokenURI tokenURI for the ERC-721. This is a virtual function, and child contracts need to implement it.
approve A high level function which can be used for approval, and is compatible with both ERC-20 and ERC-721. If the valueOrId is less than total mint count, it assumes it be an ERC-721 approval.
erc721Approve Function to approve ERC-721 token. This function is used internally in the approve function, if the valueOrId is less than total mint count.
erc20Approve Function to approve ERC-20 token. This function is used internally in the approve function, if the valueOrId is greater than total mint count.
setApprovalForAll Function to give or remove approval to spender for all of the tokens.
transferFrom A high level function to transfer both ERC-20 and ERC-721, where the operator may be different than “from”. If the valueOrId is less than total mint count, it assumes it to be an ERC-721 transfer.
erc721TransferFrom Function to transfer ERC-721 token. The function is used internally in the transferFrom function, if the valueOrId is less than total mint count.
erc20TransferFrom Function to transfer ERC-20 token. The function is used internally in the transferFrom function, if the valueOfId is greater than total mint count.
transfer Function to transfer ERC-20 tokens where the operator is the same as “from”.
safeTransferFrom Function to transfer ERC-721 token to recipient. It checks whether the recipient can receive the ERC-721 token avoiding the token getting locked forever.
permit Function for EIP-2612 permit, resulting in lower gas fees for token approval. Read more about EIP-2612.
erc721TransferExempt Function to check whether the address is exempt from ERC-721 transfer. The function is used internally during transfers.

Now that we have reviewed its important components, let’s use it to build our ERC-404 compatible token.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.20;
// Import the ERC-404 contract by Pandora team. Find the link below.
import "./ERC404.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

// AuthToken inherits ERC-404 contract to create a token with features of
// ERC-20 and ERC-721. 
contract AuthToken is Ownable, ERC404 {
 // Once we have inherited the ERC-404, we need to define constructor to initialize
 // the contract with contract details like name, symbol and decimals. 

 // 0x57656E20546F6B656E3F
   address initialOwner_
 ) ERC404("Web3Auth", "Auth", 18) Ownable(initialOwner_) {}
 // mintERC20 function allows the owner to mint the ERC-20 tokens to account.
 function mintERC20(address account_, uint256 value_) external onlyOwner {
   _mintERC20(account_, value_);
  // tokenURI returns the metadata for particular ERC-721 id.
 function tokenURI(uint256) public pure override returns (string memory) {
   return "";

As of now, we are not uploading any metadata for the NFT and returning an empty string. If you wish to upload your metadata, you can check out this IPFS guide explaining best practices for storing metadata.


Once we have set up the contract, it’s time to compile it and deploy. We will create a test script to help us deploy the contract and test the functionality. For now, we’ll be using the Hardhat network for testing, but you can use any EVM network of your choice.

import { ethers } from "hardhat";
import { AuthToken } from "../typechain-types";

async function main() {

 prettyPrint("Deploy Contract")
 const [owner, account, account2] = await ethers.getSigners();
 const authToken = await ethers.deployContract("AuthToken", [owner.address]);

 await authToken.waitForDeployment();

 // Initial balance should be 0
 await checkBalance(authToken, account.address, "Account 1");

 // Mint 0.5 token for account
 await mintToken(authToken, account.address);

 // Since, 0.5 doesn't make one whole unit, the account shouldn't have any NFT.
 await checkBalance(authToken, account.address, "Account 1");

 // Mint more 0.5 token for account
 await mintToken(authToken, account.address);

 // Since, we have minted additional 0.5 tokens, it makes one whole unit.
 // Account should have an NFT now.
 await checkBalance(authToken, account.address, "Account 1");

 // Check balance for Account 2, initially it should be zero.
 await checkBalance(authToken, account2.address, "Account 2");

 prettyPrint("Transfer tokens");
 // Transfer 0.2 tokens from Account 1 to Account 2.
 const transferTx = await authToken.connect(account).transfer(account2.address, ethers.parseUnits("0.2", "ether"));
 await transferTx.wait();
 console.log(`Transfer hash: ${transferTx.hash}`);

 // The balance of ERC-20 for Account 2 should be 0.2, but ERC-721
 // balance should be 0.
 await checkBalance(authToken, account2.address, "Account 2");

 // Since Account transferred 0.2 AuthToken token, the balance is now dropped to 0.8 from
 // a whole number. That means, the ERC-721 balance for Account is now 0.
 // The NFT was burned, and kept in the contract's bank.
 await checkBalance(authToken, account.address, "Account 1");

 prettyPrint("Check ERC-721 queue length")
 // Since, one NFT was burned in the previous transaction, it should be now 1.
 const queueLength = await authToken.getERC721QueueLength();
 console.log(`Queue length: ${queueLength}`);


function prettyPrint(label: string) {
 console.log(`============ ${label} ============`);

async function mintToken(authToken: AuthToken, address: string) {
 prettyPrint("Mint tokens");
 const mintTransaction = await authToken.mintERC20(address, ethers.parseUnits("0.5", "ether"));
 await mintTransaction.wait();

async function checkBalance(authToken: AuthToken, address: string, label: String) {
 prettyPrint("Check Balance");
 const accountTokenBalance = await authToken.erc20BalanceOf(address);
 const accountNFTBalance = await authToken.erc721BalanceOf(address);
 console.log(`${label} Balance: ${accountTokenBalance}\n${label} ERC721 Balance: ${accountNFTBalance}`);

main().then(() => process.exit(0)).catch((error) => {
 process.exitCode = 1;



While ERC-404 is a significant leap forward in the NFT ecosystem, it still has to go through a rigorous process of Ethereum Improvement Proposal (EIP) and Ethereum Request for Comments (ERC).  The absence of a standard going through proposals for official recognition from the Ethereum ecosystem imposes security concerns and slows down the adoption. The dual nature of ERC-404 also increases the complexity of transactions and gas fees.


ERC-404 holds the potential to tackle liquidity challenges by enabling users to invest in fractional shares of NFTs, thereby broadening accessibility to NFTs. This not only fosters inclusivity but also paves the way for innovation across various sectors, including gaming, real-world asset tokenization, and DeFi. Although it has some challenges to overcome, it'll be worth looking forward to how ERC-404 progresses from here.

Discover More 

Join the Conversation: Participate in our community discussions to share your insights on ERC-404 and connect with others in the field. Join our community call or Discord.

Would you like to learn more about Web3Auth? You can dive into the documentation, engage with our team on Discord, or schedule a call for a more in-depth discussion.