• Hacken
  • Blog
  • Discover
  • ERC-404 Under Spotlight

ERC-404 Under Spotlight

11 minutes

By Usman Arda

The experimental semi-fungible token standard, ERC-404, combines elements from ERC-20 and ERC-721 tokens. Despite rising popularity, it has yet to secure an official Ethereum Improvement Proposal (EIP) designation. However, its unique attributes, such as enabling fractional ownership of NFTs and enhancing liquidity, coupled with the potential for automated NFT minting and burning processes, suggest a strong likelihood of attaining EIP status soon, benefiting Web3 developers with streamlined transactions and enhanced functionality. 

What Is ERC-404?

ERC-404 allows users to create semi-fungible tokens supporting fungible (ERC-20) and non-fungible (ERC-721) token features. This unlocks the potential for concepts like Fractional Non-Fungible Token (NFT) ownership and an expanded utility and liquidity of digital assets. This complex functionality is achieved by effectively isolating the ERC-20 and ERC-721 logics via a process called “pathing.”

Pathing, as described by the developers, is “a lossy encoding scheme in which the token amount data and IDs occupy shared space under the assumption that negligible token transfers occupying id space do not or do not need to occur.” 

Two anonymous developers, “crtl” and “Acme” from Pandora Labs, recently released the standard’s second beta version with some improvements. 

ERC-404 Mechanics in a Nutshell

The ERC-404 token standard introduces an advanced framework for managing digital assets through dynamic buying, selling, and transferring mechanisms. This standard automates the minting of NFTs when a wallet’s token balance exceeds a whole number, effectively transforming the excess into a unique, tradable asset. Conversely, when the balance falls below this threshold, the corresponding NFTs are burned, permanently disassociating them from both the account and the fungible token.

This process seamlessly combines the fluidity of traditional cryptocurrency transactions with the unique, collectible nature of NFTs, offering a streamlined yet flexible way for users to engage with digital assets. By doing so, ERC-404 tokens elevate interactivity and utility within the blockchain ecosystem, merging fungibility and uniqueness into a cohesive system.

ERC-404 Mechanics

  • Automated NFT Minting and Burning: ERC-404 automates the creation and destruction of NFTs based on wallet token balances. This eliminates the need for manual intervention, simplifying the user experience.
  • Seamless Transactions: ERC-404 maintains the efficiency of standard token transfers on the Ethereum blockchain. Users can buy, sell, or transfer native tokens and their associated NFTs within a single transaction, minimizing complexity and transaction fees.

This innovative approach enhances digital asset interactivity and utility, bridging the gap between fungible and non-fungible tokens. ERC-404 provides a versatile framework for engaging with the blockchain.

How Does ERC-404 Work?

ERC-404 V1.0 

Balance Logic

ERC-404 v1.0 standard must be able to track the balance of ERC-20 tokens and the ownership of ERC-721 tokens effectively. The ERC-20 balance is tracked separately using mappings:

uint8 public immutable decimals; mapping(address => uint256) public balanceOf;
  • balanceOf[address]” keeps track of the ERC-20 token balance for each address. 
  • The balances are represented in fractional units, where the smallest unit is defined by the decimals property.

Ownership Logic

ERC-721 ownership is tracked with the following logic:

mapping(uint256 => address) internal _ownerOf;
  • _ownerOf[tokenId]” keeps track of the owner of each ERC-721 token ID.
mapping(uint256 => address) internal _ownerOf;
  • _owned[address]” stores an array of token IDs owned by each address.
mapping(uint256 => uint256) internal _ownedIndex;
  • _ownedIndex[tokenId]” stores the token ID’s index in the _owned array for quick access and efficient removal.

ERC-20 Transfer Logic

In addition to this efficient logic, ERC-404 adopts an innovative approach to handle ERC-20 and ERC-721 token transfers. The key components of the ERC-20 transfer logic are as follows:

function _transfer(
    address from,
    address to,
    uint256 amount
) internal returns (bool) {
    uint256 unit = _getUnit();
    uint256 balanceBeforeSender = balanceOf[from];
    uint256 balanceBeforeReceiver = balanceOf[to];

    balanceOf[from] -= amount;

    unchecked {
        balanceOf[to] += amount;
    }

    // Skip burn for certain addresses to save gas
    if (!whitelist[from]) {
        uint256 tokens_to_burn = (balanceBeforeSender / unit) - (balanceOf[from] / unit);
        for (uint256 i = 0; i < tokens_to_burn; i++) {
            _burn(from);
        }
    }

    // Skip minting for certain addresses to save gas
    if (!whitelist[to]) {
        uint256 tokens_to_mint = (balanceOf[to] / unit) - (balanceBeforeReceiver / unit);
        for (uint256 i = 0; i < tokens_to_mint; i++) {
            _mint(to);
        }
    }

    emit ERC20Transfer(from, to, amount);
    return true;
}
  • Retrieve the unit of fractional representation using _getUnit()
  • Store the balances of the sender (balanceBeforeSender) and receiver (balanceBeforeReceiver) before the transfer. 
  • Decrease the sender’s balance by the transfer amount. Increase the receiver’s balance by the transfer amount. 
  • If the sender is not whitelisted, calculate the number of tokens to burn based on the balance change and call _burn accordingly. 
  • If the receiver is not whitelisted, calculate the number of tokens to mint based on the balance change and call _mint accordingly. 
  • Emit an ERC20Transfer event to log the transfer. Return true to indicate success.

ERC-721 Transfer Logic

The ERC-721 token uses the same _transfer() function as core logic. However, since it has to handle ownership of the NFTs, the logic contains changes.

function transferFrom(
    address from,
    address to,
    uint256 amountOrId
) public virtual {
    if (amountOrId <= minted) {
        if (from != _ownerOf[amountOrId]) {
            revert InvalidSender();
        }

        if (to == address(0)) {
            revert InvalidRecipient();
        }

        if (
            msg.sender != from &&
            !isApprovedForAll[from][msg.sender] &&
            msg.sender != getApproved[amountOrId]
        ) {
            revert Unauthorized();
        }

        balanceOf[from] -= _getUnit();

        unchecked {
            balanceOf[to] += _getUnit();
        }

        _ownerOf[amountOrId] = to;
        delete getApproved[amountOrId];

        // Update _owned for sender
        uint256 updatedId = _owned[from][_owned[from].length - 1];
        _owned[from][_ownedIndex[amountOrId]] = updatedId;
        _owned[from].pop();
        _ownedIndex[updatedId] = _ownedIndex[amountOrId];

        // Add token to receiver's owned list
        _owned[to].push(amountOrId);
        _ownedIndex[amountOrId] = _owned[to].length - 1;

        emit Transfer(from, to, amountOrId);
        emit ERC20Transfer(from, to, _getUnit());
    } else {
        uint256 allowed = allowance[from][msg.sender];

        if (allowed != type(uint256).max)
            allowance[from][msg.sender] = allowed - amountOrId;

        _transfer(from, to, amountOrId);
    }
}
  • Check if “amountOrId” is less than or equal to the number of minted tokens, which indicates it’s an ERC-721 token ID.
  • Verify the ownership of the token ID by checking if from is the owner.
  • Ensure the recipient address is not a zero address.
  • Check if the sender is authorized to transfer the token (owner, approved, or operator).
  • Decrease the sender’s ERC-20 balance by one unit.
  • Increase the receiver’s ERC-20 balance by one unit.
  • Update the ownership of the ERC-721 token ID in “_ownerOf”.
  • Delete the approved address for the token ID.
  • Update the “_owned” array and “_ownedIndex” mapping for the sender to reflect the removal of the token ID.
  • Add the token ID to the receiver’s “_owned” array and update the “_ownedIndex” mapping.
  • Emit a Transfer event for the ERC-721 transfer.
  • Emit an ERC20Transfer event to log the corresponding fractional unit transfer.
  • Note:
    • If amountOrId is not an ERC-721 token ID, treat it as an ERC-20 transfer amount.
    • Check the sender’s allowance and update it accordingly.
    • Call the internal “_transfer()” function to handle the ERC-20 transfer logic as described earlier.

Insights About v1

  • The initial supply has to be passed directly into the constructor (for ERC-20 tokens). If the transfer functions are being used,  they would not only mint the total supply of ERC-20s it would also mint the entire supply of ERC-721s, which would be unnecessary and very expensive in terms of gas.
  • The minted token receivers have to be whitelisted for transfers. The contract will assume they hold the corresponding NETs when, in fact, the NFT minting step has been skipped for these initially minted tokens.

ERC-404 V2.0 Beta

The second iteration of ERC-404 contains some key updates to the code logic.

  • ERC-721 type token IDs are now banked and reused in a FIFO queue instead of increasing forever as they are minted and burned. This allows for a predictable set of NFT token IDs, as in a typical NFT collection.
    • The “DoubleEndedQueue.Uint256Deque” for managing ERC-721 token IDs in a FIFO queue. 
DoubleEndedQueue.Uint256Deque private _storedERC721Ids;
  • Transfers of full ERC-20 type tokens now transfer ERC-721 type tokens held by the sender to the recipient. In other words, if you transfer three full tokens as an ERC-20 transfer, three of the ERC-721s in your wallet will transfer directly to the recipient rather than those ERC-721s being burned and new token IDs minted to the recipient.
  • Predictable events emitted during transfers, approvals, and other operations that clearly indicate whether attributed to ERC-20 and ERC-721.  
  • Dedicated functions for returning ERC-20 and ERC-721 balances and total supply.
function erc20BalanceOf(address owner_) public view virtual returns (uint256);
function erc721BalanceOf(address owner_) public view virtual returns (uint256);
function erc20TotalSupply() public view virtual returns (uint256);
function erc721TotalSupply() public view virtual returns (uint256);
  • Removal of fixed supply cap in core contract, allowing a fixed token supply cap to be added optionally if desired.
  • Simplification and centralization of transfer logic.
function _transferERC20WithERC721(address from_, address to_, uint256 value_) internal virtual returns (bool);
function _transferERC20(address from_, address to_, uint256 value_) internal virtual;
function _transferERC721(address from_, address to_, uint256 id_) internal virtual;
  • Introduction of _mintERC20() and _retrieveOrMintERC721() functions to make the minting process easier and more dedicated. 
function _mintERC20(address to_, uint256 value_) internal virtual;
function _retrieveOrMintERC721(address to_) internal virtual;
  • EIP-2612 support for permit approvals.
function permit(address owner_, address spender_, uint256 value_, uint256 deadline_, uint8 v_, bytes32 r_, bytes32 s_) public virtual;
function DOMAIN_SEPARATOR() public view virtual returns (bytes32);
  • EIP-165 support.
function supportsInterface(bytes4 interfaceId) public view virtual returns (bool);

ERC-404 vs ERC-20 & ERC-721

As the logic explained above suggests, the ERC-404 is mostly compatible with the ERC-20 and ERC-721 standards. However, this does not mean that there are no differences between them. 

Fungibility Token RepresentationMetadata Functions and Interfaces
ERC-404Semi fungibleIdentical & Unique Token SupportNo MetadataStandard functions for fungible tokens.
ERC-20FungibleIdentical TokensMetadata for each TokenStandard functions for ownership and transfer of unique tokens.
ERC-721Non-FungibleUnique TokensMetadata for each TokenWould need to combine functions from both ERC-20 and ERC-721, potentially leading to a more complex interface.

Considerations of ERC-404 while Developing

ERC-404 is an unofficial and unaudited standard. This lack of formal validation and the fact that many developers lack specific knowledge about it create risks when using it in projects. This section highlights key points to be cautious of for both versions of ERC-404.

Missing Input Validation: The constructor and the transferFrom() function do not validate their inputs. It is crucial to ensure the provided values adhere to the protocol’s specific operational boundaries as laid out in the project specifications and documentation. If the constructors or functions lack appropriate validation checks, there’s a risk of setting state variables with values that could cause unexpected and potentially detrimental behavior within the contract’s operations, violating the intended logic of the protocol.

Gas Optimizations: Math operations, loops with high ranges and calls, and array sizes should be carefully implemented (with proper limits and caps) and tested before launching the project. If these are neglected, the gas cost can increase exponentially, and the project can experience gas-related problems such as out-of-gas exceptions or exceeding block gas limits.

Reentrancy Risk: When implementing and using transfer and allowance mechanisms, developers should follow the Checks-Effects-Interactions pattern. If this pattern fails or a valid mutex lock is not implemented, the created project can experience problems like Reentrancy attacks

Signature Malleability: ECDSA signature verification allows for slight modifications to the signature that do not change the signature’s validity. This can result in various issues, including access control, contract execution, or data integrity problems. Attackers can exploit this vulnerability to manipulate signed data or perform unauthorized transactions, leading to severe security risks.  Implementing a cryptographic signature system in Ethereum contracts often assumes that the signature is unique, but signatures can be altered without possessing the private key and still be valid. The EVM specification defines several so-called ‘precompiled’ contracts, one of them being ecrecover, which executes the elliptic curve public key recovery. A malicious user can slightly modify the three values v, r, and s to create other valid signatures. A system that performs signature verification on the contract level might be susceptible to attacks if the signature is part of the signed message hash. Valid signatures could be created by a malicious user to replay previously signed messages.

Follow @hackenclub on 𝕏 (Twitter)

Conclusion

ERC-404 marks a significant advancement in blockchain with its semi-fungible token standard, merging the best of ERC-20 and ERC-721 tokens. It introduces innovative features like fractional NFT ownership and improved liquidity, although it is still experimental and awaits official EIP endorsement. The standard enables automated NFT minting and burning, simplifies transactions, and reduces costs, which is especially advantageous for Web3 developers who focus on digital asset utility and interactivity.

However, its unofficial status and lack of formal audits require developers to exercise caution regarding potential security vulnerabilities such as input validation and reentrancy risks. Addressing these issues is vital for secure and effective project use. 

Overall, ERC-404 symbolizes significant progress in blending fungible and non-fungible tokens, with the potential to become a key tool for digital asset innovation as it evolves and possibly receives formal recognition.

Subscribe
to our newsletter

Be the first to receive our latest company updates, Web3 security insights, and exclusive content curated for the blockchain enthusiasts.

Speaker Img

Table of contents

  • What Is ERC-404?
  • ERC-404 Mechanics in a Nutshell
  • How Does ERC-404 Work?
  • Considerations of ERC-404 while Developing

Tell us about your project

Follow Us

Read next:

More related
  • Blog image
    zkEVM: Unlocking Scalability for Ethereum with Zero-Knowledge Proofs

    7 min read

    Discover

  • Blog image
  • Blog image
More related →

Trusted Web3 Security Partner