From c8c1c20836cc4e3b5faafc521ca94155da58d843 Mon Sep 17 00:00:00 2001 From: Marco <82735893+mpeyfuss@users.noreply.github.com> Date: Tue, 28 Nov 2023 08:03:24 -0700 Subject: [PATCH 01/23] Update ERC-7160: Move to Final Merged by EIP-Bot. --- ERCS/erc-7160.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ERCS/erc-7160.md b/ERCS/erc-7160.md index 57328fded0..c871256001 100644 --- a/ERCS/erc-7160.md +++ b/ERCS/erc-7160.md @@ -4,8 +4,7 @@ title: ERC-721 Multi-Metadata Extension description: Multiple metadata URIs per token, with the option to pin a primary URI. author: 0xG (@0xGh), Marco Peyfuss (@mpeyfuss) discussions-to: https://ethereum-magicians.org/t/erc721-multi-metadata-extension/14629 -status: Last Call -last-call-deadline: 2023-11-27 +status: Final type: Standards Track category: ERC created: 2023-06-09 From 6da67fa9ea8d5de2d7c4684c23482f5ee8d1d22c Mon Sep 17 00:00:00 2001 From: Francisco Date: Tue, 28 Nov 2023 12:04:00 -0300 Subject: [PATCH 02/23] Update ERC-7201: Move to Final Merged by EIP-Bot. --- ERCS/erc-7201.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ERCS/erc-7201.md b/ERCS/erc-7201.md index 35562f3255..05c4d07d5e 100644 --- a/ERCS/erc-7201.md +++ b/ERCS/erc-7201.md @@ -4,8 +4,7 @@ title: Namespaced Storage Layout description: Conventions for the storage location of structs in the namespaced storage pattern. author: Francisco Giordano (@frangio), Hadrien Croubois (@Amxx), Ernesto García (@ernestognw), Eric Lau (@ericglau) discussions-to: https://ethereum-magicians.org/t/eip-7201-namespaced-storage-layout/14796 -status: Last Call -last-call-deadline: 2023-10-01 +status: Final type: Standards Track category: ERC created: 2023-06-20 From 6d4ab9de715e5de4b6134bf2dd5bc252fd2bf10f Mon Sep 17 00:00:00 2001 From: David Leung <265220+dhl@users.noreply.github.com> Date: Tue, 28 Nov 2023 23:27:40 +0800 Subject: [PATCH 03/23] Update ERC-6596: Move to Review Merged by EIP-Bot. --- ERCS/erc-6596.md | 696 ++++++++++++++++++----------------------------- 1 file changed, 269 insertions(+), 427 deletions(-) diff --git a/ERCS/erc-6596.md b/ERCS/erc-6596.md index 3ef296f880..8915c457e0 100644 --- a/ERCS/erc-6596.md +++ b/ERCS/erc-6596.md @@ -1,10 +1,10 @@ --- eip: 6596 -title: Historical Asset Metadata JSON Schema -description: Metadata JSON Schema extension to enhance the discoverability, connectivity, and collectability of historically significant NFTs. -author: Phillip Pon , Gary Liu , Henry Chan , Joey Liu , Lauren Ho , Jeff Leung , Yvan Fatal , Brian Liang , Seungyong Moon , Joyce Li , Phoebe Kwok , Avir Mahtani , Zeon Chan , Antoine Cote , David Leung (@dhl) +title: Cultural and Historical Asset Token +description: Metadata extension to enhance the discoverability, connectivity, and collectability of culturally and historically significant NFTs. +author: Phillip Pon , Gary Liu , Henry Chan , Joey Liu , Lauren Ho , Jeff Leung , Brian Liang , Joyce Li , Avir Mahtani , Antoine Cote (@acote88), David Leung (@dhl) discussions-to: https://ethereum-magicians.org/t/eip-6596-historical-asset-metadata-json-schema/13090 -status: Stagnant +status: Review type: Standards Track category: ERC created: 2023-02-28 @@ -13,51 +13,57 @@ requires: 721, 1155 ## Abstract -This EIP proposes the establishment of a new metadata standard for Historical Asset Tokens (HATs) on the Ethereum -platform. HATs are tokens that represent a specific historical asset, such as a collectible or a rare item, and provide -comprehensive context and provenance needed to establish historical significance and value. +This EIP proposes the establishment of a comprehensive metadata standard for Cultural and Historical Asset Tokens +(CHATs) on the Ethereum platform. These tokens represent cultural and historical assets such as artwork, artifacts, +collectibles, and rare items, providing crucial context and provenance to substantiate their significance and value. -HAT is the metadata standard and smart contract for historical NFTs. While existing NFT standards offer the mechanism to -ensure immutability and decentralised ownership of assets on the blockchain, we believe a rich metadata standard (that -includes data for the underlying asset from its moment of creation to its moment of conversion) will imbue historical -NFTs with the comprehensive context and provenance needed to establish historical significance and value. Additionally, -the standard will enhance the discoverability, connectivity, and collectability of all HATs. +While existing NFT standards ensure the immutability and decentralized ownership of assets on the blockchain, based on +our research they do not adequately capture the cultural and historical importance and value of such assets needed for +widespread adoption by institutions such as museums. The CHAT standard aims to overcome these limitations by preserving +the provenance, history, and evolving context of cultural and historical assets, thus substantiating their value. +Furthermore, it incentivises museums, institutions, and asset owners to create tamper-proof records on the blockchain, +ensuring transparency and accountability and accelerating adoption of web3 protocols. Additionally, the CHAT standard +promotes interoperability with existing metadata standards in the arts and cultural sector, facilitating the search, +discovery, and connection of distributed assets. ## Motivation -In recent years, the market for collectible and rare items has experienced significant growth, with many people looking -to buy, sell, and trade these assets in an efficient and secure manner. However, the current market for historical -assets is often plagued by issues such as fraud, counterfeiting, and lack of transparency. - -The creation of a standard for HATs on the Ethereum platform has the potential to address these issues and provide a -secure and transparent marketplace for historical assets. By representing historical assets as tokens on the blockchain, -it becomes possible to create a permanent, tamper-proof record of ownership and transfer of these assets, while also -providing a level of transparency and security that is not currently available in traditional markets. - -The standard is proposed as the number and variety of NFT projects increases. The current [ERC-721](./eip-721.md) has a -modest metadata -extension, optionally allowing for the inclusion of “name” and “symbol” functions to identify NFT -collections, and for "name", "descriptions", and “image” attributes to represent assets. Metadata plays a big role in -facilitating the search and discovery of NFTs on marketplaces. As the number and variety of NFT projects increases, -disconnected and limited metadata structures makes it difficult for collectors to navigate through the ocean of NFT -assets on major marketplaces and make sense of their value. - -The provenance and history of artworks, artifacts and historical IP can take different forms and be created by -different issuers. While there is no consolidated archive, a standardised metadata structure provides connectivity -across NFTs associated with historical and cultural assets, regardless of the medium and issuer. For example, a video -clip issued by a local filmmaker about the 1997 handover of Hong Kong can be connected to the news coverage by South -China Morning Post (SCMP) of the Sino-British Joint Declaration via the HAT metadata. While well-informed collectors may -not need this programmed connectivity, a standardised metadata for HATs will make these connections easily accessible -for all collectors, scholars, and interested parties. - -Provenance and context are extremely important for historical assets. The archival and research of significant artworks, -objects, and collections are ongoing; it is a collaborative effort between creators, artist estates, galleries, auction -houses, scholars and institutions. The consolidation of information and knowledge is not only crucial to the -understanding of our cultural heritage; the richer the context, the more valuable the asset is to collectors. A shared -and standardised metadata structure enriches the context of an NFT. With the establishment of HATs, it is our hope that -this standardised structure will contribute to the evolution of cultural heritage, and fully capture the context of -culturally and historically significant assets, thereby ensuring relevant and fair value for all historically -significant NFTs. +**Preserving context and significance** - Provenance and context are crucial for cultural and historical assets. The +CHAT standard captures and preserves the provenance and history of these assets, as well as the changing contexts that +emerge from new knowledge and information. This context and provenance substantiate the significance and value of +cultural and historical assets. + +**Proof-based preservation** - The recent incidents of lost artifacts and data breaches at a number of significant +international museums points to a need in reassessing our current record keeping mechanisms. While existing systems +mostly operate on trust, blockchain technology offers opportunities to establish permanent and verifiable records in a +proof-based environment. Introducing the CHAT standard on the Ethereum platform enables museums, institutions, and +owners of significant collections to create tamper-proof records on the blockchain. By representing these valuable +cultural and historical assets as tokens on the blockchain, permanent and tamper-proof records can be established +whenever amendments are made, ensuring greater transparency and accountability. + +**Interoperability** - The proposed standard addresses the multitude of existing metadata standards used in the arts and +cultural sector. The vision is to create a metadata structure specifically built for preservation on the blockchain that +is interoperable with these existing standards and compliant with the Open Archives Initiative (OAI) as well as the +International Image Interoperability Framework protocol (IIIF). + +**Search and Discovery** - Ownership and history of artworks, artifacts, and historical intellectual properties are +often distributed. Although there may never be a fully consolidated archive, a formalized blockchain-based metadata +structure enables consolidation for search and discovery of the assets, without consolidating the ownership. For +example, an artifact from an archaeological site of the Silk Road can be connected with Buddhist paintings, statues, and +texts about the ancient trade route across museum and institutional collections internationally. The proposed CHAT +metadata structure will facilitate easy access to these connections for the general public, researchers, scholars, other +cultural professionals, brands, media, and any other interested parties. + +Currently, the [ERC-721](./eip-721.md) standard includes a basic metadata extension, which optionally provides functions +for identifying NFT collections ("name" and "symbol") and attributes for representing assets ("name," "description," +and "image"). However, to provide comprehensive context and substantiate the value of tokenized assets, NFT issuers +often create their own metadata structures. We believe that the basic extension alone is insufficient to capture the +context and significance of cultural and historical assets. The lack of interoperable and consistent rich metadata +hinders users' ability to search, discover, and connect tokenized assets on the blockchain. While connectivity among +collections may not be crucial for NFTs designed for games and memberships, it is of utmost importance for cultural and +historical assets. As the number and diversity of tokenized assets on the blockchain increase, it becomes essential to +establish a consistent and comprehensive metadata structure that provides context, substantiates value, and enables +connected search and discovery at scale. ## Specification @@ -65,378 +71,194 @@ The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "S RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174. This EIP extends [ERC-721](./eip-721.md) and [ERC-1155](./eip-1155.md) with 48 additional properties to capture the -historical significance of the underlying asset. +cultural and historical significance of the underlying asset. Compatible contracts, besides implementing the relevant metadata schemas ("Metadata JSON Schema" for [ERC-721](./eip-721.md) contracts or "Metadata URI JSON Schema" for [ERC-1155](./eip-1155.md) contracts), must implement the following metadata interface. -### Historical Asset Metadata Extension TypeScript Interface +### Cultural and Historical Asset Metadata Extension TypeScript Interface -The following TypeScript interface defines the mandatory and optional properties compatible tokens must conform to: +The following TypeScript interface defines the Metadata JSON Schema compatible tokens must conform to: ```typescript interface HistoricalAssetMetadata { - name: string; // Name of the HAT - description: string, // Full description of the HAT to provide the historical context - image: string; // A URI pointing to a resource with mime type image/* to serve as the cover image of the HAT - properties: { - id: string; // An unambiguous identifier of the HAT - summary: string; // Short description of the HAT, no more than 200 characters - assetType: // The type of the underlying asset - "newspaper_cover" - | "magazine_a1_cover" - | "newspaper_article" - | "magazine_article" - | "photo" - | "graphic" - | "video" - | "audio" - | "3d_object" - | "others"; - issuers: string[]; // Organizations or individuals who created the HAT - issueTimestamp: string; // The date and time the HAT was issued in ISO 8601 format - edition: number; // Unique serial number of the HAT - editionCount: number; // Total number of editions available for this HAT - fileURI: string; // Link to the digital file representing the HAT - fileSize: number; // Size of the digital file of the HAT in bytes - fileFormat: string; // MIME type of the digital file of the HAT - seriesName?: string; // The name of the series this HAT is a part of - assetFullText?: string; // The full text in the underlying asset of the HAT - assetCreators?: string[]; // Organizations or individuals who created the underlying asset - earliestPossibleCreationDate?: string; // Earliest possible creation date of the underlying asset in ISO 8601 date format - latestPossibleCreationDate: string; // Latest possible creation date of the underlying asset in ISO 8601 date format - assetCreationGeos?: string; // Country, subdivision, and city where the underlying asset was created. Reference to ISO 3166-2 standard for the short name of the country and subdivision. Utilize the official name for the city if it is not covered in the ISO subdivision - assetCreationLocations?: string[]; // Specific cities and named locations where the underlying asset was created - assetCreationCoordinates?: string[]; // Coordinates of the location where the underlying asset was created - relevantDates?: string[]; // Dates, in ISO 8601 date format, that are referenced and important to the significance of the HAT - relevantGeos?: string[]; // Country, subdivision, and city that are referenced and important to the significance of the HAT. Reference to ISO 3166-2 standard for the short name of the country and subdivision. Utilize the official name for the city if it is not covered in the ISO subdivision - relevantLocations?: string[]; // Specific cities and named locations that are referenced and important to the significance of the HAT - relevantPeople?: string[]; // Individuals that are referenced and important to the significance of the HAT - relevantEntities?: string[]; // Entities that are referenced and important to the significance of the HAT - assetLanguages?: string[]; // Languages used in the underlying asset. Reference to ISO 639 for code or macrolanguage names - assetHeight?: string; // Height of the underlying asset - assetWidth?: string; // Width of the underlying asset - assetDepth?: string; // Depth of the underlying asset - assetFileURI?: string; // Link to a high quality file of the underlying asset - assetFileSize?: number; // Size of the digital file of the underlying asset in bytes - assetCopyrightHolder?: string; // Copyright holder of the underlying asset - assetCopyrightDocumentURI?: string; // Link to the legal contract that outlines the copyright of the underlying asset - assetProvenanceRecordURI?: string; // Link to the existing provenance record documents of the underlying asset - isPhysicalAsset?: boolean; // Flags whether the asset is tied to a physical asset - assetMedium?: string; // The material used to create the physical underlying asset - assetFormFormat?: string; // The physical form or the digital format of the underlying asset. For digital format, a MIME type should be specified. - issuerNotes?: string; // Issuer's notes regarding the HAT and its underlying asset - tokenReplaced?: string; // Token identifier of the token this HAT replaced - tokenReferenced?: string; // Token identifier of the token this HAT referenced, or is a derivative of - isOwnerTokenCopyrightRightHolder?: boolean; // Flags whether the HAT token holder is the copyright owner of the HAT. Does not include the copyright to the underlying asset - tokenCopyrightRightDocumentURI?: string; // Link to legal document outlining the rights of the token owner. Specific dimensions include the right to display a work via digital and physical mediums, present the work publicly, create or sell copies of the work, and create or sell derivations from the HAT - isOwnerAssetCopyrightRightHolder?: boolean; // Flags whether the HAT token holder is the copyright owner of the underlying asset. Does not include the copyright to the underlying asset - assetCopyrightRightDocumentURI?: string; // Link to legal document outlining the rights of the token owner. Specific dimensions include the right to display a work via digital and physical mediums, present the work publicly, create or sell copies of the work, and create or sell derivations from the underlying asset - isOwnerTokenReprintRightHolder?: boolean; // Flags whether the token owner has non-exclusive reprint/second serial rights to the HAT. Does not include the rights to the underlying asset - tokenReprintRightDocumentURI?: string; // Link to legal document outlining the token owner’s reprint/second serial rights of the HAT - isOwnerAssetReprintRightHolder?: boolean; // Flags whether the token owner has non-exclusive reprint/second serial rights to the underlying asset. Does not include the rights to the underlying asset - assetReprintRightDocumentURI?: string; // Link to legal document outlining the token owner’s reprint/second serial rights of the underlying asset - isEligibleForRelatedTokenAirdrops?: boolean; // Flags whether the token holder is eligible for related HAT airdrops - isEligibleForMetaverseAccess?: boolean; // Flags whether the token holder can gain access to the HAT metaverse - } + name?: string; // Name of the CHAT + description?: string; // Full description of the CHAT to provide the cultural and historical + // context + image?: string; // A URI pointing to a resource with mime type image/* to serve as the + // cover image of the CHAT + attributes?: CHATAttribute[]; // A list of attributes to describe the CHAT. Attribute object may be + // repeated if a field has multiple values + attributesExt?: ExtendedCHATAttribute[]; // A list of extended attributes to describe the CHAT, not to be + // displayed. Attribute object may be repeated if a field has + // multiple values } -``` -### Historical Asset Metadata JSON Schema +type CHATAttribute = + { trait_type: "Catalogue Level", value: string } + | { trait_type: "Publication / Creation Date", value: string } + | { trait_type: "Creator Name", value: string } + | { trait_type: "Creator Bio", value: string } + | { trait_type: "Asset Type", value: string } + | { trait_type: "Classification", value: string } + | { trait_type: "Materials and Technology", value: string } + | { trait_type: "Subject Matter", value: string } + | { trait_type: "Edition", value: string } + | { trait_type: "Series name", value: string } + | { trait_type: "Dimensions Unit", value: string } + | { trait_type: "Dimensions (height)", value: number } + | { trait_type: "Dimensions (width)", value: number } + | { trait_type: "Dimensions (depth)", value: number } + | { trait_type: "Inscriptions / Marks", value: string } + | { trait_type: "Credit Line", value: string } + | { trait_type: "Current Owner", value: string } + | { trait_type: "Provenance", value: string } + | { trait_type: "Acquisition Date", value: string } + | { trait_type: "Citation", value: string } + | { trait_type: "Keyword", value: string } + | { trait_type: "Copyright Holder", value: string } + | { trait_type: "Bibliography", value: string } + | { trait_type: "Issuer", value: string } + | { trait_type: "Issue Timestamp", value: string } + | { trait_type: "Issuer Description", value: string } + | { trait_type: "Asset File Size", value: number } + | { trait_type: "Asset File Format", value: string } + | { trait_type: "Copyright / Restrictions", value: string } + | { trait_type: "Asset Creation Geo", value: string } + | { trait_type: "Asset Creation Location", value: string } + | { trait_type: "Asset Creation Coordinates", value: string } + | { trait_type: "Relevant Date", value: string } + | { trait_type: "Relevant Geo", value: string } + | { trait_type: "Relevant Location", value: string } + | { trait_type: "Relevant Person", value: string } + | { trait_type: "Relevant Entity", value: string } + | { trait_type: "Asset Language", value: string } + | { trait_type: "Is Physical Asset", value: boolean } + +type ExtendedCHATAttribute = + { trait_type: "Asset Full Text", value: string } + | { trait_type: "Exhibition / Loan History", value: string } + | { trait_type: "Copyright Document", value: string } + | { trait_type: "Provenance Document", value: string } + | { trait_type: "Asset URL", value: string } + | { trait_type: "Copyright Document of Underlying Asset", value: string } +``` -The following JSON Schema enforces the constraints set out in the above interface definition. Tokens adopting the -Historical Asset Metadata JSON Schema extension should be validated against the JSON schema prior to minting: +#### CHATAttribute Description + +| trait_type | description | +|-----------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Catalogue Level | An indication of the level of cataloging represented by the record, based on the physical form or intellectual content of the material | +| Publication / Creation Date | Earliest possible creation date of the underlying asset in ISO 8601 date format | +| Creator Name | The name, brief biographical information, and roles (if necessary) of the named or anonymous individuals or corporate bodies responsible for the design, production, manufacture, or alteration of the work, presented in a syntax suitable for display to the end-user and including any necessary indications of uncertainty, ambiguity, and nuance. If there is no known creator, make a reference to the presumed culture or nationality of the unknown creator | +| Creator Bio | The brief biography or description of creator | +| Asset Type | The type of the underlying asset | +| Classification | Classification terms or codes are used to place a work of art or architecture in a useful organizational scheme that has been devised by a repository, collector, or other person or entity. Formal classification systems are used to relate a work of art or architecture to broader, narrower, and related objects. Classification terms group similar works together according to varying criteria | +| Materials and Technology | The materials and/or techniques used to create the physical underlying asset | +| Subject Matter | Indexing terms that characterize in general terms what the work depicts or what is depicted in it. This subject analysis is the minimum required. It is recommended to also list specific subjects, if possible | +| Edition | Edition of the original work | +| Series Name | The name of the series the asset is a part of | +| Dimensions Unit | Unit of the measurement of the dimension of the asset | +| Dimensions (height) | Height of the underlying asset | +| Dimensions (width) | Width of the underlying asset | +| Dimensions (depth) | Depth of the underlying asset | +| Credit Line | Crediting details of the source or origin of an image or content being used publicly. The credit line typically includes important details such as the name of the museum, the title or description of the artwork or object, the artist's name (if applicable), the date of creation, and any other relevant information that helps identify and contextualize the work | +| Inscriptions / Marks | A description of distinguishing or identifying physical markings, lettering, annotations, texts, or labels that are a part of a work or are affixed, applied, stamped, written, inscribed, or attached to the work, excluding any mark or text inherent in materials (record watermarks in MATERIALS AND TECHNIQUES) | +| Current Owner | Name of the current owner | +| Provenance | Provenance provides crucial information about the artwork's authenticity, legitimacy, and historical significance. It includes details such as the names of previous owners, dates of acquisition, locations where the artwork or artifact resided, and any significant events or transactions related to its ownership | +| Acquisition Date | The date on which the acquirer obtained the asset | +| Citation | Citations of the asset in publications, journals, and any other medium | +| Keyword | Keywords that are relevant for researchers | +| Copyright Holder | Copyright holder of the underlying asset | +| Bibliography | Information on where this asset has been referenced, cited, consulted, and for what purpose | +| Issuer | Issuer of the token | +| Issue Timestamp | Date of token creation | +| Issuer Description | Brief description of the issuing party | +| Asset File Size | Size of the digital file of the underlying asset in bytes | +| Asset File Format | The physical form or the digital format of the underlying asset. For digital format, a MIME type should be specified | +| Copyright / Restrictions | The copyright status the work is under | +| Asset Creation Geo | Country, subdivision, and city where the underlying asset was created. Reference to ISO 3166-2 standard for the short name of the country and subdivision. Utilize the official name for the city if it is not covered in the ISO subdivision | +| Asset Creation Location | Specific cities and named locations where the underlying asset was created | +| Asset Creation Coordinates | Coordinates of the location where the underlying asset was created | +| Relevant Date | Dates, in ISO 8601 date format, referenced in, and important to the significance of the CHAT | +| Relevant Geo | Country, subdivision, and city CHATs are referenced and important to the significance of the CHAT. Reference to ISO 3166-2 standard for the short name of the country and subdivision. Utilize the official name for the city if it is not covered in the ISO subdivision | +| Relevant Location | Specific cities and named locations referenced in, and important to the significance of the CHAT | +| Relevant Person | Individuals referenced in, and important to the significance of the CHAT | +| Relevant Entity | Entities referenced in, and important to the significance of the CHAT | +| Asset Language | Languages used in the underlying asset. Reference to ISO 639 for code or macrolanguage names | +| Is Physical Asset | Flags whether the asset is tied to a physical asset | + +#### ExtendedCHATAttribute Description + +| trait_type | description | +|----------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Asset Full Text | The full text in the underlying asset of the CHAT | +| Exhibition / Loan History | Including exhibition/loan description, dates, title, type, curator, organizer, sponsor, venue | +| Copyright Document | A URI pointing to the legal contract CHATs outlines the copyright of the underlying asset | +| Provenance Document | A URI pointing to the existing provenance record documents of the underlying asset | +| Asset URL | A URI pointing to a high-quality file of the underlying asset | +| Copyright Document of Underlying Asset | A URI pointing to legal document outlining the rights of the token owner. Specific dimensions include the right to display a work via digital and physical mediums, present the work publicly, create or sell copies of the work, and create or sell derivations from the underlying asset | + +#### Example + +To illustrate the use of the CHAT metadata extension, we provide an example of a CHAT metadata JSON file for the famous +Japanese woodblock print "Under the Wave off Kanagawa" by Katsushika Hokusai, which is currently held by the Art +Institute of Chicago. + +The metadata format is compatible with the [ERC-721](./eip-721.md) and OpenSea style metadata format. ```json { - "title": "Historical Asset Metadata", - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Name of the HAT" - }, - "description": { - "type": "string", - "description": "Full description of the HAT to provide the historical context" - }, - "image": { - "type": "string", - "description": "A URI pointing to a resource with mime type image/* to serve as the cover image of the HAT", - "format": "uri" - }, - "properties": { - "type": "object", - "description": "Historical asset attributes", - "properties": { - "id": { - "type": "string", - "description": "An unambiguous identifier of the HAT" - }, - "summary": { - "type": "string", - "description": "Short description of the HAT, no more than 200 characters" - }, - "assetType": { - "type": "string", - "enum": [ - "newspaper_cover", - "magazine_a1_cover", - "newspaper_article", - "magazine_article", - "photo", - "graphic", - "video", - "audio", - "3d_object", - "others" - ], - "description": "The type of the underlying asset" - }, - "issuers": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Organizations or individuals who created the HAT" - }, - "issueTimestamp": { - "type": "string", - "description": "The date and time the HAT was issued in ISO 8601 format", - "format": "date-time" - }, - "edition": { - "type": "integer", - "description": "Unique serial number of the HAT" - }, - "editionCount": { - "type": "integer", - "description": "Total number of editions available for this HAT" - }, - "fileURI": { - "type": "string", - "description": "Link to the digital file representing the HAT", - "format": "uri" - }, - "fileSize": { - "type": "integer", - "description": "Size of the digital file of the HAT in bytes" - }, - "fileFormat": { - "type": "string", - "description": "MIME type of the digital file of the HAT" - }, - "seriesName": { - "type": "string", - "description": "The name of the series this HAT is a part of" - }, - "assetFullText": { - "type": "string", - "description": "The full text in the underlying asset of the HAT" - }, - "assetCreators": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Organizations or individuals who created the underlying asset" - }, - "earliestPossibleCreationDate": { - "type": "string", - "description": "Earliest possible creation date of the underlying asset in ISO 8601 date format", - "format": "date" - }, - "latestPossibleCreationDate": { - "type": "string", - "format": "date", - "description": "Latest possible creation date of the underlying asset in ISO 8601 date format" - }, - "assetCreationGeos": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Country, subdivision, and city where the underlying asset was created. Reference to ISO 3166-2 standard for the short name of the country and subdivision. Utilize the official name for the city if it is not covered in the ISO subdivision" - }, - "assetCreationLocations": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Specific cities and named locations where the underlying asset was created" - }, - "assetCreationCoordinates": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Coordinates of the location where the underlying asset was created" - }, - "relevantDates": { - "type": "array", - "items": { - "type": "string", - "description": "Dates, in ISO 8601 date format, that are referenced and important to the significance of the HAT", - "format": "date" - } - }, - "relevantGeos": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Country, subdivision, and city that are referenced and important to the significance of the HAT. Reference to ISO 3166-2 standard for the short name of the country and subdivision. Utilize the official name for the city if it is not covered in the ISO subdivision" - }, - "relevantLocations": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Specific cities and named locations that are referenced and important to the significance of the HAT" - }, - "relevantPeople": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Individuals that are referenced and important to the significance of the HAT" - }, - "relevantEntities": { - "type": "string", - "description": "Entities that are referenced and important to the significance of the HAT" - }, - "assetLanguages": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Languages used in the underlying asset. Reference to ISO 639 for code or macrolanguage names" - }, - "assetHeight": { - "type": "string", - "description": "Height of the underlying asset" - }, - "assetWidth": { - "type": "string", - "description": "Width of the underlying asset" - }, - "assetDepth": { - "type": "string", - "description": "Depth of the underlying asset" - }, - "assetFileURI": { - "type": "string", - "description": "Link to a high quality file of the underlying asset", - "format": "uri" - }, - "assetFileSize": { - "type": "integer", - "description": "Size of the digital file of the underlying asset in bytes" - }, - "assetCopyrightHolder": { - "type": "string", - "description": "Copyright holder of the underlying asset" - }, - "assetCopyrightDocumentURI": { - "type": "string", - "description": "Link to the legal contract that outlines the copyright of the underlying asset", - "format": "uri" - }, - "assetProvenanceRecordURI": { - "type": "string", - "description": "Link to the existing provenance record documents of the underlying asset", - "format": "uri" - }, - "isPhysicalAsset": { - "type": "boolean", - "description": "Flags whether the asset is tied to a physical asset" - }, - "assetMedium": { - "type": "string", - "description": "The material used to create the physical underlying asset" - }, - "assetFormFormat": { - "type": "string", - "description": "The physical form or the digital format of the underlying asset" - }, - "issuerNotes": { - "type": "string", - "description": "Issuer's notes regarding the HAT and its underlying asset" - }, - "tokenReplaced": { - "type": "string", - "description": "Token identifier of the token this HAT replaced" - }, - "tokenReferenced": { - "type": "string", - "description": "Token identifier of the token this HAT referenced, or is a derivative of" - }, - "isOwnerTokenCopyrightRightHolder": { - "type": "boolean", - "description": "Flags whether or not the HAT token holder is the copyright owner of the underlying asset. Does not include the copyright to the underlying asset" - }, - "tokenCopyrightRightDocumentURI": { - "type": "string", - "description": "Link to legal document outlining the rights of the token owner. Specific dimensions include the right to display a work via digital and physical mediums, present the work publicly, create or sell copies of the work, and create or sell derivations from the HAT", - "format": "uri" - }, - "isOwnerAssetCopyrightRightHolder": { - "type": "boolean", - "description": "Flags whether or not the HAT token holder is the copyright owner of the underlying asset. Does not include the copyright to the underlying asset" - }, - "assetCopyrightRightDocumentURI": { - "type": "string", - "description": "Link to legal document outlining the rights of the token owner. Specific dimensions include the right to display a work via digital and physical mediums, present the work publicly, create or sell copies of the work, and create or sell derivations from the underlying asset", - "format": "uri" - }, - "isOwnerTokenReprintRightHolder": { - "type": "boolean", - "description": "Flags whether or not the HAT token owner has non-exclusive reprint/second serial rights to the HAT. Does not include the rights to the underlying asset" - }, - "tokenReprintRightDocumentURI": { - "type": "string", - "description": "Link to legal document outlining the token owner’s reprint/second serial rights of the HAT", - "format": "uri" - }, - "isOwnerAssetReprintRightHolder": { - "type": "boolean", - "description": "Flags whether or not the HAT token owner has non-exclusive reprint/second serial rights to the underlying asset. Does not include the rights to the underlying asset" - }, - "assetReprintRightDocumentURI": { - "type": "string", - "description": "Link to legal document outlining the token owner’s reprint/second serial rights of the underlying asset", - "format": "uri" - }, - "isEligibleForRelatedTokenAirdrops": { - "type": "boolean", - "description": "Flags whether or not the HAT token holder is eligible for related HAT airdrops" - }, - "isEligibleForMetaverseAccess": { - "type": "boolean", - "description": "Flags whether or not the HAT token holder can gain access to the HAT metaverse" - } - }, - "required": [ - "assetType", - "id", - "issuers", - "summary", - "issueTimestamp", - "edition", - "editionCount", - "fileURI", - "fileSize", - "fileFormat", - "latestPossibleCreationDate" - ] - } - }, - "required": [ - "name", - "description", - "image", - "properties" + "name": "Under the Wave off Kanagawa (Kanagawa oki nami ura), also known as The Great Wave, from the series “Thirty-Six Views of Mount Fuji (Fugaku sanjūrokkei)", + "description": "Katsushika Hokusai’s much celebrated series, Thirty-Six Views of Mount Fuji (Fugaku sanjûrokkei), was begun in 1830, when the artist was 70 years old. This tour-de-force series established the popularity of landscape prints, which continues to this day. Perhaps most striking about the series is Hokusai’s copious use of the newly affordable Berlin blue pigment, featured in many of the compositions in the color for the sky and water. Mount Fuji is the protagonist in each scene, viewed from afar or up close, during various weather conditions and seasons, and from all directions.\n\nThe most famous image from the set is the “Great Wave” (Kanagawa oki nami ura), in which a diminutive Mount Fuji can be seen in the distance under the crest of a giant wave. The three impressions of Hokusai’s Great Wave in the Art Institute are all later impressions than the first state of the design.", + "image": "ipfs://bafybeiav6sqcgzxk5h5afnmb3iisgma2kpnyj5fa5gnhozwaqwzlayx6se", + "attributes": [ + { "trait_type": "Publication / Creation Date", "value": "1826/1836" }, + { "trait_type": "Creator Name", "value": "Katsushika Hokusai" }, + { "trait_type": "Creator Bio", "value": "Katsushika Hokusai’s woodblock print The Great Wave is one of the most famous and recognizable works of art in the world. Hokusai spent the majority of his life in the capital of Edo, now Tokyo, and lived in a staggering 93 separate residences. Despite this frenetic movement, he produced tens of thousands of sketches, prints, illustrated books, and paintings. He also frequently changed the name he used to sign works of art, and each change signaled a shift in artistic style and intended audience." }, + { "trait_type": "Asset Type", "value": "Painting" }, + { "trait_type": "Classification", "value": "Arts of Asia" }, + { "trait_type": "Materials and Technology", "value": "Color woodblock print, oban" }, + { "trait_type": "Subject Matter", "value": "Asian Art" }, + { "trait_type": "Subject Matter", "value": "Edo Period (1615-1868)" }, + { "trait_type": "Subject Matter", "value": "Ukiyo-e Style" }, + { "trait_type": "Subject Matter", "value": "Woodblock Prints" }, + { "trait_type": "Subject Matter", "value": "Japan 1800-1900 A.D." }, + { "trait_type": "Edition", "value": "1" }, + { "trait_type": "Series name", "value": "Thirty-Six Views of Mount Fuji (Fugaku sanjûrokkei)" }, + { "trait_type": "Dimensions Unit", "value": "cm" }, + { "trait_type": "Dimensions (height)", "value": 25.4 }, + { "trait_type": "Dimensions (width)", "value": 37.6 }, + { "trait_type": "Inscriptions / Marks", "value": "Signature: Hokusai aratame Iitsu fude" }, + { "trait_type": "Inscriptions / Marks", "value": "Publisher: Nishimura-ya Yohachi" }, + { "trait_type": "Credit Line", "value": "Clarence Buckingham Collection" }, + { "trait_type": "Current Owner", "value": "Art Institute of Chicago" }, + { "trait_type": "Provenance", "value": "Yamanaka, New York by 1905" }, + { "trait_type": "Provenance", "value": "Sold to Clarence Buckingham, Chicago by 1925" }, + { "trait_type": "Provenance", "value": "Kate S. Buckingham, Chicago, given to the Art Institute of Chicago, 1925." }, + { "trait_type": "Acquisition Date", "value": "1925" }, + { "trait_type": "Citation", "value": "James Cuno, The Art Institute of Chicago: The Essential Guide, rev. ed. (Art Institute of Chicago, 2009) p. 100." }, + { "trait_type": "Citation", "value": "James N. Wood, The Art Institute of Chicago: The Essential Guide, rev. ed. (Art Institute of Chicago, 2003), p. 86." }, + { "trait_type": "Citation", "value": "Jim Ulak, Japanese Prints (Art Institute of Chicago, 1995), p. 268." }, + { "trait_type": "Citation", "value": "Ukiyo-e Taikei (Tokyo, 1975), vol. 8, 29; XIII, I." }, + { "trait_type": "Citation", "value": "Matthi Forrer, Hokusai (Royal Academy of Arts, London 1988), p. 264." }, + { "trait_type": "Citation", "value": "Richard Lane, Hokusai: Life and Work (London, 1989), pp. 189, 192." }, + { "trait_type": "Copyright Holder", "value": "Public domain" }, + { "trait_type": "Copyright / Restrictions", "value": "CC0" }, + { "trait_type": "Asset Creation Geo", "value": "Japan" }, + { "trait_type": "Asset Creation Location", "value": "Tokyo (Edo)" }, + { "trait_type": "Asset Creation Coordinates", "value": "36.2048° N, 138.2529° E" }, + { "trait_type": "Relevant Date", "value": "18th Century" }, + { "trait_type": "Relevant Geo", "value": "Japan, Chicago" }, + { "trait_type": "Relevant Location", "value": "Art Institute of Chicago" }, + { "trait_type": "Relevant Person", "value": "Katsushika Hokusai" }, + { "trait_type": "Relevant Person", "value": "Yamanaka" }, + { "trait_type": "Relevant Person", "value": "Clarence Buckingham" }, + { "trait_type": "Relevant Person", "value": "Kate S. Buckingham" }, + { "trait_type": "Relevant Entity", "value": "Art Institute of Chicago, Clarence Buckingham Collection" }, + { "trait_type": "Asset Language", "value": "Japanese" }, + { "trait_type": "Is Physical Asset", "value": true } ] } ``` @@ -445,26 +267,45 @@ Historical Asset Metadata JSON Schema extension should be validated against the ### Choosing to Extend Off-Chain Metadata JSON Schema over On-Chain Interface -Both the [ERC-721](./eip-721.md) and [ERC-1155](./eip-1155.md) provides natural extension point in the metadata JSON -file associated with NFTs to provide enriched dataset about the underlying assets. +Both the [ERC-721](./eip-721.md) and [ERC-1155](./eip-1155.md) provide natural extension points in the metadata JSON +file associated with NFTs to supply enriched datasets about the underlying assets. + +Providing enriched datasets through off-chain metadata JSON files allows existing NFT contracts to adopt the new +metadata structure proposed in this EIP without upgrading or migrating. The off-chain design enables flexible and +progressive enhancement of any NFT collections to adopt this standard gradually. This approach allows NFT collections to +be deployed using already-audited and battle-tested smart contract code without creating or adapting new smart +contracts, reducing the risk associated with adopting and implementing a new standard. -Providing enriched dataset through off-chain metadata JSON files allow already-deployed NFT contracts to adopt the -metadata structure in this EIP without upgrade or migration. The off-chain design allows for flexible and progressive -enhancement of any NFT collections to adopt standard such as this EIP, and to progressively enhance and enrich the -collection over time. +### Capturing Attributes Extensions in `attributes` and `attributesExt` properties -By avoiding the need to newly create or adapt smart contracts, NFT collections can be deployed using audited and battle -tested smart contract code, thus reducing the risk of adopting and implementing a new standard. +In the design of the Cultural and Historical Asset Token (CHAT) metadata extension, we have made a deliberate choice to +capture the metadata attributes between two main properties: `attributes` and `attributesExt`. This division serves +two distinct purposes while ensuring maximum compatibility with existing NFT galleries and marketplaces. -### Capturing Attributes Extensions in `properties` property +**1. `attributes` Property** -The [ERC-1155](./eip-1155.md) standard has a `properties` property in its JSON Metadata Schema to define an arbitrary -set of additional metadata or properties to describe an underlying asset. Defining the metadata fields in this EIP over -this properties preserve the overall shape of the metadata schema of the [ERC-1155](./eip-1155.md), minimizing the -change needed to list and process the proposed properties. +The `attributes` property contains core metadata attributes that are integral to the identity and categorization of +CHATs. These attributes are meant to be readily accessible, displayed, and searchable by NFT galleries and marketplaces. +By placing fundamental details such as the CHAT's name, description, image, and other key characteristics +in `attributes`, we ensure that these essential elements can be easily presented to users, collectors, and researchers. +This approach allows CHATs to seamlessly integrate with existing NFT platforms and marketplaces without requiring major +modifications. -For tokens minted against the [ERC-721](./eip-721.md) standard, only one additional property needs to be added to the -metadata JSON object to take advantage of the standardized fields, simplifying the adoption of this standard. +**2. `attributesExt` Property** + +The `attributesExt` property, on the other hand, is dedicated to extended attributes that provide valuable, in-depth +information about a CHAT but are not typically intended for display or search within NFT galleries and marketplaces. +These extended attributes serve purposes such as archival documentation, provenance records, and additional context that +may not be immediately relevant to a casual observer or collector. By isolating these extended attributes +in `attributesExt`, we strike a balance between comprehensiveness and user-friendliness. This approach allows CHAT +creators to include rich historical and contextual data without overwhelming the typical user interface, making the +extended information available for scholarly or specialized use cases. + +This division of attributes into `attributes` and `attributesExt` ensures that the CHAT standard remains highly +compatible with existing NFT ecosystems, while still accommodating the specific needs of cultural and historical assets. +Users can enjoy a seamless experience in browsing and collecting CHATs, while researchers and historians have access to +comprehensive information when required, all within a framework that respects the practicalities of both user interfaces +and extended data documentation. ## Backwards Compatibility @@ -472,22 +313,23 @@ This EIP is fully backward compatible with [ERC-721](./eip-721.md) and [ERC-1155 ## Security Considerations -NFT platforms and systems working with Historical Asset Metadata JSON files are recommended to treat the files as client -supplied data and follow the appropriate best practices for processing such data. +NFT platforms and systems working with Cultural and Historical Asset Metadata JSON files are recommended to treat the +files as client-supplied data and follow the appropriate best practices for processing such data. -When processing the URI fields, backend systems should take care to prevent a malicious issuer exploiting these fields -to perform Server-Side Request Forgery (SSRF). +Specifically, when processing the URI fields, backend systems should take extra care to prevent a malicious issuer from +exploiting these fields to perform Server-Side Request Forgery (SSRF). Frontend or client-side systems are recommended to escape all control characters that may be exploited to perform Cross-Site Scripting (XSS). - -Processing systems are also recommended to take care with resource allocation to mitigate against buffer overflow due to -improper processing of variable data such as strings, arrays, and JSON objects. This is to prevent the systems from -becoming susceptible to Denial of Service (DOS) attack or circumventing security protection through arbitrary code -exception. + +Processing systems should manage resource allocation to prevent the systems from being vulnerable to Denial of Service ( +DOS) attacks or circumventing security protection through arbitrary code exceptions. Improper processing of variable +data, such as strings, arrays, and JSON objects, may result in a buffer overflow. Therefore, it is crucial to allocate +resources carefully to avoid such vulnerabilities. The metadata JSON files and the digital resources representing both the token and underlying assets should be stored in -a decentralized storage network to preserve integrity and to ensure available of data for long term preservation. +a decentralized storage network to preserve the integrity and to ensure the availability of data for long-term +preservation. Establishing the authenticity of the claims made in the Metadata JSON file is beyond the scope of this EIP, and is left to future EIPs to propose an appropriate protocol. From 707bc79e0a013d1784b005981909f6b9dc59f8a2 Mon Sep 17 00:00:00 2001 From: Joey <31974730+Joeysantoro@users.noreply.github.com> Date: Tue, 28 Nov 2023 07:29:59 -0800 Subject: [PATCH 04/23] Update ERC-7535: Move to Review Merged by EIP-Bot. --- ERCS/erc-7535.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/ERCS/erc-7535.md b/ERCS/erc-7535.md index 7f3bf663f6..f4a0d0070a 100644 --- a/ERCS/erc-7535.md +++ b/ERCS/erc-7535.md @@ -4,7 +4,7 @@ title: Native Asset ERC-4626 Tokenized Vault description: ERC-4626 Tokenized Vaults with Ether (Native Asset) as the underlying asset author: Joey Santoro (@joeysantoro) discussions-to: https://ethereum-magicians.org/t/eip-7535-eth-native-asset-tokenized-vault/16068 -status: Draft +status: Review type: Standards Track category: ERC created: 2023-10-12 @@ -57,7 +57,7 @@ Mints `shares` Vault shares to `receiver` by depositing exactly `msg.value` of E MUST have state mutability of `payable`. -MUST use `msg.value` as the primary input parameter for calculating the `shares` output. MAY ignore `assets` parameter as an input. +MUST use `msg.value` as the primary input parameter for calculating the `shares` output. I.e. MAY ignore `assets` parameter as an input. MUST emit the `Deposit` event. @@ -124,6 +124,14 @@ This standard was designed to maximize compatibility with ERC-4626 while minimiz All breaking implementation level changes with ERC-4626 are purely to accomodate for the usage of Ether or any native asset instead of an ERC-20 token. +### Allowing assets Parameter to be Ignored in a Deposit +`msg.value` must always be passed anyway to fund a `deposit`, therefore it may as well be treated as the primary input number. Allowing `assets` to be used either forces a strict equality and extra unnecessary gas overhead for redundancy, or allows different values which could cause footguns and undefined behavior. + +The last option which could work is to require that `assets` MUST be 0, but this still requires gas to enforce at the implementation level and can more easily be left unspecified, as the input is functionally ignorable in the spec as written. + +### Allowing msg.value to Not Equal assets Output in a Mint +There may be many cases where a user deposits slightly too much Ether in a `mint` call. In these cases, enforcing `msg.value` to equal `assets` would cause unnecessary reversions. It is up to the vault implementer to decide whether to refund or absorb any excess Ether, and up to depositors to deposit as close to the exact amount as possible. + ## Backwards Compatibility ERC-7535 is fully backward compatible with ERC-4626 at the function interface level. Certain implementation behaviors are different due to the fact that ETH is not ERC-20 compliant, such as the priority of `msg.value` over `assets`. From 9fcd3e24bf8cf6d0f4ecf6141933ed9ab940ff41 Mon Sep 17 00:00:00 2001 From: Sandy Sung <121860933+sandy-sung-lb@users.noreply.github.com> Date: Tue, 28 Nov 2023 23:30:37 +0800 Subject: [PATCH 05/23] Update ERC-7439: Move to Review Merged by EIP-Bot. --- ERCS/erc-7439.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ERCS/erc-7439.md b/ERCS/erc-7439.md index bbf2d7bc77..3a3dcfe15a 100644 --- a/ERCS/erc-7439.md +++ b/ERCS/erc-7439.md @@ -4,7 +4,7 @@ title: Prevent ticket touting description: An interface for customers to resell their tickets via authorized ticket resellers. author: LeadBest Consulting Group , Sandy Sung (@sandy-sung-lb), Mars Peng , Taien Wang discussions-to: https://ethereum-magicians.org/t/prevent-ticket-touting/15269 -status: Draft +status: Review type: Standards Track category: ERC created: 2023-07-28 From c6f3cbf4f0b03650fd38cedbd698b0f2b663758e Mon Sep 17 00:00:00 2001 From: g11tech Date: Tue, 28 Nov 2023 21:03:06 +0530 Subject: [PATCH 06/23] Config: Add g11tech to ERC editors (#94) --- config/eip-editors.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/eip-editors.yml b/config/eip-editors.yml index e404663bc8..8eb7e2fea4 100644 --- a/config/eip-editors.yml +++ b/config/eip-editors.yml @@ -16,6 +16,7 @@ erc: - SamWilsn - Pandapip1 - xinbenlv + - g11tech networking: - lightclient - axic From 6c59206c9a47de72115ee181deb58141896c7c3e Mon Sep 17 00:00:00 2001 From: Bofu Chen Date: Tue, 28 Nov 2023 23:33:21 +0800 Subject: [PATCH 07/23] Add ERC: Content Consent for AI/ML Data Mining Merged by EIP-Bot. --- ERCS/erc-7517.md | 222 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 ERCS/erc-7517.md diff --git a/ERCS/erc-7517.md b/ERCS/erc-7517.md new file mode 100644 index 0000000000..9e4cce532e --- /dev/null +++ b/ERCS/erc-7517.md @@ -0,0 +1,222 @@ +--- +eip: 7517 +title: Content Consent for AI/ML Data Mining +description: A proposal adding "dataMiningPreference" in the metadata to preserve the digital content's original intent and respect creator's rights. +author: Bofu Chen (@bafu), Tammy Yang (@tammyyang) +discussions-to: https://ethereum-magicians.org/t/eip-7517-content-consent-for-ai-ml-data-mining/15755 +status: Draft +type: Standards Track +category: ERC +created: 2023-09-12 +requires: 721, 7053 +--- + +## Abstract + +This EIP proposes a standardized approach to declaring mining preferences for digital media content on the EVM-compatible blockchains. This extends digital media metadata standards like [ERC-7053](./erc-7053.md) and NFT metadata standards like [ERC-721](./erc-721.md) and [ERC-1155](./erc-1155.md), allowing asset creators to specify how their assets are used in data mining, AI training, and machine learning workflows. + +## Motivation + +As digital assets become increasingly utilized in AI and machine learning workflows, it is critical that the rights and preferences of asset creators and license owners are respected, and the AI/ML creators can check and collect data easily and safely. Similar to robot.txt to websites, content owners and creators are looking for more direct control over how their creativities are used. + +This proposal standardizes a method of declaring these preferences. Adding `dataMiningPreference` in the content metadata allows creators to include the information about whether the asset may be used as part of a data mining or AI/ML training workflow. This ensures the original intent of the content is maintained. + +For AI-focused applications, this information serves as a guideline, facilitating the ethical and efficient use of content while respecting the creator's rights and building a sustainable data mining and AI/ML environment. + +The introduction of the `dataMiningPreference` property in digital asset metadata covers the considerations including: + +* Accessibility: A clear and easily accessible method with human-readibility and machine-readibility for digital asset creators and license owners to express their preferences for how their assets are used in data mining and AI/ML training workflows. The AI/ML creators can check and collect data systematically. +* Adoption: As Coalition for Content Provenance and Authenticity (C2PA) already outlines guidelines for indicating whether an asset may be used in data mining or AI/ML training, it's crucial that onchain metadata aligns with these standards. This ensures compatibility between in-media metadata and onchain records. + +## Specification + +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174. + +This EIP introduces a new property, `dataMiningPreference`, to the metadata standards which signify the choices made by the asset creators or license owners regarding the suitability of their asset for inclusion in data mining or AI/ML training workflows. `dataMiningPreference` is an object that can include one or more specific conditions. + +* `dataMining`: Allow the asset to be used in data mining for determining "patterns, trends, and correlations". +* `aiInference`: Allow the asset to be used as input to a trained AI/ML model for inferring a result. +* `aiGenerativeTraining`: Allow the asset to be used as training data for an AI/ML model that could produce derivative assets. +* `aiGenerativeTrainingWithAuthorship`: Same as `aiGenerativeTraining`, but requires that the authorship is disclosed. +* `aiTraining`: Allow the asset to be used as training data for generative and non-generative AI/ML models. +* `aiTrainingWithAuthorship`: Same as `aiTraining`, but requires that the authorship is disclosed. + +Each category is defined by a set of permissions that can take on one of three values: `allowed`, `notAllowed`, and `constraint`. + +* `allowed` indicates that the asset can be freely used for the specific purpose without any limitations or restrictions. +* `notAllowed` means that the use of the asset for that particular purpose is strictly prohibited. +* `constrained` suggests that the use of the asset is permitted, but with certain conditions or restrictions that must be adhered to. + +For instance, the `aiInference` property indicates whether the asset can be used as input for an AI/ML model to derive results. If set to `allowed`, the asset can be utilized without restrictions. If `notAllowed`, the asset is prohibited from AI inference. + +If marked as `constrained`, certain conditions, detailed in the license document, must be met. When `constraint` is selected, parties intending to use the media files should respect the rules specified in the license. To avoid discrepancies with the content license, the specifics of these constraints are not detailed within the schema, but the license reference should be included in the content metadata. + +### Schema + +The JSON schema of `dataMiningPreference` is defined as follows: + +```json +{ + "type": "object", + "properties": { + "dataMining": { + "type": "string", + "enum": ["allowed", "notAllowed", "constrained"] + }, + "aiInference": { + "type": "string", + "enum": ["allowed", "notAllowed", "constrained"] + }, + "aiTraining": { + "type": "string", + "enum": ["allowed", "notAllowed", "constrained"] + }, + "aiGenerativeTraining": { + "type": "string", + "enum": ["allowed", "notAllowed", "constrained"] + }, + "aiTrainingWithAuthorship": { + "type": "string", + "enum": ["allowed", "notAllowed", "constrained"] + }, + "aiGenerativeTrainingWithAuthorship": { + "type": "string", + "enum": ["allowed", "notAllowed", "constrained"] + } + }, + "additionalProperties": true +} +``` + +### Examples + +The mining preference example for not allowing generative AI training: + +```json +{ + "dataMiningPreference": { + "dataMining": "allowed", + "aiInference": "allowed", + "aiTrainingWithAuthorship": "allowed", + "aiGenerativeTraining": "notAllowed" + } +} +``` + +The mining preference example for only allowing for AI inference: + +```json +{ + "dataMiningPreference": { + "aiInference": "allowed", + "aiTraining": "notAllowed", + "aiGenerativeTraining": "notAllowed" + } +} +``` + +The mining preference example for allowing generative AI training if mentioning authorship and follow license: + +```json +{ + "dataMiningPreference": { + "dataMining": "allowed", + "aiInference": "allowed", + "aiTrainingWithAuthorship": "allowed", + "aiGenerativeTrainingWithAuthorship": "constrained" + } +} +``` + +### Example Usage with ERC-721 + +The following is an example of using the `dataMiningPreference` property in [ERC-721](./erc-721.md) NFTs. + +We can put the `dataMiningPreference` field in the NFT metadata below. The `license` field is only an example for specifying how to use a constrained condition, and is not defined in this proposal. A NFT has its way to describe its license. + +```json +{ + "name": "The Starry Night, revision", + "description": "Recreation of the oil-on-canvas painting by the Dutch Post-Impressionist painter Vincent van Gogh.", + "image": "ipfs://bafyaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "dataMiningPreference": { + "dataMining": "allowed", + "aiInference": "allowed", + "aiTrainingWithAuthorship": "allowed", + "aiGenerativeTrainingWithAuthorship": "constrained" + }, + "license": { + "name": "CC-BY-4.0", + "document": "https://creativecommons.org/licenses/by/4.0/legalcode" + } +} +``` + +### Example Usage with ERC-7053 + +The example using the `dataMiningPreference` property in onchain media provenance registration defined in [ERC-7053](./erc-7053.md). + +Assuming the Decentralized Content Identifier (CID) is `bafyaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`. We can put the `dataMiningPreference` field in the Commit data directly. After following up the CID, got the Commit data: + +```json +{ + "dataMiningPreference": { + "dataMining": "allowed", + "aiInference": "allowed", + "aiTrainingWithAuthorship": "allowed", + "aiGenerativeTrainingWithAuthorship": "constrained" + }, + "license": { + "name": "CC-BY-4.0", + "document": "https://creativecommons.org/licenses/by/4.0/legalcode" + } +} +``` + +We can also put the `dataMiningPreference` field in any custom metadata whose CID is linked in the Commit data. The `assetTreeCid` field is an example for specifying how to link a custom metadata. After following up the CID, got the Commit data: + +```json +{ + /* custom metadata CID */ + "assetTreeCid": "bafybbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" +} +``` + +Following up the `assetTreeCid` which describes the custom properties of the registered asset: + +```json +{ + "dataMiningPreference": { + "dataMining": "allowed", + "aiInference": "allowed", + "aiTrainingWithAuthorship": "allowed", + "aiGenerativeTrainingWithAuthorship": "constrained" + }, + "license": { + "name": "CC-BY-4.0", + "document": "https://creativecommons.org/licenses/by/4.0/legalcode" + } +} +``` + +## Rationale + +The technical decisions behind this EIP have been carefully considered to address specific challenges and requirements in the digital asset landscape. Here are the clarifications for the rationale behind: + +1. Adoption of JSON schema: The use of JSON facilitates ease of integration and interaction, both manually and programmatically, with the metadata. +2. Detailed control with training types: The different categories like `aiGenerativeTraining`, `aiTraining`, and `aiInference` let creators control in detail, considering both ethics and computer resource needs. +3. Authorship options included: Options like `aiGenerativeTrainingWithAuthorship` and `aiTrainingWithAuthorship` make sure creators get credit, addressing ethical and legal issues. +4. Introduction of `constrained` category: The introduction of `constrained` category serves as an intermediary between `allowed` and `notAllowed`. It signals that additional permissions or clarifications may be required, defaulting to `notAllowed` in the absence of such information. +5. C2PA alignment for interoperability: The standard aligns with C2PA guidelines, ensuring seamless mapping between onchain metadata and existing offchain standards. + +## Security Considerations + +When adopting this EIP, it’s essential to address several security aspects to ensure the safety and integrity of adoption: + +* Data Integrity: Since this EIP facilitates the declaration of mining preferences for digital media assets, the integrity of the data should be assured. Any tampering with the `dataMiningPreference` property can lead to unauthorized data mining usage. Blockchain's immutability will play a significant role here, but additional security layers, such as cryptographic signatures, can further ensure data integrity. +* Verifiable Authenticity: Ensure that the individual or entity setting the `dataMiningPreference` is the legitimate owner or authorized representative of the digital asset. Unauthorized changes to preferences can lead to data misuse. Cross-checking asset provenance and ownership becomes paramount. Services or smart contracts should be implemented to verify the authenticity of assets before trusting the `dataMiningPreference`. +* Data Privacy: Ensure that the process of recording preferences doesn't inadvertently expose sensitive information about the asset creators or owners. Although the Ethereum blockchain is public, careful consideration is required to ensure no unintended data leakage. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). From 4b701d740693c842cb60f3bcfdccb75a7c82c732 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Tue, 28 Nov 2023 09:38:55 -0800 Subject: [PATCH 08/23] fix links (#131) --- ERCS/erc-7517.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ERCS/erc-7517.md b/ERCS/erc-7517.md index 9e4cce532e..0e996bfa37 100644 --- a/ERCS/erc-7517.md +++ b/ERCS/erc-7517.md @@ -13,7 +13,7 @@ requires: 721, 7053 ## Abstract -This EIP proposes a standardized approach to declaring mining preferences for digital media content on the EVM-compatible blockchains. This extends digital media metadata standards like [ERC-7053](./erc-7053.md) and NFT metadata standards like [ERC-721](./erc-721.md) and [ERC-1155](./erc-1155.md), allowing asset creators to specify how their assets are used in data mining, AI training, and machine learning workflows. +This EIP proposes a standardized approach to declaring mining preferences for digital media content on the EVM-compatible blockchains. This extends digital media metadata standards like [ERC-7053](./eip-7053.md) and NFT metadata standards like [ERC-721](./eip-721.md) and [ERC-1155](./eip-1155.md), allowing asset creators to specify how their assets are used in data mining, AI training, and machine learning workflows. ## Motivation @@ -130,7 +130,7 @@ The mining preference example for allowing generative AI training if mentioning ### Example Usage with ERC-721 -The following is an example of using the `dataMiningPreference` property in [ERC-721](./erc-721.md) NFTs. +The following is an example of using the `dataMiningPreference` property in [ERC-721](./eip-721.md) NFTs. We can put the `dataMiningPreference` field in the NFT metadata below. The `license` field is only an example for specifying how to use a constrained condition, and is not defined in this proposal. A NFT has its way to describe its license. @@ -154,7 +154,7 @@ We can put the `dataMiningPreference` field in the NFT metadata below. The `lice ### Example Usage with ERC-7053 -The example using the `dataMiningPreference` property in onchain media provenance registration defined in [ERC-7053](./erc-7053.md). +The example using the `dataMiningPreference` property in onchain media provenance registration defined in [ERC-7053](./eip-7053.md). Assuming the Decentralized Content Identifier (CID) is `bafyaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`. We can put the `dataMiningPreference` field in the Commit data directly. After following up the CID, got the Commit data: From ce2eb4f0331ab319ad8a48c16d810fd5946f7170 Mon Sep 17 00:00:00 2001 From: PixelCircuits Date: Tue, 28 Nov 2023 16:34:52 -0500 Subject: [PATCH 09/23] Update ERC-7521: General Intents for Smart Contract Wallets rev1 Merged by EIP-Bot. --- ERCS/erc-7521.md | 124 +++++++++++++++++++++++++++++------------------ 1 file changed, 77 insertions(+), 47 deletions(-) diff --git a/ERCS/erc-7521.md b/ERCS/erc-7521.md index f19bea4e37..b484d871f4 100644 --- a/ERCS/erc-7521.md +++ b/ERCS/erc-7521.md @@ -12,23 +12,23 @@ created: 2023-09-19 ## Abstract -A generalized intent specification entry point contract which enables support for a multitude of intent standards as they evolve over time. Instead of smart contract wallets having to constantly upgrade to provide support for new intent standards as they pop up, a single entry point contract is trusted to handle signature verification which then passes off the low level intent data handling and defining to other contracts specified by users at intent sign time. These signed messages, called a `UserInent`, are gossipped around any host of mempool strategies for MEV searchers to look through and combine with their own `UserIntent` into an object called an `IntentSolution`. MEV searchers then package up an `IntentSolution` object they build into a transaction making a `handleIntents` call to a special contract. This transaction then goes through the typical MEV channels to eventually be included in a block. +A generalized intent specification entry point contract which enables support for a multitude of intent standards as they evolve over time. Instead of smart contract wallets having to constantly upgrade to provide support for new intent standards as they pop up, a single entry point contract is trusted to handle signature verification which then passes off the low level intent data handling and defining to other contracts specified by users at intent sign time. These signed messages, called a `UserIntent`, are gossipped around any host of mempool strategies for MEV searchers to look through and combine with their own `UserIntent` into an object called an `IntentSolution`. MEV searchers then package up an `IntentSolution` object they build into a transaction making a `handleIntents` call to a special contract. This transaction then goes through the typical MEV channels to eventually be included in a block. ## Motivation See also ["ERC-4337: Account Abstraction via Entry Point Contract specification"](./eip-4337.md) and the links therein for historical work and motivation. -This proposal uses the same entry point contract idea to enable a single interface which smart contract wallets can support now to unlock future proof access to an evolving intent landscape. It seeks to achieve the following goals: +This proposal uses the same entry point contract idea to enable a single interface which smart contract wallets can support now to unlock future-proof access to an evolving intent landscape. It seeks to achieve the following goals: -- **Achieve the key goal of enabling intents for users**: allow users to use smart contract wallets containing arbitrary verification logic to specify intents as described and handled by various other intent standard contracts. +- **Achieve the key goal of enabling intents for users**: allow users to use smart contract wallets containing arbitrary verification logic to specify intent execution as described and handled by various other intent standard contracts. - **Decentralization** - Allow any MEV searcher to participate in the process of solving signed intents - Allow any developer to add their own intent standard definitions for users to opt-in to at sign time - **Be forward thinking for future intent standard compatibility**: Define an intent standard interface that gives future intent standard defining contracts access to as much information about the current `handleIntents` execution context as possible. -- **Keep gas costs down to a minimum**: Include some key intent handling logic, like basic execution guarantees and a default intent standard definition, into the entry point contract itself in order to optimize gas efficiency for the most common use cases. +- **Keep gas costs down to a minimum**: Include key intent handling logic, like intent segment execution order, into the entry point contract itself in order to optimize gas efficiency for the most common use cases. - **Enable good user experience** - - Avoid the need for smart contract wallet upgrades when a user wants to use a newly developed intent standard - - Enable complex intent composition that only needs a single signature + - Avoid the need for smart contract wallet upgrades when a user wants to use a newly developed intent standard. + - Enable complex intent composition that only needs a single signature. ## Specification @@ -36,14 +36,11 @@ Users package up intents they want their wallet to participate in, in an ABI-enc | Field | Type | Description | | ------------ | --------- | ------------------------------------------------------------------------------------ | -| `standard` | `bytes32` | The intent standard identifier | -| `sender` | `address` | The wallet making the operation | -| `nonce` | `uint256` | Anti-replay parameter | -| `timestamp` | `uint256` | Time validity parameter | +| `sender` | `address` | The wallet making the intent | | `intentData` | `bytes[]` | Data defined by the intent standard broken down into multiple segments for execution | | `signature` | `bytes` | Data passed into the wallet along with the nonce during the verification step | -The `intentData` parameter is an array of arbitrary bytes whose use is defined by the intent standard. Each item in this array is referred to as an **intent segment**. Users send `UserIntent` objects to any mempool strategy that works best for the intent standard being used. A specialized class of MEV searchers called **solvers** look for these intents and ways that they can be combined with other intents (including their own) to create an ABI-encoded struct called an `IntentSolution`: +The `intentData` parameter is an array of arbitrary bytes whose use is defined by an intent standard. Each item in this array is referred to as an **intent segment**. The first 32 bytes of each segment is used to specify the **intent standard ID** to which the segment data belongs. Users send `UserIntent` objects to any mempool strategy that works best for the intent standards being used. A specialized class of MEV searchers called **solvers** look for these intents and ways that they can be combined with other intents (including their own) to create an ABI-encoded struct called an `IntentSolution`: | Field | Type | Description | | ----------- | -------------- | --------------------------------------------- | @@ -60,13 +57,9 @@ function handleIntents (IntentSolution calldata solution) external; -function simulateHandleIntents - (IntentSolution calldata solution, address target, bytes calldata targetCallData) - external; - -function simulateValidation +function validateIntent (UserIntent calldata intent) - external; + external view; function registerIntentStandard (IIntentStandard intentStandard) @@ -75,12 +68,6 @@ function registerIntentStandard function verifyExecutingIntentForStandard (IIntentStandard intentStandard) external returns (bool); - -error ValidationResult - (bool sigFailed, uint48 validAfter, uint48 validUntil); - -error ExecutionResult - (bool success, bool targetSuccess, bytes targetResult); ``` The core interface required for an intent standard to have is: @@ -93,10 +80,6 @@ function validateUserIntent function executeUserIntent (IntentSolution calldata solution, uint256 executionIndex, uint256 segmentIndex, bytes memory context) external returns (bytes memory); - -function isIntentStandardForEntryPoint - (IEntryPoint entryPoint) - external returns (bool); ``` The core interface required for a wallet to have is: @@ -104,7 +87,7 @@ The core interface required for a wallet to have is: ```solidity function validateUserIntent (UserIntent calldata intent, bytes32 intentHash) - external returns (uint256); + external view returns (address); function generalizedIntentDelegateCall (bytes memory data) @@ -119,66 +102,113 @@ In the verification loop, the `handleIntents` call must perform the following st - **Validate `timestamp` value on the `IntentSolution`** by making sure it is within an acceptable range of `block.timestamp` or some time before it. - **Call `validateUserIntent` on the wallet**, passing in the `UserIntent` and the hash of the intent. The wallet should verify the intent's signature. If any `validateUserIntent` call fails, `handleIntents` must skip execution of at least that intent, and may revert entirely. -- **Call `validateUserIntent` on the intent standard**, specified by the `UserIntent` with the `standard` parameter, passing in the `UserIntent`. The intent standard should verify the `intentData` parameter can successfully be parsed according to what the standard expects. If any `validateUserIntent` call fails, `handleIntents` must skip execution of at least that intent, and may revert entirely. In the execution loop, the `handleIntents` call must perform the following steps for all **segments** on the `intentData` bytes array parameter on each `UserIntent`: -- **Call `executeUserIntent` on the intent standard**, specified by the `UserIntent` with the `standard` parameter. This call passes in the entire `IntentSolution` as well as the current `executionIndex` (the number of times this function has already been called for any standard or intent before this), `segmentIndex` (index in the `intentData` array to execute for) and `context` data. The `executeUserIntent` function returns arbitrary bytes per intent which must be remembered and passed into the next `executeUserIntent` call for the same intent. +- **Call `executeUserIntent` on the intent standard**, specified by the first 32 bytes of the `intentData` (the intent standard ID). This call passes in the entire `IntentSolution` as well as the current `executionIndex` (the number of times this function has already been called for any standard or intent before this), `segmentIndex` (index in the `intentData` array to execute for) and `context` data. The `executeUserIntent` function returns arbitrary bytes per intent which must be remembered and passed into the next `executeUserIntent` call for the same intent. It's up to the intent standard to choose how to parse the `intentData` segment bytes and utilize the `context` data blob that persists across intent execution. The order of execution for `UserIntent` segments in the `intentData` array always follows the same order defined on the `intentData` parameter. However, the order of execution for segments between `UserIntent` objects can be specified by the `order` parameter of the `IntentSolution` object. For example, an `order` array of `[1,1,0,1]` would result in the second intent being executed twice (segments 1 and 2 on intent 2), then the first intent would be executed (segment 1 on intent 1), followed by the second intent being executed a third time (segment 3 on intent 2). If no ordering is specified in the solution, or all segments have not been processed for all intents after getting to the end of the order array, a default ordering will be used. This default ordering loops from the first intent to the last as many times as necessary until all intents have had all their segments executed. If the ordering calls for an intent to be executed after it's already been executed for all its segments, then the `executeUserIntent` call is simply skipped and execution across all intents continues. -Before accepting a `UserIntent`, solvers must use an RPC method to locally call the `simulateValidation` function of the entry point, which verifies that the signature and data formatting is correct; see the [Intent validation section below](#intent-validation) for details. +Before accepting a `UserIntent`, solvers must use an RPC method to locally call the `validateIntent` function of the entry point, which verifies that the signature and data formatting is correct; see the [Intent validation section below](#intent-validation) for details. #### Registering new entry point intent standards The entry point's `registerIntentStandard` function must allow for permissionless registration of new intent standard contracts. During the registration process, the entry point contract must verify the contract is meant to be registered by calling the `isIntentStandardForEntryPoint` function on the intent standard contract. This function passes in the entry point contract address which the intent standard can then verify and return true or false. If the intent standard contract returns true, then the entry point registers it and gives it a **standard ID** which is unique to the intent standard contract, entry point contract and chain ID. -### Extension: default intent standard - -We extend the entry point logic to support a **default intent standard** that can be used by solvers to perform basic operations in a gas efficient way. This default standard is registered with its own standard ID at entry point contract creation time. The functions `validateUserIntent` and `executeUserIntent` are included as part of entry point contracts code in order to reduce external calls. The `intentData` on this default standard is used as calldata to call to the intent `sender`. This allows the solver to perform a basic list of operations from their own wallet in a more gas efficient manner. - ### Intent standard behavior executing an intent The intent standard's `executeUserIntent` function is given access to a wide set of data, including the entire `IntentSolution` in order to allow it to be able to implement any kind of logic that may be seen as useful in the future. Each intent standard contract is expected to parse the `UserIntent` objects `intentData` parameter and use that to validate any constraints or perform any actions relevant to the standard. Intent standards can also take advantage of the `context` data it can return at the end of the `executeUserIntent` function. This data is kept by the entry point and passed in as a parameter to the `executeUserIntent` function the next time it is called for an event. This gives intent standards access to a persistent data store as other intents are executed in between others. One example of a use case for this is an intent standard that is looking for a change in state during intent execution (like releasing tokens and expecting to be given other tokens). ### Smart contract wallet behavior executing an intent -Smart contract wallets are not expected to do anything by the entry point during intent execution after validation. However, intent standards may wish for the smart contract wallet to perform some action. The smart contract wallet `generalizedIntentDelegateCall` function must perform a delegate call with the given calldata at the calling intent standard. In order for the wallet to trust making the delegate call it must call the `verifyExecutingIntentForStandard` function on the entry point contract to verify both of the following: +The entry point does not expect anything from the smart contract wallets after validation and during intent execution. However, intent standards may wish for the smart contract wallet to perform some action during execution. The smart contract wallet `generalizedIntentDelegateCall` function must perform a delegate call with the given calldata at the calling intent standard. In order for the wallet to trust making the delegate call it must call the `verifyExecutingIntentForStandard` function on the entry point contract to verify both of the following: - The `msg.sender` for `generalizedIntentDelegateCall` on the wallet is the intent standard contract that the entry point is currently calling `executeUserIntent` on. - The smart contract wallet is the `sender` on the `UserIntent` that the entry point is currently calling `executeUserIntent` for. -### Intent validation +### Smart contract wallet behavior validating an intent + +The entry point calls `validateUserIntent` for each intent on the smart contract wallet specified in the `sender` field of each `UserIntent`. This function provides the entire `UserIntent` object as well as the precomputed hash of the intent. The smart contract wallet is then expected to analyze this data to ensure it was actually sent from the specified `sender`. If the intent is not valid, the smart contract wallet should throw an error in the `validateUserIntent` function. It should be noted that `validateUserIntent` is restricted to `view` only. Any kind of updates to state for things like nonce management, should be done in an individual segment on the intent itself. This allows for maximum customization in the way users define their intents while enshrining only the minimum verification within the entry point needed to ensure intents cannot be forged. -To validate a `UserIntent`, the solver makes a view call to `simulateValidation(intent)` on the entry point. This function always reverts with `ValidationResult` as a successful response. If the call reverts with another error, the solver rejects the `UserIntent`. While running, the solver should make sure that the call's execution trace does not invoke any **forbidden opcodes**. If this condition is violated, the solver should also reject the `UserIntent`. +The function `validateUserIntent` also has an optional `address` return value for the smart contract wallet to return if the validation failed but could have been validated by a signature aggregation contract earlier. In this case, the smart contract wallet would return the address of the trusted signature aggregation smart contract; see the [Extension: signature aggregation](#extension-signature-aggregation) section below for details. If there were no issues during validation, the smart contract wallet should just return `address(0)`. -#### Forbidden opcodes +### Solver intent validation -The forbidden opcodes are to be forbidden when `depth > 2` (i.e. when it is the wallet, intent standard, or other contracts called by them that are being executed). They are: `GASPRICE`, `GASLIMIT`, `DIFFICULTY`, `TIMESTAMP`, `BASEFEE`, `BLOCKHASH`, `NUMBER`, `SELFBALANCE`, `BALANCE`, `ORIGIN`, `GAS`. The only exception is the `GAS` opcode if it is immediately followed by `CALL`, `DELEGATECALL`, `CALLCODE` or `STATICCALL`. They should only be forbidden during verification, not execution. These opcodes are forbidden because their outputs may differ between simulation and execution, so simulation of calls using these opcodes does not reliably tell what would happen if these calls are later done on-chain. +To validate a `UserIntent`, the solver makes a view call to `validateIntent(intent)` on the entry point. This function checks that the signature passes validation and that the segments on the intent are properly formatted. If the call reverts with any error, the solver should reject the `UserIntent`. ### Simulation -To simulate execution of an `IntentSolution`, the solver makes a view call to `simulateHandleIntents(solution)` on the entry point. This function always reverts with `ExecutionResult` as a successful response. If the call reverts with another error, the solver knows the `IntentSolution` was invalid. The solver also has the option to provide a `target` with `targetCallData`. At the end of simulation, the entry point will call to the target contract with the calldata to do any final analysis and return data through the `ExecutionResult` error. +Solvers are expected to handle simulation in typical MEV workflows. This most likely means dry running their solutions at the current block height to determine the outcome is as expected. Successful solutions can then be submitted as a bundle to block builders to be included in the next block. + +### Extensions + +The entry point contract may enable additional functionality to reduce gas costs for common scenarios. + +#### Extension: signature aggregation + +We add the additional function `handleIntentsAggregated` to the entry point contract that allows an aggregated signature to be provided in place of verifying signatures for intents individually. Additionally, we introduce a new interface for a contract acting as the **signature aggregator** that handles all logic for aggregated signature verification. + +The core interface required for the entry point to have is: + +```solidity +handleIntentsAggregated( + IntentSolution[] calldata solutions, + IAggregator aggregator, + bytes32 intentsToAggregate, + bytes calldata signature + ) external; +``` + +The `handleIntentsAggregated` function takes in a list of solutions, the address of the aggregation contract, a bitfield indicating which intents the aggregate signature represents (1 for included, 0 for excluded) and lastly, the aggregated signature itself. The entry point contract will call to the aggregator contract to verify the aggregated signature for the involved intents. Then, during normal validation, the entry point contract verifies that the smart contract wallets that sent the intents in the aggregated signature all return the address of the signature aggregator contract that was used; see the [Smart contract wallet behavior validating an intent](#smart-contract-wallet-behavior-validating-an-intent) section above. + +The core interface required for an aggregator to have is: + +```solidity +function validateSignatures + (UserIntent[] calldata intents, bytes calldata signature) + external view; + +function aggregateSignatures + (UserIntent[] calldata intents) + external view returns (bytes memory aggregatedSignature); +``` +The `validateSignatures` function serves as the main function for the entry point contract to call to verify an aggregated signature. The `aggregateSignatures` function can be used by solvers off-chain to calculate the aggregated signature if they do not already have optimized custom code to perform the aggregation. + +#### Extension: embedded intent standards + +We extend the entry point logic to include the logic of several identified **common intent standards**. These standards are registered with their own standard ID at entry point contract creation time. The functions `validateUserIntent` and `executeUserIntent` for these standards are included as part of the entry point contracts code in order to reduce external calls and save gas. + +#### Extension: handle multi + +We add the additional function `handleIntentsMulti(IntentSolution[] calldata solutions)` to the entry point contract. This allows multiple solutions to be executed in a single transaction to enable gas saving in intents that touch similar areas of storage. + +#### Extension: nonce management + +We add the functions `getNonce(address sender, uint256 key)` and `setNonce(uint256 key, uint256 nonce)` to the entry point contract. These functions allow nonce data to be stored in the entry point contracts storage. Nonces are stored at a per sender level and are available to be read by anyone. However, the entry point contract enforces that nonces can only be set for a user by a currently executing intent standard and only for the `sender` on the intent currently being executed. + +#### Extension: data blobs + +We enable the entry point contract to skip the validation of `UserIntent` objects with either a `sender` field of `address(0)` or an empty `intentData` field (rather than fail validation). Similarly, they are skipped during execution. The `intentData` field or `sender` field is then free to be treated as a way to inject any arbitrary data into intent execution. This data could be useful in solving an intent that has an intent standard which requires some secret to be known and proven to it, or an intent whose behavior can change according to what other intents are around it. For example, an intent standard that signals a smart contract wallet to transfer some tokens to the sender of the intent that is next in line for the execution process. ## Rationale The main challenge with a generalized intent standard is being able to adapt to the evolving world of intents. Users need to have a way to express their intents in a seamless way without having to make constant updates to their smart contract wallets. -In this proposal, we expect wallets to have a `validateUserIntent` function that takes as input a `UserOperation`, and verifies the signature. A trusted entry point contract uses this function to validate the signature and forwards the intent handling logic to an intent standard contract specified in the `UserOperation`. The wallet is then expected to have a `generalizedIntentDelegateCall` function that allows it to perform intent related actions from the intent standard contract, using the `verifyExecutingIntentForStandard` function on the entry point for security. +In this proposal, we expect wallets to have a `validateUserIntent` function that takes as input a `UserIntent`, and verifies the signature. A trusted entry point contract uses this function to validate the signature and forwards the intent handling logic to the intent standard contracts specified in the first 32 bytes of each segment in the `intentData` array field on the `UserIntent`. The wallet is then expected to have a `generalizedIntentDelegateCall` function that allows it to perform intent related actions from the intent standard contracts, using the `verifyExecutingIntentForStandard` function on the entry point for security. -The entry point-based approach allows for a clean separation between verification and intent execution, and prevents wallets from having to constantly update to support the latest version of intent composition that a user wants to use. The alternative would involve developers of new intent standards having to convince wallet software developers to support their new intent standards. This proposal moves the core definition of an intent into the hands of users at signing time. +The entry point based approach allows for a clean separation between verification and intent execution, and prevents wallets from having to constantly update to support the latest intent standard composition that a user wants to use. The alternative would involve developers of new intent standards having to convince wallet software developers to support their new intent standards. This proposal moves the core definition of an intent into the hands of users at signing time. ### Solvers Solvers facilitate the fulfillment of a user's intent in search of their own MEV. They also act as the transaction originator for executing intents on-chain, including having to front any gas fees, removing that burden from the typical user. -Solvers will rely on gossiping networks and solution algorithms that are to be determined by the individual intent standards. +Solvers will rely on gossiping networks and solution algorithms that are to be determined by the nature of the intents themselves and the individual intent standards being used. ### Entry point upgrading -Wallets are encouraged to be DELEGATECALL forwarding contracts for gas efficiency and to allow wallet upgradability. The wallet code is expected to hard-code the entry point into their code for gas efficiency. If a new entry point is introduced, whether to add new functionality, improve gas efficiency, or fix a critical security bug, users can self-call to replace their wallet's code address with a new code address containing code that points to a new entry point. During an upgrade process, it's expected that intent standard contracts will also have to be redeployed and registered to the new entry point. +Wallets are encouraged to be DELEGATECALL forwarding contracts for gas efficiency and to allow wallet upgradability. The wallet code is expected to hard-code the entry point into their code for gas efficiency. If a new entry point is introduced, whether to add new functionality, improve gas efficiency, or fix a critical security bug, users can self-call to replace their wallet's code address with a new code address containing code that points to a new entry point. During an upgrade process, it's expected that intent standard contracts will also have to be re-registered to the new entry point. #### Intent standard upgrading @@ -194,11 +224,11 @@ See `https://github.com/essential-contributions/ERC-7521` ## Security Considerations -The entry point contract will need to be very heavily audited and formally verified, because it will serve as a central trust point for _all_ [ERC-7521](./eip-7521.md) supporting wallets. In total, this architecture reduces auditing and formal verification load for the ecosystem, because the amount of work that individual _wallets_ have to do becomes much smaller (they need only verify the `validateUserIntent` function and its "check signature, increment nonce" logic) and gate any calls to `generalizedIntentDelegateCall` by checking with the entry point using the `verifyExecutingIntentForStandard` function. The concentrated security risk in the entry point contract, however, needs to be verified to be very robust since it is so highly concentrated. +The entry point contract will need to be very heavily audited and formally verified, because it will serve as a central trust point for _all_ [ERC-7521](./eip-7521.md) supporting wallets. In total, this architecture reduces auditing and formal verification load for the ecosystem, because the amount of work that individual _wallets_ have to do becomes much smaller (they need only verify the `validateUserIntent` function and its "check signature" logic) and gate any calls to `generalizedIntentDelegateCall` by checking with the entry point using the `verifyExecutingIntentForStandard` function. The concentrated security risk in the entry point contract, however, needs to be verified to be very robust since it is so highly concentrated. Verification would need to cover one primary claim (not including claims needed to protect solvers, and intent standard related infrastructure): -- **Safety against arbitrary hijacking**: The entry point only returns true for `verifyExecutingIntentForStandard` when it has successfully validated the signature of the `UserIntent` and is currently in the middle of calling `executeUserIntent` on the `standard` specified in a `UserIntent` which also has the same `sender` as the `msg.sender` wallet calling the function. +- **Safety against arbitrary hijacking**: The entry point only returns true for `verifyExecutingIntentForStandard` when it has successfully validated the signature of the `UserIntent` and is currently in the middle of calling `executeUserIntent` on the `standard` specified in the `intentData` field of a `UserIntent` which also has the same `sender` as the `msg.sender` wallet calling the function. Additional heavy auditing and formal verification will also need to be done for any intent standard contracts a user decides to interact with. From 4f2bc89bf7c318afdebb2f61e93a44afe8ba8dce Mon Sep 17 00:00:00 2001 From: Etan Kissling Date: Wed, 29 Nov 2023 00:02:29 +0100 Subject: [PATCH 10/23] Update ERC-7521: Fix Markdown link to to intent validation Merged by EIP-Bot. --- ERCS/erc-7521.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ERCS/erc-7521.md b/ERCS/erc-7521.md index b484d871f4..d2904ac86f 100644 --- a/ERCS/erc-7521.md +++ b/ERCS/erc-7521.md @@ -111,7 +111,7 @@ It's up to the intent standard to choose how to parse the `intentData` segment b The order of execution for `UserIntent` segments in the `intentData` array always follows the same order defined on the `intentData` parameter. However, the order of execution for segments between `UserIntent` objects can be specified by the `order` parameter of the `IntentSolution` object. For example, an `order` array of `[1,1,0,1]` would result in the second intent being executed twice (segments 1 and 2 on intent 2), then the first intent would be executed (segment 1 on intent 1), followed by the second intent being executed a third time (segment 3 on intent 2). If no ordering is specified in the solution, or all segments have not been processed for all intents after getting to the end of the order array, a default ordering will be used. This default ordering loops from the first intent to the last as many times as necessary until all intents have had all their segments executed. If the ordering calls for an intent to be executed after it's already been executed for all its segments, then the `executeUserIntent` call is simply skipped and execution across all intents continues. -Before accepting a `UserIntent`, solvers must use an RPC method to locally call the `validateIntent` function of the entry point, which verifies that the signature and data formatting is correct; see the [Intent validation section below](#intent-validation) for details. +Before accepting a `UserIntent`, solvers must use an RPC method to locally call the `validateIntent` function of the entry point, which verifies that the signature and data formatting is correct; see the [Intent validation section below](#solver-intent-validation) for details. #### Registering new entry point intent standards @@ -167,7 +167,7 @@ The core interface required for an aggregator to have is: ```solidity function validateSignatures - (UserIntent[] calldata intents, bytes calldata signature) + (UserIntent[] calldata intents, bytes calldata signature) external view; function aggregateSignatures @@ -190,7 +190,7 @@ We add the functions `getNonce(address sender, uint256 key)` and `setNonce(uint2 #### Extension: data blobs -We enable the entry point contract to skip the validation of `UserIntent` objects with either a `sender` field of `address(0)` or an empty `intentData` field (rather than fail validation). Similarly, they are skipped during execution. The `intentData` field or `sender` field is then free to be treated as a way to inject any arbitrary data into intent execution. This data could be useful in solving an intent that has an intent standard which requires some secret to be known and proven to it, or an intent whose behavior can change according to what other intents are around it. For example, an intent standard that signals a smart contract wallet to transfer some tokens to the sender of the intent that is next in line for the execution process. +We enable the entry point contract to skip the validation of `UserIntent` objects with either a `sender` field of `address(0)` or an empty `intentData` field (rather than fail validation). Similarly, they are skipped during execution. The `intentData` field or `sender` field is then free to be treated as a way to inject any arbitrary data into intent execution. This data could be useful in solving an intent that has an intent standard which requires some secret to be known and proven to it, or an intent whose behavior can change according to what other intents are around it. For example, an intent standard that signals a smart contract wallet to transfer some tokens to the sender of the intent that is next in line for the execution process. ## Rationale From a3a017ae805ece22f19fc61a4fa56ba5ea0f2f33 Mon Sep 17 00:00:00 2001 From: PixelCircuits Date: Tue, 28 Nov 2023 18:10:43 -0500 Subject: [PATCH 11/23] Update ERC-7521: Fix Markdown links Merged by EIP-Bot. --- ERCS/erc-7521.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ERCS/erc-7521.md b/ERCS/erc-7521.md index d2904ac86f..2c1f1cf9fe 100644 --- a/ERCS/erc-7521.md +++ b/ERCS/erc-7521.md @@ -16,7 +16,7 @@ A generalized intent specification entry point contract which enables support fo ## Motivation -See also ["ERC-4337: Account Abstraction via Entry Point Contract specification"](./eip-4337.md) and the links therein for historical work and motivation. +See also ["ERC-4337: Account Abstraction via Entry Point Contract specification"](./erc-4337.md) and the links therein for historical work and motivation. This proposal uses the same entry point contract idea to enable a single interface which smart contract wallets can support now to unlock future-proof access to an evolving intent landscape. It seeks to achieve the following goals: @@ -216,7 +216,7 @@ Because intent standards are not hardcoded into the wallet, users do not need to ## Backwards Compatibility -This ERC does not change the consensus layer, so there are no backwards compatibility issues for Ethereum as a whole. There is a little more difficulty when trying to integrate with existing smart contract wallets. If the wallet already has support for [ERC-4337](./eip-4337.md), then implementing a `validateUserIntent` function should be very similar to the `validateUserOp` function, but would require an upgrade by the user. +This ERC does not change the consensus layer, so there are no backwards compatibility issues for Ethereum as a whole. There is a little more difficulty when trying to integrate with existing smart contract wallets. If the wallet already has support for [ERC-4337](./erc-4337.md), then implementing a `validateUserIntent` function should be very similar to the `validateUserOp` function, but would require an upgrade by the user. ## Reference Implementation @@ -224,7 +224,7 @@ See `https://github.com/essential-contributions/ERC-7521` ## Security Considerations -The entry point contract will need to be very heavily audited and formally verified, because it will serve as a central trust point for _all_ [ERC-7521](./eip-7521.md) supporting wallets. In total, this architecture reduces auditing and formal verification load for the ecosystem, because the amount of work that individual _wallets_ have to do becomes much smaller (they need only verify the `validateUserIntent` function and its "check signature" logic) and gate any calls to `generalizedIntentDelegateCall` by checking with the entry point using the `verifyExecutingIntentForStandard` function. The concentrated security risk in the entry point contract, however, needs to be verified to be very robust since it is so highly concentrated. +The entry point contract will need to be very heavily audited and formally verified, because it will serve as a central trust point for _all_ [ERC-7521](./erc-7521.md) supporting wallets. In total, this architecture reduces auditing and formal verification load for the ecosystem, because the amount of work that individual _wallets_ have to do becomes much smaller (they need only verify the `validateUserIntent` function and its "check signature" logic) and gate any calls to `generalizedIntentDelegateCall` by checking with the entry point using the `verifyExecutingIntentForStandard` function. The concentrated security risk in the entry point contract, however, needs to be verified to be very robust since it is so highly concentrated. Verification would need to cover one primary claim (not including claims needed to protect solvers, and intent standard related infrastructure): From 3427aa0fda665e570a5152be761b2864c3288fa3 Mon Sep 17 00:00:00 2001 From: PixelCircuits Date: Tue, 28 Nov 2023 18:32:33 -0500 Subject: [PATCH 12/23] Update ERC-7521: Revert markdown link fix Merged by EIP-Bot. --- ERCS/erc-7521.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ERCS/erc-7521.md b/ERCS/erc-7521.md index 2c1f1cf9fe..d2904ac86f 100644 --- a/ERCS/erc-7521.md +++ b/ERCS/erc-7521.md @@ -16,7 +16,7 @@ A generalized intent specification entry point contract which enables support fo ## Motivation -See also ["ERC-4337: Account Abstraction via Entry Point Contract specification"](./erc-4337.md) and the links therein for historical work and motivation. +See also ["ERC-4337: Account Abstraction via Entry Point Contract specification"](./eip-4337.md) and the links therein for historical work and motivation. This proposal uses the same entry point contract idea to enable a single interface which smart contract wallets can support now to unlock future-proof access to an evolving intent landscape. It seeks to achieve the following goals: @@ -216,7 +216,7 @@ Because intent standards are not hardcoded into the wallet, users do not need to ## Backwards Compatibility -This ERC does not change the consensus layer, so there are no backwards compatibility issues for Ethereum as a whole. There is a little more difficulty when trying to integrate with existing smart contract wallets. If the wallet already has support for [ERC-4337](./erc-4337.md), then implementing a `validateUserIntent` function should be very similar to the `validateUserOp` function, but would require an upgrade by the user. +This ERC does not change the consensus layer, so there are no backwards compatibility issues for Ethereum as a whole. There is a little more difficulty when trying to integrate with existing smart contract wallets. If the wallet already has support for [ERC-4337](./eip-4337.md), then implementing a `validateUserIntent` function should be very similar to the `validateUserOp` function, but would require an upgrade by the user. ## Reference Implementation @@ -224,7 +224,7 @@ See `https://github.com/essential-contributions/ERC-7521` ## Security Considerations -The entry point contract will need to be very heavily audited and formally verified, because it will serve as a central trust point for _all_ [ERC-7521](./erc-7521.md) supporting wallets. In total, this architecture reduces auditing and formal verification load for the ecosystem, because the amount of work that individual _wallets_ have to do becomes much smaller (they need only verify the `validateUserIntent` function and its "check signature" logic) and gate any calls to `generalizedIntentDelegateCall` by checking with the entry point using the `verifyExecutingIntentForStandard` function. The concentrated security risk in the entry point contract, however, needs to be verified to be very robust since it is so highly concentrated. +The entry point contract will need to be very heavily audited and formally verified, because it will serve as a central trust point for _all_ [ERC-7521](./eip-7521.md) supporting wallets. In total, this architecture reduces auditing and formal verification load for the ecosystem, because the amount of work that individual _wallets_ have to do becomes much smaller (they need only verify the `validateUserIntent` function and its "check signature" logic) and gate any calls to `generalizedIntentDelegateCall` by checking with the entry point using the `verifyExecutingIntentForStandard` function. The concentrated security risk in the entry point contract, however, needs to be verified to be very robust since it is so highly concentrated. Verification would need to cover one primary claim (not including claims needed to protect solvers, and intent standard related infrastructure): From 41455f9add35cdc5bdb289f59c5d7e8346f2a7d4 Mon Sep 17 00:00:00 2001 From: zakrad <49591476+zakrad@users.noreply.github.com> Date: Wed, 29 Nov 2023 19:07:58 +0330 Subject: [PATCH 13/23] Add ERC: ERC-20 Update Allowance By Spender Merged by EIP-Bot. --- ERCS/erc-7410.md | 82 ++++++++++++++++++++++++++++++++++++ assets/erc-7410/ERC7410.sol | 33 +++++++++++++++ assets/erc-7410/IERC7410.sol | 23 ++++++++++ 3 files changed, 138 insertions(+) create mode 100644 ERCS/erc-7410.md create mode 100644 assets/erc-7410/ERC7410.sol create mode 100644 assets/erc-7410/IERC7410.sol diff --git a/ERCS/erc-7410.md b/ERCS/erc-7410.md new file mode 100644 index 0000000000..e3ad4f4964 --- /dev/null +++ b/ERCS/erc-7410.md @@ -0,0 +1,82 @@ +--- +eip: 7410 +title: ERC-20 Update Allowance By Spender +description: Extension to enable revoking and decreasing allowance approval by spender for ERC-20 +author: Mohammad Zakeri Rad (@zakrad), Adam Boudjemaa (@aboudjem), Mohamad Hammoud (@mohamadhammoud) +discussions-to: https://ethereum-magicians.org/t/eip-7410-decrease-allowance-by-spender/15222 +status: Draft +type: Standards Track +category: ERC +created: 2023-07-26 +requires: 20, 165 +--- + +## Abstract + +This extension adds a `decreaseAllowanceBySpender` function to decrease [ERC-20](./eip-20.md) allowances, in which a spender can revoke or decrease a given allowance by a specific address. This ERC extends [ERC-20](./eip-20.md). + +## Motivation + +Currently, [ERC-20](./eip-20.md) tokens offer allowances, enabling token owners to authorize spenders to use a designated amount of tokens on their behalf. However, the process of decreasing an allowance is limited to the owner's side, which can be problematic if the token owner is a treasury wallet or a multi-signature wallet that has granted an excessive allowance to a spender. In such cases, reducing the allowance from the owner's perspective can be time-consuming and challenging. + +To address this issue and enhance security measures, this ERC proposes allowing spenders to decrease or revoke the granted allowance from their end. This feature provides an additional layer of security in the event of a potential hack in the future. It also eliminates the need for a consensus or complex procedures to decrease the allowance from the token owner's side. + +## Specification + +The keywords “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119. + +Contracts using this ERC MUST implement the `IERC7410` interface. + +### Interface implementation + +```solidity +pragma solidity ^0.8.0; + +/** + * @title IERC-7410 Update Allowance By Spender Extension + * Note: the ERC-165 identifier for this interface is 0x12860fba + */ +interface IERC7410 is IERC20 { + + /** + * @notice Decreases any allowance by `owner` address for caller. + * Emits an {IERC20-Approval} event. + * + * Requirements: + * - when `subtractedValue` is equal or higher than current allowance of spender the new allowance is set to 0. + * Nullification also MUST be reflected for current allowance being type(uint256).max. + */ + function decreaseAllowanceBySpender(address owner, uint256 subtractedValue) external; + +} +``` + +The `decreaseAllowanceBySpender(address owner, uint256 subtractedValue)` function MUST be either `public` or `external`. + +The `Approval` event MUST be emitted when `decreaseAllowanceBySpender` is called. + +The `supportsInterface` method MUST return `true` when called with `0x12860fba`. + +## Rationale + +The technical design choices within this ERC are driven by the following considerations: + +- The introduction of the `decreaseAllowanceBySpender` function empowers spenders by allowing them to autonomously revoke or decrease allowances. This design choice aligns with the goal of providing more direct control to spenders over their authorization levels. +- The requirement for the `subtractedValue` to be lower than the current allowance ensures a secure implementation. Additionally, nullification is achieved by setting the new allowance to 0 when `subtractedValue` is equal to or exceeds the current allowance. This approach adds an extra layer of security and simplifies the process of decreasing allowances. +- The decision to maintain naming patterns similar to [ERC-20](./eip-20.md)'s approvals is rooted in promoting consistency and ease of understanding for developers familiar with [ERC-20](./eip-20.md) standard. + +## Backwards Compatibility + +This standard is compatible with [ERC-20](./eip-20.md). + +## Reference Implementation + +An minimal implementation is included [here](../assets/eip-7410/ERC7410.sol). + +## Security Considerations + +Users of this ERC must thoroughly consider the amount of tokens they decrease from their allowance for an `owner`. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/assets/erc-7410/ERC7410.sol b/assets/erc-7410/ERC7410.sol new file mode 100644 index 0000000000..bd030b6d9f --- /dev/null +++ b/assets/erc-7410/ERC7410.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity 0.8.17; + +import "openzeppelin-contracts/token/ERC20/ERC20.sol"; +import "./IERC7410.sol"; + +contract ERC7410 is ERC20, IERC7410 { + + constructor( + string memory name_, + string memory symbol_ + ) ERC20(name_, symbol_) {} + + function decreaseAllowanceBySpender( + address _owner, + uint256 _value + ) public override(ERC20, IERC7410) returns (bool success) { + address spender = _msgSender(); + if (allowance(_owner, spender) > _value) { + _spendAllowance(_owner, spender, _value); + } else { + _approve(_owner, spender, 0); + } + + return true; + } + + function supportsInterface( + bytes4 interfaceId + ) public view virtual returns (bool) { + return interfaceId == type(IERC7410).interfaceId; + } +} diff --git a/assets/erc-7410/IERC7410.sol b/assets/erc-7410/IERC7410.sol new file mode 100644 index 0000000000..0a57943c6b --- /dev/null +++ b/assets/erc-7410/IERC7410.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import "openzeppelin-contracts/interfaces/IERC20.sol"; +import "openzeppelin-contracts/interfaces/IERC165.sol"; + +/** + * @title ERC-7410 Update Allowance By Spender Extension + * Note: the ERC-165 identifier for this interface is 0x12860fba + */ +interface IERC7410 is IERC20, IERC165 { + + /** + * @notice Decreases any allowance by `owner` address for caller. + * Emits an {IERC20-Approval} event. + * + * Requirements: + * - when `subtractedValue` is equal or higher than current allowance of spender the new allowance is set to 0. + * Nullification also MUST be reflected for current allowance being type(uint256).max. + */ + function decreaseAllowanceBySpender(address owner, uint256 subtractedValue) external; + +} From d4e9204202be65e9182bd5dd282b1a546e0c0809 Mon Sep 17 00:00:00 2001 From: Riley Date: Wed, 29 Nov 2023 09:43:03 -0600 Subject: [PATCH 14/23] Update ERC-6909: Extensions Merged by EIP-Bot. --- ERCS/erc-6909.md | 50 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/ERCS/erc-6909.md b/ERCS/erc-6909.md index 39a1344b0b..4c8f4a4409 100644 --- a/ERCS/erc-6909.md +++ b/ERCS/erc-6909.md @@ -2,7 +2,7 @@ eip: 6909 title: Minimal Multi-Token Interface description: A minimal specification for managing multiple tokens by their id in a single contract. -author: JT Riley (@jtriley-eth), Dillon (@d1ll0n), Sara (@snreynolds), Vectorized (@Vectorized) +author: JT Riley (@jtriley-eth), Dillon (@d1ll0n), Sara (@snreynolds), Vectorized (@Vectorized), Neodaoist (@neodaoist) discussions-to: https://ethereum-magicians.org/t/eip-6909-multi-token-standard/13891 status: Draft type: Standards Track @@ -340,7 +340,9 @@ The `name` of the contract. type: function stateMutability: view - inputs: [] + inputs: + - name: id + type: uint256 outputs: - name: name @@ -356,7 +358,9 @@ The ticker `symbol` of the contract. type: function stateMutability: view - inputs: [] + inputs: + - name: id + type: uint256 outputs: - name: symbol @@ -381,10 +385,26 @@ The `amount` of decimals for a token `id`. type: uint8 ``` -### Metadata URI Extension +### Content URI Extension #### Methods +##### contractURI + +The `URI` for a token `id`. + +```yaml +- name: contractURI + type: function + stateMutability: view + + inputs: [] + + outputs: + - name: uri + type: string +``` + ##### tokenURI The `URI` for a token `id`. @@ -434,6 +454,28 @@ MUST replace occurrences of `{id}` in the returned URI string by the client. } ``` +### Token Supply Extension + +#### Methods + +##### totalSupply + +The `totalSupply` for a token `id`. + +```yaml +- name: totalSupply + type: function + stateMutability: view + + inputs: + - name: id + type: uint256 + + outputs: + - name: supply + type: uint256 +``` + ## Backwards Compatibility This is not backwards compatible with ERC-1155 as some methods are removed. However, wrappers can be implemented for the ERC-20, ERC-721, and ERC-1155 standards. From e53700bdf33fd8a9bdf6f28a638a8b2c04dd6b97 Mon Sep 17 00:00:00 2001 From: Dexaran Date: Wed, 29 Nov 2023 21:11:42 +0400 Subject: [PATCH 15/23] Update ERC-7417: Update erc-7417.md Merged by EIP-Bot. --- ERCS/erc-7417.md | 149 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 126 insertions(+), 23 deletions(-) diff --git a/ERCS/erc-7417.md b/ERCS/erc-7417.md index 7a7150a92b..33b8b711af 100644 --- a/ERCS/erc-7417.md +++ b/ERCS/erc-7417.md @@ -136,49 +136,117 @@ This service is the first of its kind and therefore does not have any backwards ## Reference Implementation ```solidity - address public ownerMultisig; - mapping (address => ERC223WrapperToken) public erc223Wrappers; // A list of token wrappers. - // First one is [ERC-20](./eip-20.md) origin, - // second one is [ERC-223](./eip-223.md) version. - mapping (address => address) public erc20Origins; - mapping (address => uint256) public erc20Supply; // Token => how much was deposited. + mapping (address => ERC223WrapperToken) public erc223Wrappers; // A list of token wrappers. First one is ERC-20 origin, second one is ERC-223 version. + mapping (address => ERC20WrapperToken) public erc20Wrappers; + + mapping (address => address) public erc223Origins; + mapping (address => address) public erc20Origins; + mapping (address => uint256) public erc20Supply; // Token => how much was deposited. + + function getERC20WrapperFor(address _token) public view returns (address, string memory) + { + if ( address(erc20Wrappers[_token]) != address(0) ) + { + return (address(erc20Wrappers[_token]), "ERC-20"); + } + + return (address(0), "Error"); + } + + function getERC223WrapperFor(address _token) public view returns (address, string memory) + { + if ( address(erc223Wrappers[_token]) != address(0) ) + { + return (address(erc223Wrappers[_token]), "ERC-223"); + } + + return (address(0), "Error"); + } - function getWrapperFor(address _erc20Token) public view returns (address) + function getERC20OriginFor(address _token) public view returns (address) { - return address(erc223Wrappers[_erc20Token]); + return (address(erc20Origins[_token])); } - function getOriginFor(address _erc223WrappedToken) public view returns (address) + function getERC223OriginFor(address _token) public view returns (address) { - return erc20Origins[_erc223WrappedToken]; + return (address(erc223Origins[_token])); } function tokenReceived(address _from, uint _value, bytes memory _data) public override returns (bytes4) { - require(erc20Origins[msg.sender] != address(0), "ERROR: The received token is not a [ERC-223](./eip-223.md) Wrapper for any [ERC-20](./eip-20.md) token."); - safeTransfer(erc20Origins[msg.sender], _from, _value); + require(erc223Origins[msg.sender] == address(0), "Error: creating wrapper for a wrapper token."); + // There are two possible cases: + // 1. A user deposited ERC-223 origin token to convert it to ERC-20 wrapper + // 2. A user deposited ERC-223 wrapper token to unwrap it to ERC-20 origin. + + if(erc20Origins[msg.sender] != address(0)) + { + // Origin for deposited token exists. + // Unwrap ERC-223 wrapper. - erc20Supply[erc20Origins[msg.sender]] -= _value; - erc223Wrappers[msg.sender].burn(_value); + safeTransfer(erc20Origins[msg.sender], _from, _value); - return 0x8943ec02; + erc20Supply[erc20Origins[msg.sender]] -= _value; + //erc223Wrappers[msg.sender].burn(_value); + ERC223WrapperToken(msg.sender).burn(_value); + + return this.tokenReceived.selector; + } + // Otherwise origin for the sender token doesn't exist + // There are two possible cases: + // 1. ERC-20 wrapper for the deposited token exists + // 2. ERC-20 wrapper for the deposited token doesn't exist and must be created. + else if(address(erc20Wrappers[msg.sender]) == address(0)) + { + // Create ERC-20 wrapper if it doesn't exist. + createERC20Wrapper(msg.sender); + } + + // Mint ERC-20 wrapper tokens for the deposited ERC-223 token + // if the ERC-20 wrapper didn't exist then it was just created in the above statement. + erc20Wrappers[msg.sender].mint(_from, _value); + return this.tokenReceived.selector; } - function createERC223Wrapper(address _erc20Token) public returns (address) + function createERC223Wrapper(address _token) public returns (address) { - require(address(erc223Wrappers[_erc20Token]) == address(0), "ERROR: Wrapper already exists."); - require(getOriginFor(_erc20Token) == address(0), "ERROR: Cannot convert [ERC-223](./eip-223.md) to [ERC-223](./eip-223.md)."); + require(address(erc223Wrappers[_token]) == address(0), "ERROR: Wrapper exists"); + require(getERC20OriginFor(_token) == address(0), "ERROR: 20 wrapper creation"); + require(getERC223OriginFor(_token) == address(0), "ERROR: 223 wrapper creation"); - ERC223WrapperToken _newERC223Wrapper = new ERC223WrapperToken(_erc20Token); - erc223Wrappers[_erc20Token] = _newERC223Wrapper; - erc20Origins[address(_newERC223Wrapper)] = _erc20Token; + ERC223WrapperToken _newERC223Wrapper = new ERC223WrapperToken(_token); + erc223Wrappers[_token] = _newERC223Wrapper; + erc20Origins[address(_newERC223Wrapper)] = _token; return address(_newERC223Wrapper); } - function convertERC20toERC223(address _ERC20token, uint256 _amount) public returns (bool) + function createERC20Wrapper(address _token) public returns (address) + { + require(address(erc20Wrappers[_token]) == address(0), "ERROR: Wrapper already exists."); + require(getERC20OriginFor(_token) == address(0), "ERROR: 20 wrapper creation"); + require(getERC223OriginFor(_token) == address(0), "ERROR: 223 wrapper creation"); + + ERC20WrapperToken _newERC20Wrapper = new ERC20WrapperToken(_token); + erc20Wrappers[_token] = _newERC20Wrapper; + erc223Origins[address(_newERC20Wrapper)] = _token; + + return address(_newERC20Wrapper); + } + + function depositERC20(address _token, uint256 _amount) public returns (bool) + { + if(erc223Origins[_token] != address(0)) + { + return unwrapERC20toERC223(_token, _amount); + } + else return wrapERC20toERC223(_token, _amount); + } + + function wrapERC20toERC223(address _ERC20token, uint256 _amount) public returns (bool) { // If there is no active wrapper for a token that user wants to wrap // then create it. @@ -196,10 +264,40 @@ This service is the first of its kind and therefore does not have any backwards "ERROR: The transfer have not subtracted tokens from callers balance."); erc223Wrappers[_ERC20token].mint(msg.sender, _amount); + return true; + } + + function unwrapERC20toERC223(address _ERC20token, uint256 _amount) public returns (bool) + { + require(IERC20(_ERC20token).balanceOf(msg.sender) >= _amount, "Error: Insufficient balance."); + require(erc223Origins[_ERC20token] != address(0), "Error: provided token is not a ERC-20 wrapper."); + + ERC20WrapperToken(_ERC20token).burn(msg.sender, _amount); + IERC223(erc223Origins[_ERC20token]).transfer(msg.sender, _amount); return true; } + function isWrapper(address _token) public view returns (bool) + { + return erc20Origins[_token] != address(0) || erc223Origins[_token] != address(0); + } + +/* + function convertERC223toERC20(address _from, uint256 _amount) public returns (bool) + { + // If there is no active wrapper for a token that user wants to wrap + // then create it. + if(address(erc20Wrappers[msg.sender]) == address(0)) + { + createERC223Wrapper(msg.sender); + } + + erc20Wrappers[msg.sender].mint(_from, _amount); + return true; + } +*/ + function rescueERC20(address _token) external { require(msg.sender == ownerMultisig, "ERROR: Only owner can do this."); uint256 _stuckTokens = IERC20(_token).balanceOf(address(this)) - erc20Supply[_token]; @@ -208,10 +306,15 @@ This service is the first of its kind and therefore does not have any backwards function transferOwnership(address _newOwner) public { - require(msg.sender == ownerMultisig, "ERROR: Only owner can do this."); + require(msg.sender == ownerMultisig, "ERROR: Only owner can call this function."); ownerMultisig = _newOwner; } + // ************************************************************ + // Functions that address problems with tokens that pretend to be ERC-20 + // but in fact are not compatible with the ERC-20 standard transferring methods. + // EIP20 https://eips.ethereum.org/EIPS/eip-20 + // ************************************************************ function safeTransfer(address token, address to, uint value) internal { // bytes4(keccak256(bytes('transfer(address,uint256)'))); (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0xa9059cbb, to, value)); From b51a744af6c1bee10bc91799f8797436f8932074 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Wed, 29 Nov 2023 09:12:11 -0800 Subject: [PATCH 16/23] Add ERC: NFT Dynamic Traits (#26) * Add ERC * fix eip -> erc links * fix more links * Add ERC * fix eip -> erc links * fix more links * fix links * - remove decimal "bits" - use 0x000...000 instead of 0x0 - add min/maxValue for decimal - update impl to latest * address review: move paragraph to Rationale section * update name for assets directory * still link to assets/eip- even though folder is named assetc/erc- --------- Co-authored-by: g11tech Co-authored-by: Sam Wilson <57262657+SamWilsn@users.noreply.github.com> --- ERCS/erc-7496.md | 219 ++++++++++++++++++++++ assets/erc-7496/DynamicTraits.sol | 131 +++++++++++++ assets/erc-7496/DynamicTraitsSchema.json | 156 +++++++++++++++ assets/erc-7496/ERC721DynamicTraits.sol | 57 ++++++ assets/erc-7496/ERC721DynamicTraits.t.sol | 123 ++++++++++++ 5 files changed, 686 insertions(+) create mode 100644 ERCS/erc-7496.md create mode 100644 assets/erc-7496/DynamicTraits.sol create mode 100644 assets/erc-7496/DynamicTraitsSchema.json create mode 100644 assets/erc-7496/ERC721DynamicTraits.sol create mode 100644 assets/erc-7496/ERC721DynamicTraits.t.sol diff --git a/ERCS/erc-7496.md b/ERCS/erc-7496.md new file mode 100644 index 0000000000..ca0023cc72 --- /dev/null +++ b/ERCS/erc-7496.md @@ -0,0 +1,219 @@ +--- +eip: 7496 +title: NFT Dynamic Traits +description: Extension to ERC-721 and ERC-1155 for dynamic onchain traits +author: Adam Montgomery (@montasaurus), Ryan Ghods (@ryanio), 0age (@0age), James Wenzel (@jameswenzel), Stephan Min (@stephankmin) +discussions-to: https://ethereum-magicians.org/t/erc-7496-nft-dynamic-traits/15484 +status: Draft +type: Standards Track +category: ERC +created: 2023-07-28 +requires: 165, 721, 1155 +--- + +## Abstract + +This specification introduces a new interface that extends [ERC-721](./eip-721.md) and [ERC-1155](./eip-1155.md) that defines methods for setting and getting dynamic onchain traits associated with non-fungible tokens. These dynamic traits can be used to represent properties, characteristics, redeemable entitlements, or other attributes that can change over time. By defining these traits onchain, they can be used and modified by other onchain contracts. + +## Motivation + +Trait values for non-fungible tokens are often stored offchain. This makes it difficult to query and mutate these values in contract code. Specifying the ability to set and get traits onchain allows for new use cases like redeeming onchain entitlements and transacting based on a token's traits. + +Onchain traits can be used by contracts in a variety of different scenarios. For example, a contract that wants to entitle a token to a consumable benefit (e.g. a redeemable) can robustly reflect that onchain. Marketplaces can allow bidding on these tokens based on the trait value without having to rely on offchain state and exposing users to frontrunning attacks. The motivating use case behind this proposal is to protect users from frontrunning attacks on marketplaces where users can list NFTs with certain traits where they are expected to be upheld during fulfillment. + +## Specification + +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174. + +Contracts implementing this EIP MUST include the events, getters, and setters as defined below, and MUST return `true` for [ERC-165](./eip-165.md) `supportsInterface` for `0xaf332f3e`, the 4 byte `interfaceId` for this ERC. + +```solidity +interface IERC7496 { + /* Events */ + event TraitUpdated(bytes32 indexed traitKey, uint256 tokenId, bytes32 traitValue); + event TraitUpdatedRange(bytes32 indexed traitKey, uint256 fromTokenId, uint256 toTokenId); + event TraitUpdatedRangeUniformValue(bytes32 indexed traitKey, uint256 fromTokenId, uint256 toTokenId, bytes32 traitValue); + event TraitUpdatedList(bytes32 indexed traitKey, uint256[] tokenIds); + event TraitUpdatedListUniformValue(bytes32 indexed traitKey, uint256[] tokenIds, bytes32 traitValue); + event TraitMetadataURIUpdated(); + + /* Getters */ + function getTraitValue(uint256 tokenId, bytes32 traitKey) external view returns (bytes32 traitValue); + function getTraitValues(uint256 tokenId, bytes32[] calldata traitKeys) external view returns (bytes32[] traitValues); + function getTraitMetadataURI() external view returns (string memory uri); + + /* Setters */ + function setTrait(uint256 tokenId, bytes32 traitKey, bytes32 newValue) external; +} +``` + +### Keys & Names + +The `traitKey` is used to identify a trait. The `traitKey` MUST be a unique `bytes32` value identifying a single trait. + +The `traitKey` SHOULD be a `keccak256` hash of a human readable trait name. + +### Metadata + +Trait metadata is an optional way to define additional information about which traits are present in a contract, how to parse and display trait values, and permissions for setting trait values. + +The trait metadata must be compliant with the [specified schema](../assets/eip-7496/DynamicTraitsSchema.json). + +The trait metadata URI MAY be a data URI or point to an offchain resource. + +The keys in the `traits` object MUST be unique trait names. If the trait name is 32 byte hex string starting with `0x` then it is interpreted as a literal `traitKey`. Otherwise, the `traitKey` is defined as the `keccak256` hash of the trait name. A literal `traitKey` MUST NOT collide with the `keccak256` hash of any other traits defined in the metadata. + +The `displayName` values MUST be unique and MUST NOT collide with the `displayName` of any other traits defined in the metadata. + +The `validateOnSale` value provides a signal to marketplaces on how to validate the trait value when a token is being sold. If the validation criteria is not met, the sale MUST not be permitted by the marketplace contract. If specified, the value of `validateOnSale` MUST be one of the following (or it is assumed to be `none`): + +- `none`: No validation is necessary. +- `requireEq`: The `bytes32` `traitValue` MUST be equal to the value at the time the offer to purchase was made. +- `requireUintGte`: The `bytes32` `traitValue` MUST be greater than or equal to the value at the time the offer to purchase was made. This comparison is made using the `uint256` representation of the `bytes32` value. +- `requireUintLte`: The `bytes32` `traitValue` MUST be less than or equal to the value at the time the offer to purchase was made. This comparison is made using the `uint256` representation of the `bytes32` value. + +Note that even though this specification requires marketplaces to validate the required trait values, buyers and sellers cannot fully rely on marketplaces to do this and must also take their own precautions to research the current trait values prior to initiating the transaction. + +Here is an example of the specified schema: + +```json +{ + "traits": { + "color": { + "displayName": "Color", + "dataType": { + "type": "string", + "acceptableValues": ["red", "green", "blue"] + } + }, + "points": { + "displayName": "Total Score", + "dataType": { + "type": "decimal", + "signed": false, + "decimals": 0 + }, + "validateOnSale": "requireUintGte" + }, + "name": { + "displayName": "Name", + "dataType": { + "type": "string", + "minLength": 1, + "maxLength": 32, + "valueMappings": { + "0x0000000000000000000000000000000000000000000000000000000000000000": "Unnamed", + "0x92e75d5e42b80de937d204558acf69c8ea586a244fe88bc0181323fe3b9e3ebf": "🙂" + } + }, + "tokenOwnerCanUpdateValue": true + }, + "birthday": { + "displayName": "Birthday", + "dataType": { + "type": "epochSeconds", + "valueMappings": { + "0x0000000000000000000000000000000000000000000000000000000000000000": null + } + } + }, + "0x77c2fd45bd8bdef5b5bc773f46759bb8d169f3468caab64d7d5f2db16bb867a8": { + "displayName": "🚢 📅", + "dataType": { + "type": "epochSeconds", + "valueMappings": { + "0x0000000000000000000000000000000000000000000000000000000000000000": 1696702201 + } + } + } + } +} +``` + +#### `string` Metadata Type + +The `string` metadata type allows for a string value to be set for a trait. + +The `dataType` object MAY have a `minLength` and `maxLength` value defined. If `minLength` is not specified, it is assumed to be 0. If `maxLength` is not specified, it is assumed to be a reasonable length. + +The `dataType` object MAY have a `valueMappings` object defined. If the `valueMappings` object is defined, the `valueMappings` object MUST be a mapping of `bytes32` values to `string` or unset `null` values. The `bytes32` values SHOULD be the `keccak256` hash of the `string` value. The `string` values MUST be unique. If the trait for a token is updated to `null`, it is expected offchain indexers to delete the trait for the token. + +#### `decimal` Metadata Type + +The `decimal` metadata type allows for a numeric value to be set for a trait in decimal form. + +The `dataType` object MAY have a `signed` value defined. If `signed` is not specified, it is assumed to be `false`. This determines whether the `traitValue` returned is interpreted as a signed or unsigned integer. + +The `dataType` object MAY have `minValue` and `maxValue` values defined. These should be formatted with the decimals specified. If `minValue` is not specified, it is assumed to be the minimum value of `signed` and `decimals`. If `maxValue` is not specified, it is assumed to be the maximum value of the `signed` and `decimals`. + +The `dataType` object MAY have a `decimals` value defined. The `decimals` value MUST be a non-negative integer. The `decimals` value determines the number of decimal places included in the `traitValue` returned onchain. If `decimals` is not specified, it is assumed to be 0. + +The `dataType` object MAY have a `valueMappings` object defined. If the `valueMappings` object is defined, the `valueMappings` object MUST be a mapping of `bytes32` values to numeric or unset `null` values. + +#### `boolean` Metadata Type + +The `boolean` metadata type allows for a boolean value to be set for a trait. + +The `dataType` object MAY have a `valueMappings` object defined. If the `valueMappings` object is defined, the `valueMappings` object MUST be a mapping of `bytes32` values to `boolean` or unset `null` values. The `boolean` values MUST be unique. + +If `valueMappings` is not used, the default trait values for `boolean` should be `bytes32(0)` for `false` and `bytes32(uint256(1))` (`0x0000000000000000000000000000000000000000000000000000000000000001`) for `true`. + +#### `epochSeconds` Metadata Type + +The `epochSeconds` metadata type allows for a numeric value to be set for a trait in seconds since the Unix epoch. + +The `dataType` object MAY have a `valueMappings` object defined. If the `valueMappings` object is defined, the `valueMappings` object MUST be a mapping of `bytes32` values to integer or unset `null` values. + +### Events + +Updating traits MUST emit one of: + +- `TraitUpdated` +- `TraitUpdatedRange` +- `TraitUpdatedRangeUniformValue` +- `TraitUpdatedList` +- `TraitUpdatedListUniformValue` + +For the `Range` events, the `fromTokenId` and `toTokenId` MUST be a consecutive range of tokens IDs and MUST be treated as an inclusive range. + +For the `List` events, the `tokenIds` MAY be in any order. + +It is RECOMMENDED to use the `UniformValue` events when the trait value is uniform across all token ids, so offchain indexers can more quickly process bulk updates rather than fetching each trait value individually. + +Updating the trait metadata MUST emit the event `TraitMetadataURIUpdated` so offchain indexers can be notified to query the contract for the latest changes via `getTraitMetadataURI()`. + +### `setTrait` + +If a trait defines `tokenOwnerCanUpdateValue` as `true`, then the trait value MUST be updatable by the token owner by calling `setTrait`. + +If the value the token owner is attempting to set is not valid, the transaction MUST revert. If the value is valid, the trait value MUST be updated and one of the `TraitUpdated` events MUST be emitted. + +If the trait has a `valueMappings` entry defined for the desired value being set, `setTrait` MUST be called with the corresponding `traitValue`. + +## Rationale + +The design of this specification is primarily a key-value mapping for maximum flexibility. This interface for traits was chosen instead of relying on using regular `getFoo()` and `setFoo()` style functions to allow for brevity in defining, setting, and getting traits. Otherwise, contracts would need to know both the getter and setter function selectors including the parameters that go along with it. In defining general but explicit get and set functions, the function signatures are known and only the trait key and values are needed to query and set the values. Contracts can also add new traits in the future without needing to modify contract code. + +The traits metadata allows for customizability of both display and behavior. The `valueMappings` property can define human-readable values to enhance the traits, for example, the default label of the `0` value (e.g. if the key was "redeemed", "0" could be mapped to "No", and "1" to "Yes"). The `validateOnSale` property lets the token creator define which traits should be protected on order creation and fulfillment, to protect end users against frontrunning. + +## Backwards Compatibility + +As a new EIP, no backwards compatibility issues are present, except for the point in the specification above that it is explicitly required that the onchain traits MUST override any conflicting values specified by the ERC-721 or ERC-1155 metadata URIs. + +## Test Cases + +Authors have included Foundry tests covering functionality of the specification in the [assets folder](../assets/eip-7496/ERC721DynamicTraits.t.sol). + +## Reference Implementation + +Authors have included reference implementations of the specification in the [assets folder](../assets/eip-7496/DynamicTraits.sol). + +## Security Considerations + +The set\* methods exposed externally MUST be permissioned so they are not callable by everyone but only by select roles or addresses. + +Marketplaces SHOULD NOT trust offchain state of traits as they can be frontrunned. Marketplaces SHOULD check the current state of onchain traits at the time of transfer. Marketplaces MAY check certain traits that change the value of the NFT (e.g. redemption status, defined by metadata values with `validateOnSale` property) or they MAY hash all the trait values to guarantee the same state at the time of order creation. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/assets/erc-7496/DynamicTraits.sol b/assets/erc-7496/DynamicTraits.sol new file mode 100644 index 0000000000..edeb0a5a4d --- /dev/null +++ b/assets/erc-7496/DynamicTraits.sol @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.19; + +import {IERC7496} from "./interfaces/IERC7496.sol"; + +library DynamicTraitsStorage { + struct Layout { + /// @dev A mapping of token ID to a mapping of trait key to trait value. + mapping(uint256 tokenId => mapping(bytes32 traitKey => bytes32 traitValue)) _traits; + /// @dev An offchain string URI that points to a JSON file containing trait metadata. + string _traitMetadataURI; + } + + bytes32 internal constant STORAGE_SLOT = keccak256("contracts.storage.erc7496-dynamictraits"); + + function layout() internal pure returns (Layout storage l) { + bytes32 slot = STORAGE_SLOT; + assembly { + l.slot := slot + } + } +} + +/** + * @title DynamicTraits + * + * @dev Implementation of [ERC-7496](https://eips.ethereum.org/EIPS/eip-7496) Dynamic Traits. + * Uses a storage layout pattern for upgradeable contracts. + * + * Requirements: + * - Overwrite `setTrait` with access role restriction. + * - Expose a function for `setTraitMetadataURI` with access role restriction if desired. + */ +contract DynamicTraits is IERC7496 { + using DynamicTraitsStorage for DynamicTraitsStorage.Layout; + + /** + * @notice Get the value of a trait for a given token ID. + * @param tokenId The token ID to get the trait value for + * @param traitKey The trait key to get the value of + */ + function getTraitValue(uint256 tokenId, bytes32 traitKey) public view virtual returns (bytes32 traitValue) { + // Return the trait value. + return DynamicTraitsStorage.layout()._traits[tokenId][traitKey]; + } + + /** + * @notice Get the values of traits for a given token ID. + * @param tokenId The token ID to get the trait values for + * @param traitKeys The trait keys to get the values of + */ + function getTraitValues(uint256 tokenId, bytes32[] calldata traitKeys) + public + view + virtual + returns (bytes32[] memory traitValues) + { + // Set the length of the traitValues return array. + uint256 length = traitKeys.length; + traitValues = new bytes32[](length); + + // Assign each trait value to the corresopnding key. + for (uint256 i = 0; i < length;) { + bytes32 traitKey = traitKeys[i]; + traitValues[i] = getTraitValue(tokenId, traitKey); + unchecked { + ++i; + } + } + } + + /** + * @notice Get the URI for the trait metadata + */ + function getTraitMetadataURI() external view virtual returns (string memory labelsURI) { + // Return the trait metadata URI. + return DynamicTraitsStorage.layout()._traitMetadataURI; + } + + /** + * @notice Set the value of a trait for a given token ID. + * Reverts if the trait value is unchanged. + * @dev IMPORTANT: Override this method with access role restriction. + * @param tokenId The token ID to set the trait value for + * @param traitKey The trait key to set the value of + * @param newValue The new trait value to set + */ + function setTrait(uint256 tokenId, bytes32 traitKey, bytes32 newValue) public virtual { + // Revert if the new value is the same as the existing value. + bytes32 existingValue = DynamicTraitsStorage.layout()._traits[tokenId][traitKey]; + if (existingValue == newValue) { + revert TraitValueUnchanged(); + } + + // Set the new trait value. + _setTrait(tokenId, traitKey, newValue); + + // Emit the event noting the update. + emit TraitUpdated(traitKey, tokenId, newValue); + } + + /** + * @notice Set the trait value (without emitting an event). + * @param tokenId The token ID to set the trait value for + * @param traitKey The trait key to set the value of + * @param newValue The new trait value to set + */ + function _setTrait(uint256 tokenId, bytes32 traitKey, bytes32 newValue) internal virtual { + // Set the new trait value. + DynamicTraitsStorage.layout()._traits[tokenId][traitKey] = newValue; + } + + /** + * @notice Set the URI for the trait metadata. + * @param uri The new URI to set. + */ + function _setTraitMetadataURI(string memory uri) internal virtual { + // Set the new trait metadata URI. + DynamicTraitsStorage.layout()._traitMetadataURI = uri; + + // Emit the event noting the update. + emit TraitMetadataURIUpdated(); + } + + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) { + return interfaceId == type(IERC7496).interfaceId; + } +} diff --git a/assets/erc-7496/DynamicTraitsSchema.json b/assets/erc-7496/DynamicTraitsSchema.json new file mode 100644 index 0000000000..14ff7f63dd --- /dev/null +++ b/assets/erc-7496/DynamicTraitsSchema.json @@ -0,0 +1,156 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "required": ["traits"], + "properties": { + "traits": { + "type": "object", + "additionalProperties": { + "type": "object", + "required": ["dataType"], + "properties": { + "displayName": { + "type": ["string"], + "description": "The user-facing display name for the trait." + }, + "validateOnSale": { + "enum": ["none", "requireEq", "requireUintGte", "requireUintLte"], + "description": "Whether the trait value should be validated when the token is sold. If this isn't specified, it is assumed to be `none`." + }, + "tokenOwnerCanUpdateValue": { + "type": "boolean", + "description": "Whether the token owner is able to set the trait value directly by calling `setTrait`. If this isn't specified, it is assumed to be false." + }, + "dataType": { + "oneOf": [ + { + "type": "object", + "required": ["type"], + "properties": { + "type": { "const": "string" }, + "acceptableValues": { + "type": "array", + "description": "An exclusive list of possible string values that can be set for the trait. If this is not specified, the trait can be set to any reasonable string value. If `valueMappings` is specified, this list must be the `mappedValue`s.", + "items": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "maxLength": { + "type": "number", + "description": "The maximum length of the string value that can be set for the trait (inclusive). If this is not specified, the trait can be set to any reasonable string length." + }, + "minLength": { + "type": "number", + "description": "The minimum length of the string value that can be set for the trait (inclusive). If this is not specified, it is assumed to be 0." + }, + "valueMappings": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "description": "A dictionary mapping of `traitValue`s returned from the contract to values that an offchain indexer should display. The keys to the dictionary are the onchain values and the dictionary values are the offchain values. Useful when longer than the 32 ASCII characters that bytes32 allows for." + } + } + }, + { + "type": "object", + "required": ["type"], + "properties": { + "type": { "const": "decimal" }, + "signed": { + "type": "boolean", + "description": "Whether the trait value being returned is signed. If this is not specified, it is assumed to be false." + }, + "decimals": { + "type": "integer", + "description": "The number of decimal places that the trait value is returned with onchain. If this is not specified, it is assumed to be 0 (i.e. an integer value)." + }, + "minValue": { + "type": "number", + "description": "The minimum value that the trait value can be set to (inclusive). If this is not specified, it is assumed to be the minimum value of the `signed` and `decimals`." + }, + "maxValue": { + "type": "number", + "description": "The maximum value that the trait value can be set to (inclusive). If this is not specified, it is assumed to be the maximum value of the `signed` and `decimals`." + }, + "valueMappings": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "description": "A dictionary mapping of `traitValue`s returned from the contract to values that an offchain indexer should display. The keys to the dictionary are the onchain values and the dictionary values are the offchain values. Useful for default values of 0x0 and large or magic numbers." + } + } + }, + { + "type": "object", + "required": ["type"], + "properties": { + "type": { "const": "boolean" }, + "valueMappings": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "description": "A dictionary mapping of `traitValue`s returned from the contract to values that an offchain indexer should display. The keys to the dictionary are the onchain values and the dictionary values are the offchain values. Useful for default values of 0x0 and magic numbers." + } + } + }, + { + "type": "object", + "required": ["type"], + "properties": { + "type": { "const": "epochSeconds" }, + "valueMappings": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + }, + "description": "A dictionary mapping of `traitValue`s returned from the contract to values that an offchain indexer should display. The keys to the dictionary are the onchain values and the dictionary values are the offchain values. Useful for default values of 0x0 and magic numbers." + } + }, + "description": "A datetime type that is the number of seconds since the Unix epoch (January 1, 1970 00:00:00 UTC). Must return an integer value." + } + ], + "description": "The data type definition of the trait." + } + } + } + } + } +} diff --git a/assets/erc-7496/ERC721DynamicTraits.sol b/assets/erc-7496/ERC721DynamicTraits.sol new file mode 100644 index 0000000000..9b22705a08 --- /dev/null +++ b/assets/erc-7496/ERC721DynamicTraits.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.19; + +import {ERC721} from "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; +import {Ownable} from "openzeppelin-contracts/access/Ownable.sol"; +import {DynamicTraits} from "src/dynamic-traits/DynamicTraits.sol"; + +contract ERC721DynamicTraits is DynamicTraits, Ownable, ERC721 { + constructor() Ownable(msg.sender) ERC721("ERC721DynamicTraits", "ERC721DT") { + _setTraitMetadataURI("https://example.com"); + } + + function setTrait(uint256 tokenId, bytes32 traitKey, bytes32 value) public virtual override onlyOwner { + // Revert if the token doesn't exist. + _requireOwned(tokenId); + + // Call the internal function to set the trait. + DynamicTraits.setTrait(tokenId, traitKey, value); + } + + function getTraitValue(uint256 tokenId, bytes32 traitKey) + public + view + virtual + override + returns (bytes32 traitValue) + { + // Revert if the token doesn't exist. + _requireOwned(tokenId); + + // Call the internal function to get the trait value. + return DynamicTraits.getTraitValue(tokenId, traitKey); + } + + function getTraitValues(uint256 tokenId, bytes32[] calldata traitKeys) + public + view + virtual + override + returns (bytes32[] memory traitValues) + { + // Revert if the token doesn't exist. + _requireOwned(tokenId); + + // Call the internal function to get the trait values. + return DynamicTraits.getTraitValues(tokenId, traitKeys); + } + + function setTraitMetadataURI(string calldata uri) external onlyOwner { + // Set the new metadata URI. + _setTraitMetadataURI(uri); + } + + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721, DynamicTraits) returns (bool) { + return ERC721.supportsInterface(interfaceId) || DynamicTraits.supportsInterface(interfaceId); + } +} diff --git a/assets/erc-7496/ERC721DynamicTraits.t.sol b/assets/erc-7496/ERC721DynamicTraits.t.sol new file mode 100644 index 0000000000..33febdafa3 --- /dev/null +++ b/assets/erc-7496/ERC721DynamicTraits.t.sol @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import {IERC721Errors} from "openzeppelin-contracts/contracts/interfaces/draft-IERC6093.sol"; +import {ERC721} from "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; +import {Ownable} from "openzeppelin-contracts/contracts/access/Ownable.sol"; +import {IERC7496} from "src/dynamic-traits/interfaces/IERC7496.sol"; +import {ERC721DynamicTraits} from "src/dynamic-traits/ERC721DynamicTraits.sol"; +import {Solarray} from "solarray/Solarray.sol"; + +contract ERC721DynamicTraitsMintable is ERC721DynamicTraits { + constructor() ERC721DynamicTraits() {} + + function mint(address to, uint256 tokenId) public onlyOwner { + _mint(to, tokenId); + } +} + +contract ERC721DynamicTraitsTest is Test { + ERC721DynamicTraitsMintable token; + + /* Events */ + event TraitUpdated(bytes32 indexed traitKey, uint256 tokenId, bytes32 trait); + event TraitMetadataURIUpdated(); + + function setUp() public { + token = new ERC721DynamicTraitsMintable(); + } + + function testSupportsInterfaceId() public { + assertTrue(token.supportsInterface(type(IERC7496).interfaceId)); + } + + function testReturnsValueSet() public { + bytes32 key = bytes32("testKey"); + bytes32 value = bytes32("foo"); + uint256 tokenId = 12345; + token.mint(address(this), tokenId); + + vm.expectEmit(true, true, true, true); + emit TraitUpdated(key, tokenId, value); + + token.setTrait(tokenId, key, value); + + assertEq(token.getTraitValue(tokenId, key), value); + } + + function testOnlyOwnerCanSetValues() public { + address alice = makeAddr("alice"); + vm.prank(alice); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, alice)); + token.setTrait(0, bytes32("test"), bytes32("test")); + } + + function testSetTrait_Unchanged() public { + bytes32 key = bytes32("testKey"); + bytes32 value = bytes32("foo"); + uint256 tokenId = 1; + token.mint(address(this), tokenId); + + token.setTrait(tokenId, key, value); + vm.expectRevert(IERC7496.TraitValueUnchanged.selector); + token.setTrait(tokenId, key, value); + } + + function testGetTraitValues() public { + bytes32 key1 = bytes32("testKeyOne"); + bytes32 key2 = bytes32("testKeyTwo"); + bytes32 value1 = bytes32("foo"); + bytes32 value2 = bytes32("bar"); + uint256 tokenId = 1; + token.mint(address(this), tokenId); + + token.setTrait(tokenId, key1, value1); + token.setTrait(tokenId, key2, value2); + + bytes32[] memory values = token.getTraitValues(tokenId, Solarray.bytes32s(key1, key2)); + assertEq(values[0], value1); + assertEq(values[1], value2); + } + + function testGetAndSetTraitMetadataURI() public { + string memory uri = "https://example.com/labels.json"; + + vm.expectEmit(true, true, true, true); + emit TraitMetadataURIUpdated(); + token.setTraitMetadataURI(uri); + + assertEq(token.getTraitMetadataURI(), uri); + + vm.prank(address(0x1234)); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, address(0x1234))); + token.setTraitMetadataURI(uri); + } + + function testGetAndSetTraitValue_NonexistantToken() public { + bytes32 key = bytes32("testKey"); + bytes32 value = bytes32(uint256(1)); + uint256 tokenId = 1; + + vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721NonexistentToken.selector, tokenId)); + token.setTrait(tokenId, key, value); + + vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721NonexistentToken.selector, tokenId)); + token.getTraitValue(tokenId, key); + + vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721NonexistentToken.selector, tokenId)); + token.getTraitValues(tokenId, Solarray.bytes32s(key)); + } + + function testGetTraitValue_DefaultZeroValue() public { + bytes32 key = bytes32("testKey"); + uint256 tokenId = 1; + token.mint(address(this), tokenId); + + bytes32 value = token.getTraitValue(tokenId, key); + assertEq(value, bytes32(0), "should return bytes32(0)"); + + bytes32[] memory values = token.getTraitValues(tokenId, Solarray.bytes32s(key)); + assertEq(values[0], bytes32(0), "should return bytes32(0)"); + } +} From 288e729788cc0aada3633f4bade7b8e176b5f716 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Wed, 29 Nov 2023 10:16:20 -0800 Subject: [PATCH 17/23] Add ERC: NFT Redeemables Merged by EIP-Bot. --- ERCS/erc-7498.md | 310 +++++++++++ assets/erc-7498/ERC721ShipyardRedeemable.sol | 53 ++ .../erc-7498/ERC721ShipyardRedeemable.t.sol | 499 ++++++++++++++++++ assets/erc-7498/ERC7498NFTRedeemables.sol | 394 ++++++++++++++ assets/erc-7498/IERC7498.sol | 32 ++ assets/erc-7498/IRedemptionMintable.sol | 14 + assets/erc-7498/RedeemablesErrors.sol | 38 ++ assets/erc-7498/RedeemablesStructs.sol | 28 + 8 files changed, 1368 insertions(+) create mode 100644 ERCS/erc-7498.md create mode 100644 assets/erc-7498/ERC721ShipyardRedeemable.sol create mode 100644 assets/erc-7498/ERC721ShipyardRedeemable.t.sol create mode 100644 assets/erc-7498/ERC7498NFTRedeemables.sol create mode 100644 assets/erc-7498/IERC7498.sol create mode 100644 assets/erc-7498/IRedemptionMintable.sol create mode 100644 assets/erc-7498/RedeemablesErrors.sol create mode 100644 assets/erc-7498/RedeemablesStructs.sol diff --git a/ERCS/erc-7498.md b/ERCS/erc-7498.md new file mode 100644 index 0000000000..3789c4b3e0 --- /dev/null +++ b/ERCS/erc-7498.md @@ -0,0 +1,310 @@ +--- +eip: 7498 +title: NFT Redeemables +description: Extension to ERC-721 and ERC-1155 for onchain and offchain redeemables +author: Ryan Ghods (@ryanio), 0age (@0age), Adam Montgomery (@montasaurus), Stephan Min (@stephankmin) +discussions-to: https://ethereum-magicians.org/t/erc-7498-nft-redeemables/15485 +status: Draft +type: Standards Track +category: ERC +created: 2023-07-28 +requires: 165, 712, 721, 1155, 1271 +--- + +## Abstract + +This specification introduces a new interface that extends [ERC-721](./eip-721.md) and [ERC-1155](./eip-1155.md) to enable the discovery and use of onchain and offchain redeemables for NFTs. Onchain getters and events facilitate discovery of redeemable campaigns and their requirements. New onchain mints use an interface that gives context to the minting contract of what was redeemed. For redeeming physical products and goods (offchain redeemables) a `redemptionHash` and `signer` can tie onchain redemptions with offchain order identifiers that contain chosen product and shipping information. + +## Motivation + +Creators frequently use NFTs to create redeemable entitlements for digital and physical goods. However, without a standard interface, it is challenging for users and apps to discover and interact with these NFTs in a predictable and standard way. This standard aims to encompass enabling broad functionality for: + +- discovery: events and getters that provide information about the requirements of a redemption campaign +- onchain: token mints with context of items spent +- offchain: the ability to associate with ecommerce orders (through `redemptionHash`) +- trait redemptions: improving the burn-to-redeem experience with [ERC-7496](./eip-7496.md) Dynamic Traits. + +## Specification + +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174. + +The token MUST have the following interface and MUST return `true` for [ERC-165](./eip-165.md) supportsInterface for `0x1ac61e13`, the 4 byte interfaceId of the below. + +```solidity +interface IERC7498 { + /* Events */ + event CampaignUpdated(uint256 indexed campaignId, Campaign campaign, string metadataURI); + event Redemption(uint256 indexed campaignId, uint256 requirementsIndex, bytes32 redemptionHash, uint256[] considerationTokenIds, uint256[] traitRedemptionTokenIds, address redeemedBy); + + /* Structs */ + struct Campaign { + CampaignParams params; + CampaignRequirements[] requirements; // one requirement must be fully satisfied for a successful redemption + } + struct CampaignParams { + uint32 startTime; + uint32 endTime; + uint32 maxCampaignRedemptions; + address manager; // the address that can modify the campaign + address signer; // null address means no EIP-712 signature required + } + struct CampaignRequirements { + OfferItem[] offer; + ConsiderationItem[] consideration; + TraitRedemption[] traitRedemptions; + } + struct TraitRedemption { + uint8 substandard; + address token; + bytes32 traitKey; + bytes32 traitValue; + bytes32 substandardValue; + } + + /* Getters */ + function getCampaign(uint256 campaignId) external view returns (Campaign memory campaign, string memory metadataURI, uint256 totalRedemptions); + + /* Setters */ + function createCampaign(Campaign calldata campaign, string calldata metadataURI) external returns (uint256 campaignId); + function updateCampaign(uint256 campaignId, Campaign calldata campaign, string calldata metadataURI) external; + function redeem(uint256[] calldata considerationTokenIds, address recipient, bytes calldata extraData) external payable; +} + +--- + +/* Seaport structs, for reference, used in offer/consideration above */ +enum ItemType { + NATIVE, + ERC20, + ERC721, + ERC1155 +} +struct OfferItem { + ItemType itemType; + address token; + uint256 identifierOrCriteria; + uint256 startAmount; + uint256 endAmount; +} +struct ConsiderationItem extends OfferItem { + address payable recipient; + // (note: psuedocode above, as of this writing can't extend structs in solidity) +} +struct SpentItem { + ItemType itemType; + address token; + uint256 identifier; + uint256 amount; +} +``` + +### Creating campaigns + +When creating a new campaign, `createCampaign` MUST be used and MUST return the newly created `campaignId` along with the `CampaignUpdated` event. The `campaignId` MUST be a counter incremented with each new campaign. The first campaign MUST have an id of `1`. + +### Updating campaigns + +Updates to campaigns MAY use `updateCampaign` and MUST emit the `CampaignUpdated` event. If an address other than the `manager` tries to update the campaign, it MUST revert with `NotManager()`. If the manager wishes to make the campaign immutable, the `manager` MAY be set to the null address. + +### Offer + +If tokens are set in the params `offer`, the tokens MUST implement the `IRedemptionMintable` interface in order to support minting new items. The implementation SHOULD be however the token mechanics are desired. The implementing token MUST return true for ERC-165 `supportsInterface` for the interfaceId of `IRedemptionMintable`, `0x81fe13c2`. + +```solidity +interface IRedemptionMintable { + function mintRedemption( + uint256 campaignId, + address recipient, + OfferItem calldata offer, + ConsiderationItem[] calldata consideration, + TraitRedemption[] calldata traitRedemptions + ) external; +} +``` + +When `mintRedemption` is called, it is RECOMMENDED to replace the token identifiers in the consideration items and trait redemptions with the items actually being redeemed. + +### Consideration + +Any token may be specified in the campaign requirement `consideration`. This will ensure the token is transferred to the `recipient`. If the token is meant to be burned, the recipient SHOULD be `0x000000000000000000000000000000000000dEaD`. If the token can internally handle burning its own tokens and reducing totalSupply, the token MAY burn the token instead of transferring to the recipient `0x000000000000000000000000000000000000dEaD`. + +### Dynamic traits + +Including trait redemptions is optional, but if the token would like to enable trait redemptions the token MUST include [ERC-7496](./eip-7496.md) Dynamic Traits. + +### Signer + +A signer MAY be specified to provide a signature to process the redemption. If the signer is not the null address, the signature MUST recover to the signer address via [EIP-712](./eip-712.md) or [ERC-1271](./eip-1271.md). + +The EIP-712 struct for signing MUST be as follows: `SignedRedeem(address owner,uint256[] considerationTokenIds,uint256[] traitRedemptionTokenIds,uint256 campaignId,uint256 requirementsIndex, bytes32 redemptionHash, uint256 salt)"` + +### Redeem function + +The `redeem` function MUST use the `consideration`, `offer`, and `traitRedemptions` specified by the `requirements` determined by the `campaignId` and `requirementsIndex`: + +- Execute the transfers in the `consideration` +- Mutate the traits specified by `traitRedemptions` according to ERC-7496 Dynamic Traits +- Call `mintRedemption()` on every `offer` item + +The `Redemption` event MUST be emitted for every valid redemption that occurs. + +#### Redemption extraData + +The extraData layout MUST conform to the below: + +| bytes | value | description / notes | +| -------- | --------------------------------- | ------------------------------------------------------------------------------------ | +| 0-32 | campaignId | | +| 32-64 | requirementsIndex | index of the campaign requirements met | +| 64-96 | redemptionHash | hash of offchain order ids | +| 96-\* | uint256[] traitRedemptionTokenIds | token ids for trait redemptions, MUST be in same order of campaign TraitRedemption[] | +| \*-(+32) | salt | if signer != address(0) | +| \*-(+\*) | signature | if signer != address(0). can be for EIP-712 or ERC-1271 | + +The `requirementsIndex` MUST be the index in the `requirements` array that satisfies the redemption. This helps reduce gas to find the requirement met. + +The `traitRedemptionTokenIds` specifies the token IDs required for the trait redemptions in the requirements array. The order MUST be the same order of the token addresses expected in the array of `TraitRedemption` structs in the campaign requirement used. + +If the campaign `signer` is the null address the `salt` and `signature` MUST be omitted. + +The `redemptionHash` is designated for offchain redemptions to reference offchain order identifiers to track the redemption to. + +The function MUST check that the campaign is active (using the same boundary check as Seaport, `startTime <= block.timestamp < endTime`). If it is not active, it MUST revert with `NotActive()`. + +### Trait redemptions + +The token MUST respect the TraitRedemption substandards as follows: + +| substandard ID | description | substandard value | +| -------------- | ------------------------------- | ------------------------------------------------------------------ | +| 1 | set value to `traitValue` | prior required value. if blank, cannot be the `traitValue` already | +| 2 | increment trait by `traitValue` | max value | +| 3 | decrement trait by `traitValue` | min value | +| 4 | check value is `traitValue` | n/a | + +### Max campaign redemptions + +The token MUST check that the `maxCampaignRedemptions` is not exceeded. If the redemption does exceed `maxCampaignRedemptions`, it MUST revert with `MaxCampaignRedemptionsReached(uint256 total, uint256 max)` + +### Metadata URI + +The metadata URI MUST conform to the below JSON schema: + +```json +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "campaigns": { + "type": "array", + "items": { + "type": "object", + "properties": { + "campaignId": { + "type": "number" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string", + "description": "A one-line summary of the redeemable. Markdown is not supported." + }, + "details": { + "type": "string", + "description": "A multi-line or multi-paragraph description of the details of the redeemable. Markdown is supported." + }, + "imageUrls": { + "type": "array", + "items": { + "type": "string" + }, + "description": "A list of image URLs for the redeemable. The first image will be used as the thumbnail. Will rotate in a carousel if multiple images are provided. Maximum 5 images." + }, + "bannerUrl": { + "type": "string", + "description": "The banner image for the redeemable." + }, + "faq": { + "type": "array", + "items": { + "type": "object", + "properties": { + "question": { + "type": "string" + }, + "answer": { + "type": "string" + }, + "required": ["question", "answer"] + } + } + }, + "contentLocale": { + "type": "string", + "description": "The language tag for the content provided by this metadata. https://www.rfc-editor.org/rfc/rfc9110.html#name-language-tags" + }, + "maxRedemptionsPerToken": { + "type": "string", + "description": "The maximum number of redemptions per token. When isBurn is true should be 1, else can be a number based on the trait redemptions limit." + }, + "isBurn": { + "type": "string", + "description": "If the redemption burns the token." + }, + "uuid": { + "type": "string", + "description": "An optional unique identifier for the campaign, for backends to identify when draft campaigns are published onchain." + }, + "productLimitForRedemption": { + "type": "number", + "description": "The number of products which are able to be chosen from the products array for a single redemption." + }, + "products": { + "type": "object", + "properties": "https://schema.org/Product", + "required": ["name", "url", "description"] + } + }, + "required": ["campaignId", "name", "description", "imageUrls", "isBurn"] + } + } + } +} +``` + +Future EIPs MAY inherit this one and add to the above metadata to add more features and functionality. + +### ERC-1155 (Semi-fungibles) + +This standard MAY be applied to ERC-1155 but the redemptions would apply to all token amounts for specific token identifiers. If the ERC-1155 contract only has tokens with amount of 1, then this specification MAY be used as written. + +## Rationale + +The "offer" and "consideration" structs from Seaport were used to create a similar language for redeemable campaigns. The "offer" is what is being offered, e.g. a new onchain token, and the "consideration" is what must be satisfied to complete the redemption. The "consideration" field has a "recipient", who the token should be transferred to. For trait updates that do not require moving of a token, `traitRedemptionTokenIds` is specified instead. + +The "salt" and "signature" fields are provided primarily for offchain redemptions where a provider would want to sign approval for a redemption before it is conducted onchain, to prevent the need for irregular state changes. For example, if a user lives outside a region supported by the shipping of an offchain redeemable, during the offchain order creation process the signature would not be provided for the onchain redemption when seeing that the user's shipping country is unsupported. This prevents the user from redeeming the NFT, then later finding out the shipping isn't supported after their NFT is already burned or trait is mutated. + +[ERC-7496](./eip-7496.md) Dynamic Traits is used for trait redemptions to support onchain enforcement of trait values for secondary market orders. + +## Backwards Compatibility + +As a new EIP, no backwards compatibility issues are present. + +## Test Cases + +Authors have included Foundry tests covering functionality of the specification in the [assets folder](../assets/eip-7498/ERC721ShipyardRedeemable.t.sol). + +## Reference Implementation + +Authors have included reference implementations of the specification in the [assets folder](../assets/eip-7498/ERC7498NFTRedeemables.sol). + +## Security Considerations + +If trait redemptions are desired, tokens implementing this EIP must properly implement [ERC-7496](./eip-7496.md) Dynamic Traits. + +For tokens to be minted as part of the params `offer`, the `mintRedemption` function contained as part of `IRedemptionMintable` MUST be permissioned and ONLY allowed to be called by specified addresses. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/assets/erc-7498/ERC721ShipyardRedeemable.sol b/assets/erc-7498/ERC721ShipyardRedeemable.sol new file mode 100644 index 0000000000..96b4fd734a --- /dev/null +++ b/assets/erc-7498/ERC721ShipyardRedeemable.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.19; + +import {ERC721ConduitPreapproved_Solady} from "shipyard-core/src/tokens/erc721/ERC721ConduitPreapproved_Solady.sol"; +import {ERC721} from "solady/src/tokens/ERC721.sol"; +import {Ownable} from "solady/src/auth/Ownable.sol"; +import {ERC7498NFTRedeemables} from "./lib/ERC7498NFTRedeemables.sol"; +import {CampaignParams} from "./lib/RedeemablesStructs.sol"; + +contract ERC721ShipyardRedeemable is ERC721ConduitPreapproved_Solady, ERC7498NFTRedeemables, Ownable { + constructor() ERC721ConduitPreapproved_Solady() { + _initializeOwner(msg.sender); + } + + function name() public pure override returns (string memory) { + return "ERC721ShipyardRedeemable"; + } + + function symbol() public pure override returns (string memory) { + return "SY-RDM"; + } + + function tokenURI(uint256 /* tokenId */ ) public pure override returns (string memory) { + return "https://example.com/"; + } + + function createCampaign(CampaignParams calldata params, string calldata uri) + public + override + onlyOwner + returns (uint256 campaignId) + { + campaignId = ERC7498NFTRedeemables.createCampaign(params, uri); + } + + function _useInternalBurn() internal pure virtual override returns (bool) { + return true; + } + + function _internalBurn(uint256 id, uint256 /* amount */ ) internal virtual override { + _burn(id); + } + + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(ERC721, ERC7498NFTRedeemables) + returns (bool) + { + return ERC721.supportsInterface(interfaceId) || ERC7498NFTRedeemables.supportsInterface(interfaceId); + } +} diff --git a/assets/erc-7498/ERC721ShipyardRedeemable.t.sol b/assets/erc-7498/ERC721ShipyardRedeemable.t.sol new file mode 100644 index 0000000000..a589b983d7 --- /dev/null +++ b/assets/erc-7498/ERC721ShipyardRedeemable.t.sol @@ -0,0 +1,499 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.19; + +import {Test} from "forge-std/Test.sol"; +import {Solarray} from "solarray/Solarray.sol"; +import {ERC721} from "solady/src/tokens/ERC721.sol"; +import {TestERC721} from "./utils/mocks/TestERC721.sol"; +import {OfferItem, ConsiderationItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; +import {ItemType, OrderType, Side} from "seaport-sol/src/SeaportEnums.sol"; +import {CampaignParams, CampaignRequirements, TraitRedemption} from "../src/lib/RedeemablesStructs.sol"; +import {RedeemablesErrors} from "../src/lib/RedeemablesErrors.sol"; +import {ERC721RedemptionMintable} from "../src/extensions/ERC721RedemptionMintable.sol"; +import {ERC721ShipyardRedeemableOwnerMintable} from "../src/test/ERC721ShipyardRedeemableOwnerMintable.sol"; + +contract TestERC721ShipyardRedeemable is RedeemablesErrors, Test { + event Redemption( + uint256 indexed campaignId, + uint256 requirementsIndex, + bytes32 redemptionHash, + uint256[] considerationTokenIds, + uint256[] traitRedemptionTokenIds, + address redeemedBy + ); + + ERC721ShipyardRedeemableOwnerMintable redeemToken; + ERC721RedemptionMintable receiveToken; + address alice; + + address constant _BURN_ADDRESS = 0x000000000000000000000000000000000000dEaD; + + function setUp() public { + redeemToken = new ERC721ShipyardRedeemableOwnerMintable(); + receiveToken = new ERC721RedemptionMintable(address(redeemToken)); + alice = makeAddr("alice"); + + vm.label(address(redeemToken), "redeemToken"); + vm.label(address(receiveToken), "receiveToken"); + vm.label(alice, "alice"); + } + + function testBurnInternalToken() public { + uint256 tokenId = 2; + redeemToken.mint(address(this), tokenId); + + OfferItem[] memory offer = new OfferItem[](1); + offer[0] = OfferItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(receiveToken), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1 + }); + + ConsiderationItem[] memory consideration = new ConsiderationItem[](1); + consideration[0] = ConsiderationItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(redeemToken), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1, + recipient: payable(_BURN_ADDRESS) + }); + + CampaignRequirements[] memory requirements = new CampaignRequirements[]( + 1 + ); + requirements[0].offer = offer; + requirements[0].consideration = consideration; + + { + CampaignParams memory params = CampaignParams({ + requirements: requirements, + signer: address(0), + startTime: uint32(block.timestamp), + endTime: uint32(block.timestamp + 1000), + maxCampaignRedemptions: 5, + manager: address(this) + }); + + redeemToken.createCampaign(params, ""); + } + + { + OfferItem[] memory offerFromEvent = new OfferItem[](1); + offerFromEvent[0] = OfferItem({ + itemType: ItemType.ERC721, + token: address(receiveToken), + identifierOrCriteria: tokenId, + startAmount: 1, + endAmount: 1 + }); + ConsiderationItem[] memory considerationFromEvent = new ConsiderationItem[](1); + considerationFromEvent[0] = ConsiderationItem({ + itemType: ItemType.ERC721, + token: address(redeemToken), + identifierOrCriteria: tokenId, + startAmount: 1, + endAmount: 1, + recipient: payable(_BURN_ADDRESS) + }); + + assertGt(uint256(consideration[0].itemType), uint256(considerationFromEvent[0].itemType)); + + // campaignId: 1 + // requirementsIndex: 0 + // redemptionHash: bytes32(0) + bytes memory extraData = abi.encode(1, 0, bytes32(0)); + consideration[0].identifierOrCriteria = tokenId; + + uint256[] memory considerationTokenIds = Solarray.uint256s(tokenId); + uint256[] memory traitRedemptionTokenIds; + + vm.expectEmit(true, true, true, true); + emit Redemption(1, 0, bytes32(0), considerationTokenIds, traitRedemptionTokenIds, address(this)); + redeemToken.redeem(considerationTokenIds, address(this), extraData); + + vm.expectRevert(ERC721.TokenDoesNotExist.selector); + redeemToken.ownerOf(tokenId); + + assertEq(receiveToken.ownerOf(1), address(this)); + } + } + + function testRevert721ConsiderationItemInsufficientBalance() public { + uint256 tokenId = 2; + uint256 invalidTokenId = tokenId + 1; + redeemToken.mint(address(this), tokenId); + redeemToken.mint(alice, invalidTokenId); + + OfferItem[] memory offer = new OfferItem[](1); + offer[0] = OfferItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(receiveToken), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1 + }); + + ConsiderationItem[] memory consideration = new ConsiderationItem[](1); + consideration[0] = ConsiderationItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(redeemToken), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1, + recipient: payable(_BURN_ADDRESS) + }); + + CampaignRequirements[] memory requirements = new CampaignRequirements[]( + 1 + ); + requirements[0].offer = offer; + requirements[0].consideration = consideration; + + { + CampaignParams memory params = CampaignParams({ + requirements: requirements, + signer: address(0), + startTime: uint32(block.timestamp), + endTime: uint32(block.timestamp + 1000), + maxCampaignRedemptions: 5, + manager: address(this) + }); + + redeemToken.createCampaign(params, ""); + } + + { + OfferItem[] memory offerFromEvent = new OfferItem[](1); + offerFromEvent[0] = OfferItem({ + itemType: ItemType.ERC721, + token: address(receiveToken), + identifierOrCriteria: tokenId, + startAmount: 1, + endAmount: 1 + }); + ConsiderationItem[] memory considerationFromEvent = new ConsiderationItem[](1); + considerationFromEvent[0] = ConsiderationItem({ + itemType: ItemType.ERC721, + token: address(redeemToken), + identifierOrCriteria: tokenId, + startAmount: 1, + endAmount: 1, + recipient: payable(_BURN_ADDRESS) + }); + + assertGt(uint256(consideration[0].itemType), uint256(considerationFromEvent[0].itemType)); + + // campaignId: 1 + // requirementsIndex: 0 + // redemptionHash: bytes32(0) + bytes memory extraData = abi.encode(1, 0, bytes32(0)); + consideration[0].identifierOrCriteria = tokenId; + + uint256[] memory tokenIds = Solarray.uint256s(invalidTokenId); + + vm.expectRevert( + abi.encodeWithSelector( + ConsiderationItemInsufficientBalance.selector, + requirements[0].consideration[0].token, + 0, + requirements[0].consideration[0].startAmount + ) + ); + redeemToken.redeem(tokenIds, address(this), extraData); + + assertEq(redeemToken.ownerOf(tokenId), address(this)); + + vm.expectRevert(ERC721.TokenDoesNotExist.selector); + receiveToken.ownerOf(1); + } + } + + function testRevertConsiderationLengthNotMet() public { + ERC721ShipyardRedeemableOwnerMintable secondRedeemToken = new ERC721ShipyardRedeemableOwnerMintable(); + + uint256 tokenId = 2; + redeemToken.mint(address(this), tokenId); + + OfferItem[] memory offer = new OfferItem[](1); + offer[0] = OfferItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(receiveToken), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1 + }); + + ConsiderationItem[] memory consideration = new ConsiderationItem[](2); + consideration[0] = ConsiderationItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(redeemToken), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1, + recipient: payable(_BURN_ADDRESS) + }); + consideration[1] = ConsiderationItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(secondRedeemToken), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1, + recipient: payable(_BURN_ADDRESS) + }); + + CampaignRequirements[] memory requirements = new CampaignRequirements[]( + 1 + ); + requirements[0].offer = offer; + requirements[0].consideration = consideration; + + { + CampaignParams memory params = CampaignParams({ + requirements: requirements, + signer: address(0), + startTime: uint32(block.timestamp), + endTime: uint32(block.timestamp + 1000), + maxCampaignRedemptions: 5, + manager: address(this) + }); + + redeemToken.createCampaign(params, ""); + } + + { + OfferItem[] memory offerFromEvent = new OfferItem[](1); + offerFromEvent[0] = OfferItem({ + itemType: ItemType.ERC721, + token: address(receiveToken), + identifierOrCriteria: tokenId, + startAmount: 1, + endAmount: 1 + }); + ConsiderationItem[] memory considerationFromEvent = new ConsiderationItem[](1); + considerationFromEvent[0] = ConsiderationItem({ + itemType: ItemType.ERC721, + token: address(redeemToken), + identifierOrCriteria: tokenId, + startAmount: 1, + endAmount: 1, + recipient: payable(_BURN_ADDRESS) + }); + + assertGt(uint256(consideration[0].itemType), uint256(considerationFromEvent[0].itemType)); + + // campaignId: 1 + // requirementsIndex: 0 + // redemptionHash: bytes32(0) + bytes memory extraData = abi.encode(1, 0, bytes32(0)); + consideration[0].identifierOrCriteria = tokenId; + + uint256[] memory tokenIds = Solarray.uint256s(tokenId); + + vm.expectRevert(abi.encodeWithSelector(TokenIdsDontMatchConsiderationLength.selector, 2, 1)); + + redeemToken.redeem(tokenIds, address(this), extraData); + + assertEq(redeemToken.ownerOf(tokenId), address(this)); + + vm.expectRevert(ERC721.TokenDoesNotExist.selector); + receiveToken.ownerOf(1); + } + } + + function testBurnWithSecondConsiderationItem() public { + ERC721ShipyardRedeemableOwnerMintable secondRedeemToken = new ERC721ShipyardRedeemableOwnerMintable(); + vm.label(address(secondRedeemToken), "secondRedeemToken"); + secondRedeemToken.setApprovalForAll(address(redeemToken), true); + + uint256 tokenId = 2; + redeemToken.mint(address(this), tokenId); + secondRedeemToken.mint(address(this), tokenId); + + OfferItem[] memory offer = new OfferItem[](1); + offer[0] = OfferItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(receiveToken), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1 + }); + + ConsiderationItem[] memory consideration = new ConsiderationItem[](2); + consideration[0] = ConsiderationItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(redeemToken), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1, + recipient: payable(_BURN_ADDRESS) + }); + consideration[1] = ConsiderationItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(secondRedeemToken), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1, + recipient: payable(_BURN_ADDRESS) + }); + + CampaignRequirements[] memory requirements = new CampaignRequirements[]( + 1 + ); + requirements[0].offer = offer; + requirements[0].consideration = consideration; + + { + CampaignParams memory params = CampaignParams({ + requirements: requirements, + signer: address(0), + startTime: uint32(block.timestamp), + endTime: uint32(block.timestamp + 1000), + maxCampaignRedemptions: 5, + manager: address(this) + }); + + redeemToken.createCampaign(params, ""); + } + + { + OfferItem[] memory offerFromEvent = new OfferItem[](1); + offerFromEvent[0] = OfferItem({ + itemType: ItemType.ERC721, + token: address(receiveToken), + identifierOrCriteria: tokenId, + startAmount: 1, + endAmount: 1 + }); + ConsiderationItem[] memory considerationFromEvent = new ConsiderationItem[](1); + considerationFromEvent[0] = ConsiderationItem({ + itemType: ItemType.ERC721, + token: address(redeemToken), + identifierOrCriteria: tokenId, + startAmount: 1, + endAmount: 1, + recipient: payable(_BURN_ADDRESS) + }); + + assertGt(uint256(consideration[0].itemType), uint256(considerationFromEvent[0].itemType)); + + // campaignId: 1 + // requirementsIndex: 0 + // redemptionHash: bytes32(0) + bytes memory extraData = abi.encode(1, 0, bytes32(0)); + consideration[0].identifierOrCriteria = tokenId; + + uint256[] memory tokenIds = Solarray.uint256s(tokenId, tokenId); + + redeemToken.redeem(tokenIds, address(this), extraData); + + vm.expectRevert(ERC721.TokenDoesNotExist.selector); + redeemToken.ownerOf(tokenId); + + assertEq(secondRedeemToken.ownerOf(tokenId), _BURN_ADDRESS); + + assertEq(receiveToken.ownerOf(1), address(this)); + } + } + + function testBurnWithSecondRequirementsIndex() public { + ERC721ShipyardRedeemableOwnerMintable secondRedeemToken = new ERC721ShipyardRedeemableOwnerMintable(); + vm.label(address(secondRedeemToken), "secondRedeemToken"); + secondRedeemToken.setApprovalForAll(address(redeemToken), true); + + uint256 tokenId = 2; + redeemToken.mint(address(this), tokenId); + secondRedeemToken.mint(address(this), tokenId); + + OfferItem[] memory offer = new OfferItem[](1); + offer[0] = OfferItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(receiveToken), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1 + }); + + ConsiderationItem[] memory consideration = new ConsiderationItem[](1); + consideration[0] = ConsiderationItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(redeemToken), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1, + recipient: payable(_BURN_ADDRESS) + }); + + ConsiderationItem[] memory secondRequirementConsideration = new ConsiderationItem[](1); + secondRequirementConsideration[0] = ConsiderationItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(secondRedeemToken), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1, + recipient: payable(_BURN_ADDRESS) + }); + + CampaignRequirements[] memory requirements = new CampaignRequirements[]( + 2 + ); + requirements[0].offer = offer; + requirements[0].consideration = consideration; + + requirements[1].offer = offer; + requirements[1].consideration = secondRequirementConsideration; + + { + CampaignParams memory params = CampaignParams({ + requirements: requirements, + signer: address(0), + startTime: uint32(block.timestamp), + endTime: uint32(block.timestamp + 1000), + maxCampaignRedemptions: 5, + manager: address(this) + }); + + redeemToken.createCampaign(params, ""); + } + + { + OfferItem[] memory offerFromEvent = new OfferItem[](1); + offerFromEvent[0] = OfferItem({ + itemType: ItemType.ERC721, + token: address(receiveToken), + identifierOrCriteria: tokenId, + startAmount: 1, + endAmount: 1 + }); + ConsiderationItem[] memory considerationFromEvent = new ConsiderationItem[](1); + considerationFromEvent[0] = ConsiderationItem({ + itemType: ItemType.ERC721, + token: address(redeemToken), + identifierOrCriteria: tokenId, + startAmount: 1, + endAmount: 1, + recipient: payable(_BURN_ADDRESS) + }); + + assertGt(uint256(consideration[0].itemType), uint256(considerationFromEvent[0].itemType)); + + // campaignId: 1 + // requirementsIndex: 0 + // redemptionHash: bytes32(0) + bytes memory extraData = abi.encode(1, 1, bytes32(0)); + consideration[0].identifierOrCriteria = tokenId; + + uint256[] memory tokenIds = Solarray.uint256s(tokenId); + + redeemToken.redeem(tokenIds, address(this), extraData); + + assertEq(redeemToken.ownerOf(tokenId), address(this)); + + assertEq(secondRedeemToken.ownerOf(tokenId), _BURN_ADDRESS); + + assertEq(receiveToken.ownerOf(1), address(this)); + } + } +} diff --git a/assets/erc-7498/ERC7498NFTRedeemables.sol b/assets/erc-7498/ERC7498NFTRedeemables.sol new file mode 100644 index 0000000000..fb854b3b19 --- /dev/null +++ b/assets/erc-7498/ERC7498NFTRedeemables.sol @@ -0,0 +1,394 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.19; + +import {IERC20} from "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; +import {IERC721} from "openzeppelin-contracts/contracts/interfaces/IERC721.sol"; +import {IERC1155} from "openzeppelin-contracts/contracts/interfaces/IERC1155.sol"; +import {OfferItem, ConsiderationItem, SpentItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; +import {ItemType} from "seaport-types/src/lib/ConsiderationEnums.sol"; +import {DynamicTraits} from "shipyard-core/src/dynamic-traits/DynamicTraits.sol"; +import {IERC7498} from "./IERC7498.sol"; +import {IRedemptionMintable} from "./IRedemptionMintable.sol"; +import {RedeemablesErrors} from "./RedeemablesErrors.sol"; +import {CampaignParams, CampaignRequirements, TraitRedemption} from "./RedeemablesStructs.sol"; + +contract ERC7498NFTRedeemables is IERC7498, RedeemablesErrors { + /// @dev Counter for next campaign id. + uint256 private _nextCampaignId = 1; + + /// @dev The campaign parameters by campaign id. + mapping(uint256 campaignId => CampaignParams params) private _campaignParams; + + /// @dev The campaign URIs by campaign id. + mapping(uint256 campaignId => string campaignURI) private _campaignURIs; + + /// @dev The total current redemptions by campaign id. + mapping(uint256 campaignId => uint256 count) private _totalRedemptions; + + /// @dev The burn address. + address constant _BURN_ADDRESS = 0x000000000000000000000000000000000000dEaD; + + struct RedemptionParams { + uint256[] considerationTokenIds; + address recipient; + bytes extraData; + } + + function multiRedeem(RedemptionParams[] calldata params) external payable { + for (uint256 i; i < params.length;) { + redeem(params[i].considerationTokenIds, params[i].recipient, params[i].extraData); + unchecked { + ++i; + } + } + } + + function redeem(uint256[] calldata considerationTokenIds, address recipient, bytes calldata extraData) + public + payable + { + // Get the campaign id and requirementsIndex from extraData. + uint256 campaignId = uint256(bytes32(extraData[0:32])); + uint256 requirementsIndex = uint256(bytes32(extraData[32:64])); + + // Get the campaign params. + CampaignParams storage params = _campaignParams[campaignId]; + + // Validate the campaign time and total redemptions. + _validateRedemption(campaignId, params); + + // Increment totalRedemptions. + ++_totalRedemptions[campaignId]; + + // Get the campaign requirements. + if (requirementsIndex >= params.requirements.length) { + revert RequirementsIndexOutOfBounds(); + } + CampaignRequirements storage requirements = params.requirements[requirementsIndex]; + + // Process the redemption. + _processRedemption(campaignId, requirements, considerationTokenIds, recipient); + + // TODO: decode traitRedemptionTokenIds from extraData. + uint256[] memory traitRedemptionTokenIds; + + // Emit the Redemption event. + emit Redemption( + campaignId, requirementsIndex, bytes32(0), considerationTokenIds, traitRedemptionTokenIds, msg.sender + ); + } + + function getCampaign(uint256 campaignId) + external + view + override + returns (CampaignParams memory params, string memory uri, uint256 totalRedemptions) + { + // Revert if campaign id is invalid. + if (campaignId >= _nextCampaignId) revert InvalidCampaignId(); + + // Get the campaign params. + params = _campaignParams[campaignId]; + + // Get the campaign URI. + uri = _campaignURIs[campaignId]; + + // Get the total redemptions. + totalRedemptions = _totalRedemptions[campaignId]; + } + + function createCampaign(CampaignParams calldata params, string calldata uri) + public + virtual + returns (uint256 campaignId) + { + // Validate the campaign params, reverts if invalid. + _validateCampaignParams(params); + + // Set the campaignId and increment the next one. + campaignId = _nextCampaignId; + ++_nextCampaignId; + + // Set the campaign params. + _campaignParams[campaignId] = params; + + // Set the campaign URI. + _campaignURIs[campaignId] = uri; + + emit CampaignUpdated(campaignId, params, uri); + } + + function updateCampaign(uint256 campaignId, CampaignParams calldata params, string calldata uri) external { + // Revert if the campaign id is invalid. + if (campaignId == 0 || campaignId >= _nextCampaignId) { + revert InvalidCampaignId(); + } + + // Revert if msg.sender is not the manager. + address existingManager = _campaignParams[campaignId].manager; + if (params.manager != msg.sender && (existingManager != address(0) && existingManager != params.manager)) { + revert NotManager(); + } + + // Validate the campaign params and revert if invalid. + _validateCampaignParams(params); + + // Set the campaign params. + _campaignParams[campaignId] = params; + + // Update the campaign uri if it was provided. + if (bytes(uri).length != 0) { + _campaignURIs[campaignId] = uri; + } + + emit CampaignUpdated(campaignId, params, _campaignURIs[campaignId]); + } + + function _validateCampaignParams(CampaignParams memory params) internal pure { + // Revert if startTime is past endTime. + if (params.startTime > params.endTime) { + revert InvalidTime(); + } + + // Iterate over the requirements. + for (uint256 i = 0; i < params.requirements.length;) { + CampaignRequirements memory requirements = params.requirements[i]; + + // Validate each consideration item. + for (uint256 j = 0; j < requirements.consideration.length;) { + ConsiderationItem memory c = requirements.consideration[j]; + + // Revert if any of the consideration item recipients is the zero address. + // 0xdead address should be used instead. + // For internal burn, override _internalBurn and set _useInternalBurn to true. + if (c.recipient == address(0)) { + revert ConsiderationItemRecipientCannotBeZeroAddress(); + } + + if (c.startAmount == 0) { + revert ConsiderationItemAmountCannotBeZero(); + } + + // Revert if startAmount != endAmount, as this requires more complex logic. + if (c.startAmount != c.endAmount) { + revert NonMatchingConsiderationItemAmounts(i, c.startAmount, c.endAmount); + } + + unchecked { + ++j; + } + } + + unchecked { + ++i; + } + } + } + + function _validateRedemption(uint256 campaignId, CampaignParams memory params) internal view { + if (_isInactive(params.startTime, params.endTime)) { + revert NotActive_(block.timestamp, params.startTime, params.endTime); + } + + // Revert if max total redemptions would be exceeded. + if (_totalRedemptions[campaignId] + 1 > params.maxCampaignRedemptions) { + revert MaxCampaignRedemptionsReached(_totalRedemptions[campaignId] + 1, params.maxCampaignRedemptions); + } + } + + function _transferConsiderationItem(uint256 id, ConsiderationItem memory c) internal { + // If consideration item is this contract, recipient is burn address, and _useInternalBurn() fn returns true, + // call the internal burn function and return. + if (c.token == address(this) && c.recipient == payable(_BURN_ADDRESS) && _useInternalBurn()) { + _internalBurn(id, c.startAmount); + return; + } + + // Transfer the token to the consideration recipient. + if (c.itemType == ItemType.ERC721 || c.itemType == ItemType.ERC721_WITH_CRITERIA) { + // ERC721_WITH_CRITERIA with identifier 0 is wildcard: any id is valid. + // Criteria is not yet implemented, for that functionality use the contract offerer. + if (c.itemType == ItemType.ERC721 && id != c.identifierOrCriteria) { + revert InvalidConsiderationTokenIdSupplied(c.token, id, c.identifierOrCriteria); + } + IERC721(c.token).safeTransferFrom(msg.sender, c.recipient, id); + } else if ((c.itemType == ItemType.ERC1155 || c.itemType == ItemType.ERC1155_WITH_CRITERIA)) { + // ERC1155_WITH_CRITERIA with identifier 0 is wildcard: any id is valid. + // Criteria is not yet implemented, for that functionality use the contract offerer. + if (c.itemType == ItemType.ERC1155 && id != c.identifierOrCriteria) { + revert InvalidConsiderationTokenIdSupplied(c.token, id, c.identifierOrCriteria); + } + IERC1155(c.token).safeTransferFrom(msg.sender, c.recipient, id, c.startAmount, ""); + } else if (c.itemType == ItemType.ERC20) { + IERC20(c.token).transferFrom(msg.sender, c.recipient, c.startAmount); + } else { + // ItemType.NATIVE + (bool success,) = c.recipient.call{value: msg.value}(""); + if (!success) revert EtherTransferFailed(); + } + } + + /// @dev Override this function to return true if `_internalBurn` is used. + function _useInternalBurn() internal pure virtual returns (bool) { + return false; + } + + /// @dev Function that is called to burn amounts of a token internal to this inherited contract. + /// Override with token implementation calling internal burn. + function _internalBurn(uint256 id, uint256 amount) internal virtual { + // Override with your token implementation calling internal burn. + } + + function _isInactive(uint256 startTime, uint256 endTime) internal view returns (bool inactive) { + // Using the same check for time boundary from Seaport. + // startTime <= block.timestamp < endTime + assembly { + inactive := or(iszero(gt(endTime, timestamp())), gt(startTime, timestamp())) + } + } + + function _processRedemption( + uint256 campaignId, + CampaignRequirements memory requirements, + uint256[] memory tokenIds, + address recipient + ) internal { + // Get the campaign consideration. + ConsiderationItem[] memory consideration = requirements.consideration; + + // Revert if the tokenIds length does not match the consideration length. + if (consideration.length != tokenIds.length) { + revert TokenIdsDontMatchConsiderationLength(consideration.length, tokenIds.length); + } + + // Keep track of the total native value to validate. + uint256 totalNativeValue; + + // Iterate over the consideration items. + for (uint256 j; j < consideration.length;) { + // Get the consideration item. + ConsiderationItem memory c = consideration[j]; + + // Get the identifier. + uint256 id = tokenIds[j]; + + // Get the token balance. + uint256 balance; + if (c.itemType == ItemType.ERC721 || c.itemType == ItemType.ERC721_WITH_CRITERIA) { + balance = IERC721(c.token).ownerOf(id) == msg.sender ? 1 : 0; + } else if (c.itemType == ItemType.ERC1155 || c.itemType == ItemType.ERC1155_WITH_CRITERIA) { + balance = IERC1155(c.token).balanceOf(msg.sender, id); + } else if (c.itemType == ItemType.ERC20) { + balance = IERC20(c.token).balanceOf(msg.sender); + } else { + // ItemType.NATIVE + totalNativeValue += c.startAmount; + // Total native value is validated after the loop. + } + + // Ensure the balance is sufficient. + if (balance < c.startAmount) { + revert ConsiderationItemInsufficientBalance(c.token, balance, c.startAmount); + } + + // Transfer the consideration item. + _transferConsiderationItem(id, c); + + // Get the campaign offer. + OfferItem[] memory offer = requirements.offer; + + // Mint the new tokens. + for (uint256 k; k < offer.length;) { + IRedemptionMintable(offer[k].token).mintRedemption( + campaignId, recipient, requirements.consideration, requirements.traitRedemptions + ); + + unchecked { + ++k; + } + } + + unchecked { + ++j; + } + } + + // Validate the correct native value is sent with the transaction. + if (msg.value != totalNativeValue) { + revert InvalidTxValue(msg.value, totalNativeValue); + } + + // Process trait redemptions. + // TraitRedemption[] memory traitRedemptions = requirements.traitRedemptions; + // _setTraits(traitRedemptions); + } + + function _setTraits(TraitRedemption[] calldata traitRedemptions) internal { + /* + // Iterate over the trait redemptions and set traits on the tokens. + for (uint256 i; i < traitRedemptions.length;) { + // Get the trait redemption token address and place on the stack. + address token = traitRedemptions[i].token; + + uint256 identifier = traitRedemptions[i].identifier; + + // Declare a new block to manage stack depth. + { + // Get the substandard and place on the stack. + uint8 substandard = traitRedemptions[i].substandard; + + // Get the substandard value and place on the stack. + bytes32 substandardValue = traitRedemptions[i].substandardValue; + + // Get the trait key and place on the stack. + bytes32 traitKey = traitRedemptions[i].traitKey; + + bytes32 traitValue = traitRedemptions[i].traitValue; + + // Get the current trait value and place on the stack. + bytes32 currentTraitValue = getTraitValue(traitKey, identifier); + + // If substandard is 1, set trait to traitValue. + if (substandard == 1) { + // Revert if the current trait value does not match the substandard value. + if (currentTraitValue != substandardValue) { + revert InvalidRequiredValue(currentTraitValue, substandardValue); + } + + // Set the trait to the trait value. + _setTrait(traitRedemptions[i].traitKey, identifier, traitValue); + // If substandard is 2, increment trait by traitValue. + } else if (substandard == 2) { + // Revert if the current trait value is greater than the substandard value. + if (currentTraitValue > substandardValue) { + revert InvalidRequiredValue(currentTraitValue, substandardValue); + } + + // Increment the trait by the trait value. + uint256 newTraitValue = uint256(currentTraitValue) + uint256(traitValue); + + _setTrait(traitRedemptions[i].traitKey, identifier, bytes32(newTraitValue)); + } else if (substandard == 3) { + // Revert if the current trait value is less than the substandard value. + if (currentTraitValue < substandardValue) { + revert InvalidRequiredValue(currentTraitValue, substandardValue); + } + + uint256 newTraitValue = uint256(currentTraitValue) - uint256(traitValue); + + // Decrement the trait by the trait value. + _setTrait(traitRedemptions[i].traitKey, traitRedemptions[i].identifier, bytes32(newTraitValue)); + } + } + + unchecked { + ++i; + } + } + */ + } + + function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) { + return interfaceId == type(IERC7498).interfaceId; + } +} diff --git a/assets/erc-7498/IERC7498.sol b/assets/erc-7498/IERC7498.sol new file mode 100644 index 0000000000..e8bfbe8565 --- /dev/null +++ b/assets/erc-7498/IERC7498.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.19; + +import {OfferItem, ConsiderationItem, SpentItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; +import {CampaignParams, TraitRedemption} from "./RedeemablesStructs.sol"; + +interface IERC7498 { + event CampaignUpdated(uint256 indexed campaignId, CampaignParams params, string uri); + event Redemption( + uint256 indexed campaignId, + uint256 requirementsIndex, + bytes32 redemptionHash, + uint256[] considerationTokenIds, + uint256[] traitRedemptionTokenIds, + address redeemedBy + ); + + function createCampaign(CampaignParams calldata params, string calldata uri) + external + returns (uint256 campaignId); + + function updateCampaign(uint256 campaignId, CampaignParams calldata params, string calldata uri) external; + + function getCampaign(uint256 campaignId) + external + view + returns (CampaignParams memory params, string memory uri, uint256 totalRedemptions); + + function redeem(uint256[] calldata considerationTokenIds, address recipient, bytes calldata extraData) + external + payable; +} diff --git a/assets/erc-7498/IRedemptionMintable.sol b/assets/erc-7498/IRedemptionMintable.sol new file mode 100644 index 0000000000..f79bf5caae --- /dev/null +++ b/assets/erc-7498/IRedemptionMintable.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.19; + +import {ConsiderationItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; +import {TraitRedemption} from "./RedeemablesStructs.sol"; + +interface IRedemptionMintable { + function mintRedemption( + uint256 campaignId, + address recipient, + ConsiderationItem[] calldata consideration, + TraitRedemption[] calldata traitRedemptions + ) external; +} diff --git a/assets/erc-7498/RedeemablesErrors.sol b/assets/erc-7498/RedeemablesErrors.sol new file mode 100644 index 0000000000..653db7486c --- /dev/null +++ b/assets/erc-7498/RedeemablesErrors.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.19; + +import {SpentItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; +import {CampaignParams} from "./RedeemablesStructs.sol"; + +interface RedeemablesErrors { + /// Configuration errors + error NotManager(); + error InvalidTime(); + error ConsiderationItemRecipientCannotBeZeroAddress(); + error ConsiderationItemAmountCannotBeZero(); + error NonMatchingConsiderationItemAmounts(uint256 itemIndex, uint256 startAmount, uint256 endAmount); + + /// Redemption errors + error InvalidCampaignId(); + error CampaignAlreadyExists(); + error InvalidCaller(address caller); + error NotActive_(uint256 currentTimestamp, uint256 startTime, uint256 endTime); + error MaxRedemptionsReached(uint256 total, uint256 max); + error MaxCampaignRedemptionsReached(uint256 total, uint256 max); + error NativeTransferFailed(); + error InvalidOfferLength(uint256 got, uint256 want); + error InvalidNativeOfferItem(); + error InvalidOwner(); + error InvalidRequiredValue(bytes32 got, bytes32 want); + //error InvalidSubstandard(uint256 substandard); + error InvalidTraitRedemption(); + error InvalidTraitRedemptionToken(address token); + error ConsiderationRecipientNotFound(address token); + error RedemptionValuesAreImmutable(); + error RequirementsIndexOutOfBounds(); + error ConsiderationItemInsufficientBalance(address token, uint256 balance, uint256 amount); + error EtherTransferFailed(); + error InvalidTxValue(uint256 got, uint256 want); + error InvalidConsiderationTokenIdSupplied(address token, uint256 got, uint256 want); + error TokenIdsDontMatchConsiderationLength(uint256 considerationLength, uint256 tokenIdsLength); +} diff --git a/assets/erc-7498/RedeemablesStructs.sol b/assets/erc-7498/RedeemablesStructs.sol new file mode 100644 index 0000000000..4b43223c59 --- /dev/null +++ b/assets/erc-7498/RedeemablesStructs.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.19; + +import {OfferItem, ConsiderationItem, SpentItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; + +struct CampaignParams { + uint32 startTime; + uint32 endTime; + uint32 maxCampaignRedemptions; + address manager; + address signer; + CampaignRequirements[] requirements; +} + +struct CampaignRequirements { + OfferItem[] offer; + ConsiderationItem[] consideration; + TraitRedemption[] traitRedemptions; +} + +struct TraitRedemption { + uint8 substandard; + address token; + uint256 identifier; + bytes32 traitKey; + bytes32 traitValue; + bytes32 substandardValue; +} From dc42deecc519a9083e4eb5c5b13dee674ec5f2f3 Mon Sep 17 00:00:00 2001 From: Philipp Bolte Date: Thu, 30 Nov 2023 19:54:00 +0100 Subject: [PATCH 18/23] Add ERC: Trusted Hint Registry Merged by EIP-Bot. --- ERCS/erc-7506.md | 467 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 467 insertions(+) create mode 100644 ERCS/erc-7506.md diff --git a/ERCS/erc-7506.md b/ERCS/erc-7506.md new file mode 100644 index 0000000000..7b28407568 --- /dev/null +++ b/ERCS/erc-7506.md @@ -0,0 +1,467 @@ +--- +eip: 7506 +title: Trusted Hint Registry +description: A system for managing on-chain metadata, enabling verification of ecosystem claims. +author: Philipp Bolte (@strumswell), Dennis von der Bey (@DennisVonDerBey), Lauritz Leifermann (@lleifermann) +discussions-to: https://ethereum-magicians.org/t/eip-trusted-hint-registry/15615 +status: Draft +type: Standards Track +category: ERC +created: 2023-08-31 +requires: 712 +--- + +## Abstract + +This EIP standardizes a system for managing on-chain metadata (hints), enabling claim interpretation, reliability, +and verification. It structures these hints within defined namespaces and lists, enabling structured organization and +retrieval, as well as permissioned write access. The system permits namespace owners to delegate hint management tasks, +enhancing operational flexibility. It incorporates secure meta transactions via [EIP-712](./eip-712.md)-enabled +signatures and offers optional ENS integration for trust verification and discoverability. The interface is equipped to +emit specific events for activities like hint modifications, facilitating easy traceability of changes to hints. This +setup aims to provide a robust, standardized framework for managing claim- and ecosystem-related metadata, essential for +maintaining integrity and trustworthiness in decentralized environments. + +## Motivation + +In an increasingly interconnected and decentralized landscape, the formation of trust among entities remains a critical +concern. Ecosystems, both on-chain and off-chain—spanning across businesses, social initiatives, and other organized +frameworks—frequently issue claims for or about entities within their networks. These claims serve as the foundational +elements of trust, facilitating interactions and transactions in environments that are essentially untrustworthy by +nature. While the decentralization movement has brought about significant improvements around trustless technologies, +many ecosystems building on top of these are in need of technologies that build trust in their realm. Real-world +applications have shown that verifiable claims alone are not enough for this purpose. Moreover, a supporting layer of +on-chain metadata is needed to support a reliable exchange and verification of those claims. + +The absence of a structured mechanism to manage claim metadata on-chain poses a significant hurdle to the formation and +maintenance of trust among participating entities in an ecosystem. This necessitates the introduction of a layer of +on-chain metadata, which can assist in the reliable verification and interpretation of these claims. Termed "hints" in +this specification, this metadata can be used in numerous ways, each serving to bolster the integrity and reliability +of the ecosystem's claims. Hints can perform various tasks, such as providing revocation details, identifying trusted +issuers, or offering timestamping hashes. These are just a few examples that enable ecosystems to validate and +authenticate claims, as well as verify data integrity over time. + +The proposed "Trusted Hint Registry" aims to provide a robust, flexible, and standardized interface for managing such +hints. The registry allows any address to manage multiple lists of hints, with a set of features that not only make it +easier to create and manage these hints but also offer the flexibility of delegating these capabilities to trusted +entities. In practice, this turns the hint lists into dynamic tools adaptable to varying requirements and use cases. +Moreover, an interface has been designed with a keen focus on interoperability, taking into consideration existing W3C +specifications around Decentralized Identifiers and Verifiable Credentials, as well as aligning with on-chain projects +like the Ethereum Attestation Service. + +By providing a standardized smart contract interface for hint management, this specification plays an integral role in +enabling and scaling trust in decentralized ecosystems. It offers a foundational layer upon which claims — both on-chain +and off-chain — can be reliably issued, verified, and interpreted, thus serving as an essential building block for the +credible operation of any decentralized ecosystem. Therefore, the Trusted Hint Registry is not just an addition to the +ecosystem but a necessary evolution in the complex topology of decentralized trust. + +## 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. + +This EIP specifies a contract called `TrustedHintRegistry` and standardizes a set of **REQUIRED** core hint functions, +while also providing a common set of **OPTIONAL** management functions, enabling various ways for collaborative hint +management. Ecosystems **MAY** use this specification to build their own hint registry contracts with ecosystem-specific, +non-standardized features. Governance is deliberately excluded from this ERC and **MAY** be implemented according to an +ecosystem's need. + +### Definitions + +- `claim`: A claim is a statement about an entity made by another entity. +- `hint`: A "hint" refers to a small piece of information that provides insights, aiding in the interpretation, + reliability, or verifiability of decentralized ecosystem data. +- `namespace`: A namespace is a representation of an Ethereum address inside the registry that corresponds to its + owner’s address. A namespace contains hint lists for different use cases. +- `hint list`: A hint list is identified by a unique value that contains a number of hint keys that resolve to hint + values. An example of this is a revocation key that resolves to a revocation state. +- `hint key`: A hint key is a unique value that resolves to a hint value. An example of this is a trusted issuer + identifier, which resolves to the trust status of that identifier. +- `hint value`: A hint value expresses data about an entity in an ecosystem. +- `delegate`: An Ethereum address that has been granted writing permissions to a hint list by its owner. + +### Interface + +#### Hint Management + +##### getHint + +A method with the following signature **MUST** be implemented that returns the hint value in a hint list of a namespace. + +```solidity +function getHint(address _namespace, bytes32 _list, bytes32 _key) external view returns (bytes32); +``` + +##### setHint + +A method with the following signature **MUST** be implemented that changes the hint value in a hint list of a namespace. +An overloaded method with an additional `bytes calldata _metadata` parameter **MAY** be implemented to set metadata +together with the hint value. + +```solidity +function setHint(address _namespace, bytes32 _list, bytes32 _key, bytes32 _value) public; +``` + +##### setHintSigned + +A method with the following signature **MAY** be implemented that changes the hint value in a hint list of a namespace +with a raw signature. The raw signature **MUST** be generated following the Meta Transactions section. An overloaded +method with an additional `bytes calldata _metadata` parameter **MAY** be implemented to set metadata together with the +hint value. + +```solidity +function setHintSigned(address _namespace, bytes32 _list, bytes32 _key, bytes32 _value, address _signer, bytes calldata _signature) public; +``` + +##### setHints + +A method with the following signature **MUST** be implemented that changes multiple hint values in a hint list of a +namespace. An overloaded method with an additional `bytes calldata _metadata` parameter **MAY** be implemented to set +metadata together with the hint value. + +```solidity +function setHints(address _namespace, bytes32 _list, bytes32[] calldata _keys, bytes32[] calldata _values) public; +``` + +##### setHintsSigned + +A method with the following signature **MUST** be implemented that multiple hint values in a hint list of a namespace +with a raw signature. The raw signature **MUST** be generated following the Meta Transactions section. An overloaded +method with an additional `bytes calldata _metadata` parameter **MAY** be implemented to set metadata together with the +hint value. + +```solidity +function setHintsSigned(address _namespace, bytes32 _list, bytes32[] calldata _keys, bytes32[] calldata _values, address _signer, bytes calldata _signature) public; +``` + +#### Delegated Hint Management + +A namespace owner can add delegate addresses to specific hint lists in their namespace. These delegates **SHALL** have +write access to the specific lists via a specific set of methods. + +##### setHintDelegated + +A method with the following signature **MAY** be implemented that changes the hint value in a hint list of a namespace +for pre-approved delegates. An overloaded method with an additional `bytes calldata _metadata` parameter **MAY** be +implemented to set metadata together with the hint value. + +```solidity +function setHintDelegated(address _namespace, bytes32 _list, bytes32 _key, bytes32 _value) public; +``` + +##### setHintDelegatedSigned + +A method with the following signature **MAY** be implemented that changes the hint value in a hint list of a namespace +for pre-approved delegates with a raw signature. The raw signature **MUST** be generated following the Meta Transactions +section. An overloaded method with an additional `bytes calldata _metadata` parameter **MAY** be implemented to set +metadata together with the hint value. + +```solidity +function setHintDelegatedSigned(address _namespace, bytes32 _list, bytes32 _key, bytes32 _value, address _signer, bytes calldata _signature) public; +``` + +##### setHintsDelegated + +A method with the following signature **MAY** be implemented that changes multiple hint values in a hint list of a +namespace for pre-approved delegates. An overloaded method with an additional `bytes calldata _metadata` parameter +**MAY** be implemented to set metadata together with the hint value. + +```solidity +function setHintsDelegated(address _namespace, bytes32 _list, bytes32[] calldata _keys, bytes32[] calldata _values) public; +``` + +##### setHintsDelegatedSigned + +A method with the following signature **MAY** be implemented that has multiple hint values in a hint list of a namespace +for pre-approved delegates with a raw signature. The raw signature **MUST** be generated following the Meta Transactions +section. An overloaded method with an additional `bytes calldata _metadata` parameter **MAY** be implemented to set +metadata together with the hint value. + +```solidity +function setHintsDelegatedSigned(address _namespace, bytes32 _list, bytes32[] calldata _keys, bytes32[] calldata _values, address _signer, bytes calldata _signature) public; +``` + +#### Hint List Management + +##### setListStatus + +A method with the following signature **MAY** be implemented that changes the validity state of a hint list. This +enables one to (un)-revoke a whole list of hint values. + +```solidity +function setListStatus(address _namespace, bytes32 _list, bool _revoked) public; +``` + +##### setListStatusSigned + +A method with the following signature **MAY** be implemented that changes the validity state of a hint list with a raw +signature. This enables one to (un)-revoke a whole list of hint values. + +```solidity +function setListStatusSigned(address _namespace, bytes32 _list, bool _revoked, address _signer, bytes calldata _signature) public; +``` + +##### setListOwner + +A method with the following signature **MAY** be implemented that transfers the ownership of a trust list to another +address. Changing the owner of a list **SHALL NOT** change the namespace the hint list resides in, to retain references +of paths to a hint value. + +```solidity +function setListOwner(address _namespace, bytes32 _list, address _newOwner) public; +``` + +##### setListOwnerSigned + +A method with the following signature **MAY** be implemented that transfers the ownership of a trust list to another +address with a raw signature. The raw signature **MUST** be generated following the Meta Transactions section. Changing +the owner of a list **SHALL NOT** change the namespace the hint list resides in, to retain references to paths to a hint +value. + +```solidity +function setListOwnerSigned(address _namespace, bytes32 _list, address _newOwner, address _signer, bytes calldata _signature) public; +``` + +##### addListDelegate + +A method with the following signature **MAY** be implemented to add a delegate to an owner’s hint list in a namespace. + +```solidity +function addListDelegate(address _namespace, bytes32 _list, address _delegate, uint256 _untilTimestamp) public; +``` + +##### addListDelegateSigned + +A method with the following signature **MAY** be implemented to add a delegate to an owner’s hint list in a namespace +with a raw signature. The raw signature **MUST** be generated following the Meta Transactions section. + +```solidity +function addListDelegateSigned(address _namespace, bytes32 _list, address _delegate, uint256 _untilTimestamp, address _signer, bytes calldata _signature) public; +``` + +##### removeListDelegate + +A method with the following signature **MAY** be implemented to remove a delegate from an owner’s revocation hint list +in a namespace. + +```solidity +function removeListDelegate(address _namespace, bytes32 _list, address _delegate) public; +``` + +##### removeListDelegateSigned + +A method with the following signature **MAY** be implemented to remove a delegate from an owner’s revocation hint list +in a namespace with a raw signature. The raw signature **MUST** be generated following the Meta Transactions section. + +```solidity +function removeListDelegateSigned(address _namespace, bytes32 _list, address _delegate, address _signer, bytes calldata _signature) public; +``` + +#### Metadata Management + +##### getMetadata + +A method with the following signature **MAY** be implemented to retrieve metadata for a hint. + +```solidity +function getMetadata(address _namespace, bytes32 _list, bytes32 _key, bytes32 _value) external view returns (bytes memory); +``` + +##### setMetadata + +A method with the following signature **MAY** be implemented to set metadata for a hint. + +```solidity +function setMetadata(address _namespace, bytes32 _list, bytes32 _key, bytes32 _value, bytes calldata _metadata) public; +``` + +##### setMetadataSigned + +A method with the following signature **MAY** be implemented to set metadata for a hint with a raw signature. The raw +signature **MUST** be generated following the Meta Transactions section. + +```solidity +function setMetadataSigned(address _namespace, bytes32 _list, bytes32 _key, bytes32 _value, bytes calldata _metadata, address _signer, bytes calldata _signature) public; +``` + +#### setMetadataDelegated + +A method with the following signature **MAY** be implemented to set metadata for a hint as a pre-approved delegate of +the hint list. + +```solidity +function setMetadataDelegated(address _namespace, bytes32 _list, bytes32 _key, bytes32 _value, bytes calldata _metadata) public; +``` + +##### setMetadataDelegatedSigned + +A method with the following signature **MAY** be implemented to set metadata for a hint as a pre-approved delegate of +the hint list with a raw signature. The raw signature **MUST** be generated following the Meta Transactions section. + +```solidity +function setMetadataDelegatedSigned(address _namespace, bytes32 _list, bytes32 _key, bytes32 _value, bytes calldata _metadata, address _signer, bytes calldata _signature) public; +``` + +#### Events + +##### HintValueChanged + +**MUST** be emitted when a hint value has changed. + +```solidity +event HintValueChanged( + address indexed namespace, + bytes32 indexed list, + bytes32 indexed key, + bytes32 value +); +``` + +##### HintListOwnerChanged + +**MUST** be emitted when the owner of a list has changed. + +```solidity +event HintListOwnerChanged( + address indexed namespace, + bytes32 indexed list, + address indexed newOwner +); +``` + +##### HintListDelegateAdded + +**MUST** be emitted when a delegate has been added to a hint list. + +```solidity +event HintListDelegateAdded( + address indexed namespace, + bytes32 indexed list, + address indexed newDelegate +); +``` + +##### HintListDelegateRemoved + +**MUST** be emitted when a delegate has been removed from a hint list. + +```solidity +event HintListDelegateRemoved( + address indexed namespace, + bytes32 indexed list, + address indexed oldDelegate +); +``` + +##### HintListStatusChanged + +**MUST** be emitted when the validity status of the hint list has been changed. + +```solidity +event HintListStatusChanged( + address indexed namespace, + bytes32 indexed list, + bool indexed revoked +); +``` + +### Meta Transactions + +This section uses the following terms: + +- **`transaction signer`**: An Ethereum address that signs arbitrary data for the contract to execute **BUT** does not + commit the transaction. +- **`transaction sender`**: An Ethereum address that takes signed data from a **transaction signer** and commits it + wrapped in a transaction to the smart contract. + +A **transaction signer** **MAY** be able to deliver a signed payload off-band to a **transaction sender** that initiates +the Ethereum interaction with the smart contract. The signed payload **MUST** be limited to being used only +once (see Signed Hash and Nonce). + +#### Signed Hash + +The signature of the **transaction signer** **MUST** conform to [EIP-712](./eip-712.md). This helps users understand +what the payload they are signing consists of, and it provides protection against replay attacks. + +#### Nonce + +This EIP **RECOMMENDS** the use of a **dedicated nonce mapping** for meta transactions. If the signature of the +**transaction sender** and its meta-contents are verified, the contract increases a nonce for this +**transaction signer**. This effectively removes the possibility for any other sender to execute the same transaction +again with another wallet. + +### Trust Anchor via ENS + +Ecosystems that use an Ethereum Name Service (ENS) domain can increase trust by using ENS entries to share information +about a hint list registry. This method takes advantage of the ENS domain's established credibility to make it easier to +find a reliable hint registry contract, as well as the appropriate namespace and hint list customized for particular +ecosystem needs. Implementing a trust anchor through ENS is **OPTIONAL**. + +For each use case, a specific ENS subdomain **SHALL** be created only used for a specific hint list, e.g., +“trusted-issuers.ens.eth”. The following records **SHALL** be set: + +- ADDRESS ETH - address of the trusted hint registry contract +- TEXT - key: “hint.namespace”; value: owner address of namespace + +The following records **MAY** be set: + +- TEXT - key: “hint.list”; value: bytes32 key of hint list +- TEXT - key: “hint.key”; value: bytes32 key of hint key +- TEXT - key: “hint.value”; value: bytes32 key of hint value +- ABI - ABI of trusted hint registry contract + +To create a two-way connection, a namespace owner **SHALL** set metadata referencing the ENS subdomain hash for the hint +list. Metadata **SHALL** be set in the owners namespace, hint list `0x0`, and hint key `0x0` where the value is the +ENS subdomain keccak256 hash. + +By establishing this connection, a robust foundation for trust and discovery within an ecosystem is created. + +## Rationale + +Examining the method signatures reveals a deliberate architecture and data hierarchy within this ERC: A namespace +address maps to a hint list, which in turn maps to a hint key, which then reveals the hint value. + +```solidity +// namespace hint list hint key hint value +mapping(address => mapping(bytes32 => mapping(bytes32 => bytes32))) hints; +``` + +This structure is designed to implicitly establish the initial ownership of all lists under a given namespace, +eliminating the need for subsequent claiming actions. As a result, it simplifies the process of verifying and enforcing +write permissions, thereby reducing potential attack surfaces. Additional data structures must be established and +validated for features like delegate management and ownership transfer of hint lists. These structures won't affect the +main namespace layout; rather, they serve as a secondary mechanism for permission checks. + +One of the primary objectives of this ERC is to include management features, as these significantly influence the ease +of collaboration and maintainability of hint lists. These features also enable platforms to hide complexities while +offering user-friendly interfaces. Specifically, the use of meta-transactions allows users to maintain control over +their private keys while outsourcing the technical heavy lifting to platforms, which is achieved simply by signing an +[EIP-712](./eip-712.md) payload. + +## Backwards Compatibility + +No backward compatibility issues found. + +## Security Considerations + +### Meta Transactions + +The signature of signed transactions could potentially be replayed on different chains or deployed versions of the +registry implementing this ERC. This security consideration is addressed by the usage +of [EIP-712](./eip-712.md). + +### Rights Management + +The different roles and their inherent permissions are meant to prevent changes from unauthorized entities. The hint +list owner should always be in complete control over its hint list and who has writing access to it. + +### Governance + +It is recognized that ecosystems might have processes in place that might also apply to changes in hint lists. This ERC +explicitly leaves room for implementers or users of the registry to apply a process that fits the requirements of their +ecosystem. Possible solutions can be an extension of the contract with governance features around specific methods, the +usage of multi-sig wallets, or off-chain processes enforced by an entity. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). From a42f5101641f304ab822be825f62bdfc75236679 Mon Sep 17 00:00:00 2001 From: Qi Zhou Date: Thu, 30 Nov 2023 14:18:31 -0800 Subject: [PATCH 19/23] Update ERC-6860: Add nand2 as co-author and fix minor digit format Merged by EIP-Bot. --- ERCS/erc-6860.md | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/ERCS/erc-6860.md b/ERCS/erc-6860.md index 63ba35329e..fab03bc6f5 100644 --- a/ERCS/erc-6860.md +++ b/ERCS/erc-6860.md @@ -2,7 +2,7 @@ eip: 6860 title: Web3 URL to EVM Call Message Translation description: A translation of an HTTP-style Web3 URL to an EVM call message -author: Qi Zhou (@qizhou), Chao Pi (@pichaoqkc), Sam Wilson (@SamWilsn) +author: Qi Zhou (@qizhou), Chao Pi (@pichaoqkc), Sam Wilson (@SamWilsn), Nicolas Deschildre (@nand2) discussions-to: https://ethereum-magicians.org/t/eip-4804-web3-url-to-evm-call-message-translation/8300 status: Draft type: Standards Track @@ -57,7 +57,7 @@ domainName = *( unreserved / pct-encoded / sub-delims ) ; As in RFC 3986 The way to resolve the domain name from a domain name service to an address is specified in [ERC-6821](./eip-6821.md) for the Ethereum Name service, and will be discussed in later ERCs for other name services. ``` -chainid = 1*DIGIT +chainid = %x31-39 *DIGIT ``` **chainid** indicates which chain to resolve **contractName** and call the message. If not specified, the protocol will use the primary chain of the name service provider used, e.g., 1 for eth. If no name service provider was used, the default chainid is 1. @@ -184,14 +184,16 @@ attrName = "returns" / "returnTypes" attrValue = [ "(" [ retTypes ] ")" ] retTypes = retType *( "," retType ) -retType = *( "[]" ) retRawType -retRawType = "bool" / "uint" [ intSizes ] / "int" [ intSize ] / "address" / "bytes" [ bytesSizes ] / "string" -bytesSizes = DIGIT - / ( "1" / "2" ) DIGIT - / "31" / "32" +retType = retRawType *( "[" [ %x31-39 *DIGIT ] "]" ) +retRawType = "(" retTypes ")" + / retBaseType +retBaseType = "bool" / "uint" [ intSizes ] / "int" [ intSize ] / "address" / "bytes" [ bytesSizes ] / "string" +bytesSizes = %x31-39 ; 1-9 + / ( "1" / "2" ) DIGIT ; 10-29 + / "31" / "32" ; 31-32 ``` -The "returns" attribute in **aQuery** tells the format of the returned data. +The "returns" attribute in **aQuery** tells the format of the returned data. It follows the syntax of the arguments part of the ethereum ABI function signature (``uint`` and ``int`` aliases are authorized). - If the "returns" attribute value is undefined or empty, the returned message data will be treated as ABI-encoded bytes and the decoded bytes will be returned to the frontend. The MIME type returned to the frontend will be undefined by default, but will be overriden if the last argument is of string type and has a **fileExtension**, in which case the MIME type will be deduced from the filename extension. (Note that **fileExtension** is not excluded from the string argument given to the smartcontract) - If the "returns" attribute value is equal to "()", the raw bytes of the returned message data will be returned, encoded as a "0x"-prefixed hex string in an array in JSON format: ``["0xXXXXX"]`` @@ -288,7 +290,7 @@ schema = "w3" / "web3" userinfo = address contractName = address / domainName -chainid = 1*DIGIT +chainid = %x31-39 *DIGIT pathQuery = mPathQuery ; path+query for manual mode / aPathQuery ; path+query for auto mode @@ -375,11 +377,13 @@ attrName = "returns" / "returnTypes" attrValue = [ "(" [ retTypes ] ")" ] retTypes = retType *( "," retType ) -retType = retRawType *( "[]" ) -retRawType = "bool" / "uint" [ intSizes ] / "int" [ intSize ] / "address" / "bytes" [ bytesSizes ] / "string" -bytesSizes = DIGIT - / ( "1" / "2" ) DIGIT - / "31" / "32" +retType = retRawType *( "[" [ %x31-39 *DIGIT ] "]" ) +retRawType = "(" retTypes ")" + / retBaseType +retBaseType = "bool" / "uint" [ intSizes ] / "int" [ intSize ] / "address" / "bytes" [ bytesSizes ] / "string" +bytesSizes = %x31-39 ; 1-9 + / ( "1" / "2" ) DIGIT ; 10-29 + / "31" / "32" ; 31-32 domainName = *( unreserved / pct-encoded / sub-delims ) ; As in RFC 3986 From 4f6d6adbe315452e27819a2f2fde6231e6a7524e Mon Sep 17 00:00:00 2001 From: fangting-alchemy <119372438+fangting-alchemy@users.noreply.github.com> Date: Fri, 1 Dec 2023 12:43:33 -0800 Subject: [PATCH 20/23] Update ERC-6900: spec update 6 Merged by EIP-Bot. --- ERCS/erc-6900.md | 113 +++++++++++++++++++++++++++++++---------------- 1 file changed, 76 insertions(+), 37 deletions(-) diff --git a/ERCS/erc-6900.md b/ERCS/erc-6900.md index 70dbe1a4bb..1505452bb3 100644 --- a/ERCS/erc-6900.md +++ b/ERCS/erc-6900.md @@ -94,7 +94,7 @@ Each step is modular, supporting different implementations for each execution fu **Modular Smart Contract Accounts** **MAY** implement -- `IPluginLoupe.sol` to support visibility in plugin configuration on-chain. +- `IAccountLoupe.sol` to support visibility in plugin configuration on-chain. **Plugins** **MUST** implement @@ -123,8 +123,14 @@ interface IPluginManager { uint8 postExecHookFunctionId; } - event PluginInstalled(address indexed plugin, bytes32 manifestHash); - event PluginUninstalled(address indexed plugin, bytes32 manifestHash, bool onUninstallSucceeded); + event PluginInstalled( + address indexed plugin, + bytes32 manifestHash, + FunctionReference[] dependencies, + InjectedHook[] injectedHooks + ); + + event PluginUninstalled(address indexed plugin, bool indexed callbacksSucceeded); /// @notice Install a plugin to the modular account. /// @param plugin The plugin to install. @@ -168,12 +174,12 @@ Standard execute functions SHOULD check whether the call's target implements the **If the target is a plugin, the call SHOULD revert.** This prevents accidental misconfiguration or misuse of plugins (both installed and uninstalled). ```solidity -struct Execution { +struct Call { // The target contract for account to execute. address target; - // The value for the execution. + // The value for the call. uint256 value; - // The call data for the execution. + // The call data for the call. bytes data; } @@ -181,16 +187,16 @@ interface IStandardExecutor { /// @notice Standard execute method. /// @dev If the target is a plugin, the call SHOULD revert. /// @param target The target contract for account to execute. - /// @param value The value for the execution. - /// @param data The call data for the execution. + /// @param value The value for the call. + /// @param data The call data for the call. /// @return The return data from the call. function execute(address target, uint256 value, bytes calldata data) external payable returns (bytes memory); /// @notice Standard executeBatch method. /// @dev If the target is a plugin, the call SHOULD revert. - /// @param executions The array of executions. + /// @param calls The array of calls. /// @return An array containing the return data from the calls. - function executeBatch(Execution[] calldata executions) external payable returns (bytes[] memory); + function executeBatch(Call[] calldata calls) external payable returns (bytes[] memory); } ``` @@ -220,7 +226,7 @@ interface IPluginExecutor { } ``` -#### `IPluginLoupe.sol` +#### `IAccountLoupe.sol` Plugin inspection interface. Modular Smart Contract Accounts **MAY** implement this interface to support visibility in plugin configuration on-chain. @@ -228,32 +234,55 @@ Plugin inspection interface. Modular Smart Contract Accounts **MAY** implement t // Treats the first 20 bytes as an address, and the last byte as a function identifier. type FunctionReference is bytes21; -interface IPluginLoupe { - // Config for a Plugin Execution function +interface IAccountLoupe { + /// @notice Config for an execution function, given a selector struct ExecutionFunctionConfig { address plugin; FunctionReference userOpValidationFunction; FunctionReference runtimeValidationFunction; } + /// @notice Pre and post hooks for a given selector + /// @dev It's possible for one of either `preExecHook` or `postExecHook` to be empty struct ExecutionHooks { FunctionReference preExecHook; FunctionReference postExecHook; } + /// @notice Gets the validation functions and plugin address for a selector + /// @dev If the selector is a native function, the plugin address will be the address of the account + /// @param selector The selector to get the configuration for + /// @return The configuration for this selector function getExecutionFunctionConfig(bytes4 selector) external view returns (ExecutionFunctionConfig memory); + /// @notice Gets the pre and post execution hooks for a selector + /// @param selector The selector to get the hooks for + /// @return The pre and post execution hooks for this selector function getExecutionHooks(bytes4 selector) external view returns (ExecutionHooks[] memory); + /// @notice Gets the pre and post permitted call hooks applied for a plugin calling this selector + /// @param callingPlugin The plugin that is calling the selector + /// @param selector The selector the plugin is calling + /// @return The pre and post permitted call hooks for this selector function getPermittedCallHooks(address callingPlugin, bytes4 selector) external view returns (ExecutionHooks[] memory); - function getPreUserOpValidationHooks(bytes4 selector) external view returns (FunctionReference[] memory); - - function getPreRuntimeValidationHooks(bytes4 selector) external view returns (FunctionReference[] memory); + /// @notice Gets the pre user op and runtime validation hooks associated with a selector + /// @param selector The selector to get the hooks for + /// @return preUserOpValidationHooks The pre user op validation hooks for this selector + /// @return preRuntimeValidationHooks The pre runtime validation hooks for this selector + function getPreValidationHooks(bytes4 selector) + external + view + returns ( + FunctionReference[] memory preUserOpValidationHooks, + FunctionReference[] memory preRuntimeValidationHooks + ); + /// @notice Gets an array of all installed plugins + /// @return The addresses of all installed plugins function getInstalledPlugins() external view returns (address[] memory); } ``` @@ -379,11 +408,6 @@ enum ManifestAssociatedFunctionType { PRE_HOOK_ALWAYS_DENY } -struct ManifestExecutionFunction { - bytes4 selector; - string[] permissions; -} - // For functions of type `ManifestAssociatedFunctionType.DEPENDENCY`, the MSCA MUST find the plugin address // of the function at `dependencies[dependencyIndex]` during the call to `installPlugin(config)`. struct ManifestFunction { @@ -403,7 +427,19 @@ struct ManifestExecutionHook { ManifestFunction postExecHook; } -struct PluginManifest { +struct ManifestExternalCallPermission { + address externalAddress; + bool permitAnySelector; + bytes4[] selectors; +} + +struct SelectorPermission { + bytes4 functionSelector; + string permissionDescription; +} + +/// @dev A struct holding fields to describe the plugin in a purely view context. Intended for front end clients. +struct PluginMetadata { // A human-readable name of the plugin. string name; // The version of the plugin, following the semantic versioning scheme. @@ -411,26 +447,28 @@ struct PluginManifest { // The author field SHOULD be a username representing the identity of the user or organization // that created this plugin. string author; + // String desciptions of the relative sensitivity of specific functions. The selectors MUST be selectors for + // functions implemented by this plugin. + SelectorPermission[] permissionDescriptors; +} +/// @dev A struct describing how the plugin should be installed on a modular account. +struct PluginManifest { // List of ERC-165 interfaceIds to add to account to support introspection checks. bytes4[] interfaceIds; - // If this plugin depends on other plugins' validation functions and/or hooks, the interface IDs of // those plugins MUST be provided here, with its position in the array matching the `dependencyIndex` // members of `ManifestFunction` structs used in the manifest. bytes4[] dependencyInterfaceIds; - // Execution functions defined in this plugin to be installed on the MSCA. - ManifestExecutionFunction[] executionFunctions; - - // Native functions or execution functions already installed on the MSCA that this plugin will be - // able to call. + bytes4[] executionFunctions; + // Plugin execution functions already installed on the MSCA that this plugin will be able to call. bytes4[] permittedExecutionSelectors; - - // External contract calls that this plugin will be able to make. - bool permitAnyExternalContract; + // Boolean to indicate whether the plugin can call any external contract addresses. + bool permitAnyExternalAddress; + // Boolean to indicate whether the plugin needs access to spend native tokens of the account. + bool canSpendNativeToken; ManifestExternalCallPermission[] permittedExternalCalls; - ManifestAssociatedFunction[] userOpValidationFunctions; ManifestAssociatedFunction[] runtimeValidationFunctions; ManifestAssociatedFunction[] preUserOpValidationHooks; @@ -438,6 +476,7 @@ struct PluginManifest { ManifestExecutionHook[] executionHooks; ManifestExecutionHook[] permittedCallHooks; } + ``` ### Expected behavior @@ -474,7 +513,7 @@ The function MUST store the plugin's permitted function selectors and external c The function MUST parse through the execution functions, validation functions, and hooks in the manifest and add them to the modular account after resolving each `ManifestFunction` type. - Each function selector MUST be added as a valid execution function on the modular account. If the function selector has already been added or matches the selector of a native function, the function SHOULD revert. -- If an associated function that is to be added already exists, the function SHOULD revert. +- If a validation function is to be added to a selector that already has that type of validation function, the function SHOULD revert. Next, the function MUST call the plugin's `onInstall` callback with the data provided in the `installData` parameter. This serves to initialize the plugin state for the modular account. If `onInstall` reverts, the `installPlugin` function MUST revert. @@ -493,11 +532,11 @@ The function MUST revert if the plugin is not installed on the modular account. The function SHOULD perform the following checks: - Revert if the hash of the manifest used at install time does not match the computed Keccak-256 hash of the plugin's current manifest. This prevents unclean removal of plugins that attempt to force a removal of a different plugin configuration than the one that was originally approved by the client for installation. To allow for removal of such plugins, the modular account MAY implement the capability for the manifest to be encoded in the config field as a parameter. -- Revert if there is at least 1 other installed plugin that depends on execution functions, validation functions, or hooks added by this plugin. Plugins used as dependencies must not be uninstalled while dependent plugins exist. +- Revert if there is at least 1 other installed plugin that depends on validation functions or hooks added by this plugin. Plugins used as dependencies must not be uninstalled while dependent plugins exist. -The function SHOULD update account storage to reflect the uninstall via inspection functions, such as those defined by `IPluginLoupe`. Each dependency's record SHOULD also be updated to reflect that it has no longer has this plugin as a dependent. +The function SHOULD update account storage to reflect the uninstall via inspection functions, such as those defined by `IAccountLoupe`. Each dependency's record SHOULD also be updated to reflect that it has no longer has this plugin as a dependent. -The function MUST remove records for the plugin's dependencies, injected permitted call hooks, permitted function selectors, and external contract calls. The hooks to remove MUST be exactly the same as what was provided during installation. It is up to the implementing modular account to decide how to keep this invariant. The config parameter field MAY be used. +The function MUST remove records for the plugin's dependencies, injected permitted call hooks, permitted function selectors, and permitted external calls. The hooks to remove MUST be exactly the same as what was provided during installation. It is up to the implementing modular account to decide how to keep this invariant. The config parameter field MAY be used. The function MUST parse through the execution functions, validation functions, and hooks in the manifest and remove them from the modular account after resolving each `ManifestFunction` type. @@ -550,7 +589,7 @@ No backward compatibility issues found. ## Reference Implementation -See `https://github.com/alchemyplatform/ERC-6900-Ref-Implementation` +See `https://github.com/erc6900/reference-implementation` ## Security Considerations From 2464989314d989f644abf3932ff1e0e8d4d8f303 Mon Sep 17 00:00:00 2001 From: Jay Paik Date: Mon, 4 Dec 2023 15:02:37 -0500 Subject: [PATCH 21/23] Update ERC-6900: Add missing `pluginMetadata()` method Merged by EIP-Bot. --- ERCS/erc-6900.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ERCS/erc-6900.md b/ERCS/erc-6900.md index 1505452bb3..24e8dd02f6 100644 --- a/ERCS/erc-6900.md +++ b/ERCS/erc-6900.md @@ -380,6 +380,11 @@ interface IPlugin { /// @dev The manifest MUST stay constant over time. /// @return A manifest describing the contents and intended configuration of the plugin. function pluginManifest() external pure returns (PluginManifest memory); + + /// @notice Describe the metadata of the plugin. + /// @dev This metadata MUST stay constant over time. + /// @return A metadata struct describing the plugin. + function pluginMetadata() external pure returns (PluginMetadata memory); } ``` From fedd46432e386ccb98934088012423838b1d06cc Mon Sep 17 00:00:00 2001 From: Ming Jiang Date: Tue, 5 Dec 2023 11:03:59 +0800 Subject: [PATCH 22/23] Add ERC: Cross-Contract Hierarchical NFT Merged by EIP-Bot. --- ERCS/erc-7510.md | 251 +++++++++++++++++++++++++ assets/erc-7510/contracts/ERC7510.sol | 81 ++++++++ assets/erc-7510/contracts/IERC7510.sol | 32 ++++ assets/erc-7510/hardhat.config.ts | 16 ++ assets/erc-7510/package.json | 30 +++ assets/erc-7510/test/ERC7510.test.ts | 95 ++++++++++ assets/erc-7510/tsconfig.json | 11 ++ 7 files changed, 516 insertions(+) create mode 100644 ERCS/erc-7510.md create mode 100644 assets/erc-7510/contracts/ERC7510.sol create mode 100644 assets/erc-7510/contracts/IERC7510.sol create mode 100644 assets/erc-7510/hardhat.config.ts create mode 100644 assets/erc-7510/package.json create mode 100644 assets/erc-7510/test/ERC7510.test.ts create mode 100644 assets/erc-7510/tsconfig.json diff --git a/ERCS/erc-7510.md b/ERCS/erc-7510.md new file mode 100644 index 0000000000..87e4d3941b --- /dev/null +++ b/ERCS/erc-7510.md @@ -0,0 +1,251 @@ +--- +eip: 7510 +title: Cross-Contract Hierarchical NFT +description: An extension of ERC-721 to maintain hierarchical relationship between tokens from different contracts. +author: Ming Jiang (@minkyn), Zheng Han (@hanbsd), Fan Yang (@fayang) +discussions-to: https://ethereum-magicians.org/t/eip-7510-cross-contract-hierarchical-nft/15687 +status: Draft +type: Standards Track +category: ERC +created: 2023-08-24 +requires: 721 +--- + +## Abstract + +This standard is an extension of [ERC-721](./eip-721.md). It proposes a way to maintain hierarchical relationship between tokens from different contracts. This standard provides an interface to query the parent tokens of an NFT or whether the parent relation exists between two NFTs. + +## Motivation + +Some NFTs want to generate derivative assets as new NFTs. For example, a 2D NFT image would like to publish its 3D model as a new derivative NFT. An NFT may also be derived from multiple parent NFTs. Such cases include a movie NFT featuring multiple characters from other NFTs. This standard is proposed to record such hierarchical relationship between derivative NFTs. + +Existing [ERC-6150](./eip-6150.md) introduces a similar feature, but it only builds hierarchy between tokens within the same contract. More than often we need to create a new NFT collection with the derivative tokens, which requires cross-contract relationship establishment. In addition, deriving from multiple parents is very common in the scenario of IP licensing, but the existing standard doesn't support that either. + +## Specification + +Solidity interface available at [`IERC7510.sol`](../assets/eip-7510/contracts/IERC7510.sol): + +```solidity +/// @notice The struct used to reference a token in an NFT contract +struct Token { + address collection; + uint256 id; +} + +interface IERC7510 { + + /// @notice Emitted when the parent tokens for an NFT is updated + event UpdateParentTokens(uint256 indexed tokenId); + + /// @notice Get the parent tokens of an NFT + /// @param tokenId The NFT to get the parent tokens for + /// @return An array of parent tokens for this NFT + function parentTokensOf(uint256 tokenId) external view returns (Token[] memory); + + /// @notice Check if another token is a parent of an NFT + /// @param tokenId The NFT to check its parent for + /// @param otherToken Another token to check as a parent or not + /// @return Whether `otherToken` is a parent of `tokenId` + function isParentToken(uint256 tokenId, Token memory otherToken) external view returns (bool); + + /// @notice Set the parent tokens for an NFT + /// @param tokenId The NFT to set the parent tokens for + /// @param parentTokens The parent tokens to set + function setParentTokens(uint256 tokenId, Token[] memory parentTokens) external; + +} +``` + +## Rationale + +This standard differs from [ERC-6150](./eip-6150.md) in mainly two aspects: supporting cross-contract token reference, and allowing multiple parents. But we try to keep the naming consistent overall. + +In addition, we didn't include `child` relation in the interface. An original NFT exists before its derivative NFTs. Therefore we know what parent tokens to include when minting derivative NFTs, but we wouldn't know the children tokens when minting the original NFT. If we have to record the children, that means whenever we mint a derivative NFT, we need to call on its original NFT to add it as a child. However, those two NFTs may belong to different contracts and thus require different write permissions, making it impossible to combine the two operations into a single transaction in practice. As a result, we decide to only record the `parent` relation from the derivative NFTs. + +## Backwards Compatibility + +No backwards compatibility issues found. + +## Test Cases + +Test cases available at: [`ERC7510.test.ts`](../assets/eip-7510/test/ERC7510.test.ts): + +```typescript +import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers"; +import { expect } from "chai"; +import { ethers } from "hardhat"; + +const NAME = "NAME"; +const SYMBOL = "SYMBOL"; +const TOKEN_ID = 1234; + +const PARENT_1_COLLECTION = "0xDEAdBEEf00000000000000000123456789ABCdeF"; +const PARENT_1_ID = 8888; +const PARENT_1_TOKEN = { collection: PARENT_1_COLLECTION, id: PARENT_1_ID }; + +const PARENT_2_COLLECTION = "0xBaDc0ffEe0000000000000000123456789aBCDef"; +const PARENT_2_ID = 9999; +const PARENT_2_TOKEN = { collection: PARENT_2_COLLECTION, id: PARENT_2_ID }; + +describe("ERC7510", function () { + + async function deployContractFixture() { + const [deployer, owner] = await ethers.getSigners(); + + const contract = await ethers.deployContract("ERC7510", [NAME, SYMBOL], deployer); + await contract.mint(owner, TOKEN_ID); + + return { contract, owner }; + } + + describe("Functions", function () { + it("Should not set parent tokens if not owner or approved", async function () { + const { contract } = await loadFixture(deployContractFixture); + + await expect(contract.setParentTokens(TOKEN_ID, [PARENT_1_TOKEN])) + .to.be.revertedWith("ERC7510: caller is not owner or approved"); + }); + + it("Should correctly query token without parents", async function () { + const { contract } = await loadFixture(deployContractFixture); + + expect(await contract.parentTokensOf(TOKEN_ID)).to.have.lengthOf(0); + + expect(await contract.isParentToken(TOKEN_ID, PARENT_1_TOKEN)).to.equal(false); + }); + + it("Should set parent tokens and then update", async function () { + const { contract, owner } = await loadFixture(deployContractFixture); + + await contract.connect(owner).setParentTokens(TOKEN_ID, [PARENT_1_TOKEN]); + + let parentTokens = await contract.parentTokensOf(TOKEN_ID); + expect(parentTokens).to.have.lengthOf(1); + expect(parentTokens[0].collection).to.equal(PARENT_1_COLLECTION); + expect(parentTokens[0].id).to.equal(PARENT_1_ID); + + expect(await contract.isParentToken(TOKEN_ID, PARENT_1_TOKEN)).to.equal(true); + expect(await contract.isParentToken(TOKEN_ID, PARENT_2_TOKEN)).to.equal(false); + + await contract.connect(owner).setParentTokens(TOKEN_ID, [PARENT_2_TOKEN]); + + parentTokens = await contract.parentTokensOf(TOKEN_ID); + expect(parentTokens).to.have.lengthOf(1); + expect(parentTokens[0].collection).to.equal(PARENT_2_COLLECTION); + expect(parentTokens[0].id).to.equal(PARENT_2_ID); + + expect(await contract.isParentToken(TOKEN_ID, PARENT_1_TOKEN)).to.equal(false); + expect(await contract.isParentToken(TOKEN_ID, PARENT_2_TOKEN)).to.equal(true); + }); + + it("Should burn and clear parent tokens", async function () { + const { contract, owner } = await loadFixture(deployContractFixture); + + await contract.connect(owner).setParentTokens(TOKEN_ID, [PARENT_1_TOKEN, PARENT_2_TOKEN]); + await contract.burn(TOKEN_ID); + + await expect(contract.parentTokensOf(TOKEN_ID)).to.be.revertedWith("ERC7510: query for nonexistent token"); + await expect(contract.isParentToken(TOKEN_ID, PARENT_1_TOKEN)).to.be.revertedWith("ERC7510: query for nonexistent token"); + await expect(contract.isParentToken(TOKEN_ID, PARENT_2_TOKEN)).to.be.revertedWith("ERC7510: query for nonexistent token"); + + await contract.mint(owner, TOKEN_ID); + + expect(await contract.parentTokensOf(TOKEN_ID)).to.have.lengthOf(0); + expect(await contract.isParentToken(TOKEN_ID, PARENT_1_TOKEN)).to.equal(false); + expect(await contract.isParentToken(TOKEN_ID, PARENT_2_TOKEN)).to.equal(false); + }); + }); + + describe("Events", function () { + it("Should emit event when set parent tokens", async function () { + const { contract, owner } = await loadFixture(deployContractFixture); + + await expect(contract.connect(owner).setParentTokens(TOKEN_ID, [PARENT_1_TOKEN, PARENT_2_TOKEN])) + .to.emit(contract, "UpdateParentTokens").withArgs(TOKEN_ID); + }); + }); + +}); +``` + +## Reference Implementation + +Reference implementation available at: [`ERC7510.sol`](../assets/eip-7510/contracts/ERC7510.sol): + +```solidity +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + +import "./IERC7510.sol"; + +contract ERC7510 is ERC721, IERC7510 { + + mapping(uint256 => Token[]) private _parentTokens; + mapping(uint256 => mapping(address => mapping(uint256 => bool))) private _isParentToken; + + constructor( + string memory name, string memory symbol + ) ERC721(name, symbol) {} + + function supportsInterface( + bytes4 interfaceId + ) public view virtual override returns (bool) { + return interfaceId == type(IERC7510).interfaceId || super.supportsInterface(interfaceId); + } + + function parentTokensOf( + uint256 tokenId + ) public view virtual override returns (Token[] memory) { + require(_exists(tokenId), "ERC7510: query for nonexistent token"); + return _parentTokens[tokenId]; + } + + function isParentToken( + uint256 tokenId, Token memory otherToken + ) public view virtual override returns (bool) { + require(_exists(tokenId), "ERC7510: query for nonexistent token"); + return _isParentToken[tokenId][otherToken.collection][otherToken.id]; + } + + function setParentTokens( + uint256 tokenId, Token[] memory parentTokens + ) public virtual override { + require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC7510: caller is not owner or approved"); + _clear(tokenId); + for (uint256 i = 0; i < parentTokens.length; i++) { + _parentTokens[tokenId].push(parentTokens[i]); + _isParentToken[tokenId][parentTokens[i].collection][parentTokens[i].id] = true; + } + emit UpdateParentTokens(tokenId); + } + + function _burn( + uint256 tokenId + ) internal virtual override { + super._burn(tokenId); + _clear(tokenId); + } + + function _clear( + uint256 tokenId + ) private { + Token[] storage parentTokens = _parentTokens[tokenId]; + for (uint256 i = 0; i < parentTokens.length; i++) { + delete _isParentToken[tokenId][parentTokens[i].collection][parentTokens[i].id]; + } + delete _parentTokens[tokenId]; + } + +} +``` + +## Security Considerations + +Parent tokens of an NFT may point to invalid data for two reasons. First, parent tokens could be burned later. Second, a contract implementing `setParentTokens` might not check the validity of `parentTokens` arguments. For security consideration, applications that retrieve parent tokens of an NFT need to verify they exist as valid tokens. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/assets/erc-7510/contracts/ERC7510.sol b/assets/erc-7510/contracts/ERC7510.sol new file mode 100644 index 0000000000..eb7f66b9ae --- /dev/null +++ b/assets/erc-7510/contracts/ERC7510.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + +import "./IERC7510.sol"; + +contract ERC7510 is ERC721, IERC7510 { + + mapping(uint256 => Token[]) private _parentTokens; + mapping(uint256 => mapping(address => mapping(uint256 => bool))) private _isParentToken; + + constructor( + string memory name, string memory symbol + ) ERC721(name, symbol) {} + + function supportsInterface( + bytes4 interfaceId + ) public view virtual override returns (bool) { + return interfaceId == type(IERC7510).interfaceId || super.supportsInterface(interfaceId); + } + + function parentTokensOf( + uint256 tokenId + ) public view virtual override returns (Token[] memory) { + require(_exists(tokenId), "ERC7510: query for nonexistent token"); + return _parentTokens[tokenId]; + } + + function isParentToken( + uint256 tokenId, Token memory otherToken + ) public view virtual override returns (bool) { + require(_exists(tokenId), "ERC7510: query for nonexistent token"); + return _isParentToken[tokenId][otherToken.collection][otherToken.id]; + } + + function setParentTokens( + uint256 tokenId, Token[] memory parentTokens + ) public virtual override { + require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC7510: caller is not owner or approved"); + _clear(tokenId); + for (uint256 i = 0; i < parentTokens.length; i++) { + _parentTokens[tokenId].push(parentTokens[i]); + _isParentToken[tokenId][parentTokens[i].collection][parentTokens[i].id] = true; + } + emit UpdateParentTokens(tokenId); + } + + function _burn( + uint256 tokenId + ) internal virtual override { + super._burn(tokenId); + _clear(tokenId); + } + + function _clear( + uint256 tokenId + ) private { + Token[] storage parentTokens = _parentTokens[tokenId]; + for (uint256 i = 0; i < parentTokens.length; i++) { + delete _isParentToken[tokenId][parentTokens[i].collection][parentTokens[i].id]; + } + delete _parentTokens[tokenId]; + } + + // For test only + function mint( + address to, uint256 tokenId + ) public virtual { + _mint(to, tokenId); + } + + // For test only + function burn( + uint256 tokenId + ) public virtual { + _burn(tokenId); + } + +} diff --git a/assets/erc-7510/contracts/IERC7510.sol b/assets/erc-7510/contracts/IERC7510.sol new file mode 100644 index 0000000000..98ce7916c7 --- /dev/null +++ b/assets/erc-7510/contracts/IERC7510.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity ^0.8.0; + +/// @notice The struct used to reference a token in an NFT contract +struct Token { + address collection; + uint256 id; +} + +interface IERC7510 { + + /// @notice Emitted when the parent tokens for an NFT is updated + event UpdateParentTokens(uint256 indexed tokenId); + + /// @notice Get the parent tokens of an NFT + /// @param tokenId The NFT to get the parent tokens for + /// @return An array of parent tokens for this NFT + function parentTokensOf(uint256 tokenId) external view returns (Token[] memory); + + /// @notice Check if another token is a parent of an NFT + /// @param tokenId The NFT to check its parent for + /// @param otherToken Another token to check as a parent or not + /// @return Whether `otherToken` is a parent of `tokenId` + function isParentToken(uint256 tokenId, Token memory otherToken) external view returns (bool); + + /// @notice Set the parent tokens for an NFT + /// @param tokenId The NFT to set the parent tokens for + /// @param parentTokens The parent tokens to set + function setParentTokens(uint256 tokenId, Token[] memory parentTokens) external; + +} diff --git a/assets/erc-7510/hardhat.config.ts b/assets/erc-7510/hardhat.config.ts new file mode 100644 index 0000000000..e47e09fab8 --- /dev/null +++ b/assets/erc-7510/hardhat.config.ts @@ -0,0 +1,16 @@ +import { HardhatUserConfig } from "hardhat/config"; +import "@nomicfoundation/hardhat-toolbox"; + +const config: HardhatUserConfig = { + solidity: { + version: "0.8.21", + settings: { + optimizer: { + enabled: true, + runs: 200, + }, + }, + }, +}; + +export default config; diff --git a/assets/erc-7510/package.json b/assets/erc-7510/package.json new file mode 100644 index 0000000000..475a1fb7e5 --- /dev/null +++ b/assets/erc-7510/package.json @@ -0,0 +1,30 @@ +{ + "name": "erc-7510", + "version": "1.0.0", + "description": "Reference implementation and test cases for ERC-7510", + "author": "Comoco Labs", + "license": "CC0-1.0", + "devDependencies": { + "@nomicfoundation/hardhat-chai-matchers": "^2.0.2", + "@nomicfoundation/hardhat-ethers": "^3.0.4", + "@nomicfoundation/hardhat-network-helpers": "^1.0.9", + "@nomicfoundation/hardhat-toolbox": "^3.0.0", + "@nomicfoundation/hardhat-verify": "^1.1.1", + "@typechain/ethers-v6": "^0.4.3", + "@typechain/hardhat": "^8.0.3", + "@types/chai": "^4.3.5", + "@types/mocha": "^10.0.1", + "@types/node": "^20.5.9", + "chai": "^4.3.8", + "ethers": "^6.7.1", + "hardhat": "^2.17.2", + "hardhat-gas-reporter": "^1.0.9", + "solidity-coverage": "^0.8.4", + "ts-node": "^10.9.1", + "typechain": "^8.3.1", + "typescript": "^5.2.2" + }, + "dependencies": { + "@openzeppelin/contracts": "^4.9.3" + } +} diff --git a/assets/erc-7510/test/ERC7510.test.ts b/assets/erc-7510/test/ERC7510.test.ts new file mode 100644 index 0000000000..3b1811c25f --- /dev/null +++ b/assets/erc-7510/test/ERC7510.test.ts @@ -0,0 +1,95 @@ +import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers"; +import { expect } from "chai"; +import { ethers } from "hardhat"; + +const NAME = "NAME"; +const SYMBOL = "SYMBOL"; +const TOKEN_ID = 1234; + +const PARENT_1_COLLECTION = "0xDEAdBEEf00000000000000000123456789ABCdeF"; +const PARENT_1_ID = 8888; +const PARENT_1_TOKEN = { collection: PARENT_1_COLLECTION, id: PARENT_1_ID }; + +const PARENT_2_COLLECTION = "0xBaDc0ffEe0000000000000000123456789aBCDef"; +const PARENT_2_ID = 9999; +const PARENT_2_TOKEN = { collection: PARENT_2_COLLECTION, id: PARENT_2_ID }; + +describe("ERC7510", function () { + + async function deployContractFixture() { + const [deployer, owner] = await ethers.getSigners(); + + const contract = await ethers.deployContract("ERC7510", [NAME, SYMBOL], deployer); + await contract.mint(owner, TOKEN_ID); + + return { contract, owner }; + } + + describe("Functions", function () { + it("Should not set parent tokens if not owner or approved", async function () { + const { contract } = await loadFixture(deployContractFixture); + + await expect(contract.setParentTokens(TOKEN_ID, [PARENT_1_TOKEN])) + .to.be.revertedWith("ERC7510: caller is not owner or approved"); + }); + + it("Should correctly query token without parents", async function () { + const { contract } = await loadFixture(deployContractFixture); + + expect(await contract.parentTokensOf(TOKEN_ID)).to.have.lengthOf(0); + + expect(await contract.isParentToken(TOKEN_ID, PARENT_1_TOKEN)).to.equal(false); + }); + + it("Should set parent tokens and then update", async function () { + const { contract, owner } = await loadFixture(deployContractFixture); + + await contract.connect(owner).setParentTokens(TOKEN_ID, [PARENT_1_TOKEN]); + + let parentTokens = await contract.parentTokensOf(TOKEN_ID); + expect(parentTokens).to.have.lengthOf(1); + expect(parentTokens[0].collection).to.equal(PARENT_1_COLLECTION); + expect(parentTokens[0].id).to.equal(PARENT_1_ID); + + expect(await contract.isParentToken(TOKEN_ID, PARENT_1_TOKEN)).to.equal(true); + expect(await contract.isParentToken(TOKEN_ID, PARENT_2_TOKEN)).to.equal(false); + + await contract.connect(owner).setParentTokens(TOKEN_ID, [PARENT_2_TOKEN]); + + parentTokens = await contract.parentTokensOf(TOKEN_ID); + expect(parentTokens).to.have.lengthOf(1); + expect(parentTokens[0].collection).to.equal(PARENT_2_COLLECTION); + expect(parentTokens[0].id).to.equal(PARENT_2_ID); + + expect(await contract.isParentToken(TOKEN_ID, PARENT_1_TOKEN)).to.equal(false); + expect(await contract.isParentToken(TOKEN_ID, PARENT_2_TOKEN)).to.equal(true); + }); + + it("Should burn and clear parent tokens", async function () { + const { contract, owner } = await loadFixture(deployContractFixture); + + await contract.connect(owner).setParentTokens(TOKEN_ID, [PARENT_1_TOKEN, PARENT_2_TOKEN]); + await contract.burn(TOKEN_ID); + + await expect(contract.parentTokensOf(TOKEN_ID)).to.be.revertedWith("ERC7510: query for nonexistent token"); + await expect(contract.isParentToken(TOKEN_ID, PARENT_1_TOKEN)).to.be.revertedWith("ERC7510: query for nonexistent token"); + await expect(contract.isParentToken(TOKEN_ID, PARENT_2_TOKEN)).to.be.revertedWith("ERC7510: query for nonexistent token"); + + await contract.mint(owner, TOKEN_ID); + + expect(await contract.parentTokensOf(TOKEN_ID)).to.have.lengthOf(0); + expect(await contract.isParentToken(TOKEN_ID, PARENT_1_TOKEN)).to.equal(false); + expect(await contract.isParentToken(TOKEN_ID, PARENT_2_TOKEN)).to.equal(false); + }); + }); + + describe("Events", function () { + it("Should emit event when set parent tokens", async function () { + const { contract, owner } = await loadFixture(deployContractFixture); + + await expect(contract.connect(owner).setParentTokens(TOKEN_ID, [PARENT_1_TOKEN, PARENT_2_TOKEN])) + .to.emit(contract, "UpdateParentTokens").withArgs(TOKEN_ID); + }); + }); + +}); diff --git a/assets/erc-7510/tsconfig.json b/assets/erc-7510/tsconfig.json new file mode 100644 index 0000000000..574e785c71 --- /dev/null +++ b/assets/erc-7510/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true + } +} From 8a1778f88cd9f959bd4f8ebf44ea74d39f81742a Mon Sep 17 00:00:00 2001 From: saitama2009 <107180872+saitama2009@users.noreply.github.com> Date: Wed, 6 Dec 2023 05:28:20 +0800 Subject: [PATCH 23/23] Add ERC: Open IP Protocol built on NFTs Merged by EIP-Bot. --- ERCS/erc-7548.md | 182 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 ERCS/erc-7548.md diff --git a/ERCS/erc-7548.md b/ERCS/erc-7548.md new file mode 100644 index 0000000000..e16fd3fa2c --- /dev/null +++ b/ERCS/erc-7548.md @@ -0,0 +1,182 @@ +--- +eip: 7548 +title: Open IP Protocol built on NFTs +description: A protocol that enables users to remix NFTs and generate new NFT derivative works, while their relationships can be traced on chain. +author: Combo , Saitama (@saitama2009), CT29 , Luigi +discussions-to: https://ethereum-magicians.org/t/draft-open-ip-protocol/16373 +status: Draft +type: Standards Track +category: ERC +created: 2023-10-31 +requires: 165, 721 +--- + +## Abstract + +This proposal aims to establish a standardized method for creating new intellectual properties (IPs) by remixing multiple existing IPs in a decentralized manner. + +The protocol is built on the foundation of NFTs (Non-Fungible Tokens). Within this protocol, each intellectual property is represented as an NFT. It extends the [ERC-721](./eip-721.md) standard, enabling users to generate a new NFT by remixing multiple existing NFTs. To ensure transparency and traceability in the creation process, the relationships between the new NFT and the original NFTs are recorded on the blockchain and made publicly accessible. + +Furthermore, to enhance the liquidity of IP, users not only have the ability to remix NFTs they own but can also grant permission to others to participate in the creation of new NFTs using their own NFTs. + +## Motivation + +The internet is flooded with fresh content every day, but with the traditional IP infrastructure, IP registration and licensing is a headache for digital creators. The rapid creation of content has eclipsed the slower pace of IP registration, leaving much of this content unprotected. This means digital creators can't fairly earn from their work's spread. + +||Traditional IP Infrastructure|Open IP Infrastructure| +|-|-|-| +|IP Registration|Long waits, heaps of paperwork, and tedious back-and-forths.|An NFT represents intellectual property; the owner of the NFT holds the rights to the IP.| +|IP Licensing|Lengthy discussions, legal jargon, and case-by-case agreements.|A one-stop global IP licensing market that supports various licensing agreements.| + +With this backdrop, we're passionate about building an Open IP ecosystem tailored for today's digital creators. Here, with just a few clicks, creators can register, license, and monetize their content globally, without geographical or linguistic barriers. + +## Specification + +The keywords “MUST,” “MUST NOT,” “REQUIRED,” “SHALL,” “SHALL NOT,” “SHOULD,” “SHOULD NOT,” “RECOMMENDED,” “MAY,” and “OPTIONAL” in this document are to be interpreted as described in RFC 2119. + +**Interface** + +This protocol standardizes how to remix multiple existing NFTs and create a new NFT derivative work (known as a combo), while their relationships can be traced on the blockchain. It contains three core modules, remix module, network module, and license module. + +### Remix Module + +This module extends the ERC-721 standard and enables users to create a new NFT by remixing multiple existing NFTs, whether they’re ERC-721 or [ERC-1155](./eip-1155.md). + +```solidity +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.10; + +interface IERC721X { + // Events + + /// @dev Emits when a combo is minted. + /// @param owner The owner address of the newly minted combo + /// @param comboId The newly minted combo identifier + event ComboMinted(address indexed owner, uint256 indexed comboId); + + // Structs + + /// @param tokenAddress The NFT's collection address + /// @param tokenId The NFT identifier + struct Token { + address tokenAddress; + uint256 tokenId; + } + + /// @param amount The number of NFTs used + /// @param licenseId Which license to be used to verify this component + struct Component { + Token token; + uint256 amount; + uint256 licenseId; + } + + // Functions + + /// @dev Mints a NFT by remixing multiple existing NFTs. + /// @param components The NFTs remixed to mint a combo + /// @param hash The hash representing the algorithm about how to generate the combo's metadata when remixing multiple existing NFTs. + function mint( + Component[] calldata components, + string calldata hash + ) external; + + /// @dev Retrieve a combo's components. + function getComponents( + uint256 comboId + ) external view returns (Component[] memory); +} +``` + +### License Module + +By default, users can only remix multiple NFTs they own to create new NFT derivative works. This module enables NFT holders to grant others permission to use their NFTs in the remixing process. + +```solidity +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.10; + +import "./IERC721X.sol"; + +interface ILicense { + /// @dev Verify the permission when minting a combo + /// @param user The minter + /// @param combo The new NFT to be minted by remixing multiple existing NFTs + /// @return components The multiple existing NFTs used to mint the new combo + function verify( + address user, + IERC721X.Token calldata combo, + IERC721X.Component[] calldata components + ) external returns (bool); +} +``` + +### Network Module + +This module follows the singleton pattern and is used to track all relationships between the original NFTs and their NFT derivative works. + +```solidity +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.10; + +import "./IERC721X.sol"; + +interface INFTNetIndexer { + /// @dev Verify if the `child` was created by remixing the `parent` with other NFTs. + /// @param parent Any NFT + /// @param child Any NFT + function isParent( + IERC721X.Token calldata parent, + IERC721X.Token calldata child + ) external view returns (bool); + + /// @dev Verify if `a` and `b` have common `parent`s + /// @param a Any NFT + /// @param b Any NFT + function isSibling( + IERC721X.Token calldata a, + IERC721X.Token calldata b + ) external view returns (bool, IERC721X.Token[] memory commonParents); + + /// @dev Return all parents of a `token` + /// @param token Any NFT + /// @return parents All NFTs used to mint the `token` + function getParents( + IERC721X.Token calldata token + ) external view returns (IERC721X.Token[] memory parents); +} +``` + +## Rationale + +The Open IP Protocol is built on the "1 premise, 2 extensions, 1 constant" principle. + +The “1 premise” means that for any IP in the Open IP ecosystem, an NFT stands for that IP. So, if you have the NFT, you own the IP. That’s why the Open IP Protocol is designed as an extended protocol compatible with ERC-721. + +The “2 extensions” refer to the diversification of IP licensing and remixing. + +- IP licensing methods are diverse. For example, delegating an NFT to someone else is one type of licensing, setting a price for the number of usage rights is another type of licensing, and even pricing based on auction, AMM, or other pricing mechanisms can develop different licensing methods. Therefore, the license module is designed allowing various custom licensing methods. + +- IP remixing rules are also diverse. When remixing multiple existing NFTs, whether to support ERC-1155, whether to limit the range of NFT selection, and whether the NFT is consumed after remixing, there is no standard. So, the remix module is designed to support custom remixing rules. + +The "1 constant" refers to the fact that the traceability information of IP licensing is always public and unchangeable. Regardless of how users license or remix IPs, the relationship between the original and new IPs remains consistent. Moreover, if all IP relationships are recorded in the same database, it would create a vast IP network. If other social or gaming dApps leverage this network, it can lead to entirely novel user experiences. Hence, this protocol's network module is designed as a singleton. + +## Backwards Compatibility + +This proposal is fully backwards compatible with the existing ERC-721 standard, extending the standard with new functions that do not affect the core functionality. + + + +## Security Considerations + +This standard highlights several security concerns that need attention: + +* **Ownership and Permissions**: Only the NFT owner or those granted by them should be allowed to remix NFTs into NFT derivative works. It's vital to have strict access controls to prevent unauthorized creations. + +* **Reentrancy Risks**: Creating derivative works might require interacting with multiple external contracts, like the remix, license, and network modules. This could open the door to reentrancy attacks, so protective measures are necessary. + +* **Gas Usage**: Remixing NFTs can be computation-heavy and involve many contract interactions, which might result in high gas fees. It's important to optimize these processes to keep costs down and maintain user-friendliness. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md).