// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; // SAMPLE ONLY: not deployed, compiled, or audited in this package. // Install @openzeppelin/contracts 5.x; verify a compatible pinned release. import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; /// @notice One collector NFT = one admission to ONE specified event. /// @dev Payment token must be the verified six-decimal USDC on your network. /// Funds remain in escrow until the event ends; artist split is not paid upfront. contract SGAllstarsFanEdition is ERC721, Ownable, ReentrancyGuard { using SafeERC20 for IERC20; uint256 public constant PRICE = 3_000_000; // 3 USDC, gas additional uint256 public constant ARTIST_BPS = 9000; // Sample 90% artist / 10% service IERC20 public immutable paymentToken; address public immutable artist; address public immutable serviceRecipient; uint256 public immutable editionLimit; uint256 public immutable eventStart; uint256 public immutable eventEnd; string public eventDetailsURI; // fixed public date/venue/age/admission terms string private editionMetadataURI; // fixed IPFS metadata with SG logo uint256 public totalMinted; uint256 public escrow; bool public canceled; bool public settled; mapping(uint256 => bool) public entryUsed; mapping(address => bool) public gateOperator; event AdmissionUsed(uint256 indexed tokenId, address indexed holder); event EventCanceled(); event PurchaseRefunded(uint256 indexed tokenId, address indexed holder); constructor(address owner_, address usdc_, address artist_, address service_, uint256 capacity_, uint256 start_, uint256 end_, string memory metadataURI_, string memory eventTermsURI_) ERC721("SG Allstars Fan Edition", "SGFAN") Ownable(owner_) { require(usdc_.code.length > 0, "Payment token missing"); require(IERC20Metadata(usdc_).decimals() == 6, "Requires 6 decimals"); require(artist_ != address(0) && service_ != address(0), "Missing recipients"); require(capacity_ > 0 && start_ > block.timestamp && end_ > start_, "Set event and capacity"); require(bytes(metadataURI_).length > 0 && bytes(eventTermsURI_).length > 0, "Terms required"); paymentToken = IERC20(usdc_); artist = artist_; serviceRecipient = service_; editionLimit = capacity_; eventStart = start_; eventEnd = end_; editionMetadataURI = metadataURI_; eventDetailsURI = eventTermsURI_; } function tokenURI(uint256 tokenId) public view override returns (string memory) { _requireOwned(tokenId); return editionMetadataURI; } function mint() external nonReentrant returns (uint256 tokenId) { require(!canceled && !settled && block.timestamp < eventStart, "Sale closed"); require(totalMinted < editionLimit, "Sold out"); uint256 beforeBalance = paymentToken.balanceOf(address(this)); paymentToken.safeTransferFrom(msg.sender, address(this), PRICE); require(paymentToken.balanceOf(address(this)) == beforeBalance + PRICE, "Exact payment required"); escrow += PRICE; tokenId = ++totalMinted; _safeMint(msg.sender, tokenId); } function setGateOperator(address operator, bool allowed) external onlyOwner { gateOperator[operator] = allowed; } /// Gate staff must verify live ownership with a signed wallet challenge, /// never accept a screenshot. No holder address is exposed in a QR secret. function redeemEntry(uint256 tokenId, address verifiedHolder) external { require(gateOperator[msg.sender], "Gate operator only"); require(!canceled && block.timestamp >= eventStart && block.timestamp <= eventEnd, "Event not open"); require(ownerOf(tokenId) == verifiedHolder && !entryUsed[tokenId], "Invalid or used pass"); entryUsed[tokenId] = true; emit AdmissionUsed(tokenId, verifiedHolder); } function cancelEvent() external onlyOwner { require(!settled && !canceled, "Already finalized"); canceled = true; emit EventCanceled(); } /// Current holder may return a canceled-event NFT for its 3 USDC mint price. /// Secondary-market premiums and gas are not reimbursed by this sample. function refund(uint256 tokenId) external nonReentrant { require(canceled && !settled && ownerOf(tokenId) == msg.sender, "No refund available"); _burn(tokenId); escrow -= PRICE; paymentToken.safeTransfer(msg.sender, PRICE); emit PurchaseRefunded(tokenId, msg.sender); } function settleAfterEvent() external onlyOwner nonReentrant { require(!canceled && !settled && block.timestamp > eventEnd, "Cannot settle"); settled = true; uint256 amount = escrow; escrow = 0; uint256 artistAmount = amount * ARTIST_BPS / 10_000; paymentToken.safeTransfer(artist, artistAmount); paymentToken.safeTransfer(serviceRecipient, amount - artistAmount); } }