はじめに
初めまして。 CryptoGamesというブロックチェーンゲーム企業でエンジニアをしている cardene(かるでね) です! スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。
代表的なゲームはクリプトスペルズというブロックチェーンゲームです。
今回は、ERC721拡張機能で、オンチェーンでの時間管理をサポートするための追加機能であるstartTime、endTimeを提案している、ERC5007の説明をしていきます!
以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。
動機
一部のNFTは使用期間が決められており、その期間外では使用できません。 時間情報が定義されていない従来のERC721規格のNFTでは、NFTを時間情報によって無効/有効するには、処理が煩雑でガス代が余分にかかってしまいます。
一部の従来のERC721規格のNFTには時間機能が含まれていますが、インターフェースが統一されていないため、サードパーティのプラットフォームで使用するのが難しかったです。
startTime、endTimeを導入することで、NFTを自動的に有効または無効にすることが可能になります。
仕様
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タイムスタンプとして取得します。
オプション仕様
以下のインターフェースはオプションの実装となります。
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と互換性があります。
テストケース
以下のファイルとコマンドで実行できます。
cd ../assets/eip-5007
npm install truffle -g
npm install
truffle testconst { 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");
});
});参考実装
// 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に興味がある」、「スマートコントラクトの開発に携わりたい」など、少しでも興味を持っている方はまずはお話ししましょう!
