diff --git a/EIPS/eip-5007.md b/EIPS/eip-5007.md new file mode 100755 index 00000000000000..b52325d7a3b859 --- /dev/null +++ b/EIPS/eip-5007.md @@ -0,0 +1,73 @@ +--- +eip: 5007 +title: TimeNFT, ERC-721 Time Extension +description: Add start time and end time to ERC-721 tokens. +author: Anders (@0xanders), Lance (@LanceSnow), Shrug +discussions-to: https://ethereum-magicians.org/t/eip5007-erc721-time-extension/8924 +status: Draft +type: Standards Track +category: ERC +created: 2022-04-13 +requires: 165, 721 +--- + +## Abstract + +This standard is an extension of [ERC-721](./eip-721.md). It proposes some additional functions (`startTime`, `endTime`) to help with on-chain time management. + +## Motivation + +Some NFTs have a defined usage period and cannot be used outside of that period. With traditional NFTs that do not include time information, if you want to mark a token as invalid or enable it at a specific time, you need to actively submit a transaction—a process both cumbersome and expensive. + +Some existing NFTs contain time functions, but their interfaces are not consistent, so it is difficult to develop third-party platforms for them. + +By introducing these functions (`startTime`, `endTime`), it is possible to enable and disable NFT automatically on chain. + +## Specification + +The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in RFC 2119. + +```solidity +interface IERC5007 /* is IERC721 */ { + /// @notice Get the start time of the NFT + /// @dev Throws if `tokenId` is not valid NFT + /// @param tokenId The tokenId of the NFT + /// @return The start time of the NFT + function startTime(uint256 tokenId) external view returns (uint64); + + /// @notice Get the end time of the NFT + /// @dev Throws if `tokenId` is not valid NFT + /// @param tokenId The tokenId of the NFT + /// @return The end time of the NFT + function endTime(uint256 tokenId) external view returns (uint64); + +} +``` + +The `supportsInterface` method MUST return `true` when called with `0x7a0cdf92`. + +## Rationale + +### Time Data Type + +The max value of `uint64` is 18446744073709551615, timestamp 18446744073709551615 is about year 584942419325. `uint256` is too big for some software such as MySQL, Elastic Search, and `uint64` is natively supported on mainstream programming languages. + +## Backwards Compatibility + +As mentioned in the specifications section, this standard can be fully ERC-721 compatible by adding an extension function set. + +## Test Cases + +Test cases are included in [test.js](../assets/eip-5007/test/test.js). + +See [README.md](../assets/eip-5007/README.md) for how to run the test cases. + +## Reference Implementation +See [ERC5007.sol](../assets/eip-5007/contracts/ERC5007.sol). + +## Security Considerations + +No security issues found. + +## Copyright +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/assets/eip-5007/.gitignore b/assets/eip-5007/.gitignore new file mode 100644 index 00000000000000..25c8fdbaba62c3 --- /dev/null +++ b/assets/eip-5007/.gitignore @@ -0,0 +1,2 @@ +node_modules +package-lock.json \ No newline at end of file diff --git a/assets/eip-5007/README.md b/assets/eip-5007/README.md new file mode 100644 index 00000000000000..9bc3c31a7ba96a --- /dev/null +++ b/assets/eip-5007/README.md @@ -0,0 +1,15 @@ +# EIP-5007 +This standard is an extension of [ERC-721](../../EIPS/eip-721.md). It proposes some additional functions (`startTime`, `endTime`) to help with on-chain time management. + +## Tools +* [Truffle](https://truffleframework.com/) - a development framework for Ethereum + +## Install +``` +npm install +``` + +## Test +``` +truffle test +``` diff --git a/assets/eip-5007/contracts/ERC5007.sol b/assets/eip-5007/contracts/ERC5007.sol new file mode 100644 index 00000000000000..9bf99ae7b1af32 --- /dev/null +++ b/assets/eip-5007/contracts/ERC5007.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: CC0 +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import "./IERC5007.sol"; + +contract ERC5007 is ERC721, IERC5007 { + + struct TimeNftInfo { + uint64 startTime; + uint64 endTime; + } + + mapping(uint256 /* tokenId */ => TimeNftInfo) internal _timeNftMapping; + + constructor(string memory name_, string memory symbol_) ERC721(name_, symbol_){ + } + + /// @notice Get the start time of the token + /// @dev Throws if `tokenId` is not valid token + /// @param tokenId The tokenId of the token + /// @return The start time of the token + function startTime(uint256 tokenId) public view virtual override returns (uint64) { + require(_exists(tokenId),"invalid tokenId"); + return _timeNftMapping[tokenId].startTime; + } + + /// @notice Get the end time of the token + /// @dev Throws if `tokenId` is not valid token + /// @param tokenId The tokenId of the token + /// @return The end time of the token + function endTime(uint256 tokenId) public view virtual override returns (uint64) { + require(_exists(tokenId),"invalid tokenId"); + return _timeNftMapping[tokenId].endTime; + } + + + /// @notice mint a new time NFT + /// @param to_ The owner of the new token + /// @param id_ The id of the new token + /// @param startTime_ The start time of the new token + /// @param endTime_ The end time of the new token + function _mintTimeNft(address to_, uint256 id_, uint64 startTime_, uint64 endTime_) internal virtual { + _mint(to_, id_); + + TimeNftInfo storage info = _timeNftMapping[id_]; + info.startTime = startTime_; + info.endTime = endTime_; + } + + + /// @notice burn a time NFT + /// @param tokenId The id of the token + 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); + } +} diff --git a/assets/eip-5007/contracts/ERC5007Demo.sol b/assets/eip-5007/contracts/ERC5007Demo.sol new file mode 100644 index 00000000000000..faa54bc2854f8d --- /dev/null +++ b/assets/eip-5007/contracts/ERC5007Demo.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: CC0 +pragma solidity ^0.8.0; + +import "./ERC5007.sol"; + +contract ERC5007Demo is ERC5007{ + + constructor(string memory name_, string memory symbol_) ERC5007(name_, symbol_){ + } + + /// @notice mint a new original time NFT + /// @param to_ The owner of the new token + /// @param id_ The id of the new token + /// @param startTime_ The start time of the new token + /// @param endTime_ The end time of the new token + function mint(address to_, uint256 id_, uint64 startTime_, uint64 endTime_) public { + _mintTimeNft(to_, id_, startTime_, endTime_); + } + + function getInterfaceId() public pure returns (bytes4) { + return type(IERC5007).interfaceId ; + } +} diff --git a/assets/eip-5007/contracts/IERC5007.sol b/assets/eip-5007/contracts/IERC5007.sol new file mode 100644 index 00000000000000..93192f3c32b73f --- /dev/null +++ b/assets/eip-5007/contracts/IERC5007.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: CC0 + +pragma solidity ^0.8.0; + +interface IERC5007 /* is IERC721 */ { + /// @notice Get the start time of the NFT + /// @dev Throws if `tokenId` is not valid NFT + /// @param tokenId The tokenId of the NFT + /// @return The start time of the NFT + function startTime(uint256 tokenId) external view returns (uint64); + + /// @notice Get the end time of the NFT + /// @dev Throws if `tokenId` is not valid NFT + /// @param tokenId The tokenId of the NFT + /// @return The end time of the NFT + function endTime(uint256 tokenId) external view returns (uint64); +} diff --git a/assets/eip-5007/migrations/1_initial_migration.js b/assets/eip-5007/migrations/1_initial_migration.js new file mode 100644 index 00000000000000..733b919614237f --- /dev/null +++ b/assets/eip-5007/migrations/1_initial_migration.js @@ -0,0 +1,6 @@ +const ERC5007Demo = artifacts.require("ERC5007Demo"); + +module.exports = function (deployer) { + deployer.deploy(ERC5007Demo,'ERC5007Demo','ERC5007Demo'); +}; + diff --git a/assets/eip-5007/package.json b/assets/eip-5007/package.json new file mode 100644 index 00000000000000..20afa0d6ed70ba --- /dev/null +++ b/assets/eip-5007/package.json @@ -0,0 +1,20 @@ +{ + "name": "ERC5007", + "version": "1.0.0", + "description": "", + "main": "truffle-config.js", + "directories": { + "test": "test" + }, + "scripts": {}, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@openzeppelin/contracts": "^4.3.3", + "@types/chai": "^4.3.0", + "@types/mocha": "^9.1.0", + "bignumber.js": "^9.0.1", + "chai": "^4.3.6" + } +} diff --git a/assets/eip-5007/test/test.js b/assets/eip-5007/test/test.js new file mode 100644 index 00000000000000..b6aba5f7dba418 --- /dev/null +++ b/assets/eip-5007/test/test.js @@ -0,0 +1,33 @@ +const { assert } = require("chai"); + +const { BigNumber } = require("bignumber.js") + +const ERC5007Demo = artifacts.require("ERC5007Demo"); + +contract("test ERC5007", async accounts => { + + it("test TimeNFT", 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("InterfaceId:", await demo.getInterfaceId()) + let isSupport = await demo.supportsInterface('0x7a0cdf92'); + assert.equal(isSupport, true , "supportsInterface error"); + + }); +}); \ No newline at end of file diff --git a/assets/eip-5007/truffle-config.js b/assets/eip-5007/truffle-config.js new file mode 100644 index 00000000000000..ccc194a481b5f6 --- /dev/null +++ b/assets/eip-5007/truffle-config.js @@ -0,0 +1,117 @@ +/** + * Use this file to configure your truffle project. It's seeded with some + * common settings for different networks and features like migrations, + * compilation and testing. Uncomment the ones you need or modify + * them to suit your project as necessary. + * + * More information about configuration can be found at: + * + * trufflesuite.com/docs/advanced/configuration + * + * To deploy via Infura you'll need a wallet provider (like @truffle/hdwallet-provider) + * to sign your transactions before they're sent to a remote public node. Infura accounts + * are available for free at: infura.io/register. + * + * You'll also need a mnemonic - the twelve word phrase the wallet uses to generate + * public/private key pairs. If you're publishing your code to GitHub make sure you load this + * phrase from a file you've .gitignored so it doesn't accidentally become public. + * + */ + +// const HDWalletProvider = require('@truffle/hdwallet-provider'); +// +// const fs = require('fs'); +// const mnemonic = fs.readFileSync(".secret").toString().trim(); + +module.exports = { + /** + * Networks define how you connect to your ethereum client and let you set the + * defaults web3 uses to send transactions. If you don't specify one truffle + * will spin up a development blockchain for you on port 9545 when you + * run `develop` or `test`. You can ask a truffle command to use a specific + * network from the command line, e.g + * + * $ truffle test --network + */ + + networks: { + // Useful for testing. The `development` name is special - truffle uses it by default + // if it's defined here and no other network is specified at the command line. + // You should run a client (like ganache-cli, geth or parity) in a separate terminal + // tab if you use this network and you must also set the `host`, `port` and `network_id` + // options below to some value. + // + // development: { + // host: "127.0.0.1", // Localhost (default: none) + // port: 8545, // Standard Ethereum port (default: none) + // network_id: "*", // Any network (default: none) + // }, + // Another network with more advanced options... + // advanced: { + // port: 8777, // Custom port + // network_id: 1342, // Custom network + // gas: 8500000, // Gas sent with each transaction (default: ~6700000) + // gasPrice: 20000000000, // 20 gwei (in wei) (default: 100 gwei) + // from:
, // Account to send txs from (default: accounts[0]) + // websocket: true // Enable EventEmitter interface for web3 (default: false) + // }, + // Useful for deploying to a public network. + // NB: It's important to wrap the provider as a function. + // ropsten: { + // provider: () => new HDWalletProvider(mnemonic, `https://ropsten.infura.io/v3/YOUR-PROJECT-ID`), + // network_id: 3, // Ropsten's id + // gas: 5500000, // Ropsten has a lower block limit than mainnet + // confirmations: 2, // # of confs to wait between deployments. (default: 0) + // timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50) + // skipDryRun: true // Skip dry run before migrations? (default: false for public nets ) + // }, + // Useful for private networks + // private: { + // provider: () => new HDWalletProvider(mnemonic, `https://network.io`), + // network_id: 2111, // This network is yours, in the cloud. + // production: true // Treats this network as if it was a public net. (default: false) + // } + }, + + // Set default mocha options here, use special reporters etc. + mocha: { + // timeout: 100000 + }, + + // Configure your compilers + compilers: { + solc: { + version: "0.8.10", // Fetch exact version from solc-bin (default: truffle's version) + // docker: true, // Use "0.5.1" you've installed locally with docker (default: false) + settings: { // See the solidity docs for advice about optimization and evmVersion + optimizer: { + enabled: false, + runs: 200 + } + // , + // evmVersion: "byzantium" + // } + } + }, + + // Truffle DB is currently disabled by default; to enable it, change enabled: + // false to enabled: true. The default storage location can also be + // overridden by specifying the adapter settings, as shown in the commented code below. + // + // NOTE: It is not possible to migrate your contracts to truffle DB and you should + // make a backup of your artifacts to a safe location before enabling this feature. + // + // After you backed up your artifacts you can utilize db by running migrate as follows: + // $ truffle migrate --reset --compile-all + // + // db: { + // enabled: false, + // host: "127.0.0.1", + // adapter: { + // name: "sqlite", + // settings: { + // directory: ".db" + // } + // } + } +};