AlertSourceDiscuss
Skip to content
On this page

ERC-5727: Semi-Fungible Soulbound Token

An interface for soulbound tokens, also known as badges or account-bound tokens, that can be both fungible and non-fungible.

⚠️ DraftERC

Draft Notice

This EIP is in the process of being drafted. The content of this EIP is not final and can change at any time; this EIP is not yet suitable for use in production. Thank you!

AuthorsAustin Zhu (@AustinZhu), Terry Chen <terry.chen@phaneroz.io>
Created2022-10-19

Abstract

An interface for soulbound tokens (SBT), which are non-transferable tokens representing a person's identity, credentials, affiliations, and reputation.

Our interface can handle a combination of fungible and non-fungible tokens in an organized way. It provides a set of core methods that can be used to manage the lifecycle of soulbound tokens, as well as a rich set of extensions that enables DAO governance, privacy protection, token expiration, and account recovery.

This interface aims to provide a flexible and extensible framework for the development of soulbound token systems.

Motivation

The Web3 ecosystem nowadays is largely dominated by highly-financialized tokens, which are designed to be freely transferable and interchangeable. However, there are many use cases in our society that require non-transferablity. For example, a membership card guarantees one's proprietary rights in a community, and such rights should not be transferable to others.

We have already seen many attempts to create such non-transferable tokens in the Ethereum community. However, they lack the flexibility to support both fungible and non-fungible tokens and do not provide extensible features for critical use cases.

Our interface can be used to represent non-transferable ownerships, and provides features for common use cases including but not limited to:

  • granular lifecycle management of SBTs (e.g. minting, revocation, expiration)
  • management of SBTs via community voting and delegation (e.g. DAO governance, operators)
  • recovery of SBTs (e.g. switching to a new wallet)
  • token visibility control (e.g. private SBTs, hiding negative tokens)
  • fungible and non-fungible SBTs (e.g. membership card and loyalty points)
  • the grouping of SBTs using slots (e.g. complex reward schemes with a combination of vouchers, points, and badges)

A common interface for soulbound tokens will not only help enrich the Web3 ecosystem but also facilitates the growth of a decentralized society.

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.

A token is identified by its tokenId, which is a 256-bit unsigned integer. A token can also have a value denoting its denomination.

A slot is identified by its slotId, which is a 256-bit unsigned integer. Slots are used to group fungible and non-fungible tokens together, thus make tokens semi-fungible. A token can only belong to one slot at a time.

Core

The core methods are used to manage the lifecycle of SBTs. They MUST be supported by all semi-fungible SBT implementations.

solidity
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/utils/introspection/IERC165.sol";

/**
 * @title ERC5727 Soulbound Token Interface
 * @dev The core interface. It allows basic query of information about tokens and slots.
 * @dev interfaceId = 0x35f61d8a
 */
interface IERC5727 is IERC165 {
    /**
     * @dev MUST emit when a token is minted.
     * @param owner The address that the token is minted to
     * @param tokenId The token minted
     * @param value The value of the token minted
     */
    event Minted(address indexed owner, uint256 indexed tokenId, uint256 value);

    /**
     * @dev MUST emit when a token is revoked.
     * @param owner The owner of the revoked token
     * @param tokenId The revoked token
     */
    event Revoked(address indexed owner, uint256 indexed tokenId);

    /**
     * @dev MUST emit when a token is charged.
     * @param tokenId The token to charge
     * @param value The value to charge
     */
    event Charged(uint256 indexed tokenId, uint256 value);

    /**
     * @dev MUST emit when a token is consumed.
     * @param tokenId The token to consume
     * @param value The value to consume
     */
    event Consumed(uint256 indexed tokenId, uint256 value);

    /**
     * @dev MUST emit when a token is destroyed.
     * @param owner The owner of the destroyed token
     * @param tokenId The token to destroy.
     */
    event Destroyed(address indexed owner, uint256 indexed tokenId);

    /**
     * @dev MUST emit when the slot of a token is set or changed.
     * @dev In case a new slot is set, the `oldSlot` MUST be 0.
     * @param tokenId The token of which slot is set or changed
     * @param oldSlot The previous slot of the token
     * @param newSlot The updated slot of the token
     */
    event SlotChanged(
        uint256 indexed tokenId,
        uint256 indexed oldSlot,
        uint256 indexed newSlot
    );

    /**
     * @notice Get the value of a token.
     * @dev MUST revert if the `tokenId` does not exist
     * @param tokenId the token for which to query the balance
     * @return The value of `tokenId`
     */
    function valueOf(uint256 tokenId) external view returns (uint256);

    /**
     * @notice Get the slot of a token.
     * @dev MUST revert if the `tokenId` does not exist
     * @param tokenId the token for which to query the slot
     * @return The slot of `tokenId`
     */
    function slotOf(uint256 tokenId) external view returns (uint256);

    /**
     * @notice Get the owner of a token.
     * @dev MUST revert if the `tokenId` does not exist
     * @param tokenId the token for which to query the owner
     * @return The address of the owner of `tokenId`
     */
    function ownerOf(uint256 tokenId) external view returns (address);

    /**
     * @notice Get the validity of a token.
     * @dev MUST revert if the `tokenId` does not exist
     * @dev A token is valid if it is not revoked.
     * @param tokenId the token for which to query the validity
     * @return If the token is valid
     */
    function isValid(uint256 tokenId) external view returns (bool);

    /**
     * @notice Get the issuer of a token.
     * @dev MUST revert if the `tokenId` does not exist
     * @param tokenId the token for which to query the issuer
     * @return The address of the issuer of `tokenId`
     */
    function issuerOf(uint256 tokenId) external view returns (address);
}

Extensions

All extensions below are OPTIONAL for ERC-5727 implementations. An implementation MAY choose to implement some, none, or all of them.

Enumerable

This extension provides methods to enumerate the tokens of a owner. It is recommended to be implemented together with the core interface.

solidity
pragma solidity ^0.8.0;

import "./IERC5727.sol";

/**
 * @title ERC5727 Soulbound Token Enumerable Interface
 * @dev This extension allows querying the tokens of a owner.
 * @dev interfaceId = 0x211ec300
 */
interface IERC5727Enumerable is IERC5727 {
    /**
     * @notice Get the total number of tokens emitted.
     * @return The total number of tokens emitted
     */
    function emittedCount() external view returns (uint256);

    /**
     * @notice Get the total number of owners.
     * @return The total number of owners
     */
    function ownersCount() external view returns (uint256);

    /**
     * @notice Get the tokenId with `index` of the `owner`.
     * @dev MUST revert if the `index` exceed the number of tokens owned by the `owner`.
     * @param owner The owner whose token is queried for.
     * @param index The index of the token queried for
     * @return The token is queried for
     */
    function tokenOfSoulByIndex(address owner, uint256 index)
        external
        view
        returns (uint256);

    /**
     * @notice Get the tokenId with `index` of all the tokens.
     * @dev MUST revert if the `index` exceed the total number of tokens.
     * @param index The index of the token queried for
     * @return The token is queried for
     */
    function tokenByIndex(uint256 index) external view returns (uint256);

    /**
     * @notice Get the number of tokens owned by the `owner`.
     * @dev MUST revert if the `owner` does not have any token.
     * @param owner The owner whose balance is queried for
     * @return The number of tokens of the `owner`
     */
    function balanceOf(address owner) external view returns (uint256);

    /**
     * @notice Get if the `owner` owns any valid tokens.
     * @param owner The owner whose valid token information is queried for
     * @return if the `owner` owns any valid tokens
     */
    function hasValid(address owner) external view returns (bool);
}

Metadata

This extension provides methods to fetch the metadata of a token, a slot and the contract itself. It is recommended to be implemented if you need to specify the appearance and properties of tokens, slots and the contract (i.e. the SBT collection).

solidity
pragma solidity ^0.8.0;

import "./IERC5727.sol";

/**
 * @title ERC5727 Soulbound Token Metadata Interface
 * @dev This extension allows querying the metadata of soulbound tokens.
 * @dev interfaceId = 0xba3e1a9d
 */
interface IERC5727Metadata is IERC5727 {
    /**
     * @notice Get the name of the contract.
     * @return The name of the contract
     */
    function name() external view returns (string memory);

    /**
     * @notice Get the symbol of the contract.
     * @return The symbol of the contract
     */
    function symbol() external view returns (string memory);

    /**
     * @notice Get the URI of a token.
     * @dev MUST revert if the `tokenId` token does not exist.
     * @param tokenId The token whose URI is queried for
     * @return The URI of the `tokenId` token
     */
    function tokenURI(uint256 tokenId) external view returns (string memory);

    /**
     * @notice Get the URI of the contract.
     * @return The URI of the contract
     */
    function contractURI() external view returns (string memory);

    /**
     * @notice Get the URI of a slot.
     * @dev MUST revert if the `slot` does not exist.
     * @param slot The slot whose URI is queried for
     * @return The URI of the `slot`
     */
    function slotURI(uint256 slot) external view returns (string memory);
}

Governance

This extension provides methods to manage the mint and revocation permissions through voting. It is useful if you want to rely on a group of voters to decide the issuance a particular SBT.

solidity
pragma solidity ^0.8.0;

import "./IERC5727.sol";

/**
 * @title ERC5727 Soulbound Token Governance Interface
 * @dev This extension allows minting and revocation of tokens by community voting.
 * @dev interfaceId = 0x3ba738d1
 */
interface IERC5727Governance is IERC5727 {
    /**
     * @notice Get the voters of the contract.
     * @return The array of the voters
     */
    function voters() external view returns (address[] memory);

    /**
     * @notice Approve to mint the token described by the `approvalRequestId` to `owner`.
     * @dev MUST revert if the caller is not a voter.
     * @param owner The owner which the token to mint to
     * @param approvalRequestId The approval request describing the value and slot of the token to mint
     */
    function approveMint(address owner, uint256 approvalRequestId) external;

    /**
     * @notice Approve to revoke the `tokenId`.
     * @dev MUST revert if the `tokenId` does not exist.
     * @param tokenId The token to revert
     */
    function approveRevoke(uint256 tokenId) external;

    /**
     * @notice Create an approval request describing the `value` and `slot` of a token.
     * @dev MUST revert when `value` is zero.
     * @param value The value of the approval request to create
     */
    function createApprovalRequest(uint256 value, uint256 slot) external returns (uint256 approvalRequestId);

    /**
     * @notice Remove `approvalRequestId` approval request.
     * @dev MUST revert if the caller is not the creator of the approval request.
     * @param approvalRequestId The approval request to remove
     */
    function removeApprovalRequest(uint256 approvalRequestId) external;

    /**
     * @notice Add a new voter `newVoter`.
     * @dev MUST revert if the caller is not an administrator.
     *  MUST revert if `newVoter` is already a voter.
     * @param newVoter the new voter to add
     */
    function addVoter(address newVoter) external;

    /**
     * @notice Remove the `voter` from the contract.
     * @dev MUST revert if the caller is not an administrator.
     *  MUST revert if `voter` is not a voter.
     * @param voter the voter to remove
     */
    function removeVoter(address voter) external;
}

Delegate

This extension provides methods to delegate a one-time mint and revocation right to an operator. It is useful if you want to temporarily allow an operator to mint and revoke tokens on your behalf.

solidity
pragma solidity ^0.8.0;

import "./IERC5727.sol";

/**
 * @title ERC5727 Soulbound Token Delegate Interface
 * @dev This extension allows delegation of (batch) minting and revocation of tokens to operator(s).
 * @dev interfaceId = 0x3da384b4
 */
interface IERC5727Delegate is IERC5727 {
    /**
     * @notice Delegate a one-time minting right to `operator` for `delegateRequestId` delegate request.
     * @dev MUST revert if the caller does not have the right to delegate.
     * @param operator The owner to which the minting right is delegated
     * @param delegateRequestId The delegate request describing the owner, value and slot of the token to mint
     */
    function mintDelegate(address operator, uint256 delegateRequestId) external;

    /**
     * @notice Delegate one-time minting rights to `operators` for corresponding delegate request in `delegateRequestIds`.
     * @dev MUST revert if the caller does not have the right to delegate.
     *   MUST revert if the length of `operators` and `delegateRequestIds` do not match.
     * @param operators The owners to which the minting right is delegated
     * @param delegateRequestIds The delegate requests describing the owner, value and slot of the tokens to mint
     */
    function mintDelegateBatch(
        address[] memory operators,
        uint256[] memory delegateRequestIds
    ) external;

    /**
     * @notice Delegate a one-time revoking right to `operator` for `tokenId` token.
     * @dev MUST revert if the caller does not have the right to delegate.
     * @param operator The owner to which the revoking right is delegated
     * @param tokenId The token to revoke
     */
    function revokeDelegate(address operator, uint256 tokenId) external;

    /**
     * @notice Delegate one-time minting rights to `operators` for corresponding token in `tokenIds`.
     * @dev MUST revert if the caller does not have the right to delegate.
     *   MUST revert if the length of `operators` and `tokenIds` do not match.
     * @param operators The owners to which the revoking right is delegated
     * @param tokenIds The tokens to revoke
     */
    function revokeDelegateBatch(
        address[] memory operators,
        uint256[] memory tokenIds
    ) external;

    /**
     * @notice Mint a token described by `delegateRequestId` delegate request as a delegate.
     * @dev MUST revert if the caller is not delegated.
     * @param delegateRequestId The delegate requests describing the owner, value and slot of the token to mint.
     */
    function delegateMint(uint256 delegateRequestId) external;

    /**
     * @notice Mint tokens described by `delegateRequestIds` delegate request as a delegate.
     * @dev MUST revert if the caller is not delegated.
     * @param delegateRequestIds The delegate requests describing the owner, value and slot of the tokens to mint.
     */
    function delegateMintBatch(uint256[] memory delegateRequestIds) external;

    /**
     * @notice Revoke a token as a delegate.
     * @dev MUST revert if the caller is not delegated.
     * @param tokenId The token to revoke.
     */
    function delegateRevoke(uint256 tokenId) external;

    /**
     * @notice Revoke multiple tokens as a delegate.
     * @dev MUST revert if the caller is not delegated.
     * @param tokenIds The tokens to revoke.
     */
    function delegateRevokeBatch(uint256[] memory tokenIds) external;

    /**
     * @notice Create a delegate request describing the `owner`, `value` and `slot` of a token.
     * @param owner The owner of the delegate request.
     * @param value The value of the delegate request.
     * @param slot The slot of the delegate request.
     * @return delegateRequestId The id of the delegate request
     */
    function createDelegateRequest(
        address owner,
        uint256 value,
        uint256 slot
    ) external returns (uint256 delegateRequestId);

    /**
     * @notice Remove a delegate request.
     * @dev MUST revert if the delegate request does not exists.
     *   MUST revert if the caller is not the creator of the delegate request.
     * @param delegateRequestId The delegate request to remove.
     */
    function removeDelegateRequest(uint256 delegateRequestId) external;
}

Recovery

This extension provides methods to recover tokens from a stale owner. It is recommended to use this extension so that users are able to retrieve their tokens from a compromised or old wallet in certain situations.

solidity
pragma solidity ^0.8.0;

import "./IERC5727.sol";

/**
 * @title ERC5727 Soulbound Token Recovery Interface
 * @dev This extension allows recovering soulbound tokens from an address provided its signature.
 * @dev interfaceId = 0x379f4e66
 */
interface IERC5727Recovery is IERC5727 {
    /**
     * @notice Recover the tokens of `owner` with `signature`.
     * @dev MUST revert if the signature is invalid.
     * @param owner The owner whose tokens are recovered
     * @param signature The signature signed by the `owner`
     */
    function recover(address owner, bytes memory signature) external;
}

Expirable

This extension provides methods to manage the expiration of tokens. It is useful if you want to expire/invalidate tokens after a certain period of time.

solidity
pragma solidity ^0.8.0;

import "./IERC5727.sol";

/**
 * @title ERC5727 Soulbound Token Expirable Interface
 * @dev This extension allows soulbound tokens to be expired.
 * @dev interfaceId = 0x2a8cf5aa
 */
interface IERC5727Expirable is IERC5727 {
    /**
     * @notice Get the expire date of a token.
     * @dev MUST revert if the `tokenId` token does not exist.
     * @param tokenId The token for which the expiry date is queried
     * @return The expiry date of the token
     */
    function expiryDate(uint256 tokenId) external view returns (uint256);

    /**
     * @notice Get if a token is expired.
     * @dev MUST revert if the `tokenId` token does not exist.
     * @param tokenId The token for which the expired status is queried
     * @return If the token is expired
     */
    function isExpired(uint256 tokenId) external view returns (bool);

    /**
     * @notice Set the expiry date of a token.
     * @dev MUST revert if the `tokenId` token does not exist.
     *   MUST revert if the `date` is in the past.
     * @param tokenId The token whose expiry date is set
     * @param date The expire date to set
     */
    function setExpiryDate(uint256 tokenId, uint256 date) external;

    /**
     * @notice Set the expiry date of multiple tokens.
     * @dev MUST revert if the `tokenIds` tokens does not exist.
     *   MUST revert if the `dates` is in the past.
     *   MUST revert if the length of `tokenIds` and `dates` do not match.
     * @param tokenIds The tokens whose expiry dates are set
     * @param dates The expire dates to set
     */
    function setBatchExpiryDates(
        uint256[] memory tokenIds,
        uint256[] memory dates
    ) external;
}

Shadow

This extension provides methods to manage the visibility of tokens. It is useful if you want to hide tokens that you don't want to show to the public.

solidity
pragma solidity ^0.8.0;

import "./IERC5727.sol";

/**
 * @title ERC5727 Soulbound Token Shadow Interface
 * @dev This extension allows restricting the visibility of specific soulbound tokens.
 * @dev interfaceId = 0x3475cd68
 */
interface IERC5727Shadow is IERC5727 {
    /**
     * @notice Shadow a token.
     * @dev MUST revert if the `tokenId` token does not exists.
     * @param tokenId The token to shadow
     */
    function shadow(uint256 tokenId) external;

    /**
     * @notice Reveal a token.
     * @dev MUST revert if the `tokenId` token does not exists.
     * @param tokenId The token to reveal
     */
    function reveal(uint256 tokenId) external;
}

SlotEnumerable

This extension provides methods to enumerate slots. A slot is used to group tokens that share similar utility and properties.

solidity
pragma solidity ^0.8.0;

import "./IERC5727.sol";
import "./IERC5727Enumerable.sol";

/**
 * @title ERC5727 Soulbound Token Slot Enumerable Interface
 * @dev This extension allows querying information about slots.
 * @dev interfaceId = 0x3b741b9e
 */
interface IERC5727SlotEnumerable is IERC5727, IERC5727Enumerable {
    /**
     * @notice Get the total number of slots.
     * @return The total number of slots.
     */
    function slotCount() external view returns (uint256);

    /**
     * @notice Get the slot with `index` among all the slots.
     * @dev MUST revert if the `index` exceed the total number of slots.
     * @param index The index of the slot queried for
     * @return The slot is queried for
     */
    function slotByIndex(uint256 index) external view returns (uint256);

    /**
     * @notice Get the number of tokens in a slot.
     * @dev MUST revert if the slot does not exist.
     * @param slot The slot whose number of tokens is queried for
     * @return The number of tokens in the `slot`
     */
    function tokenSupplyInSlot(uint256 slot) external view returns (uint256);

    /**
     * @notice Get the tokenId with `index` of the `slot`.
     * @dev MUST revert if the `index` exceed the number of tokens in the `slot`.
     * @param slot The slot whose token is queried for.
     * @param index The index of the token queried for
     * @return The token is queried for
     */
    function tokenInSlotByIndex(uint256 slot, uint256 index)
        external
        view
        returns (uint256);
    
    /**
     * @notice Get the number of owners in a slot.
     * @dev MUST revert if the slot does not exist.
     * @param slot The slot whose number of owners is queried for
     * @return The number of owners in the `slot`
     */
    function ownersInSlot(uint256 slot) external view returns (uint256);

    /**
     * @notice Check if a owner is in a slot.
     * @dev MUST revert if the slot does not exist.
     * @param owner The owner whose existence in the slot is queried for
     * @param slot The slot whose existence of the owner is queried for
     * @return True if the `owner` is in the `slot`, false otherwise
     */
    function isOwnerInSlot(
        address owner,
        uint256 slot
    ) external view returns (bool);

    /**
     * @notice Get the owner with `index` of the `slot`.
     * @dev MUST revert if the `index` exceed the number of owners in the `slot`.
     * @param slot The slot whose owner is queried for.
     * @param index The index of the owner queried for
     * @return The owner is queried for
     */
    function ownerInSlotByIndex(
        uint256 slot,
        uint256 index
    ) external view returns (address);

    /**
     * @notice Get the number of slots of a owner.
     * @param owner The owner whose number of slots is queried for
     * @return The number of slots of the `owner`
     */
    function slotCountOfOwner(address owner) external view returns (uint256);

    /**
     * @notice Get the slot with `index` of the `owner`.
     * @dev MUST revert if the `index` exceed the number of slots of the `owner`.
     * @param owner The owner whose slot is queried for.
     * @param index The index of the slot queried for
     * @return The slot is queried for
     */
    function slotOfOwnerByIndex(
        address owner,
        uint256 index
    ) external view returns (uint256);
}

Rationale

Token storage model

We adopt semi-fungible token storage models designed to support both fungible and non-fungible tokens, inspired by the semi-fungible token standard. We found that such a model is better suited to the representation of SBT than the model used in ERC-1155.

Firstly, each slot can be used to represent different categories of SBTs. For instance, a DAO can have membership SBTs, role badges, scores, etc. in one SBT collection.

Secondly, unlike ERC-1155, in which each unit of fungible tokens is exactly the same, our interface can help differentiate between similar tokens. This is justified by that credential scores obtained from different entities differ not only in value but also in their effects, validity periods, origins, etc. However, they still share the same slot as they all contribute to a person's credibility, membership, etc.

Recovery mechanism

To prevent the loss of SBTs, we propose a recovery mechanism that allows users to recover their tokens by providing a signature signed by their owner address. This mechanism is inspired by ERC-1271.

Since SBTs are bound to an address and are meant to represent the identity of the address, which cannot be split into fractions. Therefore, each recovery should be considered as a transfer of all the tokens of the owner. This is why we use the recover function instead of transferFrom or safeTransferFrom.

Token visibility control

Our interface allows users to control the visibility of their tokens (shadowing and revealing). This is useful when a user wants to hide some of their tokens from the public, for example, when they want to keep their membership secret. Generally, the issuer and the owner of the token have access to the token by default and can control the visibility of the token. After the token is shadowed, information about the token (e.g. token URI, owner of the token) cannot be queried by the public.

Backwards Compatibility

This EIP proposes a new token interface which is compatible with ERC-721 and ERC-3525.

This EIP is also compatible with ERC-165.

Test Cases

Our sample implementation includes test cases written using Hardhat.

Reference Implementation

You can find our sample implementation here.

Security Considerations

This EIP does not involve the general transfer of tokens, and thus there will be no security issues related to token transfer generally.

However, users should be aware of the security risks of using the recovery mechanism. If a user loses his/her private key, all his/her soulbound tokens will be exposed to potential theft. The attacker can create a signature and restore all SBTs of the victim. Therefore, users should always keep their private keys safe. We recommend developers implement a recovery mechanism that requires multiple signatures to restore SBTs.

Copyright and related rights waived via CC0.

Citation

Please cite this document as:

Austin Zhu, Terry Chen, "ERC-5727: Semi-Fungible Soulbound Token[DRAFT]," Ethereum Improvement Proposals, no. 5727, 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-5727.