From 72cafb2c4f6a0f4eb74568cae89fe9d5ceb568c2 Mon Sep 17 00:00:00 2001 From: radiocaca Date: Sat, 30 Apr 2022 02:36:54 +0800 Subject: [PATCH 01/40] EIP-5058:Lockable ERC-721 Standard --- EIPS/eip-5058.md | 184 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 EIPS/eip-5058.md diff --git a/EIPS/eip-5058.md b/EIPS/eip-5058.md new file mode 100644 index 00000000000000..9a47d052c37293 --- /dev/null +++ b/EIPS/eip-5058.md @@ -0,0 +1,184 @@ +--- +eip: 5058 +title: Lockable ERC-721 Standard +description: Lockable ERC-721 can be uesed for locking, staking, lending, crowdfunding, etc. +author: Tyler(@radiocaca), Alex(@gojazdev) +discussions-to: +status: Draft +type: Standards Track +category: ERC +created: 2022-04-30 +requires (*optional): 165,721 +--- + +## Abstract +A secure locking standard for non-fungible tokens (NFTs). The NFT owner approve the operator to lock the NFT through `setLockApprovalForAll()` or `lockApprove()`. The apporved operator locks the NFT through `lockFrom`. The locked NFT cannot be transferred until the end of the lock period. NFTs that implement this standard can participate in NFTFi projects that are compatible with this protocol without leaving the owner's wallet. + +## Motivation +With the continuous development of the NFT ecosystem, the market value of NFTs is growing, and more and more blue-chip NFTs have appeared in the market. The problem of poor liquidity of NFTs has become increasingly prominent. Many projects have also introduced solutions to solve the liquidity of NFTs, such as: [NFTFi](https://www.nftfi.com/), [BendDAO](https://www.benddao.xyz/), etc., based on the current ERC-721 protocol standard, when users use these projects, they must transfer their NFTs to the project contract. This behavior has huge risks: + +1. Potential bugs in NFTFi project contracts may lead to loss of users' NFTs +2. NFT has its use value, such as being used as PFP, when the user wallet does not own the NFT after mortgage, the use value of NFT is limited. +3. Many NFTs have airdrops. When NFTs are pledged, users cannot directly receive airdrops, which brings huge losses to users. + +This standard perfectly solves the problems caused by the lack of the above [ERC-721](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md) standard: + +1. Compared with transferring the NFT to the project contract, it is more secure and guarantees the user's ownership of the NFT. Unless sold and transferred, in any other scenario, the NFT can participate and can be kept in the user's wallet. +2. When the NFT is locked, it just cannot be transferred, it does not affect the use of the NFT, and the various rights and uses brought by the NFT can be enjoyed without distinction. +3. The NFT is always kept in the user's wallet. Even if the NFT is locked due to participating in the pledge, the NFT airdrop can still be claimed directly. + +This standard manages the control of NFTs in a safer and more convenient way, enabling NFTs to natively support NFTFi activities such as locking, staking, lending, crowdfunding, etc., which will encourage NFT holders to participate more actively in NFTFi projects, indirectly Solve the value liquidity of NFT. + +## Specification +The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119. + +Lockable ERC-721 **MUST** implement the `IERC721Lockable` interfaces: +```solidity +// SPDX-License-Identifier: MIT +// Creator: tyler@radiocaca.com + +pragma solidity ^0.8.8; + +import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; + +/** + * @dev ERC-721 Non-Fungible Token Standard, optional lockable extension + * ERC721 Token that can be locked for a certain period and cannot be transferred. + * This is designed for a non-escrow staking contract that comes later to lock a user's NFT + * while still letting them keep it in their wallet. + * This extension can ensure the security of user tokens during the staking period. + * If the nft lending protocol is compatible with this extension, the trouble caused by the NFT + * airdrop can be avoided, because the airdrop is still in the user's wallet + */ +interface IERC721Lockable is IERC165 { + /** + * @dev Emitted when `tokenId` token is locked from `from`. + */ + event Locked(address indexed operator, address indexed from, uint256 indexed tokenId, uint256 expired); + + /** + * @dev Emitted when `owner` enables `approved` to lock the `tokenId` token. + */ + event LockApproval(address indexed owner, address indexed approved, uint256 indexed tokenId); + + /** + * @dev Emitted when `owner` enables or disables (`approved`) `operator` to lock all of its tokens. + */ + event LockApprovalForAll(address indexed owner, address indexed operator, bool approved); + + /** + * @dev Returns the current locker of the `tokenId` token. + * + * Requirements: + * + * - `tokenId` must exist. + */ + function lockerOf(uint256 tokenId) external view returns (address locker); + + /** + * @dev Lock `tokenId` token until `expired` from `from`. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `tokenId` token must be owned by `from`. + * - `expired` must be greater than block.timestamp + * - If the caller is not `from`, it must be approved to lock this token + * by either {lockApprove} or {setLockApprovalForAll}. + * + * Emits a {Locked} event. + */ + function lockFrom( + address from, + uint256 tokenId, + uint256 expired + ) external; + + /** + * @dev Unlock `tokenId` token until `expired` from `from`. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `tokenId` token must be owned by `from`. + * - `expired` must be less than the current expiration time. + * - the caller must be the operator who locks the token by {lockFrom} + * + * Emits a {Locked} event. + */ + function unlockFrom( + address from, + uint256 tokenId, + uint256 expired + ) external; + + /** + * @dev Gives permission to `to` to lock `tokenId` token. + * + * Requirements: + * + * - The caller must own the token or be an approved lock operator. + * - `tokenId` must exist. + * + * Emits an {LockApproval} event. + */ + function lockApprove(address to, uint256 tokenId) external; + + /** + * @dev Approve or remove `operator` as an lock operator for the caller. + * Operators can call {lockFrom} for any token owned by the caller. + * + * Requirements: + * + * - The `operator` cannot be the caller. + * + * Emits an {LockApprovalForAll} event. + */ + function setLockApprovalForAll(address operator, bool _approved) external; + + /** + * @dev Returns the account lock approved for `tokenId` token. + * + * Requirements: + * + * - `tokenId` must exist. + */ + function getLockApproved(uint256 tokenId) external view returns (address operator); + + /** + * @dev Returns if the `operator` is allowed to lock all of the assets of `owner`. + * + * See {setLockApprovalForAll} + */ + function isLockApprovedForAll(address owner, address operator) external view returns (bool); + + /** + * @dev Returns if the `tokenId` token is locked. + */ + function isLocked(uint256 tokenId) external view returns (bool); +} +``` + +## Rationale + +### NFT lock approvals +The NFT owner can give another trusted operator the right to lock his NFT through the approve function. The `lockApprove()` function only approve the specified NFT, and `setLockApprovalForAll()` approve all NFTs of the collection under the user wallet. When a user participates in NFTFi, the NFTFi project contract calls `lockFrom()` to lock the user's NFT.Locked NFT will not be able to be transferred, the NFTFi project contract can also use the unlock function (`unlockFrom()`) to unlock the NFT. + +### NFT lock period +When the NFT is locked, the lock expiration time needs to be set. The expiration time must be greater than the current block time. After the lock expires, the NFT is automatically released and can be transferred. + +### Bound NFT +This standard takes into account that some NFTFi projects want to learn more about NFT participation, such as viewing NFT information through lock-up contracts, so we designed a boundNFT. When the lock contract locks the NFT, it can choose to mint the boundNFT of the NFT to the lock contract address. The boundNFT is consistent with the original NFT metadata and other information, but the boundNFT has no value and cannot be transferred. It is just a lock certificate. When the user's NFT lockup ends, the boundNFT will be destroyed. + + +## Backwards Compatibility +This standard is compatible with current ERC-721 standards. + +## Reference Implementation +You can find an implementation of this standard in [RadioCaca](https://github.com/radiocaca/ERC721L/tree/main/contracts/ERC721L). + +## Security Considerations +There are no security considerations related directly to the implementation of this standard. + +## Copyright +Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). From 469636841fecef44f989c4680fdc9227d211540d Mon Sep 17 00:00:00 2001 From: radiocaca Date: Sun, 1 May 2022 20:02:34 +0800 Subject: [PATCH 02/40] update eip --- EIPS/eip-5058.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/EIPS/eip-5058.md b/EIPS/eip-5058.md index 9a47d052c37293..70334a7a3e0d25 100644 --- a/EIPS/eip-5058.md +++ b/EIPS/eip-5058.md @@ -56,6 +56,11 @@ interface IERC721Lockable is IERC165 { */ event Locked(address indexed operator, address indexed from, uint256 indexed tokenId, uint256 expired); + /** + * @dev Emitted when `tokenId` token is unlocked from `from`. + */ + event Unlocked(address indexed operator, address indexed from, uint256 indexed tokenId); + /** * @dev Emitted when `owner` enables `approved` to lock the `tokenId` token. */ @@ -101,16 +106,11 @@ interface IERC721Lockable is IERC165 { * * - `from` cannot be the zero address. * - `tokenId` token must be owned by `from`. - * - `expired` must be less than the current expiration time. * - the caller must be the operator who locks the token by {lockFrom} * - * Emits a {Locked} event. + * Emits a {Unlocked} event. */ - function unlockFrom( - address from, - uint256 tokenId, - uint256 expired - ) external; + function unlockFrom(address from, uint256 tokenId) external; /** * @dev Gives permission to `to` to lock `tokenId` token. @@ -157,6 +157,7 @@ interface IERC721Lockable is IERC165 { */ function isLocked(uint256 tokenId) external view returns (bool); } + ``` ## Rationale @@ -175,7 +176,7 @@ This standard takes into account that some NFTFi projects want to learn more abo This standard is compatible with current ERC-721 standards. ## Reference Implementation -You can find an implementation of this standard in [RadioCaca](https://github.com/radiocaca/ERC721L/tree/main/contracts/ERC721L). +You can find an implementation of this standard in [RadioCaca](https://github.com/radiocaca/ERC721L/tree/main/contracts/EIP5058). ## Security Considerations There are no security considerations related directly to the implementation of this standard. From 49c31fffc292f0e48d043d8c29a3de6ec44cf434 Mon Sep 17 00:00:00 2001 From: radiocaca <84221999+radiocaca@users.noreply.github.com> Date: Thu, 5 May 2022 00:30:51 +0800 Subject: [PATCH 03/40] Update eip-5058.md --- EIPS/eip-5058.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/EIPS/eip-5058.md b/EIPS/eip-5058.md index 70334a7a3e0d25..ea42deebf900d3 100644 --- a/EIPS/eip-5058.md +++ b/EIPS/eip-5058.md @@ -1,31 +1,31 @@ --- eip: 5058 title: Lockable ERC-721 Standard -description: Lockable ERC-721 can be uesed for locking, staking, lending, crowdfunding, etc. -author: Tyler(@radiocaca), Alex(@gojazdev) +description: Lockable ERC-721 tokens +author: Tyler (@radiocaca), Alex (@gojazdev) discussions-to: status: Draft type: Standards Track category: ERC created: 2022-04-30 -requires (*optional): 165,721 +requires: 165, 721 --- ## Abstract -A secure locking standard for non-fungible tokens (NFTs). The NFT owner approve the operator to lock the NFT through `setLockApprovalForAll()` or `lockApprove()`. The apporved operator locks the NFT through `lockFrom`. The locked NFT cannot be transferred until the end of the lock period. NFTs that implement this standard can participate in NFTFi projects that are compatible with this protocol without leaving the owner's wallet. +A secure locking standard for non-fungible tokens (NFTs). The NFT owner approves the operator to lock the NFT through `setLockApprovalForAll()` or `lockApprove()`. The apporved operator locks the NFT through `lockFrom`. The locked NFT cannot be transferred until the end of the lock period. NFTs that implement this standard can participate in NFTFi projects that are compatible with this protocol without leaving the owner's wallet. ## Motivation With the continuous development of the NFT ecosystem, the market value of NFTs is growing, and more and more blue-chip NFTs have appeared in the market. The problem of poor liquidity of NFTs has become increasingly prominent. Many projects have also introduced solutions to solve the liquidity of NFTs, such as: [NFTFi](https://www.nftfi.com/), [BendDAO](https://www.benddao.xyz/), etc., based on the current ERC-721 protocol standard, when users use these projects, they must transfer their NFTs to the project contract. This behavior has huge risks: 1. Potential bugs in NFTFi project contracts may lead to loss of users' NFTs 2. NFT has its use value, such as being used as PFP, when the user wallet does not own the NFT after mortgage, the use value of NFT is limited. -3. Many NFTs have airdrops. When NFTs are pledged, users cannot directly receive airdrops, which brings huge losses to users. +3. Many NFTs have airdrops. When their NFTs are staked, users cannot directly receive airdrops, which brings huge losses to users. This standard perfectly solves the problems caused by the lack of the above [ERC-721](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md) standard: -1. Compared with transferring the NFT to the project contract, it is more secure and guarantees the user's ownership of the NFT. Unless sold and transferred, in any other scenario, the NFT can participate and can be kept in the user's wallet. +1. Compared with transferring the NFT to the project contract, it is more secure and can guarantee user's ownership of the NFT. Unless sold and transferred, in any other scenario, the NFT can participate and can be kept in the user's wallet. 2. When the NFT is locked, it just cannot be transferred, it does not affect the use of the NFT, and the various rights and uses brought by the NFT can be enjoyed without distinction. -3. The NFT is always kept in the user's wallet. Even if the NFT is locked due to participating in the pledge, the NFT airdrop can still be claimed directly. +3. The NFT is always kept in the user's wallet. Even if the NFT is locked during the staking, the NFT airdrop can still be claimed directly. This standard manages the control of NFTs in a safer and more convenient way, enabling NFTs to natively support NFTFi activities such as locking, staking, lending, crowdfunding, etc., which will encourage NFT holders to participate more actively in NFTFi projects, indirectly Solve the value liquidity of NFT. From 6b21dd194f16853b5db10eb083018b72761f0a60 Mon Sep 17 00:00:00 2001 From: John Sfumato Date: Tue, 10 May 2022 02:13:38 -0700 Subject: [PATCH 04/40] Update eip-5058.md proposal language --- EIPS/eip-5058.md | 44 ++++++++++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/EIPS/eip-5058.md b/EIPS/eip-5058.md index ea42deebf900d3..5cd9e6726ba654 100644 --- a/EIPS/eip-5058.md +++ b/EIPS/eip-5058.md @@ -1,38 +1,42 @@ --- eip: 5058 title: Lockable ERC-721 Standard -description: Lockable ERC-721 tokens -author: Tyler (@radiocaca), Alex (@gojazdev) +description: An extension to the ERC-721 standard to enable a trustless lock mechanism, which is useful for many use cases such as locking, staking, lending, or crowdfunding. +author: Tyler(@radiocaca), Alex(@gojazdev) discussions-to: status: Draft type: Standards Track category: ERC created: 2022-04-30 -requires: 165, 721 +requires (*optional): 165,721 --- ## Abstract -A secure locking standard for non-fungible tokens (NFTs). The NFT owner approves the operator to lock the NFT through `setLockApprovalForAll()` or `lockApprove()`. The apporved operator locks the NFT through `lockFrom`. The locked NFT cannot be transferred until the end of the lock period. NFTs that implement this standard can participate in NFTFi projects that are compatible with this protocol without leaving the owner's wallet. + +A standard to extend the [ERC-721](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md) non-fungible tokens (NFTs) standard. The NFT owners approve the operator to lock the NFT through `setLockApprovalForAll()` or `lockApprove()`. The apporved operator locks the NFT through `lockFrom`. The locked NFTs cannot be transferred until the end of the locking period. An immediate use case is that the NFTs can participate in smart contracts without leaving the wallets of their owners. ## Motivation -With the continuous development of the NFT ecosystem, the market value of NFTs is growing, and more and more blue-chip NFTs have appeared in the market. The problem of poor liquidity of NFTs has become increasingly prominent. Many projects have also introduced solutions to solve the liquidity of NFTs, such as: [NFTFi](https://www.nftfi.com/), [BendDAO](https://www.benddao.xyz/), etc., based on the current ERC-721 protocol standard, when users use these projects, they must transfer their NFTs to the project contract. This behavior has huge risks: -1. Potential bugs in NFTFi project contracts may lead to loss of users' NFTs -2. NFT has its use value, such as being used as PFP, when the user wallet does not own the NFT after mortgage, the use value of NFT is limited. -3. Many NFTs have airdrops. When their NFTs are staked, users cannot directly receive airdrops, which brings huge losses to users. +The NFTs, enabled by [ERC-721](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md), has been on the explosion in demand. The total market value and the ecosystem continue to grow with more and more blue chip NFTs, which are approximately equivalent to popular intellectual properties in a conventional sense. Despite the vast success, something is left to be desired. Liquidity has always been one of the biggest challenges for NFTs. Attempts have been made to solve the liquidity challenge: [NFTFi](https://www.nftfi.com/) and [BendDAO](https://www.benddao.xyz/), to name a few. Utilizing the currently prevalent ERC-721 standard, these projects require NFTs to be transferred from the owners' wallets to the projects' contracts, which poses inconveniences and risks: + +1. Smart contract risks: NFTs can be lost or stolen due to bugs or vulnerabilities in the contracts. +2. Loss of utility: NFTs have utility values, such as being used as profile pictures or bragging rights, which are lost when the NFTs are no longer seen under the owners custody. +3. Missing Airdrops: The owners can no longer directly receive airdrops entitled to the NFTs. Considering the values and price fluctuation of some of the airdrops, missing or getting them untimely can can both financially impact the owners. -This standard perfectly solves the problems caused by the lack of the above [ERC-721](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md) standard: +All-of-the-above are bad UX, and we believe the ERC-721 standard can be improved by adopting the proposed standard: -1. Compared with transferring the NFT to the project contract, it is more secure and can guarantee user's ownership of the NFT. Unless sold and transferred, in any other scenario, the NFT can participate and can be kept in the user's wallet. -2. When the NFT is locked, it just cannot be transferred, it does not affect the use of the NFT, and the various rights and uses brought by the NFT can be enjoyed without distinction. -3. The NFT is always kept in the user's wallet. Even if the NFT is locked during the staking, the NFT airdrop can still be claimed directly. +1. Instead of being transferred to project contracts, a NFT remains being self-custody but locked. +2. While the NFT is locked, only the change of its ownership is disabled. Other utilities are not affected. +3. The owners can receive or claim airdrops themselves. -This standard manages the control of NFTs in a safer and more convenient way, enabling NFTs to natively support NFTFi activities such as locking, staking, lending, crowdfunding, etc., which will encourage NFT holders to participate more actively in NFTFi projects, indirectly Solve the value liquidity of NFT. +The proposed standard allows the underlying NFT assets to be managed securely and conveniently and extends programmability of the ERC-721 standard to enable native support of common NFTFi functionalities, locking, staking, lending, crowdfunding, etc. We believe the proposed standard will encourage NFT owners to participate more actively in NFTFi projects and, hence, improve the liquidity thereof. ## Specification + The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119. Lockable ERC-721 **MUST** implement the `IERC721Lockable` interfaces: + ```solidity // SPDX-License-Identifier: MIT // Creator: tyler@radiocaca.com @@ -163,23 +167,31 @@ interface IERC721Lockable is IERC165 { ## Rationale ### NFT lock approvals -The NFT owner can give another trusted operator the right to lock his NFT through the approve function. The `lockApprove()` function only approve the specified NFT, and `setLockApprovalForAll()` approve all NFTs of the collection under the user wallet. When a user participates in NFTFi, the NFTFi project contract calls `lockFrom()` to lock the user's NFT.Locked NFT will not be able to be transferred, the NFTFi project contract can also use the unlock function (`unlockFrom()`) to unlock the NFT. + +A NFT owner can give another trusted operator the right to lock his NFT through the approve functions. The `lockApprove()` function only approves for the specified NFT, whereas `setLockApprovalForAll()` approves for all NFTs of the collection under the wallet. When a user participates in a NFTFi, the NFTFi project contract calls `lockFrom()` to lock the user's NFT. Locked NFTs cannot be transferred, the NFTFi project contract can use the unlock function (`unlockFrom()`) to unlock the NFT. ### NFT lock period -When the NFT is locked, the lock expiration time needs to be set. The expiration time must be greater than the current block time. After the lock expires, the NFT is automatically released and can be transferred. + +When the NFT is locked, it is required to set the lock expiration time, which must be greater than the current block height. Upon the lock expiration, the NFT is automatically released and can be transferred. ### Bound NFT -This standard takes into account that some NFTFi projects want to learn more about NFT participation, such as viewing NFT information through lock-up contracts, so we designed a boundNFT. When the lock contract locks the NFT, it can choose to mint the boundNFT of the NFT to the lock contract address. The boundNFT is consistent with the original NFT metadata and other information, but the boundNFT has no value and cannot be transferred. It is just a lock certificate. When the user's NFT lockup ends, the boundNFT will be destroyed. +While the underlying NFT asset is being locked in the owner's wallet, the NFT may still be desirable to provide certain visibility to the lock-up contracts of the NFTFI project. We designed a bound NFT (boundNFT) to take this requirement into account. + +When a contract locks the NFT, it can opt to mint a boundNFT. The boundNFT contains the original NFT's metadata and other information. A boundNFT does not have a transfer function. A boundNFT is mere a certificate. When the NFT asset is released, the bNFT will be atomically destroyed. ## Backwards Compatibility + This standard is compatible with current ERC-721 standards. ## Reference Implementation + You can find an implementation of this standard in [RadioCaca](https://github.com/radiocaca/ERC721L/tree/main/contracts/EIP5058). ## Security Considerations + There are no security considerations related directly to the implementation of this standard. ## Copyright + Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). From 1e4c9a066c96fee2470775714c584aba44c0357b Mon Sep 17 00:00:00 2001 From: John Sfumato Date: Tue, 10 May 2022 02:13:38 -0700 Subject: [PATCH 05/40] Update eip-5058.md proposal language --- EIPS/eip-5058.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/EIPS/eip-5058.md b/EIPS/eip-5058.md index 5cd9e6726ba654..5543f9f297ef0e 100644 --- a/EIPS/eip-5058.md +++ b/EIPS/eip-5058.md @@ -1,7 +1,7 @@ --- eip: 5058 title: Lockable ERC-721 Standard -description: An extension to the ERC-721 standard to enable a trustless lock mechanism, which is useful for many use cases such as locking, staking, lending, or crowdfunding. +description: An extension to the ERC-721 standard to enable a trustless locking mechanism, which is useful for use cases such as locking, staking, lending, or crowdfunding. author: Tyler(@radiocaca), Alex(@gojazdev) discussions-to: status: Draft @@ -13,23 +13,23 @@ requires (*optional): 165,721 ## Abstract -A standard to extend the [ERC-721](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md) non-fungible tokens (NFTs) standard. The NFT owners approve the operator to lock the NFT through `setLockApprovalForAll()` or `lockApprove()`. The apporved operator locks the NFT through `lockFrom`. The locked NFTs cannot be transferred until the end of the locking period. An immediate use case is that the NFTs can participate in smart contracts without leaving the wallets of their owners. +We propose to extend the ERC-721 standard with a secure locking mechanism. The NFT owners approve the operator to lock the NFT through `setLockApprovalForAll()` or `lockApprove()`. The apporved operator locks the NFT through `lockFrom()`. The locked NFTs cannot be transferred until the end of the locking period. An immediate use case is to allow NFTs to participate in smart contracts without leaving the wallets of their owners. ## Motivation -The NFTs, enabled by [ERC-721](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md), has been on the explosion in demand. The total market value and the ecosystem continue to grow with more and more blue chip NFTs, which are approximately equivalent to popular intellectual properties in a conventional sense. Despite the vast success, something is left to be desired. Liquidity has always been one of the biggest challenges for NFTs. Attempts have been made to solve the liquidity challenge: [NFTFi](https://www.nftfi.com/) and [BendDAO](https://www.benddao.xyz/), to name a few. Utilizing the currently prevalent ERC-721 standard, these projects require NFTs to be transferred from the owners' wallets to the projects' contracts, which poses inconveniences and risks: +NFTs, enabled by [ERC-721](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md), has been on the explosion in demand. The total market value and the ecosystem continue to grow with more and more blue chip NFTs, which are approximately equivalent to popular intellectual properties in a conventional sense. Despite the vast success, something is left to be desired. Liquidity has always been one of the biggest challenges for NFTs. Several attempts have been made to tackle the liquidity challenge: [NFTFi](https://www.nftfi.com/) and [BendDAO](https://www.benddao.xyz/), to name a few. Utilizing the currently prevalent ERC-721 standard, these projects require participating NFTs to be transferred to the projects' contracts, which poses inconveniences and risks to the owners: 1. Smart contract risks: NFTs can be lost or stolen due to bugs or vulnerabilities in the contracts. -2. Loss of utility: NFTs have utility values, such as being used as profile pictures or bragging rights, which are lost when the NFTs are no longer seen under the owners custody. -3. Missing Airdrops: The owners can no longer directly receive airdrops entitled to the NFTs. Considering the values and price fluctuation of some of the airdrops, missing or getting them untimely can can both financially impact the owners. +2. Loss of utility: NFTs have utility values, such as profile pictures and bragging rights, which are lost when the NFTs are no longer seen under the owners' custody. +3. Missing Airdrops: The owners can no longer directly receive airdrops entitled to the NFTs. Considering the values and price fluctuation of some of the airdrops, either missing or getting the airdrop not on time can financially impact the owners. -All-of-the-above are bad UX, and we believe the ERC-721 standard can be improved by adopting the proposed standard: +All-of-the-above are bad UX, and we believe the ERC-721 standard can be improved by adopting a native locking mechanism: -1. Instead of being transferred to project contracts, a NFT remains being self-custody but locked. -2. While the NFT is locked, only the change of its ownership is disabled. Other utilities are not affected. +1. Instead of being transferred to a smart contract, a NFT remains being self-custody but locked. +2. While a NFT is locked, only the change of its ownership is disabled. Other utilities are not affected. 3. The owners can receive or claim airdrops themselves. -The proposed standard allows the underlying NFT assets to be managed securely and conveniently and extends programmability of the ERC-721 standard to enable native support of common NFTFi functionalities, locking, staking, lending, crowdfunding, etc. We believe the proposed standard will encourage NFT owners to participate more actively in NFTFi projects and, hence, improve the liquidity thereof. +The proposed standard allows the underlying NFT assets to be managed securely and conveniently and extends the ERC-721 standard to natively support of common NFTFi use cases including locking, staking, lending, and crowdfunding. We believe the proposed standard will encourage NFT owners to participate more actively in NFTFi projects and, hence, improve the livelihood of the whole NFT ecosystem. ## Specification @@ -168,17 +168,17 @@ interface IERC721Lockable is IERC165 { ### NFT lock approvals -A NFT owner can give another trusted operator the right to lock his NFT through the approve functions. The `lockApprove()` function only approves for the specified NFT, whereas `setLockApprovalForAll()` approves for all NFTs of the collection under the wallet. When a user participates in a NFTFi, the NFTFi project contract calls `lockFrom()` to lock the user's NFT. Locked NFTs cannot be transferred, the NFTFi project contract can use the unlock function (`unlockFrom()`) to unlock the NFT. +A NFT owner can give another trusted operator the right to lock his NFT through the approve functions. The `lockApprove()` function only approves for the specified NFT, whereas `setLockApprovalForAll()` approves for all NFTs of the collection under the wallet. When a user participates in a NFTFi project, the project contract calls `lockFrom()` to lock the user's NFT. Locked NFTs cannot be transferred, but the NFTFi project contract can use the unlock function `unlockFrom()` to unlock the NFT. ### NFT lock period -When the NFT is locked, it is required to set the lock expiration time, which must be greater than the current block height. Upon the lock expiration, the NFT is automatically released and can be transferred. +When locking a NFT, it is required to specify the lock expiration block height, which must be greater than the current block height. Upon the lock expiration, the NFT is automatically released and can be transferred. ### Bound NFT -While the underlying NFT asset is being locked in the owner's wallet, the NFT may still be desirable to provide certain visibility to the lock-up contracts of the NFTFI project. We designed a bound NFT (boundNFT) to take this requirement into account. +While the underlying NFT asset is being locked in the owner's wallet, the NFT may still be expected to provide certain visibility to the locking contracts. We designed a bound NFT (boundNFT) to take this requirement into account. -When a contract locks the NFT, it can opt to mint a boundNFT. The boundNFT contains the original NFT's metadata and other information. A boundNFT does not have a transfer function. A boundNFT is mere a certificate. When the NFT asset is released, the bNFT will be atomically destroyed. +When a contract locks a NFT, it can opt to mint a boundNFT. The boundNFT contains the original NFT's metadata and other information. A boundNFT does not have a transfer function. A boundNFT is mere a certificate. When the NFT asset is released, the bNFT will be atomically destroyed. ## Backwards Compatibility From 1e5f6641e2406c733dc61ffdef60e81f9f489bbb Mon Sep 17 00:00:00 2001 From: radiocaca <84221999+radiocaca@users.noreply.github.com> Date: Wed, 11 May 2022 01:24:19 +0800 Subject: [PATCH 06/40] Update eip-5058.md --- EIPS/eip-5058.md | 44 ++++++++++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/EIPS/eip-5058.md b/EIPS/eip-5058.md index 5543f9f297ef0e..6ac4d2e3de3a5e 100644 --- a/EIPS/eip-5058.md +++ b/EIPS/eip-5058.md @@ -1,14 +1,14 @@ --- eip: 5058 title: Lockable ERC-721 Standard -description: An extension to the ERC-721 standard to enable a trustless locking mechanism, which is useful for use cases such as locking, staking, lending, or crowdfunding. -author: Tyler(@radiocaca), Alex(@gojazdev) -discussions-to: +description: Lockable ERC-721 tokens +author: Tyler (@radiocaca), Alex (@gojazdev), John (@sfumato00) +discussions-to: [9201](https://ethereum-magicians.org/t/eip-5058-erc-721-lockable-standard/9201) status: Draft type: Standards Track category: ERC created: 2022-04-30 -requires (*optional): 165,721 +requires: 165, 721 --- ## Abstract @@ -29,6 +29,8 @@ All-of-the-above are bad UX, and we believe the ERC-721 standard can be improved 2. While a NFT is locked, only the change of its ownership is disabled. Other utilities are not affected. 3. The owners can receive or claim airdrops themselves. +The value of NFT can be reflected in two aspects: collection value and utility value. Collection value needs to ensure that the holder's wallet retains ownership of the NFT forever. Utility value requires ensuring that the holder can verify their NFT ownership in other projects. Both of these aspects require that the NFT be kept in the owner's wallet all the time. + The proposed standard allows the underlying NFT assets to be managed securely and conveniently and extends the ERC-721 standard to natively support of common NFTFi use cases including locking, staking, lending, and crowdfunding. We believe the proposed standard will encourage NFT owners to participate more actively in NFTFi projects and, hence, improve the livelihood of the whole NFT ecosystem. ## Specification @@ -39,11 +41,10 @@ Lockable ERC-721 **MUST** implement the `IERC721Lockable` interfaces: ```solidity // SPDX-License-Identifier: MIT -// Creator: tyler@radiocaca.com pragma solidity ^0.8.8; -import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; /** * @dev ERC-721 Non-Fungible Token Standard, optional lockable extension @@ -54,14 +55,14 @@ import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; * If the nft lending protocol is compatible with this extension, the trouble caused by the NFT * airdrop can be avoided, because the airdrop is still in the user's wallet */ -interface IERC721Lockable is IERC165 { +interface IERC721Lockable is IERC721 { /** - * @dev Emitted when `tokenId` token is locked from `from`. + * @dev Emitted when `tokenId` token is locked by `operator` from `from`. */ event Locked(address indexed operator, address indexed from, uint256 indexed tokenId, uint256 expired); /** - * @dev Emitted when `tokenId` token is unlocked from `from`. + * @dev Emitted when `tokenId` token is unlocked by `operator` from `from`. */ event Unlocked(address indexed operator, address indexed from, uint256 indexed tokenId); @@ -76,7 +77,7 @@ interface IERC721Lockable is IERC165 { event LockApprovalForAll(address indexed owner, address indexed operator, bool approved); /** - * @dev Returns the current locker of the `tokenId` token. + * @dev Returns the locker who is locking the `tokenId` token. * * Requirements: * @@ -85,13 +86,13 @@ interface IERC721Lockable is IERC165 { function lockerOf(uint256 tokenId) external view returns (address locker); /** - * @dev Lock `tokenId` token until `expired` from `from`. + * @dev Lock `tokenId` token until the block number is greater than `expired` to be unlocked. * * Requirements: * * - `from` cannot be the zero address. * - `tokenId` token must be owned by `from`. - * - `expired` must be greater than block.timestamp + * - `expired` must be greater than block.number * - If the caller is not `from`, it must be approved to lock this token * by either {lockApprove} or {setLockApprovalForAll}. * @@ -104,7 +105,7 @@ interface IERC721Lockable is IERC165 { ) external; /** - * @dev Unlock `tokenId` token until `expired` from `from`. + * @dev Unlock `tokenId` token. * * Requirements: * @@ -138,7 +139,7 @@ interface IERC721Lockable is IERC165 { * * Emits an {LockApprovalForAll} event. */ - function setLockApprovalForAll(address operator, bool _approved) external; + function setLockApprovalForAll(address operator, bool approved) external; /** * @dev Returns the account lock approved for `tokenId` token. @@ -161,7 +162,6 @@ interface IERC721Lockable is IERC165 { */ function isLocked(uint256 tokenId) external view returns (bool); } - ``` ## Rationale @@ -170,27 +170,35 @@ interface IERC721Lockable is IERC165 { A NFT owner can give another trusted operator the right to lock his NFT through the approve functions. The `lockApprove()` function only approves for the specified NFT, whereas `setLockApprovalForAll()` approves for all NFTs of the collection under the wallet. When a user participates in a NFTFi project, the project contract calls `lockFrom()` to lock the user's NFT. Locked NFTs cannot be transferred, but the NFTFi project contract can use the unlock function `unlockFrom()` to unlock the NFT. +### NFT lock/unlock +Authorized project contract has permission to lock NFT through lockFrom method, locked NFT cannot be transferred, unless the lock time expires. Of course, the project contract also has permission to unlock NFT in advance through unlockFrom. Note: only the address of the locked NFT has permission to unlock NFT. + ### NFT lock period When locking a NFT, it is required to specify the lock expiration block height, which must be greater than the current block height. Upon the lock expiration, the NFT is automatically released and can be transferred. ### Bound NFT +Bound NFT is an extension of the EIP, which implements the ability to mint a boundNFT during the NFT locking period. The boundNFT is identical to the locked NFT metadata and can be transferred. However, boundNFT only exists during the NFT locking period, and will be destroyed after the NFT is unlocked. +BoundNFT can be used to lend, as a staking credential for the contract. The credential can be locked in the contract, but also to the user. In NFT leasing, boundNFT can be rented to users because boundNFT is essentially equivalent to NFT. This consensus, if accepted by all projects, boundNFT will bring more creativity to NFT. -While the underlying NFT asset is being locked in the owner's wallet, the NFT may still be expected to provide certain visibility to the locking contracts. We designed a bound NFT (boundNFT) to take this requirement into account. +### Bound NFT Factory +Bound NFT Factory is a common boundNFT factory, similar to uniswap's erc20-pairs factory. It uses the create2 method to create a boundNFT contract address for any NFT deterministic. BoundNFT contract that has been created can only be controlled by the original NFT contract. -When a contract locks a NFT, it can opt to mint a boundNFT. The boundNFT contains the original NFT's metadata and other information. A boundNFT does not have a transfer function. A boundNFT is mere a certificate. When the NFT asset is released, the bNFT will be atomically destroyed. ## Backwards Compatibility This standard is compatible with current ERC-721 standards. +## Test Cases +Test case written using hardhat: [here](https://github.com/radiocaca/ERC721L/blob/main/test/ERC721Lockable.ts) + ## Reference Implementation You can find an implementation of this standard in [RadioCaca](https://github.com/radiocaca/ERC721L/tree/main/contracts/EIP5058). ## Security Considerations -There are no security considerations related directly to the implementation of this standard. +After being locked, the NFT can not be transferred, so before authorizing locking rights to other project contracts, you must confirm that the project contract can unlock NFT. Otherwise there is a risk of NFT being permanently locked. It is recommended to give a reasonable locking period in use for projects. NFT can be automatically unlocked, which can reduce the risk to a certain extent. ## Copyright From fa18ed679b712f124c36bdf9b1dfa1651738d24e Mon Sep 17 00:00:00 2001 From: radiocaca <84221999+radiocaca@users.noreply.github.com> Date: Wed, 11 May 2022 01:31:11 +0800 Subject: [PATCH 07/40] Update eip-5058.md --- EIPS/eip-5058.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/EIPS/eip-5058.md b/EIPS/eip-5058.md index 6ac4d2e3de3a5e..0aa8de57f14f82 100644 --- a/EIPS/eip-5058.md +++ b/EIPS/eip-5058.md @@ -3,7 +3,7 @@ eip: 5058 title: Lockable ERC-721 Standard description: Lockable ERC-721 tokens author: Tyler (@radiocaca), Alex (@gojazdev), John (@sfumato00) -discussions-to: [9201](https://ethereum-magicians.org/t/eip-5058-erc-721-lockable-standard/9201) +discussions-to: https://ethereum-magicians.org/t/eip-5058-erc-721-lockable-standard/9201 status: Draft type: Standards Track category: ERC @@ -40,7 +40,7 @@ The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL Lockable ERC-721 **MUST** implement the `IERC721Lockable` interfaces: ```solidity -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.8; @@ -194,7 +194,7 @@ Test case written using hardhat: [here](https://github.com/radiocaca/ERC721L/blo ## Reference Implementation -You can find an implementation of this standard in [RadioCaca](https://github.com/radiocaca/ERC721L/tree/main/contracts/EIP5058). +You can find an implementation of this standard in the assets folder: [RadioCaca](https://github.com/radiocaca/ERC721L/tree/main/contracts/EIP5058). ## Security Considerations From a49ef35b1b7d4310cb85b747e986692a7c5e6752 Mon Sep 17 00:00:00 2001 From: radiocaca Date: Wed, 11 May 2022 14:21:20 +0800 Subject: [PATCH 08/40] upload example --- EIPS/eip-5058.md | 2 +- assets/eip-5058/ERC721Lockable.sol | 274 ++++++++++++++++++++ assets/eip-5058/IERC721Lockable.sol | 123 +++++++++ assets/eip-5058/extensions/EIP5058Bound.sol | 54 ++++ assets/eip-5058/factory/EIP5058Factory.sol | 69 +++++ assets/eip-5058/factory/ERC721Bound.sol | 181 +++++++++++++ assets/eip-5058/factory/IEIP5058Factory.sol | 18 ++ assets/eip-5058/factory/IERC721Bound.sol | 26 ++ 8 files changed, 746 insertions(+), 1 deletion(-) create mode 100644 assets/eip-5058/ERC721Lockable.sol create mode 100644 assets/eip-5058/IERC721Lockable.sol create mode 100644 assets/eip-5058/extensions/EIP5058Bound.sol create mode 100644 assets/eip-5058/factory/EIP5058Factory.sol create mode 100644 assets/eip-5058/factory/ERC721Bound.sol create mode 100644 assets/eip-5058/factory/IEIP5058Factory.sol create mode 100644 assets/eip-5058/factory/IERC721Bound.sol diff --git a/EIPS/eip-5058.md b/EIPS/eip-5058.md index 0aa8de57f14f82..ae666c5fa89643 100644 --- a/EIPS/eip-5058.md +++ b/EIPS/eip-5058.md @@ -194,7 +194,7 @@ Test case written using hardhat: [here](https://github.com/radiocaca/ERC721L/blo ## Reference Implementation -You can find an implementation of this standard in the assets folder: [RadioCaca](https://github.com/radiocaca/ERC721L/tree/main/contracts/EIP5058). +You can find an implementation of this standard in the [assets](../assets/eip-5058) folder. ## Security Considerations diff --git a/assets/eip-5058/ERC721Lockable.sol b/assets/eip-5058/ERC721Lockable.sol new file mode 100644 index 00000000000000..6f3d274d38449d --- /dev/null +++ b/assets/eip-5058/ERC721Lockable.sol @@ -0,0 +1,274 @@ +// SPDX-License-Identifier: MIT +// Creator: tyler@radiocaca.com + +pragma solidity ^0.8.8; + +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import "./IERC721Lockable.sol"; + +/** + * @dev Implementation ERC721 Lockable Token + */ +abstract contract ERC721Lockable is ERC721, IERC721Lockable { + // Mapping from token ID to unlock time + mapping(uint256 => uint256) public lockedTokens; + + // Mapping from token ID to lock approved address + mapping(uint256 => address) private _lockApprovals; + + // Mapping from owner to lock operator approvals + mapping(address => mapping(address => bool)) private _lockOperatorApprovals; + + /** + * @dev See {IERC721Lockable-lockApprove}. + */ + function lockApprove(address to, uint256 tokenId) public virtual override { + require(!isLocked(tokenId), "ERC721L: token is locked"); + address owner = ERC721.ownerOf(tokenId); + require(to != owner, "ERC721L: lock approval to current owner"); + + require( + _msgSender() == owner || isLockApprovedForAll(owner, _msgSender()), + "ERC721L: lock approve caller is not owner nor approved for all" + ); + + _lockApprove(owner, to, tokenId); + } + + /** + * @dev See {IERC721Lockable-getLockApproved}. + */ + function getLockApproved(uint256 tokenId) public view virtual override returns (address) { + require(_exists(tokenId), "ERC721L: lock approved query for nonexistent token"); + + return _lockApprovals[tokenId]; + } + + /** + * @dev See {IERC721Lockable-lockerOf}. + */ + function lockerOf(uint256 tokenId) public view virtual override returns (address) { + require(_exists(tokenId), "ERC721L: locker query for nonexistent token"); + require(isLocked(tokenId), "ERC721L: locker query for non-locked token"); + + return _lockApprovals[tokenId]; + } + + /** + * @dev See {IERC721Lockable-setLockApprovalForAll}. + */ + function setLockApprovalForAll(address operator, bool approved) public virtual override { + _setLockApprovalForAll(_msgSender(), operator, approved); + } + + /** + * @dev See {IERC721Lockable-isLockApprovedForAll}. + */ + function isLockApprovedForAll(address owner, address operator) public view virtual override returns (bool) { + return _lockOperatorApprovals[owner][operator]; + } + + /** + * @dev See {IERC721Lockable-isLocked}. + */ + function isLocked(uint256 tokenId) public view virtual override returns (bool) { + return lockedTokens[tokenId] > block.number; + } + + /** + * @dev See {IERC721Lockable-lockFrom}. + */ + function lockFrom( + address from, + uint256 tokenId, + uint256 expired + ) public virtual override { + //solhint-disable-next-line max-line-length + require(_isLockApprovedOrOwner(_msgSender(), tokenId), "ERC721L: lock caller is not owner nor approved"); + require(expired > block.number, "ERC721L: expired time must be greater than current block number"); + require(!isLocked(tokenId), "ERC721L: token is locked"); + + _lock(_msgSender(), from, tokenId, expired); + } + + /** + * @dev See {IERC721Lockable-unlockFrom}. + */ + function unlockFrom(address from, uint256 tokenId) public virtual override { + require(lockerOf(tokenId) == _msgSender(), "ERC721L: unlock caller is not lock operator"); + require(ERC721.ownerOf(tokenId) == from, "ERC721L: unlock from incorrect owner"); + + _beforeTokenLock(_msgSender(), from, tokenId, 0); + + delete lockedTokens[tokenId]; + + emit Unlocked(_msgSender(), from, tokenId); + + _afterTokenLock(_msgSender(), from, tokenId, 0); + } + + /** + * @dev Locks `tokenId` from `from` until `expired`. + * + * Requirements: + * + * - `tokenId` token must be owned by `from`. + * + * Emits a {Locked} event. + */ + function _lock( + address operator, + address from, + uint256 tokenId, + uint256 expired + ) internal virtual { + require(ERC721.ownerOf(tokenId) == from, "ERC721L: lock from incorrect owner"); + + _beforeTokenLock(operator, from, tokenId, expired); + + lockedTokens[tokenId] = expired; + _lockApprovals[tokenId] = _msgSender(); + + emit Locked(operator, from, tokenId, expired); + + _afterTokenLock(operator, from, tokenId, expired); + } + + /** + * @dev Safely mints `tokenId` and transfers it to `to`, but the `tokenId` is locked and cannot be transferred. + * + * Requirements: + * + * - `tokenId` must not exist. + * + * Emits {Locked} and {Transfer} event. + */ + function _safeLockMint( + address to, + uint256 tokenId, + uint256 expired, + bytes memory _data + ) internal virtual { + require(expired > block.number, "ERC721L: lock mint for invalid lock block number"); + + _safeMint(to, tokenId, _data); + + _lock(address(0), to, tokenId, expired); + } + + /** + * @dev See {ERC721-_burn}. This override additionally clears the lock approvals for the token. + */ + function _burn(uint256 tokenId) internal virtual override { + address owner = ERC721.ownerOf(tokenId); + super._burn(tokenId); + + _beforeTokenLock(_msgSender(), owner, tokenId, 0); + + // clear lock approvals + delete lockedTokens[tokenId]; + delete _lockApprovals[tokenId]; + + _afterTokenLock(_msgSender(), owner, tokenId, 0); + } + + /** + * @dev Approve `to` to lock operate on `tokenId` + * + * Emits a {LockApproval} event. + */ + function _lockApprove( + address owner, + address to, + uint256 tokenId + ) internal virtual { + _lockApprovals[tokenId] = to; + emit LockApproval(owner, to, tokenId); + } + + /** + * @dev Approve `operator` to lock operate on all of `owner` tokens + * + * Emits a {LockApprovalForAll} event. + */ + function _setLockApprovalForAll( + address owner, + address operator, + bool approved + ) internal virtual { + require(owner != operator, "ERC721L: lock approve to caller"); + _lockOperatorApprovals[owner][operator] = approved; + emit LockApprovalForAll(owner, operator, approved); + } + + /** + * @dev Returns whether `spender` is allowed to lock `tokenId`. + * + * Requirements: + * + * - `tokenId` must exist. + */ + function _isLockApprovedOrOwner(address spender, uint256 tokenId) internal view virtual returns (bool) { + require(_exists(tokenId), "ERC721L: lock operator query for nonexistent token"); + address owner = ERC721.ownerOf(tokenId); + return (spender == owner || isLockApprovedForAll(owner, spender) || getLockApproved(tokenId) == spender); + } + + /** + * @dev See {ERC721-_beforeTokenTransfer}. + * + * Requirements: + * + * - the `tokenId` must not be locked. + */ + function _beforeTokenTransfer( + address from, + address to, + uint256 tokenId + ) internal virtual override { + super._beforeTokenTransfer(from, to, tokenId); + + require(!isLocked(tokenId), "ERC721L: token transfer while locked"); + } + + /** + * @dev Hook that is called before any token lock/unlock. + * + * Calling conditions: + * + * - `from` is non-zero. + * - When `expired` is zero, `tokenId` will be unlock for `from`. + * - When `expired` is non-zero, ``from``'s `tokenId` will be locked. + * + */ + function _beforeTokenLock( + address operator, + address from, + uint256 tokenId, + uint256 expired + ) internal virtual {} + + /** + * @dev Hook that is called after any lock/unlock of tokens. + * + * Calling conditions: + * + * - `from` is non-zero. + * - When `expired` is zero, `tokenId` will be unlock for `from`. + * - When `expired` is non-zero, ``from``'s `tokenId` will be locked. + * + */ + function _afterTokenLock( + address operator, + address from, + uint256 tokenId, + uint256 expired + ) internal virtual {} + + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC721) returns (bool) { + return interfaceId == type(IERC721Lockable).interfaceId || super.supportsInterface(interfaceId); + } +} diff --git a/assets/eip-5058/IERC721Lockable.sol b/assets/eip-5058/IERC721Lockable.sol new file mode 100644 index 00000000000000..808b2237195ca9 --- /dev/null +++ b/assets/eip-5058/IERC721Lockable.sol @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: MIT +// Creator: tyler@radiocaca.com + +pragma solidity ^0.8.8; + +import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; + +/** + * @dev ERC-721 Non-Fungible Token Standard, optional lockable extension + * ERC721 Token that can be locked for a certain period and cannot be transferred. + * This is designed for a non-escrow staking contract that comes later to lock a user's NFT + * while still letting them keep it in their wallet. + * This extension can ensure the security of user tokens during the staking period. + * If the nft lending protocol is compatible with this extension, the trouble caused by the NFT + * airdrop can be avoided, because the airdrop is still in the user's wallet + */ +interface IERC721Lockable is IERC721 { + /** + * @dev Emitted when `tokenId` token is locked by `operator` from `from`. + */ + event Locked(address indexed operator, address indexed from, uint256 indexed tokenId, uint256 expired); + + /** + * @dev Emitted when `tokenId` token is unlocked by `operator` from `from`. + */ + event Unlocked(address indexed operator, address indexed from, uint256 indexed tokenId); + + /** + * @dev Emitted when `owner` enables `approved` to lock the `tokenId` token. + */ + event LockApproval(address indexed owner, address indexed approved, uint256 indexed tokenId); + + /** + * @dev Emitted when `owner` enables or disables (`approved`) `operator` to lock all of its tokens. + */ + event LockApprovalForAll(address indexed owner, address indexed operator, bool approved); + + /** + * @dev Returns the locker who is locking the `tokenId` token. + * + * Requirements: + * + * - `tokenId` must exist. + */ + function lockerOf(uint256 tokenId) external view returns (address locker); + + /** + * @dev Lock `tokenId` token until the block number is greater than `expired` to be unlocked. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `tokenId` token must be owned by `from`. + * - `expired` must be greater than block.number + * - If the caller is not `from`, it must be approved to lock this token + * by either {lockApprove} or {setLockApprovalForAll}. + * + * Emits a {Locked} event. + */ + function lockFrom( + address from, + uint256 tokenId, + uint256 expired + ) external; + + /** + * @dev Unlock `tokenId` token. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `tokenId` token must be owned by `from`. + * - the caller must be the operator who locks the token by {lockFrom} + * + * Emits a {Unlocked} event. + */ + function unlockFrom(address from, uint256 tokenId) external; + + /** + * @dev Gives permission to `to` to lock `tokenId` token. + * + * Requirements: + * + * - The caller must own the token or be an approved lock operator. + * - `tokenId` must exist. + * + * Emits an {LockApproval} event. + */ + function lockApprove(address to, uint256 tokenId) external; + + /** + * @dev Approve or remove `operator` as an lock operator for the caller. + * Operators can call {lockFrom} for any token owned by the caller. + * + * Requirements: + * + * - The `operator` cannot be the caller. + * + * Emits an {LockApprovalForAll} event. + */ + function setLockApprovalForAll(address operator, bool approved) external; + + /** + * @dev Returns the account lock approved for `tokenId` token. + * + * Requirements: + * + * - `tokenId` must exist. + */ + function getLockApproved(uint256 tokenId) external view returns (address operator); + + /** + * @dev Returns if the `operator` is allowed to lock all of the assets of `owner`. + * + * See {setLockApprovalForAll} + */ + function isLockApprovedForAll(address owner, address operator) external view returns (bool); + + /** + * @dev Returns if the `tokenId` token is locked. + */ + function isLocked(uint256 tokenId) external view returns (bool); +} diff --git a/assets/eip-5058/extensions/EIP5058Bound.sol b/assets/eip-5058/extensions/EIP5058Bound.sol new file mode 100644 index 00000000000000..c715c0999d6895 --- /dev/null +++ b/assets/eip-5058/extensions/EIP5058Bound.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT +// Creator: tyler@radiocaca.com + +pragma solidity ^0.8.8; + +import "../factory/IEIP5058Factory.sol"; +import "../factory/IERC721Bound.sol"; +import "../ERC721Lockable.sol"; + +abstract contract EIP5058Bound is ERC721Lockable { + address public bound; + + function _setFactory(address _factory) internal { + bound = IEIP5058Factory(_factory).boundOf(address(this)); + } + + function _setBoundBaseTokenURI(string memory uri) internal { + IERC721Bound(bound).setBaseTokenURI(uri); + } + + function _setBoundContractURI(string memory uri) internal { + IERC721Bound(bound).setContractURI(uri); + } + + function burnBound(uint256 tokenId) external { + IERC721Bound(bound).burn(tokenId); + } + + // NOTE: + // + // this will be called when `lockFrom` or `unlockFrom` + function _afterTokenLock( + address operator, + address from, + uint256 tokenId, + uint256 expired + ) internal virtual override { + super._afterTokenLock(operator, from, tokenId, expired); + + if (bound != address(0)) { + if (expired != 0) { + // lock mint + if (operator != address(0)) { + IERC721Bound(bound).safeMint(msg.sender, tokenId, ""); + } + } else { + // unlock + if (IERC721Bound(bound).exists(tokenId)) { + IERC721Bound(bound).burn(tokenId); + } + } + } + } +} diff --git a/assets/eip-5058/factory/EIP5058Factory.sol b/assets/eip-5058/factory/EIP5058Factory.sol new file mode 100644 index 00000000000000..54c64523f4b0a5 --- /dev/null +++ b/assets/eip-5058/factory/EIP5058Factory.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: MIT +// Creator: tyler@radiocaca.com + +pragma solidity ^0.8.8; + +import "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; +import "./ERC721Bound.sol"; +import "./IEIP5058Factory.sol"; + +contract EIP5058Factory is IEIP5058Factory { + address[] private _allBounds; + + // Mapping from preimage to bound + mapping(address => address) private _bounds; + + function allBoundsLength() public view virtual override returns (uint256) { + return _allBounds.length; + } + + function boundByIndex(uint256 index) public view virtual override returns (address) { + require(index < _allBounds.length, "EIP5058Factory: index out of bounds"); + + return _allBounds[index]; + } + + function existBound(address preimage) public view virtual override returns (bool) { + return _bounds[preimage] != address(0); + } + + function boundOf(address preimage) public view virtual override returns (address) { + require(existBound(preimage), "EIP5058Factory: query for nonexistent bound"); + return _bounds[preimage]; + } + + function boundDeploy(address preimage) public virtual override returns (address) { + require(!existBound(preimage), "EIP5058Factory: bound nft is already deployed"); + + return _deploy(preimage, keccak256(abi.encode(preimage)), "Bound"); + } + + function _deploy( + address preimage, + bytes32 salt, + bytes memory prefix + ) internal returns (address) { + IERC721Metadata collection = IERC721Metadata(preimage); + bytes memory code = type(ERC721Bound).creationCode; + bytes memory bytecode = abi.encodePacked( + code, + abi.encode( + preimage, + abi.encodePacked(prefix, " ", collection.name()), + abi.encodePacked(prefix, collection.symbol()) + ) + ); + + address addr; + assembly { + addr := create2(0, add(bytecode, 0x20), mload(bytecode), salt) + } + + emit DeployedBound(preimage, addr); + + _bounds[preimage] = addr; + _allBounds.push(addr); + + return addr; + } +} diff --git a/assets/eip-5058/factory/ERC721Bound.sol b/assets/eip-5058/factory/ERC721Bound.sol new file mode 100644 index 00000000000000..ade71da598ca8f --- /dev/null +++ b/assets/eip-5058/factory/ERC721Bound.sol @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: MIT +// Creator: tyler@radiocaca.com + +pragma solidity ^0.8.8; + +import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; +import "@openzeppelin/contracts/interfaces/IERC2981.sol"; +import "./IERC721Bound.sol"; + +interface IPreimage { + /** + * @dev Returns if the `tokenId` token of preimage is locked. [MUST] + */ + function isLocked(uint256 tokenId) external view returns (bool); + + /** + * @dev Opensea-contract-level metadata. [OPTIONAL] + * Details: https://docs.opensea.io/docs/contract-level-metadata + */ + function contractURI() external view returns (string memory); +} + +/** + * @dev This implements an optional extension of {ERC721Lockable} defined in the EIP. + * The bound token is exactly the same as the locked token metadata, the bound token can be transferred, + * but it is guaranteed that only one bound token and the original token can be traded in the market at + * the same time. When the original token lock expires, the bound token must be destroyed. + */ +contract ERC721Bound is ERC721Enumerable, IERC2981, IERC721Bound { + address private _preimage; + + string private _contractURI; + + string private _baseTokenURI; + + constructor( + address preimage_, + string memory name_, + string memory symbol_ + ) ERC721(name_, symbol_) { + _preimage = preimage_; + } + + /** + * @dev Throws if called by any account other than the preimage. + */ + modifier onlyPreimage() { + require(_preimage == msg.sender, "ERC721Bound: caller is not the preimage"); + _; + } + + function preimage() public view virtual override returns (address) { + return _preimage; + } + + /** + * @dev See {ERC721-_baseURI}. + */ + function _baseURI() internal view virtual override returns (string memory) { + return _baseTokenURI; + } + + /** + * @dev See {IERC721Metadata-tokenURI}. + */ + function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { + if (bytes(_baseTokenURI).length > 0) { + return super.tokenURI(tokenId); + } + + return IERC721Metadata(_preimage).tokenURI(tokenId); + } + + /** + * @dev See {IERC2981-royaltyInfo}. + */ + function royaltyInfo(uint256 tokenId, uint256 salePrice) public view virtual override returns (address, uint256) { + return IERC2981(_preimage).royaltyInfo(tokenId, salePrice); + } + + /** + * @dev See {IPreimage-contractURI}. + */ + function contractURI() public view returns (string memory) { + if (bytes(_contractURI).length > 0) { + return _contractURI; + } + + if (IERC165(_preimage).supportsInterface(IPreimage.contractURI.selector)) { + return IPreimage(_preimage).contractURI(); + } + + return ""; + } + + /** + * @dev Returns whether `tokenId` exists. + */ + function exists(uint256 tokenId) public view returns (bool) { + return _exists(tokenId); + } + + // @dev Sets the base token URI prefix. + function setBaseTokenURI(string memory baseTokenURI) public virtual override onlyPreimage { + _baseTokenURI = baseTokenURI; + } + + // @dev Sets the contract URI. + function setContractURI(string memory uri) public virtual override onlyPreimage { + _contractURI = uri; + } + + /** + * @dev Mints bound `tokenId` and transfers it to `to`. + * + * Requirements: + * + * - `tokenId` must not exist. + * - `to` cannot be the zero address. + * caller must be preimage contract. + * + * Emits a {Transfer} event. + */ + function safeMint( + address to, + uint256 tokenId, + bytes memory data + ) public virtual override onlyPreimage { + _safeMint(to, tokenId, data); + } + + /** + * @dev Destroys `tokenId`. + * The approval is cleared when the token is burned. + * + * Requirements: + * + * - `tokenId` must exist. + * caller must be preimage contract. + * + * Emits a {Transfer} event. + */ + function burn(uint256 tokenId) public virtual override onlyPreimage { + _burn(tokenId); + } + + /** + * @dev See {ERC721-_beforeTokenTransfer}. + */ + function _beforeTokenTransfer( + address from, + address to, + uint256 tokenId + ) internal virtual override { + super._beforeTokenTransfer(from, to, tokenId); + + if (from == address(0)) { + require(IPreimage(_preimage).isLocked(tokenId), "ERC721Bound: token mint while preimage not locked"); + } + if (to == address(0)) { + require(!IPreimage(_preimage).isLocked(tokenId), "ERC721Bound: token burn while preimage locked"); + } + } + + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(IERC165, ERC721Enumerable) + returns (bool) + { + return + interfaceId == type(IERC721Bound).interfaceId || + interfaceId == type(IERC2981).interfaceId || + interfaceId == IPreimage.contractURI.selector || + super.supportsInterface(interfaceId); + } +} diff --git a/assets/eip-5058/factory/IEIP5058Factory.sol b/assets/eip-5058/factory/IEIP5058Factory.sol new file mode 100644 index 00000000000000..20181b2c00ebff --- /dev/null +++ b/assets/eip-5058/factory/IEIP5058Factory.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +// Creator: tyler@radiocaca.com + +pragma solidity ^0.8.8; + +interface IEIP5058Factory { + event DeployedBound(address indexed preimage, address bound); + + function allBoundsLength() external view returns (uint256); + + function boundByIndex(uint256 index) external view returns (address); + + function existBound(address preimage) external view returns (bool); + + function boundOf(address preimage) external view returns (address); + + function boundDeploy(address preimage) external returns (address); +} diff --git a/assets/eip-5058/factory/IERC721Bound.sol b/assets/eip-5058/factory/IERC721Bound.sol new file mode 100644 index 00000000000000..817e50d5e44755 --- /dev/null +++ b/assets/eip-5058/factory/IERC721Bound.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +// Creator: tyler@radiocaca.com + +pragma solidity ^0.8.8; + +import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; + +interface IERC721Bound is IERC721 { + function preimage() external view returns (address); + + function contractURI() external view returns (string memory); + + function exists(uint256 tokenId) external view returns (bool); + + function setBaseTokenURI(string memory _baseTokenURI) external; + + function setContractURI(string memory uri) external; + + function safeMint( + address to, + uint256 tokenId, + bytes memory data + ) external; + + function burn(uint256 tokenId) external; +} From 07f7fe6036137109abe11b28ded0b2422a9477af Mon Sep 17 00:00:00 2001 From: radiocaca <84221999+radiocaca@users.noreply.github.com> Date: Thu, 12 May 2022 00:58:54 +0800 Subject: [PATCH 09/40] Update assets/eip-5058/factory/IERC721Bound.sol Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> --- assets/eip-5058/factory/IERC721Bound.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/eip-5058/factory/IERC721Bound.sol b/assets/eip-5058/factory/IERC721Bound.sol index 817e50d5e44755..47feb80cabb0c7 100644 --- a/assets/eip-5058/factory/IERC721Bound.sol +++ b/assets/eip-5058/factory/IERC721Bound.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: CC0-1.0 // Creator: tyler@radiocaca.com pragma solidity ^0.8.8; From 456c60ee2b5a056715faf03004f96607891cde99 Mon Sep 17 00:00:00 2001 From: radiocaca <84221999+radiocaca@users.noreply.github.com> Date: Thu, 12 May 2022 00:59:16 +0800 Subject: [PATCH 10/40] Update assets/eip-5058/ERC721Lockable.sol Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> --- assets/eip-5058/ERC721Lockable.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/eip-5058/ERC721Lockable.sol b/assets/eip-5058/ERC721Lockable.sol index 6f3d274d38449d..f711a66f1880e2 100644 --- a/assets/eip-5058/ERC721Lockable.sol +++ b/assets/eip-5058/ERC721Lockable.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: CC0-1.0 // Creator: tyler@radiocaca.com pragma solidity ^0.8.8; From cdbe4397e85759da6c58a528137af7a9ede17279 Mon Sep 17 00:00:00 2001 From: radiocaca <84221999+radiocaca@users.noreply.github.com> Date: Thu, 12 May 2022 01:00:13 +0800 Subject: [PATCH 11/40] Update assets/eip-5058/IERC721Lockable.sol Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> --- assets/eip-5058/IERC721Lockable.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/eip-5058/IERC721Lockable.sol b/assets/eip-5058/IERC721Lockable.sol index 808b2237195ca9..f5f396dcb5dc46 100644 --- a/assets/eip-5058/IERC721Lockable.sol +++ b/assets/eip-5058/IERC721Lockable.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: CC0-1.0 // Creator: tyler@radiocaca.com pragma solidity ^0.8.8; From 2a7e7377f1ad51db3d446f0eb9e8c7e23331080f Mon Sep 17 00:00:00 2001 From: radiocaca <84221999+radiocaca@users.noreply.github.com> Date: Thu, 12 May 2022 01:00:21 +0800 Subject: [PATCH 12/40] Update assets/eip-5058/factory/IEIP5058Factory.sol Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> --- assets/eip-5058/factory/IEIP5058Factory.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/eip-5058/factory/IEIP5058Factory.sol b/assets/eip-5058/factory/IEIP5058Factory.sol index 20181b2c00ebff..17f0ff98904a83 100644 --- a/assets/eip-5058/factory/IEIP5058Factory.sol +++ b/assets/eip-5058/factory/IEIP5058Factory.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: CC0-1.0 // Creator: tyler@radiocaca.com pragma solidity ^0.8.8; From fcd52e2e8ec8ef7b3e5639d23d55dc6c158f3ba3 Mon Sep 17 00:00:00 2001 From: radiocaca <84221999+radiocaca@users.noreply.github.com> Date: Thu, 12 May 2022 01:00:29 +0800 Subject: [PATCH 13/40] Update assets/eip-5058/factory/ERC721Bound.sol Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> --- assets/eip-5058/factory/ERC721Bound.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/eip-5058/factory/ERC721Bound.sol b/assets/eip-5058/factory/ERC721Bound.sol index ade71da598ca8f..e1789a7871c0d0 100644 --- a/assets/eip-5058/factory/ERC721Bound.sol +++ b/assets/eip-5058/factory/ERC721Bound.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: CC0-1.0 // Creator: tyler@radiocaca.com pragma solidity ^0.8.8; From 5415e9c3af98f53347ab8fd22715e9a9138c20d1 Mon Sep 17 00:00:00 2001 From: radiocaca <84221999+radiocaca@users.noreply.github.com> Date: Thu, 12 May 2022 01:00:42 +0800 Subject: [PATCH 14/40] Update assets/eip-5058/factory/EIP5058Factory.sol Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> --- assets/eip-5058/factory/EIP5058Factory.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/eip-5058/factory/EIP5058Factory.sol b/assets/eip-5058/factory/EIP5058Factory.sol index 54c64523f4b0a5..abf4bfd33f4e67 100644 --- a/assets/eip-5058/factory/EIP5058Factory.sol +++ b/assets/eip-5058/factory/EIP5058Factory.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: CC0-1.0 // Creator: tyler@radiocaca.com pragma solidity ^0.8.8; From 2c29f8c8185afa2d5837245de75ef9819fbeda41 Mon Sep 17 00:00:00 2001 From: radiocaca <84221999+radiocaca@users.noreply.github.com> Date: Thu, 12 May 2022 01:01:02 +0800 Subject: [PATCH 15/40] Update assets/eip-5058/extensions/EIP5058Bound.sol Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> --- assets/eip-5058/extensions/EIP5058Bound.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/eip-5058/extensions/EIP5058Bound.sol b/assets/eip-5058/extensions/EIP5058Bound.sol index c715c0999d6895..07552aa03e12ac 100644 --- a/assets/eip-5058/extensions/EIP5058Bound.sol +++ b/assets/eip-5058/extensions/EIP5058Bound.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: CC0-1.0 // Creator: tyler@radiocaca.com pragma solidity ^0.8.8; From 82b65848b1264bc4cec4b1d38005088e329f31d0 Mon Sep 17 00:00:00 2001 From: radiocaca Date: Thu, 12 May 2022 01:21:03 +0800 Subject: [PATCH 16/40] rename to ERC5058 --- EIPS/eip-5058.md | 2 +- assets/eip-5058/{ERC721Lockable.sol => ERC5058.sol} | 9 ++++----- assets/eip-5058/{IERC721Lockable.sol => IERC5058.sol} | 4 ++-- .../extensions/{EIP5058Bound.sol => ERC5058Bound.sol} | 11 +++++------ .../{EIP5058Factory.sol => ERC5058Factory.sol} | 7 +++---- assets/eip-5058/factory/ERC721Bound.sol | 3 +-- .../{IEIP5058Factory.sol => IERC5058Factory.sol} | 4 ++-- assets/eip-5058/factory/IERC721Bound.sol | 3 +-- 8 files changed, 19 insertions(+), 24 deletions(-) rename assets/eip-5058/{ERC721Lockable.sol => ERC5058.sol} (96%) rename assets/eip-5058/{IERC721Lockable.sol => IERC5058.sol} (98%) rename assets/eip-5058/extensions/{EIP5058Bound.sol => ERC5058Bound.sol} (83%) rename assets/eip-5058/factory/{EIP5058Factory.sol => ERC5058Factory.sol} (93%) rename assets/eip-5058/factory/{IEIP5058Factory.sol => IERC5058Factory.sol} (90%) diff --git a/EIPS/eip-5058.md b/EIPS/eip-5058.md index ae666c5fa89643..1c2f9acb9ac4e0 100644 --- a/EIPS/eip-5058.md +++ b/EIPS/eip-5058.md @@ -194,7 +194,7 @@ Test case written using hardhat: [here](https://github.com/radiocaca/ERC721L/blo ## Reference Implementation -You can find an implementation of this standard in the [assets](../assets/eip-5058) folder. +You can find an implementation of this standard in the [assets](../assets/eip-5058/ERC5058.sol) folder. ## Security Considerations diff --git a/assets/eip-5058/ERC721Lockable.sol b/assets/eip-5058/ERC5058.sol similarity index 96% rename from assets/eip-5058/ERC721Lockable.sol rename to assets/eip-5058/ERC5058.sol index f711a66f1880e2..76002f20aa6f2c 100644 --- a/assets/eip-5058/ERC721Lockable.sol +++ b/assets/eip-5058/ERC5058.sol @@ -1,15 +1,14 @@ // SPDX-License-Identifier: CC0-1.0 -// Creator: tyler@radiocaca.com -pragma solidity ^0.8.8; +pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import "./IERC721Lockable.sol"; +import "./IERC5058.sol"; /** * @dev Implementation ERC721 Lockable Token */ -abstract contract ERC721Lockable is ERC721, IERC721Lockable { +abstract contract ERC5058 is ERC721, IERC5058 { // Mapping from token ID to unlock time mapping(uint256 => uint256) public lockedTokens; @@ -269,6 +268,6 @@ abstract contract ERC721Lockable is ERC721, IERC721Lockable { * @dev See {IERC165-supportsInterface}. */ function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC721) returns (bool) { - return interfaceId == type(IERC721Lockable).interfaceId || super.supportsInterface(interfaceId); + return interfaceId == type(IERC5058).interfaceId || super.supportsInterface(interfaceId); } } diff --git a/assets/eip-5058/IERC721Lockable.sol b/assets/eip-5058/IERC5058.sol similarity index 98% rename from assets/eip-5058/IERC721Lockable.sol rename to assets/eip-5058/IERC5058.sol index f5f396dcb5dc46..6a5d3146a13989 100644 --- a/assets/eip-5058/IERC721Lockable.sol +++ b/assets/eip-5058/IERC5058.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: CC0-1.0 // Creator: tyler@radiocaca.com -pragma solidity ^0.8.8; +pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; @@ -14,7 +14,7 @@ import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; * If the nft lending protocol is compatible with this extension, the trouble caused by the NFT * airdrop can be avoided, because the airdrop is still in the user's wallet */ -interface IERC721Lockable is IERC721 { +interface IERC5058 is IERC721 { /** * @dev Emitted when `tokenId` token is locked by `operator` from `from`. */ diff --git a/assets/eip-5058/extensions/EIP5058Bound.sol b/assets/eip-5058/extensions/ERC5058Bound.sol similarity index 83% rename from assets/eip-5058/extensions/EIP5058Bound.sol rename to assets/eip-5058/extensions/ERC5058Bound.sol index 07552aa03e12ac..12bd9adfeb1031 100644 --- a/assets/eip-5058/extensions/EIP5058Bound.sol +++ b/assets/eip-5058/extensions/ERC5058Bound.sol @@ -1,17 +1,16 @@ // SPDX-License-Identifier: CC0-1.0 -// Creator: tyler@radiocaca.com -pragma solidity ^0.8.8; +pragma solidity ^0.8.0; -import "../factory/IEIP5058Factory.sol"; +import "../factory/IERC5058Factory.sol"; import "../factory/IERC721Bound.sol"; -import "../ERC721Lockable.sol"; +import "../ERC5058.sol"; -abstract contract EIP5058Bound is ERC721Lockable { +abstract contract ERC5058Bound is ERC5058 { address public bound; function _setFactory(address _factory) internal { - bound = IEIP5058Factory(_factory).boundOf(address(this)); + bound = IERC5058Factory(_factory).boundOf(address(this)); } function _setBoundBaseTokenURI(string memory uri) internal { diff --git a/assets/eip-5058/factory/EIP5058Factory.sol b/assets/eip-5058/factory/ERC5058Factory.sol similarity index 93% rename from assets/eip-5058/factory/EIP5058Factory.sol rename to assets/eip-5058/factory/ERC5058Factory.sol index abf4bfd33f4e67..66b47b93401997 100644 --- a/assets/eip-5058/factory/EIP5058Factory.sol +++ b/assets/eip-5058/factory/ERC5058Factory.sol @@ -1,13 +1,12 @@ // SPDX-License-Identifier: CC0-1.0 -// Creator: tyler@radiocaca.com -pragma solidity ^0.8.8; +pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; import "./ERC721Bound.sol"; -import "./IEIP5058Factory.sol"; +import "./IERC5058Factory.sol"; -contract EIP5058Factory is IEIP5058Factory { +contract ERC5058Factory is IERC5058Factory { address[] private _allBounds; // Mapping from preimage to bound diff --git a/assets/eip-5058/factory/ERC721Bound.sol b/assets/eip-5058/factory/ERC721Bound.sol index e1789a7871c0d0..cce217d3d904eb 100644 --- a/assets/eip-5058/factory/ERC721Bound.sol +++ b/assets/eip-5058/factory/ERC721Bound.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: CC0-1.0 -// Creator: tyler@radiocaca.com -pragma solidity ^0.8.8; +pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; import "@openzeppelin/contracts/interfaces/IERC2981.sol"; diff --git a/assets/eip-5058/factory/IEIP5058Factory.sol b/assets/eip-5058/factory/IERC5058Factory.sol similarity index 90% rename from assets/eip-5058/factory/IEIP5058Factory.sol rename to assets/eip-5058/factory/IERC5058Factory.sol index 17f0ff98904a83..283abb5432a7c1 100644 --- a/assets/eip-5058/factory/IEIP5058Factory.sol +++ b/assets/eip-5058/factory/IERC5058Factory.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: CC0-1.0 // Creator: tyler@radiocaca.com -pragma solidity ^0.8.8; +pragma solidity ^0.8.0; -interface IEIP5058Factory { +interface IERC5058Factory { event DeployedBound(address indexed preimage, address bound); function allBoundsLength() external view returns (uint256); diff --git a/assets/eip-5058/factory/IERC721Bound.sol b/assets/eip-5058/factory/IERC721Bound.sol index 47feb80cabb0c7..e48ca9a2269fb7 100644 --- a/assets/eip-5058/factory/IERC721Bound.sol +++ b/assets/eip-5058/factory/IERC721Bound.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: CC0-1.0 -// Creator: tyler@radiocaca.com -pragma solidity ^0.8.8; +pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; From 9676777301f8292ec00196508268eb359ce55ac5 Mon Sep 17 00:00:00 2001 From: radiocaca Date: Thu, 12 May 2022 01:32:38 +0800 Subject: [PATCH 17/40] update revert domain --- assets/eip-5058/ERC5058.sol | 48 +++++++++++----------- assets/eip-5058/factory/ERC5058Factory.sol | 6 +-- assets/eip-5058/factory/ERC721Bound.sol | 2 +- 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/assets/eip-5058/ERC5058.sol b/assets/eip-5058/ERC5058.sol index 76002f20aa6f2c..17b7ad9b1831ec 100644 --- a/assets/eip-5058/ERC5058.sol +++ b/assets/eip-5058/ERC5058.sol @@ -19,63 +19,63 @@ abstract contract ERC5058 is ERC721, IERC5058 { mapping(address => mapping(address => bool)) private _lockOperatorApprovals; /** - * @dev See {IERC721Lockable-lockApprove}. + * @dev See {IERC5058-lockApprove}. */ function lockApprove(address to, uint256 tokenId) public virtual override { - require(!isLocked(tokenId), "ERC721L: token is locked"); + require(!isLocked(tokenId), "ERC5058: token is locked"); address owner = ERC721.ownerOf(tokenId); - require(to != owner, "ERC721L: lock approval to current owner"); + require(to != owner, "ERC5058: lock approval to current owner"); require( _msgSender() == owner || isLockApprovedForAll(owner, _msgSender()), - "ERC721L: lock approve caller is not owner nor approved for all" + "ERC5058: lock approve caller is not owner nor approved for all" ); _lockApprove(owner, to, tokenId); } /** - * @dev See {IERC721Lockable-getLockApproved}. + * @dev See {IERC5058-getLockApproved}. */ function getLockApproved(uint256 tokenId) public view virtual override returns (address) { - require(_exists(tokenId), "ERC721L: lock approved query for nonexistent token"); + require(_exists(tokenId), "ERC5058: lock approved query for nonexistent token"); return _lockApprovals[tokenId]; } /** - * @dev See {IERC721Lockable-lockerOf}. + * @dev See {IERC5058-lockerOf}. */ function lockerOf(uint256 tokenId) public view virtual override returns (address) { - require(_exists(tokenId), "ERC721L: locker query for nonexistent token"); - require(isLocked(tokenId), "ERC721L: locker query for non-locked token"); + require(_exists(tokenId), "ERC5058: locker query for nonexistent token"); + require(isLocked(tokenId), "ERC5058: locker query for non-locked token"); return _lockApprovals[tokenId]; } /** - * @dev See {IERC721Lockable-setLockApprovalForAll}. + * @dev See {IERC5058-setLockApprovalForAll}. */ function setLockApprovalForAll(address operator, bool approved) public virtual override { _setLockApprovalForAll(_msgSender(), operator, approved); } /** - * @dev See {IERC721Lockable-isLockApprovedForAll}. + * @dev See {IERC5058-isLockApprovedForAll}. */ function isLockApprovedForAll(address owner, address operator) public view virtual override returns (bool) { return _lockOperatorApprovals[owner][operator]; } /** - * @dev See {IERC721Lockable-isLocked}. + * @dev See {IERC5058-isLocked}. */ function isLocked(uint256 tokenId) public view virtual override returns (bool) { return lockedTokens[tokenId] > block.number; } /** - * @dev See {IERC721Lockable-lockFrom}. + * @dev See {IERC5058-lockFrom}. */ function lockFrom( address from, @@ -83,19 +83,19 @@ abstract contract ERC5058 is ERC721, IERC5058 { uint256 expired ) public virtual override { //solhint-disable-next-line max-line-length - require(_isLockApprovedOrOwner(_msgSender(), tokenId), "ERC721L: lock caller is not owner nor approved"); - require(expired > block.number, "ERC721L: expired time must be greater than current block number"); - require(!isLocked(tokenId), "ERC721L: token is locked"); + require(_isLockApprovedOrOwner(_msgSender(), tokenId), "ERC5058: lock caller is not owner nor approved"); + require(expired > block.number, "ERC5058: expired time must be greater than current block number"); + require(!isLocked(tokenId), "ERC5058: token is locked"); _lock(_msgSender(), from, tokenId, expired); } /** - * @dev See {IERC721Lockable-unlockFrom}. + * @dev See {IERC5058-unlockFrom}. */ function unlockFrom(address from, uint256 tokenId) public virtual override { - require(lockerOf(tokenId) == _msgSender(), "ERC721L: unlock caller is not lock operator"); - require(ERC721.ownerOf(tokenId) == from, "ERC721L: unlock from incorrect owner"); + require(lockerOf(tokenId) == _msgSender(), "ERC5058: unlock caller is not lock operator"); + require(ERC721.ownerOf(tokenId) == from, "ERC5058: unlock from incorrect owner"); _beforeTokenLock(_msgSender(), from, tokenId, 0); @@ -121,7 +121,7 @@ abstract contract ERC5058 is ERC721, IERC5058 { uint256 tokenId, uint256 expired ) internal virtual { - require(ERC721.ownerOf(tokenId) == from, "ERC721L: lock from incorrect owner"); + require(ERC721.ownerOf(tokenId) == from, "ERC5058: lock from incorrect owner"); _beforeTokenLock(operator, from, tokenId, expired); @@ -148,7 +148,7 @@ abstract contract ERC5058 is ERC721, IERC5058 { uint256 expired, bytes memory _data ) internal virtual { - require(expired > block.number, "ERC721L: lock mint for invalid lock block number"); + require(expired > block.number, "ERC5058: lock mint for invalid lock block number"); _safeMint(to, tokenId, _data); @@ -195,7 +195,7 @@ abstract contract ERC5058 is ERC721, IERC5058 { address operator, bool approved ) internal virtual { - require(owner != operator, "ERC721L: lock approve to caller"); + require(owner != operator, "ERC5058: lock approve to caller"); _lockOperatorApprovals[owner][operator] = approved; emit LockApprovalForAll(owner, operator, approved); } @@ -208,7 +208,7 @@ abstract contract ERC5058 is ERC721, IERC5058 { * - `tokenId` must exist. */ function _isLockApprovedOrOwner(address spender, uint256 tokenId) internal view virtual returns (bool) { - require(_exists(tokenId), "ERC721L: lock operator query for nonexistent token"); + require(_exists(tokenId), "ERC5058: lock operator query for nonexistent token"); address owner = ERC721.ownerOf(tokenId); return (spender == owner || isLockApprovedForAll(owner, spender) || getLockApproved(tokenId) == spender); } @@ -227,7 +227,7 @@ abstract contract ERC5058 is ERC721, IERC5058 { ) internal virtual override { super._beforeTokenTransfer(from, to, tokenId); - require(!isLocked(tokenId), "ERC721L: token transfer while locked"); + require(!isLocked(tokenId), "ERC5058: token transfer while locked"); } /** diff --git a/assets/eip-5058/factory/ERC5058Factory.sol b/assets/eip-5058/factory/ERC5058Factory.sol index 66b47b93401997..571e029bace68b 100644 --- a/assets/eip-5058/factory/ERC5058Factory.sol +++ b/assets/eip-5058/factory/ERC5058Factory.sol @@ -17,7 +17,7 @@ contract ERC5058Factory is IERC5058Factory { } function boundByIndex(uint256 index) public view virtual override returns (address) { - require(index < _allBounds.length, "EIP5058Factory: index out of bounds"); + require(index < _allBounds.length, "ERC5058Factory: index out of bounds"); return _allBounds[index]; } @@ -27,12 +27,12 @@ contract ERC5058Factory is IERC5058Factory { } function boundOf(address preimage) public view virtual override returns (address) { - require(existBound(preimage), "EIP5058Factory: query for nonexistent bound"); + require(existBound(preimage), "ERC5058Factory: query for nonexistent bound"); return _bounds[preimage]; } function boundDeploy(address preimage) public virtual override returns (address) { - require(!existBound(preimage), "EIP5058Factory: bound nft is already deployed"); + require(!existBound(preimage), "ERC5058Factory: bound nft is already deployed"); return _deploy(preimage, keccak256(abi.encode(preimage)), "Bound"); } diff --git a/assets/eip-5058/factory/ERC721Bound.sol b/assets/eip-5058/factory/ERC721Bound.sol index cce217d3d904eb..3532cf7cbcfd6e 100644 --- a/assets/eip-5058/factory/ERC721Bound.sol +++ b/assets/eip-5058/factory/ERC721Bound.sol @@ -20,7 +20,7 @@ interface IPreimage { } /** - * @dev This implements an optional extension of {ERC721Lockable} defined in the EIP. + * @dev This implements an optional extension of {ERC5058} defined in the EIP. * The bound token is exactly the same as the locked token metadata, the bound token can be transferred, * but it is guaranteed that only one bound token and the original token can be traded in the market at * the same time. When the original token lock expires, the bound token must be destroyed. From 9002b062ca590365c87e03eb7b8024dcbe018217 Mon Sep 17 00:00:00 2001 From: radiocaca <84221999+radiocaca@users.noreply.github.com> Date: Fri, 13 May 2022 23:15:53 +0800 Subject: [PATCH 18/40] Update EIPS/eip-5058.md Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> --- EIPS/eip-5058.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5058.md b/EIPS/eip-5058.md index 1c2f9acb9ac4e0..46c0bb4f6ac6d4 100644 --- a/EIPS/eip-5058.md +++ b/EIPS/eip-5058.md @@ -202,4 +202,4 @@ After being locked, the NFT can not be transferred, so before authorizing lockin ## Copyright -Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). +Copyright and related rights waived via [CC0](../LICENSE.md). From 06d1955e39f7dbb61abe5738192069b7e74b73fe Mon Sep 17 00:00:00 2001 From: radiocaca <84221999+radiocaca@users.noreply.github.com> Date: Tue, 17 May 2022 10:20:10 +0800 Subject: [PATCH 19/40] Update EIPS/eip-5058.md Co-authored-by: Junghyun Colin Kim --- EIPS/eip-5058.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5058.md b/EIPS/eip-5058.md index 46c0bb4f6ac6d4..84ada2bf96ad01 100644 --- a/EIPS/eip-5058.md +++ b/EIPS/eip-5058.md @@ -13,7 +13,7 @@ requires: 165, 721 ## Abstract -We propose to extend the ERC-721 standard with a secure locking mechanism. The NFT owners approve the operator to lock the NFT through `setLockApprovalForAll()` or `lockApprove()`. The apporved operator locks the NFT through `lockFrom()`. The locked NFTs cannot be transferred until the end of the locking period. An immediate use case is to allow NFTs to participate in smart contracts without leaving the wallets of their owners. +We propose to extend the ERC-721 standard with a secure locking mechanism. The NFT owners approve the operator to lock the NFT through `setLockApprovalForAll()` or `lockApprove()`. The approved operator locks the NFT through `lockFrom()`. The locked NFTs cannot be transferred until the end of the locking period. An immediate use case is to allow NFTs to participate in projects like [NFTFi](https://www.nftfi.com/) without changing the ownership of an NFT. ## Motivation From 38c75b8b913f1ec8e2c952c6939d344211c90915 Mon Sep 17 00:00:00 2001 From: radiocaca <84221999+radiocaca@users.noreply.github.com> Date: Tue, 17 May 2022 10:42:46 +0800 Subject: [PATCH 20/40] Update EIPS/eip-5058.md Co-authored-by: Junghyun Colin Kim --- EIPS/eip-5058.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5058.md b/EIPS/eip-5058.md index 84ada2bf96ad01..db5d96fa568524 100644 --- a/EIPS/eip-5058.md +++ b/EIPS/eip-5058.md @@ -171,7 +171,7 @@ interface IERC721Lockable is IERC721 { A NFT owner can give another trusted operator the right to lock his NFT through the approve functions. The `lockApprove()` function only approves for the specified NFT, whereas `setLockApprovalForAll()` approves for all NFTs of the collection under the wallet. When a user participates in a NFTFi project, the project contract calls `lockFrom()` to lock the user's NFT. Locked NFTs cannot be transferred, but the NFTFi project contract can use the unlock function `unlockFrom()` to unlock the NFT. ### NFT lock/unlock -Authorized project contract has permission to lock NFT through lockFrom method, locked NFT cannot be transferred, unless the lock time expires. Of course, the project contract also has permission to unlock NFT in advance through unlockFrom. Note: only the address of the locked NFT has permission to unlock NFT. +Authorized project contract has permission to lock an NFT through lockFrom method, the locked NFT cannot be transferred, unless the lock time expires. Of course, the project contract also has permission to unlock NFT in advance through unlockFrom. Note: only the address of the locked NFT has permission to unlock the NFT. ### NFT lock period From 135a48bd948ed0d1be8a8d973761d075cced8bbe Mon Sep 17 00:00:00 2001 From: radiocaca Date: Sun, 31 Jul 2022 13:22:54 +0800 Subject: [PATCH 21/40] remove all external links and add lockExpiredTime function --- EIPS/eip-5058.md | 13 +++++++++---- assets/eip-5058/ERC5058.sol | 14 +++++++++++--- assets/eip-5058/IERC5058.sol | 8 ++++++-- assets/eip-5058/extensions/ERC5058Bound.sol | 2 +- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/EIPS/eip-5058.md b/EIPS/eip-5058.md index db5d96fa568524..63b0b1ffe48038 100644 --- a/EIPS/eip-5058.md +++ b/EIPS/eip-5058.md @@ -13,11 +13,11 @@ requires: 165, 721 ## Abstract -We propose to extend the ERC-721 standard with a secure locking mechanism. The NFT owners approve the operator to lock the NFT through `setLockApprovalForAll()` or `lockApprove()`. The approved operator locks the NFT through `lockFrom()`. The locked NFTs cannot be transferred until the end of the locking period. An immediate use case is to allow NFTs to participate in projects like [NFTFi](https://www.nftfi.com/) without changing the ownership of an NFT. +We propose to extend the ERC-721 standard with a secure locking mechanism. The NFT owners approve the operator to lock the NFT through `setLockApprovalForAll()` or `lockApprove()`. The apporved operator locks the NFT through `lockFrom()`. The locked NFTs cannot be transferred until the end of the locking period. An immediate use case is to allow NFTs to participate in smart contracts without leaving the wallets of their owners. ## Motivation -NFTs, enabled by [ERC-721](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md), has been on the explosion in demand. The total market value and the ecosystem continue to grow with more and more blue chip NFTs, which are approximately equivalent to popular intellectual properties in a conventional sense. Despite the vast success, something is left to be desired. Liquidity has always been one of the biggest challenges for NFTs. Several attempts have been made to tackle the liquidity challenge: [NFTFi](https://www.nftfi.com/) and [BendDAO](https://www.benddao.xyz/), to name a few. Utilizing the currently prevalent ERC-721 standard, these projects require participating NFTs to be transferred to the projects' contracts, which poses inconveniences and risks to the owners: +NFTs, enabled by [ERC-721](./eip-721.md), has been on the explosion in demand. The total market value and the ecosystem continue to grow with more and more blue chip NFTs, which are approximately equivalent to popular intellectual properties in a conventional sense. Despite the vast success, something is left to be desired. Liquidity has always been one of the biggest challenges for NFTs. Several attempts have been made to tackle the liquidity challenge: NFTFi and BendDAO, to name a few. Utilizing the currently prevalent ERC-721 standard, these projects require participating NFTs to be transferred to the projects' contracts, which poses inconveniences and risks to the owners: 1. Smart contract risks: NFTs can be lost or stolen due to bugs or vulnerabilities in the contracts. 2. Loss of utility: NFTs have utility values, such as profile pictures and bragging rights, which are lost when the NFTs are no longer seen under the owners' custody. @@ -161,6 +161,11 @@ interface IERC721Lockable is IERC721 { * @dev Returns if the `tokenId` token is locked. */ function isLocked(uint256 tokenId) external view returns (bool); + + /** + * @dev Returns the `tokenId` token lock expired time. + */ + function lockExpiredTime(uint256 tokenId) external view returns (uint256); } ``` @@ -171,7 +176,7 @@ interface IERC721Lockable is IERC721 { A NFT owner can give another trusted operator the right to lock his NFT through the approve functions. The `lockApprove()` function only approves for the specified NFT, whereas `setLockApprovalForAll()` approves for all NFTs of the collection under the wallet. When a user participates in a NFTFi project, the project contract calls `lockFrom()` to lock the user's NFT. Locked NFTs cannot be transferred, but the NFTFi project contract can use the unlock function `unlockFrom()` to unlock the NFT. ### NFT lock/unlock -Authorized project contract has permission to lock an NFT through lockFrom method, the locked NFT cannot be transferred, unless the lock time expires. Of course, the project contract also has permission to unlock NFT in advance through unlockFrom. Note: only the address of the locked NFT has permission to unlock the NFT. +Authorized project contract has permission to lock NFT through lockFrom method, locked NFT cannot be transferred, unless the lock time expires. Of course, the project contract also has permission to unlock NFT in advance through unlockFrom. Note: only the address of the locked NFT has permission to unlock NFT. ### NFT lock period @@ -202,4 +207,4 @@ After being locked, the NFT can not be transferred, so before authorizing lockin ## Copyright -Copyright and related rights waived via [CC0](../LICENSE.md). +Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). diff --git a/assets/eip-5058/ERC5058.sol b/assets/eip-5058/ERC5058.sol index 17b7ad9b1831ec..534177953a5259 100644 --- a/assets/eip-5058/ERC5058.sol +++ b/assets/eip-5058/ERC5058.sol @@ -74,6 +74,13 @@ abstract contract ERC5058 is ERC721, IERC5058 { return lockedTokens[tokenId] > block.number; } + /** + * @dev See {IERC5058-lockExpiredTime}. + */ + function lockExpiredTime(uint256 tokenId) public view virtual override returns (uint256) { + return lockedTokens[tokenId]; + } + /** * @dev See {IERC5058-lockFrom}. */ @@ -91,11 +98,12 @@ abstract contract ERC5058 is ERC721, IERC5058 { } /** - * @dev See {IERC5058-unlockFrom}. + * @dev See {IERC5058-unlock}. */ - function unlockFrom(address from, uint256 tokenId) public virtual override { + function unlock(uint256 tokenId) public virtual override { require(lockerOf(tokenId) == _msgSender(), "ERC5058: unlock caller is not lock operator"); - require(ERC721.ownerOf(tokenId) == from, "ERC5058: unlock from incorrect owner"); + + address from = ERC721.ownerOf(tokenId); _beforeTokenLock(_msgSender(), from, tokenId, 0); diff --git a/assets/eip-5058/IERC5058.sol b/assets/eip-5058/IERC5058.sol index 6a5d3146a13989..9a9da130a78076 100644 --- a/assets/eip-5058/IERC5058.sol +++ b/assets/eip-5058/IERC5058.sol @@ -68,13 +68,12 @@ interface IERC5058 is IERC721 { * * Requirements: * - * - `from` cannot be the zero address. * - `tokenId` token must be owned by `from`. * - the caller must be the operator who locks the token by {lockFrom} * * Emits a {Unlocked} event. */ - function unlockFrom(address from, uint256 tokenId) external; + function unlock(uint256 tokenId) external; /** * @dev Gives permission to `to` to lock `tokenId` token. @@ -120,4 +119,9 @@ interface IERC5058 is IERC721 { * @dev Returns if the `tokenId` token is locked. */ function isLocked(uint256 tokenId) external view returns (bool); + + /** + * @dev Returns the `tokenId` token lock expired time. + */ + function lockExpiredTime(uint256 tokenId) external view returns (uint256); } diff --git a/assets/eip-5058/extensions/ERC5058Bound.sol b/assets/eip-5058/extensions/ERC5058Bound.sol index 12bd9adfeb1031..e2e64a1917c5c8 100644 --- a/assets/eip-5058/extensions/ERC5058Bound.sol +++ b/assets/eip-5058/extensions/ERC5058Bound.sol @@ -27,7 +27,7 @@ abstract contract ERC5058Bound is ERC5058 { // NOTE: // - // this will be called when `lockFrom` or `unlockFrom` + // this will be called when `lockFrom` or `unlock` function _afterTokenLock( address operator, address from, From df1c0645a0cf7bf96b18db57a6935561797ec309 Mon Sep 17 00:00:00 2001 From: radiocaca Date: Sun, 31 Jul 2022 13:57:26 +0800 Subject: [PATCH 22/40] add tests --- EIPS/eip-5058.md | 16 +-- assets/eip-5058/mock/EIP5058Mock.sol | 54 +++++++++ assets/eip-5058/test/test.ts | 157 +++++++++++++++++++++++++++ 3 files changed, 219 insertions(+), 8 deletions(-) create mode 100644 assets/eip-5058/mock/EIP5058Mock.sol create mode 100644 assets/eip-5058/test/test.ts diff --git a/EIPS/eip-5058.md b/EIPS/eip-5058.md index 63b0b1ffe48038..4278d231331f5d 100644 --- a/EIPS/eip-5058.md +++ b/EIPS/eip-5058.md @@ -13,17 +13,17 @@ requires: 165, 721 ## Abstract -We propose to extend the ERC-721 standard with a secure locking mechanism. The NFT owners approve the operator to lock the NFT through `setLockApprovalForAll()` or `lockApprove()`. The apporved operator locks the NFT through `lockFrom()`. The locked NFTs cannot be transferred until the end of the locking period. An immediate use case is to allow NFTs to participate in smart contracts without leaving the wallets of their owners. +We propose to extend the `ERC721` standard with a secure locking mechanism. The NFT owners approve the operator to lock the NFT through `setLockApprovalForAll()` or `lockApprove()`. The apporved operator locks the NFT through `lockFrom()`. The locked NFTs cannot be transferred until the end of the locking period. An immediate use case is to allow NFTs to participate in smart contracts without leaving the wallets of their owners. ## Motivation -NFTs, enabled by [ERC-721](./eip-721.md), has been on the explosion in demand. The total market value and the ecosystem continue to grow with more and more blue chip NFTs, which are approximately equivalent to popular intellectual properties in a conventional sense. Despite the vast success, something is left to be desired. Liquidity has always been one of the biggest challenges for NFTs. Several attempts have been made to tackle the liquidity challenge: NFTFi and BendDAO, to name a few. Utilizing the currently prevalent ERC-721 standard, these projects require participating NFTs to be transferred to the projects' contracts, which poses inconveniences and risks to the owners: +NFTs, enabled by [ERC721](./eip-721.md), has been on the explosion in demand. The total market value and the ecosystem continue to grow with more and more blue chip NFTs, which are approximately equivalent to popular intellectual properties in a conventional sense. Despite the vast success, something is left to be desired. Liquidity has always been one of the biggest challenges for NFTs. Several attempts have been made to tackle the liquidity challenge: NFTFi and BendDAO, to name a few. Utilizing the currently prevalent `ERC721` standard, these projects require participating NFTs to be transferred to the projects' contracts, which poses inconveniences and risks to the owners: 1. Smart contract risks: NFTs can be lost or stolen due to bugs or vulnerabilities in the contracts. 2. Loss of utility: NFTs have utility values, such as profile pictures and bragging rights, which are lost when the NFTs are no longer seen under the owners' custody. 3. Missing Airdrops: The owners can no longer directly receive airdrops entitled to the NFTs. Considering the values and price fluctuation of some of the airdrops, either missing or getting the airdrop not on time can financially impact the owners. -All-of-the-above are bad UX, and we believe the ERC-721 standard can be improved by adopting a native locking mechanism: +All-of-the-above are bad UX, and we believe the `ERC721` standard can be improved by adopting a native locking mechanism: 1. Instead of being transferred to a smart contract, a NFT remains being self-custody but locked. 2. While a NFT is locked, only the change of its ownership is disabled. Other utilities are not affected. @@ -31,13 +31,13 @@ All-of-the-above are bad UX, and we believe the ERC-721 standard can be improved The value of NFT can be reflected in two aspects: collection value and utility value. Collection value needs to ensure that the holder's wallet retains ownership of the NFT forever. Utility value requires ensuring that the holder can verify their NFT ownership in other projects. Both of these aspects require that the NFT be kept in the owner's wallet all the time. -The proposed standard allows the underlying NFT assets to be managed securely and conveniently and extends the ERC-721 standard to natively support of common NFTFi use cases including locking, staking, lending, and crowdfunding. We believe the proposed standard will encourage NFT owners to participate more actively in NFTFi projects and, hence, improve the livelihood of the whole NFT ecosystem. +The proposed standard allows the underlying NFT assets to be managed securely and conveniently and extends the `ERC721` standard to natively support of common NFTFi use cases including locking, staking, lending, and crowdfunding. We believe the proposed standard will encourage NFT owners to participate more actively in NFTFi projects and, hence, improve the livelihood of the whole NFT ecosystem. ## Specification The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119. -Lockable ERC-721 **MUST** implement the `IERC721Lockable` interfaces: +Lockable `ERC721` **MUST** implement the `IERC721Lockable` interfaces: ```solidity // SPDX-License-Identifier: CC0-1.0 @@ -187,15 +187,15 @@ Bound NFT is an extension of the EIP, which implements the ability to mint a bou BoundNFT can be used to lend, as a staking credential for the contract. The credential can be locked in the contract, but also to the user. In NFT leasing, boundNFT can be rented to users because boundNFT is essentially equivalent to NFT. This consensus, if accepted by all projects, boundNFT will bring more creativity to NFT. ### Bound NFT Factory -Bound NFT Factory is a common boundNFT factory, similar to uniswap's erc20-pairs factory. It uses the create2 method to create a boundNFT contract address for any NFT deterministic. BoundNFT contract that has been created can only be controlled by the original NFT contract. +Bound NFT Factory is a common boundNFT factory, similar to uniswap's erc20 pairs factory. It uses the create2 method to create a boundNFT contract address for any NFT deterministic. BoundNFT contract that has been created can only be controlled by the original NFT contract. ## Backwards Compatibility -This standard is compatible with current ERC-721 standards. +This standard is compatible with current `ERC721` standards. ## Test Cases -Test case written using hardhat: [here](https://github.com/radiocaca/ERC721L/blob/main/test/ERC721Lockable.ts) +Test case written using hardhat: [here](../assets/eip-5058/test/test.ts) ## Reference Implementation diff --git a/assets/eip-5058/mock/EIP5058Mock.sol b/assets/eip-5058/mock/EIP5058Mock.sol new file mode 100644 index 00000000000000..2afa121fd541bc --- /dev/null +++ b/assets/eip-5058/mock/EIP5058Mock.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.8; + +import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; +import "../ERC5058.sol"; + +contract EIP5058Mock is ERC721Enumerable, ERC5058 { + constructor(string memory name, string memory symbol) ERC721(name, symbol) {} + + function exists(uint256 tokenId) public view returns (bool) { + return _exists(tokenId); + } + + function lockMint( + address to, + uint256 tokenId, + uint256 expired + ) external { + _safeLockMint(to, tokenId, expired, ""); + } + + function mint(address to, uint256 tokenId) external { + _mint(to, tokenId); + } + + function burn(uint256 tokenId) external { + require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not owner nor approved"); + + _burn(tokenId); + } + + function _burn(uint256 tokenId) internal virtual override(ERC721, ERC5058) { + super._burn(tokenId); + } + + function _beforeTokenTransfer( + address from, + address to, + uint256 tokenId + ) internal virtual override(ERC721Enumerable, ERC5058) { + super._beforeTokenTransfer(from, to, tokenId); + } + + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(ERC721Enumerable, ERC5058) + returns (bool) + { + return super.supportsInterface(interfaceId); + } +} diff --git a/assets/eip-5058/test/test.ts b/assets/eip-5058/test/test.ts new file mode 100644 index 00000000000000..ce546265ff82d7 --- /dev/null +++ b/assets/eip-5058/test/test.ts @@ -0,0 +1,157 @@ +import "@nomiclabs/hardhat-ethers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { EIP5058Mock } from "typechain-types"; + +describe("EIP5058 contract", function() { + let owner: SignerWithAddress; + let alice: SignerWithAddress; + let EIP5058: EIP5058Mock; + + beforeEach(async () => { + [owner, alice] = await ethers.getSigners(); + + const EIP5058Factory = await ethers.getContractFactory("EIP5058Mock"); + + EIP5058 = await EIP5058Factory.deploy("Mock", "M"); + }); + + it("Deployment should assign the total supply of tokens to the owner", async function() { + const ownerBalance = await EIP5058.balanceOf(owner.address); + expect(await EIP5058.totalSupply()).to.equal(ownerBalance); + }); + + it("lockMint works", async function() { + const NFTId = 0; + const block = await ethers.provider.getBlockNumber(); + const blockBefore = await ethers.provider.getBlock(block); + const timestamp = blockBefore.timestamp; + await EIP5058.lockMint(alice.address, NFTId, timestamp + 2); + + expect(await EIP5058.isLocked(NFTId)).eq(true); + expect(await EIP5058.lockerOf(NFTId)).eq(owner.address); + }); + + it("Can not transfer when token is locked", async function() { + const NFTId = 0; + const block = await ethers.provider.getBlockNumber(); + const blockBefore = await ethers.provider.getBlock(block); + const timestamp = blockBefore.timestamp; + await EIP5058.lockMint(owner.address, NFTId, timestamp + 3); + + // can not transfer when token is locked + await expect(EIP5058.transferFrom(owner.address, alice.address, NFTId)).to.be.revertedWith( + "EIP5058: token transfer while locked", + ); + + // can transfer when token is unlocked + await ethers.provider.send("evm_mine", []); + await EIP5058.transferFrom(owner.address, alice.address, NFTId); + expect(await EIP5058.ownerOf(NFTId)).eq(alice.address); + }); + + it("isLocked works", async function() { + const NFTId = 0; + const block = await ethers.provider.getBlockNumber(); + const blockBefore = await ethers.provider.getBlock(block); + const timestamp = blockBefore.timestamp; + await EIP5058.lockMint(owner.address, NFTId, timestamp + 2); + + // isLocked works + expect(await EIP5058.isLocked(NFTId)).eq(true); + await ethers.provider.send("evm_mine", []); + expect(await EIP5058.isLocked(NFTId)).eq(false); + }); + + it("lockFrom works", async function() { + const NFTId = 0; + let block = await ethers.provider.getBlockNumber(); + const blockBefore = await ethers.provider.getBlock(block); + const timestamp = blockBefore.timestamp; + await EIP5058.lockMint(owner.address, NFTId, timestamp + 3); + + await expect(EIP5058.lockFrom(owner.address, NFTId, timestamp + 5)).to.be.revertedWith( + "EIP5058: token is locked", + ); + + await ethers.provider.send("evm_mine", []); + await EIP5058.lockFrom(owner.address, NFTId, timestamp + 5); + }); + + it("unlock works with lockMint", async function() { + const NFTId = 0; + const block = await ethers.provider.getBlockNumber(); + const blockBefore = await ethers.provider.getBlock(block); + const timestamp = blockBefore.timestamp; + await EIP5058.lockMint(owner.address, NFTId, timestamp + 3); + + // unlock works + expect(await EIP5058.isLocked(NFTId)).eq(true); + expect(await EIP5058.lockerOf(NFTId)).eq(owner.address); + await EIP5058.unlock(NFTId); + expect(await EIP5058.isLocked(NFTId)).eq(false); + }); + + it("unlock works", async function() { + const NFTId = 0; + + await EIP5058.mint(owner.address, NFTId); + + await expect(EIP5058.unlock(NFTId)).to.be.revertedWith( + "EIP5058: locker query for non-locked token", + ); + const block = await ethers.provider.getBlockNumber(); + const blockBefore = await ethers.provider.getBlock(block); + const timestamp = blockBefore.timestamp; + await EIP5058.lockFrom(owner.address, NFTId, timestamp + 3); + expect(await EIP5058.isLocked(NFTId)).eq(true); + await EIP5058.unlock(NFTId); + expect(await EIP5058.isLocked(NFTId)).eq(false); + }); + + it("lockApprove works", async function() { + const NFTId = 0; + await EIP5058.mint(alice.address, NFTId); + + let block = await ethers.provider.getBlockNumber(); + const blockBefore = await ethers.provider.getBlock(block); + const timestamp = blockBefore.timestamp; + await expect(EIP5058.lockFrom(owner.address, NFTId, timestamp + 2)).to.be.revertedWith( + "EIP5058: lock caller is not owner nor approved", + ); + + await EIP5058.connect(alice).lockApprove(owner.address, NFTId); + expect(await EIP5058.getLockApproved(NFTId)).eq(owner.address); + + await expect(EIP5058.lockFrom(owner.address, NFTId, timestamp + 4)).to.be.revertedWith( + "EIP5058: lock from incorrect owner", + ); + await EIP5058.lockFrom(alice.address, NFTId, timestamp + 6); + expect(await EIP5058.isLocked(NFTId)).eq(true); + + await expect(EIP5058.lockApprove(alice.address, NFTId)).to.be.revertedWith( + "EIP5058: token is locked", + ); + }); + + it("setLockApproveForAll works", async function() { + const NFTId = 0; + + await EIP5058.mint(alice.address, NFTId); + const block = await ethers.provider.getBlockNumber(); + const blockBefore = await ethers.provider.getBlock(block); + const timestamp = blockBefore.timestamp; + await expect(EIP5058.lockFrom(alice.address, NFTId, timestamp + 2)).to.be.revertedWith( + "EIP5058: lock caller is not owner nor approved", + ); + + await EIP5058.connect(alice).setLockApprovalForAll(owner.address, true); + expect(await EIP5058.isLockApprovedForAll(alice.address, owner.address)).eq(true); + + await EIP5058.lockFrom(alice.address, NFTId, timestamp + 6); + + await EIP5058.connect(alice).setLockApprovalForAll(owner.address, false); + expect(await EIP5058.isLockApprovedForAll(alice.address, owner.address)).eq(false); + }); +}); From 9999772092915b0e1b50fd644d794add35d554d8 Mon Sep 17 00:00:00 2001 From: radiocaca Date: Mon, 1 Aug 2022 02:13:08 +0800 Subject: [PATCH 23/40] rename from to owner --- assets/eip-5058/ERC5058.sol | 22 +++++++++++----------- assets/eip-5058/IERC5058.sol | 14 +++++++------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/assets/eip-5058/ERC5058.sol b/assets/eip-5058/ERC5058.sol index 534177953a5259..8a50867b2e0671 100644 --- a/assets/eip-5058/ERC5058.sol +++ b/assets/eip-5058/ERC5058.sol @@ -85,7 +85,7 @@ abstract contract ERC5058 is ERC721, IERC5058 { * @dev See {IERC5058-lockFrom}. */ function lockFrom( - address from, + address owner, uint256 tokenId, uint256 expired ) public virtual override { @@ -94,7 +94,7 @@ abstract contract ERC5058 is ERC721, IERC5058 { require(expired > block.number, "ERC5058: expired time must be greater than current block number"); require(!isLocked(tokenId), "ERC5058: token is locked"); - _lock(_msgSender(), from, tokenId, expired); + _lock(_msgSender(), owner, tokenId, expired); } /** @@ -125,20 +125,20 @@ abstract contract ERC5058 is ERC721, IERC5058 { */ function _lock( address operator, - address from, + address owner, uint256 tokenId, uint256 expired ) internal virtual { - require(ERC721.ownerOf(tokenId) == from, "ERC5058: lock from incorrect owner"); + require(ERC721.ownerOf(tokenId) == owner, "ERC5058: lock from incorrect owner"); - _beforeTokenLock(operator, from, tokenId, expired); + _beforeTokenLock(operator, owner, tokenId, expired); lockedTokens[tokenId] = expired; _lockApprovals[tokenId] = _msgSender(); - emit Locked(operator, from, tokenId, expired); + emit Locked(operator, owner, tokenId, expired); - _afterTokenLock(operator, from, tokenId, expired); + _afterTokenLock(operator, owner, tokenId, expired); } /** @@ -243,14 +243,14 @@ abstract contract ERC5058 is ERC721, IERC5058 { * * Calling conditions: * - * - `from` is non-zero. + * - `owner` is non-zero. * - When `expired` is zero, `tokenId` will be unlock for `from`. * - When `expired` is non-zero, ``from``'s `tokenId` will be locked. * */ function _beforeTokenLock( address operator, - address from, + address owner, uint256 tokenId, uint256 expired ) internal virtual {} @@ -260,14 +260,14 @@ abstract contract ERC5058 is ERC721, IERC5058 { * * Calling conditions: * - * - `from` is non-zero. + * - `owner` is non-zero. * - When `expired` is zero, `tokenId` will be unlock for `from`. * - When `expired` is non-zero, ``from``'s `tokenId` will be locked. * */ function _afterTokenLock( address operator, - address from, + address owner, uint256 tokenId, uint256 expired ) internal virtual {} diff --git a/assets/eip-5058/IERC5058.sol b/assets/eip-5058/IERC5058.sol index 9a9da130a78076..ad09cbcb31b7fa 100644 --- a/assets/eip-5058/IERC5058.sol +++ b/assets/eip-5058/IERC5058.sol @@ -16,14 +16,14 @@ import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; */ interface IERC5058 is IERC721 { /** - * @dev Emitted when `tokenId` token is locked by `operator` from `from`. + * @dev Emitted when `tokenId` token is locked by `operator` from `owner`. */ - event Locked(address indexed operator, address indexed from, uint256 indexed tokenId, uint256 expired); + event Locked(address indexed operator, address indexed owner, uint256 indexed tokenId, uint256 expired); /** - * @dev Emitted when `tokenId` token is unlocked by `operator` from `from`. + * @dev Emitted when `tokenId` token is unlocked by `operator` from `owner`. */ - event Unlocked(address indexed operator, address indexed from, uint256 indexed tokenId); + event Unlocked(address indexed operator, address indexed owner, uint256 indexed tokenId); /** * @dev Emitted when `owner` enables `approved` to lock the `tokenId` token. @@ -49,8 +49,8 @@ interface IERC5058 is IERC721 { * * Requirements: * - * - `from` cannot be the zero address. - * - `tokenId` token must be owned by `from`. + * - `owner` cannot be the zero address. + * - `tokenId` token must be owned by `owner`. * - `expired` must be greater than block.number * - If the caller is not `from`, it must be approved to lock this token * by either {lockApprove} or {setLockApprovalForAll}. @@ -58,7 +58,7 @@ interface IERC5058 is IERC721 { * Emits a {Locked} event. */ function lockFrom( - address from, + address owner, uint256 tokenId, uint256 expired ) external; From 9610e08a9337b8e44ec5ad55e08fabe8985cf5ba Mon Sep 17 00:00:00 2001 From: radiocaca <84221999+radiocaca@users.noreply.github.com> Date: Wed, 3 Aug 2022 14:44:00 +0800 Subject: [PATCH 24/40] Update EIPS/eip-5058.md Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> --- EIPS/eip-5058.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5058.md b/EIPS/eip-5058.md index 4278d231331f5d..2728b0ef18049c 100644 --- a/EIPS/eip-5058.md +++ b/EIPS/eip-5058.md @@ -1,6 +1,6 @@ --- eip: 5058 -title: Lockable ERC-721 Standard +title: Lockable Non-Fungible Tokens description: Lockable ERC-721 tokens author: Tyler (@radiocaca), Alex (@gojazdev), John (@sfumato00) discussions-to: https://ethereum-magicians.org/t/eip-5058-erc-721-lockable-standard/9201 From ba9ebea00cc3efaccc5b1e2935ad9d5f68b21172 Mon Sep 17 00:00:00 2001 From: radiocaca <84221999+radiocaca@users.noreply.github.com> Date: Wed, 3 Aug 2022 14:44:49 +0800 Subject: [PATCH 25/40] Update EIPS/eip-5058.md Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> --- EIPS/eip-5058.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5058.md b/EIPS/eip-5058.md index 2728b0ef18049c..363d981edfd184 100644 --- a/EIPS/eip-5058.md +++ b/EIPS/eip-5058.md @@ -17,7 +17,7 @@ We propose to extend the `ERC721` standard with a secure locking mechanism. The ## Motivation -NFTs, enabled by [ERC721](./eip-721.md), has been on the explosion in demand. The total market value and the ecosystem continue to grow with more and more blue chip NFTs, which are approximately equivalent to popular intellectual properties in a conventional sense. Despite the vast success, something is left to be desired. Liquidity has always been one of the biggest challenges for NFTs. Several attempts have been made to tackle the liquidity challenge: NFTFi and BendDAO, to name a few. Utilizing the currently prevalent `ERC721` standard, these projects require participating NFTs to be transferred to the projects' contracts, which poses inconveniences and risks to the owners: +NFTs, enabled by [EIP-721](./eip-721.md), have exploded in demand. The total market value and the ecosystem continue to grow with more and more blue chip NFTs, which are approximately equivalent to popular intellectual properties in a conventional sense. Despite the vast success, something is left to be desired. Liquidity has always been one of the biggest challenges for NFTs. Several attempts have been made to tackle the liquidity challenge: NFTFi and BendDAO, to name a few. Utilizing the currently prevalent EIP-721 standard, these projects require participating NFTs to be transferred to the projects' contracts, which poses inconveniences and risks to the owners: 1. Smart contract risks: NFTs can be lost or stolen due to bugs or vulnerabilities in the contracts. 2. Loss of utility: NFTs have utility values, such as profile pictures and bragging rights, which are lost when the NFTs are no longer seen under the owners' custody. From 552e31336f64b97984c697c80938e014f69ee538 Mon Sep 17 00:00:00 2001 From: radiocaca <84221999+radiocaca@users.noreply.github.com> Date: Wed, 3 Aug 2022 14:45:01 +0800 Subject: [PATCH 26/40] Update EIPS/eip-5058.md Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> --- EIPS/eip-5058.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5058.md b/EIPS/eip-5058.md index 363d981edfd184..774caa624d9bc6 100644 --- a/EIPS/eip-5058.md +++ b/EIPS/eip-5058.md @@ -31,7 +31,7 @@ All-of-the-above are bad UX, and we believe the `ERC721` standard can be improve The value of NFT can be reflected in two aspects: collection value and utility value. Collection value needs to ensure that the holder's wallet retains ownership of the NFT forever. Utility value requires ensuring that the holder can verify their NFT ownership in other projects. Both of these aspects require that the NFT be kept in the owner's wallet all the time. -The proposed standard allows the underlying NFT assets to be managed securely and conveniently and extends the `ERC721` standard to natively support of common NFTFi use cases including locking, staking, lending, and crowdfunding. We believe the proposed standard will encourage NFT owners to participate more actively in NFTFi projects and, hence, improve the livelihood of the whole NFT ecosystem. +The proposed standard allows the underlying NFT assets to be managed securely and conveniently by extending the EIP-721 standard to natively support common NFTFi use cases including locking, staking, lending, and crowdfunding. We believe the proposed standard will encourage NFT owners to participate more actively in NFTFi projects and, hence, improve the livelihood of the whole NFT ecosystem. ## Specification From e5f1160bffa1c3c159affd1def8261f476786fd9 Mon Sep 17 00:00:00 2001 From: radiocaca <84221999+radiocaca@users.noreply.github.com> Date: Wed, 3 Aug 2022 14:45:14 +0800 Subject: [PATCH 27/40] Update EIPS/eip-5058.md Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> --- EIPS/eip-5058.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5058.md b/EIPS/eip-5058.md index 774caa624d9bc6..cc9075185d925d 100644 --- a/EIPS/eip-5058.md +++ b/EIPS/eip-5058.md @@ -23,7 +23,7 @@ NFTs, enabled by [EIP-721](./eip-721.md), have exploded in demand. The total mar 2. Loss of utility: NFTs have utility values, such as profile pictures and bragging rights, which are lost when the NFTs are no longer seen under the owners' custody. 3. Missing Airdrops: The owners can no longer directly receive airdrops entitled to the NFTs. Considering the values and price fluctuation of some of the airdrops, either missing or getting the airdrop not on time can financially impact the owners. -All-of-the-above are bad UX, and we believe the `ERC721` standard can be improved by adopting a native locking mechanism: +All of the above are bad UX, and we believe the EIP-721 standard can be improved by adopting a native locking mechanism: 1. Instead of being transferred to a smart contract, a NFT remains being self-custody but locked. 2. While a NFT is locked, only the change of its ownership is disabled. Other utilities are not affected. From 3f3d03751e9f18df1baa6ef7da58f0dc1528298d Mon Sep 17 00:00:00 2001 From: radiocaca <84221999+radiocaca@users.noreply.github.com> Date: Wed, 3 Aug 2022 14:51:55 +0800 Subject: [PATCH 28/40] Update EIPS/eip-5058.md Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> --- EIPS/eip-5058.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5058.md b/EIPS/eip-5058.md index cc9075185d925d..fd5df8e58bd049 100644 --- a/EIPS/eip-5058.md +++ b/EIPS/eip-5058.md @@ -37,7 +37,7 @@ The proposed standard allows the underlying NFT assets to be managed securely an The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119. -Lockable `ERC721` **MUST** implement the `IERC721Lockable` interfaces: +Lockable EIP-721 **MUST** implement the `IERC721Lockable` interfaces: ```solidity // SPDX-License-Identifier: CC0-1.0 From 9d3980ae06453896548219e4e2716cb14ba3a60e Mon Sep 17 00:00:00 2001 From: radiocaca <84221999+radiocaca@users.noreply.github.com> Date: Wed, 3 Aug 2022 15:08:12 +0800 Subject: [PATCH 29/40] Update assets/eip-5058/IERC5058.sol Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> --- assets/eip-5058/IERC5058.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/assets/eip-5058/IERC5058.sol b/assets/eip-5058/IERC5058.sol index ad09cbcb31b7fa..fcbef18b19a38a 100644 --- a/assets/eip-5058/IERC5058.sol +++ b/assets/eip-5058/IERC5058.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.0; -import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; /** * @dev ERC-721 Non-Fungible Token Standard, optional lockable extension From 286289b9458e2e3153882d7f67f020162a58f1b1 Mon Sep 17 00:00:00 2001 From: radiocaca <84221999+radiocaca@users.noreply.github.com> Date: Wed, 3 Aug 2022 15:19:06 +0800 Subject: [PATCH 30/40] Update EIPS/eip-5058.md Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> --- EIPS/eip-5058.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5058.md b/EIPS/eip-5058.md index fd5df8e58bd049..cca9441ef84f4d 100644 --- a/EIPS/eip-5058.md +++ b/EIPS/eip-5058.md @@ -207,4 +207,4 @@ After being locked, the NFT can not be transferred, so before authorizing lockin ## Copyright -Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). +Copyright and related rights waived via [CC0](../LICENSE.md). From eb1fc0016efbecd7932d3ae2ca4c65bd22bd7e2d Mon Sep 17 00:00:00 2001 From: radiocaca <84221999+radiocaca@users.noreply.github.com> Date: Wed, 3 Aug 2022 15:19:41 +0800 Subject: [PATCH 31/40] Update EIPS/eip-5058.md Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> --- EIPS/eip-5058.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5058.md b/EIPS/eip-5058.md index cca9441ef84f4d..3aaf69c6fc81b7 100644 --- a/EIPS/eip-5058.md +++ b/EIPS/eip-5058.md @@ -192,7 +192,7 @@ Bound NFT Factory is a common boundNFT factory, similar to uniswap's erc20 pairs ## Backwards Compatibility -This standard is compatible with current `ERC721` standards. +This standard is compatible with EIP-721. ## Test Cases Test case written using hardhat: [here](../assets/eip-5058/test/test.ts) From 724e27756b26858b2b8c297a264ffd0d5777ec61 Mon Sep 17 00:00:00 2001 From: radiocaca <84221999+radiocaca@users.noreply.github.com> Date: Wed, 3 Aug 2022 15:20:15 +0800 Subject: [PATCH 32/40] Update EIPS/eip-5058.md Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> --- EIPS/eip-5058.md | 1 + 1 file changed, 1 insertion(+) diff --git a/EIPS/eip-5058.md b/EIPS/eip-5058.md index 3aaf69c6fc81b7..b3e6a1d0bcc092 100644 --- a/EIPS/eip-5058.md +++ b/EIPS/eip-5058.md @@ -195,6 +195,7 @@ Bound NFT Factory is a common boundNFT factory, similar to uniswap's erc20 pairs This standard is compatible with EIP-721. ## Test Cases + Test case written using hardhat: [here](../assets/eip-5058/test/test.ts) ## Reference Implementation From 481078055bbe95af72bf5987820eace1d39ed221 Mon Sep 17 00:00:00 2001 From: radiocaca <84221999+radiocaca@users.noreply.github.com> Date: Wed, 3 Aug 2022 15:23:10 +0800 Subject: [PATCH 33/40] Apply suggestions from code review Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> --- EIPS/eip-5058.md | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/EIPS/eip-5058.md b/EIPS/eip-5058.md index b3e6a1d0bcc092..522e8f0b845aa3 100644 --- a/EIPS/eip-5058.md +++ b/EIPS/eip-5058.md @@ -8,12 +8,12 @@ status: Draft type: Standards Track category: ERC created: 2022-04-30 -requires: 165, 721 +requires: 20, 165, 721 --- ## Abstract -We propose to extend the `ERC721` standard with a secure locking mechanism. The NFT owners approve the operator to lock the NFT through `setLockApprovalForAll()` or `lockApprove()`. The apporved operator locks the NFT through `lockFrom()`. The locked NFTs cannot be transferred until the end of the locking period. An immediate use case is to allow NFTs to participate in smart contracts without leaving the wallets of their owners. +We propose to extend the [EIP-721](./eip-721.md) standard with a secure locking mechanism. The NFT owners approve the operator to lock the NFT through `setLockApprovalForAll()` or `lockApprove()`. The approved operator locks the NFT through `lockFrom()`. The locked NFTs cannot be transferred until the end of the locking period. An immediate use case is to allow NFTs to participate in smart contracts without leaving the wallets of their owners. ## Motivation @@ -21,15 +21,15 @@ NFTs, enabled by [EIP-721](./eip-721.md), have exploded in demand. The total mar 1. Smart contract risks: NFTs can be lost or stolen due to bugs or vulnerabilities in the contracts. 2. Loss of utility: NFTs have utility values, such as profile pictures and bragging rights, which are lost when the NFTs are no longer seen under the owners' custody. -3. Missing Airdrops: The owners can no longer directly receive airdrops entitled to the NFTs. Considering the values and price fluctuation of some of the airdrops, either missing or getting the airdrop not on time can financially impact the owners. +3. Missing Airdrops: The owners can no longer directly receive airdrops entitled to the NFTs. Considering the values and price fluctuation of some of the airdrops, either missing or not getting the airdrop on time can financially impact the owners. All of the above are bad UX, and we believe the EIP-721 standard can be improved by adopting a native locking mechanism: -1. Instead of being transferred to a smart contract, a NFT remains being self-custody but locked. -2. While a NFT is locked, only the change of its ownership is disabled. Other utilities are not affected. +1. Instead of being transferred to a smart contract, an NFT remains in self-custody but locked. +2. While an NFT is locked, its transfer is prohibited. Other properties remain unaffected. 3. The owners can receive or claim airdrops themselves. -The value of NFT can be reflected in two aspects: collection value and utility value. Collection value needs to ensure that the holder's wallet retains ownership of the NFT forever. Utility value requires ensuring that the holder can verify their NFT ownership in other projects. Both of these aspects require that the NFT be kept in the owner's wallet all the time. +The value of an NFT can be reflected in two aspects: collection value and utility value. Collection value needs to ensure that the holder's wallet retains ownership of the NFT forever. Utility value requires ensuring that the holder can verify their NFT ownership in other projects. Both of these aspects require that the NFT remain in its owner's wallet. The proposed standard allows the underlying NFT assets to be managed securely and conveniently by extending the EIP-721 standard to natively support common NFTFi use cases including locking, staking, lending, and crowdfunding. We believe the proposed standard will encourage NFT owners to participate more actively in NFTFi projects and, hence, improve the livelihood of the whole NFT ecosystem. @@ -173,21 +173,24 @@ interface IERC721Lockable is IERC721 { ### NFT lock approvals -A NFT owner can give another trusted operator the right to lock his NFT through the approve functions. The `lockApprove()` function only approves for the specified NFT, whereas `setLockApprovalForAll()` approves for all NFTs of the collection under the wallet. When a user participates in a NFTFi project, the project contract calls `lockFrom()` to lock the user's NFT. Locked NFTs cannot be transferred, but the NFTFi project contract can use the unlock function `unlockFrom()` to unlock the NFT. +An NFT owner can give another trusted operator the right to lock his NFT through the approve functions. The `lockApprove()` function only approves for the specified NFT, whereas `setLockApprovalForAll()` approves for all NFTs of the collection under the wallet. When a user participates in an NFTFi project, the project contract calls `lockFrom()` to lock the user's NFT. Locked NFTs cannot be transferred, but the NFTFi project contract can use the unlock function `unlockFrom()` to unlock the NFT. ### NFT lock/unlock -Authorized project contract has permission to lock NFT through lockFrom method, locked NFT cannot be transferred, unless the lock time expires. Of course, the project contract also has permission to unlock NFT in advance through unlockFrom. Note: only the address of the locked NFT has permission to unlock NFT. + +Authorized project contracts have permission to lock NFT with the `lockFrom` method. Locked NFTs cannot be transferred until the lock time expires. The project contract also has permission to unlock NFT in advance through the `unlockFrom` function. Note that only the address of the locked NFT has permission to unlock that NFT. ### NFT lock period -When locking a NFT, it is required to specify the lock expiration block height, which must be greater than the current block height. Upon the lock expiration, the NFT is automatically released and can be transferred. +When locking an NFT, one must specify the lock expiration block number, which must be greater than the current block number. When the current block number exceeds the expiration block number, the NFT is automatically released and can be transferred. ### Bound NFT -Bound NFT is an extension of the EIP, which implements the ability to mint a boundNFT during the NFT locking period. The boundNFT is identical to the locked NFT metadata and can be transferred. However, boundNFT only exists during the NFT locking period, and will be destroyed after the NFT is unlocked. + +Bound NFT is an extension of this EIP, which implements the ability to mint a boundNFT during the NFT locking period. The boundNFT is identical to the locked NFT metadata and can be transferred. However, a boundNFT only exists during the NFT locking period and will be destroyed after the NFT is unlocked. BoundNFT can be used to lend, as a staking credential for the contract. The credential can be locked in the contract, but also to the user. In NFT leasing, boundNFT can be rented to users because boundNFT is essentially equivalent to NFT. This consensus, if accepted by all projects, boundNFT will bring more creativity to NFT. ### Bound NFT Factory -Bound NFT Factory is a common boundNFT factory, similar to uniswap's erc20 pairs factory. It uses the create2 method to create a boundNFT contract address for any NFT deterministic. BoundNFT contract that has been created can only be controlled by the original NFT contract. + +Bound NFT Factory is a common boundNFT factory, similar to Uniswap's [EIP-20](./eip-20.md) pairs factory. It uses the create2 method to create a boundNFT contract address for any NFT deterministic. BoundNFT contract that has been created can only be controlled by the original NFT contract. ## Backwards Compatibility @@ -196,7 +199,7 @@ This standard is compatible with EIP-721. ## Test Cases -Test case written using hardhat: [here](../assets/eip-5058/test/test.ts) +Test cases written using hardhat can be found [here](../assets/eip-5058/test/test.ts) ## Reference Implementation From 051e456d1164c4b5af4567255ac5e2aae2fdd1e0 Mon Sep 17 00:00:00 2001 From: radiocaca <84221999+radiocaca@users.noreply.github.com> Date: Wed, 3 Aug 2022 16:38:42 +0800 Subject: [PATCH 34/40] Apply suggestions from code review Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> --- assets/eip-5058/factory/IERC5058Factory.sol | 1 - assets/eip-5058/mock/EIP5058Mock.sol | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/assets/eip-5058/factory/IERC5058Factory.sol b/assets/eip-5058/factory/IERC5058Factory.sol index 283abb5432a7c1..61ecb19b9af776 100644 --- a/assets/eip-5058/factory/IERC5058Factory.sol +++ b/assets/eip-5058/factory/IERC5058Factory.sol @@ -1,5 +1,4 @@ // SPDX-License-Identifier: CC0-1.0 -// Creator: tyler@radiocaca.com pragma solidity ^0.8.0; diff --git a/assets/eip-5058/mock/EIP5058Mock.sol b/assets/eip-5058/mock/EIP5058Mock.sol index 2afa121fd541bc..006feab3dcd316 100644 --- a/assets/eip-5058/mock/EIP5058Mock.sol +++ b/assets/eip-5058/mock/EIP5058Mock.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.8; From 9a95d53b160789a7d03c98acf4b2003f66321239 Mon Sep 17 00:00:00 2001 From: radiocaca Date: Wed, 3 Aug 2022 16:43:05 +0800 Subject: [PATCH 35/40] fix lockFrom --- EIPS/eip-5058.md | 16 ++++------------ assets/eip-5058/ERC5058.sol | 13 ++++--------- assets/eip-5058/IERC5058.sol | 10 ++-------- 3 files changed, 10 insertions(+), 29 deletions(-) diff --git a/EIPS/eip-5058.md b/EIPS/eip-5058.md index 522e8f0b845aa3..64a69857c840af 100644 --- a/EIPS/eip-5058.md +++ b/EIPS/eip-5058.md @@ -37,15 +37,13 @@ The proposed standard allows the underlying NFT assets to be managed securely an The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119. -Lockable EIP-721 **MUST** implement the `IERC721Lockable` interfaces: +Lockable EIP-721 **MUST** implement the `IERC5058` interfaces: ```solidity // SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.8; -import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; - /** * @dev ERC-721 Non-Fungible Token Standard, optional lockable extension * ERC721 Token that can be locked for a certain period and cannot be transferred. @@ -55,7 +53,7 @@ import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; * If the nft lending protocol is compatible with this extension, the trouble caused by the NFT * airdrop can be avoided, because the airdrop is still in the user's wallet */ -interface IERC721Lockable is IERC721 { +interface IERC5058 { /** * @dev Emitted when `tokenId` token is locked by `operator` from `from`. */ @@ -90,7 +88,6 @@ interface IERC721Lockable is IERC721 { * * Requirements: * - * - `from` cannot be the zero address. * - `tokenId` token must be owned by `from`. * - `expired` must be greater than block.number * - If the caller is not `from`, it must be approved to lock this token @@ -98,24 +95,19 @@ interface IERC721Lockable is IERC721 { * * Emits a {Locked} event. */ - function lockFrom( - address from, - uint256 tokenId, - uint256 expired - ) external; + function lockFrom(uint256 tokenId, uint256 expired) external; /** * @dev Unlock `tokenId` token. * * Requirements: * - * - `from` cannot be the zero address. * - `tokenId` token must be owned by `from`. * - the caller must be the operator who locks the token by {lockFrom} * * Emits a {Unlocked} event. */ - function unlockFrom(address from, uint256 tokenId) external; + function unlock(uint256 tokenId) external; /** * @dev Gives permission to `to` to lock `tokenId` token. diff --git a/assets/eip-5058/ERC5058.sol b/assets/eip-5058/ERC5058.sol index 8a50867b2e0671..17d0493001ea4d 100644 --- a/assets/eip-5058/ERC5058.sol +++ b/assets/eip-5058/ERC5058.sol @@ -84,17 +84,13 @@ abstract contract ERC5058 is ERC721, IERC5058 { /** * @dev See {IERC5058-lockFrom}. */ - function lockFrom( - address owner, - uint256 tokenId, - uint256 expired - ) public virtual override { + function lock(uint256 tokenId, uint256 expired) public virtual override { //solhint-disable-next-line max-line-length require(_isLockApprovedOrOwner(_msgSender(), tokenId), "ERC5058: lock caller is not owner nor approved"); require(expired > block.number, "ERC5058: expired time must be greater than current block number"); require(!isLocked(tokenId), "ERC5058: token is locked"); - _lock(_msgSender(), owner, tokenId, expired); + _lock(_msgSender(), tokenId, expired); } /** @@ -125,11 +121,10 @@ abstract contract ERC5058 is ERC721, IERC5058 { */ function _lock( address operator, - address owner, uint256 tokenId, uint256 expired ) internal virtual { - require(ERC721.ownerOf(tokenId) == owner, "ERC5058: lock from incorrect owner"); + address owner = ERC721.ownerOf(tokenId); _beforeTokenLock(operator, owner, tokenId, expired); @@ -160,7 +155,7 @@ abstract contract ERC5058 is ERC721, IERC5058 { _safeMint(to, tokenId, _data); - _lock(address(0), to, tokenId, expired); + _lock(address(0), tokenId, expired); } /** diff --git a/assets/eip-5058/IERC5058.sol b/assets/eip-5058/IERC5058.sol index fcbef18b19a38a..e916b6a4b23649 100644 --- a/assets/eip-5058/IERC5058.sol +++ b/assets/eip-5058/IERC5058.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.0; - /** * @dev ERC-721 Non-Fungible Token Standard, optional lockable extension * ERC721 Token that can be locked for a certain period and cannot be transferred. @@ -13,7 +12,7 @@ pragma solidity ^0.8.0; * If the nft lending protocol is compatible with this extension, the trouble caused by the NFT * airdrop can be avoided, because the airdrop is still in the user's wallet */ -interface IERC5058 is IERC721 { +interface IERC5058 { /** * @dev Emitted when `tokenId` token is locked by `operator` from `owner`. */ @@ -48,7 +47,6 @@ interface IERC5058 is IERC721 { * * Requirements: * - * - `owner` cannot be the zero address. * - `tokenId` token must be owned by `owner`. * - `expired` must be greater than block.number * - If the caller is not `from`, it must be approved to lock this token @@ -56,11 +54,7 @@ interface IERC5058 is IERC721 { * * Emits a {Locked} event. */ - function lockFrom( - address owner, - uint256 tokenId, - uint256 expired - ) external; + function lock(uint256 tokenId, uint256 expired) external; /** * @dev Unlock `tokenId` token. From 21731b247f67557a0acbc679b476cff6e38d9d89 Mon Sep 17 00:00:00 2001 From: radiocaca Date: Wed, 3 Aug 2022 16:44:59 +0800 Subject: [PATCH 36/40] update md --- EIPS/eip-5058.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/EIPS/eip-5058.md b/EIPS/eip-5058.md index 64a69857c840af..2ba89c16d1b58d 100644 --- a/EIPS/eip-5058.md +++ b/EIPS/eip-5058.md @@ -1,7 +1,7 @@ --- eip: 5058 title: Lockable Non-Fungible Tokens -description: Lockable ERC-721 tokens +description: Lockable EIP-721 tokens author: Tyler (@radiocaca), Alex (@gojazdev), John (@sfumato00) discussions-to: https://ethereum-magicians.org/t/eip-5058-erc-721-lockable-standard/9201 status: Draft @@ -45,7 +45,7 @@ Lockable EIP-721 **MUST** implement the `IERC5058` interfaces: pragma solidity ^0.8.8; /** - * @dev ERC-721 Non-Fungible Token Standard, optional lockable extension + * @dev EIP-721 Non-Fungible Token Standard, optional lockable extension * ERC721 Token that can be locked for a certain period and cannot be transferred. * This is designed for a non-escrow staking contract that comes later to lock a user's NFT * while still letting them keep it in their wallet. From adaad9e1e9e9f5700e8a6df615763ff8cfbe92a7 Mon Sep 17 00:00:00 2001 From: radiocaca Date: Wed, 3 Aug 2022 16:50:39 +0800 Subject: [PATCH 37/40] fix lockFrom tests --- assets/eip-5058/ERC5058.sol | 2 +- assets/eip-5058/IERC5058.sol | 4 ++-- assets/eip-5058/extensions/ERC5058Bound.sol | 2 +- assets/eip-5058/test/test.ts | 18 +++++++++--------- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/assets/eip-5058/ERC5058.sol b/assets/eip-5058/ERC5058.sol index 17d0493001ea4d..1199678a6a87ab 100644 --- a/assets/eip-5058/ERC5058.sol +++ b/assets/eip-5058/ERC5058.sol @@ -82,7 +82,7 @@ abstract contract ERC5058 is ERC721, IERC5058 { } /** - * @dev See {IERC5058-lockFrom}. + * @dev See {IERC5058-lock}. */ function lock(uint256 tokenId, uint256 expired) public virtual override { //solhint-disable-next-line max-line-length diff --git a/assets/eip-5058/IERC5058.sol b/assets/eip-5058/IERC5058.sol index e916b6a4b23649..f3d8595a013fc1 100644 --- a/assets/eip-5058/IERC5058.sol +++ b/assets/eip-5058/IERC5058.sol @@ -62,7 +62,7 @@ interface IERC5058 { * Requirements: * * - `tokenId` token must be owned by `from`. - * - the caller must be the operator who locks the token by {lockFrom} + * - the caller must be the operator who locks the token by {lock} * * Emits a {Unlocked} event. */ @@ -82,7 +82,7 @@ interface IERC5058 { /** * @dev Approve or remove `operator` as an lock operator for the caller. - * Operators can call {lockFrom} for any token owned by the caller. + * Operators can call {lock} for any token owned by the caller. * * Requirements: * diff --git a/assets/eip-5058/extensions/ERC5058Bound.sol b/assets/eip-5058/extensions/ERC5058Bound.sol index e2e64a1917c5c8..e0eebb614a2929 100644 --- a/assets/eip-5058/extensions/ERC5058Bound.sol +++ b/assets/eip-5058/extensions/ERC5058Bound.sol @@ -27,7 +27,7 @@ abstract contract ERC5058Bound is ERC5058 { // NOTE: // - // this will be called when `lockFrom` or `unlock` + // this will be called when `lock` or `unlock` function _afterTokenLock( address operator, address from, diff --git a/assets/eip-5058/test/test.ts b/assets/eip-5058/test/test.ts index ce546265ff82d7..f67c5cb0691e49 100644 --- a/assets/eip-5058/test/test.ts +++ b/assets/eip-5058/test/test.ts @@ -64,19 +64,19 @@ describe("EIP5058 contract", function() { expect(await EIP5058.isLocked(NFTId)).eq(false); }); - it("lockFrom works", async function() { + it("lock works", async function() { const NFTId = 0; let block = await ethers.provider.getBlockNumber(); const blockBefore = await ethers.provider.getBlock(block); const timestamp = blockBefore.timestamp; await EIP5058.lockMint(owner.address, NFTId, timestamp + 3); - await expect(EIP5058.lockFrom(owner.address, NFTId, timestamp + 5)).to.be.revertedWith( + await expect(EIP5058.lock(NFTId, timestamp + 5)).to.be.revertedWith( "EIP5058: token is locked", ); await ethers.provider.send("evm_mine", []); - await EIP5058.lockFrom(owner.address, NFTId, timestamp + 5); + await EIP5058.lock(NFTId, timestamp + 5); }); it("unlock works with lockMint", async function() { @@ -104,7 +104,7 @@ describe("EIP5058 contract", function() { const block = await ethers.provider.getBlockNumber(); const blockBefore = await ethers.provider.getBlock(block); const timestamp = blockBefore.timestamp; - await EIP5058.lockFrom(owner.address, NFTId, timestamp + 3); + await EIP5058.lock(NFTId, timestamp + 3); expect(await EIP5058.isLocked(NFTId)).eq(true); await EIP5058.unlock(NFTId); expect(await EIP5058.isLocked(NFTId)).eq(false); @@ -117,17 +117,17 @@ describe("EIP5058 contract", function() { let block = await ethers.provider.getBlockNumber(); const blockBefore = await ethers.provider.getBlock(block); const timestamp = blockBefore.timestamp; - await expect(EIP5058.lockFrom(owner.address, NFTId, timestamp + 2)).to.be.revertedWith( + await expect(EIP5058.lock(NFTId, timestamp + 2)).to.be.revertedWith( "EIP5058: lock caller is not owner nor approved", ); await EIP5058.connect(alice).lockApprove(owner.address, NFTId); expect(await EIP5058.getLockApproved(NFTId)).eq(owner.address); - await expect(EIP5058.lockFrom(owner.address, NFTId, timestamp + 4)).to.be.revertedWith( + await expect(EIP5058.lock(NFTId, timestamp + 4)).to.be.revertedWith( "EIP5058: lock from incorrect owner", ); - await EIP5058.lockFrom(alice.address, NFTId, timestamp + 6); + await EIP5058.lock(NFTId, timestamp + 6); expect(await EIP5058.isLocked(NFTId)).eq(true); await expect(EIP5058.lockApprove(alice.address, NFTId)).to.be.revertedWith( @@ -142,14 +142,14 @@ describe("EIP5058 contract", function() { const block = await ethers.provider.getBlockNumber(); const blockBefore = await ethers.provider.getBlock(block); const timestamp = blockBefore.timestamp; - await expect(EIP5058.lockFrom(alice.address, NFTId, timestamp + 2)).to.be.revertedWith( + await expect(EIP5058.lock(NFTId, timestamp + 2)).to.be.revertedWith( "EIP5058: lock caller is not owner nor approved", ); await EIP5058.connect(alice).setLockApprovalForAll(owner.address, true); expect(await EIP5058.isLockApprovedForAll(alice.address, owner.address)).eq(true); - await EIP5058.lockFrom(alice.address, NFTId, timestamp + 6); + await EIP5058.lock(NFTId, timestamp + 6); await EIP5058.connect(alice).setLockApprovalForAll(owner.address, false); expect(await EIP5058.isLockApprovedForAll(alice.address, owner.address)).eq(false); From 0c6ee82b1ae2ea4125ddf65467d430e37ac4ab4f Mon Sep 17 00:00:00 2001 From: radiocaca Date: Sun, 14 Aug 2022 12:30:30 +0800 Subject: [PATCH 38/40] remove oz deps --- assets/eip-5058/ERC5058.sol | 1 - assets/eip-5058/IERC5058.sol | 3 +-- assets/eip-5058/factory/ERC5058Factory.sol | 1 - assets/eip-5058/factory/ERC721Bound.sol | 2 -- assets/eip-5058/factory/IERC721Bound.sol | 2 -- assets/eip-5058/mock/EIP5058Mock.sol | 3 +-- 6 files changed, 2 insertions(+), 10 deletions(-) diff --git a/assets/eip-5058/ERC5058.sol b/assets/eip-5058/ERC5058.sol index 1199678a6a87ab..03f311294f4e25 100644 --- a/assets/eip-5058/ERC5058.sol +++ b/assets/eip-5058/ERC5058.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.0; -import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "./IERC5058.sol"; /** diff --git a/assets/eip-5058/IERC5058.sol b/assets/eip-5058/IERC5058.sol index f3d8595a013fc1..4f8846eff2c412 100644 --- a/assets/eip-5058/IERC5058.sol +++ b/assets/eip-5058/IERC5058.sol @@ -1,5 +1,4 @@ // SPDX-License-Identifier: CC0-1.0 -// Creator: tyler@radiocaca.com pragma solidity ^0.8.0; @@ -12,7 +11,7 @@ pragma solidity ^0.8.0; * If the nft lending protocol is compatible with this extension, the trouble caused by the NFT * airdrop can be avoided, because the airdrop is still in the user's wallet */ -interface IERC5058 { +interface IERC5058 is IERC721 { /** * @dev Emitted when `tokenId` token is locked by `operator` from `owner`. */ diff --git a/assets/eip-5058/factory/ERC5058Factory.sol b/assets/eip-5058/factory/ERC5058Factory.sol index 571e029bace68b..3fda931920aa95 100644 --- a/assets/eip-5058/factory/ERC5058Factory.sol +++ b/assets/eip-5058/factory/ERC5058Factory.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.0; -import "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; import "./ERC721Bound.sol"; import "./IERC5058Factory.sol"; diff --git a/assets/eip-5058/factory/ERC721Bound.sol b/assets/eip-5058/factory/ERC721Bound.sol index 3532cf7cbcfd6e..28e9f17ee761bc 100644 --- a/assets/eip-5058/factory/ERC721Bound.sol +++ b/assets/eip-5058/factory/ERC721Bound.sol @@ -2,8 +2,6 @@ pragma solidity ^0.8.0; -import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; -import "@openzeppelin/contracts/interfaces/IERC2981.sol"; import "./IERC721Bound.sol"; interface IPreimage { diff --git a/assets/eip-5058/factory/IERC721Bound.sol b/assets/eip-5058/factory/IERC721Bound.sol index e48ca9a2269fb7..c8b86a9a00f375 100644 --- a/assets/eip-5058/factory/IERC721Bound.sol +++ b/assets/eip-5058/factory/IERC721Bound.sol @@ -2,8 +2,6 @@ pragma solidity ^0.8.0; -import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; - interface IERC721Bound is IERC721 { function preimage() external view returns (address); diff --git a/assets/eip-5058/mock/EIP5058Mock.sol b/assets/eip-5058/mock/EIP5058Mock.sol index 006feab3dcd316..0d02a748eaec1a 100644 --- a/assets/eip-5058/mock/EIP5058Mock.sol +++ b/assets/eip-5058/mock/EIP5058Mock.sol @@ -1,8 +1,7 @@ // SPDX-License-Identifier: CC0-1.0 -pragma solidity ^0.8.8; +pragma solidity ^0.8.0; -import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; import "../ERC5058.sol"; contract EIP5058Mock is ERC721Enumerable, ERC5058 { From 2a2945737fea1930ecb8c192578f920d4755c6a4 Mon Sep 17 00:00:00 2001 From: radiocaca Date: Tue, 16 Aug 2022 00:11:10 +0800 Subject: [PATCH 39/40] modify md for eip change --- EIPS/eip-5058.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/EIPS/eip-5058.md b/EIPS/eip-5058.md index 2ba89c16d1b58d..e6fe7b6987f46a 100644 --- a/EIPS/eip-5058.md +++ b/EIPS/eip-5058.md @@ -13,7 +13,7 @@ requires: 20, 165, 721 ## Abstract -We propose to extend the [EIP-721](./eip-721.md) standard with a secure locking mechanism. The NFT owners approve the operator to lock the NFT through `setLockApprovalForAll()` or `lockApprove()`. The approved operator locks the NFT through `lockFrom()`. The locked NFTs cannot be transferred until the end of the locking period. An immediate use case is to allow NFTs to participate in smart contracts without leaving the wallets of their owners. +We propose to extend the [EIP-721](./eip-721.md) standard with a secure locking mechanism. The NFT owners approve the operator to lock the NFT through `setLockApprovalForAll()` or `lockApprove()`. The approved operator locks the NFT through `lock()`. The locked NFTs cannot be transferred until the end of the locking period. An immediate use case is to allow NFTs to participate in smart contracts without leaving the wallets of their owners. ## Motivation @@ -88,22 +88,22 @@ interface IERC5058 { * * Requirements: * - * - `tokenId` token must be owned by `from`. + * - `tokenId` token must be owned by `owner`. * - `expired` must be greater than block.number - * - If the caller is not `from`, it must be approved to lock this token + * - If the caller is not `owner`, it must be approved to lock this token * by either {lockApprove} or {setLockApprovalForAll}. * * Emits a {Locked} event. */ - function lockFrom(uint256 tokenId, uint256 expired) external; + function lock(uint256 tokenId, uint256 expired) external; /** * @dev Unlock `tokenId` token. * * Requirements: * - * - `tokenId` token must be owned by `from`. - * - the caller must be the operator who locks the token by {lockFrom} + * - `tokenId` token must be owned by `owner`. + * - the caller must be the operator who locks the token by {lock} * * Emits a {Unlocked} event. */ @@ -123,7 +123,7 @@ interface IERC5058 { /** * @dev Approve or remove `operator` as an lock operator for the caller. - * Operators can call {lockFrom} for any token owned by the caller. + * Operators can call {lock} for any token owned by the caller. * * Requirements: * @@ -165,11 +165,11 @@ interface IERC5058 { ### NFT lock approvals -An NFT owner can give another trusted operator the right to lock his NFT through the approve functions. The `lockApprove()` function only approves for the specified NFT, whereas `setLockApprovalForAll()` approves for all NFTs of the collection under the wallet. When a user participates in an NFTFi project, the project contract calls `lockFrom()` to lock the user's NFT. Locked NFTs cannot be transferred, but the NFTFi project contract can use the unlock function `unlockFrom()` to unlock the NFT. +An NFT owner can give another trusted operator the right to lock his NFT through the approve functions. The `lockApprove()` function only approves for the specified NFT, whereas `setLockApprovalForAll()` approves for all NFTs of the collection under the wallet. When a user participates in an NFTFi project, the project contract calls `lock()` to lock the user's NFT. Locked NFTs cannot be transferred, but the NFTFi project contract can use the unlock function `unlock()` to unlock the NFT. ### NFT lock/unlock -Authorized project contracts have permission to lock NFT with the `lockFrom` method. Locked NFTs cannot be transferred until the lock time expires. The project contract also has permission to unlock NFT in advance through the `unlockFrom` function. Note that only the address of the locked NFT has permission to unlock that NFT. +Authorized project contracts have permission to lock NFT with the `lock` method. Locked NFTs cannot be transferred until the lock time expires. The project contract also has permission to unlock NFT in advance through the `unlock` function. Note that only the address of the locked NFT has permission to unlock that NFT. ### NFT lock period From 18afdec1a6bd405243830d561e0a841aec8742fa Mon Sep 17 00:00:00 2001 From: radiocaca Date: Wed, 17 Aug 2022 20:29:22 +0800 Subject: [PATCH 40/40] refine tests --- assets/eip-5058/ERC5058.sol | 4 +- assets/eip-5058/test/test.ts | 71 ++++++++++++++---------------------- 2 files changed, 30 insertions(+), 45 deletions(-) diff --git a/assets/eip-5058/ERC5058.sol b/assets/eip-5058/ERC5058.sol index 03f311294f4e25..90fc3efcdd9594 100644 --- a/assets/eip-5058/ERC5058.sol +++ b/assets/eip-5058/ERC5058.sol @@ -128,7 +128,7 @@ abstract contract ERC5058 is ERC721, IERC5058 { _beforeTokenLock(operator, owner, tokenId, expired); lockedTokens[tokenId] = expired; - _lockApprovals[tokenId] = _msgSender(); + _lockApprovals[tokenId] = operator; emit Locked(operator, owner, tokenId, expired); @@ -154,7 +154,7 @@ abstract contract ERC5058 is ERC721, IERC5058 { _safeMint(to, tokenId, _data); - _lock(address(0), tokenId, expired); + _lock(_msgSender(), tokenId, expired); } /** diff --git a/assets/eip-5058/test/test.ts b/assets/eip-5058/test/test.ts index f67c5cb0691e49..4b728647987364 100644 --- a/assets/eip-5058/test/test.ts +++ b/assets/eip-5058/test/test.ts @@ -4,7 +4,7 @@ import { expect } from "chai"; import { ethers } from "hardhat"; import { EIP5058Mock } from "typechain-types"; -describe("EIP5058 contract", function() { +describe("ERC5058 contract", function() { let owner: SignerWithAddress; let alice: SignerWithAddress; let EIP5058: EIP5058Mock; @@ -12,9 +12,9 @@ describe("EIP5058 contract", function() { beforeEach(async () => { [owner, alice] = await ethers.getSigners(); - const EIP5058Factory = await ethers.getContractFactory("EIP5058Mock"); + const ERC5058Factory = await ethers.getContractFactory("EIP5058Mock"); - EIP5058 = await EIP5058Factory.deploy("Mock", "M"); + EIP5058 = await ERC5058Factory.deploy("ERC5058Mock", "ERC5058"); }); it("Deployment should assign the total supply of tokens to the owner", async function() { @@ -25,10 +25,9 @@ describe("EIP5058 contract", function() { it("lockMint works", async function() { const NFTId = 0; const block = await ethers.provider.getBlockNumber(); - const blockBefore = await ethers.provider.getBlock(block); - const timestamp = blockBefore.timestamp; - await EIP5058.lockMint(alice.address, NFTId, timestamp + 2); + await EIP5058.lockMint(alice.address, NFTId, block + 2); + expect(await EIP5058.lockExpiredTime(NFTId)).eq(block + 2); expect(await EIP5058.isLocked(NFTId)).eq(true); expect(await EIP5058.lockerOf(NFTId)).eq(owner.address); }); @@ -36,17 +35,18 @@ describe("EIP5058 contract", function() { it("Can not transfer when token is locked", async function() { const NFTId = 0; const block = await ethers.provider.getBlockNumber(); - const blockBefore = await ethers.provider.getBlock(block); - const timestamp = blockBefore.timestamp; - await EIP5058.lockMint(owner.address, NFTId, timestamp + 3); + await EIP5058.lockMint(owner.address, NFTId, block + 3); + expect(await EIP5058.isLocked(NFTId)).eq(true); // can not transfer when token is locked await expect(EIP5058.transferFrom(owner.address, alice.address, NFTId)).to.be.revertedWith( - "EIP5058: token transfer while locked", + "ERC5058: token transfer while locked", ); // can transfer when token is unlocked await ethers.provider.send("evm_mine", []); + + expect(await EIP5058.isLocked(NFTId)).eq(false); await EIP5058.transferFrom(owner.address, alice.address, NFTId); expect(await EIP5058.ownerOf(NFTId)).eq(alice.address); }); @@ -54,9 +54,7 @@ describe("EIP5058 contract", function() { it("isLocked works", async function() { const NFTId = 0; const block = await ethers.provider.getBlockNumber(); - const blockBefore = await ethers.provider.getBlock(block); - const timestamp = blockBefore.timestamp; - await EIP5058.lockMint(owner.address, NFTId, timestamp + 2); + await EIP5058.lockMint(owner.address, NFTId, block + 2); // isLocked works expect(await EIP5058.isLocked(NFTId)).eq(true); @@ -67,24 +65,22 @@ describe("EIP5058 contract", function() { it("lock works", async function() { const NFTId = 0; let block = await ethers.provider.getBlockNumber(); - const blockBefore = await ethers.provider.getBlock(block); - const timestamp = blockBefore.timestamp; - await EIP5058.lockMint(owner.address, NFTId, timestamp + 3); + await EIP5058.lockMint(owner.address, NFTId, block + 3); - await expect(EIP5058.lock(NFTId, timestamp + 5)).to.be.revertedWith( - "EIP5058: token is locked", + expect(await EIP5058.isLocked(NFTId)).eq(true); + await expect(EIP5058.lock(NFTId, block + 5)).to.be.revertedWith( + "ERC5058: token is locked", ); await ethers.provider.send("evm_mine", []); - await EIP5058.lock(NFTId, timestamp + 5); + expect(await EIP5058.isLocked(NFTId)).eq(false); + await EIP5058.lock(NFTId, block + 5); }); it("unlock works with lockMint", async function() { const NFTId = 0; const block = await ethers.provider.getBlockNumber(); - const blockBefore = await ethers.provider.getBlock(block); - const timestamp = blockBefore.timestamp; - await EIP5058.lockMint(owner.address, NFTId, timestamp + 3); + await EIP5058.lockMint(owner.address, NFTId, block + 3); // unlock works expect(await EIP5058.isLocked(NFTId)).eq(true); @@ -97,14 +93,11 @@ describe("EIP5058 contract", function() { const NFTId = 0; await EIP5058.mint(owner.address, NFTId); - await expect(EIP5058.unlock(NFTId)).to.be.revertedWith( - "EIP5058: locker query for non-locked token", + "ERC5058: locker query for non-locked token", ); const block = await ethers.provider.getBlockNumber(); - const blockBefore = await ethers.provider.getBlock(block); - const timestamp = blockBefore.timestamp; - await EIP5058.lock(NFTId, timestamp + 3); + await EIP5058.lock(NFTId, block + 3); expect(await EIP5058.isLocked(NFTId)).eq(true); await EIP5058.unlock(NFTId); expect(await EIP5058.isLocked(NFTId)).eq(false); @@ -113,25 +106,19 @@ describe("EIP5058 contract", function() { it("lockApprove works", async function() { const NFTId = 0; await EIP5058.mint(alice.address, NFTId); - let block = await ethers.provider.getBlockNumber(); - const blockBefore = await ethers.provider.getBlock(block); - const timestamp = blockBefore.timestamp; - await expect(EIP5058.lock(NFTId, timestamp + 2)).to.be.revertedWith( - "EIP5058: lock caller is not owner nor approved", - ); + await expect(EIP5058.lock(NFTId, block + 4)).to.be.revertedWith( + "ERC5058: lock caller is not owner nor approved", + ); await EIP5058.connect(alice).lockApprove(owner.address, NFTId); expect(await EIP5058.getLockApproved(NFTId)).eq(owner.address); - await expect(EIP5058.lock(NFTId, timestamp + 4)).to.be.revertedWith( - "EIP5058: lock from incorrect owner", - ); - await EIP5058.lock(NFTId, timestamp + 6); + await EIP5058.lock(NFTId, block + 8); expect(await EIP5058.isLocked(NFTId)).eq(true); await expect(EIP5058.lockApprove(alice.address, NFTId)).to.be.revertedWith( - "EIP5058: token is locked", + "ERC5058: token is locked", ); }); @@ -140,16 +127,14 @@ describe("EIP5058 contract", function() { await EIP5058.mint(alice.address, NFTId); const block = await ethers.provider.getBlockNumber(); - const blockBefore = await ethers.provider.getBlock(block); - const timestamp = blockBefore.timestamp; - await expect(EIP5058.lock(NFTId, timestamp + 2)).to.be.revertedWith( - "EIP5058: lock caller is not owner nor approved", + await expect(EIP5058.lock(NFTId, block + 2)).to.be.revertedWith( + "ERC5058: lock caller is not owner nor approved", ); await EIP5058.connect(alice).setLockApprovalForAll(owner.address, true); expect(await EIP5058.isLockApprovedForAll(alice.address, owner.address)).eq(true); - await EIP5058.lock(NFTId, timestamp + 6); + await EIP5058.lock(NFTId, block + 6); await EIP5058.connect(alice).setLockApprovalForAll(owner.address, false); expect(await EIP5058.isLockApprovedForAll(alice.address, owner.address)).eq(false);