Skip to main content

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

totalSupply
uint256
Maximum number of tickets that can be minted for this event
ticketPrice
uint256
Price per ticket in wei (e.g., 0.01 ether = 10000000000000000)
saleStart
uint256
Unix timestamp when ticket sales start
saleEnd
uint256
Unix timestamp when ticket sales end
eventDate
uint256
Unix timestamp of the actual event (check-in enabled after this)
tokenCounter
uint256
Counter for the next token ID to be minted (starts at 1)
platformFeeRecipient
address
Address that receives platform fees from ticket sales
platformFeeBps
uint256
Platform fee in basis points (250 = 2.5%)
allowListRoot
bytes32
Merkle root for allowlist verification (0x0 if no allowlist)
proofOfAttendance
ProofOfAttendance
Reference to the associated POA contract
maxTicketsPerWallet
uint256
Maximum number of tickets one wallet can purchase (0 = no limit, default: 5)

Mappings

ticketUsed
mapping(uint256 => bool)
Tracks which ticket IDs have been used for check-in
allowListClaimed
mapping(address => bool)
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
string
required
Name of the event (e.g., “DevCon 2024”)
symbol
string
required
Symbol for the NFT collection (e.g., “DEVCON24”)
organizer
address
required
Address that will own the contract and receive funds
_totalSupply
uint256
required
Maximum number of tickets (1-10000)
_ticketPrice
uint256
required
Price per ticket in wei
_saleStart
uint256
required
When ticket sales begin (Unix timestamp)
_saleEnd
uint256
required
When ticket sales end (Unix timestamp)
_eventDate
uint256
required
When the event occurs (Unix timestamp)
baseTokenURI
string
required
Base URI for token metadata
_platformFeeRecipient
address
required
Where platform fees are sent
_platformFeeBps
uint256
required
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
quantity
uint256
required
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
quantity
uint256
required
Number of tickets to mint (1-3 for allowlist)
merkleProof
bytes32[]
required
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
to
address
required
Address to receive the tickets
quantity
uint256
required
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
tokenId
uint256
required
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:
  1. Ticket is marked as used
  2. Proof of Attendance NFT is minted to the ticket holder
  3. TicketCheckedIn and ProofOfAttendanceMinted events are emitted
Example:
JavaScript
// 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
tokenId
uint256
required
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:
  1. Calculate platform fee: balance * platformFeeBps / 10000
  2. Transfer platform fee to platformFeeRecipient
  3. Transfer remaining balance to organizer
  4. Emit WithdrawalCompleted event
Example:
JavaScript
// 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

Administrative Functions

Set Allowlist

Update the Merkle root for the allowlist.
function setAllowListRoot(bytes32 newRoot) external onlyOwner
newRoot
bytes32
required
New Merkle tree root hash
Example:
JavaScript
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
newMax
uint256
required
New maximum (0 = no limit)
Example:
JavaScript
// 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:
JavaScript
// 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)
return
bool
True if ticket exists and hasn’t been checked in
Example:
JavaScript
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)
remaining
uint256
Number of tickets that can still be minted
Example:
JavaScript
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)
return
string
Full URI for token metadata (baseURI + tokenId)

Events

TicketMinted
event
event TicketMinted(address indexed to, uint256 indexed tokenId, uint256 price);
Emitted when a ticket is minted
TicketCheckedIn
event
event TicketCheckedIn(uint256 indexed tokenId, address indexed holder);
Emitted when a ticket is checked in
ProofOfAttendanceMinted
event
event ProofOfAttendanceMinted(address indexed attendee, uint256 indexed ticketId, uint256 poaTokenId);
Emitted when a POA is minted during check-in
AllowListUpdated
event
event AllowListUpdated(bytes32 newRoot);
Emitted when the allowlist Merkle root is updated
WithdrawalCompleted
event
event WithdrawalCompleted(address indexed organizer, uint256 amount);
Emitted when the organizer withdraws funds

Errors

SaleNotActive
error
Sale is not currently active (before saleStart or after saleEnd)
InsufficientPayment
error
Not enough ETH sent with the transaction
MaxSupplyExceeded
error
Requested quantity would exceed total supply
MaxTicketsPerWalletExceeded
error
Purchase would exceed wallet’s ticket limit
NotTokenOwner
error
Caller does not own the specified ticket
TicketAlreadyUsed
error
Ticket has already been checked in
EventNotStarted
error
Event date has not been reached yet
WithdrawalTooEarly
error
Attempting to withdraw before event date
InvalidProof
error
Merkle proof verification failed
AlreadyClaimed
error
Address has already used their allowlist allocation

Complete Example

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

Build docs developers (and LLMs) love