eip: 5008
title: ERC-721 Nonce and Metadata Update Extension
description: Add a nonce property to ERC-721 tokens.
author: Anders (@0xanders), Lance (@LanceSnow), Shrug shrug@emojidao.org
discussions-to:
status: Draft
type: Standards Track
category: ERC
created: 2022-04-10
requires: 165, 721
Abstract
This standard is an extension of ERC-721. It proposes adding a nonce
property and MetadataUpdate
event to ERC-721 tokens.
Motivation
Some orders of NFT marketplace has been attacked and the NFTs have been sold in a lower price than market floor price. One reason is that users transfer NFT to another wallet and then, after a certain period of time, transfer it back to the original wallet, and the order becomes valid again.
This EIP proposes adding an nonce
property to ERC-721 tokens, and the nonce
will be changed when transfer. If nonce
is added to an order, the order can be checked to avoid attacks.
Many ERC-721 contracts emit their custom event when metadata changed. It is easy to update metadata of one NFT by specific event, but it is difficult for third-party platforms such as NFT marketplace to update metadata of many NFTs based on custom events.
Having a standard MetadataUpdate
event will make it easy for third-party platforms to timely update metadata of many NFTs.
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 IERC5008 {
/// @dev This event emits when the metadata of a token is changed.
/// So that the third-party platforms such as NFT market could
/// timely update the images and related attributes of the NFT
event MetadataUpdate(uint256 tokenId);
// Logged when the nonce of a NFT is changed
/// @notice Emitted when the `nonce` of an NFT is changed
event UpdateNonce(uint256 tokenId, uint256 newNonce);
/// @notice Get the nonce of an NFT
/// Throws if `tokenId` is not a valid NFT
/// @param tokenId The NFT to get the nonce for
/// @return The nonce of this NFT
function nonce(uint256 tokenId) external view returns(uint256);
}
The nonce(uint256 tokenId)
function MAY be implemented as pure
or view
.
The UpdateNonce
event MUST be emitted when the nonce of a NFT is changed.
The MetadataUpdate
event MUST be emitted when the metadata of a token is changed.
Rationale
At first transferCount
was considered as function name, but there may some case to change the nonce
besides transfer, such as important properties are changed, then we changed transferCount
to nonce
.
Different NFTs have different metadata, and metadata generally has multiple fields. bytes data
could be used to represents the modified value of metadata. It is difficult for third-party platforms to identify various types of bytes data
, so there is only one parameter uint256 indexed _tokenId
in MetadataUpdate
event. After capturing the MetadataUpdate
event, a third party can update the metadata with information returned from the tokenURI(uint256 _tokenId)
of ERC721.
Backwards Compatibility
This standard is compatible with current ERC-721 standards.
Test Cases
Test Contract
pragma solidity 0.8.10;
import "./ERC5008.sol";
contract ERC5008Demo is ERC5008{
mapping(uint256 => uint256) private _tokenData;
constructor(string memory name_, string memory symbol_)ERC721WithNonce(name_, symbol_){
}
/// @notice mint a new NFT
/// @param to The owner of the new token
/// @param tokenId The id of the new token
function mint(address to, uint256 tokenId) public {
_mint(to, id);
}
/// @notice update the data of the NFT
/// @param id The id of the token
/// @param data The data of the token
function update(uint256 tokenId, uint256 data) public {
require(_exists(tokenId), "Error: nonexistent token");
_tokenData[tokenId] = data;
emit MetadataUpdate(tokenId);
}
}
Test Code
run in terminal: npm hardhat test
import { expect } from "chai";
import { ethers } from "hardhat";
describe("Test ERC5008 ", function () {
let [alice, bob] = await ethers.getSigners();
const ERC5008Demo = await ethers.getContractFactory("ERC5008Demo");
let contract = await ERC5008Demo.deploy();
let tokenId = 1;
await contract.mint(alice.address, tokenId);
expect(await contract.nonce(tokenId)).equals(1);
await contract.transferFrom(alice.address, bob.address, tokenId);
expect(await contract.nonce(tokenId)).equals(2);
});
Reference Implementation
// SPDX-License-Identifier: CC0
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "./IERC5008.sol";
contract ERC5008 is ERC721, IERC5008 {
mapping(uint256 => uint256) private _tokenNonce;
constructor(string memory name_, string memory symbol_)ERC721(name_, symbol_){
}
/// @notice Get the nonce of an NFT
/// Throws if `tokenId` is not a valid NFT
/// @param tokenId The NFT to get the nonce for
/// @return The nonce of this NFT
function nonce(uint256 tokenId) public virtual override view returns(uint256) {
require(_exists(tokenId), "Error: query for nonexistent token");
return _tokenNonce[tokenId];
}
function _beforeTokenTransfer(
address from,
address to,
uint256 tokenId
) internal virtual override{
super._beforeTokenTransfer(from, to, tokenId);
_tokenNonce[tokenId]++;
emit UpdateNonce(tokenId, tokenNonce[tokenId]);
}
/// @dev See {IERC165-supportsInterface}.
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
return interfaceId == type(IERC5008).interfaceId || super.supportsInterface(interfaceId);
}
}
Security Considerations
No security issues found.