ERC404: The Experimental Semi-Fungible Standard

15 min read

What is an EIP and ERC?

EIP - Ethereum Improvement Proposals are standards specifying potential new features or processes for Ethereum. EIPs contain technical specifications for the proposed changes and act as the "source of truth” for the community. Network upgrades and application standards for Ethereum are discussed and developed through the EIP process. EIPs play a central role in how changes happen and are documented on Ethereum. They are the way for people to propose, debate, and adopt changes.

There are three types of EIPs:

  • Standards Track: describes any change that affects most or all Ethereum implementations.
  • Meta Track: describes a process surrounding Ethereum or proposes a change to a process.
  • Informational Track: describes an Ethereum design issue or provides general guidelines or information to the Ethereum community.

Specifically, the Ethereum Request for Comment (ERC) ****is a Standards Track EIP type and essentially a set of technical documents containing guidelines on developing a smart contract. They define a specific set of functions for each token type and facilitate the interaction between applications and smart contracts. Anyone can create an ERC. However, it requires going through the process of an Ethereum Improvement Proposal (EIP).

Once a developer submits their proposal, it is assessed and scrutinized by Ethereum’s core developers. If the community deems it an important addition to the blockchain ecosystem, the proposal is accepted, finalised, and implemented.

As soon as this process is complete, the initial document becomes an ERC standard that other developers can use as guidelines to create their own tokens.

What is ERC404?

Before delving into the specifics of ERC404, it is essential to examine the pre-existing token standards: ERC20, ERC721, and ERC1155.

  • ERC20: The standard for creating fungible tokens, where no token is unique.
  • ERC721: The standard for non-fungible tokens (NFTs), with every token having a unique ID.
  • ERC1155: The standard for semi-fungible tokens, allowing multiple tokens to share the same ID.

At the beginning of February 2024, a new experimental token standard was introduced named ERC404. Even though it's not an official standard, it gained enormous popularity for a brief period of time, because of its main idea to merge the fungibility of ERC20 tokens with the uniqueness of ERC721 tokens into semi-fungible tokens, despite these standards not being intended to be combined.

In its current implementation, ERC404 effectively isolates ERC20 and ERC721 standard logic or introduces pathing where possible. Pathing could best be described as a lossy encoding scheme in which 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.

It is important to note that the initial implementation has not been formally audited. Despite the testing, the integration of overlapping standards means that the protocols may not fully understand their combined functionality.

Consider a scenario involving an ERC721 NFT contract that represents a chocolate bar.

In the traditional setup, creating a single bar NFT increases an owner's balance from 0 to 1. If the owner transfers their NFT, their balance returns to 0. The system requires trading in whole NFTs only.

However, envision this chocolate bar represented by ERC404 tokens.

With this contract, there is a base unit akin to what is found in ERC20 tokens. For this scenario, 100 could be used as the base unit (though typically it might be 10^18).

Creating a bar NFT under this system means an owner's balance is 100, not just 1. Initially, this may seem similar to the traditional approach. However, the system diverges significantly at this point, as this behavior allows for the trading of portions of an NFT.

For example, an owner could transfer 20 fractions of their chocolate NFT to another party. Subsequently, their balance would reduce to 80. With a balance below 100, the owner no longer possesses a complete NFT. Maintaining at least 1 base unit (100 in this scenario) in fractions is essential for NFT ownership.

The number of NFTs an address holds is determined by dividing their balance by the base units and taking the floor value.

Owning 169 fractions equates to 1 NFT. Possessing 199 fractions still results in 1 NFT. However, having 200 fractions translates to 2 NFTs.

ERC404 Unique Features and Behaviors

ERC404 utilizes minting and burning mechanics to facilitate fractional ownership of the tokens every time fractions are traded between owners.

Upon examining the implementation of an ERC404 token, it becomes apparent that several state variable mappings are responsible for managing the token accounting:

High-level overview of all ERC404 functions

Here is a high-level overview of all ERC404 functions and their purposes:

Ownership Management Functions

  • transferOwnership(address _owner): Allows the current owner to transfer ownership of the contract to a new address. It emits an OwnershipTransferred event upon success.
  • revokeOwnership(): Enables the current owner to revoke ownership, effectively leaving the contract without an owner. This action emits an OwnershipTransferred event, indicating the owner address is now address(0).

Token Management Functions

  • setWhitelist(address target, bool state): Allows the contract owner to add or remove addresses from the whitelist.
  • ownerOf(uint256 id): Returns the owner of a specific token ID, commonly used in ERC721 token standards to track ownership of individual tokens.
  • approve(address spender, uint256 amountOrId): Sets or updates the allowance for a spender to manage either a specific amount of tokens or a particular token ID on behalf of the caller.
  • setApprovalForAll(address operator, bool approved): Grants or revokes permission for an operator to manage all of the caller's tokens.
  • transferFrom(address from, address to, uint256 amountOrId): Enables a spender to transfer either fractions or a whole token from one address to another, subject to ownership and allowance checks.
  • transfer(address to, uint256 amount): Allows a token holder to transfer fractions of tokens to another address.
  • safeTransferFrom(address from, address to, uint256 id) and safeTransferFrom(address from, address to, uint256 id, bytes calldata data): These functions extend transferFrom for ERC721 tokens, adding checks to ensure the recipient can properly handle the token (compliance with the ERC721Receiver interface).

Internal Helper Functions

  • _transfer(address from, address to, uint256 amount): An internal function facilitating the transfer of fractions of tokens, including minting or burning tokens if necessary based on the whitelist status of the sender and receiver.
  • _getUnit(): Returns the unit of tokens based on the decimals state variable, aiding in calculations for transfers, minting, and burning.
  • _mint(address to): Mints a new token to a specified address, increasing the minted count and updating ownership mappings.
  • _burn(address from): Burns a token from a specified address, reducing the minted count and clearing related ownership and approval mappings.

While all of the functions differ from the ERC20 and ERC721’s, the most significant differences are seen in the transferFrom() and _transfer() functions, and pointing them out is worth noting.

The transferFrom() function is divided into two parts - “fungible” and “non-fungible” token transfer. The function distinguishes between the two by comparing the amountOrId parameter against the minted count. If amountOrId is less than or equal to minted, the operation is treated as an NFT transfer. The contract adjusts the arrays tracking the tokens owned by the from and to addresses. This involves removing the token ID from the from address's array and adding it to the to address's array.

Otherwise, for amountOrId greater than the minted variable, the fractional transfer of a token is invoked, which consists of using the _transfer() method. However, consider what happens when a tiny token amount is swapped on Uniswap, for example. Assuming the caller has given the router permission to spend ERC721 tokens, the swap uses the specified amount. However, due to line 232, balanceOf[from] -= _getUnit();, and line 235, balanceOf[to] += _getUnit();, the pair receives a full base unit (token). This means the difference, _getUnit() - amount, is effectively given to the pair, acting as a fee distributed to liquidity providers.

From a different perspective, this design could be susceptible to another type of attack.

Consider Alice depositing her ERC404 NFT with ID 12, the last minted NFT, into a vault that accepts such tokens. This deposit activates the “non-fungible” aspect of the transferFrom() function. Then, Bob deposits 100 ERC404 tokens, a number greater than the total minted NFTs, leading to a “fungible” transfer. Later, Bob attempts to withdraw a smaller number of tokens, specifically 12, which triggers the function's first if statement, resulting in Bob stealing Alice's token.

Clearly, overloading existing function signatures with new and non-obvious mechanics presents challenges, and many systems that support ERC404 tokens could be vulnerable to such an attack.

transferFrom code snipper

The _transfer() function handles the transferring fractions of the token, namely normal ERC20 transfer. However, it uniquely operates such that if the recipient's balance (to) becomes sufficient for a new NFT post-transfer, it mints as many NFTs as there are new base units (tokens) formed. Conversely, for the sender’s address (from), if the balance becomes insufficient to constitute a base unit, its last NFTs are burned.

This aspect of the “standard” is the most controversial and has sparked considerable debate within the community, given its implications. For instance, if the last NFT held is rare and valuable, transferring even a mere fraction of it could result in its loss.

Consider the chocolate bar NFT example again: if an owner possesses 100 units, equivalent to one base token, and transfers 50 units to another party, their NFT would be burned, leaving neither party with a complete NFT until one address accumulates enough to meet or exceed the base unit.

From another perspective, one could artificially reduce the “ERC721 supply” down to zero. Consider the implications of applying this tokenization model to significant assets, like houses.

Furthermore, it's worth mentioning that there's a part that skips minting and burning and just transfers the actual fractions for addresses not on the whitelist, which also can be seen as odd behavior.

_transfer code snippet

How developers and auditors responded to ERC404?

Highlighting the unusual behavior of the transfers, along with the multiple mappings and balances managing ownership that results in high gas costs - about 125k gas for a simple transfer, twice that of an ERC721 transfer - it's clear that ERC404 has ignited numerous discussions and varying opinions in the community.

One of the first ones to highlight the anomalies in the “standard” was CharlesWang:

I think the main problem with ERC404 is actually the fact that if you execute a normal ERC20 transfer, your unique ID is irreversibly burned. Think of owning the token with ID = 1. As soon as you do a normal ERC20 transfer, adding lp, whatsoever, the ID 1 is permanently burned and the recipient will just get the next currentID minted.

Simultaneously, Simon Tian introduced ERC404A, which addressed the issue of burning the last NFT upon transfer by adding a _swapPosition() function. This function enables the reordering of NFTs within an owner's collection and ensures that the operation is only permissible for NFTs owned by the same entity.

Followed by Pop Punk the next day, who stated he was working on an improved version of ERC404, but without the confusing “ERC” at the beginning:

Okay it's happening. I assembled the avengers of gas optimization and we're building a better 404... NOW. And we won't tack ERC to the name of it. No more of this.
We saw a single contract wreck the ethereum experience by spiking gas for everyone. Within hours we got some of the smartest developers in the space together to build a better solution and correct this.

Approximately 48 hours after the first tweet, the new “standard” - DN404, was completed:

DN404 development is complete. We're now getting feedback from developers and auditing the smart contracts. By the way, DN stands for Divisible NFT.

It's important to note that DN404's code hasn't been formally audited, and users should use it at their own risk. However, the new “standard” addresses the issues of the original one by introducing a "base" ERC20 contract and a "mirror" ERC721 one, instead of a single contract. Most trading will be conducted on the fully compliant base contract that tracks user balances. When tokens are transferred, the mirrored NFTs will be minted/burned on the ERC721 contract, which is also fully compliant with its standard. More information is available here.

Projects using ERC404

Marking a rise from $0 to $375M Market Cap in a week, these are some of the first projects accountable for that result:

Pandora

Pandora is an early adopter of ERC404, featuring 10,000 PANDORA ERC404 tokens and an equal number of “Replicant” NFTs. Purchasing a PANDORA token automatically results in a Replicant NFT being minted in the purchaser's wallet.

DeFrogs

DeFrogs offers a collection of 10,000 Pepe the Frog-themed NFTs, marking the first PFP collection under the ERC404 standard.

Other projects leveraging the “standard” include Monkees (MONKEES), Punks404 (PUNK), and EtherRock404 (ROCK).