Skip to content

Commit

Permalink
Add EIP-6105: Marketplace extension for EIP-721 (ethereum#6105)
Browse files Browse the repository at this point in the history
* Proposal for ERC721 Marketplace extension

* Update code examples, Fix some linting issues

* Update and rename eip-erc721-marketplace-extension.md to eip-6105.md

* Update eip-6105.md

* Update eip-6105.md

* Commit necessary changes

* Update eip-6105.md

* Apply suggestions from code review

Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com>

* Update eip-6105.md

* Update interface and reference implementation

The rationale can only be updated once the interface and reference implementation have been identified

* Update eip-6105.md

* Update eip-6105.md

* Update eip-6105.md

Update Specification, Rationale and Reference Implementation.

* Update eip-6105.md

---------

Co-authored-by: 5660.eth <76733013+5660-eth@users.noreply.github.com>
Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com>
  • Loading branch information
3 people authored and iseriohn committed Feb 16, 2023
1 parent 2203fe1 commit f985249
Showing 1 changed file with 291 additions and 0 deletions.
291 changes: 291 additions & 0 deletions EIPS/eip-6105.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
---
eip: 6105
title: Marketplace Extension for EIP-721
description: Adds a basic marketplace functionality to EIP-721.
author: 5660-eth (@5660-eth), Silvere Heraudeau (@lambdalf-dev), Martin McConnell (@offgridgecko), Abu <team10kuni@gmail.com>, Wizard Wang
discussions-to: https://ethereum-magicians.org/t/eip6105-no-intermediary-nft-trading-protocol/12171
status: Draft
type: Standards Track
category: ERC
created: 2022-12-02
requires: 165, 721, 2981
---

## Abstract

Add a basic marketplace functionality to [EIP-721](./eip-721.md)
to enable non-fungible token trading without relying on an intermediary trading platform.


## Motivation

Most current NFT trading relies on an NFT trading platform acting as an intermediary, which has the following problems:

1. 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.
2. 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.
3. 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.
4. The project parties' copyright tax income is dependent on centralized decision-making by NFT trading platforms. Some trading platforms disregard the interests of project parties and implement zero copyright tax, which is a violation of their interests.
5. 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:

```solidity
interface IERC6105 {
/// @notice Emitted when a token is listed for sale or delisted.
/// @dev The zero price 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 to - address of who this listing is for,
/// can be address zero for a public listing,
/// or non zero address for a private listing
/// @param price - the price the token is being sold for
/// @param expires - UNIX timestamp, the buyer could buy the token before expires
event LogUpdateListing(uint256 indexed tokenId, address indexed from, address indexed to, uint256 price, uint64 expires);
/// @notice Emitted when a token that was listed for sale 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 price - the price the token is being sold for
event LogPurchased(uint256 indexed tokenId, address indexed from, address indexed to, uint256 price);
/// @notice Create or update a listing for `tokenId`
/// Setting `buyer` to the NULL address will create a public listing
/// `price` MUST NOT be set to zero
/// @param tokenId - identifier of the token being listed
/// @param price - the price the token is being sold for
/// @param expires - UNIX timestamp, the buyer could buy the token before expires
/// @param to - optional address of who this listing is for,
/// can be address zero for a public listing,
/// or non zero address for a private listing
/// Requirements:
/// - `tokenId` must exist
/// - Caller must be owner, authorised operators or approved address of the token
/// - `price` must not be zero
/// - Must emit a {LogUpdateListing} event.
function listItem(uint256 tokenId, uint256 price, uint64 expires, address to) external;
/// @notice Removes 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 a {LogUpdateListing} event
function delistItem(uint256 tokenId) external;
/// @notice Purchases the listed token `tokenId`
/// @param tokenId - identifier of the token being purchased
/// Requirements:
/// - `tokenId` must exist and be listed for sale
/// - Caller must be able to pay the listed price for `tokenId`
/// - Must emit a {LogPurchased} event.
function buyItem(uint256 tokenId) external payable;
/// @notice Returns the listing for `tokenId`
/// @dev The zero price indicates that the token is not for sale
/// The zero expires indicates that the token is not for sale
/// The zero address indicates that the token is for a public listing
/// @param tokenId - identifier of the token whose listing is being queried
/// @return the specified listing (price, expires, intended recipient)
function getListing(uint256 tokenId) external view returns (uint256, uint64, address);
}
```

The `listItem(uint256 tokenId, uint256 price, uint64 expires, address to)` function MAY be implemented as `public` or `external`.And the `price` in this function MUST NOT be set to zero.

The `delistItem(uint256 tokenId)` function MAY be implemented as `public` or `external`.

The `buyItem(uint256 tokenId)` function MUST be implemented as `payable` and MAY be implemented as `public` or `external`.

The `getListing(uint256 tokenId)` function MAY be implemented as `pure` or `view`.

The `LogUpdateListing` event MUST be emitted when a token is listed for sale or delisted.

The `LogPurchased` event MUST be emitted when a token is traded.

The `supportsInterface` method MUST return `true` when called with `0x6de8e04d`.

## Rationale

Out of consideration for the safety and efficiency of buyer' assets, it does not provide bidding functions and auction functions, but only adds listing funcitons.

The `price` 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.

## Backwards Compatibility

This standard is compatible with [EIP-721](./eip-721.md) and [EIP-2981](./eip-2981.md).

## Reference Implementation

```solidity
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.8;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/common/ERC2981.sol";
import "./IERC6105.sol";
contract ERC6105 is ERC721, ERC2981, IERC6105 {
/// @dev A structure representing a listed token
/// The zero price indicates that the token is not for sale
/// The zero expires indicates that the token is not for sale
/// @param price - the price the token is being sold for
/// @param expires - UNIX timestamp, the buyer could buy the token before expires
/// @param to - address of who this listing is for,
/// can be address zero for a public listing,
/// or non zero address for a private listing
struct Listing {
uint256 price;
uint64 expires;
address to;
}
// Mapping from token Id to listing index
mapping(uint256 => Listing) private _listings;
constructor(string memory name_, string memory symbol_)
ERC721(name_, symbol_)
{
}
/// @notice Create or update a listing for `tokenId`
/// Setting `buyer` to the NULL address will create a public listing
/// `price` MUST NOT be set to zero
/// @param tokenId - identifier of the token being listed
/// @param price - the price the token is being sold for
/// @param expires - UNIX timestamp, the buyer could buy the token before expires
/// @param to - optional address of who this listing is for,
/// can be address zero for a public listing,
/// or non zero address for a private listing
function listItem (uint256 tokenId, uint256 price, uint64 expires, address to) external virtual {
address tokenOwner = ownerOf(tokenId);
require(price > 0,"ERC6105: token sale price MUST NOT be set to zero");
require(_isApprovedOrOwner(_msgSender(), tokenId),"ERC6105: caller is not owner nor approved");
_listItem(tokenId, price, tokenOwner, expires, to);
}
/// @notice Removes the listing for `tokenId`
/// @param tokenId - identifier of the token being delisted
function delistItem(uint256 tokenId) external virtual {
require(_isApprovedOrOwner(_msgSender(), tokenId),"ERC6105: caller is not owner nor approved");
require(_isForSale(tokenId), "ERC6105: invalid listing" );
_removeListing(tokenId);
}
/// @notice Purchases the listed token `tokenId`
/// @param tokenId - identifier of the token being purchased
function buyItem(uint256 tokenId) external virtual payable {
address tokenOwner = ownerOf(tokenId);
address buyer = msg.sender;
uint256 value = msg.value;
uint256 price = _listings[tokenId].price;
require(_isForSale(tokenId), "ERC6105: invalid listing");
require(
buyer == _listings[tokenId].to ||
_listings[tokenId].to == address(0),
"ERC6105: invalid sale address"
);
require(value == price, "ERC6105: incorrect price");
_transfer(tokenOwner, buyer, tokenId);
emit LogPurchased(tokenId, tokenOwner, buyer, price);
/// @dev Handle royalties
(address royaltyRecipient, uint256 royalties) = royaltyInfo(tokenId, msg.value);
uint256 payment = msg.value - royalties;
_processEthPayment(royalties, royaltyRecipient);
_processEthPayment(payment, tokenOwner);
}
/// @notice Returns the listing for `tokenId`
/// @dev The zero price indicates that the token is not for sale
/// The zero expires indicates that the token is not for sale
/// The zero address indicates that the token is for a public listing
/// @param tokenId - identifier of the token whose listing is being queried
/// @return the specified listing (price, expires, intended recipient)
function getListing(uint256 tokenId) external view virtual returns (uint256, uint64, address) {
uint256 price = _listings[tokenId].price;
uint64 expires = _listings[tokenId].expires;
address to = _listings[tokenId].to;
return (price, expires, to);
}
///@dev check if the token `tokenId` is for sale
function _isForSale(uint256 tokenId) internal virtual returns(bool){
if(_listings[tokenId].price > 0 && _listings[tokenId].expires >= block.timestamp){
return true;
}
else{
return false;
}
}
/// @dev Create or update a listing for `tokenId`
/// Setting `buyer` to the NULL address will create a public listing
/// `price` MUST NOT be set to zero
/// @param tokenId - identifier of the token being listed
/// @param price - the price the token is being sold for
/// @param tokenOwner - current owner of the token
/// @param expires - UNIX timestamp, the buyer could buy the token before expires
/// @param to - optional address of who this listing is for,
/// can be address zero for a public listing,
/// or non zero address for a private listing
function _listItem(uint256 tokenId, uint256 price, address tokenOwner, uint64 expires, address to) internal virtual {
_listings[tokenId].price = price;
_listings[tokenId].expires = expires;
_listings[tokenId].to = to;
emit LogUpdateListing(tokenId, tokenOwner, to, price, expires);
}
/// @dev Removes the listing for `tokenId`
/// @param tokenId - identifier of the token being delisted
function _removeListing(uint256 tokenId) internal virtual {
address tokenOwner = ownerOf(tokenId);
delete _listings[tokenId];
emit LogUpdateListing(tokenId, tokenOwner, address(0), 0, 0);
}
/// @dev Processes an ether of `amount` payment to `recipient`.
/// @param amount - the amount to send
/// @param recipient - the payment recipient
function _processEthPayment(uint256 amount, address recipient) internal virtual {
(bool success,) = payable(recipient).call{value: amount}("");
require(success, "Ether Transfer Fail");
}
/// @dev See {IERC165-supportsInterface}.
function supportsInterface(bytes4 interfaceId) public view virtual override (ERC721, ERC2981) returns (bool) {
return interfaceId == type(IERC6105).interfaceId || super.supportsInterface(interfaceId);
}
function _beforeTokenTransfer(address from, address to, uint256 tokenId, uint256 batchSize) internal virtual override{
super._beforeTokenTransfer(from, to, tokenId, batchSize);
if(_isForSale(tokenId)){
delete _listings[tokenId];
emit LogUpdateListing(tokenId, to, address(0), 0, 0);
}
}
}
```

## Security Considerations

Needs discussion.

## Copyright

Copyright and related rights waived via [CC0](../LICENSE.md).

0 comments on commit f985249

Please sign in to comment.