ERC-5516 Soulbound Multi-Owner Token (Soulbound Multi-owner Tokens)
Final · NFTs & multi-token · created 2022-08-19
A standard that lets non-transferable attestation tokens (soulbound tokens) be issued to multiple accounts at once in a single transaction, and lets multiple token types live in the same contract.
Why it is needed
A token that should not be handed to anyone else, such as a diploma or proof of attendance, is called a soulbound token (SBT). In existing account-bound token standards, a token is tied to a single account, so if a user loses the account key or replaces it, the token is lost too. Also, issuing the same certificate to many people requires sending a separate issuing transaction for each recipient, which raises transaction costs, and deploying a separate contract for each token type uploads the same code to the blockchain repeatedly. This standard solves these problems by binding one token to multiple accounts at once, issuing to multiple recipients in a single transaction, and letting a single contract hold multiple token types. A recipient can also renounce a token they do not want.
What to implement
event Renounced(uint256 indexed tokenId, address indexed who);: Emitted when a holder voluntarily renounces their own soulbound token.function renounce(uint256 tokenId) external;: A holder voluntarily renounces their own token. Renouncing is final, so the same tokenId cannot be issued again to the same address.function has(address who, uint256 tokenId) external view returns (bool);: Checks whether the given address holds the token with the given tokenId.function issuerOf(uint256 tokenId) external view returns (address);: Returns the original issuer address of the given token ID.function uri(uint256 tokenId) external view returns (string memory);: Returns the URI of the token metadata. This value cannot be changed after it is first set.
In depth
Interface and ERC-165
A contract implementing this standard MUST implement all functions of the ERC-5516 interface, and MUST return true from ERC-165 supportsInterface when 0x85a5f87c is passed as the interfaceID. The interface consists of seven functions, issue, renounce, has, hasRenounced, issuerOf, uri, and deriveTokenId, and two events, Issued and Renounced. It was inspired by the multi-token structure of ERC-1155, but it drops partial compatibility and is designed as a minimal interface suited to the purpose of issuing attestations.
Token ID derivation rule
The tokenId must be derived deterministically from the (issuer, metadataURI) pair, and the derivation function must be collision resistant over that pair. The recommended implementation is uint256(keccak256(abi.encodePacked(issuer, metadataURI))). A call to issue(recipients, metadataURI) must return the same value as deriveTokenId(msg.sender, metadataURI). deriveTokenId must be a function of its arguments and immutable deployment parameters only, and must not depend on mutable contract state. Because the caller address is mixed into the tokenId, an attacker who sees a pending issuance transaction in the mempool cannot claim the same tokenId first. A copy transaction sent by the attacker produces a different identifier based on the attacker's own address.
Re-issuance and atomicity
If the same msg.sender calls issue again with the same metadataURI, it resolves to the same tokenId and must expand that tokenId's holder set instead of creating a new token. An issue call from an address other than the original issuer that targets an existing tokenId must be rejected. As requirements, recipients must not be empty and must not contain the zero address, and if even one address has already held that tokenId or has renounced it in the past, the entire call reverts. This is because allowing partial success would make the recipients in the Issued event unreliable as the set of addresses that were actually granted the credential. Therefore an issuer re-issuing to a large group must filter out addresses that have already received or renounced the token off-chain before making the call.
Finality of renunciation (renounce)
When renounce(tokenId) succeeds, the caller's holding is erased and the erasure is permanent for that address. Any later issue call that includes that address in recipients for the same tokenId must be rejected. This is an intended design: even if the issuer cooperates or is coerced, it cannot reattach the token, which protects the holder's right to decide which credentials remain on their account. To restore a credential for a holder who has changed their mind after renouncing, the issuer must issue a new tokenId with a different metadataURI.
Metadata and URI immutability
The value returned by uri(tokenId) is fixed at first issuance and cannot be changed by re-issuance or any other operation. Because the tokenId is derived from the URI, changing the URI on chain would break the binding between the identifier and the metadata. If the URI string contains {id}, clients must substitute it with the hexadecimal representation of the token ID, written in lowercase [0-9a-f] without a 0x prefix and zero-padded on the left to 64 characters. An implementation may build the uri value by appending the metadataURI received at issuance to a fixed base URI. In that case, a verifier that recomputes the tokenId must use the metadataURI originally passed to issue, not the value returned by uri(tokenId). The base URI must be immutable after deployment, and no function that modifies it may be exposed.
Event rules and indexing
Every issue call (including re-issuance) must emit exactly one Issued event, and the recipients in the event must contain only the addresses added by that call, not the cumulative holder set. Each renounce call likewise emits exactly one Renounced event. Thanks to this rule, an exact record of all current token balances can be built from the contract's event logs alone, and a database or explorer can listen to the events to index, classify and search every token in the contract. An indexer must process both events and must not assume that the most recent Issued event for a tokenId contains its full holder set.
Deliberate omission of issuer revocation
The base interface has no issuer-side revocation. Once a token is issued, the only operation that erases the (address, tokenId) binding is the holder's voluntary renounce. Revocation policy varies widely by use case, such as expiry, replacement or misconduct, so standardizing one model in the base interface would constrain implementations that need a different one. An issuer that needs revocation can extend the interface on its own, or model it by issuing a new replacement tokenId and having verifiers accept only the issuer's latest credential. Leaving revocation out strengthens the holder-side guarantee that, even if the issuer key is later leaked, no one other than the holder can erase the binding.
Multiple ownership and key loss
With a non-transferable token, a holder can lose their link to the token if the account key is leaked or rotated. This standard offers a solution by allowing one token to be bound to multiple accounts. Issuing to a contract account that implements multisig functionality is another option, and it matches the approach recommended in the exception handling of ERC-4973. Thanks to the multi-token structure, multiple token types are stored in one contract, which avoids redundant bytecode deployments and lets all issued tokens be accessed from the same contract address. This standard has been finalized with Final status.
Things to watch out for
- Issuer impersonation and metadata imitation: Anyone can call issue with an arbitrary metadataURI, so they can imitate a legitimate issuer's metadata. However, because the address differs, the tokenId differs and issuerOf returns the impersonator's address. A verifier should not trust the metadata content alone. It should compute the tokenId with deriveTokenId(expected issuer, expected URI) and then compare issuerOf with the expected issuer. In addition, the contract address itself must not be supplied together with the credential being verified. It should be obtained through a separate channel, such as a known deployment registry or a signed issuer statement.
- Front-running: Because the issuer address is mixed into the tokenId derivation, an attacker who sees a pending issue transaction cannot claim that tokenId first. Sending from a different address produces a different identifier. An attacker can create a token under their own address with the same metadataURI, but this falls under the impersonation case and is filtered out by the issuerOf check.
- Scope of renunciation finality: Renunciation is final only per (tokenId, address). An issuer creating a new tokenId with a different metadataURI and including the renouncer as a recipient is allowed, because it is a new credential. A UI that displays renunciation must not misleadingly suggest that it also blocks new credentials from the same issuer.
- Issuer key leak: If an issuer key is leaked, an attacker can extend the credentials that issuer originally issued through re-issuance. However, the attacker cannot change issuerOf for tokens issued by other addresses or reattach renounced credentials. Issuers that handle important credentials, such as degrees, qualifications or regulatory ones, are recommended to use a multisig or smart contract wallet as the issuing entity instead of a single externally owned account.
- Holder key loss: Multiple ownership is this standard's primary mitigation for holder key loss. Credentials can be issued to several addresses managed by the same person or to a smart contract wallet that supports key rotation. A verifier that checks holding with has(who, tokenId) must not assume that a single address is the entirety of the holder's identity for that credential.