はじめに

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

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

今回は、NFTに対して絵文字をつけてNFTに対しての印象を表すことができる*ERC6381**についてまとめていきます!

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

概要

この公共非代替トークンEmotリポジトリ標準は、ERC721およびERC1155に対して、NFTに感情を込めることを可能にする強化された対話型ユーティリティを提供します。

この提案では、Unicode標準の絵文字を使用してNFTにリアクションを示す機能を導入します。 これにより、NFTを所有する人々は、感情を表現するために絵文字を使用できます。 これは、公共の非ゲート型リポジトリスマートコントラクト内で実現され、すべてのネットワークで同じアドレスからアクセスできるようになります。 これは、特定のアドレスでアクセス可能なスマートコントラクトの集まりです。

動機

NFTがEthereumエコシステム内で広く使用され、さまざまなユースケースに利用されている中で、それらに追加のユーティリティを標準化する時が来ています。 NFTと対話する能力を持つことで、NFTを所有すること自体に対話的な要素を導入し、フィードバックに基づくNFTのメカニズムを開放します。

このERCは、ERC721ベースのトークンに対して以下の分野で新たなユーティリティを導入します。

  1. 対話性
  2. フィードバックに基づく進化
  3. 評価

対話性

NFTに感情を込める機能は、NFTを所有することに対話的な要素を導入します。 これは、感情を込める人(NFTに感情を示す人)への賞賛を反映する場合もありますし、トークンの所有者が特定のアクションを実行した結果である場合もあります。 トークンに対する感情が累積されると、そのトークンのユニークさや価値が高まる可能性があります。

フィードバックに基づく進化

NFTに対する標準化されたオンチェーンのリアクションは、フィードバックに基づく進化を可能にします。

現在の解決策は、NFTに対する対話的な機能が、プロプライエタリ(独自の)な方法やオフチェーン(ブロックチェーン上でない)の手段で提供されていることを意味します。 しかし、これらの方法は操作や不信感のリスクにさらされることがあります。 オンチェーン(ブロックチェーン上)でNFTとのインタラクションをトラッキングする能力を導入します。 これにより、NFTとのやり取りが透明にブロックチェーン上で記録され、信頼性のある情報源となります。 このアプローチは、操作や不正のリスクを軽減し、事実に基づいた客観的な評価を可能にします。

さらに、特定のエモートの回数などの閾値を満たすと、トークンが進化する仕組みを導入することが提案されています。 つまり、NFTに対するエモーショナルな対話が増えることで、トークン自体が進化し、新たな特性や価値を獲得することができるようになります。 これはトークンコレクションへのインタラクションを奨励し、コレクションの魅力を高める一因となります。

評価

現在のNFT市場は、トークンが売却された過去の価格、リストされたトークンの最低価格、マーケットプレイスが提供する希少性データに強く依存しています。 特定のトークンの賞賛や魅力のリアルタイムの指標はありません。 ユーザーがトークンに感情を込める機能を持つことで、トークンが収集した印象に基づいてトークンの価値を評価することが可能となります。

仕様

javascript
pragma solidity ^0.8.16;

interface IERC6381 /*is IERC165*/ {
    event Emoted(
        address indexed emoter,
        address indexed collection,
        uint256 indexed tokenId,
        bytes4 emoji,
        bool on
    );

    function emoteCountOf(
        address collection,
        uint256 tokenId,
        bytes4 emoji
    ) external view returns (uint256);

    function bulkEmoteCountOf(
        address[] memory collections,
        uint256[] memory tokenIds,
        bytes4[] memory emojis
    ) external view returns (uint256[] memory);

    function hasEmoterUsedEmote(
        address emoter,
        address collection,
        uint256 tokenId,
        bytes4 emoji
    ) external view returns (bool);

    function haveEmotersUsedEmotes(
        address[] memory emoters,
        address[] memory collections,
        uint256[] memory tokenIds,
        bytes4[] memory emojis
    ) external view returns (bool[] memory);

    function prepareMessageToPresignEmote(
        address collection,
        uint256 tokenId,
        bytes4 emoji,
        bool state,
        uint256 deadline
    ) external view returns (bytes32);

    function bulkPrepareMessagesToPresignEmote(
        address[] memory collections,
        uint256[] memory tokenIds,
        bytes4[] memory emojis,
        bool[] memory states,
        uint256[] memory deadlines
    ) external view returns (bytes32[] memory);

    function emote(
        address collection,
        uint256 tokenId,
        bytes4 emoji,
        bool state
    ) external;

    function bulkEmote(
        address[] memory collections,
        uint256[] memory tokenIds,
        bytes4[] memory emojis,
        bool[] memory states
    ) external;

    function presignedEmote(
        address emoter,
        address collection,
        uint256 tokenId,
        bytes4 emoji,
        bool state,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external;

    function bulkPresignedEmote(
        address[] memory emoters,
        address[] memory collections,
        uint256[] memory tokenIds,
        bytes4[] memory emojis,
        bool[] memory states,
        uint256[] memory deadlines,
        uint8[] memory v,
        bytes32[] memory r,
        bytes32[] memory s
    ) external;
}

Emoted

NFTに対してエモーションが行われたり、リアクションが取り消され時に発行されるイベント。

パラメータ

  • emoter
    • エモートまたはリアクションを行ったアカウントのアドレス。
  • collection
    • エモートまたはリアクションを行ったNFTが存在するコレクションのスマートコントラクトアドレス。
  • tokenId
    • エモートまたはリアクションを行ったNFTのトークンID。
  • emoji
    • エモートやリアクションに使用されたUnicode絵文字の識別子。
  • on
    • NFTに対するエモート (true) またはリアクションの取り消し (false) の状態。

emoteCountOf

特定のNFTに対して特定の絵文字がエモートされた回数を取得する関数。

パラメータ

  • collection
    • 絵文字のエモート回数を確認するNFTが存在するコレクションのスマートコントラクトアドレス。
  • tokenId
    • 絵文字のエモート回数を確認するNFTのトークンID。
  • emoji
    • エモート回数を取得したい絵文字のUnicode識別子。

戻り値

指定されたNFTに対する指定された絵文字のエモート回数。

bulkEmoteCountOf

複数のNFTに対して複数の絵文字がエモートされた回数を一括で取得する関数。

パラメータ

  • collections
    • エモート回数を確認するNFTが存在する複数のコレクションのスマートコントラクトアドレスの配列。
  • tokenIds
    • エモート回数を確認するNFTのトークンIDの配列。
  • emojis
    • エモート回数を取得したい絵文字のUnicode識別子の配列。

戻り値

各NFTに対する各絵文字のエモート回数の配列。

hasEmoterUsedEmote

特定のアドレスが特定のNFTに特定の絵文字を使用したかどうかを確認する関数。

  • emoter
    • リアクションを確認したいアカウントのアドレス。
  • collection
    • リアクションを確認したいNFTが存在するコレクションのスマートコントラクトアドレス。
  • tokenId
    • リアクションを確認したいNFTのトークンID。
  • emoji
    • リアクションを確認したい絵文字のASCIIコード。

戻り値

指定されたアドレスが各NFTに対して各絵文字を使用したかどうかを示すbool値。

haveEmotersUsedEmotes

複数のアドレスが複数のNFTに複数の絵文字を使用したかどうかを一括で確認する関数。

  • emoters
    • リアクションを確認したいアカウントのアドレスの配列。
  • collections
    • リアクションを確認したいNFTが存在するコレクションのスマートコントラクトアドレスの配列。
  • tokenIds
    • リアクションを確認したいNFTのトークンIDの配列。
  • emojis
    • リアクションを確認したい絵文字のASCIIコードの配列。

戻り値

各アドレスが各NFTに対して各絵文字を使用したかどうかを示すbool値の配列。

prepareMessageToPresignEmote

他の誰かがリアクションを提出するためにemoterによって署名されるメッセージを取得する関数。

  • collection
    • エモートが行われるNFTが存在するコレクションのスマートコントラクトアドレス。
  • tokenId
    • エモートが行われるNFTのトークンID。
  • emoji
    • エモートに使用されるUnicode絵文字の識別子。
  • state
    • エモートする (true) か取り消す (false) かを示すbool値。
  • deadline
    • 署名が提出される期限のUNIXタイムスタンプ。

戻り値

他の誰かがリアクションを提出するためにemoterによって署名されるメッセージのバイト列。

bulkPrepareMessagesToPresignEmote

複数のNFTに対して複数の絵文字のリアクションを提出するためにemoterによって署名される複数のメッセージを一括で取得する関数。

  • collections
    • エモートが行われるNFTが存在するコレクションのスマートコントラクトアドレスの配列。
  • tokenIds
    • エモートが行われるNFTのトークンIDの配列。
  • emojis
    • エモートに使用されるUnicode絵文字の識別子の配列。
  • states
    • エモートする (true) か取り消す (false) かを示すbool値の配列。
  • deadlines
    • 署名が提出される期限のUNIXタイムスタンプの配列。

戻り値

他の誰かがリアクションを提出するためにemoterによって署名される複数のメッセージのバイト列の配列。

emote

NFTに対してエモートまたはリアクションを取り消すための関数。 既存の状態を変更しようとした場合は何もしません。

  • collection
    • エモートが行われるNFTが存在するコレクションのスマートコントラクトアドレス。
  • tokenId
    • エモートが行われるNFTのトークンID。
  • emoji
    • エモートに使用されるUnicode絵文字の識別子。
  • state
    • エモートする (true) か取り消す (false) かを示すbool値。

bulkEmote

複数のNFTに対して一括でエモートまたはリアクションを取り消すための関数。 既存の状態を変更しようとした場合は何もしません。

  • collections
    • エモートが行われるNFTが存在するコレクションのスマートコントラクトアドレスの配列。
  • tokenIds
    • エモートが行われるNFTのトークンIDの配列。
  • emojis
    • エモートに使用されるUnicode絵文字の識別子の配列。
  • states
    • エモートする (true) か取り消す (false) かを示すbool値の配列。

presignedEmote

他の誰かの代わりにエモートまたはリアクションを取り消すための関数。 署名されたメッセージに基づいて操作を行います。

  • emoter
    • エモートを事前に署名したアドレス。
  • collection
    • エモートが行われるNFTが存在するコレクションのスマートコントラクトアドレス。
  • tokenId
    • エモートが行われるNFTのトークンID。
  • emoji
    • エモートに使用されるUnicode絵文字の識別子。
  • state
    • エモートする (true) か取り消す (false) かを示すbool値。
  • deadline
    • 署名が提出される期限のUNIXタイムスタンプ。
  • v
    • ECDSA署名のv値。
  • r
    • ECDSA署名のr値。
  • s
    • ECDSA署名のs値。

bulkPresignedEmote

複数のNFTに対して他の誰かの代わりに一括でエモートまたはリアクションを取り消すための関数。 署名されたメッセージに基づいて操作を行います。

  • emoters
    • エモートを事前に署名したアドレスの配列。
  • collections
    • エモートが行われるNFTが存在するコレクションのスマートコントラクトアドレスの配列。
  • tokenIds
    • エモートが行われるNFTのトークンIDの配列。
  • emojis
    • エモートに使用されるUnicode絵文字の識別子の配列。
  • states
    • エモートする (true) か取り消す (false) かを示すbool値の配列。
  • deadlines
    • 署名が提出される期限のUNIXタイムスタンプの配列。
  • v
    • ECDSA署名のv値の配列。
  • r
    • ECDSA署名のr値の配列。
  • s
    • ECDSA署名のs値の配列。

署名付きエモートのメッセージフォーマット

リアクションが他の誰かによって送信されるために、emoterによって署名されるメッセージは、次のようにフォーマットされます。

javascript
keccak256(
        abi.encode(
            DOMAIN_SEPARATOR,
            collection,
            tokenId,
            emoji,
            state,
            deadline
        )
    );
  • DOMAIN_SEPARATOR
    • 生成されたドメインセパレーター。
  • collection
    • エモートが行われるNFTが存在するコレクションのスマートコントラクトアドレス。
  • tokenId
    • エモートが行われるNFTのトークンID。
  • emoji
    • エモートに使用されるUnicode絵文字の識別子。
  • state
    • エモートする (true) か取り消す (false) かを示すbool値。
  • deadline
    • 署名が提出される期限のUNIXタイムスタンプ。

これらの値を使用して、署名対象のメッセージが生成されます。

以上の手順に従って、ドメインセパレーターと署名対象メッセージが生成され、署名されることでセキュリティと認証が確保されます。

DOMAIN_SEPARATOR (ドメインセパレーター) の生成は以下のように行われます。

javascript
keccak256(
        abi.encode(
            "ERC-6381: Public Non-Fungible Token Emote Repository",
            "1",
            block.chainid,
            address(this)
        )
    );

上から順に以下の値を示しています。

  • "ERC-6381: Public Non-Fungible Token Emote Repository"
    • メッセージハッシュに使用するハッシュアルゴリズムの識別子。
  • "1"
    • メッセージのバージョン番号。
  • block.chainid
    • メッセージが存在するブロックチェーンのチェーンID。
  • address(this)
    • Emotableリポジトリのスマートコントラクトのアドレス。

Emotableリポジトリ・スマートコントラクトがデプロイされる各チェーンは、チェーンIDが異なるため、異なるDOMAIN_SEPARATOR値を持ちます。

Emotableリポジトリのスマートコントラクトの事前に決定されたアドレス

Emotableリポジトリのスマートコントラクトのアドレスは、その機能を反映するように設計されています。 このアドレスは、0x311073から始まります。 これは「EMOTE」の抽象的な表現です。 具体的なアドレスは以下のようになります。

0x31107354b61A0412E722455A771bC462901668eA

このアドレスは、Emotableリポジトリのスマートコントラクトが実際に存在する場所を特定するためのものであり、アドレスの一部に「EMOTE」を含むことで、その目的や役割を示しています。 このアドレスはユーザーによって使用され、エモート関連の操作が行われる場所として機能します。

補足

1. 提案はカスタムエモートをサポートするのか、それともUnicodeで指定されたエモートのみをサポートするのか?

この提案は、Unicode識別子(bytes4値)のみを受け入れます。 これは、標準化された絵文字を使用してリアクションを追加することが奨励される一方で、Unicode標準に含まれていない値はカスタムエモートとして使用できます。 ただし、その場合、リアクションを表示するインターフェースは、どの種類の画像を描画するかを知っている必要があります。 そのため、カスタムエモートはインターフェースやマーケットプレース内で限定的に追加される可能性があります。

2. 提案はNFTの印象を伝えるために絵文字を使用すべきか、他の方法を採用すべきか?

NFTの印象は、ユーザーが提供する文字列や数値を使用して伝えることも考えられましたが、我々は感情や印象を伝える手段として広く認知されている絵文字を使用することを選びました。

3. 提案はEmotable拡張を確立すべきか、共通のリポジトリとして確立すべきか?

当初、我々はEmotable拡張を作成して、任意のERC721準拠トークンと共に使用することを目指していました。 しかし、提案が古いものから新しいものまで、すべてのNFTトークンをリアクション対象とする共通のリポジトリとして提供される方がより有用であると判断しました。 これにより、リアクションを受けることができるトークンは新しいものだけでなく、提案以前から存在している古いものも対象となります。

4. 単一のアクション操作のみを含めるべきか、複数のアクション操作のみを含めるべきか、それとも両方を含めるべきか?

単一のアクション操作のみを含めることも検討しましたが、最終的には単一のアクション操作と複数のアクション操作の両方を含めることにしました。 これにより、ユーザーは単一のトークンまたは複数のトークンに対してエモートまたはエモートの取り消しを選択できます。 ネットワークのガスコストやコレクション内のトークン数に基づいて、ユーザーは最も費用効果の高いエモート方法を選択できます。

5. 他人の代わりにエモートする機能を追加すべきか?

当初、提案の一部としてこれを追加するつもりはありませんでしたが、後にこの機能が有用であると気付きました。 これにより、ユーザーは他の誰かの代わりにエモートすることができます。 例えば、その他の理由で自分で行うことができない場合や、オフチェーンの活動によってエモートが獲得される場合などです。

6. 他人の代わりにエモートすることが正当であることをどのように保証するか?

提案にデリゲートを追加することで、ユーザーがエモート権利を他の誰かに委任することができるようにすることができます。 しかし、これは提案に多くの複雑さと追加のロジックを加えることになります。 ECDSA署名を使用することで、ユーザーが他人によるエモートを許可したことを確認できます。 ユーザーはエモートのパラメータを含むメッセージに署名し、その署名を他の誰かが提出することができます。

7. トークンにリアクションする際にチェーンIDをパラメータとして追加すべきか?

提案の議論の中で、トークンにリアクションする際にチェーンIDをパラメータとして追加する提案が出ました。 これにより、ユーザーはあるチェーン上のトークンに対して別のチェーン上でリアクションをすることができます。 しかし、我々はこれに反対しました。 追加のパラメータはほとんど使用されない可能性があり、リアクショントランザクションに追加のコストをもたらす可能性があるためです。 また、コレクションスマートコントラクトがその内部でトークンに対するリアクションを活用する場合、リアクションは同じチェーン上で記録される必があります。 この提案を統合するマーケットプレースやウォレットも同様に、リアクションは同じチェーン上に存在するものと想定されます。

テスト

以下がテストファイルになります。

javascript
import { ethers } from "hardhat";
import { expect } from "chai";
import { BigNumber, Contract } from "ethers";
import { loadFixture } from "@nomicfoundation/hardhat-network-helpers";
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
import { ERC721Mock, EmotableRepository } from "../typechain-types";

function bn(x: number): BigNumber {
  return BigNumber.from(x);
}

async function tokenFixture() {
  const factory = await ethers.getContractFactory("ERC721Mock");
  const token = await factory.deploy("Chunky", "CHNK");
  await token.deployed();

  return token;
}

async function emotableRepositoryFixture() {
  const factory = await ethers.getContractFactory("EmotableRepository");
  const repository = await factory.deploy();
  await repository.deployed();

  return repository;
}

describe("RMRKEmotableRepositoryMock", async function () {
  let token: ERC721Mock;
  let repository: EmotableRepository;
  let owner: SignerWithAddress;
  let addrs: SignerWithAddress[];
  const tokenId = bn(1);
  const emoji1 = Buffer.from("😎");
  const emoji2 = Buffer.from("😁");

  beforeEach(async function () {
    [owner, ...addrs] = await ethers.getSigners();
    token = await loadFixture(tokenFixture);
    repository = await loadFixture(emotableRepositoryFixture);
  });

  it("can support IERC6381", async function () {
    expect(await repository.supportsInterface("0xd9fac55a")).to.equal(true);
  });

  it("can support IERC165", async function () {
    expect(await repository.supportsInterface("0x01ffc9a7")).to.equal(true);
  });

  it("does not support other interfaces", async function () {
    expect(await repository.supportsInterface("0xffffffff")).to.equal(false);
  });

  describe("With minted tokens", async function () {
    beforeEach(async function () {
      await token.mint(owner.address, tokenId);
    });

    it("can emote", async function () {
      await expect(
        repository.connect(addrs[0]).emote(token.address, tokenId, emoji1, true)
      )
        .to.emit(repository, "Emoted")
        .withArgs(
          addrs[0].address,
          token.address,
          tokenId.toNumber(),
          emoji1,
          true
        );
      expect(
        await repository.emoteCountOf(token.address, tokenId, emoji1)
      ).to.equal(bn(1));
    });

    it("can undo emote", async function () {
      await repository.emote(token.address, tokenId, emoji1, true);

      await expect(repository.emote(token.address, tokenId, emoji1, false))
        .to.emit(repository, "Emoted")
        .withArgs(
          owner.address,
          token.address,
          tokenId.toNumber(),
          emoji1,
          false
        );
      expect(
        await repository.emoteCountOf(token.address, tokenId, emoji1)
      ).to.equal(bn(0));
    });

    it("can be emoted from different accounts", async function () {
      await repository
        .connect(addrs[0])
        .emote(token.address, tokenId, emoji1, true);
      await repository
        .connect(addrs[1])
        .emote(token.address, tokenId, emoji1, true);
      await repository
        .connect(addrs[2])
        .emote(token.address, tokenId, emoji2, true);
      expect(
        await repository.emoteCountOf(token.address, tokenId, emoji1)
      ).to.equal(bn(2));
      expect(
        await repository.emoteCountOf(token.address, tokenId, emoji2)
      ).to.equal(bn(1));
    });

    it("can add multiple emojis to same NFT", async function () {
      await repository.emote(token.address, tokenId, emoji1, true);
      await repository.emote(token.address, tokenId, emoji2, true);
      expect(
        await repository.emoteCountOf(token.address, tokenId, emoji1)
      ).to.equal(bn(1));
      expect(
        await repository.emoteCountOf(token.address, tokenId, emoji2)
      ).to.equal(bn(1));
    });

    it("does nothing if new state is the same as old state", async function () {
      await repository.emote(token.address, tokenId, emoji1, true);
      await repository.emote(token.address, tokenId, emoji1, true);
      expect(
        await repository.emoteCountOf(token.address, tokenId, emoji1)
      ).to.equal(bn(1));

      await repository.emote(token.address, tokenId, emoji2, false);
      expect(
        await repository.emoteCountOf(token.address, tokenId, emoji2)
      ).to.equal(bn(0));
    });

    it("can bulk emote", async function () {
      expect(
        await repository.bulkEmoteCountOf(
          [token.address, token.address],
          [tokenId, tokenId],
          [emoji1, emoji2]
        )
      ).to.eql([bn(0), bn(0)]);

      expect(
        await repository.haveEmotersUsedEmotes(
          [owner.address, owner.address],
          [token.address, token.address],
          [tokenId, tokenId],
          [emoji1, emoji2]
        )
      ).to.eql([false, false]);

      await expect(
        repository.bulkEmote(
          [token.address, token.address],
          [tokenId, tokenId],
          [emoji1, emoji2],
          [true, true]
        )
      )
        .to.emit(repository, "Emoted")
        .withArgs(
          owner.address,
          token.address,
          tokenId.toNumber(),
          emoji1,
          true
        )
        .to.emit(repository, "Emoted")
        .withArgs(
          owner.address,
          token.address,
          tokenId.toNumber(),
          emoji2,
          true
        );

      expect(
        await repository.bulkEmoteCountOf(
          [token.address, token.address],
          [tokenId, tokenId],
          [emoji1, emoji2]
        )
      ).to.eql([bn(1), bn(1)]);

      expect(
        await repository.haveEmotersUsedEmotes(
          [owner.address, owner.address],
          [token.address, token.address],
          [tokenId, tokenId],
          [emoji1, emoji2]
        )
      ).to.eql([true, true]);
    });

    it("can bulk undo emote", async function () {
      await expect(
        repository.bulkEmote(
          [token.address, token.address],
          [tokenId, tokenId],
          [emoji1, emoji2],
          [true, true]
        )
      )
        .to.emit(repository, "Emoted")
        .withArgs(
          owner.address,
          token.address,
          tokenId.toNumber(),
          emoji1,
          true
        )
        .to.emit(repository, "Emoted")
        .withArgs(
          owner.address,
          token.address,
          tokenId.toNumber(),
          emoji2,
          true
        );

      expect(
        await repository.bulkEmoteCountOf(
          [token.address, token.address],
          [tokenId, tokenId],
          [emoji1, emoji2]
        )
      ).to.eql([bn(1), bn(1)]);

      expect(
        await repository.haveEmotersUsedEmotes(
          [owner.address, owner.address],
          [token.address, token.address],
          [tokenId, tokenId],
          [emoji1, emoji2]
        )
      ).to.eql([true, true]);

      await expect(
        repository.bulkEmote(
          [token.address, token.address],
          [tokenId, tokenId],
          [emoji1, emoji2],
          [false, false]
        )
      )
        .to.emit(repository, "Emoted")
        .withArgs(
          owner.address,
          token.address,
          tokenId.toNumber(),
          emoji1,
          false
        )
        .to.emit(repository, "Emoted")
        .withArgs(
          owner.address,
          token.address,
          tokenId.toNumber(),
          emoji2,
          false
        );

      expect(
        await repository.bulkEmoteCountOf(
          [token.address, token.address],
          [tokenId, tokenId],
          [emoji1, emoji2]
        )
      ).to.eql([bn(0), bn(0)]);

      expect(
        await repository.haveEmotersUsedEmotes(
          [owner.address, owner.address],
          [token.address, token.address],
          [tokenId, tokenId],
          [emoji1, emoji2]
        )
      ).to.eql([false, false]);
    });

    it("can bulk emote and unemote at the same time", async function () {
      await repository.emote(token.address, tokenId, emoji2, true);

      expect(
        await repository.bulkEmoteCountOf(
          [token.address, token.address],
          [tokenId, tokenId],
          [emoji1, emoji2]
        )
      ).to.eql([bn(0), bn(1)]);

      expect(
        await repository.haveEmotersUsedEmotes(
          [owner.address, owner.address],
          [token.address, token.address],
          [tokenId, tokenId],
          [emoji1, emoji2]
        )
      ).to.eql([false, true]);

      await expect(
        repository.bulkEmote(
          [token.address, token.address],
          [tokenId, tokenId],
          [emoji1, emoji2],
          [true, false]
        )
      )
        .to.emit(repository, "Emoted")
        .withArgs(
          owner.address,
          token.address,
          tokenId.toNumber(),
          emoji1,
          true
        )
        .to.emit(repository, "Emoted")
        .withArgs(
          owner.address,
          token.address,
          tokenId.toNumber(),
          emoji2,
          false
        );

      expect(
        await repository.bulkEmoteCountOf(
          [token.address, token.address],
          [tokenId, tokenId],
          [emoji1, emoji2]
        )
      ).to.eql([bn(1), bn(0)]);

      expect(
        await repository.haveEmotersUsedEmotes(
          [owner.address, owner.address],
          [token.address, token.address],
          [tokenId, tokenId],
          [emoji1, emoji2]
        )
      ).to.eql([true, false]);
    });

    it("can not bulk emote if passing arrays of different length", async function () {
      await expect(
        repository.bulkEmote(
          [token.address, token.address],
          [tokenId, tokenId],
          [emoji1, emoji2],
          [true]
        )
      ).to.be.revertedWithCustomError(
        repository,
        "BulkParametersOfUnequalLength"
      );

      await expect(
        repository.bulkEmote(
          [token.address],
          [tokenId, tokenId],
          [emoji1, emoji2],
          [true, true]
        )
      ).to.be.revertedWithCustomError(
        repository,
        "BulkParametersOfUnequalLength"
      );

      await expect(
        repository.bulkEmote(
          [token.address, token.address],
          [tokenId],
          [emoji1, emoji2],
          [true, true]
        )
      ).to.be.revertedWithCustomError(
        repository,
        "BulkParametersOfUnequalLength"
      );

      await expect(
        repository.bulkEmote(
          [token.address, token.address],
          [tokenId, tokenId],
          [emoji1],
          [true, true]
        )
      ).to.be.revertedWithCustomError(
        repository,
        "BulkParametersOfUnequalLength"
      );
    });

    it("can use presigned emote to react to token", async function () {
      const message = await repository.prepareMessageToPresignEmote(
        token.address,
        tokenId,
        emoji1,
        true,
        bn(9999999999)
      );

      const signature = await owner.signMessage(ethers.utils.arrayify(message));

      const r: string = signature.substring(0, 66);
      const s: string = "0x" + signature.substring(66, 130);
      const v: number = parseInt(signature.substring(130, 132), 16);

      await expect(
        repository
          .connect(addrs[0])
          .presignedEmote(
            owner.address,
            token.address,
            tokenId,
            emoji1,
            true,
            bn(9999999999),
            v,
            r,
            s
          )
      )
        .to.emit(repository, "Emoted")
        .withArgs(
          owner.address,
          token.address,
          tokenId.toNumber(),
          emoji1,
          true
        );
    });

    it("can use presigned emotes to bulk react to token", async function () {
      const messages = await repository.bulkPrepareMessagesToPresignEmote(
        [token.address, token.address],
        [tokenId, tokenId],
        [emoji1, emoji2],
        [true, true],
        [bn(9999999999), bn(9999999999)]
      );

      const signature1 = await owner.signMessage(
        ethers.utils.arrayify(messages[0])
      );
      const signature2 = await owner.signMessage(
        ethers.utils.arrayify(messages[1])
      );

      const r1: string = signature1.substring(0, 66);
      const s1: string = "0x" + signature1.substring(66, 130);
      const v1: number = parseInt(signature1.substring(130, 132), 16);
      const r2: string = signature2.substring(0, 66);
      const s2: string = "0x" + signature2.substring(66, 130);
      const v2: number = parseInt(signature2.substring(130, 132), 16);

      await expect(
        repository
          .connect(addrs[0])
          .bulkPresignedEmote(
            [owner.address, owner.address],
            [token.address, token.address],
            [tokenId, tokenId],
            [emoji1, emoji2],
            [true, true],
            [bn(9999999999), bn(9999999999)],
            [v1, v2],
            [r1, r2],
            [s1, s2]
          )
      )
        .to.emit(repository, "Emoted")
        .withArgs(
          owner.address,
          token.address,
          tokenId.toNumber(),
          emoji1,
          true
        )
        .to.emit(repository, "Emoted")
        .withArgs(
          owner.address,
          token.address,
          tokenId.toNumber(),
          emoji2,
          true
        );
    });
  });
});

以下のコマンドでテストを実行できます。

text
cd ../assets/eip-6381
npm install
npx hardhat test

参考実装

javascript
// SPDX-License-Identifier: CC0-1.0

pragma solidity ^0.8.16;

import "./IERC6381.sol";
import "@openzeppelin/contracts/utils/introspection/ERC165.sol";

error BulkParametersOfUnequalLength();
error ExpiredPresignedEmote();
error InvalidSignature();

contract EmotableRepository is IERC6381 {
    bytes32 public immutable DOMAIN_SEPARATOR = keccak256(
        abi.encode(
            "ERC-6381: Public Non-Fungible Token Emote Repository",
            "1",
            block.chainid,
            address(this)
        )
    );

    // Used to avoid double emoting and control undoing
    mapping(address => mapping(address => mapping(uint256 => mapping(bytes4 => uint256))))
        private _emotesUsedByEmoter; // Cheaper than using a bool
    mapping(address => mapping(uint256 => mapping(bytes4 => uint256)))
        private _emotesPerToken;

    function emoteCountOf(
        address collection,
        uint256 tokenId,
        bytes4 emoji
    ) public view returns (uint256) {
        return _emotesPerToken[collection][tokenId][emoji];
    }

    function bulkEmoteCountOf(
        address[] memory collections,
        uint256[] memory tokenIds,
        bytes4[] memory emojis
    ) public view returns (uint256[] memory) {
        if(
            collections.length != tokenIds.length ||
                collections.length != emojis.length
        ){
            revert BulkParametersOfUnequalLength();
        }

        uint256[] memory counts = new uint256[](collections.length);
        for (uint256 i; i < collections.length; ) {
            counts[i] = _emotesPerToken[collections[i]][tokenIds[i]][emojis[i]];
            unchecked {
                ++i;
            }
        }
        return counts;
    }

    function hasEmoterUsedEmote(
        address emoter,
        address collection,
        uint256 tokenId,
        bytes4 emoji
    ) public view returns (bool) {
        return _emotesUsedByEmoter[emoter][collection][tokenId][emoji] == 1;
    }

    function haveEmotersUsedEmotes(
        address[] memory emoters,
        address[] memory collections,
        uint256[] memory tokenIds,
        bytes4[] memory emojis
    ) public view returns (bool[] memory) {
        if(
            emoters.length != collections.length ||
                emoters.length != tokenIds.length ||
                emoters.length != emojis.length
        ){
            revert BulkParametersOfUnequalLength();
        }

        bool[] memory states = new bool[](collections.length);
        for (uint256 i; i < collections.length; ) {
            states[i] = _emotesUsedByEmoter[emoters[i]][collections[i]][tokenIds[i]][emojis[i]] == 1;
            unchecked {
                ++i;
            }
        }
        return states;
    }

    function emote(
        address collection,
        uint256 tokenId,
        bytes4 emoji,
        bool state
    ) public {
        bool currentVal = _emotesUsedByEmoter[msg.sender][collection][tokenId][
            emoji
        ] == 1;
        if (currentVal != state) {
            if (state) {
                _emotesPerToken[collection][tokenId][emoji] += 1;
            } else {
                _emotesPerToken[collection][tokenId][emoji] -= 1;
            }
            _emotesUsedByEmoter[msg.sender][collection][tokenId][emoji] = state
                ? 1
                : 0;
            emit Emoted(msg.sender, collection, tokenId, emoji, state);
        }
    }

    function bulkEmote(
        address[] memory collections,
        uint256[] memory tokenIds,
        bytes4[] memory emojis,
        bool[] memory states
    ) public {
        if(
            collections.length != tokenIds.length ||
                collections.length != emojis.length ||
                collections.length != states.length
        ){
            revert BulkParametersOfUnequalLength();
        }

        bool currentVal;
        for (uint256 i; i < collections.length; ) {
            currentVal = _emotesUsedByEmoter[msg.sender][collections[i]][tokenIds[i]][
                emojis[i]
            ] == 1;
            if (currentVal != states[i]) {
                if (states[i]) {
                    _emotesPerToken[collections[i]][tokenIds[i]][emojis[i]] += 1;
                } else {
                    _emotesPerToken[collections[i]][tokenIds[i]][emojis[i]] -= 1;
                }
                _emotesUsedByEmoter[msg.sender][collections[i]][tokenIds[i]][emojis[i]] = states[i]
                    ? 1
                    : 0;
                emit Emoted(msg.sender, collections[i], tokenIds[i], emojis[i], states[i]);
            }
            unchecked {
                ++i;
            }
        }
    }
    
    function prepareMessageToPresignEmote(
        address collection,
        uint256 tokenId,
        bytes4 emoji,
        bool state,
        uint256 deadline
    ) public view returns (bytes32) {
        return keccak256(
            abi.encode(
                DOMAIN_SEPARATOR,
                collection,
                tokenId,
                emoji,
                state,
                deadline
            )
        );
    }
    
    function bulkPrepareMessagesToPresignEmote(
        address[] memory collections,
        uint256[] memory tokenIds,
        bytes4[] memory emojis,
        bool[] memory states,
        uint256[] memory deadlines
    ) public view returns (bytes32[] memory) {
        if(
            collections.length != tokenIds.length ||
                collections.length != emojis.length ||
                collections.length != states.length ||
                collections.length != deadlines.length
        ){
            revert BulkParametersOfUnequalLength();
        }

        bytes32[] memory messages = new bytes32[](collections.length);
        for (uint256 i; i < collections.length; ) {
            messages[i] = keccak256(
                abi.encode(
                    DOMAIN_SEPARATOR,
                    collections[i],
                    tokenIds[i],
                    emojis[i],
                    states[i],
                    deadlines[i]
                )
            );
            unchecked {
                ++i;
            }
        }
        
        return messages;
    }

    function presignedEmote(
        address emoter,
        address collection,
        uint256 tokenId,
        bytes4 emoji,
        bool state,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) public {
        if(block.timestamp > deadline){
            revert ExpiredPresignedEmote();
        }
        bytes32 digest = keccak256(
            abi.encodePacked(
                "\x19Ethereum Signed Message:\n32",
                keccak256(
                    abi.encode(
                        DOMAIN_SEPARATOR,
                        collection,
                        tokenId,
                        emoji,
                        state,
                        deadline
                    )
                )
            )
        );
        address signer = ecrecover(digest, v, r, s);
        if(signer != emoter){
            revert InvalidSignature();
        }
        
        bool currentVal = _emotesUsedByEmoter[signer][collection][tokenId][
            emoji
        ] == 1;
        if (currentVal != state) {
            if (state) {
                _emotesPerToken[collection][tokenId][emoji] += 1;
            } else {
                _emotesPerToken[collection][tokenId][emoji] -= 1;
            }
            _emotesUsedByEmoter[signer][collection][tokenId][emoji] = state
                ? 1
                : 0;
            emit Emoted(signer, collection, tokenId, emoji, state);
        }
    }
    
    function bulkPresignedEmote(
        address[] memory emoters,
        address[] memory collections,
        uint256[] memory tokenIds,
        bytes4[] memory emojis,
        bool[] memory states,
        uint256[] memory deadlines,
        uint8[] memory v,
        bytes32[] memory r,
        bytes32[] memory s
    ) public {
        if(
            emoters.length != collections.length ||
                emoters.length != tokenIds.length ||
                emoters.length != emojis.length ||
                emoters.length != states.length ||
                emoters.length != deadlines.length ||
                emoters.length != v.length ||
                emoters.length != r.length ||
                emoters.length != s.length
        ){
            revert BulkParametersOfUnequalLength();
        }

        bytes32 digest;
        address signer;
        bool currentVal;
        for (uint256 i; i < collections.length; ) {
            if (block.timestamp > deadlines[i]){
                revert ExpiredPresignedEmote();
            }
            digest = keccak256(
                abi.encodePacked(
                    "\x19Ethereum Signed Message:\n32",
                    keccak256(
                        abi.encode(
                            DOMAIN_SEPARATOR,
                            collections[i],
                            tokenIds[i],
                            emojis[i],
                            states[i],
                            deadlines[i]
                        )
                    )
                )
            );
            signer = ecrecover(digest, v[i], r[i], s[i]);
            if(signer != emoters[i]){
                revert InvalidSignature();
            }
            
            currentVal = _emotesUsedByEmoter[signer][collections[i]][tokenIds[i]][
                emojis[i]
            ] == 1;
            if (currentVal != states[i]) {
                if (states[i]) {
                    _emotesPerToken[collections[i]][tokenIds[i]][emojis[i]] += 1;
                } else {
                    _emotesPerToken[collections[i]][tokenIds[i]][emojis[i]] -= 1;
                }
                _emotesUsedByEmoter[signer][collections[i]][tokenIds[i]][emojis[i]] = states[i]
                    ? 1
                    : 0;
                emit Emoted(signer, collections[i], tokenIds[i], emojis[i], states[i]);
            }
            unchecked {
                ++i;
            }
        }
    }

    function supportsInterface(
        bytes4 interfaceId
    ) public view virtual returns (bool) {
        return
            interfaceId == type(IERC6381).interfaceId ||
            interfaceId == type(IERC165).interfaceId;
    }
}

セキュリティ考慮事項

この提案では、ユーザーからの資産を取り扱うことは想定されていないため、Emoteリポジトリとの対話時に資産が危険にさらされることはありません。

他の人の代わりにECDSA署名を使用してエモートする機能は、リプレイ攻撃のリスクを導入する可能性がありますが、署名対象のメッセージの形式がその対策となります。 署名対象のメッセージに使用されるDOMAIN_SEPARATORは、そのチェーンに展開されたリポジトリスマートコントラクト固有のものです。 したがって、署名は他のどのチェーンでも無効であり、それらのチェーン上に展開されたEmoteリポジトリは、リプレイ攻撃が試みられた場合に操作を取り消すはずです。

もう一つの考慮事項は、事前に署名されたメッセージの再利用の可能性です。 メッセージには署名の有効期限が含まれており、その有効期限が到来する前にメッセージを何度でも再利用することができます。 提案では特定のトークンに対して特定の絵文字で単一のリアクションのみが有効ですので、事前に署名されたメッセージを使用してトークン上のリアクション数を増やすことはできません。 しかし、リポジトリを使用するサービスが特定のアクション後にリアクションを取り消す機能を依存している場合、有効な事前署名メッセージを使用してトークンに再度リアクションを行うことができます。 事前署名メッセージを使用する際には、合理的に短い時間で事前署名メッセージを無効にする期限を設定することを提案します。

非監査済みのコントラクトを取り扱う際には注意が必要です。

#引用

Bruno Škvorc (@Swader), Steven Pineda (@steven2308), Stevan Bogosavljevic (@stevyhacker), Jan Turk (@ThunderDeliverer), "ERC-6381: Public Non-Fungible Token Emote Repository," Ethereum Improvement Proposals, no. 6381, January 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-6381.

考察

NFTに対して絵文字をつけられること自体は面白い試みだなと思いました。 しかし、実際の活用場面がはっきりとイメージしていないです。 パッと思いつくのは、SBT形式のNFTにしたのちコミュニティ内での活動などに応じて他のメンバーが絵文字を付与してくれて、その数に応じて報酬をもらえるとかですかね🤔 ブロックチェーン上でいいねのような絵文字を管理する意味合いがまだ見出せていないです...。 絵文字を付与するためには少額ですがガス代を支払う必要があるため、「本当に良い」と思ったNFTに対して絵文字を付与するはずです。 そのため、記事などに対して絵文字を付与して、その絵文字に数に応じて報酬をもらえるとかでも面白そうですね。 まだまだ理解が足りていないかもしれないので、引き続き調べていきます。

最後に

今回は「NFTに対して絵文字をつけてNFTに対しての印象を表すことができる*ERC6381**」についてまとめてきました! いかがだったでしょうか?

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

採用強化中!

CryptoGamesでは一緒に働く仲間を大募集中です。

この記事で書いた自分の経験からもわかるように、裁量権を持って働くことができて一気に成長できる環境です。 「ブロックチェーンやWeb3、NFTに興味がある」、「スマートコントラクトの開発に携わりたい」など、少しでも興味を持っている方はまずはお話ししましょう!

参考