Abstract

This standard is an extension of ERC-721 and defines standard functions outlining a scope for ticketing agents or event organizers to take preventative actions to stop audiences being exploited in the ticket scalping market and allow customers to resell their tickets via authorized ticket resellers.

Motivation

Industrial-scale ticket touting has been a longstanding issue, with its associated fraud and criminal problems leading to unfortunate incidents and waste of social resources. It is also hugely damaging to artists at all levels of their careers and to related businesses across the board. Although the governments of various countries have begun to legislate to restrict the behavior of scalpers, the effect is limited. They still sold tickets for events at which resale was banned or did not yet own then obtained substantial illegal profits from speculative selling. We consulted many opinions to provide a consumer-friendly resale interface, enabling buyers to resell or reallocate a ticket at the price they initially paid or less is the efficient way to rip off “secondary ticketing”.that enables ticketing agents to utilize

The typical ticket may be a “piece of paper” or even a voucher in your email inbox, making it easy to counterfeit or circulate. To restrict the transferability of these tickets, we have designed a mechanism that prohibits ticket transfers for all parties, including the ticket owner, except for specific accounts that are authorized to transfer tickets. The specific accounts may be ticketing agents, managers, promoters and authorized resale platforms. Therefore, the ticket touts are unable to transfer tickets as they wish. Furthermore, to enhance functionality, we have implemented a token info schema to each ticket, allowing only authorized accounts(excluding the owner) to modify these records.

This standard defines a framework that enables ticketing agents to utilize ERC-721 tokens as event tickets and restricts token transferability to prevent ticket touting. By implementing this standard, we aim to protect customers from scams and fraudulent activities.

Specification

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119 and RFC 8174.

Interface

The interface and structure referenced here are as follows:

  • TokenInfo
    • signature: Recommend that the adapter self-defines what to sign using the user’s private key or agent’s private key to prove the token validity.
    • status: Represent token current status.
    • expireTime: Recommend set to the event due time.
  • TokenStatus
    • Sold: When a token is sold, it MUST change to Sold. The token is valid in this status.
    • Resell: When a token is in the secondary market, it MUST be changed to Resell. The token is valid in this status.
    • Void: When the token owner engages in an illegal transaction, the token status MUST be set to Void, and the token is invalid in this status.
    • Redeemed: When the token is used, it is RECOMMENDED to change the token status to Redeemed.
/// @title IERC7439 Prevent Ticket Touting Interface
interface IERC7439 /* is ERC721 */ {
    /// @dev TokenStatus represent the token current status, only specific role can change status
    enum TokenStatus {
        Sold,    // 0
        Resell,  // 1
        Void,    // 2
        Redeemed // 3
    }

    /// @param signature Data signed by user's private key or agent's private key
    /// @param tokenStatus Token status changing to
    /// @param expireTime Event due time
    struct TokenInfo {
        bytes signature;
        TokenStatus tokenStatus;
        uint256 expireTime;
    }

    /// @notice Used to notify listeners that the token with the specified ID has been changed status
    /// @param tokenId The token has been changed status
    /// @param tokenStatus Token status has been changed to
    /// @param signature Data signed by user's private key or agent's private key
    event TokenStatusChanged(
        uint256 indexed tokenId,
        TokenStatus indexed tokenStatus,
        bytes signature
    );

    /// @notice Used to mint token with token status
    /// @dev MUST emit the `TokenStatusChanged` event if the token status is changed.
    /// @param to The receiptent of token
    /// @param signature Data signed by user's private key or agent's private key
    function safeMint(address to, bytes memory signature) external;

    /// @notice Used to change token status and can only be invoked by a specific role
    /// @dev MUST emit the `TokenStatusChanged` event if the token status is changed.
    /// @param tokenId The token need to change status
    /// @param signature Data signed by user's private key or agent's private key
    /// @param tokenStatus Token status changing to
    /// @param newExpireTime New event due time
    function changeState(
        uint256 tokenId,
        bytes memory signature,
        TokenStatus tokenStatus,
        uint256 newExpireTime
    ) external;
}

The supportsInterface method MUST return true when called with 0x15fbb306.

Rationale

Designing the proposal, we considered the following questions:

  1. What is the most crucial for ticketing agents, performers, and audiences?
    • For ticketing companies, selling out all tickets is crucial. Sometimes, to create a vibrant sales environment, ticketing companies may even collaborate with scalpers. This practice can be detrimental to both the audience and performers. To prevent such situations, there must be an open and transparent primary sales channel, as well as a fair secondary sales mechanism. In the safeMint function, which is a public function, we hope that everyone can mint tickets transparently at a listed price by themselves. At that time, TokenInfo adds a signature that only the buyer account or agent can resolve depending on the mechanism, to prove the ticket validity. And the token status is Sold. Despite this, we must also consider the pressures on ticketing companies. They aim to maximize the utility of every valid ticket, meaning selling out each one. In the traditional mechanism, ticketing companies only benefit from the initial sale, implying that they do not enjoy the excess profits from secondary sales. Therefore, we have designed a secondary sales process that is manageable for ticketing companies. In the _beforeTokenTransfer() function, you can see that it is an accessControl function, and only the PARTNER_ROLE mint or burn situation can transfer the ticket. The PARTNER_ROLE can be the ticket agency or a legal secondary ticket selling platform, which may be a state supervision or the ticket agency designated platform. To sustain the fair ticketing market, we cannot allow them to transfer tickets themselves, because we can’t distinguish whether the buyer is a scalper.

    • For performers or event holder, they aren’t willing to see bad news during ticket selling. A smooth ticketing process or no news that may damage their performers’ reputation is what they want. Other than that, what really matters is all the audience true fans who come. Tickets ending up in the hands of scalpers or entering a chaotic secondary market doesn’t really appeal to genuine fans. We believe performers wouldn’t be pleased with such a situation. Through the transparant mechanism, performers or event holder can control the real sales status at all times form cross-comparison of token mint amount and TokenInfo-TokenStatus.
         enum TokenStatus {
             Sold,    // 0
             Resell,  // 1
             Void,    // 2
             Redeemed // 3
         }
      
    • For audiences, the only thing they need is to get a valid ticket. In the traditional mechanism,fans encounter many obstacles. At hot concerts, fans who try to snag tickets can run into some foes, like scalpers and ticketing companies. These scalpers are like pros, all organized and strategic in grabbing up tickets. Surprisingly, ticketing companies might actually team up with these scalpers. Or, they might just keep a bunch of freebies or VIP tickets to themselves. A transparent mechanism is equally important for the audiences.
  2. How to establish a healthy ticketing ecosystem?
    • Clear ticketing rules are key to making sure the supply and demand stay in balance.

    • An open pricing system is a must to make sure consumers are protected.

    • Excellent liquidity. In the initial market, users can mint tickets themselves. If needed, purchased tickets can also be transferred in a transparent and open secondary market. Audiences who didn’t buy tickets during the initial sale can also confidently purchase tickets in a legal secondary market. The changeState function is to help the ticket have good liquidity. Only PARTNER_ROLE can change the ticket status. Once the sold ticket needs to be sold in the secondary market, it needs to ask the secondary market to help it change to resell status. The process of changing status is a kind of official verification of the secondary sale ticket. It is a protection mechanism to the second hand buyer.

  3. How to design a smooth ticketing process?
    • Easy to buy/sell. Audiences can buy ticket as mint NFT. This is a well-known practice.

    • Easy to refund. When something extreme happens and you need to cancel the show. Handling ticket refunds can be a straightforward process.

    • Easy to redeem. Before the show, the ticket agency can verify the ticket by the signature to confirm if the audience is genuine. TokenStatus needs to be equal to sold, and expireTime can distinguish whether the audience has arrived at the correct session. After verification is passed, the ticket agency can change the TokenStatus to Redeemed.

    • Normal Flow Alt text

    • Void Flow Alt text

    • Resell Flow Alt text

Backwards Compatibility

This standard is compatible with ERC-721.

Test Cases

const { expectRevert } = require("@openzeppelin/test-helpers");
const { expect } = require("chai");
const ERC7439 = artifacts.require("ERC7439");

contract("ERC7439", (accounts) => {
  const [deployer, partner, userA, userB] = accounts;
  const expireTime = 19999999;
  const tokenId = 0;
  const signature = "0x993dab3dd91f5c6dc28e17439be475478f5635c92a56e17e82349d3fb2f166196f466c0b4e0c146f285204f0dcb13e5ae67bc33f4b888ec32dfe0a063e8f3f781b"
  const zeroHash = "0x";

  beforeEach(async () => {
    this.erc7439 = await ERC7439.new({
      from: deployer,
    });
    await this.erc7439.mint(userA, signature, { from: deployer });
  });

  it("Should mint a token", async () => {
    const tokenInfo = await this.erc7439.tokenInfo(tokenId);

    expect(await this.erc7439.ownerOf(tokenId)).to.equal(userA);
    expect(tokenInfo.signature).equal(signature);
    expect(tokenInfo.status).equal("0"); // Sold
    expect(tokenInfo.expireTime).equal(expireTime);
  });

  it("should ordinary users cannot transfer successfully", async () => {
    expectRevert(await this.erc7439.transferFrom(userA, userB, tokenId, { from: userA }), "ERC7439: You cannot transfer this NFT!");
  });

  it("should partner can transfer successfully and chage the token info to resell status", async () => {
    const tokenStatus = 1; // Resell

    await this.erc7439.changeState(tokenId, zeroHash, tokenStatus, { from: partner });
    await this.erc7439.transferFrom(userA, partner, tokenId, { from: partner });

    expect(tokenInfo.tokenHash).equal(zeroHash);
    expect(tokenInfo.status).equal(tokenStatus); // Resell
    expect(await this.erc7439.ownerOf(tokenId)).to.equal(partner);
  });

  it("should partner can change the token status to void", async () => {
    const tokenStatus = 2; // Void

    await this.erc7439.changeState(tokenId, zeroHash, tokenStatus, { from: partner });

    expect(tokenInfo.tokenHash).equal(zeroHash);
    expect(tokenInfo.status).equal(tokenStatus); // Void
  });

  it("should partner can change the token status to redeemed", async () => {
    const tokenStatus = 3; // Redeemed

    await this.erc7439.changeState(tokenId, zeroHash, tokenStatus, { from: partner });

    expect(tokenInfo.tokenHash).equal(zeroHash);
    expect(tokenInfo.status).equal(tokenStatus); // Redeemed
  });

  it("should partner can resell the token and change status from resell to sold", async () => {    
    let tokenStatus = 1; // Resell
    await this.erc7439.changeState(tokenId, zeroHash, tokenStatus, { from: partner });
    await this.erc7439.transferFrom(userA, partner, tokenId, { from: partner });
    
    expect(tokenInfo.status).equal(tokenStatus); // Resell
    expect(tokenInfo.tokenHash).equal(zeroHash);

    tokenStatus = 0; // Sold
    const newSignature = "0x113hqb3ff45f5c6ec28e17439be475478f5635c92a56e17e82349d3fb2f166196f466c0b4e0c146f285204f0dcb13e5ae67bc33f4b888ec32dfe0a063w7h2f742f";
    await this.erc7439.changeState(tokenId, newSignature, tokenStatus, { from: partner });
    await this.erc7439.transferFrom(partner, userB, tokenId, { from: partner });

    expect(tokenInfo.status).equal(tokenStatus); // Sold
    expect(tokenInfo.tokenHash).equal(newSignature);
  });
});

Reference Implementation

// SPDX-License-Identifier: CC0-1.0
pragma solidity 0.8.19;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
// If you need additional metadata, you can import ERC721URIStorage
// import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "./IERC7439.sol";

contract ERC7439 is ERC721, AccessControl, IERC7439 {
    using Counters for Counters.Counter;

    bytes32 public constant PARTNER_ROLE = keccak256("PARTNER_ROLE");
    Counters.Counter private _tokenIdCounter;

    uint256 public expireTime;

    mapping(uint256 => TokenInfo) public tokenInfo;

    constructor(uint256 _expireTime) ERC721("MyToken", "MTK") {
        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _grantRole(PARTNER_ROLE, msg.sender);
        expireTime = _expireTime;
    }

    function safeMint(address to, bytes memory signature) public {
        uint256 tokenId = _tokenIdCounter.current();
        _tokenIdCounter.increment();
        _safeMint(to, tokenId);
        tokenInfo[tokenId] = TokenInfo(signature, TokenStatus.Sold, expireTime);
        emit TokenStatusChanged(tokenId, TokenStatus.Sold, signature);
    }

    function changeState(
        uint256 tokenId,
        bytes memory signature,
        TokenStatus tokenStatus,
        uint256 newExpireTime
    ) public onlyRole(PARTNER_ROLE) {
        tokenInfo[tokenId] = TokenInfo(signature, tokenStatus, newExpireTime);
        emit TokenStatusChanged(tokenId, tokenStatus, signature);
    }
    
    function _burn(uint256 tokenId) internal virtual override(ERC721) {
        super._burn(tokenId);

        if (_exists(tokenId)) {
            delete tokenInfo[tokenId];
            // If you import ERC721URIStorage
            // delete _tokenURIs[tokenId];
        }
    }

    function supportsInterface(
        bytes4 interfaceId
    ) public view virtual override(AccessControl, ERC721) returns (bool) {
        return
            interfaceId == type(IERC7439).interfaceId ||
            super.supportsInterface(interfaceId);
    }

    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 tokenId
    ) internal virtual override(ERC721) {
        if (!hasRole(PARTNER_ROLE, _msgSender())) {
            require(
                from == address(0) || to == address(0),
                "ERC7439: You cannot transfer this NFT!"
            );
        }

        super._beforeTokenTransfer(from, to, tokenId);
    }
}

Security Considerations

There are no security considerations related directly to the implementation of this standard.

Copyright and related rights waived via CC0.