Many proposals for lockable ERC721 contracts exist in different phases of development:
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
For an implementation, you can look at
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.