Overview
EventTicket is an ERC721 NFT contract that represents event tickets with built-in sales mechanisms, check-in functionality, and automatic Proof of Attendance (POA) minting.
Contract Location : src/packages/contracts/src/EventTicket.solInherits : ERC721, ERC721Enumerable, Ownable, ReentrancyGuard, Pausable
Features
NFT Tickets ERC721-compliant tickets that can be traded on secondary markets
Time-Gated Sales Configure when ticket sales start and end
Allowlist Support Merkle tree-based allowlist for presales
Check-In System On-chain ticket validation and POA minting
Wallet Limits Configurable maximum tickets per wallet
Platform Fees Automatic fee distribution to platform and organizer
Contract State
Public Variables
Maximum number of tickets that can be minted for this event
Price per ticket in wei (e.g., 0.01 ether = 10000000000000000)
Unix timestamp when ticket sales start
Unix timestamp when ticket sales end
Unix timestamp of the actual event (check-in enabled after this)
Counter for the next token ID to be minted (starts at 1)
Address that receives platform fees from ticket sales
Platform fee in basis points (250 = 2.5%)
Merkle root for allowlist verification (0x0 if no allowlist)
Reference to the associated POA contract
Maximum number of tickets one wallet can purchase (0 = no limit, default: 5)
Mappings
Tracks which ticket IDs have been used for check-in
Tracks which addresses have already used their allowlist allocation
Initialization
The EventTicket contract uses a proxy pattern and must be initialized after deployment.
function initialize (
string memory name ,
string memory symbol ,
address organizer ,
uint256 _totalSupply ,
uint256 _ticketPrice ,
uint256 _saleStart ,
uint256 _saleEnd ,
uint256 _eventDate ,
string memory baseTokenURI ,
address _platformFeeRecipient ,
uint256 _platformFeeBps
) external
This function should only be called once by the factory contract. It cannot be called directly.
Name of the event (e.g., “DevCon 2024”)
Symbol for the NFT collection (e.g., “DEVCON24”)
Address that will own the contract and receive funds
Maximum number of tickets (1-10000)
When ticket sales begin (Unix timestamp)
When ticket sales end (Unix timestamp)
When the event occurs (Unix timestamp)
Base URI for token metadata
Where platform fees are sent
Platform fee in basis points (e.g., 250 = 2.5%)
Ticket Minting
Public Mint
Purchase tickets during the sale period.
function mint ( uint256 quantity ) external payable nonReentrant onlySaleActive whenNotPaused
Number of tickets to mint (1-10 per transaction)
Requirements:
Current time is between saleStart and saleEnd
Contract is not paused
quantity is between 1 and 10
Sufficient supply available
Wallet limit not exceeded (if set)
Sufficient payment provided (msg.value >= ticketPrice * quantity)
Example:
// Mint 3 tickets
const tx = await eventTicket . mint ( 3 , {
value: ethers . parseEther ( '0.03' ) // 3 tickets at 0.01 ETH each
});
await tx . wait ();
Events Emitted:
event TicketMinted ( address indexed to , uint256 indexed tokenId , uint256 price );
If you send more ETH than required, the excess is automatically refunded: // Mint 1 ticket at 0.01 ETH but send 0.015 ETH
const tx = await eventTicket . mint ( 1 , { value: ethers . parseEther ( '0.015' ) });
// You'll receive 0.005 ETH back automatically
Allowlist Mint
Mint tickets during presale using a Merkle proof.
function allowListMint (
uint256 quantity ,
bytes32 [] calldata merkleProof
) external payable nonReentrant onlySaleActive whenNotPaused
Number of tickets to mint (1-3 for allowlist)
Merkle proof proving the caller is on the allowlist
Requirements:
All requirements from mint() apply
Valid Merkle proof provided
Address has not already claimed allowlist allocation
quantity is between 1 and 3
Example:
const { MerkleTree } = require ( 'merkletreejs' );
const keccak256 = require ( 'keccak256' );
// Generate proof for an address
const allowlist = [ '0xAddress1' , '0xAddress2' , '0xAddress3' ];
const leaves = allowlist . map ( addr => keccak256 ( addr ));
const tree = new MerkleTree ( leaves , keccak256 , { sortPairs: true });
const proof = tree . getHexProof ( keccak256 ( userAddress ));
// Mint with proof
const tx = await eventTicket . allowListMint ( 2 , proof , {
value: ethers . parseEther ( '0.02' )
});
await tx . wait ();
Organizer Mint
Organizers can mint tickets for free to any address.
function mintFor ( address to , uint256 quantity ) external onlyOwner nonReentrant whenNotPaused
Address to receive the tickets
Number of tickets to mint (1-50)
Use Cases:
Comp tickets for VIPs
Team allocations
Marketing giveaways
Reserve tickets
Check-In System
Self Check-In
Ticket holders check themselves in after the event starts.
function checkIn ( uint256 tokenId ) external onlyEventStarted
The ticket token ID to check in
Requirements:
Current time is after eventDate
Caller owns the ticket
Ticket has not been used yet
What Happens:
Ticket is marked as used
Proof of Attendance NFT is minted to the ticket holder
TicketCheckedIn and ProofOfAttendanceMinted events are emitted
Example:
// Check in ticket #42
const tx = await eventTicket . checkIn ( 42 );
const receipt = await tx . wait ();
// Find POA token ID from events
const poaEvent = receipt . logs . find ( log =>
log . eventName === 'ProofOfAttendanceMinted'
);
console . log ( 'POA Token ID:' , poaEvent . args . poaTokenId );
Events Emitted:
event TicketCheckedIn ( uint256 indexed tokenId , address indexed holder );
event ProofOfAttendanceMinted ( address indexed attendee , uint256 indexed ticketId , uint256 poaTokenId );
Organizer Check-In
Organizers can check in tickets on behalf of attendees (for assisted check-in).
function organizerCheckIn ( uint256 tokenId ) external onlyOwner onlyEventStarted
The ticket token ID to check in
Requirements:
Caller is the contract owner (organizer)
Current time is after eventDate
Ticket has not been used yet
Use Case:
Assisted check-in at the door
Batch check-in for groups
Helping attendees without wallet access
Revenue Management
Withdraw Funds
Organizers can withdraw ticket sale proceeds after the event.
function withdraw () external onlyOwner nonReentrant
Requirements:
Caller is the contract owner
Current time is after eventDate
Contract has a balance > 0
What Happens:
Calculate platform fee: balance * platformFeeBps / 10000
Transfer platform fee to platformFeeRecipient
Transfer remaining balance to organizer
Emit WithdrawalCompleted event
Example:
// After event date has passed
const tx = await eventTicket . withdraw ();
const receipt = await tx . wait ();
// Get withdrawal amount from event
const withdrawalEvent = receipt . logs . find ( log =>
log . eventName === 'WithdrawalCompleted'
);
console . log ( 'Withdrawn:' , ethers . formatEther ( withdrawalEvent . args . amount ));
Fee Calculation Example:
Total sales: 100 ETH
Platform fee: 2.5% (250 bps)
Platform receives: 2.5 ETH
Organizer receives: 97.5 ETH
Total sales: 50 MATIC
Platform fee: 5% (500 bps)
Platform receives: 2.5 MATIC
Organizer receives: 47.5 MATIC
Administrative Functions
Set Allowlist
Update the Merkle root for the allowlist.
function setAllowListRoot ( bytes32 newRoot ) external onlyOwner
New Merkle tree root hash
Example:
const { MerkleTree } = require ( 'merkletreejs' );
const keccak256 = require ( 'keccak256' );
// Create allowlist
const allowlist = [
'0x1234567890123456789012345678901234567890' ,
'0x2345678901234567890123456789012345678901' ,
'0x3456789012345678901234567890123456789012'
];
const leaves = allowlist . map ( addr => keccak256 ( addr ));
const tree = new MerkleTree ( leaves , keccak256 , { sortPairs: true });
const root = tree . getRoot ();
// Set root on contract
const tx = await eventTicket . setAllowListRoot ( root );
await tx . wait ();
Set Max Tickets Per Wallet
Update the maximum tickets one wallet can purchase.
function setMaxTicketsPerWallet ( uint256 newMax ) external onlyOwner
New maximum (0 = no limit)
Example:
// Allow maximum 10 tickets per wallet
await eventTicket . setMaxTicketsPerWallet ( 10 );
// Remove limit entirely
await eventTicket . setMaxTicketsPerWallet ( 0 );
Pause/Unpause
Emergency pause functionality for the organizer.
function pause () external onlyOwner
function unpause () external onlyOwner
Use Cases:
Security incidents
Technical issues
Emergency situations
Example:
// Pause minting
await eventTicket . pause ();
// Resume after fixing issue
await eventTicket . unpause ();
When paused, mint(), allowListMint(), and mintFor() will revert. Check-in and withdrawal are not affected.
View Functions
Check Ticket Validity
Check if a ticket exists and hasn’t been used.
function isTicketValid ( uint256 tokenId ) external view returns ( bool )
True if ticket exists and hasn’t been checked in
Example:
const isValid = await eventTicket . isTicketValid ( 42 );
if ( isValid ) {
console . log ( 'Ticket #42 is valid and unused' );
}
Get Available Tickets
Get the number of tickets still available for purchase.
function availableTickets () external view returns ( uint256 remaining )
Number of tickets that can still be minted
Example:
const available = await eventTicket . availableTickets ();
console . log ( ` ${ available } tickets remaining` );
Token URI
Get metadata URI for a token.
function tokenURI ( uint256 tokenId ) public view override returns ( string memory )
Full URI for token metadata (baseURI + tokenId)
Events
event TicketMinted ( address indexed to , uint256 indexed tokenId , uint256 price );
Emitted when a ticket is minted
event TicketCheckedIn ( uint256 indexed tokenId , address indexed holder );
Emitted when a ticket is checked in
event ProofOfAttendanceMinted ( address indexed attendee , uint256 indexed ticketId , uint256 poaTokenId );
Emitted when a POA is minted during check-in
event AllowListUpdated ( bytes32 newRoot );
Emitted when the allowlist Merkle root is updated
event WithdrawalCompleted ( address indexed organizer , uint256 amount );
Emitted when the organizer withdraws funds
Errors
Sale is not currently active (before saleStart or after saleEnd)
Not enough ETH sent with the transaction
Requested quantity would exceed total supply
MaxTicketsPerWalletExceeded
Purchase would exceed wallet’s ticket limit
Caller does not own the specified ticket
Ticket has already been checked in
Event date has not been reached yet
Attempting to withdraw before event date
Merkle proof verification failed
Address has already used their allowlist allocation
Complete Example
Full Lifecycle
Full Lifecycle
const { ethers } = require ( 'ethers' );
// 1. Create event via factory
const factory = new ethers . Contract ( factoryAddress , factoryABI , signer );
const tx1 = await factory . createEvent (
'DevCon 2024' ,
'DEVCON24' ,
1000 ,
ethers . parseEther ( '0.01' ),
Math . floor ( Date . now () / 1000 ),
Math . floor ( Date . now () / 1000 ) + 604800 ,
Math . floor ( Date . now () / 1000 ) + 1209600 ,
'https://api.example.com/metadata/'
);
const receipt1 = await tx1 . wait ();
const eventAddress = receipt1 . logs [ 0 ]. args . contractAddress ;
// 2. Connect to event contract
const eventTicket = new ethers . Contract ( eventAddress , eventTicketABI , signer );
// 3. Buy tickets
const tx2 = await eventTicket . mint ( 2 , {
value: ethers . parseEther ( '0.02' )
});
await tx2 . wait ();
// 4. Check available tickets
const available = await eventTicket . availableTickets ();
console . log ( ` ${ available } tickets remaining` );
// 5. After event, check in
const tx3 = await eventTicket . checkIn ( 1 );
await tx3 . wait ();
// 6. Organizer withdraws funds
const organizerSigner = new ethers . Wallet ( organizerKey , provider );
const eventTicketAsOrganizer = eventTicket . connect ( organizerSigner );
const tx4 = await eventTicketAsOrganizer . withdraw ();
await tx4 . wait ();
Security Considerations
Reentrancy : Protected by ReentrancyGuard on all state-changing functions
Overflow : Using Solidity 0.8.24 with built-in overflow checks
Access Control : Critical functions protected by onlyOwner modifier
Pausable : Emergency stop mechanism for security incidents
Gas Optimization Tips
Mint multiple tickets in one transaction (up to 10)
Use mintFor for batch comp tickets (up to 50)
Allowlist minting uses less gas than regular minting
Check-in gas cost is constant regardless of ticket price
EventTicketFactory Factory contract for deploying EventTicket instances
ProofOfAttendance POA NFTs minted during check-in