// SPDX-License-Identifier: MIT /* * Adapted from PancakeSwap's NFT MarketPlace. */ pragma solidity ^0.8.0; pragma abicoder v2; import "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/utils/Address.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; interface IERC721EIP2981 is IERC721{ function royaltyInfo( uint256 _tokenId, uint256 _salePrice ) external view returns ( address receiver, uint256 royaltyAmount ); } contract PunksPlace is ERC721Holder, Ownable, ReentrancyGuard { using EnumerableSet for EnumerableSet.AddressSet; using EnumerableSet for EnumerableSet.UintSet; using Address for address; using SafeERC20 for IERC20; enum CollectionStatus { Pending, Open, Close } uint256 public constant TOTAL_MAX_FEE = 1000; // 10% of a sale bytes4 private constant _INTERFACE_ID_ERC2981 = 0x2a55205a; address public treasuryAddress; uint256 constant public MAX_ROYALTY = 1000; uint256 constant public DENOMINATOR = 10000; mapping(address => uint256) public pendingRevenue; // For creator/treasury to claim mapping(address => uint256) public collectionToVolume; EnumerableSet.AddressSet private _collectionAddressSet; mapping(address => mapping(uint256 => Ask)) private _askDetails; // Ask details (price + seller address) for a given collection and a tokenId mapping(address => EnumerableSet.UintSet) private _askTokenIds; // Set of tokenIds for a collection mapping(address => Collection) private _collections; // Details about the collections mapping(address => mapping(uint256 => uint256)) private _lastPrice; // Last price a given item was traded for mapping(address => mapping(address => EnumerableSet.UintSet)) private _tokenIdsOfSellerForCollection; struct Ask { address seller; // address of the seller uint256 price; // price of the token uint256 listingTime; // listing timestamp } struct Collection { CollectionStatus status; // status of the collection address creatorAddress; // address of the creator uint256 tradingFee; // trading fee (100 = 1%, 500 = 5%, 5 = 0.05%) uint256 creatorFee; // creator fee (100 = 1%, 500 = 5%, 5 = 0.05%) bool eip2981ish; // in the case where the collection supports EIP-2981, // which version does it support } // Ask order is cancelled event AskCancel(address indexed collection, address indexed seller, uint256 indexed tokenId); // Ask order is created event AskNew(address indexed collection, address indexed seller, uint256 indexed tokenId, uint256 askPrice); // Ask order is updated event AskUpdate(address indexed collection, address indexed seller, uint256 indexed tokenId, uint256 askPrice); // Collection is closed for trading and new listings event CollectionClose(address indexed collection); // New collection is added event CollectionNew( address indexed collection, address indexed creator, uint256 tradingFee, uint256 creatorFee, bool eip2981ish ); // Existing collection is updated event CollectionUpdate( address indexed collection, address indexed creator, uint256 tradingFee, uint256 creatorFee, bool eip2981ish ); // Admin and Treasury Addresses are updated event NewTreasuryAddress(address indexed treasury); // Recover NFT tokens sent by accident event NonFungibleTokenRecovery(address indexed token, uint256 indexed tokenId); // Pending revenue is claimed event RevenueClaim(address indexed claimer, uint256 amount); // Recover ERC20 tokens sent by accident event TokenRecovery(address indexed token, uint256 amount); // Ask order is matched by a trade event Trade( address indexed collection, uint256 indexed tokenId, address indexed seller, address buyer, uint256 askPrice, uint256 netPrice ); /** * @notice Constructor * @param _treasuryAddress: address of the treasury */ constructor( address _treasuryAddress ) { require(_treasuryAddress != address(0), "Operations: Treasury address cannot be zero"); treasuryAddress = _treasuryAddress; } /** * @notice Cancel existing ask order * @param _collection: contract address of the NFT * @param _tokenId: tokenId of the NFT */ function cancelAskOrder(address _collection, uint256 _tokenId) external nonReentrant { // Verify the sender has listed it require(_tokenIdsOfSellerForCollection[msg.sender][_collection].contains(_tokenId), "Order: Token not listed"); // Adjust the information _tokenIdsOfSellerForCollection[msg.sender][_collection].remove(_tokenId); delete _askDetails[_collection][_tokenId]; _askTokenIds[_collection].remove(_tokenId); // Transfer the NFT back to the user IERC721(_collection).transferFrom(address(this), address(msg.sender), _tokenId); // Emit event emit AskCancel(_collection, msg.sender, _tokenId); } /** * @notice Claim pending revenue (treasury or creators) */ function claimPendingRevenue() external payable nonReentrant { uint256 revenueToClaim = pendingRevenue[msg.sender]; require(revenueToClaim != 0, "Claim: Nothing to claim"); pendingRevenue[msg.sender] = 0; // payable(msg.sender).transfer(revenueToClaim); (bool success,) = payable(msg.sender).call{value: revenueToClaim}(""); require(success, "Transfer failed"); emit RevenueClaim(msg.sender, revenueToClaim); } /** * @notice Create ask order * @param _collection: contract address of the NFT * @param _tokenId: tokenId of the NFT * @param _askPrice: price for listing (in wei) */ function createAskOrder( address _collection, uint256 _tokenId, uint256 _askPrice ) external nonReentrant { // Verify price is not too low require(_askPrice > 0, "Order: Price should be greater than 0"); // Verify collection is accepted require(_collections[_collection].status == CollectionStatus.Open, "Collection: Not for listing"); // Transfer NFT to this contract IERC721(_collection).safeTransferFrom(address(msg.sender), address(this), _tokenId); // Adjust the information _tokenIdsOfSellerForCollection[msg.sender][_collection].add(_tokenId); _askDetails[_collection][_tokenId] = Ask({seller: msg.sender, price: _askPrice, listingTime: block.timestamp}); // Add tokenId to the askTokenIds set _askTokenIds[_collection].add(_tokenId); // Emit event emit AskNew(_collection, msg.sender, _tokenId, _askPrice); } /** * @notice Modify existing ask order * @param _collection: contract address of the NFT * @param _tokenId: tokenId of the NFT * @param _newPrice: new price for listing (in wei) */ function modifyAskOrder( address _collection, uint256 _tokenId, uint256 _newPrice ) external nonReentrant { // Verify new price is not too low/high require(_newPrice > 0, "Order: Price should be greater than 0"); // Verify collection is accepted require(_collections[_collection].status == CollectionStatus.Open, "Collection: Not for listing"); // Verify the sender has listed it require(_tokenIdsOfSellerForCollection[msg.sender][_collection].contains(_tokenId), "Order: Token not listed"); // Adjust the information _askDetails[_collection][_tokenId].price = _newPrice; _askDetails[_collection][_tokenId].listingTime = block.timestamp; // Emit event emit AskUpdate(_collection, msg.sender, _tokenId, _newPrice); } /** * @notice Buy token by matching the price of an existing ask order * @param _collection: contract address of the NFT * @param _tokenId: tokenId of the NFT purchased */ function buyToken( address _collection, uint256 _tokenId ) external payable nonReentrant { require(_collections[_collection].status == CollectionStatus.Open, "Collection: Not for trading"); require(_askTokenIds[_collection].contains(_tokenId), "Buy: Not for sale"); Ask memory askOrder = _askDetails[_collection][_tokenId]; // Front-running protection require(msg.value == askOrder.price, "Buy: Incorrect price"); require(msg.sender != askOrder.seller, "Buy: Buyer cannot be seller"); // Calculate the net price (collected by seller), trading fee (collected by treasury), creator fee (collected by creator) (uint256 netPrice, uint256 tradingFee, uint256 creatorFee) = _calculatePriceAndFeesForCollection( _collection, msg.value ); // Handle royalties with ERC-165-compliant contracts // if(IERC721EIP2981(_collection).supportsInterface(_INTERFACE_ID_ERC2981)){ // (address _royaltiesReceiver, uint256 _priceAfterRoyalties) = // IERC721EIP2981(_collection).royaltyInfo(_tokenId, msg.value); // uint256 _royalties = msg.value - _priceAfterRoyalties; // netPrice -= _royalties; // (bool success_, ) = payable(_royaltiesReceiver).call{value:_royalties}(""); // require(success_, "Transfer failed"); // } // Verify if NFT collection implements EIP-2981ish try IERC721EIP2981(_collection).royaltyInfo(_tokenId, msg.value) returns (address receiver, uint256 royaltyAmount){ uint256 _royalties; if(_collections[_collection].eip2981ish){ _royalties = msg.value - royaltyAmount; // royaltyAmount = price after royalty }else{ _royalties = royaltyAmount; // royaltyAmount = royalty } require((_royalties * DENOMINATOR) / netPrice < MAX_ROYALTY, "Royalty too high"); netPrice -= _royalties; (bool success_, ) = payable(receiver).call{value:_royalties}(""); require(success_, "Transfer failed"); } catch { } // update total volume collectionToVolume[_collection] += askOrder.price; // Update storage information _lastPrice[_collection][_tokenId] = askOrder.price; _tokenIdsOfSellerForCollection[askOrder.seller][_collection].remove(_tokenId); delete _askDetails[_collection][_tokenId]; _askTokenIds[_collection].remove(_tokenId); // Transfer CELO (bool success, ) = payable(askOrder.seller).call{value:netPrice}(""); require(success, "Transfer failed"); // Update pending revenues for treasury/creator (if any!) if (creatorFee != 0) { pendingRevenue[_collections[_collection].creatorAddress] += creatorFee; } // Update trading fee if not equal to 0 if (tradingFee != 0) { pendingRevenue[treasuryAddress] += tradingFee; } // Transfer NFT to buyer IERC721(_collection).safeTransferFrom(address(this), address(msg.sender), _tokenId); // Emit event emit Trade(_collection, _tokenId, askOrder.seller, msg.sender, msg.value, netPrice); } /** * @notice Add a new collection * @param _collection: collection address * @param _creator: creator address (must be 0x00 if none) * @param _tradingFee: trading fee (100 = 1%, 500 = 5%, 5 = 0.05%) * @param _creatorFee: creator fee (100 = 1%, 500 = 5%, 5 = 0.05%, 0 if creator is 0x00) * @dev Callable by admin */ function addCollection( address _creator, address _collection, uint256 _tradingFee, uint256 _creatorFee, bool _eip2981ish ) external onlyOwner { require(!_collectionAddressSet.contains(_collection), "Operations: Collection already listed"); require(IERC721(_collection).supportsInterface(0x80ac58cd), "Operations: Not ERC721"); require( (_creatorFee == 0 && _creator == address(0)) || (_creatorFee != 0 && _creator != address(0)), "Operations: Creator parameters incorrect" ); // does not take into account potential royalties require(_tradingFee + _creatorFee <= TOTAL_MAX_FEE, "Operations: Sum of fee must inferior to TOTAL_MAX_FEE"); _collectionAddressSet.add(_collection); _collections[_collection] = Collection({ status: CollectionStatus.Open, creatorAddress: _creator, tradingFee: _tradingFee, creatorFee: _creatorFee, eip2981ish: _eip2981ish }); emit CollectionNew(_collection, _creator, _tradingFee, _creatorFee, _eip2981ish); } /** * @notice Allows the admin to close collection for trading and new listing * @param _collection: collection address * @dev Callable by admin */ function closeCollectionForTradingAndListing(address _collection) external onlyOwner { require(_collectionAddressSet.contains(_collection), "Operations: Collection not listed"); _collections[_collection].status = CollectionStatus.Close; _collectionAddressSet.remove(_collection); emit CollectionClose(_collection); } /** * @notice Modify collection characteristics * @param _collection: collection address * @param _creator: creator address (must be 0x00 if none) * @param _tradingFee: trading fee (100 = 1%, 500 = 5%, 5 = 0.05%) * @param _creatorFee: creator fee (100 = 1%, 500 = 5%, 5 = 0.05%, 0 if creator is 0x00) * @dev Callable by admin */ function modifyCollection( address _collection, address _creator, uint256 _tradingFee, uint256 _creatorFee, bool _eip2981ish ) external onlyOwner { require(_collectionAddressSet.contains(_collection), "Operations: Collection not listed"); require( (_creatorFee == 0 && _creator == address(0)) || (_creatorFee != 0 && _creator != address(0)), "Operations: Creator parameters incorrect" ); // Check if collection supports royalties require(_tradingFee + _creatorFee <= TOTAL_MAX_FEE, "Operations: Sum of fee must inferior to TOTAL_MAX_FEE"); _collections[_collection] = Collection({ status: CollectionStatus.Open, creatorAddress: _creator, tradingFee: _tradingFee, creatorFee: _creatorFee, eip2981ish: _eip2981ish }); emit CollectionUpdate(_collection, _creator, _tradingFee, _creatorFee, _eip2981ish); } /** * @notice Allows the owner to recover tokens sent to the contract by mistake * @param _token: token address * @dev Callable by owner */ function recoverFungibleTokens(address _token) external onlyOwner { uint256 amountToRecover = IERC20(_token).balanceOf(address(this)); require(amountToRecover != 0, "Operations: No token to recover"); IERC20(_token).safeTransfer(address(msg.sender), amountToRecover); emit TokenRecovery(_token, amountToRecover); } /** * @notice Allows the owner to recover NFTs sent to the contract by mistake * @param _token: NFT token address * @param _tokenId: tokenId * @dev Callable by owner */ function recoverNonFungibleToken(address _token, uint256 _tokenId) external onlyOwner nonReentrant { require(!_askTokenIds[_token].contains(_tokenId), "Operations: NFT not recoverable"); IERC721(_token).safeTransferFrom(address(this), address(msg.sender), _tokenId); emit NonFungibleTokenRecovery(_token, _tokenId); } /** * @notice Set admin address * @dev Only callable by owner * @param _treasuryAddress: address of the treasury */ function setTreasuryAddresse(address _treasuryAddress) external onlyOwner { require(_treasuryAddress != address(0), "Operations: Treasury address cannot be zero"); treasuryAddress = _treasuryAddress; emit NewTreasuryAddress(_treasuryAddress); } /** * @notice Check asks for an array of tokenIds in a collection * @param collection: address of the collection * @param tokenIds: array of tokenId */ function viewAsksByCollectionAndTokenIds(address collection, uint256[] calldata tokenIds) external view returns (bool[] memory statuses, Ask[] memory askInfo, uint256[] memory lastPrices) { uint256 length = tokenIds.length; statuses = new bool[](length); askInfo = new Ask[](length); lastPrices = new uint256[](length); for (uint256 i = 0; i < length; i++) { if (_askTokenIds[collection].contains(tokenIds[i])) { statuses[i] = true; } else { statuses[i] = false; } askInfo[i] = _askDetails[collection][tokenIds[i]]; lastPrices[i] = _lastPrice[collection][tokenIds[i]]; } return (statuses, askInfo, lastPrices); } /** * @notice View ask orders for a given collection across all sellers * @param collection: address of the collection * @param cursor: cursor * @param size: size of the response */ function viewAsksByCollection( address collection, uint256 cursor, uint256 size ) external view returns ( uint256[] memory tokenIds, Ask[] memory askInfo, uint256[] memory lastPrices, uint256 ) { uint256 length = size; if (length > _askTokenIds[collection].length() - cursor) { length = _askTokenIds[collection].length() - cursor; } tokenIds = new uint256[](length); askInfo = new Ask[](length); lastPrices = new uint256[](length); for (uint256 i = 0; i < length; i++) { tokenIds[i] = _askTokenIds[collection].at(cursor + i); askInfo[i] = _askDetails[collection][tokenIds[i]]; lastPrices[i] = _lastPrice[collection][tokenIds[i]]; } return (tokenIds, askInfo, lastPrices, cursor + length); } /** * @notice View ask orders for a given collection and a seller * @param collection: address of the collection * @param seller: address of the seller * @param cursor: cursor * @param size: size of the response */ function viewAsksByCollectionAndSeller( address collection, address seller, uint256 cursor, uint256 size ) external view returns ( uint256[] memory tokenIds, Ask[] memory askInfo, uint256[] memory lastPrices, uint256 ) { uint256 length = size; if (length > _tokenIdsOfSellerForCollection[seller][collection].length() - cursor) { length = _tokenIdsOfSellerForCollection[seller][collection].length() - cursor; } tokenIds = new uint256[](length); askInfo = new Ask[](length); lastPrices = new uint256[](length); for (uint256 i = 0; i < length; i++) { tokenIds[i] = _tokenIdsOfSellerForCollection[seller][collection].at(cursor + i); askInfo[i] = _askDetails[collection][tokenIds[i]]; lastPrices[i] = _lastPrice[collection][tokenIds[i]]; } return (tokenIds, askInfo, lastPrices, cursor + length); } /* * @notice View addresses and details for all the collections available for trading * @param cursor: cursor * @param size: size of the response */ function viewCollections(uint256 cursor, uint256 size) external view returns ( address[] memory collectionAddresses, Collection[] memory collectionDetails, uint256 ) { uint256 length = size; if (length > _collectionAddressSet.length() - cursor) { length = _collectionAddressSet.length() - cursor; } collectionAddresses = new address[](length); collectionDetails = new Collection[](length); for (uint256 i = 0; i < length; i++) { collectionAddresses[i] = _collectionAddressSet.at(cursor + i); collectionDetails[i] = _collections[collectionAddresses[i]]; } return (collectionAddresses, collectionDetails, cursor + length); } /** * @notice Calculate price and associated fees for a collection * @param collection: address of the collection * @param price: listed price */ function calculatePriceAndFeesForCollection(address collection, uint256 price) external view returns ( uint256 netPrice, uint256 tradingFee, uint256 creatorFee ) { if (_collections[collection].status != CollectionStatus.Open) { return (0, 0, 0); } return (_calculatePriceAndFeesForCollection(collection, price)); } /** * @notice Calculate price and associated fees for a collection * @param _collection: address of the collection * @param _askPrice: listed price */ function _calculatePriceAndFeesForCollection(address _collection, uint256 _askPrice) internal view returns ( uint256 netPrice, uint256 tradingFee, uint256 creatorFee ) { tradingFee = (_askPrice * _collections[_collection].tradingFee) / DENOMINATOR; creatorFee = (_askPrice * _collections[_collection].creatorFee) / DENOMINATOR; netPrice = _askPrice - tradingFee - creatorFee; return (netPrice, tradingFee, creatorFee); } }