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:
EventTicket.sol Core Variables
Ticket Minting Function
// 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:
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);
}
Each ticket NFT includes metadata stored on IPFS or centralized storage:
Example Metadata
TokenURI Function
{
"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:
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