From 2d4f0502230f0ee430acd65a1c957b28fb5b7cf5 Mon Sep 17 00:00:00 2001 From: Lambdalf the White Date: Thu, 8 Dec 2022 13:11:24 -0700 Subject: [PATCH 01/14] Proposal for ERC721 Marketplace extension --- EIPS/eip-erc721-marketplace-extension.md | 308 +++++++++++++++++++++++ 1 file changed, 308 insertions(+) create mode 100644 EIPS/eip-erc721-marketplace-extension.md diff --git a/EIPS/eip-erc721-marketplace-extension.md b/EIPS/eip-erc721-marketplace-extension.md new file mode 100644 index 00000000000000..286bfa046f8c10 --- /dev/null +++ b/EIPS/eip-erc721-marketplace-extension.md @@ -0,0 +1,308 @@ +--- +eip: +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). From e0eadc4add6c913511b1c4dbf15194037246eee1 Mon Sep 17 00:00:00 2001 From: Lambdalf the White Date: Wed, 14 Dec 2022 08:34:18 -0700 Subject: [PATCH 02/14] Update code examples, Fix some linting issues --- EIPS/eip-erc721-marketplace-extension.md | 507 ++++++++++++----------- 1 file changed, 261 insertions(+), 246 deletions(-) diff --git a/EIPS/eip-erc721-marketplace-extension.md b/EIPS/eip-erc721-marketplace-extension.md index 286bfa046f8c10..2d5b38482930a5 100644 --- a/EIPS/eip-erc721-marketplace-extension.md +++ b/EIPS/eip-erc721-marketplace-extension.md @@ -1,7 +1,7 @@ --- -eip: -title: Marketplace extension for ERC-721 -description: We propose to add a basic marketplace functionality to the ERC721 standard. +eip: erc721-marketplace-extension +title: Marketplace extension for EIP-721 +description: We propose to add a basic marketplace functionality to the EIP-721. 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 @@ -11,16 +11,16 @@ 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. +We propose to add a basic marketplace functionality to the [EIP-721](./eip-721.md) +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 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 @@ -28,11 +28,16 @@ It includes: ## 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. +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. +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: @@ -44,96 +49,97 @@ 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 ); + /// + /// @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 ); } ``` @@ -141,7 +147,7 @@ interface IERC721MarketplaceExtension { ## Backwards Compatibility -This standard is compatible with current EIP-721[./eip-721.md] and EIP-2981[./eip-2981.md] standards. +This standard is compatible with current [EIP-721](./eip-721.md) and [EIP-2981](./eip-2981.md) standards. ## Reference Implementation @@ -150,152 +156,161 @@ 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; - } +contract Example is IERC721MarketplaceExtension, ERC721, ERC2981 { + // List of all items for sale + Listing[] private _listings; + + // Mapping from token ID to sale status + mapping( uint256 => bool ) private _isForSale; + + // Mapping from token ID to listing index + mapping( uint256 => uint256 ) private _listingIndex; + + // INTERNAL + /** + * @dev Create or update a listing for `tokenId_`. + * + * Note: Setting `buyer_` to the NULL address will create a public listing. + * + * @param tokenId_ : identifier of the token being listed + * @param price_ : the sale price of the token being listed + * @param tokenOwner_ : current owner of the token + * @param buyer_ : optional address the token is being sold too + */ + function _listItem( uint256 tokenId_, uint256 price_, address tokenOwner_, address buyer_ ) internal { + // Update existing listing or create a new listing + if ( _isForSale[ tokenId_ ] ) { + _listings[ _listingIndex[ tokenId_ ] ].tokenprice = price_; + _listings[ _listingIndex[ tokenId_ ] ].to = buyer_; + } + else { + Listing memory _listing_ = Listing( tokenId_, price_, buyer_ ); + _listings.push( _listing_ ); + } + emit Listed( tokenId_, expiry_, tokenOwner_, price_ ); + } + + /** + * @dev Removes the listing for `tokenId_`. + * + * @param tokenId_ : identifier of the token being listed + */ + function _removeListing( uint256 tokenId_ ) internal { + uint256 _len_ = _listings.length; + uint256 _index_ = _listingIndex[ tokenId_ ]; + if ( _index_ + 1 != _len_ ) { + _listings[ _index_ ] = _listings[ _len_ - 1 ]; + } + _listings.pop(); + } + + /** + * @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 { + ( boold _success_, ) = payable( recipient_ ).call{ value: amount_ }( "" ); + require( success, "Ether Transfer Fail" ); + } + + /** + * @dev Transfers `tokenId_` from `fromAddress_` to `toAddress_`. + * + * This internal function can be used to implement alternative mechanisms to perform + * token transfer, such as signature-based, or token burning. + * + * @param fromAddress_ : previous owner of the token + * @param toAddress_ : new owner of the token + * @param tokenId_ : identifier of the token being transferred + * + * Emits a {Transfer} event. + */ + function _transfer( address fromAddress_, address toAddress_, uint256 tokenId_ ) internal override { + if ( _isForSale[ tokenId_ ] ) { + _removeListing( tokenId_ ); + } + super._transfer( fromAddress_, toAddress_, tokenId_ ); + } + + // PUBLIC + function listItem( uint256 tokenId, uint256 price, address to ) external { + /** + * @notice Create or update a listing for `tokenId_`. + * + * Note: Setting `buyer_` to the NULL address will create a public listing. + * + * @param tokenId_ : identifier of the token being listed + * @param price_ : the sale price of the token being listed + * @param buyer_ : optional address the token is being sold too + */ + function listItem( uint256 tokenId_, uint256 price_, address buyer_ ) external { + address _tokenOwner_ = ownerOf( tokenId_ ); + require( _tokenOwner_ == ownerOf( tokenId_ ), "Not token owner" ); + + _createListing( tokenId_, price_, _tokenOwner_, buyer_ ); + } + + /** + * @notice Removes the listing for `tokenId_`. + * + * @param tokenId_ : identifier of the token being listed + */ + function delistItem( uint256 tokenId_ ) external { + address _tokenOwner_ = ownerOf( tokenId_ ); + require( _tokenOwner_ == ownerOf( tokenId_ ), "Not token owner" ); + + require( _isForSale[ tokenId_ ], "Invalid listing" ); + + _removeListing( _index_ ); + emit Delisted( tokenId_ ); + } + + /** + * @notice Purchases the listed token `tokenId_`. + * + * @param tokenId_ : identifier of the token being purchased + */ + function buyItem( uint256 tokenId_ ) external payable { + require( _isForSale[ tokenId_ ], "Invalid listing" ); + require( + msg.sender == _listings[ _listingIndex[ tokenId_ ] ].to || + _listings[ _listingIndex[ tokenId_ ] ].to == address( 0 ), + "Invalid sale address" + ) + require( msg.value == _listings[ _listingIndex[ tokenId_ ] ].price, "Incorrect price" ); + + address _tokenOwner_ = ownerOf( tokenId_ ); + _tranfer( _tokenOwner_, msg.sender, tokenId_ ); + emit Purchased( tokenId_, _tokenOwner_, msg.sender, _listing_.price ); + + // Handle royalties + ( address _royaltyRecipient_, uint256 _royalties_ ) = royaltyInfo( tokenId_, msg.value ); + _processEthPayment( _royalties_, _royaltyRecipient_ ); + + uint256 _payment_ = msg.value - _royalties_; + _processEthPayment( _payment_, _tokenOwner_ ); + } + + // VIEW + /** + * @notice Returns the list of all existing listings. + * + * @return the list of all existing listings + */ + function getAllListings() external view returns ( Listing[] memory ) { + return _listings; + } + + /** + * @notice returns the existing listing for `tokenId_`. + * + * @return the existing listing for the requested token. + */ + function getListing( uint256 tokenId ) external view returns ( Listing memory ) { + return _listings[ _listingIndex[ tokenId_ ] ]; + } } ``` From 3184224ef05debdcecc94f61c06abc008e1af90d Mon Sep 17 00:00:00 2001 From: "5660.eth" <76733013+5660-eth@users.noreply.github.com> Date: Sun, 15 Jan 2023 12:22:33 +0800 Subject: [PATCH 03/14] Update and rename eip-erc721-marketplace-extension.md to eip-6105.md --- ...1-marketplace-extension.md => eip-6105.md} | 58 +++++++++++++------ 1 file changed, 41 insertions(+), 17 deletions(-) rename EIPS/{eip-erc721-marketplace-extension.md => eip-6105.md} (70%) diff --git a/EIPS/eip-erc721-marketplace-extension.md b/EIPS/eip-6105.md similarity index 70% rename from EIPS/eip-erc721-marketplace-extension.md rename to EIPS/eip-6105.md index 2d5b38482930a5..3bf6d4f5a5b28b 100644 --- a/EIPS/eip-erc721-marketplace-extension.md +++ b/EIPS/eip-6105.md @@ -1,22 +1,22 @@ --- -eip: erc721-marketplace-extension +eip: 6105 title: Marketplace extension for EIP-721 -description: We propose to add a basic marketplace functionality to the EIP-721. -author: Silvere Heraudeau (@lambdalf-dev), Martin McConnell (@offgridgecko) +description: Add a basic marketplace functionality to the EIP-721. +author: Silvere Heraudeau (@lambdalf-dev), Martin McConnell (@offgridgecko), 5660-eth (@5660-eth), Wizard Wang, Abu 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 +requires: 721, 165 --- "Not your marketplace, not your royalties" ## Abstract -We propose to add a basic marketplace functionality to the [EIP-721](./eip-721.md) -to allow project creators to gain back control of the distribution of their NFTs. +Add a basic marketplace functionality to the [EIP-721](./eip-721.md) +to to realize non-fungible tokens (NFTs) trading without relying on an NFTs trading intermediary platform. It includes: - a method to list an item for sale or update an existing listing, whether @@ -28,9 +28,17 @@ It includes: ## 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. +Most of the current NFTs trading rely on the NFTs trading platform acting as an intermediary, which has the following problems. + +1.Security issues. For example, security issues caused by authorization through `setApprovalForAll`. The permission obtained by the NFT trading platform has carried out unnecessary risk exposure. Once there is a problem with the trading platform contract, it will bring huge losses to the entire industry.If the user has authorized the operation right of his/her NFTs to the NFT trading platform, the latest phishing website fraud method is to trick the user into signing, and let the victim place an order at a very low price on the NFTs trading platform, and designate the recipient. The scammer then accepts the deal, thereby reaping illicit benefits. Ordinary users are hard-pressed to guard against this. + +2.High trading costs. On the one hand, with the increase in the number of trading platforms, the liquidity of NFTs is scattered. If a user needs to make a deal as quickly as possible, he needs to authorize and place orders on multiple platforms, which further increases the risk exposure, and each authorization needs to spend gas. Taking BAYC as an example, the total number of BAYC is 10,000, and the number of current holders exceeds 6,000. The average number of BAYC held by each holder is less than 2. Although `setApprovalForAll` saves the subsequent pending order gas expenditure of a single trading platform, due to the need to authorize to Multi-platform, in essence, leads to an increase in gas expenditures for users. On the other hand, more importantly, the trading service fee charged by the trading platform must also be listed as the cost of user trading, which is much higher than the required gas spending for authorization. + +3.The aggregator provides a solution to aggregate liquidity, but whether the aggregator aggregates the liquidity of a certain trading platform, its decision is centralized. And because the order information of the trading platform is off-chain, the efficiency of the aggregator to obtain order data is affected by the frequency of the trading platform API, and sometimes the NFT trading platform will suspend the distribution of APIs and limit the frequency of APIs. + +4.Whether the NFT project party’s copyright tax income is obtained depends on the centralized decision-making of the NFT trading platform. Some trading platforms ignore the interests of the project party and implement 0 copyright tax, which infringes the interests of the project party. + +5.Not resistant to censorship. Some platforms have delisted many NFTs, and the formulation and implementation of delisting rules are centralized and not transparent enough. Previously, some NFT trading platforms have also failed and wrongly delisted some NFTs, which caused market panic. ## Specification @@ -39,16 +47,11 @@ 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: +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 { +interface IERC6105 { /// /// @dev A structure representing a listed token /// @@ -143,6 +146,22 @@ interface IERC721MarketplaceExtension { } ``` +The `listItem( uint256 tokenId, uint256 price, address to )` function MAY be implemented as `public` or `external`. + +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 `Listed` event MUST be emitted when an NFT is listed. + +The `Delisted` event MUST be emitted when an NFT is delisted. + +The `Purchased` event MUST be emitted when an NFT is traded. + +The `supportsInterface` method MUST return `true` when called with `...`. + ## Rationale ## Backwards Compatibility @@ -156,7 +175,7 @@ pragma solidity ^0.8.0; import './ERC721.sol'; import './ERC2981.sol'; -contract Example is IERC721MarketplaceExtension, ERC721, ERC2981 { +contract Example is IERC6105, ERC721, ERC2981 { // List of all items for sale Listing[] private _listings; @@ -312,6 +331,11 @@ contract Example is IERC721MarketplaceExtension, ERC721, ERC2981 { return _listings[ _listingIndex[ tokenId_ ] ]; } } + + /// @dev See {IERC165-supportsInterface}. + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + return interfaceId == type(IERC6105).interfaceId || super.supportsInterface(interfaceId); + ``` ## Security Considerations From 5e89487373f2ad08e064edc35a3867bc6c5e51cd Mon Sep 17 00:00:00 2001 From: "5660.eth" <76733013+5660-eth@users.noreply.github.com> Date: Sun, 15 Jan 2023 12:55:44 +0800 Subject: [PATCH 04/14] Update eip-6105.md --- EIPS/eip-6105.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-6105.md b/EIPS/eip-6105.md index 3bf6d4f5a5b28b..e2b2818f9de38e 100644 --- a/EIPS/eip-6105.md +++ b/EIPS/eip-6105.md @@ -2,7 +2,7 @@ eip: 6105 title: Marketplace extension for EIP-721 description: Add a basic marketplace functionality to the EIP-721. -author: Silvere Heraudeau (@lambdalf-dev), Martin McConnell (@offgridgecko), 5660-eth (@5660-eth), Wizard Wang, Abu +author: Silvere Heraudeau (@lambdalf-dev), Martin McConnell (@offgridgecko), 5660-eth (@5660-eth), Abu , Wizard Wang discussions-to: https://ethereum-magicians.org/t/idea-a-marketplace-extension-to-erc721-standard/11975 status: Draft type: Standards Track From 860c5aded43da344cef39a7b83c51acdfa617a1b Mon Sep 17 00:00:00 2001 From: "5660.eth" <76733013+5660-eth@users.noreply.github.com> Date: Mon, 16 Jan 2023 22:37:09 +0800 Subject: [PATCH 05/14] Update eip-6105.md --- EIPS/eip-6105.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/EIPS/eip-6105.md b/EIPS/eip-6105.md index e2b2818f9de38e..e5c80a3168336a 100644 --- a/EIPS/eip-6105.md +++ b/EIPS/eip-6105.md @@ -8,7 +8,7 @@ status: Draft type: Standards Track category: ERC created: 2022-12-02 -requires: 721, 165 +requires: 165, 721 --- "Not your marketplace, not your royalties" @@ -19,8 +19,7 @@ Add a basic marketplace functionality to the [EIP-721](./eip-721.md) to to realize non-fungible tokens (NFTs) trading without relying on an NFTs trading intermediary platform. 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 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 From b5375b0b12fb7d42ded08abc2da130d8a66b1b13 Mon Sep 17 00:00:00 2001 From: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Date: Wed, 25 Jan 2023 09:37:16 -0500 Subject: [PATCH 06/14] Commit necessary changes --- EIPS/eip-6105.md | 38 +++++++++++++------------------------- 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/EIPS/eip-6105.md b/EIPS/eip-6105.md index e5c80a3168336a..569985dd455d48 100644 --- a/EIPS/eip-6105.md +++ b/EIPS/eip-6105.md @@ -1,7 +1,7 @@ --- eip: 6105 -title: Marketplace extension for EIP-721 -description: Add a basic marketplace functionality to the EIP-721. +title: Marketplace Extension for EIP-721 +description: Adds a basic marketplace functionality to EIP-721. author: Silvere Heraudeau (@lambdalf-dev), Martin McConnell (@offgridgecko), 5660-eth (@5660-eth), Abu , Wizard Wang discussions-to: https://ethereum-magicians.org/t/idea-a-marketplace-extension-to-erc721-standard/11975 status: Draft @@ -11,33 +11,21 @@ created: 2022-12-02 requires: 165, 721 --- -"Not your marketplace, not your royalties" - ## Abstract -Add a basic marketplace functionality to the [EIP-721](./eip-721.md) -to to realize non-fungible tokens (NFTs) trading without relying on an NFTs trading intermediary platform. +Add a basic marketplace functionality to [EIP-721](./eip-721.md) +to enable non-fungible token trading without relying on an intermediary trading platform. -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 -Most of the current NFTs trading rely on the NFTs trading platform acting as an intermediary, which has the following problems. - -1.Security issues. For example, security issues caused by authorization through `setApprovalForAll`. The permission obtained by the NFT trading platform has carried out unnecessary risk exposure. Once there is a problem with the trading platform contract, it will bring huge losses to the entire industry.If the user has authorized the operation right of his/her NFTs to the NFT trading platform, the latest phishing website fraud method is to trick the user into signing, and let the victim place an order at a very low price on the NFTs trading platform, and designate the recipient. The scammer then accepts the deal, thereby reaping illicit benefits. Ordinary users are hard-pressed to guard against this. - -2.High trading costs. On the one hand, with the increase in the number of trading platforms, the liquidity of NFTs is scattered. If a user needs to make a deal as quickly as possible, he needs to authorize and place orders on multiple platforms, which further increases the risk exposure, and each authorization needs to spend gas. Taking BAYC as an example, the total number of BAYC is 10,000, and the number of current holders exceeds 6,000. The average number of BAYC held by each holder is less than 2. Although `setApprovalForAll` saves the subsequent pending order gas expenditure of a single trading platform, due to the need to authorize to Multi-platform, in essence, leads to an increase in gas expenditures for users. On the other hand, more importantly, the trading service fee charged by the trading platform must also be listed as the cost of user trading, which is much higher than the required gas spending for authorization. - -3.The aggregator provides a solution to aggregate liquidity, but whether the aggregator aggregates the liquidity of a certain trading platform, its decision is centralized. And because the order information of the trading platform is off-chain, the efficiency of the aggregator to obtain order data is affected by the frequency of the trading platform API, and sometimes the NFT trading platform will suspend the distribution of APIs and limit the frequency of APIs. - -4.Whether the NFT project party’s copyright tax income is obtained depends on the centralized decision-making of the NFT trading platform. Some trading platforms ignore the interests of the project party and implement 0 copyright tax, which infringes the interests of the project party. +Most current NFT trading relies on an NFT trading platform acting as an intermediary, which has the following problems: -5.Not resistant to censorship. Some platforms have delisted many NFTs, and the formulation and implementation of delisting rules are centralized and not transparent enough. Previously, some NFT trading platforms have also failed and wrongly delisted some NFTs, which caused market panic. +1. Security issues. For example, security issues caused by authorization through `setApprovalForAll`. The permission obtained by the NFT trading platform has carried out unnecessary risk exposure. Once there is a problem with the trading platform contract, it will bring huge losses to the entire industry.If the user has authorized the operation right of his/her NFTs to the NFT trading platform, the latest phishing website fraud method is to trick the user into signing, and let the victim place an order at a very low price on the NFTs trading platform, and designate the recipient. The scammer then accepts the deal, thereby reaping illicit benefits. Ordinary users are hard-pressed to guard against this. +2. High trading costs. On the one hand, with the increase in the number of trading platforms, the liquidity of NFTs is scattered. If a user needs to make a deal as quickly as possible, he needs to authorize and place orders on multiple platforms, which further increases the risk exposure, and each authorization needs to spend gas. Taking BAYC as an example, the total number of BAYC is 10,000, and the number of current holders exceeds 6,000. The average number of BAYC held by each holder is less than 2. Although `setApprovalForAll` saves the subsequent pending order gas expenditure of a single trading platform, due to the need to authorize to Multi-platform, in essence, leads to an increase in gas expenditures for users. On the other hand, more importantly, the trading service fee charged by the trading platform must also be listed as the cost of user trading, which is much higher than the required gas spending for authorization. +3. The aggregator provides a solution to aggregate liquidity, but whether the aggregator aggregates the liquidity of a certain trading platform, its decision is centralized. And because the order information of the trading platform is off-chain, the efficiency of the aggregator to obtain order data is affected by the frequency of the trading platform API, and sometimes the NFT trading platform will suspend the distribution of APIs and limit the frequency of APIs. +4. Whether the NFT project party’s copyright tax income is obtained depends on the centralized decision-making of the NFT trading platform. Some trading platforms ignore the interests of the project party and implement 0 copyright tax, which infringes the interests of the project party. +5. Not resistant to censorship. Some platforms have delisted many NFTs, and the formulation and implementation of delisting rules are centralized and not transparent enough. Previously, some NFT trading platforms have also failed and wrongly delisted some NFTs, which caused market panic. ## Specification @@ -46,7 +34,7 @@ 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: +Compliant contracts MUST implement the following interface: ```solidity @@ -165,7 +153,7 @@ The `supportsInterface` method MUST return `true` when called with `...`. ## Backwards Compatibility -This standard is compatible with current [EIP-721](./eip-721.md) and [EIP-2981](./eip-2981.md) standards. +This standard is compatible with [EIP-721](./eip-721.md) and [EIP-2981](./eip-2981.md). ## Reference Implementation @@ -339,7 +327,7 @@ contract Example is IERC6105, ERC721, ERC2981 { ## Security Considerations -There are no security considerations related directly to the implementation of this standard. +Needs discussion. ## Copyright From 3dcb81c983b278993aeb0158b93d869746a0a874 Mon Sep 17 00:00:00 2001 From: "5660.eth" <76733013+5660-eth@users.noreply.github.com> Date: Wed, 25 Jan 2023 23:38:46 +0800 Subject: [PATCH 07/14] Update eip-6105.md --- EIPS/eip-6105.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/EIPS/eip-6105.md b/EIPS/eip-6105.md index 569985dd455d48..b45b6d9b2474bd 100644 --- a/EIPS/eip-6105.md +++ b/EIPS/eip-6105.md @@ -21,11 +21,11 @@ to enable non-fungible token trading without relying on an intermediary trading Most current NFT trading relies on an NFT trading platform acting as an intermediary, which has the following problems: -1. Security issues. For example, security issues caused by authorization through `setApprovalForAll`. The permission obtained by the NFT trading platform has carried out unnecessary risk exposure. Once there is a problem with the trading platform contract, it will bring huge losses to the entire industry.If the user has authorized the operation right of his/her NFTs to the NFT trading platform, the latest phishing website fraud method is to trick the user into signing, and let the victim place an order at a very low price on the NFTs trading platform, and designate the recipient. The scammer then accepts the deal, thereby reaping illicit benefits. Ordinary users are hard-pressed to guard against this. -2. High trading costs. On the one hand, with the increase in the number of trading platforms, the liquidity of NFTs is scattered. If a user needs to make a deal as quickly as possible, he needs to authorize and place orders on multiple platforms, which further increases the risk exposure, and each authorization needs to spend gas. Taking BAYC as an example, the total number of BAYC is 10,000, and the number of current holders exceeds 6,000. The average number of BAYC held by each holder is less than 2. Although `setApprovalForAll` saves the subsequent pending order gas expenditure of a single trading platform, due to the need to authorize to Multi-platform, in essence, leads to an increase in gas expenditures for users. On the other hand, more importantly, the trading service fee charged by the trading platform must also be listed as the cost of user trading, which is much higher than the required gas spending for authorization. -3. The aggregator provides a solution to aggregate liquidity, but whether the aggregator aggregates the liquidity of a certain trading platform, its decision is centralized. And because the order information of the trading platform is off-chain, the efficiency of the aggregator to obtain order data is affected by the frequency of the trading platform API, and sometimes the NFT trading platform will suspend the distribution of APIs and limit the frequency of APIs. -4. Whether the NFT project party’s copyright tax income is obtained depends on the centralized decision-making of the NFT trading platform. Some trading platforms ignore the interests of the project party and implement 0 copyright tax, which infringes the interests of the project party. -5. Not resistant to censorship. Some platforms have delisted many NFTs, and the formulation and implementation of delisting rules are centralized and not transparent enough. Previously, some NFT trading platforms have also failed and wrongly delisted some NFTs, which caused market panic. +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 From 918d057cdd0a0bd6f5fae40632b0b8cf4727d179 Mon Sep 17 00:00:00 2001 From: Lambdalf the White Date: Wed, 25 Jan 2023 08:57:18 -0700 Subject: [PATCH 08/14] Apply suggestions from code review Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> --- EIPS/eip-6105.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/EIPS/eip-6105.md b/EIPS/eip-6105.md index 569985dd455d48..b45b6d9b2474bd 100644 --- a/EIPS/eip-6105.md +++ b/EIPS/eip-6105.md @@ -21,11 +21,11 @@ to enable non-fungible token trading without relying on an intermediary trading Most current NFT trading relies on an NFT trading platform acting as an intermediary, which has the following problems: -1. Security issues. For example, security issues caused by authorization through `setApprovalForAll`. The permission obtained by the NFT trading platform has carried out unnecessary risk exposure. Once there is a problem with the trading platform contract, it will bring huge losses to the entire industry.If the user has authorized the operation right of his/her NFTs to the NFT trading platform, the latest phishing website fraud method is to trick the user into signing, and let the victim place an order at a very low price on the NFTs trading platform, and designate the recipient. The scammer then accepts the deal, thereby reaping illicit benefits. Ordinary users are hard-pressed to guard against this. -2. High trading costs. On the one hand, with the increase in the number of trading platforms, the liquidity of NFTs is scattered. If a user needs to make a deal as quickly as possible, he needs to authorize and place orders on multiple platforms, which further increases the risk exposure, and each authorization needs to spend gas. Taking BAYC as an example, the total number of BAYC is 10,000, and the number of current holders exceeds 6,000. The average number of BAYC held by each holder is less than 2. Although `setApprovalForAll` saves the subsequent pending order gas expenditure of a single trading platform, due to the need to authorize to Multi-platform, in essence, leads to an increase in gas expenditures for users. On the other hand, more importantly, the trading service fee charged by the trading platform must also be listed as the cost of user trading, which is much higher than the required gas spending for authorization. -3. The aggregator provides a solution to aggregate liquidity, but whether the aggregator aggregates the liquidity of a certain trading platform, its decision is centralized. And because the order information of the trading platform is off-chain, the efficiency of the aggregator to obtain order data is affected by the frequency of the trading platform API, and sometimes the NFT trading platform will suspend the distribution of APIs and limit the frequency of APIs. -4. Whether the NFT project party’s copyright tax income is obtained depends on the centralized decision-making of the NFT trading platform. Some trading platforms ignore the interests of the project party and implement 0 copyright tax, which infringes the interests of the project party. -5. Not resistant to censorship. Some platforms have delisted many NFTs, and the formulation and implementation of delisting rules are centralized and not transparent enough. Previously, some NFT trading platforms have also failed and wrongly delisted some NFTs, which caused market panic. +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 From 80151f716bf3ab2c785bd06357d071533981c2fc Mon Sep 17 00:00:00 2001 From: "5660.eth" <76733013+5660-eth@users.noreply.github.com> Date: Thu, 26 Jan 2023 00:10:20 +0800 Subject: [PATCH 09/14] Update eip-6105.md --- EIPS/eip-6105.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/EIPS/eip-6105.md b/EIPS/eip-6105.md index b45b6d9b2474bd..6ca33c6243659d 100644 --- a/EIPS/eip-6105.md +++ b/EIPS/eip-6105.md @@ -317,11 +317,12 @@ contract Example is IERC6105, ERC721, ERC2981 { function getListing( uint256 tokenId ) external view returns ( Listing memory ) { return _listings[ _listingIndex[ tokenId_ ] ]; } -} /// @dev See {IERC165-supportsInterface}. function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { return interfaceId == type(IERC6105).interfaceId || super.supportsInterface(interfaceId); + } +} ``` From ce11fb087457b7aeb020f245d4fd98595cda60fc Mon Sep 17 00:00:00 2001 From: "5660.eth" <76733013+5660-eth@users.noreply.github.com> Date: Mon, 30 Jan 2023 08:52:39 +0800 Subject: [PATCH 10/14] Update interface and reference implementation The rationale can only be updated once the interface and reference implementation have been identified --- EIPS/eip-6105.md | 367 ++++++++++++++++++++--------------------------- 1 file changed, 155 insertions(+), 212 deletions(-) diff --git a/EIPS/eip-6105.md b/EIPS/eip-6105.md index 6ca33c6243659d..dbd5c3126b58ab 100644 --- a/EIPS/eip-6105.md +++ b/EIPS/eip-6105.md @@ -39,115 +39,79 @@ Compliant contracts MUST implement the following interface: ```solidity interface IERC6105 { - /// - /// @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. - /// + + /// @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 - 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 expires - UNIX timestamp, the buyer could buy the token before expires + event UpdateListing(uint256 indexed tokenId, address indexed from, address to, uint256 indexed price ,uint64 expires); + + /// @notice 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 ); + event Purchased(uint256 indexed tokenId, address indexed from, address indexed to, uint256 price); - /// - /// @dev Lists token `tokenId` for sale. - /// + /// @notice Lists token `tokenId` for sale + /// @dev `price` MUST NOT be set to zero /// @param tokenId - the NFT asset being listed + /// @param price - the price the token is being sold for, regardless of currency + /// @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 - /// @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 - /// + /// - Caller must be owner, authorised operators or approved address of the token + /// - `price` must not be zero + /// - Must emit a {UpdateListings} event. + function listItem(uint256 tokenId, uint256 price, uint64 expires, address to) external; + + /// @notice 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. - /// + /// - Caller must be owner, authorised operators or approved address of the token + /// - Must emit a {UpdateListings} event. + function delistItem(uint256 tokenId) external; + + /// @notice 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 ); + 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 + /// @return the specified listing (price, expires, intended recipient) + function getListing(uint256 tokenId) external view returns (uint256, uint64, address); } ``` -The `listItem( uint256 tokenId, uint256 price, address to )` function MAY be implemented as `public` or `external`. - -The `delistItem( uint256 tokenId )` function MAY be implemented as `public` or `external`. +The `listItem(uint256 tokenId, uint256 price, uint64 expires, address to)` function MAY be implemented as `public` or `external`.And `price` MUST NOT be set to zero. -The `buyItem( uint256 tokenId )` function MUST be implemented as `payable` and MAY be implemented as `public` or `external`. +The `delistItem(uint256 tokenId)` function MAY be implemented as `public` or `external`. -The `getListing( uint256 tokenId )` function MAY be implemented as `pure` or `view`. +The `buyItem(uint256 tokenId)` function MUST be implemented as `payable` and MAY be implemented as `public` or `external`. -The `Listed` event MUST be emitted when an NFT is listed. +The `getListing(uint256 tokenId)` function MAY be implemented as `pure` or `view`. -The `Delisted` event MUST be emitted when an NFT is delisted. +The `UpdateListing` event MUST be emitted when a token is listed for sale or delisted. -The `Purchased` event MUST be emitted when an NFT is traded. +The `Purchased` event MUST be emitted when a token is traded. -The `supportsInterface` method MUST return `true` when called with `...`. +The `supportsInterface` method MUST return `true` when called with `0x6de8e04d`. ## Rationale @@ -158,170 +122,149 @@ This standard is compatible with [EIP-721](./eip-721.md) and [EIP-2981](./eip-29 ## Reference Implementation ```solidity + // SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.0; -import './ERC721.sol'; -import './ERC2981.sol'; +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import "@openzeppelin/contracts/token/common/ERC2981.sol"; +import "./IERC6105.sol"; -contract Example is IERC6105, ERC721, ERC2981 { - // List of all items for sale - Listing[] private _listings; +contract ERC6105 is ERC721, ERC2981, IERC6105 { - // Mapping from token ID to sale status - mapping( uint256 => bool ) private _isForSale; + /// @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, regardless of currency + /// @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 => uint256 ) private _listingIndex; - - // INTERNAL - /** - * @dev Create or update a listing for `tokenId_`. - * - * Note: Setting `buyer_` to the NULL address will create a public listing. - * - * @param tokenId_ : identifier of the token being listed - * @param price_ : the sale price of the token being listed - * @param tokenOwner_ : current owner of the token - * @param buyer_ : optional address the token is being sold too - */ - function _listItem( uint256 tokenId_, uint256 price_, address tokenOwner_, address buyer_ ) internal { - // Update existing listing or create a new listing - if ( _isForSale[ tokenId_ ] ) { - _listings[ _listingIndex[ tokenId_ ] ].tokenprice = price_; - _listings[ _listingIndex[ tokenId_ ] ].to = buyer_; - } - else { - Listing memory _listing_ = Listing( tokenId_, price_, buyer_ ); - _listings.push( _listing_ ); - } - emit Listed( tokenId_, expiry_, tokenOwner_, price_ ); - } + mapping(uint256 => Listing) private _listings; - /** - * @dev Removes the listing for `tokenId_`. - * - * @param tokenId_ : identifier of the token being listed - */ - function _removeListing( uint256 tokenId_ ) internal { - uint256 _len_ = _listings.length; - uint256 _index_ = _listingIndex[ tokenId_ ]; - if ( _index_ + 1 != _len_ ) { - _listings[ _index_ ] = _listings[ _len_ - 1 ]; + constructor(string memory name_, string memory symbol_) + ERC721(name_, symbol_) + { } - _listings.pop(); - } - /** - * @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 { - ( boold _success_, ) = payable( recipient_ ).call{ value: amount_ }( "" ); - require( success, "Ether Transfer Fail" ); +/// @dev Create or update a listing for `tokenId`. +/// Setting `buyer` to the NULL address will create a public listing. +/// @param tokenId : identifier of the token being listed +/// @param price : the sale price of the token being listed +/// @param tokenOwner : current owner of the token +/// @param expires - UNIX timestamp, the buyer could buy the token before expires +/// @param buyer : optional address the token is being sold too + function _listItem(uint256 tokenId, uint256 price, address tokenOwner, uint64 expires, address buyer) internal { + _listings[tokenId].price = price; + _listings[tokenId].expires = expires; + _listings[tokenId].to = buyer; + emit UpdateListing(tokenId, tokenOwner, buyer, price, expires); } - /** - * @dev Transfers `tokenId_` from `fromAddress_` to `toAddress_`. - * - * This internal function can be used to implement alternative mechanisms to perform - * token transfer, such as signature-based, or token burning. - * - * @param fromAddress_ : previous owner of the token - * @param toAddress_ : new owner of the token - * @param tokenId_ : identifier of the token being transferred - * - * Emits a {Transfer} event. - */ - function _transfer( address fromAddress_, address toAddress_, uint256 tokenId_ ) internal override { - if ( _isForSale[ tokenId_ ] ) { - _removeListing( tokenId_ ); - } - super._transfer( fromAddress_, toAddress_, tokenId_ ); +/// @dev Removes the listing for `tokenId`. +/// @param tokenId : identifier of the token being listed + function _removeListing(uint256 tokenId) internal { + address tokenOwner = ownerOf(tokenId); + delete _listings[tokenId]; + emit UpdateListing(tokenId, tokenOwner, address(0), 0, 0); } - // PUBLIC - function listItem( uint256 tokenId, uint256 price, address to ) external { - /** - * @notice Create or update a listing for `tokenId_`. - * - * Note: Setting `buyer_` to the NULL address will create a public listing. - * - * @param tokenId_ : identifier of the token being listed - * @param price_ : the sale price of the token being listed - * @param buyer_ : optional address the token is being sold too - */ - function listItem( uint256 tokenId_, uint256 price_, address buyer_ ) external { - address _tokenOwner_ = ownerOf( tokenId_ ); - require( _tokenOwner_ == ownerOf( tokenId_ ), "Not token owner" ); - - _createListing( tokenId_, price_, _tokenOwner_, buyer_ ); +/// @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 { + (bool success,) = payable(recipient).call{value: amount}(""); + require(success, "Ether Transfer Fail"); } - /** - * @notice Removes the listing for `tokenId_`. - * - * @param tokenId_ : identifier of the token being listed - */ - function delistItem( uint256 tokenId_ ) external { - address _tokenOwner_ = ownerOf( tokenId_ ); - require( _tokenOwner_ == ownerOf( tokenId_ ), "Not token owner" ); +/// @notice Create or update a listing for `tokenId`. +/// Setting `buyer` to the NULL address will create a public listing. +/// @param tokenId : identifier of the token being listed +/// @param expires : UNIX timestamp, the buyer could buy the token before expires +/// @param price : the sale price of the token being listed +/// @param buyer : optional address the token is being sold too + function listItem (uint256 tokenId, uint256 price,uint64 expires, address buyer) external { + address tokenOwner = ownerOf(tokenId); + require(_isApprovedOrOwner(_msgSender(), tokenId),"ERC6105: caller is not owner nor approved"); + + _listItem(tokenId, price, tokenOwner,expires, buyer); + } - require( _isForSale[ tokenId_ ], "Invalid listing" ); + function _isForSale(uint256 tokenId) internal virtual returns(bool){ - _removeListing( _index_ ); - emit Delisted( tokenId_ ); + if(_listings[tokenId].price > 0 && _listings[tokenId].expires >= block.timestamp) + { + return true; + } + else{ + return false; + } + } + +/// @notice Removes the listing for `tokenId_`. +/// @param tokenId : identifier of the token being listed + function delistItem(uint256 tokenId) external { + 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 payable { - require( _isForSale[ tokenId_ ], "Invalid listing" ); +/// @notice Purchases the listed token `tokenId_`. +/// @param tokenId : identifier of the token being purchased + function buyItem(uint256 tokenId) external 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( - msg.sender == _listings[ _listingIndex[ tokenId_ ] ].to || - _listings[ _listingIndex[ tokenId_ ] ].to == address( 0 ), - "Invalid sale address" - ) - require( msg.value == _listings[ _listingIndex[ tokenId_ ] ].price, "Incorrect price" ); - - address _tokenOwner_ = ownerOf( tokenId_ ); - _tranfer( _tokenOwner_, msg.sender, tokenId_ ); - emit Purchased( tokenId_, _tokenOwner_, msg.sender, _listing_.price ); - - // Handle royalties - ( address _royaltyRecipient_, uint256 _royalties_ ) = royaltyInfo( tokenId_, msg.value ); - _processEthPayment( _royalties_, _royaltyRecipient_ ); - - uint256 _payment_ = msg.value - _royalties_; - _processEthPayment( _payment_, _tokenOwner_ ); - } + buyer == _listings[tokenId].to || + _listings[tokenId].to == address(0), + "ERC6105: invalid sale address" + ); + require(value == price, "ERC6105: incorrect price"); - // VIEW - /** - * @notice Returns the list of all existing listings. - * - * @return the list of all existing listings - */ - function getAllListings() external view returns ( Listing[] memory ) { - return _listings; + _transfer(tokenOwner, buyer, tokenId); + emit Purchased(tokenId, tokenOwner, buyer, price); + + /// @dev Handle royalties + (address royaltyRecipient, uint256 royalties) = royaltyInfo(tokenId, msg.value); + _processEthPayment(royalties, royaltyRecipient); + + uint256 payment = msg.value - royalties; + _processEthPayment(payment, tokenOwner); } - /** - * @notice returns the existing listing for `tokenId_`. - * - * @return the existing listing for the requested token. - */ - function getListing( uint256 tokenId ) external view returns ( Listing memory ) { - return _listings[ _listingIndex[ tokenId_ ] ]; + /// @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 + /// @return the specified listing (price, expires, intended recipient) + function getListing(uint256 tokenId) external view returns (uint256, uint64, address) { + uint256 price = _listings[tokenId].price; + uint64 expires = _listings[tokenId].expires; + address to = _listings[tokenId].to; + return (price, expires, to); } /// @dev See {IERC165-supportsInterface}. - function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + 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 UpdateListing(tokenId, to, address(0), 0, 0); + } + } } ``` From 4b4621bfb2977469c42db87ca0b3f5c487d3c480 Mon Sep 17 00:00:00 2001 From: "5660.eth" <76733013+5660-eth@users.noreply.github.com> Date: Mon, 30 Jan 2023 08:55:16 +0800 Subject: [PATCH 11/14] Update eip-6105.md --- EIPS/eip-6105.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-6105.md b/EIPS/eip-6105.md index dbd5c3126b58ab..b952ea01b0b0ac 100644 --- a/EIPS/eip-6105.md +++ b/EIPS/eip-6105.md @@ -99,7 +99,7 @@ interface IERC6105 { } ``` -The `listItem(uint256 tokenId, uint256 price, uint64 expires, address to)` function MAY be implemented as `public` or `external`.And `price` MUST NOT be set to zero. +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`. From dac4b5e11bc4f3559e2ef671f384dfb56edd15c9 Mon Sep 17 00:00:00 2001 From: "5660.eth" <76733013+5660-eth@users.noreply.github.com> Date: Mon, 30 Jan 2023 15:39:14 +0800 Subject: [PATCH 12/14] Update eip-6105.md --- EIPS/eip-6105.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-6105.md b/EIPS/eip-6105.md index b952ea01b0b0ac..ce731a90731393 100644 --- a/EIPS/eip-6105.md +++ b/EIPS/eip-6105.md @@ -50,7 +50,7 @@ interface IERC6105 { /// or non zero address for a private listing /// @param price - the price the token is being sold for, regardless of currency /// @param expires - UNIX timestamp, the buyer could buy the token before expires - event UpdateListing(uint256 indexed tokenId, address indexed from, address to, uint256 indexed price ,uint64 expires); + event UpdateListing(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 - the NFT asset being purchased From d8b3e26dbcb1360db9c373bbf538dc46ce074ba2 Mon Sep 17 00:00:00 2001 From: "5660.eth" <76733013+5660-eth@users.noreply.github.com> Date: Mon, 30 Jan 2023 17:46:36 +0800 Subject: [PATCH 13/14] Update eip-6105.md Update Specification, Rationale and Reference Implementation. --- EIPS/eip-6105.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/EIPS/eip-6105.md b/EIPS/eip-6105.md index ce731a90731393..4870a466edf125 100644 --- a/EIPS/eip-6105.md +++ b/EIPS/eip-6105.md @@ -115,6 +115,12 @@ The `supportsInterface` method MUST return `true` when called with `0x6de8e04d`. ## Rationale +Out of consideration for the safety and efficiency of buyer' asserts, 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. On the one hand, a caller rarely sets the price to 0, and when this behavior occurs, it is often the caller’s operation error and will cause assets loss. On the other hand, a caller needs to spend gas to call this function, so if a caller can set the token price to 0, his or her income is actually negative at this time, which does not conform to the "economic man" thinking in economics. What's more, when a token's price is 0, we assume that it is not for sale, which will make the reference implementation more concise. + +Setting `expires` in the `listItem` function will allow callers to better manage their listings. In addition, if a listing expires automatically, a token owner no longer needs to `delistItem`, thus saving gas. + ## Backwards Compatibility This standard is compatible with [EIP-721](./eip-721.md) and [EIP-2981](./eip-2981.md). From eb6a08dbfed4cf662aa099173261fde54fc255b1 Mon Sep 17 00:00:00 2001 From: "5660.eth" <76733013+5660-eth@users.noreply.github.com> Date: Thu, 9 Feb 2023 20:16:41 +0800 Subject: [PATCH 14/14] Update eip-6105.md --- EIPS/eip-6105.md | 195 ++++++++++++++++++++++++----------------------- 1 file changed, 101 insertions(+), 94 deletions(-) diff --git a/EIPS/eip-6105.md b/EIPS/eip-6105.md index 4870a466edf125..d673f53dd439bc 100644 --- a/EIPS/eip-6105.md +++ b/EIPS/eip-6105.md @@ -2,13 +2,13 @@ eip: 6105 title: Marketplace Extension for EIP-721 description: Adds a basic marketplace functionality to EIP-721. -author: Silvere Heraudeau (@lambdalf-dev), Martin McConnell (@offgridgecko), 5660-eth (@5660-eth), Abu , Wizard Wang -discussions-to: https://ethereum-magicians.org/t/idea-a-marketplace-extension-to-erc721-standard/11975 +author: 5660-eth (@5660-eth), Silvere Heraudeau (@lambdalf-dev), Martin McConnell (@offgridgecko), Abu , 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 +requires: 165, 721, 2981 --- ## Abstract @@ -37,63 +37,64 @@ 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 - the NFT asset being listed + /// @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, regardless of currency + /// @param price - the price the token is being sold for /// @param expires - UNIX timestamp, the buyer could buy the token before expires - event UpdateListing(uint256 indexed tokenId, address indexed from, address indexed to, uint256 price ,uint64 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 - the NFT asset 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, regardless of currency - event Purchased(uint256 indexed tokenId, address indexed from, address indexed to, uint256 price); - - /// @notice Lists token `tokenId` for sale - /// @dev `price` MUST NOT be set to zero - /// @param tokenId - the NFT asset being listed - /// @param price - the price the token is being sold for, regardless of currency + /// @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 - address of who this listing is for, + /// @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 {UpdateListings} event. + /// - Must emit a {LogUpdateListing} event. function listItem(uint256 tokenId, uint256 price, uint64 expires, address to) external; - /// @notice Delists token `tokenId` that was listed for sale - /// @param tokenId - the NFT asset being delisted + /// @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 {UpdateListings} event. + /// - Must emit a {LogUpdateListing} event function delistItem(uint256 tokenId) external; - /// @notice Buys a token and transfers it to the caller. - /// @param tokenId - the NFT asset being purchased + /// @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 {Purchased} event. + /// - 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); } @@ -107,19 +108,19 @@ The `buyItem(uint256 tokenId)` function MUST be implemented as `payable` and MAY The `getListing(uint256 tokenId)` function MAY be implemented as `pure` or `view`. -The `UpdateListing` event MUST be emitted when a token is listed for sale or delisted. +The `LogUpdateListing` event MUST be emitted when a token is listed for sale or delisted. -The `Purchased` event MUST be emitted when a token is traded. +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' asserts, it does not provide bidding functions and auction functions, but only adds listing funcitons. +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. On the one hand, a caller rarely sets the price to 0, and when this behavior occurs, it is often the caller’s operation error and will cause assets loss. On the other hand, a caller needs to spend gas to call this function, so if a caller can set the token price to 0, his or her income is actually negative at this time, which does not conform to the "economic man" thinking in economics. What's more, when a token's price is 0, we assume that it is not for sale, which will make the reference implementation more concise. +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 will allow callers to better manage their listings. In addition, if a listing expires automatically, a token owner no longer needs to `delistItem`, thus saving gas. +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 @@ -129,7 +130,7 @@ This standard is compatible with [EIP-721](./eip-721.md) and [EIP-2981](./eip-29 ```solidity // SPDX-License-Identifier: CC0-1.0 -pragma solidity ^0.8.0; +pragma solidity ^0.8.8; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/common/ERC2981.sol"; import "./IERC6105.sol"; @@ -139,7 +140,7 @@ 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, regardless of currency + /// @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, @@ -150,7 +151,7 @@ contract ERC6105 is ERC721, ERC2981, IERC6105 { address to; } - // Mapping from token ID to listing index + // Mapping from token Id to listing index mapping(uint256 => Listing) private _listings; constructor(string memory name_, string memory symbol_) @@ -158,72 +159,35 @@ contract ERC6105 is ERC721, ERC2981, IERC6105 { { } -/// @dev Create or update a listing for `tokenId`. -/// Setting `buyer` to the NULL address will create a public listing. -/// @param tokenId : identifier of the token being listed -/// @param price : the sale price of the token being listed -/// @param tokenOwner : current owner of the token -/// @param expires - UNIX timestamp, the buyer could buy the token before expires -/// @param buyer : optional address the token is being sold too - function _listItem(uint256 tokenId, uint256 price, address tokenOwner, uint64 expires, address buyer) internal { - _listings[tokenId].price = price; - _listings[tokenId].expires = expires; - _listings[tokenId].to = buyer; - emit UpdateListing(tokenId, tokenOwner, buyer, price, expires); - } - -/// @dev Removes the listing for `tokenId`. -/// @param tokenId : identifier of the token being listed - function _removeListing(uint256 tokenId) internal { - address tokenOwner = ownerOf(tokenId); - delete _listings[tokenId]; - emit UpdateListing(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 { - (bool success,) = payable(recipient).call{value: amount}(""); - require(success, "Ether Transfer Fail"); - } - -/// @notice Create or update a listing for `tokenId`. -/// Setting `buyer` to the NULL address will create a public listing. -/// @param tokenId : identifier of the token being listed -/// @param expires : UNIX timestamp, the buyer could buy the token before expires -/// @param price : the sale price of the token being listed -/// @param buyer : optional address the token is being sold too - function listItem (uint256 tokenId, uint256 price,uint64 expires, address buyer) external { + /// @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, buyer); + _listItem(tokenId, price, tokenOwner, expires, to); } - function _isForSale(uint256 tokenId) internal virtual returns(bool){ - - if(_listings[tokenId].price > 0 && _listings[tokenId].expires >= block.timestamp) - { - return true; - } - else{ - return false; - } - } - -/// @notice Removes the listing for `tokenId_`. -/// @param tokenId : identifier of the token being listed - function delistItem(uint256 tokenId) external { + /// @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 payable { + /// @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; @@ -237,42 +201,85 @@ contract ERC6105 is ERC721, ERC2981, IERC6105 { require(value == price, "ERC6105: incorrect price"); _transfer(tokenOwner, buyer, tokenId); - emit Purchased(tokenId, tokenOwner, buyer, price); + emit LogPurchased(tokenId, tokenOwner, buyer, price); /// @dev Handle royalties (address royaltyRecipient, uint256 royalties) = royaltyInfo(tokenId, msg.value); - _processEthPayment(royalties, royaltyRecipient); uint256 payment = msg.value - royalties; + _processEthPayment(royalties, royaltyRecipient); _processEthPayment(payment, tokenOwner); } - /// @notice Returns the listing for `tokenId_` + /// @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) { + 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{ + 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 UpdateListing(tokenId, to, address(0), 0, 0); + emit LogUpdateListing(tokenId, to, address(0), 0, 0); } } -} - +} ``` ## Security Considerations