Abstract

We propose to extend the EIP-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 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

NFTs, enabled by EIP-721, 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.
  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, 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 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.

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 EIP-721 MUST implement the IERC5058 interfaces:

// SPDX-License-Identifier: CC0-1.0

pragma solidity ^0.8.8;

/**
 * @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.
 * 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 IERC5058 {
    /**
     * @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:
     *
     * - `tokenId` token must be owned by `owner`.
     * - `expired` must be greater than block.number
     * - If the caller is not `owner`, it must be approved to lock this token
     * by either {lockApprove} or {setLockApprovalForAll}.
     *
     * Emits a {Locked} event.
     */
    function lock(uint256 tokenId, uint256 expired) external;

    /**
     * @dev Unlock `tokenId` token.
     *
     * Requirements:
     *
     * - `tokenId` token must be owned by `owner`.
     * - the caller must be the operator who locks the token by {lock}
     *
     * Emits a {Unlocked} event.
     */
    function unlock(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 {lock} 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);

    /**
     * @dev Returns the `tokenId` token lock expired time.
     */
    function lockExpiredTime(uint256 tokenId) external view returns (uint256);
}

Rationale

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 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 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

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 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 EIP-20 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 EIP-721.

Test Cases

Test cases written using hardhat can be found here

Reference Implementation

You can find an implementation of this standard in the assets folder.

Security Considerations

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 and related rights waived via CC0.