はじめに

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

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

今回は、ERC721拡張機能で、オンチェーンでの時間管理をサポートするための追加機能であるstartTime、endTimeを提案している、ERC5007の説明をしていきます!

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

動機

一部のNFTは使用期間が決められており、その期間外では使用できません。 時間情報が定義されていない従来のERC721規格のNFTでは、NFTを時間情報によって無効/有効するには、処理が煩雑でガス代が余分にかかってしまいます。

一部の従来のERC721規格のNFTには時間機能が含まれていますが、インターフェースが統一されていないため、サードパーティのプラットフォームで使用するのが難しかったです。

startTime、endTimeを導入することで、NFTを自動的に有効または無効にすることが可能になります。

仕様

javascript
interface IERC5007 /* is IERC721 */ {
    function startTime(uint256 tokenId) external view returns (uint64);
    
    function endTime(uint256 tokenId) external view returns (uint64);
}

startTime

NFTが有効となる時間を取得する関数。

引数に渡されたtokenIdに対応するNFTの有効となる開始時間を、UNIXタイムスタンプとして取得します。

endTime

NFTが無効となる時間を取得する関数。

引数に渡されたtokenIdに対応するNFTの無効となる終了時間を、UNIXタイムスタンプとして取得します。

オプション仕様

以下のインターフェースはオプションの実装となります。

javascript
interface IERC5007Composable /* is IERC5007 */ {
    function assetId(uint256 tokenId) external view returns (uint256);

    function split(
        uint256 oldTokenId,
        uint256 newToken1Id,
        address newToken1Owner,
        uint256 newToken2Id,
        address newToken2Owner,
        uint64 splitTime
    ) external;

    function merge(
        uint256 firstTokenId,
        uint256 secondTokenId,
        address newTokenOwner,
        uint256 newTokenId
    ) external;
}

assetId

特定NFTのアセットIDを取得する関数。

アセットIDとは、既存のNFTから発行された新しいNFTの識別子のことです。

引数に渡されたtokenIdに対応するNFTのアセットIDを取得します。

split

特定のNFTを新しい2つのトークンに分割する関数。 新しいNFTのアセットIDは古いNFTと同じアセットID(トークンID)と同じになります。

以下の条件があります。

  • oldTokenIdは存在している。
  • newToken1IdとnewToken2Idが存在しないトークンIDである。
  • newToken1OwnerとnewToken2Ownerが0アドレスではない。
  • splitTimeが古いNFTの有効期間内(startTime以上かつendTime未満)である。

merge

2つのNFTを1つのNFTに統合する関数。

以下の条件があります。

  • firstTokenIdとsecondTokenIdが存在している。
  • firstTokenIdのNFTのendTimeが、secondTokenIdのNFTのstartTimeに一致する。
  • newTokenOwnerが0アドレスでない。
  • newTokenIdが存在しないトークンIDである。

補足

uint64の最大値は18,446,744,073,709,551,615です。 これをタイムスタンプとして扱うと、約584,942,419,325年に相当します。 uint256はC、C++、Java、Goなどの主要なプログラミング言語では扱いにくく、uint64は主流のプログラミング言語でネイティブにサポートされています。

後方互換性

この標準は完全にERC721と互換性があります。

テストケース

以下のファイルとコマンドで実行できます。

text
cd ../assets/eip-5007
npm install truffle -g
npm install
truffle test
javascript
const { assert } = require("chai");

const { BigNumber } = require("bignumber.js")

const ERC5007Demo = artifacts.require("ERC5007Demo");
const ERC5007ComposableTest = artifacts.require("ERC5007ComposableTest");

contract("test ERC5007", async accounts => {

    it("test ERC5007", async () => {
        const Alice = accounts[0];

        const instance = await ERC5007Demo.deployed("ERC5007Demo", "ERC5007Demo");
        const demo = instance;

        let now = Math.floor(new Date().getTime()/1000);
        let inputStartTime1 =  new BigNumber(now - 10000);
        let inputEndTime1 = new BigNumber(now + 10000);
        let id1 = 1;
        
        await demo.mint(Alice, id1, inputStartTime1.toFixed(0), inputEndTime1.toFixed(0));

    
        let outputStartTime1 = await demo.startTime(id1);
        let outputEndTime1 = await demo.endTime(id1);
        assert.equal(inputStartTime1.comparedTo(outputStartTime1) == 0  && inputEndTime1.comparedTo(outputEndTime1) == 0, true, "wrong data");


        console.log("IERC5007 InterfaceId:", await demo.getInterfaceId())
        let isSupport = await demo.supportsInterface('0x7a0cdf92');
        assert.equal(isSupport, true , "supportsInterface error");
        
    });

    it("test ERC5007Composable", async () => {
        const Alice = accounts[0];
        const Bob = accounts[1];
        const Carl = accounts[2];

        const instance = await ERC5007ComposableTest.deployed("ERC5007ComposableTest", "ERC5007ComposableTest");
        const demo = instance;

        let now = Math.floor(new Date().getTime()/1000);
        let token1InputStartTime =  new BigNumber(now - 10000);
        let token1InputEndTime = new BigNumber(now + 10000);
        let id1 = 1;
        let assetId = 1000;

        console.log("mint NFT:")
        await demo.mint(Alice, id1, assetId, token1InputStartTime.toFixed(0),
         token1InputEndTime.toFixed(0));

        let token1OutputStartTime = new BigNumber( await demo.startTime(id1));
        let token1OutputEndTime = new BigNumber( await demo.endTime(id1));
        let token1assetId = new BigNumber( await demo.assetId(id1));
        assert.equal(token1InputStartTime.comparedTo(token1OutputStartTime) == 0
        && token1InputEndTime.comparedTo(token1OutputEndTime) == 0 
        && token1assetId.comparedTo(assetId) == 0,
         true, "wrong data");

        let id2 = 2;
        let id3 = 3;
        let splitTime = token1InputStartTime.plus(5000);
        console.log("split NFT:")
        await demo.split(id1, id2, Bob, id3, Carl, splitTime.toFixed(0));

        let token2StartTime = new BigNumber( await demo.startTime(id2));
        let token2EndTime = new BigNumber( await demo.endTime(id2));

        let token3StartTime = new BigNumber( await demo.startTime(id3));
        let token3EndTime = new BigNumber( await demo.endTime(id3));

        assert.equal(token1InputStartTime.comparedTo(token2StartTime) == 0
        && token2EndTime.comparedTo(splitTime) == 0, true, "wrong data");

        assert.equal(token3StartTime.comparedTo(splitTime.plus(1)) == 0
        && token3EndTime.comparedTo(token1InputEndTime) == 0, true, "wrong data");

        let token2assetId = await demo.assetId(id2);
        let token3assetId = await demo.assetId(id3);
        assert.equal(token2assetId == assetId && token3assetId == assetId, true, 'wrong data');
    
        
        console.log("merge NFT:")
        let id4 = 4;
        await demo.setApprovalForAll(Alice, true,{from: Bob});
        await demo.setApprovalForAll(Alice, true,{from: Carl});
        await demo.merge(id2, id3, Alice, id4);

        let token4StartTime = new BigNumber( await demo.startTime(id4));
        let token4EndTime = new BigNumber( await demo.endTime(id4));
        let token4assetId = await demo.assetId(id4);
        let token4Owner = await demo.ownerOf(id4);

        assert.equal(token1InputStartTime.comparedTo(token4StartTime) == 0
        && token4EndTime.comparedTo(token1InputEndTime) == 0, true, "wrong start time or end time");

        assert.equal(token4assetId == assetId, true, 'wrong rootId');
        assert.equal(token4Owner == Alice, true, 'wrong owner');
        

        console.log("IERC5007Composable InterfaceId:", await demo.getInterfaceId())
        let isSupport = await demo.supportsInterface('0x75cf3842');
        assert.equal(isSupport, true , "supportsInterface error");
    });

});

参考実装

javascript
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "./IERC5007.sol";

abstract contract ERC5007 is ERC721, IERC5007 {
    struct TimeNftInfo {
        uint64 startTime;
        uint64 endTime;
    }

    mapping(uint256 => TimeNftInfo) internal _timeNftMapping;

    /**
     * @dev See {IERC5007-startTime}.
     */
    function startTime(uint256 tokenId)
        public
        view
        virtual
        override
        returns (uint64) {
        require(_exists(tokenId), "ERC5007: invalid tokenId");
        return _timeNftMapping[tokenId].startTime;
    }

    /**
     * @dev See {IERC5007-endTime}.
     */
    function endTime(uint256 tokenId)
        public
        view
        virtual
        override
        returns (uint64) {
        require(_exists(tokenId), "ERC5007: invalid tokenId");
        return _timeNftMapping[tokenId].endTime;
    }

    /**
     * @dev mint a new time NFT.
     *
     * Requirements:
     *
     * - `tokenId_` must not exist.
     * - `to_` cannot be the zero address.
     * - `endTime_` should be equal or greater than `startTime_`
     */
    function _mintTimeNft(
        address to_,
        uint256 tokenId_,
        uint64 startTime_,
        uint64 endTime_
    ) internal virtual {
        require(endTime_ >= startTime_, 'ERC5007: invalid endTime');
        _mint(to_, tokenId_);
        TimeNftInfo storage info = _timeNftMapping[tokenId_];
        info.startTime = startTime_;
        info.endTime = endTime_;
    }


    /**
     * @dev Destroys `tokenId`.
     *
     * Requirements:
     *
     * - `tokenId` must exist.
     *
     */
    function _burn(uint256 tokenId) internal virtual override {
        super._burn(tokenId);
        delete _timeNftMapping[tokenId];
    }

    /**
     * @dev See {IERC165-supportsInterface}.
     */
    function supportsInterface(bytes4 interfaceId)
        public
        view
        virtual
        override
        returns (bool) {
        return
            interfaceId == type(IERC5007).interfaceId ||
            super.supportsInterface(interfaceId);
    }
}

最後に

今回は「ERC721拡張機能で、オンチェーンでの時間管理をサポートするための追加機能であるstartTime、endTimeを提案している、ERC5007」についてまとめてきました! いかがだったでしょうか?

実装については今後追記していきます。

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

採用強化中!

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

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