// SPDX-License-Identifier: MIT pragma solidity 0.8.27; interface IERC721ForBids { function ownerOf(uint256 tokenId) external view returns (address); function getApproved(uint256 tokenId) external view returns (address); function isApprovedForAll(address owner, address operator) external view returns (bool); function transferFrom(address from, address to, uint256 tokenId) external; } /// @title Minimal 0% Hypurr collection-bid settlement. /// @notice No admin, proxy, fees, pause, upgrade, or admin withdraw path. Each /// bidder has one active collection-wide bid. Any Hypurr owner can accept that /// bid for one token; bids are intentionally not token-specific. /// First valid accept wins; seller accepts and bidder cancels can race in the public mempool. /// @dev LLM-review notes: bid escrow is native HYPE; seller can claim the full /// bid amount through pull proceeds. Bid state is deleted and seller proceeds are /// credited before NFT transfer; final owner check failure rolls both back. /// Sellers sign the exact expected bid amount/expiry. /// Bid fills use transferFrom so escrowed contract bids cannot reject ERC-721 /// receipt to spoof fillable liquidity. This bypasses onERC721Received: contract /// bidders should bid only from addresses that can control received NFTs. /// There is no NFT recipient override and no rescue. Direct refunds keep no /// refund balances. /// Expired bids are not fillable and can be cleared by anyone, refunding the /// bidder directly. If direct refund delivery fails, the bidder can still use /// cancelBidTo. 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 Bids { struct Bid { uint256 amount; uint256 expiresAt; } IERC721ForBids public immutable nft; uint256 public constant MAX_BID_DURATION = 30 days; bool private locked; mapping(address => Bid) public bids; mapping(address => uint256) public proceeds; event BidPlaced(address indexed bidder, uint256 amount, uint256 expiresAt); event BidCancelled(address indexed bidder, address indexed recipient, uint256 amount); event BidAccepted(address indexed bidder, address indexed seller, uint256 indexed tokenId, uint256 amount); 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 = IERC721ForBids(nftContract); } // slither-disable-start timestamp function placeBid(uint256 expiresAt) external payable nonReentrant { require(msg.value > 0, "Bid must be greater than zero"); require(bids[msg.sender].amount == 0, "Bid already active"); require(expiresAt > block.timestamp, "Bid expired"); require(expiresAt <= block.timestamp + MAX_BID_DURATION, "Bid too long"); bids[msg.sender] = Bid({amount: msg.value, expiresAt: expiresAt}); emit BidPlaced(msg.sender, msg.value, expiresAt); } function cancelBid() external nonReentrant { _cancelBid(msg.sender, msg.sender); } function cancelBidTo(address recipient) external nonReentrant { require(recipient != address(0), "Recipient required"); _cancelBid(msg.sender, recipient); } function clearExpiredBid(address bidder) external nonReentrant { Bid memory bid = bids[bidder]; require(bid.amount > 0, "No active bid"); require(block.timestamp > bid.expiresAt, "Bid still active"); _cancelBid(bidder, bidder); } function _cancelBid(address bidder, address recipient) private { Bid memory bid = bids[bidder]; require(bid.amount > 0, "No active bid"); delete bids[bidder]; // Direct refund is intentional; cancelBidTo handles rejecting bidder wallets. // slither-disable-next-line low-level-calls (bool success,) = payable(recipient).call{value: bid.amount}(""); require(success, "Bid refund failed"); emit BidCancelled(bidder, recipient, bid.amount); } // slither-disable-start arbitrary-send-eth function acceptBid( address bidder, uint256 tokenId, uint256 expectedAmount, uint256 expectedExpiresAt, uint256 deadline ) external nonReentrant { Bid memory bid = bids[bidder]; require(bid.amount > 0, "No active bid"); require(block.timestamp <= deadline, "Transaction expired"); require(block.timestamp <= bid.expiresAt, "Bid expired"); require(bidder != msg.sender, "Cannot accept own bid"); require(expectedAmount > 0, "Expected amount required"); require(bid.amount == expectedAmount, "Bid amount changed"); require(bid.expiresAt == expectedExpiresAt, "Bid expiry changed"); address seller = msg.sender; require(nft.ownerOf(tokenId) == seller, "Not token owner"); require( nft.getApproved(tokenId) == address(this) || nft.isApprovedForAll(seller, address(this)), "Bids not approved" ); delete bids[bidder]; proceeds[seller] += bid.amount; nft.transferFrom(seller, bidder, tokenId); require(nft.ownerOf(tokenId) == bidder, "NFT transfer failed"); emit BidAccepted(bidder, seller, tokenId, bid.amount); } 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); } // slither-disable-end arbitrary-send-eth // slither-disable-end timestamp }