ERC-6105 Intermediary-Free NFT Trading Protocol (No Intermediary NFT Trading Protocol)

Final · NFTs & multi-token · created 2022-12-02

A standard that builds listing and purchase functions into the NFT contract itself, so ERC-721 tokens can be bought and sold without an intermediary marketplace. Creators can implement a wider variety of royalty schemes.

Why it is needed

Most NFT trading today goes through intermediary platforms. To trade, you must use the setApprovalForAll function to give the platform authority over all of your NFTs. If the platform contract has a problem, you can suffer large losses, and phishing scams can abuse this authority to trick you into signing orders at low prices. As platforms multiply, liquidity fragments and you must repeat approvals and orders across several of them, which raises gas costs and risk, and platform fees are also a burden. Because order data lives off-chain, the efficiency of aggregators (services that collect and display listings from multiple platforms) depends on platform APIs. Whether royalties are enforced also depends on a platform's centralized decision. Some platforms made royalties optional without the consent of the projects, and delisting criteria are opaque, which leaves them open to censorship.

What to implement

In depth

Base interface: list, cancel, buy, query

listItem creates or updates a listing. The tokenId must exist, the caller must be the owner, an approved operator or an approved address, salePrice cannot be 0, and expires must be valid. It must emit an UpdateListing event. delistItem cancels the sale of a listed token and also emits an UpdateListing event. buyItem is the function through which the caller pays and receives the token. The salePrice and supportedToken passed in must match the listed values, and it emits a Purchased event when the trade settles. getListing returns (sale price, expiry time, payment token, benchmark price). If the price or the expiry is 0, the token is not for sale, and a payment token address of 0 means payment in ETH.

Design rationale for salePrice, expires and supportedToken

There are three reasons salePrice cannot be 0. Listing at a price of 0 is mostly an operating mistake and easily leads to loss of assets. Listing costs gas, so a price of 0 makes the proceeds negative, which makes no economic sense. Using a price of 0 as the marker for not for sale also simplifies implementations. With expires, a listing expires automatically, so the owner does not need to call delistItem separately, which saves gas. With supportedToken, the seller or the contract owner can choose the payment token to receive and is not tied to a single token. The zero address means ETH. The design rationale for the parameters of acceptCollectionOffer and acceptItemOffer is the same.

benchmarkPrice and varied royalty schemes

Adding a benchmarkPrice parameter to listItem, acceptCollectionOffer and acceptItemOffer lets implementations replace the _salePrice passed to ERC-2981's royaltyInfo(uint256 _tokenId, uint256 _salePrice) with a taxablePrice, which allows a variety of royalty schemes. The original text gives four examples. Value-added royalty (VAR) uses taxablePrice=max(salePrice-historicalPrice, 0) and charges royalties only on the seller's profit. Sales royalty (SR) uses taxablePrice=salePrice, capped royalty (CR) uses taxablePrice=min(salePrice, constant), and quota royalty (QR) uses taxablePrice=constant, which charges a fixed royalty on every trade.

Optional extensions: collection offers and item offers

Both extensions are optional. In the collection offer extension, a prospective buyer uses makeCollectionOffer to propose buying up to amount of any tokens in the collection at salePrice each. The caller must hold enough of the payment token and have approved enough for the contract. When a token owner or approved address accepts with acceptCollectionOffer, a Purchased event is emitted and the desired quantity amount decreases by 1. In the item offer extension, a proposal is placed on a specific tokenId with makeItemOffer and accepted with acceptItemOffer, and when it is filled the offer data is deleted. In both extensions, only the account that made the offer can call the cancel function, and the accept functions have an overload that takes benchmarkPrice.

How the reference implementation handles it

The reference implementation uses the value-added royalty (VAR) scheme. The Listing struct stores historicalPrice, the price at which the seller last bought this token, and _calculateRoyalties passes the difference to royaltyInfo only when salePrice is greater than historicalPrice. buyItem is protected by nonReentrant. For ETH payment, msg.value must equal salePrice. For ERC-20 payment, it checks that the allowance is at least salePrice, then uses transferFrom to split the payment between the royalty recipient and the seller, and transfers the token. In addition, _beforeTokenTransfer automatically deletes the listing when a listed token is transferred, which prevents a stale listing from remaining after the owner changes.

Blocklists are outside the standard

Some argue that tokens should be blocked from trading on intermediary markets that do not honor royalties, but this standard only provides intermediary-free trading and does not define a standard interface for blocking such markets. If the project team and community decide protection is needed, they can consider adding a blocklist to the implementing contract to block trades on platforms that do not follow the royalty policy. This standard has been finalized with Final status, is compatible with ERC-721 and ERC-2981, and assumes ERC-20, ERC-165, ERC-721 and ERC-2981.

Things to watch out for

Original ERC · ← ERCs