// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import "@openzeppelin/contracts/interfaces/IERC2981.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; /** * @title AereNFTMarketplace * @notice On-chain NFT marketplace for AERE Network with EIP-2981 royalty support. * @dev Sellers list owned ERC-721 tokens for a fixed price in native AERE. * Buyers purchase atomically: protocol fee → fee recipient, royalty → creator, * remainder → seller. Buyers can also place expiring offers on any token. * Marketplace must be approved (setApprovalForAll or approve) before listing. */ contract AereNFTMarketplace is Ownable, ReentrancyGuard { uint96 public constant MAX_FEE_BPS = 1000; // 10% uint96 public protocolFeeBps = 250; // 2.5% address public feeRecipient; struct Listing { address seller; uint256 price; uint64 expiresAt; // 0 = never } struct Offer { address buyer; uint256 price; uint64 expiresAt; } // collection => tokenId => Listing mapping(address => mapping(uint256 => Listing)) public listings; // collection => tokenId => buyer => Offer mapping(address => mapping(uint256 => mapping(address => Offer))) public offers; // escrowed offer balances per buyer (refundable) mapping(address => uint256) public pendingWithdrawals; event Listed(address indexed collection, uint256 indexed tokenId, address indexed seller, uint256 price, uint64 expiresAt); event ListingCancelled(address indexed collection, uint256 indexed tokenId, address indexed seller); event Sold(address indexed collection, uint256 indexed tokenId, address seller, address indexed buyer, uint256 price); event OfferPlaced(address indexed collection, uint256 indexed tokenId, address indexed buyer, uint256 price, uint64 expiresAt); event OfferCancelled(address indexed collection, uint256 indexed tokenId, address indexed buyer); event OfferAccepted(address indexed collection, uint256 indexed tokenId, address seller, address indexed buyer, uint256 price); event ProtocolFeeUpdated(uint96 newFeeBps); event FeeRecipientUpdated(address newRecipient); constructor(address _feeRecipient) { require(_feeRecipient != address(0), "zero"); feeRecipient = _feeRecipient; } // ── Listings ──────────────────────────────────────────────── function list(address collection, uint256 tokenId, uint256 price, uint64 expiresAt) external { require(price > 0, "price=0"); IERC721 nft = IERC721(collection); require(nft.ownerOf(tokenId) == msg.sender, "not owner"); require( nft.getApproved(tokenId) == address(this) || nft.isApprovedForAll(msg.sender, address(this)), "not approved" ); listings[collection][tokenId] = Listing(msg.sender, price, expiresAt); emit Listed(collection, tokenId, msg.sender, price, expiresAt); } function cancelListing(address collection, uint256 tokenId) external { Listing memory l = listings[collection][tokenId]; require(l.seller == msg.sender, "not seller"); delete listings[collection][tokenId]; emit ListingCancelled(collection, tokenId, msg.sender); } function buy(address collection, uint256 tokenId) external payable nonReentrant { Listing memory l = listings[collection][tokenId]; require(l.seller != address(0), "not listed"); require(l.expiresAt == 0 || block.timestamp <= l.expiresAt, "expired"); require(msg.value == l.price, "bad value"); delete listings[collection][tokenId]; _settleSale(collection, tokenId, l.seller, msg.sender, l.price); emit Sold(collection, tokenId, l.seller, msg.sender, l.price); } // ── Offers ────────────────────────────────────────────────── function placeOffer(address collection, uint256 tokenId, uint64 expiresAt) external payable nonReentrant { require(msg.value > 0, "price=0"); require(expiresAt > block.timestamp, "expired"); Offer memory existing = offers[collection][tokenId][msg.sender]; if (existing.price > 0) { // refund prior offer to escrow pendingWithdrawals[msg.sender] += existing.price; } offers[collection][tokenId][msg.sender] = Offer(msg.sender, msg.value, expiresAt); emit OfferPlaced(collection, tokenId, msg.sender, msg.value, expiresAt); } function cancelOffer(address collection, uint256 tokenId) external nonReentrant { Offer memory o = offers[collection][tokenId][msg.sender]; require(o.price > 0, "no offer"); delete offers[collection][tokenId][msg.sender]; pendingWithdrawals[msg.sender] += o.price; emit OfferCancelled(collection, tokenId, msg.sender); } function acceptOffer(address collection, uint256 tokenId, address buyer) external nonReentrant { IERC721 nft = IERC721(collection); require(nft.ownerOf(tokenId) == msg.sender, "not owner"); Offer memory o = offers[collection][tokenId][buyer]; require(o.price > 0, "no offer"); require(block.timestamp <= o.expiresAt, "expired"); delete offers[collection][tokenId][buyer]; // active listing (if any) becomes invalid after transfer; clear it if (listings[collection][tokenId].seller == msg.sender) { delete listings[collection][tokenId]; } _settleSale(collection, tokenId, msg.sender, buyer, o.price); emit OfferAccepted(collection, tokenId, msg.sender, buyer, o.price); } function withdraw() external nonReentrant { uint256 amount = pendingWithdrawals[msg.sender]; require(amount > 0, "nothing"); pendingWithdrawals[msg.sender] = 0; (bool ok, ) = msg.sender.call{value: amount}(""); require(ok, "transfer"); } // ── Internal settlement ───────────────────────────────────── function _settleSale(address collection, uint256 tokenId, address seller, address buyer, uint256 price) internal { IERC721(collection).safeTransferFrom(seller, buyer, tokenId); uint256 fee = (price * protocolFeeBps) / 10000; uint256 royalty = 0; address royaltyReceiver = address(0); try IERC2981(collection).royaltyInfo(tokenId, price) returns (address r, uint256 amt) { if (r != address(0) && amt > 0 && amt + fee <= price) { royaltyReceiver = r; royalty = amt; } } catch { /* collection doesn't implement EIP-2981 */ } uint256 sellerProceeds = price - fee - royalty; if (fee > 0) _send(feeRecipient, fee); if (royalty > 0) _send(royaltyReceiver, royalty); _send(seller, sellerProceeds); } function _send(address to, uint256 amount) internal { (bool ok, ) = to.call{value: amount}(""); if (!ok) { // fall back to escrow so a malicious receiver can't block sales pendingWithdrawals[to] += amount; } } // ── Admin ─────────────────────────────────────────────────── function setProtocolFee(uint96 newFeeBps) external onlyOwner { require(newFeeBps <= MAX_FEE_BPS, "too high"); protocolFeeBps = newFeeBps; emit ProtocolFeeUpdated(newFeeBps); } function setFeeRecipient(address newRecipient) external onlyOwner { require(newRecipient != address(0), "zero"); feeRecipient = newRecipient; emit FeeRecipientUpdated(newRecipient); } }