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;
}
}
Build and Grow By Nam Le Thanh