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#
| Concept | What it is |
|---|---|
| Issuer | A contract (or the authority behind it) that mints SBTs and can renew or revoke them. E.g. a proof-of-humanity service. |
| Registry | A 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. |
| Class | A category of token within an issuer (e.g. “verified human”, “course: Rust 101”). An account holds at most one token per (issuer, class). |
| Token metadata | Includes the class, issued_at and expires_at (Unix milliseconds), and optional reference / reference_hash to off-chain data. |
Registry operations#
| Method | Kind | What it does |
|---|---|---|
sbt_mint | change (issuer) | Issue tokens to accounts. |
sbt_renew | change (issuer) | Extend the expiry of tokens. |
sbt_revoke | change (issuer) | Invalidate tokens — burn them, or mark them expired. |
sbt_recover | change (issuer) | Move an account’s tokens from a lost/compromised account to a new one. |
sbt_soul_transfer | change (owner) | Move all of the caller’s SBTs to another account atomically; the old account is banned from receiving SBTs. |
sbt, sbts | view | Look up tokens by issuer and token ID. |
sbt_tokens_by_owner | view | List an account’s SBTs, optionally filtered by issuer. |
sbt_supply, sbt_supply_by_class, sbt_supply_by_owner | view | Counts per issuer, class or owner. |
// 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");
}
}// 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.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