[ERC5007] NFTの有効期間を管理する仕組みを理解しよう！

# はじめに

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

https://cryptogames.co.jp/

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

https://cryptospells.jp/

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

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

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

# 動機
一部の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を取得する関数。

:::note info
アセット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**と互換性があります。

# テストケース

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

```terminal
cd ../assets/eip-5007
npm install truffle -g
npm install
truffle test
```

https://eips.ethereum.org/assets/eip-5007/test/test.js

```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などからお気軽に質問してください！

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

# 採用強化中！

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

https://www.wantedly.com/companies/cryptogames

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