Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add EIP: Lockable Extension for ERC-721 #7066

Merged
merged 57 commits into from
Jun 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
a47a153
erc721lockable init
May 25, 2023
a8ed235
update discussion
May 25, 2023
ae7c47d
update license
May 25, 2023
b9f6c76
update readme
May 25, 2023
6671aae
add eip header
May 25, 2023
d1bdba3
update title
May 25, 2023
b0d7f74
update readme
May 25, 2023
ab1866e
update readme
May 25, 2023
82b4ed1
update readme
May 25, 2023
59d0de1
update readme
May 25, 2023
964ba2e
update eip number
May 25, 2023
2d23193
update eip number
May 25, 2023
990d9ff
update eip number
May 25, 2023
d339b67
update eip number
May 25, 2023
b8dcef0
update eip number
May 25, 2023
930ec58
update readme
May 25, 2023
5bfacfe
update readme
May 25, 2023
624b444
update readme
May 25, 2023
548f97e
update readme
May 25, 2023
8c55c14
error codes
May 25, 2023
9166465
added interface to specification
May 30, 2023
383f2b4
lint fixe
May 31, 2023
42c2c4e
transfer with lock/approve
Jun 8, 2023
fbec9a7
rename
Jun 8, 2023
c6cb04e
readme update
Jun 8, 2023
e7383b9
optimize lock/unlock
Jun 9, 2023
3dc3465
readme with new functions
Jun 9, 2023
9499213
readme update
Jun 9, 2023
44170bb
error codes
Jun 9, 2023
deee3f8
discussion link
Jun 9, 2023
452b536
tokenId
Jun 10, 2023
95609b4
remove redundancy
Jun 13, 2023
7d7da36
updates test
Jun 13, 2023
73601d1
updates doc
Jun 13, 2023
1c38c46
Merge branch 'master' into ERC721Lockable
Jun 13, 2023
fa29104
review fixes
Jun 14, 2023
b2b2df3
Merge branch 'ERC721Lockable' of https://github.com/streamnft-tech/EI…
Jun 14, 2023
6e442df
Merge branch 'master' into ERC721Lockable
Jun 14, 2023
ce80ffa
overrides and interface correction
Jun 16, 2023
f059e05
Merge branch 'ERC721Lockable' of https://github.com/streamnft-tech/EI…
Jun 16, 2023
e8bc642
add locker to transferAndLock
Jun 16, 2023
9bc7fe7
remove locker
Jun 16, 2023
9147291
license
Jun 17, 2023
8422c63
Merge branch 'master' into ERC721Lockable
Jun 17, 2023
0b40ce1
doc update
Jun 17, 2023
cb8f306
Merge branch 'ERC721Lockable' of https://github.com/streamnft-tech/EI…
Jun 17, 2023
2afa055
doc update
Jun 17, 2023
0bfb0f8
doc update
Jun 17, 2023
1e15d1d
Merge pull request #1 from piyush-chittara/ERC721Lockable
Jun 17, 2023
ab46fa0
Merge branch 'master' into ERC721Lockable
Jun 19, 2023
943374a
Merge branch 'master' into ERC721Lockable
Jun 19, 2023
94c8e7a
author updates
Jun 19, 2023
5dc7ee4
Merge branch 'ERC721Lockable' of https://github.com/streamnft-tech/EI…
Jun 19, 2023
fec2757
Merge branch 'master' into ERC721Lockable
Jun 20, 2023
f7f0826
Merge branch 'master' into ERC721Lockable
Jun 22, 2023
e314b93
Merge branch 'master' into ERC721Lockable
Jun 24, 2023
c5beb51
Merge branch 'master' into ERC721Lockable
SamWilsn Jun 26, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 156 additions & 0 deletions EIPS/eip-7066.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
---
eip: 7066
title: Lockable Extension for ERC-721
description: Interface for enabling locking of ERC-721 using locker and approved
author: Piyush Chittara (@piyush-chittara), StreamNFT (@streamnft-tech), Srinivas Joshi (@SrinivasJoshi)
discussions-to: https://ethereum-magicians.org/t/eip-7066-lockable-extension-for-erc721/14425
status: Draft
type: Standards Track
category: ERC
created: 2023-05-25
requires: 165, 721
---

## Abstract

An extension of [ERC-721](./eip-721.md), this standard incorporates `locking` features into NFTs, allowing for various uses while preventing sale or transfer. The token's `owner` can `lock` it, setting up locker address (either an EOA or a contract) that exclusively holds the power to unlock the token. Owner can also provide approval for tokenId, enabling ability to lock asset while address holds the token approval. Token can also be locked by `approved`, assigning locker to itself. Upon token transfer, these rights get purged.

## Motivation

[ERC-721](./eip-721.md) has sparked an unprecedented surge in demand for NFTs. However, despite this tremendous success, NFT economy suffers from secondary liquidity where it remains Illiquid in owner’s wallet. There are projects such as NFTfi, Paraspace which aims to address the liquidity challenge, but they entail below mentioned inconveniences and risks for owners as they necessitate transferring the participating NFTs to the projects' contracts.

- Loss of utility: The utility value of NFTs diminishes when they are transferred to an escrow account, no longer remaining under the direct custody of the owners.
- Lack of composability: The market could benefit from increased liquidity if NFT owners had access to multiple financial tools, such as leveraging loans and renting out their assets for maximum returns. Composability serves as the missing piece in creating a more efficient market.
- Smart contract vulnerabilities: NFTs are susceptible to loss or theft due to potential bugs or vulnerabilities present in the smart contracts they rely on.

The aforementioned issues contribute to a poor user experience (UX), and we propose enhancing the [ERC-721](./eip-721.md) standard by implementing a native locking mechanism:
Rather than being transferred to a smart contract, an NFT remains securely stored in self-custody but is locked.
During the lock period, the NFT's transfer is restricted while its other properties remain unchanged.
NFT Owner retains the ability to use or distribute it’s utility

NFTs have numerous use cases where the NFT must remain within the owner's wallet, even when it serves as collateral for a loan. Whether it's authorizing access to a Discord server, or utilizing NFT within a play-to-earn (P2E) game, owner should have the freedom to do so throughout the lending period. Just as real estate owner can continue living in their mortgaged house, take personal loan or keep tenants to generate passive income, these functionalities should be available to NFT owners to bring more investors in NFT economy.


Lockable NFTs enable the following use cases :

- NFT-collateralized loans: Utilize NFT as collateral for a loan without locking it on the lending protocol contract. Instead, lock it within owner’s wallet while still enjoying all the utility of NFT.
- No collateral rentals of NFTs: Borrow an NFT for a fee without the need for significant collateral. Renter can use the NFT but not transfer it, ensuring the lender's safety. The borrowing service contract automatically returns the NFT to the lender once the borrowing period expires.
- Buy Now Pay Later: The buyer receives the locked NFT and can immediately begin using it. However, they are unable to sell the NFT until all installments are paid. Failure to complete the full payment results in the NFT returning to the seller, along with a fee.
- Composability: Maximize liquidity by having access to multiple financial tools. Imagine taking a loan against NFT and putting it on rentals to generate passive income.
- Primary sales: Mint an NFT for a partial payment and settle the remaining amount once owner is satisfied with the collection's progress.
- Soulbound: Organization can mint and self-assign `locker`, send token to user and lock the asset.
- Safety: Safely and conveniently use exclusive blue chip NFTs. Lockable extension allows owner to lock NFT and designate secure cold wallet as the unlocker. This way, owner can keep NFT on MetaMask and easily use it, even if a hacker gains access to MetaMask account. Without access to the cold wallet, the hacker cannot transfer NFT, ensuring its safety.

This proposal is different from other locking proposals in number of ways:

- This implementation provides a minimal implementation of `lock` and `unlock` and believes other conditions like time-bound are great ideas but can be achieved without creating a specific implementation. Locking and Unlocking can be based on any conditions (e.g. repayment, expiry). Therefore time-bound unlocks a relatively specific use case that can be achieved via smart-contracts themselves without that being a part of the token contract.
- This implementation proposes a separation of rights between locker and approver. Token can be locked with approval and approved can unlock and withdraw tokens (opening up opportunities like renting, lending, bnpl etc), and token can be locked lacking the rights to revoke token, yet can unlock if required (opening up opportunities like account-bound NFTs).
- Our proposal implement ability to `transferAndLock` which can be used to transfer, lock and optionally approve token. Enabling the possibility of revocation after transfer.

streamnft-tech marked this conversation as resolved.
Show resolved Hide resolved
By extending the [ERC-721](./eip-721.md) standard, the proposed standard enables secure and convenient management of underlying NFT assets. It natively supports prevalent NFTFi use cases such as staking, lending, and renting. We anticipate that this proposed standard will foster increased engagement of NFT owners in NFTFi projects, thereby enhancing the overall vitality of the NFT ecosystem.

## Specification

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174.

### Overview

[ERC-721](./eip-721.md) compliant contracts MAY implement this EIP to provide standard methods of locking and unlocking the token at its current owner address.

Token owner MAY `lock` the token and assign `locker` to some `address` using `lock(uint256 tokenId, address _locker)` function, this MUST set `locker` to `_locker`. Token owner or approved MAY `lock` the token using `lock(uint256 tokenId)` function, this MUST set `locker` to `msg.sender`. Token MAY be `unlocked` by `locker` using `unlock` function. `unlock` function MUST delete `locker` mapping and default to `address(0)`.

If the token is `locked`, the `lockerOf` function MUST return an address that is `locker` and can `unlock` the token. For tokens that are not `locked`, the `lockerOf` function MUST return `address(0)`.

`lock` function MUST revert if token is not already locked. `unlock` function MUST revert if token is not locked. ERC-721 `approve` function MUST revert if token is locked. ERC-721 `_tansfer` function MUST revert if token is locked. ERC-721 `_transfer` function MUST pass if token is locked and `msg.sender` is `approved` and `locker` both. After ERC-721 `_transfer`, values of `locker` and `approved` MUST be purged.

Token MAY be transferred and `locked`, and OPTIONAL setup `approval` to `locker` using `transferAndLock` function. This is RECOMMENDED for use-cases where Token transfer and subsequent revocation is REQUIRED.

### Interface

```
// SPDX-License-Identifier: CC0-1.0

pragma solidity >=0.7.0 <0.9.0;

/// @title Lockable Extension for ERC721
/// @dev Interface for the Lockable extension
/// @author StreamNFT

interface IERC7066{

/**
* @dev Emitted when tokenId is locked
*/
event Lock (uint256 indexed tokenId, address _locker);

/**
* @dev Emitted when tokenId is unlocked
*/
event Unlock (uint256 indexed tokenId);

/**
* @dev Lock the tokenId if msg.sender is owner or approved and set locker to msg.sender
*/
function lock(uint256 tokenId) external;

/**
* @dev Lock the tokenId if msg.sender is owner and set locker to _locker
*/
function lock(uint256 tokenId, address _locker) external;

Copy link
Contributor

@sullof sullof Jun 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

@sullof sullof Jun 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I may add to ERC-6982 a second event that accept the locker as parameters.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello @sullof , thanks for the suggestion. I have tried to remove redundancies in ERC7066 and I believe EIP6982 is using couple of more functions to achieve the locking functionality. Effectively ERC7066 has lesser code and optimized gas. Let me know your thoughts

/**
* @dev Unlocks the tokenId if msg.sender is locker
*/
function unlock(uint256 tokenId) external;

/**
* @dev Tranfer and lock the token if the msg.sender is owner or approved.
* Lock the token and set locker to caller
* Optionally approve caller if bool setApprove flag is true
*/
function transferAndLock(uint256 tokenId, address from, address to, bool setApprove) external;

/**
* @dev Returns the wallet, that is stated as unlocking wallet for the tokenId.
* If address(0) returned, that means token is not locked. Any other result means token is locked.
*/
function lockerOf(uint256 tokenId) external view returns (address);
}
```

## Rationale

This proposal set `locker[tokenId]` to `address(0)` when token is `unlocked` because we delete mapping on `locker[tokenId]` freeing up space. Also, this assertion helps our contract to validate if token is `locked` or `unlocked` for internal function calls.

streamnft-tech marked this conversation as resolved.
Show resolved Hide resolved
This proposal exposes `transferAndLock(uint256 tokenId, address from, address to, bool setApprove)` which can be used to transfer token and lock at the receiver's address. This additionally accepts input `bool setApprove` which on `true` assign `approval` to `locker`, hence enabling `locker` to revoke the token (revocation conditions can be defined in contracts and `approval` provided to contract). This provides conditional ownership to receiver, without the privilege to `transfer` token.

## Backwards Compatibility

This standard is compatible with [ERC-721](./eip-721.md) standards.

Existing Upgradedable [ERC-721](./eip-721.md) can upgrade to this standard, enabling locking capability inherently and unlock underlying liquidity features.

## Test Cases

Test cases can be found [here](../assets/eip-7066/test/test.js).

## Reference Implementation

Reference Interface can be found [here](../assets/eip-7066/IERC7066.sol).

Reference Implementation can be found [here](../assets/eip-7066/ERC7066.sol).

## Security Considerations

There are no security considerations related directly to the implementation of this standard for the contract that manages [ERC-721](./eip-721.md).

### Considerations for the contracts that work with lockable tokens

- Once `locked`, token can not be further `approved` or `transfered`.
- If token is `locked` and caller is `locker` and `appoved` both, caller can transfer the token.
- `locked` token with `locker` as in-accesible account or un-verified contract address can lead to permanent lock of the token.
- There are no MEV considerations regarding lockable tokens as only authorized parties are allowed to lock and unlock.

## Copyright

Copyright and related rights waived via [CC0](../LICENSE.md).
160 changes: 160 additions & 0 deletions assets/eip-7066/ERC7066.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// SPDX-License-Identifier: CC0-1.0

pragma solidity ^0.8.0;

import "./IERC7066.sol";

/// @title ERC7066: Lockable Extension for ERC721
/// @dev Implementation for the Lockable extension ERC7066 for ERC721
/// @author StreamNFT

abstract contract ERC7066 is ERC721,IERC7066{


/*///////////////////////////////////////////////////////////////
ERC7066 EXTENSION STORAGE
//////////////////////////////////////////////////////////////*/

//Mapping from tokenId to user address for locker
mapping(uint256 => address) internal locker;

/*///////////////////////////////////////////////////////////////
ERC7066 LOGIC
//////////////////////////////////////////////////////////////*/

/**
* @dev Returns the locker for the tokenId
* address(0) means token is not locked
* reverts if token does not exist
*/
function lockerOf(uint256 tokenId) public virtual view override returns(address){
require(_exists(tokenId), "ERC7066: Nonexistent token");
return locker[tokenId];
}

/**
* @dev Public function to lock the token. Verifies if the msg.sender is owner or approved
* reverts otherwise
*/
function lock(uint256 tokenId) public virtual override{
require(locker[tokenId]==address(0), "ERC7066: Locked");
require(_isApprovedOrOwner(_msgSender(), tokenId), "Require owner or approved");
_lock(tokenId,msg.sender);
}

/**
* @dev Public function to lock the token. Verifies if the msg.sender is owner
* reverts otherwise
*/
function lock(uint256 tokenId, address _locker) public virtual override{
require(locker[tokenId]==address(0), "ERC7066: Locked");
require(ownerOf(tokenId)==msg.sender, "ERC7066: Require owner");
_lock(tokenId,_locker);
}

/**
* @dev Internal function to lock the token.
*/
function _lock(uint256 tokenId, address _locker) internal {
locker[tokenId]=_locker;
emit Lock(tokenId, _locker);
}

/**
* @dev Public function to unlock the token. Verifies the msg.sender is locker
* reverts otherwise
*/
function unlock(uint256 tokenId) public virtual override{
require(locker[tokenId]!=address(0), "ERC7066: Unlocked");
require(locker[tokenId]==msg.sender);
_unlock(tokenId);
}

/**
* @dev Internal function to unlock the token.
*/
function _unlock(uint256 tokenId) internal{
delete locker[tokenId];
emit Unlock(tokenId);
}

/**
* @dev Public function to tranfer and lock the token. Reverts if caller is not owner or approved.
* Lock the token and set locker to caller
*. Optionally approve caller if bool setApprove flag is true
*/
function transferAndLock(address from, address to, uint256 tokenId, bool setApprove) public virtual override{
SamWilsn marked this conversation as resolved.
Show resolved Hide resolved
_transferAndLock(tokenId,from,to,setApprove);
}

/**
* @dev Internal function to tranfer, update locker/approve and lock the token.
*/
function _transferAndLock(uint256 tokenId, address from, address to, bool setApprove) internal {
transferFrom(from, to, tokenId);
if(setApprove){
_approve(msg.sender, tokenId);
}
_lock(tokenId,msg.sender);
}

/*///////////////////////////////////////////////////////////////
OVERRIDES
//////////////////////////////////////////////////////////////*/

/**
* @dev Override approve to make sure token is unlocked
*/
function approve(address to, uint256 tokenId) public virtual override(IERC721, ERC721) {
require (locker[tokenId]==address(0), "ERC7066: Locked");
super.approve(to, tokenId);
}

/**
* @dev Override _beforeTokenTransfer to make sure token is unlocked or msg.sender is approved if
* token is lockApproved
*/
function _beforeTokenTransfer(
address from,
address to,
uint256 startTokenId,
uint256 quantity
) internal virtual override {
// if it is a Transfer or Burn, we always deal with one token, that is startTokenId
if (from != address(0)) {
require(locker[startTokenId]==address(0)
|| ( locker[startTokenId]==msg.sender && (isApprovedForAll(ownerOf(startTokenId), msg.sender)
|| getApproved(startTokenId) == msg.sender)), "ERC7066: Locked" );
}
super._beforeTokenTransfer(from,to,startTokenId,quantity);
}

/**
* @dev Override _afterTokenTransfer to make locker is purged
*/
function _afterTokenTransfer(
address from,
address to,
uint256 startTokenId,
uint256 quantity
) internal virtual override {
// if it is a Transfer or Burn, we always deal with one token, that is startTokenId
if (from != address(0)) {
delete locker[startTokenId];
}
super._afterTokenTransfer(from,to,startTokenId,quantity);
}

/*///////////////////////////////////////////////////////////////
ERC165 LOGIC
//////////////////////////////////////////////////////////////*/

/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC721) returns (bool) {
return
interfaceId == type(IERC7066).interfaceId ||
super.supportsInterface(interfaceId);
}
}
48 changes: 48 additions & 0 deletions assets/eip-7066/IERC7066.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// SPDX-License-Identifier: CC0-1.0

pragma solidity ^0.8.0;

/// @title Lockable Extension for ERC721
/// @dev Interface for ERC7066
/// @author StreamNFT

interface IERC7066 is IERC721{

/**
* @dev Emitted when tokenId is locked
*/
event Lock (uint256 indexed tokenId, address _locker);

/**
* @dev Emitted when tokenId is unlocked
*/
event Unlock (uint256 indexed tokenId);

/**
* @dev Lock the tokenId if msg.sender is owner or approved and set locker to msg.sender
*/
function lock(uint256 tokenId) external;

/**
* @dev Lock the tokenId if msg.sender is owner and set locker to _locker
*/
function lock(uint256 tokenId, address _locker) external;

/**
* @dev Unlocks the tokenId if msg.sender is locker
*/
function unlock(uint256 tokenId) external;

/**
* @dev Tranfer and lock the token if the msg.sender is owner or approved.
* Lock the token and set locker to caller
* Optionally approve caller if bool setApprove flag is true
*/
function transferAndLock(address from, address to, uint256 tokenId, bool setApprove) external;

/**
* @dev Returns the wallet, that is stated as unlocking wallet for the tokenId.
* If address(0) returned, that means token is not locked. Any other result means token is locked.
*/
function lockerOf(uint256 tokenId) external view returns (address);
}
Loading