From ee0025af0dabe3e687fb0f2cb1ac8c1c59750fc7 Mon Sep 17 00:00:00 2001 From: Jan Turk Date: Mon, 10 Oct 2022 23:21:32 +0200 Subject: [PATCH 01/21] Propose Multi-Resource Token standard RMRK team has developed a next step in NFTs where one NFT can be tied to multiple resources. --- ...ti_resource_non_fungible_token_standard.md | 366 ++++++++++++++++++ 1 file changed, 366 insertions(+) create mode 100644 EIPS/eip-multi_resource_non_fungible_token_standard.md diff --git a/EIPS/eip-multi_resource_non_fungible_token_standard.md b/EIPS/eip-multi_resource_non_fungible_token_standard.md new file mode 100644 index 00000000000000..3250d26433f839 --- /dev/null +++ b/EIPS/eip-multi_resource_non_fungible_token_standard.md @@ -0,0 +1,366 @@ +--- +eip: +title: Multi-Resource Token standard +description: A standard interface for Multi-Resource tokens. +author: Bruno Škvorc (@Swader), Cicada (@CicadaNCR), Steven Pineda (@steven2308), Stevan Bogosavljevic (@stevyhacker), Jan Turk (@ThunderDeliverer) +discussions-to: +status: Draft +type: Standards Track +category: ERC +created: 2022-10-10 +requires: [165](./eip-165.md), [721](./eip-721.md) +--- + +## Abstract + +The Multi-Resource NFT standard 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 the owner of the token and the issuer of the token. + +## Motivation + +In the four years since the original [EIP-721](./eip-721.md) was published, the need for additional utility resulted in countless implementations on how to provide it. The Multi-Resource Non-Fungible Token standard improves upon it in the following areas: + +- [Cross-metaverse compatibility](#cross-metaverse-compatibility) +- [Multi-media output](#multi-media-output) +- [Media redundancy](#media-redundancy) +- [NFT evolution](#nft-evolution) + +### Cross-metaverse compatibility + +Cross-metaverse compatibility could also be referred to as cross-engine compatibility and (for example) solves the issue where cosmetic item for game A is not portable into game B because the engines are different - it is not a simple matter of just having said cosmetic item, or an NFT. + +With Multi-Resource NFTs, it is. + +One resource is a cosmetic item for game A, an actual cosmetic item file. Another is a cosmetic item file for game B. 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 cosmetic item that renders outside of any of the two games. + +When using the NFT in such a game, not only don't the game developers need to pre-build the asset into the game and then allow it based on NFT balance in the logged in web3 account, but the NFT has everything it needs in its cosmetic item file, making storage and ownership of this cosmetic item decentralized and not reliant on the game development team. + +After the fact, this NFT can be given further utility by means of new additional resources: more games, more cosmetic items, appended to the same NFT. Thus, a game cosmetic item 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 an eBook reader, audio if loaded into an audio book application. 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 combination on Sia, another of the same combination 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 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 replaces username/password logins with reading an account's NFT 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](#cross-metaverse-compatibility) 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 the 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. + +## 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. + +````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 IMultiResource { + + struct Resource { + uint64 id; + string metadataURI; + } + + /// @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 been 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 token's 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); + + /// @notice Accepts the resource from pending resources. + /// @dev Moves the resource from the pending array to the accepted array. Array + /// order is not preserved. + /// @param tokenId The ID of the token to accept a resource + /// @param index The index of the resource in the pending resources array 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 ID of the token to reject a resource + /// @param index The index of the resource in the pending resources array 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 ID of 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. + /// @dev The length of the priorities array MUST + /// be equal to the present length of the active resources array. + /// @param tokenId the ID of 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 + /// in the storage. + /// @param tokenId The ID of the token of which to retrieve the active resource set + /// @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 + /// in the storage. + /// @param tokenId The ID of the token of which to retrieve the pending resource set + /// @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 validation is done on resource priority ranges, sorting MUST be + /// handled by the frontend. + /// @param tokenId The ID of the token of which to retrieve the active resource set + /// priorities + /// @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 ID of the token of which we want to overwrite the pending resource + /// @param resId The resource ID which MAY overwrite another + /// @return A uint64 corresponding to the resource ID of the resource that would be overwritten + function getResourceOverwrites(uint256 tokenId, uint64 resId) external view returns(uint64); + + /// @notice Returns raw bytes of `customResourceId` of `resourceId` + /// @dev Raw bytes are stored by reference in a double mapping structure of + /// `resourceId` => `customResourceId`. + /// @dev Custom data is intended to be stored as generic bytes and decode by + /// various protocols on an as-needed basis. + /// @param resourceId The ID of the resource for which we are trying to retrieve the + /// resource meta + /// @return The raw bytes of `customResourceId` + function getResourceMeta(uint64 resourceId) external view returns (string memory); + + /// @notice Fetches resource data for the token's active resource with the given index. + /// @dev Resources are stored by reference mapping _resources[resourceId]. + /// @dev MAY be overridden to implement enumerate, fallback or other custom logic. + /// @param tokenId The ID of the token for which we are getting the resource data for + /// @param resourceIndex The index of the active resource in the token + /// @return The metadata URI for the the resource we are fetching + function getResourceMetaForToken(uint256 tokenId, uint64 resourceIndex) external view returns (string memory); + + /// @notice Returns the IDs of all of the stored resources. + function getAllResources() external view returns (uint64[] memory); + + /// @notice Change or reaffirm the approved address for resources for an . + /// @dev The zero address indicates there is no approved address. + /// @dev MUST revert unless `msg.sender` is the current NFT owner, or an authorized + /// operator of the current owner. + /// @dev MUST emit ApprovalForResources event. + /// @param to The new approved token controller + /// @param tokenId The ID of the token 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 MUST emit the ApprovalForAllForResources event. + /// @dev 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 token. + /// @dev MUST revert if `tokenId` is not a valid token ID. + /// @param tokenId The ID of the token to find the approved address for + /// @return The approved address for this token, 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 tokens + /// @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 ERC165 { + + function supportsInterface(bytes4 interfaceID) external view returns (bool); + +} +```` + +## Rationale + +With NFTs being a widespread form of tokens in the Ethereum ecosystem and being used for a variety of use cases, it is time to standardize additional utility for them. Having multiple resources associated with a single NFT allows for greater utility and usability. + +### Resource fields + +The MultiResource token standard supports two resource fields: + +- `id`: a `uint64` resource identifier +- `metadataURI`: a `string` pointing to the metadata URI associated with the resource + +### Multi-Resource Storage Schema + +Resources are stored within 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 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` resource ID references. + +Such a structure allows that, a generic resource can be added to the storage one time, and a reference to it can be added to the token contract as many times as we desire. 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 in a new token will only take 16 bytes of storage in the resource array per token for recurrent as well as `tokenId` dependent resources. + +Structuring token's resources in such a way allows for URIs to be derived programmatically through concatenation, especially when they differ only by `tokenId`. + +### Propose-Commit pattern for resource addition + +Adding resources to an existing token MUST be done in 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's owner. The *"Pending"* resources array SHOULD be 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 MUST also be included. + +## Backward compatibility + +The MultiResource token standard has been made compatible with [EIP-721](./eip-721.md) in order to take advantage of the robust tooling available for implementations of EIP-721 and to ensure compatibility with existing EIP-721 infrastructure. + +## Test cases + +The RMRK MultiResource lego block implementation includes test cases written using Hardhat. + +## Reference implementations + +[RMRK MultiResource lego block](https://github.com/rmrk-team/MultiResourceEIP) and [documentation](https://docs.rmrk.app/lego2-multi-resource) + +- Compatible with the original version of the standard + +Neon Crisis, by [CicadaNCR](https://github.com/CicadaNCR) + +- A NFT game utilizing RMRK MultiResource lego block + +Snake Soldiers, by [Steven Pineda](https://github.com/steven2308) + +- A NFT game utilizing RMRK MultiResource lego block + +### Implementation extras + +We designed additional **internal** implementations of methods to be used to add resource entries and add resources to tokens, but these were not considered crucial for the proposal to be functional. We expect these functions to only be callable by an issuer or an administrator. This is achieved with an `onlyIssuer` modifier of the following example: + +````solidity +pragma solidity ^0.8.15; + +contract Issued { + address private _issuer; + + constructor(){ + _setIssuer(_msgSender()); + } + + modifier onlyIssuer() { + require(_msgSender() == _issuer, "RMRK: Only issuer"); + _; + } + + function setIssuer(address issuer) external onlyIssuer { + _setIssuer(issuer); + } + + function getIssuer() external view returns (address) { + return _issuer; + } + + function _setIssuer(address issuer) private { + _issuer = issuer; + } +} +```` + +A `RenderUtils` utility smart contract was developed to aid in getting the metadata URIs for different use cases. Such utility smart contract can be deployed once per chain and provide services to all MultiResource NFT compatible smart contracts: + +````solidity +interface IRenderUtils { + /// @notice Returns resource metadata at `index` of active resource array on `tokenId`. + /// @param target The address of the smart contract that we want to get the resource metadata + /// @param tokenId The token ID of the token to which belongs the resource + /// @param index Index of the resource. It must be inside the range of active resource array + /// @return A stringified metadata URI of the resource + function getResourceByIndex( + address target, + uint256 tokenId, + uint256 index + ) external view returns (string memory); + + /// @notice Returns resource metadata at `index` of pending resource array on `tokenId`. + /// @param target The address of the smart contract that we want to get the resource metadata + /// @param tokenId The token ID of the token to which belongs the pending resource array + /// @param index Index of the resource in the pending resources array. It must be inside the range of pending + /// resource array + /// @return A stringified metadata URI of the pending resource + function getPendingResourceByIndex( + address target, + uint256 tokenId, + uint256 index + ) external view returns (string memory); + + /// @notice Returns resource metadata for the given IDs. + /// @param target The address of the smart contract that we want to get the resource metadata + /// @param resourceIds Resource IDs for which to retrieve the metadata strings + /// @return An array of metadata URIs for the requested resources + function getResourcesById(address target, uint64[] calldata resourceIds) + external + view + returns (string[] memory); + + /// @notice Returns the resource metadata with the highest priority for the given token. + /// @param target The address of the smart contract that we want to get the resource metadata + /// @param tokenId Token ID of which we are retrieving the metadata + /// @return Metadata URI with the highest priority + function getTopResourceMetaForToken(address target, uint256 tokenId) + external + view + returns (string memory); +} +```` + +Example implementation of such utility can be found in the [RMRK's MultiResource lego block implementation](https://github.com/rmrk-team/MultiResourceEIP/blob/master/contracts/MultiResource_EIP/utils/RenderUtils.sol). + +## Security considerations + +The same security considerations as with [EIP-721](./eip-721.md) 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. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). \ No newline at end of file From 758ecdcbbb745f75940eea53472101809e709e7c Mon Sep 17 00:00:00 2001 From: Jan Turk Date: Mon, 10 Oct 2022 23:35:55 +0200 Subject: [PATCH 02/21] Address issues reported by EIP repository's CI --- EIPS/eip-multi_resource_non_fungible_token_standard.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/EIPS/eip-multi_resource_non_fungible_token_standard.md b/EIPS/eip-multi_resource_non_fungible_token_standard.md index 3250d26433f839..d092ceed4c2187 100644 --- a/EIPS/eip-multi_resource_non_fungible_token_standard.md +++ b/EIPS/eip-multi_resource_non_fungible_token_standard.md @@ -251,7 +251,7 @@ Adding resources to an existing token MUST be done in the form of a propose-comm 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 MUST also be included. -## Backward compatibility +## Backwards compatibility The MultiResource token standard has been made compatible with [EIP-721](./eip-721.md) in order to take advantage of the robust tooling available for implementations of EIP-721 and to ensure compatibility with existing EIP-721 infrastructure. @@ -261,15 +261,15 @@ The RMRK MultiResource lego block implementation includes test cases written usi ## Reference implementations -[RMRK MultiResource lego block](https://github.com/rmrk-team/MultiResourceEIP) and [documentation](https://docs.rmrk.app/lego2-multi-resource) +RMRK MultiResource lego block and documentation - Compatible with the original version of the standard -Neon Crisis, by [CicadaNCR](https://github.com/CicadaNCR) +Neon Crisis, by CicadaNCR - A NFT game utilizing RMRK MultiResource lego block -Snake Soldiers, by [Steven Pineda](https://github.com/steven2308) +Snake Soldiers, by Steven Pineda - A NFT game utilizing RMRK MultiResource lego block @@ -353,7 +353,7 @@ interface IRenderUtils { } ```` -Example implementation of such utility can be found in the [RMRK's MultiResource lego block implementation](https://github.com/rmrk-team/MultiResourceEIP/blob/master/contracts/MultiResource_EIP/utils/RenderUtils.sol). +Example implementation of such utility can be found in the RMRK's MultiResource lego block implementation. ## Security considerations From e3a880b5678bef66302cbe6875493999be9f242a Mon Sep 17 00:00:00 2001 From: Jan Turk Date: Mon, 10 Oct 2022 23:40:47 +0200 Subject: [PATCH 03/21] Fix styling discrepancies in section titles --- EIPS/eip-multi_resource_non_fungible_token_standard.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/EIPS/eip-multi_resource_non_fungible_token_standard.md b/EIPS/eip-multi_resource_non_fungible_token_standard.md index d092ceed4c2187..5a0753b773efc4 100644 --- a/EIPS/eip-multi_resource_non_fungible_token_standard.md +++ b/EIPS/eip-multi_resource_non_fungible_token_standard.md @@ -251,15 +251,15 @@ Adding resources to an existing token MUST be done in the form of a propose-comm 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 MUST also be included. -## Backwards compatibility +## Backwards Compatibility The MultiResource token standard has been made compatible with [EIP-721](./eip-721.md) in order to take advantage of the robust tooling available for implementations of EIP-721 and to ensure compatibility with existing EIP-721 infrastructure. -## Test cases +## Test Cases The RMRK MultiResource lego block implementation includes test cases written using Hardhat. -## Reference implementations +## Reference Implementation RMRK MultiResource lego block and documentation @@ -355,7 +355,7 @@ interface IRenderUtils { Example implementation of such utility can be found in the RMRK's MultiResource lego block implementation. -## Security considerations +## Security Considerations The same security considerations as with [EIP-721](./eip-721.md) apply: hidden logic may be present in any of the functions, including burn, add resource, accept resource, and more. From 365eb1cd675b9023e06d4d5d40d395692c028a99 Mon Sep 17 00:00:00 2001 From: Jan Turk Date: Mon, 10 Oct 2022 23:45:32 +0200 Subject: [PATCH 04/21] Address CI's issues in the preabmle --- EIPS/eip-multi_resource_non_fungible_token_standard.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/EIPS/eip-multi_resource_non_fungible_token_standard.md b/EIPS/eip-multi_resource_non_fungible_token_standard.md index 5a0753b773efc4..071b15eb4f8093 100644 --- a/EIPS/eip-multi_resource_non_fungible_token_standard.md +++ b/EIPS/eip-multi_resource_non_fungible_token_standard.md @@ -1,14 +1,14 @@ --- eip: -title: Multi-Resource Token standard -description: A standard interface for Multi-Resource tokens. +title: Multi-Resource Token +description: An interface for Multi-Resource tokens. author: Bruno Škvorc (@Swader), Cicada (@CicadaNCR), Steven Pineda (@steven2308), Stevan Bogosavljevic (@stevyhacker), Jan Turk (@ThunderDeliverer) discussions-to: status: Draft type: Standards Track category: ERC created: 2022-10-10 -requires: [165](./eip-165.md), [721](./eip-721.md) +requires: 165, 721 --- ## Abstract From 69b2ffde488d2e149bf673af4fae0a1bb66815c7 Mon Sep 17 00:00:00 2001 From: Jan Turk Date: Sat, 15 Oct 2022 21:46:23 +0200 Subject: [PATCH 05/21] Add discussion URL --- EIPS/eip-multi_resource_non_fungible_token_standard.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-multi_resource_non_fungible_token_standard.md b/EIPS/eip-multi_resource_non_fungible_token_standard.md index 071b15eb4f8093..819ca999104042 100644 --- a/EIPS/eip-multi_resource_non_fungible_token_standard.md +++ b/EIPS/eip-multi_resource_non_fungible_token_standard.md @@ -3,7 +3,7 @@ eip: title: Multi-Resource Token description: An interface for Multi-Resource tokens. author: Bruno Škvorc (@Swader), Cicada (@CicadaNCR), Steven Pineda (@steven2308), Stevan Bogosavljevic (@stevyhacker), Jan Turk (@ThunderDeliverer) -discussions-to: +discussions-to: [https://ethereum-magicians.org/t/multiresource-tokens/11326](https://ethereum-magicians.org/t/multiresource-tokens/11326) status: Draft type: Standards Track category: ERC From 08c0038a11a2f752985289c1e3b0e710e770234a Mon Sep 17 00:00:00 2001 From: Jan Turk Date: Sat, 15 Oct 2022 21:48:02 +0200 Subject: [PATCH 06/21] Fix discussion URL formatting --- EIPS/eip-multi_resource_non_fungible_token_standard.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-multi_resource_non_fungible_token_standard.md b/EIPS/eip-multi_resource_non_fungible_token_standard.md index 819ca999104042..c80e11a22ac25a 100644 --- a/EIPS/eip-multi_resource_non_fungible_token_standard.md +++ b/EIPS/eip-multi_resource_non_fungible_token_standard.md @@ -3,7 +3,7 @@ eip: title: Multi-Resource Token description: An interface for Multi-Resource tokens. author: Bruno Škvorc (@Swader), Cicada (@CicadaNCR), Steven Pineda (@steven2308), Stevan Bogosavljevic (@stevyhacker), Jan Turk (@ThunderDeliverer) -discussions-to: [https://ethereum-magicians.org/t/multiresource-tokens/11326](https://ethereum-magicians.org/t/multiresource-tokens/11326) +discussions-to: https://ethereum-magicians.org/t/multiresource-tokens/11326 status: Draft type: Standards Track category: ERC From f192cbced8200e24d4578373a684082bbdb28681 Mon Sep 17 00:00:00 2001 From: Jan Turk Date: Tue, 18 Oct 2022 23:05:17 +0200 Subject: [PATCH 07/21] Add the EIP number --- ...ulti_resource_non_fungible_token_standard.md => eip-5773.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename EIPS/{eip-multi_resource_non_fungible_token_standard.md => eip-5773.md} (99%) diff --git a/EIPS/eip-multi_resource_non_fungible_token_standard.md b/EIPS/eip-5773.md similarity index 99% rename from EIPS/eip-multi_resource_non_fungible_token_standard.md rename to EIPS/eip-5773.md index c80e11a22ac25a..fd861a1f7b4141 100644 --- a/EIPS/eip-multi_resource_non_fungible_token_standard.md +++ b/EIPS/eip-5773.md @@ -1,5 +1,5 @@ --- -eip: +eip: 5773 title: Multi-Resource Token description: An interface for Multi-Resource tokens. author: Bruno Škvorc (@Swader), Cicada (@CicadaNCR), Steven Pineda (@steven2308), Stevan Bogosavljevic (@stevyhacker), Jan Turk (@ThunderDeliverer) From d8622f510674eba74fd3a8846dfabfea2b0691d0 Mon Sep 17 00:00:00 2001 From: Jan Turk Date: Fri, 21 Oct 2022 15:06:21 +0200 Subject: [PATCH 08/21] Fix getResourceMeta specification --- EIPS/eip-5773.md | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/EIPS/eip-5773.md b/EIPS/eip-5773.md index fd861a1f7b4141..d126ccf518b615 100644 --- a/EIPS/eip-5773.md +++ b/EIPS/eip-5773.md @@ -160,14 +160,10 @@ interface IMultiResource { /// @return A uint64 corresponding to the resource ID of the resource that would be overwritten function getResourceOverwrites(uint256 tokenId, uint64 resId) external view returns(uint64); - /// @notice Returns raw bytes of `customResourceId` of `resourceId` - /// @dev Raw bytes are stored by reference in a double mapping structure of - /// `resourceId` => `customResourceId`. - /// @dev Custom data is intended to be stored as generic bytes and decode by - /// various protocols on an as-needed basis. + /// @notice Returns the metadata of the resource associated with `resourceId` /// @param resourceId The ID of the resource for which we are trying to retrieve the - /// resource meta - /// @return The raw bytes of `customResourceId` + /// resource metadata + /// @return The metadata of the resource with ID equal to `resourceId` function getResourceMeta(uint64 resourceId) external view returns (string memory); /// @notice Fetches resource data for the token's active resource with the given index. From 897561a762c871f8eb8d831d08f95220ad5d6b15 Mon Sep 17 00:00:00 2001 From: Jan Turk Date: Wed, 26 Oct 2022 11:23:42 +0200 Subject: [PATCH 09/21] Apply changes based on PR comments --- EIPS/eip-5773.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/EIPS/eip-5773.md b/EIPS/eip-5773.md index d126ccf518b615..5a86f110746bbc 100644 --- a/EIPS/eip-5773.md +++ b/EIPS/eip-5773.md @@ -19,7 +19,7 @@ An NFT can have multiple resources (outputs), and orders them by priority. They ## Motivation -In the four years since the original [EIP-721](./eip-721.md) was published, the need for additional utility resulted in countless implementations on how to provide it. The Multi-Resource Non-Fungible Token standard improves upon it in the following areas: +In the four years since [EIP-721](./eip-721.md) was published, the need for additional functionality has resulted in countless extensions. This EIP improves upon EIP-721 in the following areas: - [Cross-metaverse compatibility](#cross-metaverse-compatibility) - [Multi-media output](#multi-media-output) @@ -28,19 +28,15 @@ In the four years since the original [EIP-721](./eip-721.md) was published, the ### Cross-metaverse compatibility -Cross-metaverse compatibility could also be referred to as cross-engine compatibility and (for example) solves the issue where cosmetic item for game A is not portable into game B because the engines are different - it is not a simple matter of just having said cosmetic item, or an NFT. +Cross-metaverse compatibility could also be referred to as cross-engine compatibility. An example of this is where a cosmetic item for game A is not available in game B because the frameworks are incompatible. -With Multi-Resource NFTs, it is. +The following is a more concrete example. One resource is a cosmetic item for game A, a file containing the cosmetic assets. Another is a cosmetic asset file for game B. A third is a generic resource intended to be shown in catalogs, marketplaces, portfolio trackers, or other generalized NFT viewers, containing a representation, stylized thumbnail, and animated demo/trailer of the cosmetic item. -One resource is a cosmetic item for game A, an actual cosmetic item file. Another is a cosmetic item file for game B. 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 cosmetic item that renders outside of any of the two games. - -When using the NFT in such a game, not only don't the game developers need to pre-build the asset into the game and then allow it based on NFT balance in the logged in web3 account, but the NFT has everything it needs in its cosmetic item file, making storage and ownership of this cosmetic item decentralized and not reliant on the game development team. - -After the fact, this NFT can be given further utility by means of new additional resources: more games, more cosmetic items, appended to the same NFT. Thus, a game cosmetic item as an NFT becomes an ever-evolving NFT of infinite utility. +This EIP adds a layer of abstraction, allowing game developers to directly pull asset data from a user's NFTs instead of hard-coding it. ### 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 an eBook reader, audio if loaded into an audio book application. 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. +An NFT of an eBook can be represented as a PDF, MP3, or some other format, depending on what software loads it. If loaded into an eBook reader, a PDF should be displayed, and if loaded into an audiobook application, the MP3 representation should be used. Other metadata could be present in the NFT (perhaps the book’s cover image) for identification on various marketplaces, SERP pages, or portfolio trackers. ### Media redundancy @@ -65,9 +61,9 @@ As a concrete example, think of Pokemon™️ evolving - once enough experience 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. ````solidity -/// @title ERC-**** Multi-Resource Token Standard -/// @dev See https://eips.ethereum.org/EIPS/******** -/// Note: the ERC-165 identifier for this interface is 0x********. +/// @title ERC-5773 Multi-Resource Token Standard +/// @dev See https://eips.ethereum.org/EIPS/eip-5773 +/// Note: the ERC-165 identifier for this interface is 0xc65a6425. pragma solidity ^0.8.9; interface IMultiResource { @@ -220,6 +216,10 @@ interface ERC165 { With NFTs being a widespread form of tokens in the Ethereum ecosystem and being used for a variety of use cases, it is time to standardize additional utility for them. Having multiple resources associated with a single NFT allows for greater utility and usability. +The cross-metaverse or rather cross-engine compatibility issue could be solved by this EIP by having resources compatible with multiple engines or metaverses associated with the same NFT. + +Such NFT can be given further utility by means of new additional resources: more games, more cosmetic items, appended to the same NFT. Thus, a game cosmetic item as an NFT becomes an ever-evolving NFT of infinite utility. + ### Resource fields The MultiResource token standard supports two resource fields: From 674cf116f94c5aa4e5eb9622e50440e74c56b936 Mon Sep 17 00:00:00 2001 From: Jan Turk Date: Thu, 27 Oct 2022 16:26:00 +0200 Subject: [PATCH 10/21] Replace "primitive" with a clearer explanation Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> --- EIPS/eip-5773.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5773.md b/EIPS/eip-5773.md index 5a86f110746bbc..e8d039c60abe94 100644 --- a/EIPS/eip-5773.md +++ b/EIPS/eip-5773.md @@ -13,7 +13,7 @@ requires: 165, 721 ## Abstract -The Multi-Resource NFT standard allows for the construction of a new primitive: context-dependent output of multimedia information per single NFT. +The Multi-Resource NFT standard allows for the construction of a new simple, clearly defined structure for 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 the owner of the token and the issuer of the token. From e2bee2483ff33c320955fbeb39d39cb48c7d59ba Mon Sep 17 00:00:00 2001 From: Jan Turk Date: Tue, 15 Nov 2022 14:32:54 +0100 Subject: [PATCH 11/21] Apply changes based on ECH call #5 Applied the changes based on feedback received on the EIP Editing Office Hour Meeting #5. This commit includes changes to the proposal as well as adds an exaple implementation to assets/ directory. --- EIPS/eip-5773.md | 627 +++++++++------ assets/eip-5773/contracts/IMultiResource.sol | 94 +++ .../eip-5773/contracts/MultiResourceToken.sol | 722 ++++++++++++++++++ .../contracts/library/MultiResourceLib.sol | 31 + .../contracts/mocks/ERC721ReceiverMock.sol | 16 + .../mocks/MultiResourceTokenMock.sol | 59 ++ .../contracts/mocks/NonReceiverMock.sol | 7 + .../utils/MultiResourceRenderUtils.sol | 152 ++++ assets/eip-5773/hardhat.config.ts | 47 ++ assets/eip-5773/package.json | 42 + assets/eip-5773/test/multiresource.ts | 677 ++++++++++++++++ assets/eip-5773/test/renderUtils.ts | 86 +++ 12 files changed, 2317 insertions(+), 243 deletions(-) create mode 100644 assets/eip-5773/contracts/IMultiResource.sol create mode 100644 assets/eip-5773/contracts/MultiResourceToken.sol create mode 100644 assets/eip-5773/contracts/library/MultiResourceLib.sol create mode 100644 assets/eip-5773/contracts/mocks/ERC721ReceiverMock.sol create mode 100644 assets/eip-5773/contracts/mocks/MultiResourceTokenMock.sol create mode 100644 assets/eip-5773/contracts/mocks/NonReceiverMock.sol create mode 100644 assets/eip-5773/contracts/utils/MultiResourceRenderUtils.sol create mode 100644 assets/eip-5773/hardhat.config.ts create mode 100644 assets/eip-5773/package.json create mode 100644 assets/eip-5773/test/multiresource.ts create mode 100644 assets/eip-5773/test/renderUtils.ts diff --git a/EIPS/eip-5773.md b/EIPS/eip-5773.md index e8d039c60abe94..269ed524e234fb 100644 --- a/EIPS/eip-5773.md +++ b/EIPS/eip-5773.md @@ -1,7 +1,7 @@ --- eip: 5773 -title: Multi-Resource Token -description: An interface for Multi-Resource tokens. +title: Multi-Resource context-dependent tokens +description: An interface for Multi-Resource tokens with context dependent resource type output controlled by owner's preference. author: Bruno Škvorc (@Swader), Cicada (@CicadaNCR), Steven Pineda (@steven2308), Stevan Bogosavljevic (@stevyhacker), Jan Turk (@ThunderDeliverer) discussions-to: https://ethereum-magicians.org/t/multiresource-tokens/11326 status: Draft @@ -15,10 +15,14 @@ requires: 165, 721 The Multi-Resource NFT standard allows for the construction of a new simple, clearly defined structure for 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 the owner of the token and the issuer of the token. +The context-dependent output of multimedia information means that the resource in an appropriate format is displayed based on how the token is being accessed. I.e. if the token is being opened in an e-book reader, the PDF resource is displayed, if the token is opened in the marketplace, the PNG or the SVG resource is displayed and if the token is accessed from within a game, the 3D model resource is accessed. + +An NFT can have multiple resources (outputs), which can be any kind of file to be served to the consumer, 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 the owner of the token and the issuer of the token. ## Motivation +With NFTs being a widespread form of tokens in the Ethereum ecosystem and being used for a variety of use cases, it is time to standardize additional utility for them. Having multiple resources associated with a single NFT allows for greater utility, usability and forward compatibility. + In the four years since [EIP-721](./eip-721.md) was published, the need for additional functionality has resulted in countless extensions. This EIP improves upon EIP-721 in the following areas: - [Cross-metaverse compatibility](#cross-metaverse-compatibility) @@ -28,15 +32,19 @@ In the four years since [EIP-721](./eip-721.md) was published, the need for addi ### Cross-metaverse compatibility +At the time of writing this proposal, the metaverse is still a fledgling, not full defined, term. No matter how the definition of metaverse evolves, the proposal can support any number of different implementations. + Cross-metaverse compatibility could also be referred to as cross-engine compatibility. An example of this is where a cosmetic item for game A is not available in game B because the frameworks are incompatible. +Such NFT can be given further utility by means of new additional resources: more games, more cosmetic items, appended to the same NFT. Thus, a game cosmetic item as an NFT becomes an ever-evolving NFT of infinite utility. + The following is a more concrete example. One resource is a cosmetic item for game A, a file containing the cosmetic assets. Another is a cosmetic asset file for game B. A third is a generic resource intended to be shown in catalogs, marketplaces, portfolio trackers, or other generalized NFT viewers, containing a representation, stylized thumbnail, and animated demo/trailer of the cosmetic item. This EIP adds a layer of abstraction, allowing game developers to directly pull asset data from a user's NFTs instead of hard-coding it. ### Multi-media output -An NFT of an eBook can be represented as a PDF, MP3, or some other format, depending on what software loads it. If loaded into an eBook reader, a PDF should be displayed, and if loaded into an audiobook application, the MP3 representation should be used. Other metadata could be present in the NFT (perhaps the book’s cover image) for identification on various marketplaces, SERP pages, or portfolio trackers. +An NFT of an eBook can be represented as a PDF, MP3, or some other format, depending on what software loads it. If loaded into an eBook reader, a PDF should be displayed, and if loaded into an audiobook application, the MP3 representation should be used. Other metadata could be present in the NFT (perhaps the book's cover image) for identification on various marketplaces, Search Engine Result Pages (SERPs), or portfolio trackers. ### Media redundancy @@ -46,186 +54,403 @@ By adding the same metadata file as different resources, e.g., one resource of a ### 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 replaces username/password logins with reading an account's NFT balance. +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 replaces username/password logins with reading an account's NFT 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](#cross-metaverse-compatibility) 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. +Replacing a resource could, to some extent, be similar to replacing an EIP-721 token's URI. When a resource is replaced a clear line of traceability remains; the old resource is still reachable and verifiable. Overwriting a resource's metadata URI obscures this lineage. It also gives more trust to the token owner if the issuer cannot replace the resource of the NFT at will. The propose-accept resource replacement mechanic of this proposal provides this assurance. + 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 the 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. +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 the 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. ## 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. ````solidity -/// @title ERC-5773 Multi-Resource Token Standard +/// @title ERC-5773 Multi-Resource context-dependent tokens /// @dev See https://eips.ethereum.org/EIPS/eip-5773 -/// Note: the ERC-165 identifier for this interface is 0xc65a6425. -pragma solidity ^0.8.9; +/// Note: the ERC-165 identifier for this interface is 0xb0ecc5ae. + +pragma solidity ^0.8.16; interface IMultiResource { + /** + * @notice Used to notify listeners that a resource object is initialized at `resourceId`. + * @param resourceId ID of the resource that was initialized + */ + event ResourceSet(uint64 resourceId); + + /** + * @notice Used to notify listeners that a resource object at `resourceId` is added to token's pending resource + * array. + * @param tokenId ID of the token that received a new pending resource + * @param resourceId ID of the resource that has been added to the token's pending resources array + * @param overwritesId ID of the resource that would be overwritten + */ + event ResourceAddedToToken( + uint256 indexed tokenId, + uint64 indexed resourceId, + uint64 indexed overwritesId + ); + + /** + * @notice Used to notify listeners that a resource object at `resourceId` is accepted by the token and migrated + * from token's pending resources array to active resources array of the token. + * @param tokenId ID of the token that had a new resource accepted + * @param resourceId ID of the resource that was accepted + * @param overwritesId ID of the resource that would be overwritten + */ + event ResourceAccepted( + uint256 indexed tokenId, + uint64 indexed resourceId, + uint64 indexed overwritesId + ); + + /** + * @notice Used to notify listeners that a resource object at `resourceId` is rejected from token and is dropped + * from the pending resources array of the token. + * @param tokenId ID of the token that had a resource rejected + * @param resourceId ID of the resource that was rejected + */ + event ResourceRejected(uint256 indexed tokenId, uint64 indexed resourceId); + + /** + * @notice Used to notify listeners that token's prioritiy array is reordered. + * @param tokenId ID of the token that had the resource priority array updated + */ + event ResourcePrioritySet(uint256 indexed tokenId); - struct Resource { - uint64 id; - string metadataURI; - } + /** + * @notice Used to notify listeners that owner has granted an approval to the user to manage the resources of a + * given token. + * @dev Approvals must be cleared on transfer + * @param owner Address of the account that has granted the approval for all token's resources + * @param approved Address of the account that has been granted approval to manage the token's resources + * @param tokenId ID of the token on which the approval was granted + */ + event ApprovalForResources( + address indexed owner, + address indexed approved, + uint256 indexed tokenId + ); + + /** + * @notice Used to notify listeners that owner has granted approval to the user to manage resources of all of their + * tokens. + * @param owner Address of the account that has granted the approval for all resources on all of their tokens + * @param operator Address of the account that has been granted the approval to manage the token's resources on all of the + * tokens + * @param approved Boolean value signifying whether the permission has been granted (`true`) or revoked (`false`) + */ + event ApprovalForAllForResources( + address indexed owner, + address indexed operator, + bool approved + ); + + /** + * @notice Accepts a resource at from the pending array of given token. + * @dev Migrates the resource from the token's pending resource array to the token's active resource array. + * @dev Active resources cannot be removed by anyone, but can be replaced by a new resource. + * @dev Requirements: + * + * - The caller must own the token or be approved to manage the token's resources + * - `tokenId` must exist. + * - `index` must be in range of the length of the pending resource array. + * @dev Emits an {ResourceAccepted} event. + * @param tokenId ID of the token for which to accept the pending resource + * @param index Index of the resource in the pending array to accept + * @param resourceId expected to be in the index + */ + function acceptResource( + uint256 tokenId, + uint256 index, + uint64 resourceId + ) external; + + /** + * @notice Rejects a resource from the pending array of given token. + * @dev Removes the resource from the token's pending resource array. + * @dev Requirements: + * + * - The caller must own the token or be approved to manage the token's resources + * - `tokenId` must exist. + * - `index` must be in range of the length of the pending resource array. + * @dev Emits a {ResourceRejected} event. + * @param tokenId ID of the token that the resource is being rejected from + * @param index Index of the resource in the pending array to be rejected + * @param resourceId expected to be in the index + */ + function rejectResource( + uint256 tokenId, + uint256 index, + uint64 resourceId + ) external; + + /** + * @notice Rejects all resources from the pending array of a given token. + * @dev Effecitvely deletes the pending array. + * @dev Requirements: + * + * - The caller must own the token or be approved to manage the token's resources + * - `tokenId` must exist. + * @dev Emits a {ResourceRejected} event with resourceId = 0. + * @param tokenId ID of the token of which to clear the pending array + * @param maxRejections to prevent from rejecting resources which arrive just before this operation. + */ + function rejectAllResources(uint256 tokenId, uint256 maxRejections) + external; + + /** + * @notice Sets a new priority array for a given token. + * @dev The priority array is a non-sequential list of `uint16`s, where the lowest value is considered highest + * priority. + * @dev Value `0` of a priority is a special case equivalent to unitialized. + * @dev Requirements: + * + * - The caller must own the token or be approved to manage the token's resources + * - `tokenId` must exist. + * - The length of `priorities` must be equal the length of the active resources array. + * @dev Emits a {ResourcePrioritySet} event. + * @param tokenId ID of the token to set the priorities for + * @param priorities An array of priorities of active resources. The succesion of items in the priorities array + * matches that of the succesion of items in the active array + */ + function setPriority(uint256 tokenId, uint16[] calldata priorities) + external; + + /** + * @notice Used to retrieve IDs of the active resources of given token. + * @dev Resource data is stored by reference, in order to access the data corresponding to the ID, call + * `getResourceMetadata(tokenId, resourceId)`. + * @dev You can safely get 10k + * @param tokenId ID of the token to retrieve the IDs of the active resources + * @return uint64[] An array of active resource IDs of the given token + */ + function getActiveResources(uint256 tokenId) + external + view + returns (uint64[] memory); + + /** + * @notice Used to retrieve IDs of the pending resources of given token. + * @dev Resource data is stored by reference, in order to access the data corresponding to the ID, call + * `getResourceMetadata(tokenId, resourceId)`. + * @param tokenId ID of the token to retrieve the IDs of the pending resources + * @return uint64[] An array of pending resource IDs of the given token + */ + function getPendingResources(uint256 tokenId) + external + view + returns (uint64[] memory); + + /** + * @notice Used to retrieve the priorities of the active resoources of a given token. + * @dev Resource priorities are a non-sequential array of uint16 values with an array size equal to active resource + * priorites. + * @param tokenId ID of the token for which to retrieve the priorities of the active resources + * @return uint16[] An array of priorities of the active resources of the given token + */ + function getActiveResourcePriorities(uint256 tokenId) + external + view + returns (uint16[] memory); + + /** + * @notice Used to retrieve the resource that will be overriden if a given resource from the token's pending array + * is accepted. + * @dev Resource data is stored by reference, in order to access the data corresponding to the ID, call + * `getResourceMetadata(tokenId, resourceId)`. + * @param tokenId ID of the token to check + * @param newResourceId ID of the pending resource which will be accepted + * @return uint64 ID of the resource which will be replaced + */ + function getResourceOverwrites(uint256 tokenId, uint64 newResourceId) + external + view + returns (uint64); + + /** + * @notice Used to fetch the resource metadata of the specified token's active resource with the given index. + * @dev Can be overriden to implement enumerate, fallback or other custom logic. + * @param tokenId ID of the token from which to retrieve the resource metadata + * @param resourceId Resource Id, must be in the active resources array + * @return string The metadata of the resource belonging to the specified index in the token's active resources + * array + */ + function getResourceMetadata(uint256 tokenId, uint64 resourceId) + external + view + returns (string memory); - /// @dev This emits whenever a resource is set. - event ResourceSet(uint64 id); + /** + * @notice Used to grant permission to the user to manage token's resources. + * @dev This differs from transfer approvals, as approvals are not cleared when the approved party accepts or + * rejects a resource, or sets resource priorities. This approval is cleared on token transfer. + * @dev Only a single account can be approved at a time, so approving the `0x0` address clears previous approvals. + * @dev Requirements: + * + * - The caller must own the token or be an approved operator. + * - `tokenId` must exist. + * @dev Emits an {ApprovalForResources} event. + * @param to Address of the account to grant the approval to + * @param tokenId ID of the token for which the approval to manage the resources is granted + */ + function approveForResources(address to, uint256 tokenId) external; + + /** + * @notice Used to retrieve the address of the account approved to manage resources of a given token. + * @dev Requirements: + * + * - `tokenId` must exist. + * @param tokenId ID of the token for which to retrieve the approved address + * @return address Address of the account that is approved to manage the specified token's resources + */ + function getApprovedForResources(uint256 tokenId) + external + view + returns (address); + + /** + * @notice Used to add or remove an operator of resources for the caller. + * @dev Operators can call {acceptResource}, {rejectResource}, {rejectAllResources} or {setPriority} for any token + * owned by the caller. + * @dev Requirements: + * + * - The `operator` cannot be the caller. + * @dev Emits an {ApprovalForAllForResources} event. + * @param operator Address of the account to which the operator role is granted or revoked from + * @param approved The boolean value indicating whether the operator role is being granted (`true`) or revoked + * (`false`) + */ + function setApprovalForAllForResources(address operator, bool approved) + external; + + /** + * @notice Used to check whether the address has been granted the operator role by a given address or not. + * @dev See {setApprovalForAllForResources}. + * @param owner Address of the account that we are checking for whether it has granted the operator role + * @param operator Address of the account that we are checking whether it has the operator role or not + * @return bool The boolean value indicating wehter the account we are checking has been granted the operator role + */ + function isApprovedForAllForResources(address owner, address operator) + external + view + returns (bool); +} +```` - /// @dev This emits whenever a pending resource has been added to a token's pending resources. - event ResourceAddedToToken(uint256 indexed tokenId, uint64 resourceId); +The metadata, to which the metadata URI of the resource points, MAY contain a JSON response with the following fields: + +````json +{ + "title": "Asset Metadata", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Identifies the name of the asset associated with the resource" + }, + "description": { + "type": "string", + "description": "Identifies the general notes, abstracts, or summaries about the contents of the resource" + }, + "type": { + "type": "string", + "description": "Identifies the definition of the type of content of the resource" + }, + "locale": { + "type": "string", + "description": "Identifies metadata locale in ISO 639-1 format for translations and localisation of the resource" + }, + "license": { + "type": "string", + "description": "Identifies the license attached to the resource" + }, + "licenseUri": { + "type": "string", + "description": "Identifies the URI to the license statement of the license attached to the resource" + }, + "mediaUri": { + "type": "string", + "description": "Identifies the URI of the main media file associated with the resource" + }, + "thumbnailUri": { + "type": "string", + "description": "Identifies the URI of the thumbnail image associated with the resource to be used for preview of the resource in the wallets and client applications (the recommended maximum size is 350x350 px)" + }, + "externalUri": { + "type": "string", + "description": "Identifies the URI to the additional information about the subject or content of the resource" + }, + "properties": { + "type": "object", + "properties": "Identifies the optional custom attributes of the resource" + } + } +} +```` - /// @dev This emits whenever a resource has been accepted by the token owner. - event ResourceAccepted(uint256 indexed tokenId, uint64 resourceId); +While this is the suggested JSON schema for the resource metadata, it is not enforced and MAY be stuctured completely differently based on implementer's preference. + +The optional properties of the metadata JSON MAY include the following fields, or it MAY incorporate any number of custom fields, but MAY also not be included in the schema at all: + +````json + "properties": { + "rarity": { + "type": "string", + "value": "epic" + }, + "color": { + "type": "string", + "value": "red" + }, + "height": { + "type": "float", + "value": 192.4 + }, + "tags": { + "type": "array", + "value": ["music", "2020", "best"] + } + } +```` - /// @dev This emits whenever a pending resource has been dropped from the pending resources array. - event ResourceRejected(uint256 indexed tokenId, uint64 resourceId); +## Rationale - /// @dev This emits whenever a token's resource's priority has been set. - event ResourcePrioritySet(uint256 indexed tokenId); +Desinging the proposal, we considered the following questions: - /// @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); - - /// @notice Accepts the resource from pending resources. - /// @dev Moves the resource from the pending array to the accepted array. Array - /// order is not preserved. - /// @param tokenId The ID of the token to accept a resource - /// @param index The index of the resource in the pending resources array 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 ID of the token to reject a resource - /// @param index The index of the resource in the pending resources array 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 ID of 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. - /// @dev The length of the priorities array MUST - /// be equal to the present length of the active resources array. - /// @param tokenId the ID of 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 - /// in the storage. - /// @param tokenId The ID of the token of which to retrieve the active resource set - /// @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 - /// in the storage. - /// @param tokenId The ID of the token of which to retrieve the pending resource set - /// @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 validation is done on resource priority ranges, sorting MUST be - /// handled by the frontend. - /// @param tokenId The ID of the token of which to retrieve the active resource set - /// priorities - /// @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 ID of the token of which we want to overwrite the pending resource - /// @param resId The resource ID which MAY overwrite another - /// @return A uint64 corresponding to the resource ID of the resource that would be overwritten - function getResourceOverwrites(uint256 tokenId, uint64 resId) external view returns(uint64); - - /// @notice Returns the metadata of the resource associated with `resourceId` - /// @param resourceId The ID of the resource for which we are trying to retrieve the - /// resource metadata - /// @return The metadata of the resource with ID equal to `resourceId` - function getResourceMeta(uint64 resourceId) external view returns (string memory); - - /// @notice Fetches resource data for the token's active resource with the given index. - /// @dev Resources are stored by reference mapping _resources[resourceId]. - /// @dev MAY be overridden to implement enumerate, fallback or other custom logic. - /// @param tokenId The ID of the token for which we are getting the resource data for - /// @param resourceIndex The index of the active resource in the token - /// @return The metadata URI for the the resource we are fetching - function getResourceMetaForToken(uint256 tokenId, uint64 resourceIndex) external view returns (string memory); - - /// @notice Returns the IDs of all of the stored resources. - function getAllResources() external view returns (uint64[] memory); - - /// @notice Change or reaffirm the approved address for resources for an . - /// @dev The zero address indicates there is no approved address. - /// @dev MUST revert unless `msg.sender` is the current NFT owner, or an authorized - /// operator of the current owner. - /// @dev MUST emit ApprovalForResources event. - /// @param to The new approved token controller - /// @param tokenId The ID of the token 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 MUST emit the ApprovalForAllForResources event. - /// @dev 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 token. - /// @dev MUST revert if `tokenId` is not a valid token ID. - /// @param tokenId The ID of the token to find the approved address for - /// @return The approved address for this token, 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 tokens - /// @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); +**1. Why are EIP-712 permit-style signatures to manage approvals not used?** -} +For consistency. This proposal extends ERC-721 which already uses 1 transaction for approving operations with tokens. It would be inconsistent to have this and also support signing messages for operations with resources. -interface ERC165 { +**2. Why use indexes?** - function supportsInterface(bytes4 interfaceID) external view returns (bool); +To reduce the gas consumption. If the resource ID was used to find which token to accept or reject, iteration over arrays would be required and the cost of the operation would depend on the size of the active or pending children arrays. With the index, the cost is fixed. A list of active and pending children arrays per token need to be maintained, since methods to get them are part of the proposed interface. -} -```` +To avoid race conditions in which the index of a resource changes, the expected resource ID is included in operations requiring resource index, to verify that the resource being accessed using the index is the expected resource. -## Rationale +Implementation that whould internally keep track of indices using mapping was attemped. The average cost of adding a resource to a token increased by over 25%, costs of accepting and rejecting resources also increased 4.6% and 7.1% respeticvely. We concluded that it is not necessary for this proposal and can be implemented as an extension for use cases willing to accept this cost. In the sample implementation provided, there are several hooks which make this possible. -With NFTs being a widespread form of tokens in the Ethereum ecosystem and being used for a variety of use cases, it is time to standardize additional utility for them. Having multiple resources associated with a single NFT allows for greater utility and usability. +**3. Why is a method to get all the resources not included?** -The cross-metaverse or rather cross-engine compatibility issue could be solved by this EIP by having resources compatible with multiple engines or metaverses associated with the same NFT. +Getting all resources might not be an operation necessary for all implementers. Additionally, it can be added either as an extension, doable with hooks, or can be emulated using an indexer. -Such NFT can be given further utility by means of new additional resources: more games, more cosmetic items, appended to the same NFT. Thus, a game cosmetic item as an NFT becomes an ever-evolving NFT of infinite utility. +**4. Why is pagination not included?** + +Resource IDs use `uint64`, testing has confirmed that the limit of IDs you can read before reaching the gas limit is around 30.000. This is not expected to be a common use case so it is not a part of the interface. However, an implementer can create an extension for this use case if needed. -### Resource fields +**5. How does this proposal differ from the other proposals trying to address a similar problem?** -The MultiResource token standard supports two resource fields: +After reviewing them, we concluded that each contains at least one of these limitations: -- `id`: a `uint64` resource identifier -- `metadataURI`: a `string` pointing to the metadata URI associated with the resource +- Using a single URI which is replaced as new resources are needed, this introduces a trust issue for the token owner. +- Focusing only on a type of resource, while this proposal is resource type agnostic. +- Having a different token for each new use case, this means that the token is not forward-compatible. ### Multi-Resource Storage Schema @@ -253,103 +478,19 @@ The MultiResource token standard has been made compatible with [EIP-721](./eip-7 ## Test Cases -The RMRK MultiResource lego block implementation includes test cases written using Hardhat. - -## Reference Implementation - -RMRK MultiResource lego block and documentation - -- Compatible with the original version of the standard - -Neon Crisis, by CicadaNCR - -- A NFT game utilizing RMRK MultiResource lego block - -Snake Soldiers, by Steven Pineda - -- A NFT game utilizing RMRK MultiResource lego block - -### Implementation extras - -We designed additional **internal** implementations of methods to be used to add resource entries and add resources to tokens, but these were not considered crucial for the proposal to be functional. We expect these functions to only be callable by an issuer or an administrator. This is achieved with an `onlyIssuer` modifier of the following example: - -````solidity -pragma solidity ^0.8.15; - -contract Issued { - address private _issuer; - - constructor(){ - _setIssuer(_msgSender()); - } - - modifier onlyIssuer() { - require(_msgSender() == _issuer, "RMRK: Only issuer"); - _; - } - - function setIssuer(address issuer) external onlyIssuer { - _setIssuer(issuer); - } +Tests are included in [`multiresource.ts`](../assets/eip-5773/test/multiresource.ts). - function getIssuer() external view returns (address) { - return _issuer; - } - - function _setIssuer(address issuer) private { - _issuer = issuer; - } -} -```` +To run them in terminal, you can use the following commands: -A `RenderUtils` utility smart contract was developed to aid in getting the metadata URIs for different use cases. Such utility smart contract can be deployed once per chain and provide services to all MultiResource NFT compatible smart contracts: +``` +cd ../assets/eip-5773 +npm install +npx hardhat test +``` -````solidity -interface IRenderUtils { - /// @notice Returns resource metadata at `index` of active resource array on `tokenId`. - /// @param target The address of the smart contract that we want to get the resource metadata - /// @param tokenId The token ID of the token to which belongs the resource - /// @param index Index of the resource. It must be inside the range of active resource array - /// @return A stringified metadata URI of the resource - function getResourceByIndex( - address target, - uint256 tokenId, - uint256 index - ) external view returns (string memory); - - /// @notice Returns resource metadata at `index` of pending resource array on `tokenId`. - /// @param target The address of the smart contract that we want to get the resource metadata - /// @param tokenId The token ID of the token to which belongs the pending resource array - /// @param index Index of the resource in the pending resources array. It must be inside the range of pending - /// resource array - /// @return A stringified metadata URI of the pending resource - function getPendingResourceByIndex( - address target, - uint256 tokenId, - uint256 index - ) external view returns (string memory); - - /// @notice Returns resource metadata for the given IDs. - /// @param target The address of the smart contract that we want to get the resource metadata - /// @param resourceIds Resource IDs for which to retrieve the metadata strings - /// @return An array of metadata URIs for the requested resources - function getResourcesById(address target, uint64[] calldata resourceIds) - external - view - returns (string[] memory); - - /// @notice Returns the resource metadata with the highest priority for the given token. - /// @param target The address of the smart contract that we want to get the resource metadata - /// @param tokenId Token ID of which we are retrieving the metadata - /// @return Metadata URI with the highest priority - function getTopResourceMetaForToken(address target, uint256 tokenId) - external - view - returns (string memory); -} -```` +## Reference Implementation -Example implementation of such utility can be found in the RMRK's MultiResource lego block implementation. +See [`MultiResourceToken.sol`](../assets/eip-5773/contracts/MultiResourceToken.sol). ## Security Considerations diff --git a/assets/eip-5773/contracts/IMultiResource.sol b/assets/eip-5773/contracts/IMultiResource.sol new file mode 100644 index 00000000000000..b7d3cc3164600c --- /dev/null +++ b/assets/eip-5773/contracts/IMultiResource.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.0; + +interface IMultiResource { + event ResourceSet(uint64 resourceId); + + event ResourceAddedToToken( + uint256 indexed tokenId, + uint64 indexed resourceId, + uint64 indexed overwritesId + ); + + event ResourceAccepted( + uint256 indexed tokenId, + uint64 indexed resourceId, + uint64 indexed overwritesId + ); + + event ResourceRejected(uint256 indexed tokenId, uint64 indexed resourceId); + + event ResourcePrioritySet(uint256 indexed tokenId); + + event ApprovalForResources( + address indexed owner, + address indexed approved, + uint256 indexed tokenId + ); + + event ApprovalForAllForResources( + address indexed owner, + address indexed operator, + bool approved + ); + + function acceptResource( + uint256 tokenId, + uint256 index, + uint64 resourceId + ) external; + + function rejectResource( + uint256 tokenId, + uint256 index, + uint64 resourceId + ) external; + + function rejectAllResources(uint256 tokenId, uint256 maxRejections) + external; + + function setPriority(uint256 tokenId, uint16[] calldata priorities) + external; + + function getActiveResources(uint256 tokenId) + external + view + returns (uint64[] memory); + + function getPendingResources(uint256 tokenId) + external + view + returns (uint64[] memory); + + function getActiveResourcePriorities(uint256 tokenId) + external + view + returns (uint16[] memory); + + function getResourceOverwrites(uint256 tokenId, uint64 newResourceId) + external + view + returns (uint64); + + function getResourceMetadata(uint256 tokenId, uint64 resourceId) + external + view + returns (string memory); + + // Approvals + function approveForResources(address to, uint256 tokenId) external; + + function getApprovedForResources(uint256 tokenId) + external + view + returns (address); + + function setApprovalForAllForResources(address operator, bool approved) + external; + + function isApprovedForAllForResources(address owner, address operator) + external + view + returns (bool); +} diff --git a/assets/eip-5773/contracts/MultiResourceToken.sol b/assets/eip-5773/contracts/MultiResourceToken.sol new file mode 100644 index 00000000000000..8d6531da2d65a0 --- /dev/null +++ b/assets/eip-5773/contracts/MultiResourceToken.sol @@ -0,0 +1,722 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.15; + +import "./IMultiResource.sol"; +import "./library/MultiResourceLib.sol"; +import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; +import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; +import "@openzeppelin/contracts/utils/Context.sol"; + +contract MultiResourceToken is Context, IERC721, IMultiResource { + using MultiResourceLib for uint256; + using MultiResourceLib for uint64[]; + using MultiResourceLib for uint128[]; + using Address for address; + using Strings for uint256; + + // Token name + string private _name; + + // Token symbol + string private _symbol; + + // Mapping from token ID to owner address + mapping(uint256 => address) private _owners; + + // Mapping owner address to token count + mapping(address => uint256) private _balances; + + // Mapping from token ID to approved address + mapping(uint256 => address) private _tokenApprovals; + + // Mapping from owner to operator approvals + mapping(address => mapping(address => bool)) private _operatorApprovals; + + // Mapping from token ID to approved address for resources + mapping(uint256 => address) internal _tokenApprovalsForResources; + + // Mapping from owner to operator approvals for resources + mapping(address => mapping(address => bool)) + internal _operatorApprovalsForResources; + + //mapping of uint64 Ids to resource object + mapping(uint64 => string) internal _resources; + + //mapping of tokenId to new resource, to resource to be replaced + mapping(uint256 => mapping(uint64 => uint64)) private _resourceOverwrites; + + //mapping of tokenId to all resources + mapping(uint256 => uint64[]) internal _activeResources; + + //mapping of tokenId to an array of resource priorities + mapping(uint256 => uint16[]) internal _activeResourcePriorities; + + //Double mapping of tokenId to active resources + mapping(uint256 => mapping(uint64 => bool)) private _tokenResources; + + //mapping of tokenId to all resources by priority + mapping(uint256 => uint64[]) internal _pendingResources; + + constructor(string memory name_, string memory symbol_) { + _name = name_; + _symbol = symbol_; + } + + //////////////////////////////////////// + // ERC-721 COMPLIANCE + //////////////////////////////////////// + + function supportsInterface(bytes4 interfaceId) public pure returns (bool) { + return + interfaceId == type(IMultiResource).interfaceId || + interfaceId == type(IERC721).interfaceId || + interfaceId == type(IERC165).interfaceId; + } + + function balanceOf(address owner) + public + view + virtual + override + returns (uint256) + { + require( + owner != address(0), + "ERC721: address zero is not a valid owner" + ); + return _balances[owner]; + } + + function ownerOf(uint256 tokenId) + public + view + virtual + override + returns (address) + { + address owner = _owners[tokenId]; + require( + owner != address(0), + "ERC721: owner query for nonexistent token" + ); + return owner; + } + + function name() public view virtual returns (string memory) { + return _name; + } + + function symbol() public view virtual returns (string memory) { + return _symbol; + } + + function approve(address to, uint256 tokenId) public virtual { + address owner = ownerOf(tokenId); + require(to != owner, "MultiResource: approval to current owner"); + require( + _msgSender() == owner || isApprovedForAll(owner, _msgSender()), + "MultiResource: approve caller is not owner nor approved for all" + ); + + _approve(to, tokenId); + } + + function approveForResources(address to, uint256 tokenId) external virtual { + address owner = ownerOf(tokenId); + require(to != owner, "MultiResource: approval to current owner"); + require( + _msgSender() == owner || + isApprovedForAllForResources(owner, _msgSender()), + "MultiResource: approve caller is not owner nor approved for all" + ); + _approveForResources(to, tokenId); + } + + function getApproved(uint256 tokenId) + public + view + virtual + override + returns (address) + { + require( + _exists(tokenId), + "MultiResource: approved query for nonexistent token" + ); + + return _tokenApprovals[tokenId]; + } + + function getApprovedForResources(uint256 tokenId) + public + view + virtual + returns (address) + { + require( + _exists(tokenId), + "MultiResource: approved query for nonexistent token" + ); + return _tokenApprovalsForResources[tokenId]; + } + + function setApprovalForAll(address operator, bool approved) + public + virtual + override + { + _setApprovalForAll(_msgSender(), operator, approved); + } + + function isApprovedForAll(address owner, address operator) + public + view + virtual + override + returns (bool) + { + return _operatorApprovals[owner][operator]; + } + + function setApprovalForAllForResources(address operator, bool approved) + public + virtual + override + { + _setApprovalForAllForResources(_msgSender(), operator, approved); + } + + function isApprovedForAllForResources(address owner, address operator) + public + view + virtual + returns (bool) + { + return _operatorApprovalsForResources[owner][operator]; + } + + function transferFrom( + address from, + address to, + uint256 tokenId + ) public virtual override { + //solhint-disable-next-line max-line-length + require( + _isApprovedOrOwner(_msgSender(), tokenId), + "MultiResource: transfer caller is not owner nor approved" + ); + + _transfer(from, to, tokenId); + } + + function safeTransferFrom( + address from, + address to, + uint256 tokenId + ) public virtual override { + safeTransferFrom(from, to, tokenId, ""); + } + + function safeTransferFrom( + address from, + address to, + uint256 tokenId, + bytes memory data + ) public virtual override { + require( + _isApprovedOrOwner(_msgSender(), tokenId), + "MultiResource: transfer caller is not owner nor approved" + ); + _safeTransfer(from, to, tokenId, data); + } + + function _safeTransfer( + address from, + address to, + uint256 tokenId, + bytes memory data + ) internal virtual { + _transfer(from, to, tokenId); + require( + _checkOnERC721Received(from, to, tokenId, data), + "MultiResource: transfer to non ERC721 Receiver implementer" + ); + } + + function _exists(uint256 tokenId) internal view virtual returns (bool) { + return _owners[tokenId] != address(0); + } + + function _isApprovedOrOwner(address spender, uint256 tokenId) + internal + view + virtual + returns (bool) + { + require( + _exists(tokenId), + "MultiResource: approved query for nonexistent token" + ); + address owner = ownerOf(tokenId); + return (spender == owner || + isApprovedForAll(owner, spender) || + getApproved(tokenId) == spender); + } + + function _isApprovedForResourcesOrOwner(address user, uint256 tokenId) + internal + view + virtual + returns (bool) + { + require( + _exists(tokenId), + "MultiResource: approved query for nonexistent token" + ); + address owner = ownerOf(tokenId); + return (user == owner || + isApprovedForAllForResources(owner, user) || + getApprovedForResources(tokenId) == user); + } + + function _safeMint(address to, uint256 tokenId) internal virtual { + _safeMint(to, tokenId, ""); + } + + function _safeMint( + address to, + uint256 tokenId, + bytes memory data + ) internal virtual { + _mint(to, tokenId); + require( + _checkOnERC721Received(address(0), to, tokenId, data), + "MultiResource: transfer to non ERC721 Receiver implementer" + ); + } + + function _mint(address to, uint256 tokenId) internal virtual { + require(to != address(0), "MultiResource: mint to the zero address"); + require(!_exists(tokenId), "MultiResource: token already minted"); + + _beforeTokenTransfer(address(0), to, tokenId); + + _balances[to] += 1; + _owners[tokenId] = to; + + emit Transfer(address(0), to, tokenId); + + _afterTokenTransfer(address(0), to, tokenId); + } + + function _burn(uint256 tokenId) internal virtual { + // WARNING: If you intend to allow the reminting of a burned token, you + // might want to clean the resources for the token, that is: + // _pendingResources, _activeResources, _resourceOverwrites + // _activeResourcePriorities and _tokenResources. + address owner = ownerOf(tokenId); + + _beforeTokenTransfer(owner, address(0), tokenId); + + // Clear approvals + _approve(address(0), tokenId); + _approveForResources(address(0), tokenId); + + _balances[owner] -= 1; + delete _owners[tokenId]; + + emit Transfer(owner, address(0), tokenId); + + _afterTokenTransfer(owner, address(0), tokenId); + } + + function _transfer( + address from, + address to, + uint256 tokenId + ) internal virtual { + require( + ownerOf(tokenId) == from, + "MultiResource: transfer from incorrect owner" + ); + require( + to != address(0), + "MultiResource: transfer to the zero address" + ); + + _beforeTokenTransfer(from, to, tokenId); + + // Clear approvals from the previous owner + _approve(address(0), tokenId); + _approveForResources(address(0), tokenId); + + _balances[from] -= 1; + _balances[to] += 1; + _owners[tokenId] = to; + + emit Transfer(from, to, tokenId); + + _afterTokenTransfer(from, to, tokenId); + } + + function _approve(address to, uint256 tokenId) internal virtual { + _tokenApprovals[tokenId] = to; + emit Approval(ownerOf(tokenId), to, tokenId); + } + + function _approveForResources(address to, uint256 tokenId) + internal + virtual + { + _tokenApprovalsForResources[tokenId] = to; + emit ApprovalForResources(ownerOf(tokenId), to, tokenId); + } + + function _setApprovalForAll( + address owner, + address operator, + bool approved + ) internal virtual { + require(owner != operator, "MultiResource: approve to caller"); + _operatorApprovals[owner][operator] = approved; + emit ApprovalForAll(owner, operator, approved); + } + + function _setApprovalForAllForResources( + address owner, + address operator, + bool approved + ) internal virtual { + require(owner != operator, "MultiResource: approve to caller"); + _operatorApprovalsForResources[owner][operator] = approved; + emit ApprovalForAllForResources(owner, operator, approved); + } + + function _checkOnERC721Received( + address from, + address to, + uint256 tokenId, + bytes memory data + ) private returns (bool) { + if (to.isContract()) { + try + IERC721Receiver(to).onERC721Received( + _msgSender(), + from, + tokenId, + data + ) + returns (bytes4 retval) { + return + retval == + IERC721Receiver.onERC721Received.selector; + } catch (bytes memory reason) { + if (reason.length == 0) { + revert( + "MultiResource: transfer to non ERC721 Receiver implementer" + ); + } else { + /// @solidity memory-safe-assembly + assembly { + revert(add(32, reason), mload(reason)) + } + } + } + } else { + return true; + } + } + + function _beforeTokenTransfer( + address from, + address to, + uint256 tokenId + ) internal virtual {} + + function _afterTokenTransfer( + address from, + address to, + uint256 tokenId + ) internal virtual {} + + //////////////////////////////////////// + // RESOURCES + //////////////////////////////////////// + + function acceptResource( + uint256 tokenId, + uint256 index, + uint64 resourceId + ) external virtual { + require( + index < _pendingResources[tokenId].length, + "MultiResource: index out of bounds" + ); + require( + _isApprovedForResourcesOrOwner(_msgSender(), tokenId), + "MultiResource: not owner or approved" + ); + require( + resourceId == _pendingResources[tokenId][index], + "MultiResource: Unexpected resource" + ); + + _beforeAcceptResource(tokenId, index, resourceId); + _pendingResources[tokenId].removeItemByIndex(index); + + uint64 overwrite = _resourceOverwrites[tokenId][resourceId]; + if (overwrite != uint64(0)) { + // It could have been overwritten previously so it's fine if it's not found. + // If it's not deleted (not found), we don't want to send it on the event + if (!_activeResources[tokenId].removeItemByValue(overwrite)) + overwrite = uint64(0); + else delete _tokenResources[tokenId][overwrite]; + delete (_resourceOverwrites[tokenId][resourceId]); + } + _activeResources[tokenId].push(resourceId); + //Push 0 value of uint16 to array, e.g., uninitialized + _activeResourcePriorities[tokenId].push(uint16(0)); + emit ResourceAccepted(tokenId, resourceId, overwrite); + _afterAcceptResource(tokenId, index, resourceId); + } + + function rejectResource( + uint256 tokenId, + uint256 index, + uint64 resourceId + ) external virtual { + require( + index < _pendingResources[tokenId].length, + "MultiResource: index out of bounds" + ); + require( + _pendingResources[tokenId].length > index, + "MultiResource: Pending resource index out of range" + ); + require( + _isApprovedForResourcesOrOwner(_msgSender(), tokenId), + "MultiResource: not owner or approved" + ); + + _beforeRejectResource(tokenId, index, resourceId); + _pendingResources[tokenId].removeItemByValue(resourceId); + delete _tokenResources[tokenId][resourceId]; + delete _resourceOverwrites[tokenId][resourceId]; + + emit ResourceRejected(tokenId, resourceId); + _afterRejectResource(tokenId, index, resourceId); + } + + function rejectAllResources(uint256 tokenId, uint256 maxRejections) external virtual { + require( + _isApprovedForResourcesOrOwner(_msgSender(), tokenId), + "MultiResource: not owner or approved" + ); + + uint256 len = _pendingResources[tokenId].length; + if (len > maxRejections) revert ("Unexpected number of resources"); + + _beforeRejectAllResources(tokenId); + for (uint256 i; i < len; ) { + uint64 resourceId = _pendingResources[tokenId][i]; + delete _resourceOverwrites[tokenId][resourceId]; + unchecked { + ++i; + } + } + delete (_pendingResources[tokenId]); + + emit ResourceRejected(tokenId, uint64(0)); + _afterRejectAllResources(tokenId); + } + + function setPriority(uint256 tokenId, uint16[] memory priorities) + external + virtual + { + uint256 length = priorities.length; + require( + length == _activeResources[tokenId].length, + "MultiResource: Bad priority list length" + ); + require( + _isApprovedForResourcesOrOwner(_msgSender(), tokenId), + "MultiResource: not owner or approved" + ); + + _beforeSetPriority(tokenId, priorities); + _activeResourcePriorities[tokenId] = priorities; + + emit ResourcePrioritySet(tokenId); + _afterSetPriority(tokenId, priorities); + } + + function getActiveResources(uint256 tokenId) + public + view + virtual + returns (uint64[] memory) + { + return _activeResources[tokenId]; + } + + function getPendingResources(uint256 tokenId) + public + view + virtual + returns (uint64[] memory) + { + return _pendingResources[tokenId]; + } + + function getActiveResourcePriorities(uint256 tokenId) + public + view + virtual + returns (uint16[] memory) + { + return _activeResourcePriorities[tokenId]; + } + + function getResourceOverwrites(uint256 tokenId, uint64 newResourceId) + public + view + virtual + returns (uint64) + { + return _resourceOverwrites[tokenId][newResourceId]; + } + + function getResourceMetadata(uint256 tokenId, uint64 resourceId) + public + view + virtual + returns (string memory) + { + if (!_tokenResources[tokenId][resourceId]) + revert("MultiResource: Token does not have resource"); + return _resources[resourceId]; + } + + function tokenURI(uint256 tokenId) + public + view + virtual + returns (string memory) + { + return ""; + } + + // To be implemented with custom guards + + function _addResourceEntry(uint64 id, string memory metadataURI) internal { + require(id != uint64(0), "RMRK: Write to zero"); + require( + bytes(_resources[id]).length == 0, + "RMRK: resource already exists" + ); + + _beforeAddResource(id, metadataURI); + _resources[id] = metadataURI; + + emit ResourceSet(id); + _afterAddResource(id, metadataURI); + } + + function _addResourceToToken( + uint256 tokenId, + uint64 resourceId, + uint64 overwrites + ) internal { + require( + !_tokenResources[tokenId][resourceId], + "MultiResource: Resource already exists on token" + ); + + require( + bytes(_resources[resourceId]).length != 0, + "MultiResource: Resource not found in storage" + ); + + require( + _pendingResources[tokenId].length < 128, + "MultiResource: Max pending resources reached" + ); + + _beforeAddResourceToToken(tokenId, resourceId, overwrites); + _tokenResources[tokenId][resourceId] = true; + _pendingResources[tokenId].push(resourceId); + + if (overwrites != uint64(0)) { + _resourceOverwrites[tokenId][resourceId] = overwrites; + } + + emit ResourceAddedToToken(tokenId, resourceId, overwrites); + _afterAddResourceToToken(tokenId, resourceId, overwrites); + } + + // HOOKS + + function _beforeAddResource(uint64 id, string memory metadataURI) + internal + virtual + {} + + function _afterAddResource(uint64 id, string memory metadataURI) + internal + virtual + {} + + function _beforeAddResourceToToken( + uint256 tokenId, + uint64 resourceId, + uint64 overwrites + ) internal virtual {} + + function _afterAddResourceToToken( + uint256 tokenId, + uint64 resourceId, + uint64 overwrites + ) internal virtual {} + + function _beforeAcceptResource( + uint256 tokenId, + uint256 index, + uint256 resourceId + ) internal virtual {} + + function _afterAcceptResource( + uint256 tokenId, + uint256 index, + uint256 resourceId + ) internal virtual {} + + function _beforeRejectResource( + uint256 tokenId, + uint256 index, + uint256 resourceId + ) internal virtual {} + + function _afterRejectResource( + uint256 tokenId, + uint256 index, + uint256 resourceId + ) internal virtual {} + + function _beforeRejectAllResources(uint256 tokenId) internal virtual {} + + function _afterRejectAllResources(uint256 tokenId) internal virtual {} + + function _beforeSetPriority(uint256 tokenId, uint16[] memory priorities) + internal + virtual + {} + + function _afterSetPriority(uint256 tokenId, uint16[] memory priorities) + internal + virtual + {} +} diff --git a/assets/eip-5773/contracts/library/MultiResourceLib.sol b/assets/eip-5773/contracts/library/MultiResourceLib.sol new file mode 100644 index 00000000000000..bf13af5f8bf7e7 --- /dev/null +++ b/assets/eip-5773/contracts/library/MultiResourceLib.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.0; + +library MultiResourceLib { + function removeItemByValue(uint64[] storage array, uint64 value) + internal + returns (bool) + { + uint64[] memory memArr = array; //Copy array to memory, check for gas savings here + uint256 length = memArr.length; //gas savings + for (uint256 i; i < length; ) { + if (memArr[i] == value) { + removeItemByIndex(array, i); + return true; + } + unchecked { + ++i; + } + } + return false; + } + + //For reasource storage array + function removeItemByIndex(uint64[] storage array, uint256 index) internal { + //Check to see if this is already gated by require in all calls + require(index < array.length); + array[index] = array[array.length - 1]; + array.pop(); + } +} diff --git a/assets/eip-5773/contracts/mocks/ERC721ReceiverMock.sol b/assets/eip-5773/contracts/mocks/ERC721ReceiverMock.sol new file mode 100644 index 00000000000000..5bb7cb7633ba4f --- /dev/null +++ b/assets/eip-5773/contracts/mocks/ERC721ReceiverMock.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.15; + +contract ERC721ReceiverMock { + bytes4 constant ERC721_RECEIVED = 0x150b7a02; + + function onERC721Received( + address, + address, + uint256, + bytes calldata + ) public returns (bytes4) { + return ERC721_RECEIVED; + } +} diff --git a/assets/eip-5773/contracts/mocks/MultiResourceTokenMock.sol b/assets/eip-5773/contracts/mocks/MultiResourceTokenMock.sol new file mode 100644 index 00000000000000..53f2bae0a4a445 --- /dev/null +++ b/assets/eip-5773/contracts/mocks/MultiResourceTokenMock.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.15; + +import "../MultiResourceToken.sol"; + +contract MultiResourceTokenMock is MultiResourceToken { + address private _issuer; + + constructor(string memory name, string memory symbol) + MultiResourceToken(name, symbol) + { + _setIssuer(_msgSender()); + } + + modifier onlyIssuer() { + require(_msgSender() == _issuer, "RMRK: Only issuer"); + _; + } + + function setIssuer(address issuer) external onlyIssuer { + _setIssuer(issuer); + } + + function getIssuer() external view returns (address) { + return _issuer; + } + + function mint(address to, uint256 tokenId) external onlyIssuer { + _mint(to, tokenId); + } + + function transfer(address to, uint256 tokenId) external { + _transfer(msg.sender, to, tokenId); + } + + function burn(uint256 tokenId) external { + _burn(tokenId); + } + + function addResourceToToken( + uint256 tokenId, + uint64 resourceId, + uint64 overwrites + ) external onlyIssuer { + _addResourceToToken(tokenId, resourceId, overwrites); + } + + function addResourceEntry(uint64 id, string memory metadataURI) + external + onlyIssuer + { + _addResourceEntry(id, metadataURI); + } + + function _setIssuer(address issuer) private { + _issuer = issuer; + } +} diff --git a/assets/eip-5773/contracts/mocks/NonReceiverMock.sol b/assets/eip-5773/contracts/mocks/NonReceiverMock.sol new file mode 100644 index 00000000000000..c1763ec4f58ed4 --- /dev/null +++ b/assets/eip-5773/contracts/mocks/NonReceiverMock.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.15; + +contract NonReceiverMock { + function dummy() external {} +} diff --git a/assets/eip-5773/contracts/utils/MultiResourceRenderUtils.sol b/assets/eip-5773/contracts/utils/MultiResourceRenderUtils.sol new file mode 100644 index 00000000000000..9bd0cfb7f5d288 --- /dev/null +++ b/assets/eip-5773/contracts/utils/MultiResourceRenderUtils.sol @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: Apache-2.0 + +import "../IMultiResource.sol"; + +pragma solidity ^0.8.15; + +/** + * @dev Extra utility functions for composing RMRK resources. + */ + +contract MultiResourceRenderUtils { + uint16 private constant _LOWEST_POSSIBLE_PRIORITY = 2**16 - 1; + + struct ActiveResource { + uint64 id; + uint16 priority; + string metadata; + } + + struct PendingResource { + uint64 id; + uint128 acceptRejectIndex; + uint64 overwritesResourceWithId; + string metadata; + } + + function getActiveResources(address target, uint256 tokenId) + public + view + virtual + returns (ActiveResource[] memory) + { + IMultiResource target_ = IMultiResource(target); + + uint64[] memory resources = target_.getActiveResources(tokenId); + uint16[] memory priorities = target_.getActiveResourcePriorities( + tokenId + ); + uint256 len = resources.length; + if (len == 0) { + revert("Token has no resources"); + } + + ActiveResource[] memory activeResources = new ActiveResource[](len); + string memory metadata; + for (uint256 i; i < len; ) { + metadata = target_.getResourceMetadata(tokenId, resources[i]); + activeResources[i] = ActiveResource({ + id: resources[i], + priority: priorities[i], + metadata: metadata + }); + unchecked { + ++i; + } + } + return activeResources; + } + + function getPendingResources(address target, uint256 tokenId) + public + view + virtual + returns (PendingResource[] memory) + { + IMultiResource target_ = IMultiResource(target); + + uint64[] memory resources = target_.getPendingResources(tokenId); + uint256 len = resources.length; + if (len == 0) { + revert("Token has no resources"); + } + + PendingResource[] memory pendingResources = new PendingResource[](len); + string memory metadata; + uint64 overwritesResourceWithId; + for (uint256 i; i < len; ) { + metadata = target_.getResourceMetadata(tokenId, resources[i]); + overwritesResourceWithId = target_.getResourceOverwrites( + tokenId, + resources[i] + ); + pendingResources[i] = PendingResource({ + id: resources[i], + acceptRejectIndex: uint128(i), + overwritesResourceWithId: overwritesResourceWithId, + metadata: metadata + }); + unchecked { + ++i; + } + } + return pendingResources; + } + + /** + * @notice Returns resource metadata strings for the given ids + * + * Requirements: + * + * - `resourceIds` must exist. + */ + function getResourcesById( + address target, + uint256 tokenId, + uint64[] calldata resourceIds + ) public view virtual returns (string[] memory) { + IMultiResource target_ = IMultiResource(target); + uint256 len = resourceIds.length; + string[] memory resources = new string[](len); + for (uint256 i; i < len; ) { + resources[i] = target_.getResourceMetadata(tokenId, resourceIds[i]); + unchecked { + ++i; + } + } + return resources; + } + + /** + * @notice Returns the resource metadata with the highest priority for the given token + */ + function getTopResourceMetaForToken(address target, uint256 tokenId) + external + view + returns (string memory) + { + IMultiResource target_ = IMultiResource(target); + uint16[] memory priorities = target_.getActiveResourcePriorities( + tokenId + ); + uint64[] memory resources = target_.getActiveResources(tokenId); + uint256 len = priorities.length; + if (len == 0) { + revert("Token has no resources"); + } + + uint16 maxPriority = _LOWEST_POSSIBLE_PRIORITY; + uint64 maxPriorityResource; + for (uint64 i; i < len; ) { + uint16 currentPrio = priorities[i]; + if (currentPrio < maxPriority) { + maxPriority = currentPrio; + maxPriorityResource = resources[i]; + } + unchecked { + ++i; + } + } + return target_.getResourceMetadata(tokenId, maxPriorityResource); + } +} diff --git a/assets/eip-5773/hardhat.config.ts b/assets/eip-5773/hardhat.config.ts new file mode 100644 index 00000000000000..68a2634c68b5ec --- /dev/null +++ b/assets/eip-5773/hardhat.config.ts @@ -0,0 +1,47 @@ +import * as dotenv from 'dotenv'; + +import { HardhatUserConfig, task } from 'hardhat/config'; +import '@nomicfoundation/hardhat-chai-matchers'; +import '@nomiclabs/hardhat-etherscan'; +import '@typechain/hardhat'; +import 'hardhat-contract-sizer'; +import 'hardhat-gas-reporter'; +import 'solidity-coverage'; + +dotenv.config(); + +// This is a sample Hardhat task. To learn how to create your own go to +// https://hardhat.org/guides/create-task.html +task('accounts', 'Prints the list of accounts', async (taskArgs, hre) => { + const accounts = await hre.ethers.getSigners(); + + for (const account of accounts) { + console.log(account.address); + } +}); + +// You need to export an object to set up your config +// Go to https://hardhat.org/config/ to learn more + +const config: HardhatUserConfig = { + solidity: { + version: '0.8.15', + settings: { + optimizer: { + enabled: true, + runs: 200, + }, + }, + }, + gasReporter: { + enabled: process.env.REPORT_GAS !== undefined, + currency: 'USD', + coinmarketcap: process.env.COIN_MARKET_CAP_KEY || "", + gasPrice: 50, + }, + etherscan: { + apiKey: process.env.ETHERSCAN_API_KEY, + }, +}; + +export default config; diff --git a/assets/eip-5773/package.json b/assets/eip-5773/package.json new file mode 100644 index 00000000000000..564fab7bd96f66 --- /dev/null +++ b/assets/eip-5773/package.json @@ -0,0 +1,42 @@ +{ + "name": "multiresource-eip", + "dependencies": { + "@openzeppelin/contracts": "^4.6.0" + }, + "devDependencies": { + "@nomicfoundation/hardhat-chai-matchers": "^1.0.1", + "@nomicfoundation/hardhat-network-helpers": "^1.0.3", + "@nomiclabs/hardhat-ethers": "^2.2.1", + "@nomiclabs/hardhat-etherscan": "^3.1.0", + "@openzeppelin/test-helpers": "^0.5.15", + "@primitivefi/hardhat-dodoc": "^0.2.3", + "@typechain/ethers-v5": "^10.1.0", + "@typechain/hardhat": "^6.1.2", + "@types/chai": "^4.3.1", + "@types/mocha": "^9.1.0", + "@types/node": "^18.0.3", + "@typescript-eslint/eslint-plugin": "^5.30.6", + "@typescript-eslint/parser": "^5.30.6", + "chai": "^4.3.6", + "dotenv": "^10.0.0", + "eslint": "^8.27.0", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-prettier": "^4.2.1", + "eslint-plugin-promise": "^6.0.0", + "ethers": "^5.6.9", + "hardhat": "^2.12.2", + "hardhat-contract-sizer": "^2.6.1", + "hardhat-gas-reporter": "^1.0.8", + "prettier": "2.7.1", + "prettier-plugin-solidity": "^1.0.0-beta.20", + "solc": "^0.8.9", + "solhint": "^3.3.7", + "solidity-coverage": "^0.8.2", + "ts-node": "^10.8.2", + "typechain": "^8.1.0", + "typescript": "^4.7.4", + "walk-sync": "^3.0.0" + } +} diff --git a/assets/eip-5773/test/multiresource.ts b/assets/eip-5773/test/multiresource.ts new file mode 100644 index 00000000000000..c901b74de49d21 --- /dev/null +++ b/assets/eip-5773/test/multiresource.ts @@ -0,0 +1,677 @@ +import { ethers } from 'hardhat'; +import { expect } from 'chai'; +import { + ERC721ReceiverMock, + MultiResourceReceiverMock, + MultiResourceTokenMock, + NonReceiverMock, + MultiResourceRenderUtils, +} from '../typechain-types'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; + +describe('MultiResource', async () => { + let token: MultiResourceTokenMock; + let renderUtils: MultiResourceRenderUtils; + let nonReceiver: NonReceiverMock; + let receiver721: ERC721ReceiverMock; + + let owner: SignerWithAddress; + let addrs: SignerWithAddress[]; + + const name = 'RmrkTest'; + const symbol = 'RMRKTST'; + + const metaURIDefault = 'metaURI'; + + beforeEach(async () => { + const [signersOwner, ...signersAddr] = await ethers.getSigners(); + owner = signersOwner; + addrs = signersAddr; + + const multiresourceFactory = await ethers.getContractFactory('MultiResourceTokenMock'); + token = await multiresourceFactory.deploy(name, symbol); + await token.deployed(); + + const renderFactory = await ethers.getContractFactory('MultiResourceRenderUtils'); + renderUtils = await renderFactory.deploy(); + await renderUtils.deployed(); + }); + + describe('Init', async function () { + it('Name', async function () { + expect(await token.name()).to.equal(name); + }); + + it('Symbol', async function () { + expect(await token.symbol()).to.equal(symbol); + }); + }); + + describe('ERC165 check', async function () { + it('can support IERC165', async function () { + expect(await token.supportsInterface('0x01ffc9a7')).to.equal(true); + }); + + it('can support IERC721', async function () { + expect(await token.supportsInterface('0x80ac58cd')).to.equal(true); + }); + + it('can support IMultiResource', async function () { + expect(await token.supportsInterface('0xb0ecc5ae')).to.equal(true); + }); + + it('cannot support other interfaceId', async function () { + expect(await token.supportsInterface('0xffffffff')).to.equal(false); + }); + }); + + describe('Check OnReceived ERC721 and Multiresource', async function () { + it('Revert on transfer to non onERC721/onMultiresource implementer', async function () { + const tokenId = 1; + await token.mint(owner.address, tokenId); + + const NonReceiver = await ethers.getContractFactory('NonReceiverMock'); + nonReceiver = await NonReceiver.deploy(); + await nonReceiver.deployed(); + + await expect( + token + .connect(owner) + ['safeTransferFrom(address,address,uint256)'](owner.address, nonReceiver.address, 1), + ).to.be.revertedWith('MultiResource: transfer to non ERC721 Receiver implementer'); + }); + + it('onERC721Received callback on transfer', async function () { + const tokenId = 1; + await token.mint(owner.address, tokenId); + + const ERC721Receiver = await ethers.getContractFactory('ERC721ReceiverMock'); + receiver721 = await ERC721Receiver.deploy(); + await receiver721.deployed(); + + await token + .connect(owner) + ['safeTransferFrom(address,address,uint256)'](owner.address, receiver721.address, 1); + expect(await token.ownerOf(1)).to.equal(receiver721.address); + }); + }); + + describe('Resource storage', async function () { + it('can add resource', async function () { + const id = 10; + + await expect(token.addResourceEntry(id, metaURIDefault)) + .to.emit(token, 'ResourceSet') + .withArgs(id); + }); + + it('cannot get non existing resource', async function () { + const tokenId = 1; + const resId = 10; + await token.mint(owner.address, tokenId); + await expect(token.getResourceMetadata(tokenId, resId)).to.be.revertedWith( + 'MultiResource: Token does not have resource', + ); + }); + + it('cannot add resource entry if not issuer', async function () { + const id = 10; + await expect(token.connect(addrs[1]).addResourceEntry(id, metaURIDefault)).to.be.revertedWith( + 'RMRK: Only issuer', + ); + }); + + it('can set and get issuer', async function () { + const newIssuerAddr = addrs[1].address; + expect(await token.getIssuer()).to.equal(owner.address); + + await token.setIssuer(newIssuerAddr); + expect(await token.getIssuer()).to.equal(newIssuerAddr); + }); + + it('cannot set issuer if not issuer', async function () { + const newIssuer = addrs[1]; + await expect(token.connect(newIssuer).setIssuer(newIssuer.address)).to.be.revertedWith( + 'RMRK: Only issuer', + ); + }); + + it('cannot overwrite resource', async function () { + const id = 10; + + await token.addResourceEntry(id, metaURIDefault); + await expect(token.addResourceEntry(id, metaURIDefault)).to.be.revertedWith( + 'RMRK: resource already exists', + ); + }); + + it('cannot add resource with id 0', async function () { + const id = ethers.utils.hexZeroPad('0x0', 8); + + await expect(token.addResourceEntry(id, metaURIDefault)).to.be.revertedWith( + 'RMRK: Write to zero', + ); + }); + + it('cannot add same resource twice', async function () { + const id = 10; + + await expect(token.addResourceEntry(id, metaURIDefault)) + .to.emit(token, 'ResourceSet') + .withArgs(id); + + await expect(token.addResourceEntry(id, metaURIDefault)).to.be.revertedWith( + 'RMRK: resource already exists', + ); + }); + }); + + describe('Adding resources', async function () { + it('can add resource to token', async function () { + const resId = 1; + const resId2 = 2; + const tokenId = 1; + + await token.mint(owner.address, tokenId); + await addResources([resId, resId2]); + await expect(token.addResourceToToken(tokenId, resId, 0)).to.emit( + token, + 'ResourceAddedToToken', + ); + await expect(token.addResourceToToken(tokenId, resId2, 0)).to.emit( + token, + 'ResourceAddedToToken', + ); + + const pendingIds = await token.getPendingResources(tokenId); + expect(await renderUtils.getResourcesById(token.address, tokenId, pendingIds)).to.be.eql([ + metaURIDefault, + metaURIDefault, + ]); + }); + + it('cannot add non existing resource to token', async function () { + const resId = 1; + const tokenId = 1; + + await token.mint(owner.address, tokenId); + await expect(token.addResourceToToken(tokenId, resId, 0)).to.be.revertedWith( + 'MultiResource: Resource not found in storage', + ); + }); + + it('can add resource to non existing token and it is pending when minted', async function () { + const resId = 1; + const tokenId = 1; + await addResources([resId]); + + await token.addResourceToToken(tokenId, resId, 0); + await token.mint(owner.address, tokenId); + expect(await token.getPendingResources(tokenId)).to.eql([ethers.BigNumber.from(resId)]); + }); + + it('cannot add resource twice to the same token', async function () { + const resId = 1; + const tokenId = 1; + + await token.mint(owner.address, tokenId); + await addResources([resId]); + await token.addResourceToToken(tokenId, resId, 0); + await expect( + token.addResourceToToken(tokenId, ethers.BigNumber.from(resId), 0), + ).to.be.revertedWith('MultiResource: Resource already exists on token'); + }); + + it('cannot add too many resources to the same token', async function () { + const tokenId = 1; + + await token.mint(owner.address, tokenId); + for (let i = 1; i <= 128; i++) { + await addResources([i]); + await token.addResourceToToken(tokenId, i, 0); + } + + // Now it's full, next should fail + const resId = 129; + await addResources([resId]); + await expect(token.addResourceToToken(tokenId, resId, 0)).to.be.revertedWith( + 'MultiResource: Max pending resources reached', + ); + }); + + it('can add same resource to 2 different tokens', async function () { + const resId = 1; + const tokenId1 = 1; + const tokenId2 = 2; + + await token.mint(owner.address, tokenId1); + await token.mint(owner.address, tokenId2); + await addResources([resId]); + await token.addResourceToToken(tokenId1, resId, 0); + await token.addResourceToToken(tokenId2, resId, 0); + }); + }); + + describe('Accepting resources', async function () { + it('can accept resource if owner', async function () { + const { tokenOwner, tokenId } = await mintSampleToken(); + const approved = tokenOwner; + + await checkAcceptFromAddress(approved, tokenId); + }); + + it('can accept resource if approved for resources', async function () { + const { tokenId } = await mintSampleToken(); + const approved = addrs[1]; + + await token.approveForResources(approved.address, tokenId); + await checkAcceptFromAddress(approved, tokenId); + }); + + it('can accept resource if approved for resources for all', async function () { + const { tokenId } = await mintSampleToken(); + const approved = addrs[2]; + + await token.setApprovalForAllForResources(approved.address, true); + await checkAcceptFromAddress(approved, tokenId); + }); + + it('can accept multiple resources', async function () { + const resId = 1; + const resId2 = 2; + const tokenId = 1; + + await token.mint(owner.address, tokenId); + await addResources([resId, resId2]); + await token.addResourceToToken(tokenId, resId, 0); + await token.addResourceToToken(tokenId, resId2, 0); + await expect(token.acceptResource(tokenId, 1, resId2)) + .to.emit(token, 'ResourceAccepted') + .withArgs(tokenId, resId2, 0); + await expect(token.acceptResource(tokenId, 0, resId)) + .to.emit(token, 'ResourceAccepted') + .withArgs(tokenId, resId, 0); + + expect(await token.getPendingResources(tokenId)).to.be.eql([]); + + const activeIds = await token.getActiveResources(tokenId); + expect(await renderUtils.getResourcesById(token.address, tokenId, activeIds)).to.eql([ + metaURIDefault, + metaURIDefault, + ]); + }); + + it('cannot accept resource twice', async function () { + const resId = 1; + const tokenId = 1; + + await token.mint(owner.address, tokenId); + await addResources([resId]); + await token.addResourceToToken(tokenId, resId, 0); + await token.acceptResource(tokenId, 0, resId); + }); + + it('cannot accept resource if not owner', async function () { + const resId = 1; + const tokenId = 1; + + await token.mint(owner.address, tokenId); + await addResources([resId]); + await token.addResourceToToken(tokenId, resId, 0); + await expect(token.connect(addrs[1]).acceptResource(tokenId, 0, resId)).to.be.revertedWith( + 'MultiResource: not owner or approved', + ); + }); + + it('cannot accept non existing resource', async function () { + const tokenId = 1; + + await token.mint(owner.address, tokenId); + await expect(token.acceptResource(tokenId, 0, 1)).to.be.revertedWith( + 'MultiResource: index out of bounds', + ); + }); + }); + + describe('Overwriting resources', async function () { + it('can add resource to token overwritting an existing one', async function () { + const resId = 1; + const resId2 = 2; + const tokenId = 1; + + await token.mint(owner.address, tokenId); + await addResources([resId, resId2]); + await token.addResourceToToken(tokenId, resId, 0); + await token.acceptResource(tokenId, 0, resId); + + // Add new resource to overwrite the first, and accept + const activeResources = await token.getActiveResources(tokenId); + await expect(token.addResourceToToken(tokenId, resId2, activeResources[0])).to.emit(token, 'ResourceAddedToToken') + .withArgs(tokenId, resId2, resId); + const pendingResources = await token.getPendingResources(tokenId); + + expect(await token.getResourceOverwrites(tokenId, pendingResources[0])).to.eql( + activeResources[0], + ); + await expect(token.acceptResource(tokenId, 0, resId2)).to.emit(token, 'ResourceAccepted') + .withArgs(tokenId, resId2, resId); + + const activeIds = await token.getActiveResources(tokenId); + expect(await renderUtils.getResourcesById(token.address, tokenId, activeIds)).to.eql([metaURIDefault]); + // Overwrite should be gone + expect(await token.getResourceOverwrites(tokenId, pendingResources[0])).to.eql( + ethers.BigNumber.from(0), + ); + }); + + it('can overwrite non existing resource to token, it could have been deleted', async function () { + const resId = 1; + const tokenId = 1; + + await token.mint(owner.address, tokenId); + await addResources([resId]); + await token.addResourceToToken(tokenId, resId, ethers.utils.hexZeroPad('0x1', 8)); + await token.acceptResource(tokenId, 0, resId); + + const activeIds = await token.getActiveResources(tokenId); + expect(await renderUtils.getResourcesById(token.address, tokenId, activeIds)).to.eql([metaURIDefault]); + }); + }); + + describe('Rejecting resources', async function () { + it('can reject resource if owner', async function () { + const { tokenOwner, tokenId } = await mintSampleToken(); + const approved = tokenOwner; + + await checkRejectFromAddress(approved, tokenId); + }); + + it('can reject resource if approved for resources', async function () { + const { tokenId } = await mintSampleToken(); + const approved = addrs[1]; + + await token.approveForResources(approved.address, tokenId); + await checkRejectFromAddress(approved, tokenId); + }); + + it('can reject resource if approved for resources for all', async function () { + const { tokenId } = await mintSampleToken(); + const approved = addrs[2]; + + await token.setApprovalForAllForResources(approved.address, true); + await checkRejectFromAddress(approved, tokenId); + }); + + it('can reject all resources if owner', async function () { + const { tokenOwner, tokenId } = await mintSampleToken(); + const approved = tokenOwner; + + await checkRejectAllFromAddress(approved, tokenId); + }); + + it('can reject all resources if approved for resources', async function () { + const { tokenId } = await mintSampleToken(); + const approved = addrs[1]; + + await token.approveForResources(approved.address, tokenId); + await checkRejectAllFromAddress(approved, tokenId); + }); + + it('can reject all resources if approved for resources for all', async function () { + const { tokenId } = await mintSampleToken(); + const approved = addrs[2]; + + await token.setApprovalForAllForResources(approved.address, true); + await checkRejectAllFromAddress(approved, tokenId); + }); + + it('can reject resource and overwrites are cleared', async function () { + const resId = 1; + const resId2 = 2; + const tokenId = 1; + + await token.mint(owner.address, tokenId); + await addResources([resId, resId2]); + await token.addResourceToToken(tokenId, resId, 0); + await token.acceptResource(tokenId, 0, resId); + + // Will try to overwrite but we reject it + await token.addResourceToToken(tokenId, resId2, resId); + await token.rejectResource(tokenId, 0, resId2); + + expect(await token.getResourceOverwrites(tokenId, resId2)).to.eql(ethers.BigNumber.from(0)); + }); + + it('can reject all resources and overwrites are cleared', async function () { + const resId = 1; + const resId2 = 2; + const tokenId = 1; + + await token.mint(owner.address, tokenId); + await addResources([resId, resId2]); + await token.addResourceToToken(tokenId, resId, 0); + await token.acceptResource(tokenId, 0, resId); + + // Will try to overwrite but we reject all + await token.addResourceToToken(tokenId, resId2, resId); + await token.rejectAllResources(tokenId, 1); + + expect(await token.getResourceOverwrites(tokenId, resId2)).to.eql(ethers.BigNumber.from(0)); + }); + + it('can reject all pending resources at max capacity', async function () { + const tokenId = 1; + const resArr = []; + + for (let i = 1; i < 128; i++) { + resArr.push(i); + } + + await token.mint(owner.address, tokenId); + await addResources(resArr); + + for (let i = 1; i < 128; i++) { + await token.addResourceToToken(tokenId, i, 1); + } + await token.rejectAllResources(tokenId, 128); + + expect(await token.getResourceOverwrites(1, 2)).to.eql(ethers.BigNumber.from(0)); + }); + + it('cannot reject resource twice', async function () { + const resId = 1; + const tokenId = 1; + + await token.mint(owner.address, tokenId); + await addResources([resId]); + await token.addResourceToToken(tokenId, resId, 0); + await token.rejectResource(tokenId, 0, resId); + }); + + it('cannot reject resource nor reject all if not owner', async function () { + const resId = 1; + const tokenId = 1; + + await token.mint(owner.address, tokenId); + await addResources([resId]); + await token.addResourceToToken(tokenId, resId, 0); + + await expect(token.connect(addrs[1]).rejectResource(tokenId, 0, resId)).to.be.revertedWith( + 'MultiResource: not owner or approved', + ); + await expect(token.connect(addrs[1]).rejectAllResources(tokenId, 1)).to.be.revertedWith( + 'MultiResource: not owner or approved', + ); + }); + + it('cannot reject non existing resource', async function () { + const tokenId = 1; + + await token.mint(owner.address, tokenId); + await expect(token.rejectResource(tokenId, 0, 1)).to.be.revertedWith( + 'MultiResource: index out of bounds', + ); + }); + }); + + describe('Priorities', async function () { + it('can set and get priorities', async function () { + const tokenId = 1; + await addResourcesToToken(tokenId); + + expect(await token.getActiveResourcePriorities(tokenId)).to.be.eql([0, 0]); + await expect(token.setPriority(tokenId, [2, 1])) + .to.emit(token, 'ResourcePrioritySet') + .withArgs(tokenId); + expect(await token.getActiveResourcePriorities(tokenId)).to.be.eql([2, 1]); + }); + + it('cannot set priorities for non owned token', async function () { + const tokenId = 1; + await addResourcesToToken(tokenId); + await expect(token.connect(addrs[1]).setPriority(tokenId, [2, 1])).to.be.revertedWith( + 'MultiResource: not owner or approved', + ); + }); + + it('cannot set different number of priorities', async function () { + const tokenId = 1; + await addResourcesToToken(tokenId); + await expect(token.connect(addrs[1]).setPriority(tokenId, [1])).to.be.revertedWith( + 'MultiResource: Bad priority list length', + ); + await expect(token.connect(addrs[1]).setPriority(tokenId, [2, 1, 3])).to.be.revertedWith( + 'MultiResource: Bad priority list length', + ); + }); + + it('cannot set priorities for non existing token', async function () { + const tokenId = 1; + await expect(token.connect(addrs[1]).setPriority(tokenId, [])).to.be.revertedWith( + 'MultiResource: approved query for nonexistent token', + ); + }); + }); + + describe('Approval Cleaning', async function () { + it('cleans token and resources approvals on transfer', async function () { + const tokenId = 1; + const tokenOwner = addrs[1]; + const newOwner = addrs[2]; + const approved = addrs[3]; + await token.mint(tokenOwner.address, tokenId); + await token.connect(tokenOwner).approve(approved.address, tokenId); + await token.connect(tokenOwner).approveForResources(approved.address, tokenId); + + expect(await token.getApproved(tokenId)).to.eql(approved.address); + expect(await token.getApprovedForResources(tokenId)).to.eql(approved.address); + + await token.connect(tokenOwner).transfer(newOwner.address, tokenId); + + expect(await token.getApproved(tokenId)).to.eql(ethers.constants.AddressZero); + expect(await token.getApprovedForResources(tokenId)).to.eql(ethers.constants.AddressZero); + }); + + it('cleans token and resources approvals on burn', async function () { + const tokenId = 1; + const tokenOwner = addrs[1]; + const approved = addrs[3]; + await token.mint(tokenOwner.address, tokenId); + await token.connect(tokenOwner).approve(approved.address, tokenId); + await token.connect(tokenOwner).approveForResources(approved.address, tokenId); + + expect(await token.getApproved(tokenId)).to.eql(approved.address); + expect(await token.getApprovedForResources(tokenId)).to.eql(approved.address); + + await token.connect(tokenOwner).burn(tokenId); + + await expect(token.getApproved(tokenId)).to.be.revertedWith( + 'MultiResource: approved query for nonexistent token', + ); + await expect(token.getApprovedForResources(tokenId)).to.be.revertedWith( + 'MultiResource: approved query for nonexistent token', + ); + }); + }); + + async function mintSampleToken(): Promise<{ tokenOwner: SignerWithAddress; tokenId: number }> { + const tokenOwner = owner; + const tokenId = 1; + await token.mint(tokenOwner.address, tokenId); + + return { tokenOwner, tokenId }; + } + + async function addResources(ids: number[]): Promise { + ids.forEach(async (resId) => { + await token.addResourceEntry(resId, metaURIDefault); + }); + } + + async function addResourcesToToken(tokenId: number): Promise { + const resId = 1; + const resId2 = 2; + await token.mint(owner.address, tokenId); + await addResources([resId, resId2]); + await token.addResourceToToken(tokenId, resId, 0); + await token.addResourceToToken(tokenId, resId2, 0); + await token.acceptResource(tokenId, 0, resId); + await token.acceptResource(tokenId, 0, resId2); + } + + async function checkAcceptFromAddress( + accepter: SignerWithAddress, + tokenId: number, + ): Promise { + const resId = 1; + + await addResources([resId]); + await token.addResourceToToken(tokenId, resId, 0); + await expect(token.connect(accepter).acceptResource(tokenId, 0, resId)) + .to.emit(token, 'ResourceAccepted') + .withArgs(tokenId, resId, 0); + + expect(await token.getPendingResources(tokenId)).to.be.eql([]); + + const activeIds = await token.getActiveResources(tokenId); + expect(await renderUtils.getResourcesById(token.address, tokenId, activeIds)).to.eql([metaURIDefault]); + } + + async function checkRejectFromAddress( + rejecter: SignerWithAddress, + tokenId: number, + ): Promise { + const resId = 1; + + await addResources([resId]); + await token.addResourceToToken(tokenId, resId, 0); + + await expect(token.connect(rejecter).rejectResource(tokenId, 0, resId)).to.emit( + token, + 'ResourceRejected', + ); + + expect(await token.getPendingResources(tokenId)).to.be.eql([]); + expect(await token.getActiveResources(tokenId)).to.be.eql([]); + } + + async function checkRejectAllFromAddress( + rejecter: SignerWithAddress, + tokenId: number, + ): Promise { + const resId = 1; + const resId2 = 2; + + await addResources([resId, resId2]); + await token.addResourceToToken(tokenId, resId, 0); + await token.addResourceToToken(tokenId, resId2, 0); + + await expect(token.connect(rejecter).rejectAllResources(tokenId, 2)).to.emit( + token, + 'ResourceRejected', + ); + + expect(await token.getPendingResources(tokenId)).to.be.eql([]); + expect(await token.getActiveResources(tokenId)).to.be.eql([]); + } +}); diff --git a/assets/eip-5773/test/renderUtils.ts b/assets/eip-5773/test/renderUtils.ts new file mode 100644 index 00000000000000..77bf776dcb90b4 --- /dev/null +++ b/assets/eip-5773/test/renderUtils.ts @@ -0,0 +1,86 @@ +import { BigNumber } from 'ethers'; +import { ethers } from 'hardhat'; +import { expect } from 'chai'; +import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; +import { MultiResourceTokenMock, MultiResourceRenderUtils } from '../typechain-types'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; + +function bn(x: number): BigNumber { + return BigNumber.from(x); +} + +async function resourcesFixture() { + const multiresourceFactory = await ethers.getContractFactory('MultiResourceTokenMock'); + const renderUtilsFactory = await ethers.getContractFactory('MultiResourceRenderUtils'); + + const multiresource = await multiresourceFactory.deploy('Chunky', 'CHNK'); + await multiresource.deployed(); + + const renderUtils = await renderUtilsFactory.deploy(); + await renderUtils.deployed(); + + return { multiresource, renderUtils }; +} + +describe('Render Utils', async function () { + let owner: SignerWithAddress; + let multiresource: MultiResourceTokenMock; + let renderUtils: MultiResourceRenderUtils; + let tokenId: number; + + const resId = bn(1); + const resId2 = bn(2); + const resId3 = bn(3); + const resId4 = bn(4); + + before(async function () { + ({ multiresource, renderUtils } = await loadFixture(resourcesFixture)); + + const signers = await ethers.getSigners(); + owner = signers[0]; + + tokenId = 1; + await multiresource.mint(owner.address, tokenId); + await multiresource.addResourceEntry(resId, 'ipfs://res1.jpg'); + await multiresource.addResourceEntry(resId2, 'ipfs://res2.jpg'); + await multiresource.addResourceEntry(resId3, 'ipfs://res3.jpg'); + await multiresource.addResourceEntry(resId4, 'ipfs://res4.jpg'); + await multiresource.addResourceToToken(tokenId, resId, 0); + await multiresource.addResourceToToken(tokenId, resId2, 0); + await multiresource.addResourceToToken(tokenId, resId3, resId); + await multiresource.addResourceToToken(tokenId, resId4, 0); + + await multiresource.acceptResource(tokenId, 0, resId); + await multiresource.acceptResource(tokenId, 1, resId2); + await multiresource.setPriority(tokenId, [10, 5]); + }); + + describe('Render Utils MultiResource', async function () { + it('can get active resources', async function () { + expect(await renderUtils.getActiveResources(multiresource.address, tokenId)).to.eql([ + [resId, 10, 'ipfs://res1.jpg'], + [resId2, 5, 'ipfs://res2.jpg'], + ]); + }); + it('can get pending resources', async function () { + expect(await renderUtils.getPendingResources(multiresource.address, tokenId)).to.eql([ + [resId4, bn(0), bn(0), 'ipfs://res4.jpg'], + [resId3, bn(1), resId, 'ipfs://res3.jpg'], + ]); + }); + + it('can get top resource by priority', async function () { + expect(await renderUtils.getTopResourceMetaForToken(multiresource.address, tokenId)).to.eql( + 'ipfs://res2.jpg', + ); + }); + + it('cannot get top resource if token has no resources', async function () { + const otherTokenId = 2; + await multiresource.mint(owner.address, otherTokenId); + await expect( + renderUtils.getTopResourceMetaForToken(multiresource.address, otherTokenId), + ).to.be.revertedWith('Token has no resources'); + }); + }); +}); \ No newline at end of file From fabcd1b4d200982f1bde9c714375b1e80c4399f1 Mon Sep 17 00:00:00 2001 From: Jan Turk Date: Tue, 15 Nov 2022 14:41:47 +0100 Subject: [PATCH 12/21] Address the stylistic issues reported by the CI --- EIPS/eip-5773.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/EIPS/eip-5773.md b/EIPS/eip-5773.md index 269ed524e234fb..77f77211b435b3 100644 --- a/EIPS/eip-5773.md +++ b/EIPS/eip-5773.md @@ -13,7 +13,7 @@ requires: 165, 721 ## Abstract -The Multi-Resource NFT standard allows for the construction of a new simple, clearly defined structure for context-dependent output of multimedia information per single NFT. +The Multi-Resource NFT standard allows for the construction of a new primitive: context-dependent output of multimedia information per single NFT. The context-dependent output of multimedia information means that the resource in an appropriate format is displayed based on how the token is being accessed. I.e. if the token is being opened in an e-book reader, the PDF resource is displayed, if the token is opened in the marketplace, the PNG or the SVG resource is displayed and if the token is accessed from within a game, the 3D model resource is accessed. @@ -424,11 +424,11 @@ The optional properties of the metadata JSON MAY include the following fields, o Desinging the proposal, we considered the following questions: -**1. Why are EIP-712 permit-style signatures to manage approvals not used?** +1. **Why are EIP-712 permit-style signatures to manage approvals not used?** For consistency. This proposal extends ERC-721 which already uses 1 transaction for approving operations with tokens. It would be inconsistent to have this and also support signing messages for operations with resources. -**2. Why use indexes?** +2. **Why use indexes?** To reduce the gas consumption. If the resource ID was used to find which token to accept or reject, iteration over arrays would be required and the cost of the operation would depend on the size of the active or pending children arrays. With the index, the cost is fixed. A list of active and pending children arrays per token need to be maintained, since methods to get them are part of the proposed interface. @@ -436,15 +436,15 @@ To avoid race conditions in which the index of a resource changes, the expected Implementation that whould internally keep track of indices using mapping was attemped. The average cost of adding a resource to a token increased by over 25%, costs of accepting and rejecting resources also increased 4.6% and 7.1% respeticvely. We concluded that it is not necessary for this proposal and can be implemented as an extension for use cases willing to accept this cost. In the sample implementation provided, there are several hooks which make this possible. -**3. Why is a method to get all the resources not included?** +3. **Why is a method to get all the resources not included?** Getting all resources might not be an operation necessary for all implementers. Additionally, it can be added either as an extension, doable with hooks, or can be emulated using an indexer. -**4. Why is pagination not included?** +4. **Why is pagination not included?** Resource IDs use `uint64`, testing has confirmed that the limit of IDs you can read before reaching the gas limit is around 30.000. This is not expected to be a common use case so it is not a part of the interface. However, an implementer can create an extension for this use case if needed. -**5. How does this proposal differ from the other proposals trying to address a similar problem?** +5. **How does this proposal differ from the other proposals trying to address a similar problem?** After reviewing them, we concluded that each contains at least one of these limitations: @@ -500,4 +500,4 @@ Caution is advised when dealing with non-audited contracts. ## Copyright -Copyright and related rights waived via [CC0](../LICENSE.md). \ No newline at end of file +Copyright and related rights waived via [CC0](../LICENSE.md). From 67718ce8ef8f2f0410b2fdd6b675bd909409d331 Mon Sep 17 00:00:00 2001 From: Jan Turk Date: Tue, 15 Nov 2022 14:44:33 +0100 Subject: [PATCH 13/21] Fix a reference to EIP-712 --- EIPS/eip-5773.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/EIPS/eip-5773.md b/EIPS/eip-5773.md index 77f77211b435b3..b3676c71784891 100644 --- a/EIPS/eip-5773.md +++ b/EIPS/eip-5773.md @@ -424,9 +424,9 @@ The optional properties of the metadata JSON MAY include the following fields, o Desinging the proposal, we considered the following questions: -1. **Why are EIP-712 permit-style signatures to manage approvals not used?** +1. **Why are [EIP-712](./eip-712.md) permit-style signatures to manage approvals not used?** -For consistency. This proposal extends ERC-721 which already uses 1 transaction for approving operations with tokens. It would be inconsistent to have this and also support signing messages for operations with resources. +For consistency. This proposal extends EIP-721 which already uses 1 transaction for approving operations with tokens. It would be inconsistent to have this and also support signing messages for operations with resources. 2. **Why use indexes?** From a9543d88d2406b2a9252847cb8e6e54a2c8e2527 Mon Sep 17 00:00:00 2001 From: Jan Turk Date: Tue, 15 Nov 2022 15:14:46 +0100 Subject: [PATCH 14/21] Minor typo fixes --- EIPS/eip-5773.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/EIPS/eip-5773.md b/EIPS/eip-5773.md index b3676c71784891..a8acfc0c99556b 100644 --- a/EIPS/eip-5773.md +++ b/EIPS/eip-5773.md @@ -79,8 +79,8 @@ pragma solidity ^0.8.16; interface IMultiResource { /** - * @notice Used to notify listeners that a resource object is initialized at `resourceId`. - * @param resourceId ID of the resource that was initialized + * @notice Used to notify listeners that a resource object is initialised at `resourceId`. + * @param resourceId ID of the resource that was initialised */ event ResourceSet(uint64 resourceId); @@ -119,7 +119,7 @@ interface IMultiResource { event ResourceRejected(uint256 indexed tokenId, uint64 indexed resourceId); /** - * @notice Used to notify listeners that token's prioritiy array is reordered. + * @notice Used to notify listeners that token's priority array is reordered. * @param tokenId ID of the token that had the resource priority array updated */ event ResourcePrioritySet(uint256 indexed tokenId); @@ -193,7 +193,7 @@ interface IMultiResource { /** * @notice Rejects all resources from the pending array of a given token. - * @dev Effecitvely deletes the pending array. + * @dev Effectively deletes the pending array. * @dev Requirements: * * - The caller must own the token or be approved to manage the token's resources @@ -209,7 +209,7 @@ interface IMultiResource { * @notice Sets a new priority array for a given token. * @dev The priority array is a non-sequential list of `uint16`s, where the lowest value is considered highest * priority. - * @dev Value `0` of a priority is a special case equivalent to unitialized. + * @dev Value `0` of a priority is a special case equivalent to uninitialised. * @dev Requirements: * * - The caller must own the token or be approved to manage the token's resources @@ -217,8 +217,8 @@ interface IMultiResource { * - The length of `priorities` must be equal the length of the active resources array. * @dev Emits a {ResourcePrioritySet} event. * @param tokenId ID of the token to set the priorities for - * @param priorities An array of priorities of active resources. The succesion of items in the priorities array - * matches that of the succesion of items in the active array + * @param priorities An array of priorities of active resources. The succession of items in the priorities array + * matches that of the succession of items in the active array */ function setPriority(uint256 tokenId, uint16[] calldata priorities) external; @@ -249,7 +249,7 @@ interface IMultiResource { returns (uint64[] memory); /** - * @notice Used to retrieve the priorities of the active resoources of a given token. + * @notice Used to retrieve the priorities of the active resources of a given token. * @dev Resource priorities are a non-sequential array of uint16 values with an array size equal to active resource * priorites. * @param tokenId ID of the token for which to retrieve the priorities of the active resources @@ -335,7 +335,7 @@ interface IMultiResource { * @dev See {setApprovalForAllForResources}. * @param owner Address of the account that we are checking for whether it has granted the operator role * @param operator Address of the account that we are checking whether it has the operator role or not - * @return bool The boolean value indicating wehter the account we are checking has been granted the operator role + * @return bool The boolean value indicating whether the account we are checking has been granted the operator role */ function isApprovedForAllForResources(address owner, address operator) external @@ -395,7 +395,7 @@ The metadata, to which the metadata URI of the resource points, MAY contain a JS } ```` -While this is the suggested JSON schema for the resource metadata, it is not enforced and MAY be stuctured completely differently based on implementer's preference. +While this is the suggested JSON schema for the resource metadata, it is not enforced and MAY be structured completely differently based on implementer's preference. The optional properties of the metadata JSON MAY include the following fields, or it MAY incorporate any number of custom fields, but MAY also not be included in the schema at all: @@ -422,7 +422,7 @@ The optional properties of the metadata JSON MAY include the following fields, o ## Rationale -Desinging the proposal, we considered the following questions: +Designing the proposal, we considered the following questions: 1. **Why are [EIP-712](./eip-712.md) permit-style signatures to manage approvals not used?** @@ -434,7 +434,7 @@ To reduce the gas consumption. If the resource ID was used to find which token t To avoid race conditions in which the index of a resource changes, the expected resource ID is included in operations requiring resource index, to verify that the resource being accessed using the index is the expected resource. -Implementation that whould internally keep track of indices using mapping was attemped. The average cost of adding a resource to a token increased by over 25%, costs of accepting and rejecting resources also increased 4.6% and 7.1% respeticvely. We concluded that it is not necessary for this proposal and can be implemented as an extension for use cases willing to accept this cost. In the sample implementation provided, there are several hooks which make this possible. +Implementation that would internally keep track of indices using mapping was attempted. The average cost of adding a resource to a token increased by over 25%, costs of accepting and rejecting resources also increased 4.6% and 7.1% respectively. We concluded that it is not necessary for this proposal and can be implemented as an extension for use cases willing to accept this cost. In the sample implementation provided, there are several hooks which make this possible. 3. **Why is a method to get all the resources not included?** From 1eecc072cb843145968982cd76825612f239cf52 Mon Sep 17 00:00:00 2001 From: Steven Pineda Date: Tue, 15 Nov 2022 10:47:11 -0500 Subject: [PATCH 15/21] Updates `getResourceMetadata` description Co-authored-by: Sam Wilson <57262657+SamWilsn@users.noreply.github.com> --- EIPS/eip-5773.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5773.md b/EIPS/eip-5773.md index a8acfc0c99556b..e3f3beec171dfd 100644 --- a/EIPS/eip-5773.md +++ b/EIPS/eip-5773.md @@ -344,7 +344,7 @@ interface IMultiResource { } ```` -The metadata, to which the metadata URI of the resource points, MAY contain a JSON response with the following fields: +The `getResourceMetadata` function returns the resource's metadata URI. The metadata, to which the metadata URI of the resource points, MAY contain a JSON response with the following fields: ````json { From 4522c8d602de0d561eb958bce56aee1a7e041922 Mon Sep 17 00:00:00 2001 From: Jan Turk Date: Wed, 16 Nov 2022 22:00:52 +0100 Subject: [PATCH 16/21] Rename MultiResource -> MultiAsset and relicense The proposal was renamed to Context-Dependent Multi-Asset Tokens to better illustrate its function. Another example was added to represent the possible IoT usecase and the explanation on the naming decision was added to the rationale. The examples were relicensed to CC0, to conform to the requirements of EIP repository. --- EIPS/eip-5773.md | 332 ++++----- assets/eip-5773/contracts/IMultiAsset.sol | 94 +++ assets/eip-5773/contracts/IMultiResource.sol | 94 --- ...iResourceToken.sol => MultiAssetToken.sol} | 346 +++++---- ...MultiResourceLib.sol => MultiAssetLib.sol} | 4 +- .../contracts/mocks/ERC721ReceiverMock.sol | 2 +- ...eTokenMock.sol => MultiAssetTokenMock.sol} | 18 +- .../contracts/mocks/NonReceiverMock.sol | 2 +- .../contracts/utils/MultiAssetRenderUtils.sol | 152 ++++ .../utils/MultiResourceRenderUtils.sol | 152 ---- assets/eip-5773/hardhat.config.ts | 28 +- assets/eip-5773/package.json | 3 +- assets/eip-5773/test/multiasset.ts | 673 +++++++++++++++++ assets/eip-5773/test/multiresource.ts | 677 ------------------ assets/eip-5773/test/renderUtils.ts | 68 +- 15 files changed, 1307 insertions(+), 1338 deletions(-) create mode 100644 assets/eip-5773/contracts/IMultiAsset.sol delete mode 100644 assets/eip-5773/contracts/IMultiResource.sol rename assets/eip-5773/contracts/{MultiResourceToken.sol => MultiAssetToken.sol} (54%) rename assets/eip-5773/contracts/library/{MultiResourceLib.sol => MultiAssetLib.sol} (92%) rename assets/eip-5773/contracts/mocks/{MultiResourceTokenMock.sol => MultiAssetTokenMock.sol} (70%) create mode 100644 assets/eip-5773/contracts/utils/MultiAssetRenderUtils.sol delete mode 100644 assets/eip-5773/contracts/utils/MultiResourceRenderUtils.sol create mode 100644 assets/eip-5773/test/multiasset.ts delete mode 100644 assets/eip-5773/test/multiresource.ts diff --git a/EIPS/eip-5773.md b/EIPS/eip-5773.md index e3f3beec171dfd..294cdbbbb37b55 100644 --- a/EIPS/eip-5773.md +++ b/EIPS/eip-5773.md @@ -1,7 +1,7 @@ --- eip: 5773 -title: Multi-Resource context-dependent tokens -description: An interface for Multi-Resource tokens with context dependent resource type output controlled by owner's preference. +title: Context-Dependent Multi-Asset Tokens +description: An interface for Multi-Asset tokens with context dependent asset type output controlled by owner's preference. author: Bruno Škvorc (@Swader), Cicada (@CicadaNCR), Steven Pineda (@steven2308), Stevan Bogosavljevic (@stevyhacker), Jan Turk (@ThunderDeliverer) discussions-to: https://ethereum-magicians.org/t/multiresource-tokens/11326 status: Draft @@ -13,15 +13,15 @@ requires: 165, 721 ## Abstract -The Multi-Resource NFT standard allows for the construction of a new primitive: context-dependent output of multimedia information per single NFT. +The Multi-Asset NFT standard allows for the construction of a new primitive: context-dependent output of information per single NFT. -The context-dependent output of multimedia information means that the resource in an appropriate format is displayed based on how the token is being accessed. I.e. if the token is being opened in an e-book reader, the PDF resource is displayed, if the token is opened in the marketplace, the PNG or the SVG resource is displayed and if the token is accessed from within a game, the 3D model resource is accessed. +The context-dependent output of information means that the asset in an appropriate format is displayed based on how the token is being accessed. I.e. if the token is being opened in an e-book reader, the PDF asset is displayed, if the token is opened in the marketplace, the PNG or the SVG asset is displayed, if the token is accessed from within a game, the 3D model asset is accessed and if the token is accessed by the (Internet of Things) IoT hub, the asset providing the neseccary addressing and specification information is accessed. -An NFT can have multiple resources (outputs), which can be any kind of file to be served to the consumer, 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 the owner of the token and the issuer of the token. +An NFT can have multiple assets (outputs), which can be any kind of file to be served to the consumer, and orders them by priority. They do not have to match in mimetype or tokenURI, nor do they depend on one another. Assets 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 the owner of the token and the issuer of the token. ## Motivation -With NFTs being a widespread form of tokens in the Ethereum ecosystem and being used for a variety of use cases, it is time to standardize additional utility for them. Having multiple resources associated with a single NFT allows for greater utility, usability and forward compatibility. +With NFTs being a widespread form of tokens in the Ethereum ecosystem and being used for a variety of use cases, it is time to standardize additional utility for them. Having multiple assets associated with a single NFT allows for greater utility, usability and forward compatibility. In the four years since [EIP-721](./eip-721.md) was published, the need for additional functionality has resulted in countless extensions. This EIP improves upon EIP-721 in the following areas: @@ -36,9 +36,9 @@ At the time of writing this proposal, the metaverse is still a fledgling, not fu Cross-metaverse compatibility could also be referred to as cross-engine compatibility. An example of this is where a cosmetic item for game A is not available in game B because the frameworks are incompatible. -Such NFT can be given further utility by means of new additional resources: more games, more cosmetic items, appended to the same NFT. Thus, a game cosmetic item as an NFT becomes an ever-evolving NFT of infinite utility. +Such NFT can be given further utility by means of new additional assets: more games, more cosmetic items, appended to the same NFT. Thus, a game cosmetic item as an NFT becomes an ever-evolving NFT of infinite utility. -The following is a more concrete example. One resource is a cosmetic item for game A, a file containing the cosmetic assets. Another is a cosmetic asset file for game B. A third is a generic resource intended to be shown in catalogs, marketplaces, portfolio trackers, or other generalized NFT viewers, containing a representation, stylized thumbnail, and animated demo/trailer of the cosmetic item. +The following is a more concrete example. One asset is a cosmetic item for game A, a file containing the cosmetic assets. Another is a cosmetic asset file for game B. A third is a generic asset intended to be shown in catalogs, marketplaces, portfolio trackers, or other generalized NFT viewers, containing a representation, stylized thumbnail, and animated demo/trailer of the cosmetic item. This EIP adds a layer of abstraction, allowing game developers to directly pull asset data from a user's NFTs instead of hard-coding it. @@ -50,159 +50,161 @@ An NFT of an eBook can be represented as a PDF, MP3, or some other format, depen 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 combination on Sia, another of the same combination 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 less likely. +By adding the same metadata file as different assets, e.g., one asset of a metadata and its linked image on Arweave, one asset of this same combination on Sia, another of the same combination on IPFS, etc., the resilience of the metadata and its referenced information increases exponentially as the chances of all the protocols going down at once become 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 replaces username/password logins with reading an account's NFT 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](#cross-metaverse-compatibility) compatibility above). +When the server goes down or the game shuts down, the player ends up with nothing (loss of experience) or something unrelated (assets or accessories unrelated to the game experience, spamming the wallet, incompatible with other “verses” - see [cross-metaverse](#cross-metaverse-compatibility) 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. +With Multi-Asset NFTs, a minter or another pre-approved entity is allowed to suggest a new asset to the NFT owner who can then accept it or reject it. The asset can even target an existing asset which is to be replaced. -Replacing a resource could, to some extent, be similar to replacing an EIP-721 token's URI. When a resource is replaced a clear line of traceability remains; the old resource is still reachable and verifiable. Overwriting a resource's metadata URI obscures this lineage. It also gives more trust to the token owner if the issuer cannot replace the resource of the NFT at will. The propose-accept resource replacement mechanic of this proposal provides this assurance. +Replacing an asset could, to some extent, be similar to replacing an EIP-721 token's URI. When an asset is replaced a clear line of traceability remains; the old asset is still reachable and verifiable. Overwriting a asset's metadata URI obscures this lineage. It also gives more trust to the token owner if the issuer cannot replace the asset of the NFT at will. The propose-accept asset replacement mechanic of this proposal provides this assurance. -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. +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 asset being added to the NFT, and once accepted, this new asset 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 the 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. +As a concrete example, think of Pokemon™️ evolving - once enough experience has been attained, a trainer can choose to evolve their monster. With Multi-Asset NFTs, it is not necessary to have centralized control over metadata to replace it, nor is it necessary to airdrop another NFT into the user's wallet - instead, a new Raichu asset is minted onto Pikachu, and if accepted, the Pikachu asset is gone, replaced by Raichu, which now has its own attributes, values, etc. + +Alternative example of this, could be version control of an IoT device's firmware. An asset could represent its current firmware and once an update becomes available, the current asset could be replaced with the one containing the updated firmware. ## 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. -````solidity -/// @title ERC-5773 Multi-Resource context-dependent tokens +```solidity +/// @title ERC-5773 Multi-Asset context-dependent tokens /// @dev See https://eips.ethereum.org/EIPS/eip-5773 -/// Note: the ERC-165 identifier for this interface is 0xb0ecc5ae. +/// @dev Note: the ERC-165 identifier for this interface is 0xfa73a1e2. pragma solidity ^0.8.16; -interface IMultiResource { +interface IMultiAsset { /** - * @notice Used to notify listeners that a resource object is initialised at `resourceId`. - * @param resourceId ID of the resource that was initialised + * @notice Used to notify listeners that a asset object is initialised at `assetId`. + * @param assetId ID of the asset that was initialised */ - event ResourceSet(uint64 resourceId); + event AssetSet(uint64 assetId); /** - * @notice Used to notify listeners that a resource object at `resourceId` is added to token's pending resource + * @notice Used to notify listeners that a asset object at `assetId` is added to token's pending asset * array. - * @param tokenId ID of the token that received a new pending resource - * @param resourceId ID of the resource that has been added to the token's pending resources array - * @param overwritesId ID of the resource that would be overwritten + * @param tokenId ID of the token that received a new pending asset + * @param assetId ID of the asset that has been added to the token's pending assets array + * @param overwritesId ID of the asset that would be overwritten */ - event ResourceAddedToToken( + event AssetAddedToToken( uint256 indexed tokenId, - uint64 indexed resourceId, + uint64 indexed assetId, uint64 indexed overwritesId ); /** - * @notice Used to notify listeners that a resource object at `resourceId` is accepted by the token and migrated - * from token's pending resources array to active resources array of the token. - * @param tokenId ID of the token that had a new resource accepted - * @param resourceId ID of the resource that was accepted - * @param overwritesId ID of the resource that would be overwritten + * @notice Used to notify listeners that a asset object at `assetId` is accepted by the token and migrated + * from token's pending assets array to active assets array of the token. + * @param tokenId ID of the token that had a new asset accepted + * @param assetId ID of the asset that was accepted + * @param overwritesId ID of the asset that would be overwritten */ - event ResourceAccepted( + event AssetAccepted( uint256 indexed tokenId, - uint64 indexed resourceId, + uint64 indexed assetId, uint64 indexed overwritesId ); /** - * @notice Used to notify listeners that a resource object at `resourceId` is rejected from token and is dropped - * from the pending resources array of the token. - * @param tokenId ID of the token that had a resource rejected - * @param resourceId ID of the resource that was rejected + * @notice Used to notify listeners that a asset object at `assetId` is rejected from token and is dropped + * from the pending assets array of the token. + * @param tokenId ID of the token that had a asset rejected + * @param assetId ID of the asset that was rejected */ - event ResourceRejected(uint256 indexed tokenId, uint64 indexed resourceId); + event AssetRejected(uint256 indexed tokenId, uint64 indexed assetId); /** * @notice Used to notify listeners that token's priority array is reordered. - * @param tokenId ID of the token that had the resource priority array updated + * @param tokenId ID of the token that had the asset priority array updated */ - event ResourcePrioritySet(uint256 indexed tokenId); + event AssetPrioritySet(uint256 indexed tokenId); /** - * @notice Used to notify listeners that owner has granted an approval to the user to manage the resources of a + * @notice Used to notify listeners that owner has granted an approval to the user to manage the assets of a * given token. * @dev Approvals must be cleared on transfer - * @param owner Address of the account that has granted the approval for all token's resources - * @param approved Address of the account that has been granted approval to manage the token's resources + * @param owner Address of the account that has granted the approval for all token's assets + * @param approved Address of the account that has been granted approval to manage the token's assets * @param tokenId ID of the token on which the approval was granted */ - event ApprovalForResources( + event ApprovalForAssets( address indexed owner, address indexed approved, uint256 indexed tokenId ); /** - * @notice Used to notify listeners that owner has granted approval to the user to manage resources of all of their + * @notice Used to notify listeners that owner has granted approval to the user to manage assets of all of their * tokens. - * @param owner Address of the account that has granted the approval for all resources on all of their tokens - * @param operator Address of the account that has been granted the approval to manage the token's resources on all of the + * @param owner Address of the account that has granted the approval for all assets on all of their tokens + * @param operator Address of the account that has been granted the approval to manage the token's assets on all of the * tokens * @param approved Boolean value signifying whether the permission has been granted (`true`) or revoked (`false`) */ - event ApprovalForAllForResources( + event ApprovalForAllForAssets( address indexed owner, address indexed operator, bool approved ); /** - * @notice Accepts a resource at from the pending array of given token. - * @dev Migrates the resource from the token's pending resource array to the token's active resource array. - * @dev Active resources cannot be removed by anyone, but can be replaced by a new resource. + * @notice Accepts a asset at from the pending array of given token. + * @dev Migrates the asset from the token's pending asset array to the token's active asset array. + * @dev Active assets cannot be removed by anyone, but can be replaced by a new asset. * @dev Requirements: * - * - The caller must own the token or be approved to manage the token's resources + * - The caller must own the token or be approved to manage the token's assets * - `tokenId` must exist. - * - `index` must be in range of the length of the pending resource array. - * @dev Emits an {ResourceAccepted} event. - * @param tokenId ID of the token for which to accept the pending resource - * @param index Index of the resource in the pending array to accept - * @param resourceId expected to be in the index + * - `index` must be in range of the length of the pending asset array. + * @dev Emits an {AssetAccepted} event. + * @param tokenId ID of the token for which to accept the pending asset + * @param index Index of the asset in the pending array to accept + * @param assetId Id of the asset expected to be in the index */ - function acceptResource( + function acceptAsset( uint256 tokenId, uint256 index, - uint64 resourceId + uint64 assetId ) external; /** - * @notice Rejects a resource from the pending array of given token. - * @dev Removes the resource from the token's pending resource array. + * @notice Rejects a asset from the pending array of given token. + * @dev Removes the asset from the token's pending asset array. * @dev Requirements: * - * - The caller must own the token or be approved to manage the token's resources + * - The caller must own the token or be approved to manage the token's assets * - `tokenId` must exist. - * - `index` must be in range of the length of the pending resource array. - * @dev Emits a {ResourceRejected} event. - * @param tokenId ID of the token that the resource is being rejected from - * @param index Index of the resource in the pending array to be rejected - * @param resourceId expected to be in the index + * - `index` must be in range of the length of the pending asset array. + * @dev Emits a {AssetRejected} event. + * @param tokenId ID of the token that the asset is being rejected from + * @param index Index of the asset in the pending array to be rejected + * @param assetId Id of the asset expected to be in the index */ - function rejectResource( + function rejectAsset( uint256 tokenId, uint256 index, - uint64 resourceId + uint64 assetId ) external; /** - * @notice Rejects all resources from the pending array of a given token. + * @notice Rejects all assets from the pending array of a given token. * @dev Effectively deletes the pending array. * @dev Requirements: * - * - The caller must own the token or be approved to manage the token's resources + * - The caller must own the token or be approved to manage the token's assets * - `tokenId` must exist. - * @dev Emits a {ResourceRejected} event with resourceId = 0. + * @dev Emits a {AssetRejected} event with assetId = 0. * @param tokenId ID of the token of which to clear the pending array - * @param maxRejections to prevent from rejecting resources which arrive just before this operation. + * @param maxRejections to prevent from rejecting assets which arrive just before this operation. */ - function rejectAllResources(uint256 tokenId, uint256 maxRejections) + function rejectAllAssets(uint256 tokenId, uint256 maxRejections) external; /** @@ -212,194 +214,194 @@ interface IMultiResource { * @dev Value `0` of a priority is a special case equivalent to uninitialised. * @dev Requirements: * - * - The caller must own the token or be approved to manage the token's resources + * - The caller must own the token or be approved to manage the token's assets * - `tokenId` must exist. - * - The length of `priorities` must be equal the length of the active resources array. - * @dev Emits a {ResourcePrioritySet} event. + * - The length of `priorities` must be equal the length of the active assets array. + * @dev Emits a {AssetPrioritySet} event. * @param tokenId ID of the token to set the priorities for - * @param priorities An array of priorities of active resources. The succession of items in the priorities array + * @param priorities An array of priorities of active assets. The succession of items in the priorities array * matches that of the succession of items in the active array */ function setPriority(uint256 tokenId, uint16[] calldata priorities) external; /** - * @notice Used to retrieve IDs of the active resources of given token. - * @dev Resource data is stored by reference, in order to access the data corresponding to the ID, call - * `getResourceMetadata(tokenId, resourceId)`. + * @notice Used to retrieve IDs of the active assets of given token. + * @dev Asset data is stored by reference, in order to access the data corresponding to the ID, call + * `getAssetMetadata(tokenId, assetId)`. * @dev You can safely get 10k - * @param tokenId ID of the token to retrieve the IDs of the active resources - * @return uint64[] An array of active resource IDs of the given token + * @param tokenId ID of the token to retrieve the IDs of the active assets + * @return uint64[] An array of active asset IDs of the given token */ - function getActiveResources(uint256 tokenId) + function getActiveAssets(uint256 tokenId) external view returns (uint64[] memory); /** - * @notice Used to retrieve IDs of the pending resources of given token. - * @dev Resource data is stored by reference, in order to access the data corresponding to the ID, call - * `getResourceMetadata(tokenId, resourceId)`. - * @param tokenId ID of the token to retrieve the IDs of the pending resources - * @return uint64[] An array of pending resource IDs of the given token + * @notice Used to retrieve IDs of the pending assets of given token. + * @dev Asset data is stored by reference, in order to access the data corresponding to the ID, call + * `getAssetMetadata(tokenId, assetId)`. + * @param tokenId ID of the token to retrieve the IDs of the pending assets + * @return uint64[] An array of pending asset IDs of the given token */ - function getPendingResources(uint256 tokenId) + function getPendingAssets(uint256 tokenId) external view returns (uint64[] memory); /** - * @notice Used to retrieve the priorities of the active resources of a given token. - * @dev Resource priorities are a non-sequential array of uint16 values with an array size equal to active resource + * @notice Used to retrieve the priorities of the active assets of a given token. + * @dev Asset priorities are a non-sequential array of uint16 values with an array size equal to active asset * priorites. - * @param tokenId ID of the token for which to retrieve the priorities of the active resources - * @return uint16[] An array of priorities of the active resources of the given token + * @param tokenId ID of the token for which to retrieve the priorities of the active assets + * @return uint16[] An array of priorities of the active assets of the given token */ - function getActiveResourcePriorities(uint256 tokenId) + function getActiveAssetPriorities(uint256 tokenId) external view returns (uint16[] memory); /** - * @notice Used to retrieve the resource that will be overriden if a given resource from the token's pending array + * @notice Used to retrieve the asset that will be overriden if a given asset from the token's pending array * is accepted. - * @dev Resource data is stored by reference, in order to access the data corresponding to the ID, call - * `getResourceMetadata(tokenId, resourceId)`. + * @dev Asset data is stored by reference, in order to access the data corresponding to the ID, call + * `getAssetMetadata(tokenId, assetId)`. * @param tokenId ID of the token to check - * @param newResourceId ID of the pending resource which will be accepted - * @return uint64 ID of the resource which will be replaced + * @param newAssetId ID of the pending asset which will be accepted + * @return uint64 ID of the asset which will be replaced */ - function getResourceOverwrites(uint256 tokenId, uint64 newResourceId) + function getAssetOverwrites(uint256 tokenId, uint64 newAssetId) external view returns (uint64); /** - * @notice Used to fetch the resource metadata of the specified token's active resource with the given index. + * @notice Used to fetch the asset metadata of the specified token's active asset with the given index. * @dev Can be overriden to implement enumerate, fallback or other custom logic. - * @param tokenId ID of the token from which to retrieve the resource metadata - * @param resourceId Resource Id, must be in the active resources array - * @return string The metadata of the resource belonging to the specified index in the token's active resources + * @param tokenId ID of the token from which to retrieve the asset metadata + * @param assetId Asset Id, must be in the active assets array + * @return string The metadata of the asset belonging to the specified index in the token's active assets * array */ - function getResourceMetadata(uint256 tokenId, uint64 resourceId) + function getAssetMetadata(uint256 tokenId, uint64 assetId) external view returns (string memory); /** - * @notice Used to grant permission to the user to manage token's resources. + * @notice Used to grant permission to the user to manage token's assets. * @dev This differs from transfer approvals, as approvals are not cleared when the approved party accepts or - * rejects a resource, or sets resource priorities. This approval is cleared on token transfer. + * rejects a asset, or sets asset priorities. This approval is cleared on token transfer. * @dev Only a single account can be approved at a time, so approving the `0x0` address clears previous approvals. * @dev Requirements: * * - The caller must own the token or be an approved operator. * - `tokenId` must exist. - * @dev Emits an {ApprovalForResources} event. + * @dev Emits an {ApprovalForAssets} event. * @param to Address of the account to grant the approval to - * @param tokenId ID of the token for which the approval to manage the resources is granted + * @param tokenId ID of the token for which the approval to manage the assets is granted */ - function approveForResources(address to, uint256 tokenId) external; + function approveForAssets(address to, uint256 tokenId) external; /** - * @notice Used to retrieve the address of the account approved to manage resources of a given token. + * @notice Used to retrieve the address of the account approved to manage assets of a given token. * @dev Requirements: * * - `tokenId` must exist. * @param tokenId ID of the token for which to retrieve the approved address - * @return address Address of the account that is approved to manage the specified token's resources + * @return address Address of the account that is approved to manage the specified token's assets */ - function getApprovedForResources(uint256 tokenId) + function getApprovedForAssets(uint256 tokenId) external view returns (address); /** - * @notice Used to add or remove an operator of resources for the caller. - * @dev Operators can call {acceptResource}, {rejectResource}, {rejectAllResources} or {setPriority} for any token + * @notice Used to add or remove an operator of assets for the caller. + * @dev Operators can call {acceptAsset}, {rejectAsset}, {rejectAllAssets} or {setPriority} for any token * owned by the caller. * @dev Requirements: * * - The `operator` cannot be the caller. - * @dev Emits an {ApprovalForAllForResources} event. + * @dev Emits an {ApprovalForAllForAssets} event. * @param operator Address of the account to which the operator role is granted or revoked from * @param approved The boolean value indicating whether the operator role is being granted (`true`) or revoked * (`false`) */ - function setApprovalForAllForResources(address operator, bool approved) + function setApprovalForAllForAssets(address operator, bool approved) external; /** * @notice Used to check whether the address has been granted the operator role by a given address or not. - * @dev See {setApprovalForAllForResources}. + * @dev See {setApprovalForAllForAssets}. * @param owner Address of the account that we are checking for whether it has granted the operator role * @param operator Address of the account that we are checking whether it has the operator role or not * @return bool The boolean value indicating whether the account we are checking has been granted the operator role */ - function isApprovedForAllForResources(address owner, address operator) + function isApprovedForAllForAssets(address owner, address operator) external view returns (bool); } -```` +``` -The `getResourceMetadata` function returns the resource's metadata URI. The metadata, to which the metadata URI of the resource points, MAY contain a JSON response with the following fields: +The `getAssetMetadata` function returns the asset's metadata URI. The metadata, to which the metadata URI of the asset points, MAY contain a JSON response with the following fields: -````json +```json { "title": "Asset Metadata", "type": "object", "properties": { "name": { "type": "string", - "description": "Identifies the name of the asset associated with the resource" + "description": "Identifies the name of the asset associated with the asset" }, "description": { "type": "string", - "description": "Identifies the general notes, abstracts, or summaries about the contents of the resource" + "description": "Identifies the general notes, abstracts, or summaries about the contents of the asset" }, "type": { "type": "string", - "description": "Identifies the definition of the type of content of the resource" + "description": "Identifies the definition of the type of content of the asset" }, "locale": { "type": "string", - "description": "Identifies metadata locale in ISO 639-1 format for translations and localisation of the resource" + "description": "Identifies metadata locale in ISO 639-1 format for translations and localisation of the asset" }, "license": { "type": "string", - "description": "Identifies the license attached to the resource" + "description": "Identifies the license attached to the asset" }, "licenseUri": { "type": "string", - "description": "Identifies the URI to the license statement of the license attached to the resource" + "description": "Identifies the URI to the license statement of the license attached to the asset" }, "mediaUri": { "type": "string", - "description": "Identifies the URI of the main media file associated with the resource" + "description": "Identifies the URI of the main media file associated with the asset" }, "thumbnailUri": { "type": "string", - "description": "Identifies the URI of the thumbnail image associated with the resource to be used for preview of the resource in the wallets and client applications (the recommended maximum size is 350x350 px)" + "description": "Identifies the URI of the thumbnail image associated with the asset to be used for preview of the asset in the wallets and client applications (the recommended maximum size is 350x350 px)" }, "externalUri": { "type": "string", - "description": "Identifies the URI to the additional information about the subject or content of the resource" + "description": "Identifies the URI to the additional information about the subject or content of the asset" }, "properties": { "type": "object", - "properties": "Identifies the optional custom attributes of the resource" + "properties": "Identifies the optional custom attributes of the asset" } } } -```` +``` -While this is the suggested JSON schema for the resource metadata, it is not enforced and MAY be structured completely differently based on implementer's preference. +While this is the suggested JSON schema for the asset metadata, it is not enforced and MAY be structured completely differently based on implementer's preference. The optional properties of the metadata JSON MAY include the following fields, or it MAY incorporate any number of custom fields, but MAY also not be included in the schema at all: -````json +```json "properties": { "rarity": { "type": "string", @@ -418,67 +420,73 @@ The optional properties of the metadata JSON MAY include the following fields, o "value": ["music", "2020", "best"] } } -```` +``` ## Rationale Designing the proposal, we considered the following questions: -1. **Why are [EIP-712](./eip-712.md) permit-style signatures to manage approvals not used?** +1. **Should we use Asset or Resource when referring to the structure that comprises the token?** + +The original idea was to call the proposal Multi-Resource, but while this denoted the broadness of the structures that could be held by a single token, the term *asset* represents it better. + +An asset is defined as something that is owned by a person, company, or organization, such as money, property, or land. This is the best representation of what an asset of this proposal can be. An asset in this proposal can be a multimedia file, technical information, a land deed, or anything that the implementer has decided to be an asset of the token they are implementing. + +2. **Why are [EIP-712](./eip-712.md) permit-style signatures to manage approvals not used?** -For consistency. This proposal extends EIP-721 which already uses 1 transaction for approving operations with tokens. It would be inconsistent to have this and also support signing messages for operations with resources. +For consistency. This proposal extends EIP-721 which already uses 1 transaction for approving operations with tokens. It would be inconsistent to have this and also support signing messages for operations with assets. 2. **Why use indexes?** -To reduce the gas consumption. If the resource ID was used to find which token to accept or reject, iteration over arrays would be required and the cost of the operation would depend on the size of the active or pending children arrays. With the index, the cost is fixed. A list of active and pending children arrays per token need to be maintained, since methods to get them are part of the proposed interface. +To reduce the gas consumption. If the asset ID was used to find which token to accept or reject, iteration over arrays would be required and the cost of the operation would depend on the size of the active or pending children arrays. With the index, the cost is fixed. A list of active and pending children arrays per token need to be maintained, since methods to get them are part of the proposed interface. -To avoid race conditions in which the index of a resource changes, the expected resource ID is included in operations requiring resource index, to verify that the resource being accessed using the index is the expected resource. +To avoid race conditions in which the index of a asset changes, the expected asset ID is included in operations requiring asset index, to verify that the asset being accessed using the index is the expected asset. -Implementation that would internally keep track of indices using mapping was attempted. The average cost of adding a resource to a token increased by over 25%, costs of accepting and rejecting resources also increased 4.6% and 7.1% respectively. We concluded that it is not necessary for this proposal and can be implemented as an extension for use cases willing to accept this cost. In the sample implementation provided, there are several hooks which make this possible. +Implementation that would internally keep track of indices using mapping was attempted. The average cost of adding a asset to a token increased by over 25%, costs of accepting and rejecting assets also increased 4.6% and 7.1% respectively. We concluded that it is not necessary for this proposal and can be implemented as an extension for use cases willing to accept this cost. In the sample implementation provided, there are several hooks which make this possible. -3. **Why is a method to get all the resources not included?** +3. **Why is a method to get all the assets not included?** -Getting all resources might not be an operation necessary for all implementers. Additionally, it can be added either as an extension, doable with hooks, or can be emulated using an indexer. +Getting all assets might not be an operation necessary for all implementers. Additionally, it can be added either as an extension, doable with hooks, or can be emulated using an indexer. 4. **Why is pagination not included?** -Resource IDs use `uint64`, testing has confirmed that the limit of IDs you can read before reaching the gas limit is around 30.000. This is not expected to be a common use case so it is not a part of the interface. However, an implementer can create an extension for this use case if needed. +Asset IDs use `uint64`, testing has confirmed that the limit of IDs you can read before reaching the gas limit is around 30.000. This is not expected to be a common use case so it is not a part of the interface. However, an implementer can create an extension for this use case if needed. 5. **How does this proposal differ from the other proposals trying to address a similar problem?** After reviewing them, we concluded that each contains at least one of these limitations: -- Using a single URI which is replaced as new resources are needed, this introduces a trust issue for the token owner. -- Focusing only on a type of resource, while this proposal is resource type agnostic. +- Using a single URI which is replaced as new assets are needed, this introduces a trust issue for the token owner. +- Focusing only on a type of asset, while this proposal is asset type agnostic. - Having a different token for each new use case, this means that the token is not forward-compatible. -### Multi-Resource Storage Schema +### Multi-Asset Storage Schema -Resources are stored within a token as an array of `uint64` identifiers. +Assets are stored within 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 inner storage. A resource entry on the storage is stored via a `uint64` mapping to resource data. +In order to reduce redundant on-chain string storage, multi asset tokens store assets by reference via inner storage. A asset entry on the storage is stored via a `uint64` mapping to asset data. -A resource array is an array of these `uint64` resource ID references. +A asset array is an array of these `uint64` asset ID references. -Such a structure allows that, a generic resource can be added to the storage one time, and a reference to it can be added to the token contract as many times as we desire. 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 in a new token will only take 16 bytes of storage in the resource array per token for recurrent as well as `tokenId` dependent resources. +Such a structure allows that, a generic asset can be added to the storage one time, and a reference to it can be added to the token contract as many times as we desire. Implementers can then use string concatenation to procedurally generate a link to a content-addressed archive based on the base *SRC* in the asset and the *token ID*. Storing the asset in a new token will only take 16 bytes of storage in the asset array per token for recurrent as well as `tokenId` dependent assets. -Structuring token's resources in such a way allows for URIs to be derived programmatically through concatenation, especially when they differ only by `tokenId`. +Structuring token's assets in such a way allows for URIs to be derived programmatically through concatenation, especially when they differ only by `tokenId`. -### Propose-Commit pattern for resource addition +### Propose-Commit pattern for asset addition -Adding resources to an existing token MUST be done in 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's owner. The *"Pending"* resources array SHOULD be limited to 128 slots to prevent spam and griefing. +Adding assets to an existing token MUST be done in the form of a propose-commit pattern to allow for limited mutability by a 3rd party. When adding a asset to a token, it is first placed in the *"Pending"* array, and MUST be migrated to the *"Active"* array by the token's owner. The *"Pending"* assets array SHOULD be limited to 128 slots to prevent spam and griefing. -### Resource management +### Asset 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 MUST also be included. +Several functions for asset management are included. In addition to permissioned migration from "Pending" to "Active", the owner of a token MAY also drop assets from both the active and the pending array -- an emergency function to clear all entries from the pending array MUST also be included. ## Backwards Compatibility -The MultiResource token standard has been made compatible with [EIP-721](./eip-721.md) in order to take advantage of the robust tooling available for implementations of EIP-721 and to ensure compatibility with existing EIP-721 infrastructure. +The MultiAsset token standard has been made compatible with [EIP-721](./eip-721.md) in order to take advantage of the robust tooling available for implementations of EIP-721 and to ensure compatibility with existing EIP-721 infrastructure. ## Test Cases -Tests are included in [`multiresource.ts`](../assets/eip-5773/test/multiresource.ts). +Tests are included in [`multiasset.ts`](../assets/eip-5773/test/multiasset.ts). To run them in terminal, you can use the following commands: @@ -490,11 +498,11 @@ npx hardhat test ## Reference Implementation -See [`MultiResourceToken.sol`](../assets/eip-5773/contracts/MultiResourceToken.sol). +See [`MultiAssetToken.sol`](../assets/eip-5773/contracts/MultiAssetToken.sol). ## Security Considerations -The same security considerations as with [EIP-721](./eip-721.md) apply: hidden logic may be present in any of the functions, including burn, add resource, accept resource, and more. +The same security considerations as with [EIP-721](./eip-721.md) apply: hidden logic may be present in any of the functions, including burn, add asset, accept asset, and more. Caution is advised when dealing with non-audited contracts. diff --git a/assets/eip-5773/contracts/IMultiAsset.sol b/assets/eip-5773/contracts/IMultiAsset.sol new file mode 100644 index 00000000000000..19457f9f18c969 --- /dev/null +++ b/assets/eip-5773/contracts/IMultiAsset.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: CC0 + +pragma solidity ^0.8.0; + +interface IMultiAsset { + event AssetSet(uint64 assetId); + + event AssetAddedToToken( + uint256 indexed tokenId, + uint64 indexed assetId, + uint64 indexed overwritesId + ); + + event AssetAccepted( + uint256 indexed tokenId, + uint64 indexed assetId, + uint64 indexed overwritesId + ); + + event AssetRejected(uint256 indexed tokenId, uint64 indexed assetId); + + event AssetPrioritySet(uint256 indexed tokenId); + + event ApprovalForAssets( + address indexed owner, + address indexed approved, + uint256 indexed tokenId + ); + + event ApprovalForAllForAssets( + address indexed owner, + address indexed operator, + bool approved + ); + + function acceptAsset( + uint256 tokenId, + uint256 index, + uint64 assetId + ) external; + + function rejectAsset( + uint256 tokenId, + uint256 index, + uint64 assetId + ) external; + + function rejectAllAssets(uint256 tokenId, uint256 maxRejections) + external; + + function setPriority(uint256 tokenId, uint16[] calldata priorities) + external; + + function getActiveAssets(uint256 tokenId) + external + view + returns (uint64[] memory); + + function getPendingAssets(uint256 tokenId) + external + view + returns (uint64[] memory); + + function getActiveAssetPriorities(uint256 tokenId) + external + view + returns (uint16[] memory); + + function getAssetOverwrites(uint256 tokenId, uint64 newAssetId) + external + view + returns (uint64); + + function getAssetMetadata(uint256 tokenId, uint64 assetId) + external + view + returns (string memory); + + // Approvals + function approveForAssets(address to, uint256 tokenId) external; + + function getApprovedForAssets(uint256 tokenId) + external + view + returns (address); + + function setApprovalForAllForAssets(address operator, bool approved) + external; + + function isApprovedForAllForAssets(address owner, address operator) + external + view + returns (bool); +} diff --git a/assets/eip-5773/contracts/IMultiResource.sol b/assets/eip-5773/contracts/IMultiResource.sol deleted file mode 100644 index b7d3cc3164600c..00000000000000 --- a/assets/eip-5773/contracts/IMultiResource.sol +++ /dev/null @@ -1,94 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -pragma solidity ^0.8.0; - -interface IMultiResource { - event ResourceSet(uint64 resourceId); - - event ResourceAddedToToken( - uint256 indexed tokenId, - uint64 indexed resourceId, - uint64 indexed overwritesId - ); - - event ResourceAccepted( - uint256 indexed tokenId, - uint64 indexed resourceId, - uint64 indexed overwritesId - ); - - event ResourceRejected(uint256 indexed tokenId, uint64 indexed resourceId); - - event ResourcePrioritySet(uint256 indexed tokenId); - - event ApprovalForResources( - address indexed owner, - address indexed approved, - uint256 indexed tokenId - ); - - event ApprovalForAllForResources( - address indexed owner, - address indexed operator, - bool approved - ); - - function acceptResource( - uint256 tokenId, - uint256 index, - uint64 resourceId - ) external; - - function rejectResource( - uint256 tokenId, - uint256 index, - uint64 resourceId - ) external; - - function rejectAllResources(uint256 tokenId, uint256 maxRejections) - external; - - function setPriority(uint256 tokenId, uint16[] calldata priorities) - external; - - function getActiveResources(uint256 tokenId) - external - view - returns (uint64[] memory); - - function getPendingResources(uint256 tokenId) - external - view - returns (uint64[] memory); - - function getActiveResourcePriorities(uint256 tokenId) - external - view - returns (uint16[] memory); - - function getResourceOverwrites(uint256 tokenId, uint64 newResourceId) - external - view - returns (uint64); - - function getResourceMetadata(uint256 tokenId, uint64 resourceId) - external - view - returns (string memory); - - // Approvals - function approveForResources(address to, uint256 tokenId) external; - - function getApprovedForResources(uint256 tokenId) - external - view - returns (address); - - function setApprovalForAllForResources(address operator, bool approved) - external; - - function isApprovedForAllForResources(address owner, address operator) - external - view - returns (bool); -} diff --git a/assets/eip-5773/contracts/MultiResourceToken.sol b/assets/eip-5773/contracts/MultiAssetToken.sol similarity index 54% rename from assets/eip-5773/contracts/MultiResourceToken.sol rename to assets/eip-5773/contracts/MultiAssetToken.sol index 8d6531da2d65a0..4fcc8031c1b128 100644 --- a/assets/eip-5773/contracts/MultiResourceToken.sol +++ b/assets/eip-5773/contracts/MultiAssetToken.sol @@ -1,19 +1,19 @@ -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: CC0 pragma solidity ^0.8.15; -import "./IMultiResource.sol"; -import "./library/MultiResourceLib.sol"; +import "./IMultiAsset.sol"; +import "./library/MultiAssetLib.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import "@openzeppelin/contracts/utils/Address.sol"; import "@openzeppelin/contracts/utils/Strings.sol"; import "@openzeppelin/contracts/utils/Context.sol"; -contract MultiResourceToken is Context, IERC721, IMultiResource { - using MultiResourceLib for uint256; - using MultiResourceLib for uint64[]; - using MultiResourceLib for uint128[]; +contract MultiAssetToken is Context, IERC721, IMultiAsset { + using MultiAssetLib for uint256; + using MultiAssetLib for uint64[]; + using MultiAssetLib for uint128[]; using Address for address; using Strings for uint256; @@ -35,30 +35,30 @@ contract MultiResourceToken is Context, IERC721, IMultiResource { // Mapping from owner to operator approvals mapping(address => mapping(address => bool)) private _operatorApprovals; - // Mapping from token ID to approved address for resources - mapping(uint256 => address) internal _tokenApprovalsForResources; + // Mapping from token ID to approved address for assets + mapping(uint256 => address) internal _tokenApprovalsForAssets; - // Mapping from owner to operator approvals for resources + // Mapping from owner to operator approvals for assets mapping(address => mapping(address => bool)) - internal _operatorApprovalsForResources; + internal _operatorApprovalsForAssets; - //mapping of uint64 Ids to resource object - mapping(uint64 => string) internal _resources; + //mapping of uint64 Ids to asset object + mapping(uint64 => string) internal _assets; - //mapping of tokenId to new resource, to resource to be replaced - mapping(uint256 => mapping(uint64 => uint64)) private _resourceOverwrites; + //mapping of tokenId to new asset, to asset to be replaced + mapping(uint256 => mapping(uint64 => uint64)) private _assetOverwrites; - //mapping of tokenId to all resources - mapping(uint256 => uint64[]) internal _activeResources; + //mapping of tokenId to all assets + mapping(uint256 => uint64[]) internal _activeAssets; - //mapping of tokenId to an array of resource priorities - mapping(uint256 => uint16[]) internal _activeResourcePriorities; + //mapping of tokenId to an array of asset priorities + mapping(uint256 => uint16[]) internal _activeAssetPriorities; - //Double mapping of tokenId to active resources - mapping(uint256 => mapping(uint64 => bool)) private _tokenResources; + //Double mapping of tokenId to active assets + mapping(uint256 => mapping(uint64 => bool)) private _tokenAssets; - //mapping of tokenId to all resources by priority - mapping(uint256 => uint64[]) internal _pendingResources; + //mapping of tokenId to all assets by priority + mapping(uint256 => uint64[]) internal _pendingAssets; constructor(string memory name_, string memory symbol_) { _name = name_; @@ -69,9 +69,9 @@ contract MultiResourceToken is Context, IERC721, IMultiResource { // ERC-721 COMPLIANCE //////////////////////////////////////// - function supportsInterface(bytes4 interfaceId) public pure returns (bool) { + function supportsInterface(bytes4 interfaceId) public view returns (bool) { return - interfaceId == type(IMultiResource).interfaceId || + interfaceId == type(IMultiAsset).interfaceId || interfaceId == type(IERC721).interfaceId || interfaceId == type(IERC165).interfaceId; } @@ -115,24 +115,24 @@ contract MultiResourceToken is Context, IERC721, IMultiResource { function approve(address to, uint256 tokenId) public virtual { address owner = ownerOf(tokenId); - require(to != owner, "MultiResource: approval to current owner"); + require(to != owner, "MultiAsset: approval to current owner"); require( _msgSender() == owner || isApprovedForAll(owner, _msgSender()), - "MultiResource: approve caller is not owner nor approved for all" + "MultiAsset: approve caller is not owner nor approved for all" ); _approve(to, tokenId); } - function approveForResources(address to, uint256 tokenId) external virtual { + function approveForAssets(address to, uint256 tokenId) external virtual { address owner = ownerOf(tokenId); - require(to != owner, "MultiResource: approval to current owner"); + require(to != owner, "MultiAsset: approval to current owner"); require( _msgSender() == owner || - isApprovedForAllForResources(owner, _msgSender()), - "MultiResource: approve caller is not owner nor approved for all" + isApprovedForAllForAssets(owner, _msgSender()), + "MultiAsset: approve caller is not owner nor approved for all" ); - _approveForResources(to, tokenId); + _approveForAssets(to, tokenId); } function getApproved(uint256 tokenId) @@ -144,13 +144,13 @@ contract MultiResourceToken is Context, IERC721, IMultiResource { { require( _exists(tokenId), - "MultiResource: approved query for nonexistent token" + "MultiAsset: approved query for nonexistent token" ); return _tokenApprovals[tokenId]; } - function getApprovedForResources(uint256 tokenId) + function getApprovedForAssets(uint256 tokenId) public view virtual @@ -158,9 +158,9 @@ contract MultiResourceToken is Context, IERC721, IMultiResource { { require( _exists(tokenId), - "MultiResource: approved query for nonexistent token" + "MultiAsset: approved query for nonexistent token" ); - return _tokenApprovalsForResources[tokenId]; + return _tokenApprovalsForAssets[tokenId]; } function setApprovalForAll(address operator, bool approved) @@ -181,21 +181,21 @@ contract MultiResourceToken is Context, IERC721, IMultiResource { return _operatorApprovals[owner][operator]; } - function setApprovalForAllForResources(address operator, bool approved) + function setApprovalForAllForAssets(address operator, bool approved) public virtual override { - _setApprovalForAllForResources(_msgSender(), operator, approved); + _setApprovalForAllForAssets(_msgSender(), operator, approved); } - function isApprovedForAllForResources(address owner, address operator) + function isApprovedForAllForAssets(address owner, address operator) public view virtual returns (bool) { - return _operatorApprovalsForResources[owner][operator]; + return _operatorApprovalsForAssets[owner][operator]; } function transferFrom( @@ -206,7 +206,7 @@ contract MultiResourceToken is Context, IERC721, IMultiResource { //solhint-disable-next-line max-line-length require( _isApprovedOrOwner(_msgSender(), tokenId), - "MultiResource: transfer caller is not owner nor approved" + "MultiAsset: transfer caller is not owner nor approved" ); _transfer(from, to, tokenId); @@ -228,7 +228,7 @@ contract MultiResourceToken is Context, IERC721, IMultiResource { ) public virtual override { require( _isApprovedOrOwner(_msgSender(), tokenId), - "MultiResource: transfer caller is not owner nor approved" + "MultiAsset: transfer caller is not owner nor approved" ); _safeTransfer(from, to, tokenId, data); } @@ -242,7 +242,7 @@ contract MultiResourceToken is Context, IERC721, IMultiResource { _transfer(from, to, tokenId); require( _checkOnERC721Received(from, to, tokenId, data), - "MultiResource: transfer to non ERC721 Receiver implementer" + "MultiAsset: transfer to non ERC721 Receiver implementer" ); } @@ -258,7 +258,7 @@ contract MultiResourceToken is Context, IERC721, IMultiResource { { require( _exists(tokenId), - "MultiResource: approved query for nonexistent token" + "MultiAsset: approved query for nonexistent token" ); address owner = ownerOf(tokenId); return (spender == owner || @@ -266,7 +266,7 @@ contract MultiResourceToken is Context, IERC721, IMultiResource { getApproved(tokenId) == spender); } - function _isApprovedForResourcesOrOwner(address user, uint256 tokenId) + function _isApprovedForAssetsOrOwner(address user, uint256 tokenId) internal view virtual @@ -274,12 +274,12 @@ contract MultiResourceToken is Context, IERC721, IMultiResource { { require( _exists(tokenId), - "MultiResource: approved query for nonexistent token" + "MultiAsset: approved query for nonexistent token" ); address owner = ownerOf(tokenId); return (user == owner || - isApprovedForAllForResources(owner, user) || - getApprovedForResources(tokenId) == user); + isApprovedForAllForAssets(owner, user) || + getApprovedForAssets(tokenId) == user); } function _safeMint(address to, uint256 tokenId) internal virtual { @@ -294,13 +294,13 @@ contract MultiResourceToken is Context, IERC721, IMultiResource { _mint(to, tokenId); require( _checkOnERC721Received(address(0), to, tokenId, data), - "MultiResource: transfer to non ERC721 Receiver implementer" + "MultiAsset: transfer to non ERC721 Receiver implementer" ); } function _mint(address to, uint256 tokenId) internal virtual { - require(to != address(0), "MultiResource: mint to the zero address"); - require(!_exists(tokenId), "MultiResource: token already minted"); + require(to != address(0), "MultiAsset: mint to the zero address"); + require(!_exists(tokenId), "MultiAsset: token already minted"); _beforeTokenTransfer(address(0), to, tokenId); @@ -314,16 +314,16 @@ contract MultiResourceToken is Context, IERC721, IMultiResource { function _burn(uint256 tokenId) internal virtual { // WARNING: If you intend to allow the reminting of a burned token, you - // might want to clean the resources for the token, that is: - // _pendingResources, _activeResources, _resourceOverwrites - // _activeResourcePriorities and _tokenResources. + // might want to clean the assets for the token, that is: + // _pendingAssets, _activeAssets, _assetOverwrites + // _activeAssetPriorities and _tokenAssets. address owner = ownerOf(tokenId); _beforeTokenTransfer(owner, address(0), tokenId); // Clear approvals _approve(address(0), tokenId); - _approveForResources(address(0), tokenId); + _approveForAssets(address(0), tokenId); _balances[owner] -= 1; delete _owners[tokenId]; @@ -340,18 +340,15 @@ contract MultiResourceToken is Context, IERC721, IMultiResource { ) internal virtual { require( ownerOf(tokenId) == from, - "MultiResource: transfer from incorrect owner" - ); - require( - to != address(0), - "MultiResource: transfer to the zero address" + "MultiAsset: transfer from incorrect owner" ); + require(to != address(0), "MultiAsset: transfer to the zero address"); _beforeTokenTransfer(from, to, tokenId); // Clear approvals from the previous owner _approve(address(0), tokenId); - _approveForResources(address(0), tokenId); + _approveForAssets(address(0), tokenId); _balances[from] -= 1; _balances[to] += 1; @@ -367,12 +364,9 @@ contract MultiResourceToken is Context, IERC721, IMultiResource { emit Approval(ownerOf(tokenId), to, tokenId); } - function _approveForResources(address to, uint256 tokenId) - internal - virtual - { - _tokenApprovalsForResources[tokenId] = to; - emit ApprovalForResources(ownerOf(tokenId), to, tokenId); + function _approveForAssets(address to, uint256 tokenId) internal virtual { + _tokenApprovalsForAssets[tokenId] = to; + emit ApprovalForAssets(ownerOf(tokenId), to, tokenId); } function _setApprovalForAll( @@ -380,19 +374,19 @@ contract MultiResourceToken is Context, IERC721, IMultiResource { address operator, bool approved ) internal virtual { - require(owner != operator, "MultiResource: approve to caller"); + require(owner != operator, "MultiAsset: approve to caller"); _operatorApprovals[owner][operator] = approved; emit ApprovalForAll(owner, operator, approved); } - function _setApprovalForAllForResources( + function _setApprovalForAllForAssets( address owner, address operator, bool approved ) internal virtual { - require(owner != operator, "MultiResource: approve to caller"); - _operatorApprovalsForResources[owner][operator] = approved; - emit ApprovalForAllForResources(owner, operator, approved); + require(owner != operator, "MultiAsset: approve to caller"); + _operatorApprovalsForAssets[owner][operator] = approved; + emit ApprovalForAllForAssets(owner, operator, approved); } function _checkOnERC721Received( @@ -410,13 +404,11 @@ contract MultiResourceToken is Context, IERC721, IMultiResource { data ) returns (bytes4 retval) { - return - retval == - IERC721Receiver.onERC721Received.selector; + return retval == IERC721Receiver.onERC721Received.selector; } catch (bytes memory reason) { if (reason.length == 0) { revert( - "MultiResource: transfer to non ERC721 Receiver implementer" + "MultiAsset: transfer to non ERC721 Receiver implementer" ); } else { /// @solidity memory-safe-assembly @@ -443,94 +435,97 @@ contract MultiResourceToken is Context, IERC721, IMultiResource { ) internal virtual {} //////////////////////////////////////// - // RESOURCES + // ASSETS //////////////////////////////////////// - function acceptResource( + function acceptAsset( uint256 tokenId, uint256 index, - uint64 resourceId + uint64 assetId ) external virtual { require( - index < _pendingResources[tokenId].length, - "MultiResource: index out of bounds" + index < _pendingAssets[tokenId].length, + "MultiAsset: index out of bounds" ); require( - _isApprovedForResourcesOrOwner(_msgSender(), tokenId), - "MultiResource: not owner or approved" + _isApprovedForAssetsOrOwner(_msgSender(), tokenId), + "MultiAsset: not owner or approved" ); require( - resourceId == _pendingResources[tokenId][index], - "MultiResource: Unexpected resource" + assetId == _pendingAssets[tokenId][index], + "MultiAsset: Unexpected asset" ); - _beforeAcceptResource(tokenId, index, resourceId); - _pendingResources[tokenId].removeItemByIndex(index); + _beforeAcceptAsset(tokenId, index, assetId); + _pendingAssets[tokenId].removeItemByIndex(index); - uint64 overwrite = _resourceOverwrites[tokenId][resourceId]; + uint64 overwrite = _assetOverwrites[tokenId][assetId]; if (overwrite != uint64(0)) { // It could have been overwritten previously so it's fine if it's not found. // If it's not deleted (not found), we don't want to send it on the event - if (!_activeResources[tokenId].removeItemByValue(overwrite)) + if (!_activeAssets[tokenId].removeItemByValue(overwrite)) overwrite = uint64(0); - else delete _tokenResources[tokenId][overwrite]; - delete (_resourceOverwrites[tokenId][resourceId]); + else delete _tokenAssets[tokenId][overwrite]; + delete (_assetOverwrites[tokenId][assetId]); } - _activeResources[tokenId].push(resourceId); + _activeAssets[tokenId].push(assetId); //Push 0 value of uint16 to array, e.g., uninitialized - _activeResourcePriorities[tokenId].push(uint16(0)); - emit ResourceAccepted(tokenId, resourceId, overwrite); - _afterAcceptResource(tokenId, index, resourceId); + _activeAssetPriorities[tokenId].push(uint16(0)); + emit AssetAccepted(tokenId, assetId, overwrite); + _afterAcceptAsset(tokenId, index, assetId); } - function rejectResource( + function rejectAsset( uint256 tokenId, uint256 index, - uint64 resourceId + uint64 assetId ) external virtual { require( - index < _pendingResources[tokenId].length, - "MultiResource: index out of bounds" + index < _pendingAssets[tokenId].length, + "MultiAsset: index out of bounds" ); require( - _pendingResources[tokenId].length > index, - "MultiResource: Pending resource index out of range" + _pendingAssets[tokenId].length > index, + "MultiAsset: Pending asset index out of range" ); require( - _isApprovedForResourcesOrOwner(_msgSender(), tokenId), - "MultiResource: not owner or approved" + _isApprovedForAssetsOrOwner(_msgSender(), tokenId), + "MultiAsset: not owner or approved" ); - _beforeRejectResource(tokenId, index, resourceId); - _pendingResources[tokenId].removeItemByValue(resourceId); - delete _tokenResources[tokenId][resourceId]; - delete _resourceOverwrites[tokenId][resourceId]; + _beforeRejectAsset(tokenId, index, assetId); + _pendingAssets[tokenId].removeItemByValue(assetId); + delete _tokenAssets[tokenId][assetId]; + delete _assetOverwrites[tokenId][assetId]; - emit ResourceRejected(tokenId, resourceId); - _afterRejectResource(tokenId, index, resourceId); + emit AssetRejected(tokenId, assetId); + _afterRejectAsset(tokenId, index, assetId); } - function rejectAllResources(uint256 tokenId, uint256 maxRejections) external virtual { + function rejectAllAssets(uint256 tokenId, uint256 maxRejections) + external + virtual + { require( - _isApprovedForResourcesOrOwner(_msgSender(), tokenId), - "MultiResource: not owner or approved" + _isApprovedForAssetsOrOwner(_msgSender(), tokenId), + "MultiAsset: not owner or approved" ); - uint256 len = _pendingResources[tokenId].length; - if (len > maxRejections) revert ("Unexpected number of resources"); + uint256 len = _pendingAssets[tokenId].length; + if (len > maxRejections) revert("Unexpected number of assets"); - _beforeRejectAllResources(tokenId); + _beforeRejectAllAssets(tokenId); for (uint256 i; i < len; ) { - uint64 resourceId = _pendingResources[tokenId][i]; - delete _resourceOverwrites[tokenId][resourceId]; + uint64 assetId = _pendingAssets[tokenId][i]; + delete _assetOverwrites[tokenId][assetId]; unchecked { ++i; } } - delete (_pendingResources[tokenId]); + delete (_pendingAssets[tokenId]); - emit ResourceRejected(tokenId, uint64(0)); - _afterRejectAllResources(tokenId); + emit AssetRejected(tokenId, uint64(0)); + _afterRejectAllAssets(tokenId); } function setPriority(uint256 tokenId, uint16[] memory priorities) @@ -539,66 +534,66 @@ contract MultiResourceToken is Context, IERC721, IMultiResource { { uint256 length = priorities.length; require( - length == _activeResources[tokenId].length, - "MultiResource: Bad priority list length" + length == _activeAssets[tokenId].length, + "MultiAsset: Bad priority list length" ); require( - _isApprovedForResourcesOrOwner(_msgSender(), tokenId), - "MultiResource: not owner or approved" + _isApprovedForAssetsOrOwner(_msgSender(), tokenId), + "MultiAsset: not owner or approved" ); _beforeSetPriority(tokenId, priorities); - _activeResourcePriorities[tokenId] = priorities; + _activeAssetPriorities[tokenId] = priorities; - emit ResourcePrioritySet(tokenId); + emit AssetPrioritySet(tokenId); _afterSetPriority(tokenId, priorities); } - function getActiveResources(uint256 tokenId) + function getActiveAssets(uint256 tokenId) public view virtual returns (uint64[] memory) { - return _activeResources[tokenId]; + return _activeAssets[tokenId]; } - function getPendingResources(uint256 tokenId) + function getPendingAssets(uint256 tokenId) public view virtual returns (uint64[] memory) { - return _pendingResources[tokenId]; + return _pendingAssets[tokenId]; } - function getActiveResourcePriorities(uint256 tokenId) + function getActiveAssetPriorities(uint256 tokenId) public view virtual returns (uint16[] memory) { - return _activeResourcePriorities[tokenId]; + return _activeAssetPriorities[tokenId]; } - function getResourceOverwrites(uint256 tokenId, uint64 newResourceId) + function getAssetOverwrites(uint256 tokenId, uint64 newAssetId) public view virtual returns (uint64) { - return _resourceOverwrites[tokenId][newResourceId]; + return _assetOverwrites[tokenId][newAssetId]; } - function getResourceMetadata(uint256 tokenId, uint64 resourceId) + function getAssetMetadata(uint256 tokenId, uint64 assetId) public view virtual returns (string memory) { - if (!_tokenResources[tokenId][resourceId]) - revert("MultiResource: Token does not have resource"); - return _resources[resourceId]; + if (!_tokenAssets[tokenId][assetId]) + revert("MultiAsset: Token does not have asset"); + return _assets[assetId]; } function tokenURI(uint256 tokenId) @@ -612,103 +607,100 @@ contract MultiResourceToken is Context, IERC721, IMultiResource { // To be implemented with custom guards - function _addResourceEntry(uint64 id, string memory metadataURI) internal { + function _addAssetEntry(uint64 id, string memory metadataURI) internal { require(id != uint64(0), "RMRK: Write to zero"); - require( - bytes(_resources[id]).length == 0, - "RMRK: resource already exists" - ); + require(bytes(_assets[id]).length == 0, "RMRK: asset already exists"); - _beforeAddResource(id, metadataURI); - _resources[id] = metadataURI; + _beforeAddAsset(id, metadataURI); + _assets[id] = metadataURI; - emit ResourceSet(id); - _afterAddResource(id, metadataURI); + emit AssetSet(id); + _afterAddAsset(id, metadataURI); } - function _addResourceToToken( + function _addAssetToToken( uint256 tokenId, - uint64 resourceId, + uint64 assetId, uint64 overwrites ) internal { require( - !_tokenResources[tokenId][resourceId], - "MultiResource: Resource already exists on token" + !_tokenAssets[tokenId][assetId], + "MultiAsset: Asset already exists on token" ); require( - bytes(_resources[resourceId]).length != 0, - "MultiResource: Resource not found in storage" + bytes(_assets[assetId]).length != 0, + "MultiAsset: Asset not found in storage" ); require( - _pendingResources[tokenId].length < 128, - "MultiResource: Max pending resources reached" + _pendingAssets[tokenId].length < 128, + "MultiAsset: Max pending assets reached" ); - _beforeAddResourceToToken(tokenId, resourceId, overwrites); - _tokenResources[tokenId][resourceId] = true; - _pendingResources[tokenId].push(resourceId); + _beforeAddAssetToToken(tokenId, assetId, overwrites); + _tokenAssets[tokenId][assetId] = true; + _pendingAssets[tokenId].push(assetId); if (overwrites != uint64(0)) { - _resourceOverwrites[tokenId][resourceId] = overwrites; + _assetOverwrites[tokenId][assetId] = overwrites; } - emit ResourceAddedToToken(tokenId, resourceId, overwrites); - _afterAddResourceToToken(tokenId, resourceId, overwrites); + emit AssetAddedToToken(tokenId, assetId, overwrites); + _afterAddAssetToToken(tokenId, assetId, overwrites); } // HOOKS - function _beforeAddResource(uint64 id, string memory metadataURI) + function _beforeAddAsset(uint64 id, string memory metadataURI) internal virtual {} - function _afterAddResource(uint64 id, string memory metadataURI) + function _afterAddAsset(uint64 id, string memory metadataURI) internal virtual {} - function _beforeAddResourceToToken( + function _beforeAddAssetToToken( uint256 tokenId, - uint64 resourceId, + uint64 assetId, uint64 overwrites ) internal virtual {} - function _afterAddResourceToToken( + function _afterAddAssetToToken( uint256 tokenId, - uint64 resourceId, + uint64 assetId, uint64 overwrites ) internal virtual {} - function _beforeAcceptResource( + function _beforeAcceptAsset( uint256 tokenId, uint256 index, - uint256 resourceId + uint256 assetId ) internal virtual {} - function _afterAcceptResource( + function _afterAcceptAsset( uint256 tokenId, uint256 index, - uint256 resourceId + uint256 assetId ) internal virtual {} - function _beforeRejectResource( + function _beforeRejectAsset( uint256 tokenId, uint256 index, - uint256 resourceId + uint256 assetId ) internal virtual {} - function _afterRejectResource( + function _afterRejectAsset( uint256 tokenId, uint256 index, - uint256 resourceId + uint256 assetId ) internal virtual {} - function _beforeRejectAllResources(uint256 tokenId) internal virtual {} + function _beforeRejectAllAssets(uint256 tokenId) internal virtual {} - function _afterRejectAllResources(uint256 tokenId) internal virtual {} + function _afterRejectAllAssets(uint256 tokenId) internal virtual {} function _beforeSetPriority(uint256 tokenId, uint16[] memory priorities) internal diff --git a/assets/eip-5773/contracts/library/MultiResourceLib.sol b/assets/eip-5773/contracts/library/MultiAssetLib.sol similarity index 92% rename from assets/eip-5773/contracts/library/MultiResourceLib.sol rename to assets/eip-5773/contracts/library/MultiAssetLib.sol index bf13af5f8bf7e7..0f5438a4e4a9a6 100644 --- a/assets/eip-5773/contracts/library/MultiResourceLib.sol +++ b/assets/eip-5773/contracts/library/MultiAssetLib.sol @@ -1,8 +1,8 @@ -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: CC0 pragma solidity ^0.8.0; -library MultiResourceLib { +library MultiAssetLib { function removeItemByValue(uint64[] storage array, uint64 value) internal returns (bool) diff --git a/assets/eip-5773/contracts/mocks/ERC721ReceiverMock.sol b/assets/eip-5773/contracts/mocks/ERC721ReceiverMock.sol index 5bb7cb7633ba4f..fd37b9e100b24f 100644 --- a/assets/eip-5773/contracts/mocks/ERC721ReceiverMock.sol +++ b/assets/eip-5773/contracts/mocks/ERC721ReceiverMock.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: CC0 pragma solidity ^0.8.15; diff --git a/assets/eip-5773/contracts/mocks/MultiResourceTokenMock.sol b/assets/eip-5773/contracts/mocks/MultiAssetTokenMock.sol similarity index 70% rename from assets/eip-5773/contracts/mocks/MultiResourceTokenMock.sol rename to assets/eip-5773/contracts/mocks/MultiAssetTokenMock.sol index 53f2bae0a4a445..c6bb64dddfd402 100644 --- a/assets/eip-5773/contracts/mocks/MultiResourceTokenMock.sol +++ b/assets/eip-5773/contracts/mocks/MultiAssetTokenMock.sol @@ -1,14 +1,14 @@ -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: CC0 pragma solidity ^0.8.15; -import "../MultiResourceToken.sol"; +import "../MultiAssetToken.sol"; -contract MultiResourceTokenMock is MultiResourceToken { +contract MultiAssetTokenMock is MultiAssetToken { address private _issuer; constructor(string memory name, string memory symbol) - MultiResourceToken(name, symbol) + MultiAssetToken(name, symbol) { _setIssuer(_msgSender()); } @@ -38,19 +38,19 @@ contract MultiResourceTokenMock is MultiResourceToken { _burn(tokenId); } - function addResourceToToken( + function addAssetToToken( uint256 tokenId, - uint64 resourceId, + uint64 assetId, uint64 overwrites ) external onlyIssuer { - _addResourceToToken(tokenId, resourceId, overwrites); + _addAssetToToken(tokenId, assetId, overwrites); } - function addResourceEntry(uint64 id, string memory metadataURI) + function addAssetEntry(uint64 id, string memory metadataURI) external onlyIssuer { - _addResourceEntry(id, metadataURI); + _addAssetEntry(id, metadataURI); } function _setIssuer(address issuer) private { diff --git a/assets/eip-5773/contracts/mocks/NonReceiverMock.sol b/assets/eip-5773/contracts/mocks/NonReceiverMock.sol index c1763ec4f58ed4..90ccc503cf28b6 100644 --- a/assets/eip-5773/contracts/mocks/NonReceiverMock.sol +++ b/assets/eip-5773/contracts/mocks/NonReceiverMock.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: CC0 pragma solidity ^0.8.15; diff --git a/assets/eip-5773/contracts/utils/MultiAssetRenderUtils.sol b/assets/eip-5773/contracts/utils/MultiAssetRenderUtils.sol new file mode 100644 index 00000000000000..f762af1034f5e2 --- /dev/null +++ b/assets/eip-5773/contracts/utils/MultiAssetRenderUtils.sol @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: Apache-2.0 + +import "../IMultiAsset.sol"; + +pragma solidity ^0.8.15; + +/** + * @dev Extra utility functions for composing RMRK assets. + */ + +contract MultiAssetRenderUtils { + uint16 private constant _LOWEST_POSSIBLE_PRIORITY = 2**16 - 1; + + struct ActiveAsset { + uint64 id; + uint16 priority; + string metadata; + } + + struct PendingAsset { + uint64 id; + uint128 acceptRejectIndex; + uint64 overwritesAssetWithId; + string metadata; + } + + function getActiveAssets(address target, uint256 tokenId) + public + view + virtual + returns (ActiveAsset[] memory) + { + IMultiAsset target_ = IMultiAsset(target); + + uint64[] memory assets = target_.getActiveAssets(tokenId); + uint16[] memory priorities = target_.getActiveAssetPriorities( + tokenId + ); + uint256 len = assets.length; + if (len == 0) { + revert("Token has no assets"); + } + + ActiveAsset[] memory activeAssets = new ActiveAsset[](len); + string memory metadata; + for (uint256 i; i < len; ) { + metadata = target_.getAssetMetadata(tokenId, assets[i]); + activeAssets[i] = ActiveAsset({ + id: assets[i], + priority: priorities[i], + metadata: metadata + }); + unchecked { + ++i; + } + } + return activeAssets; + } + + function getPendingAssets(address target, uint256 tokenId) + public + view + virtual + returns (PendingAsset[] memory) + { + IMultiAsset target_ = IMultiAsset(target); + + uint64[] memory assets = target_.getPendingAssets(tokenId); + uint256 len = assets.length; + if (len == 0) { + revert("Token has no assets"); + } + + PendingAsset[] memory pendingAssets = new PendingAsset[](len); + string memory metadata; + uint64 overwritesAssetWithId; + for (uint256 i; i < len; ) { + metadata = target_.getAssetMetadata(tokenId, assets[i]); + overwritesAssetWithId = target_.getAssetOverwrites( + tokenId, + assets[i] + ); + pendingAssets[i] = PendingAsset({ + id: assets[i], + acceptRejectIndex: uint128(i), + overwritesAssetWithId: overwritesAssetWithId, + metadata: metadata + }); + unchecked { + ++i; + } + } + return pendingAssets; + } + + /** + * @notice Returns asset metadata strings for the given ids + * + * Requirements: + * + * - `assetIds` must exist. + */ + function getAssetsById( + address target, + uint256 tokenId, + uint64[] calldata assetIds + ) public view virtual returns (string[] memory) { + IMultiAsset target_ = IMultiAsset(target); + uint256 len = assetIds.length; + string[] memory assets = new string[](len); + for (uint256 i; i < len; ) { + assets[i] = target_.getAssetMetadata(tokenId, assetIds[i]); + unchecked { + ++i; + } + } + return assets; + } + + /** + * @notice Returns the asset metadata with the highest priority for the given token + */ + function getTopAssetMetaForToken(address target, uint256 tokenId) + external + view + returns (string memory) + { + IMultiAsset target_ = IMultiAsset(target); + uint16[] memory priorities = target_.getActiveAssetPriorities( + tokenId + ); + uint64[] memory assets = target_.getActiveAssets(tokenId); + uint256 len = priorities.length; + if (len == 0) { + revert("Token has no assets"); + } + + uint16 maxPriority = _LOWEST_POSSIBLE_PRIORITY; + uint64 maxPriorityAsset; + for (uint64 i; i < len; ) { + uint16 currentPrio = priorities[i]; + if (currentPrio < maxPriority) { + maxPriority = currentPrio; + maxPriorityAsset = assets[i]; + } + unchecked { + ++i; + } + } + return target_.getAssetMetadata(tokenId, maxPriorityAsset); + } +} diff --git a/assets/eip-5773/contracts/utils/MultiResourceRenderUtils.sol b/assets/eip-5773/contracts/utils/MultiResourceRenderUtils.sol deleted file mode 100644 index 9bd0cfb7f5d288..00000000000000 --- a/assets/eip-5773/contracts/utils/MultiResourceRenderUtils.sol +++ /dev/null @@ -1,152 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -import "../IMultiResource.sol"; - -pragma solidity ^0.8.15; - -/** - * @dev Extra utility functions for composing RMRK resources. - */ - -contract MultiResourceRenderUtils { - uint16 private constant _LOWEST_POSSIBLE_PRIORITY = 2**16 - 1; - - struct ActiveResource { - uint64 id; - uint16 priority; - string metadata; - } - - struct PendingResource { - uint64 id; - uint128 acceptRejectIndex; - uint64 overwritesResourceWithId; - string metadata; - } - - function getActiveResources(address target, uint256 tokenId) - public - view - virtual - returns (ActiveResource[] memory) - { - IMultiResource target_ = IMultiResource(target); - - uint64[] memory resources = target_.getActiveResources(tokenId); - uint16[] memory priorities = target_.getActiveResourcePriorities( - tokenId - ); - uint256 len = resources.length; - if (len == 0) { - revert("Token has no resources"); - } - - ActiveResource[] memory activeResources = new ActiveResource[](len); - string memory metadata; - for (uint256 i; i < len; ) { - metadata = target_.getResourceMetadata(tokenId, resources[i]); - activeResources[i] = ActiveResource({ - id: resources[i], - priority: priorities[i], - metadata: metadata - }); - unchecked { - ++i; - } - } - return activeResources; - } - - function getPendingResources(address target, uint256 tokenId) - public - view - virtual - returns (PendingResource[] memory) - { - IMultiResource target_ = IMultiResource(target); - - uint64[] memory resources = target_.getPendingResources(tokenId); - uint256 len = resources.length; - if (len == 0) { - revert("Token has no resources"); - } - - PendingResource[] memory pendingResources = new PendingResource[](len); - string memory metadata; - uint64 overwritesResourceWithId; - for (uint256 i; i < len; ) { - metadata = target_.getResourceMetadata(tokenId, resources[i]); - overwritesResourceWithId = target_.getResourceOverwrites( - tokenId, - resources[i] - ); - pendingResources[i] = PendingResource({ - id: resources[i], - acceptRejectIndex: uint128(i), - overwritesResourceWithId: overwritesResourceWithId, - metadata: metadata - }); - unchecked { - ++i; - } - } - return pendingResources; - } - - /** - * @notice Returns resource metadata strings for the given ids - * - * Requirements: - * - * - `resourceIds` must exist. - */ - function getResourcesById( - address target, - uint256 tokenId, - uint64[] calldata resourceIds - ) public view virtual returns (string[] memory) { - IMultiResource target_ = IMultiResource(target); - uint256 len = resourceIds.length; - string[] memory resources = new string[](len); - for (uint256 i; i < len; ) { - resources[i] = target_.getResourceMetadata(tokenId, resourceIds[i]); - unchecked { - ++i; - } - } - return resources; - } - - /** - * @notice Returns the resource metadata with the highest priority for the given token - */ - function getTopResourceMetaForToken(address target, uint256 tokenId) - external - view - returns (string memory) - { - IMultiResource target_ = IMultiResource(target); - uint16[] memory priorities = target_.getActiveResourcePriorities( - tokenId - ); - uint64[] memory resources = target_.getActiveResources(tokenId); - uint256 len = priorities.length; - if (len == 0) { - revert("Token has no resources"); - } - - uint16 maxPriority = _LOWEST_POSSIBLE_PRIORITY; - uint64 maxPriorityResource; - for (uint64 i; i < len; ) { - uint16 currentPrio = priorities[i]; - if (currentPrio < maxPriority) { - maxPriority = currentPrio; - maxPriorityResource = resources[i]; - } - unchecked { - ++i; - } - } - return target_.getResourceMetadata(tokenId, maxPriorityResource); - } -} diff --git a/assets/eip-5773/hardhat.config.ts b/assets/eip-5773/hardhat.config.ts index 68a2634c68b5ec..f283a2948f00b0 100644 --- a/assets/eip-5773/hardhat.config.ts +++ b/assets/eip-5773/hardhat.config.ts @@ -1,6 +1,4 @@ -import * as dotenv from 'dotenv'; - -import { HardhatUserConfig, task } from 'hardhat/config'; +import { HardhatUserConfig } from 'hardhat/config'; import '@nomicfoundation/hardhat-chai-matchers'; import '@nomiclabs/hardhat-etherscan'; import '@typechain/hardhat'; @@ -8,21 +6,6 @@ import 'hardhat-contract-sizer'; import 'hardhat-gas-reporter'; import 'solidity-coverage'; -dotenv.config(); - -// This is a sample Hardhat task. To learn how to create your own go to -// https://hardhat.org/guides/create-task.html -task('accounts', 'Prints the list of accounts', async (taskArgs, hre) => { - const accounts = await hre.ethers.getSigners(); - - for (const account of accounts) { - console.log(account.address); - } -}); - -// You need to export an object to set up your config -// Go to https://hardhat.org/config/ to learn more - const config: HardhatUserConfig = { solidity: { version: '0.8.15', @@ -33,15 +16,6 @@ const config: HardhatUserConfig = { }, }, }, - gasReporter: { - enabled: process.env.REPORT_GAS !== undefined, - currency: 'USD', - coinmarketcap: process.env.COIN_MARKET_CAP_KEY || "", - gasPrice: 50, - }, - etherscan: { - apiKey: process.env.ETHERSCAN_API_KEY, - }, }; export default config; diff --git a/assets/eip-5773/package.json b/assets/eip-5773/package.json index 564fab7bd96f66..e6f2346786f858 100644 --- a/assets/eip-5773/package.json +++ b/assets/eip-5773/package.json @@ -1,5 +1,5 @@ { - "name": "multiresource-eip", + "name": "eip-5773", "dependencies": { "@openzeppelin/contracts": "^4.6.0" }, @@ -18,7 +18,6 @@ "@typescript-eslint/eslint-plugin": "^5.30.6", "@typescript-eslint/parser": "^5.30.6", "chai": "^4.3.6", - "dotenv": "^10.0.0", "eslint": "^8.27.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-import": "^2.26.0", diff --git a/assets/eip-5773/test/multiasset.ts b/assets/eip-5773/test/multiasset.ts new file mode 100644 index 00000000000000..cd129c7e34fbc4 --- /dev/null +++ b/assets/eip-5773/test/multiasset.ts @@ -0,0 +1,673 @@ +import { ethers } from 'hardhat'; +import { expect } from 'chai'; +import { + ERC721ReceiverMock, + MultiAssetReceiverMock, + MultiAssetTokenMock, + NonReceiverMock, + MultiAssetRenderUtils, +} from '../typechain-types'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; + +describe('MultiAsset', async () => { + let token: MultiAssetTokenMock; + let renderUtils: MultiAssetRenderUtils; + let nonReceiver: NonReceiverMock; + let receiver721: ERC721ReceiverMock; + + let owner: SignerWithAddress; + let addrs: SignerWithAddress[]; + + const name = 'RmrkTest'; + const symbol = 'RMRKTST'; + + const metaURIDefault = 'metaURI'; + + beforeEach(async () => { + const [signersOwner, ...signersAddr] = await ethers.getSigners(); + owner = signersOwner; + addrs = signersAddr; + + const multiassetFactory = await ethers.getContractFactory('MultiAssetTokenMock'); + token = await multiassetFactory.deploy(name, symbol); + await token.deployed(); + + const renderFactory = await ethers.getContractFactory('MultiAssetRenderUtils'); + renderUtils = await renderFactory.deploy(); + await renderUtils.deployed(); + }); + + describe('Init', async function () { + it('Name', async function () { + expect(await token.name()).to.equal(name); + }); + + it('Symbol', async function () { + expect(await token.symbol()).to.equal(symbol); + }); + }); + + describe('ERC165 check', async function () { + it('can support IERC165', async function () { + expect(await token.supportsInterface('0x01ffc9a7')).to.equal(true); + }); + + it('can support IERC721', async function () { + expect(await token.supportsInterface('0x80ac58cd')).to.equal(true); + }); + + it('can support IMultiAsset', async function () { + expect(await token.supportsInterface('0xfa73a1e2')).to.equal(true); + }); + + it('cannot support other interfaceId', async function () { + expect(await token.supportsInterface('0xffffffff')).to.equal(false); + }); + }); + + describe('Check OnReceived ERC721 and Multiasset', async function () { + it('Revert on transfer to non onERC721/onMultiasset implementer', async function () { + const tokenId = 1; + await token.mint(owner.address, tokenId); + + const NonReceiver = await ethers.getContractFactory('NonReceiverMock'); + nonReceiver = await NonReceiver.deploy(); + await nonReceiver.deployed(); + + await expect( + token + .connect(owner) + ['safeTransferFrom(address,address,uint256)'](owner.address, nonReceiver.address, 1), + ).to.be.revertedWith('MultiAsset: transfer to non ERC721 Receiver implementer'); + }); + + it('onERC721Received callback on transfer', async function () { + const tokenId = 1; + await token.mint(owner.address, tokenId); + + const ERC721Receiver = await ethers.getContractFactory('ERC721ReceiverMock'); + receiver721 = await ERC721Receiver.deploy(); + await receiver721.deployed(); + + await token + .connect(owner) + ['safeTransferFrom(address,address,uint256)'](owner.address, receiver721.address, 1); + expect(await token.ownerOf(1)).to.equal(receiver721.address); + }); + }); + + describe('Asset storage', async function () { + it('can add asset', async function () { + const id = 10; + + await expect(token.addAssetEntry(id, metaURIDefault)).to.emit(token, 'AssetSet').withArgs(id); + }); + + it('cannot get non existing asset', async function () { + const tokenId = 1; + const resId = 10; + await token.mint(owner.address, tokenId); + await expect(token.getAssetMetadata(tokenId, resId)).to.be.revertedWith( + 'MultiAsset: Token does not have asset', + ); + }); + + it('cannot add asset entry if not issuer', async function () { + const id = 10; + await expect(token.connect(addrs[1]).addAssetEntry(id, metaURIDefault)).to.be.revertedWith( + 'RMRK: Only issuer', + ); + }); + + it('can set and get issuer', async function () { + const newIssuerAddr = addrs[1].address; + expect(await token.getIssuer()).to.equal(owner.address); + + await token.setIssuer(newIssuerAddr); + expect(await token.getIssuer()).to.equal(newIssuerAddr); + }); + + it('cannot set issuer if not issuer', async function () { + const newIssuer = addrs[1]; + await expect(token.connect(newIssuer).setIssuer(newIssuer.address)).to.be.revertedWith( + 'RMRK: Only issuer', + ); + }); + + it('cannot overwrite asset', async function () { + const id = 10; + + await token.addAssetEntry(id, metaURIDefault); + await expect(token.addAssetEntry(id, metaURIDefault)).to.be.revertedWith( + 'RMRK: asset already exists', + ); + }); + + it('cannot add asset with id 0', async function () { + const id = ethers.utils.hexZeroPad('0x0', 8); + + await expect(token.addAssetEntry(id, metaURIDefault)).to.be.revertedWith( + 'RMRK: Write to zero', + ); + }); + + it('cannot add same asset twice', async function () { + const id = 10; + + await expect(token.addAssetEntry(id, metaURIDefault)).to.emit(token, 'AssetSet').withArgs(id); + + await expect(token.addAssetEntry(id, metaURIDefault)).to.be.revertedWith( + 'RMRK: asset already exists', + ); + }); + }); + + describe('Adding assets', async function () { + it('can add asset to token', async function () { + const resId = 1; + const resId2 = 2; + const tokenId = 1; + + await token.mint(owner.address, tokenId); + await addAssets([resId, resId2]); + await expect(token.addAssetToToken(tokenId, resId, 0)).to.emit(token, 'AssetAddedToToken'); + await expect(token.addAssetToToken(tokenId, resId2, 0)).to.emit(token, 'AssetAddedToToken'); + + const pendingIds = await token.getPendingAssets(tokenId); + expect(await renderUtils.getAssetsById(token.address, tokenId, pendingIds)).to.be.eql([ + metaURIDefault, + metaURIDefault, + ]); + }); + + it('cannot add non existing asset to token', async function () { + const resId = 1; + const tokenId = 1; + + await token.mint(owner.address, tokenId); + await expect(token.addAssetToToken(tokenId, resId, 0)).to.be.revertedWith( + 'MultiAsset: Asset not found in storage', + ); + }); + + it('can add asset to non existing token and it is pending when minted', async function () { + const resId = 1; + const tokenId = 1; + await addAssets([resId]); + + await token.addAssetToToken(tokenId, resId, 0); + await token.mint(owner.address, tokenId); + expect(await token.getPendingAssets(tokenId)).to.eql([ethers.BigNumber.from(resId)]); + }); + + it('cannot add asset twice to the same token', async function () { + const resId = 1; + const tokenId = 1; + + await token.mint(owner.address, tokenId); + await addAssets([resId]); + await token.addAssetToToken(tokenId, resId, 0); + await expect( + token.addAssetToToken(tokenId, ethers.BigNumber.from(resId), 0), + ).to.be.revertedWith('MultiAsset: Asset already exists on token'); + }); + + it('cannot add too many assets to the same token', async function () { + const tokenId = 1; + + await token.mint(owner.address, tokenId); + for (let i = 1; i <= 128; i++) { + await addAssets([i]); + await token.addAssetToToken(tokenId, i, 0); + } + + // Now it's full, next should fail + const resId = 129; + await addAssets([resId]); + await expect(token.addAssetToToken(tokenId, resId, 0)).to.be.revertedWith( + 'MultiAsset: Max pending assets reached', + ); + }); + + it('can add same asset to 2 different tokens', async function () { + const resId = 1; + const tokenId1 = 1; + const tokenId2 = 2; + + await token.mint(owner.address, tokenId1); + await token.mint(owner.address, tokenId2); + await addAssets([resId]); + await token.addAssetToToken(tokenId1, resId, 0); + await token.addAssetToToken(tokenId2, resId, 0); + }); + }); + + describe('Accepting assets', async function () { + it('can accept asset if owner', async function () { + const { tokenOwner, tokenId } = await mintSampleToken(); + const approved = tokenOwner; + + await checkAcceptFromAddress(approved, tokenId); + }); + + it('can accept asset if approved for assets', async function () { + const { tokenId } = await mintSampleToken(); + const approved = addrs[1]; + + await token.approveForAssets(approved.address, tokenId); + await checkAcceptFromAddress(approved, tokenId); + }); + + it('can accept asset if approved for assets for all', async function () { + const { tokenId } = await mintSampleToken(); + const approved = addrs[2]; + + await token.setApprovalForAllForAssets(approved.address, true); + await checkAcceptFromAddress(approved, tokenId); + }); + + it('can accept multiple assets', async function () { + const resId = 1; + const resId2 = 2; + const tokenId = 1; + + await token.mint(owner.address, tokenId); + await addAssets([resId, resId2]); + await token.addAssetToToken(tokenId, resId, 0); + await token.addAssetToToken(tokenId, resId2, 0); + await expect(token.acceptAsset(tokenId, 1, resId2)) + .to.emit(token, 'AssetAccepted') + .withArgs(tokenId, resId2, 0); + await expect(token.acceptAsset(tokenId, 0, resId)) + .to.emit(token, 'AssetAccepted') + .withArgs(tokenId, resId, 0); + + expect(await token.getPendingAssets(tokenId)).to.be.eql([]); + + const activeIds = await token.getActiveAssets(tokenId); + expect(await renderUtils.getAssetsById(token.address, tokenId, activeIds)).to.eql([ + metaURIDefault, + metaURIDefault, + ]); + }); + + it('cannot accept asset twice', async function () { + const resId = 1; + const tokenId = 1; + + await token.mint(owner.address, tokenId); + await addAssets([resId]); + await token.addAssetToToken(tokenId, resId, 0); + await token.acceptAsset(tokenId, 0, resId); + }); + + it('cannot accept asset if not owner', async function () { + const resId = 1; + const tokenId = 1; + + await token.mint(owner.address, tokenId); + await addAssets([resId]); + await token.addAssetToToken(tokenId, resId, 0); + await expect(token.connect(addrs[1]).acceptAsset(tokenId, 0, resId)).to.be.revertedWith( + 'MultiAsset: not owner or approved', + ); + }); + + it('cannot accept non existing asset', async function () { + const tokenId = 1; + + await token.mint(owner.address, tokenId); + await expect(token.acceptAsset(tokenId, 0, 1)).to.be.revertedWith( + 'MultiAsset: index out of bounds', + ); + }); + }); + + describe('Overwriting assets', async function () { + it('can add asset to token overwritting an existing one', async function () { + const resId = 1; + const resId2 = 2; + const tokenId = 1; + + await token.mint(owner.address, tokenId); + await addAssets([resId, resId2]); + await token.addAssetToToken(tokenId, resId, 0); + await token.acceptAsset(tokenId, 0, resId); + + // Add new asset to overwrite the first, and accept + const activeAssets = await token.getActiveAssets(tokenId); + await expect(token.addAssetToToken(tokenId, resId2, activeAssets[0])) + .to.emit(token, 'AssetAddedToToken') + .withArgs(tokenId, resId2, resId); + const pendingAssets = await token.getPendingAssets(tokenId); + + expect(await token.getAssetOverwrites(tokenId, pendingAssets[0])).to.eql(activeAssets[0]); + await expect(token.acceptAsset(tokenId, 0, resId2)) + .to.emit(token, 'AssetAccepted') + .withArgs(tokenId, resId2, resId); + + const activeIds = await token.getActiveAssets(tokenId); + expect(await renderUtils.getAssetsById(token.address, tokenId, activeIds)).to.eql([ + metaURIDefault, + ]); + // Overwrite should be gone + expect(await token.getAssetOverwrites(tokenId, pendingAssets[0])).to.eql( + ethers.BigNumber.from(0), + ); + }); + + it('can overwrite non existing asset to token, it could have been deleted', async function () { + const resId = 1; + const tokenId = 1; + + await token.mint(owner.address, tokenId); + await addAssets([resId]); + await token.addAssetToToken(tokenId, resId, ethers.utils.hexZeroPad('0x1', 8)); + await token.acceptAsset(tokenId, 0, resId); + + const activeIds = await token.getActiveAssets(tokenId); + expect(await renderUtils.getAssetsById(token.address, tokenId, activeIds)).to.eql([ + metaURIDefault, + ]); + }); + }); + + describe('Rejecting assets', async function () { + it('can reject asset if owner', async function () { + const { tokenOwner, tokenId } = await mintSampleToken(); + const approved = tokenOwner; + + await checkRejectFromAddress(approved, tokenId); + }); + + it('can reject asset if approved for assets', async function () { + const { tokenId } = await mintSampleToken(); + const approved = addrs[1]; + + await token.approveForAssets(approved.address, tokenId); + await checkRejectFromAddress(approved, tokenId); + }); + + it('can reject asset if approved for assets for all', async function () { + const { tokenId } = await mintSampleToken(); + const approved = addrs[2]; + + await token.setApprovalForAllForAssets(approved.address, true); + await checkRejectFromAddress(approved, tokenId); + }); + + it('can reject all assets if owner', async function () { + const { tokenOwner, tokenId } = await mintSampleToken(); + const approved = tokenOwner; + + await checkRejectAllFromAddress(approved, tokenId); + }); + + it('can reject all assets if approved for assets', async function () { + const { tokenId } = await mintSampleToken(); + const approved = addrs[1]; + + await token.approveForAssets(approved.address, tokenId); + await checkRejectAllFromAddress(approved, tokenId); + }); + + it('can reject all assets if approved for assets for all', async function () { + const { tokenId } = await mintSampleToken(); + const approved = addrs[2]; + + await token.setApprovalForAllForAssets(approved.address, true); + await checkRejectAllFromAddress(approved, tokenId); + }); + + it('can reject asset and overwrites are cleared', async function () { + const resId = 1; + const resId2 = 2; + const tokenId = 1; + + await token.mint(owner.address, tokenId); + await addAssets([resId, resId2]); + await token.addAssetToToken(tokenId, resId, 0); + await token.acceptAsset(tokenId, 0, resId); + + // Will try to overwrite but we reject it + await token.addAssetToToken(tokenId, resId2, resId); + await token.rejectAsset(tokenId, 0, resId2); + + expect(await token.getAssetOverwrites(tokenId, resId2)).to.eql(ethers.BigNumber.from(0)); + }); + + it('can reject all assets and overwrites are cleared', async function () { + const resId = 1; + const resId2 = 2; + const tokenId = 1; + + await token.mint(owner.address, tokenId); + await addAssets([resId, resId2]); + await token.addAssetToToken(tokenId, resId, 0); + await token.acceptAsset(tokenId, 0, resId); + + // Will try to overwrite but we reject all + await token.addAssetToToken(tokenId, resId2, resId); + await token.rejectAllAssets(tokenId, 1); + + expect(await token.getAssetOverwrites(tokenId, resId2)).to.eql(ethers.BigNumber.from(0)); + }); + + it('can reject all pending assets at max capacity', async function () { + const tokenId = 1; + const resArr = []; + + for (let i = 1; i < 128; i++) { + resArr.push(i); + } + + await token.mint(owner.address, tokenId); + await addAssets(resArr); + + for (let i = 1; i < 128; i++) { + await token.addAssetToToken(tokenId, i, 1); + } + await token.rejectAllAssets(tokenId, 128); + + expect(await token.getAssetOverwrites(1, 2)).to.eql(ethers.BigNumber.from(0)); + }); + + it('cannot reject asset twice', async function () { + const resId = 1; + const tokenId = 1; + + await token.mint(owner.address, tokenId); + await addAssets([resId]); + await token.addAssetToToken(tokenId, resId, 0); + await token.rejectAsset(tokenId, 0, resId); + }); + + it('cannot reject asset nor reject all if not owner', async function () { + const resId = 1; + const tokenId = 1; + + await token.mint(owner.address, tokenId); + await addAssets([resId]); + await token.addAssetToToken(tokenId, resId, 0); + + await expect(token.connect(addrs[1]).rejectAsset(tokenId, 0, resId)).to.be.revertedWith( + 'MultiAsset: not owner or approved', + ); + await expect(token.connect(addrs[1]).rejectAllAssets(tokenId, 1)).to.be.revertedWith( + 'MultiAsset: not owner or approved', + ); + }); + + it('cannot reject non existing asset', async function () { + const tokenId = 1; + + await token.mint(owner.address, tokenId); + await expect(token.rejectAsset(tokenId, 0, 1)).to.be.revertedWith( + 'MultiAsset: index out of bounds', + ); + }); + }); + + describe('Priorities', async function () { + it('can set and get priorities', async function () { + const tokenId = 1; + await addAssetsToToken(tokenId); + + expect(await token.getActiveAssetPriorities(tokenId)).to.be.eql([0, 0]); + await expect(token.setPriority(tokenId, [2, 1])) + .to.emit(token, 'AssetPrioritySet') + .withArgs(tokenId); + expect(await token.getActiveAssetPriorities(tokenId)).to.be.eql([2, 1]); + }); + + it('cannot set priorities for non owned token', async function () { + const tokenId = 1; + await addAssetsToToken(tokenId); + await expect(token.connect(addrs[1]).setPriority(tokenId, [2, 1])).to.be.revertedWith( + 'MultiAsset: not owner or approved', + ); + }); + + it('cannot set different number of priorities', async function () { + const tokenId = 1; + await addAssetsToToken(tokenId); + await expect(token.connect(addrs[1]).setPriority(tokenId, [1])).to.be.revertedWith( + 'MultiAsset: Bad priority list length', + ); + await expect(token.connect(addrs[1]).setPriority(tokenId, [2, 1, 3])).to.be.revertedWith( + 'MultiAsset: Bad priority list length', + ); + }); + + it('cannot set priorities for non existing token', async function () { + const tokenId = 1; + await expect(token.connect(addrs[1]).setPriority(tokenId, [])).to.be.revertedWith( + 'MultiAsset: approved query for nonexistent token', + ); + }); + }); + + describe('Approval Cleaning', async function () { + it('cleans token and assets approvals on transfer', async function () { + const tokenId = 1; + const tokenOwner = addrs[1]; + const newOwner = addrs[2]; + const approved = addrs[3]; + await token.mint(tokenOwner.address, tokenId); + await token.connect(tokenOwner).approve(approved.address, tokenId); + await token.connect(tokenOwner).approveForAssets(approved.address, tokenId); + + expect(await token.getApproved(tokenId)).to.eql(approved.address); + expect(await token.getApprovedForAssets(tokenId)).to.eql(approved.address); + + await token.connect(tokenOwner).transfer(newOwner.address, tokenId); + + expect(await token.getApproved(tokenId)).to.eql(ethers.constants.AddressZero); + expect(await token.getApprovedForAssets(tokenId)).to.eql(ethers.constants.AddressZero); + }); + + it('cleans token and assets approvals on burn', async function () { + const tokenId = 1; + const tokenOwner = addrs[1]; + const approved = addrs[3]; + await token.mint(tokenOwner.address, tokenId); + await token.connect(tokenOwner).approve(approved.address, tokenId); + await token.connect(tokenOwner).approveForAssets(approved.address, tokenId); + + expect(await token.getApproved(tokenId)).to.eql(approved.address); + expect(await token.getApprovedForAssets(tokenId)).to.eql(approved.address); + + await token.connect(tokenOwner).burn(tokenId); + + await expect(token.getApproved(tokenId)).to.be.revertedWith( + 'MultiAsset: approved query for nonexistent token', + ); + await expect(token.getApprovedForAssets(tokenId)).to.be.revertedWith( + 'MultiAsset: approved query for nonexistent token', + ); + }); + }); + + async function mintSampleToken(): Promise<{ tokenOwner: SignerWithAddress; tokenId: number }> { + const tokenOwner = owner; + const tokenId = 1; + await token.mint(tokenOwner.address, tokenId); + + return { tokenOwner, tokenId }; + } + + async function addAssets(ids: number[]): Promise { + ids.forEach(async (resId) => { + await token.addAssetEntry(resId, metaURIDefault); + }); + } + + async function addAssetsToToken(tokenId: number): Promise { + const resId = 1; + const resId2 = 2; + await token.mint(owner.address, tokenId); + await addAssets([resId, resId2]); + await token.addAssetToToken(tokenId, resId, 0); + await token.addAssetToToken(tokenId, resId2, 0); + await token.acceptAsset(tokenId, 0, resId); + await token.acceptAsset(tokenId, 0, resId2); + } + + async function checkAcceptFromAddress( + accepter: SignerWithAddress, + tokenId: number, + ): Promise { + const resId = 1; + + await addAssets([resId]); + await token.addAssetToToken(tokenId, resId, 0); + await expect(token.connect(accepter).acceptAsset(tokenId, 0, resId)) + .to.emit(token, 'AssetAccepted') + .withArgs(tokenId, resId, 0); + + expect(await token.getPendingAssets(tokenId)).to.be.eql([]); + + const activeIds = await token.getActiveAssets(tokenId); + expect(await renderUtils.getAssetsById(token.address, tokenId, activeIds)).to.eql([ + metaURIDefault, + ]); + } + + async function checkRejectFromAddress( + rejecter: SignerWithAddress, + tokenId: number, + ): Promise { + const resId = 1; + + await addAssets([resId]); + await token.addAssetToToken(tokenId, resId, 0); + + await expect(token.connect(rejecter).rejectAsset(tokenId, 0, resId)).to.emit( + token, + 'AssetRejected', + ); + + expect(await token.getPendingAssets(tokenId)).to.be.eql([]); + expect(await token.getActiveAssets(tokenId)).to.be.eql([]); + } + + async function checkRejectAllFromAddress( + rejecter: SignerWithAddress, + tokenId: number, + ): Promise { + const resId = 1; + const resId2 = 2; + + await addAssets([resId, resId2]); + await token.addAssetToToken(tokenId, resId, 0); + await token.addAssetToToken(tokenId, resId2, 0); + + await expect(token.connect(rejecter).rejectAllAssets(tokenId, 2)).to.emit( + token, + 'AssetRejected', + ); + + expect(await token.getPendingAssets(tokenId)).to.be.eql([]); + expect(await token.getActiveAssets(tokenId)).to.be.eql([]); + } +}); diff --git a/assets/eip-5773/test/multiresource.ts b/assets/eip-5773/test/multiresource.ts deleted file mode 100644 index c901b74de49d21..00000000000000 --- a/assets/eip-5773/test/multiresource.ts +++ /dev/null @@ -1,677 +0,0 @@ -import { ethers } from 'hardhat'; -import { expect } from 'chai'; -import { - ERC721ReceiverMock, - MultiResourceReceiverMock, - MultiResourceTokenMock, - NonReceiverMock, - MultiResourceRenderUtils, -} from '../typechain-types'; -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; - -describe('MultiResource', async () => { - let token: MultiResourceTokenMock; - let renderUtils: MultiResourceRenderUtils; - let nonReceiver: NonReceiverMock; - let receiver721: ERC721ReceiverMock; - - let owner: SignerWithAddress; - let addrs: SignerWithAddress[]; - - const name = 'RmrkTest'; - const symbol = 'RMRKTST'; - - const metaURIDefault = 'metaURI'; - - beforeEach(async () => { - const [signersOwner, ...signersAddr] = await ethers.getSigners(); - owner = signersOwner; - addrs = signersAddr; - - const multiresourceFactory = await ethers.getContractFactory('MultiResourceTokenMock'); - token = await multiresourceFactory.deploy(name, symbol); - await token.deployed(); - - const renderFactory = await ethers.getContractFactory('MultiResourceRenderUtils'); - renderUtils = await renderFactory.deploy(); - await renderUtils.deployed(); - }); - - describe('Init', async function () { - it('Name', async function () { - expect(await token.name()).to.equal(name); - }); - - it('Symbol', async function () { - expect(await token.symbol()).to.equal(symbol); - }); - }); - - describe('ERC165 check', async function () { - it('can support IERC165', async function () { - expect(await token.supportsInterface('0x01ffc9a7')).to.equal(true); - }); - - it('can support IERC721', async function () { - expect(await token.supportsInterface('0x80ac58cd')).to.equal(true); - }); - - it('can support IMultiResource', async function () { - expect(await token.supportsInterface('0xb0ecc5ae')).to.equal(true); - }); - - it('cannot support other interfaceId', async function () { - expect(await token.supportsInterface('0xffffffff')).to.equal(false); - }); - }); - - describe('Check OnReceived ERC721 and Multiresource', async function () { - it('Revert on transfer to non onERC721/onMultiresource implementer', async function () { - const tokenId = 1; - await token.mint(owner.address, tokenId); - - const NonReceiver = await ethers.getContractFactory('NonReceiverMock'); - nonReceiver = await NonReceiver.deploy(); - await nonReceiver.deployed(); - - await expect( - token - .connect(owner) - ['safeTransferFrom(address,address,uint256)'](owner.address, nonReceiver.address, 1), - ).to.be.revertedWith('MultiResource: transfer to non ERC721 Receiver implementer'); - }); - - it('onERC721Received callback on transfer', async function () { - const tokenId = 1; - await token.mint(owner.address, tokenId); - - const ERC721Receiver = await ethers.getContractFactory('ERC721ReceiverMock'); - receiver721 = await ERC721Receiver.deploy(); - await receiver721.deployed(); - - await token - .connect(owner) - ['safeTransferFrom(address,address,uint256)'](owner.address, receiver721.address, 1); - expect(await token.ownerOf(1)).to.equal(receiver721.address); - }); - }); - - describe('Resource storage', async function () { - it('can add resource', async function () { - const id = 10; - - await expect(token.addResourceEntry(id, metaURIDefault)) - .to.emit(token, 'ResourceSet') - .withArgs(id); - }); - - it('cannot get non existing resource', async function () { - const tokenId = 1; - const resId = 10; - await token.mint(owner.address, tokenId); - await expect(token.getResourceMetadata(tokenId, resId)).to.be.revertedWith( - 'MultiResource: Token does not have resource', - ); - }); - - it('cannot add resource entry if not issuer', async function () { - const id = 10; - await expect(token.connect(addrs[1]).addResourceEntry(id, metaURIDefault)).to.be.revertedWith( - 'RMRK: Only issuer', - ); - }); - - it('can set and get issuer', async function () { - const newIssuerAddr = addrs[1].address; - expect(await token.getIssuer()).to.equal(owner.address); - - await token.setIssuer(newIssuerAddr); - expect(await token.getIssuer()).to.equal(newIssuerAddr); - }); - - it('cannot set issuer if not issuer', async function () { - const newIssuer = addrs[1]; - await expect(token.connect(newIssuer).setIssuer(newIssuer.address)).to.be.revertedWith( - 'RMRK: Only issuer', - ); - }); - - it('cannot overwrite resource', async function () { - const id = 10; - - await token.addResourceEntry(id, metaURIDefault); - await expect(token.addResourceEntry(id, metaURIDefault)).to.be.revertedWith( - 'RMRK: resource already exists', - ); - }); - - it('cannot add resource with id 0', async function () { - const id = ethers.utils.hexZeroPad('0x0', 8); - - await expect(token.addResourceEntry(id, metaURIDefault)).to.be.revertedWith( - 'RMRK: Write to zero', - ); - }); - - it('cannot add same resource twice', async function () { - const id = 10; - - await expect(token.addResourceEntry(id, metaURIDefault)) - .to.emit(token, 'ResourceSet') - .withArgs(id); - - await expect(token.addResourceEntry(id, metaURIDefault)).to.be.revertedWith( - 'RMRK: resource already exists', - ); - }); - }); - - describe('Adding resources', async function () { - it('can add resource to token', async function () { - const resId = 1; - const resId2 = 2; - const tokenId = 1; - - await token.mint(owner.address, tokenId); - await addResources([resId, resId2]); - await expect(token.addResourceToToken(tokenId, resId, 0)).to.emit( - token, - 'ResourceAddedToToken', - ); - await expect(token.addResourceToToken(tokenId, resId2, 0)).to.emit( - token, - 'ResourceAddedToToken', - ); - - const pendingIds = await token.getPendingResources(tokenId); - expect(await renderUtils.getResourcesById(token.address, tokenId, pendingIds)).to.be.eql([ - metaURIDefault, - metaURIDefault, - ]); - }); - - it('cannot add non existing resource to token', async function () { - const resId = 1; - const tokenId = 1; - - await token.mint(owner.address, tokenId); - await expect(token.addResourceToToken(tokenId, resId, 0)).to.be.revertedWith( - 'MultiResource: Resource not found in storage', - ); - }); - - it('can add resource to non existing token and it is pending when minted', async function () { - const resId = 1; - const tokenId = 1; - await addResources([resId]); - - await token.addResourceToToken(tokenId, resId, 0); - await token.mint(owner.address, tokenId); - expect(await token.getPendingResources(tokenId)).to.eql([ethers.BigNumber.from(resId)]); - }); - - it('cannot add resource twice to the same token', async function () { - const resId = 1; - const tokenId = 1; - - await token.mint(owner.address, tokenId); - await addResources([resId]); - await token.addResourceToToken(tokenId, resId, 0); - await expect( - token.addResourceToToken(tokenId, ethers.BigNumber.from(resId), 0), - ).to.be.revertedWith('MultiResource: Resource already exists on token'); - }); - - it('cannot add too many resources to the same token', async function () { - const tokenId = 1; - - await token.mint(owner.address, tokenId); - for (let i = 1; i <= 128; i++) { - await addResources([i]); - await token.addResourceToToken(tokenId, i, 0); - } - - // Now it's full, next should fail - const resId = 129; - await addResources([resId]); - await expect(token.addResourceToToken(tokenId, resId, 0)).to.be.revertedWith( - 'MultiResource: Max pending resources reached', - ); - }); - - it('can add same resource to 2 different tokens', async function () { - const resId = 1; - const tokenId1 = 1; - const tokenId2 = 2; - - await token.mint(owner.address, tokenId1); - await token.mint(owner.address, tokenId2); - await addResources([resId]); - await token.addResourceToToken(tokenId1, resId, 0); - await token.addResourceToToken(tokenId2, resId, 0); - }); - }); - - describe('Accepting resources', async function () { - it('can accept resource if owner', async function () { - const { tokenOwner, tokenId } = await mintSampleToken(); - const approved = tokenOwner; - - await checkAcceptFromAddress(approved, tokenId); - }); - - it('can accept resource if approved for resources', async function () { - const { tokenId } = await mintSampleToken(); - const approved = addrs[1]; - - await token.approveForResources(approved.address, tokenId); - await checkAcceptFromAddress(approved, tokenId); - }); - - it('can accept resource if approved for resources for all', async function () { - const { tokenId } = await mintSampleToken(); - const approved = addrs[2]; - - await token.setApprovalForAllForResources(approved.address, true); - await checkAcceptFromAddress(approved, tokenId); - }); - - it('can accept multiple resources', async function () { - const resId = 1; - const resId2 = 2; - const tokenId = 1; - - await token.mint(owner.address, tokenId); - await addResources([resId, resId2]); - await token.addResourceToToken(tokenId, resId, 0); - await token.addResourceToToken(tokenId, resId2, 0); - await expect(token.acceptResource(tokenId, 1, resId2)) - .to.emit(token, 'ResourceAccepted') - .withArgs(tokenId, resId2, 0); - await expect(token.acceptResource(tokenId, 0, resId)) - .to.emit(token, 'ResourceAccepted') - .withArgs(tokenId, resId, 0); - - expect(await token.getPendingResources(tokenId)).to.be.eql([]); - - const activeIds = await token.getActiveResources(tokenId); - expect(await renderUtils.getResourcesById(token.address, tokenId, activeIds)).to.eql([ - metaURIDefault, - metaURIDefault, - ]); - }); - - it('cannot accept resource twice', async function () { - const resId = 1; - const tokenId = 1; - - await token.mint(owner.address, tokenId); - await addResources([resId]); - await token.addResourceToToken(tokenId, resId, 0); - await token.acceptResource(tokenId, 0, resId); - }); - - it('cannot accept resource if not owner', async function () { - const resId = 1; - const tokenId = 1; - - await token.mint(owner.address, tokenId); - await addResources([resId]); - await token.addResourceToToken(tokenId, resId, 0); - await expect(token.connect(addrs[1]).acceptResource(tokenId, 0, resId)).to.be.revertedWith( - 'MultiResource: not owner or approved', - ); - }); - - it('cannot accept non existing resource', async function () { - const tokenId = 1; - - await token.mint(owner.address, tokenId); - await expect(token.acceptResource(tokenId, 0, 1)).to.be.revertedWith( - 'MultiResource: index out of bounds', - ); - }); - }); - - describe('Overwriting resources', async function () { - it('can add resource to token overwritting an existing one', async function () { - const resId = 1; - const resId2 = 2; - const tokenId = 1; - - await token.mint(owner.address, tokenId); - await addResources([resId, resId2]); - await token.addResourceToToken(tokenId, resId, 0); - await token.acceptResource(tokenId, 0, resId); - - // Add new resource to overwrite the first, and accept - const activeResources = await token.getActiveResources(tokenId); - await expect(token.addResourceToToken(tokenId, resId2, activeResources[0])).to.emit(token, 'ResourceAddedToToken') - .withArgs(tokenId, resId2, resId); - const pendingResources = await token.getPendingResources(tokenId); - - expect(await token.getResourceOverwrites(tokenId, pendingResources[0])).to.eql( - activeResources[0], - ); - await expect(token.acceptResource(tokenId, 0, resId2)).to.emit(token, 'ResourceAccepted') - .withArgs(tokenId, resId2, resId); - - const activeIds = await token.getActiveResources(tokenId); - expect(await renderUtils.getResourcesById(token.address, tokenId, activeIds)).to.eql([metaURIDefault]); - // Overwrite should be gone - expect(await token.getResourceOverwrites(tokenId, pendingResources[0])).to.eql( - ethers.BigNumber.from(0), - ); - }); - - it('can overwrite non existing resource to token, it could have been deleted', async function () { - const resId = 1; - const tokenId = 1; - - await token.mint(owner.address, tokenId); - await addResources([resId]); - await token.addResourceToToken(tokenId, resId, ethers.utils.hexZeroPad('0x1', 8)); - await token.acceptResource(tokenId, 0, resId); - - const activeIds = await token.getActiveResources(tokenId); - expect(await renderUtils.getResourcesById(token.address, tokenId, activeIds)).to.eql([metaURIDefault]); - }); - }); - - describe('Rejecting resources', async function () { - it('can reject resource if owner', async function () { - const { tokenOwner, tokenId } = await mintSampleToken(); - const approved = tokenOwner; - - await checkRejectFromAddress(approved, tokenId); - }); - - it('can reject resource if approved for resources', async function () { - const { tokenId } = await mintSampleToken(); - const approved = addrs[1]; - - await token.approveForResources(approved.address, tokenId); - await checkRejectFromAddress(approved, tokenId); - }); - - it('can reject resource if approved for resources for all', async function () { - const { tokenId } = await mintSampleToken(); - const approved = addrs[2]; - - await token.setApprovalForAllForResources(approved.address, true); - await checkRejectFromAddress(approved, tokenId); - }); - - it('can reject all resources if owner', async function () { - const { tokenOwner, tokenId } = await mintSampleToken(); - const approved = tokenOwner; - - await checkRejectAllFromAddress(approved, tokenId); - }); - - it('can reject all resources if approved for resources', async function () { - const { tokenId } = await mintSampleToken(); - const approved = addrs[1]; - - await token.approveForResources(approved.address, tokenId); - await checkRejectAllFromAddress(approved, tokenId); - }); - - it('can reject all resources if approved for resources for all', async function () { - const { tokenId } = await mintSampleToken(); - const approved = addrs[2]; - - await token.setApprovalForAllForResources(approved.address, true); - await checkRejectAllFromAddress(approved, tokenId); - }); - - it('can reject resource and overwrites are cleared', async function () { - const resId = 1; - const resId2 = 2; - const tokenId = 1; - - await token.mint(owner.address, tokenId); - await addResources([resId, resId2]); - await token.addResourceToToken(tokenId, resId, 0); - await token.acceptResource(tokenId, 0, resId); - - // Will try to overwrite but we reject it - await token.addResourceToToken(tokenId, resId2, resId); - await token.rejectResource(tokenId, 0, resId2); - - expect(await token.getResourceOverwrites(tokenId, resId2)).to.eql(ethers.BigNumber.from(0)); - }); - - it('can reject all resources and overwrites are cleared', async function () { - const resId = 1; - const resId2 = 2; - const tokenId = 1; - - await token.mint(owner.address, tokenId); - await addResources([resId, resId2]); - await token.addResourceToToken(tokenId, resId, 0); - await token.acceptResource(tokenId, 0, resId); - - // Will try to overwrite but we reject all - await token.addResourceToToken(tokenId, resId2, resId); - await token.rejectAllResources(tokenId, 1); - - expect(await token.getResourceOverwrites(tokenId, resId2)).to.eql(ethers.BigNumber.from(0)); - }); - - it('can reject all pending resources at max capacity', async function () { - const tokenId = 1; - const resArr = []; - - for (let i = 1; i < 128; i++) { - resArr.push(i); - } - - await token.mint(owner.address, tokenId); - await addResources(resArr); - - for (let i = 1; i < 128; i++) { - await token.addResourceToToken(tokenId, i, 1); - } - await token.rejectAllResources(tokenId, 128); - - expect(await token.getResourceOverwrites(1, 2)).to.eql(ethers.BigNumber.from(0)); - }); - - it('cannot reject resource twice', async function () { - const resId = 1; - const tokenId = 1; - - await token.mint(owner.address, tokenId); - await addResources([resId]); - await token.addResourceToToken(tokenId, resId, 0); - await token.rejectResource(tokenId, 0, resId); - }); - - it('cannot reject resource nor reject all if not owner', async function () { - const resId = 1; - const tokenId = 1; - - await token.mint(owner.address, tokenId); - await addResources([resId]); - await token.addResourceToToken(tokenId, resId, 0); - - await expect(token.connect(addrs[1]).rejectResource(tokenId, 0, resId)).to.be.revertedWith( - 'MultiResource: not owner or approved', - ); - await expect(token.connect(addrs[1]).rejectAllResources(tokenId, 1)).to.be.revertedWith( - 'MultiResource: not owner or approved', - ); - }); - - it('cannot reject non existing resource', async function () { - const tokenId = 1; - - await token.mint(owner.address, tokenId); - await expect(token.rejectResource(tokenId, 0, 1)).to.be.revertedWith( - 'MultiResource: index out of bounds', - ); - }); - }); - - describe('Priorities', async function () { - it('can set and get priorities', async function () { - const tokenId = 1; - await addResourcesToToken(tokenId); - - expect(await token.getActiveResourcePriorities(tokenId)).to.be.eql([0, 0]); - await expect(token.setPriority(tokenId, [2, 1])) - .to.emit(token, 'ResourcePrioritySet') - .withArgs(tokenId); - expect(await token.getActiveResourcePriorities(tokenId)).to.be.eql([2, 1]); - }); - - it('cannot set priorities for non owned token', async function () { - const tokenId = 1; - await addResourcesToToken(tokenId); - await expect(token.connect(addrs[1]).setPriority(tokenId, [2, 1])).to.be.revertedWith( - 'MultiResource: not owner or approved', - ); - }); - - it('cannot set different number of priorities', async function () { - const tokenId = 1; - await addResourcesToToken(tokenId); - await expect(token.connect(addrs[1]).setPriority(tokenId, [1])).to.be.revertedWith( - 'MultiResource: Bad priority list length', - ); - await expect(token.connect(addrs[1]).setPriority(tokenId, [2, 1, 3])).to.be.revertedWith( - 'MultiResource: Bad priority list length', - ); - }); - - it('cannot set priorities for non existing token', async function () { - const tokenId = 1; - await expect(token.connect(addrs[1]).setPriority(tokenId, [])).to.be.revertedWith( - 'MultiResource: approved query for nonexistent token', - ); - }); - }); - - describe('Approval Cleaning', async function () { - it('cleans token and resources approvals on transfer', async function () { - const tokenId = 1; - const tokenOwner = addrs[1]; - const newOwner = addrs[2]; - const approved = addrs[3]; - await token.mint(tokenOwner.address, tokenId); - await token.connect(tokenOwner).approve(approved.address, tokenId); - await token.connect(tokenOwner).approveForResources(approved.address, tokenId); - - expect(await token.getApproved(tokenId)).to.eql(approved.address); - expect(await token.getApprovedForResources(tokenId)).to.eql(approved.address); - - await token.connect(tokenOwner).transfer(newOwner.address, tokenId); - - expect(await token.getApproved(tokenId)).to.eql(ethers.constants.AddressZero); - expect(await token.getApprovedForResources(tokenId)).to.eql(ethers.constants.AddressZero); - }); - - it('cleans token and resources approvals on burn', async function () { - const tokenId = 1; - const tokenOwner = addrs[1]; - const approved = addrs[3]; - await token.mint(tokenOwner.address, tokenId); - await token.connect(tokenOwner).approve(approved.address, tokenId); - await token.connect(tokenOwner).approveForResources(approved.address, tokenId); - - expect(await token.getApproved(tokenId)).to.eql(approved.address); - expect(await token.getApprovedForResources(tokenId)).to.eql(approved.address); - - await token.connect(tokenOwner).burn(tokenId); - - await expect(token.getApproved(tokenId)).to.be.revertedWith( - 'MultiResource: approved query for nonexistent token', - ); - await expect(token.getApprovedForResources(tokenId)).to.be.revertedWith( - 'MultiResource: approved query for nonexistent token', - ); - }); - }); - - async function mintSampleToken(): Promise<{ tokenOwner: SignerWithAddress; tokenId: number }> { - const tokenOwner = owner; - const tokenId = 1; - await token.mint(tokenOwner.address, tokenId); - - return { tokenOwner, tokenId }; - } - - async function addResources(ids: number[]): Promise { - ids.forEach(async (resId) => { - await token.addResourceEntry(resId, metaURIDefault); - }); - } - - async function addResourcesToToken(tokenId: number): Promise { - const resId = 1; - const resId2 = 2; - await token.mint(owner.address, tokenId); - await addResources([resId, resId2]); - await token.addResourceToToken(tokenId, resId, 0); - await token.addResourceToToken(tokenId, resId2, 0); - await token.acceptResource(tokenId, 0, resId); - await token.acceptResource(tokenId, 0, resId2); - } - - async function checkAcceptFromAddress( - accepter: SignerWithAddress, - tokenId: number, - ): Promise { - const resId = 1; - - await addResources([resId]); - await token.addResourceToToken(tokenId, resId, 0); - await expect(token.connect(accepter).acceptResource(tokenId, 0, resId)) - .to.emit(token, 'ResourceAccepted') - .withArgs(tokenId, resId, 0); - - expect(await token.getPendingResources(tokenId)).to.be.eql([]); - - const activeIds = await token.getActiveResources(tokenId); - expect(await renderUtils.getResourcesById(token.address, tokenId, activeIds)).to.eql([metaURIDefault]); - } - - async function checkRejectFromAddress( - rejecter: SignerWithAddress, - tokenId: number, - ): Promise { - const resId = 1; - - await addResources([resId]); - await token.addResourceToToken(tokenId, resId, 0); - - await expect(token.connect(rejecter).rejectResource(tokenId, 0, resId)).to.emit( - token, - 'ResourceRejected', - ); - - expect(await token.getPendingResources(tokenId)).to.be.eql([]); - expect(await token.getActiveResources(tokenId)).to.be.eql([]); - } - - async function checkRejectAllFromAddress( - rejecter: SignerWithAddress, - tokenId: number, - ): Promise { - const resId = 1; - const resId2 = 2; - - await addResources([resId, resId2]); - await token.addResourceToToken(tokenId, resId, 0); - await token.addResourceToToken(tokenId, resId2, 0); - - await expect(token.connect(rejecter).rejectAllResources(tokenId, 2)).to.emit( - token, - 'ResourceRejected', - ); - - expect(await token.getPendingResources(tokenId)).to.be.eql([]); - expect(await token.getActiveResources(tokenId)).to.be.eql([]); - } -}); diff --git a/assets/eip-5773/test/renderUtils.ts b/assets/eip-5773/test/renderUtils.ts index 77bf776dcb90b4..63644c4d2df0f3 100644 --- a/assets/eip-5773/test/renderUtils.ts +++ b/assets/eip-5773/test/renderUtils.ts @@ -2,30 +2,30 @@ import { BigNumber } from 'ethers'; import { ethers } from 'hardhat'; import { expect } from 'chai'; import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; -import { MultiResourceTokenMock, MultiResourceRenderUtils } from '../typechain-types'; +import { MultiAssetTokenMock, MultiAssetRenderUtils } from '../typechain-types'; import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; function bn(x: number): BigNumber { return BigNumber.from(x); } -async function resourcesFixture() { - const multiresourceFactory = await ethers.getContractFactory('MultiResourceTokenMock'); - const renderUtilsFactory = await ethers.getContractFactory('MultiResourceRenderUtils'); +async function assetsFixture() { + const multiassetFactory = await ethers.getContractFactory('MultiAssetTokenMock'); + const renderUtilsFactory = await ethers.getContractFactory('MultiAssetRenderUtils'); - const multiresource = await multiresourceFactory.deploy('Chunky', 'CHNK'); - await multiresource.deployed(); + const multiasset = await multiassetFactory.deploy('Chunky', 'CHNK'); + await multiasset.deployed(); const renderUtils = await renderUtilsFactory.deploy(); await renderUtils.deployed(); - return { multiresource, renderUtils }; + return { multiasset, renderUtils }; } describe('Render Utils', async function () { let owner: SignerWithAddress; - let multiresource: MultiResourceTokenMock; - let renderUtils: MultiResourceRenderUtils; + let multiasset: MultiAssetTokenMock; + let renderUtils: MultiAssetRenderUtils; let tokenId: number; const resId = bn(1); @@ -34,53 +34,53 @@ describe('Render Utils', async function () { const resId4 = bn(4); before(async function () { - ({ multiresource, renderUtils } = await loadFixture(resourcesFixture)); + ({ multiasset, renderUtils } = await loadFixture(assetsFixture)); const signers = await ethers.getSigners(); owner = signers[0]; tokenId = 1; - await multiresource.mint(owner.address, tokenId); - await multiresource.addResourceEntry(resId, 'ipfs://res1.jpg'); - await multiresource.addResourceEntry(resId2, 'ipfs://res2.jpg'); - await multiresource.addResourceEntry(resId3, 'ipfs://res3.jpg'); - await multiresource.addResourceEntry(resId4, 'ipfs://res4.jpg'); - await multiresource.addResourceToToken(tokenId, resId, 0); - await multiresource.addResourceToToken(tokenId, resId2, 0); - await multiresource.addResourceToToken(tokenId, resId3, resId); - await multiresource.addResourceToToken(tokenId, resId4, 0); + await multiasset.mint(owner.address, tokenId); + await multiasset.addAssetEntry(resId, 'ipfs://res1.jpg'); + await multiasset.addAssetEntry(resId2, 'ipfs://res2.jpg'); + await multiasset.addAssetEntry(resId3, 'ipfs://res3.jpg'); + await multiasset.addAssetEntry(resId4, 'ipfs://res4.jpg'); + await multiasset.addAssetToToken(tokenId, resId, 0); + await multiasset.addAssetToToken(tokenId, resId2, 0); + await multiasset.addAssetToToken(tokenId, resId3, resId); + await multiasset.addAssetToToken(tokenId, resId4, 0); - await multiresource.acceptResource(tokenId, 0, resId); - await multiresource.acceptResource(tokenId, 1, resId2); - await multiresource.setPriority(tokenId, [10, 5]); + await multiasset.acceptAsset(tokenId, 0, resId); + await multiasset.acceptAsset(tokenId, 1, resId2); + await multiasset.setPriority(tokenId, [10, 5]); }); - describe('Render Utils MultiResource', async function () { - it('can get active resources', async function () { - expect(await renderUtils.getActiveResources(multiresource.address, tokenId)).to.eql([ + describe('Render Utils MultiAsset', async function () { + it('can get active assets', async function () { + expect(await renderUtils.getActiveAssets(multiasset.address, tokenId)).to.eql([ [resId, 10, 'ipfs://res1.jpg'], [resId2, 5, 'ipfs://res2.jpg'], ]); }); - it('can get pending resources', async function () { - expect(await renderUtils.getPendingResources(multiresource.address, tokenId)).to.eql([ + it('can get pending assets', async function () { + expect(await renderUtils.getPendingAssets(multiasset.address, tokenId)).to.eql([ [resId4, bn(0), bn(0), 'ipfs://res4.jpg'], [resId3, bn(1), resId, 'ipfs://res3.jpg'], ]); }); - it('can get top resource by priority', async function () { - expect(await renderUtils.getTopResourceMetaForToken(multiresource.address, tokenId)).to.eql( + it('can get top asset by priority', async function () { + expect(await renderUtils.getTopAssetMetaForToken(multiasset.address, tokenId)).to.eql( 'ipfs://res2.jpg', ); }); - it('cannot get top resource if token has no resources', async function () { + it('cannot get top asset if token has no assets', async function () { const otherTokenId = 2; - await multiresource.mint(owner.address, otherTokenId); + await multiasset.mint(owner.address, otherTokenId); await expect( - renderUtils.getTopResourceMetaForToken(multiresource.address, otherTokenId), - ).to.be.revertedWith('Token has no resources'); + renderUtils.getTopAssetMetaForToken(multiasset.address, otherTokenId), + ).to.be.revertedWith('Token has no assets'); }); }); -}); \ No newline at end of file +}); From 05306844f58158365b9f1f47cd21b5e156b08a15 Mon Sep 17 00:00:00 2001 From: Jan Turk Date: Fri, 18 Nov 2022 23:11:34 +0100 Subject: [PATCH 17/21] Fix license identifier & Specification contract title --- EIPS/eip-5773.md | 2 +- assets/eip-5773/contracts/IMultiAsset.sol | 2 +- assets/eip-5773/contracts/MultiAssetToken.sol | 2 +- assets/eip-5773/contracts/library/MultiAssetLib.sol | 2 +- assets/eip-5773/contracts/mocks/ERC721ReceiverMock.sol | 2 +- assets/eip-5773/contracts/mocks/MultiAssetTokenMock.sol | 2 +- assets/eip-5773/contracts/mocks/NonReceiverMock.sol | 2 +- assets/eip-5773/contracts/utils/MultiAssetRenderUtils.sol | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/EIPS/eip-5773.md b/EIPS/eip-5773.md index 294cdbbbb37b55..4ea3636b4f2643 100644 --- a/EIPS/eip-5773.md +++ b/EIPS/eip-5773.md @@ -73,7 +73,7 @@ Alternative example of this, could be version control of an IoT device's firmwar 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. ```solidity -/// @title ERC-5773 Multi-Asset context-dependent tokens +/// @title EIP-5773 Multi-Asset context-dependent tokens /// @dev See https://eips.ethereum.org/EIPS/eip-5773 /// @dev Note: the ERC-165 identifier for this interface is 0xfa73a1e2. diff --git a/assets/eip-5773/contracts/IMultiAsset.sol b/assets/eip-5773/contracts/IMultiAsset.sol index 19457f9f18c969..f370306153343c 100644 --- a/assets/eip-5773/contracts/IMultiAsset.sol +++ b/assets/eip-5773/contracts/IMultiAsset.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: CC0 +// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.0; diff --git a/assets/eip-5773/contracts/MultiAssetToken.sol b/assets/eip-5773/contracts/MultiAssetToken.sol index 4fcc8031c1b128..c097989c0a0e4c 100644 --- a/assets/eip-5773/contracts/MultiAssetToken.sol +++ b/assets/eip-5773/contracts/MultiAssetToken.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: CC0 +// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.15; diff --git a/assets/eip-5773/contracts/library/MultiAssetLib.sol b/assets/eip-5773/contracts/library/MultiAssetLib.sol index 0f5438a4e4a9a6..c858c816a2fe58 100644 --- a/assets/eip-5773/contracts/library/MultiAssetLib.sol +++ b/assets/eip-5773/contracts/library/MultiAssetLib.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: CC0 +// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.0; diff --git a/assets/eip-5773/contracts/mocks/ERC721ReceiverMock.sol b/assets/eip-5773/contracts/mocks/ERC721ReceiverMock.sol index fd37b9e100b24f..1f0665b8c304ca 100644 --- a/assets/eip-5773/contracts/mocks/ERC721ReceiverMock.sol +++ b/assets/eip-5773/contracts/mocks/ERC721ReceiverMock.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: CC0 +// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.15; diff --git a/assets/eip-5773/contracts/mocks/MultiAssetTokenMock.sol b/assets/eip-5773/contracts/mocks/MultiAssetTokenMock.sol index c6bb64dddfd402..77f7025b98a55f 100644 --- a/assets/eip-5773/contracts/mocks/MultiAssetTokenMock.sol +++ b/assets/eip-5773/contracts/mocks/MultiAssetTokenMock.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: CC0 +// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.15; diff --git a/assets/eip-5773/contracts/mocks/NonReceiverMock.sol b/assets/eip-5773/contracts/mocks/NonReceiverMock.sol index 90ccc503cf28b6..e9d2d6ed3ecb2e 100644 --- a/assets/eip-5773/contracts/mocks/NonReceiverMock.sol +++ b/assets/eip-5773/contracts/mocks/NonReceiverMock.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: CC0 +// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.15; diff --git a/assets/eip-5773/contracts/utils/MultiAssetRenderUtils.sol b/assets/eip-5773/contracts/utils/MultiAssetRenderUtils.sol index f762af1034f5e2..799e27aeb2b5ce 100644 --- a/assets/eip-5773/contracts/utils/MultiAssetRenderUtils.sol +++ b/assets/eip-5773/contracts/utils/MultiAssetRenderUtils.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: CC0-1.0 import "../IMultiAsset.sol"; From b2d3efac6ff3fe70fa1bc3c295ef0353ca9c94dc Mon Sep 17 00:00:00 2001 From: Jan Turk Date: Mon, 21 Nov 2022 00:21:59 +0100 Subject: [PATCH 18/21] Fix indexes of Rationale questions --- EIPS/eip-5773.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/EIPS/eip-5773.md b/EIPS/eip-5773.md index 4ea3636b4f2643..9bc950d59aa0e2 100644 --- a/EIPS/eip-5773.md +++ b/EIPS/eip-5773.md @@ -436,7 +436,7 @@ An asset is defined as something that is owned by a person, company, or organiza For consistency. This proposal extends EIP-721 which already uses 1 transaction for approving operations with tokens. It would be inconsistent to have this and also support signing messages for operations with assets. -2. **Why use indexes?** +3. **Why use indexes?** To reduce the gas consumption. If the asset ID was used to find which token to accept or reject, iteration over arrays would be required and the cost of the operation would depend on the size of the active or pending children arrays. With the index, the cost is fixed. A list of active and pending children arrays per token need to be maintained, since methods to get them are part of the proposed interface. @@ -444,15 +444,15 @@ To avoid race conditions in which the index of a asset changes, the expected ass Implementation that would internally keep track of indices using mapping was attempted. The average cost of adding a asset to a token increased by over 25%, costs of accepting and rejecting assets also increased 4.6% and 7.1% respectively. We concluded that it is not necessary for this proposal and can be implemented as an extension for use cases willing to accept this cost. In the sample implementation provided, there are several hooks which make this possible. -3. **Why is a method to get all the assets not included?** +4. **Why is a method to get all the assets not included?** Getting all assets might not be an operation necessary for all implementers. Additionally, it can be added either as an extension, doable with hooks, or can be emulated using an indexer. -4. **Why is pagination not included?** +5. **Why is pagination not included?** Asset IDs use `uint64`, testing has confirmed that the limit of IDs you can read before reaching the gas limit is around 30.000. This is not expected to be a common use case so it is not a part of the interface. However, an implementer can create an extension for this use case if needed. -5. **How does this proposal differ from the other proposals trying to address a similar problem?** +6. **How does this proposal differ from the other proposals trying to address a similar problem?** After reviewing them, we concluded that each contains at least one of these limitations: From b0f75d928e4ccd7a08e17e6ee5f9aaf86ea3a933 Mon Sep 17 00:00:00 2001 From: Jan Turk Date: Mon, 21 Nov 2022 12:22:18 +0100 Subject: [PATCH 19/21] Minor polishing of the rationale --- EIPS/eip-5773.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5773.md b/EIPS/eip-5773.md index 9bc950d59aa0e2..cfb71fabc8b8d4 100644 --- a/EIPS/eip-5773.md +++ b/EIPS/eip-5773.md @@ -438,7 +438,7 @@ For consistency. This proposal extends EIP-721 which already uses 1 transaction 3. **Why use indexes?** -To reduce the gas consumption. If the asset ID was used to find which token to accept or reject, iteration over arrays would be required and the cost of the operation would depend on the size of the active or pending children arrays. With the index, the cost is fixed. A list of active and pending children arrays per token need to be maintained, since methods to get them are part of the proposed interface. +To reduce the gas consumption. If the asset ID was used to find which asset to accept or reject, iteration over arrays would be required and the cost of the operation would depend on the size of the active or pending assets arrays. With the index, the cost is fixed. A list of active and pending assets arrays per token need to be maintained, since methods to get them are part of the proposed interface. To avoid race conditions in which the index of a asset changes, the expected asset ID is included in operations requiring asset index, to verify that the asset being accessed using the index is the expected asset. From 3d6c7ecc9434c6be5130d2132ebda73a28b20ad4 Mon Sep 17 00:00:00 2001 From: Jan Turk Date: Mon, 21 Nov 2022 23:40:42 +0100 Subject: [PATCH 20/21] Update references to assets to use an istead of a in Rationale --- EIPS/eip-5773.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/EIPS/eip-5773.md b/EIPS/eip-5773.md index cfb71fabc8b8d4..d035e8bee2aadc 100644 --- a/EIPS/eip-5773.md +++ b/EIPS/eip-5773.md @@ -464,9 +464,9 @@ After reviewing them, we concluded that each contains at least one of these limi Assets are stored within a token as an array of `uint64` identifiers. -In order to reduce redundant on-chain string storage, multi asset tokens store assets by reference via inner storage. A asset entry on the storage is stored via a `uint64` mapping to asset data. +In order to reduce redundant on-chain string storage, multi asset tokens store assets by reference via inner storage. An asset entry on the storage is stored via a `uint64` mapping to asset data. -A asset array is an array of these `uint64` asset ID references. +An asset array is an array of these `uint64` asset ID references. Such a structure allows that, a generic asset can be added to the storage one time, and a reference to it can be added to the token contract as many times as we desire. Implementers can then use string concatenation to procedurally generate a link to a content-addressed archive based on the base *SRC* in the asset and the *token ID*. Storing the asset in a new token will only take 16 bytes of storage in the asset array per token for recurrent as well as `tokenId` dependent assets. From 554ceed13c8d95295811511c147e907b82726bea Mon Sep 17 00:00:00 2001 From: Jan Turk Date: Fri, 25 Nov 2022 01:44:08 +0100 Subject: [PATCH 21/21] Final polishes --- EIPS/eip-5773.md | 41 ++++++------ assets/eip-5773/contracts/IMultiAsset.sol | 10 ++- assets/eip-5773/contracts/MultiAssetToken.sol | 66 +++++++++++-------- .../contracts/library/MultiAssetLib.sol | 15 ++--- .../contracts/utils/MultiAssetRenderUtils.sol | 10 +-- assets/eip-5773/test/multiasset.ts | 14 ++-- 6 files changed, 79 insertions(+), 77 deletions(-) diff --git a/EIPS/eip-5773.md b/EIPS/eip-5773.md index d035e8bee2aadc..d9a61a0808a988 100644 --- a/EIPS/eip-5773.md +++ b/EIPS/eip-5773.md @@ -60,7 +60,7 @@ When the server goes down or the game shuts down, the player ends up with nothin With Multi-Asset NFTs, a minter or another pre-approved entity is allowed to suggest a new asset to the NFT owner who can then accept it or reject it. The asset can even target an existing asset which is to be replaced. -Replacing an asset could, to some extent, be similar to replacing an EIP-721 token's URI. When an asset is replaced a clear line of traceability remains; the old asset is still reachable and verifiable. Overwriting a asset's metadata URI obscures this lineage. It also gives more trust to the token owner if the issuer cannot replace the asset of the NFT at will. The propose-accept asset replacement mechanic of this proposal provides this assurance. +Replacing an asset could, to some extent, be similar to replacing an EIP-721 token's URI. When an asset is replaced a clear line of traceability remains; the old asset is still reachable and verifiable. Replacing an asset's metadata URI obscures this lineage. It also gives more trust to the token owner if the issuer cannot replace the asset of the NFT at will. The propose-accept asset replacement mechanic of this proposal provides this assurance. 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 asset being added to the NFT, and once accepted, this new asset replaces the old one. @@ -75,47 +75,47 @@ The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL ```solidity /// @title EIP-5773 Multi-Asset context-dependent tokens /// @dev See https://eips.ethereum.org/EIPS/eip-5773 -/// @dev Note: the ERC-165 identifier for this interface is 0xfa73a1e2. +/// @dev Note: the ERC-165 identifier for this interface is 0xd1526708. pragma solidity ^0.8.16; interface IMultiAsset { /** - * @notice Used to notify listeners that a asset object is initialised at `assetId`. + * @notice Used to notify listeners that an asset object is initialised at `assetId`. * @param assetId ID of the asset that was initialised */ event AssetSet(uint64 assetId); /** - * @notice Used to notify listeners that a asset object at `assetId` is added to token's pending asset + * @notice Used to notify listeners that an asset object at `assetId` is added to token's pending asset * array. * @param tokenId ID of the token that received a new pending asset * @param assetId ID of the asset that has been added to the token's pending assets array - * @param overwritesId ID of the asset that would be overwritten + * @param replacesId ID of the asset that would be replaced */ event AssetAddedToToken( uint256 indexed tokenId, uint64 indexed assetId, - uint64 indexed overwritesId + uint64 indexed replacesId ); /** - * @notice Used to notify listeners that a asset object at `assetId` is accepted by the token and migrated + * @notice Used to notify listeners that an asset object at `assetId` is accepted by the token and migrated * from token's pending assets array to active assets array of the token. * @param tokenId ID of the token that had a new asset accepted * @param assetId ID of the asset that was accepted - * @param overwritesId ID of the asset that would be overwritten + * @param replacesId ID of the asset that was replaced */ event AssetAccepted( uint256 indexed tokenId, uint64 indexed assetId, - uint64 indexed overwritesId + uint64 indexed replacesId ); /** - * @notice Used to notify listeners that a asset object at `assetId` is rejected from token and is dropped + * @notice Used to notify listeners that an asset object at `assetId` is rejected from token and is dropped * from the pending assets array of the token. - * @param tokenId ID of the token that had a asset rejected + * @param tokenId ID of the token that had an asset rejected * @param assetId ID of the asset that was rejected */ event AssetRejected(uint256 indexed tokenId, uint64 indexed assetId); @@ -155,7 +155,7 @@ interface IMultiAsset { ); /** - * @notice Accepts a asset at from the pending array of given token. + * @notice Accepts an asset at from the pending array of given token. * @dev Migrates the asset from the token's pending asset array to the token's active asset array. * @dev Active assets cannot be removed by anyone, but can be replaced by a new asset. * @dev Requirements: @@ -175,7 +175,7 @@ interface IMultiAsset { ) external; /** - * @notice Rejects a asset from the pending array of given token. + * @notice Rejects an asset from the pending array of given token. * @dev Removes the asset from the token's pending asset array. * @dev Requirements: * @@ -204,8 +204,7 @@ interface IMultiAsset { * @param tokenId ID of the token of which to clear the pending array * @param maxRejections to prevent from rejecting assets which arrive just before this operation. */ - function rejectAllAssets(uint256 tokenId, uint256 maxRejections) - external; + function rejectAllAssets(uint256 tokenId, uint256 maxRejections) external; /** * @notice Sets a new priority array for a given token. @@ -263,7 +262,7 @@ interface IMultiAsset { returns (uint16[] memory); /** - * @notice Used to retrieve the asset that will be overriden if a given asset from the token's pending array + * @notice Used to retrieve the asset that will be replaced if a given asset from the token's pending array * is accepted. * @dev Asset data is stored by reference, in order to access the data corresponding to the ID, call * `getAssetMetadata(tokenId, assetId)`. @@ -271,7 +270,7 @@ interface IMultiAsset { * @param newAssetId ID of the pending asset which will be accepted * @return uint64 ID of the asset which will be replaced */ - function getAssetOverwrites(uint256 tokenId, uint64 newAssetId) + function getAssetReplacements(uint256 tokenId, uint64 newAssetId) external view returns (uint64); @@ -292,7 +291,7 @@ interface IMultiAsset { /** * @notice Used to grant permission to the user to manage token's assets. * @dev This differs from transfer approvals, as approvals are not cleared when the approved party accepts or - * rejects a asset, or sets asset priorities. This approval is cleared on token transfer. + * rejects an asset, or sets asset priorities. This approval is cleared on token transfer. * @dev Only a single account can be approved at a time, so approving the `0x0` address clears previous approvals. * @dev Requirements: * @@ -440,9 +439,9 @@ For consistency. This proposal extends EIP-721 which already uses 1 transaction To reduce the gas consumption. If the asset ID was used to find which asset to accept or reject, iteration over arrays would be required and the cost of the operation would depend on the size of the active or pending assets arrays. With the index, the cost is fixed. A list of active and pending assets arrays per token need to be maintained, since methods to get them are part of the proposed interface. -To avoid race conditions in which the index of a asset changes, the expected asset ID is included in operations requiring asset index, to verify that the asset being accessed using the index is the expected asset. +To avoid race conditions in which the index of an asset changes, the expected asset ID is included in operations requiring asset index, to verify that the asset being accessed using the index is the expected asset. -Implementation that would internally keep track of indices using mapping was attempted. The average cost of adding a asset to a token increased by over 25%, costs of accepting and rejecting assets also increased 4.6% and 7.1% respectively. We concluded that it is not necessary for this proposal and can be implemented as an extension for use cases willing to accept this cost. In the sample implementation provided, there are several hooks which make this possible. +Implementation that would internally keep track of indices using mapping was attempted. The average cost of adding an asset to a token increased by over 25%, costs of accepting and rejecting assets also increased 4.6% and 7.1% respectively. We concluded that it is not necessary for this proposal and can be implemented as an extension for use cases willing to accept this cost. In the sample implementation provided, there are several hooks which make this possible. 4. **Why is a method to get all the assets not included?** @@ -474,7 +473,7 @@ Structuring token's assets in such a way allows for URIs to be derived programma ### Propose-Commit pattern for asset addition -Adding assets to an existing token MUST be done in the form of a propose-commit pattern to allow for limited mutability by a 3rd party. When adding a asset to a token, it is first placed in the *"Pending"* array, and MUST be migrated to the *"Active"* array by the token's owner. The *"Pending"* assets array SHOULD be limited to 128 slots to prevent spam and griefing. +Adding assets to an existing token MUST be done in the form of a propose-commit pattern to allow for limited mutability by a 3rd party. When adding an asset to a token, it is first placed in the *"Pending"* array, and MUST be migrated to the *"Active"* array by the token's owner. The *"Pending"* assets array SHOULD be limited to 128 slots to prevent spam and griefing. ### Asset management diff --git a/assets/eip-5773/contracts/IMultiAsset.sol b/assets/eip-5773/contracts/IMultiAsset.sol index f370306153343c..a8453ccd26c664 100644 --- a/assets/eip-5773/contracts/IMultiAsset.sol +++ b/assets/eip-5773/contracts/IMultiAsset.sol @@ -8,13 +8,13 @@ interface IMultiAsset { event AssetAddedToToken( uint256 indexed tokenId, uint64 indexed assetId, - uint64 indexed overwritesId + uint64 indexed replacesId ); event AssetAccepted( uint256 indexed tokenId, uint64 indexed assetId, - uint64 indexed overwritesId + uint64 indexed replacesId ); event AssetRejected(uint256 indexed tokenId, uint64 indexed assetId); @@ -45,8 +45,7 @@ interface IMultiAsset { uint64 assetId ) external; - function rejectAllAssets(uint256 tokenId, uint256 maxRejections) - external; + function rejectAllAssets(uint256 tokenId, uint256 maxRejections) external; function setPriority(uint256 tokenId, uint16[] calldata priorities) external; @@ -66,7 +65,7 @@ interface IMultiAsset { view returns (uint16[] memory); - function getAssetOverwrites(uint256 tokenId, uint64 newAssetId) + function getAssetReplacements(uint256 tokenId, uint64 newAssetId) external view returns (uint64); @@ -76,7 +75,6 @@ interface IMultiAsset { view returns (string memory); - // Approvals function approveForAssets(address to, uint256 tokenId) external; function getApprovedForAssets(uint256 tokenId) diff --git a/assets/eip-5773/contracts/MultiAssetToken.sol b/assets/eip-5773/contracts/MultiAssetToken.sol index c097989c0a0e4c..86d051ede66c80 100644 --- a/assets/eip-5773/contracts/MultiAssetToken.sol +++ b/assets/eip-5773/contracts/MultiAssetToken.sol @@ -46,7 +46,7 @@ contract MultiAssetToken is Context, IERC721, IMultiAsset { mapping(uint64 => string) internal _assets; //mapping of tokenId to new asset, to asset to be replaced - mapping(uint256 => mapping(uint64 => uint64)) private _assetOverwrites; + mapping(uint256 => mapping(uint64 => uint64)) private _assetReplacements; //mapping of tokenId to all assets mapping(uint256 => uint64[]) internal _activeAssets; @@ -315,7 +315,7 @@ contract MultiAssetToken is Context, IERC721, IMultiAsset { function _burn(uint256 tokenId) internal virtual { // WARNING: If you intend to allow the reminting of a burned token, you // might want to clean the assets for the token, that is: - // _pendingAssets, _activeAssets, _assetOverwrites + // _pendingAssets, _activeAssets, _assetReplacements // _activeAssetPriorities and _tokenAssets. address owner = ownerOf(tokenId); @@ -457,21 +457,31 @@ contract MultiAssetToken is Context, IERC721, IMultiAsset { ); _beforeAcceptAsset(tokenId, index, assetId); + uint64 replacesId = _assetReplacements[tokenId][assetId]; + uint256 replaceIndex; + bool replacefound; + if (replacesId != uint64(0)) + (replaceIndex, replacefound) = _activeAssets[tokenId].indexOf( + replacesId + ); + + if (replacefound) { + // We don't want to remove and then push a new asset. + // This way we also keep the priority of the original resource + _activeAssets[tokenId][index] = assetId; + delete _tokenAssets[tokenId][replacesId]; + } else { + // We use the current size as next priority, by default priorities would be [0,1,2...] + _activeAssetPriorities[tokenId].push( + uint16(_activeAssets[tokenId].length) + ); + _activeAssets[tokenId].push(assetId); + replacesId = uint64(0); + } _pendingAssets[tokenId].removeItemByIndex(index); + delete _assetReplacements[tokenId][assetId]; - uint64 overwrite = _assetOverwrites[tokenId][assetId]; - if (overwrite != uint64(0)) { - // It could have been overwritten previously so it's fine if it's not found. - // If it's not deleted (not found), we don't want to send it on the event - if (!_activeAssets[tokenId].removeItemByValue(overwrite)) - overwrite = uint64(0); - else delete _tokenAssets[tokenId][overwrite]; - delete (_assetOverwrites[tokenId][assetId]); - } - _activeAssets[tokenId].push(assetId); - //Push 0 value of uint16 to array, e.g., uninitialized - _activeAssetPriorities[tokenId].push(uint16(0)); - emit AssetAccepted(tokenId, assetId, overwrite); + emit AssetAccepted(tokenId, assetId, replacesId); _afterAcceptAsset(tokenId, index, assetId); } @@ -494,9 +504,9 @@ contract MultiAssetToken is Context, IERC721, IMultiAsset { ); _beforeRejectAsset(tokenId, index, assetId); - _pendingAssets[tokenId].removeItemByValue(assetId); + _pendingAssets[tokenId].removeItemByIndex(index); delete _tokenAssets[tokenId][assetId]; - delete _assetOverwrites[tokenId][assetId]; + delete _assetReplacements[tokenId][assetId]; emit AssetRejected(tokenId, assetId); _afterRejectAsset(tokenId, index, assetId); @@ -517,7 +527,7 @@ contract MultiAssetToken is Context, IERC721, IMultiAsset { _beforeRejectAllAssets(tokenId); for (uint256 i; i < len; ) { uint64 assetId = _pendingAssets[tokenId][i]; - delete _assetOverwrites[tokenId][assetId]; + delete _assetReplacements[tokenId][assetId]; unchecked { ++i; } @@ -576,13 +586,13 @@ contract MultiAssetToken is Context, IERC721, IMultiAsset { return _activeAssetPriorities[tokenId]; } - function getAssetOverwrites(uint256 tokenId, uint64 newAssetId) + function getAssetReplacements(uint256 tokenId, uint64 newAssetId) public view virtual returns (uint64) { - return _assetOverwrites[tokenId][newAssetId]; + return _assetReplacements[tokenId][newAssetId]; } function getAssetMetadata(uint256 tokenId, uint64 assetId) @@ -621,7 +631,7 @@ contract MultiAssetToken is Context, IERC721, IMultiAsset { function _addAssetToToken( uint256 tokenId, uint64 assetId, - uint64 overwrites + uint64 replacesAssetWithId ) internal { require( !_tokenAssets[tokenId][assetId], @@ -638,16 +648,16 @@ contract MultiAssetToken is Context, IERC721, IMultiAsset { "MultiAsset: Max pending assets reached" ); - _beforeAddAssetToToken(tokenId, assetId, overwrites); + _beforeAddAssetToToken(tokenId, assetId, replacesAssetWithId); _tokenAssets[tokenId][assetId] = true; _pendingAssets[tokenId].push(assetId); - if (overwrites != uint64(0)) { - _assetOverwrites[tokenId][assetId] = overwrites; + if (replacesAssetWithId != uint64(0)) { + _assetReplacements[tokenId][assetId] = replacesAssetWithId; } - emit AssetAddedToToken(tokenId, assetId, overwrites); - _afterAddAssetToToken(tokenId, assetId, overwrites); + emit AssetAddedToToken(tokenId, assetId, replacesAssetWithId); + _afterAddAssetToToken(tokenId, assetId, replacesAssetWithId); } // HOOKS @@ -665,13 +675,13 @@ contract MultiAssetToken is Context, IERC721, IMultiAsset { function _beforeAddAssetToToken( uint256 tokenId, uint64 assetId, - uint64 overwrites + uint64 replacesAssetWithId ) internal virtual {} function _afterAddAssetToToken( uint256 tokenId, uint64 assetId, - uint64 overwrites + uint64 replacesAssetWithId ) internal virtual {} function _beforeAcceptAsset( diff --git a/assets/eip-5773/contracts/library/MultiAssetLib.sol b/assets/eip-5773/contracts/library/MultiAssetLib.sol index c858c816a2fe58..5aa3e505895ba3 100644 --- a/assets/eip-5773/contracts/library/MultiAssetLib.sol +++ b/assets/eip-5773/contracts/library/MultiAssetLib.sol @@ -3,22 +3,21 @@ pragma solidity ^0.8.0; library MultiAssetLib { - function removeItemByValue(uint64[] storage array, uint64 value) + function indexOf(uint64[] memory A, uint64 a) internal - returns (bool) + pure + returns (uint256, bool) { - uint64[] memory memArr = array; //Copy array to memory, check for gas savings here - uint256 length = memArr.length; //gas savings + uint256 length = A.length; for (uint256 i; i < length; ) { - if (memArr[i] == value) { - removeItemByIndex(array, i); - return true; + if (A[i] == a) { + return (i, true); } unchecked { ++i; } } - return false; + return (0, false); } //For reasource storage array diff --git a/assets/eip-5773/contracts/utils/MultiAssetRenderUtils.sol b/assets/eip-5773/contracts/utils/MultiAssetRenderUtils.sol index 799e27aeb2b5ce..418c3c934f2308 100644 --- a/assets/eip-5773/contracts/utils/MultiAssetRenderUtils.sol +++ b/assets/eip-5773/contracts/utils/MultiAssetRenderUtils.sol @@ -33,9 +33,7 @@ contract MultiAssetRenderUtils { IMultiAsset target_ = IMultiAsset(target); uint64[] memory assets = target_.getActiveAssets(tokenId); - uint16[] memory priorities = target_.getActiveAssetPriorities( - tokenId - ); + uint16[] memory priorities = target_.getActiveAssetPriorities(tokenId); uint256 len = assets.length; if (len == 0) { revert("Token has no assets"); @@ -76,7 +74,7 @@ contract MultiAssetRenderUtils { uint64 overwritesAssetWithId; for (uint256 i; i < len; ) { metadata = target_.getAssetMetadata(tokenId, assets[i]); - overwritesAssetWithId = target_.getAssetOverwrites( + overwritesAssetWithId = target_.getAssetReplacements( tokenId, assets[i] ); @@ -126,9 +124,7 @@ contract MultiAssetRenderUtils { returns (string memory) { IMultiAsset target_ = IMultiAsset(target); - uint16[] memory priorities = target_.getActiveAssetPriorities( - tokenId - ); + uint16[] memory priorities = target_.getActiveAssetPriorities(tokenId); uint64[] memory assets = target_.getActiveAssets(tokenId); uint256 len = priorities.length; if (len == 0) { diff --git a/assets/eip-5773/test/multiasset.ts b/assets/eip-5773/test/multiasset.ts index cd129c7e34fbc4..0685a44e78792e 100644 --- a/assets/eip-5773/test/multiasset.ts +++ b/assets/eip-5773/test/multiasset.ts @@ -57,7 +57,7 @@ describe('MultiAsset', async () => { }); it('can support IMultiAsset', async function () { - expect(await token.supportsInterface('0xfa73a1e2')).to.equal(true); + expect(await token.supportsInterface('0xd1526708')).to.equal(true); }); it('cannot support other interfaceId', async function () { @@ -341,7 +341,7 @@ describe('MultiAsset', async () => { .withArgs(tokenId, resId2, resId); const pendingAssets = await token.getPendingAssets(tokenId); - expect(await token.getAssetOverwrites(tokenId, pendingAssets[0])).to.eql(activeAssets[0]); + expect(await token.getAssetReplacements(tokenId, pendingAssets[0])).to.eql(activeAssets[0]); await expect(token.acceptAsset(tokenId, 0, resId2)) .to.emit(token, 'AssetAccepted') .withArgs(tokenId, resId2, resId); @@ -351,7 +351,7 @@ describe('MultiAsset', async () => { metaURIDefault, ]); // Overwrite should be gone - expect(await token.getAssetOverwrites(tokenId, pendingAssets[0])).to.eql( + expect(await token.getAssetReplacements(tokenId, pendingAssets[0])).to.eql( ethers.BigNumber.from(0), ); }); @@ -433,7 +433,7 @@ describe('MultiAsset', async () => { await token.addAssetToToken(tokenId, resId2, resId); await token.rejectAsset(tokenId, 0, resId2); - expect(await token.getAssetOverwrites(tokenId, resId2)).to.eql(ethers.BigNumber.from(0)); + expect(await token.getAssetReplacements(tokenId, resId2)).to.eql(ethers.BigNumber.from(0)); }); it('can reject all assets and overwrites are cleared', async function () { @@ -450,7 +450,7 @@ describe('MultiAsset', async () => { await token.addAssetToToken(tokenId, resId2, resId); await token.rejectAllAssets(tokenId, 1); - expect(await token.getAssetOverwrites(tokenId, resId2)).to.eql(ethers.BigNumber.from(0)); + expect(await token.getAssetReplacements(tokenId, resId2)).to.eql(ethers.BigNumber.from(0)); }); it('can reject all pending assets at max capacity', async function () { @@ -469,7 +469,7 @@ describe('MultiAsset', async () => { } await token.rejectAllAssets(tokenId, 128); - expect(await token.getAssetOverwrites(1, 2)).to.eql(ethers.BigNumber.from(0)); + expect(await token.getAssetReplacements(1, 2)).to.eql(ethers.BigNumber.from(0)); }); it('cannot reject asset twice', async function () { @@ -513,7 +513,7 @@ describe('MultiAsset', async () => { const tokenId = 1; await addAssetsToToken(tokenId); - expect(await token.getActiveAssetPriorities(tokenId)).to.be.eql([0, 0]); + expect(await token.getActiveAssetPriorities(tokenId)).to.be.eql([0, 1]); await expect(token.setPriority(tokenId, [2, 1])) .to.emit(token, 'AssetPrioritySet') .withArgs(tokenId);