diff --git a/EIPS/eip-6105.md b/EIPS/eip-6105.md index f672a6632542e2..6fe6771ca7031a 100644 --- a/EIPS/eip-6105.md +++ b/EIPS/eip-6105.md @@ -1,10 +1,10 @@ --- eip: 6105 title: No Intermediary NFT Trading Protocol -description: Adds a marketplace functionality with a mandatory and more diverse royalty scheme to ERC-721 +description: Adds a marketplace functionality with more diverse royalty schemes to ERC-721 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 +status: Review type: Standards Track category: ERC created: 2022-12-02 @@ -13,7 +13,7 @@ requires: 20, 165, 721, 2981 ## Abstract -Add a marketplace functionality to [ERC-721](./eip-721.md) to enable non-fungible token trading without relying on an intermediary trading platform. At the same time, implement a mandatory and more diverse royalty scheme. +This ERC adds a marketplace functionality to [ERC-721](./eip-721.md) to enable non-fungible token trading without relying on an intermediary trading platform. At the same time, creators may implement more diverse royalty schemes. ## Motivation @@ -47,17 +47,17 @@ interface IERC6105 { /// @param supportedToken - contract addresses of supported token or zero address /// The zero address indicates that the supported token is ETH /// Buyer needs to purchase item with supported token - /// @param benchmarkPrice2 - Additional price parameter, may be used when calculating royalties + /// @param benchmarkPrice - Additional price parameter, may be used when calculating royalties event UpdateListing( uint256 indexed tokenId, address indexed from, uint256 salePrice, uint64 expires, address supportedToken, - uint256 benchmarkPrice2 + uint256 benchmarkPrice ); - /// @notice Emitted when a token that was listed for sale is being purchased. + /// @notice Emitted when a token is being purchased /// @param tokenId - identifier of the token being purchased /// @param from - address of who is selling the token /// @param to - address of who is buying the token @@ -96,7 +96,7 @@ interface IERC6105 { address supportedToken ) external; - /// @notice Create or update a listing for `tokenId` + /// @notice Create or update a listing for `tokenId` with `benchmarkPrice` /// @dev `salePrice` MUST NOT be set to zero /// @param tokenId - identifier of the token being listed /// @param salePrice - the price the token is being sold for @@ -104,7 +104,7 @@ interface IERC6105 { /// @param supportedToken - contract addresses of supported token or zero address /// The zero address indicates that the supported token is ETH /// Buyer needs to purchase item with supported token - /// @param benchmarkPrice1 - Additional price parameter, may be used when calculating royalties + /// @param benchmarkPrice - Additional price parameter, may be used when calculating royalties /// Requirements: /// - `tokenId` must exist /// - Caller must be owner, authorised operators or approved address of the token @@ -116,10 +116,10 @@ interface IERC6105 { uint256 salePrice, uint64 expires, address supportedToken, - uint256 benchmarkPrice1 + uint256 benchmarkPrice ) external; - /// @notice Removes the listing for `tokenId` + /// @notice Remove the listing for `tokenId` /// @param tokenId - identifier of the token being delisted /// Requirements: /// - `tokenId` must exist and be listed for sale @@ -127,18 +127,20 @@ interface IERC6105 { /// - Must emit an {UpdateListing} event function delistItem(uint256 tokenId) external; - /// @notice Buys a token and transfers it to the caller - /// @dev Must check if `salePrice` matches the expected purchase price to prevent front-running attacks + /// @notice Buy a token and transfer it to the caller + /// @dev `salePrice` and `supportedToken` must match the expected purchase price and token to prevent front-running attacks /// @param tokenId - identifier of the token being purchased /// @param salePrice - the price the token is being sold for + /// @param supportedToken - contract addresses of supported token or zero address /// Requirements: /// - `tokenId` must exist and be listed for sale /// - `salePrice` must matches the expected purchase price to prevent front-running attacks + /// - `supportedToken` must matches the expected purchase token to prevent front-running attacks /// - Caller must be able to pay the listed price for `tokenId` /// - Must emit a {Purchased} event - function buyItem(uint256 tokenId, uint256 salePrice) external payable; + function buyItem(uint256 tokenId, uint256 salePrice, address supportedToken) external payable; - /// @notice Returns the listing for `tokenId` + /// @notice Return the listing for `tokenId` /// @dev The zero sale price indicates that the token is not for sale /// The zero expires indicates that the token is not for sale /// The zero supported token address indicates that the supported token is ETH @@ -148,19 +150,163 @@ interface IERC6105 { } ``` -The `listItem(uint256 tokenId, uint256 salePrice, uint64 expires, address supportedToken)` function MAY be implemented as `public` or `external`. And the `salePrice` in this function MUST NOT be set to zero. +### Optional collection offer extention -The `listItem(uint256 tokenId, uint256 salePrice, uint64 expires, address supportedToken, uint256 benchmarkPrice1)` function MAY be implemented as `public` or `external`. And the `salePrice` in this function MUST NOT be set to zero. +```solidity +/// The collection offer extension is OPTIONAL for ERC-6105 smart contracts. This allows smart contract to support collection offer functionality. +interface IERC6105CollectionOffer { + + /// @notice Emitted when the collection receives an offer or an offer is canceled + /// @dev The zero `salePrice` indicates that the collection offer of the token is canceled + /// The zero `expires` indicates that the collection offer of the token is canceled + /// @param from - address of who make collection offer + /// @param amount - the amount the offerer wants to buy at `salePrice` per token + /// @param salePrice - the price of each token is being offered for the collection + /// @param expires - UNIX timestamp, the offer could be accepted before expires + /// @param supportedToken - contract addresses of supported ERC20 token + /// Buyer wants to purchase items with supported token + event UpdateCollectionOffer(address indexed from, uint256 amount, uint256 salePrice ,uint64 expires, address supportedToken); + + /// @notice Create or update an offer for the collection + /// @dev `salePrice` MUST NOT be set to zero + /// @param amount - the amount the offerer wants to buy at `salePrice` per token + /// @param salePrice - the price of each token is being offered for the collection + /// @param expires - UNIX timestamp, the offer could be accepted before expires + /// @param supportedToken - contract addresses of supported token + /// Buyer wants to purchase items with supported token + /// Requirements: + /// - The caller must have enough supported tokens, and has approved the contract a sufficient amount + /// - `salePrice` must not be zero + /// - `amount` must not be zero + /// - `expires` must be valid + /// - Must emit an {UpdateCollectionOffer} event + function makeCollectionOffer(uint256 amount, uint256 salePrice, uint64 expires, address supportedToken) external; + + /// @notice Accepts collection offer and transfers the token to the buyer + /// @dev `salePrice` and `supportedToken` must match the expected purchase price and token to prevent front-running attacks + /// When the trading is completed, the `amount` of NFTs the buyer wants to purchase needs to be reduced by 1 + /// @param tokenId - identifier of the token being offered + /// @param salePrice - the price the token is being offered for + /// @param supportedToken - contract addresses of supported token + /// @param buyer - address of who wants to buy the token + /// Requirements: + /// - `tokenId` must exist and and be offered for + /// - Caller must be owner, authorised operators or approved address of the token + /// - Must emit a {Purchased} event + function acceptCollectionOffer(uint256 tokenId, uint256 salePrice, address supportedToken, address buyer) external; + + /// @notice Accepts collection offer and transfers the token to the buyer + /// @dev `salePrice` and `supportedToken` must match the expected purchase price and token to prevent front-running attacks + /// When the trading is completed, the `amount` of NFTs the buyer wants to purchase needs to be reduced by 1 + /// @param tokenId - identifier of the token being offered + /// @param salePrice - the price the token is being offered for + /// @param supportedToken - contract addresses of supported token + /// @param buyer - address of who wants to buy the token + /// @param benchmarkPrice - additional price parameter, may be used when calculating royalties + /// Requirements: + /// - `tokenId` must exist and and be offered for + /// - Caller must be owner, authorised operators or approved address of the token + /// - Must emit a {Purchased} event + function acceptCollectionOffer(uint256 tokenId, uint256 salePrice, address supportedToken, address buyer, uint256 benchmarkPrice) external; -The `delistItem(uint256 tokenId)` function MAY be implemented as `public` or `external`. + /// @notice Removes the offer for the collection + /// Requirements: + /// - Caller must be the offerer + /// - Must emit an {UpdateCollectionOffer} event + function cancelCollectionOffer() external; + + /// @notice Returns the offer for `tokenId` maked by `buyer` + /// @dev The zero amount indicates there is no offer + /// The zero sale price indicates there is no offer + /// The zero expires indicates that there is no offer + /// @param buyer address of who wants to buy the token + /// @return the specified offer (amount, sale price, expires, supported token) + function getCollectionOffer(address buyer) external view returns (uint256, uint256, uint64, address); +} +``` -The `buyItem(uint256 tokenId, uint256 salePrice)` function MUST be implemented as `payable` and MAY be implemented as `public` or `external`. +### Optional item offer extention -The `getListing(uint256 tokenId)` function MAY be implemented as `pure` or `view`. +```solidity +/// The item offer extension is OPTIONAL for ERC-6105 smart contracts. This allows smart contract to support item offer functionality. +interface IERC6105ItemOffer { + + /// @notice Emitted when a token receives an offer or an offer is canceled + /// @dev The zero `salePrice` indicates that the offer of the token is canceled + /// The zero `expires` indicates that the offer of the token is canceled + /// @param tokenId - identifier of the token being offered + /// @param from - address of who wants to buy the token + /// @param salePrice - the price the token is being offered for + /// @param expires - UNIX timestamp, the offer could be accepted before expires + /// @param supportedToken - contract addresses of supported token + /// Buyer wants to purchase item with supported token + event UpdateItemOffer( + uint256 indexed tokenId, + address indexed from, + uint256 salePrice, + uint64 expires, + address supportedToken + ); -The `UpdateListing` event MUST be emitted when a token is listed for sale or delisted. + /// @notice Create or update an offer for `tokenId` + /// @dev `salePrice` MUST NOT be set to zero + /// @param tokenId - identifier of the token being offered + /// @param salePrice - the price the token is being offered for + /// @param expires - UNIX timestamp, the offer could be accepted before expires + /// @param supportedToken - contract addresses of supported token + /// Buyer wants to purchase item with supported token + /// Requirements: + /// - `tokenId` must exist + /// - The caller must have enough supported tokens, and has approved the contract a sufficient amount + /// - `salePrice` must not be zero + /// - `expires` must be valid + /// - Must emit an {UpdateItemOffer} event. + function makeItemOffer(uint256 tokenId, uint256 salePrice, uint64 expires, address supportedToken) external; -The `Purchased` event MUST be emitted when a token is traded. + /// @notice Remove the offer for `tokenId` + /// @param tokenId - identifier of the token being canceled offer + /// Requirements: + /// - `tokenId` must exist and be offered for + /// - Caller must be the offerer + /// - Must emit an {UpdateItemOffer} event + function cancelItemOffer(uint256 tokenId) external; + + /// @notice Accept offer and transfer the token to the buyer + /// @dev `salePrice` and `supportedToken` must match the expected purchase price and token to prevent front-running attacks + /// When the trading is completed, the offer infomation needs to be removed + /// @param tokenId - identifier of the token being offered + /// @param salePrice - the price the token is being offered for + /// @param supportedToken - contract addresses of supported token + /// @param buyer - address of who wants to buy the token + /// Requirements: + /// - `tokenId` must exist and be offered for + /// - Caller must be owner, authorised operators or approved address of the token + /// - Must emit a {Purchased} event + function acceptItemOffer(uint256 tokenId, uint256 salePrice, address supportedToken, address buyer) external; + + /// @notice Accepts offer and transfers the token to the buyer + /// @dev `salePrice` and `supportedToken` must match the expected purchase price and token to prevent front-running attacks + /// When the trading is completed, the offer infomation needs to be removed + /// @param tokenId - identifier of the token being offered + /// @param salePrice - the price the token is being offered for + /// @param supportedToken - contract addresses of supported token + /// @param buyer - address of who wants to buy the token + /// @param benchmarkPrice - additional price parameter, may be used when calculating royalties + /// Requirements: + /// - `tokenId` must exist and be offered for + /// - Caller must be owner, authorised operators or approved address of the token + /// - Must emit a {Purchased} event + function acceptItemOffer(uint256 tokenId, uint256 salePrice, address supportedToken, address buyer, uint256 benchmarkPrice) external; + + /// @notice Return the offer for `tokenId` maked by `buyer` + /// @dev The zero sale price indicates there is no offer + /// The zero expires indicates that there is no offer + /// @param tokenId identifier of the token being queried + /// @param buyer address of who wants to buy the token + /// @return the specified offer (sale price, expires, supported token) + function getItemOffer(uint256 tokenId, address buyer) external view returns (uint256, uint64, address); +} +``` ## Rationale @@ -172,11 +318,11 @@ Setting `expires` in the `listItem` function allows callers to better manage the Setting `supportedToken` in the `listItem` function allows the caller or contract owner to choose which tokens they want to accept, rather than being limited to a single token. -### Mandatory, but more diverse royalty scheme +The rationales of variable setting in the `acceptCollectionOffer` and `acceptItemOffer` functions are the same as described above. -Mandatory royalty can only be realized by writing the NFT trading function into its own contract. +### More diverse royalty schemes -Meanwhile, a choice of A or B is better than a choice of have or nothing. By introducing the parameter `benchmarkPrice` in the `listItem` function, the `_salePrice` in the `royaltyInfo(uint256 _tokenId, uint256 _salePrice)` function in the [ERC-2981](./eip-2981.md) interface can be changed to `taxablePrice`, making the ERC-2981 royalty scheme more diverse. Here are several examples of royalty schemes: +By introducing the parameter `benchmarkPrice` in the `listItem`, `acceptCollectionOffer` and `acceptItemOffer` functions, the `_salePrice` in the `royaltyInfo(uint256 _tokenId, uint256 _salePrice)` function in the [ERC-2981](./eip-2981.md) interface can be changed to `taxablePrice`, making the ERC-2981 royalty scheme more diverse. Here are several examples of royalty schemes: `(address royaltyRecipient, uint256 royalties) = royaltyInfo(tokenId, taxablePrice)` @@ -187,7 +333,7 @@ Meanwhile, a choice of A or B is better than a choice of have or nothing. By int ### Optional Blocklist -This standard provides a method for intermediary-free NFT trading, but it does not provide a way to prohibit NFTs from being traded on other intermediary platforms. If deemed necessary to better protect the interests of the project team and community, they may consider adding a blocklist to their implementation contracts to prevent NFTs from being traded on platforms that do not comply with the project’s royalty scheme. +Some viewpoints suggest that tokens should be prevented from trading on intermediary markets that do not comply with royalty schemes, but this standard only provides a functionality for non-intermediary NFT trading and does not offer a standardized interface to prevent tokens from trading on these markets. If deemed necessary to better protect the interests of the project team and community, they may consider adding a blocklist to their implementation contracts to prevent NFTs from being traded on platforms that do not comply with the project’s royalty scheme. ## Backwards Compatibility @@ -249,7 +395,7 @@ contract ERC6105 is ERC721, ERC2981, IERC6105, ReentrancyGuard{ listItem(tokenId, salePrice, expires, supportedToken, 0); } - /// @notice Create or update a listing for `tokenId` + /// @notice Create or update a listing for `tokenId` with `historicalPrice` /// @dev `price` MUST NOT be set to zero /// @param tokenId - identifier of the token being listed /// @param salePrice - the price the token is being sold for @@ -271,10 +417,11 @@ contract ERC6105 is ERC721, ERC2981, IERC6105, ReentrancyGuard{ require(expires > block.timestamp, "ERC6105: invalid expires"); require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC6105: caller is not owner nor approved"); - _listItem(tokenId, salePrice, tokenOwner, expires, supportedToken, historicalPrice); + _listings[tokenId] = Listing(salePrice, expires, supportedToken, historicalPrice); + emit UpdateListing(tokenId, tokenOwner, salePrice, expires, supportedToken, historicalPrice); } - /// @notice Removes the listing for `tokenId` + /// @notice Remove the listing for `tokenId` /// @param tokenId - identifier of the token being listed function delistItem(uint256 tokenId) external virtual{ require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC6105: caller is not owner nor approved"); @@ -283,17 +430,18 @@ contract ERC6105 is ERC721, ERC2981, IERC6105, ReentrancyGuard{ _removeListing(tokenId); } - /// @notice Buys a token and transfers it to the caller - /// @dev Must check if `salePrice` matches the expected purchase price to prevent front-running attacks + /// @notice Buy a token and transfers it to the caller + /// @dev `salePrice` and `supportedToken` must match the expected purchase price and token to prevent front-running attacks /// @param tokenId - identifier of the token being purchased /// @param salePrice - the price the token is being sold for - function buyItem(uint256 tokenId, uint256 salePrice) external nonReentrant payable virtual{ + /// @param supportedToken - contract addresses of supported token or zero address + function buyItem(uint256 tokenId, uint256 salePrice, address supportedToken) external nonReentrant payable virtual{ address tokenOwner = ownerOf(tokenId); address buyer = msg.sender; uint256 historicalPrice = _listings[tokenId].historicalPrice; - address supportedToken = _listings[tokenId].supportedToken; require(salePrice == _listings[tokenId].salePrice, "ERC6105: inconsistent prices"); + require(supportedToken == _listings[tokenId].supportedToken,"ERC6105: inconsistent tokens"); require(_isForSale(tokenId), "ERC6105: invalid listing"); /// @dev Handle royalties @@ -301,13 +449,13 @@ contract ERC6105 is ERC721, ERC2981, IERC6105, ReentrancyGuard{ uint256 payment = salePrice - royalties; if(supportedToken == address(0)){ - require(msg.value == salePrice, "ERC6105: incorrect price"); + require(msg.value == salePrice, "ERC6105: incorrect value"); _processSupportedTokenPayment(royalties, buyer, royaltyRecipient, address(0)); _processSupportedTokenPayment(payment, buyer, tokenOwner, address(0)); } else{ - uint256 amount = IERC20(supportedToken).allowance(buyer, address(this)); - require (amount >= salePrice, "ERC6105: insufficient allowance"); + uint256 num = IERC20(supportedToken).allowance(buyer, address(this)); + require (num >= salePrice, "ERC6105: insufficient allowance"); _processSupportedTokenPayment(royalties, buyer, royaltyRecipient, supportedToken); _processSupportedTokenPayment(payment, buyer, tokenOwner, supportedToken); } @@ -316,7 +464,7 @@ contract ERC6105 is ERC721, ERC2981, IERC6105, ReentrancyGuard{ emit Purchased(tokenId, tokenOwner, buyer, salePrice, supportedToken, royalties); } - /// @notice Returns the listing for `tokenId` + /// @notice Return the listing for `tokenId` /// @dev The zero sale price indicates that the token is not for sale /// The zero expires indicates that the token is not for sale /// The zero supported token address indicates that the supported token is ETH @@ -335,29 +483,7 @@ contract ERC6105 is ERC721, ERC2981, IERC6105, ReentrancyGuard{ } } - - /// @dev Create or update a listing for `tokenId`. - /// @param tokenId - identifier of the token being listed - /// @param salePrice - 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 supportedToken - contract addresses of supported ERC20 token or zero address - /// The zero address indicates that the supported token is ETH - /// Buyer needs to purchase item with supported token - /// @param historicalPrice - The price at which the seller last bought this token - function _listItem( - uint256 tokenId, - uint256 salePrice, - address tokenOwner, - uint64 expires, - address supportedToken, - uint256 historicalPrice - ) internal virtual{ - _listings[tokenId] = Listing(salePrice, expires, supportedToken, historicalPrice); - emit UpdateListing(tokenId, tokenOwner, salePrice, expires, supportedToken, historicalPrice); - } - - /// @dev Removes the listing for `tokenId` + /// @dev Remove the listing for `tokenId` /// @param tokenId - identifier of the token being delisted function _removeListing(uint256 tokenId) internal virtual{ address tokenOwner = ownerOf(tokenId); @@ -393,7 +519,7 @@ contract ERC6105 is ERC721, ERC2981, IERC6105, ReentrancyGuard{ return(royaltyRecipient, royalties); } - /// @dev Processes a `supportedToken` of `amount` payment to `recipient`. + /// @dev Process a `supportedToken` of `amount` payment to `recipient`. /// @param amount - the amount to send /// @param from - the payment payer /// @param recipient - the payment recipient @@ -433,9 +559,11 @@ contract ERC6105 is ERC721, ERC2981, IERC6105, ReentrancyGuard{ ## Security Considerations -The `buyItem(uint256 tokenId, uint256 salePrice)` function has a potential front-running risk. Make sure to check that `salePrice` matches the expected purchase price to prevent front-running attacks. +The `buyItem` function, as well as the `acceptCollectionOffer` and `acceptItemOffer` functions, has a potential front-running risk. Must check that `salePrice` and `supportedToken` match the expected price and token to prevent front-running attacks + +There is a potential re-entrancy risk with the `acceptCollectionOffer` and `acceptItemOffer` functions. Make sure to obey the checks, effects, interactions pattern or use a reentrancy guard. -If a buyer uses [ERC-20](./eip-20.md) tokens to purchase an NFT, he needs to first call the `approve(address spender, uint256 amount)` function of the ERC-20 token to grant the NFT contract access to a certain `amount` of tokens. Please make sure to authorize an appropriate `amount`. +If a buyer uses [ERC-20](./eip-20.md) tokens to purchase an NFT, the buyer needs to first call the `approve(address spender, uint256 amount)` function of the ERC-20 token to grant the NFT contract access to a certain `amount` of tokens. Please make sure to authorize an appropriate `amount`. Furthermore, caution is advised when dealing with non-audited contracts. ## Copyright