FINAL EIP-5192 - Minimal Soulbound NFTs

Sounds great @TimDaub, sorry that I didn’t make the PR earlier, pretty was busy with project work :slight_smile:

EIP-5192: Minimal Soulbound NFTs was accepted today and is now available at https://eips.ethereum.org/EIPS/eip-5192

Attempting to move to “Last Call”: Move to Last Call by TimDaub · Pull Request #5549 · ethereum/EIPs · GitHub

last call is active until 2022-09-12: EIP-5192: Minimal Soulbound NFTs

EIP5192 is now final.

2 Likes

Just to understand the compactness of using EIP-5192 in NTTs that revert on throw. With just a few lines of code, a lot can be done that can help indexers to identify Soulbound tokens.

What follows is a proposed change set for an NTT contract throwing on all transferring functions that EIP-5192 was proposed to added. It’s a total of 6 additions: Add first cut ERC5192 interface by TimDaub · Pull Request #6 · public-assembly/curation-protocol · GitHub

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