EIP-3525: Semi-Fungible Token Standard
| Author | Will Wang, Mike Meng, TsaiYee, Ryan Chow, Zhongxin Wu, AlvisDu |
|---|---|
| Discussions-To | https://github.com/ethereum/EIPs/issues/3641 |
| Status | Draft |
| Type | Standards Track |
| Category | ERC |
| Created | 2020-12-01 |
| Requires | 165, 721 |
Table of Contents
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
- How vnft can improve position management in uniswap-v3
- What are digital assets
- Vnft tokens vs.erc-20 vs. erc-721
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
- ERC-721 Non-Fungible Token Standard
- ERC-165 Standard Interface Detection
- JSON Schema
- RFC 2119 Key words for use in RFCs to Indicate Requirement Levels
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 `_index`th 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;
}
ERC-3525 Token Receiver
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.
*/
interface IERC3525Receiver {
function onERC3525Received(address operator, address from, uint256 tokenId,
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;
function safeTransferFrom(address from, address to, uint256 tokenId, uint256 targetTokenId, uint256 units,
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.
setApprovalForAllSHOULD 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
approveSHOULD indicate that the_tokenIdis approved to the operator, but not the units of that token. - The units level
approvewith the_unitsparameter SHOULD indicate that only the specified amount of units are approved to the operator, but not the whole token. - Any
approveMUST revert ifmsg.senderis equal to_toor_operator. - The units level
approveMUST revert ifmsg.senderis not the owner of_tokenIdnor set approval for all tokens. - The units level
approveMUST emit theApprovalUnitsevent.
splitting rules:
- MUST revert if
_tokenIdis not a valid token. - MUST revert if
msg.senderis neither the owner of_tokenIdnor set approval for all. - MUST revert if the sum of all
_unitsexceeds the actual amount of units in_tokenId. - MUST return an array containing the ids of the generated tokens after splitting.
- MUST emit the
Splitevent.
merging rules:
- MUST revert if
_targetTokenIdor any of_tokenIdsis not a valid token. - MUST revert if the owner of
tokenIdis not the owner of_targetTokenId. - MUST revert if
msg.senderis neither the owner of all_tokenIdsandtargetTokenIdnor having been set approval for all. - MUST revert if any of
_tokenIdsis equal to_targetTokenId. - Each of
_tokenIdsMUST be burnt after being merged. - MUST emit the
Mergeevent.
transferFrom rules:
- The
transferFromwithout the_targetTokenIdparameter 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.senderis 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
_tokenIdis not a valid token. - MUST revert if
_fromis not the current owner of_tokenId. - MUST revert if
_tois 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
TransferUnitsevent.
- MUST revert unless
- The
transferFromwith both the_unitsand the_targetTokenIdparameters SHOULD indicate transferring units to an existing token of the recipient.- MUST revert unless
msg.senderis 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
_tokenIdor_targetTokenIdis not a valid token. - MUST revert if
_fromis not the current owner of_tokenId. - MUST revert if
_tois not the current owner of_targetTokenId. - MUST revert if
_tois 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
TransferUnitsevent.
- MUST revert unless
safeTransferFrom rules:
safeTransferFromSHOULD be used to implement the same function astransferFrom, with an extra step to check if the recipient is capable of receiving ERC-3525 token units by implementing theonERC3525Receivedinterface.- MUST obey the above rules set for
transferFrom. - MUST check if
_tois a smart contract (code size > 0). If so,safeTransferFromMUST callonERC3525Receivedon_toand MUST revert if the return value does not matchbytes4(keccak256("onERC3525Received(address,address,uint256,uint256,bytes)"))
Metadata
Metadata Extensions
ERC-3525 metadata extensions are compatible ERC-721 metadata extensions.
The optional ERC3525Metadata extension can be identified with the ERC-165 Standard Interface Detection.
pragma solidity ^0.7.6;
interface ERC3525Metadata /* is IERC721Metadata */ {
function contractURI() external view returns (string memory);
function slotURI(uint256 slot_) external view returns (string memory);
}
ERC-3525 Metadata URI JSON Schema
This is the “ERC-3525 Metadata JSON Schema for contractURI()” referenced above.
{
"title": "Contract Metadata",
"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.
{
"title": "Slot Metadata",
"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.
{
"title": "Token Metadata",
"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
Metadata generation
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:
- 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.
- 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
onERC3525Receivedfunction so that it can be called whenever a token is transferred usingsafeTransferFrom. However, in this situation, an approved contract with customized transfer functions like deposit etc. SHOULD never callsafeTransferFromsince it will result in confusion that whetheronERC3525Receivedis 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:
- Approval of an id does not result in the ability to partial transfer units from this id by the approved operator;
- Approval of all units in a token does not result in the ability to transfer the token entity by the approved operator;
setApprovalForAllwill result in the ability to transfer any tokens from the owner, as well as the ability to partial transfer units from any token.setApprovalForAllwill 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
Copyright and related rights waived via CC0.
Citation
Please cite this document as:
Will Wang, Mike Meng, TsaiYee, Ryan Chow, Zhongxin Wu, AlvisDu, "EIP-3525: Semi-Fungible Token Standard [DRAFT]," Ethereum Improvement Proposals, no. 3525, December 2020. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-3525.