ERC-5516 - Soulbound Multi-owner Tokens

Created 2022-08-19
Status Draft
Category ERC
Type Standards Track
Authors
Requires

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

Applications

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 interface.

Smart contracts implementing this EIP MUST implement the EIP-165 supportsInterface function and MUST return the constant value true if 0xe150bdab is passed through the interfaceID argument.

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

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:

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

We saw the vision of the ERC-1155 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:

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.

Guaranteed log trace

The ERC-5516 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 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).

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.

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.