This standard addresses an extension to the ERC-721 specification by allowing signatures on NFTs representing works of art. This provides improved provenance by creating functionality for an artist to designate an original and signed limited-edition prints of their work.
Abstract
ERC-3440 is an ERC-721 extension specifically designed to make NFTs more robust for works of art. This extends the original ERC-721 spec by providing the ability to designate the original and limited-edition prints with a specialized enumeration extension similar to the original 721 extension built-in. The key improvement of this extension is allowing artists to designate the limited nature of their prints and provide a signed piece of data that represents their unique signature to a given token Id, much like an artist would sign a print of their work.
Motivation
Currently the link between a NFT and the digital work of art is only enforced in the token metadata stored in the shared tokenURI state of a NFT. While the blockchain provides an immutable record of history back to the origin of an NFT, often the origin is not a key that an artist maintains as closely as they would a hand written signature.
An edition is a printed replica of an original piece of art. ERC-721 is not specifically designed to be used for works of art, such as digital art and music. ERC-721 (NFT) was originally created to handle deeds and other contracts. Eventually ERC-721 evolved into gaming tokens, where metadata hosted by servers may be sufficient. This proposal takes the position that we can create a more tangible link between the NFT, digital art, owner, and artist. By making a concise standard for art, it will be easier for an artist to maintain a connection with the Ethereum blockchain as well as their fans that purchase their tokens.
The use cases for NFTs have evolved into works of digital art, and there is a need to designate an original NFT and printed editions with signatures in a trustless manner. ERC-721 contracts may or may not be deployed by artists, and currently, the only way to understand that something is uniquely touched by an artist is to display it on 3rd party applications that assume a connection via metadata that exists on servers, external to the blockchain. This proposal helps remove that distance with readily available functionality for artists to sign their work and provides a standard for 3rd party applications to display the uniqueness of a NFT for those that purchase them. The designation of limited-editions combined with immutable signatures, creates a trustlessly enforced link. This signature is accompanied by view functions that allow applications to easily display these signatures and limited-edition prints as evidence of uniqueness by showing that artists specifically used their key to designate the total supply and sign each NFT.
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.
ERC-721 compliant contracts MAY implement this ERC for editions to provide a standard method for designating the original and limited-edition prints with signatures from the artist.
Implementations of ERC-3440 MUST designate which token Id is the original NFT (defaulted to Id 0), and which token Id is a unique replica. The original print SHOULD be token Id number 0 but MAY be assigned to a different Id. The original print MUST only be designated once. The implementation MUST designate a maximum number of minted editions, after which new Ids MUST NOT be printed / minted.
Artists MAY use the signing feature to sign the original or limited edition prints but this is OPTIONAL. A standard message to sign is RECOMMENDED to be simply a hash of the integer of the token Id.
A contract that is compliant with ERC-3440 shall implement the following abstract contract (referred to as ERC3440.sol):
// SPDX-License-Identifier: MIT
pragmasolidity^0.8.0;import"@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";import"@openzeppelin/contracts/utils/cryptography/ECDSA.sol";/**
* @dev ERC721 token with editions extension.
*/abstractcontractERC3440isERC721URIStorage{// eip-712
structEIP712Domain{stringname;stringversion;uint256chainId;addressverifyingContract;}// Contents of message to be signed
structSignature{addressverificationAddress;// ensure the artists signs only address(this) for each piece
stringartist;addresswallet;stringcontents;}// type hashes
bytes32constantEIP712DOMAIN_TYPEHASH=keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");bytes32constantSIGNATURE_TYPEHASH=keccak256("Signature(address verifyAddress,string artist,address wallet, string contents)");bytes32publicDOMAIN_SEPARATOR;// Optional mapping for signatures
mapping(uint256=>bytes)private_signatures;// A view to display the artist's address
addresspublicartist;// A view to display the total number of prints created
uintpubliceditionSupply=0;// A view to display which ID is the original copy
uintpublicoriginalId=0;// A signed token event
eventSigned(addressindexedfrom,uint256indexedtokenId);/**
* @dev Sets `artist` as the original artist.
* @param `address _artist` the wallet of the signing artist (TODO consider multiple
* signers and contract signers (non-EOA)
*/function_designateArtist(address_artist)internalvirtual{require(artist==address(0),"ERC721Extensions: the artist has already been set");// If there is no special designation for the artist, set it.
artist=_artist;}/**
* @dev Sets `tokenId as the original print` as the tokenURI of `tokenId`.
* @param `uint256 tokenId` the nft id of the original print
*/function_designateOriginal(uint256_tokenId)internalvirtual{require(msg.sender==artist,"ERC721Extensions: only the artist may designate originals");require(_exists(_tokenId),"ERC721Extensions: Original query for nonexistent token");require(originalId==0,"ERC721Extensions: Original print has already been designated as a different Id");// If there is no special designation for the original, set it.
originalId=_tokenId;}/**
* @dev Sets total number printed editions of the original as the tokenURI of `tokenId`.
* @param `uint256 _maxEditionSupply` max supply
*/function_setLimitedEditions(uint256_maxEditionSupply)internalvirtual{require(msg.sender==artist,"ERC721Extensions: only the artist may designate max supply");require(editionSupply==0,"ERC721Extensions: Max number of prints has already been created");// If there is no max supply of prints, set it. Leaving supply at 0 indicates there are no prints of the original
editionSupply=_maxEditionSupply;}/**
* @dev Creates `tokenIds` representing the printed editions.
* @param `string memory _tokenURI` the metadata attached to each nft
*/function_createEditions(stringmemory_tokenURI)internalvirtual{require(msg.sender==artist,"ERC721Extensions: only the artist may create prints");require(editionSupply>0,"ERC721Extensions: the edition supply is not set to more than 0");for(uinti=0;i<editionSupply;i++){_mint(msg.sender,i);_setTokenURI(i,_tokenURI);}}/**
* @dev internal hashing utility
* @param `Signature memory _message` the signature message struct to be signed
* the address of this contract is enforced in the hashing
*/function_hash(Signaturememory_message)internalviewreturns(bytes32){returnkeccak256(abi.encodePacked("\x19\x01",DOMAIN_SEPARATOR,keccak256(abi.encode(SIGNATURE_TYPEHASH,address(this),_message.artist,_message.wallet,_message.contents))));}/**
* @dev Signs a `tokenId` representing a print.
* @param `uint256 _tokenId` id of the NFT being signed
* @param `Signature memory _message` the signed message
* @param `bytes memory _signature` signature bytes created off-chain
*
* Requirements:
*
* - `tokenId` must exist.
*
* Emits a {Signed} event.
*/function_signEdition(uint256_tokenId,Signaturememory_message,bytesmemory_signature)internalvirtual{require(msg.sender==artist,"ERC721Extensions: only the artist may sign their work");require(_signatures[_tokenId].length==0,"ERC721Extensions: this token is already signed");bytes32digest=hash(_message);addressrecovered=ECDSA.recover(digest,_signature);require(recovered==artist,"ERC721Extensions: artist signature mismatch");_signatures[_tokenId]=_signature;emitSigned(artist,_tokenId);}/**
* @dev displays a signature from the artist.
* @param `uint256 _tokenId` NFT id to verify isSigned
* @returns `bytes` gets the signature stored on the token
*/functiongetSignature(uint256_tokenId)externalviewvirtualreturns(bytesmemory){require(_signatures[_tokenId].length!=0,"ERC721Extensions: no signature exists for this Id");return_signatures[_tokenId];}/**
* @dev returns `true` if the message is signed by the artist.
* @param `Signature memory _message` the message signed by an artist and published elsewhere
* @param `bytes memory _signature` the signature on the message
* @param `uint _tokenId` id of the token to be verified as being signed
* @returns `bool` true if signed by artist
* The artist may broadcast signature out of band that will verify on the nft
*/functionisSigned(Signaturememory_message,bytesmemory_signature,uint_tokenId)externalviewvirtualreturns(bool){bytes32messageHash=hash(_message);address_artist=ECDSA.recover(messageHash,_signature);return(_artist==artist&&_equals(_signatures[_tokenId],_signature));}/**
* @dev Utility function that checks if two `bytes memory` variables are equal. This is done using hashing,
* which is much more gas efficient then comparing each byte individually.
* Equality means that:
* - 'self.length == other.length'
* - For 'n' in '[0, self.length)', 'self[n] == other[n]'
*/function_equals(bytesmemory_self,bytesmemory_other)internalpurereturns(boolequal){if(_self.length!=_other.length){returnfalse;}uintaddr;uintaddr2;uintlen=_self.length;assembly{addr:=add(_self,/*BYTES_HEADER_SIZE*/32)addr2:=add(_other,/*BYTES_HEADER_SIZE*/32)}assembly{equal:=eq(keccak256(addr,len),keccak256(addr2,len))}}}
Rationale
A major role of NFTs is to display uniqueness in digital art. Provenance is a desired feature of works of art, and this standard will help improve a NFT by providing a better way to verify uniqueness. Taking this extra step by an artist to explicitly sign tokens provides a better connection between the artists and their work on the blockchain. Artists can now retain their private key and sign messages in the future showing that the same signature is present on a unique NFT.
Backwards Compatibility
This proposal combines already available 721 extensions and is backwards compatible with the ERC-721 standard.
Test Cases
An example implementation including tests can be found here.
Reference Implementation
// SPDX-License-Identifier: MIT
pragmasolidity^0.8.0;import"./ERC3440.sol";/**
* @dev ERC721 token with editions extension.
*/contractArtTokenisERC3440{/**
* @dev Sets `address artist` as the original artist to the account deploying the NFT.
*/constructor(stringmemory_name,stringmemory_symbol,uint_numberOfEditions,stringmemorytokenURI,uint_originalId)ERC721(_name,_symbol){_designateArtist(msg.sender);_setLimitedEditions(_numberOfEditions);_createEditions(tokenURI);_designateOriginal(_originalId);DOMAIN_SEPARATOR=keccak256(abi.encode(EIP712DOMAIN_TYPEHASH,keccak256(bytes("Artist's Editions")),keccak256(bytes("1")),1,address(this)));}/**
* @dev Signs a `tokenId` representing a print.
*/functionsign(uint256_tokenId,Signaturememory_message,bytesmemory_signature)public{_signEdition(_tokenId,_message,_signature);}}
Security Considerations
This extension gives an artist the ability to designate an original edition, set the maximum supply of editions as well as print the editions and uses the tokenURI extension to supply a link to the art work. To minimize the risk of an artist changing this value after selling an original piece this function can only happen once. Ensuring that these functions can only happen once provides consistency with uniqueness and verifiability. Due to this, the reference implementation handles these features in the constructor function. An edition may only be signed once, and care should be taken that the edition is signed correctly before release of the token/s.