FINAL EIP-5192 - Minimal Soulbound NFTs

Hey @TimDaub, quick question on the EIP that seems to be a bit unclear even tho it has been set as “Final”. With regards to _mint operations, it is clear that the Locked event should be emitted. What about _burn operations? Should they emit the Unlocked event since the NFT is no longer bound? Thanks for clarifying in advance, I believe this information should be included in the EIP itself as well.

Thanks for your feedback. I agree that we missed defining lock and unlock for burn. But as burning is transferring to address(0) the token must be unlocked before a burn. But then what should happen after calling burn is out of scope as my opinion is that the token stops existing. Does that help?

While this has been marked as Final, it is pretty odd to have a Locked and Unlocked event. It is unfortunate that this EIP did not receive the proper review and consideration before being approved.

Typically, one would expect a single Lock function with data representing the state.

Different events for the two are not emitted when minting and burning tokens. Simply, the data within those events are updated.

    /// @dev This emits when ownership of any NFT changes by any mechanism.
    ///  This event emits when NFTs are created (`from` == 0) and destroyed
    ///  (`to` == 0). Exception: during contract creation, any number of NFTs
    ///  may be created and assigned without emitting Transfer. At the time of
    ///  any transfer, the approved address for that NFT (if any) is reset to none.
    event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);

Now, to emit an event, devs have two options, and users have one:

Developers:

  • Use an if statement that wastes gas
  • Bifurcate function logic to work around EIP definition

Users:

  • Pay more in gas

The expected state of only having 1 event is echoed in EIP-5633: Composable Soulbound NFT, EIP-1155 Extension.

Is there a reason as to why having two events is preferred?

2 Likes

You might enjoy the discussion in this comment: EIP-5192: add event LockingStatusChanged(uint256 tokenId, bool status) by 0xanders · Pull Request #5459 · ethereum/EIPs · GitHub We had considered using a single event and opted for two for better readability. So this was reviewed.

But yeah, gas efficiency wasn’t considered, as it’s an implementation detail and not part of the interface.

That is unfortunate, but I appreciate the link share! This choice will severely limit adoption and make it impossible to recommend.

It is less about actual financial cost as chains inevitably get cheaper daily. Still, this EIP requires spaghetti code in a sea of EIPs that swim vigorously in the opposite direction.

Especially since even if someone wants to emit both events from the same function, there must be a check to determine what event to emit and if they can emit that event unless individuals are allowed to multi-emit the same duplicate event multiple times.

While “just an interface” it has extremely broad implementation impacts that should have been considered.

Thanks for your time.

2 Likes

(I wrote this post in preparation for PeepAnEIP - have fun reading.)

The history of EIP-5192 - Minimal Soulbound tokens

This is the story of how EIP-5192 Minimal Soulbound tokens got started and how we reached a status final on the interface definition. It’s not a very long story, actually, but what’s remarkable is that it starts at 2 am in the morning with me sitting on the computer hammering words into the keys.

EIP-5192 is a product of quickly having to sketch out an idea as a document as a means to make my sleep more peaceful and not full of thoughts about Solidity interfaces.

But the reason I’ve ended up in this situation is that long before EIP-5192, I had involved myself with specifying EIP-4973 “Account-bound tokens,” and so a frustration in not being able to find solutions to the community feedback I had received eventually evolved into this document for composable “Minimal Soulbound tokens.”

The Timeline

Early in the year, in January, Vitalik posted their blog post titled “Soulbound,” and so when I visited ETHDenver to flee the German COVID-winter, I enjoyed the conversations there on Harberger taxes and what Kevin Owocki and others called non-skeuomorphic property. So even after returning, I dwelled on these thoughts, and by April fool’s day, I submitted a solution for EIP issue “1238” (Non-transferrable tokens) that would end up being called EIP-4973 “Account-bound tokens.”

A month later, Weyl et al. then released the Decentralized Society, and while I had been working with clients - I’m a freelancer - on implementing EIP-4973 as badges, this brought more attention to our previous work than we could have imagined or managed to deal with.

So to address the profound stream of user feedback we were getting for most of “Soulbound summer,” we huddled to produce meaningful and explanatory content on EIP-4973 regarding soul-binding, account-binding, and generally how to deal with that on a cultural, operational security, and technical level.

Quite a few times, I felt like withdrawing as the requirements seemed complex and overwhelming. And during one of those frustrating days, when I was about to go to bed, I had this idea about a minimal non-transferrable token specification for SBTs: Not even to aid EIP-4973 “Account-bound tokens” but rather to hedge my risk - as quantum thought and to cement these potentially divergent missions. So on August 1, 2022, I published EIP-5192 “Minimal Soulbound tokens” on GitHub.

The Motivation

What had become particularly frustrating with the DeSoc publication had been that although we had directly associated ourselves with the term “Soulbound tokens,” our EIP-4973 hadn’t been mentioned by Weyl et al.'s mega-popular paper. And so suddenly, left and right, I saw ourselves being confronted with Medium articles on how to “EASILY IMPLEMENT SOULBOUND TOKENS” and their content being this one message: “Use EIP-721 and just revert on all transfer functions.”

contract NFT {
//...
  function transferFrom(
    address _from,
    address _to,
    uint256 _tokenId) external payable {
    revert("SOULBOUND");
  }
}

And having worked on Account-bound tokens for four months already, I found it incredibly frustrating that the most basic composability ideas were missing from those medium tutorials. So I felt compelled to use my newly found EIP-editing skills to improve the situation, and that one night, I had the deciding idea.
It would just be a particular “Soulbound token” EIP-165 identifier that supportsInterface(bytes4 interfaceID) would yield `true’ upon. And so here’s what that first iteration looked like (I had called the standard EIP-5555 as a placeholder name):

So essentially, EIP-5192 was an EIP-165 identifier value where, e.g., EIP-721 contracts were supposed to yield true upon calling supportsInterface(bytes4 interfaceID). And that’s all. We didn’t specify events, and generally, when that EIP was to be implemented, a token would be permanently bound to an account.

The Problem

But as I’ve already mentioned in my ERC lightning talk at devcon in Bogota, the above wouldn’t be truly an EIP if it wasn’t heavily factoring in feedback from the community. And one such item of feedback we had been getting excessively but reasonably already in EIP-4973 was that binding tokens to accounts is considered an anti-pattern. This is because users want to reserve the freedom of key rotating their private keys, and since account abstraction isn’t ready for prime time yet: So the concern for account-binding is really that, although it’s potentially possible to account-bind only to contracts that enable key rotation, practically users and developers will bind to EOAs, and hence the standard must consider this in the design.

Confidently, I can herein say that this is a real problem that needs to be addressed in the Soulbound token specifications - and there are no safe escape paths from that line of reasoning for now. I ended up accepting that, and so when being confronted with the same “account-binding” rationale in EIP-5192 as in EIP-4973, I accepted its importance and gladly ended up realizing that an opinionated approach towards standardizing this concept of Soulbound tokens is anyways a better one. Here’s how we solved that problem.

The Compromise

So eventually, @aram fixed the specification by introducing the function locked for a uint256 tokenId and that it returns a boolean conditionally.

And actually, while I have been fairly skeptical at first about this change - now it has been growing on me tremendously. Because I accept that standard documents cannot enforce implementation details anyways - and with that, we also have to acknowledge that, fundamentally, account-binding, soul-binding, or whatever you may wanna call it is a property of implementation and not part of the standard interface.

But before I continue on this line of reasoning, I wanna reveal now the finalized version of EIP-5192 below for anyone to see:

An overview:

  • Any soul-bindable EIP-721 token is now recognizable by a machine/marketplace/wallet using the EIP-165 interface function.
  • It is subject to the implementor whether an EIP-5192 token shall be permanently bound to an account. But any other configuration is available through the unopinionated interface definition (e.g., even implementing a permanently transferrable EIP-721 token).
  • Dynamic locking use cases, e.g., the one outlined in DeSoc with community recovery, are implementable using the events and the locked view function.

The Implementation

And so that’s how EIP-5192 got standardized, and just for you, the reader, to understand why I put such great focus on the document’s provenance: It’s because I think it’s important to highlight its history to explain the design decision we took towards finalization.

And then lastly, I also want to point out a first minimal implementation. Namely, a long-shot pull request I recently did for the public-assembly project. In fact, they had implemented a non-transferable token exactly in the way how medium tutorials had initially suggested - and so the entire fix was actually not that complicated. It consisted of adding the function locked but always returning true and by emitting an event Locked(...) when minting the NTT. And importing the interface. Here’s the entire change in a screenshot:

and the maintainer’s feedback:

So I personally see this as a huge win, as it means we’ve gone from 0 to 1 on the specification and on the implementation. Now: It validates the NTT use case for now - and arguably, it doesn’t validate the Soulbound token use cases just yet. But I feel that we have a pretty general interface that allows implementing many token-locking use cases for wallets, marketplaces, and inventory management systems. So I’m excited for the future and where this specification might be headed then.

The Conclusion

The conclusion is that I submitted a specification document on a whim and that the Ethereum Magicians community managed to turn it into something genuinely useful for an Ethereum community project. It’s important and useful to factor in external feedback and allows open participation. While this can be scary at times through losing oversights, it pays off over the long term by creating a more credibly neutral solution.

6 Likes

Recording of PEEPanEIP #89: EIP-5192: Minimal Soulbound NFTs with @TimDaub

2 Likes

Is there some sample code?

yeah here FINAL EIP-5192 - Minimal Soulbound NFTs - #14 by TimDaub

Thank you.
Is this completed?
I can’t understand how the Lock function works.

1 Like

That implementation does not follow the spec because locked doesn’t throw when passing a tokenId assigned to the zero address, or am i missing something?

Your code:

/*
 *  EIP-5192 Functions
 */
function locked(uint256 tokenId) external view returns (bool) {
  return true;
}

The spec:

/// @notice Returns the locking status of an Soulbound Token
/// @dev SBTs assigned to zero address are considered invalid, and queries
/// about them do throw.
/// @param tokenId The identifier for an SBT.
function locked(uint256 tokenId) external view returns (bool);
1 Like

you are correct it should throw

1 Like

I am looking for something such as this standard, but for 1155’s as well. Couldn’t this be achieved? Soulbound 1155’s (with fungibility)?

EDIT: this is the closest I have found EIP-5727: Semi-Fungible Soulbound Token - #11 by 0xTimepunk

I submitted an EIP that is the same simple interface EIP-5192. Please take a look.

1 Like

I just posted a similar solution (Minimalistic non-transferable interface - #4 by sullof) and discovered this one thanks to a comment. The issue with proposal 5192 is that it assumes that there is a specific point at which the token switches from being transferable to non-transferable. In my work in the gaming industry, while it is necessary to know if a token is locked or not, the reason for this lock is often dependent on other contracts. There is no event that switches the status of the NFT, but the NFT can be locked for various reasons such as the owner having a locked balance, only a certain number of tokens being unlocked at the same time, or the owner having to own a second specific token in order to unlock the first. In all these cases, the NFT is monitoring other contracts and determining if it is transferable or not based on their status. If we were to implement a system where the NFT switches its transferability through direct transactions in a game, it would happen dozens of times a day and would be unsustainable. I believe that the token should simply return its status without emitting any events.

Interesting, use case!

If you don’t want to emit events every time, it can make sense that you implement EIP-5192’s function locked(), and then upon status changes, you don’t emit the Locked and Unlock events. It’s not ideal but at least you have partial standard support then. In that case, although you’d have to notify indexers, at least all wallets etc. could check if your token was locked by calling the function locked.

1 Like

Maybe I wrongly assumed that your proposal was requiring the emission of events at status change.
It looks like it is not Add EIP-5192 - Minimal Soulbound NFTs by TimDaub · Pull Request #5192 · ethereum/EIPs · GitHub
Actually, if it just is

interface IERC5192 {
  function locked(uint256 tokenId) external view returns (bool);
}

it looks good to me. I will remove mine and will support this.
It will force me to find a new naming for ERC721Lockable but it is ok :smiley:

Never mind, I realized that was an old implementation and the new one is here

Exchanges listen to events to update their databases. That means that OpenSea is not going to call the locked view, most likely it will assume that if no event has been emitted, the status has not changed. For this reason I suggest that the events MUST be emitted if someone implements the interface.

  /// @notice Emitted when the locking status is changed to locked.
  /// @dev If a token is minted and the status is locked, this event MUST be emitted.
  /// @param tokenId The identifier for a token.
  event Locked(uint256 tokenId);

Leaving the implementer the freedom of deciding if emitting an event or not risks to create confusion.

1 Like

Just for confirmation, the actual valid specification is always here (and it won’t change anymore because it has been marked “final”): ERC-5192: Minimal Soulbound NFTs

Exchanges listen to events to update their databases. That means that OpenSea is not going to call the locked view, most likely it will assume that if no event has been emitted, the status has not changed. For this reason I suggest that the events MUST be emitted if someone implements the interface.

Yes, sadly, true. Would you mind expanding on why sometimes emitting the Locked and Unlocked events aren’t in your interest? I understand that it’d increase your gas costs to emit those events. But e.g., for an indexer, I honestly see no better way than working with events and emitting them on every state change.

If a transaction causes a change in status, it may make sense to emit an event. In games, things are more dynamic. A token may be transferable or not depending on what happens in the game. For example, a token can only be transferred if the owner has enough rewards in the game’s ERC20 token or if they also own another token. Additionally, only one token can be transferred at a time if the owner has more assets in the same family. These issues are often resolved by staking tokens in a pool and giving up ownership. However, the current trend is to allow the owner to keep ownership while locking the token in different ways. In this case, the feature that is most affected is the transferability. I believe that emitting events makes sense with Soulbound NFTs, but not with NFTs used in games or other dynamic environments. That is why I suggest a minimalistic interface that only tells the caller if the token is transferable or not, ignoring the nature of the token. In my opinion, a Soulbound token is just one example of a non-transferable token and we should not create a standard for this sub-case. Of course, this is just my opinion and I may not be correct.

1 Like