EIP-4973 - Account-bound Tokens

Some internal discussions in the EIP-4973 Telegram group have reasoned that it doesn’t really make sense to replicate the now-final logic of EIP-5192 Minimal Soulbound tokens here in EIP-4973 and that the specification’s differentiating quality is consensual minting. So some have said that we should, e.g., pull out consensual minting into its own specification as it’s arguably useful for transferable NFTs and potentially even scalar tokens.

But then @glu pointed out that we could do the inverse of it, which is removing all the account-binding nonsense that has caused so much controversy and essentially keeping the consensual minting and I don’t know why but it’s making tons of sense and is something that we can also reasonably standardize as “final,” whereas all the account-binding and soul-binding related concepts are still too early to opinionatedly finalize.

Over the long term, I think we’ll need account abstraction for that, and the Ethereum space isn’t ready for it yet, sadly. Besides, we have addressed account-binding in an un-opinionated way in EIP-5192, and so I’d see EIP-4973 and EIP-5192 being usable in combination too.

What do stakeholders here think?

1 Like

While “removing all the account-binding nonsense” may appear more accessible, that would no longer be the same EIP, idea, or surrounding considerations.

Consensual minting is functionally just a nuanced implementation of any token standard, 20, 721, 1155. It is not something that needs to be solved with an EIP that removes all ability to use the existing and properly standardized tokens. The ability to standardize the process without significant feature loss is possible.

To that, the idea of genuinely consensual minting is only possible through account abstraction (using contracts to block unexpected mints) or having the minter opt-in. Today, the EIPs of 20, 721, and 1155 are already designed for consensual minting. It is just opt-in, which the proposed EIP would also be.

If the situation is entirely reliant on the minters opting in, I would expect it to be a token-type extension rather than an entirely new one. Further, extensions have not historically been designed for use cases but rather essential functionality that individuals can build on, given their own opinion and use cases. This EIP and proposed actions carry a large amount of opinion and use-case specification. I would appreciate an EIP that provides new functionality (an extension) without being so limited as the current and future (consensual minting) currently stand.

As referenced in your previous comment here,
EIP-4494 which is serving a similar vertical, is an Extension, which at most, this EIP would and should become given the complete removal of “account-bound” functionality.

With this, though, I would not favor EIP-5484 either as no above-mentioned proposal solves the problem without massive and broad-reaching implications and assumptions.

To the relevant conversation here, EIP-5192 is much better defined however still riddled with issues and misalignment in the way standards are written. Prefer to see further clarity before supporting anything or seeing another half-built EIP that drowns in nuance.

I think the main idea behind removing the “account-binding nonsense”, is in semantics and wording. I’d argue that consensual minting is the crux of what has been discussed/developed here ad nausium and the update I was referring to brings the spec up to date with that design.

I think we risk running around in circles further trying to argue/define the canonical Soulbound/Account-Bound/Consensual/Non-transferable EIP when I feel like those terms only recently came into the zeitgeist and don’t even have fully agreeable definitions. The most conversation I’ve seen are links to Glen’s paper, Vitalik’s articles, and some Twitter threads. Semantically, one could argue specs referencing non-transferable NFT is a misnomer since it involves an initial minting, but I digress. So definitely in agreement that we can take out some of the opinions.

I think with some of those nuanced phrasings removed/updated, which I will take a stab at, we could take a step back and really see what this EIP is all about. Going forward I think we (me) can provide some more examples on top of what @TimDaub has already given to convince myself and others that this EIP is warranted and will be beneficial over other specs.

1 Like

How do wallets/explorers/marketplaces etc. coordinate on implementing consensual minting without a standard/protocol?

The key nuance of this sentence:

It is not something that needs to be solved with an EIP that removes all ability to use the existing and properly standardized tokens. The ability to standardize the process without significant feature loss is possible.

I don’t understand sorry, mind elaborating?

I noticed that this EIP uses the same Transfer event as EIP-721, and uses the EIP721Metadata interface. I assume this is hoping to be compatible with existing NFT indexers out of the box.

However, this EIP states that the contract must not implement EIP-721, which means that the contract will not advertise EIP-712 compatibility through EIP-165.

This is going to be an issue for some indexers.

The origin of the issue is in the fact that indexing parameters in an event changes the way the event data is encoded but not the topic0 of the event. topic0 is to events what bytes4 function selectors are to functions. The consequence of that is that EIP-20’s Transfer and EIP-721’s Transfer have different data encoding but similar topic0.

So when you listen for Transfer event all over the blockchain, you’ll get EIP-20 and EIP-721 transfers at the same time.

What (some) indexers do is that whenever they see such an event, they try to determine if it comes from an EIP-721 contract. This is done using EIP-165 (and cached). An example of this can be seen in an EIP-721 indexing subgraph.

export function fetchERC721(address: Address): ERC721Contract | null {
	let erc721   = IERC721.bind(address)

	// Try load entry
	let contract = ERC721Contract.load(address)
	if (contract != null) {
		return contract
	}

	// Detect using ERC165
	let detectionId      = address.concat(Bytes.fromHexString('80ac58cd')) // Address + ERC721
	let detectionAccount = Account.load(detectionId)

	// On missing cache
	if (detectionAccount == null) {
		detectionAccount = new Account(detectionId)
		let introspection_01ffc9a7 = supportsInterface(erc721, '01ffc9a7') // ERC165
		let introspection_80ac58cd = supportsInterface(erc721, '80ac58cd') // ERC721
		let introspection_00000000 = supportsInterface(erc721, '00000000', false)
		let isERC721               = introspection_01ffc9a7 && introspection_80ac58cd && introspection_00000000
		detectionAccount.asERC721  = isERC721 ? address : null
		detectionAccount.save()
	}

	// If an ERC721, build entry
	if (detectionAccount.asERC721) {
		contract                  = new ERC721Contract(address)
		let try_name              = erc721.try_name()
		let try_symbol            = erc721.try_symbol()
		contract.name             = try_name.reverted   ? '' : try_name.value
		contract.symbol           = try_symbol.reverted ? '' : try_symbol.value
		contract.supportsMetadata = supportsInterface(erc721, '5b5e139f') // ERC721Metadata
		contract.asAccount        = address
		contract.save()

		let account               = fetchAccount(address)
		account.asERC721          = address
		account.save()
	}

	return contract
}

This function will return null if the address is not an EIP-721 contract.

By not including that interfaceId, EIP-4973 contracts will be disregarded by this indexing systems (and probably others) which will assume its an EIP-20 contract.

1 Like

Good point, I’ll follow up by implementing EIP-5192 for soulbinding in EIP-4973.

Updates to the specification and reference implementation

  • Removed EIP-2098 support as EIP-1271 expects a “naively” concatenated signature and not an EIP-2098-style compact signature. This was pushed upstream with @frangio removing support for EIP-2098 in the SignatureChecker library.
  • Change string tokenURI in structure data hash and give and take inputs to a more generic bytes metadata. PR: Use opaque metadata by frangio · Pull Request #52 · attestate/ERC4973 · GitHub
  • Start using forge fmt for all Solidity code formatting

PRs involved in this update:

Links:

We are experimenting with implement soulbinding via ERC5192 in ERC4973. Here’s the a draft pull request: Create compatibility with IERC721 and IERC5192 by TimDaub · Pull Request #58 · attestate/ERC4973 · GitHub

For ERC4973 users, this is a big interface change. Please give us feedback.
I think the benefits are that it’ll streamline development. ERC4973 defines a generalization of signature-based allow lists. ERC5192 is about lockable NFTs. Composed, they create Soulbound tokens.

2 Likes

I like that this EIP is converging using signatures to prove the token acceptance from the 2 sides of the agreement. That something i’ve myself implemented to build the ticketing/credentials protocol Rouge.

But I feel that the EIP is trying to drive to much how this should be implemented and organized. Why not remove give and take from the specs and replace by having just a mandatory event Bound having 1 or two signatures (from issuer and receiver of the token) - of course also metadata & tokenId -. In case the event is emitted by an issuer’s tx, its signature is superfluous and can be null, and respectively if the event is emitted by a receiver tx, its signature can be null.

Wallet should only be able to tell if the token had approval from both sides to be bound, so only one event should be enough.

I also feel that unequip should be removed from the specs and replace by a symmetric event Unbound with the same double approval mechanism above.

2 Likes

ChatGPT now knows about EIP-4973. I also asked it for specifics about the give function, and it could answer them. WTF!

3 Likes

I leave this comment in github: comment

I think the signature system is a good idea but, in addition to the case that I raised in the comment, if someone gives me an ABT and then I unequip it, if I want to equip it again I would need that someone to give it to me
I would not have full control of my ABT

I read through your github comment, here’s my two cents;
I think signatures need to be invalidated / only be usable once. We solved a similar issue (signature-based authorization, must only be usable once to avoid spam/scam etc) in ERC-6956. We specified that via “MUST redeem each ATTESTATION in the same transaction as any authorized state-changing operation.” That would solve the spam-issue you describe in the github-comment, correct?

However, reading through the EIP I rather have the feeling a dedicated reequip(tokenId) would make sense, specified through MUST throw when msg.sender != previous owner who called unequip. This would require the unequip method to store the acount, a particular tokenId has been unequipped from.

But I somehow have the impression to miss something, this sounds too easy?

1 Like

Agree - invalidation of signatures after one-use makes total sense to me.

In the case of unequipping it, requipping could only occur from a new signature – the old once should be invalid since it was already used.

Was this perhaps solved in other EIPs by any chance that we could inspire from?

@tbergmueller couldn’t find the ERC-6956 anywhere. Can you share link?

edit: Should now be easy to find, was still a PR at time of writing

1 Like

Hey everyone, long time reader, first time poster.

First off, thanks @TimDaub for creating this proposal, I used this for an NFT public auction to give people an account-bound participation card with dynamic metadata and art that was reflecting their position in the auction.

I’ve used the EIP draft and started doing my own variations of it due to some varying use cases. Sometimes, there was a need to only give and sometimes to only take ABTs from the contract, which led me to split up the interface to a base ERC4973 and then have three auxiliary interfaces for adding functions for give, take and both functions, respectively.

I like how ERC721 has held off on defining a clear mint function as it gives freedom and adding these intended additional interfaces could make the standard a bit more flexible and not encourage implementations of functions with strict reverts in either of the give or take functions.

Would you think this could be relevant? I’m more than happy to slap together a PR for the update if so and we can figure out some good naming conventions along the overall theme.

2 Likes

Was thinking more about the scenario of unequipping and then re-equipping using the original attestation/signature.

Considering that requipping is done as an explicit action, and only by the recipient (or msg.sender), I feel that us accommodating for “requipping a burnt ABT only using new signatures” being a redundant scenario. Once I (as a recipient of an ABT) have the attestation that I can mint an ABT, I am essentially able to use it whenever. The attestations curently do not have a lifecycle of their own and are stateless. To introduce state and consider for it’s mutation over a course of time, I believe would belong to a new spec. For example, checkout attest.sh which maintains a base layer of attestations that indeed have a lifecycle i.e., they are stateful. While I find a lot of value in generally supporting this, I think using stateful attestations might be an extension to this spec than be a core part of it. We may need to keep this simple to allow for extensions. Happy to hear more thoughts on this.

I would personally think that the attestation could be open for change for the extender of the base either way.

Given what you’re saying and what I’m also mentioning above, it sounds like the base itself for this could use being more generic and we could provide extensions of it further. At least, we could work on splitting up the interfaces based on the “Interface Segregation Principle” from good ol’ OOO and then let the user implement what interface they want to use. I pushed up an open version of the code I made for the work I mentioned above which showcases this here: GitHub - felixnorden/erc4973o

What I’m thinking could work for the re-equip would be instead of having unequip burn the ABT, it instead stores it on the contract by transferring the ABT to the contract. That way, you could just call equip to transfer it back. Given that the ID is based on the attestation, it should be enough to infer the eligibility and attestation without providing it explicitly; in case attestation is modified, we’d just have to make sure that the implementer fulfills the requirement for equip to resolve the token and attestation properly.
Furthermore, burning could be something like destroy like you would do in WoW by pulling it out of your inventory. In this case, the contract would act as everyone’s “inventory” in the sense of a bank.

Maybe, this line of thought could be baked into the base contract either way as it gives a counterpart to the unequip?