// SPDX-License-Identifier: MIT pragma solidity 0.8.27; interface IERC721Minimal { function ownerOf(uint256 tokenId) external view returns (address); function getApproved(uint256 tokenId) external view returns (address); function safeTransferFrom(address from, address to, uint256 tokenId) external; } /// @title Minimal 0% Hypurr fixed-price ask settlement. /// @notice Hypurr-only, native-HYPE, no admin, proxy, fees, pause, upgrade, or /// admin withdraw path. Buyer pays exactly the ask price; seller claims the full amount. /// @dev LLM-review notes: Non-escrow: this contract checks only current ownerOf/getApproved /// and cannot observe whether the token moved after listing. /// Residual risk: an uncancelled ask can revive if the recorded seller revokes /// then reapproves, or transfers away, reacquires, and reapproves before expiry. /// Sellers should cancel/clear old asks before reapproving the same token. /// Frontends should not treat approval revocation as cancellation; block /// reapproval/listing while any stored ask exists. /// Mitigations: 1-day max expiry, token-level approval only, no in-place updates, /// and permissionless stale clearing. Full prevention requires escrow or NFT /// transfer-nonce/wrapper support; Hypurr exposes no ERC-4494 or transfer nonce. /// Seller proceeds are pull-based and claimable only by the credited seller. /// The listing is deleted and proceeds are credited before NFT transfer; any /// transfer revert or final owner-check failure rolls both state changes back. /// Do not transfer NFTs directly here; no rescue. /// Constructor code-length only rejects no-code NFT addresses; deployment checks /// must still verify the canonical Hypurr NFT address and behavior. contract Marketplace { struct Listing { address seller; uint256 price; uint256 expiresAt; } IERC721Minimal public immutable nft; uint256 public constant MAX_LISTING_DURATION = 1 days; bool private locked; mapping(uint256 => Listing) public listings; mapping(address => uint256) public proceeds; event ListingCreated(uint256 indexed tokenId, address indexed seller, uint256 price, uint256 expiresAt); event ListingCancelled(uint256 indexed tokenId, address indexed seller); event ListingInvalidated(uint256 indexed tokenId, address indexed seller); event Sale(uint256 indexed tokenId, address indexed buyer, address indexed seller, uint256 price); event ProceedsClaimed(address indexed seller, address indexed recipient, uint256 amount); modifier nonReentrant() { require(!locked, "Reentrant call"); locked = true; _; locked = false; } constructor(address nftContract) { require(nftContract != address(0), "NFT required"); require(nftContract.code.length > 0, "NFT must be contract"); nft = IERC721Minimal(nftContract); } // slither-disable-start timestamp function listToken(uint256 tokenId, uint256 price, uint256 expiresAt) external nonReentrant { require(listings[tokenId].price == 0, "Listing exists"); require(price > 0, "Price must be greater than zero"); require(expiresAt > block.timestamp, "Listing expired"); require(expiresAt <= block.timestamp + MAX_LISTING_DURATION, "Listing too long"); require(nft.ownerOf(tokenId) == msg.sender, "Not token owner"); require(nft.getApproved(tokenId) == address(this), "Marketplace not approved"); listings[tokenId] = Listing({seller: msg.sender, price: price, expiresAt: expiresAt}); emit ListingCreated(tokenId, msg.sender, price, expiresAt); } function cancelListing(uint256 tokenId) external nonReentrant { Listing memory listing = listings[tokenId]; require(listing.seller == msg.sender, "Not the seller"); delete listings[tokenId]; emit ListingCancelled(tokenId, msg.sender); } function clearStaleListing(uint256 tokenId) external nonReentrant { Listing memory listing = listings[tokenId]; require(listing.price > 0, "Token not listed"); require(_listingStale(tokenId, listing), "Listing still valid"); delete listings[tokenId]; emit ListingInvalidated(tokenId, listing.seller); } function buyToken( uint256 tokenId, address expectedSeller, uint256 expectedPrice, uint256 expectedExpiresAt, uint256 deadline ) external payable nonReentrant { Listing memory listing = listings[tokenId]; require(listing.price > 0, "Token not listed"); require(listing.seller == expectedSeller, "Seller changed"); require(listing.price == expectedPrice, "Price changed"); require(listing.expiresAt == expectedExpiresAt, "Expiry changed"); require(msg.sender != listing.seller, "Seller cannot buy own listing"); require(msg.value == expectedPrice, "Incorrect payment amount"); require(block.timestamp <= deadline, "Transaction expired"); require(block.timestamp <= listing.expiresAt, "Listing expired"); require(nft.ownerOf(tokenId) == listing.seller, "Seller no longer owner"); require(nft.getApproved(tokenId) == address(this), "Marketplace not approved"); delete listings[tokenId]; proceeds[listing.seller] += msg.value; nft.safeTransferFrom(listing.seller, msg.sender, tokenId); require(nft.ownerOf(tokenId) == msg.sender, "NFT transfer failed"); emit Sale(tokenId, msg.sender, listing.seller, msg.value); } function claimProceeds(address payable recipient) external nonReentrant { require(recipient != address(0), "Recipient required"); uint256 amount = proceeds[msg.sender]; require(amount > 0, "No proceeds"); proceeds[msg.sender] = 0; // slither-disable-next-line low-level-calls (bool success,) = recipient.call{value: amount}(""); require(success, "Proceeds claim failed"); emit ProceedsClaimed(msg.sender, recipient, amount); } function _listingStale(uint256 tokenId, Listing memory listing) private view returns (bool) { if (block.timestamp > listing.expiresAt) return true; try nft.ownerOf(tokenId) returns (address owner) { if (owner != listing.seller) return true; // Defensive for nonstandard ERC-721s: approval lookup failures are stale. try nft.getApproved(tokenId) returns (address approved) { return approved != address(this); } catch { return true; } } catch { return true; } } // slither-disable-end timestamp }