⚠️ This EIP is not recommended for general use or implementation as it is likely to change.

EIP-3525: Semi-Fungible Token Standard Source

Author Will Wang, Mike Meng, TsaiYee, Ryan Chow, Zhongxin Wu, AlvisDu https://github.com/ethereum/EIPs/issues/3641 Draft Standards Track ERC 2020-12-01 165, 721

Simple Summary

This is a standard for semi-fungible tokens. The set of smart contract interfaces described in this document defines an ERC-721 extension, by introducing an <ID, SLOT, AMOUNT> triple scalar model to represent the semi-fungible nature of a token. This standard also introduces an optional set of interfaces called ‘UnderlyingContainer’, for an ERC-3525 token to act as a representing layer of existing tokens, which is one of the key purpose of tokenization.

Abstract

ERC-3525 is ERC-721 compatible, which means, as an ERC-721 token, each ERC-3525 token contains an ID property to identify itself as a universally unique entity. What empowers a ERC-3525 token is that it contains a ‘units’ property, representing the quantitative nature of the token. Thus, a ERC-3525 token can be split into several different ERC-3525 tokens, with certain properties maintained unchanged but the sum of the units of all split-out token equals that of the original one. Nevertheless, each ERC-3525 token has a ‘SLOT’ attribute, which labels its logical category. Several ERC-3525 tokens can be merged into one if their SLOT attributes indicate that they are of the same category.

Motivation

Tokenization of assets is one of the most important applications in crypto. Normally there are two options when tokenizing assets: fungible and non-fungible. The first one generally uses the ERC-20 standard, in the case that every unit of assets is identical to each other, ERC-20 standard provides a flexible and efficient way to manipulate fungible tokens. The second one predominately uses the ERC-721 token standard, for that each asset needs to be described by one or more customized properties. For example, when a decentralized exchange that supports the Automatic Market-Making model allows its liquidity providers to specify their positions at different price ranges, an LP token can be implemented in ERC-721, since this token standard has the capability to identify each position as an entity, with different attributes for each entity.

Both options have significant drawbacks. In the fungible way, one needs to create a separate ERC-20 contract for each different value or combination of customizable properties, which can easily require an unacceptable number of ERC-20 contracts in practice. On the other hand, there is no quantitative feature in an ERC-721, hence significantly reducing the computability, liquidity, and manageability. For example, when we want to stake part of the position LP in some smart contract, the liquidity has to be withdrawn from the LP to create a new one, causes inconvenience and temporary decrease of liquidity.

An intuitive and direct way to solve the problem is to add a property to represent the quantitative nature directly to an ERC-721 token, making it best for both property customization and semi-fungibility. Furthermore, the ERC-721 compatibility would help the new standard easily utilize existing infrastructures and gain fast adoption.

For further design motivations, see papers and documents below:

Articles & Discussions

Specification

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.

Related Standards

Every ERC-3525 compliant contract must implement the ERC3525, ERC721 and ERC165 interfaces

pragma solidity 0.7.6;

/**
* @title ERC-3525 Semi-Fungible Token Standard
* @dev See https://eips.ethereum.org/EIPS/eip-3525
* Note: the ERC-165 identifier for this interface is 0x1487d183.
*/
interface IERC3525 /* is ERC-721 */{

/**
* @dev This emits when partial units of a token are transferred to another.
* @param _from The address of the owner of _tokenId
* @param _to The address of the owner of _targetTokenId
* @param _tokenId The token to partially transfer
* @param _targetTokenId The token to receive the units transferred
@ @param _transferUnits The amount of units to transfer
*/
event TransferUnits(address indexed _from, address indexed _to, uint256 indexed _tokenId, uint256 _targetTokenId, uint256 _transferUnits);

/**
* @dev This emits when a token is split into two.
* @param _owner The address of the owner of both _tokenId and _newTokenId
* @param _tokenId The token to be split
* @param _newTokenId The new token created after split
@ @param _splitUnits The amount of units to be split from _tokenId to _newTokenId
*/
event Split(address indexed _owner, uint256 indexed _tokenId, uint256 _newTokenId, uint256 _splitUnits);

/**
* @dev This emits when a token is merged into another.
* @param _owner The address of the owner of both _tokenId and _targetTokenId
* @param _tokenId The token to be merged into _targetTokenId
* @param _targetTokenId The token to receive all units of _tokenId
@ @param _mergeUnits The amount of units to be merged from _tokenId to _targetTokenId
*/
event Merge(address indexed _owner, uint256 indexed _tokenId, uint256 indexed _targetTokenId, uint256 _mergeUnits);

/**
* @dev This emits when the approved units of the approved address for a token is set or changed.
* @param _owner The address of the owner of the token
* @param _approved The address of the approved operator
* @param _tokenId The token to approve
@ @param _approvalUnits The amount of approved units for the operator
*/
event ApprovalUnits(address indexed _owner, address indexed _approved, uint256 indexed _tokenId, uint256 _approvalUnits);

/**
* @dev Find the slot of a token.
* @param _tokenId The identifier for a token
* @return The slot of the token
*/
function slotOf(uint256 _tokenId)  external view returns(uint256);

/**
* @dev Count all tokens holding the same slot.
* @param _slot The slot of which to count tokens
* @return The number of tokens of the specified slot
*/
function supplyOfSlot(uint256 _slot) external view returns (uint256);

/**
* @dev Find the number of decimals a token uses for units - e.g. 6, means the user representation of the units of a token can be calculated by dividing it by 1,000,000.
* @return The number of decimals for units of a token
*/
function unitDecimals() external view return (uint8);
/**
* @dev Enumerate all tokens of a slot.
* @param _slot The slot of which to enumerate tokens
* @param _index The index in the token list of the slot
* @return The id for the _indexth token in the token list of the slot
*/
function tokenOfSlotByIndex(uint256 _slot, uint256 _index) external view returns (uint256);

/**
* @dev Find the amount of units of a token.
* @param _tokenId The token to query units
* @return The amount of units of _tokenId
*/
function unitsInToken(uint256 _tokenId) external view returns (uint256);

/**
* @dev Set or change the approved units of an operator for a token.
* @param _to The address of the operator to be approved
* @param _tokenId The token to approve
* @param _units The amount of approved units for the operator
*/
function approve(address _to, uint256 _tokenId, uint256 _units) external;

/**
* @dev Find the approved units of an operator for a token.
* @param _tokenId The token to find the operator for
* @param _spender The address of an operator
* @return The approved units of _spender for _tokenId
*/
function allowance(uint256 _tokenId, address _spender) external view returns (uint256);

/**
* @dev Split a token into several by separating its units and assigning each portion to a new created token.
* @param _tokenId The token to split
* @param _units The amounts to split, i.e., the units of the new tokens created after split
* @return The ids of the new tokens created after split
*/
function split(uint256 _tokenId, uint256[] calldata _units) external returns (uint256[] memory);

/**
* @dev Merge several tokens into one by merging their units into a target token before burning them.
* @param _tokenIds The tokens to merge
* @param _targetTokenId The token to receive all units of the merged tokens
*/
function merge(uint256[] calldata _tokenIds, uint256 _targetTokenId) external;

/**
* @dev Transfer units from a token to a newly created token. When transferring to a smart contract, the caller SHOULD check if the recipient is capable of receiving ERC-3525 token units.
* @param _from The address of the owner of the token to transfer
* @param _to The address of the owner the newly created token
* @param _tokenId The token to partially transfer
* @param _units The amount of units to transfer
* @return The token created after transfer containing the transferred units
*/
function transferFrom(address _from, address _to, uint256 _tokenId, uint256 _units) external returns (uint256);

/**
* @dev Transfer partial units of a token to a newly created token. If _to is a smart contract, this function MUST call onERC3525Received on _to after transferring and then verify the return value.
* @param _from The address of the owner of the token to transfer
* @param _to The address of the owner the newly created token
* @param _tokenId The token to partially transfer
* @param _units The amount of units to transfer
* @param _data
* @return The token created after transfer containing the transferred units
*/
function safeTransferFrom(address _from, address _to, uint256 _tokenId, uint256 _units, bytes calldata _data) external returns (uint256);

/**
* @dev Transfer units from a token to another token. When transferring to a smart contract, the caller SHOULD check if the recipient is capable of receiving ERC-3525 token units.
* @param _from The address of the owner of the token to transfer
* @param _to The address of the owner the token to receive units
* @param _tokenId The token to transfer units from
* @param _targetTokenId The token to receive units
* @param _units The amount of units to transfer
*/
function transferFrom(address _from, address _to, uint256 _tokenId, uint256 _targetTokenId, uint256 _units) external;

/**
* @dev Transfer partial units of a token to an existing token. If _to is a smart contract, this function MUST call onERC3525Received on _to after transferring and then verify the return value.
* @param _from The address of the owner of the token to transfer
* @param _to The address of the owner the token to receive units
* @param _tokenId The token to partially transfer
* @param _targetTokenId The token to receive units
* @param _units The amount of units to transfer
* @param _data
*/
function safeTransferFrom(address _from, address _to, uint256 _tokenId, uint256 _targetTokenId, uint256 _units, bytes calldata _data) external;
}


Smart contracts MUST implement all of the functions in the IERC3525Receiver interface to accept transfers. See “Safe Transfer Rules” for further detail.

 /**
@notice Handle the receipt of a ERC-3525 token type.
@dev A ERC-3525 compliant smart contract MUST call this function on the token recipient contract, at the end of a safeTransferFrom after the balance has been updated.
This function MUST return bytes4(keccak256("onERC3525Received(address,address,uint256,uint256,bytes)")) (i.e. 0xb382cdcd) if it accepts the transfer.
This function MUST revert if it rejects the transfer.
Return of any other value than the prescribed keccak256 generated value MUST result in the transaction being reverted by the caller.
@param operator  The address which initiated the transfer (i.e. msg.sender)
@param from      The address which previously owned the token
@param id        The ID of the token being transferred
@param units     The units of tokenId being transferred
@param data      Additional data with no specified format
@return           bytes4(keccak256("onERC3525Received(address,address,uint256,uint256,bytes)"))
Note: the ERC-165 identifier for this interface is 0xb382cdcd.
*/
uint256 units, bytes calldata data) external returns (bytes4);
}


Token Manipulation

Scenarios

Transfer:

An ERC-3525 token is ERC-721 compatible, it adds units transfer ability, that can transfer units from one token to another token.

The units level transfer has two types of interfaces, and both have safe and unsafe versions:


1. function transferFrom(address from, address to, uint256 tokenId, uint256 targetTokenId, uint256 units) external;
bytes calldata data) external;

2. function transferFrom(address from, address to, uint256 tokenId, uint256 units) external returns (uint256 newTokenId);
function safeTransferFrom(address from, address to, uint256 tokenId, uint256 units, bytes calldata data)
external returns (uint256 newTokenId);



The main difference between the two kinds of interface is whether the application or the contract is responsible for determining/generating the target token ID in the transfer.

Since transferring units of a token will possibly result in new token id creation, it’s important to give the implementing contract the ability to do that. On the other hand, since part of a token can be transferred to a token with the same slot, we want to keep the flexibility for dapps to determine whether to use this ability, resulting in less contract complexity and less gas consumption.

Merge:

Several tokes with the same SLOT can be merged together using merge(uint256[] calldata tokenIds, uint256 targetTokenId);. targetTokenId should already exist, and cannot be one of tokenIds. After merging, targetTokenId owns all the units from the merged tokens, and the merged tokens will be burned.

Split:

One token can be split into several tokens, usingsplit(uint256 tokenId, uint256[] calldata units) returns (uint256[] memory newTokenIds);. This will result in several newly generated tokens containing units equal to the parameter units.

Rules

approving rules:

• For being compatible with ERC721, there are three kinds of approving operations, which SHOULD be used to indicate different levels of approval.
• setApprovalForAll SHOULD indicate the top level of approval, the authorized operators are capable of handling all tokens, including their units, owned by the owner.
• The ID level approve SHOULD indicate that the _tokenId is approved to the operator, but not the units of that token.
• The units level approve with the _units parameter SHOULD indicate that only the specified amount of units are approved to the operator, but not the whole token.
• Any approve MUST revert if msg.sender is equal to _to or _operator.
• The units level approve MUST revert if msg.sender is not the owner of _tokenId nor set approval for all tokens.
• The units level approve MUST emit the ApprovalUnits event.

splitting rules:

• MUST revert if _tokenId is not a valid token.
• MUST revert if msg.sender is neither the owner of _tokenId nor set approval for all.
• MUST revert if the sum of all _units exceeds the actual amount of units in _tokenId.
• MUST return an array containing the ids of the generated tokens after splitting.
• MUST emit the Split event.

merging rules:

• MUST revert if _targetTokenId or any of _tokenIds is not a valid token.
• MUST revert if the owner of tokenId is not the owner of _targetTokenId.
• MUST revert if msg.sender is neither the owner of all _tokenIds and targetTokenId nor having been set approval for all.
• MUST revert if any of _tokenIds is equal to _targetTokenId.
• Each of _tokenIds MUST be burnt after being merged.
• MUST emit the Merge event.

transferFrom rules:

• The transferFrom without the _targetTokenId parameter SHOULD indicate transferring units to the recipient, whether the units are transferred into an existing token or a generated new token, should be decided by the implementation contract.
• MUST revert unless msg.sender is the owner of _tokenId, or having been set approval for all tokens, or having been approved for a certain number units of _tokenId.
• MUST revert if _tokenId is not a valid token.
• MUST revert if _from is not the current owner of _tokenId.
• MUST revert if _to is the zero address.
• MUST revert if the transfer amount exceeds the actual amount of units in _tokenId.
• MUST revert if the transfer amount exceeds the approved units limit.
• MUST return the newly created token of the recipient containing the transferred units.
• MUST emit the TransferUnits event.
• The transferFrom with both the _units and the _targetTokenId parameters SHOULD indicate transferring units to an existing token of the recipient.
• MUST revert unless msg.sender is the owner of _tokenId, or having been set approval for all tokens, or the transfer amount is within the ERC-3525 approved units limit.
• MUST revert if either _tokenId or _targetTokenId is not a valid token.
• MUST revert if _from is not the current owner of _tokenId.
• MUST revert if _to is not the current owner of _targetTokenId.
• MUST revert if _to is the zero address.
• MUST revert if the transfer amount exceeds the actual amount of units in _tokenId.
• MUST revert if the transfer amount exceeds the ERC-3525 approved units limit.
• MUST emit the ERC-3525 level TransferUnits event.

safeTransferFrom rules:

• safeTransferFrom SHOULD be used to implement the same function as transferFrom, with an extra step to check if the recipient is capable of receiving ERC-3525 token units by implementing the onERC3525Received interface.
• MUST obey the above rules set for transferFrom.
• MUST check if _to is a smart contract (code size > 0). If so, safeTransferFrom MUST call onERC3525Received on _to and MUST revert if the return value does not match bytes4(keccak256("onERC3525Received(address,address,uint256,uint256,bytes)"))

The optional ERC3525Metadata extension can be identified with the ERC-165 Standard Interface Detection.


pragma solidity ^0.7.6;
function contractURI() external view returns (string memory);
function slotURI(uint256 slot_) external view returns (string memory);
}


This is the “ERC-3525 Metadata JSON Schema for contractURI()” referenced above.

{
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Contract Name"
},
"description": {
"type": "string",
"description": "Describes the contract "
},
"unitDecimals": {
"type": "integer",
"description": "The number of decimal places that the units should display - e.g. 18, means to divide the token units by 1000000000000000000 to get its user representation."
},
"properties": {
"type": "object",
"description": "Arbitrary properties. Values may be strings, numbers, object or arrays."
}
}
}


This is the “ERC-3525 Metadata JSON Schema for slotURI(uint)” referenced above.

{
"type": "object",
"properties": {
"tokensInSlot": {
"type": "integer",
"description": "Number of tokens in this slot, i.e., the number of tokens that have same business attributes represented by the slot."
},
"unitsInSlot": {
"type": "integer",
"description": "Total units in the slot, since tokens in same slot are fungible, this value equals to the sum of units of all tokens in this slot."
},
"properties": {
"type": "object",
"description": "Arbitrary properties. Values may be strings, numbers, object or arrays."
}
}
}


This is the “ERC-3525 Metadata JSON Schema for tokenURI(uint)” referenced above.

{
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Identifies the asset to which this NFT represents"
},
"description": {
"type": "string",
"description": "Describes the asset to which this NFT represents"
},
"image": {
"type": "string",
"description": "A URI pointing to a resource with mime type image/* representing the asset to which this NFT represents."
},
"units": {
"type": "BigNumber",
"description": ""
},
"slot": {
"type": "BigNumber",
"description": "The id of the slot that this token belongs to."
},
"properties": {
"type": "object",
"description": "Arbitrary properties. Values may be strings, numbers, object or arrays."
}
}
}


Approval

ERC-3525 adds a new approval model, that is, one can approve operators to transfer units from a token with certain ID, the new interface is: function approve(address to, uint256 tokenId, uint256 units);

Rationale

Since ERC-3525 is designed for representing underlying assets, rather than artifacts for gaming or arts, the implementation should give out the metadata directly from contract code, rather than give a URL of a server for returning metadata.

Design decision: Keep unsafe transfer

There are mainly two reasons we keep the unsafe transfer interfaces:

1. Since ERC-3525 token is ERC-721 compatible, we must keep compatibility for all wallets and contracts that are still calling unsafe transfer interfaces for ERC-721 tokens.
2. We want to keep the ability that dapps can trigger business logic on contracts by simply transferring ERC-3525 tokens to them, that is, a contract can put business logic in onERC3525Received function so that it can be called whenever a token is transferred using safeTransferFrom. However, in this situation, an approved contract with customized transfer functions like deposit etc. SHOULD never call safeTransferFrom since it will result in confusion that whether onERC3525Received is called by itself or other dapps that safe transfer a token to it.

Approval

For maximum semantical compatibility with ERC-721, as well as simplifying the approval model, we decided to make the relationship between two levels of approval like that:

1. Approval of an id does not result in the ability to partial transfer units from this id by the approved operator;
2. Approval of all units in a token does not result in the ability to transfer the token entity by the approved operator;
3. setApprovalForAll will result in the ability to transfer any tokens from the owner, as well as the ability to partial transfer units from any token.
4. setApprovalForAll will result in the ability to approve any tokens of the owner to third parties, as well as the ability to approve partial transfer units of any token to third parties.

Backwards Compatibility

As mentioned at the very beginning, a ERC-3525 is an extension interface of ERC721, hence it is 100% compatible with ERC-721.

Reference Implementation

Copyright and related rights waived via CC0.