Title: ERC_6982__Default_Lockable_Proposal
Category: ERCs

Author: sullof
Date: 2023-03-17T06:04:05.227Z

Many proposals for lockable ERC721 contracts exist in different phases of development:


Ethereum Improvement Proposals


ERC-5192: Minimal Soulbound NFTs
Minimal interface for soulbinding EIP-721 NFTs







Ethereum Improvement Proposals


ERC-5633: Composable Soulbound NFT, EIP-1155 Extension
Add composable soulbound property to EIP-1155 tokens







Ethereum Improvement Proposals


ERC-5753: Lockable Extension for EIP-721
Interface for disabling token transfers (locking) and re-enabling them (unlocking).





and many others.
Unfortunately, any of them misses something or is too complicated and add extra functions that do not need to be part of a standard.
I tried to influence ERC-5192 making many comments and a PR that was closed by @Pandapip1 who suggested I make a new proposal. So, here we are.
The updated Interface (based on comment and discussions):
pragma solidity ^0.8.9;

// ERC165 interfaceId 0x6b61a747
interface IERC6982 {
  // This event MUST be emitted upon deployment of the contract, establishing 
  // the default lock status for any tokens that will be minted in the future.
  // If the default lock status changes for any reason, this event 
  // MUST be re-emitted to update the default status for all tokens.
  // Note that emitting a new DefaultLocked event does not affect the lock 
  // status of any tokens for which a Locked event has previously been emitted.
  event DefaultLocked(bool locked);

  // This event MUST be emitted whenever the lock status of a specific token 
  // changes, effectively overriding the default lock status for this token.
  event Locked(uint256 indexed tokenId, bool locked);

  // This function returns the current default lock status for tokens.
  // It reflects the value set by the latest DefaultLocked event.
  function defaultLocked() external view returns (bool);

  // This function returns the lock status of a specific token.
  // If no Locked event has been emitted for a given tokenId, it MUST return 
  // the value that defaultLocked() returns, which represents the default 
  // lock status.
  // This function MUST revert if the token does not exist.
  function locked(uint256 tokenId) external view returns (bool);
}

The primary limit in EIP-5192 (which I liked and I used in a couple of projects) is that


it has 2 events for Locked and Unlocked, which is not optimal.
To make a comparison, it’s like in the ERC721 instead of Transfer(from, to, id) used for mints, transfers and burns, there were Transfer(from, to, id), Mint(to, id), Burn(from, id), etc.


it forces you to emit an event even when the token is minted, causing a waste of gas when a token borns with a status and dies with it.


Take for example most soulbounds and non-transferable badges. They will be locked forever and it does not make sense to emit an extra event for all the tokens.
Using this interface, instead, the contract emits DefaultLocked(bool locked) when deployed, and that event sets the initial status of every token. Sometimes, as suggested by @tbergmueller in the comments, a token can have an initial status that changes at some point. If that happens, the DefaultLocked event can be emitted again. This implies that marketplaces and other observers must refer to last emitted DefaultLocked event if a Locked event has not been emitted for a specific tokenId.
The Locked events define the new status of any tokenId.
locked returns the current status, allowing other contracts to interact with the token.
defaultLocked returns the default status (since other contracts cannot get the event). The method also allows to have an interfaceId different than ERC5192, avoiding conflicts (thanks to @urataps)
This is an efficient solution that reduces gas consumption and covers most scenarios.
I think that functions to lock, unlock, lock approvals, etc. should be managed as extensions, and should be not included in a minimalistic interface about lockability.
The official EIP

Ethereum Improvement Proposals


ERC-6982: Efficient Default Lockable Tokens
A gas-efficient approach to lockable ERC-721 tokens





For an implementation, you can look at
https://github.com/ndujaLabs/erc721lockable
Notes:
On May 2nd, I added the suggestion to emit DefaultLocked() again if the default behavior changes, as suggested by @ tbergmueller
On May 6th, I added a defaultLocked function to avoid conflicts with ERC5192, thanks to @urataps
PS. I will keep the code of the interface above updated to avoid misunderstanding.

Author: andyscraven
Date: 2023-03-19T23:52:05.544Z

I like this approach as not all use cases need to allow for everything as not all use cases need everything. It is, after all, best practice to us the Single Responsibility principle when possible.

Author: tbergmueller
Date: 2023-05-02T14:46:26.217Z

Still on the search on “the” interface to expose transfer-locks in our ERC-6956 … and since there are so many similar interfaces all doing the same I have the feeling I’m spamming the complete forum here, but nonetheless;
For us, also this interface would work with a small modification;
We do see use-cases, where the default lock-status changes over time, similar as the well-known mechnics of metadata-reveal. So a collection may be minted and the collection owner decides they cannot be transferred for the first 6 months. And after 6 months, tokens per default become transferable.
I suggest to define that DefaultLocked must be emitted whenever the default-lock status changes which includes at contract-deployment time.

Author: sullof
Date: 2023-05-03T00:42:33.274Z

Thanks. I like the suggestion.
I will include it in the EIP proposal, when (if) I will make it — I am so busy that I can’t find the time to make it 
For now I made an update here, and added a note about your suggestion.

Author: xtools-at
Date: 2023-05-03T16:01:00.124Z

thanks @sullof for the work! i’m in the same boat, have used EIP-5192 before but don’t like it as of the stated limitations either, and the other examples are also not exactly appealing to me for various reasons. just implementing your proposal for my next project  i hope this becomes an official standard, i’d also be happy to support with writing the EIP if you like.

Author: sullof
Date: 2023-05-03T17:37:52.300Z

I just created a PR on the EIP repo at
https://github.com/ethereum/EIPs/pull/6982
@xtools-at if you have suggestion to improve the text, let me know. Any feedback is much appreciated.

Author: sullof
Date: 2023-05-05T21:31:29.688Z

I did what @Pandapip1 suggested (see post), but nobody is reviewing the PR above.
Has anyone any idea about the process?

Author: urataps
Date: 2023-05-06T01:54:00.345Z

I consider this idea simplistic and more efficient in many ways.
Using a single status event instead of two mirroring ones is easier to listen and index. Also, the DefaultLocked event is a smart way to avoid the burden of emitting upon each mint.
However, the modified Locked event introduces a backward compatibility issue with events from EIP-5192, which already left past the review stage and is used by multiple projects. It would be important to address this by either stating clearly that events are incompatible or change to the less efficient version 
Secondly, the locked function’s description should be more explicit in stating that it must return the latest default locked state if no token-specific lock actions have been performed, as in the observer and marketplaces example you provided.
Also, I’m curious to discover how the locked function would implement this functionality for unlockable tokens. One possible solution to think about would be to have the timestamp when the latest default state is changed, and disregard any outdated token-specific locks until then.

Author: sullof
Date: 2023-05-06T04:45:16.120Z

Those are good points. Thanks.



 urataps:

However, the modified Locked event introduces a backward compatibility issue with events from EIP-5192, which already left past the review stage and is used by multiple projects. It would be important to address this by either stating clearly that events are incompatible or change to the less efficient version 


I think that this proposal is an alternative to EIP-5192. So, whoever implements it, should not implement the first. If someone must implement both, the contract will be forced to emit two events for the same action, which does not make much sense.
From the point of view of a marketplace, I assume that the marketplace first check the interfaceId and then, depending on it, listen to Locked(id) and Unlocked(id) or to Locked(id,isLocked).
The (painful) alternative would be to rename the event and call it some other way.



 urataps:

Secondly, the locked function’s description should be more explicit in stating that it must return the latest default locked state if no token-specific lock actions have been performed, as in the observer and marketplaces example you provided.


I totally agree on this. I added a note to the EIP.



 urataps:

Also, I’m curious to discover how the locked function would implement this functionality for unlockable tokens. One possible solution to think about would be to have the timestamp when the latest default state is changed, and disregard any outdated token-specific locks until then.


That depends on the specific project. There can be so many scenarios.

Author: sullof
Date: 2023-05-06T04:48:49.961Z

What if I use the world sealed instead of locked? The interface would become
interface IERC6982 /* is IERC165 */ {
  
    // MUST be emitted when the contract is deployed, 
    // defining the default status of any token that will be minted.
    // It may be emitted again if/when the default behavior changes.
    event DefaultSealed(bool sealed);

    // MUST be emitted any time the status of a specific tokenId changes
    event Sealed(uint256 indexed tokenId, bool sealed);

   // Returns the status of the tokenId.
   // If no Locked event occurred for the tokenId, it MUST return the default status.
   // It MUST revert if the token does not exist.
    function sealed(uint256 tokenId) external view returns (bool);
}


I like it, but I see it as an extreme scenario, because I have already implemented that interface in production in a couple of projects and updating the contracts to change the event would create a lot of issues in the services that listen to the events.

Author: urataps
Date: 2023-05-06T11:05:13.820Z

sullof:

From the point of view of a marketplace, I assume that the marketplace first check the interfaceId and then, depending on it, listen to Locked(id) and Unlocked(id) or to Locked(id,isLocked).


If that’s the case then it should have a separate interfaceId from the EIP-5192 in order to identify it. Since the 5192 one is already decided by the selector of the locked function, I think we need a different signature for default lockable tokens. Going with the sealed version would solve this issue and still make sense naming-wise. If incompatibility with EIP-5192 is assumed, I would go for it.



 sullof:

I like it, but I see it as an extreme scenario, because I have already implemented that interface in production in a couple of projects and updating the contracts to change the event would create a lot of issues in the services that listen to the events.


However, this is a valid argument for keeping the original locked signature. Also, there might be other projects that desire interface compatibility with 5192 since marketplaces and off-chain services are used to it, which would force them to choose one version of the two.The only way I see to keep supporting this and also identify default lockable tokens is to add an extra method.
To avoid boilerplate and keep things simple, I would propose a defaultLocked(uint256 tokenId) external view returns(bool)  method which returns false whenever a token-specific lock status is changed since the last default lock, and true if none happened. This would also prove that the smart contract is keeping track of the status correctly and “resets” all tokens to the default state. For tokens that don’t support token-specific locks this method could just easily return true every time. In this way the interface is still compatible with EIP-5192 and also identifiable such that marketplace know what types of events to listen.

Author: sullof
Date: 2023-05-06T19:20:32.121Z

That is a great suggestion. I will add it, thanks.
Considering how minimalistic is the proposal, I would be happy to add you as a contributor to the EIP, if you are interested. If so, just let me know.
But I would prefer to add a
function defaultLocked() external view returns (bool);

which returns the default status.

Author: urataps
Date: 2023-05-08T17:02:35.386Z

Returning the default status is also a good solution, and it would be even simpler.
I would be glab to be a contributor to this EIP, thank you for the proposal, @sullof.

Author: sullof
Date: 2023-05-10T20:51:49.926Z

I changed the status of the PR from Draft to Review.

Author: SamWilsn
Date: 2023-06-02T01:04:15.063Z

Why do you limit changing the default to only before the first token event? Should probably explain the reasoning behind that in the rationale section.

Author: hiddenintheworld
Date: 2023-06-02T02:55:11.780Z

Could you modify it in a way so it is more customizable, maybe adding a threshold decided by the owner, or decided by voting power? Adding something like allowing the use of ZK proof to lock and unlock could also be something that add modularity.

Author: sullof
Date: 2023-06-02T06:24:30.080Z

SamWilsn:

Why do you limit changing the default to only before the first token event? Should probably explain the reasoning behind that in the rationale section.


Thank you for bringing up this concern. I can see how my original explanation may have led to some confusion. I will update the ERC.
The proposal does not, in fact, restrict changes to the default status solely to before the first token event. Rather, the DefaultLocked event can be triggered anytime there is a change in the default status applicable to all tokens.
The primary area of uncertainty pertains to whether a newly emitted DefaultLocked event should supersede all previously emitted Locked events, or whether it should only apply to tokens that have not yet been impacted by a Locked event.
To address this, I’m contemplating the introduction of an override parameter to the DefaultLocked event. This modification would look like this:
event DefaultLocked(bool status, bool override);

Here, if override is set to true, the event will take precedence over any previously emitted Locked events, effectively resetting the status of all tokens. However, if override is false, the event will only influence tokens that have yet to be subjected to a Locked event. I would appreciate your thoughts on this potential solution.

Author: sullof
Date: 2023-06-02T06:36:48.165Z

hiddenintheworld:

Could you modify it in a way so it is more customizable, maybe adding a threshold decided by the owner, or decided by voting power? Adding something like allowing the use of ZK proof to lock and unlock could also be something that add modularity.


Thank you for your input. I appreciate your suggestions to enhance the modularity of the proposal. However, I believe that the interface should primarily focus on providing a broad approach that can be readily applied in a variety of scenarios. It would be more prudent for the implementer to optimize it further based on specific use-cases.
Adding conditions on who can lock or unlock tokens might complicate the implementation, especially in simpler scenarios. For instance, in the case of badges and soulbound tokens, the status is usually fixed at the start and remains unchanged. Additional complexities could make the application of this standard unnecessarily burdensome.
When you mention making the proposal more customizable, could you clarify what aspects you’re referring to? If the goal is to manage more granular details of token transferability, you might find ERC-6454 more suitable. It provides a comprehensive framework for managing transferability and would not conflict with this proposal.
As an additional note, I’d like to direct your attention to an example implementation of this proposal available at:

github.com


ethereum/EIPs/blob/721d967b4f27f2d644117f8c578166e274b7171f/assets/eip-6982/contracts/ERC721Lockable.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

// Authors: Francesco Sullo <francesco@sullo.co>

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "./IERC721Lockable.sol";
import "./IERC6982.sol";

// This is an example of lockable ERC721 using IERC6982 as basic interface

contract ERC721Lockable is IERC6982, IERC721Lockable, Ownable, ERC721, ERC721Enumerable {
  using Address for address;

  mapping(address => bool) private _locker;
  mapping(uint256 => address) private _lockedBy;





  This file has been truncated. show original





This implementation showcases a system managing NFTs that can be locked in place as opposed to being staked. This approach affords the token owner the benefit of retaining ownership while still imposing a lock on the token. It’s worth noting that this implementation has been utilized effectively in production for several months.
However, despite the success of this specific implementation, I am of the opinion that further additions or complexities to ERC-6982 may not necessarily be advantageous. The proposal aims to maintain a balance between functionality and simplicity, and I believe it achieves that as it currently stands.
Thus said, I am totally available to change it if there is a strong support for it.

Author: hiddenintheworld
Date: 2023-06-02T07:06:25.668Z

You could make the criteria for locking and unlocking tokens more customizable. For instance, rather than having a fixed locking status for each token, it could depend on certain conditions or be modified by specific users. Ideas like controlling the lock status dynamically via bytecode. However, be aware that it’s a complex task with many potential pitfalls, and potentially security risks, which I’m outlining in a simplified manner below.
We will create a contract that accepts bytecode and executes it to determine if a tokenId is locked or not. The owner can set the bytecode logic.
contract DynamicLockERC721 is ERC721, Ownable {
    // Mapping from token ID to bytecode
    mapping (uint256 => bytes) private _logic;

    constructor(string memory name, string memory symbol) ERC721(name, symbol) {}

    // Function to set the logic of a token
    function setLogic(uint256 tokenId, bytes memory bytecode) public onlyOwner {
        _logic[tokenId] = bytecode;
    }

    // Function to check the lock status of a token
    function isLocked(uint256 tokenId) public view returns (bool) {
        bytes memory bytecode = _logic[tokenId];
        bytes32 result;
        
        assembly {
            result := mload(add(bytecode, 0x20))
        }
        
        // Assuming that the bytecode returns a boolean, convert the result into a boolean
        return result != bytes32(0);
    }

    // Override transfer function to include lock status check
    function _transfer(address from, address to, uint256 tokenId) internal override {
        require(!isLocked(tokenId), "ERC721: token is locked");

        super._transfer(from, to, tokenId);
    }
}

Author: SamWilsn
Date: 2023-06-02T14:53:37.552Z

sullof:

The primary area of uncertainty pertains to whether a newly emitted DefaultLocked event should supersede all previously emitted Locked events, or whether it should only apply to tokens that have not yet been impacted by a Locked event.


Personally I would not expect the status of already minted tokens to change when the default changes. Doing otherwise would mean tokens exist in one of three states: locked, unlocked, and undefined. That behaviour might be a little unexpected.

Author: sullof
Date: 2023-06-02T15:56:54.449Z

I am not sure I got it. The proposal does not give any recommendation about how to lock/unlock tokens, leaving a lot of freedom to the user.

Author: sullof
Date: 2023-06-02T16:03:25.701Z

SamWilsn:

Personally I would not expect the status of already minted tokens to change when the default changes.


The default status does not apply only to un-minted tokens, it applies to any token. But a token is not in an undefined status, it is in the default status.
The idea is that at deployment, before than any token is minted, the default status is defined. Than, any minted token inherit that status. So, if initially the tokens are unlocked, all the minted tokens will be unlocked.
Let’s say that — like in the ERC721Lockable example — the tokens are stakable in a pool, which would mean that the pool is authorized to lock the token, if an owner “stakes” their token, that token is locked.
At that point, even if the default status would change, that specific token would remain locked, because it has been locked by the pool (which clear the doubt I had, actually).
Maybe, it makes sense to request that the default locked status can be changed only before than any single status changes.

Author: SamWilsn
Date: 2023-06-02T16:41:13.486Z

Ah, I think I misunderstood your comment then.

Author: sullof
Date: 2023-06-02T17:07:05.599Z

I will try to make the text of the ERC more clear. Maybe ChatGPT can help me edit it

Author: sullof
Date: 2023-06-02T17:15:58.383Z

I updated the comments in the interface. Now it looks like this and I think that the recommendations are more clear:
interface IERC6982 {
  // This event MUST be emitted upon deployment of the contract, establishing 
  // the default lock status for any tokens that will be minted in the future.
  // If the default lock status changes for any reason, this event 
  // MUST be re-emitted to update the default status for all tokens.
  // Note that emitting a new DefaultLocked event does not affect the lock 
  // status of any tokens for which a Locked event has previously been emitted.
  event DefaultLocked(bool locked);

  // This event MUST be emitted whenever the lock status of a specific token 
  // changes, effectively overriding the default lock status for this token.
  event Locked(uint256 indexed tokenId, bool locked);

  // This function returns the current default lock status for tokens.
  // It reflects the value set by the latest DefaultLocked event.
  function defaultLocked() external view returns (bool);

  // This function returns the lock status of a specific token.
  // If no Locked event has been emitted for a given tokenId, it MUST return 
  // the value that defaultLocked() returns, which represents the default 
  // lock status.
  // This function MUST revert if the token does not exist.
  function locked(uint256 tokenId) external view returns (bool);
}

Author: hiddenintheworld
Date: 2023-06-03T10:56:42.060Z

We still have a long way to go on this one, more concrete end goals are needed:

Where is the interoperability with another contract (what state should be mutable)
Any threshold state variable?
Does the owner control it all?

Author: sullof
Date: 2023-06-03T19:42:25.510Z

hiddenintheworld:

Where is the interoperability with another contract (what state should be mutable)


The contract has two main states: locked and unlocked. These states are mutable, but the exact rules of mutation, such as who can change them and under what conditions, are left to the discretion of the implementer.
As an example implementation of the interface, I utilized the ERC721Lockable contract by ndujaLabs.
interface IERC721Lockable is IERC6982 {
  event LockerSet(address locker);
  event LockerRemoved(address locker);
  event ForcefullyUnlocked(uint256 tokenId);

  function lockerOf(uint256 tokenID) external view returns (address);
  function isLocker(address _locker) external view returns (bool);
  function setLocker(address pool) external payable;
  function removeLocker(address pool) external;
  function hasLocks(address owner) external view returns (bool);
  function lock(uint256 tokenID) external;
  function unlock(uint256 tokenID) external;
  function unlockIfRemovedLocker(uint256 tokenID) external;
}

It extends ERC6982 and adds the necessary features to make the token “lockable.” The intention here is to allow tokens to be “staked” in a pool without the need for actual staking, instead, the tokens are simply locked in place leaving the ownership to the tokenId’s owner.
However, this is merely one possible scenario. The goal of this proposal is not to dictate the specifics of implementation, but rather to provide a basic framework. This framework aims to be as flexible as possible, allowing for various implementations of lockable tokens, similar to what ERC173 and ERC5313 do with ownership.



 hiddenintheworld:

Does the owner control it all?


The role of the owner varies depending on the specific implementation and scenario. In the example of the ERC721Lockable contract, the owner of the NFT grants approval to a pool, which then has the ability to lock/unlock the token.
In other scenarios, such as soulbounds and badges, it’s likely that the contract owner is the one who locks them. Yet, other scenarios may present different criteria. This proposal does not prescribe a specific role for the owner. Instead, it aims to establish a common base that can be adapted to various needs and contexts.

Author: SamWilsn
Date: 2023-06-08T21:16:01.504Z

Your comment, @sullof, on the pull request was most illuminating:


About the apparent contradiction, there are tokens that are locked during the pre-reveal phase and so would emit a DefaultLocked(true) event, that will be unlocked after the reveal. In that case, forcing the contract to emit a Locked(tokenId, false) for every tokenId would go against the primary goal behind this proposal, i.e., saving gas when a locked/unlocked status is applicable to all the tokens. I think that if that happens, emitting a single DefaultLocked(false) event would be better.


Now I understand why changing the DefaultLocked status could be useful.
I’m still concerned that an application subscribing to the events from a ERC-6982 compatible contract cannot unambiguously know the lock state of a token. After the following events, what is the locked state of 0x1, 0x2, and 0x3, and which requirements from the EIP determine those states?

DefaultLocked(locked=true)
Transfer(_from=0x000...000, _to=0xaaa...aaa, _tokenId=0x1)
Transfer(_from=0x000...000, _to=0xaaa...aaa, _tokenId=0x2)
Transfer(_from=0x000...000, _to=0xaaa...aaa, _tokenId=0x3)
Locked(tokenId=0x1, locked=true)
Locked(tokenId=0x2, locked=false)
DefaultLocked(locked=false)

Author: sullof
Date: 2023-06-10T00:04:04.916Z

After 1, 2, 3, 4
0x1, 0x2 and 0x3 are locked
After 5
0x1 is locked because a specific event has been emitted for it
After 6
0x2 is unlocked because a specific event has been emitted for it
After 7
0x1 and 0x2 keeps their status, since a Locked event was emitted for them
0x3 moves from locked to unlocked because no specific event has been emitted for it and so it refers to default Locked that in the meantime has changed
In general, the rule is that DefaultLocked refers to any tokenId for which no Locked events has been emitted. In other words, the Locked event has priority on DefaultLocked

Author: hiddenintheworld
Date: 2023-06-10T11:24:00.747Z

lock and unlock can be written into
function flip(){
locked = !locked;
}

to always flip the state, so we have one function instead of two, but this may not be good if you want to have a smart contract that locks something, even if it is already locked, you don’t want to flip the state but still wants to lock is again(not changing the state).

Author: sullof
Date: 2023-06-12T22:03:13.613Z

That flip function make sense. However, the interface is supposed to include only generic events and views that work in most scenarios. Any function to manage the status is left to the specific implementation.

Author: sullof
Date: 2023-06-16T20:57:04.168Z

Contracts that may use ERC-6982 as a base for pools and other DeFi systems, may need to log the address of the locker/unlocker. I am considering the possibility of moving from
event Locked(uint256 indexed tokenId, bool locked);

to
event Locked(uint256 indexed tokenId, bool locked, address locker);

where the third parameter is optional and can be set to ZeroAddress when unnecessary.
Personally, since I have implemented the interface in production, this change would put my deployed contracts off-standard, but that may be solved by ERC-5269 by @xinbenlv
@urataps @SamWilsn @xtools-at @tbergmueller What do you think?

Author: urataps
Date: 2023-06-17T12:15:43.360Z

sullof:

Contracts that may use ERC-6982 as a base for pools and other DeFi systems, may need to log the address of the locker/unlocker


Personally, I think the third locker argument in the event is unnecessary, the proposal should be generic enough to cover all use-cases and not delve into implementation details. If some specific applications need to emit the address of the locker, they can create a separate event for that. Otherwise, other contracts are forced to emit the redundant zero address that may incur additional gas fees.
Also, I want to catch up on this previous comment.



 sullof:

The primary area of uncertainty pertains to whether a newly emitted DefaultLocked event should supersede all previously emitted Locked events, or whether it should only apply to tokens that have not yet been impacted by a Locked event.


Now I understand there are two cases to be addressed: 1) The DefaultLocked event resets any token-specific Lock state or 2) individual token-specific Lock is not impacted by a default state change. I see prospective use-cases in both situations and this proposal shouldn’t be bound just to one. Therefore I consider the introduction of a second bool override argument to the DefaultLocked event as appropriate, since it eliminates this ambiguity.

Author: sullof
Date: 2023-06-17T22:52:20.896Z

urataps:

 I think the third locker argument in the event is unnecessary, the proposal should be generic enough to cover all use-cases and not delve into implementation details. 


I concur with your viewpoint. It’s indeed beneficial to maintain the proposal’s simplicity and generality, avoiding getting entangled in specific implementation details.



 urataps:

 Therefore I consider the introduction of a second bool override argument to the DefaultLocked event as appropriate, since it eliminates this ambiguity. 


Upon testing various scenarios, I’ve observed that altering the default status post utilization of tokens is a rare occurrence. The usual pattern involves locking during a sale, followed by unlocking after the sale, with the default status generally remaining untouched thereafter.
If we consider a situation where numerous assets are locked while the default status is set to unlocked, can we identify a context where you’d desire to unlock all assets simultaneously? I find it hard to visualize.
Let’s consider the following sequence:

The default status is unlocked.
TokenId #3 gets locked.
defaultLocked is switched to locked (TokenId #3 remains unaffected).
defaultLocked is switched back to unlocked.

This presents an odd scenario that doesn’t seem to hold much practical significance. Consequently, I lean towards maintaining the current structure without the override feature.

Author: tbergmueller
Date: 2023-06-19T11:02:16.735Z

I tend to agree with @urataps that the address of locker may not be needed in many use-cases. Also for the DeFi-UseCase you referenced I believe that in many scenarios rather the address(es), who have the authority to unlock a token, would be more interesting.
Unlocking is often more complex, as more than one party may have permission to unlock and the permission to unlock may change during lock-time as well (e.g. you get a loan with an NFT as collateral, the loan may get sold to another DeFi account, so suddenly the new DeFi account can unlock it.
I think an implementers interested in recording who locked/unlocked a token would rather implement additional lockerOf(tokenId) and canUnlock(tokenId, address operator) views.

Author: hiddenintheworld
Date: 2023-06-19T18:01:13.705Z

Maybe add a function to lock and unlock individual tokens: While the interface allows for locking and unlocking of tokens through the Locked event, it might be useful to provide a function that enables contract owners to lock and unlock individual tokens programmatically. This function could take a tokenId parameter and a bool parameter to indicate the new lock status of the token.


You can also add a mapping function and a variable that allows all mapped NFT to this pointer could be toggled as it is mapped separately, this gives more flexibility.


When the mapping is disabled, then it will revert back to its initial state (assume it is False here, meaning no locking)

Author: urataps
Date: 2023-06-20T07:20:07.308Z

sullof:

If we consider a situation where numerous assets are locked while the default status is set to unlocked, can we identify a context where you’d desire to unlock all assets simultaneously?


I understand our problem now, for me it’s hard to visualize such a scenario as well, once a token’s lock status is individually changed then a default override should not make sense. The contract can also provide means to deactivate the individual status for the default one, but that is an implementation detail we should not pursue in this proposal.
So in my opinion, for your provided scenario, the step 4 would leave TokenId#3 untouched. But, just for the sake of curiosity, let’s assume step 4 would change the status of TokenId#3 to unlocked in some kind of strict marketplace, there still is the locked(uint256 tokenId) function to rely on in this case.



 hiddenintheworld:

Maybe add a function to lock and unlock individual tokens: While the interface allows for locking and unlocking of tokens through the Locked event, it might be useful to provide a function that enables contract owners to lock and unlock individual tokens programmatically.


I see why such a function would make sense, but because of the vast set of possible implementations of the lock/unlock process, this proposal should not specify any particular locking function. Consider the token standards, they provide the events and the view methods for querying tokens, but not the minting function. It’s left at the discretion of the implementer of the standard, which is responsible only for emitting the events and set the state correspondingly.



 hiddenintheworld:



You can also add a mapping function and a variable that allows all mapped NFT to this pointer could be toggled as it is mapped separately, this gives more flexibility.


When the mapping is disabled, then it will revert back to its initial state (assume it is False here, meaning no locking)




I’m sorry I don’t understand the idea here. What is mapped to what? What kind of flexibility would we get if we introduce this mapping? What do you mean by disable a mapping?

Author: sullof
Date: 2023-06-21T19:58:57.073Z

I appreciate your insights and I believe the consensus leans towards maintaining the existing interface as it is.
To address @urataps’ point, indeed, the locked(uint256 tokenId) function does provide a safety net. However, this interface was primarily designed to streamline the process for event loggers. It’s doubtful that platforms like OpenSea would regularly invoke locked(id) to verify a token’s status. For a more dynamic handling of the locking status, projects could consider adopting ERC6454.
As for @hiddenintheworld’s suggestion of a function that allows contract owners to manipulate the lock status of individual tokens programmatically, it’s a valid point. But introducing more functions could inadvertently favor certain implementations over others, which might not be ideal. It’s crucial to remember that these functions should be tailored to suit the specific objectives of the project. For instance, consider the interface used in ERC721Lockable (erc721lockable/contracts/IERC721Lockable.sol at main · ndujaLabs/erc721lockable · GitHub).
I did contemplate proposing it as a standard, but considering the vast array of unique use cases in this field, it dawned on me that standardizing it might not be the best course of action.

Author: sullof
Date: 2023-07-27T01:27:40.330Z

@urataps I am using this in production in many projects and I know of others that are doing the same. I am moving it to the review stage. Is that ok for you?

Author: urataps
Date: 2023-07-27T08:39:53.295Z

Sure it is @sullof, I think the proposal can be moved to review stage

Author: conner
Date: 2023-08-16T03:42:00.018Z

Hey I really appreciate the focus on simplicity and gas consciousness in this EIP. I’ve seen many lockable NFT proposals and this one is my favorite for the reasons that motivated you. I’m most concerned with the clashing of interfaceId with ERC-5192 that was mentioned already in the thread. I’m close to wanting to put this in my own contracts but am holding off because of that point. Is there a final decision if an interfaceId clash is accepted or are we going to change the naming to “seal” or something else?

Author: sullof
Date: 2023-08-18T19:35:44.307Z

There was an initial clash because in the initial proposal there was only a locked(tokenId) function, like in ERC5192. Later we added the (actually very useful) defaultLocked() function that solved the issue, altering the interfaceId.

Author: urataps
Date: 2023-09-14T09:30:21.971Z

Added small readability improvements Update EIP-6982: Minor Improvements EIP6982 by urataps · Pull Request #7713 · ethereum/EIPs · GitHub before moving to Last Call @sullof

Author: sullof
Date: 2023-11-14T17:21:12.804Z

The ERC moved today to Last Call. If there is any other comment, please, make it asap.

Author: 0xCLARITY
Date: 2023-11-17T16:44:56.771Z

Some brief thoughts:


I might reword the comment for the DefaultLocked event. In initially reading the ERC, I thought that emitting a new DefaultLocked event would only change the locked status for “tokens that will be minted in the future” (and not change the locked status for tokens that had already been minted).


It is nicer for listeners to only have a single event to subscribe to, but it is slightly more expensive in terms of gas to have two arguments in the event, and have one of them indexed (as compared to Locked(uint256 tokenId) in ERC-5192). Since one of the goals here is gas efficiency, perhaps that should be considered.


Aside: I’ve been thinking about this as well and put together ERC-7558 - Batch Minimal Soulbound NFTs. Since you’ve been thinking about this problem longer than I have I would appreciate any thoughts / feedback!

Author: sullof
Date: 2023-11-19T19:27:15.906Z

0xCLARITY:

I might reword the comment for the DefaultLocked event. In initially reading the ERC, I thought that emitting a new DefaultLocked event would only change the locked status for “tokens that will be minted in the future” (and not change the locked status for tokens that had already been minted).


Thanks, I will clarify that.



 0xCLARITY:

It is nicer for listeners to only have a single event to subscribe to, but it is slightly more expensive in terms of gas to have two arguments in the event, and have one of them indexed (as compared to Locked(uint256 tokenId) in ERC-5192). Since one of the goals here is gas efficiency, perhaps that should be considered.


The reason for having a single Locked event is to reduce the amount of code you write in the smart contract. Also to keep the interface as minimalistic as possible. I agree that the locked event consume a bit more gas, but it sounded like a good compromise since the boolean adds only 280 gas.



 0xCLARITY:

Aside: I’ve been thinking about this as well and put together ERC-7558 - Batch Minimal Soulbound NFTs. Since you’ve been thinking about this problem longer than I have I would appreciate any thoughts / feedback!


About your EIP, you could have posted here your suggestions and we could have worked together on it. I think that it is always a good practice to participate in the existing proposal, instead of making a new one just for a detail. Initially, I asked for changes in the ERC5192, but it was already in final state when I saw it, and than I was forced to start a new one, but if not I would have definitely tried to improve 5192 if it was not finalized.

Author: sullof
Date: 2023-11-19T19:30:40.237Z

0xCLARITY:

I thought that emitting a new DefaultLocked event would only change the locked status for “tokens that will be minted in the future”


The current description says:
  // This event MUST be emitted upon deployment of the contract, establishing 
  // the default lock status for any tokens that will be minted in the future.
  // If the default lock status changes for any reason, this event 
  // MUST be re-emitted to update the default status for all tokens.

I thought that last line is clear enough specifying that must be re-emitted to update the status for all tokens. I modified it this way:
  // This event MUST be emitted upon deployment of the contract, establishing 
  // the default lock status for any tokens that will be minted in the future.
  // If the default lock status changes for any reason, this event 
  // MUST be re-emitted to update the default status for all tokens, including the
  // ones already minted.

Author: 0xCLARITY
Date: 2023-11-22T20:48:11.183Z

sullof:

  // This event MUST be emitted upon deployment of the contract, establishing 
  // the default lock status for any tokens that will be minted in the future.
  // If the default lock status changes for any reason, this event 
  // MUST be re-emitted to update the default status for all tokens, including the
  // ones already minted.



I think that’s sufficient to clear it up - but I might even go with something like this (but realize this is a minor nit)
  // This event MUST be emitted upon deployment of the contract, 
  // establishing the default lock status for this contract's tokens.
  // If the default lock status changes for any reason, this event 
  // MUST be re-emitted to update the default status for all tokens,
  // including the ones already minted.
  // 




 sullof:

About your EIP, you could have posted here your suggestions and we could have worked together on it. I think that it is always a good practice to participate in the existing proposal, instead of making a new one just for a detail. Initially, I asked for changes in the ERC5192, but it was already in final state when I saw it, and than I was forced to start a new one, but if not I would have definitely tried to improve 5192 if it was not finalized.


Makes sense - I wasn’t sure what the best practice was here. Since this proposal is incompatible with ERC-5192 though, I do think it might still make sense to have a separate proposal that merely extends that, especially since 5192 has some adoption already (supported by OpenSea).



ERC-7558: Minimal Lockable Range NFTs

interface ERC7558 is ERC6982 {
  event RangeLocked(uint256 _fromTokenId, uint256 _toTokenId, boolean locked);
}



You proposed this in the other thread - but perhaps it should be a part of the core ERC-6982 proposal? I think having all of [defaultStatus, individualTokenStatus, tokenRange] would make this proposal even more flexible.

Author: sullof
Date: 2023-11-22T23:28:49.963Z

0xCLARITY:

 Makes sense - I wasn’t sure what the best practice was here. Since this proposal is incompatible with ERC-5192, though, I do think it might still make sense to have a separate proposal that merely extends that, especially since 5192 has some adoption already (supported by OpenSea). 


Actually, it’s not incompatible. In fact, it simply adds two more events to ERC-5192, so the type(ERC7558).interfaceId would remain the same. I’m assuming you’re suggesting a unique interfaceId for it, perhaps something like 0x75887588. For all these reasons, I believe that extending ERC-5192 is quite reasonable.



 0xCLARITY:

 You proposed this in the other thread - but perhaps it should be a part of the core ERC-6982 proposal? I think having all of [defaultStatus, individualTokenStatus, tokenRange] would make this proposal even more flexible. 


The issue here is that ERC-6982 is intended to be a generic proposal. If we were to add anything to this proposal, it should be treated as an extension. I think it is better to keep the proposal as it is.
Moreover, ERC-6982 is expected to be finalized by the end of the month. I’m not sure if it’s appropriate to include an extension at this stage.
@urataps, what are your thoughts?

Author: sullof
Date: 2023-11-22T23:30:42.588Z

@SamWilsn I would appreciate your opinion about the point in the previous comment about adding an extension like this to the proposal (with a magic interfaceId like in ERC4906)
interface ERC6892Range is ERC6982 {
  event RangeLocked(uint256 fromTokenId, uint256 toTokenId, boolean locked);
}

My primary doubt is that it would make harder for the listeners to understand what the events mean.

Author: sullof
Date: 2023-11-26T21:56:56.936Z

0xCLARITY:

I think that’s sufficient to clear it up - but I might even go with something like this (but realize this is a minor nit)


I tried to improve the comments in the interface, but the PR produces errors I can’t fix, and that could stuck the process. If so, I may close it and leave the description as it is now. Let’s hope for the best.

Author: SamWilsn
Date: 2023-11-27T17:08:17.568Z

I am notoriously bad at predicting what developers will find easy to understand

Author: sullof
Date: 2023-12-15T23:17:55.205Z

ERC6982 has been finalized. Thanks everyone for the contributions.

Author: sullof
Date: 2024-01-30T18:06:39.481Z

Waiting for Open Zeppelin to (hopefully) add the interface in its contracts, you can get it with
npm install @ndujalabs/erc721lockable

@ndujalabs/erc721lockable is the implementation of an NFT lockable in pools. You don’t need to implement it, of course, just import the IERC6982 interface as
import {IERC6982} from "@ndujalabs/erc721lockable/IERC6982.sol";

Author: tbergmueller
Date: 2024-02-02T08:28:21.770Z

Hi,
I’m glad to see this moved through! We’re a bit slower with the Asset-Bound NFTs (ERC-6956), as it’s a quite complex proposal.
ERC-6982 duplicates essentially parts of the mechanics of our optional IERC6956Floatable interface, in particular the events FloatingStateChange, FloatingAllStateChange.
To avoid standard-duplication and also motivate external platforms (e.g. OpenSea) to implement interfaces, I’m inclined to drop our two events from IERC6956Floatable and RECOMMEND implementing ERC-6982 together with IERC6956Floatable. I think this could also be included in the Full reference implementation of ERC-6956 (which implements all optional interfaces).
The logic would be:

Implement Events from ERC-6982 instead of FloatingStateChange, FloatingAllStateChange
locked(tokenId) returns !floating(anchorByToken(tokenId))
defaultLocked() is implemented additionally.

Questions

Do you see any conflicts between ERC-6956 and your ERC-6982?
This marries IERC6956Floatable somewhat to ERC-6982, although ERC-6956 cannot formally list ERC-6982 as required for ERC-6956, since only an optional extension is affected. Anybody seeing issues with this?

Thanks!

Author: sullof
Date: 2024-02-02T22:12:32.385Z

tbergmueller:

Do you see any conflicts between ERC-6956 and your ERC-6982?


I don’t see problems with the functions, but you may have issues with the events. For example
event FloatingStateChange(bytes32 indexed anchor, uint256 indexed tokenId, FloatState isFloating, address operator);

expects 4 parameters, instead of 2, which means that if you replace it with the Locked events in IERC6982, you would either lose information or having to rethink the logic .



 tbergmueller:

This marries IERC6956Floatable somewhat to ERC-6982, although ERC-6956 cannot formally list ERC-6982 as required for ERC-6956, since only an optional extension is affected. Anybody seeing issues with this?


Maybe @SamWilsn can respond to that.

Author: SamWilsn
Date: 2024-02-03T01:30:29.891Z

tbergmueller:

This marries IERC6956Floatable somewhat to ERC-6982, although ERC-6956 cannot formally list ERC-6982 as required for ERC-6956, since only an optional extension is affected. Anybody seeing issues with this?


If you’re talking about the required preamble header, think of it more as “required reading” and not “required to implement”.

Author: tbergmueller
Date: 2024-02-05T09:54:20.693Z

Thanks for your comments @sullof  and @SamWilsn!



 sullof:

expects 4 parameters, instead of 2, which means that if you replace it with the Locked events in IERC6982, you would either lose information or having to rethink the logic .


Were considering to simplify our whole proposal and make the mapping tokenId <> anchor optional. A much simpler way with identical security is when anchor (bytes32) is used directly as a tokenId (uint256 == bytes32).
This would revert our rationale “Why do you use an anchor<>tokenId mapping and not simply use tokenIds directly?”. Consequently, we’ll need to change all ERC-6956 events and omit the anchor from events. Since I do not know of any large adoptions besides ours yet, this breaking change seems acceptable. dApps interested in the mapping anchor <> tokenId will need to call anchorByTokenId().



 SamWilsn:

If you’re talking about the required preamble header, think of it more as “required reading” and not “required to implement”.


Yes I was, thanks for the clarification! If it is only “required to read” it’s fine IMHO to list ERC-6982 there.