EIP-3525: The Semi-fungible Token

We proposed a Semi-fungible Token for define and implement customizable financial instruments, EIP-3525.

This standard describes a new token format that have both ID and value property, where the ID property has the same semantics as ERC-721, and the value has the same meaning as ERC-20.

The semi-fungibility of the tokens is defined by the SLOT property, when two tokens have the same SLOT, their value are fungible, that is: they can be add together like ERC-20 tokens do.

This structure is best for defining flexible financial instruments, since one does not need to create a separate ERC-20 contract for each financial product. Also the value of the financial products can be manipulated in standard level, rather than treated separately at implementation level (like Uniswap V3’s LP which is using ERC-721).

3 Likes

This is the discussion thread for ERC-3525, The Semi-fungible Token Standard. The newest proposal document can be found at Ethereum EIP repository: EIP-3525

This version introduces significant changes from the last version, including two major aspects:

  1. Adopting the value/transfer conventional model as general ERC standards, remove the split/merge functions that are more related to business logic of apps
  2. Move some functions to optional interfaces, such as enumerable, etc.

Also the whole contents of the descriptions are completely revised.

1 Like

Is my understanding correct that the difference with ERC1155 is that for a specific TokenID there can only be 1 owner, but there’s a fungible value attached to each token?

If yes, then will having a slot make this ERC effectively similar to ERC1155?

ERC3525 serves a different purpose from ERC1155. The former is designed to represent financial instruments, the latter the game items.

So if you have a ERC1155 token with a balance of 10, you have 10 identical items; while it’s a ERC3525 token with a value of 10, you have one item with face value of 10.

TokenID is universally unique, as which in ERC721.

Slot is a set of attributes collectively determining the exact category of the token.

The key logic of ERC-3525 is that ‘an ID can have a value’, and the slot is an technic to judge whether the values of different IDs are fungible to each other.

The logic behind ERC-1155 is that ‘an address can have several tokens of a certain ID’.

So that although the ID in ERC-1155 is somewhat like the slot concept in ERC-3525, in that a slot can have fungible values, but the token logic is very different. For example, ERC-3525 introduces ID-to-ID value transfer model(with the help of slot), in which IDs are treated as a value holder, quite like the role of an address.

We usually classify non-fungible objects by their features, such as branch, region, and year, to make them fungible. There are no two identical leaves in the world, but they are both, leaves. It makes sense that the crypto world needs semi-fungible token standard just as it needs ERC-20 and ERC-721.

“Safe Transfer” should not require the receiver have a specific function implemented, because this breaks compatibility with most existing wallets that don’t support upgradability. Almost every contract wallet supports the ability to call an external contract though, so using a registry will give far broader wallet support and doesn’t require people replace their wallets to gain compatibility with this.

I believe there is an EIP somewhere that defines a mechanism for this, but I forget its number. You might be able to use something like EIP-1820, but I recommend looking around to see if there is a standard specifically for this and compare them.

Thanks for that.

We define a new ERC3525TokenReceiver only for those wallets as well as contracts that already specially support for EIP-3525, so that for these wallets they can call this special function when they transfer values rathe than ID to a contract.

If the destination contract does not support EIP-3525, a wallet can simply call ERC-721’s “Safe Transfer” to transfer a whole ID to that contract, and the contract just receives a ERC-721 token.

Hope this explains the design purpose of ERC3525TokenReceiver.

The problem is that the caller may be a generalized contract (e.g., Uniswap) and the receiver may be a contract wallet that can make arbitrary calls but cannot implement arbitrary interfaces. In this scenario, the caller will attempt to do a SafeTransfer and this will fail because the receiver hasn’t implemented ERC3525TokenReceiver, even though the transfer would be perfectly safe because the receiver can make arbitrary contract calls.

This pattern has been used in the past, and it is really bad for contract wallets because not every contract wallet has the ability to implement arbitrary interfaces (nor should they do so). On the other hand, almost every contract wallet supports making arbitrary calls, so the registry pattern is compatible with basically all existing wallets.

ERC-721 is one of the most well known bad actors here with their implementation of SafeTransferFrom which only allows transferring the token to contract wallets that have explicitly implemented ERC-721 support. We should not be following the behavior of ERC-721 here and instead should be doing better than ERC-721.

ERC-721 is one of the most well known bad actors here with their implementation of SafeTransferFrom which only allows transferring the token to contract wallets that have explicitly implemented ERC-721 support. We should not be following the behavior of ERC-721 here and instead should be doing better than ERC-721.

Got the idea of your suggestion, we will discuss for that thoroughly.

From this perspective, simply removing the obligation of calling ERC3525TokenReceiver in the standard also works, right?

In general, I think it is better to leave the decision to do recipient protection up to the caller rather than having it as part of the token. This allows for maximum flexibility and for the caller to utilize additional contextual information they have available to make such a decision.

One can imagine a separate EIP that provides a mechanism for recipient checking (like a registry or introspection mechanism), and the caller can use that prior to making the call if they like, no need for the token to do anything special.

2 Likes

Yes, of course, we will make necessary changes in the proposal for both flexibility and recipient protection patterns.

There is only one special scenario we need to consider: sometimes transferring of value will result in a new token to be created and transferred to the receiver, in this case, if the token is considered as a ERC-721 token (since ERC-3525 tends to be compatible with ERC-721) , whether the standard should force implementations to obey ERC-721’s logic when SafeTransfer is called.

Of course if we totally remove the ‘Safe Transfer’ pattern from this proposal, the above problem no longer exists, but I think we can/are possible to design a pattern for both compatible and flexible purposes.

2 Likes

Hi all, we are going to revise the value transfer model, by removing the SafeTransferFrom method, and introduce a ‘Check, Notify and Response’ model for better flexibility as well as simplicity.

Design decision: Notification/acceptance mechanism instead of ‘Safe Transfer’

EIP-721 and some later token standards introduced ‘Safe Transfer’ model, for better control of the ‘safety’ when transferring tokens, this mechanism leaves the choice of different transfer mode (safe/unsafe) to the sender, and may cause some potential problem:

  1. In most situation the sender don’t know how to choose between two kinds of transfer methods (safe/unsafe);
  2. If the sender calls the safeTransferFrom method, the transfer may fail when the recipient contract didn’t implements the callback function, even if the recipient contract can receive and manipulate the token with no problem.

This EIP defines a simple ‘Check, Notify and Response’ model for better flexibility as well as simplicity:

  1. No extra safeTransferFrom methods are needed, all transfer functions only need to obey the same logic;
  2. All EIP-3525 contracts MUST check for the onERC3525Received function if the receiver is a smart contract, when the function exists, they MUST call this function after the value transfer and check the result to decide whether the transfer should succeed or fail;
  3. Any smart contract can implement onERC3525Received function for purpose of being notified after receiving values, in this function it can return certain pre-defined value to accept the transfer, any other cases SHOULD cause the transfer to fail.

The corresponding interface definitions is as folows:

 pragma solidity ^0.8.0;

/**
 * @title EIP-3525 token receiver interface
 * @dev Interface for any contract that wants to be informed by EIP-3525 contracts when receiving values from other addresses.
 * Note: the EIP-165 identifier for this interface is 0x009ce20b.
 */
interface IERC3525Receiver {
    /**
     * @notice Handle the receipt of an EIP-3525 token value.
     * @dev An EIP-3525 smart contract MUST check whether this function is implemented by the recipient contract, if the
     *  recipient contract implements this function, the EIP-3525 contract MUST call this function after a 
     *  value transfer (i.e. `transferFrom(uint256,uint256,uint256,bytes)`).
     *  MUST return 0x009ce20b (i.e. `bytes4(keccak256('onERC3525Received(address,uint256,uint256,
     *  uint256,bytes)'))`) if the transfer is accepted.
     *  MUST revert or return any value other than 0x009ce20b if the transfer is rejected.
     *  The EIP-3525 smart contract that calls this function MUST revert the transfer transaction if the return value 
     *  is not equal to 0x009ce20b.
     * @param _operator The address which triggered the transfer
     * @param _fromTokenId The token id to transfer value from
     * @param _toTokenId The token id to transfer value to
     * @param _value The transferred value
     * @param _data Additional data with no specified format
     * @return `bytes4(keccak256('onERC3525Received(address,uint256,uint256,uint256,bytes)'))` 
     *  unless the transfer is rejected.
     */
    function onERC3525Received(address _operator, uint256 _fromTokenId, uint256 _toTokenId, uint256 _value, bytes calldata _data) external returns (bytes4);

}