ERC-7439 - Prevent ticket touting

Created 2023-07-28
Status Final
Category ERC
Type Standards Track
Authors
  • Sandy Sung (@sandy-sung-lb)

  • Mars Peng <mars.peng at getoken.io>

  • Taien Wang <taien.wang at getoken.io>

  • LeadBest Consulting Group <service at getoken.io>

Requires

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:

/// @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.

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

Copyright and related rights waived via CC0.