EIP-4973 - Account-bound Tokens

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

	// 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

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

	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:


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.


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.


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


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.


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?