This ERC adds a marketplace functionality to ERC-721 to enable non-fungible token trading without relying on an intermediary trading platform. At the same time, creators may implement more diverse royalty schemes.
Motivation
Most current NFT trading relies on an NFT trading platform acting as an intermediary, which has the following problems:
Security concerns arise from authorization via the setApprovalForAll function. The permissions granted to NFT trading platforms expose unnecessary risks. Should a problem occur with the trading platform contract, it would result in significant losses to the industry as a whole. Additionally, if a user has authorized the trading platform to handle their NFTs, it allows a phishing scam to trick the user into signing a message that allows the scammer to place an order at a low price on the NFT trading platform and designate themselves as the recipient. This can be difficult for ordinary users to guard against.
High trading costs are a significant issue. On one hand, as the number of trading platforms increases, the liquidity of NFTs becomes dispersed. If a user needs to make a deal quickly, they must authorize and place orders on multiple platforms, which increases the risk exposure and requires additional gas expenditures for each authorization. For example, taking BAYC as an example, with a total supply of 10,000 and over 6,000 current holders, the average number of BAYC held by each holder is less than 2. While setApprovalForAll saves on gas expenditure for pending orders on a single platform, authorizing multiple platforms results in an overall increase in gas expenditures for users. On the other hand, trading service fees charged by trading platforms must also be considered as a cost of trading, which are often much higher than the required gas expenditures for authorization.
Aggregators provide a solution by aggregating liquidity, but the decision-making process is centralized. Furthermore, as order information on trading platforms is off-chain, the aggregator’s efficiency in obtaining data is affected by the frequency of the trading platform’s API and, at times, trading platforms may suspend the distribution of APIs and limit their frequency.
The project parties’ royalty income is dependent on centralized decision-making by NFT trading platforms. Some trading platforms implement optional royalty without the consent of project parties, which is a violation of their interests.
NFT trading platforms are not resistant to censorship. Some platforms have delisted a number of NFTs and the formulation and implementation of delisting rules are centralized and not transparent enough. In the past, some NFT trading platforms have failed and wrongly delisted certain NFTs, leading to market panic.
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.
Compliant contracts MUST implement the following interface:
interfaceIERC6105{/// @notice Emitted when a token is listed for sale or delisted
/// @dev The zero `salePrice` indicates that the token is not for sale
/// The zero `expires` indicates that the token is not for sale
/// @param tokenId - identifier of the token being listed
/// @param from - address of who is selling the token
/// @param salePrice - the price the token is being sold for
/// @param expires - UNIX timestamp, the buyer could buy the token before expires
/// @param supportedToken - contract addresses of supported token or zero address
/// The zero address indicates that the supported token is ETH
/// Buyer needs to purchase item with supported token
/// @param benchmarkPrice - Additional price parameter, may be used when calculating royalties
eventUpdateListing(uint256indexedtokenId,addressindexedfrom,uint256salePrice,uint64expires,addresssupportedToken,uint256benchmarkPrice);/// @notice Emitted when a token is being purchased
/// @param tokenId - identifier of the token being purchased
/// @param from - address of who is selling the token
/// @param to - address of who is buying the token
/// @param salePrice - the price the token is being sold for
/// @param supportedToken - contract addresses of supported token or zero address
/// The zero address indicates that the supported token is ETH
/// Buyer needs to purchase item with supported token
/// @param royalties - The amount of royalties paid on this purchase
eventPurchased(uint256indexedtokenId,addressindexedfrom,addressindexedto,uint256salePrice,addresssupportedToken,uint256royalties);/// @notice Create or update a listing for `tokenId`
/// @dev `salePrice` MUST NOT be set to zero
/// @param tokenId - identifier of the token being listed
/// @param salePrice - the price the token is being sold for
/// @param expires - UNIX timestamp, the buyer could buy the token before expires
/// @param supportedToken - contract addresses of supported token or zero address
/// The zero address indicates that the supported token is ETH
/// Buyer needs to purchase item with supported token
/// Requirements:
/// - `tokenId` must exist
/// - Caller must be owner, authorised operators or approved address of the token
/// - `salePrice` must not be zero
/// - `expires` must be valid
/// - Must emit an {UpdateListing} event.
functionlistItem(uint256tokenId,uint256salePrice,uint64expires,addresssupportedToken)external;/// @notice Create or update a listing for `tokenId` with `benchmarkPrice`
/// @dev `salePrice` MUST NOT be set to zero
/// @param tokenId - identifier of the token being listed
/// @param salePrice - the price the token is being sold for
/// @param expires - UNIX timestamp, the buyer could buy the token before expires
/// @param supportedToken - contract addresses of supported token or zero address
/// The zero address indicates that the supported token is ETH
/// Buyer needs to purchase item with supported token
/// @param benchmarkPrice - Additional price parameter, may be used when calculating royalties
/// Requirements:
/// - `tokenId` must exist
/// - Caller must be owner, authorised operators or approved address of the token
/// - `salePrice` must not be zero
/// - `expires` must be valid
/// - Must emit an {UpdateListing} event.
functionlistItem(uint256tokenId,uint256salePrice,uint64expires,addresssupportedToken,uint256benchmarkPrice)external;/// @notice Remove the listing for `tokenId`
/// @param tokenId - identifier of the token being delisted
/// Requirements:
/// - `tokenId` must exist and be listed for sale
/// - Caller must be owner, authorised operators or approved address of the token
/// - Must emit an {UpdateListing} event
functiondelistItem(uint256tokenId)external;/// @notice Buy a token and transfer it to the caller
/// @dev `salePrice` and `supportedToken` must match the expected purchase price and token to prevent front-running attacks
/// @param tokenId - identifier of the token being purchased
/// @param salePrice - the price the token is being sold for
/// @param supportedToken - contract addresses of supported token or zero address
/// Requirements:
/// - `tokenId` must exist and be listed for sale
/// - `salePrice` must matches the expected purchase price to prevent front-running attacks
/// - `supportedToken` must matches the expected purchase token to prevent front-running attacks
/// - Caller must be able to pay the listed price for `tokenId`
/// - Must emit a {Purchased} event
functionbuyItem(uint256tokenId,uint256salePrice,addresssupportedToken)externalpayable;/// @notice Return the listing for `tokenId`
/// @dev The zero sale price indicates that the token is not for sale
/// The zero expires indicates that the token is not for sale
/// The zero supported token address indicates that the supported token is ETH
/// @param tokenId identifier of the token being queried
/// @return the specified listing (sale price, expires, supported token, benchmark price)
functiongetListing(uint256tokenId)externalviewreturns(uint256,uint64,address,uint256);}
Optional collection offer extention
/// The collection offer extension is OPTIONAL for ERC-6105 smart contracts. This allows smart contract to support collection offer functionality.
interfaceIERC6105CollectionOffer{/// @notice Emitted when the collection receives an offer or an offer is canceled
/// @dev The zero `salePrice` indicates that the collection offer of the token is canceled
/// The zero `expires` indicates that the collection offer of the token is canceled
/// @param from - address of who make collection offer
/// @param amount - the amount the offerer wants to buy at `salePrice` per token
/// @param salePrice - the price of each token is being offered for the collection
/// @param expires - UNIX timestamp, the offer could be accepted before expires
/// @param supportedToken - contract addresses of supported ERC20 token
/// Buyer wants to purchase items with supported token
eventUpdateCollectionOffer(addressindexedfrom,uint256amount,uint256salePrice,uint64expires,addresssupportedToken);/// @notice Create or update an offer for the collection
/// @dev `salePrice` MUST NOT be set to zero
/// @param amount - the amount the offerer wants to buy at `salePrice` per token
/// @param salePrice - the price of each token is being offered for the collection
/// @param expires - UNIX timestamp, the offer could be accepted before expires
/// @param supportedToken - contract addresses of supported token
/// Buyer wants to purchase items with supported token
/// Requirements:
/// - The caller must have enough supported tokens, and has approved the contract a sufficient amount
/// - `salePrice` must not be zero
/// - `amount` must not be zero
/// - `expires` must be valid
/// - Must emit an {UpdateCollectionOffer} event
functionmakeCollectionOffer(uint256amount,uint256salePrice,uint64expires,addresssupportedToken)external;/// @notice Accepts collection offer and transfers the token to the buyer
/// @dev `salePrice` and `supportedToken` must match the expected purchase price and token to prevent front-running attacks
/// When the trading is completed, the `amount` of NFTs the buyer wants to purchase needs to be reduced by 1
/// @param tokenId - identifier of the token being offered
/// @param salePrice - the price the token is being offered for
/// @param supportedToken - contract addresses of supported token
/// @param buyer - address of who wants to buy the token
/// Requirements:
/// - `tokenId` must exist and and be offered for
/// - Caller must be owner, authorised operators or approved address of the token
/// - Must emit a {Purchased} event
functionacceptCollectionOffer(uint256tokenId,uint256salePrice,addresssupportedToken,addressbuyer)external;/// @notice Accepts collection offer and transfers the token to the buyer
/// @dev `salePrice` and `supportedToken` must match the expected purchase price and token to prevent front-running attacks
/// When the trading is completed, the `amount` of NFTs the buyer wants to purchase needs to be reduced by 1
/// @param tokenId - identifier of the token being offered
/// @param salePrice - the price the token is being offered for
/// @param supportedToken - contract addresses of supported token
/// @param buyer - address of who wants to buy the token
/// @param benchmarkPrice - additional price parameter, may be used when calculating royalties
/// Requirements:
/// - `tokenId` must exist and and be offered for
/// - Caller must be owner, authorised operators or approved address of the token
/// - Must emit a {Purchased} event
functionacceptCollectionOffer(uint256tokenId,uint256salePrice,addresssupportedToken,addressbuyer,uint256benchmarkPrice)external;/// @notice Removes the offer for the collection
/// Requirements:
/// - Caller must be the offerer
/// - Must emit an {UpdateCollectionOffer} event
functioncancelCollectionOffer()external;/// @notice Returns the offer for `tokenId` maked by `buyer`
/// @dev The zero amount indicates there is no offer
/// The zero sale price indicates there is no offer
/// The zero expires indicates that there is no offer
/// @param buyer address of who wants to buy the token
/// @return the specified offer (amount, sale price, expires, supported token)
functiongetCollectionOffer(addressbuyer)externalviewreturns(uint256,uint256,uint64,address);}
Optional item offer extention
/// The item offer extension is OPTIONAL for ERC-6105 smart contracts. This allows smart contract to support item offer functionality.
interfaceIERC6105ItemOffer{/// @notice Emitted when a token receives an offer or an offer is canceled
/// @dev The zero `salePrice` indicates that the offer of the token is canceled
/// The zero `expires` indicates that the offer of the token is canceled
/// @param tokenId - identifier of the token being offered
/// @param from - address of who wants to buy the token
/// @param salePrice - the price the token is being offered for
/// @param expires - UNIX timestamp, the offer could be accepted before expires
/// @param supportedToken - contract addresses of supported token
/// Buyer wants to purchase item with supported token
eventUpdateItemOffer(uint256indexedtokenId,addressindexedfrom,uint256salePrice,uint64expires,addresssupportedToken);/// @notice Create or update an offer for `tokenId`
/// @dev `salePrice` MUST NOT be set to zero
/// @param tokenId - identifier of the token being offered
/// @param salePrice - the price the token is being offered for
/// @param expires - UNIX timestamp, the offer could be accepted before expires
/// @param supportedToken - contract addresses of supported token
/// Buyer wants to purchase item with supported token
/// Requirements:
/// - `tokenId` must exist
/// - The caller must have enough supported tokens, and has approved the contract a sufficient amount
/// - `salePrice` must not be zero
/// - `expires` must be valid
/// - Must emit an {UpdateItemOffer} event.
functionmakeItemOffer(uint256tokenId,uint256salePrice,uint64expires,addresssupportedToken)external;/// @notice Remove the offer for `tokenId`
/// @param tokenId - identifier of the token being canceled offer
/// Requirements:
/// - `tokenId` must exist and be offered for
/// - Caller must be the offerer
/// - Must emit an {UpdateItemOffer} event
functioncancelItemOffer(uint256tokenId)external;/// @notice Accept offer and transfer the token to the buyer
/// @dev `salePrice` and `supportedToken` must match the expected purchase price and token to prevent front-running attacks
/// When the trading is completed, the offer infomation needs to be removed
/// @param tokenId - identifier of the token being offered
/// @param salePrice - the price the token is being offered for
/// @param supportedToken - contract addresses of supported token
/// @param buyer - address of who wants to buy the token
/// Requirements:
/// - `tokenId` must exist and be offered for
/// - Caller must be owner, authorised operators or approved address of the token
/// - Must emit a {Purchased} event
functionacceptItemOffer(uint256tokenId,uint256salePrice,addresssupportedToken,addressbuyer)external;/// @notice Accepts offer and transfers the token to the buyer
/// @dev `salePrice` and `supportedToken` must match the expected purchase price and token to prevent front-running attacks
/// When the trading is completed, the offer infomation needs to be removed
/// @param tokenId - identifier of the token being offered
/// @param salePrice - the price the token is being offered for
/// @param supportedToken - contract addresses of supported token
/// @param buyer - address of who wants to buy the token
/// @param benchmarkPrice - additional price parameter, may be used when calculating royalties
/// Requirements:
/// - `tokenId` must exist and be offered for
/// - Caller must be owner, authorised operators or approved address of the token
/// - Must emit a {Purchased} event
functionacceptItemOffer(uint256tokenId,uint256salePrice,addresssupportedToken,addressbuyer,uint256benchmarkPrice)external;/// @notice Return the offer for `tokenId` maked by `buyer`
/// @dev The zero sale price indicates there is no offer
/// The zero expires indicates that there is no offer
/// @param tokenId identifier of the token being queried
/// @param buyer address of who wants to buy the token
/// @return the specified offer (sale price, expires, supported token)
functiongetItemOffer(uint256tokenId,addressbuyer)externalviewreturns(uint256,uint64,address);}
Rationale
Considerations for some local variables
The salePrice in the listItem function cannot be set to zero. Firstly, it is a rare occurrence for a caller to set the price to 0, and when it happens, it is often due to an operational error which can result in loss of assets. Secondly, a caller needs to spend gas to call this function, so if he can set the token price to 0, his income would be actually negative at this time, which does not conform to the concept of ‘economic man’ in economics. Additionally, a token price of 0 indicates that the item is not for sale, making the reference implementation more concise.
Setting expires in the listItem function allows callers to better manage their listings. If a listing expires automatically, the token owner will no longer need to manually delistItem, thus saving gas.
Setting supportedToken in the listItem function allows the caller or contract owner to choose which tokens they want to accept, rather than being limited to a single token.
The rationales of variable setting in the acceptCollectionOffer and acceptItemOffer functions are the same as described above.
More diverse royalty schemes
By introducing the parameter benchmarkPrice in the listItem, acceptCollectionOffer and acceptItemOffer functions, the _salePrice in the royaltyInfo(uint256 _tokenId, uint256 _salePrice) function in the ERC-2981 interface can be changed to taxablePrice, making the ERC-2981 royalty scheme more diverse. Here are several examples of royalty schemes:
Quantitative Royalty(QR, each token trading pays a fixed royalties): taxablePrice= constant
Optional Blocklist
Some viewpoints suggest that tokens should be prevented from trading on intermediary markets that do not comply with royalty schemes, but this standard only provides a functionality for non-intermediary NFT trading and does not offer a standardized interface to prevent tokens from trading on these markets. If deemed necessary to better protect the interests of the project team and community, they may consider adding a blocklist to their implementation contracts to prevent NFTs from being traded on platforms that do not comply with the project’s royalty scheme.
// SPDX-License-Identifier: CC0-1.0
pragmasolidity^0.8.8;import"@openzeppelin/contracts/token/ERC721/ERC721.sol";import"@openzeppelin/contracts/token/common/ERC2981.sol";import"@openzeppelin/contracts/token/ERC20/IERC20.sol";import"@openzeppelin/contracts/security/ReentrancyGuard.sol";import"./IERC6105.sol";/// @title No Intermediary NFT Trading Protocol with Value-added Royalty
/// @dev The royalty scheme used by this reference implementation is Value-Added Royalty
contractERC6105isERC721,ERC2981,IERC6105,ReentrancyGuard{/// @dev A structure representing a listed token
/// The zero `salePrice` indicates that the token is not for sale
/// The zero `expires` indicates that the token is not for sale
/// @param salePrice - the price the token is being sold for
/// @param expires - UNIX timestamp, the buyer could buy the token before expires
/// @param supportedToken - contract addresses of supported ERC20 token or zero address
/// The zero address indicates that the supported token is ETH
/// Buyer needs to purchase item with supported token
/// @param historicalPrice - The price at which the seller last bought this token
structListing{uint256salePrice;uint64expires;addresssupportedToken;uint256historicalPrice;}// Mapping from token Id to listing index
mapping(uint256=>Listing)private_listings;constructor(stringmemoryname_,stringmemorysymbol_)ERC721(name_,symbol_){}/// @notice Create or update a listing for `tokenId`
/// @dev `salePrice` MUST NOT be set to zero
/// @param tokenId - identifier of the token being listed
/// @param salePrice - the price the token is being sold for
/// @param expires - UNIX timestamp, the buyer could buy the token before expires
/// @param supportedToken - contract addresses of supported ERC20 token or zero address
/// The zero address indicates that the supported token is ETH
/// Buyer needs to purchase item with supported token
functionlistItem(uint256tokenId,uint256salePrice,uint64expires,addresssupportedToken)externalvirtual{listItem(tokenId,salePrice,expires,supportedToken,0);}/// @notice Create or update a listing for `tokenId` with `historicalPrice`
/// @dev `price` MUST NOT be set to zero
/// @param tokenId - identifier of the token being listed
/// @param salePrice - the price the token is being sold for
/// @param expires - UNIX timestamp, the buyer could buy the token before expires
/// @param supportedToken - contract addresses of supported ERC20 token or zero address
/// The zero address indicates that the supported token is ETH
/// Buyer needs to purchase item with supported token
/// @param historicalPrice - The price at which the seller last bought this token
functionlistItem(uint256tokenId,uint256salePrice,uint64expires,addresssupportedToken,uint256historicalPrice)publicvirtual{addresstokenOwner=ownerOf(tokenId);require(salePrice>0,"ERC6105: token sale price MUST NOT be set to zero");require(expires>block.timestamp,"ERC6105: invalid expires");require(_isApprovedOrOwner(_msgSender(),tokenId),"ERC6105: caller is not owner nor approved");_listings[tokenId]=Listing(salePrice,expires,supportedToken,historicalPrice);emitUpdateListing(tokenId,tokenOwner,salePrice,expires,supportedToken,historicalPrice);}/// @notice Remove the listing for `tokenId`
/// @param tokenId - identifier of the token being listed
functiondelistItem(uint256tokenId)externalvirtual{require(_isApprovedOrOwner(_msgSender(),tokenId),"ERC6105: caller is not owner nor approved");require(_isForSale(tokenId),"ERC6105: invalid listing");_removeListing(tokenId);}/// @notice Buy a token and transfers it to the caller
/// @dev `salePrice` and `supportedToken` must match the expected purchase price and token to prevent front-running attacks
/// @param tokenId - identifier of the token being purchased
/// @param salePrice - the price the token is being sold for
/// @param supportedToken - contract addresses of supported token or zero address
functionbuyItem(uint256tokenId,uint256salePrice,addresssupportedToken)externalnonReentrantpayablevirtual{addresstokenOwner=ownerOf(tokenId);addressbuyer=msg.sender;uint256historicalPrice=_listings[tokenId].historicalPrice;require(salePrice==_listings[tokenId].salePrice,"ERC6105: inconsistent prices");require(supportedToken==_listings[tokenId].supportedToken,"ERC6105: inconsistent tokens");require(_isForSale(tokenId),"ERC6105: invalid listing");/// @dev Handle royalties
(addressroyaltyRecipient,uint256royalties)=_calculateRoyalties(tokenId,salePrice,historicalPrice);uint256payment=salePrice-royalties;if(supportedToken==address(0)){require(msg.value==salePrice,"ERC6105: incorrect value");_processSupportedTokenPayment(royalties,buyer,royaltyRecipient,address(0));_processSupportedTokenPayment(payment,buyer,tokenOwner,address(0));}else{uint256num=IERC20(supportedToken).allowance(buyer,address(this));require(num>=salePrice,"ERC6105: insufficient allowance");_processSupportedTokenPayment(royalties,buyer,royaltyRecipient,supportedToken);_processSupportedTokenPayment(payment,buyer,tokenOwner,supportedToken);}_transfer(tokenOwner,buyer,tokenId);emitPurchased(tokenId,tokenOwner,buyer,salePrice,supportedToken,royalties);}/// @notice Return the listing for `tokenId`
/// @dev The zero sale price indicates that the token is not for sale
/// The zero expires indicates that the token is not for sale
/// The zero supported token address indicates that the supported token is ETH
/// @param tokenId identifier of the token being queried
/// @return the specified listing (sale price, expires, supported token, benchmark price)
functiongetListing(uint256tokenId)externalviewvirtualreturns(uint256,uint64,address,uint256){if(_listings[tokenId].salePrice>0&&_listings[tokenId].expires>=block.timestamp){uint256salePrice=_listings[tokenId].salePrice;uint64expires=_listings[tokenId].expires;addresssupportedToken=_listings[tokenId].supportedToken;uint256historicalPrice=_listings[tokenId].historicalPrice;return(salePrice,expires,supportedToken,historicalPrice);}else{return(0,0,address(0),0);}}/// @dev Remove the listing for `tokenId`
/// @param tokenId - identifier of the token being delisted
function_removeListing(uint256tokenId)internalvirtual{addresstokenOwner=ownerOf(tokenId);delete_listings[tokenId];emitUpdateListing(tokenId,tokenOwner,0,0,address(0),0);}/// @dev Check if the token is for sale
function_isForSale(uint256tokenId)internalvirtualreturns(bool){if(_listings[tokenId].salePrice>0&&_listings[tokenId].expires>=block.timestamp){returntrue;}else{returnfalse;}}/// @dev Handle Value Added Royalty
function_calculateRoyalties(uint256tokenId,uint256price,uint256historicalPrice)internalvirtualreturns(address,uint256){uint256taxablePrice;if(price>historicalPrice){taxablePrice=price-historicalPrice;}else{taxablePrice=0;}(addressroyaltyRecipient,uint256royalties)=royaltyInfo(tokenId,taxablePrice);return(royaltyRecipient,royalties);}/// @dev Process a `supportedToken` of `amount` payment to `recipient`.
/// @param amount - the amount to send
/// @param from - the payment payer
/// @param recipient - the payment recipient
/// @param supportedToken - contract addresses of supported ERC20 token or zero address
/// The zero address indicates that the supported token is ETH
function_processSupportedTokenPayment(uint256amount,addressfrom,addressrecipient,addresssupportedToken)internalvirtual{if(supportedToken==address(0)){(boolsuccess,)=payable(recipient).call{value:amount}("");require(success,"Ether Transfer Fail");}else{(boolsuccess)=IERC20(supportedToken).transferFrom(from,recipient,amount);require(success,"Supported Token Transfer Fail");}}/// @dev See {IERC165-supportsInterface}.
functionsupportsInterface(bytes4interfaceId)publicviewvirtualoverride(ERC721,ERC2981)returns(bool){returninterfaceId==type(IERC6105).interfaceId||super.supportsInterface(interfaceId);}/// @dev Before transferring the NFT, need to delete listing
function_beforeTokenTransfer(addressfrom,addressto,uint256tokenId,uint256batchSize)internalvirtualoverride{super._beforeTokenTransfer(from,to,tokenId,batchSize);if(_isForSale(tokenId)){_removeListing(tokenId);}}}
Security Considerations
The buyItem function, as well as the acceptCollectionOffer and acceptItemOffer functions, has a potential front-running risk. Must check that salePrice and supportedToken match the expected price and token to prevent front-running attacks
There is a potential re-entrancy risk with the acceptCollectionOffer and acceptItemOffer functions. Make sure to obey the checks, effects, interactions pattern or use a reentrancy guard.
If a buyer uses ERC-20 tokens to purchase an NFT, the buyer needs to first call the approve(address spender, uint256 amount) function of the ERC-20 token to grant the NFT contract access to a certain amount of tokens. Please make sure to authorize an appropriate amount. Furthermore, caution is advised when dealing with non-audited contracts.