From a47a153e56d43ce4fbc191ee6fcca49131a7fe1a Mon Sep 17 00:00:00 2001 From: "piyush.chittara" Date: Thu, 25 May 2023 10:28:46 +0530 Subject: [PATCH 01/44] erc721lockable init --- EIPS/eip-draft_ERC721Lockable.md | 105 ++++++ assets/eip-ERC721Lockable/ERC721Lockable.sol | 206 ++++++++++++ assets/eip-ERC721Lockable/IERC721Lockable.sol | 78 +++++ assets/eip-ERC721Lockable/test/test.js | 316 ++++++++++++++++++ 4 files changed, 705 insertions(+) create mode 100644 EIPS/eip-draft_ERC721Lockable.md create mode 100644 assets/eip-ERC721Lockable/ERC721Lockable.sol create mode 100644 assets/eip-ERC721Lockable/IERC721Lockable.sol create mode 100644 assets/eip-ERC721Lockable/test/test.js diff --git a/EIPS/eip-draft_ERC721Lockable.md b/EIPS/eip-draft_ERC721Lockable.md new file mode 100644 index 0000000000000..64576a24b3ef9 --- /dev/null +++ b/EIPS/eip-draft_ERC721Lockable.md @@ -0,0 +1,105 @@ +--- +title: ERC721 Lockable +description: Interface for enabling locking of ERC721 using locker and approver +author: Piyush Chittara (@streamnft-tech) +discussions-to: +status: Draft +category: ERC +created: 2023-05-25 +requires: EIP-165,EIP-721 +--- + +## Abstract +An extension of EIP-721, this standard incorporates `locking` features into NFTs, allowing for various uses while preventing sale or transfer. The token's owner or operator has the ability to lock it, specifying an unlocker address (either an EOA or a contract) that exclusively holds the power to unlock the token. Owner can also provide approval for token-id, enabling ability to lock asset while address holds the token approval. Upon token transfer these rights get purged. + + +## Motivation +EIP-721 has sparked an unprecedented surge in demand for NFTs. However, despite this tremendous success, NFT economy suffers from secondary liquidity where it remains Illiquid in owner’s wallet. There are projects such as NFTfi, Paraspace which aims to address the liquidity challenge, but they entail below mentioned inconveniences and risks for owners as they necessitate transferring the participating NFTs to the projects' contracts. + + +* Loss of utility: The utility value of NFTs diminishes when they are transferred to an escrow account, no longer remaining under the direct custody of the owners. +* Lack of composability: The market could benefit from increased liquidity if NFT owners had access to multiple financial tools, such as leveraging loans and renting out their assets for maximum returns. Composability serves as the missing piece in creating a more efficient market. +* Smart contract vulnerabilities: NFTs are susceptible to loss or theft due to potential bugs or vulnerabilities present in the smart contracts they rely on. + + +The aforementioned issues contribute to a poor user experience (UX), and we propose enhancing the EIP-721 standard by implementing a native locking mechanism: +Rather than being transferred to a smart contract, an NFT remains securely stored in self-custody but is locked. +During the lock period, the NFT's transfer is restricted while its other properties remain unchanged. +NFT Owner retains the ability to use or distribute it’s utility + + +NFTs have numerous use cases where it is crucial for the NFT to remain within the owner's wallet, even when it serves as collateral for a loan. Whether it's authorizing access to a Discord server, or utilizing NFT within a play-to-earn (P2E) game, owner should have the freedom to do so throughout the lending period. Just as real estate owner can continue living in their mortgaged house, take personal loan or keep tenants to generate passive income, these functionalities should be available to NFT owners to bring more investors in NFT economy. + + +Lockable NFTs enable the following use cases : +* NFT-collateralized loans: Utilize NFT as collateral for a loan without locking it on the lending protocol contract. Instead, lock it within owner’s wallet while still enjoying all the utility of NFT. +* No collateral rentals of NFTs: Borrow an NFT for a fee without the need for significant collateral. Renter can use the NFT but not transfer it, ensuring the lender's safety. The borrowing service contract automatically returns the NFT to the lender once the borrowing period expires. +* Buy Now Pay Later: The buyer receives the locked NFT and can immediately begin using it. However, they are unable to sell the NFT until all installments are paid. Failure to complete the full payment results in the NFT returning to the seller, along with a fee. +* Composability: Maximize liquidity by having access to multiple financial tools. Imagine taking a loan against NFT and putting it on rentals to generate passive income. +* Primary sales: Mint an NFT for a partial payment and settle the remaining amount once owner is satisfied with the collection's progress. +* Soulbound: Organization can mint and self assign `locker`, send token to user and lock the asset. +* Safety: Safely and conveniently use exclusive blue chip NFTs. Lockable extension allows owner to lock NFT and designate secure cold wallet as the unlocker. This way, owner can keep NFT on MetaMask and easily use it, even if a hacker gains access to MetaMask account. Without access to the cold wallet, the hacker cannot transfer NFT, ensuring its safety. + +By extending the EIP-721 standard, the proposed standard enables secure and convenient management of underlying NFT assets. It natively supports prevalent NFTFi use cases such as, staking, lending, and renting. We anticipate that this proposed standard will foster increased engagement of NFT owners in NFTFi projects, thereby enhancing the overall vitality of the NFT ecosystem. + + +## Specification + +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174. + +EIP-721 compliant contracts MAY implement this EIP to provide standard methods of locking and unlocking the token at its current owner address. If the token is `locked`, the `getLocked` function MUST return an address that is able to `unlock` the token. For tokens that are not `locked`, the getLocked function MUST return `address(0)`. + +Token owner MAY set `locker` to any address. Token MAY be locked by `locker` which MUST change the state from `UNLOCKED` to `LOCKED`. `lock` transaction MUST revert if token is not `UNLOCKED`. `unlock` transaction MUST revert if token is not `LOCKED`. `unlock` transaction MUST change state back to `UNLOCKED`. + +Token owner MAY set approval to any address. Token MAY be `locked` by `approver` which MUST change the state from `UNLOCKED` to `LOCKED_APPROVED`. `lockApproved` transaction MUST revert if token is not `UNLOCKED`. `unlockApproved` transaction MUST revert if token is not `LOCKED_APPROVED`. `unlockApproved` MUST change state back to `UNLOCKED`. + +`approve` transaction MUST revert if token is not `UNLOCKED`. tansfer transaction MUST revert if token state is `LOCKED`. transfer transaction MUST pass if state is `LOCKED_APPROVED` and caller is `approved`, else MUST revert otherwise. + +## Rationale + +This approach presents a minimalistic solution that focuses on locking items and specifying who has the authority to unlock them. It offers flexibility and extensibility, accommodating various potential use cases mentioned in the Motivation section. + +Moreover, when there is a requirement to grant temporary or redeemable rights for a NFT, such as rentals or purchases with installments, this EIP involves the actual transfer of the token to the temporary user's wallet, rather than simply assigning a role. This design choice ensures compatibility with existing NFT ecosystem tools and dApps, without necessitating additional interfaces or logic implementation. + +This functionality already exists on Solana, enabling ease for NFT liquidity and use cases. This EIP shall introduce same functionality to EVM ecosystem. The naming and reference implementation of functions and storage entities resemble the Approval flow outlined in [EIP-721], ensuring an intuitive user experience. + +Existing [ERC721Upgradedable] can upgrade to ERC721 Locakable to unlock locking capability inherently and unlock underlying liquidity features. + + +## Backwards Compatibility + +This standard is compatible with current EIP-721 standards. + +## Test Cases + +Test cases can be found [here](../assets/eip-ERC721Lockable/test/test.js). + +## Reference Implementation + +Reference Interface can be found [here](../assets/eip-ERC721Lockable/IERC721Lockable.sol). + +Reference Implementation can be found [here](../assets/eip-ERC721Lockable/ERC721Lockable.sol). + +## Security Considerations + +There are no security considerations related directly to the implementation of this standard for the contract that manages EIP-721 tokens. + +### Considerations for the contracts that work with lockable tokens +* Make sure that every contract that is stated as `locker` can actually unlock the `LOCKED` token only. +* Make sure that the approved contract can unlock the `LOCKED_APPROVED` token only. +* `LOCKED` token with in-accesible account or un-verified contract address can lead to permanent lock of the token. +* There are use cases, that involve transferring the token to a temporary owner and then lock it. For example, NFT rentals. Smart contracts that manage such services should always use `transferFrom` instead of `safeTransferFrom` to avoid re-entrancies. +* There are no MEV considerations regarding lockable tokens as only authorized parties are allowed to lock and unlock. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE). + +## Citation +Please cite this document as: <> + + +## Other Lockable Implementation +* [EIP-5753] Filipp Makarov (@filmakarov), "ERC-5753: Lockable Extension for EIP-721 [DRAFT]," Ethereum Improvement Proposals, no. 5753, October 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-5753. + +* [EIP-5058] Tyler (@radiocaca), Alex (@gojazdev), John (@sfumato00), "ERC-5058: Lockable Non-Fungible Tokens [DRAFT]," Ethereum Improvement Proposals, no. 5058, April 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-5058. \ No newline at end of file diff --git a/assets/eip-ERC721Lockable/ERC721Lockable.sol b/assets/eip-ERC721Lockable/ERC721Lockable.sol new file mode 100644 index 0000000000000..74c5e31cab774 --- /dev/null +++ b/assets/eip-ERC721Lockable/ERC721Lockable.sol @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity >=0.7.0 <0.9.0; + +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import "./IERC721Lockable.sol"; + +/// @title Lockable Extension for ERC721 + +abstract contract ERC721Lockable is ERC721,IERC721Lockable{ + + + /*/////////////////////////////////////////////////////////////// + LOCKABLE EXTENSION STORAGE + //////////////////////////////////////////////////////////////*/ + + //Mapping from token id to user address for locking permission + mapping(uint256 => address) internal locker; + //Mapping from token id to state of token + mapping(uint256 => State) internal state; + //Possible states of a token + enum State{UNLOCKED,LOCKED,LOCKED_APPROVED} + + /*/////////////////////////////////////////////////////////////// + LOCKABLE LOGIC + //////////////////////////////////////////////////////////////*/ + + /** + * @dev Public function to set locker. Verifies if the msg.sender is the owner + * and allows setting locker for tokenid + */ + function setLocker(uint256 id, address _locker) external virtual override { + _setLocker(id,_locker); + } + + /** + * @dev Private function to set locker. Verifies if the msg.sender is the owner + * and allows setting locker for tokenid + */ + function _setLocker(uint256 id, address _locker) private { + require(msg.sender==ownerOf(id), "ERC721Lockable : Owner Required"); + require(state[id]==State.UNLOCKED, "ERC721Lockable : Locked"); + locker[id]=_locker; + emit SetLocker(id, _locker); + } + + /** + * @dev Public function to remove locker. Verifies if the msg.sender is the owner + * and allows removal of locker for tokenid if token is unlocked + */ + function removeLocker(uint256 id) external virtual override { + _removeLocker(id); + } + + /** + * @dev Private function to remove locker. Verifies if the msg.sender is the owner + * and allows removal of locker for tokenid if token is unlocked + */ + function _removeLocker(uint256 id) private { + require(msg.sender==ownerOf(id), "ERC721Lockable : Owner Required"); + require(state[id]==State.UNLOCKED, "ERC721Lockable : Locked"); + delete locker[id]; + emit RemoveLocker(id); + } + + /** + * @dev Returns the locker for the tokenId + * address(0) means token is not locked + * reverts if token does not exist + */ + function lockerOf(uint256 id) external virtual view override returns(address){ + require(_exists(id), "ERC721Lockable: Nonexistent token"); + return locker[id]; + } + + /** + * @dev Public function to lock the token. Verifies if the msg.sender is locker + */ + function lock(uint256 id) external virtual override{ + _lock(id); + } + + /** + * @dev Private function to lock the token. Verifies if the msg.sender is locker + */ + function _lock(uint256 id) private { + require(msg.sender==locker[id], "ERC721Lockable : Locker Required"); + require(state[id]==State.UNLOCKED, "ERC721Lockable : Locked"); + state[id]=State.LOCKED; + emit Lock(id); + } + + /** + * @dev Public function to unlock the token. Verifies if the msg.sender is locker + */ + function unlock(uint256 id) external virtual override{ + _unlock(id); + } + + /** + * @dev Private function to unlock the token. Verifies if the msg.sender is locker + */ + function _unlock(uint256 id) private { + require(msg.sender==locker[id], "ERC721Lockable : Locker Required"); + require(state[id]!=State.LOCKED_APPROVED, "ERC721Lockable : Locked by approved"); + require(state[id]!=State.UNLOCKED, "ERC721Lockable : Unlocked"); + state[id]=State.UNLOCKED; + emit Unlock(id); + } + + /** + * @dev Public function to lock the token. Verifies if the msg.sender is approved + */ + function lockApproved(uint256 id) external virtual override{ + _lockApproved(id); + } + + /** + * @dev Private function to lock the token. Verifies if the msg.sender is approved + */ + function _lockApproved(uint256 id) internal { + require(isApprovedForAll(ownerOf(id), msg.sender) || getApproved(id) == msg.sender, "ERC721Lockable : Required approval"); + require(state[id]==State.UNLOCKED, "ERC721Lockable : Locked"); + state[id]=State.LOCKED_APPROVED; + emit LockApproved(id); + } + + /** + * @dev Public function to unlock the token. Verifies if the msg.sender is approved + */ + function unlockApproved(uint256 id) external virtual override{ + _unlockApproved(id); + } + + /** + * @dev Private function to unlock the token. Verifies if the msg.sender is approved + */ + function _unlockApproved(uint256 id) internal { + require(isApprovedForAll(ownerOf(id), msg.sender) || getApproved(id) == msg.sender, "ERC721Lockable : Required approval"); + require(state[id]!=State.LOCKED, "ERC721Lockable : Locked by locker"); + require(state[id]!=State.UNLOCKED, "ERC721Lockable : Unlocked"); + state[id]=State.UNLOCKED; + emit UnlockApproved(id); + } + + /*/////////////////////////////////////////////////////////////// + OVERRIDES + //////////////////////////////////////////////////////////////*/ + + /** + * @dev Override approve to make sure token is unlocked + */ + function approve(address to, uint256 tokenId) public virtual override { + require (state[tokenId]==State.UNLOCKED, "ERC721Lockable : Locked"); // so the unlocker stays approved + super.approve(to, tokenId); + } + + /** + * @dev Override _beforeTokenTransfer to make sure token is unlocked or msg.sender is approved if + * token is lockApproved + */ + function _beforeTokenTransfer( + address from, + address to, + uint256 startTokenId, + uint256 quantity + ) internal virtual override { + // if it is a Transfer or Burn, we always deal with one token, that is startTokenId + if (from != address(0)) { + require(state[startTokenId]!=State.LOCKED,"ERC721Lockable : Locked"); + require(state[startTokenId]==State.UNLOCKED || isApprovedForAll(ownerOf(startTokenId), msg.sender) + || getApproved(startTokenId) == msg.sender, "ERC721Lockable : Required approval"); + } + super._beforeTokenTransfer(from,to,startTokenId,quantity); + } + + /** + * @dev Override _afterTokenTransfer to make locker is purged + */ + function _afterTokenTransfer( + address from, + address to, + uint256 startTokenId, + uint256 quantity + ) internal virtual override { + // if it is a Transfer or Burn, we always deal with one token, that is startTokenId + if (from != address(0)) { + state[startTokenId]==State.UNLOCKED; + delete locker[startTokenId]; + } + super._afterTokenTransfer(from,to,startTokenId,quantity); + } + + /*/////////////////////////////////////////////////////////////// + ERC165 LOGIC + //////////////////////////////////////////////////////////////*/ + + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + return + interfaceId == type(IERC721Lockable).interfaceId || + super.supportsInterface(interfaceId); + } +} \ No newline at end of file diff --git a/assets/eip-ERC721Lockable/IERC721Lockable.sol b/assets/eip-ERC721Lockable/IERC721Lockable.sol new file mode 100644 index 0000000000000..fbfbac6c7e7f6 --- /dev/null +++ b/assets/eip-ERC721Lockable/IERC721Lockable.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity >=0.7.0 <0.9.0; + +/// @title IERC721Lockable +/// @dev Interface for the Lockable extension +/// @author streamNFT + +interface IERC721Lockable{ + + /** + * @dev Emitted when locker is set for token `id` + */ + event SetLocker (uint256 indexed id, address _locker); + + /** + * @dev Emitted when locker is removed for token `id` + */ + event RemoveLocker (uint256 indexed id); + + /** + * @dev Emitted when `id` token is locked by `locker` + */ + event Lock (uint256 indexed id); + + /** + * @dev Emitted when `id` token is unlocked by `locker` + */ + event Unlock (uint256 indexed id); + + /** + * @dev Emitted when `id` token is locked by `approved` + */ + event LockApproved (uint256 indexed id); + + /** + * @dev Emitted when `id` token is unlocked by `approved` + */ + event UnlockApproved (uint256 indexed id); + + + /** + * @dev Gives the `_locker` address permission to lock if msg.sender is owner + */ + function setLocker(uint256 id, address _locker) external; + + /** + * @dev Purge the permission to lock if `id` is `unlocked` and msg.sender is owner + */ + function removeLocker(uint256 id) external; + + /** + * @dev Lock the token `id` if msg.sender is locker + */ + function lock(uint256 id) external; + + /** + * @dev Unlocks the token `id` if msg.sender is locker + */ + function unlock(uint256 id) external; + + /** + * @dev Lock the token `id` if msg.sender is approved + */ + function lockApproved(uint256 id) external; + + /** + * @dev Unlock the token `id` if msg.sender is approved + */ + function unlockApproved(uint256 id) external; + + /** + * @dev Returns the wallet, that is stated as unlocking wallet for the `tokenId` token. + * If address(0) returned, that means token is not locked. Any other result means token is locked. + */ + function lockerOf(uint256 tokenId) external view returns (address); + +} \ No newline at end of file diff --git a/assets/eip-ERC721Lockable/test/test.js b/assets/eip-ERC721Lockable/test/test.js new file mode 100644 index 0000000000000..bf138f4b7608a --- /dev/null +++ b/assets/eip-ERC721Lockable/test/test.js @@ -0,0 +1,316 @@ +const { expect } = require('chai'); +const { ethers } = require('hardhat'); + +describe('MyNFT', function () { + async function deployMyNFTFixture() { + const [deployer, acc1, acc2, acc3] = await ethers.getSigners(); + const MyNFT = await ethers.getContractFactory('MyNFT'); + let myNFT = await MyNFT.deploy(); + await myNFT.deployed(); + + return { myNFT, deployer, acc1, acc2, acc3 }; + } + + async function mintAndSetAuthority() { + const { myNFT, deployer, acc1, acc2, acc3 } = await deployMyNFTFixture(); + await myNFT.connect(deployer).mint(); + await myNFT.connect(deployer).setLocker(0, acc1.address); + return { myNFT, deployer, acc1, acc2, acc3 }; + } + + async function approveAcc2() { + const { myNFT, deployer, acc1, acc2, acc3 } = await mintAndSetAuthority(); + await myNFT.connect(deployer).approve(acc2.address, 0); + await myNFT.connect(deployer).setApprovalForAll(acc2.address, true); + await myNFT.connect(acc2).lockApproved(0); + return { myNFT, deployer, acc1, acc2, acc3 }; + } + + describe('SetLocker', function () { + it('Should not allow anyone to set locker', async function () { + const { myNFT, deployer, acc1, acc2 } = await deployMyNFTFixture(); + await myNFT.connect(deployer).mint(); + await expect( + myNFT.connect(acc1).setLocker(0, acc1.address) + ).to.be.revertedWith('ERC721Lockable : Owner Required'); + }); + + it('Should not allow to set locker when token is locked', async function () { + const { myNFT, deployer, acc1, acc2 } = await deployMyNFTFixture(); + await myNFT.connect(deployer).mint(); + // set locker and let him lock the token + await myNFT.connect(deployer).setLocker(0, acc1.address); + await myNFT.connect(acc1).lock(0); + + //second check + await expect( + myNFT.connect(deployer).setLocker(0, acc2.address) + ).to.be.revertedWith('ERC721Lockable : Locked'); + }); + + it('Should not allow to set locker when token is locked_approved', async function () { + // minted token by deployer, acc1- locker, acc2- lock approved + const { myNFT, deployer, acc1, acc2 } = await approveAcc2(); + await expect( + myNFT.connect(deployer).setLocker(0, acc1.address) + ).to.be.revertedWith('ERC721Lockable : Locked'); + }); + }); + + describe('RemoveLocker', function () { + it('Should not allow anyone to remove locker', async () => { + const { myNFT, deployer, acc1 } = await mintAndSetAuthority(); + await expect(myNFT.connect(acc1).removeLocker(0)).to.be.revertedWith( + 'ERC721Lockable : Owner Required' + ); + }); + it('Should not allow to remove locker when token is locked', async () => { + const { myNFT, deployer, acc1 } = await mintAndSetAuthority(); + // lock the token by locker + await myNFT.connect(acc1).lock(0); + + await expect(myNFT.connect(deployer).removeLocker(0)).to.be.revertedWith( + 'ERC721Lockable : Locked' + ); + }); + + it('Should not allow to set locker when token is locked_approved', async function () { + // minted token by deployer, acc1- locker, acc2- lock approved + const { myNFT, deployer, acc1, acc2 } = await approveAcc2(); + await expect(myNFT.connect(deployer).removeLocker(0)).to.be.revertedWith( + 'ERC721Lockable : Locked' + ); + }); + }); + + describe('lockerOf', function () { + it('Should get the correct locker address', async () => { + const { myNFT, acc1 } = await mintAndSetAuthority(); + expect(await myNFT.connect(acc1).lockerOf(0)).to.equal(acc1.address); + }); + }); + + describe('lock', function () { + it('Should not allow anyone to lock', async function () { + const { myNFT, deployer, acc1, acc2 } = await mintAndSetAuthority(); + + await expect(myNFT.connect(acc2).lock(0)).to.be.revertedWith( + 'ERC721Lockable : Locker Required' + ); + await expect(myNFT.connect(deployer).lock(0)).to.be.revertedWith( + 'ERC721Lockable : Locker Required' + ); + }); + + it('Should not allow to lock when token is locked_approved', async function () { + // minted token by deployer, acc1- lock authority, acc2- lock approved + const { myNFT, deployer, acc1, acc2 } = await approveAcc2(); + await expect(myNFT.connect(acc1).lock(0)).to.be.revertedWith( + 'ERC721Lockable : Locked' + ); + }); + + it('Should not allow to lock when token is locked', async function () { + const { myNFT, deployer, acc1, acc2 } = await mintAndSetAuthority(); + + //lock the token by lock authority + await myNFT.connect(acc1).lock(0); + + //second check + await expect(myNFT.connect(acc1).lock(0)).to.be.revertedWith( + 'ERC721Lockable : Locked' + ); + }); + }); + + describe('unlock', function () { + it('Should not allow anyone to unlock', async function () { + const { myNFT, deployer, acc1, acc2 } = await mintAndSetAuthority(); + + await expect(myNFT.connect(acc2).unlock(0)).to.be.revertedWith( + 'ERC721Lockable : Locker Required' + ); + await expect(myNFT.connect(deployer).unlock(0)).to.be.revertedWith( + 'ERC721Lockable : Locker Required' + ); + }); + + it('Should not allow to lock when token is locked_approved', async function () { + // minted token by deployer, acc1- lock authority, acc2- lock approved + const { myNFT, deployer, acc1, acc2 } = await approveAcc2(); + await expect(myNFT.connect(acc1).unlock(0)).to.be.revertedWith( + 'ERC721Lockable : Locked by approved' + ); + }); + + it('Should not allow to unlock when token is not locked', async function () { + const { myNFT, deployer, acc1, acc2 } = await mintAndSetAuthority(); + + await expect(myNFT.connect(acc1).unlock(0)).to.be.revertedWith( + 'ERC721Lockable : Unlocked' + ); + }); + }); + + describe('approve', function () { + it('Should not approve when token is locked', async function () { + const { myNFT, deployer, acc1, acc2 } = await mintAndSetAuthority(); + + await myNFT.connect(acc1).lock(0); + + await expect( + myNFT.connect(deployer).approve(acc2.address, 0) + ).to.be.revertedWith('ERC721Lockable : Locked'); + }); + + it('Should not approve when token is locked_approved', async function () { + // minted token by deployer, acc1- lock authority, acc2- lock approved + const { myNFT, deployer, acc1, acc2 } = await approveAcc2(); + await expect( + myNFT.connect(acc1).approve(acc2.address, 0) + ).to.be.revertedWith('ERC721Lockable : Locked'); + }); + + it('Should not allow anyone to approve', async function () { + const { myNFT, deployer, acc1, acc2 } = await mintAndSetAuthority(); + + await expect( + myNFT.connect(acc1).approve(acc2.address, 0) + ).to.be.revertedWith( + 'ERC721: approve caller is not token owner or approved for all' + ); + }); + }); + + describe('lockApproved', async function () { + it('Should not allow anyone without approval', async function () { + const { myNFT, deployer, acc1, acc2 } = await mintAndSetAuthority(); + await expect(myNFT.connect(acc2).lockApproved(0)).to.be.revertedWith( + 'ERC721Lockable : Required approval' + ); + }); + + it('Should check if token is locked', async function () { + const { myNFT, deployer, acc1, acc2 } = await mintAndSetAuthority(); + + await myNFT.connect(deployer).approve(acc2.address, 0); + await myNFT.connect(deployer).setApprovalForAll(acc2.address, true); + await myNFT.connect(acc1).lock(0); + + await expect(myNFT.connect(acc2).lockApproved(0)).to.be.revertedWith( + 'ERC721Lockable : Locked' + ); + }); + }); + + describe('unlockApproved', async function () { + it('Should not allow anyone without approval', async function () { + const { myNFT, deployer, acc1, acc2 } = await mintAndSetAuthority(); + + await expect(myNFT.connect(acc2).unlockApproved(0)).to.be.revertedWith( + 'ERC721Lockable : Required approval' + ); + }); + + it('Should not allow unlockApproved when token is locked', async function () { + const { myNFT, deployer, acc1, acc2 } = await mintAndSetAuthority(); + + await myNFT.connect(deployer).approve(acc2.address, 0); + await myNFT.connect(deployer).setApprovalForAll(acc2.address, true); + + //lock the token by lock authority + await myNFT.connect(acc1).lock(0); + await expect(myNFT.connect(acc2).unlockApproved(0)).to.be.revertedWith( + 'ERC721Lockable : Locked by locker' + ); + }); + + it('Should check if token is in unlocked state', async function () { + const { myNFT, deployer, acc1, acc2 } = await mintAndSetAuthority(); + + await myNFT.connect(deployer).approve(acc2.address, 0); + await myNFT.connect(deployer).setApprovalForAll(acc2.address, true); + + await expect(myNFT.connect(acc2).unlockApproved(0)).to.be.revertedWith( + 'ERC721Lockable : Unlocked' + ); + }); + }); + + describe('transferFrom', async function () { + describe('Before Token Transfer', async function () { + it('Should not allow transfer when token is locked', async function () { + const { myNFT, deployer, acc1, acc2, acc3 } = + await mintAndSetAuthority(); + + await myNFT.connect(acc1).lock(0); + await expect( + myNFT + .connect(deployer) + .transferFrom(deployer.address, acc3.address, 0) + ).to.be.revertedWith('ERC721Lockable : Locked'); + }); + it('Should not allow transfer by anyone without approval(lock_approved,ERC721)', async function () { + const { myNFT, deployer, acc1, acc2, acc3 } = await approveAcc2(); + + await expect( + myNFT.connect(acc1).transferFrom(deployer.address, acc3.address, 0) + ).to.be.revertedWith('ERC721: caller is not token owner or approved'); + }); + + it('Should not allow transfer by anyone without approval(lock_approved,ERC721_Lockable)', async function () { + const { myNFT, deployer, acc1, acc2, acc3 } = await approveAcc2(); + + await expect( + myNFT + .connect(deployer) + .transferFrom(deployer.address, acc3.address, 0) + ).to.be.revertedWith('ERC721Lockable : Required approval'); + }); + + it('Should allow approved person to transfer token', async function () { + const { myNFT, deployer, acc1, acc2, acc3 } = await approveAcc2(); + + await myNFT + .connect(acc2) + .transferFrom(deployer.address, acc3.address, 0); + expect(await myNFT.ownerOf(0)).to.equal(acc3.address); + }); + }); + + describe('After Token Transfer', async function () { + it('Should check if token has new owner', async function () { + const { myNFT, deployer, acc1, acc2, acc3 } = + await mintAndSetAuthority(); + await myNFT + .connect(deployer) + .transferFrom(deployer.address, acc3.address, 0); + + expect(await myNFT.ownerOf(0)).to.equal(acc3.address); + }); + + it('Should check if token does not have a lock authority', async function () { + const { myNFT, deployer, acc1, acc2, acc3 } = + await mintAndSetAuthority(); + await myNFT + .connect(deployer) + .transferFrom(deployer.address, acc3.address, 0); + await expect(myNFT.connect(acc1).lock(0)).to.be.revertedWith( + 'ERC721Lockable : Locker Required' + ); + }); + + it('Should check if token state is unlocked', async function () { + const { myNFT, deployer, acc1, acc2, acc3 } = + await mintAndSetAuthority(); + await myNFT + .connect(deployer) + .transferFrom(deployer.address, acc3.address, 0); + await myNFT.connect(acc3).setLocker(0, acc1.address); + await expect(myNFT.connect(acc1).unlock(0)).to.be.revertedWith( + 'ERC721Lockable : Unlocked' + ); + }); + }); + }); +}); From a8ed235db933fc7a15846613b90c15f5758a13d3 Mon Sep 17 00:00:00 2001 From: "piyush.chittara" Date: Thu, 25 May 2023 10:42:35 +0530 Subject: [PATCH 02/44] update discussion --- EIPS/eip-draft_ERC721Lockable.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-draft_ERC721Lockable.md b/EIPS/eip-draft_ERC721Lockable.md index 64576a24b3ef9..53150aeb3aeda 100644 --- a/EIPS/eip-draft_ERC721Lockable.md +++ b/EIPS/eip-draft_ERC721Lockable.md @@ -2,7 +2,7 @@ title: ERC721 Lockable description: Interface for enabling locking of ERC721 using locker and approver author: Piyush Chittara (@streamnft-tech) -discussions-to: +discussions-to: https://ethereum-magicians.org/t/erc721-lockable/14425 status: Draft category: ERC created: 2023-05-25 From ae7c47dbdf92020060bd30ff217f549df44460a9 Mon Sep 17 00:00:00 2001 From: "piyush.chittara" Date: Thu, 25 May 2023 10:43:55 +0530 Subject: [PATCH 03/44] update license --- EIPS/eip-draft_ERC721Lockable.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-draft_ERC721Lockable.md b/EIPS/eip-draft_ERC721Lockable.md index 53150aeb3aeda..d17b09994e755 100644 --- a/EIPS/eip-draft_ERC721Lockable.md +++ b/EIPS/eip-draft_ERC721Lockable.md @@ -93,7 +93,7 @@ There are no security considerations related directly to the implementation of t ## Copyright -Copyright and related rights waived via [CC0](../LICENSE). +Copyright and related rights waived via [CC0](../LICENSE.md). ## Citation Please cite this document as: <> From b9f6c763cac414d4c5b0189b5bbc172bcf4695e0 Mon Sep 17 00:00:00 2001 From: "piyush.chittara" Date: Thu, 25 May 2023 10:49:07 +0530 Subject: [PATCH 04/44] update readme --- EIPS/eip-draft_ERC721Lockable.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/EIPS/eip-draft_ERC721Lockable.md b/EIPS/eip-draft_ERC721Lockable.md index d17b09994e755..fcc2544527e14 100644 --- a/EIPS/eip-draft_ERC721Lockable.md +++ b/EIPS/eip-draft_ERC721Lockable.md @@ -4,9 +4,10 @@ description: Interface for enabling locking of ERC721 using locker and approver author: Piyush Chittara (@streamnft-tech) discussions-to: https://ethereum-magicians.org/t/erc721-lockable/14425 status: Draft +type: Standards Track category: ERC created: 2023-05-25 -requires: EIP-165,EIP-721 +requires: 165, 721 --- ## Abstract From 6671aae8c847afaf6d86b657d01d26deaaf5be71 Mon Sep 17 00:00:00 2001 From: "piyush.chittara" Date: Thu, 25 May 2023 10:51:20 +0530 Subject: [PATCH 05/44] add eip header --- EIPS/eip-draft_ERC721Lockable.md | 1 + 1 file changed, 1 insertion(+) diff --git a/EIPS/eip-draft_ERC721Lockable.md b/EIPS/eip-draft_ERC721Lockable.md index fcc2544527e14..ab19ddd816e1b 100644 --- a/EIPS/eip-draft_ERC721Lockable.md +++ b/EIPS/eip-draft_ERC721Lockable.md @@ -1,4 +1,5 @@ --- +eip: title: ERC721 Lockable description: Interface for enabling locking of ERC721 using locker and approver author: Piyush Chittara (@streamnft-tech) From d1bdba3a7b9797abcdfc54b532ad8cefeda96ef2 Mon Sep 17 00:00:00 2001 From: "piyush.chittara" Date: Thu, 25 May 2023 10:52:55 +0530 Subject: [PATCH 06/44] update title --- EIPS/eip-draft_ERC721Lockable.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/EIPS/eip-draft_ERC721Lockable.md b/EIPS/eip-draft_ERC721Lockable.md index ab19ddd816e1b..43addd7c9a0f1 100644 --- a/EIPS/eip-draft_ERC721Lockable.md +++ b/EIPS/eip-draft_ERC721Lockable.md @@ -1,7 +1,7 @@ --- eip: -title: ERC721 Lockable -description: Interface for enabling locking of ERC721 using locker and approver +title: ERC-721 Lockable +description: Interface for enabling locking of ERC-721 using locker and approver author: Piyush Chittara (@streamnft-tech) discussions-to: https://ethereum-magicians.org/t/erc721-lockable/14425 status: Draft From b0d7f747f4e1975657d0bbec6c1f606ee1352af3 Mon Sep 17 00:00:00 2001 From: "piyush.chittara" Date: Thu, 25 May 2023 11:00:21 +0530 Subject: [PATCH 07/44] update readme --- EIPS/eip-draft_ERC721Lockable.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/EIPS/eip-draft_ERC721Lockable.md b/EIPS/eip-draft_ERC721Lockable.md index 43addd7c9a0f1..b3146c9ababfb 100644 --- a/EIPS/eip-draft_ERC721Lockable.md +++ b/EIPS/eip-draft_ERC721Lockable.md @@ -1,5 +1,5 @@ --- -eip: +eip: draft_ERC721Lockable title: ERC-721 Lockable description: Interface for enabling locking of ERC-721 using locker and approver author: Piyush Chittara (@streamnft-tech) @@ -102,6 +102,6 @@ Please cite this document as: <> ## Other Lockable Implementation -* [EIP-5753] Filipp Makarov (@filmakarov), "ERC-5753: Lockable Extension for EIP-721 [DRAFT]," Ethereum Improvement Proposals, no. 5753, October 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-5753. +* [EIP-5753](https://eips.ethereum.org/EIPS/eip-5753) -* [EIP-5058] Tyler (@radiocaca), Alex (@gojazdev), John (@sfumato00), "ERC-5058: Lockable Non-Fungible Tokens [DRAFT]," Ethereum Improvement Proposals, no. 5058, April 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-5058. \ No newline at end of file +* [EIP-5058](https://eips.ethereum.org/EIPS/eip-5058) \ No newline at end of file From ab1866e2437221f03d2ea2e2948a490234f7950d Mon Sep 17 00:00:00 2001 From: "piyush.chittara" Date: Thu, 25 May 2023 11:03:42 +0530 Subject: [PATCH 08/44] update readme --- EIPS/{eip-draft_ERC721Lockable.md => eip-7066.md} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename EIPS/{eip-draft_ERC721Lockable.md => eip-7066.md} (99%) diff --git a/EIPS/eip-draft_ERC721Lockable.md b/EIPS/eip-7066.md similarity index 99% rename from EIPS/eip-draft_ERC721Lockable.md rename to EIPS/eip-7066.md index b3146c9ababfb..7aaeb690d53ca 100644 --- a/EIPS/eip-draft_ERC721Lockable.md +++ b/EIPS/eip-7066.md @@ -1,5 +1,5 @@ --- -eip: draft_ERC721Lockable +eip: 7066 title: ERC-721 Lockable description: Interface for enabling locking of ERC-721 using locker and approver author: Piyush Chittara (@streamnft-tech) @@ -101,7 +101,7 @@ Copyright and related rights waived via [CC0](../LICENSE.md). Please cite this document as: <> -## Other Lockable Implementation +## Reference Lockable Implementation * [EIP-5753](https://eips.ethereum.org/EIPS/eip-5753) * [EIP-5058](https://eips.ethereum.org/EIPS/eip-5058) \ No newline at end of file From 82b4ed15f301f90ae9d09bbb2fd4345fcfac42db Mon Sep 17 00:00:00 2001 From: "piyush.chittara" Date: Thu, 25 May 2023 11:13:42 +0530 Subject: [PATCH 09/44] update readme --- EIPS/eip-7066.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/EIPS/eip-7066.md b/EIPS/eip-7066.md index 7aaeb690d53ca..5180eb4b7bba4 100644 --- a/EIPS/eip-7066.md +++ b/EIPS/eip-7066.md @@ -12,11 +12,11 @@ requires: 165, 721 --- ## Abstract -An extension of EIP-721, this standard incorporates `locking` features into NFTs, allowing for various uses while preventing sale or transfer. The token's owner or operator has the ability to lock it, specifying an unlocker address (either an EOA or a contract) that exclusively holds the power to unlock the token. Owner can also provide approval for token-id, enabling ability to lock asset while address holds the token approval. Upon token transfer these rights get purged. +ERC-721 extension that incorporates `locking` features into NFTs, allowing for various uses while preventing sale or transfer. The token's owner or operator has the ability to lock it, specifying an unlocker address (either an EOA or a contract) that exclusively holds the power to unlock the token. Owner can also provide approval for token-id, enabling ability to lock asset while address holds the token approval. Upon token transfer these rights get purged. ## Motivation -EIP-721 has sparked an unprecedented surge in demand for NFTs. However, despite this tremendous success, NFT economy suffers from secondary liquidity where it remains Illiquid in owner’s wallet. There are projects such as NFTfi, Paraspace which aims to address the liquidity challenge, but they entail below mentioned inconveniences and risks for owners as they necessitate transferring the participating NFTs to the projects' contracts. +ERC-721 has sparked an unprecedented surge in demand for NFTs. However, despite this tremendous success, NFT economy suffers from secondary liquidity where it remains Illiquid in owner’s wallet. There are projects such as NFTfi, Paraspace which aims to address the liquidity challenge, but they entail below mentioned inconveniences and risks for owners as they necessitate transferring the participating NFTs to the projects' contracts. * Loss of utility: The utility value of NFTs diminishes when they are transferred to an escrow account, no longer remaining under the direct custody of the owners. @@ -24,7 +24,7 @@ EIP-721 has sparked an unprecedented surge in demand for NFTs. However, despite * Smart contract vulnerabilities: NFTs are susceptible to loss or theft due to potential bugs or vulnerabilities present in the smart contracts they rely on. -The aforementioned issues contribute to a poor user experience (UX), and we propose enhancing the EIP-721 standard by implementing a native locking mechanism: +The aforementioned issues contribute to a poor user experience (UX), and we propose enhancing the ERC-721 standard by implementing a native locking mechanism: Rather than being transferred to a smart contract, an NFT remains securely stored in self-custody but is locked. During the lock period, the NFT's transfer is restricted while its other properties remain unchanged. NFT Owner retains the ability to use or distribute it’s utility @@ -42,14 +42,14 @@ Lockable NFTs enable the following use cases : * Soulbound: Organization can mint and self assign `locker`, send token to user and lock the asset. * Safety: Safely and conveniently use exclusive blue chip NFTs. Lockable extension allows owner to lock NFT and designate secure cold wallet as the unlocker. This way, owner can keep NFT on MetaMask and easily use it, even if a hacker gains access to MetaMask account. Without access to the cold wallet, the hacker cannot transfer NFT, ensuring its safety. -By extending the EIP-721 standard, the proposed standard enables secure and convenient management of underlying NFT assets. It natively supports prevalent NFTFi use cases such as, staking, lending, and renting. We anticipate that this proposed standard will foster increased engagement of NFT owners in NFTFi projects, thereby enhancing the overall vitality of the NFT ecosystem. +By extending the ERC-721 standard, the proposed standard enables secure and convenient management of underlying NFT assets. It natively supports prevalent NFTFi use cases such as, staking, lending, and renting. We anticipate that this proposed standard will foster increased engagement of NFT owners in NFTFi projects, thereby enhancing the overall vitality of the NFT ecosystem. ## Specification The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174. -EIP-721 compliant contracts MAY implement this EIP to provide standard methods of locking and unlocking the token at its current owner address. If the token is `locked`, the `getLocked` function MUST return an address that is able to `unlock` the token. For tokens that are not `locked`, the getLocked function MUST return `address(0)`. +ERC-721 compliant contracts MAY implement this EIP to provide standard methods of locking and unlocking the token at its current owner address. If the token is `locked`, the `getLocked` function MUST return an address that is able to `unlock` the token. For tokens that are not `locked`, the getLocked function MUST return `address(0)`. Token owner MAY set `locker` to any address. Token MAY be locked by `locker` which MUST change the state from `UNLOCKED` to `LOCKED`. `lock` transaction MUST revert if token is not `UNLOCKED`. `unlock` transaction MUST revert if token is not `LOCKED`. `unlock` transaction MUST change state back to `UNLOCKED`. @@ -63,14 +63,14 @@ This approach presents a minimalistic solution that focuses on locking items and Moreover, when there is a requirement to grant temporary or redeemable rights for a NFT, such as rentals or purchases with installments, this EIP involves the actual transfer of the token to the temporary user's wallet, rather than simply assigning a role. This design choice ensures compatibility with existing NFT ecosystem tools and dApps, without necessitating additional interfaces or logic implementation. -This functionality already exists on Solana, enabling ease for NFT liquidity and use cases. This EIP shall introduce same functionality to EVM ecosystem. The naming and reference implementation of functions and storage entities resemble the Approval flow outlined in [EIP-721], ensuring an intuitive user experience. +This functionality already exists on Solana, enabling ease for NFT liquidity and use cases. This EIP shall introduce same functionality to EVM ecosystem. The naming and reference implementation of functions and storage entities resemble the Approval flow outlined in [ERC-721], ensuring an intuitive user experience. Existing [ERC721Upgradedable] can upgrade to ERC721 Locakable to unlock locking capability inherently and unlock underlying liquidity features. ## Backwards Compatibility -This standard is compatible with current EIP-721 standards. +This standard is compatible with current ERC-721 standards. ## Test Cases @@ -84,7 +84,7 @@ Reference Implementation can be found [here](../assets/eip-ERC721Lockable/ERC721 ## Security Considerations -There are no security considerations related directly to the implementation of this standard for the contract that manages EIP-721 tokens. +There are no security considerations related directly to the implementation of this standard for the contract that manages ERC-721 tokens. ### Considerations for the contracts that work with lockable tokens * Make sure that every contract that is stated as `locker` can actually unlock the `LOCKED` token only. From 59d0de1d517da6931cbde5d42c354873ec7013d8 Mon Sep 17 00:00:00 2001 From: "piyush.chittara" Date: Thu, 25 May 2023 11:21:39 +0530 Subject: [PATCH 10/44] update readme --- EIPS/eip-7066.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/EIPS/eip-7066.md b/EIPS/eip-7066.md index 5180eb4b7bba4..c998e094afd24 100644 --- a/EIPS/eip-7066.md +++ b/EIPS/eip-7066.md @@ -12,11 +12,11 @@ requires: 165, 721 --- ## Abstract -ERC-721 extension that incorporates `locking` features into NFTs, allowing for various uses while preventing sale or transfer. The token's owner or operator has the ability to lock it, specifying an unlocker address (either an EOA or a contract) that exclusively holds the power to unlock the token. Owner can also provide approval for token-id, enabling ability to lock asset while address holds the token approval. Upon token transfer these rights get purged. +An extension of [EIP-721](./eip-721.md), this standard incorporates `locking` features into NFTs, allowing for various uses while preventing sale or transfer. The token's owner or operator has the ability to lock it, specifying an unlocker address (either an EOA or a contract) that exclusively holds the power to unlock the token. Owner can also provide approval for token-id, enabling ability to lock asset while address holds the token approval. Upon token transfer these rights get purged. ## Motivation -ERC-721 has sparked an unprecedented surge in demand for NFTs. However, despite this tremendous success, NFT economy suffers from secondary liquidity where it remains Illiquid in owner’s wallet. There are projects such as NFTfi, Paraspace which aims to address the liquidity challenge, but they entail below mentioned inconveniences and risks for owners as they necessitate transferring the participating NFTs to the projects' contracts. +[EIP-721](./eip-721.md) has sparked an unprecedented surge in demand for NFTs. However, despite this tremendous success, NFT economy suffers from secondary liquidity where it remains Illiquid in owner’s wallet. There are projects such as NFTfi, Paraspace which aims to address the liquidity challenge, but they entail below mentioned inconveniences and risks for owners as they necessitate transferring the participating NFTs to the projects' contracts. * Loss of utility: The utility value of NFTs diminishes when they are transferred to an escrow account, no longer remaining under the direct custody of the owners. @@ -24,7 +24,7 @@ ERC-721 has sparked an unprecedented surge in demand for NFTs. However, despite * Smart contract vulnerabilities: NFTs are susceptible to loss or theft due to potential bugs or vulnerabilities present in the smart contracts they rely on. -The aforementioned issues contribute to a poor user experience (UX), and we propose enhancing the ERC-721 standard by implementing a native locking mechanism: +The aforementioned issues contribute to a poor user experience (UX), and we propose enhancing the [EIP-721](./eip-721.md) standard by implementing a native locking mechanism: Rather than being transferred to a smart contract, an NFT remains securely stored in self-custody but is locked. During the lock period, the NFT's transfer is restricted while its other properties remain unchanged. NFT Owner retains the ability to use or distribute it’s utility @@ -42,14 +42,14 @@ Lockable NFTs enable the following use cases : * Soulbound: Organization can mint and self assign `locker`, send token to user and lock the asset. * Safety: Safely and conveniently use exclusive blue chip NFTs. Lockable extension allows owner to lock NFT and designate secure cold wallet as the unlocker. This way, owner can keep NFT on MetaMask and easily use it, even if a hacker gains access to MetaMask account. Without access to the cold wallet, the hacker cannot transfer NFT, ensuring its safety. -By extending the ERC-721 standard, the proposed standard enables secure and convenient management of underlying NFT assets. It natively supports prevalent NFTFi use cases such as, staking, lending, and renting. We anticipate that this proposed standard will foster increased engagement of NFT owners in NFTFi projects, thereby enhancing the overall vitality of the NFT ecosystem. +By extending the [EIP-721](./eip-721.md) standard, the proposed standard enables secure and convenient management of underlying NFT assets. It natively supports prevalent NFTFi use cases such as, staking, lending, and renting. We anticipate that this proposed standard will foster increased engagement of NFT owners in NFTFi projects, thereby enhancing the overall vitality of the NFT ecosystem. ## Specification The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174. -ERC-721 compliant contracts MAY implement this EIP to provide standard methods of locking and unlocking the token at its current owner address. If the token is `locked`, the `getLocked` function MUST return an address that is able to `unlock` the token. For tokens that are not `locked`, the getLocked function MUST return `address(0)`. +[EIP-721](./eip-721.md) compliant contracts MAY implement this EIP to provide standard methods of locking and unlocking the token at its current owner address. If the token is `locked`, the `getLocked` function MUST return an address that is able to `unlock` the token. For tokens that are not `locked`, the getLocked function MUST return `address(0)`. Token owner MAY set `locker` to any address. Token MAY be locked by `locker` which MUST change the state from `UNLOCKED` to `LOCKED`. `lock` transaction MUST revert if token is not `UNLOCKED`. `unlock` transaction MUST revert if token is not `LOCKED`. `unlock` transaction MUST change state back to `UNLOCKED`. @@ -63,14 +63,14 @@ This approach presents a minimalistic solution that focuses on locking items and Moreover, when there is a requirement to grant temporary or redeemable rights for a NFT, such as rentals or purchases with installments, this EIP involves the actual transfer of the token to the temporary user's wallet, rather than simply assigning a role. This design choice ensures compatibility with existing NFT ecosystem tools and dApps, without necessitating additional interfaces or logic implementation. -This functionality already exists on Solana, enabling ease for NFT liquidity and use cases. This EIP shall introduce same functionality to EVM ecosystem. The naming and reference implementation of functions and storage entities resemble the Approval flow outlined in [ERC-721], ensuring an intuitive user experience. +This functionality already exists on Solana, enabling ease for NFT liquidity and use cases. This EIP shall introduce same functionality to EVM ecosystem. The naming and reference implementation of functions and storage entities resemble the Approval flow outlined in [EIP-721](./eip-721.md), ensuring an intuitive user experience. -Existing [ERC721Upgradedable] can upgrade to ERC721 Locakable to unlock locking capability inherently and unlock underlying liquidity features. +Existing Upgradedable [EIP-721](./eip-721.md) can upgrade to this standard, enabling locking capability inherently and unlock underlying liquidity features. ## Backwards Compatibility -This standard is compatible with current ERC-721 standards. +This standard is compatible with [EIP-721](./eip-721.md) standards. ## Test Cases @@ -102,6 +102,6 @@ Please cite this document as: <> ## Reference Lockable Implementation -* [EIP-5753](https://eips.ethereum.org/EIPS/eip-5753) +* [EIP-5753](./eip-5753.md) -* [EIP-5058](https://eips.ethereum.org/EIPS/eip-5058) \ No newline at end of file +* [EIP-5058](./eip-5058.md) \ No newline at end of file From 964ba2e95d7b075c748cb014ed872daf4558757e Mon Sep 17 00:00:00 2001 From: "piyush.chittara" Date: Thu, 25 May 2023 11:32:30 +0530 Subject: [PATCH 11/44] update eip number --- EIPS/eip-7066.md | 2 +- .../ERC721Lockable.sol => eip-7066/ERC7066.sol} | 8 +++++--- .../IERC721Lockable.sol => eip-7066/IERC7066.sol} | 6 +++--- assets/{eip-ERC721Lockable => eip-7066}/test/test.js | 0 4 files changed, 9 insertions(+), 7 deletions(-) rename assets/{eip-ERC721Lockable/ERC721Lockable.sol => eip-7066/ERC7066.sol} (97%) rename assets/{eip-ERC721Lockable/IERC721Lockable.sol => eip-7066/IERC7066.sol} (95%) rename assets/{eip-ERC721Lockable => eip-7066}/test/test.js (100%) diff --git a/EIPS/eip-7066.md b/EIPS/eip-7066.md index c998e094afd24..9fd640577277b 100644 --- a/EIPS/eip-7066.md +++ b/EIPS/eip-7066.md @@ -84,7 +84,7 @@ Reference Implementation can be found [here](../assets/eip-ERC721Lockable/ERC721 ## Security Considerations -There are no security considerations related directly to the implementation of this standard for the contract that manages ERC-721 tokens. +There are no security considerations related directly to the implementation of this standard for the contract that manages [EIP-721](./eip-721.md). ### Considerations for the contracts that work with lockable tokens * Make sure that every contract that is stated as `locker` can actually unlock the `LOCKED` token only. diff --git a/assets/eip-ERC721Lockable/ERC721Lockable.sol b/assets/eip-7066/ERC7066.sol similarity index 97% rename from assets/eip-ERC721Lockable/ERC721Lockable.sol rename to assets/eip-7066/ERC7066.sol index 74c5e31cab774..9221391c793dd 100644 --- a/assets/eip-ERC721Lockable/ERC721Lockable.sol +++ b/assets/eip-7066/ERC7066.sol @@ -3,11 +3,13 @@ pragma solidity >=0.7.0 <0.9.0; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import "./IERC721Lockable.sol"; +import "./IERC7066.sol"; /// @title Lockable Extension for ERC721 +/// @dev Implementation for the Lockable extension +/// @author StreamNFT -abstract contract ERC721Lockable is ERC721,IERC721Lockable{ +abstract contract ERC7066 is ERC721,IERC7066{ /*/////////////////////////////////////////////////////////////// @@ -200,7 +202,7 @@ abstract contract ERC721Lockable is ERC721,IERC721Lockable{ */ function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { return - interfaceId == type(IERC721Lockable).interfaceId || + interfaceId == type(IERC7066).interfaceId || super.supportsInterface(interfaceId); } } \ No newline at end of file diff --git a/assets/eip-ERC721Lockable/IERC721Lockable.sol b/assets/eip-7066/IERC7066.sol similarity index 95% rename from assets/eip-ERC721Lockable/IERC721Lockable.sol rename to assets/eip-7066/IERC7066.sol index fbfbac6c7e7f6..a399c8dde63a9 100644 --- a/assets/eip-ERC721Lockable/IERC721Lockable.sol +++ b/assets/eip-7066/IERC7066.sol @@ -2,11 +2,11 @@ pragma solidity >=0.7.0 <0.9.0; -/// @title IERC721Lockable +/// @title Lockable Extension for ERC721 /// @dev Interface for the Lockable extension -/// @author streamNFT +/// @author StreamNFT -interface IERC721Lockable{ +interface IERC7066{ /** * @dev Emitted when locker is set for token `id` diff --git a/assets/eip-ERC721Lockable/test/test.js b/assets/eip-7066/test/test.js similarity index 100% rename from assets/eip-ERC721Lockable/test/test.js rename to assets/eip-7066/test/test.js From 2d23193054d2a308203f1de4c3f0c85742dba3d4 Mon Sep 17 00:00:00 2001 From: "piyush.chittara" Date: Thu, 25 May 2023 11:34:41 +0530 Subject: [PATCH 12/44] update eip number --- EIPS/eip-7066.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/EIPS/eip-7066.md b/EIPS/eip-7066.md index 9fd640577277b..3c1e0726bfbe4 100644 --- a/EIPS/eip-7066.md +++ b/EIPS/eip-7066.md @@ -12,10 +12,12 @@ requires: 165, 721 --- ## Abstract + An extension of [EIP-721](./eip-721.md), this standard incorporates `locking` features into NFTs, allowing for various uses while preventing sale or transfer. The token's owner or operator has the ability to lock it, specifying an unlocker address (either an EOA or a contract) that exclusively holds the power to unlock the token. Owner can also provide approval for token-id, enabling ability to lock asset while address holds the token approval. Upon token transfer these rights get purged. ## Motivation + [EIP-721](./eip-721.md) has sparked an unprecedented surge in demand for NFTs. However, despite this tremendous success, NFT economy suffers from secondary liquidity where it remains Illiquid in owner’s wallet. There are projects such as NFTfi, Paraspace which aims to address the liquidity challenge, but they entail below mentioned inconveniences and risks for owners as they necessitate transferring the participating NFTs to the projects' contracts. @@ -87,6 +89,7 @@ Reference Implementation can be found [here](../assets/eip-ERC721Lockable/ERC721 There are no security considerations related directly to the implementation of this standard for the contract that manages [EIP-721](./eip-721.md). ### Considerations for the contracts that work with lockable tokens + * Make sure that every contract that is stated as `locker` can actually unlock the `LOCKED` token only. * Make sure that the approved contract can unlock the `LOCKED_APPROVED` token only. * `LOCKED` token with in-accesible account or un-verified contract address can lead to permanent lock of the token. @@ -98,10 +101,12 @@ There are no security considerations related directly to the implementation of t Copyright and related rights waived via [CC0](../LICENSE.md). ## Citation + Please cite this document as: <> ## Reference Lockable Implementation + * [EIP-5753](./eip-5753.md) * [EIP-5058](./eip-5058.md) \ No newline at end of file From 990d9ffc6b36cc4579b0c07775635966b03c30a9 Mon Sep 17 00:00:00 2001 From: "piyush.chittara" Date: Thu, 25 May 2023 11:45:29 +0530 Subject: [PATCH 13/44] update eip number --- EIPS/eip-7066.md | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/EIPS/eip-7066.md b/EIPS/eip-7066.md index 3c1e0726bfbe4..1943808a0039f 100644 --- a/EIPS/eip-7066.md +++ b/EIPS/eip-7066.md @@ -36,13 +36,13 @@ NFTs have numerous use cases where it is crucial for the NFT to remain within th Lockable NFTs enable the following use cases : -* NFT-collateralized loans: Utilize NFT as collateral for a loan without locking it on the lending protocol contract. Instead, lock it within owner’s wallet while still enjoying all the utility of NFT. -* No collateral rentals of NFTs: Borrow an NFT for a fee without the need for significant collateral. Renter can use the NFT but not transfer it, ensuring the lender's safety. The borrowing service contract automatically returns the NFT to the lender once the borrowing period expires. -* Buy Now Pay Later: The buyer receives the locked NFT and can immediately begin using it. However, they are unable to sell the NFT until all installments are paid. Failure to complete the full payment results in the NFT returning to the seller, along with a fee. -* Composability: Maximize liquidity by having access to multiple financial tools. Imagine taking a loan against NFT and putting it on rentals to generate passive income. -* Primary sales: Mint an NFT for a partial payment and settle the remaining amount once owner is satisfied with the collection's progress. -* Soulbound: Organization can mint and self assign `locker`, send token to user and lock the asset. -* Safety: Safely and conveniently use exclusive blue chip NFTs. Lockable extension allows owner to lock NFT and designate secure cold wallet as the unlocker. This way, owner can keep NFT on MetaMask and easily use it, even if a hacker gains access to MetaMask account. Without access to the cold wallet, the hacker cannot transfer NFT, ensuring its safety. +1. NFT-collateralized loans: Utilize NFT as collateral for a loan without locking it on the lending protocol contract. Instead, lock it within owner’s wallet while still enjoying all the utility of NFT. +2. No collateral rentals of NFTs: Borrow an NFT for a fee without the need for significant collateral. Renter can use the NFT but not transfer it, ensuring the lender's safety. The borrowing service contract automatically returns the NFT to the lender once the borrowing period expires. +3. Buy Now Pay Later: The buyer receives the locked NFT and can immediately begin using it. However, they are unable to sell the NFT until all installments are paid. Failure to complete the full payment results in the NFT returning to the seller, along with a fee. +4. Composability: Maximize liquidity by having access to multiple financial tools. Imagine taking a loan against NFT and putting it on rentals to generate passive income. +5. Primary sales: Mint an NFT for a partial payment and settle the remaining amount once owner is satisfied with the collection's progress. +6. Soulbound: Organization can mint and self assign `locker`, send token to user and lock the asset. +7. Safety: Safely and conveniently use exclusive blue chip NFTs. Lockable extension allows owner to lock NFT and designate secure cold wallet as the unlocker. This way, owner can keep NFT on MetaMask and easily use it, even if a hacker gains access to MetaMask account. Without access to the cold wallet, the hacker cannot transfer NFT, ensuring its safety. By extending the [EIP-721](./eip-721.md) standard, the proposed standard enables secure and convenient management of underlying NFT assets. It natively supports prevalent NFTFi use cases such as, staking, lending, and renting. We anticipate that this proposed standard will foster increased engagement of NFT owners in NFTFi projects, thereby enhancing the overall vitality of the NFT ecosystem. @@ -90,11 +90,11 @@ There are no security considerations related directly to the implementation of t ### Considerations for the contracts that work with lockable tokens -* Make sure that every contract that is stated as `locker` can actually unlock the `LOCKED` token only. -* Make sure that the approved contract can unlock the `LOCKED_APPROVED` token only. -* `LOCKED` token with in-accesible account or un-verified contract address can lead to permanent lock of the token. -* There are use cases, that involve transferring the token to a temporary owner and then lock it. For example, NFT rentals. Smart contracts that manage such services should always use `transferFrom` instead of `safeTransferFrom` to avoid re-entrancies. -* There are no MEV considerations regarding lockable tokens as only authorized parties are allowed to lock and unlock. +1. Make sure that every contract that is stated as `locker` can actually unlock the `LOCKED` token only. +2. Make sure that the approved contract can unlock the `LOCKED_APPROVED` token only. +3. `LOCKED` token with in-accesible account or un-verified contract address can lead to permanent lock of the token. +4. There are use cases, that involve transferring the token to a temporary owner and then lock it. For example, NFT rentals. Smart contracts that manage such services should always use `transferFrom` instead of `safeTransferFrom` to avoid re-entrancies. +5. There are no MEV considerations regarding lockable tokens as only authorized parties are allowed to lock and unlock. ## Copyright @@ -102,7 +102,8 @@ Copyright and related rights waived via [CC0](../LICENSE.md). ## Citation -Please cite this document as: <> +Please cite this document as: +[EIP-7066](./eip-7066.md) Piyush Chittara (@filmakarov), "ERC-7066: Lockable Extension for ERC721 [DRAFT]," Ethereum Improvement Proposals, no. 7066, May 2023 ## Reference Lockable Implementation From d339b67aaf0b782c609bed554ce990af96fc0f16 Mon Sep 17 00:00:00 2001 From: "piyush.chittara" Date: Thu, 25 May 2023 11:46:04 +0530 Subject: [PATCH 14/44] update eip number --- EIPS/eip-7066.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/EIPS/eip-7066.md b/EIPS/eip-7066.md index 1943808a0039f..5f8b5de963c53 100644 --- a/EIPS/eip-7066.md +++ b/EIPS/eip-7066.md @@ -105,9 +105,8 @@ Copyright and related rights waived via [CC0](../LICENSE.md). Please cite this document as: [EIP-7066](./eip-7066.md) Piyush Chittara (@filmakarov), "ERC-7066: Lockable Extension for ERC721 [DRAFT]," Ethereum Improvement Proposals, no. 7066, May 2023 - ## Reference Lockable Implementation * [EIP-5753](./eip-5753.md) -* [EIP-5058](./eip-5058.md) \ No newline at end of file +* [EIP-5058](./eip-5058.md) From b8dcef03a8240471c64aab0cef2aa3f32980568e Mon Sep 17 00:00:00 2001 From: "piyush.chittara" Date: Thu, 25 May 2023 11:52:01 +0530 Subject: [PATCH 15/44] update eip number --- EIPS/eip-7066.md | 37 ++++++++++++++++--------------------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/EIPS/eip-7066.md b/EIPS/eip-7066.md index 5f8b5de963c53..79ad16ac2e487 100644 --- a/EIPS/eip-7066.md +++ b/EIPS/eip-7066.md @@ -21,9 +21,9 @@ An extension of [EIP-721](./eip-721.md), this standard incorporates `locking` fe [EIP-721](./eip-721.md) has sparked an unprecedented surge in demand for NFTs. However, despite this tremendous success, NFT economy suffers from secondary liquidity where it remains Illiquid in owner’s wallet. There are projects such as NFTfi, Paraspace which aims to address the liquidity challenge, but they entail below mentioned inconveniences and risks for owners as they necessitate transferring the participating NFTs to the projects' contracts. -* Loss of utility: The utility value of NFTs diminishes when they are transferred to an escrow account, no longer remaining under the direct custody of the owners. -* Lack of composability: The market could benefit from increased liquidity if NFT owners had access to multiple financial tools, such as leveraging loans and renting out their assets for maximum returns. Composability serves as the missing piece in creating a more efficient market. -* Smart contract vulnerabilities: NFTs are susceptible to loss or theft due to potential bugs or vulnerabilities present in the smart contracts they rely on. +- Loss of utility: The utility value of NFTs diminishes when they are transferred to an escrow account, no longer remaining under the direct custody of the owners. +- Lack of composability: The market could benefit from increased liquidity if NFT owners had access to multiple financial tools, such as leveraging loans and renting out their assets for maximum returns. Composability serves as the missing piece in creating a more efficient market. +- Smart contract vulnerabilities: NFTs are susceptible to loss or theft due to potential bugs or vulnerabilities present in the smart contracts they rely on. The aforementioned issues contribute to a poor user experience (UX), and we propose enhancing the [EIP-721](./eip-721.md) standard by implementing a native locking mechanism: @@ -36,13 +36,13 @@ NFTs have numerous use cases where it is crucial for the NFT to remain within th Lockable NFTs enable the following use cases : -1. NFT-collateralized loans: Utilize NFT as collateral for a loan without locking it on the lending protocol contract. Instead, lock it within owner’s wallet while still enjoying all the utility of NFT. -2. No collateral rentals of NFTs: Borrow an NFT for a fee without the need for significant collateral. Renter can use the NFT but not transfer it, ensuring the lender's safety. The borrowing service contract automatically returns the NFT to the lender once the borrowing period expires. -3. Buy Now Pay Later: The buyer receives the locked NFT and can immediately begin using it. However, they are unable to sell the NFT until all installments are paid. Failure to complete the full payment results in the NFT returning to the seller, along with a fee. -4. Composability: Maximize liquidity by having access to multiple financial tools. Imagine taking a loan against NFT and putting it on rentals to generate passive income. -5. Primary sales: Mint an NFT for a partial payment and settle the remaining amount once owner is satisfied with the collection's progress. -6. Soulbound: Organization can mint and self assign `locker`, send token to user and lock the asset. -7. Safety: Safely and conveniently use exclusive blue chip NFTs. Lockable extension allows owner to lock NFT and designate secure cold wallet as the unlocker. This way, owner can keep NFT on MetaMask and easily use it, even if a hacker gains access to MetaMask account. Without access to the cold wallet, the hacker cannot transfer NFT, ensuring its safety. +- NFT-collateralized loans: Utilize NFT as collateral for a loan without locking it on the lending protocol contract. Instead, lock it within owner’s wallet while still enjoying all the utility of NFT. +- No collateral rentals of NFTs: Borrow an NFT for a fee without the need for significant collateral. Renter can use the NFT but not transfer it, ensuring the lender's safety. The borrowing service contract automatically returns the NFT to the lender once the borrowing period expires. +- Buy Now Pay Later: The buyer receives the locked NFT and can immediately begin using it. However, they are unable to sell the NFT until all installments are paid. Failure to complete the full payment results in the NFT returning to the seller, along with a fee. +- Composability: Maximize liquidity by having access to multiple financial tools. Imagine taking a loan against NFT and putting it on rentals to generate passive income. +- Primary sales: Mint an NFT for a partial payment and settle the remaining amount once owner is satisfied with the collection's progress. +- Soulbound: Organization can mint and self assign `locker`, send token to user and lock the asset. +- Safety: Safely and conveniently use exclusive blue chip NFTs. Lockable extension allows owner to lock NFT and designate secure cold wallet as the unlocker. This way, owner can keep NFT on MetaMask and easily use it, even if a hacker gains access to MetaMask account. Without access to the cold wallet, the hacker cannot transfer NFT, ensuring its safety. By extending the [EIP-721](./eip-721.md) standard, the proposed standard enables secure and convenient management of underlying NFT assets. It natively supports prevalent NFTFi use cases such as, staking, lending, and renting. We anticipate that this proposed standard will foster increased engagement of NFT owners in NFTFi projects, thereby enhancing the overall vitality of the NFT ecosystem. @@ -90,22 +90,17 @@ There are no security considerations related directly to the implementation of t ### Considerations for the contracts that work with lockable tokens -1. Make sure that every contract that is stated as `locker` can actually unlock the `LOCKED` token only. -2. Make sure that the approved contract can unlock the `LOCKED_APPROVED` token only. -3. `LOCKED` token with in-accesible account or un-verified contract address can lead to permanent lock of the token. -4. There are use cases, that involve transferring the token to a temporary owner and then lock it. For example, NFT rentals. Smart contracts that manage such services should always use `transferFrom` instead of `safeTransferFrom` to avoid re-entrancies. -5. There are no MEV considerations regarding lockable tokens as only authorized parties are allowed to lock and unlock. +- Make sure that every contract that is stated as `locker` can actually unlock the `LOCKED` token only. +- Make sure that the approved contract can unlock the `LOCKED_APPROVED` token only. +- `LOCKED` token with in-accesible account or un-verified contract address can lead to permanent lock of the token. +- There are use cases, that involve transferring the token to a temporary owner and then lock it. For example, NFT rentals. Smart contracts that manage such services should always use `transferFrom` instead of `safeTransferFrom` to avoid re-entrancies. +- There are no MEV considerations regarding lockable tokens as only authorized parties are allowed to lock and unlock. ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md). -## Citation - -Please cite this document as: -[EIP-7066](./eip-7066.md) Piyush Chittara (@filmakarov), "ERC-7066: Lockable Extension for ERC721 [DRAFT]," Ethereum Improvement Proposals, no. 7066, May 2023 - -## Reference Lockable Implementation +## Referenced Lockable Implementation * [EIP-5753](./eip-5753.md) From 930ec58ced581bd0a4c0466e5de2dcb20cdd3970 Mon Sep 17 00:00:00 2001 From: "piyush.chittara" Date: Thu, 25 May 2023 11:56:24 +0530 Subject: [PATCH 16/44] update readme --- EIPS/eip-7066.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/EIPS/eip-7066.md b/EIPS/eip-7066.md index 79ad16ac2e487..d6cf5a88e94cf 100644 --- a/EIPS/eip-7066.md +++ b/EIPS/eip-7066.md @@ -13,7 +13,7 @@ requires: 165, 721 ## Abstract -An extension of [EIP-721](./eip-721.md), this standard incorporates `locking` features into NFTs, allowing for various uses while preventing sale or transfer. The token's owner or operator has the ability to lock it, specifying an unlocker address (either an EOA or a contract) that exclusively holds the power to unlock the token. Owner can also provide approval for token-id, enabling ability to lock asset while address holds the token approval. Upon token transfer these rights get purged. +An extension of [ERC-721](./eip-721.md), this standard incorporates `locking` features into NFTs, allowing for various uses while preventing sale or transfer. The token's owner or operator has the ability to lock it, specifying an unlocker address (either an EOA or a contract) that exclusively holds the power to unlock the token. Owner can also provide approval for token-id, enabling ability to lock asset while address holds the token approval. Upon token transfer these rights get purged. ## Motivation @@ -99,9 +99,3 @@ There are no security considerations related directly to the implementation of t ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md). - -## Referenced Lockable Implementation - -* [EIP-5753](./eip-5753.md) - -* [EIP-5058](./eip-5058.md) From 5bfacfe35c0c33472e9f6787a5fe7033140263d5 Mon Sep 17 00:00:00 2001 From: "piyush.chittara" Date: Thu, 25 May 2023 11:58:14 +0530 Subject: [PATCH 17/44] update readme --- EIPS/eip-7066.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/EIPS/eip-7066.md b/EIPS/eip-7066.md index d6cf5a88e94cf..5e1efec026cfb 100644 --- a/EIPS/eip-7066.md +++ b/EIPS/eip-7066.md @@ -18,7 +18,7 @@ An extension of [ERC-721](./eip-721.md), this standard incorporates `locking` fe ## Motivation -[EIP-721](./eip-721.md) has sparked an unprecedented surge in demand for NFTs. However, despite this tremendous success, NFT economy suffers from secondary liquidity where it remains Illiquid in owner’s wallet. There are projects such as NFTfi, Paraspace which aims to address the liquidity challenge, but they entail below mentioned inconveniences and risks for owners as they necessitate transferring the participating NFTs to the projects' contracts. +[ERC-721](./eip-721.md) has sparked an unprecedented surge in demand for NFTs. However, despite this tremendous success, NFT economy suffers from secondary liquidity where it remains Illiquid in owner’s wallet. There are projects such as NFTfi, Paraspace which aims to address the liquidity challenge, but they entail below mentioned inconveniences and risks for owners as they necessitate transferring the participating NFTs to the projects' contracts. - Loss of utility: The utility value of NFTs diminishes when they are transferred to an escrow account, no longer remaining under the direct custody of the owners. @@ -26,7 +26,7 @@ An extension of [ERC-721](./eip-721.md), this standard incorporates `locking` fe - Smart contract vulnerabilities: NFTs are susceptible to loss or theft due to potential bugs or vulnerabilities present in the smart contracts they rely on. -The aforementioned issues contribute to a poor user experience (UX), and we propose enhancing the [EIP-721](./eip-721.md) standard by implementing a native locking mechanism: +The aforementioned issues contribute to a poor user experience (UX), and we propose enhancing the [ERC-721](./eip-721.md) standard by implementing a native locking mechanism: Rather than being transferred to a smart contract, an NFT remains securely stored in self-custody but is locked. During the lock period, the NFT's transfer is restricted while its other properties remain unchanged. NFT Owner retains the ability to use or distribute it’s utility @@ -44,14 +44,14 @@ Lockable NFTs enable the following use cases : - Soulbound: Organization can mint and self assign `locker`, send token to user and lock the asset. - Safety: Safely and conveniently use exclusive blue chip NFTs. Lockable extension allows owner to lock NFT and designate secure cold wallet as the unlocker. This way, owner can keep NFT on MetaMask and easily use it, even if a hacker gains access to MetaMask account. Without access to the cold wallet, the hacker cannot transfer NFT, ensuring its safety. -By extending the [EIP-721](./eip-721.md) standard, the proposed standard enables secure and convenient management of underlying NFT assets. It natively supports prevalent NFTFi use cases such as, staking, lending, and renting. We anticipate that this proposed standard will foster increased engagement of NFT owners in NFTFi projects, thereby enhancing the overall vitality of the NFT ecosystem. +By extending the [ERC-721](./eip-721.md) standard, the proposed standard enables secure and convenient management of underlying NFT assets. It natively supports prevalent NFTFi use cases such as, staking, lending, and renting. We anticipate that this proposed standard will foster increased engagement of NFT owners in NFTFi projects, thereby enhancing the overall vitality of the NFT ecosystem. ## Specification The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174. -[EIP-721](./eip-721.md) compliant contracts MAY implement this EIP to provide standard methods of locking and unlocking the token at its current owner address. If the token is `locked`, the `getLocked` function MUST return an address that is able to `unlock` the token. For tokens that are not `locked`, the getLocked function MUST return `address(0)`. +[ERC-721](./eip-721.md) compliant contracts MAY implement this EIP to provide standard methods of locking and unlocking the token at its current owner address. If the token is `locked`, the `getLocked` function MUST return an address that is able to `unlock` the token. For tokens that are not `locked`, the getLocked function MUST return `address(0)`. Token owner MAY set `locker` to any address. Token MAY be locked by `locker` which MUST change the state from `UNLOCKED` to `LOCKED`. `lock` transaction MUST revert if token is not `UNLOCKED`. `unlock` transaction MUST revert if token is not `LOCKED`. `unlock` transaction MUST change state back to `UNLOCKED`. @@ -65,14 +65,14 @@ This approach presents a minimalistic solution that focuses on locking items and Moreover, when there is a requirement to grant temporary or redeemable rights for a NFT, such as rentals or purchases with installments, this EIP involves the actual transfer of the token to the temporary user's wallet, rather than simply assigning a role. This design choice ensures compatibility with existing NFT ecosystem tools and dApps, without necessitating additional interfaces or logic implementation. -This functionality already exists on Solana, enabling ease for NFT liquidity and use cases. This EIP shall introduce same functionality to EVM ecosystem. The naming and reference implementation of functions and storage entities resemble the Approval flow outlined in [EIP-721](./eip-721.md), ensuring an intuitive user experience. +This functionality already exists on Solana, enabling ease for NFT liquidity and use cases. This EIP shall introduce same functionality to EVM ecosystem. The naming and reference implementation of functions and storage entities resemble the Approval flow outlined in [ERC-721](./eip-721.md), ensuring an intuitive user experience. -Existing Upgradedable [EIP-721](./eip-721.md) can upgrade to this standard, enabling locking capability inherently and unlock underlying liquidity features. +Existing Upgradedable [ERC-721](./eip-721.md) can upgrade to this standard, enabling locking capability inherently and unlock underlying liquidity features. ## Backwards Compatibility -This standard is compatible with [EIP-721](./eip-721.md) standards. +This standard is compatible with [ERC-721](./eip-721.md) standards. ## Test Cases @@ -86,7 +86,7 @@ Reference Implementation can be found [here](../assets/eip-ERC721Lockable/ERC721 ## Security Considerations -There are no security considerations related directly to the implementation of this standard for the contract that manages [EIP-721](./eip-721.md). +There are no security considerations related directly to the implementation of this standard for the contract that manages [ERC-721](./eip-721.md). ### Considerations for the contracts that work with lockable tokens From 624b4445760c8e4331f5c36c1059e06caf53bf1c Mon Sep 17 00:00:00 2001 From: "piyush.chittara" Date: Thu, 25 May 2023 11:59:37 +0530 Subject: [PATCH 18/44] update readme --- EIPS/eip-7066.md | 1 + 1 file changed, 1 insertion(+) diff --git a/EIPS/eip-7066.md b/EIPS/eip-7066.md index 5e1efec026cfb..af5b74a31a5e0 100644 --- a/EIPS/eip-7066.md +++ b/EIPS/eip-7066.md @@ -36,6 +36,7 @@ NFTs have numerous use cases where it is crucial for the NFT to remain within th Lockable NFTs enable the following use cases : + - NFT-collateralized loans: Utilize NFT as collateral for a loan without locking it on the lending protocol contract. Instead, lock it within owner’s wallet while still enjoying all the utility of NFT. - No collateral rentals of NFTs: Borrow an NFT for a fee without the need for significant collateral. Renter can use the NFT but not transfer it, ensuring the lender's safety. The borrowing service contract automatically returns the NFT to the lender once the borrowing period expires. - Buy Now Pay Later: The buyer receives the locked NFT and can immediately begin using it. However, they are unable to sell the NFT until all installments are paid. Failure to complete the full payment results in the NFT returning to the seller, along with a fee. From 548f97e4b12a58eba902fdd9a6de9b5d46f3d374 Mon Sep 17 00:00:00 2001 From: "piyush.chittara" Date: Thu, 25 May 2023 12:10:03 +0530 Subject: [PATCH 19/44] update readme --- EIPS/eip-7066.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/EIPS/eip-7066.md b/EIPS/eip-7066.md index af5b74a31a5e0..b46fa735454fd 100644 --- a/EIPS/eip-7066.md +++ b/EIPS/eip-7066.md @@ -77,13 +77,13 @@ This standard is compatible with [ERC-721](./eip-721.md) standards. ## Test Cases -Test cases can be found [here](../assets/eip-ERC721Lockable/test/test.js). +Test cases can be found [here](../assets/eip-7066/test/test.js). ## Reference Implementation -Reference Interface can be found [here](../assets/eip-ERC721Lockable/IERC721Lockable.sol). +Reference Interface can be found [here](../assets/eip-7066/IERC7066.sol). -Reference Implementation can be found [here](../assets/eip-ERC721Lockable/ERC721Lockable.sol). +Reference Implementation can be found [here](../assets/eip-7066/ERC7066.sol). ## Security Considerations From 8c55c141f2eafe959bb6a28a3337bd6e6c6699f8 Mon Sep 17 00:00:00 2001 From: "piyush.chittara" Date: Thu, 25 May 2023 12:13:10 +0530 Subject: [PATCH 20/44] error codes --- assets/eip-7066/ERC7066.sol | 36 +++++++++++++------------- assets/eip-7066/test/test.js | 50 ++++++++++++++++++------------------ 2 files changed, 43 insertions(+), 43 deletions(-) diff --git a/assets/eip-7066/ERC7066.sol b/assets/eip-7066/ERC7066.sol index 9221391c793dd..40239b8b21bbd 100644 --- a/assets/eip-7066/ERC7066.sol +++ b/assets/eip-7066/ERC7066.sol @@ -40,8 +40,8 @@ abstract contract ERC7066 is ERC721,IERC7066{ * and allows setting locker for tokenid */ function _setLocker(uint256 id, address _locker) private { - require(msg.sender==ownerOf(id), "ERC721Lockable : Owner Required"); - require(state[id]==State.UNLOCKED, "ERC721Lockable : Locked"); + require(msg.sender==ownerOf(id), "ERC7066 : Owner Required"); + require(state[id]==State.UNLOCKED, "ERC7066 : Locked"); locker[id]=_locker; emit SetLocker(id, _locker); } @@ -59,8 +59,8 @@ abstract contract ERC7066 is ERC721,IERC7066{ * and allows removal of locker for tokenid if token is unlocked */ function _removeLocker(uint256 id) private { - require(msg.sender==ownerOf(id), "ERC721Lockable : Owner Required"); - require(state[id]==State.UNLOCKED, "ERC721Lockable : Locked"); + require(msg.sender==ownerOf(id), "ERC7066 : Owner Required"); + require(state[id]==State.UNLOCKED, "ERC7066 : Locked"); delete locker[id]; emit RemoveLocker(id); } @@ -71,7 +71,7 @@ abstract contract ERC7066 is ERC721,IERC7066{ * reverts if token does not exist */ function lockerOf(uint256 id) external virtual view override returns(address){ - require(_exists(id), "ERC721Lockable: Nonexistent token"); + require(_exists(id), "ERC7066: Nonexistent token"); return locker[id]; } @@ -86,8 +86,8 @@ abstract contract ERC7066 is ERC721,IERC7066{ * @dev Private function to lock the token. Verifies if the msg.sender is locker */ function _lock(uint256 id) private { - require(msg.sender==locker[id], "ERC721Lockable : Locker Required"); - require(state[id]==State.UNLOCKED, "ERC721Lockable : Locked"); + require(msg.sender==locker[id], "ERC7066 : Locker Required"); + require(state[id]==State.UNLOCKED, "ERC7066 : Locked"); state[id]=State.LOCKED; emit Lock(id); } @@ -103,9 +103,9 @@ abstract contract ERC7066 is ERC721,IERC7066{ * @dev Private function to unlock the token. Verifies if the msg.sender is locker */ function _unlock(uint256 id) private { - require(msg.sender==locker[id], "ERC721Lockable : Locker Required"); - require(state[id]!=State.LOCKED_APPROVED, "ERC721Lockable : Locked by approved"); - require(state[id]!=State.UNLOCKED, "ERC721Lockable : Unlocked"); + require(msg.sender==locker[id], "ERC7066 : Locker Required"); + require(state[id]!=State.LOCKED_APPROVED, "ERC7066 : Locked by approved"); + require(state[id]!=State.UNLOCKED, "ERC7066 : Unlocked"); state[id]=State.UNLOCKED; emit Unlock(id); } @@ -121,8 +121,8 @@ abstract contract ERC7066 is ERC721,IERC7066{ * @dev Private function to lock the token. Verifies if the msg.sender is approved */ function _lockApproved(uint256 id) internal { - require(isApprovedForAll(ownerOf(id), msg.sender) || getApproved(id) == msg.sender, "ERC721Lockable : Required approval"); - require(state[id]==State.UNLOCKED, "ERC721Lockable : Locked"); + require(isApprovedForAll(ownerOf(id), msg.sender) || getApproved(id) == msg.sender, "ERC7066 : Required approval"); + require(state[id]==State.UNLOCKED, "ERC7066 : Locked"); state[id]=State.LOCKED_APPROVED; emit LockApproved(id); } @@ -138,9 +138,9 @@ abstract contract ERC7066 is ERC721,IERC7066{ * @dev Private function to unlock the token. Verifies if the msg.sender is approved */ function _unlockApproved(uint256 id) internal { - require(isApprovedForAll(ownerOf(id), msg.sender) || getApproved(id) == msg.sender, "ERC721Lockable : Required approval"); - require(state[id]!=State.LOCKED, "ERC721Lockable : Locked by locker"); - require(state[id]!=State.UNLOCKED, "ERC721Lockable : Unlocked"); + require(isApprovedForAll(ownerOf(id), msg.sender) || getApproved(id) == msg.sender, "ERC7066 : Required approval"); + require(state[id]!=State.LOCKED, "ERC7066 : Locked by locker"); + require(state[id]!=State.UNLOCKED, "ERC7066 : Unlocked"); state[id]=State.UNLOCKED; emit UnlockApproved(id); } @@ -153,7 +153,7 @@ abstract contract ERC7066 is ERC721,IERC7066{ * @dev Override approve to make sure token is unlocked */ function approve(address to, uint256 tokenId) public virtual override { - require (state[tokenId]==State.UNLOCKED, "ERC721Lockable : Locked"); // so the unlocker stays approved + require (state[tokenId]==State.UNLOCKED, "ERC7066 : Locked"); // so the unlocker stays approved super.approve(to, tokenId); } @@ -169,9 +169,9 @@ abstract contract ERC7066 is ERC721,IERC7066{ ) internal virtual override { // if it is a Transfer or Burn, we always deal with one token, that is startTokenId if (from != address(0)) { - require(state[startTokenId]!=State.LOCKED,"ERC721Lockable : Locked"); + require(state[startTokenId]!=State.LOCKED,"ERC7066 : Locked"); require(state[startTokenId]==State.UNLOCKED || isApprovedForAll(ownerOf(startTokenId), msg.sender) - || getApproved(startTokenId) == msg.sender, "ERC721Lockable : Required approval"); + || getApproved(startTokenId) == msg.sender, "ERC7066 : Required approval"); } super._beforeTokenTransfer(from,to,startTokenId,quantity); } diff --git a/assets/eip-7066/test/test.js b/assets/eip-7066/test/test.js index bf138f4b7608a..a1479e5ed2ed4 100644 --- a/assets/eip-7066/test/test.js +++ b/assets/eip-7066/test/test.js @@ -32,7 +32,7 @@ describe('MyNFT', function () { await myNFT.connect(deployer).mint(); await expect( myNFT.connect(acc1).setLocker(0, acc1.address) - ).to.be.revertedWith('ERC721Lockable : Owner Required'); + ).to.be.revertedWith('ERC7066 : Owner Required'); }); it('Should not allow to set locker when token is locked', async function () { @@ -45,7 +45,7 @@ describe('MyNFT', function () { //second check await expect( myNFT.connect(deployer).setLocker(0, acc2.address) - ).to.be.revertedWith('ERC721Lockable : Locked'); + ).to.be.revertedWith('ERC7066 : Locked'); }); it('Should not allow to set locker when token is locked_approved', async function () { @@ -53,7 +53,7 @@ describe('MyNFT', function () { const { myNFT, deployer, acc1, acc2 } = await approveAcc2(); await expect( myNFT.connect(deployer).setLocker(0, acc1.address) - ).to.be.revertedWith('ERC721Lockable : Locked'); + ).to.be.revertedWith('ERC7066 : Locked'); }); }); @@ -61,7 +61,7 @@ describe('MyNFT', function () { it('Should not allow anyone to remove locker', async () => { const { myNFT, deployer, acc1 } = await mintAndSetAuthority(); await expect(myNFT.connect(acc1).removeLocker(0)).to.be.revertedWith( - 'ERC721Lockable : Owner Required' + 'ERC7066 : Owner Required' ); }); it('Should not allow to remove locker when token is locked', async () => { @@ -70,7 +70,7 @@ describe('MyNFT', function () { await myNFT.connect(acc1).lock(0); await expect(myNFT.connect(deployer).removeLocker(0)).to.be.revertedWith( - 'ERC721Lockable : Locked' + 'ERC7066 : Locked' ); }); @@ -78,7 +78,7 @@ describe('MyNFT', function () { // minted token by deployer, acc1- locker, acc2- lock approved const { myNFT, deployer, acc1, acc2 } = await approveAcc2(); await expect(myNFT.connect(deployer).removeLocker(0)).to.be.revertedWith( - 'ERC721Lockable : Locked' + 'ERC7066 : Locked' ); }); }); @@ -95,10 +95,10 @@ describe('MyNFT', function () { const { myNFT, deployer, acc1, acc2 } = await mintAndSetAuthority(); await expect(myNFT.connect(acc2).lock(0)).to.be.revertedWith( - 'ERC721Lockable : Locker Required' + 'ERC7066 : Locker Required' ); await expect(myNFT.connect(deployer).lock(0)).to.be.revertedWith( - 'ERC721Lockable : Locker Required' + 'ERC7066 : Locker Required' ); }); @@ -106,7 +106,7 @@ describe('MyNFT', function () { // minted token by deployer, acc1- lock authority, acc2- lock approved const { myNFT, deployer, acc1, acc2 } = await approveAcc2(); await expect(myNFT.connect(acc1).lock(0)).to.be.revertedWith( - 'ERC721Lockable : Locked' + 'ERC7066 : Locked' ); }); @@ -118,7 +118,7 @@ describe('MyNFT', function () { //second check await expect(myNFT.connect(acc1).lock(0)).to.be.revertedWith( - 'ERC721Lockable : Locked' + 'ERC7066 : Locked' ); }); }); @@ -128,10 +128,10 @@ describe('MyNFT', function () { const { myNFT, deployer, acc1, acc2 } = await mintAndSetAuthority(); await expect(myNFT.connect(acc2).unlock(0)).to.be.revertedWith( - 'ERC721Lockable : Locker Required' + 'ERC7066 : Locker Required' ); await expect(myNFT.connect(deployer).unlock(0)).to.be.revertedWith( - 'ERC721Lockable : Locker Required' + 'ERC7066 : Locker Required' ); }); @@ -139,7 +139,7 @@ describe('MyNFT', function () { // minted token by deployer, acc1- lock authority, acc2- lock approved const { myNFT, deployer, acc1, acc2 } = await approveAcc2(); await expect(myNFT.connect(acc1).unlock(0)).to.be.revertedWith( - 'ERC721Lockable : Locked by approved' + 'ERC7066 : Locked by approved' ); }); @@ -147,7 +147,7 @@ describe('MyNFT', function () { const { myNFT, deployer, acc1, acc2 } = await mintAndSetAuthority(); await expect(myNFT.connect(acc1).unlock(0)).to.be.revertedWith( - 'ERC721Lockable : Unlocked' + 'ERC7066 : Unlocked' ); }); }); @@ -160,7 +160,7 @@ describe('MyNFT', function () { await expect( myNFT.connect(deployer).approve(acc2.address, 0) - ).to.be.revertedWith('ERC721Lockable : Locked'); + ).to.be.revertedWith('ERC7066 : Locked'); }); it('Should not approve when token is locked_approved', async function () { @@ -168,7 +168,7 @@ describe('MyNFT', function () { const { myNFT, deployer, acc1, acc2 } = await approveAcc2(); await expect( myNFT.connect(acc1).approve(acc2.address, 0) - ).to.be.revertedWith('ERC721Lockable : Locked'); + ).to.be.revertedWith('ERC7066 : Locked'); }); it('Should not allow anyone to approve', async function () { @@ -186,7 +186,7 @@ describe('MyNFT', function () { it('Should not allow anyone without approval', async function () { const { myNFT, deployer, acc1, acc2 } = await mintAndSetAuthority(); await expect(myNFT.connect(acc2).lockApproved(0)).to.be.revertedWith( - 'ERC721Lockable : Required approval' + 'ERC7066 : Required approval' ); }); @@ -198,7 +198,7 @@ describe('MyNFT', function () { await myNFT.connect(acc1).lock(0); await expect(myNFT.connect(acc2).lockApproved(0)).to.be.revertedWith( - 'ERC721Lockable : Locked' + 'ERC7066 : Locked' ); }); }); @@ -208,7 +208,7 @@ describe('MyNFT', function () { const { myNFT, deployer, acc1, acc2 } = await mintAndSetAuthority(); await expect(myNFT.connect(acc2).unlockApproved(0)).to.be.revertedWith( - 'ERC721Lockable : Required approval' + 'ERC7066 : Required approval' ); }); @@ -221,7 +221,7 @@ describe('MyNFT', function () { //lock the token by lock authority await myNFT.connect(acc1).lock(0); await expect(myNFT.connect(acc2).unlockApproved(0)).to.be.revertedWith( - 'ERC721Lockable : Locked by locker' + 'ERC7066 : Locked by locker' ); }); @@ -232,7 +232,7 @@ describe('MyNFT', function () { await myNFT.connect(deployer).setApprovalForAll(acc2.address, true); await expect(myNFT.connect(acc2).unlockApproved(0)).to.be.revertedWith( - 'ERC721Lockable : Unlocked' + 'ERC7066 : Unlocked' ); }); }); @@ -248,7 +248,7 @@ describe('MyNFT', function () { myNFT .connect(deployer) .transferFrom(deployer.address, acc3.address, 0) - ).to.be.revertedWith('ERC721Lockable : Locked'); + ).to.be.revertedWith('ERC7066 : Locked'); }); it('Should not allow transfer by anyone without approval(lock_approved,ERC721)', async function () { const { myNFT, deployer, acc1, acc2, acc3 } = await approveAcc2(); @@ -265,7 +265,7 @@ describe('MyNFT', function () { myNFT .connect(deployer) .transferFrom(deployer.address, acc3.address, 0) - ).to.be.revertedWith('ERC721Lockable : Required approval'); + ).to.be.revertedWith('ERC7066 : Required approval'); }); it('Should allow approved person to transfer token', async function () { @@ -296,7 +296,7 @@ describe('MyNFT', function () { .connect(deployer) .transferFrom(deployer.address, acc3.address, 0); await expect(myNFT.connect(acc1).lock(0)).to.be.revertedWith( - 'ERC721Lockable : Locker Required' + 'ERC7066 : Locker Required' ); }); @@ -308,7 +308,7 @@ describe('MyNFT', function () { .transferFrom(deployer.address, acc3.address, 0); await myNFT.connect(acc3).setLocker(0, acc1.address); await expect(myNFT.connect(acc1).unlock(0)).to.be.revertedWith( - 'ERC721Lockable : Unlocked' + 'ERC7066 : Unlocked' ); }); }); From 91664658dfbaa132553b8a90a45e7b881265d621 Mon Sep 17 00:00:00 2001 From: "piyush.chittara" Date: Tue, 30 May 2023 22:15:22 +0530 Subject: [PATCH 21/44] added interface to specification --- EIPS/eip-7066.md | 78 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/EIPS/eip-7066.md b/EIPS/eip-7066.md index b46fa735454fd..0d3a050918a0a 100644 --- a/EIPS/eip-7066.md +++ b/EIPS/eip-7066.md @@ -52,6 +52,8 @@ By extending the [ERC-721](./eip-721.md) standard, the proposed standard enables The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174. +### Overview + [ERC-721](./eip-721.md) compliant contracts MAY implement this EIP to provide standard methods of locking and unlocking the token at its current owner address. If the token is `locked`, the `getLocked` function MUST return an address that is able to `unlock` the token. For tokens that are not `locked`, the getLocked function MUST return `address(0)`. Token owner MAY set `locker` to any address. Token MAY be locked by `locker` which MUST change the state from `UNLOCKED` to `LOCKED`. `lock` transaction MUST revert if token is not `UNLOCKED`. `unlock` transaction MUST revert if token is not `LOCKED`. `unlock` transaction MUST change state back to `UNLOCKED`. @@ -60,6 +62,82 @@ Token owner MAY set approval to any address. Token MAY be `locked` by `approver` `approve` transaction MUST revert if token is not `UNLOCKED`. tansfer transaction MUST revert if token state is `LOCKED`. transfer transaction MUST pass if state is `LOCKED_APPROVED` and caller is `approved`, else MUST revert otherwise. + + +### Interface +``` +interface IERC7066{ + + /** + * @dev Emitted when locker is set for token `id` + */ + event SetLocker (uint256 indexed id, address _locker); + + /** + * @dev Emitted when locker is removed for token `id` + */ + event RemoveLocker (uint256 indexed id); + + /** + * @dev Emitted when `id` token is locked by `locker` + */ + event Lock (uint256 indexed id); + + /** + * @dev Emitted when `id` token is unlocked by `locker` + */ + event Unlock (uint256 indexed id); + + /** + * @dev Emitted when `id` token is locked by `approved` + */ + event LockApproved (uint256 indexed id); + + /** + * @dev Emitted when `id` token is unlocked by `approved` + */ + event UnlockApproved (uint256 indexed id); + + + /** + * @dev Gives the `_locker` address permission to lock if msg.sender is owner + */ + function setLocker(uint256 id, address _locker) external; + + /** + * @dev Purge the permission to lock if `id` is `unlocked` and msg.sender is owner + */ + function removeLocker(uint256 id) external; + + /** + * @dev Lock the token `id` if msg.sender is locker + */ + function lock(uint256 id) external; + + /** + * @dev Unlocks the token `id` if msg.sender is locker + */ + function unlock(uint256 id) external; + + /** + * @dev Lock the token `id` if msg.sender is approved + */ + function lockApproved(uint256 id) external; + + /** + * @dev Unlock the token `id` if msg.sender is approved + */ + function unlockApproved(uint256 id) external; + + /** + * @dev Returns the wallet, that is stated as unlocking wallet for the `tokenId` token. + * If address(0) returned, that means token is not locked. Any other result means token is locked. + */ + function lockerOf(uint256 tokenId) external view returns (address); + +} +``` + ## Rationale This approach presents a minimalistic solution that focuses on locking items and specifying who has the authority to unlock them. It offers flexibility and extensibility, accommodating various potential use cases mentioned in the Motivation section. From 383f2b4f5d0afd9eba5ab4107fdc5ced8c057903 Mon Sep 17 00:00:00 2001 From: "piyush.chittara" Date: Wed, 31 May 2023 08:29:04 +0530 Subject: [PATCH 22/44] lint fixe --- EIPS/eip-7066.md | 1 + 1 file changed, 1 insertion(+) diff --git a/EIPS/eip-7066.md b/EIPS/eip-7066.md index 0d3a050918a0a..89fe90ff36c82 100644 --- a/EIPS/eip-7066.md +++ b/EIPS/eip-7066.md @@ -65,6 +65,7 @@ Token owner MAY set approval to any address. Token MAY be `locked` by `approver` ### Interface + ``` interface IERC7066{ From 42c2c4e94b3b6c12f5bffade2d0efd727edb594c Mon Sep 17 00:00:00 2001 From: "piyush.chittara" Date: Thu, 8 Jun 2023 11:47:21 +0530 Subject: [PATCH 23/44] transfer with lock/approve --- assets/eip-7066/ERC7066.sol | 90 ++++++++++++++++++++++++------------ assets/eip-7066/IERC7066.sol | 10 ++++ 2 files changed, 70 insertions(+), 30 deletions(-) diff --git a/assets/eip-7066/ERC7066.sol b/assets/eip-7066/ERC7066.sol index 40239b8b21bbd..169d08fa97254 100644 --- a/assets/eip-7066/ERC7066.sol +++ b/assets/eip-7066/ERC7066.sol @@ -28,39 +28,39 @@ abstract contract ERC7066 is ERC721,IERC7066{ //////////////////////////////////////////////////////////////*/ /** - * @dev Public function to set locker. Verifies if the msg.sender is the owner + * @dev External function to set locker. Verifies if the msg.sender is the owner * and allows setting locker for tokenid */ function setLocker(uint256 id, address _locker) external virtual override { + require(msg.sender==ownerOf(id), "ERC7066 : Owner Required"); + require(state[id]==State.UNLOCKED, "ERC7066 : Locked"); _setLocker(id,_locker); } /** - * @dev Private function to set locker. Verifies if the msg.sender is the owner + * @dev Internal function to set locker. Verifies if the msg.sender is the owner * and allows setting locker for tokenid */ - function _setLocker(uint256 id, address _locker) private { - require(msg.sender==ownerOf(id), "ERC7066 : Owner Required"); - require(state[id]==State.UNLOCKED, "ERC7066 : Locked"); + function _setLocker(uint256 id, address _locker) internal { locker[id]=_locker; emit SetLocker(id, _locker); } /** - * @dev Public function to remove locker. Verifies if the msg.sender is the owner + * @dev External function to remove locker. Verifies if the msg.sender is the owner * and allows removal of locker for tokenid if token is unlocked */ function removeLocker(uint256 id) external virtual override { + require(msg.sender==ownerOf(id), "ERC7066 : Owner Required"); + require(state[id]==State.UNLOCKED, "ERC7066 : Locked"); _removeLocker(id); } /** - * @dev Private function to remove locker. Verifies if the msg.sender is the owner + * @dev Internal function to remove locker. Verifies if the msg.sender is the owner * and allows removal of locker for tokenid if token is unlocked */ - function _removeLocker(uint256 id) private { - require(msg.sender==ownerOf(id), "ERC7066 : Owner Required"); - require(state[id]==State.UNLOCKED, "ERC7066 : Locked"); + function _removeLocker(uint256 id) internal { delete locker[id]; emit RemoveLocker(id); } @@ -79,33 +79,33 @@ abstract contract ERC7066 is ERC721,IERC7066{ * @dev Public function to lock the token. Verifies if the msg.sender is locker */ function lock(uint256 id) external virtual override{ + require(msg.sender==locker[id], "ERC7066 : Locker Required"); + require(state[id]==State.UNLOCKED, "ERC7066 : Locked"); _lock(id); } /** - * @dev Private function to lock the token. Verifies if the msg.sender is locker + * @dev Internal function to lock the token. Verifies if the msg.sender is locker */ - function _lock(uint256 id) private { - require(msg.sender==locker[id], "ERC7066 : Locker Required"); - require(state[id]==State.UNLOCKED, "ERC7066 : Locked"); + function _lock(uint256 id) internal { state[id]=State.LOCKED; emit Lock(id); } /** - * @dev Public function to unlock the token. Verifies if the msg.sender is locker + * @dev External function to unlock the token. Verifies if the msg.sender is locker */ function unlock(uint256 id) external virtual override{ + require(msg.sender==locker[id], "ERC7066 : Locker Required"); + require(state[id]!=State.LOCKED_APPROVED, "ERC7066 : Locked by approved"); + require(state[id]!=State.UNLOCKED, "ERC7066 : Unlocked"); _unlock(id); } /** - * @dev Private function to unlock the token. Verifies if the msg.sender is locker + * @dev Internal function to unlock the token. Verifies if the msg.sender is locker */ - function _unlock(uint256 id) private { - require(msg.sender==locker[id], "ERC7066 : Locker Required"); - require(state[id]!=State.LOCKED_APPROVED, "ERC7066 : Locked by approved"); - require(state[id]!=State.UNLOCKED, "ERC7066 : Unlocked"); + function _unlock(uint256 id) internal { state[id]=State.UNLOCKED; emit Unlock(id); } @@ -114,37 +114,67 @@ abstract contract ERC7066 is ERC721,IERC7066{ * @dev Public function to lock the token. Verifies if the msg.sender is approved */ function lockApproved(uint256 id) external virtual override{ + require(isApprovedForAll(ownerOf(id), msg.sender) || getApproved(id) == msg.sender, "ERC7066 : Required approval"); + require(state[id]==State.UNLOCKED, "ERC7066 : Locked"); _lockApproved(id); } /** - * @dev Private function to lock the token. Verifies if the msg.sender is approved + * @dev Internal function to lock the token. Verifies if the msg.sender is approved */ - function _lockApproved(uint256 id) internal { - require(isApprovedForAll(ownerOf(id), msg.sender) || getApproved(id) == msg.sender, "ERC7066 : Required approval"); - require(state[id]==State.UNLOCKED, "ERC7066 : Locked"); + function _lockApproved(uint256 id) internal { state[id]=State.LOCKED_APPROVED; emit LockApproved(id); } /** - * @dev Public function to unlock the token. Verifies if the msg.sender is approved + * @dev External function to unlock the token. Verifies if the msg.sender is approved */ function unlockApproved(uint256 id) external virtual override{ + require(isApprovedForAll(ownerOf(id), msg.sender) || getApproved(id) == msg.sender, "ERC7066 : Required approval"); + require(state[id]!=State.LOCKED, "ERC7066 : Locked by locker"); + require(state[id]!=State.UNLOCKED, "ERC7066 : Unlocked"); _unlockApproved(id); } /** - * @dev Private function to unlock the token. Verifies if the msg.sender is approved + * @dev Internal function to unlock the token. Verifies if the msg.sender is approved */ - function _unlockApproved(uint256 id) internal { - require(isApprovedForAll(ownerOf(id), msg.sender) || getApproved(id) == msg.sender, "ERC7066 : Required approval"); - require(state[id]!=State.LOCKED, "ERC7066 : Locked by locker"); - require(state[id]!=State.UNLOCKED, "ERC7066 : Unlocked"); + function _unlockApproved(uint256 id) internal{ state[id]=State.UNLOCKED; emit UnlockApproved(id); } + /** + * @dev External function to tranfer and update locker for the token. Verifies if the msg.sender is owner + */ + function transferWithLock(uint256 id, address from, address to, address _locker) external virtual override{ + _transferWithLock(id,from,to,_locker); + } + + /** + * @dev Internal function to tranfer and update locker for the token. Verifies if the msg.sender is owner + */ + function _transferWithLock(uint256 id, address from, address to, address _locker) internal { + transferFrom(from, to, id); + _setLocker(id,_locker); + } + + /** + * @dev External function to tranfer, update locker and approve locker for the token. Verifies if the msg.sender is owner + */ + function transferWithApprove(uint256 id, address from, address to, address _approved) external virtual override{ + _transferWithApprove(id,from,to,_approved); + } + + /** + * @dev Internal function to tranfer, update locker and approve locker for the token. Verifies if the msg.sender is owner + */ + function _transferWithApprove(uint256 id, address from, address to, address _approved) internal { + transferFrom(from, to, id); + _approve(_approved, id); + } + /*/////////////////////////////////////////////////////////////// OVERRIDES //////////////////////////////////////////////////////////////*/ diff --git a/assets/eip-7066/IERC7066.sol b/assets/eip-7066/IERC7066.sol index a399c8dde63a9..e408e1657cc8f 100644 --- a/assets/eip-7066/IERC7066.sol +++ b/assets/eip-7066/IERC7066.sol @@ -69,6 +69,16 @@ interface IERC7066{ */ function unlockApproved(uint256 id) external; + /** + * @dev Tranfer and update locker for the token if the msg.sender is owner + */ + function transferWithLock(uint256 id, address from, address to, address _locker) external; + + /** + * @dev Tranfer, update locker and approve locker for the token if the msg.sender is owner + */ + function transferWithApprove(uint256 id, address from, address to, address _approved) external; + /** * @dev Returns the wallet, that is stated as unlocking wallet for the `tokenId` token. * If address(0) returned, that means token is not locked. Any other result means token is locked. From fbec9a787f09c7b8ef5f67d2d257c30840faa7f8 Mon Sep 17 00:00:00 2001 From: "piyush.chittara" Date: Thu, 8 Jun 2023 12:01:06 +0530 Subject: [PATCH 24/44] rename --- assets/eip-7066/ERC7066.sol | 14 +++++++------- assets/eip-7066/IERC7066.sol | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/assets/eip-7066/ERC7066.sol b/assets/eip-7066/ERC7066.sol index 169d08fa97254..ef67449a50c12 100644 --- a/assets/eip-7066/ERC7066.sol +++ b/assets/eip-7066/ERC7066.sol @@ -148,14 +148,14 @@ abstract contract ERC7066 is ERC721,IERC7066{ /** * @dev External function to tranfer and update locker for the token. Verifies if the msg.sender is owner */ - function transferWithLock(uint256 id, address from, address to, address _locker) external virtual override{ - _transferWithLock(id,from,to,_locker); + function transferAndLock(uint256 id, address from, address to, address _locker) external virtual override{ + _transferAndLock(id,from,to,_locker); } /** * @dev Internal function to tranfer and update locker for the token. Verifies if the msg.sender is owner */ - function _transferWithLock(uint256 id, address from, address to, address _locker) internal { + function _transferAndLock(uint256 id, address from, address to, address _locker) internal { transferFrom(from, to, id); _setLocker(id,_locker); } @@ -163,16 +163,16 @@ abstract contract ERC7066 is ERC721,IERC7066{ /** * @dev External function to tranfer, update locker and approve locker for the token. Verifies if the msg.sender is owner */ - function transferWithApprove(uint256 id, address from, address to, address _approved) external virtual override{ - _transferWithApprove(id,from,to,_approved); + function transferAndApprove(uint256 id, address from, address to, address _approver) external virtual override{ + _transferAndApprove(id,from,to,_approver); } /** * @dev Internal function to tranfer, update locker and approve locker for the token. Verifies if the msg.sender is owner */ - function _transferWithApprove(uint256 id, address from, address to, address _approved) internal { + function _transferAndApprove(uint256 id, address from, address to, address _approver) internal { transferFrom(from, to, id); - _approve(_approved, id); + _approve(_approver, id); } /*/////////////////////////////////////////////////////////////// diff --git a/assets/eip-7066/IERC7066.sol b/assets/eip-7066/IERC7066.sol index e408e1657cc8f..6c5bceb64daf7 100644 --- a/assets/eip-7066/IERC7066.sol +++ b/assets/eip-7066/IERC7066.sol @@ -72,12 +72,12 @@ interface IERC7066{ /** * @dev Tranfer and update locker for the token if the msg.sender is owner */ - function transferWithLock(uint256 id, address from, address to, address _locker) external; + function transferAndLock(uint256 id, address from, address to, address _locker) external; /** * @dev Tranfer, update locker and approve locker for the token if the msg.sender is owner */ - function transferWithApprove(uint256 id, address from, address to, address _approved) external; + function transferAndApprove(uint256 id, address from, address to, address _approver) external; /** * @dev Returns the wallet, that is stated as unlocking wallet for the `tokenId` token. From c6cb04ee5985772ee45b25eadb5103974e2ff287 Mon Sep 17 00:00:00 2001 From: "piyush.chittara" Date: Thu, 8 Jun 2023 12:12:49 +0530 Subject: [PATCH 25/44] readme update --- EIPS/eip-7066.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/EIPS/eip-7066.md b/EIPS/eip-7066.md index 89fe90ff36c82..678ecd213114a 100644 --- a/EIPS/eip-7066.md +++ b/EIPS/eip-7066.md @@ -56,13 +56,13 @@ The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "S [ERC-721](./eip-721.md) compliant contracts MAY implement this EIP to provide standard methods of locking and unlocking the token at its current owner address. If the token is `locked`, the `getLocked` function MUST return an address that is able to `unlock` the token. For tokens that are not `locked`, the getLocked function MUST return `address(0)`. -Token owner MAY set `locker` to any address. Token MAY be locked by `locker` which MUST change the state from `UNLOCKED` to `LOCKED`. `lock` transaction MUST revert if token is not `UNLOCKED`. `unlock` transaction MUST revert if token is not `LOCKED`. `unlock` transaction MUST change state back to `UNLOCKED`. +Token owner MAY set `locker` to any address. Token MAY be locked by `locker` which MUST change the state from `UNLOCKED` to `LOCKED`. `lock` function MUST revert if token is not `UNLOCKED`. `unlock` function MUST revert if token is not `LOCKED`. `unlock` function MUST change state back to `UNLOCKED`. -Token owner MAY set approval to any address. Token MAY be `locked` by `approver` which MUST change the state from `UNLOCKED` to `LOCKED_APPROVED`. `lockApproved` transaction MUST revert if token is not `UNLOCKED`. `unlockApproved` transaction MUST revert if token is not `LOCKED_APPROVED`. `unlockApproved` MUST change state back to `UNLOCKED`. - -`approve` transaction MUST revert if token is not `UNLOCKED`. tansfer transaction MUST revert if token state is `LOCKED`. transfer transaction MUST pass if state is `LOCKED_APPROVED` and caller is `approved`, else MUST revert otherwise. +Token owner MAY set approval to any address. Token MAY be `locked` by `approver` which MUST change the state from `UNLOCKED` to `LOCKED_APPROVED`. `lockApproved` function MUST revert if token is not `UNLOCKED`. `unlockApproved` function MUST revert if token is not `LOCKED_APPROVED`. `unlockApproved` function MUST change state back to `UNLOCKED`. +`approve` function MUST revert if token is not `UNLOCKED`. `_tansfer` function MUST revert if token state is `LOCKED`. `_transfer` transaction MUST pass if state is `LOCKED_APPROVED` and `msg.sender` is `approved`, else MUST revert otherwise. +NFT can be transfered, with updating `locker` privileges using `transferAndLock` function. NFT can be transfered, with updating `approve` privileges using `transferAndApprove` function. ### Interface @@ -130,6 +130,16 @@ interface IERC7066{ */ function unlockApproved(uint256 id) external; + /** + * @dev Tranfer and update locker for the token if the msg.sender is owner + */ + function transferAndLock(uint256 id, address from, address to, address _locker) external; + + /** + * @dev Tranfer, update locker and approve locker for the token if the msg.sender is owner + */ + function transferAndApprove(uint256 id, address from, address to, address _approver) external; + /** * @dev Returns the wallet, that is stated as unlocking wallet for the `tokenId` token. * If address(0) returned, that means token is not locked. Any other result means token is locked. From e7383b9f7c0bb77cab060d9d22b36c506514cc95 Mon Sep 17 00:00:00 2001 From: "piyush.chittara" Date: Fri, 9 Jun 2023 12:53:37 +0530 Subject: [PATCH 26/44] optimize lock/unlock --- assets/eip-7066/ERC7066.sol | 142 +++++++++++++++++------------------ assets/eip-7066/IERC7066.sol | 40 ++-------- 2 files changed, 75 insertions(+), 107 deletions(-) diff --git a/assets/eip-7066/ERC7066.sol b/assets/eip-7066/ERC7066.sol index ef67449a50c12..b8a24138c68df 100644 --- a/assets/eip-7066/ERC7066.sol +++ b/assets/eip-7066/ERC7066.sol @@ -5,41 +5,40 @@ pragma solidity >=0.7.0 <0.9.0; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "./IERC7066.sol"; -/// @title Lockable Extension for ERC721 -/// @dev Implementation for the Lockable extension +/// @title ERC7066: Lockable Extension for ERC721 +/// @dev Implementation for the Lockable extension ERC7066 for ERC721 /// @author StreamNFT abstract contract ERC7066 is ERC721,IERC7066{ /*/////////////////////////////////////////////////////////////// - LOCKABLE EXTENSION STORAGE + ERC7066 EXTENSION STORAGE //////////////////////////////////////////////////////////////*/ - //Mapping from token id to user address for locking permission + //Mapping from token-id to user address for locking permission mapping(uint256 => address) internal locker; - //Mapping from token id to state of token + //Mapping from token-id to state of token mapping(uint256 => State) internal state; //Possible states of a token enum State{UNLOCKED,LOCKED,LOCKED_APPROVED} /*/////////////////////////////////////////////////////////////// - LOCKABLE LOGIC + ERC7066 LOGIC //////////////////////////////////////////////////////////////*/ /** - * @dev External function to set locker. Verifies if the msg.sender is the owner + * @dev Public function to set locker. Verifies if the msg.sender is the owner * and allows setting locker for tokenid */ - function setLocker(uint256 id, address _locker) external virtual override { + function setLocker(uint256 id, address _locker) public virtual override { require(msg.sender==ownerOf(id), "ERC7066 : Owner Required"); require(state[id]==State.UNLOCKED, "ERC7066 : Locked"); _setLocker(id,_locker); } /** - * @dev Internal function to set locker. Verifies if the msg.sender is the owner - * and allows setting locker for tokenid + * @dev Internal function to set locker. */ function _setLocker(uint256 id, address _locker) internal { locker[id]=_locker; @@ -47,18 +46,17 @@ abstract contract ERC7066 is ERC721,IERC7066{ } /** - * @dev External function to remove locker. Verifies if the msg.sender is the owner + * @dev Public function to remove locker. Verifies if the msg.sender is the owner * and allows removal of locker for tokenid if token is unlocked */ - function removeLocker(uint256 id) external virtual override { + function removeLocker(uint256 id) public virtual override { require(msg.sender==ownerOf(id), "ERC7066 : Owner Required"); require(state[id]==State.UNLOCKED, "ERC7066 : Locked"); _removeLocker(id); } /** - * @dev Internal function to remove locker. Verifies if the msg.sender is the owner - * and allows removal of locker for tokenid if token is unlocked + * @dev Internal function to remove locker. */ function _removeLocker(uint256 id) internal { delete locker[id]; @@ -70,109 +68,105 @@ abstract contract ERC7066 is ERC721,IERC7066{ * address(0) means token is not locked * reverts if token does not exist */ - function lockerOf(uint256 id) external virtual view override returns(address){ + function lockerOf(uint256 id) public virtual view override returns(address){ require(_exists(id), "ERC7066: Nonexistent token"); return locker[id]; } /** - * @dev Public function to lock the token. Verifies if the msg.sender is locker + * @dev Public function to lock the token. Verifies if the msg.sender is locker or approver + * reverts otherwise */ - function lock(uint256 id) external virtual override{ - require(msg.sender==locker[id], "ERC7066 : Locker Required"); + function lock(uint256 id) public virtual override{ require(state[id]==State.UNLOCKED, "ERC7066 : Locked"); - _lock(id); + if(isApprovedForAll(ownerOf(id), msg.sender) || getApproved(id)== msg.sender){ + lockApprove(id); + } else if (msg.sender==locker[id]){ + lockLocker(id); + } else{ + revert("ERC7066: Required locker or approve"); + } + } + + /** + * @dev Internal function to lock the token. Verifies if the msg.sender is approved + */ + function lockApprove(uint256 id) internal { + state[id]=State.LOCKED_APPROVED; + emit Lock(id); } /** * @dev Internal function to lock the token. Verifies if the msg.sender is locker */ - function _lock(uint256 id) internal { + function lockLocker(uint256 id) internal { state[id]=State.LOCKED; emit Lock(id); } /** - * @dev External function to unlock the token. Verifies if the msg.sender is locker + * @dev External function to unlock the token. Verifies the msg.sender is locker or approver + * reverts otherwise */ - function unlock(uint256 id) external virtual override{ - require(msg.sender==locker[id], "ERC7066 : Locker Required"); - require(state[id]!=State.LOCKED_APPROVED, "ERC7066 : Locked by approved"); + function unlock(uint256 id) public virtual override{ require(state[id]!=State.UNLOCKED, "ERC7066 : Unlocked"); - _unlock(id); + if(state[id]==State.LOCKED_APPROVED){ + unlockApprove(id); + }else if(state[id]==State.LOCKED){ + unlockLocker(id); + }else{ + revert("ERC7066: Required locker or approve"); + } } /** - * @dev Internal function to unlock the token. Verifies if the msg.sender is locker + * @dev Internal function to unlock the token. Verifies if the msg.sender is approved */ - function _unlock(uint256 id) internal { + function unlockApprove(uint256 id) internal{ + require(isApprovedForAll(ownerOf(id), msg.sender) || getApproved(id)== msg.sender,"ERC7066: Approve Required"); state[id]=State.UNLOCKED; emit Unlock(id); } /** - * @dev Public function to lock the token. Verifies if the msg.sender is approved - */ - function lockApproved(uint256 id) external virtual override{ - require(isApprovedForAll(ownerOf(id), msg.sender) || getApproved(id) == msg.sender, "ERC7066 : Required approval"); - require(state[id]==State.UNLOCKED, "ERC7066 : Locked"); - _lockApproved(id); - } - - /** - * @dev Internal function to lock the token. Verifies if the msg.sender is approved - */ - function _lockApproved(uint256 id) internal { - state[id]=State.LOCKED_APPROVED; - emit LockApproved(id); - } - - /** - * @dev External function to unlock the token. Verifies if the msg.sender is approved - */ - function unlockApproved(uint256 id) external virtual override{ - require(isApprovedForAll(ownerOf(id), msg.sender) || getApproved(id) == msg.sender, "ERC7066 : Required approval"); - require(state[id]!=State.LOCKED, "ERC7066 : Locked by locker"); - require(state[id]!=State.UNLOCKED, "ERC7066 : Unlocked"); - _unlockApproved(id); - } - - /** - * @dev Internal function to unlock the token. Verifies if the msg.sender is approved + * @dev Internal function to unlock the token. Verifies if the msg.sender is locker */ - function _unlockApproved(uint256 id) internal{ + function unlockLocker(uint256 id) internal { + require(locker[id]==msg.sender,"ERC7066: Locker Required"); state[id]=State.UNLOCKED; - emit UnlockApproved(id); + emit Unlock(id); } /** - * @dev External function to tranfer and update locker for the token. Verifies if the msg.sender is owner - */ - function transferAndLock(uint256 id, address from, address to, address _locker) external virtual override{ - _transferAndLock(id,from,to,_locker); + * @dev Public function to tranfer and lock the token. Verifies if the msg.sender is locker or approver + * reverts otherwise + */ + function transferAndLock(uint256 id, address from, address to, address operator) public virtual override{ + if(isApprovedForAll(ownerOf(id), msg.sender) || getApproved(id) == msg.sender){ + transferApprove(id,from,to,operator); + }else if(msg.sender==locker[id]){ + transferLocker(id,from,to,operator); + }else{ + revert("ERC7066: Required locker or approve"); + } } /** - * @dev Internal function to tranfer and update locker for the token. Verifies if the msg.sender is owner + * @dev Internal function to tranfer, update locker and lock the token. */ - function _transferAndLock(uint256 id, address from, address to, address _locker) internal { + function transferLocker(uint256 id, address from, address to, address operator) internal { transferFrom(from, to, id); - _setLocker(id,_locker); - } - - /** - * @dev External function to tranfer, update locker and approve locker for the token. Verifies if the msg.sender is owner - */ - function transferAndApprove(uint256 id, address from, address to, address _approver) external virtual override{ - _transferAndApprove(id,from,to,_approver); + _setLocker(id,operator); + lockLocker(id); } /** - * @dev Internal function to tranfer, update locker and approve locker for the token. Verifies if the msg.sender is owner + * @dev Internal function to tranfer, update approve and lock the token. */ - function _transferAndApprove(uint256 id, address from, address to, address _approver) internal { + function transferApprove(uint256 id, address from, address to, address operator) internal { transferFrom(from, to, id); - _approve(_approver, id); + _approve(operator, id); + lockApprove(id); } /*/////////////////////////////////////////////////////////////// diff --git a/assets/eip-7066/IERC7066.sol b/assets/eip-7066/IERC7066.sol index 6c5bceb64daf7..c75bf1d37db4a 100644 --- a/assets/eip-7066/IERC7066.sol +++ b/assets/eip-7066/IERC7066.sol @@ -19,26 +19,15 @@ interface IERC7066{ event RemoveLocker (uint256 indexed id); /** - * @dev Emitted when `id` token is locked by `locker` + * @dev Emitted when `id` token is locked */ event Lock (uint256 indexed id); /** - * @dev Emitted when `id` token is unlocked by `locker` + * @dev Emitted when `id` token is unlocked */ event Unlock (uint256 indexed id); - /** - * @dev Emitted when `id` token is locked by `approved` - */ - event LockApproved (uint256 indexed id); - - /** - * @dev Emitted when `id` token is unlocked by `approved` - */ - event UnlockApproved (uint256 indexed id); - - /** * @dev Gives the `_locker` address permission to lock if msg.sender is owner */ @@ -50,39 +39,24 @@ interface IERC7066{ function removeLocker(uint256 id) external; /** - * @dev Lock the token `id` if msg.sender is locker + * @dev Lock the token `id` if msg.sender is locker or approved */ function lock(uint256 id) external; /** - * @dev Unlocks the token `id` if msg.sender is locker + * @dev Unlocks the token `id` if msg.sender is locker or approved */ function unlock(uint256 id) external; /** - * @dev Lock the token `id` if msg.sender is approved - */ - function lockApproved(uint256 id) external; - - /** - * @dev Unlock the token `id` if msg.sender is approved - */ - function unlockApproved(uint256 id) external; - - /** - * @dev Tranfer and update locker for the token if the msg.sender is owner + * @dev Tranfer and lock the token if the msg.sender is locker or approved */ function transferAndLock(uint256 id, address from, address to, address _locker) external; /** - * @dev Tranfer, update locker and approve locker for the token if the msg.sender is owner - */ - function transferAndApprove(uint256 id, address from, address to, address _approver) external; - - /** - * @dev Returns the wallet, that is stated as unlocking wallet for the `tokenId` token. + * @dev Returns the wallet, that is stated as unlocking wallet for the `id` token. * If address(0) returned, that means token is not locked. Any other result means token is locked. */ - function lockerOf(uint256 tokenId) external view returns (address); + function lockerOf(uint256 id) external view returns (address); } \ No newline at end of file From 3dc34658420bf60b25942e7f90fb49c14d9867f5 Mon Sep 17 00:00:00 2001 From: "piyush.chittara" Date: Fri, 9 Jun 2023 13:05:01 +0530 Subject: [PATCH 27/44] readme with new functions --- EIPS/eip-7066.md | 51 ++++++++++++------------------------------------ 1 file changed, 13 insertions(+), 38 deletions(-) diff --git a/EIPS/eip-7066.md b/EIPS/eip-7066.md index 678ecd213114a..836e7ceb856b4 100644 --- a/EIPS/eip-7066.md +++ b/EIPS/eip-7066.md @@ -56,13 +56,15 @@ The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "S [ERC-721](./eip-721.md) compliant contracts MAY implement this EIP to provide standard methods of locking and unlocking the token at its current owner address. If the token is `locked`, the `getLocked` function MUST return an address that is able to `unlock` the token. For tokens that are not `locked`, the getLocked function MUST return `address(0)`. -Token owner MAY set `locker` to any address. Token MAY be locked by `locker` which MUST change the state from `UNLOCKED` to `LOCKED`. `lock` function MUST revert if token is not `UNLOCKED`. `unlock` function MUST revert if token is not `LOCKED`. `unlock` function MUST change state back to `UNLOCKED`. +Token owner MAY set `locker` to any address. Token MAY be `locked` by `locker` using `lock` function which MUST change the state from `UNLOCKED` to `LOCKED`. `locker` MAY `unlock` token by using `unlock` funtion which MUST change the state from `LOCKED` to `UNLOCKED`. -Token owner MAY set approval to any address. Token MAY be `locked` by `approver` which MUST change the state from `UNLOCKED` to `LOCKED_APPROVED`. `lockApproved` function MUST revert if token is not `UNLOCKED`. `unlockApproved` function MUST revert if token is not `LOCKED_APPROVED`. `unlockApproved` function MUST change state back to `UNLOCKED`. +Token owner MAY set `approval` to any address. Token MAY be `locked` by `approver` using `lock` function which MUST change the state from `UNLOCKED` to `LOCKED_APPROVED`. `approver` MAY `unlock` token by using `unlock` funtion which MUST change the state from `LOCKED_APPROVED` to `UNLOCKED`. -`approve` function MUST revert if token is not `UNLOCKED`. `_tansfer` function MUST revert if token state is `LOCKED`. `_transfer` transaction MUST pass if state is `LOCKED_APPROVED` and `msg.sender` is `approved`, else MUST revert otherwise. +`lock` function MUST revert if token is not `UNLOCKED`. `unlock` function MUST revert if token is not `LOCKED` or `LOCKED_APPROVED`. `unlock` function MUST change state back to `UNLOCKED`. -NFT can be transfered, with updating `locker` privileges using `transferAndLock` function. NFT can be transfered, with updating `approve` privileges using `transferAndApprove` function. +`approve` function MUST revert if token is not `UNLOCKED`. `_tansfer` function MUST revert if token state is `LOCKED`. `_transfer` transaction MUST pass if state is `LOCKED_APPROVED` and `msg.sender` is `approved`, MUST revert otherwise. + +Token MAY be transfered and `locked` while retaining the `locker` or `approval` role using `transferAndLock` function. This is RECOMMENDED for usecases where Token revocation is REQUIRED. ### Interface @@ -80,26 +82,15 @@ interface IERC7066{ event RemoveLocker (uint256 indexed id); /** - * @dev Emitted when `id` token is locked by `locker` + * @dev Emitted when `id` token is locked */ event Lock (uint256 indexed id); /** - * @dev Emitted when `id` token is unlocked by `locker` + * @dev Emitted when `id` token is unlocked */ event Unlock (uint256 indexed id); - /** - * @dev Emitted when `id` token is locked by `approved` - */ - event LockApproved (uint256 indexed id); - - /** - * @dev Emitted when `id` token is unlocked by `approved` - */ - event UnlockApproved (uint256 indexed id); - - /** * @dev Gives the `_locker` address permission to lock if msg.sender is owner */ @@ -111,41 +102,25 @@ interface IERC7066{ function removeLocker(uint256 id) external; /** - * @dev Lock the token `id` if msg.sender is locker + * @dev Lock the token `id` if msg.sender is locker or approved */ function lock(uint256 id) external; /** - * @dev Unlocks the token `id` if msg.sender is locker + * @dev Unlocks the token `id` if msg.sender is locker or approved */ function unlock(uint256 id) external; /** - * @dev Lock the token `id` if msg.sender is approved - */ - function lockApproved(uint256 id) external; - - /** - * @dev Unlock the token `id` if msg.sender is approved - */ - function unlockApproved(uint256 id) external; - - /** - * @dev Tranfer and update locker for the token if the msg.sender is owner + * @dev Tranfer and lock the token if the msg.sender is locker or approved */ function transferAndLock(uint256 id, address from, address to, address _locker) external; /** - * @dev Tranfer, update locker and approve locker for the token if the msg.sender is owner - */ - function transferAndApprove(uint256 id, address from, address to, address _approver) external; - - /** - * @dev Returns the wallet, that is stated as unlocking wallet for the `tokenId` token. + * @dev Returns the wallet, that is stated as unlocking wallet for the `id` token. * If address(0) returned, that means token is not locked. Any other result means token is locked. */ - function lockerOf(uint256 tokenId) external view returns (address); - + function lockerOf(uint256 id) external view returns (address); } ``` From 94992135ebc8f36d52d7dd49e438db82d79c2955 Mon Sep 17 00:00:00 2001 From: "piyush.chittara" Date: Fri, 9 Jun 2023 13:06:08 +0530 Subject: [PATCH 28/44] readme update --- EIPS/eip-7066.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-7066.md b/EIPS/eip-7066.md index 836e7ceb856b4..3ad3416e2f947 100644 --- a/EIPS/eip-7066.md +++ b/EIPS/eip-7066.md @@ -13,7 +13,7 @@ requires: 165, 721 ## Abstract -An extension of [ERC-721](./eip-721.md), this standard incorporates `locking` features into NFTs, allowing for various uses while preventing sale or transfer. The token's owner or operator has the ability to lock it, specifying an unlocker address (either an EOA or a contract) that exclusively holds the power to unlock the token. Owner can also provide approval for token-id, enabling ability to lock asset while address holds the token approval. Upon token transfer these rights get purged. +An extension of [ERC-721](./eip-721.md), this standard incorporates `locking` features into NFTs, allowing for various uses while preventing sale or transfer. The token's owner or operator has the ability to lock it, specifying an unlocker address (either an EOA or a contract) that exclusively holds the power to unlock the token. Owner can also provide approval for token-id, enabling ability to lock asset while address holds the token approval. Upon token transfer, these rights get purged. ## Motivation From 44170bb1fc66e4a1aa245a0044ea5f109bcd2366 Mon Sep 17 00:00:00 2001 From: "piyush.chittara" Date: Fri, 9 Jun 2023 13:38:46 +0530 Subject: [PATCH 29/44] error codes --- assets/eip-7066/ERC7066.sol | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/assets/eip-7066/ERC7066.sol b/assets/eip-7066/ERC7066.sol index b8a24138c68df..00c1298ab7757 100644 --- a/assets/eip-7066/ERC7066.sol +++ b/assets/eip-7066/ERC7066.sol @@ -32,8 +32,8 @@ abstract contract ERC7066 is ERC721,IERC7066{ * and allows setting locker for tokenid */ function setLocker(uint256 id, address _locker) public virtual override { - require(msg.sender==ownerOf(id), "ERC7066 : Owner Required"); - require(state[id]==State.UNLOCKED, "ERC7066 : Locked"); + require(msg.sender==ownerOf(id), "ERC7066: Required Owner"); + require(state[id]==State.UNLOCKED, "ERC7066: Locked"); _setLocker(id,_locker); } @@ -50,8 +50,8 @@ abstract contract ERC7066 is ERC721,IERC7066{ * and allows removal of locker for tokenid if token is unlocked */ function removeLocker(uint256 id) public virtual override { - require(msg.sender==ownerOf(id), "ERC7066 : Owner Required"); - require(state[id]==State.UNLOCKED, "ERC7066 : Locked"); + require(msg.sender==ownerOf(id), "ERC7066: Required Owner"); + require(state[id]==State.UNLOCKED, "ERC7066: Locked"); _removeLocker(id); } @@ -78,7 +78,7 @@ abstract contract ERC7066 is ERC721,IERC7066{ * reverts otherwise */ function lock(uint256 id) public virtual override{ - require(state[id]==State.UNLOCKED, "ERC7066 : Locked"); + require(state[id]==State.UNLOCKED, "ERC7066: Locked"); if(isApprovedForAll(ownerOf(id), msg.sender) || getApproved(id)== msg.sender){ lockApprove(id); } else if (msg.sender==locker[id]){ @@ -109,7 +109,7 @@ abstract contract ERC7066 is ERC721,IERC7066{ * reverts otherwise */ function unlock(uint256 id) public virtual override{ - require(state[id]!=State.UNLOCKED, "ERC7066 : Unlocked"); + require(state[id]!=State.UNLOCKED, "ERC7066: Unlocked"); if(state[id]==State.LOCKED_APPROVED){ unlockApprove(id); }else if(state[id]==State.LOCKED){ @@ -123,7 +123,7 @@ abstract contract ERC7066 is ERC721,IERC7066{ * @dev Internal function to unlock the token. Verifies if the msg.sender is approved */ function unlockApprove(uint256 id) internal{ - require(isApprovedForAll(ownerOf(id), msg.sender) || getApproved(id)== msg.sender,"ERC7066: Approve Required"); + require(isApprovedForAll(ownerOf(id), msg.sender) || getApproved(id)== msg.sender,"ERC7066: Required Approve"); state[id]=State.UNLOCKED; emit Unlock(id); } @@ -132,7 +132,7 @@ abstract contract ERC7066 is ERC721,IERC7066{ * @dev Internal function to unlock the token. Verifies if the msg.sender is locker */ function unlockLocker(uint256 id) internal { - require(locker[id]==msg.sender,"ERC7066: Locker Required"); + require(locker[id]==msg.sender,"ERC7066: Required Locker"); state[id]=State.UNLOCKED; emit Unlock(id); } @@ -177,7 +177,7 @@ abstract contract ERC7066 is ERC721,IERC7066{ * @dev Override approve to make sure token is unlocked */ function approve(address to, uint256 tokenId) public virtual override { - require (state[tokenId]==State.UNLOCKED, "ERC7066 : Locked"); // so the unlocker stays approved + require (state[tokenId]==State.UNLOCKED, "ERC7066: Locked"); // so the unlocker stays approved super.approve(to, tokenId); } @@ -193,9 +193,9 @@ abstract contract ERC7066 is ERC721,IERC7066{ ) internal virtual override { // if it is a Transfer or Burn, we always deal with one token, that is startTokenId if (from != address(0)) { - require(state[startTokenId]!=State.LOCKED,"ERC7066 : Locked"); + require(state[startTokenId]!=State.LOCKED,"ERC7066: Locked"); require(state[startTokenId]==State.UNLOCKED || isApprovedForAll(ownerOf(startTokenId), msg.sender) - || getApproved(startTokenId) == msg.sender, "ERC7066 : Required approval"); + || getApproved(startTokenId) == msg.sender, "ERC7066: Required Approved"); } super._beforeTokenTransfer(from,to,startTokenId,quantity); } From deee3f871136cc29676dae87849c3b7782e32b65 Mon Sep 17 00:00:00 2001 From: "piyush.chittara" Date: Fri, 9 Jun 2023 13:44:44 +0530 Subject: [PATCH 30/44] discussion link --- EIPS/eip-7066.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-7066.md b/EIPS/eip-7066.md index 3ad3416e2f947..9827068b50e02 100644 --- a/EIPS/eip-7066.md +++ b/EIPS/eip-7066.md @@ -3,7 +3,7 @@ eip: 7066 title: ERC-721 Lockable description: Interface for enabling locking of ERC-721 using locker and approver author: Piyush Chittara (@streamnft-tech) -discussions-to: https://ethereum-magicians.org/t/erc721-lockable/14425 +discussions-to: https://ethereum-magicians.org/t/eip-7066-lockable-extension-for-erc721/14425 status: Draft type: Standards Track category: ERC From 452b536494edc0df0e37328e606e3ac822288711 Mon Sep 17 00:00:00 2001 From: "piyush.chittara" Date: Sat, 10 Jun 2023 14:48:22 +0530 Subject: [PATCH 31/44] tokenId --- EIPS/eip-7066.md | 39 ++++++------ assets/eip-7066/ERC7066.sol | 116 +++++++++++++++++------------------ assets/eip-7066/IERC7066.sol | 36 +++++------ 3 files changed, 96 insertions(+), 95 deletions(-) diff --git a/EIPS/eip-7066.md b/EIPS/eip-7066.md index 9827068b50e02..f10da82632ecf 100644 --- a/EIPS/eip-7066.md +++ b/EIPS/eip-7066.md @@ -13,7 +13,7 @@ requires: 165, 721 ## Abstract -An extension of [ERC-721](./eip-721.md), this standard incorporates `locking` features into NFTs, allowing for various uses while preventing sale or transfer. The token's owner or operator has the ability to lock it, specifying an unlocker address (either an EOA or a contract) that exclusively holds the power to unlock the token. Owner can also provide approval for token-id, enabling ability to lock asset while address holds the token approval. Upon token transfer, these rights get purged. +An extension of [ERC-721](./eip-721.md), this standard incorporates `locking` features into NFTs, allowing for various uses while preventing sale or transfer. The token's owner or operator has the ability to lock it, specifying an unlocker address (either an EOA or a contract) that exclusively holds the power to unlock the token. Owner can also provide approval for tokenId, enabling ability to lock asset while address holds the token approval. Upon token transfer, these rights get purged. ## Motivation @@ -72,55 +72,56 @@ Token MAY be transfered and `locked` while retaining the `locker` or `approval` interface IERC7066{ /** - * @dev Emitted when locker is set for token `id` + * @dev Emitted when locker is set for tokenId */ - event SetLocker (uint256 indexed id, address _locker); + event SetLocker (uint256 indexed tokenId, address _locker); /** - * @dev Emitted when locker is removed for token `id` + * @dev Emitted when locker is removed for tokenId */ - event RemoveLocker (uint256 indexed id); + event RemoveLocker (uint256 indexed tokenId); /** - * @dev Emitted when `id` token is locked + * @dev Emitted when tokenId is locked */ - event Lock (uint256 indexed id); + event Lock (uint256 indexed tokenId); /** - * @dev Emitted when `id` token is unlocked + * @dev Emitted when tokenId is unlocked */ - event Unlock (uint256 indexed id); + event Unlock (uint256 indexed tokenId); /** * @dev Gives the `_locker` address permission to lock if msg.sender is owner */ - function setLocker(uint256 id, address _locker) external; + function setLocker(uint256 tokenId, address _locker) external; /** - * @dev Purge the permission to lock if `id` is `unlocked` and msg.sender is owner + * @dev Purge the permission to lock if tokenId is `unlocked` and msg.sender is owner */ - function removeLocker(uint256 id) external; + function removeLocker(uint256 tokenId) external; /** - * @dev Lock the token `id` if msg.sender is locker or approved + * @dev Lock the tokenId if msg.sender is locker or approved */ - function lock(uint256 id) external; + function lock(uint256 tokenId) external; /** - * @dev Unlocks the token `id` if msg.sender is locker or approved + * @dev Unlocks the tokenId if msg.sender is locker or approved */ - function unlock(uint256 id) external; + function unlock(uint256 tokenId) external; /** * @dev Tranfer and lock the token if the msg.sender is locker or approved */ - function transferAndLock(uint256 id, address from, address to, address _locker) external; + function transferAndLock(uint256 tokenId, address from, address to, address _locker) external; /** - * @dev Returns the wallet, that is stated as unlocking wallet for the `id` token. + * @dev Returns the wallet, that is stated as unlocking wallet for the tokenId. * If address(0) returned, that means token is not locked. Any other result means token is locked. */ - function lockerOf(uint256 id) external view returns (address); + function lockerOf(uint256 tokenId) external view returns (address); + } ``` diff --git a/assets/eip-7066/ERC7066.sol b/assets/eip-7066/ERC7066.sol index 00c1298ab7757..4df7e34ddf123 100644 --- a/assets/eip-7066/ERC7066.sol +++ b/assets/eip-7066/ERC7066.sol @@ -16,9 +16,9 @@ abstract contract ERC7066 is ERC721,IERC7066{ ERC7066 EXTENSION STORAGE //////////////////////////////////////////////////////////////*/ - //Mapping from token-id to user address for locking permission + //Mapping from tokenId to user address for locking permission mapping(uint256 => address) internal locker; - //Mapping from token-id to state of token + //Mapping from tokenId to state of token mapping(uint256 => State) internal state; //Possible states of a token enum State{UNLOCKED,LOCKED,LOCKED_APPROVED} @@ -31,36 +31,36 @@ abstract contract ERC7066 is ERC721,IERC7066{ * @dev Public function to set locker. Verifies if the msg.sender is the owner * and allows setting locker for tokenid */ - function setLocker(uint256 id, address _locker) public virtual override { - require(msg.sender==ownerOf(id), "ERC7066: Required Owner"); - require(state[id]==State.UNLOCKED, "ERC7066: Locked"); - _setLocker(id,_locker); + function setLocker(uint256 tokenId, address _locker) public virtual override { + require(msg.sender==ownerOf(tokenId), "ERC7066: Required Owner"); + require(state[tokenId]==State.UNLOCKED, "ERC7066: Locked"); + _setLocker(tokenId,_locker); } /** * @dev Internal function to set locker. */ - function _setLocker(uint256 id, address _locker) internal { - locker[id]=_locker; - emit SetLocker(id, _locker); + function _setLocker(uint256 tokenId, address _locker) internal { + locker[tokenId]=_locker; + emit SetLocker(tokenId, _locker); } /** * @dev Public function to remove locker. Verifies if the msg.sender is the owner * and allows removal of locker for tokenid if token is unlocked */ - function removeLocker(uint256 id) public virtual override { - require(msg.sender==ownerOf(id), "ERC7066: Required Owner"); - require(state[id]==State.UNLOCKED, "ERC7066: Locked"); - _removeLocker(id); + function removeLocker(uint256 tokenId) public virtual override { + require(msg.sender==ownerOf(tokenId), "ERC7066: Required Owner"); + require(state[tokenId]==State.UNLOCKED, "ERC7066: Locked"); + _removeLocker(tokenId); } /** * @dev Internal function to remove locker. */ - function _removeLocker(uint256 id) internal { - delete locker[id]; - emit RemoveLocker(id); + function _removeLocker(uint256 tokenId) internal { + delete locker[tokenId]; + emit RemoveLocker(tokenId); } /** @@ -68,21 +68,21 @@ abstract contract ERC7066 is ERC721,IERC7066{ * address(0) means token is not locked * reverts if token does not exist */ - function lockerOf(uint256 id) public virtual view override returns(address){ - require(_exists(id), "ERC7066: Nonexistent token"); - return locker[id]; + function lockerOf(uint256 tokenId) public virtual view override returns(address){ + require(_exists(tokenId), "ERC7066: Nonexistent token"); + return locker[tokenId]; } /** * @dev Public function to lock the token. Verifies if the msg.sender is locker or approver * reverts otherwise */ - function lock(uint256 id) public virtual override{ - require(state[id]==State.UNLOCKED, "ERC7066: Locked"); - if(isApprovedForAll(ownerOf(id), msg.sender) || getApproved(id)== msg.sender){ - lockApprove(id); - } else if (msg.sender==locker[id]){ - lockLocker(id); + function lock(uint256 tokenId) public virtual override{ + require(state[tokenId]==State.UNLOCKED, "ERC7066: Locked"); + if(isApprovedForAll(ownerOf(tokenId), msg.sender) || getApproved(tokenId)== msg.sender){ + lockApprove(tokenId); + } else if (msg.sender==locker[tokenId]){ + lockLocker(tokenId); } else{ revert("ERC7066: Required locker or approve"); } @@ -91,29 +91,29 @@ abstract contract ERC7066 is ERC721,IERC7066{ /** * @dev Internal function to lock the token. Verifies if the msg.sender is approved */ - function lockApprove(uint256 id) internal { - state[id]=State.LOCKED_APPROVED; - emit Lock(id); + function lockApprove(uint256 tokenId) internal { + state[tokenId]=State.LOCKED_APPROVED; + emit Lock(tokenId); } /** * @dev Internal function to lock the token. Verifies if the msg.sender is locker */ - function lockLocker(uint256 id) internal { - state[id]=State.LOCKED; - emit Lock(id); + function lockLocker(uint256 tokenId) internal { + state[tokenId]=State.LOCKED; + emit Lock(tokenId); } /** * @dev External function to unlock the token. Verifies the msg.sender is locker or approver * reverts otherwise */ - function unlock(uint256 id) public virtual override{ - require(state[id]!=State.UNLOCKED, "ERC7066: Unlocked"); - if(state[id]==State.LOCKED_APPROVED){ - unlockApprove(id); - }else if(state[id]==State.LOCKED){ - unlockLocker(id); + function unlock(uint256 tokenId) public virtual override{ + require(state[tokenId]!=State.UNLOCKED, "ERC7066: Unlocked"); + if(state[tokenId]==State.LOCKED_APPROVED){ + unlockApprove(tokenId); + }else if(state[tokenId]==State.LOCKED){ + unlockLocker(tokenId); }else{ revert("ERC7066: Required locker or approve"); } @@ -122,30 +122,30 @@ abstract contract ERC7066 is ERC721,IERC7066{ /** * @dev Internal function to unlock the token. Verifies if the msg.sender is approved */ - function unlockApprove(uint256 id) internal{ - require(isApprovedForAll(ownerOf(id), msg.sender) || getApproved(id)== msg.sender,"ERC7066: Required Approve"); - state[id]=State.UNLOCKED; - emit Unlock(id); + function unlockApprove(uint256 tokenId) internal{ + require(isApprovedForAll(ownerOf(tokenId), msg.sender) || getApproved(tokenId)== msg.sender,"ERC7066: Required Approve"); + state[tokenId]=State.UNLOCKED; + emit Unlock(tokenId); } /** * @dev Internal function to unlock the token. Verifies if the msg.sender is locker */ - function unlockLocker(uint256 id) internal { - require(locker[id]==msg.sender,"ERC7066: Required Locker"); - state[id]=State.UNLOCKED; - emit Unlock(id); + function unlockLocker(uint256 tokenId) internal { + require(locker[tokenId]==msg.sender,"ERC7066: Required Locker"); + state[tokenId]=State.UNLOCKED; + emit Unlock(tokenId); } /** * @dev Public function to tranfer and lock the token. Verifies if the msg.sender is locker or approver * reverts otherwise */ - function transferAndLock(uint256 id, address from, address to, address operator) public virtual override{ - if(isApprovedForAll(ownerOf(id), msg.sender) || getApproved(id) == msg.sender){ - transferApprove(id,from,to,operator); - }else if(msg.sender==locker[id]){ - transferLocker(id,from,to,operator); + function transferAndLock(uint256 tokenId, address from, address to, address operator) public virtual override{ + if(isApprovedForAll(ownerOf(tokenId), msg.sender) || getApproved(tokenId) == msg.sender){ + transferApprove(tokenId,from,to,operator); + }else if(msg.sender==locker[tokenId]){ + transferLocker(tokenId,from,to,operator); }else{ revert("ERC7066: Required locker or approve"); } @@ -154,19 +154,19 @@ abstract contract ERC7066 is ERC721,IERC7066{ /** * @dev Internal function to tranfer, update locker and lock the token. */ - function transferLocker(uint256 id, address from, address to, address operator) internal { - transferFrom(from, to, id); - _setLocker(id,operator); - lockLocker(id); + function transferLocker(uint256 tokenId, address from, address to, address operator) internal { + transferFrom(from, to, tokenId); + _setLocker(tokenId,operator); + lockLocker(tokenId); } /** * @dev Internal function to tranfer, update approve and lock the token. */ - function transferApprove(uint256 id, address from, address to, address operator) internal { - transferFrom(from, to, id); - _approve(operator, id); - lockApprove(id); + function transferApprove(uint256 tokenId, address from, address to, address operator) internal { + transferFrom(from, to, tokenId); + _approve(operator, tokenId); + lockApprove(tokenId); } /*/////////////////////////////////////////////////////////////// diff --git a/assets/eip-7066/IERC7066.sol b/assets/eip-7066/IERC7066.sol index c75bf1d37db4a..7d61c48c68f51 100644 --- a/assets/eip-7066/IERC7066.sol +++ b/assets/eip-7066/IERC7066.sol @@ -9,54 +9,54 @@ pragma solidity >=0.7.0 <0.9.0; interface IERC7066{ /** - * @dev Emitted when locker is set for token `id` + * @dev Emitted when locker is set for tokenId */ - event SetLocker (uint256 indexed id, address _locker); + event SetLocker (uint256 indexed tokenId, address _locker); /** - * @dev Emitted when locker is removed for token `id` + * @dev Emitted when locker is removed for tokenId */ - event RemoveLocker (uint256 indexed id); + event RemoveLocker (uint256 indexed tokenId); /** - * @dev Emitted when `id` token is locked + * @dev Emitted when tokenId is locked */ - event Lock (uint256 indexed id); + event Lock (uint256 indexed tokenId); /** - * @dev Emitted when `id` token is unlocked + * @dev Emitted when tokenId is unlocked */ - event Unlock (uint256 indexed id); + event Unlock (uint256 indexed tokenId); /** * @dev Gives the `_locker` address permission to lock if msg.sender is owner */ - function setLocker(uint256 id, address _locker) external; + function setLocker(uint256 tokenId, address _locker) external; /** - * @dev Purge the permission to lock if `id` is `unlocked` and msg.sender is owner + * @dev Purge the permission to lock if tokenId is `unlocked` and msg.sender is owner */ - function removeLocker(uint256 id) external; + function removeLocker(uint256 tokenId) external; /** - * @dev Lock the token `id` if msg.sender is locker or approved + * @dev Lock the tokenId if msg.sender is locker or approved */ - function lock(uint256 id) external; + function lock(uint256 tokenId) external; /** - * @dev Unlocks the token `id` if msg.sender is locker or approved + * @dev Unlocks the tokenId if msg.sender is locker or approved */ - function unlock(uint256 id) external; + function unlock(uint256 tokenId) external; /** * @dev Tranfer and lock the token if the msg.sender is locker or approved */ - function transferAndLock(uint256 id, address from, address to, address _locker) external; + function transferAndLock(uint256 tokenId, address from, address to, address _locker) external; /** - * @dev Returns the wallet, that is stated as unlocking wallet for the `id` token. + * @dev Returns the wallet, that is stated as unlocking wallet for the tokenId. * If address(0) returned, that means token is not locked. Any other result means token is locked. */ - function lockerOf(uint256 id) external view returns (address); + function lockerOf(uint256 tokenId) external view returns (address); } \ No newline at end of file From 95609b460c44462f7ccfe856ba3f61f5128b69df Mon Sep 17 00:00:00 2001 From: Piyush Date: Tue, 13 Jun 2023 10:49:01 +0530 Subject: [PATCH 32/44] remove redundancy --- EIPS/eip-7066.md | 71 +++++++---------- assets/eip-7066/ERC7066.sol | 150 +++++++++-------------------------- assets/eip-7066/IERC7066.sol | 38 +++------ 3 files changed, 81 insertions(+), 178 deletions(-) diff --git a/EIPS/eip-7066.md b/EIPS/eip-7066.md index f10da82632ecf..3f036a4949dbd 100644 --- a/EIPS/eip-7066.md +++ b/EIPS/eip-7066.md @@ -1,8 +1,8 @@ --- eip: 7066 -title: ERC-721 Lockable -description: Interface for enabling locking of ERC-721 using locker and approver -author: Piyush Chittara (@streamnft-tech) +title: Lockable Extension for ERC721 +description: Interface for enabling locking of ERC-721 using locker and approved +author: Piyush Chittara (@streamnft-tech), Srinivas Joshi (@SrinivasJoshi) discussions-to: https://ethereum-magicians.org/t/eip-7066-lockable-extension-for-erc721/14425 status: Draft type: Standards Track @@ -54,37 +54,32 @@ The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "S ### Overview -[ERC-721](./eip-721.md) compliant contracts MAY implement this EIP to provide standard methods of locking and unlocking the token at its current owner address. If the token is `locked`, the `getLocked` function MUST return an address that is able to `unlock` the token. For tokens that are not `locked`, the getLocked function MUST return `address(0)`. +[ERC-721](./eip-721.md) compliant contracts MAY implement this EIP to provide standard methods of locking and unlocking the token at its current owner address. Token owner MAY `lock` the token and assign `locker` to some `address` using `lock(uint256 tokenId, address _locker)` function, this MUST set `locker` to `_locker`. Token owner or approved MAY `lock` the token using `lock(uint256 tokenId)` function, this MUST set `locker` to `msg.sender`. Token MAY be `unlocked` by `locker` using `unlock` function. `unlock` function MUST delete `locker` mapping and default to `address(0)`. -Token owner MAY set `locker` to any address. Token MAY be `locked` by `locker` using `lock` function which MUST change the state from `UNLOCKED` to `LOCKED`. `locker` MAY `unlock` token by using `unlock` funtion which MUST change the state from `LOCKED` to `UNLOCKED`. +If the token is `locked`, the `lockerOf` function MUST return an address that is `locker` and can `unlock` the token. For tokens that are not `locked`, the `lockerOf` function MUST return `address(0)`. -Token owner MAY set `approval` to any address. Token MAY be `locked` by `approver` using `lock` function which MUST change the state from `UNLOCKED` to `LOCKED_APPROVED`. `approver` MAY `unlock` token by using `unlock` funtion which MUST change the state from `LOCKED_APPROVED` to `UNLOCKED`. +`lock` function MUST revert if token is not already locked. `unlock` function MUST revert if token is not locked. `approve` function MUST revert if token is locked. `_tansfer` function MUST revert if token is locked. `_transfer` transaction MUST pass if token is locked and `msg.sender` is `approved` and `locker` both. After `_transfer` values of `locker` and `approved` MUST +get purged. -`lock` function MUST revert if token is not `UNLOCKED`. `unlock` function MUST revert if token is not `LOCKED` or `LOCKED_APPROVED`. `unlock` function MUST change state back to `UNLOCKED`. - -`approve` function MUST revert if token is not `UNLOCKED`. `_tansfer` function MUST revert if token state is `LOCKED`. `_transfer` transaction MUST pass if state is `LOCKED_APPROVED` and `msg.sender` is `approved`, MUST revert otherwise. - -Token MAY be transfered and `locked` while retaining the `locker` or `approval` role using `transferAndLock` function. This is RECOMMENDED for usecases where Token revocation is REQUIRED. +Token MAY be transfered and `locked`, and OPTIONAL retain the value of `approval` using `transferAndLock` function. This is RECOMMENDED for usecases where Token transfer and subsequent revocation is REQUIRED. ### Interface ``` -interface IERC7066{ - - /** - * @dev Emitted when locker is set for tokenId - */ - event SetLocker (uint256 indexed tokenId, address _locker); +// SPDX-License-Identifier: GPL-3.0 - /** - * @dev Emitted when locker is removed for tokenId - */ - event RemoveLocker (uint256 indexed tokenId); +pragma solidity >=0.7.0 <0.9.0; + +/// @title Lockable Extension for ERC721 +/// @dev Interface for the Lockable extension +/// @author StreamNFT + +interface IERC7066{ /** * @dev Emitted when tokenId is locked */ - event Lock (uint256 indexed tokenId); + event Lock (uint256 indexed tokenId, address _locker); /** * @dev Emitted when tokenId is unlocked @@ -92,36 +87,32 @@ interface IERC7066{ event Unlock (uint256 indexed tokenId); /** - * @dev Gives the `_locker` address permission to lock if msg.sender is owner + * @dev Lock the tokenId if msg.sender is owner or approved and set locker to msg.sender */ - function setLocker(uint256 tokenId, address _locker) external; + function lock(uint256 tokenId) external; /** - * @dev Purge the permission to lock if tokenId is `unlocked` and msg.sender is owner - */ - function removeLocker(uint256 tokenId) external; - - /** - * @dev Lock the tokenId if msg.sender is locker or approved + * @dev Lock the tokenId if msg.sender is owner and set locker to _locker */ - function lock(uint256 tokenId) external; + function lock(uint256 tokenId, address _locker) external; /** - * @dev Unlocks the tokenId if msg.sender is locker or approved + * @dev Unlocks the tokenId if msg.sender is locker */ function unlock(uint256 tokenId) external; /** - * @dev Tranfer and lock the token if the msg.sender is locker or approved + * @dev Tranfer and lock the token if the msg.sender is owner or approved. + * Lock the token and set locker to caller + * Optionally approve caller if bool setApprove flag is true */ - function transferAndLock(uint256 tokenId, address from, address to, address _locker) external; + function transferAndLock(uint256 tokenId, address from, address to, bool setApprove) external; /** * @dev Returns the wallet, that is stated as unlocking wallet for the tokenId. - * If address(0) returned, that means token is not locked. Any other result means token is locked. + * If address(0) returned, that means token is not locked. Any other result means token is locked. */ function lockerOf(uint256 tokenId) external view returns (address); - } ``` @@ -155,11 +146,9 @@ Reference Implementation can be found [here](../assets/eip-7066/ERC7066.sol). There are no security considerations related directly to the implementation of this standard for the contract that manages [ERC-721](./eip-721.md). ### Considerations for the contracts that work with lockable tokens - -- Make sure that every contract that is stated as `locker` can actually unlock the `LOCKED` token only. -- Make sure that the approved contract can unlock the `LOCKED_APPROVED` token only. -- `LOCKED` token with in-accesible account or un-verified contract address can lead to permanent lock of the token. -- There are use cases, that involve transferring the token to a temporary owner and then lock it. For example, NFT rentals. Smart contracts that manage such services should always use `transferFrom` instead of `safeTransferFrom` to avoid re-entrancies. +- Once `locked`, token can not be further `approved` or `transfered`. +- If token is `locked` and caller is `locker` and `appoved` both, caller can transfer the token. +- `locked` token with `locker` as in-accesible account or un-verified contract address can lead to permanent lock of the token. - There are no MEV considerations regarding lockable tokens as only authorized parties are allowed to lock and unlock. ## Copyright diff --git a/assets/eip-7066/ERC7066.sol b/assets/eip-7066/ERC7066.sol index 4df7e34ddf123..cb9a8e0ac07f3 100644 --- a/assets/eip-7066/ERC7066.sol +++ b/assets/eip-7066/ERC7066.sol @@ -2,7 +2,6 @@ pragma solidity >=0.7.0 <0.9.0; -import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "./IERC7066.sol"; /// @title ERC7066: Lockable Extension for ERC721 @@ -16,53 +15,13 @@ abstract contract ERC7066 is ERC721,IERC7066{ ERC7066 EXTENSION STORAGE //////////////////////////////////////////////////////////////*/ - //Mapping from tokenId to user address for locking permission + //Mapping from tokenId to user address for locker mapping(uint256 => address) internal locker; - //Mapping from tokenId to state of token - mapping(uint256 => State) internal state; - //Possible states of a token - enum State{UNLOCKED,LOCKED,LOCKED_APPROVED} /*/////////////////////////////////////////////////////////////// ERC7066 LOGIC //////////////////////////////////////////////////////////////*/ - /** - * @dev Public function to set locker. Verifies if the msg.sender is the owner - * and allows setting locker for tokenid - */ - function setLocker(uint256 tokenId, address _locker) public virtual override { - require(msg.sender==ownerOf(tokenId), "ERC7066: Required Owner"); - require(state[tokenId]==State.UNLOCKED, "ERC7066: Locked"); - _setLocker(tokenId,_locker); - } - - /** - * @dev Internal function to set locker. - */ - function _setLocker(uint256 tokenId, address _locker) internal { - locker[tokenId]=_locker; - emit SetLocker(tokenId, _locker); - } - - /** - * @dev Public function to remove locker. Verifies if the msg.sender is the owner - * and allows removal of locker for tokenid if token is unlocked - */ - function removeLocker(uint256 tokenId) public virtual override { - require(msg.sender==ownerOf(tokenId), "ERC7066: Required Owner"); - require(state[tokenId]==State.UNLOCKED, "ERC7066: Locked"); - _removeLocker(tokenId); - } - - /** - * @dev Internal function to remove locker. - */ - function _removeLocker(uint256 tokenId) internal { - delete locker[tokenId]; - emit RemoveLocker(tokenId); - } - /** * @dev Returns the locker for the tokenId * address(0) means token is not locked @@ -74,99 +33,69 @@ abstract contract ERC7066 is ERC721,IERC7066{ } /** - * @dev Public function to lock the token. Verifies if the msg.sender is locker or approver - * reverts otherwise + * @dev Public function to lock the token. Verifies if the msg.sender is owner or approved + * reverts otherwise */ function lock(uint256 tokenId) public virtual override{ - require(state[tokenId]==State.UNLOCKED, "ERC7066: Locked"); - if(isApprovedForAll(ownerOf(tokenId), msg.sender) || getApproved(tokenId)== msg.sender){ - lockApprove(tokenId); - } else if (msg.sender==locker[tokenId]){ - lockLocker(tokenId); - } else{ - revert("ERC7066: Required locker or approve"); - } + require(locker[tokenId]==address(0), "ERC7066: Locked"); + require(_isApprovedOrOwner(_msgSender(), tokenId), "Require owner or approved"); + _lock(tokenId,msg.sender); } /** - * @dev Internal function to lock the token. Verifies if the msg.sender is approved + * @dev Public function to lock the token. Verifies if the msg.sender is owner + * reverts otherwise */ - function lockApprove(uint256 tokenId) internal { - state[tokenId]=State.LOCKED_APPROVED; - emit Lock(tokenId); + function lock(uint256 tokenId, address _locker) public virtual override{ + require(locker[tokenId]==address(0), "ERC7066: Locked"); + require(ownerOf(tokenId)==msg.sender, "ERC7066: Require owner"); + _lock(tokenId,_locker); } /** - * @dev Internal function to lock the token. Verifies if the msg.sender is locker + * @dev Internal function to lock the token. */ - function lockLocker(uint256 tokenId) internal { - state[tokenId]=State.LOCKED; - emit Lock(tokenId); + function _lock(uint256 tokenId, address _locker) internal { + locker[tokenId]=_locker; + emit Lock(tokenId, _locker); } /** - * @dev External function to unlock the token. Verifies the msg.sender is locker or approver - * reverts otherwise + * @dev Public function to unlock the token. Verifies the msg.sender is locker + * reverts otherwise */ function unlock(uint256 tokenId) public virtual override{ - require(state[tokenId]!=State.UNLOCKED, "ERC7066: Unlocked"); - if(state[tokenId]==State.LOCKED_APPROVED){ - unlockApprove(tokenId); - }else if(state[tokenId]==State.LOCKED){ - unlockLocker(tokenId); - }else{ - revert("ERC7066: Required locker or approve"); - } - } - - /** - * @dev Internal function to unlock the token. Verifies if the msg.sender is approved - */ - function unlockApprove(uint256 tokenId) internal{ - require(isApprovedForAll(ownerOf(tokenId), msg.sender) || getApproved(tokenId)== msg.sender,"ERC7066: Required Approve"); - state[tokenId]=State.UNLOCKED; - emit Unlock(tokenId); + require(locker[tokenId]!=address(0), "ERC7066: Unlocked"); + require(locker[tokenId]==msg.sender); + _unlock(tokenId); } /** - * @dev Internal function to unlock the token. Verifies if the msg.sender is locker + * @dev Internal function to unlock the token. */ - function unlockLocker(uint256 tokenId) internal { - require(locker[tokenId]==msg.sender,"ERC7066: Required Locker"); - state[tokenId]=State.UNLOCKED; + function _unlock(uint256 tokenId) internal{ + delete locker[tokenId]; emit Unlock(tokenId); } /** - * @dev Public function to tranfer and lock the token. Verifies if the msg.sender is locker or approver - * reverts otherwise - */ - function transferAndLock(uint256 tokenId, address from, address to, address operator) public virtual override{ - if(isApprovedForAll(ownerOf(tokenId), msg.sender) || getApproved(tokenId) == msg.sender){ - transferApprove(tokenId,from,to,operator); - }else if(msg.sender==locker[tokenId]){ - transferLocker(tokenId,from,to,operator); - }else{ - revert("ERC7066: Required locker or approve"); - } - } - - /** - * @dev Internal function to tranfer, update locker and lock the token. + * @dev Public function to tranfer and lock the token. Reverts if caller is not owner or approved. + * Lock the token and set locker to caller + *. Optionally approve caller if bool setApprove flag is true */ - function transferLocker(uint256 tokenId, address from, address to, address operator) internal { - transferFrom(from, to, tokenId); - _setLocker(tokenId,operator); - lockLocker(tokenId); + function transferAndLock(uint256 tokenId, address from, address to, bool setApprove) public virtual override{ + _transferAndLock(tokenId,from,to,setApprove); } /** - * @dev Internal function to tranfer, update approve and lock the token. + * @dev Internal function to tranfer, update locker/approve and lock the token. */ - function transferApprove(uint256 tokenId, address from, address to, address operator) internal { + function _transferAndLock(uint256 tokenId, address from, address to, bool setApprove) internal { transferFrom(from, to, tokenId); - _approve(operator, tokenId); - lockApprove(tokenId); + if(setApprove){ + _approve(msg.sender, tokenId); + } + _lock(tokenId,msg.sender); } /*/////////////////////////////////////////////////////////////// @@ -177,7 +106,7 @@ abstract contract ERC7066 is ERC721,IERC7066{ * @dev Override approve to make sure token is unlocked */ function approve(address to, uint256 tokenId) public virtual override { - require (state[tokenId]==State.UNLOCKED, "ERC7066: Locked"); // so the unlocker stays approved + require (locker[tokenId]==address(0), "ERC7066: Locked"); super.approve(to, tokenId); } @@ -193,9 +122,9 @@ abstract contract ERC7066 is ERC721,IERC7066{ ) internal virtual override { // if it is a Transfer or Burn, we always deal with one token, that is startTokenId if (from != address(0)) { - require(state[startTokenId]!=State.LOCKED,"ERC7066: Locked"); - require(state[startTokenId]==State.UNLOCKED || isApprovedForAll(ownerOf(startTokenId), msg.sender) - || getApproved(startTokenId) == msg.sender, "ERC7066: Required Approved"); + require(locker[startTokenId]==address(0) + || ( locker[startTokenId]==msg.sender && (isApprovedForAll(ownerOf(startTokenId), msg.sender) + || getApproved(startTokenId) == msg.sender)), "ERC7066: Locked" ); } super._beforeTokenTransfer(from,to,startTokenId,quantity); } @@ -211,7 +140,6 @@ abstract contract ERC7066 is ERC721,IERC7066{ ) internal virtual override { // if it is a Transfer or Burn, we always deal with one token, that is startTokenId if (from != address(0)) { - state[startTokenId]==State.UNLOCKED; delete locker[startTokenId]; } super._afterTokenTransfer(from,to,startTokenId,quantity); diff --git a/assets/eip-7066/IERC7066.sol b/assets/eip-7066/IERC7066.sol index 7d61c48c68f51..ebd6e6a07e67f 100644 --- a/assets/eip-7066/IERC7066.sol +++ b/assets/eip-7066/IERC7066.sol @@ -3,25 +3,15 @@ pragma solidity >=0.7.0 <0.9.0; /// @title Lockable Extension for ERC721 -/// @dev Interface for the Lockable extension +/// @dev Interface for the ERC7066 /// @author StreamNFT interface IERC7066{ - - /** - * @dev Emitted when locker is set for tokenId - */ - event SetLocker (uint256 indexed tokenId, address _locker); - - /** - * @dev Emitted when locker is removed for tokenId - */ - event RemoveLocker (uint256 indexed tokenId); /** * @dev Emitted when tokenId is locked */ - event Lock (uint256 indexed tokenId); + event Lock (uint256 indexed tokenId, address _locker); /** * @dev Emitted when tokenId is unlocked @@ -29,34 +19,30 @@ interface IERC7066{ event Unlock (uint256 indexed tokenId); /** - * @dev Gives the `_locker` address permission to lock if msg.sender is owner + * @dev Lock the tokenId if msg.sender is owner or approved and set locker to msg.sender */ - function setLocker(uint256 tokenId, address _locker) external; + function lock(uint256 tokenId) external; /** - * @dev Purge the permission to lock if tokenId is `unlocked` and msg.sender is owner + * @dev Lock the tokenId if msg.sender is owner and set locker to _locker */ - function removeLocker(uint256 tokenId) external; - - /** - * @dev Lock the tokenId if msg.sender is locker or approved - */ - function lock(uint256 tokenId) external; + function lock(uint256 tokenId, address _locker) external; /** - * @dev Unlocks the tokenId if msg.sender is locker or approved + * @dev Unlocks the tokenId if msg.sender is locker */ function unlock(uint256 tokenId) external; /** - * @dev Tranfer and lock the token if the msg.sender is locker or approved + * @dev Tranfer and lock the token if the msg.sender is owner or approved. + * Lock the token and set locker to caller + * Optionally approve caller if bool setApprove flag is true */ - function transferAndLock(uint256 tokenId, address from, address to, address _locker) external; + function transferAndLock(uint256 tokenId, address from, address to, bool setApprove) external; /** * @dev Returns the wallet, that is stated as unlocking wallet for the tokenId. - * If address(0) returned, that means token is not locked. Any other result means token is locked. + * If address(0) returned, that means token is not locked. Any other result means token is locked. */ function lockerOf(uint256 tokenId) external view returns (address); - } \ No newline at end of file From 7d7da36a542c3493bc1dc67d5fc5672ea67cbde1 Mon Sep 17 00:00:00 2001 From: Piyush Date: Tue, 13 Jun 2023 13:53:38 +0530 Subject: [PATCH 33/44] updates test --- assets/eip-7066/test/test.js | 398 ++++++++++++++--------------------- 1 file changed, 163 insertions(+), 235 deletions(-) diff --git a/assets/eip-7066/test/test.js b/assets/eip-7066/test/test.js index a1479e5ed2ed4..9cda9e3b867cb 100644 --- a/assets/eip-7066/test/test.js +++ b/assets/eip-7066/test/test.js @@ -1,315 +1,243 @@ const { expect } = require('chai'); const { ethers } = require('hardhat'); -describe('MyNFT', function () { +describe('NewToken', function () { + let LOCK1 = 'lock(uint256)'; + let LOCK2 = 'lock(uint256,address)'; async function deployMyNFTFixture() { const [deployer, acc1, acc2, acc3] = await ethers.getSigners(); const MyNFT = await ethers.getContractFactory('MyNFT'); let myNFT = await MyNFT.deploy(); await myNFT.deployed(); + await myNFT.connect(deployer).mint(); return { myNFT, deployer, acc1, acc2, acc3 }; } - async function mintAndSetAuthority() { + async function approveUser1() { const { myNFT, deployer, acc1, acc2, acc3 } = await deployMyNFTFixture(); - await myNFT.connect(deployer).mint(); - await myNFT.connect(deployer).setLocker(0, acc1.address); - return { myNFT, deployer, acc1, acc2, acc3 }; - } - - async function approveAcc2() { - const { myNFT, deployer, acc1, acc2, acc3 } = await mintAndSetAuthority(); - await myNFT.connect(deployer).approve(acc2.address, 0); - await myNFT.connect(deployer).setApprovalForAll(acc2.address, true); - await myNFT.connect(acc2).lockApproved(0); + await myNFT.connect(deployer).approve(acc1.address, 0); + await myNFT.connect(deployer).setApprovalForAll(acc1.address, true); return { myNFT, deployer, acc1, acc2, acc3 }; } - describe('SetLocker', function () { - it('Should not allow anyone to set locker', async function () { - const { myNFT, deployer, acc1, acc2 } = await deployMyNFTFixture(); - await myNFT.connect(deployer).mint(); - await expect( - myNFT.connect(acc1).setLocker(0, acc1.address) - ).to.be.revertedWith('ERC7066 : Owner Required'); - }); - - it('Should not allow to set locker when token is locked', async function () { - const { myNFT, deployer, acc1, acc2 } = await deployMyNFTFixture(); - await myNFT.connect(deployer).mint(); - // set locker and let him lock the token - await myNFT.connect(deployer).setLocker(0, acc1.address); - await myNFT.connect(acc1).lock(0); - - //second check - await expect( - myNFT.connect(deployer).setLocker(0, acc2.address) - ).to.be.revertedWith('ERC7066 : Locked'); + describe('lockerOf', () => { + it('Should return zero address for an unlocked token', async () => { + const { myNFT } = await deployMyNFTFixture(); + expect(await myNFT.lockerOf(0)).to.equal(ethers.constants.AddressZero); }); - it('Should not allow to set locker when token is locked_approved', async function () { - // minted token by deployer, acc1- locker, acc2- lock approved - const { myNFT, deployer, acc1, acc2 } = await approveAcc2(); - await expect( - myNFT.connect(deployer).setLocker(0, acc1.address) - ).to.be.revertedWith('ERC7066 : Locked'); + it('Should revert as token does not exist', async () => { + const { myNFT } = await deployMyNFTFixture(); + await expect(myNFT.lockerOf(1)).to.be.revertedWith( + 'ERC7066: Nonexistent token' + ); }); }); - describe('RemoveLocker', function () { - it('Should not allow anyone to remove locker', async () => { - const { myNFT, deployer, acc1 } = await mintAndSetAuthority(); - await expect(myNFT.connect(acc1).removeLocker(0)).to.be.revertedWith( - 'ERC7066 : Owner Required' + describe('lock - with one parameter', () => { + it('Should not lock if already locked', async () => { + const { myNFT, deployer } = await deployMyNFTFixture(); + await myNFT.connect(deployer)[LOCK1](0); + await expect(myNFT.connect(deployer)[LOCK1](0)).to.be.revertedWith( + 'ERC7066: Locked' ); }); - it('Should not allow to remove locker when token is locked', async () => { - const { myNFT, deployer, acc1 } = await mintAndSetAuthority(); - // lock the token by locker - await myNFT.connect(acc1).lock(0); - await expect(myNFT.connect(deployer).removeLocker(0)).to.be.revertedWith( - 'ERC7066 : Locked' + it('Should not allow random user to lock', async () => { + const { myNFT, acc1 } = await deployMyNFTFixture(); + await expect(myNFT.connect(acc1)[LOCK1](0)).to.be.revertedWith( + 'Require owner or approved' ); }); - it('Should not allow to set locker when token is locked_approved', async function () { - // minted token by deployer, acc1- locker, acc2- lock approved - const { myNFT, deployer, acc1, acc2 } = await approveAcc2(); - await expect(myNFT.connect(deployer).removeLocker(0)).to.be.revertedWith( - 'ERC7066 : Locked' - ); + it('Should be able to lock token by owner', async () => { + const { myNFT, deployer } = await deployMyNFTFixture(); + await myNFT.connect(deployer)[LOCK1](0); + expect(await myNFT.lockerOf(0)).to.equal(deployer.address); }); - }); - describe('lockerOf', function () { - it('Should get the correct locker address', async () => { - const { myNFT, acc1 } = await mintAndSetAuthority(); - expect(await myNFT.connect(acc1).lockerOf(0)).to.equal(acc1.address); + it('Should be able to lock token by approved_user', async () => { + const { myNFT, acc1 } = await approveUser1(); + await myNFT.connect(acc1)[LOCK1](0); + expect(await myNFT.lockerOf(0)).to.equal(acc1.address); }); }); - describe('lock', function () { - it('Should not allow anyone to lock', async function () { - const { myNFT, deployer, acc1, acc2 } = await mintAndSetAuthority(); - - await expect(myNFT.connect(acc2).lock(0)).to.be.revertedWith( - 'ERC7066 : Locker Required' - ); - await expect(myNFT.connect(deployer).lock(0)).to.be.revertedWith( - 'ERC7066 : Locker Required' - ); + describe('lock - with two parameters', () => { + it('Should not lock token if already locked', async () => { + const { myNFT, deployer } = await deployMyNFTFixture(); + await myNFT.connect(deployer)[LOCK2](0, deployer.address); + await expect( + myNFT.connect(deployer)[LOCK2](0, deployer.address) + ).to.be.revertedWith('ERC7066: Locked'); }); - it('Should not allow to lock when token is locked_approved', async function () { - // minted token by deployer, acc1- lock authority, acc2- lock approved - const { myNFT, deployer, acc1, acc2 } = await approveAcc2(); - await expect(myNFT.connect(acc1).lock(0)).to.be.revertedWith( - 'ERC7066 : Locked' - ); + it('Should not allow random user to lock', async () => { + const { myNFT, acc1 } = await deployMyNFTFixture(); + await expect( + myNFT.connect(acc1)[LOCK2](0, acc1.address) + ).to.be.revertedWith('ERC7066: Require owner'); }); - it('Should not allow to lock when token is locked', async function () { - const { myNFT, deployer, acc1, acc2 } = await mintAndSetAuthority(); - - //lock the token by lock authority - await myNFT.connect(acc1).lock(0); - - //second check - await expect(myNFT.connect(acc1).lock(0)).to.be.revertedWith( - 'ERC7066 : Locked' - ); + it('Should not allow approved_user to lock', async () => { + const { myNFT, acc1 } = await approveUser1(); + await expect( + myNFT.connect(acc1)[LOCK2](0, acc1.address) + ).to.be.revertedWith('ERC7066: Require owner'); }); - }); - - describe('unlock', function () { - it('Should not allow anyone to unlock', async function () { - const { myNFT, deployer, acc1, acc2 } = await mintAndSetAuthority(); - await expect(myNFT.connect(acc2).unlock(0)).to.be.revertedWith( - 'ERC7066 : Locker Required' - ); - await expect(myNFT.connect(deployer).unlock(0)).to.be.revertedWith( - 'ERC7066 : Locker Required' - ); + it('Should allow token owner to lock, locker is owner', async () => { + const { myNFT, deployer } = await deployMyNFTFixture(); + await myNFT.connect(deployer)[LOCK2](0, deployer.address); + expect(await myNFT.lockerOf(0)).to.equal(deployer.address); }); - it('Should not allow to lock when token is locked_approved', async function () { - // minted token by deployer, acc1- lock authority, acc2- lock approved - const { myNFT, deployer, acc1, acc2 } = await approveAcc2(); - await expect(myNFT.connect(acc1).unlock(0)).to.be.revertedWith( - 'ERC7066 : Locked by approved' - ); + it('Should allow token owner to lock, locker is zero-address', async () => { + const { myNFT, deployer } = await deployMyNFTFixture(); + await myNFT.connect(deployer)[LOCK2](0, ethers.constants.AddressZero); + expect(await myNFT.lockerOf(0)).to.equal(ethers.constants.AddressZero); }); + }); - it('Should not allow to unlock when token is not locked', async function () { - const { myNFT, deployer, acc1, acc2 } = await mintAndSetAuthority(); - - await expect(myNFT.connect(acc1).unlock(0)).to.be.revertedWith( - 'ERC7066 : Unlocked' + describe('unlock', () => { + it('Should not unlock token if not locked', async () => { + const { myNFT, deployer } = await deployMyNFTFixture(); + await expect(myNFT.connect(deployer).unlock(0)).to.be.revertedWith( + 'ERC7066: Unlocked' ); }); - }); - - describe('approve', function () { - it('Should not approve when token is locked', async function () { - const { myNFT, deployer, acc1, acc2 } = await mintAndSetAuthority(); - await myNFT.connect(acc1).lock(0); - - await expect( - myNFT.connect(deployer).approve(acc2.address, 0) - ).to.be.revertedWith('ERC7066 : Locked'); + it('Should not unlock token if msg.sender is not the locker', async () => { + const { myNFT, deployer, acc1 } = await deployMyNFTFixture(); + await myNFT.connect(deployer)[LOCK1](0); + await expect(myNFT.connect(acc1).unlock(0)).to.be.reverted; }); - it('Should not approve when token is locked_approved', async function () { - // minted token by deployer, acc1- lock authority, acc2- lock approved - const { myNFT, deployer, acc1, acc2 } = await approveAcc2(); - await expect( - myNFT.connect(acc1).approve(acc2.address, 0) - ).to.be.revertedWith('ERC7066 : Locked'); + it('Should allow owner to unlock', async () => { + const { myNFT, deployer } = await deployMyNFTFixture(); + await myNFT.connect(deployer)[LOCK1](0); + await myNFT.connect(deployer).unlock(0); + expect(await myNFT.lockerOf(0)).to.equal(ethers.constants.AddressZero); }); - it('Should not allow anyone to approve', async function () { - const { myNFT, deployer, acc1, acc2 } = await mintAndSetAuthority(); - - await expect( - myNFT.connect(acc1).approve(acc2.address, 0) - ).to.be.revertedWith( - 'ERC721: approve caller is not token owner or approved for all' - ); + it('Should allow approver to unlock', async () => { + const { myNFT, acc1 } = await approveUser1(); + await myNFT.connect(acc1)[LOCK1](0); + await myNFT.connect(acc1).unlock(0); + expect(await myNFT.lockerOf(0)).to.equal(ethers.constants.AddressZero); }); }); - describe('lockApproved', async function () { - it('Should not allow anyone without approval', async function () { - const { myNFT, deployer, acc1, acc2 } = await mintAndSetAuthority(); - await expect(myNFT.connect(acc2).lockApproved(0)).to.be.revertedWith( - 'ERC7066 : Required approval' - ); - }); - - it('Should check if token is locked', async function () { - const { myNFT, deployer, acc1, acc2 } = await mintAndSetAuthority(); - - await myNFT.connect(deployer).approve(acc2.address, 0); - await myNFT.connect(deployer).setApprovalForAll(acc2.address, true); - await myNFT.connect(acc1).lock(0); - - await expect(myNFT.connect(acc2).lockApproved(0)).to.be.revertedWith( - 'ERC7066 : Locked' - ); + describe('transferAndLock', () => { + it('Should not allow if the user is not owner or approved', async () => { + const { myNFT, deployer, acc1 } = await deployMyNFTFixture(); + await expect( + myNFT + .connect(acc1) + .transferAndLock(0, deployer.address, acc1.address, false) + ).to.be.reverted; + }); + + it('Should transfer and lock, msg.sender - owner ,setApproval - true', async () => { + const { myNFT, deployer, acc1 } = await deployMyNFTFixture(); + await myNFT + .connect(deployer) + .transferAndLock(0, deployer.address, acc1.address, true); + expect(await myNFT.ownerOf(0)).to.equal(acc1.address); + expect(await myNFT.lockerOf(0)).to.equal(deployer.address); + expect(await myNFT.getApproved(0)).to.equal(deployer.address); + }); + + it('Should transfer and lock,msg.sender - owner, setApproval - false', async () => { + const { myNFT, deployer, acc1 } = await deployMyNFTFixture(); + await myNFT + .connect(deployer) + .transferAndLock(0, deployer.address, acc1.address, false); + expect(await myNFT.ownerOf(0)).to.equal(acc1.address); + expect(await myNFT.lockerOf(0)).to.equal(deployer.address); + expect(await myNFT.getApproved(0)).to.equal(ethers.constants.AddressZero); + }); + + it('Should transfer and lock, msg.sender - approved_user, setApproval - true', async () => { + const { myNFT, deployer, acc1 } = await approveUser1(); + await myNFT + .connect(acc1) + .transferAndLock(0, deployer.address, acc1.address, true); + expect(await myNFT.ownerOf(0)).to.equal(acc1.address); + expect(await myNFT.lockerOf(0)).to.equal(acc1.address); + expect(await myNFT.getApproved(0)).to.equal(acc1.address); + }); + + it('Should transfer and lock, msg.sender - approved_user,setApproval - false', async () => { + const { myNFT, deployer, acc1 } = await approveUser1(); + await myNFT + .connect(acc1) + .transferAndLock(0, deployer.address, acc1.address, false); + expect(await myNFT.ownerOf(0)).to.equal(acc1.address); + expect(await myNFT.lockerOf(0)).to.equal(acc1.address); + expect(await myNFT.getApproved(0)).to.equal(ethers.constants.AddressZero); }); }); - describe('unlockApproved', async function () { - it('Should not allow anyone without approval', async function () { - const { myNFT, deployer, acc1, acc2 } = await mintAndSetAuthority(); - - await expect(myNFT.connect(acc2).unlockApproved(0)).to.be.revertedWith( - 'ERC7066 : Required approval' - ); + describe('approve', () => { + it('Should not allow if the user is not owner/approved_user', async () => { + const { myNFT, acc1 } = await deployMyNFTFixture(); + await expect(myNFT.connect(acc1).approve(acc1.address, 0)).to.be.reverted; }); - it('Should not allow unlockApproved when token is locked', async function () { - const { myNFT, deployer, acc1, acc2 } = await mintAndSetAuthority(); - - await myNFT.connect(deployer).approve(acc2.address, 0); - await myNFT.connect(deployer).setApprovalForAll(acc2.address, true); - - //lock the token by lock authority - await myNFT.connect(acc1).lock(0); - await expect(myNFT.connect(acc2).unlockApproved(0)).to.be.revertedWith( - 'ERC7066 : Locked by locker' - ); + it('Should not allow if token is locked', async () => { + const { myNFT, deployer, acc1 } = await deployMyNFTFixture(); + await myNFT.connect(deployer)[LOCK1](0); + await expect( + myNFT.connect(deployer).approve(acc1.address, 0) + ).to.be.revertedWith('ERC7066: Locked'); }); - it('Should check if token is in unlocked state', async function () { - const { myNFT, deployer, acc1, acc2 } = await mintAndSetAuthority(); - - await myNFT.connect(deployer).approve(acc2.address, 0); - await myNFT.connect(deployer).setApprovalForAll(acc2.address, true); - - await expect(myNFT.connect(acc2).unlockApproved(0)).to.be.revertedWith( - 'ERC7066 : Unlocked' - ); + it('Should allow user with isApprovedForAll to approve', async () => { + const { myNFT, acc1, acc2 } = await approveUser1(); + await myNFT.connect(acc1).approve(acc2.address, 0); + expect(await myNFT.getApproved(0)).to.equal(acc2.address); }); }); - describe('transferFrom', async function () { - describe('Before Token Transfer', async function () { - it('Should not allow transfer when token is locked', async function () { - const { myNFT, deployer, acc1, acc2, acc3 } = - await mintAndSetAuthority(); - - await myNFT.connect(acc1).lock(0); + describe('transferFrom', () => { + describe('beforeTokenTransfer', () => { + it('Should not allow transfer if the token is locked', async () => { + const { myNFT, deployer, acc1 } = await deployMyNFTFixture(); + await myNFT.connect(deployer)[LOCK1](0); await expect( myNFT .connect(deployer) - .transferFrom(deployer.address, acc3.address, 0) - ).to.be.revertedWith('ERC7066 : Locked'); + .transferFrom(deployer.address, acc1.address, 0) + ).to.be.revertedWith('ERC7066: Locked'); }); - it('Should not allow transfer by anyone without approval(lock_approved,ERC721)', async function () { - const { myNFT, deployer, acc1, acc2, acc3 } = await approveAcc2(); + it('Should not allow transfer if user is not owner/approved_user', async () => { + const { myNFT, deployer, acc1, acc2 } = await approveUser1(); await expect( - myNFT.connect(acc1).transferFrom(deployer.address, acc3.address, 0) + myNFT.connect(acc2).transferFrom(deployer.address, acc1.address, 0) ).to.be.revertedWith('ERC721: caller is not token owner or approved'); }); - - it('Should not allow transfer by anyone without approval(lock_approved,ERC721_Lockable)', async function () { - const { myNFT, deployer, acc1, acc2, acc3 } = await approveAcc2(); - - await expect( - myNFT - .connect(deployer) - .transferFrom(deployer.address, acc3.address, 0) - ).to.be.revertedWith('ERC7066 : Required approval'); - }); - - it('Should allow approved person to transfer token', async function () { - const { myNFT, deployer, acc1, acc2, acc3 } = await approveAcc2(); - - await myNFT - .connect(acc2) - .transferFrom(deployer.address, acc3.address, 0); - expect(await myNFT.ownerOf(0)).to.equal(acc3.address); - }); }); - describe('After Token Transfer', async function () { - it('Should check if token has new owner', async function () { - const { myNFT, deployer, acc1, acc2, acc3 } = - await mintAndSetAuthority(); + describe('afterTokenTransfer', async () => { + it('Should check if token has a locker', async () => { + const { myNFT, deployer, acc1 } = await deployMyNFTFixture(); await myNFT .connect(deployer) - .transferFrom(deployer.address, acc3.address, 0); - - expect(await myNFT.ownerOf(0)).to.equal(acc3.address); + .transferFrom(deployer.address, acc1.address, 0); + expect(await myNFT.lockerOf(0)).to.equal(ethers.constants.AddressZero); + expect(await myNFT.ownerOf(0)).to.equal(acc1.address); }); - it('Should check if token does not have a lock authority', async function () { - const { myNFT, deployer, acc1, acc2, acc3 } = - await mintAndSetAuthority(); + it('Should be able to transfer by approved_user', async () => { + const { myNFT, deployer, acc1 } = await approveUser1(); await myNFT - .connect(deployer) - .transferFrom(deployer.address, acc3.address, 0); - await expect(myNFT.connect(acc1).lock(0)).to.be.revertedWith( - 'ERC7066 : Locker Required' - ); - }); - - it('Should check if token state is unlocked', async function () { - const { myNFT, deployer, acc1, acc2, acc3 } = - await mintAndSetAuthority(); - await myNFT - .connect(deployer) - .transferFrom(deployer.address, acc3.address, 0); - await myNFT.connect(acc3).setLocker(0, acc1.address); - await expect(myNFT.connect(acc1).unlock(0)).to.be.revertedWith( - 'ERC7066 : Unlocked' - ); + .connect(acc1) + .transferFrom(deployer.address, acc1.address, 0); + expect(await myNFT.lockerOf(0)).to.equal(ethers.constants.AddressZero); + expect(await myNFT.ownerOf(0)).to.equal(acc1.address); }); }); }); From 73601d1ec995f4ddaaa27b2edf93a36674193ace Mon Sep 17 00:00:00 2001 From: Piyush Date: Tue, 13 Jun 2023 13:59:09 +0530 Subject: [PATCH 34/44] updates doc --- EIPS/eip-7066.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/EIPS/eip-7066.md b/EIPS/eip-7066.md index 3f036a4949dbd..5ab3bb1ad6ac9 100644 --- a/EIPS/eip-7066.md +++ b/EIPS/eip-7066.md @@ -1,6 +1,6 @@ --- eip: 7066 -title: Lockable Extension for ERC721 +title: Lockable Extension for ERC-721 description: Interface for enabling locking of ERC-721 using locker and approved author: Piyush Chittara (@streamnft-tech), Srinivas Joshi (@SrinivasJoshi) discussions-to: https://ethereum-magicians.org/t/eip-7066-lockable-extension-for-erc721/14425 @@ -146,6 +146,7 @@ Reference Implementation can be found [here](../assets/eip-7066/ERC7066.sol). There are no security considerations related directly to the implementation of this standard for the contract that manages [ERC-721](./eip-721.md). ### Considerations for the contracts that work with lockable tokens + - Once `locked`, token can not be further `approved` or `transfered`. - If token is `locked` and caller is `locker` and `appoved` both, caller can transfer the token. - `locked` token with `locker` as in-accesible account or un-verified contract address can lead to permanent lock of the token. From fa29104d6310c0de32d8b9b46aed34da8c26b216 Mon Sep 17 00:00:00 2001 From: Piyush Date: Wed, 14 Jun 2023 13:19:53 +0530 Subject: [PATCH 35/44] review fixes --- EIPS/eip-7066.md | 39 ++++++++++++++++++------------------ assets/eip-7066/ERC7066.sol | 2 +- assets/eip-7066/IERC7066.sol | 2 +- 3 files changed, 21 insertions(+), 22 deletions(-) diff --git a/EIPS/eip-7066.md b/EIPS/eip-7066.md index 5ab3bb1ad6ac9..9ec3f9b7b7a60 100644 --- a/EIPS/eip-7066.md +++ b/EIPS/eip-7066.md @@ -13,26 +13,22 @@ requires: 165, 721 ## Abstract -An extension of [ERC-721](./eip-721.md), this standard incorporates `locking` features into NFTs, allowing for various uses while preventing sale or transfer. The token's owner or operator has the ability to lock it, specifying an unlocker address (either an EOA or a contract) that exclusively holds the power to unlock the token. Owner can also provide approval for tokenId, enabling ability to lock asset while address holds the token approval. Upon token transfer, these rights get purged. - +An extension of [ERC-721](./eip-721.md), this standard incorporates `locking` features into NFTs, allowing for various uses while preventing sale or transfer. The token's `owner` can `lock` it, setting up locker address (either an EOA or a contract) that exclusively holds the power to unlock the token. Owner can also provide approval for tokenId, enabling ability to lock asset while address holds the token approval. Token can also be locked by `approved`, assigning locker to itself. Upon token transfer, these rights get purged. ## Motivation [ERC-721](./eip-721.md) has sparked an unprecedented surge in demand for NFTs. However, despite this tremendous success, NFT economy suffers from secondary liquidity where it remains Illiquid in owner’s wallet. There are projects such as NFTfi, Paraspace which aims to address the liquidity challenge, but they entail below mentioned inconveniences and risks for owners as they necessitate transferring the participating NFTs to the projects' contracts. - - Loss of utility: The utility value of NFTs diminishes when they are transferred to an escrow account, no longer remaining under the direct custody of the owners. - Lack of composability: The market could benefit from increased liquidity if NFT owners had access to multiple financial tools, such as leveraging loans and renting out their assets for maximum returns. Composability serves as the missing piece in creating a more efficient market. - Smart contract vulnerabilities: NFTs are susceptible to loss or theft due to potential bugs or vulnerabilities present in the smart contracts they rely on. - The aforementioned issues contribute to a poor user experience (UX), and we propose enhancing the [ERC-721](./eip-721.md) standard by implementing a native locking mechanism: Rather than being transferred to a smart contract, an NFT remains securely stored in self-custody but is locked. During the lock period, the NFT's transfer is restricted while its other properties remain unchanged. NFT Owner retains the ability to use or distribute it’s utility - -NFTs have numerous use cases where it is crucial for the NFT to remain within the owner's wallet, even when it serves as collateral for a loan. Whether it's authorizing access to a Discord server, or utilizing NFT within a play-to-earn (P2E) game, owner should have the freedom to do so throughout the lending period. Just as real estate owner can continue living in their mortgaged house, take personal loan or keep tenants to generate passive income, these functionalities should be available to NFT owners to bring more investors in NFT economy. +NFTs have numerous use cases where the NFT must remain within the owner's wallet, even when it serves as collateral for a loan. Whether it's authorizing access to a Discord server, or utilizing NFT within a play-to-earn (P2E) game, owner should have the freedom to do so throughout the lending period. Just as real estate owner can continue living in their mortgaged house, take personal loan or keep tenants to generate passive income, these functionalities should be available to NFT owners to bring more investors in NFT economy. Lockable NFTs enable the following use cases : @@ -42,11 +38,16 @@ Lockable NFTs enable the following use cases : - Buy Now Pay Later: The buyer receives the locked NFT and can immediately begin using it. However, they are unable to sell the NFT until all installments are paid. Failure to complete the full payment results in the NFT returning to the seller, along with a fee. - Composability: Maximize liquidity by having access to multiple financial tools. Imagine taking a loan against NFT and putting it on rentals to generate passive income. - Primary sales: Mint an NFT for a partial payment and settle the remaining amount once owner is satisfied with the collection's progress. -- Soulbound: Organization can mint and self assign `locker`, send token to user and lock the asset. +- Soulbound: Organization can mint and self-assign `locker`, send token to user and lock the asset. - Safety: Safely and conveniently use exclusive blue chip NFTs. Lockable extension allows owner to lock NFT and designate secure cold wallet as the unlocker. This way, owner can keep NFT on MetaMask and easily use it, even if a hacker gains access to MetaMask account. Without access to the cold wallet, the hacker cannot transfer NFT, ensuring its safety. -By extending the [ERC-721](./eip-721.md) standard, the proposed standard enables secure and convenient management of underlying NFT assets. It natively supports prevalent NFTFi use cases such as, staking, lending, and renting. We anticipate that this proposed standard will foster increased engagement of NFT owners in NFTFi projects, thereby enhancing the overall vitality of the NFT ecosystem. +This proposal is different from other locking proposals in number of ways: + +- This implementation provides a minimal implementation of `lock` and `unlock` and believes other conditions like time-bound are great ideas but can be achieved without creating a specific implementation. Locking and Unlocking can be based on any conditions (e.g. repayment, expiry). Therefore time-bound unlocks a relatively specific use case that can be achieved via smart-contracts themselves without that being a part of the token contract. +- This implementation proposes a separation of rights between locker and approver. Token can be locked with approval and approved can unlock and withdraw tokens (opening up opportunities like renting, lending, bnpl etc), and token can be locked lacking the rights to revoke token, yet can unlock if required (opening up opportunities like account-bound NFTs). +- Our proposal implement ability to `transferAndLock` which can be used to transfer, lock and optionally approve token. Enabling the possibility of revocation after transfer. +By extending the [ERC-721](./eip-721.md) standard, the proposed standard enables secure and convenient management of underlying NFT assets. It natively supports prevalent NFTFi use cases such as staking, lending, and renting. We anticipate that this proposed standard will foster increased engagement of NFT owners in NFTFi projects, thereby enhancing the overall vitality of the NFT ecosystem. ## Specification @@ -54,19 +55,20 @@ The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "S ### Overview -[ERC-721](./eip-721.md) compliant contracts MAY implement this EIP to provide standard methods of locking and unlocking the token at its current owner address. Token owner MAY `lock` the token and assign `locker` to some `address` using `lock(uint256 tokenId, address _locker)` function, this MUST set `locker` to `_locker`. Token owner or approved MAY `lock` the token using `lock(uint256 tokenId)` function, this MUST set `locker` to `msg.sender`. Token MAY be `unlocked` by `locker` using `unlock` function. `unlock` function MUST delete `locker` mapping and default to `address(0)`. +[ERC-721](./eip-721.md) compliant contracts MAY implement this EIP to provide standard methods of locking and unlocking the token at its current owner address. + +Token owner MAY `lock` the token and assign `locker` to some `address` using `lock(uint256 tokenId, address _locker)` function, this MUST set `locker` to `_locker`. Token owner or approved MAY `lock` the token using `lock(uint256 tokenId)` function, this MUST set `locker` to `msg.sender`. Token MAY be `unlocked` by `locker` using `unlock` function. `unlock` function MUST delete `locker` mapping and default to `address(0)`. If the token is `locked`, the `lockerOf` function MUST return an address that is `locker` and can `unlock` the token. For tokens that are not `locked`, the `lockerOf` function MUST return `address(0)`. -`lock` function MUST revert if token is not already locked. `unlock` function MUST revert if token is not locked. `approve` function MUST revert if token is locked. `_tansfer` function MUST revert if token is locked. `_transfer` transaction MUST pass if token is locked and `msg.sender` is `approved` and `locker` both. After `_transfer` values of `locker` and `approved` MUST -get purged. +`lock` function MUST revert if token is not already locked. `unlock` function MUST revert if token is not locked. ERC-721 `approve` function MUST revert if token is locked. ERC-721 `_tansfer` function MUST revert if token is locked. ERC-721 `_transfer` function MUST pass if token is locked and `msg.sender` is `approved` and `locker` both. After ERC-721 `_transfer`, values of `locker` and `approved` MUST be purged. -Token MAY be transfered and `locked`, and OPTIONAL retain the value of `approval` using `transferAndLock` function. This is RECOMMENDED for usecases where Token transfer and subsequent revocation is REQUIRED. +Token MAY be transferred and `locked`, and OPTIONAL setup `approval` to `locker` using `transferAndLock` function. This is RECOMMENDED for use-cases where Token transfer and subsequent revocation is REQUIRED. ### Interface ``` -// SPDX-License-Identifier: GPL-3.0 +// SPDX-License-Identifier: CC0-1.0 pragma solidity >=0.7.0 <0.9.0; @@ -118,19 +120,16 @@ interface IERC7066{ ## Rationale -This approach presents a minimalistic solution that focuses on locking items and specifying who has the authority to unlock them. It offers flexibility and extensibility, accommodating various potential use cases mentioned in the Motivation section. - -Moreover, when there is a requirement to grant temporary or redeemable rights for a NFT, such as rentals or purchases with installments, this EIP involves the actual transfer of the token to the temporary user's wallet, rather than simply assigning a role. This design choice ensures compatibility with existing NFT ecosystem tools and dApps, without necessitating additional interfaces or logic implementation. - -This functionality already exists on Solana, enabling ease for NFT liquidity and use cases. This EIP shall introduce same functionality to EVM ecosystem. The naming and reference implementation of functions and storage entities resemble the Approval flow outlined in [ERC-721](./eip-721.md), ensuring an intuitive user experience. - -Existing Upgradedable [ERC-721](./eip-721.md) can upgrade to this standard, enabling locking capability inherently and unlock underlying liquidity features. +This proposal set `locker[tokenId]` to `address(0)` when token is `unlocked` because we delete mapping on `locker[tokenId]` freeing up space. Also, this assertion helps our contract to validate if token is `locked` or `unlocked` for internal function calls. +This proposal exposes `transferAndLock(uint256 tokenId, address from, address to, bool setApprove)` which can be used to transfer token and lock at the receiver's address. This additionally accepts input `bool setApprove` which on `true` assign `approval` to `locker`, hence enabling `locker` to revoke the token (revocation conditions can be defined in contracts and `approval` provided to contract). This provides conditional ownership to receiver, without the privilege to `transfer` token. ## Backwards Compatibility This standard is compatible with [ERC-721](./eip-721.md) standards. +Existing Upgradedable [ERC-721](./eip-721.md) can upgrade to this standard, enabling locking capability inherently and unlock underlying liquidity features. + ## Test Cases Test cases can be found [here](../assets/eip-7066/test/test.js). diff --git a/assets/eip-7066/ERC7066.sol b/assets/eip-7066/ERC7066.sol index cb9a8e0ac07f3..34f5c3d84d277 100644 --- a/assets/eip-7066/ERC7066.sol +++ b/assets/eip-7066/ERC7066.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-3.0 +// SPDX-License-Identifier: CC0-1.0 pragma solidity >=0.7.0 <0.9.0; diff --git a/assets/eip-7066/IERC7066.sol b/assets/eip-7066/IERC7066.sol index ebd6e6a07e67f..dd952aee0a4d5 100644 --- a/assets/eip-7066/IERC7066.sol +++ b/assets/eip-7066/IERC7066.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-3.0 +// SPDX-License-Identifier: CC0-1.0 pragma solidity >=0.7.0 <0.9.0; From ce80ffa985371e280d55dbf11dd1ab1343f84c4a Mon Sep 17 00:00:00 2001 From: Piyush Date: Fri, 16 Jun 2023 11:20:09 +0530 Subject: [PATCH 36/44] overrides and interface correction --- assets/eip-7066/ERC7066.sol | 8 ++++---- assets/eip-7066/IERC7066.sol | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/assets/eip-7066/ERC7066.sol b/assets/eip-7066/ERC7066.sol index 34f5c3d84d277..dbabbefbe9d0a 100644 --- a/assets/eip-7066/ERC7066.sol +++ b/assets/eip-7066/ERC7066.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: CC0-1.0 -pragma solidity >=0.7.0 <0.9.0; +pragma solidity ^0.8.0; import "./IERC7066.sol"; @@ -83,7 +83,7 @@ abstract contract ERC7066 is ERC721,IERC7066{ * Lock the token and set locker to caller *. Optionally approve caller if bool setApprove flag is true */ - function transferAndLock(uint256 tokenId, address from, address to, bool setApprove) public virtual override{ + function transferAndLock(address from, address to, uint256 tokenId, bool setApprove) public virtual override{ _transferAndLock(tokenId,from,to,setApprove); } @@ -105,7 +105,7 @@ abstract contract ERC7066 is ERC721,IERC7066{ /** * @dev Override approve to make sure token is unlocked */ - function approve(address to, uint256 tokenId) public virtual override { + function approve(address to, uint256 tokenId) public virtual override(IERC721, ERC721) { require (locker[tokenId]==address(0), "ERC7066: Locked"); super.approve(to, tokenId); } @@ -152,7 +152,7 @@ abstract contract ERC7066 is ERC721,IERC7066{ /** * @dev See {IERC165-supportsInterface}. */ - function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC721) returns (bool) { return interfaceId == type(IERC7066).interfaceId || super.supportsInterface(interfaceId); diff --git a/assets/eip-7066/IERC7066.sol b/assets/eip-7066/IERC7066.sol index dd952aee0a4d5..4cabcc3a2c4f3 100644 --- a/assets/eip-7066/IERC7066.sol +++ b/assets/eip-7066/IERC7066.sol @@ -1,12 +1,12 @@ -// SPDX-License-Identifier: CC0-1.0 +// SPDX-License-Identifier: GPL-3.0 -pragma solidity >=0.7.0 <0.9.0; +pragma solidity ^0.8.0; /// @title Lockable Extension for ERC721 /// @dev Interface for the ERC7066 /// @author StreamNFT -interface IERC7066{ +interface IERC7066 is IERC721{ /** * @dev Emitted when tokenId is locked @@ -38,7 +38,7 @@ interface IERC7066{ * Lock the token and set locker to caller * Optionally approve caller if bool setApprove flag is true */ - function transferAndLock(uint256 tokenId, address from, address to, bool setApprove) external; + function transferAndLock(address from, address to, uint256 tokenId, bool setApprove) external; /** * @dev Returns the wallet, that is stated as unlocking wallet for the tokenId. From e8bc642db220bb38f5d654904f636efca3a190f4 Mon Sep 17 00:00:00 2001 From: Piyush Date: Fri, 16 Jun 2023 14:04:55 +0530 Subject: [PATCH 37/44] add locker to transferAndLock --- assets/eip-7066/ERC7066.sol | 10 +++++----- assets/eip-7066/IERC7066.sol | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/assets/eip-7066/ERC7066.sol b/assets/eip-7066/ERC7066.sol index dbabbefbe9d0a..32be26bb1d940 100644 --- a/assets/eip-7066/ERC7066.sol +++ b/assets/eip-7066/ERC7066.sol @@ -83,19 +83,19 @@ abstract contract ERC7066 is ERC721,IERC7066{ * Lock the token and set locker to caller *. Optionally approve caller if bool setApprove flag is true */ - function transferAndLock(address from, address to, uint256 tokenId, bool setApprove) public virtual override{ - _transferAndLock(tokenId,from,to,setApprove); + function transferAndLock(address from, address to, uint256 tokenId, address _locker, bool setApprove) public virtual override{ + _transferAndLock(tokenId,from,to,_locker,setApprove); } /** * @dev Internal function to tranfer, update locker/approve and lock the token. */ - function _transferAndLock(uint256 tokenId, address from, address to, bool setApprove) internal { + function _transferAndLock(uint256 tokenId, address from, address to, address _locker, bool setApprove) internal { transferFrom(from, to, tokenId); if(setApprove){ - _approve(msg.sender, tokenId); + _approve(_locker, tokenId); } - _lock(tokenId,msg.sender); + _lock(tokenId,_locker); } /*/////////////////////////////////////////////////////////////// diff --git a/assets/eip-7066/IERC7066.sol b/assets/eip-7066/IERC7066.sol index 4cabcc3a2c4f3..dc6909e28fc02 100644 --- a/assets/eip-7066/IERC7066.sol +++ b/assets/eip-7066/IERC7066.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.0; /// @title Lockable Extension for ERC721 -/// @dev Interface for the ERC7066 +/// @dev Interface for ERC7066 /// @author StreamNFT interface IERC7066 is IERC721{ @@ -38,7 +38,7 @@ interface IERC7066 is IERC721{ * Lock the token and set locker to caller * Optionally approve caller if bool setApprove flag is true */ - function transferAndLock(address from, address to, uint256 tokenId, bool setApprove) external; + function transferAndLock(address from, address to, uint256 tokenId, address _locker, bool setApprove) external; /** * @dev Returns the wallet, that is stated as unlocking wallet for the tokenId. From 9bc7fe7734fa617ee51526d68f4e90ea7a5d78b9 Mon Sep 17 00:00:00 2001 From: Piyush Date: Fri, 16 Jun 2023 16:11:03 +0530 Subject: [PATCH 38/44] remove locker --- assets/eip-7066/ERC7066.sol | 10 +++++----- assets/eip-7066/IERC7066.sol | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/assets/eip-7066/ERC7066.sol b/assets/eip-7066/ERC7066.sol index 32be26bb1d940..dbabbefbe9d0a 100644 --- a/assets/eip-7066/ERC7066.sol +++ b/assets/eip-7066/ERC7066.sol @@ -83,19 +83,19 @@ abstract contract ERC7066 is ERC721,IERC7066{ * Lock the token and set locker to caller *. Optionally approve caller if bool setApprove flag is true */ - function transferAndLock(address from, address to, uint256 tokenId, address _locker, bool setApprove) public virtual override{ - _transferAndLock(tokenId,from,to,_locker,setApprove); + function transferAndLock(address from, address to, uint256 tokenId, bool setApprove) public virtual override{ + _transferAndLock(tokenId,from,to,setApprove); } /** * @dev Internal function to tranfer, update locker/approve and lock the token. */ - function _transferAndLock(uint256 tokenId, address from, address to, address _locker, bool setApprove) internal { + function _transferAndLock(uint256 tokenId, address from, address to, bool setApprove) internal { transferFrom(from, to, tokenId); if(setApprove){ - _approve(_locker, tokenId); + _approve(msg.sender, tokenId); } - _lock(tokenId,_locker); + _lock(tokenId,msg.sender); } /*/////////////////////////////////////////////////////////////// diff --git a/assets/eip-7066/IERC7066.sol b/assets/eip-7066/IERC7066.sol index dc6909e28fc02..409fd36582c32 100644 --- a/assets/eip-7066/IERC7066.sol +++ b/assets/eip-7066/IERC7066.sol @@ -38,7 +38,7 @@ interface IERC7066 is IERC721{ * Lock the token and set locker to caller * Optionally approve caller if bool setApprove flag is true */ - function transferAndLock(address from, address to, uint256 tokenId, address _locker, bool setApprove) external; + function transferAndLock(address from, address to, uint256 tokenId, bool setApprove) external; /** * @dev Returns the wallet, that is stated as unlocking wallet for the tokenId. From 9147291ef706993429885bbd44b9246211927566 Mon Sep 17 00:00:00 2001 From: Piyush Date: Sat, 17 Jun 2023 14:47:18 +0530 Subject: [PATCH 39/44] license --- assets/eip-7066/IERC7066.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/eip-7066/IERC7066.sol b/assets/eip-7066/IERC7066.sol index 409fd36582c32..0dbb08fafe162 100644 --- a/assets/eip-7066/IERC7066.sol +++ b/assets/eip-7066/IERC7066.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-3.0 +// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.0; From 0b40ce1245794af6978bd1151af03101dbbe260f Mon Sep 17 00:00:00 2001 From: Piyush Date: Sat, 17 Jun 2023 16:12:53 +0530 Subject: [PATCH 40/44] doc update --- EIPS/eip-7066.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-7066.md b/EIPS/eip-7066.md index 9ec3f9b7b7a60..13bf77e69618a 100644 --- a/EIPS/eip-7066.md +++ b/EIPS/eip-7066.md @@ -2,7 +2,7 @@ eip: 7066 title: Lockable Extension for ERC-721 description: Interface for enabling locking of ERC-721 using locker and approved -author: Piyush Chittara (@streamnft-tech), Srinivas Joshi (@SrinivasJoshi) +author: Piyush Chittara (@piyush-chittara), Srinivas Joshi (@SrinivasJoshi) discussions-to: https://ethereum-magicians.org/t/eip-7066-lockable-extension-for-erc721/14425 status: Draft type: Standards Track From 2afa055727380593ee41660ec292c5e0d9f29dac Mon Sep 17 00:00:00 2001 From: Piyush Date: Sat, 17 Jun 2023 16:18:25 +0530 Subject: [PATCH 41/44] doc update --- EIPS/eip-7066.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-7066.md b/EIPS/eip-7066.md index 13bf77e69618a..9ec3f9b7b7a60 100644 --- a/EIPS/eip-7066.md +++ b/EIPS/eip-7066.md @@ -2,7 +2,7 @@ eip: 7066 title: Lockable Extension for ERC-721 description: Interface for enabling locking of ERC-721 using locker and approved -author: Piyush Chittara (@piyush-chittara), Srinivas Joshi (@SrinivasJoshi) +author: Piyush Chittara (@streamnft-tech), Srinivas Joshi (@SrinivasJoshi) discussions-to: https://ethereum-magicians.org/t/eip-7066-lockable-extension-for-erc721/14425 status: Draft type: Standards Track From 0bfb0f86c83259ca5ead8c4da8801e8f2b831440 Mon Sep 17 00:00:00 2001 From: Piyush Date: Sat, 17 Jun 2023 16:21:55 +0530 Subject: [PATCH 42/44] doc update --- EIPS/eip-7066.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-7066.md b/EIPS/eip-7066.md index 9ec3f9b7b7a60..13bf77e69618a 100644 --- a/EIPS/eip-7066.md +++ b/EIPS/eip-7066.md @@ -2,7 +2,7 @@ eip: 7066 title: Lockable Extension for ERC-721 description: Interface for enabling locking of ERC-721 using locker and approved -author: Piyush Chittara (@streamnft-tech), Srinivas Joshi (@SrinivasJoshi) +author: Piyush Chittara (@piyush-chittara), Srinivas Joshi (@SrinivasJoshi) discussions-to: https://ethereum-magicians.org/t/eip-7066-lockable-extension-for-erc721/14425 status: Draft type: Standards Track From 94c8e7a405689e4d838d8cbc19d690410860c12a Mon Sep 17 00:00:00 2001 From: Piyush Date: Mon, 19 Jun 2023 23:59:16 +0530 Subject: [PATCH 43/44] author updates --- EIPS/eip-7066.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-7066.md b/EIPS/eip-7066.md index 13bf77e69618a..cdf6a11e70d4b 100644 --- a/EIPS/eip-7066.md +++ b/EIPS/eip-7066.md @@ -2,7 +2,7 @@ eip: 7066 title: Lockable Extension for ERC-721 description: Interface for enabling locking of ERC-721 using locker and approved -author: Piyush Chittara (@piyush-chittara), Srinivas Joshi (@SrinivasJoshi) +author: Piyush Chittara (@piyush-chittara), StreamNFT (@streamnft-tech), Srinivas Joshi (@SrinivasJoshi) discussions-to: https://ethereum-magicians.org/t/eip-7066-lockable-extension-for-erc721/14425 status: Draft type: Standards Track From 2ef930dfd99b6f502bc030a80a5493c114dc4923 Mon Sep 17 00:00:00 2001 From: Piyush Date: Tue, 18 Jul 2023 18:03:01 +0530 Subject: [PATCH 44/44] EIP7066 review --- EIPS/eip-7066.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-7066.md b/EIPS/eip-7066.md index cdf6a11e70d4b..e6910b8d1b43b 100644 --- a/EIPS/eip-7066.md +++ b/EIPS/eip-7066.md @@ -4,7 +4,7 @@ title: Lockable Extension for ERC-721 description: Interface for enabling locking of ERC-721 using locker and approved author: Piyush Chittara (@piyush-chittara), StreamNFT (@streamnft-tech), Srinivas Joshi (@SrinivasJoshi) discussions-to: https://ethereum-magicians.org/t/eip-7066-lockable-extension-for-erc721/14425 -status: Draft +status: Review type: Standards Track category: ERC created: 2023-05-25