-
Notifications
You must be signed in to change notification settings - Fork 5.3k
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
eth-bot
merged 17 commits into
ethereum:master
from
lambdalf-dev:ERC721-marketplace-extension
Feb 12, 2023
Merged
Changes from 1 commit
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
2d4f050
Proposal for ERC721 Marketplace extension
lambdalf-dev e0eadc4
Update code examples, Fix some linting issues
lambdalf-dev 3184224
Update and rename eip-erc721-marketplace-extension.md to eip-6105.md
5660-eth 5e89487
Update eip-6105.md
5660-eth 27ec8ed
Merge pull request #1 from 5660-eth/patch-3
lambdalf-dev 860c5ad
Update eip-6105.md
5660-eth dd01c64
Merge pull request #2 from 5660-eth/patch-4
lambdalf-dev b5375b0
Commit necessary changes
Pandapip1 3dcb81c
Update eip-6105.md
5660-eth 918d057
Apply suggestions from code review
lambdalf-dev 80151f7
Update eip-6105.md
5660-eth ce11fb0
Update interface and reference implementation
5660-eth 4b4621b
Update eip-6105.md
5660-eth dac4b5e
Update eip-6105.md
5660-eth d8b3e26
Update eip-6105.md
5660-eth 371fea0
Merge pull request #3 from 5660-eth/patch-5
5660-eth eb6a08d
Update eip-6105.md
5660-eth File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
|
||
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). |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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 ofListing
type so that the code is much simpler.There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.