diff --git a/EIPS/eip-5327.md b/EIPS/eip-5327.md new file mode 100644 index 00000000000000..631759a352c21b --- /dev/null +++ b/EIPS/eip-5327.md @@ -0,0 +1,270 @@ +--- +eip: 5327 +title: Rental NFT, ERC-721 User And Expires And Level Extension +description: Add a time-limited role with restricted permissions to ERC-721 tokens. +author: Yan (@yan253319066) +discussions-to: https://ethereum-magicians.org/t/erc-721-user-and-expires-and-level-extension/10097 +status: Draft +type: Standards Track +category: ERC +created: 2022-07-25 +requires: 165, 721 +--- + +## Abstract + +This standard is an extension of [EIP-721](./eip-721.md). It proposes an additional role (`user`) which can be granted to addresses, and a time where the role is automatically revoked (`expires`) and (`level`) . The `user` role represents permission to "use" the NFT, but not the ability to transfer it or set users. + +## Motivation + +Some NFTs have certain utilities. For example, virtual land can be "used" to build scenes, and NFTs representing game assets can be "used" in-game. In some cases, the owner and user may not always be the same. There may be an owner of the NFT that rents it out to a “user”. The actions that a “user” should be able to take with an NFT would be different from the “owner” (for instance, “users” usually shouldn’t be able to sell ownership of the NFT).  In these situations, it makes sense to have separate roles that identify whether an address represents an “owner” or a “user” and manage permissions to perform actions accordingly. + +Some projects already use this design scheme under different names such as “operator” or “controller” but as it becomes more and more prevalent, we need a unified standard to facilitate collaboration amongst all applications. + +Furthermore, applications of this model (such as renting) often demand that user addresses have only temporary access to using the NFT. Normally, this means the owner needs to submit two on-chain transactions, one to list a new address as the new user role at the start of the duration and one to reclaim the user role at the end. This is inefficient in both labor and gas and so an “expires” and “level” function is introduced that would facilitate the automatic end of a usage term without the need of a second transaction. + +## 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. + +### Contract Interface +Solidity Interface with NatSpec & OpenZeppelin v4 Interfaces (also available at [IERC5327.sol](../assets/eip-5327/contracts/IERC5327.sol)): + +```solidity +interface IERC5327 { + + // Logged when the user of a NFT is changed or expires and level is changed + /// @notice Emitted when the `user` of an NFT or the `expires` of the `user` is changed or the user `level` is changed + /// The zero address for user indicates that there is no user address + event UpdateUser(uint256 indexed tokenId, address indexed user, uint64 expires, uint8 level); + + /// @notice set the user and expires and level of a NFT + /// @dev The zero address indicates there is no user + /// Throws if `tokenId` is not valid NFT + /// @param user The new user of the NFT + /// @param expires UNIX timestamp, The new user could use the NFT before expires + /// @param level user level + function setUser(uint256 tokenId, address user, uint64 expires, uint8 level) external; + + /// @notice Get the user address of an NFT + /// @dev The zero address indicates that there is no user or the user is expired + /// @param tokenId The NFT to get the user address for + /// @return The user address for this NFT + function userOf(uint256 tokenId) external view returns(address); + + /// @notice Get the user expires of an NFT + /// @dev The zero value indicates that there is no user + /// @param tokenId The NFT to get the user expires for + /// @return The user expires for this NFT + function userExpires(uint256 tokenId) external view returns(uint256); + + /// @notice Get the user level of an NFT + /// @dev The zero value indicates that there is no user + /// @param tokenId The NFT to get the user level for + /// @return The user level for this NFT + function userLevel(uint256 tokenId) external view returns(uint256); +} +``` + +The `userOf(uint256 tokenId)` function MAY be implemented as `pure` or `view`. + +The `userExpires(uint256 tokenId)` function MAY be implemented as `pure` or `view`. + +The `userLevel(uint256 tokenId)` function MAY be implemented as `pure` or `view`. + +The `setUser(uint256 tokenId, address user, uint64 expires)` function MAY be implemented as `public` or `external`. + +The `UpdateUser` event MUST be emitted when a user address is changed or the user expires is changed or the user level is changed. + +The `supportsInterface` method MUST return `true` when called with `0xad092b5c`. + +## Rationale + +This model is intended to facilitate easy implementation. Here are some of the problems that are solved by this standard: + +### Clear Rights Assignment + +With Dual “owner” and “user” roles, it becomes significantly easier to manage what lenders and borrowers can and cannot do with the NFT (in other words, their rights). Additionally, owners can control who the user is and it’s easy for other projects to assign their own rights to either the owners or the users. + +### Simple On-chain Time Management + +Once a rental period is over, the user role needs to be reset and the “user” has to lose access to the right to use the NFT. This is usually accomplished with a second on-chain transaction but that is gas inefficient and can lead to complications because it’s imprecise. With the `expires` function, there is no need for another transaction because the “user” is invalidated automatically after the duration is over. + +### Easy Third-Party Integration + +In the spirit of permission less interoperability, this standard makes it easier for third-party protocols to manage NFT usage rights without permission from the NFT issuer or the NFT application. Once a project has adopted the additional `user` role and `expires` and `level`, any other project can directly interact with these features and implement their own type of transaction. For example, a PFP NFT using this standard can be integrated into both a rental platform where users can rent the NFT for 30 days AND, at the same time, a mortgage platform where users can use the NFT while eventually buying ownership of the NFT with installment payments. This would all be done without needing the permission of the original PFP project. + +## Backwards Compatibility + +As mentioned in the specifications section, this standard can be fully ERC-721 compatible by adding an extension function set. + +In addition, new functions introduced in this standard have many similarities with the existing functions in ERC-721. This allows developers to easily adopt the standard quickly. + +## Test Cases + +### Test Contract +ERC5327Demo Implementation: [ERC5327Demo.sol](../assets/eip-5327/contracts/ERC5327Demo.sol) + +```solidity +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.0; + +import "./ERC5327.sol"; + +contract ERC5327Demo is ERC5327 { + + constructor(string memory name, string memory symbol) + ERC5327(name,symbol) + { + } + + function mint(uint256 tokenId, address to) public { + _mint(to, tokenId); + } + +} +``` + +### Test Code +[test.js](../assets/eip-5327/test/test.js) + +```JavaScript +const { assert } = require("chai"); + +const ERC5327Demo = artifacts.require("ERC5327Demo"); + +contract("test", async accounts => { + + it("should set user to Bob", async () => { + // Get initial balances of first and second account. + const Alice = accounts[0]; + const Bob = accounts[1]; + + const instance = await ERC5327Demo.deployed("T", "T"); + const demo = instance; + + await demo.mint(1, Alice); + let expires = Math.floor(new Date().getTime()/1000) + 1000; + let level = 1; + await demo.setUser(1, Bob, BigInt(expires), level); + + let user_1 = await demo.userOf(1); + + assert.equal( + user_1, + Bob, + "User of NFT 1 should be Bob" + ); + + let owner_1 = await demo.ownerOf(1); + assert.equal( + owner_1, + Alice , + "Owner of NFT 1 should be Alice" + ); + }); +}); + + +``` + +run in Terminal: +``` +truffle test ./test/test.js +``` + +## Reference Implementation +ERC5327 Implementation: [ERC5327.sol](../assets/eip-5327/contracts/ERC5327.sol) +```solidity +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import "./IERC5327.sol"; + +contract ERC5327 is ERC721, IERC5327 { + struct UserInfo + { + address user; // address of user role + uint64 expires; // unix timestamp, user expires + uint8 level; // user level + } + + mapping (uint256 => UserInfo) internal _users; + + constructor(string memory name_, string memory symbol_) + ERC721(name_,symbol_) + { + } + + /// @notice set the user and expires and level of a NFT + /// @dev The zero address indicates there is no user + /// Throws if `tokenId` is not valid NFT + /// @param user The new user of the NFT + /// @param expires UNIX timestamp, The new user could use the NFT before expires + /// @param level user level + function setUser(uint256 tokenId, address user, uint64 expires, uint8 level) public virtual{ + require(_isApprovedOrOwner(msg.sender, tokenId),"ERC721: transfer caller is not owner nor approved"); + UserInfo storage info = _users[tokenId]; + info.user = user; + info.expires = expires; + info.level = level + emit UpdateUser(tokenId,user,expires,level); + } + + /// @notice Get the user address of an NFT + /// @dev The zero address indicates that there is no user or the user is expired + /// @param tokenId The NFT to get the user address for + /// @return The user address for this NFT + function userOf(uint256 tokenId)public view virtual returns(address){ + if( uint256(_users[tokenId].expires) >= block.timestamp){ + return _users[tokenId].user; + } + else{ + return address(0); + } + } + + /// @notice Get the user expires of an NFT + /// @dev The zero value indicates that there is no user + /// @param tokenId The NFT to get the user expires for + /// @return The user expires for this NFT + function userExpires(uint256 tokenId) public view virtual returns(uint256){ + return _users[tokenId].expires; + } + + /// @notice Get the user level of an NFT + /// @dev The zero value indicates that there is no user + /// @param tokenId The NFT to get the user level for + /// @return The user level for this NFT + function userLevel(uint256 tokenId) public view virtual returns(uint256){ + return _users[tokenId].level; + } + + /// @dev See {IERC165-supportsInterface}. + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + return interfaceId == type(IERC5327).interfaceId || super.supportsInterface(interfaceId); + } + + function _beforeTokenTransfer( + address from, + address to, + uint256 tokenId + ) internal virtual override{ + super._beforeTokenTransfer(from, to, tokenId); + + if (from != to && _users[tokenId].user != address(0)) { + delete _users[tokenId]; + emit UpdateUser(tokenId, address(0), 0, 0); + } + } +} +``` + +## Security Considerations + +This EIP standard can completely protect the rights of the owner, the owner can change the NFT user and expires and level at any time. + +## Copyright +Copyright and related rights waived via [CC0](../LICENSE.md). + diff --git a/assets/eip-5327/.gitignore b/assets/eip-5327/.gitignore new file mode 100644 index 00000000000000..504afef81fbadc --- /dev/null +++ b/assets/eip-5327/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +package-lock.json diff --git a/assets/eip-5327/README.md b/assets/eip-5327/README.md new file mode 100644 index 00000000000000..6e54131c5983da --- /dev/null +++ b/assets/eip-5327/README.md @@ -0,0 +1,20 @@ +# EIP-5327 +EIP-5327 is an extension of ERC-721. It proposes an additional role **user** and a valid duration indicator **expires** adn **level**. It allows users and developers manage the use right more simple and efficient. + +### Tools +* [Visual Studio Code](https://code.visualstudio.com/) +* [Solidity](https://marketplace.visualstudio.com/items?itemName=JuanBlanco.solidity) - Solidity support for Visual Studio code +* [Truffle](https://truffleframework.com/) - the most popular development framework for Ethereum + +### Install +``` +npm install +``` + +### Test +``` +truffle test +``` + +### Additional Resources +* [Official Truffle Documentation](http://truffleframework.com/docs/) for complete and detailed guides, tips, and sample code. \ No newline at end of file diff --git a/assets/eip-5327/contracts/ERC5327.sol b/assets/eip-5327/contracts/ERC5327.sol new file mode 100644 index 00000000000000..0635960de241e0 --- /dev/null +++ b/assets/eip-5327/contracts/ERC5327.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import "./IERC5327.sol"; + +contract ERC5327 is ERC721, IERC5327 { + struct UserInfo + { + address user; // address of user role + uint64 expires; // unix timestamp, user expires + uint8 level; // user level + } + + mapping (uint256 => UserInfo) internal _users; + + constructor(string memory name_, string memory symbol_) + ERC721(name_,symbol_) + { + } + + /// @notice set the user and expires and level of a NFT + /// @dev The zero address indicates there is no user + /// Throws if `tokenId` is not valid NFT + /// @param user The new user of the NFT + /// @param expires UNIX timestamp, The new user could use the NFT before expires + /// @param level user level + function setUser(uint256 tokenId, address user, uint64 expires, uint8 level) public virtual{ + require(_isApprovedOrOwner(msg.sender, tokenId),"ERC721: transfer caller is not owner nor approved"); + UserInfo storage info = _users[tokenId]; + info.user = user; + info.expires = expires; + info.level = level + emit UpdateUser(tokenId,user,expires,level); + } + + /// @notice Get the user address of an NFT + /// @dev The zero address indicates that there is no user or the user is expired + /// @param tokenId The NFT to get the user address for + /// @return The user address for this NFT + function userOf(uint256 tokenId)public view virtual returns(address){ + if( uint256(_users[tokenId].expires) >= block.timestamp){ + return _users[tokenId].user; + } + else{ + return address(0); + } + } + + /// @notice Get the user expires of an NFT + /// @dev The zero value indicates that there is no user + /// @param tokenId The NFT to get the user expires for + /// @return The user expires for this NFT + function userExpires(uint256 tokenId) public view virtual returns(uint256){ + return _users[tokenId].expires; + } + + /// @notice Get the user level of an NFT + /// @dev The zero value indicates that there is no user + /// @param tokenId The NFT to get the user level for + /// @return The user level for this NFT + function userLevel(uint256 tokenId) public view virtual returns(uint256){ + return _users[tokenId].level; + } + + /// @dev See {IERC165-supportsInterface}. + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + return interfaceId == type(IERC5327).interfaceId || super.supportsInterface(interfaceId); + } + + function _beforeTokenTransfer( + address from, + address to, + uint256 tokenId + ) internal virtual override{ + super._beforeTokenTransfer(from, to, tokenId); + + if (from != to && _users[tokenId].user != address(0)) { + delete _users[tokenId]; + emit UpdateUser(tokenId, address(0), 0, 0); + } + } +} + diff --git a/assets/eip-5327/contracts/ERC5327Demo.sol b/assets/eip-5327/contracts/ERC5327Demo.sol new file mode 100644 index 00000000000000..9f36a0450791df --- /dev/null +++ b/assets/eip-5327/contracts/ERC5327Demo.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.0; + +import "./ERC5327.sol"; + +contract ERC5327Demo is ERC5327 { + + constructor(string memory name_, string memory symbol_) + ERC4907(name_,symbol_) + { + } + + function mint(uint256 tokenId, address to) public { + _mint(to, tokenId); + } + +} + diff --git a/assets/eip-5327/contracts/IERC5327.sol b/assets/eip-5327/contracts/IERC5327.sol new file mode 100644 index 00000000000000..b81ee831b3c7ef --- /dev/null +++ b/assets/eip-5327/contracts/IERC5327.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity ^0.8.0; + +interface IERC5327 { + // Logged when the user of a token assigns a new user or updates expires + /// @notice Emitted when the `user` of an NFT or the `expires` of the `user` is changed or the `level` of the `user` is changed + /// The zero address for user indicates that there is no user address + event UpdateUser(uint256 indexed tokenId, address indexed user, uint64 expires, uint8 level); + + /// @notice set the user and expires of a NFT + /// @dev The zero address indicates there is no user + /// Throws if `tokenId` is not valid NFT + /// @param user The new user of the NFT + /// @param expires UNIX timestamp, The new user could use the NFT before expires + /// @param level user level + function setUser(uint256 tokenId, address user, uint64 expires, uint8 level) external ; + + /// @notice Get the user address of an NFT + /// @dev The zero address indicates that there is no user or the user is expired + /// @param tokenId The NFT to get the user address for + /// @return The user address for this NFT + function userOf(uint256 tokenId) external view returns(address); + + /// @notice Get the user expires of an NFT + /// @dev The zero value indicates that there is no user + /// @param tokenId The NFT to get the user expires for + /// @return The user expires for this NFT + function userExpires(uint256 tokenId) external view returns(uint256); + + /// @notice Get the user level of an NFT + /// @dev The zero value indicates that there is no user + /// @param tokenId The NFT to get the user level for + /// @return The user level for this NFT + function userLevel(uint256 tokenId) external view returns(uint256); +} diff --git a/assets/eip-5327/migrations/1_initial_migration.js b/assets/eip-5327/migrations/1_initial_migration.js new file mode 100644 index 00000000000000..5f32b8e587bf98 --- /dev/null +++ b/assets/eip-5327/migrations/1_initial_migration.js @@ -0,0 +1,6 @@ +const ERC5327Demo = artifacts.require("ERC5327Demo"); + +module.exports = function (deployer) { + deployer.deploy(ERC5327Demo,'ERC5327Demo','ERC5327Demo'); +}; + diff --git a/assets/eip-5327/package.json b/assets/eip-5327/package.json new file mode 100644 index 00000000000000..828b6804810dee --- /dev/null +++ b/assets/eip-5327/package.json @@ -0,0 +1,19 @@ +{ + "name": "ERC-5327", + "version": "1.0.0", + "description": "", + "main": "truffle-config.js", + "directories": { + "test": "test" + }, + "scripts": {}, + "keywords": [], + "author": "", + "license": "CC0-1.0", + "dependencies": { + "@openzeppelin/contracts": "^4.3.3", + "@types/chai": "^4.3.0", + "@types/mocha": "^9.1.0", + "chai": "^4.3.6" + } +} diff --git a/assets/eip-5327/test/test.js b/assets/eip-5327/test/test.js new file mode 100644 index 00000000000000..8059f2f63d62f0 --- /dev/null +++ b/assets/eip-5327/test/test.js @@ -0,0 +1,37 @@ +const { assert } = require("chai"); + +const ERC5327Demo = artifacts.require("ERC5327Demo"); + +contract("test", async accounts => { + + it("should set user to Bob", async () => { + // Get initial balances of first and second account. + const Alice = accounts[0]; + const Bob = accounts[1]; + + const instance = await ERC5327Demo.deployed("T", "T"); + const demo = instance; + + await demo.mint(1, Alice); + let expires = Math.floor(new Date().getTime()/1000) + 1000; + let level = 1; + await demo.setUser(1, Bob, BigInt(expires), level); + + let user_1 = await demo.userOf(1); + + assert.equal( + user_1, + Bob, + "User of NFT 1 should be Bob" + ); + + let owner_1 = await demo.ownerOf(1); + assert.equal( + owner_1, + Alice , + "Owner of NFT 1 should be Alice" + ); + }); +}); + + diff --git a/assets/eip-5327/truffle-config.js b/assets/eip-5327/truffle-config.js new file mode 100644 index 00000000000000..ccc194a481b5f6 --- /dev/null +++ b/assets/eip-5327/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" + // } + // } + } +};