--- eip: <to be assigned> title: Multi Resource NFT description: Drop-in replacement for ERC721 adding multi-resource logic author: Bruno Skvorc <bruno@rmrk.app> discussions-to: TODO status: Draft type: Standards category (*only required for Standards Track): ERC created: <date created on, in ISO 8601 (yyyy-mm-dd) format> requires: 721 --- # EIP-XXX: Multi-resource drop-in replacement for ERC721 NFTs ## Abstract The Multi Resource NFT standard is a standalone part of [RMRK concepts](https://docs.rmrk.app/concepts) and a drop-in replacement for 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: 1. cross-metaverse compatibility 2. multi-media output 3. media redundancy 4. 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](#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](https://docs.rmrk.app/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. Every ERC-XXX implementation MUST, in addition to ERC721 and ERC165 as specified in [EIP721](https://eips.ethereum.org/EIPS/eip-721) (added below for reference, without docblocks) implement the following interfaces: ```solidity= /// @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 IERCMultiResource { struct Resource { bytes8 id; string src; string thumb; string metadataURI; bytes custom; } event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId); event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId); //see isApprovedForAll event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved); function balanceOf(address _owner) external view returns (uint256); function ownerOf(uint256 _tokenId) external view returns (address); function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes memory data) external; function safeTransferFrom(address _from, address _to, uint256 _tokenId) external; function transferFrom(address _from, address _to, uint256 _tokenId) external; function approve(address _approved, uint256 _tokenId) external; function setApprovalForAll(address _operator, bool _approved) external; function getApproved(uint256 _tokenId) external view returns (address); //May not be included in the standard -- needs review function isApprovedForAll(address _owner, address _operator) external view returns (bool); //RMRK STUFF BELOW HERE /* @notice Adds a resource to the pendingResources array of a token. @dev In order to be considered valid for rendering, the resource must be approved by the user via the acceptResource function. Pending resources are capped at a length of 128 for grief prevention. @param _tokenId the Id of the token to add a resource to. @param _resourceAddress the address of the contract storing the resource @param _resouceId the bytes8 identifier of the resource as it exists on the target contract @param _overwrites optional parameter to signal that, on acceptance, this resource will overwrite another resource already present in the acceptedResources array */ function addResourceToToken(uint256 _tokenId, address _resourceAddress, bytes8 _resourceId, bytes16 _overwrites) external; /* @notice Accepts the resouce 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 _resourceIndex the index of the resource to accept */ function acceptResource(uint256 _tokenId, uint256 resourceIndex) external; /* @notice Reject 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 _resourceIndex the index of the resource to reject */ function rejectResource(uint256 _tokenId, uint256 resourceIndex) external; /* @notice Reject all resources, clearing the pending array. @dev Sets the pending array to empty. @param _tokenId the token to reject a resource */ function rejectAllResources(uint256 _tokenId) external; /* @notice Set 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 byte16 identifiers from the active resources array for resource lookup. @dev Each bytes16 resource corresponds to a local mapping of (bytes16 => (address, bytes8)), where address is the address of a resource storage contract, and bytes8 is the id of the relevant resource on that storage contract. See addResourceEntry dev comment for rationale. @param _tokenId the token of the active resource set to get @return an array of bytes16 local resource ids corresponding to active resources */ function getActiveResources(uint256 _tokenId) external view returns(bytes16[] memory); /* @notice Returns an array of byte16 identifiers from the pending resources array for resource lookup. @dev Each bytes16 resource corresponds to a local mapping of (bytes16 => (address, bytes8)), where address is the address of a resource storage contract, and bytes8 is the id of the relevant resource on that storage contract. See addResourceEntry dev comment for rationale. @param _tokenId the token of the active resource set to get @return an array of bytes16 local resource ids corresponding to pending resources */ function getPendingResources(uint256 _tokenId) external view returns(bytes16[] 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 bytes16 resource ID a given token will overwrite if overwrite is enabled for a pending resource. @param _tokenId the token of the active pending overwrite @param _resId the resource ID which will be potentially overwritten @return a bytes16 corresponding to the local resource ID of the resource that will overwrite @param _resId */ function getResourceOverwrites(uint256 _tokenId, bytes16 _resId) external view returns(bytes16); /* @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 Returns every resource object for a given tokenId. @param _tokenId the token to query for a resource array @return an array of Resource objects */ function getFullResources(uint256 _tokenId) external view returns (IResourceStorage.Resource[] memory); } interface ERC165 { function supportsInterface(bytes4 interfaceID) external view returns (bool); } interface IResourceStorage { struct Resource { bytes8 id; string src; string thumb; string metadataURI; bytes custom; } /* @notice Adds a resource entry to the resource storage contract. @dev Resources are stored on a separate contract and referred to by reference for two reasons: generic reference storage on the multiresource token contract for variable resource struct types, and to reduce redundant storage on the multiresource token contract. With this structure, a generic resource can be added once on the storage contract, and a reference to it can be added to it 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. @param _id the id of the resource @param _src a link to the source of the resource @param _thumb a link to a low-resolution thumbnail of the resource @param _metadataURI a link the the metadata of the resource @param _custom additional data to be stored */ function addResourceEntry(bytes8 _id, string memory _src, string memory _thumb, string memory _metadataURI, bytes memory _custom) external; /* @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(bytes8 _resourceId) external view returns (Resource memory); } ``` ## Rationale TODO The rationale fleshes out the specification by describing what motivated the design and why particular design decisions were made. It should describe alternate designs that were considered and related work, e.g. how the feature is supported in other languages. ## Backwards Compatibility The EIP is fully compatible with ERC721 and is a drop-in replacement. Changing the priority of a resource _might_ require refreshing the metadata on some popular marketplaces, but other than that there are no other caveats. ## Reference Implementation A reference implementation by [Neon Crisis](https://neoncrisis.io/) developer [CicadaNCR](https://github.com/CicadaNCR) is available in the RMRK EIP branch of the RMRK EVM contract suite: https://github.com/rmrk-team/evm/blob/eip/contracts/MultiResource_EIP/ERCMultiResourceToken.so ## 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, send, and more. Caution is advised when dealing with non-audited contracts. Because each NFT made this way is basically a 2 of 2 multi-sig where both owner and issuer must agree on changes (adding, removing, replacing a resource), it is not possible for an issuer to rug-pull art, not for an owner to add arbitrary new resources to an NFT. One key consideration to keep in mind is the size of the pending resources array. There is a griefing vector in that the pending resources array can, in theory, be populated by more than the capacity of the array of resources. Should this happen, iterating through the array will become more expensive, and new resources might not be addable until the array is cleared. We believe this is not an option due to only the issuer having this ability, and it would be akin to sabotage. ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md).