Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add EIP-6105: Marketplace extension for EIP-721 #6105

Merged
merged 17 commits into from
Feb 12, 2023
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
308 changes: 308 additions & 0 deletions EIPS/eip-erc721-marketplace-extension.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,308 @@
---
eip: <to be assigned>
title: Marketplace extension for ERC-721
description: We propose to add a basic marketplace functionality to the ERC721 standard.
author: Silvere Heraudeau (@lambdalf-dev), Martin McConnell (@offgridgecko)
discussions-to: https://ethereum-magicians.org/t/idea-a-marketplace-extension-to-erc721-standard/11975
status: Draft
type: Standards Track
category: ERC
created: 2022-12-02
requires: 721
---

## Simple Summary

"Not your marketplace, not your royalties"

## Abstract

We propose to add a basic marketplace functionality to the EIP-721[./eip-721.md] standard to allow project creators to gain back control of the distribution of their NFTs.

It includes:
- a method to list an item for sale or update an existing listing, whether private sale (only to a specific address) or public (to anyone),
- a method to delist an item that has previously been listed,
- a method to purchase a listed item,
- a method to view all items listed for sale, and
- a method to view a specific listing.

## Motivation

OpenSea’s latest code snippet gives them the ability to entirely control which platform your NFTs can (or cannot) be traded on. Our goal is to give that control back to the project creators.

## 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.

Implementers of this standard **MUST** have all of the following functions:

```solidity
pragma solidity ^0.8.0;
import './IERC721.sol'

///
/// @dev Interface for the ERC721 Marketplace Extension
///
interface IERC721MarketplaceExtension {
///
/// @dev A structure representing a listed token
///
/// @param tokenId - the NFT asset being listed
/// @param tokenPrice - the price the token is being sold for, regardless of currency
/// @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 tokenId;
uint256 tokenPrice;
address to;
}
///
/// @dev Emitted when a token is listed for sale.
///
/// @param tokenId - the NFT asset 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, regardless of currency
///
event Listed( uint256 indexed tokenId, address indexed from, address to, uint256 indexed price );
///
/// @dev Emitted when a token that was listed for sale is being delisted
///
/// @param tokenId - the NFT asset being delisted
///
event Delisted( uint256 indexed tokenId );
///
/// @dev Emitted when a token that was listed for sale is being purchased.
///
/// @param tokenId - the NFT asset 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, regardless of currency
///
event Purchased( uint256 indexed tokenId, address indexed from, address indexed to, uint256 price );
///
/// @dev Lists token `tokenId` for sale.
///
/// @param tokenId - the NFT asset being listed
/// @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, regardless of currency
///
/// Requirements:
/// - `tokenId` must exist
/// - Caller must own `tokenId`
/// - Must emit a {Listed} event.
///
function listItem( uint256 tokenId, uint256 price, address to ) external;
///
/// @dev Delists token `tokenId` that was listed for sale
///
/// @param tokenId - the NFT asset being delisted
///
/// Requirements:
/// - `tokenId` must exist and be listed for sale
/// - Caller must own `tokenId`
/// - Must emit a {Delisted} event.
///
function delistItem( uint256 tokenId ) external;
///
/// @dev Buys a token and transfers it to the caller.
///
/// @param tokenId - the NFT asset 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 {Purchased} event.
///
function buyItem( uint256 tokenId ) external payable;
///
/// @dev Returns a list of all current listings.
///
/// @return the list of all currently listed tokens,
/// along with their price and intended recipient
///
function getAllListings() external view returns ( Listing[] memory );
///
/// @dev Returns the listing for `tokenId`
///
/// @return the specified listing (tokenId, price, intended recipient)
///
function getListing( uint256 tokenId ) external view returns ( Listing memory );
}
```

## Rationale

## Backwards Compatibility

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

## Reference Implementation

```solidity
pragma solidity ^0.8.0;
import './ERC721.sol';
import './ERC2981.sol';

contract Example is ERC721, ERC2981 {
struct Listing{
uint256 tokenid;
uint256 tokenprice;
address to;
}
event Listed( uint256 indexed tokenId, address indexed from, address to, uint256 indexed price );
event Delisted( uint256 indexed tokenId );
event Purchased( uint256 indexed tokenId, address indexed from, address indexed to, uint256 price );

// list of all sale items by tokenId
uint256[] private saleItems;

// mappings are tied to tokenId
mapping(uint256 => uint256) private tokenPrice;
mapping(uint256 => bool) private isForSale;
mapping(uint256 => address) private privateRecipient;

// listing index location of each listed token
mapping(uint256 => uint256) private listId;

uint256 private itemsForSale;

// INTERNAL
function _listItem( uint256 tokenId, uint256 price ) internal {
if ( isForSale[ tokenId ] ) {
tokenPrice[ tokenId ] = price;
}
else {
tokenPrice[ tokenId ] = price;
isForSale[ tokenId ] = true;
saleItems.push( tokenId );
unchecked {
++itemsForSale;
}
}
}

function _delistItem( uint256 tokenId ) internal {
delete tokenPrice[ tokenId ];
delete isForSale[ tokenId ];
delete privateRecipient[ tokenId ];
unchecked {
--itemsForSale;
}

//update token list
uint256 tempTokenId = saleItems[ itemsForSale ];
if ( tempTokenId == tokenId ) {
saleItems.pop();
}
else {
//record the listId of the token we want to get rid of
uint256 tempListId = listId[ tokenId ];

//store the last tokenId from the array into the newly vacant slot
saleItems[ tempListId ] = tempTokenId;

//record the new index of said token
listId[ tempTokenId ] = tempListId;

//remove the last element from the array
saleItems.pop();
}
}

function _beforeTokenTransfer( address from, address to, uint256 tokenId ) internal override {
super._beforeTokenTransfer( from, to, tokenId );

// if the token is still marked as listed then we need to delist it
if ( isForSale[ tokenId ] ) {
_delistItem( tokenId );
}

emit Transfer( from, to, tokenId );
}

// PUBLIC
function listItem( uint256 tokenId, uint256 price, address to ) external {
address tokenOwner = ownerOf( tokenId );
require( msg.sender == tokenOwner, "Invalid Lister" );
_listItem( tokenId, price );
privateRecipient[ tokenId ] = to;
emit Listed( tokenId, tokenOwner, to, price );
}

function delistItem( uint256 tokenId ) external {
require( isForSale[ tokenId ], "Item not for Sale" );
require( msg.sender == ownerOf( tokenId ), "Invalid Lister" );
_delistItem( tokenId );
emit Delisted( tokenId );
}

function buyItem( uint256 tokenId ) external payable {
require( isForSale[ tokenId ], "Item not for Sale" );
uint256 totalPrice = tokenPrice[ tokenId ];
require(msg.value == totalPrice, "Incorrect price");
( address royaltyRecipient, uint256 royaltyPrice ) = royaltyInfo( tokenId, totalPrice );

address buyer = msg.sender;
if ( privateRecipient[ tokenId ] != address( 0 ) ) {
require( buyer == privateRecipient[ tokenId ], "invalid sale address" );
}

address tokenOwner = ownerOf( tokenId );
_delistItem( tokenId );
emit Purchased( tokenId, tokenOwner, buyer, totalPrice );

( bool success, ) = payable( tokenOwner ).call{ value: totalPrice - royaltyPrice }( "" );
require( success, "Transaction Unsuccessful" );

if (royalty > 0){
// Can also be set to payout directly to specified wallet
// alternately this block can be removed to save gas and
// royalties will be stored on the smart contract.
( success, ) = payable( royaltyRecipient ).call{ value: royaltyPrice }( "" );
require( success, "Transaction Unsuccessful" );
}

_transfer( tokenOwner, buyer, tokenId );
}

// VIEW
function getAllListings() external view returns ( Listing[] memory ) {
uint256 arraylen = saleItems.length;
Copy link

@numtel numtel Dec 11, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be better to have pagination? If the marketplace is very large or if accessing the data from another contract when gas cost would matter, it is very important to be able to limit the scope.

And change the salesItems contract property to be of Listing type so that the code is much simpler.

function getListings(uint startIndex, uint fetchCount) external view returns(Listing[] memory) {
  uint itemCount = salesItems.length;
  if(itemCount == 0) {
    return new Listing[](0);
  }
  // Error if starting after end
  require(startIndex < itemCount);
  // Trim if fetchCount goes beyond the upper bound
  if(startIndex + fetchCount >= itemCount) {
    fetchCount = itemCount - startIndex;
  }
  Listing[] memory out = new Listing[](fetchCount);
  for(uint i; i < fetchCount; i++) {
    out[i] = salesItems[startIndex + i];
  }
  return out;
}

Copy link

@offgridgecko offgridgecko Dec 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Except it's not accessing data from "another contract." That would defeat the entire purpose of the code. You might be right that putting some kind of index basis on it could be helpful, as could adding some kind of fetch or get that limits by the cost of the listings or any other criteria you see fit. So perhaps the better solution would be to remove getAllListings from the EIP and leave it up to the developer to create sorting algorithms.
The other way would be to include such searches in here to make it more universal for front-end searches so that aggregators will have a better interfact to gather data from a wide number of compliant collections.

I will look into salesItems again. There was some reason it was set up for tokenId rather than the actual struct and for brevity sake we wanted to provide a solution to customers immediately who did not wish to comply with OpenSeas blacklist functionality but still wanted to have control of their own nfts on their own marketplace. Will look into that more this morning to see about cleaning things up.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated salesItems array like you mentioned. There were some other conflicts when I initially wrote the code but they have resolved themselves now. There still exists two mappings to track indexes for individual tokens to avoid the need to loop through the whole array, and the return values are now much cleaner for the current two fetch functions.
Your method of fetching several at a time should also be feasible, but I have not implemented it yet.

I've tasked @lambdalf-dev with updating the code here on the EIP, it should be updated shortly. Also includes removal of a bug that I found while retesting the codebase.


Listing[] memory activeSales = new Listing[]( arraylen );

for( uint256 i; i < arraylen; ++i ) {
uint256 tokenId = saleItems[ i ];
activeSales[ i ].tokenid = tokenId;
activeSales[ i ].tokenprice = tokenPrice[ tokenId ];
activeSales[ i ].to = privateRecipient[ tokenId ];
}

return activeSales;
}

function getListing( uint256 tokenId ) external view returns ( Listing memory ) {
Listing memory listing = Listing(
tokenId,
tokenPrice[ tokenId ],
privateRecipient[ tokenId ]
);
return listing;
}
}
```

## Security Considerations

There are no security considerations related directly to the implementation of this standard.

## Copyright

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