ERC-721 Editions Standard
Simple Summary
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.
Signature messages MUST use the EIP-712 standard.
A contract that is compliant with ERC-3440 shall implement the following abstract contract (referred to as ERC3440.sol):
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
/**
* @dev ERC721 token with editions extension.
*/
abstract contract ERC3440 is ERC721URIStorage {
// eip-712
struct EIP712Domain {
string name;
string version;
uint256 chainId;
address verifyingContract;
}
// Contents of message to be signed
struct Signature {
address verificationAddress; // ensure the artists signs only address(this) for each piece
string artist;
address wallet;
string contents;
}
// type hashes
bytes32 constant EIP712DOMAIN_TYPEHASH = keccak256(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
);
bytes32 constant SIGNATURE_TYPEHASH = keccak256(
"Signature(address verifyAddress,string artist,address wallet, string contents)"
);
bytes32 public DOMAIN_SEPARATOR;
// Optional mapping for signatures
mapping (uint256 => bytes) private _signatures;
// A view to display the artist's address
address public artist;
// A view to display the total number of prints created
uint public editionSupply = 0;
// A view to display which ID is the original copy
uint public originalId = 0;
// A signed token event
event Signed(address indexed from, uint256 indexed tokenId);
/**
* @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) internal virtual {
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) internal virtual {
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) internal virtual {
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(string memory _tokenURI) internal virtual {
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(uint i=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(Signature memory _message) internal view returns (bytes32) {
return keccak256(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, Signature memory _message, bytes memory _signature) internal virtual {
require(msg.sender == artist, "ERC721Extensions: only the artist may sign their work");
require(_signatures[_tokenId].length == 0, "ERC721Extensions: this token is already signed");
bytes32 digest = hash(_message);
address recovered = ECDSA.recover(digest, _signature);
require(recovered == artist, "ERC721Extensions: artist signature mismatch");
_signatures[_tokenId] = _signature;
emit Signed(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
*/
function getSignature(uint256 _tokenId) external view virtual returns (bytes memory) {
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
*/
function isSigned(Signature memory _message, bytes memory _signature, uint _tokenId) external view virtual returns (bool) {
bytes32 messageHash = 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(bytes memory _self, bytes memory _other) internal pure returns (bool equal) {
if (_self.length != _other.length) {
return false;
}
uint addr;
uint addr2;
uint len = _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
pragma solidity ^0.8.0;
import "./ERC3440.sol";
/**
* @dev ERC721 token with editions extension.
*/
contract ArtToken is ERC3440 {
/**
* @dev Sets `address artist` as the original artist to the account deploying the NFT.
*/
constructor (
string memory _name,
string memory _symbol,
uint _numberOfEditions,
string memory tokenURI,
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.
*/
function sign(uint256 _tokenId, Signature memory _message, bytes memory _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.
Copyright
Copyright and related rights waived via CC0.