diff --git a/EIPS/eip-6105.md b/EIPS/eip-6105.md index 569985dd455d48..4870a466edf125 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 @@ -39,118 +39,88 @@ 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 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 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 the `price` in this function 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 +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). @@ -158,170 +128,150 @@ 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"); + + _transfer(tokenOwner, buyer, tokenId); + emit Purchased(tokenId, tokenOwner, buyer, price); + + /// @dev Handle royalties + (address royaltyRecipient, uint256 royalties) = royaltyInfo(tokenId, msg.value); + _processEthPayment(royalties, royaltyRecipient); - // 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; + 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); + } + } +} ```