On-Chain Event Ticketing (Anti-Scam)

What it does: Issues tickets as non-transferable or controlled-transfer NFTs with capped resale pricing.

Why it matters: Reduces scalping, counterfeits, and unfair markups.

How it works:

  • NFT tickets minted for verified wallets.

  • Optional resale with predefined limits.

  • Entry verified cryptographically at venue.

  • Lost tickets recoverable via identity attestations.

      // SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;

/**
 * @title OnChainEventTicketing (Anti-Scam)
 * @dev NFT-based ticketing system with resale rules, refund policy,
 *      anti-scalping price caps, and verifiable ticket authenticity.
 */

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract OnChainEventTicketing is ERC721Enumerable, Ownable {
    struct EventInfo {
        string name;
        string venue;
        uint256 startTime;
        uint256 basePrice;
        uint256 maxResalePrice;   // Scalping protection
        uint256 refundDeadline;   // Timestamp
        uint256 maxTickets;
        bool finalized;           // After finalization no new minting
    }

    EventInfo public eventInfo;

    mapping(uint256 => bytes32) public ticketVerificationHash;
    mapping(uint256 => bool) public refunded;
    mapping(address => uint256) public purchases;

    uint256 public ticketCounter;
    uint256 public maxPerWallet = 4;

    event TicketPurchased(address indexed buyer, uint256 indexed tokenId, uint256 price);
    event TicketTransferred(address indexed from, address indexed to, uint256 indexed tokenId, uint256 price);
    event TicketRefunded(address indexed owner, uint256 indexed tokenId, uint256 amount);
    event EventFinalized();

    constructor(
        string memory _eventName,
        string memory _venue,
        uint256 _startTime,
        uint256 _basePrice,
        uint256 _maxResalePrice,
        uint256 _refundDeadline,
        uint256 _maxTickets
    ) ERC721("EventTicket", "ETIX") {
        require(_startTime > block.timestamp, "Event must be in future");
        require(_refundDeadline = _basePrice, "Resale cap must be >= base");

        eventInfo = EventInfo({
            name: _eventName,
            venue: _venue,
            startTime: _startTime,
            basePrice: _basePrice,
            maxResalePrice: _maxResalePrice,
            refundDeadline: _refundDeadline,
            maxTickets: _maxTickets,
            finalized: false
        });
    }

    // -------------------------------
    // PRIMARY SALE
    // -------------------------------

    function buyTicket(bytes32 qrHash) external payable {
        require(!eventInfo.finalized, "Sales closed");
        require(msg.value == eventInfo.basePrice, "Incorrect price");
        require(ticketCounter < eventInfo.maxTickets, "Sold out");
        require(purchases[msg.sender] < maxPerWallet, "Purchase limit reached");

        ticketCounter++;
        purchases[msg.sender]++;

        _safeMint(msg.sender, ticketCounter);
        ticketVerificationHash[ticketCounter] = qrHash;

        emit TicketPurchased(msg.sender, ticketCounter, msg.value);
    }

    // -------------------------------
    // REFUND LOGIC
    // -------------------------------

    function refund(uint256 tokenId) external {
        require(ownerOf(tokenId) == msg.sender, "Not owner");
        require(block.timestamp <= eventInfo.refundDeadline, "Refund period ended");
        require(!refunded[tokenId], "Already refunded");

        refunded[tokenId] = true;

        _transfer(msg.sender, address(this), tokenId);

        payable(msg.sender).transfer(eventInfo.basePrice);

        emit TicketRefunded(msg.sender, tokenId, eventInfo.basePrice);
    }

    // -------------------------------
    // SECONDARY TRANSFERS w/ PRICE CAP
    // -------------------------------

    function safeTransferWithPayment(
        address to,
        uint256 tokenId
    ) external payable {
        require(ownerOf(tokenId) == msg.sender, "Not owner");
        require(block.timestamp < eventInfo.startTime, "Transfers disabled at event start");
        require(msg.value <= eventInfo.maxResalePrice, "Exceeds allowed resale price");
        require(!refunded[tokenId], "Refunded ticket not valid");

        // Pay seller
        payable(msg.sender).transfer(msg.value);

        _safeTransfer(msg.sender, to, tokenId, "");

        emit TicketTransferred(msg.sender, to, tokenId, msg.value);
    }

    // -------------------------------
    // TICKET VERIFICATION (ANTI-FAKE)
    // -------------------------------

    /**
     * Off-chain QR scan → hash check on-chain.
     * The QR should embed a secret string revealed at venue.
     */
    function verifyTicket(
        uint256 tokenId,
        string calldata providedSecret
    ) external view returns (bool) {
        require(_exists(tokenId), "Invalid ticket");

        bytes32 expected = ticketVerificationHash[tokenId];
        return expected == keccak256(abi.encodePacked(providedSecret));
    }

    // -------------------------------
    // ADMIN CONTROLS
    // -------------------------------

    function finalizeEvent() external onlyOwner {
        eventInfo.finalized = true;
        emit EventFinalized();
    }

    function withdrawProceeds() external onlyOwner {
        payable(owner()).transfer(address(this).balance);
    }

    function setMaxPerWallet(uint256 limit) external onlyOwner {
        maxPerWallet = limit;
    }
}