[ERC7439] チケットの正式な再販と不正なチケット転売を防止するインターフェースの仕組みを理解しよう！

# はじめに

初めまして。
**CryptoGames**というブロックチェーンゲーム企業でエンジニアをしている **cardene（かるでね）** です！
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。

https://cryptogames.co.jp/

代表的なゲームは**クリプトスペルズ**というブロックチェーンゲームです。

https://cryptospells.jp/

今回は、チケットの不正な転売を防ぎつつ、正式なチケット再販業者がチケットを再販するインターフェースを提案している規格である**ERC7439**についてまとめていきます！

:::note info
以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。
:::

https://eips.ethereum.org/EIPS/eip-7439

:::note warn
**7439**は現在（2023年12月7日）では「**Review**」段階です。
:::

他にも様々なERCについてまとめています。

https://cardene.notion.site/ERC-EIP-2a03fa3ea33d43baa9ed82288f98d4a9?pvs=4

# 概要

この標準は**ERC721**の改良版で、チケットの転売市場での問題に対処するための新しい方法を提供します。
主な目的は、チケットの不正な転売を防ぎ、顧客がチケットを公認リセール業者を通じて合法的に再販できるようにすることです。

:::note info
**ERC721**については以下を参考にしてください。
:::

https://chaldene.net/erc721

具体的には以下のような特徴があります。

### チケットの転売制限
この標準により、チケットの転売に関するルールが設定されます。
これにより、チケットの不正な転売や価格の釣り上げを防ぐことができます。

### 公認リセール業者の利用
チケットの所有者は、公認されたリセール業者を通じてチケットを正式に再販することができます。
これにより、市場の健全化が図られます。

### スマートコントラクトの活用
チケットの発行から転売、再販に至るまでのプロセスは、スマートコントラクトを通じて自動化されます。
スマートコントラクトは、特定の条件が満たされたときに自動的に実行されるコードベースのコントラクトです。

### 透明性とセキュリティの向上
ブロックチェーン技術を使用することで、取引の透明性が保たれ、セキュリティが強化されます。

### カスタマイズ可能な機能
イベント主催者やチケット販売代理店は、自身のニーズに応じてスマートコントラクトをカスタマイズできます。

この標準は、チケット市場における不正を減らし、より安全で公平な取引環境を作ることを目指しています。

# 動機

このテキストは、チケット転売市場での問題に取り組む新しい標準について説明しています。
この標準では、イベントチケットとして**ERC721**トークンを利用し、チケットの不正転売を防ぐためにトークンの移転を制限します。

### チケット転売市場の問題
長い間、大規模なチケット転売が問題となっており、詐欺や犯罪が社会的なリソースの浪費を引き起こしています。
これはアーティストや関連ビジネスにも悪影響を及ぼしています。
いくつかの国は転売を制限する法律を制定しましたが、その効果は限られています。

### 消費者に優しいリセールインターフェース
チケット購入者が元の価格以下でチケットを再販売または譲渡できるようにすることが、「**セカンダリーチケット**」市場の問題に対処する効率的な方法です。

### チケットの移転制限
紙やメールのバウチャー形式のチケットは容易に偽造や流通されます。
これを防ぐため、チケット代理店、マネージャー、プロモーター、承認されたリセールプラットフォームなど特定のアカウント以外のチケット移転を禁止するシステムを設計しました。

### 機能の向上
トークン情報スキーマを各チケットに実装し、所有者以外の承認されたアカウントだけがこれらの記録を変更できるようにしました。

この新しい標準の導入により、顧客を詐欺や不正行為から守るとともに、より公平で安全なチケット取引環境を提供することを目指しています。

# 仕様

この章では、イベントチケットとして使う**ERC721**トークンの情報と構造について話しています。
重要なポイントは以下の通りです。

### `TokenInfo`（トークン情報）
- `signature`（署名）
    - トークンが本物であることを証明するために、ユーザーや代理店が自分の秘密鍵で署名する内容を、それぞれ自分で決めることが推奨されます。
- `status`（状態）
    - トークンの現在の状況を示す。
- `expireTime`（有効期限）
    - イベントが終わる時刻に合わせて設定するのが良い。

### `TokenStatus`（トークンの状態）
- `Sold`（販売済み）
    - トークンが売られたら、状態は「**Sold**」になる。
    - この状態のトークンは使えます。
- `Resell`（再販売）
    - トークンが二次市場に出されたら、状態は「**Resell**」に変わる。
    - この状態でもトークンは使えます。
- `Void`（無効）
    - トークンの所有者が不正な取引をしたら、状態は「**Void**」に設定され、そのトークンは使えなくなります。
- `Redeemed`（利用済み）
    - トークンを使った後は、「**Redeemed**」という状態に変更するのが推奨されます。

これらの機能は、イベントチケットとして**ERC721**トークンを使用する時に、トークンの状態を管理し、その有効性を保つために重要です。

```javascript
/// @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;
}
```

## 関数以外

---

### TokenStatus

```javascript
enum TokenStatus {
    Sold,    // 0
    Resell,  // 1
    Void,    // 2
    Redeemed // 3
}
```

**概要**

トークンの現在の状態を表す列挙型。

**詳細**

この列挙型は、トークンが取り得るさまざまな状態を定義しています。
特定のロールのみがトークンの状態を変更できます。

**パラメータ**

- `Sold`
    - トークンが売られた状態。
- `Resell`
    - トークンが再販売されている状態。
- `Void`
    - トークンが無効になった状態。
- `Redeemed`
    - トークンが使用された、または引き換えられた状態。

---

### TokenInfo

```javascript
struct TokenInfo {
    bytes signature;
    TokenStatus tokenStatus;
    uint256 expireTime;
}
```

**概要**

トークンに関する情報を含む構造体。

**詳細**

この構造体は、トークンの署名、状態、および有効期限を格納します。

**パラメータ**

- `signature`
    - ユーザーまたは代理店のプライベートキーによって署名されたデータ。
- `tokenStatus`
    - 変更されるトークンの状態。
- `expireTime`
    - イベントの終了時刻。

---

### TokenStatusChanged

```javascript
event TokenStatusChanged(
    uint256 indexed tokenId,
    TokenStatus indexed tokenStatus,
    bytes signature
);
```

**概要**

トークンの状態が変更されたことを通知するイベント。

**詳細**

このイベントは、特定のトークンIDのトークンの状態が変更されたときに発行されます。
署名データも含まれています。

**パラメータ**

- `tokenId`
    - 状態が変更されたトークンのID。
- `tokenStatus`
    - 変更されたトークンの状態。
- `signature`
    - ユーザーまたは代理店のプライベートキーによって署名されたデータ。

---

## 関数

---

### `safeMint`

```javascript
function safeMint(address to, bytes memory signature) external;
```

**概要**

トークンの状態を含む新しいトークンを発行する関数。

**詳細**

この関数は外部から呼び出され、トークンの状態が変更された場合には`TokenStatusChanged`イベントを発行する必要があります。
トークンは指定された受取人に割り当てられます。

**引数**

- `to`
    - トークンの受取人のアドレス。
- `signature`
    - ユーザーまたは代理店のプライベートキーで署名されたデータ。

---

### `changeState`

```javascript
function changeState(
    uint256 tokenId,
    bytes memory signature,
    TokenStatus tokenStatus,
    uint256 newExpireTime
) external;
```

**概要**

特定のロールによってのみ呼び出されるトークンの状態を変更する関数。

**詳細**

この関数は外部から呼び出され、トークンの状態が変更された場合には`TokenStatusChanged`イベントを発行する必要があります。
トークンの状態や有効期限を変更することができます。

**引数**

- `tokenId`
    - 状態を変更するトークンのID。
- `signature`
    - ユーザーまたは代理店のプライベートキーで署名されたデータ。
- `tokenStatus`
    - 変更するトークンの状態。
- `newExpireTime`
    - 新しいイベント終了時刻。

---

`supportsInterface`メソッドは、`0xd010ceae`で呼び出されたときに`true`を返す必要があります。

# 補足

この章では、新しいチケット販売システムの設計における重要な考慮事項について説明しています。

## チケット販売代理店、パフォーマー、観客にとっての最重要事項

### チケット販売代理店
代理店にとっては、すべてのチケットを売り切ることが最優先事項です。
活気のある販売環境を作るため、時にはダフ屋と協力することがありますが、これは観客やパフォーマーにとって有害です。
このような状況を防ぐため、オープンで透明な初回販売チャネルと公平なセカンダリー販売機構が必要です。
`safeMint`関数では、誰もが公示価格で透明にチケットを発行でき、`TokenInfo`には購入者または代理店が解決できる署名が追加され、チケットの有効性を証明します。
しかし、チケット販売代理店にはプレッシャーもあります。
彼らはすべての有効なチケットを最大限に活用し、売り切ることを目指しています。
`_beforeTokenTransfer()`関数はアクセスコントロール機能を備え、`PARTNER_ROLE`のみがチケットの移転を行うことができます。

### パフォーマーまたはイベント主催者
チケット販売中に悪いニュースを避けたいと思っています。
彼らにとって重要なのは、真のファンが来場することです。
ダフ屋の手に渡るチケットや混沌としたセカンダリーマーケットは、真のファンには魅力的ではありません。
透明なメカニズムを通じて、パフォーマーまたはイベント主催者は実際の販売状況を常に把握できます。

### 観客
観客にとっては、有効なチケットを手に入れることが唯一の必要事項です。
伝統的な販売方法では、ファンはダフ屋やチケット販売代理店との戦いに直面することがあります。
透明な販売メカニズムは観客にとっても重要です。

:::note info
**ERC20**については以下を参考にしてください。
:::

https://chaldene.net/erc20

## 健全なチケットエコシステムの確立
供給と需要をバランス良く保つためには、明確なチケット販売ルールが必要です。
消費者を保護するために、オープンな価格設定システムが重要です。
流動性の高い市場が必要で、ユーザーは自分でチケットを発行（`mint`）でき、必要に応じてセカンダリーマーケットで移転することができます。
`changeState`関数を使って、`PARTNER_ROLE`だけがチケットの状態を「**再販**」に変更でき、これにより二次市場の正式な検証と保護が行われます。

## スムーズなチケット販売プロセスの設計
チケットの購入や販売は簡単で、NFTとしてチケットを購入（`mint`）することができます。
ショーがキャンセルになった場合の返金処理も簡単です。
ショー前にチケット代理店がチケットの検証を行い、`TokenStatus`が「**売済み**」であることを確認し、`expireTime`を使ってセッションの正確さを判断します。
検証が完了すると、`TokenStatus`を「**引き換え済み**」に変更できます。

このシステムは、**ERC20**や**ERC721**などのブロックチェーン技術を使用して、より透明で効率的なチケット販売を実現し、全ての関係者の利益を保護することを目指しています。

### Normal Flow
![normal.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570269/e99b0e36-42de-9817-37a5-81b4cee6599c.png)
引用: https://eips.ethereum.org/EIPS/eip-7439

### Void Flow
![void.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570269/25055f5f-0cba-d459-ae7e-c8c9458c338f.png)
引用: https://eips.ethereum.org/EIPS/eip-7439

### Resell Flow
![resell.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570269/4749b8e0-fa46-d3b2-2e25-f84d342d328f.png)
引用: https://eips.ethereum.org/EIPS/eip-7439

# 後方互換性

この規格は**ERC721**と互換性があります。

# テスト

```javascript
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);
  });
});
```

# 参考実装

```javascript
// 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);
    }
}
```

# セキュリティ考慮事項

この規格の実装に直接関係するセキュリティ上の考慮事項はないです。

# 引用

LeadBest Consulting Group <service@getoken.io>, Sandy Sung (@sandy-sung-lb), Mars Peng <mars.peng@getoken.io>, Taien Wang <taien.wang@getoken.io>, "ERC-7439: Prevent ticket touting [DRAFT]," Ethereum Improvement Proposals, no. 7439, July 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7439.

# 最後に

今回は「チケットの不正な転売を防ぎつつ、正式なチケット再販業者がチケットを再販するインターフェースを提案している規格である**ERC7439**」についてまとめてきました！
いかがだったでしょうか？

質問などがある方は以下のTwitterのDMなどからお気軽に質問してください！

[Twitter @cardene777](https://twitter.com/cardene777)

他の媒体でも情報発信しているのでぜひ他も見ていってください！

https://chaldene.net/

https://zenn.dev/heku

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58


https://cardene.notion.site/ERC-EIP-2a03fa3ea33d43baa9ed82288f98d4a9?pvs=4