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

EIP-3525: Fractionalized NFT Standard Source

AuthorWill Wang, Mike Meng, TsaiYee, Ryan Chow, Zhongxin Wu, AlvisDu
Discussions-Tohttps://github.com/ethereum/EIPs/issues/3641
StatusDraft
TypeStandards Track
CategoryERC
Created2020-12-01
Requires 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, which enhances ERC-721 by adding a ‘units’ property to represent the quantitative nature of a token and enabling some quantitative operations for the token, most notably splitting and merging.

Abstract

VNFT is ERC-721 compatible, which means, as an ERC-721 token, each VNFT contains an ID property to identify itself as a universally unique entity. What empowers a VNFT is that it contains a ‘units’ property, representing the quantitative nature of the token. Thus, this VNFT can be split into several different VNFTs, with certain properties maintained unchanged but the sum of the units of all split-out VNFTs equals that of the original one. Nevertheless, each VNFT has a ‘SLOT’ attribute, which labels its logical category. Several VNFTs can be merged into one VNFT 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 contract implementing the VNFT standard MUST implement the VNFT interfaces as follows:

pragma solidity 0.7.6;

/**
 * @title VNFT Versatile Non-Fungible Token Standard
 * @dev See https://eips.ethereum.org/EIPS/eip-VNFT
 * Note: the ERC-165 identifier for this interface is 0x1487d183.
 */
interface VNFT /* 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 PartialTransfer(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 decimals() 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 partial units of a token to a newly created token. When transferring to a smart contract, the caller SHOULD check if the recipient is capable of receiving VNFTs.
     * @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 `onVNFTReceived` 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 partial units of a token to an existing token. When transferring to a smart contract, the caller SHOULD check if the recipient is capable of receiving VNFTs.
     * @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
     */
    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 `onVNFTReceived` 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;
}

VNFT Token Receiver

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

 /**
        @notice Handle the receipt of a VNFT token type.
        @dev A VNFT-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("onVNFTReceived(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("onVNFTReceived(address,address,uint256,uint256,bytes)"))`
	Note: the ERC-165 identifier for this interface is 0xb382cdcd.
 */
interface IVNFTReceiver {
    function onVNFTReceived(address operator, address from, uint256 tokenId,
        uint256 units, bytes calldata data) external returns (bytes4);
}

Token Manipulation

Scenarios

Transfer:

Since a VNFT token is ERC-721 compatible, it has ID level transfer and units level transfer.

The ID level transfer SHOULD obey ERC-721 transfer rules with neither extra restrictions nor special treatments.

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 partial transfer 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 ERC721 level transferFrom without the _units and the _targetTokenId parameters SHOULD indicate transferring a whole token, including all of its units.
    • MUST revert unless msg.sender is the owner of _tokenId, or the ERC721 level approved address, or having been set approval for all tokens.
    • 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 emit the ERC721 level Transfer event
  • The VNFT level transferFrom without the _targetTokenId parameter SHOULD indicate transferring partial units to a new token of the recipient.
    • MUST revert unless msg.sender is the owner of _tokenId, or having been set approval for all tokens, or having been VNFT approved 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 VNFT approved units limit.
    • MUST return the newly created token of the recipient containing the transferred units.
    • MUST emit the VNFT level PartialTransfer event.
  • The VNFT level transferFrom with both the _units and the _targetTokenId parameters SHOULD indicate transferring partial 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 VNFT 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 VNFT approved units limit.
    • MUST emit the VNFT level PartialTransfer 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 VNFTs by implementing the onVNFTReceived 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 onVNFTReceived on _to and MUST revert if the return value does not match bytes4(keccak256("onVNFTReceived(address,address,uint256,uint256,bytes)"))

Metadata

Metadata Extensions

VNFT metadata extensions are compatible ERC-721 metadata extensions.

VNFT Metadata URI JSON Schema

This is the “VNFT Metadata JSON Schema” referenced above.

{
    "title": "Asset 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. Consider making any images at a width between 320 and 1080 pixels and aspect ratio between 1.91:1 and 4:5 inclusive."
        },
         "decimals": {
            "type": "integer",
            "description": "The number of decimal places that the token amount should display - e.g. 18, means to divide the token amount by 1000000000000000000 to get its user representation."
        },
        "properties": {
            "type": "object",
            "description": "Arbitrary properties. Values may be strings, numbers, object or arrays."
        }
    }
}

Approval

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

Rationale

Metadata generation

Since VNFT 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 VNFT 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 VNFT tokens to them, that is, a contract can put business logic in onVNFTReceived 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 onVNFTReceived 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 VNFT contract is basically an ERC721 contract, hence it is 100% compatible with ERC-721.

Reference Implementation

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: Fractionalized NFT Standard [DRAFT]," Ethereum Improvement Proposals, no. 3525, December 2020. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-3525.