Skip to content

Ayuilos/MultiResourceEIP

Repository files navigation

Simple Summary

A standard interface for multi-resource non-fungible tokens.

Abstract

The Multi Resource NFT standard is a standalone part of RMRK concepts and can be easily togehter with ERC-721. It allows for the construction of a new primitive: context-dependent output of multimedia information per single NFT.

An NFT can have multiple resources (outputs), and orders them by priority. They do not have to match in mimetype or tokenURI, nor do they depend on one another. Resources are not standalone entities, but should be thought of as “namespaced tokenURIs” that can be ordered at will by the NFT owner, but only modified, updated, added, or removed if agreed on by both owner and minter.

Motivation

There are four key use cases that the current ERC721 standard is ill-equipped to handle:

  • cross-metaverse compatibility
  • multi-media output
  • media redundancy
  • NFT evolution

Let us look at each in depth.

Cross-metaverse compatibility

Perhaps better phrased as cross-engine compatibility, solves the (very valid) complaint of gamer communities when they say that a skin for Counterstrike is not portable into something like Fortnite because the engines are different - it is not a simple matter of just having an NFT.

With Multi-resource NFTs, it is.

One resource is a skin for Fortnite, an actual skin file. Another is a skin file for Counterstrike. A third is a generic resource intended to be shown in catalogs, marketplaces, portfolio trackers - a representation, stylized thumbnail, or animated demo or trailer of the skin that renders outside of any of the two games.

When using the NFT in one such game, not only do the game developers not need to pre-build the asset into the game and then allow it based on NFT balance in the logged in web3 address, but the NFT has everything it needs in its skin file, making storage and ownership of this skin actually decentralized and not reliant on the gamedev team.

After the fact, this NFT can be given further utility by means of new additional resources: more games, more skins, appended to the same NFT. Thus, a game skin as an NFT becomes an ever-evolving NFT of infinite utility.

Multi-media output

An NFT that is an eBook can be both a PDF and an audio file at the same time, and depending on which software loads it, that is the media output that gets consumed: PDF if loaded into Kindle, audio if loaded into Audible. Additionally, an extra resource that is a simple image can be present in the NFT, intended for showing on the various marketplaces, SERP pages, portfolio trackers and others - perhaps the book’s cover image.

Media Redundancy

Many NFTs are minted hastily without best practices in mind - specifically, many NFTs are minted with metadata centralized on a server somewhere or, in some cases, a hardcoded IPFS gateway which can also go down, instead of just an IPFS hash.

By adding the same metadata file as different resources, e.g., one resource of a metadata and its linked image on Arweave, one resource of this same combo on Sia, another of the same combo on IPFS, etc., the resilience of the metadata and its referenced media increases exponentially as the chances of all the protocols going down at once become ever less likely.

NFT Evolution

Many NFTs, particularly game related ones, require evolution. This is especially the case in modern metaverses where no metaverse is actually a metaverse - it is just a multiplayer game hosted on someone’s server which replaced username/password logins with reading an NFT’s balance.

When the server goes down or the game shuts down, the player ends up with nothing (loss of experience) or something unrelated (resources or accessories unrelated to the game experience, spamming the wallet, incompatible with other “verses” - see cross-metaverse compatibility above).

With Multi-resource NFTs, a minter or another pre-approved entity is allowed to suggest a new resource to the NFT owner who can then accept it or reject it. The resource can even target an existing resource which is to be replaced.

This allows level-up mechanics where, once enough experience has been collected, a user can accept the level-up. The level-up consists of a new resource being added to the NFT, and once accepted, this new resource replaces the old one.

As a concrete example, think of Pokemon™️ evolving - once enough experience has been attained, a trainer can choose to evolve their monster. With Multi-resource NFTs, it is not necessary to have centralized control over metadata to replace it, nor is it necessary to airdrop another NFT into a user’s wallet - instead, a new Raichu resource is minted onto Pikachu, and if accepted, the Pikachu resource is gone, replaced by Raichu, which now has its own attributes, values, etc.

The level-up mechanic can be further expanded by being combined with nesting and equippables as specified in the RMRK concepts but this is outside of the scope of this EIP.

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.

/// @title ERC-**** Multi-Resource Token Standard
/// @dev See https://eips.ethereum.org/EIPS/********
///  Note: the ERC-165 identifier for this interface is 0x********.
pragma solidity ^0.8.9;

interface IMultiResource {

    struct Resource {
      uint64 id;
      string metadataURI;
      uint128[] custom;
    }

    /*
    @dev This emits whenever a resource is set.
    */
    event ResourceSet(uint64 id);

    /*
    @dev This emits whenever a pending resource has been added to a token's pending resources.
    */
    event ResourceAddedToToken(uint256 indexed tokenId, uint64 resourceId);

    /*
    @dev This emits whenever a resource has accepted by the token owner.
    */
    event ResourceAccepted(uint256 indexed tokenId, uint64 resourceId);

    /*
    @dev This emits whenever a pending resource has been dropped from the pending resources array.
    */
    event ResourceRejected(uint256 indexed tokenId, uint64 resourceId);

    /*
    @dev This emits whenever a resource's priority has been set.
    */
    event ResourcePrioritySet(uint256 indexed tokenId);

    /*
    @dev This emits whenever a pending resource also proposes to overwrite an existing resource.
    */
    event ResourceOverwriteProposed(uint256 indexed tokenId, uint64 resourceId, uint64 overwrites);

    /*
    @dev This emits whenever a pending resource overwrites an existing resource.
    */
    event ResourceOverwritten(uint256 indexed tokenId, uint64 overwritten);

    /*
    @dev This emits whenever resource custom data is set.
    */
    event ResourceCustomDataSet(uint64 resourceId, uint128 customResourceId);

    /*
    @dev This emits whenever resource custom data is added to the list of custom on a resource.
    */
    event ResourceCustomDataAdded(
        uint64 resourceId,
        uint128 customResourceId
    );

    /*
    @dev This emits whenever resource custom data is removed from the list of custom on a resource.
    */
    event ResourceCustomDataRemoved(
        uint64 resourceId,
        uint128 customResourceId
    );

    /// @dev This emits when the approved address for resources for an NFT is
    ///  changed or reaffirmed. The zero address indicates there is no approved
    ///  address. When a Transfer event emits, this also indicates that the
    ///  approved address for resources for that NFT (if any) is reset to none.
    event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);

    /// @dev This emits when an operator for resources is enabled or disabled
    ///  for an owner. The operator can manage resources for all NFTs of the
    /// owner.
    event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);

    /*
    @notice Accepts the resource from pending.
    @dev Moves the resource from the pending array to the accepted array. Array
      order is not preserved.
    @param tokenId the token to accept a resource
    @param index the index of the resource to accept
    */
    function acceptResource(uint256 tokenId, uint256 index) external;

    /*
    @notice Rejects a resource, dropping it from the pending array.
    @dev Drops the resource from the pending array. Array order is not preserved.
    @param tokenId the token to reject a resource
    @param index the index of the resource to reject
    */
    function rejectResource(uint256 tokenId, uint256 index) external;

    /*
    @notice Rejects all resources, clearing the pending array.
    @dev Sets the pending array to empty.
    @param tokenId the token to reject all resources from
    */
    function rejectAllResources(uint256 tokenId) external;

    /*
    @notice Sets the priority of the active resources array.
    @dev Priorities have a 1:1 relationship with their corresponding index in
      the active resources array. E.G, a priority array of [1, 3, 2] indicates
      that the the active resource at index 1 of the active resource array
      has a priority of 1, index 2 has a priority of 3, and index 3 has a priority
      of 2. There is no validation on priority value input; out of order indexes
      must be handled by the frontend. The length of the priorities array must
      be equal to the present length of the active resources array.
    @param tokenId the token of the resource priority to set
    @param priorities An array of priorities to set.
    */
    function setPriority(uint256 tokenId, uint16[] memory priorities) external;

    /*
    @notice Returns an array of uint64 identifiers from the active resources
      array for resource lookup.
    @dev Each uint64 resource corresponds to the id of the relevant resource
    on the storage.
    @param tokenId the token of the active resource set to get
    @return an array of uint64 resource ids corresponding to active resources
    */
    function getActiveResources(uint256 tokenId) external view returns(uint64[] memory);

    /*
    @notice Returns an array of uint64 identifiers from the pending resources
      array for resource lookup.
    @dev Each uint64 resource corresponds to the id of the relevant resource
      on the storage.
    @param tokenId the token of the pending resource set to get
    @return an array of uint64 resource ids corresponding to pending resources
    */
    function getPendingResources(uint256 tokenId) external view returns(uint64[] memory);

    /*
    @notice Returns an array of uint16 resource priorities
    @dev No checking is done on resource priority ranges, sorting must be
      handled by the frontend.
    @param tokenId the token of the active resource set to get
    @return an array of uint16 resource priorities corresponding to active resources
    */
    function getActiveResourcePriorities(uint256 tokenId) external view returns(uint16[] memory);

    /*
    @notice Returns the uint64 resource ID that would be overwritten when accepting the
      pending resource with id resId on token
    @param tokenId the token of the pending overwrite
    @param resId the resource ID which may overwrite another
    @return a uint64 corresponding to the resource ID of the resource that will be overwritten
    */
    function getResourceOverwrites(uint256 tokenId, uint64 resId) external view returns(uint64);

    /*
    @notice Returns the resource at the id.
    @dev Exact struct data types are left to the implementer
    @param resourceId the id of the resource to return
    */
    function getResource(uint64 resourceId) external view returns (Resource memory);

    /*
    @notice Returns the custom resource data associated to the resource and custom resource Id.
    @dev Structure to handle custom data is up to the implementer
    @param resourceId the id of the resource to return
    */
    function getCustomResourceData(
        uint64 resourceId,
        uint128 customResourceId
    ) external view returns (bytes memory);

    /*
    @notice Returns the src field of the first active resource on the token,
      otherwise returns a fallback src.
    @param tokenId the token to query for a URI
    @return the string URI of the token
    */
    function tokenURI(uint256 tokenId) external view returns (string memory);

    /// @notice Change or reaffirm the approved address for resources for an NFT
    /// @dev The zero address indicates there is no approved address.
    ///  Throws unless `msg.sender` is the current NFT owner, or an authorized
    ///  operator of the current owner. Emits ApprovalForResources event.
    /// @param to The new approved NFT controller
    /// @param tokenId The NFT to approve
    function approveForResources(address to, uint256 tokenId) external payable;

    /// @notice Enable or disable approval for a third party ("operator") to manage
    ///  resources for all of the `msg.sender`'s assets
    /// @dev Emits the ApprovalForAllForResources event. The contract MUST allow
    ///  multiple operators per owner.
    /// @param operator Address to add to the set of authorized operators
    /// @param approved True if the operator is approved, false to revoke approval
    function setApprovalForAllForResources(address operator, bool approved) external;

    /// @notice Get the approved address for resources for a single NFT
    /// @dev Throws if `tokenId` is not a valid NFT.
    /// @param tokenId The NFT to find the approved address for
    /// @return The approved address for this NFT, or the zero address if there is none
    function getApprovedForResources(uint256 tokenId) external view returns (address);

    /// @notice Query if an address is an authorized operator for resources of
    ///  another address
    /// @param owner The address that owns the NFTs
    /// @param operator The address that acts on behalf of the owner
    /// @return True if `operator` is an approved operator for `owner`, false otherwise
    function isApprovedForAllForResources(address owner, address operator) external view returns (bool);

}

interface ERC721 /* is ERC165 */ {
    /// @dev This emits when ownership of any NFT changes by any mechanism.
    ///  This event emits when NFTs are created (`from` == 0) and destroyed
    ///  (`to` == 0). Exception: during contract creation, any number of NFTs
    ///  may be created and assigned without emitting Transfer. At the time of
    ///  any transfer, the approved address for that NFT (if any) is reset to none.
    event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);

    /// @dev This emits when the approved address for an NFT is changed or
    ///  reaffirmed. The zero address indicates there is no approved address.
    ///  When a Transfer event emits, this also indicates that the approved
    ///  address for that NFT (if any) is reset to none.
    event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);

    /// @dev This emits when an operator is enabled or disabled for an owner.
    ///  The operator can manage all NFTs of the owner.
    event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);

    /// @notice Count all NFTs assigned to an owner
    /// @dev NFTs assigned to the zero address are considered invalid, and this
    ///  function throws for queries about the zero address.
    /// @param _owner An address for whom to query the balance
    /// @return The number of NFTs owned by `_owner`, possibly zero
    function balanceOf(address _owner) external view returns (uint256);

    /// @notice Find the owner of an NFT
    /// @dev NFTs assigned to zero address are considered invalid, and queries
    ///  about them do throw.
    /// @param _tokenId The identifier for an NFT
    /// @return The address of the owner of the NFT
    function ownerOf(uint256 _tokenId) external view returns (address);

    /// @notice Transfers the ownership of an NFT from one address to another address
    /// @dev Throws unless `msg.sender` is the current owner, an authorized
    ///  operator, or the approved address for this NFT. Throws if `_from` is
    ///  not the current owner. Throws if `_to` is the zero address. Throws if
    ///  `_tokenId` is not a valid NFT. When transfer is complete, this function
    ///  checks if `_to` is a smart contract (code size > 0). If so, it calls
    ///  `onERC721Received` on `_to` and throws if the return value is not
    ///  `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`.
    /// @param _from The current owner of the NFT
    /// @param _to The new owner
    /// @param _tokenId The NFT to transfer
    /// @param data Additional data with no specified format, sent in call to `_to`
    function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;

    /// @notice Transfers the ownership of an NFT from one address to another address
    /// @dev This works identically to the other function with an extra data parameter,
    ///  except this function just sets data to "".
    /// @param _from The current owner of the NFT
    /// @param _to The new owner
    /// @param _tokenId The NFT to transfer
    function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;

    /// @notice Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE
    ///  TO CONFIRM THAT `_to` IS CAPABLE OF RECEIVING NFTS OR ELSE
    ///  THEY MAY BE PERMANENTLY LOST
    /// @dev Throws unless `msg.sender` is the current owner, an authorized
    ///  operator, or the approved address for this NFT. Throws if `_from` is
    ///  not the current owner. Throws if `_to` is the zero address. Throws if
    ///  `_tokenId` is not a valid NFT.
    /// @param _from The current owner of the NFT
    /// @param _to The new owner
    /// @param _tokenId The NFT to transfer
    function transferFrom(address _from, address _to, uint256 _tokenId) external payable;

    /// @notice Change or reaffirm the approved address for an NFT
    /// @dev The zero address indicates there is no approved address.
    ///  Throws unless `msg.sender` is the current NFT owner, or an authorized
    ///  operator of the current owner.
    /// @param _approved The new approved NFT controller
    /// @param _tokenId The NFT to approve
    function approve(address _approved, uint256 _tokenId) external payable;

    /// @notice Enable or disable approval for a third party ("operator") to manage
    ///  all of `msg.sender`'s assets
    /// @dev Emits the ApprovalForAll event. The contract MUST allow
    ///  multiple operators per owner.
    /// @param _operator Address to add to the set of authorized operators
    /// @param _approved True if the operator is approved, false to revoke approval
    function setApprovalForAll(address _operator, bool _approved) external;

    /// @notice Get the approved address for a single NFT
    /// @dev Throws if `_tokenId` is not a valid NFT.
    /// @param _tokenId The NFT to find the approved address for
    /// @return The approved address for this NFT, or the zero address if there is none
    function getApproved(uint256 _tokenId) external view returns (address);

    /// @notice Query if an address is an authorized operator for another address
    /// @param _owner The address that owns the NFTs
    /// @param _operator The address that acts on behalf of the owner
    /// @return True if `_operator` is an approved operator for `_owner`, false otherwise
    function isApprovedForAll(address _owner, address _operator) external view returns (bool);
}

interface ERC165 {

    function supportsInterface(bytes4 interfaceID) external view returns (bool);

}

Rationale

Resource fields

The MultiResource token standard supports three fields:

  • id: a uint64 resource identifier
  • src: a string pointing to the media associated with the resource
  • custom: A bytes array that may be used to store generic data

Multi-Resource Storage Schema

Resources are stored on a token as an array of uint64 identifiers.

In order to reduce redundant on-chain string storage, multi resource tokens store resources by reference via a inner storage. A resource entry on the storage is stored via a uint64 mapping to resource data.

A resource array is an array of these uint64 references.

With this structure, a generic resource can be added once on the storage, and a reference to it can be added once on the token contract. Implementers can then use string concatenation to procedurally generate a link to a content-addressed archive based on the base SRC in the resource and the token ID. Storing the resource on a new token will only take 16 bytes of storage in the resource array per token for repeated / tokenID dependent resources.

This structure ensures that for tokens whose source differs only via their tokenId, URIs may still be derived programmatically through concatenation.

Propose-Commit pattern for resource addition

Adding resources to an existing token takes the form of a propose-commit pattern to allow for limited mutability by a 3rd party. When adding a resource to a token, it is first placed in the "Pending" array, and must be migrated to the "Active" array by the token owner. The "Pending" resources array is limited to 128 slots to prevent spam and griefing.

Resource management

Several functions for resource management are included. In addition to permissioned migration from "Pending" to "Active", the owner of a token may also drop resources from both the active and the pending array -- an emergency function to clear all entries from the pending array is also included.

Backward Compatibility

The Multi Resource token standard has been made compatible with ERC721 implementations in order to take advantage of the robust tooling available for ERC721 implementations and to ensure compatibility with existing ERC721 infrastructure.

Reference implementation

A reference implementation by Neon Crisis developer: CicadaNCR and Snake Soldiers developer: Steven Pineda is available in the suite here .sol There is also a full implementation with onlyIssuer protection for admin settings here

Implementation extras

Some useful functionalities were added on the implementation but were not considered crucial to be part of the interface.

    /*
    @notice Returns the src field of the resource at given index on the token,
      otherwise returns a fallback src.
    @param tokenId the token to query for a URI
    @param index of the resource to use
    @return the string URI of the token
    */
    function tokenURIAtIndex(
        uint256 tokenId,
        uint256 index
    ) public view virtual returns (string memory);

    /*
    @notice Returns the src field of the resource which has custom resource
      with id customResourceId with the associated value customResourceValue.
      For instance, you may have a custom resource to indicate the file type,
      and you want to get the resource which has 'pdf' as value it.
    @param tokenId the token to query for a URI
    @param customResourceId
    @param customResourceValue
    @return the string URI of the token
    */
    function tokenURIForCustomValue(
        uint256 tokenId,
        uint128 customResourceId,
        bytes memory customResourceValue
    ) public view virtual returns (string memory);

Besides those, there are external functions to get pending and accepted Resources with for different scenarios, and to identify if a resource is enumerated. Resources set as enumerated will append the token id to the end of src, for the resource associated with a token when getting the tokenURI

There are also internal implementations of to add resource entries, manage custom resource data, add resource to token, set fallback URI and set enumerated resources. We expect these functions to be only callable by an issuer or admin. This is included with an onlyIssuer guard on the mock here.

Security Considerations

The same security considerations as with ERC721 apply: hidden logic may be present in any of the functions, including burn, add resource, accept resource, and more.

Caution is advised when dealing with non-audited contracts.

Develop

These contracts are tested in Hardhat. Install Hardhat and run npx hardhat test to run the test script on the mock MultiResource implementation.

About

Multi-resource EIP fork

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published