Protocol Standard

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 VIP-3525 compliant contract must implement the VIP-3525, VIP-721 and VIP-165 interfaces

pragma solidity ^0.8.0;

/**
 * @title VIP-3525 Semi-Fungible Token Standard
 * Note: the VIP-165 identifier for this interface is 0xd5358140.
 */
interface IVRC3525 /* is IVRC165, IVRC721 */ {
    /**
     * @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 VIP-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 VIP-3525 tokens.
     * @dev This function MUST create a new VIP-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 VIP-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 VIP-165 identifier for this interface is 0x3b741b9e.
 */
interface IVRC3525SlotEnumerable is IVRC3525 /* , IVRC721Enumerable */ {

    /**
     * @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 VIP-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.
 * Note: the VIP-165 identifier for this interface is 0xb688be58.
 */
interface IVRC3525SlotApprovable is IVRC3525 {
    /**
     * @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 VIP-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 VIP-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);
}

VIP-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 IVRC3525Receiver 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 VIP-3525 token receiver interface
 * @dev Interface for a smart contract that wants to be informed by VIP-3525 contracts when receiving values from ANY addresses or VIP-3525 tokens.
 * Note: the VIP-165 identifier for this interface is 0x009ce20b.
 */
interface IVRC3525Receiver {
    /**
     * @notice Handle the recVIPt of an VIP-3525 token value.
     * @dev An VIP-3525 smart contract MUST check whether this function is implemented by the recipient contract, if the
     *  recipient contract implements this function, the VIP-3525 contract MUST call this function after a 
     *  value transfer (i.e. `transferFrom(uint256,uint256,uint256,bytes)`).
     *  MUST return 0x009ce20b (i.e. `bytes4(keccak256('onVRC3525Received(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('onVRC3525Received(address,uint256,uint256,uint256,bytes)'))` 
     *  unless the transfer is rejected.
     */
    function onVRC3525Received(address _operator, uint256 _fromTokenId, uint256 _toTokenId, uint256 _value, bytes calldata _data) external returns (bytes4);

}

Token Manipulation
Scenarios
Transfer:

Besides VIP-721 compatible token transfer methods, this VIP 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 VIP 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 VIP-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 VIP-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 onVRC3525Received 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 VIP-3525 token owned by the address _to or create a new VIP-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 onVRC3525Received 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
VIP-3525 metadata extensions are compatible VIP-721 metadata extensions.

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

pragma solidity ^0.8.0;

/**
 * @title VIP-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 VIP-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. 
 * Note: the VIP-165 identifier for this interface is 0xe1600902.
 */
interface IVRC3525Metadata is
    IVRC3525 /* , IVRC721Metadata */
{
    /**
     * @notice Returns the Uniform Resource Identifier (URI) for the current VIP-3525 contract.
     * @dev This function SHOULD return the URI for this contract in JSON format, starting with
     *  header `data:application/json;`.
     * @return The JSON formatted URI of the current VIP-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;`.
     * @return The JSON formatted URI of `_slot`
     */
    function slotURI(uint256 _slot) external view returns (string memory);
}

VIP-3525 Metadata URI JSON Schema
This is the “VIP-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 “VIP-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 “VIP-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 VIP-3525 Metadata JSON Schema for slotURI(uint) if you need a better description attribute."
    }
  }
}