EIP-3525: Semi-Fungible Token Source

Defines a specification where EIP-721 compatible tokens with the same SLOT and different IDs are fungible.

AuthorWill Wang, Mike Meng, Ethan Y. Tsai, Ryan Chow, Zhongxin Wu, AlvisDu
Discussions-Tohttps://ethereum-magicians.org/t/eip-3525-the-semi-fungible-token
StatusFinal
TypeStandards Track
CategoryERC
Created2020-12-01
Requires 20, 165, 721

Abstract

This is a standard for semi-fungible tokens. The set of smart contract interfaces described in this document defines an EIP-721 compatible token standard. This standard introduces an <ID, SLOT, VALUE> triple scalar model that represents the semi-fungible structure of a token. It also introduces new transfer models as well as approval models that reflect the semi-fungible nature of the tokens.

Token contains an EIP-721 equivalent ID property to identify itself as a universally unique entity, so that the tokens can be transferred between addresses and approved to be operated in EIP-721 compatible way.

Token also contains a value property, representing the quantitative nature of the token. The meaning of the ‘value’ property is quite like that of the ‘balance’ property of an EIP-20 token. Each token has a ‘slot’ attribute, ensuring that the value of two tokens with the same slot be treated as fungible, adding fungibility to the value property of the tokens.

This EIP introduces new token transfer models for semi-fungibility, including value transfer between two tokens of the same slot and value transfer from a token to an address.

Motivation

Tokenization is one of the most important trends by which to use and control digital assets in crypto. Traditionally, there have been two approaches to do so: fungible and non-fungible tokens. Fungible tokens generally use the EIP-20 standard, where every unit of an asset is identical to each other. EIP-20 is a flexible and efficient way to manipulate fungible tokens. Non-fungible tokens are predominantly EIP-721 tokens, a standard capable of distinguishing digital assets from one another based on identity.

However, both have significant drawbacks. For example, EIP-20 requires that users create a separate EIP-20 contract for each individual data structure or combination of customizable properties. In practice, this results in an extraordinarily large amount of EIP-20 contracts that need to be created. On the other hand, EIP-721 tokens provide no quantitative feature, significantly undercutting their computability, liquidity, and manageability. For example, if one was to create financial instruments such as bonds, insurance policy, or vesting plans using EIP-721, no standard interfaces are available for us to control the value in them, making it impossible, for example, to transfer a portion of the equity in the contract represented by the token.

A more intuitive and straightforward way to solve the problem is to create a semi-fungible token that has the quantitative features of EIP-20 and qualitative attributes of EIP-721. The backwards-compatibility with EIP-721 of such semi-fungible tokens would help utilize existing infrastructures already in use and lead to faster adoption.

Specification

The keywords “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.

Every EIP-3525 compliant contract must implement the EIP-3525, EIP-721 and EIP-165 interfaces

pragma solidity ^0.8.0;

/**
 * @title EIP-3525 Semi-Fungible Token Standard
 * Note: the EIP-165 identifier for this interface is 0xd5358140.
 */
interface IERC3525 /* is IERC165, IERC721 */ {
    /**
     * @dev MUST emit when value of a token is transferred to another token with the same slot,
     *  including zero value transfers (_value == 0) as well as transfers when tokens are created
     *  (`_fromTokenId` == 0) or destroyed (`_toTokenId` == 0).
     * @param _fromTokenId The token id to transfer value from
     * @param _toTokenId The token id to transfer value to
     * @param _value The transferred value
     */
    event TransferValue(uint256 indexed _fromTokenId, uint256 indexed _toTokenId, uint256 _value);

    /**
     * @dev MUST emit when the approval value of a token is set or changed.
     * @param _tokenId The token to approve
     * @param _operator The operator to approve for
     * @param _value The maximum value that `_operator` is allowed to manage
     */
    event ApprovalValue(uint256 indexed _tokenId, address indexed _operator, uint256 _value);
    
    /**
     * @dev MUST emit when the slot of a token is set or changed.
     * @param _tokenId The token of which slot is set or changed
     * @param _oldSlot The previous slot of the token
     * @param _newSlot The updated slot of the token
     */ 
    event SlotChanged(uint256 indexed _tokenId, uint256 indexed _oldSlot, uint256 indexed _newSlot);

    /**
     * @notice Get the number of decimals the token uses for value - e.g. 6, means the user
     *  representation of the value of a token can be calculated by dividing it by 1,000,000.
     *  Considering the compatibility with third-party wallets, this function is defined as
     *  `valueDecimals()` instead of `decimals()` to avoid conflict with EIP-20 tokens.
     * @return The number of decimals for value
     */
    function valueDecimals() external view returns (uint8);

    /**
     * @notice Get the value of a token.
     * @param _tokenId The token for which to query the balance
     * @return The value of `_tokenId`
     */
    function balanceOf(uint256 _tokenId) external view returns (uint256);

    /**
     * @notice Get 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);

    /**
     * @notice Allow an operator to manage the value of a token, up to the `_value`.
     * @dev MUST revert unless caller is the current owner, an authorized operator, or the approved
     *  address for `_tokenId`.
     *  MUST emit the ApprovalValue event.
     * @param _tokenId The token to approve
     * @param _operator The operator to be approved
     * @param _value The maximum value of `_toTokenId` that `_operator` is allowed to manage
     */
    function approve(
        uint256 _tokenId,
        address _operator,
        uint256 _value
    ) external payable;

    /**
     * @notice Get the maximum value of a token that an operator is allowed to manage.
     * @param _tokenId The token for which to query the allowance
     * @param _operator The address of an operator
     * @return The current approval value of `_tokenId` that `_operator` is allowed to manage
     */
    function allowance(uint256 _tokenId, address _operator) external view returns (uint256);

    /**
     * @notice Transfer value from a specified token to another specified token with the same slot.
     * @dev Caller MUST be the current owner, an authorized operator or an operator who has been
     *  approved the whole `_fromTokenId` or part of it.
     *  MUST revert if `_fromTokenId` or `_toTokenId` is zero token id or does not exist.
     *  MUST revert if slots of `_fromTokenId` and `_toTokenId` do not match.
     *  MUST revert if `_value` exceeds the balance of `_fromTokenId` or its allowance to the
     *  operator.
     *  MUST emit `TransferValue` event.
     * @param _fromTokenId The token to transfer value from
     * @param _toTokenId The token to transfer value to
     * @param _value The transferred value
     */
    function transferFrom(
        uint256 _fromTokenId,
        uint256 _toTokenId,
        uint256 _value
    ) external payable;


    /**
     * @notice Transfer value from a specified token to an address. The caller should confirm that
     *  `_to` is capable of receiving EIP-3525 tokens.
     * @dev This function MUST create a new EIP-3525 token with the same slot for `_to`, 
     *  or find an existing token with the same slot owned by `_to`, to receive the transferred value.
     *  MUST revert if `_fromTokenId` is zero token id or does not exist.
     *  MUST revert if `_to` is zero address.
     *  MUST revert if `_value` exceeds the balance of `_fromTokenId` or its allowance to the
     *  operator.
     *  MUST emit `Transfer` and `TransferValue` events.
     * @param _fromTokenId The token to transfer value from
     * @param _to The address to transfer value to
     * @param _value The transferred value
     * @return ID of the token which receives the transferred value
     */
    function transferFrom(
        uint256 _fromTokenId,
        address _to,
        uint256 _value
    ) external payable returns (uint256);
}

The slot’s enumeration extension is OPTIONAL. This allows your contract to publish its full list of SLOTs and make them discoverable.

pragma solidity ^0.8.0;

/**
 * @title EIP-3525 Semi-Fungible Token Standard, optional extension for slot enumeration
 * @dev Interfaces for any contract that wants to support enumeration of slots as well as tokens 
 *  with the same slot.
 * Note: the EIP-165 identifier for this interface is 0x3b741b9e.
 */
interface IERC3525SlotEnumerable is IERC3525 /* , IERC721Enumerable */ {

    /**
     * @notice Get the total amount of slots stored by the contract.
     * @return The total amount of slots
     */
    function slotCount() external view returns (uint256);

    /**
     * @notice Get the slot at the specified index of all slots stored by the contract.
     * @param _index The index in the slot list
     * @return The slot at `index` of all slots.
     */
    function slotByIndex(uint256 _index) external view returns (uint256);

    /**
     * @notice Get the total amount of tokens with the same slot.
     * @param _slot The slot to query token supply for
     * @return The total amount of tokens with the specified `_slot`
     */
    function tokenSupplyInSlot(uint256 _slot) external view returns (uint256);

    /**
     * @notice Get the token at the specified index of all tokens with the same slot.
     * @param _slot The slot to query tokens with
     * @param _index The index in the token list of the slot
     * @return The token ID at `_index` of all tokens with `_slot`
     */
    function tokenInSlotByIndex(uint256 _slot, uint256 _index) external view returns (uint256);
}

The slot level approval is OPTIONAL. This allows any contract that wants to support approval for slots, which allows an operator to manage one’s tokens with the same slot.

pragma solidity ^0.8.0;

/**
 * @title EIP-3525 Semi-Fungible Token Standard, optional extension for approval of slot level
 * @dev Interfaces for any contract that wants to support approval of slot level, which allows an
 *  operator to manage one's tokens with the same slot.
 *  See https://eips.ethereum.org/EIPS/eip-3525
 * Note: the EIP-165 identifier for this interface is 0xb688be58.
 */
interface IERC3525SlotApprovable is IERC3525 {
    /**
     * @dev MUST emit when an operator is approved or disapproved to manage all of `_owner`'s
     *  tokens with the same slot.
     * @param _owner The address whose tokens are approved
     * @param _slot The slot to approve, all of `_owner`'s tokens with this slot are approved
     * @param _operator The operator being approved or disapproved
     * @param _approved Identify if `_operator` is approved or disapproved
     */
    event ApprovalForSlot(address indexed _owner, uint256 indexed _slot, address indexed _operator, bool _approved);

    /**
     * @notice Approve or disapprove an operator to manage all of `_owner`'s tokens with the
     *  specified slot.
     * @dev Caller SHOULD be `_owner` or an operator who has been authorized through
     *  `setApprovalForAll`.
     *  MUST emit ApprovalSlot event.
     * @param _owner The address that owns the EIP-3525 tokens
     * @param _slot The slot of tokens being queried approval of
     * @param _operator The address for whom to query approval
     * @param _approved Identify if `_operator` would be approved or disapproved
     */
    function setApprovalForSlot(
        address _owner,
        uint256 _slot,
        address _operator,
        bool _approved
    ) external payable;

    /**
     * @notice Query if `_operator` is authorized to manage all of `_owner`'s tokens with the
     *  specified slot.
     * @param _owner The address that owns the EIP-3525 tokens
     * @param _slot The slot of tokens being queried approval of
     * @param _operator The address for whom to query approval
     * @return True if `_operator` is authorized to manage all of `_owner`'s tokens with `_slot`,
     *  false otherwise.
     */
    function isApprovedForSlot(
        address _owner,
        uint256 _slot,
        address _operator
    ) external view returns (bool);
}

EIP-3525 Token Receiver

If a smart contract wants to be informed when they receive values from other addresses, it should implement all of the functions in the IERC3525Receiver interface, in the implementation it can decide whether to accept or reject the transfer. See “Transfer Rules” for further detail.

 pragma solidity ^0.8.0;

/**
 * @title EIP-3525 token receiver interface
 * @dev Interface for a smart contract that wants to be informed by EIP-3525 contracts when receiving values from ANY addresses or EIP-3525 tokens.
 * 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.
     * @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);

}

Token Manipulation

Scenarios

Transfer:

Besides EIP-721 compatible token transfer methods, this EIP introduces two new transfer models: value transfer from ID to ID, and value transfer from ID to address.

function transferFrom(uint256 _fromTokenId, uint256 _toTokenId, uint256 _value) external payable;
	
function transferFrom(uint256 _fromTokenId, address _to, uint256 _value) external payable returns (uint256 toTokenId_);

The first one allows value transfers from one token (specified by _fromTokenId) to another token (specified by _toTokenId) within the same slot, resulting in the _value being subtracted from the value of the source token and added to the value of the destination token;

The second one allows value transfers from one token (specified by _fromTokenId) to an address (specified by _to), the value is actually transferred to a token owned by the address, and the id of the destination token should be returned. Further explanation can be found in the ‘design decision’ section for this method.

Rules

approving rules:

This EIP provides four kinds of approving functions indicating different levels of approvals, which can be described as full level approval, slot level approval, token ID level approval as well as value level approval.

  • setApprovalForAll, compatible with EIP-721, SHOULD indicate the full level of approval, which means that the authorized operators are capable of managing all the tokens, including their values, owned by the owner.
  • setApprovalForSlot (optional) SHOULD indicate the slot level of approval, which means that the authorized operators are capable of managing all the tokens with the specified slot, including their values, owned by the owner.
  • The token ID level approve function, compatible with EIP-721, SHOULD indicate that the authorized operator is capable of managing only the specified token ID, including its value, owned by the owner.
  • The value level approve function, SHOULD indicate that the authorized operator is capable of managing the specified maximum value of the specified token owned by the owner.
  • For any approving function, the caller MUST be the owner or has been approved with a higher level of authority.

transferFrom rules:

  • The transferFrom(uint256 _fromTokenId, uint256 _toTokenId, uint256 _value) function, SHOULD indicate value transfers from one token to another token, in accordance with the rules below:

    • MUST revert unless msg.sender is the owner of _fromTokenId, an authorized operator or an operator who has been approved the whole token or at least _value of it.
    • MUST revert if _fromTokenId or _toTokenId is zero token id or does not exist.
    • MUST revert if slots of _fromTokenId and _toTokenId do not match.
    • MUST revert if _value exceeds the value of _fromTokenId or its allowance to the operator.
    • MUST check for the onERC3525Received function if the owner of _toTokenId is a smart contract, if the function exists, MUST call this function after the value transfer, MUST revert if the result is not equal to 0x009ce20b;
    • MUST emit TransferValue event.
  • The transferFrom(uint256 _fromTokenId, address _to, uint256 _value) function, which transfers value from one token ID to an address, SHOULD follow the rule below:

    • MUST either find a EIP-3525 token owned by the address _to or create a new EIP-3525 token, with the same slot of _fromTokenId, to receive the transferred value.
    • MUST revert unless msg.sender is the owner of _fromTokenId, an authorized operator or an operator who has been approved the whole token or at least _value of it.
    • MUST revert if _fromTokenId is zero token id or does not exist.
    • MUST revert if _to is zero address.
    • MUST revert if _value exceeds the value of _fromTokenId or its allowance to the operator.
    • MUST check for the onERC3525Received function if the _to address is a smart contract, if the function exists, MUST call this function after the value transfer, MUST revert if the result is not equal to 0x009ce20b;
    • MUST emit Transfer and TransferValue events.

Metadata

Metadata Extensions

EIP-3525 metadata extensions are compatible EIP-721 metadata extensions.

This optional interface can be identified with the EIP-165 Standard Interface Detection.

pragma solidity ^0.8.0;

/**
 * @title EIP-3525 Semi-Fungible Token Standard, optional extension for metadata
 * @dev Interfaces for any contract that wants to support query of the Uniform Resource Identifier
 *  (URI) for the EIP-3525 contract as well as a specified slot. 
 *  Because of the higher reliability of data stored in smart contracts compared to data stored in 
 *  centralized systems, it is recommended that metadata, including `contractURI`, `slotURI` and 
 *  `tokenURI`, be directly returned in JSON format, instead of being returned with a url pointing 
 *  to any resource stored in a centralized system. 
 *  See https://eips.ethereum.org/EIPS/eip-3525
 * Note: the EIP-165 identifier for this interface is 0xe1600902.
 */
interface IERC3525Metadata is
    IERC3525 /* , IERC721Metadata */
{
    /**
     * @notice Returns the Uniform Resource Identifier (URI) for the current EIP-3525 contract.
     * @dev This function SHOULD return the URI for this contract in JSON format, starting with
     *  header `data:application/json;`.
     *  See https://eips.ethereum.org/EIPS/eip-3525 for the JSON schema for contract URI.
     * @return The JSON formatted URI of the current EIP-3525 contract
     */
    function contractURI() external view returns (string memory);

    /**
     * @notice Returns the Uniform Resource Identifier (URI) for the specified slot.
     * @dev This function SHOULD return the URI for `_slot` in JSON format, starting with header
     *  `data:application/json;`.
     *  See https://eips.ethereum.org/EIPS/eip-3525 for the JSON schema for slot URI.
     * @return The JSON formatted URI of `_slot`
     */
    function slotURI(uint256 _slot) external view returns (string memory);
}

EIP-3525 Metadata URI JSON Schema

This is the “EIP-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"
    },
    "image": {
      "type": "string",
      "description": "Optional. Either a base64 encoded imgae data or a URI pointing to a resource with mime type image/* representing what this contract represents."
    },
    "external_link": {
      "type": "string",
      "description": "Optional. A URI pointing to an external resource."
    },
    "valueDecimals": {
      "type": "integer",
      "description": "The number of decimal places that the balance should display - e.g. 18, means to divide the token value by 1000000000000000000 to get its user representation."
    }
  }
}

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

{
  "title": "Slot Metadata",
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "description": "Identifies the asset category to which this slot represents"
    },
    "description": {
      "type": "string",
      "description": "Describes the asset category to which this slot represents"
    },
    "image": {
      "type": "string",
      "description": "Optional. Either a base64 encoded imgae data or a URI pointing to a resource with mime type image/* representing the asset category to which this slot represents."
    },
    "properties": {
      "type": "array",
      "description": "Each item of `properties` SHOULD be organized in object format, including name, description, value, order (optional), display_type (optional), etc."
      "items": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "The name of this property."
          },
          "description": {
            "type": "string",
            "description": "Describes this property."
          }
          "value": {
            "description": "The value of this property, which may be a string or a number."
          },
          "is_intrinsic": {
            "type": "boolean",
            "description": "According to the definition of `slot`, one of the best practice to generate the value of a slot is utilizing the `keccak256` algorithm to calculate the hash value of multi properties. In this scenario, the `properties` field should contain all the properties that are used to calculate the value of `slot`, and if a property is used in the calculation, is_intrinsic must be TRUE."
          },
          "order": {
            "type": "integer",
            "description": "Optional, related to the value of is_intrinsic. If is_intrinsic is TRUE, it must be the order of this property appeared in the calculation method of the slot."
          },
          "display_type": {
            "type": "string",
            "description": "Optional. Specifies in what form this property should be displayed."
          }
        }
      }
    }
  }
}

This is the “EIP-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 token represents"
    },
    "description": {
      "type": "string",
      "description": "Describes the asset to which this token represents"
    },
    "image": {
      "type": "string",
      "description": "Either a base64 encoded imgae data or a URI pointing to a resource with mime type image/* representing the asset to which this token represents."
    },
    "balance": {
      "type": "integer",
      "description": "THe value held by this token."
    },
    "slot": {
      "type": "integer",
      "description": "The id of the slot that this token belongs to."
    },
    "properties": {
      "type": "object",
      "description": "Arbitrary properties. Values may be strings, numbers, objects or arrays. Optional, you can use the same schema as the properties section of EIP-3525 Metadata JSON Schema for slotURI(uint) if you need a better description attribute."
    }
  }
}

Rationale

Metadata generation

This token standard is designed to represent semi-fungible assets, which are most suited for financial instruments rather than collectibles or in-game items. For maximum transparency and safety of digital assets, we strongly recommend that all implementations should generate metadata directly from contract code rather than giving out an off-chain server URL.

Design decision: Value transfer from token to address

The ‘value’ of a token is a property of the token and is not linked to an address, so to transfer the value to an address would be actually transferring it to a token owned by that address, not the address itself.

From the implementation perspective, the process of transferring values from token to address could be done as follows: (1) create a new token for the recipient’s address, (2) transfer the value to the new token from the ‘source token’. So that this method is not fully independent from the ID-to-ID transfer method, and can be viewed as syntactic sugar that wraps the process described above.

In a special case, if the destination address owns one or more tokens with the same slot value as the source token, this method will have an alternative implementation as follows: (1) find one token owned by the address with the same slot value of the source token, (2) transfer the value to the found token.

Both implementations described above should be treated as compliant with this standard.

The purpose of maintaining id-to-address transfer function is to maximize the compatibility with most wallet apps, since for most of the token standards, the destination of token transfer are addresses. This syntactic wrapping will help wallet apps easily implement the value transfer function from a token to any address.

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 that 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 callers only need to call one kind of transfer;
  2. All EIP-3525 contracts need to MUST check for the existence of onERC3525Received on the recipient contract and call the function when it exists;
  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, or any other value to reject.

There is a special case for this notification/acceptance mechanism, since EIP-3525 allow value transfer from an address to itself, so that when a smart contract which implements onERC3525Received transfers value to itself, this function will also be called. So that the contract is responsible to choose different rules of acceptance between self-value-transfer and receiving value from other addresses.

Design decision: Relationship between different approval models

For semantic compatibility with EIP-721 as well as the flexibility of value manipulation of tokens, we decided to define the relationships between some of the levels of approval like that:

  1. Approval of an id will lead to the ability to partially transfer values from this id by the approved operator, this will simplify the value approval for an id. However, the approval of total values in a token should not lead to the ability to transfer the token entity by the approved operator.
  2. setApprovalForAll will lead to the ability to partially transfer values from any token, as well as the ability to approve partial transfer of values from any token to a third party, this will simplify the value transfer and approval of all tokens owned by an address.

Backwards Compatibility

As mentioned in the beginning, this EIP is backward compatible with EIP-721.

Reference Implementation

Security Considerations

The value level approval and slot level approval(optional) is isolated from EIP-721 approval models, so that approving value should not affect EIP-721 level approvals, implementations of this EIP must obey this principle.

Since this EIP is EIP-721 compatible, any wallets and smart contracts that can hold and manipulate standard EIP-721 tokens will have no risks of asset loss for EIP-3525 tokens.

Copyright and related rights waived via CC0.

Citation

Please cite this document as:

Will Wang, Mike Meng, Ethan Y. Tsai, Ryan Chow, Zhongxin Wu, AlvisDu, "EIP-3525: Semi-Fungible Token," Ethereum Improvement Proposals, no. 3525, December 2020. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-3525.