--- eip: 5516 title: Soulbound Multi-owner Tokens description: An interface for non-transferable, Multi-owner NFTs binding to Ethereum accounts author: Lucas Martín Grasso Ramos (@LucasGrasso), Matias Arazi (@MatiArazi) discussions-to: https://ethereum-magicians.org/t/EIP-5516-soulbound-multi-token-standard/10485 status: Draft type: Standards Track category: ERC created: 2022-08-19 requires: 165 --- ## Abstract This EIP proposes a standard interface for non-transferable, multi-owner Soulbound tokens. Previous account-bound token standards face the issue of users losing their account keys or having them rotated, thereby losing their tokens in the process. This EIP provides a solution to this issue that allows for the recycling of SBTs. ## Motivation This EIP was inspired by the main characteristics of the [ERC-1155](./eip-1155.md) token standard and by articles in which benefits and potential use cases of Soulbound/Accountbound Tokens (SBTs) were presented. This design also allows for batch token transfers, saving on transaction costs. Trading of multiple tokens can be built on top of this standard and it removes the need to approve individual token contracts separately. It is also easy to describe and mix multiple fungible or non-fungible token types in a single contract. ### Characteristics - The NFT will be non-transferable after the initial transfer - Multi-Token - Multi-Owner - Semi-Fungible ### Applications - Academic Degrees - Code audits - POAPs (Proof of Attendance Protocol NFTs) ## Specification The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119. **Smart contracts implementing this EIP MUST implement all of the functions in the [ERC-5516](./eip-5516.md) interface.** **Smart contracts implementing this EIP MUST implement the [EIP-165](./eip-165.md) `supportsInterface` function and MUST return the constant value `true` if `0xe150bdab` is passed through the `interfaceID` argument.** ```solidity // SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.4; /** @title Soulbound, Multi-Token standard. @notice Interface of the EIP-5516 Note: The ERC-165 identifier for this interface is 0xe150bdab. */ interface IERC5516 { /** * @dev Emitted when `issuer` creates a new soulbound token and distributes it to `recipients[]`. * * @param tokenId The unique identifier of the newly created token. * @param issuer The address of the entity that issued the credential. * @param recipients Array of addresses that received the soulbound token. * @param metadataURI URI pointing to the token metadata (e.g., IPFS hash). */ event Issued( uint256 indexed tokenId, address indexed issuer, address[] recipients, string metadataURI ); /** * @dev Emitted when `who` voluntarily renounces their soulbound token under `tokenId`. * * @param tokenId The unique identifier of the renounced token. * @param who The address that renounced ownership of the token. */ event Renounced(uint256 indexed tokenId, address indexed who); /** * @dev Issues a soulbound token to multiple recipients. * * Creates or Re-Issues a unique token identifier and distributes it to all addresses in `recipients[]`. * `tokenId` should be deterministically generated as a function of `msg.sender` and `metadataURI` to prevent front-running and ensure uniqueness. * The token is non-transferable after issuance. * * Requirements: * - `recipients[]` MUST NOT be empty. * - All addresses in `recipients[]` MUST be non-zero. * - All addresses in `recipients[]` MUST NOT already own a token under the generated `tokenId`. * - No address in `recipients[]` MUST have previously renounced the generated `tokenId`. * - When issuing an existing `tokenId` (re-issuing), the caller MUST be the original issuer of that `tokenId`. * * Emits an {Issued} event. * * @param recipients Array of addresses that will receive the soulbound token. * @param metadataURI URI pointing to the token metadata (IPFS, Arweave, HTTP, etc.). * @return tokenId The unique identifier of the token. */ function issue( address[] memory recipients, string calldata metadataURI ) external returns (uint256 tokenId); /** * @dev Allows the token holder to voluntarily renounce their soulbound token. * * Renunciation is final: once renounced, the holder cannot reclaim the * token, and re-issuance to the renouncer's address under the same * `tokenId` MUST revert. To restore a credential to a renouncer, the * issuer MUST mint a new `tokenId` with a different `metadataURI`. * * Requirements: * - Caller MUST own the token under `tokenId`. * - `tokenId` MUST exist. * * Emits a {Renounced} event. * * @param tokenId The unique identifier of the token to renounce. */ function renounce(uint256 tokenId) external; /** * @dev Checks if a given address owns a specific soulbound token. * * @param who The address to check ownership for. * @param tokenId The unique identifier of the token. * @return True if `who` owns the token under `tokenId`, false otherwise. */ function has(address who, uint256 tokenId) external view returns (bool); /** * @dev Returns the original issuer of a given token ID. * * @param tokenId The unique identifier of the token. * @return The address of the original issuer of the token. */ function issuerOf(uint256 tokenId) external view returns (address); /** * @dev Returns the URI for a given token ID. * * The URI typically points to a JSON file containing token metadata. * This may be an IPFS hash, Arweave transaction ID, or HTTP URL. * * The URI for a given `tokenId` MUST be immutable once set: because the * `tokenId` is deterministically derived from `(issuer, metadataURI)`, * mutating the URI on-chain would break the binding between identifier * and metadata. Issuers wishing to publish updated metadata MUST issue * a new `tokenId` with the new `metadataURI`. * * Requirements: * - `tokenId` MUST exist. * * @param tokenId The unique identifier of the token. * @return The complete URI string for the token metadata. */ function uri(uint256 tokenId) external view returns (string memory); } ``` ## Rationale ### [ERC-5516](./eip-5516.md) as certificates The original idea for this proposal aroused from a neccesity of emitting on-chain certificates to multiple people. We thought that having to emit one token per account has redundant, and we originally developed a [ERC-1155](./eip-1155.md) partial-compatible implementation. After revisiting our proposal, we thought that it would be cleaner to have a more minimal interface that just serves this purpose only, so we decided to drop the partial backwards compatibility with [ERC-1155](./eip-1155.md). ### Re-issuance A common real-world use case for on-chain credentials is reusing the same conceptual credential across cohorts of recipients (for example, a university issuing the same "Knows Python" credential to successive classes of students). To support this without introducing a separate "extend" function or making issuers manage opaque counter-derived identifiers, this EIP specifies that `issue` is the single entry point for both _creation_ and _re-issuance_. To make re-issuance safe and frictionless, the `tokenId` is RECOMMENDED to be deterministically derived from `(msg.sender, metadataURI)`. This binding has several consequences that the standard relies on: - **Idempotent identity.** The same issuer calling `issue` with the same `metadataURI` always lands on the same `tokenId`. The second cohort, third cohort, and so on each emit a fresh `Issued` event under that `tokenId` while extending the holder set. No counter, registry, or cohort id is required. - **Front-running resistance.** Because the `tokenId` mixes in the caller's address, an adversary observing a pending `issue` transaction in the mempool cannot pre-claim that `tokenId`; their copy would derive a different identifier under their own address. Verifiers MUST check `issuerOf(tokenId)` against the expected issuer rather than trusting metadata alone (see Security Considerations). - **Metadata immutability.** Because the identifier is a hash of the URI, mutating the URI on-chain after issuance would break the binding. The `uri(tokenId)` value MUST therefore be fixed at first issuance. Issuers wishing to publish a revised credential MUST issue a _new_ `tokenId` under a different `metadataURI`. - **Append-only semantics.** Each call to `issue` appends to the holder set for that `tokenId`. Implementations MUST emit an `Issued` event per call, allowing off-chain indexers to reconstruct the full holder set (and any subsequent renunciations) from event logs. Renunciation interacts with re-issuance deliberately: once an address has renounced a `tokenId`, the issuer MUST NOT be able to re-attach it via a subsequent `issue` call. This preserves holder agency over which credentials remain bound to their soul, even in the face of a cooperative or compelled issuer. The cost is that a renouncer who later changes their mind must receive a new `tokenId` under a different `metadataURI`; this is considered acceptable given the strong semantics it preserves around the word "renounce". ### SBT as a _spinoff_ of [EIP-1155](./eip-1155.md) We saw the vision of the [ERC-1155](./eip-1155.md#metadata) and tried to apply it to Soulbound/Accountbound tokens: We think that having the ability to prove that you own a token, not a particular identifier is valuable, and that it has real world use cases. ### Metadata. We implement a standard method of obtaining metadata (`uri`) similar to the one defined in [ERC-1155](./eip-1155.md#metadata): The URI value allows for ID substitution by clients. If the string `{id}` exists in any URI, clients MUST replace this with the actual token ID in hexadecimal form. This allows for a large number of tokens to use the same on-chain string by defining a URI once, for that large number of tokens. - The string format of the substituted hexadecimal ID MUST be lowercase alphanumeric: `[0-9a-f]` with no 0x prefix. - The string format of the substituted hexadecimal ID MUST be leading zero padded to 64 hex characters length if necessary. Example of such a URI: `https://token-cdn-domain/{id}.json` would be replaced with `https://token-cdn-domain/000000000000000000000000000000000000000000000000000000000004cce0.json` if the client is referring to token ID 314592/0x4CCE0. ### Guaranteed log trace The [ERC-5516](./eip-5516.md) standard guarantees that event logs emitted by the smart contract will provide enough data to create an accurate record of all current token balances. A database or explorer may listen to events and be able to provide indexed and categorized searches of every [ERC-5516](./eip-5516.md) token in the contract. ### Exception handling Given the non-transferability property of SBTs, if a user's keys to an account get compromised or rotated, such user may lose the ability to associate themselves with the token. **Given the multi-owner characteristic of this EIP, SBTs will be able to bind to multiple accounts, providing a potential solution to the issue.** Multi-owner SBTs can also be issued to a contract account that implements a multi-signature functionality (As recommended in [EIP-4973](./eip-4973.md#exception-handling)). ### Multi-token The multi-token functionality permits the implementation of multiple token types in the same contract. Furthermore, all emitted tokens are stored in the same contract, preventing redundant bytecode from being deployed to the blockchain. It also facilitates transfer to token issuers, since all issued tokens are stored and can be accessed under the same contract address. ## Backwards Compatibility This is a new token type and is not meant to be backward compatible with any existing tokens other than existing viable souls (any asset that can be identified by `[address,id]`). ## Reference Implementation You can find an implementation of this standard [here](../assets/eip-5516/ERC5516.sol). ## Security Considerations ### Issuer impersonation and metadata collisions Because `tokenId` is a hash of `(msg.sender, metadataURI)` and the contract enforces no global ownership of a `metadataURI`, anyone MAY call `issue` with arbitrary `metadataURI` values, including values that imitate or duplicate a legitimate issuer's metadata. The resulting `tokenId` will differ from the legitimate one (since `msg.sender` differs) and `issuerOf(tokenId)` will return the impostor's address. Verifiers MUST therefore treat metadata content as untrusted on its own. To trust a credential, a verifier MUST: 1. Resolve the credential's `tokenId`. 2. Call `issuerOf(tokenId)` and compare the returned address against the expected issuer (e.g., a known university wallet, a multisig, or an address recorded in an out-of-band registry). 3. Optionally verify by re-deriving `tokenId` as `keccak256(abi.encodePacked(expectedIssuer, expectedMetadataURI))` and confirming it matches. Indexers and UIs displaying ERC-5516 credentials SHOULD surface the issuer prominently and MUST NOT imply authenticity from metadata alone. ### Front-running The deterministic, issuer-bound `tokenId` derivation is intentionally designed to make front-running ineffective. An adversary who observes a pending `issue` transaction cannot pre-claim the resulting `tokenId`, because submitting their own `issue` from a different address yields a different identifier. They can, however, mint a token with the same `metadataURI` under their own address; this collapses to the impersonation case above, mitigated by `issuerOf` verification. ### Renunciation finality Renunciation under this standard is irreversible: once an address has called `renounce(tokenId)`, the implementation MUST refuse any subsequent `issue` call that includes the same address among `recipients[]` for that `tokenId`. This protects holders from coerced or unilateral re-attachment by a cooperative or compromised issuer. Holders SHOULD be aware that this finality is per `(tokenId, address)`; if the issuer mints a _new_ `tokenId` (under a different `metadataURI`) and includes the renouncer, that is a new credential and is allowed. UIs surfacing renunciation MUST NOT misrepresent it as also blocking new credentials from the same issuer. ### Loss or compromise of an issuer key If an issuer's key is compromised, the attacker can extend any existing credentials they originally minted (since they control `msg.sender` for re-issuance). They cannot, however, retroactively rewrite `issuerOf(tokenId)` for tokens minted by other addresses, nor can they re-attach renounced credentials. Issuers handling credentials of consequence (academic, professional, regulatory) SHOULD use multisig or smart-contract wallets as the `msg.sender` for `issue` so that the issuer identity is governance-bound rather than tied to a single externally-owned account, consistent with the Exception handling rationale above. ### Loss or compromise of a holder key The multi-owner property of this EIP is the standard's primary mitigation for holder key loss: a credential MAY be issued to multiple addresses controlled by the same person (or to a smart-contract wallet that implements key rotation). Verifiers checking ownership via `has(who, tokenId)` SHOULD NOT assume that a single address represents the totality of a holder's identity for that credential. ### Indexing and event integrity Implementations MUST emit one `Issued` event per `issue` call (including re-issuance calls) and one `Renounced` event per `renounce` call. Off-chain indexers reconstructing holder sets MUST process both event types and MUST NOT assume that the most recent `Issued` event represents the full holder set for a `tokenId`; it represents only the recipients added in that call. ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md).