// SPDX-License-Identifier: MIT pragma solidity 0.8.30; import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; interface IPIDCollection is IERC721 { function totalMinted() external view returns (uint256); } /// @notice Native-BNB marketplace restricted to one immutable Genesis Workers collection. contract PIDMarket is ReentrancyGuard { using EnumerableSet for EnumerableSet.AddressSet; using EnumerableSet for EnumerableSet.UintSet; uint256 public constant FEE_BPS = 500; IPIDCollection public immutable collection; address public immutable feeRecipient; uint256 public totalVolume; uint256 private revision; struct Listing { address seller; uint96 price; uint64 expiry; uint256 orderId; } struct Offer { uint96 price; uint64 expiry; uint256 orderId; } struct Item { uint256 tokenId; address owner; Listing listing; } struct Bid { address bidder; Offer offer; } struct AccountBid { uint256 tokenId; Offer offer; } struct Trade { uint256 tokenId; address seller; address buyer; uint96 price; uint64 time; bool viaOffer; } mapping(uint256 => Listing) public listings; mapping(uint256 => mapping(address => Offer)) public offers; mapping(uint256 => EnumerableSet.AddressSet) private bidders; mapping(address => EnumerableSet.UintSet) private bidTokens; mapping(address => uint256) public credits; Trade[] private history; event Listed(uint256 indexed tokenId, address indexed seller, uint256 price, uint64 expiry, uint256 orderId); event Unlisted(uint256 indexed tokenId, address indexed seller, uint256 orderId); event Offered(uint256 indexed tokenId, address indexed bidder, uint256 price, uint64 expiry, uint256 orderId); event OfferCancelled(uint256 indexed tokenId, address indexed bidder, uint256 orderId); event Traded(uint256 indexed tokenId, address indexed seller, address indexed buyer, uint256 price, uint256 fee, bool viaOffer); event Withdrawn(address indexed claimant, address indexed recipient, uint256 value); error InvalidOrder(); error NotSeller(); error StaleOrder(); error SelfTrade(); error InvalidPage(); error PaymentFailed(); constructor(address pid, address treasury) { if (pid.code.length == 0 || treasury == address(0) || treasury == address(this)) revert InvalidOrder(); collection = IPIDCollection(pid); feeRecipient = treasury; } function validateTerms(uint256 price, uint64 expiry) private view { if (price == 0 || price > type(uint96).max || expiry <= block.timestamp) revert InvalidOrder(); } function list(uint256 tokenId, uint96 price, uint64 expiry) external nonReentrant { validateTerms(price, expiry); if (listings[tokenId].seller != address(0) || collection.ownerOf(tokenId) != msg.sender) revert NotSeller(); collection.transferFrom(msg.sender, address(this), tokenId); setListing(tokenId, price, expiry); } function setListing(uint256 tokenId, uint96 price, uint64 expiry) private { uint256 id = ++revision; listings[tokenId] = Listing(msg.sender, price, expiry, id); emit Listed(tokenId, msg.sender, price, expiry, id); } function reprice(uint256 tokenId, uint256 expectedOrder, uint96 price, uint64 expiry) external nonReentrant { validateTerms(price, expiry); requireSellerOrder(tokenId, expectedOrder); setListing(tokenId, price, expiry); } function requireSellerOrder(uint256 tokenId, uint256 expectedOrder) private view { Listing storage order = listings[tokenId]; if (order.seller != msg.sender) revert NotSeller(); if (order.orderId != expectedOrder) revert StaleOrder(); } function cancelListing(uint256 tokenId, uint256 expectedOrder) external nonReentrant { requireSellerOrder(tokenId, expectedOrder); delete listings[tokenId]; collection.transferFrom(address(this), msg.sender, tokenId); emit Unlisted(tokenId, msg.sender, expectedOrder); } function buy(uint256 tokenId, uint256 expectedOrder) external payable nonReentrant { Listing memory order = listings[tokenId]; if (order.orderId != expectedOrder || order.seller == address(0)) revert StaleOrder(); if (order.expiry <= block.timestamp || msg.value != order.price) revert InvalidOrder(); if (order.seller == msg.sender) revert SelfTrade(); delete listings[tokenId]; settle(tokenId, order.seller, msg.sender, order.price, true, false); } function makeOffer(uint256 tokenId, uint64 expiry) external payable nonReentrant { validateTerms(msg.value, expiry); address owner = collection.ownerOf(tokenId); if (owner == msg.sender || listings[tokenId].seller == msg.sender) revert SelfTrade(); Offer memory previous = offers[tokenId][msg.sender]; if (previous.orderId != 0) credits[msg.sender] += previous.price; uint256 id = ++revision; offers[tokenId][msg.sender] = Offer(uint96(msg.value), expiry, id); bidders[tokenId].add(msg.sender); bidTokens[msg.sender].add(tokenId); emit Offered(tokenId, msg.sender, msg.value, expiry, id); } function cancelOffer(uint256 tokenId, uint256 expectedOrder) external nonReentrant { Offer memory bid = takeOffer(tokenId, msg.sender, expectedOrder); credits[msg.sender] += bid.price; emit OfferCancelled(tokenId, msg.sender, expectedOrder); } function takeOffer(uint256 tokenId, address bidder, uint256 expectedOrder) private returns (Offer memory bid) { bid = offers[tokenId][bidder]; if (bid.orderId == 0 || bid.orderId != expectedOrder) revert StaleOrder(); delete offers[tokenId][bidder]; bidders[tokenId].remove(bidder); bidTokens[bidder].remove(tokenId); } function acceptOffer(uint256 tokenId, address bidder, uint256 expectedOrder) external nonReentrant { Offer memory bid = takeOffer(tokenId, bidder, expectedOrder); if (bid.expiry <= block.timestamp) revert InvalidOrder(); if (bidder == msg.sender) revert SelfTrade(); Listing memory ask = listings[tokenId]; bool escrowed = ask.seller != address(0); if ((escrowed ? ask.seller : collection.ownerOf(tokenId)) != msg.sender) revert NotSeller(); if (escrowed) delete listings[tokenId]; settle(tokenId, msg.sender, bidder, bid.price, escrowed, true); } function settle(uint256 tokenId, address seller, address buyer, uint96 price, bool escrowed, bool viaOffer) private { uint256 fee = uint256(price) * FEE_BPS / 10000; credits[feeRecipient] += fee; credits[seller] += price - fee; totalVolume += price; history.push(Trade(tokenId, seller, buyer, price, uint64(block.timestamp), viaOffer)); collection.safeTransferFrom(escrowed ? address(this) : seller, buyer, tokenId); emit Traded(tokenId, seller, buyer, price, fee, viaOffer); } function withdraw(address payable recipient) external nonReentrant { uint256 amount = credits[msg.sender]; if (recipient == address(0) || recipient == address(this) || amount == 0) revert InvalidOrder(); credits[msg.sender] = 0; (bool sent,) = recipient.call{value: amount}(""); if (!sent) revert PaymentFailed(); emit Withdrawn(msg.sender, recipient, amount); } function pageSize(uint256 total, uint256 offset, uint256 limit) private pure returns (uint256) { if (limit == 0 || limit > 64) revert InvalidPage(); if (offset >= total) return 0; return limit < total - offset ? limit : total - offset; } /// @notice Genesis token IDs are sequential from 1. Listed items report their seller as beneficial owner. function itemsPage(uint256 offset, uint256 limit) external view returns (Item[] memory result) { result = new Item[](pageSize(collection.totalMinted(), offset, limit)); for (uint256 i; i < result.length; ++i) { uint256 id = offset + i + 1; Listing memory ask = listings[id]; result[i] = Item(id, ask.seller == address(0) ? collection.ownerOf(id) : ask.seller, ask); } } function bidsPage(uint256 tokenId, uint256 offset, uint256 limit) external view returns (Bid[] memory result, uint256 total) { total = bidders[tokenId].length(); result = new Bid[](pageSize(total, offset, limit)); for (uint256 i; i < result.length; ++i) { address bidder = bidders[tokenId].at(offset + i); result[i] = Bid(bidder, offers[tokenId][bidder]); } } function accountBidsPage(address bidder, uint256 offset, uint256 limit) external view returns (AccountBid[] memory result, uint256 total) { total = bidTokens[bidder].length(); result = new AccountBid[](pageSize(total, offset, limit)); for (uint256 i; i < result.length; ++i) { uint256 tokenId = bidTokens[bidder].at(offset + i); result[i] = AccountBid(tokenId, offers[tokenId][bidder]); } } function tradesPage(uint256 offset, uint256 limit) external view returns (Trade[] memory result, uint256 total) { total = history.length; result = new Trade[](pageSize(total, offset, limit)); for (uint256 i; i < result.length; ++i) result[i] = history[total - offset - i - 1]; } }