Near Learn

NEP-393: Soulbound tokens

NEP-393 soulbound token (SBT) standard on NEAR: non-transferable tokens, issuers and registries, token classes, expiry, revocation, recovery and soul transfer.

Intermediate4 min read3-question check

A soulbound token (SBT) is a token bound to one account that the owner cannot transfer: a proof of personhood, a KYC attestation, a course certificate, a DAO membership badge. NEP-393 standardises how such tokens are issued, queried, renewed, revoked and — when an account is compromised — recovered.

Unlike NFTs, the point of an SBT is that the issuer vouches for the holder. Selling it would defeat its purpose, so ordinary transfers are not part of the interface at all.

The moving parts#

ConceptWhat it is
IssuerA contract (or the authority behind it) that mints SBTs and can renew or revoke them. E.g. a proof-of-humanity service.
RegistryA contract that stores SBTs from many issuers in one place, so apps can query “what does this account hold?” in one call and so cross-issuer operations (recovery, soul transfer, bans) are atomic. Several registries can coexist.
ClassA category of token within an issuer (e.g. “verified human”, “course: Rust 101”). An account holds at most one token per (issuer, class).
Token metadataIncludes the class, issued_at and expires_at (Unix milliseconds), and optional reference / reference_hash to off-chain data.

Registry operations#

MethodKindWhat it does
sbt_mintchange (issuer)Issue tokens to accounts.
sbt_renewchange (issuer)Extend the expiry of tokens.
sbt_revokechange (issuer)Invalidate tokens — burn them, or mark them expired.
sbt_recoverchange (issuer)Move an account’s tokens from a lost/compromised account to a new one.
sbt_soul_transferchange (owner)Move all of the caller’s SBTs to another account atomically; the old account is banned from receiving SBTs.
sbt, sbtsviewLook up tokens by issuer and token ID.
sbt_tokens_by_ownerviewList an account’s SBTs, optionally filtered by issuer.
sbt_supply, sbt_supply_by_class, sbt_supply_by_ownerviewCounts per issuer, class or owner.
Gating a feature on “has a valid SBT” (sketch)
Solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

// ERC-5192 ("minimal soulbound NFTs") just marks an ERC-721 token as locked.
interface IERC5192 {
    function locked(uint256 tokenId) external view returns (bool);
}

interface IERC721 {
    function balanceOf(address owner) external view returns (uint256);
}

contract Gate {
    IERC721 public immutable badge;
    constructor(IERC721 b) { badge = b; }

    function enter() external view {
        require(badge.balanceOf(msg.sender) > 0, "no badge");
    }
}
Rust
// Off-chain or in a cross-contract call, an app asks the registry:
//
//   registry.sbt_tokens_by_owner(account: "alice.near", issuer: "humans.near", ...)
//
// and then checks, for the class it cares about:
//   - a token exists for (issuer, class)
//   - its expires_at (ms) is in the future
//   - the token has not been revoked
//
// On-chain this is an async cross-contract call: query the registry,
// then decide in a #[private] callback — see cross-contract calls.
Coming from EVM

ERC-5192 only adds a “locked” flag to ERC-721 NFTs. NEP-393 is a dedicated design with issuers, classes, expiry, revocation, recovery and a shared registry.

Check yourself

3 questions · progress saved in this browser

  1. 1.Why do SBTs have no regular transfer method?
  2. 2.What problem does sbt_recover solve?
  3. 3.An app gates access on “holds a verified-human SBT”. What should it check?