Skip to main content
GatePass implements event tickets as ERC721 NFTs on the Polygon network, providing true ownership, transferability, and blockchain-based verification.

Overview

GatePass tickets are minted as ERC721 NFTs using the EventTicket.sol smart contract. Each ticket is a unique, verifiable digital asset that can be owned, transferred, and used for event access.

Key Features

True Ownership

Tickets are stored in user wallets as NFTs, giving attendees complete control over their tickets

Transferability

Event organizers can enable or disable ticket transfers based on event requirements

On-Chain Verification

All ticket data is verifiable on the blockchain, preventing counterfeits

Metadata Storage

Ticket metadata includes event details, seat information, and visual assets

Smart Contract Architecture

The EventTicket.sol contract implements ERC721 with additional features for event ticketing:
// Maximum number of tickets that can be minted
uint256 public totalSupply;

// Price per ticket in wei
uint256 public ticketPrice;

// Timestamp when ticket sales start
uint256 public saleStart;

// Timestamp when ticket sales end
uint256 public saleEnd;

// Timestamp of the actual event
uint256 public eventDate;

// Counter for minted tickets
uint256 public tokenCounter = 1;

// Track which tickets have been used for check-in
mapping(uint256 => bool) public ticketUsed;

// Maximum tickets per wallet (0 = no limit)
uint256 public maxTicketsPerWallet = 5;

Ticket Lifecycle

1. Deployment

Event tickets are deployed through the EventTicketFactory contract using minimal proxy clones for gas efficiency:
EventTicketFactory.sol
function createEvent(
    string memory eventName,
    string memory eventSymbol,
    uint256 totalSupply,
    uint256 ticketPrice,
    uint256 saleStart,
    uint256 saleEnd,
    uint256 eventDate,
    string memory baseTokenURI
) external nonReentrant returns (address eventContract) {
    // Deploy minimal proxy
    eventContract = implementation.clone();
    
    // Initialize the contract
    EventTicket(eventContract).initialize(
        eventName,
        eventSymbol,
        msg.sender, // organizer
        totalSupply,
        ticketPrice,
        saleStart,
        saleEnd,
        eventDate,
        baseTokenURI,
        platformFeeRecipient,
        platformFeeBps
    );
}

2. Minting

Users can mint tickets during the sale period:
  • Public Mint: Anyone can purchase up to maxTicketsPerWallet tickets
  • Allowlist Mint: Whitelisted addresses can mint during private sales
  • Organizer Mint: Event organizers can mint complimentary tickets

3. Transfer

Tickets follow standard ERC721 transfer mechanics:
// Transfer ticket to another wallet
function transferFrom(address from, address to, uint256 tokenId) public override {
    // Standard ERC721 transfer logic
    // Can be restricted by organizers using pause() function
}

4. Check-In

Tickets are marked as used during check-in and automatically mint a Proof of Attendance NFT:
function checkIn(uint256 tokenId) external onlyEventStarted {
    if (ownerOf(tokenId) != msg.sender) {
        revert NotTokenOwner();
    }
    
    if (ticketUsed[tokenId]) {
        revert TicketAlreadyUsed();
    }

    ticketUsed[tokenId] = true;

    // Mint Proof of Attendance NFT
    uint256 poaTokenId = proofOfAttendance.mintPOA(msg.sender, tokenId);
    
    emit TicketCheckedIn(tokenId, msg.sender);
    emit ProofOfAttendanceMinted(msg.sender, tokenId, poaTokenId);
}

Token Metadata

Each ticket NFT includes metadata stored on IPFS or centralized storage:
{
  "name": "Tech Conference 2024 - Ticket #42",
  "description": "Admission ticket for Tech Conference 2024",
  "image": "ipfs://QmXxxx.../ticket-42.png",
  "attributes": [
    {
      "trait_type": "Event",
      "value": "Tech Conference 2024"
    },
    {
      "trait_type": "Date",
      "value": "2024-06-15"
    },
    {
      "trait_type": "Venue",
      "value": "San Francisco Convention Center"
    },
    {
      "trait_type": "Tier",
      "value": "VIP"
    },
    {
      "trait_type": "Seat",
      "value": "A-42"
    }
  ]
}

Database Integration

Ticket data is also stored in the database for efficient querying:
schema.prisma
model Ticket {
  id       String @id @default(cuid())
  tokenId  Int
  
  // Blockchain data
  contractAddress String
  chainId         Int
  txHash          String?
  blockNumber     Int?
  
  // Metadata
  metadataUri String?
  seatNumber  String?
  section     String?
  tier        String?
  
  // Status
  isUsed      Boolean @default(false)
  usedAt      DateTime?
  
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  // Relations
  event   Event  @relation(fields: [eventId], references: [id])
  eventId String
  
  order   Order  @relation(fields: [orderId], references: [id])
  orderId String
  
  checkIn CheckIn?

  @@unique([contractAddress, tokenId])
  @@map("tickets")
}

Security Features

All minting and financial functions use OpenZeppelin’s nonReentrant modifier to prevent reentrancy attacks.
The onlySaleActive modifier ensures tickets can only be minted during the configured sale period.
Hard cap on total supply prevents over-minting. Per-wallet limits prevent hoarding.
Event organizers can pause ticket sales in case of emergencies using the pause() function.

Allowlist Minting

Event organizers can configure allowlists using Merkle trees for private sales:
function allowListMint(
    uint256 quantity,
    bytes32[] calldata merkleProof
) external payable nonReentrant onlySaleActive whenNotPaused {
    if (allowListClaimed[msg.sender]) {
        revert AlreadyClaimed();
    }

    bytes32 leaf = keccak256(abi.encodePacked(msg.sender));
    if (!MerkleProof.verify(merkleProof, allowListRoot, leaf)) {
        revert InvalidProof();
    }

    allowListClaimed[msg.sender] = true;

    // Mint logic...
}
Merkle trees provide gas-efficient allowlist verification without storing all addresses on-chain.

Revenue Management

Event organizers can withdraw ticket sales revenue after the event:
function withdraw() external onlyOwner nonReentrant {
    if (block.timestamp < eventDate) {
        revert WithdrawalTooEarly();
    }

    uint256 balance = address(this).balance;
    require(balance > 0, "No funds to withdraw");

    // Calculate platform fee (2.5% default)
    uint256 platformFee = (balance * platformFeeBps) / 10000;
    uint256 organizerAmount = balance - platformFee;

    // Transfer platform fee
    if (platformFee > 0) {
        payable(platformFeeRecipient).transfer(platformFee);
    }

    // Transfer remaining to organizer
    payable(owner()).transfer(organizerAmount);

    emit WithdrawalCompleted(owner(), organizerAmount);
}

Querying Tickets

Check ticket availability and validity:
// Get available tickets for purchase
function availableTickets() external view returns (uint256) {
    return totalSupply >= tokenCounter ? totalSupply - tokenCounter + 1 : 0;
}

// Check if ticket is valid and unused
function isTicketValid(uint256 tokenId) external view returns (bool) {
    return _ownerOf(tokenId) != address(0) && !ticketUsed[tokenId];
}

Best Practices

Set Appropriate Supply

Configure totalSupply based on venue capacity and expected demand

Configure Sale Periods

Set saleStart and saleEnd to control when tickets can be purchased

Enable Transfer Controls

Use pause() to prevent transfers if tickets should be non-transferable

Set Per-Wallet Limits

Configure maxTicketsPerWallet to prevent scalping and ensure fair distribution

Next Steps

Learn About Ticket Verification

Discover how tickets are verified at event entrances using QR codes and blockchain data

Build docs developers (and LLMs) love