From 365e8c2a0a8518a709b07fa678898a48a166571c Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Wed, 14 Sep 2022 08:12:55 +0200 Subject: [PATCH 01/89] feat: adding soulbound token --- specs/Standards/Sould Bound Token/README.md | 202 ++++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 specs/Standards/Sould Bound Token/README.md diff --git a/specs/Standards/Sould Bound Token/README.md b/specs/Standards/Sould Bound Token/README.md new file mode 100644 index 000000000..4dfa51a6e --- /dev/null +++ b/specs/Standards/Sould Bound Token/README.md @@ -0,0 +1,202 @@ +# NEP: Soulbound Token + +--- + +NEP: TODO +Title: Soulbound Token +Author: Robert Zaremba <@robert-zaremba>, Noak Lindqvist <@KazanderDad> +DiscussionsTo: +Status: Draft +Type: Standards Track +Category: Contract +Created: 12-Sep-2022 +Requires: -- + +--- + +## Summary + +Soulbound Tokens (SBT) are non transferrable NFTs. Even though tranferability is not available, we define a recoverability mechanism. + +SBTs are well suited of carrying proof-of-attendence NFTs, proof-of-unique-human "stamps" and other similar credibility-carriers. + +## Motivation + +Many operations are currently impossible or very hard to achieve on-chain without a way to query for proof-of-human (or even proof-of-unique-human). Examples include one-person-one-vote, fair airdrops & ICOs, universal basic income and other any other scenarios prone to sybil attacks. + +Soulbound tokens need to be recoverable in case a user's private key is compromised (due to extortion, loss, etc). This becomes especially important for proof-of-human stamps and other NFTs that can only be issued once per user. + +Two safeguards against misuse of recovery are contemplated. 1) Users cannot reover an SBT by themselves. The issuer, a DAO or a smart contract dedicated to manage the recovery should be assigned. 2) Whenever a recovery is triggered then the wallet from which the NFT was recovered gets blacklisted (burned). Recovering one NFT triggers a blacklist that applies across all NFTs that share the same address. + +Soulbound tokens should also have an option to be renewable. Examples include mandatory renewal with some frequency to check that the owner is still alive, or renew membership to a DAO that uses SBTs as membership gating. + +## Specification + +### Smart contract interface + +The Soulbound Token interface is a subset of NFT, hence the interface follows the [NEP-171](https://github.com/near/NEPs/blob/master/neps/nep-0171.md). + +```rust +/// TokenMetadata defines attributes for each SBT token. +pub struct TokenMetadata { + pub title: Option, // ex. "fist bump with Robert" + pub description: Option, // free-form description + pub media: Option, // URL to associated media, preferably to decentralized, content-addressed storage + pub media_hash: Option, // Base64-encoded sha256 hash of content referenced by the `media` field. Required if `media` is included. + pub copies: Option, // number of copies of this set of metadata in existence when token was minted. + pub issued_at: Option, // When token was issued or minted, Unix epoch in milliseconds + pub expires_at: Option, // When token expires, Unix epoch in milliseconds + pub starts_at: Option, // When token starts being valid, Unix epoch in milliseconds + pub updated_at: Option, // When token was last updated, Unix epoch in milliseconds + pub extra: Option, // anything extra the SBT wants to store on-chain. Can be stringified JSON. + pub reference: Option, // URL to an off-chain JSON file with more info. + pub reference_hash: Option, // Base64-encoded sha256 hash of JSON from reference field. Required if `reference` is included. +} + + +trait SBT { + // ************ + // QUERIES + + + // get the information about specific token ID + fn sbt(&self, token_id: TokenId) -> Option; + + // returns total amount of tokens minted by this contract + fn sbt_total_supply(&self) -> U64; + + // returns total supply of SBTs for a given owner + fn sbt_supply_by_owner(&self, account: AccountId); + + // Query for sbt tokens + fn sbt_tokens(&self, from_index: Option, limit: Option) -> Vec; + + // Query sbt tokens by owner + fn sbt_tokens_by_owner( + &self, + account: AccountId, + from_index: Option, + limit: Option, + ) -> Vec; + + + /********** + * Transactions + **********/ + + /// creates a new, unique token and assigns it to the `receiver`. + #[payable] + fn sbt_mint(&mut self, metadata: TokenMetadata, receiver: AccountId); + + /// sbt_recover reassigns all tokens from the old owner to a new owner, + /// and registers `old_owner` to a burned addresses registry. + /// Must be called by operator. + /// Must provide 5 miliNEAR to cover registry storage cost. Operator should + /// put that cost to the requester (old_owner), eg by asking operation fee. + #[payable] + fn sbt_recover(&mut self, from: AccountId, to: AccountId); + + /// sbt_renew will update the expire time of provided tokens. + /// `expires_at` is a unix timestamp (in seconds). + #[payable] + pub fn sbt_renew(&mut self, tokens: Vec, expires_at: u64, memo: Option); + + + +} +``` + +### Logs + +```typescript +interface SbtEventLogData { + standard: "nepXXX"; + version: "1.0.0"; + event: "sbt_mint" | "sbt_recover" | "sbt_renew"; + data: SbtMintLog[] | SbtRecoverLog[] | SbtRenew[]; +} + +// An event emitted when a new SBT is minted. +// Arguments +// * `owner`: "account.near" +// * `tokens`: ["1", "abc"] +// * `memo`: optional message +interface SbtMintLog { + owner: string; + tokens: string[]; + memo?: string; +} + +// An event emitted when a recovery process succeeded to reassign SBT. +// Arguments +// * `old_owner`: "old_account.near" +// * `new_owner`: "new_account.near" +// * `token_ids`: ["1", "abc"] +// * `memo`: optional message +interface SbtRecoverLog { + old_owner: string; + new_owner: string; + tokens: string[]; + memo?: string; +} + +// An event emitted when a existing tokens are renewed. +// Arguments +// * `tokens`: ["1", "abc"] +// * `memo`: optional message +interface SbtRenewLog { + tokens: uint64[]; + memo?: string; +} +``` + +Whenever a recovery is made, a SBT must call `SbtBurnedAccounts.burn(account_id)`. + +```typescript +class SbtBurnedAccounts { + burn(account_id) { + this.burned_accounts[this.account_id][this.predecessor_id] = true; + } + + // query + is_burned(account_id, sbt_token_contract): bool { + return this.burned_accounts[account_id][sbt_token_id]; + } +} +``` + +### Recommended functions + +Although the funcitons below are not part of the standard (depending on a use case, they may need different parameters), we recommend them as a part of every implementation and we also provide them in the reference implementation. + +```typescript= + +interface SBT { + + + // Function for recovery committee, which can be either + // the issuer, DAO, or an operator smart contract, authorized for the + // recovery process. + // Emits `SbtRecoverLog` when the recover process succeeds. + // Returns an error if the recovery process failed. + recover(token_id: uint64, old_address: string, new_address: string): error | undefined; + + + +} + +``` + +## Reference Implementation + +- https://github.com/alpha-fi/i-am-human/tree/master/contracts/soulbound + +## Example Flow + +## Future possibilities + +## Copyright + +[copyright]: #copyright + +Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). From 7aa2f8b812720b77ed706956081dd086fc7db939 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Tue, 29 Nov 2022 00:40:21 +0100 Subject: [PATCH 02/89] set NEP number --- specs/Standards/Sould Bound Token/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/Standards/Sould Bound Token/README.md b/specs/Standards/Sould Bound Token/README.md index 4dfa51a6e..2f5b09812 100644 --- a/specs/Standards/Sould Bound Token/README.md +++ b/specs/Standards/Sould Bound Token/README.md @@ -2,7 +2,7 @@ --- -NEP: TODO +NEP: 393 Title: Soulbound Token Author: Robert Zaremba <@robert-zaremba>, Noak Lindqvist <@KazanderDad> DiscussionsTo: From 1783a3e799ac1ed197499e846acdcc8b471d0b0b Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Tue, 29 Nov 2022 00:45:49 +0100 Subject: [PATCH 03/89] Renamed queries *by_owner to *for_owner Renamed query methods to make it NEP-181 compatible: * sbt_tokens_by_owner -> sbt_tokens_for_owner * sbt_supply_by_owner -> sbt_supply_for_owner --- specs/Standards/Sould Bound Token/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/specs/Standards/Sould Bound Token/README.md b/specs/Standards/Sould Bound Token/README.md index 2f5b09812..b7af4d409 100644 --- a/specs/Standards/Sould Bound Token/README.md +++ b/specs/Standards/Sould Bound Token/README.md @@ -4,7 +4,7 @@ NEP: 393 Title: Soulbound Token -Author: Robert Zaremba <@robert-zaremba>, Noak Lindqvist <@KazanderDad> +Authors: Robert Zaremba <@robert-zaremba>, Noak Lindqvist <@KazanderDad> DiscussionsTo: Status: Draft Type: Standards Track @@ -66,13 +66,13 @@ trait SBT { fn sbt_total_supply(&self) -> U64; // returns total supply of SBTs for a given owner - fn sbt_supply_by_owner(&self, account: AccountId); + fn sbt_supply_for_owner(&self, account: AccountId); // Query for sbt tokens fn sbt_tokens(&self, from_index: Option, limit: Option) -> Vec; // Query sbt tokens by owner - fn sbt_tokens_by_owner( + fn sbt_tokens_for_owner( &self, account: AccountId, from_index: Option, From 515f35d92ad2f67ce57af9648f206158f58d8d0f Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Tue, 29 Nov 2022 00:50:24 +0100 Subject: [PATCH 04/89] use proper doc comment syntax --- specs/Standards/Sould Bound Token/README.md | 34 +++++++++------------ 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/specs/Standards/Sould Bound Token/README.md b/specs/Standards/Sould Bound Token/README.md index b7af4d409..b0d8c8a56 100644 --- a/specs/Standards/Sould Bound Token/README.md +++ b/specs/Standards/Sould Bound Token/README.md @@ -55,23 +55,23 @@ pub struct TokenMetadata { trait SBT { - // ************ - // QUERIES - + /********** + * QUERIES + **********/ - // get the information about specific token ID + /// get the information about specific token ID fn sbt(&self, token_id: TokenId) -> Option; - // returns total amount of tokens minted by this contract + /// returns total amount of tokens minted by this contract fn sbt_total_supply(&self) -> U64; - // returns total supply of SBTs for a given owner + /// returns total supply of SBTs for a given owner fn sbt_supply_for_owner(&self, account: AccountId); - // Query for sbt tokens + /// Query for sbt tokens fn sbt_tokens(&self, from_index: Option, limit: Option) -> Vec; - // Query sbt tokens by owner + /// Query sbt tokens by owner fn sbt_tokens_for_owner( &self, account: AccountId, @@ -81,7 +81,7 @@ trait SBT { /********** - * Transactions + * TRANSACTIONS **********/ /// creates a new, unique token and assigns it to the `receiver`. @@ -173,16 +173,12 @@ Although the funcitons below are not part of the standard (depending on a use ca interface SBT { - - // Function for recovery committee, which can be either - // the issuer, DAO, or an operator smart contract, authorized for the - // recovery process. - // Emits `SbtRecoverLog` when the recover process succeeds. - // Returns an error if the recovery process failed. + /// Function for recovery committee, which can be either + /// the issuer, DAO, or an operator smart contract, authorized for the + /// recovery process. + /// Emits `SbtRecoverLog` when the recover process succeeds. + /// Returns an error if the recovery process failed. recover(token_id: uint64, old_address: string, new_address: string): error | undefined; - - - } ``` @@ -197,6 +193,4 @@ interface SBT { ## Copyright -[copyright]: #copyright - Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). From fb488a80b818db2ee2b4277c369f80071e084cc5 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Tue, 29 Nov 2022 00:54:23 +0100 Subject: [PATCH 05/89] add copyright --- specs/Standards/Sould Bound Token/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/specs/Standards/Sould Bound Token/README.md b/specs/Standards/Sould Bound Token/README.md index b0d8c8a56..154fd030d 100644 --- a/specs/Standards/Sould Bound Token/README.md +++ b/specs/Standards/Sould Bound Token/README.md @@ -193,4 +193,6 @@ interface SBT { ## Copyright -Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). +## Copyright + +[Creative Commons Attribution 4.0 International Public License (CC BY 4.0)](https://creativecommons.org/licenses/by/4.0/) From 56397d0f467bb6afd28520b62ec6d1d85d9a0ff4 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Tue, 29 Nov 2022 00:56:29 +0100 Subject: [PATCH 06/89] adding optional sbt_token_for_owner --- specs/Standards/Sould Bound Token/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/specs/Standards/Sould Bound Token/README.md b/specs/Standards/Sould Bound Token/README.md index 154fd030d..052743ac5 100644 --- a/specs/Standards/Sould Bound Token/README.md +++ b/specs/Standards/Sould Bound Token/README.md @@ -79,6 +79,10 @@ trait SBT { limit: Option, ) -> Vec; + /// Optional. If the SBT implementaiton assures that one account can have maximum one SBT + /// the the following function should be implemented. + /// Returns Some(Token) if an `account` owns an SBT, otherwise returns None. + fn sbt_token_for_owner(&self, account: AccountId) -> Option {} /********** * TRANSACTIONS From 41d602034aa30cd9ba995f31123f31d25ec2ad9f Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Tue, 29 Nov 2022 00:58:52 +0100 Subject: [PATCH 07/89] typos --- specs/Standards/Sould Bound Token/README.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/specs/Standards/Sould Bound Token/README.md b/specs/Standards/Sould Bound Token/README.md index 052743ac5..a29a46094 100644 --- a/specs/Standards/Sould Bound Token/README.md +++ b/specs/Standards/Sould Bound Token/README.md @@ -16,17 +16,17 @@ Requires: -- ## Summary -Soulbound Tokens (SBT) are non transferrable NFTs. Even though tranferability is not available, we define a recoverability mechanism. +Soulbound Tokens (SBT) are non transferable NFTs. Even though tranferability is not available, we define a recoverability mechanism. -SBTs are well suited of carrying proof-of-attendence NFTs, proof-of-unique-human "stamps" and other similar credibility-carriers. +SBTs are well suited of carrying proof-of-attendance NFTs, proof-of-unique-human "stamps" and other similar credibility-carriers. ## Motivation -Many operations are currently impossible or very hard to achieve on-chain without a way to query for proof-of-human (or even proof-of-unique-human). Examples include one-person-one-vote, fair airdrops & ICOs, universal basic income and other any other scenarios prone to sybil attacks. +Many operations are currently impossible or very hard to achieve on-chain without a way to query for proof-of-human (or even proof-of-unique-human). Examples include one-person-one-vote, fair airdrops & ICOs, universal basic income and other any other scenarios prone to Sybil attacks. Soulbound tokens need to be recoverable in case a user's private key is compromised (due to extortion, loss, etc). This becomes especially important for proof-of-human stamps and other NFTs that can only be issued once per user. -Two safeguards against misuse of recovery are contemplated. 1) Users cannot reover an SBT by themselves. The issuer, a DAO or a smart contract dedicated to manage the recovery should be assigned. 2) Whenever a recovery is triggered then the wallet from which the NFT was recovered gets blacklisted (burned). Recovering one NFT triggers a blacklist that applies across all NFTs that share the same address. +Two safeguards against misuse of recovery are contemplated. 1) Users cannot recover an SBT by themselves. The issuer, a DAO or a smart contract dedicated to manage the recovery should be assigned. 2) Whenever a recovery is triggered then the wallet from which the NFT was recovered gets blacklisted (burned). Recovering one NFT triggers a blacklist that applies across all NFTs that share the same address. Soulbound tokens should also have an option to be renewable. Examples include mandatory renewal with some frequency to check that the owner is still alive, or renew membership to a DAO that uses SBTs as membership gating. @@ -171,7 +171,7 @@ class SbtBurnedAccounts { ### Recommended functions -Although the funcitons below are not part of the standard (depending on a use case, they may need different parameters), we recommend them as a part of every implementation and we also provide them in the reference implementation. +Although the functions below are not part of the standard (depending on a use case, they may need different parameters), we recommend them as a part of every implementation and we also provide them in the reference implementation. ```typescript= @@ -197,6 +197,4 @@ interface SBT { ## Copyright -## Copyright - [Creative Commons Attribution 4.0 International Public License (CC BY 4.0)](https://creativecommons.org/licenses/by/4.0/) From 6c261e1198b899b07e1d4a0cbb36cafa0186c9eb Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Tue, 29 Nov 2022 01:17:19 +0100 Subject: [PATCH 08/89] added consequences --- specs/Standards/Sould Bound Token/README.md | 23 +++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/specs/Standards/Sould Bound Token/README.md b/specs/Standards/Sould Bound Token/README.md index a29a46094..f86b82075 100644 --- a/specs/Standards/Sould Bound Token/README.md +++ b/specs/Standards/Sould Bound Token/README.md @@ -105,8 +105,6 @@ trait SBT { #[payable] pub fn sbt_renew(&mut self, tokens: Vec, expires_at: u64, memo: Option); - - } ``` @@ -184,7 +182,6 @@ interface SBT { /// Returns an error if the recovery process failed. recover(token_id: uint64, old_address: string, new_address: string): error | undefined; } - ``` ## Reference Implementation @@ -193,7 +190,25 @@ interface SBT { ## Example Flow -## Future possibilities +## Consequences + +### Positive + +- Template and set of guidelines for creating SBT tokens. +- Ability to create SBT aggregators. +- Ability to use SBT as a primitive to model non KYC identity, badges, certificates etc... +- SBT can be further used for "lego" protocols, like: Proof of Humanity (dicussed for NDC Governance), undercollateralized lending, role based authentication sytems, innovative economic and social applications... +- Stanarized recoverability mechanism. +- SBT are considered as a basic primitive for Decentralized Societies. + +### Netural + +- API follows the NEP-181 (NFT) standard. + NOTE: we can decide to use `nft_` prefix whenever possible. + +### Negative + +- new set of events to be handled by the indexer. However this is not an issue if we decide to use subset of events used by the NEP-181 (NFT). ## Copyright From cb96e8d2c7681b3ac6750e85d7f55f6543cca011 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Tue, 29 Nov 2022 01:42:24 +0100 Subject: [PATCH 09/89] fix: nep 181 -> 171 --- specs/Standards/Sould Bound Token/README.md | 35 +++++++++++++++------ 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/specs/Standards/Sould Bound Token/README.md b/specs/Standards/Sould Bound Token/README.md index f86b82075..4658732e0 100644 --- a/specs/Standards/Sould Bound Token/README.md +++ b/specs/Standards/Sould Bound Token/README.md @@ -22,15 +22,17 @@ SBTs are well suited of carrying proof-of-attendance NFTs, proof-of-unique-human ## Motivation -Many operations are currently impossible or very hard to achieve on-chain without a way to query for proof-of-human (or even proof-of-unique-human). Examples include one-person-one-vote, fair airdrops & ICOs, universal basic income and other any other scenarios prone to Sybil attacks. +Recent Decentralized Society trend opens a new area of Web3 research to model various aspects of what characterizes humans. Economic and governance value is generated by humans and their relationship. Creating a strong primitive is necessary to model new innovative systems and decentralized societies. Examples include one-person-one-vote, fair airdrops & ICOs, universal basic income, non KYC identity systems, Human DAOs as well as methods for Sybil attack resistance. -Soulbound tokens need to be recoverable in case a user's private key is compromised (due to extortion, loss, etc). This becomes especially important for proof-of-human stamps and other NFTs that can only be issued once per user. +We propose an SBT standard to model protocols described above. -Two safeguards against misuse of recovery are contemplated. 1) Users cannot recover an SBT by themselves. The issuer, a DAO or a smart contract dedicated to manage the recovery should be assigned. 2) Whenever a recovery is triggered then the wallet from which the NFT was recovered gets blacklisted (burned). Recovering one NFT triggers a blacklist that applies across all NFTs that share the same address. +## Specification -Soulbound tokens should also have an option to be renewable. Examples include mandatory renewal with some frequency to check that the owner is still alive, or renew membership to a DAO that uses SBTs as membership gating. +Soulbound tokens need to be recoverable in case a user's private key is compromised (due to extortion, loss, etc). This becomes especially important for proof-of-human stamps and other NFTs that can only be issued once per user. -## Specification +Two safeguards against misuse of recovery are contemplated. 1) Users cannot recover an SBT by themselves. The issuer, a DAO or a smart contract (eg: multisig) dedicated to manage the recovery should be assigned. 2) Whenever a recovery is triggered then the wallet from which the NFT was recovered gets blacklisted (indicator that the account identity is burned). Recovering one SBT triggers a blacklist that should apply for SBTs that share the same address. + +Soulbound tokens with expire date SHOULD have an option to be renewable. Examples include mandatory renewal with some frequency to check that the owner is still alive, or renew membership to a DAO that uses SBTs as membership gating. ### Smart contract interface @@ -104,7 +106,6 @@ trait SBT { /// `expires_at` is a unix timestamp (in seconds). #[payable] pub fn sbt_renew(&mut self, tokens: Vec, expires_at: u64, memo: Option); - } ``` @@ -129,6 +130,17 @@ interface SbtMintLog { memo?: string; } +// An event emitted when existing SBTs are revoked and burned. +// Arguments +// * `owner`: "account.near" +// * `tokens`: ["1", "abc"] +// * `memo`: optional message +interface SbtRevokeLog { + owner: string; + tokens: string[]; + memo?: string; +} + // An event emitted when a recovery process succeeded to reassign SBT. // Arguments // * `old_owner`: "old_account.near" @@ -152,7 +164,7 @@ interface SbtRenewLog { } ``` -Whenever a recovery is made, a SBT must call `SbtBurnedAccounts.burn(account_id)`. +Whenever a recovery is made in a way that an existing SBT is burned, the `SbtRevokeLog` event must be emitted. ```typescript class SbtBurnedAccounts { @@ -181,6 +193,10 @@ interface SBT { /// Emits `SbtRecoverLog` when the recover process succeeds. /// Returns an error if the recovery process failed. recover(token_id: uint64, old_address: string, new_address: string): error | undefined; + + /// Function to revoke and burn SBT. Must emit `SbtRevokeLog` event. + /// Return true if a token_id is an valid, active SBT. Otherwise returns false. + revoke(token_id: uint64): bool; } ``` @@ -200,15 +216,16 @@ interface SBT { - SBT can be further used for "lego" protocols, like: Proof of Humanity (dicussed for NDC Governance), undercollateralized lending, role based authentication sytems, innovative economic and social applications... - Stanarized recoverability mechanism. - SBT are considered as a basic primitive for Decentralized Societies. +- new way to implement Sybil attack resistance. ### Netural -- API follows the NEP-181 (NFT) standard. +- API follows the NEP-171 (NFT) standard. NOTE: we can decide to use `nft_` prefix whenever possible. ### Negative -- new set of events to be handled by the indexer. However this is not an issue if we decide to use subset of events used by the NEP-181 (NFT). +- new set of events to be handled by the indexer. However this is not an issue if we decide to use subset of events used by the NEP-171 (NFT). ## Copyright From c5c64c59bd49c794ca189eda41eebe0f1fb3da1f Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Wed, 14 Dec 2022 23:55:24 +0100 Subject: [PATCH 10/89] small note about events --- specs/Standards/Sould Bound Token/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/specs/Standards/Sould Bound Token/README.md b/specs/Standards/Sould Bound Token/README.md index 4658732e0..c832a4c08 100644 --- a/specs/Standards/Sould Bound Token/README.md +++ b/specs/Standards/Sould Bound Token/README.md @@ -181,7 +181,8 @@ class SbtBurnedAccounts { ### Recommended functions -Although the functions below are not part of the standard (depending on a use case, they may need different parameters), we recommend them as a part of every implementation and we also provide them in the reference implementation. +Although the functions below are not part of the standard (depending on a use case, they may need different parameters), we recommend them as a part of implementation and we also provide them in the reference implementation. +These functions should emit appropriate events. ```typescript= From f29bfa2aff13e823ba408e59cdd3315fafa878c1 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Wed, 21 Dec 2022 00:02:44 +0100 Subject: [PATCH 11/89] review: update summary --- specs/Standards/Sould Bound Token/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/specs/Standards/Sould Bound Token/README.md b/specs/Standards/Sould Bound Token/README.md index c832a4c08..3f762b6fc 100644 --- a/specs/Standards/Sould Bound Token/README.md +++ b/specs/Standards/Sould Bound Token/README.md @@ -16,7 +16,7 @@ Requires: -- ## Summary -Soulbound Tokens (SBT) are non transferable NFTs. Even though tranferability is not available, we define a recoverability mechanism. +Soulbound Token (SBT) is a form of a NFT token which identifies an aspect of an account (_soul_). Transferability is limited only to a case of recoverability or transferring the whole _soul_ - that would mean transferring a soul (all SBTs) from one account to another, and killing the source account. SBTs are well suited of carrying proof-of-attendance NFTs, proof-of-unique-human "stamps" and other similar credibility-carriers. @@ -26,6 +26,8 @@ Recent Decentralized Society trend opens a new area of Web3 research to model va We propose an SBT standard to model protocols described above. +_Verifiable Credentials_ (VC) could be seen as subset of SBT. However there is an important distinction: VC require set of claims and privacy protocols. It would make more sense to model VC with relation to W3 DID standard. SBT is different, it doesn't require a [resolver](https://www.w3.org/TR/did-core/#dfn-did-resolvers) nor [method](https://www.w3.org/TR/did-core/#dfn-did-methods) registry. For SBT, we need something more elastic than VC. + ## Specification Soulbound tokens need to be recoverable in case a user's private key is compromised (due to extortion, loss, etc). This becomes especially important for proof-of-human stamps and other NFTs that can only be issued once per user. From 764c68ec835580f5d15892c878c2360dc0ab572f Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Thu, 23 Mar 2023 13:33:46 +0100 Subject: [PATCH 12/89] review, cleanup and more details --- specs/Standards/Sould Bound Token/README.md | 235 +++++++++++--------- 1 file changed, 125 insertions(+), 110 deletions(-) diff --git a/specs/Standards/Sould Bound Token/README.md b/specs/Standards/Sould Bound Token/README.md index 3f762b6fc..56368e76d 100644 --- a/specs/Standards/Sould Bound Token/README.md +++ b/specs/Standards/Sould Bound Token/README.md @@ -4,7 +4,7 @@ NEP: 393 Title: Soulbound Token -Authors: Robert Zaremba <@robert-zaremba>, Noak Lindqvist <@KazanderDad> +Authors: Robert Zaremba <@robert-zaremba> DiscussionsTo: Status: Draft Type: Standards Track @@ -16,43 +16,65 @@ Requires: -- ## Summary -Soulbound Token (SBT) is a form of a NFT token which identifies an aspect of an account (_soul_). Transferability is limited only to a case of recoverability or transferring the whole _soul_ - that would mean transferring a soul (all SBTs) from one account to another, and killing the source account. +Soulbound Token (SBT) is a form of a NFT which represents an aspect of an account (_soul_). Transferability is limited only to a case of recoverability or transferring the whole _soul_ - a `sbt_soul_transfer` should coordinate with a registry to transfer all SBTs from one account to another, and _killing_ the source account. -SBTs are well suited of carrying proof-of-attendance NFTs, proof-of-unique-human "stamps" and other similar credibility-carriers. +SBTs are well suited for carrying proof-of-attendance, proof-of-unique-human "stamps" and other similar credibility-carriers. ## Motivation -Recent Decentralized Society trend opens a new area of Web3 research to model various aspects of what characterizes humans. Economic and governance value is generated by humans and their relationship. Creating a strong primitive is necessary to model new innovative systems and decentralized societies. Examples include one-person-one-vote, fair airdrops & ICOs, universal basic income, non KYC identity systems, Human DAOs as well as methods for Sybil attack resistance. +Recent Decentralized Society trends open a new area of Web3 research to model various aspects of what characterizes humans. Economic and governance value is generated by humans and their relationship. Creating a strong primitive is necessary to model new innovative systems and decentralized societies. Examples include reputation protocols, one-person-one-vote, fair airdrops & ICOs, universal basic income, non KYC identity systems, Human DAOs as well as methods for Sybil attack resistance. -We propose an SBT standard to model protocols described above. +We propose a SBT standard to model protocols described above. _Verifiable Credentials_ (VC) could be seen as subset of SBT. However there is an important distinction: VC require set of claims and privacy protocols. It would make more sense to model VC with relation to W3 DID standard. SBT is different, it doesn't require a [resolver](https://www.w3.org/TR/did-core/#dfn-did-resolvers) nor [method](https://www.w3.org/TR/did-core/#dfn-did-methods) registry. For SBT, we need something more elastic than VC. ## Specification -Soulbound tokens need to be recoverable in case a user's private key is compromised (due to extortion, loss, etc). This becomes especially important for proof-of-human stamps and other NFTs that can only be issued once per user. +Main requirement for Soubound tokens is to make it bound to a human: -Two safeguards against misuse of recovery are contemplated. 1) Users cannot recover an SBT by themselves. The issuer, a DAO or a smart contract (eg: multisig) dedicated to manage the recovery should be assigned. 2) Whenever a recovery is triggered then the wallet from which the NFT was recovered gets blacklisted (indicator that the account identity is burned). Recovering one SBT triggers a blacklist that should apply for SBTs that share the same address. +- moving tokens from one account to another should be strictly limited to case of _recoverability_ (eg in case a user's private key is compromised due to extortion, loss, etc) or user account merge (_soul transfer_). + This becomes especially important for proof-of-human stamps that can only be issued once per user. +- there should be an inherit cost when a token is moved. We propose an SBT registry, which will assure that a true _soul transfer_ is performed, and the source account will be blocked to house any new soul. -Soulbound tokens with expire date SHOULD have an option to be renewable. Examples include mandatory renewal with some frequency to check that the owner is still alive, or renew membership to a DAO that uses SBTs as membership gating. +Atomicity of `soul_transfer` in current NEAR runtime is not possible if the token balance is kept separately in each SBT smart contract. To provide atomic transfer of all user tokens, we need additional contract: the `SBT Registry`. +// TODO: add more details about the registry. + +Two safeguards against misuse of recovery are contemplated. 1) Users cannot recover an SBT by themselves. The issuer, a DAO or a smart contract (eg: multisig) dedicated to manage the recovery should be assigned. 2) Whenever a recovery is triggered then the Sould registry emits _SoulKill_ event (indicator that the account identity is burned). Recovering one SBT triggers registry blocklist that should apply for SBTs that share the same address. + +Soulbound tokens can have an expire date. This is useful for tokens which are related to real world certificates with expire time, or social mechanisms (eg, community membership). Such tokens SHOULD have an option to be renewable. Examples include mandatory renewal with some frequency to check that the owner is still alive, or renew membership to a DAO that uses SBTs as membership gating. ### Smart contract interface -The Soulbound Token interface is a subset of NFT, hence the interface follows the [NEP-171](https://github.com/near/NEPs/blob/master/neps/nep-0171.md). +For the Token ID type we propose `u64` rather than `U128`. `u64` capacity is more than 1e19. If we will mint 10'000 SBTs per second, than it will take us 58'494'241 years to get into the capacity. +Today, the JS integer limit is `2^53-1 ~ 9e15`. It will take us 28561 years to fill that when minting 10'000 SBTs per second. So, we don't need to u128 nor a String type. However, if for some reason, we will need to get u64 support for JS, then we can always add another set of methods which will return String, so making it compatible with NFT standar (which is using `U128`, which is a string). + +```rust +pub type TokenId = u64; +``` + +The Soulbound Token follows the NFT [NEP-171](https://github.com/near/NEPs/blob/master/neps/nep-0171.md) interface, with few differences: + +- token ID is `u64` (as discussed above). +- `TokenMetadata` doesn't have `title`, `description`, `media`, `media_hash`, `copies`, `extra`, `starts_at` nor `updated_at`. All that attributes except the `updated_at` can be part of the document stored at `reference`. `updated_at` can be tracked on blockchain. +- only NEP-171 Mint and Burn events are reused. Since we don't have normal transferability, we propose to use more targeted events, to better reflect the event nature. + - TODO: since the native token ID is different in SBT and NFT, maybe we should use SBT special events for mint and revoke (burn)? ```rust +/// ContractMetadata defines contract wide attributes, which describes the whole contract. +pub struct ContractMetadata { + pub spec: String, // required, essentially a version like "sbt-1.0.0" + pub name: String, // required, ex. "Mosaics" + pub symbol: String, // required, ex. "MOSAIC" + pub icon: Option, // Data URL + pub base_uri: Option, // Centralized gateway known to have reliable access to decentralized storage assets referenced by `reference` or `media` URLs + pub reference: Option, // URL to a JSON file with more info + pub reference_hash: Option, // Base64-encoded sha256 hash of JSON from reference field. Required if `reference` is included. +} + /// TokenMetadata defines attributes for each SBT token. pub struct TokenMetadata { - pub title: Option, // ex. "fist bump with Robert" - pub description: Option, // free-form description - pub media: Option, // URL to associated media, preferably to decentralized, content-addressed storage - pub media_hash: Option, // Base64-encoded sha256 hash of content referenced by the `media` field. Required if `media` is included. - pub copies: Option, // number of copies of this set of metadata in existence when token was minted. pub issued_at: Option, // When token was issued or minted, Unix epoch in milliseconds pub expires_at: Option, // When token expires, Unix epoch in milliseconds - pub starts_at: Option, // When token starts being valid, Unix epoch in milliseconds - pub updated_at: Option, // When token was last updated, Unix epoch in milliseconds - pub extra: Option, // anything extra the SBT wants to store on-chain. Can be stringified JSON. pub reference: Option, // URL to an off-chain JSON file with more info. pub reference_hash: Option, // Base64-encoded sha256 hash of JSON from reference field. Required if `reference` is included. } @@ -67,13 +89,13 @@ trait SBT { fn sbt(&self, token_id: TokenId) -> Option; /// returns total amount of tokens minted by this contract - fn sbt_total_supply(&self) -> U64; + fn sbt_total_supply(&self) -> u64; /// returns total supply of SBTs for a given owner - fn sbt_supply_for_owner(&self, account: AccountId); + fn sbt_supply_for_owner(&self, account: AccountId) -> u64; /// Query for sbt tokens - fn sbt_tokens(&self, from_index: Option, limit: Option) -> Vec; + fn sbt_tokens(&self, from_index: Option, limit: Option) -> Vec; /// Query sbt tokens by owner fn sbt_tokens_for_owner( @@ -88,118 +110,103 @@ trait SBT { /// Returns Some(Token) if an `account` owns an SBT, otherwise returns None. fn sbt_token_for_owner(&self, account: AccountId) -> Option {} - /********** - * TRANSACTIONS - **********/ + /* + * Transactions are not part of the standard. Instead, in the section below, we provide + * recommended interfaces for the functions which are related to the SBT Standard Events. + **/ - /// creates a new, unique token and assigns it to the `receiver`. - #[payable] - fn sbt_mint(&mut self, metadata: TokenMetadata, receiver: AccountId); +} +``` - /// sbt_recover reassigns all tokens from the old owner to a new owner, - /// and registers `old_owner` to a burned addresses registry. - /// Must be called by operator. - /// Must provide 5 miliNEAR to cover registry storage cost. Operator should - /// put that cost to the requester (old_owner), eg by asking operation fee. - #[payable] - fn sbt_recover(&mut self, from: AccountId, to: AccountId); +SBT smart contracts can implement NFT query interface to make it compatible with NFT tools. Note, we use U64 type rather than U128. - /// sbt_renew will update the expire time of provided tokens. - /// `expires_at` is a unix timestamp (in seconds). - #[payable] - pub fn sbt_renew(&mut self, tokens: Vec, expires_at: u64, memo: Option); -} +```rust +trait SBTNFT { + fn nft_total_supply(&self) -> U64 + fn nft_tokens_for_owner(&self, account_id: AccountId, from_index: Option, limit: Option) -> Vec + fn nft_supply_for_owner(&self, account_id: AccountId) -> U64 ``` ### Logs ```typescript -interface SbtEventLogData { - standard: "nepXXX"; +struct SbtEventKind { + standard: "nep393"; version: "1.0.0"; - event: "sbt_mint" | "sbt_recover" | "sbt_renew"; - data: SbtMintLog[] | SbtRecoverLog[] | SbtRenew[]; -} - -// An event emitted when a new SBT is minted. -// Arguments -// * `owner`: "account.near" -// * `tokens`: ["1", "abc"] -// * `memo`: optional message -interface SbtMintLog { - owner: string; - tokens: string[]; - memo?: string; + event: "mint" | "recover" | "renew"; + data: Recover | Renew | SoulTransfer; } -// An event emitted when existing SBTs are revoked and burned. -// Arguments -// * `owner`: "account.near" -// * `tokens`: ["1", "abc"] -// * `memo`: optional message -interface SbtRevokeLog { - owner: string; - tokens: string[]; - memo?: string; +// Note: for token minting, NEP-171 compatible Mint event is used. + +/// An event emitted when a recovery process succeeded to reassign SBT due to account access +/// loss. This action is usually requested by the owner, but executed by an issuer, and doesn't +/// trigger Soul Transfer. +/// Arguments +/// * `old_owner`: current holder of the SBT, whose account was compromised or lost. +/// * `new_owner`: destination account. +/// * `token_ids`: list of token ids. +/// * `memo`: optional message +struct Recover { + old_owner: AccountId; + new_owner: AccountId; + tokens: []u64; + memo: Option; } -// An event emitted when a recovery process succeeded to reassign SBT. -// Arguments -// * `old_owner`: "old_account.near" -// * `new_owner`: "new_account.near" -// * `token_ids`: ["1", "abc"] -// * `memo`: optional message -interface SbtRecoverLog { - old_owner: string; - new_owner: string; - tokens: string[]; - memo?: string; +/// An event emitted when a existing tokens are renewed. +/// Arguments +/// * `tokens`: list of token ids. +/// * `memo`: optional message +struct Renew { + tokens: []u64; + memo: string; } -// An event emitted when a existing tokens are renewed. -// Arguments -// * `tokens`: ["1", "abc"] -// * `memo`: optional message -interface SbtRenewLog { - tokens: uint64[]; - memo?: string; +/// An evenet emitted when soul transfer is happening: all SBTs owned by `from` are transferred +/// to `to`, and the `from` account is _killed_ (can't receive any new SBT). +struct SoulTransfer { + from: AccountId; + to: AccountId; + memo: Option; } ``` -Whenever a recovery is made in a way that an existing SBT is burned, the `SbtRevokeLog` event must be emitted. - -```typescript -class SbtBurnedAccounts { - burn(account_id) { - this.burned_accounts[this.account_id][this.predecessor_id] = true; - } - - // query - is_burned(account_id, sbt_token_contract): bool { - return this.burned_accounts[account_id][sbt_token_id]; - } -} -``` +Whenever a recovery is made in a way that an existing SBT is burned, the `NftBurn` event must be emitted. ### Recommended functions -Although the functions below are not part of the standard (depending on a use case, they may need different parameters), we recommend them as a part of implementation and we also provide them in the reference implementation. +Although the transaction functions below are not part of the standard (depending on a use case, they may need different parameters), we recommend them as a part of implementation and we also provide them in the reference implementation. These functions should emit appropriate events. -```typescript= +```rust -interface SBT { +trait SBTTxs { - /// Function for recovery committee, which can be either - /// the issuer, DAO, or an operator smart contract, authorized for the - /// recovery process. - /// Emits `SbtRecoverLog` when the recover process succeeds. - /// Returns an error if the recovery process failed. - recover(token_id: uint64, old_address: string, new_address: string): error | undefined; + /// Creates a new, unique token and assigns it to the `receiver`. + /// Must emit NEP-171 compatible Mint event. + /// Must provide enough NEAR to cover registry storage cost. + /// The arguments to this function can vary, depending on the use-case. + #[payable] + fn sbt_mint(&mut self, metadata: TokenMetadata, receiver: AccountId); - /// Function to revoke and burn SBT. Must emit `SbtRevokeLog` event. - /// Return true if a token_id is an valid, active SBT. Otherwise returns false. - revoke(token_id: uint64): bool; + /// sbt_recover reassigns all tokens from the old owner to a new owner, + /// and registers `old_owner` to a burned addresses registry. + /// Must emit SbtRecover event. + /// Must be called by an operator. + /// Must provide enough NEAR to cover registry storage cost. + #[payable] + fn sbt_recover(&mut self, from: AccountId, to: AccountId); + + /// sbt_renew will update the expire time of provided tokens. + /// `expires_at` is a unix timestamp (in seconds). + /// Must emit SbtRenew event. + pub fn sbt_renew(&mut self, tokens: Vec, expires_at: u64, memo: Option); + + /// Revokes and burn SBT. + /// Must emit NEP-171 compatible Burn event. + /// Returns true if a token_id is a valid, active SBT. Otherwise returns false. + pub fn sbt_revoke(token_id: uint64): bool; } ``` @@ -216,19 +223,27 @@ interface SBT { - Template and set of guidelines for creating SBT tokens. - Ability to create SBT aggregators. - Ability to use SBT as a primitive to model non KYC identity, badges, certificates etc... -- SBT can be further used for "lego" protocols, like: Proof of Humanity (dicussed for NDC Governance), undercollateralized lending, role based authentication sytems, innovative economic and social applications... -- Stanarized recoverability mechanism. +- SBT can be further used for "lego" protocols, like: Proof of Humanity (discussed for NDC Governance), undercollateralized lending, role based authentication systems, innovative economic and social applications... +- Standarized recoverability mechanism. - SBT are considered as a basic primitive for Decentralized Societies. - new way to implement Sybil attack resistance. ### Netural -- API follows the NEP-171 (NFT) standard. +- The API partially follows the NEP-171 (NFT) standard. The proposed design is to have native SBT API and also support NFT based queries. NOTE: we can decide to use `nft_` prefix whenever possible. ### Negative -- new set of events to be handled by the indexer. However this is not an issue if we decide to use subset of events used by the NEP-171 (NFT). +- new set of events to be handled by the indexer. +- complexity of integration with a registry: all SBT related transactions must go through Registry. + +## Considerations + +Being fully compatible with NFT standard is a desirable. However, given the requirements related to _soul transfer_ we didn't find an applaudable solution. Also we decided to use u64 as a Token ID, diverging further from the NFT NEP-171 standard. + +Give that our requirements are much striker, we need to reconsider the level of compatibility with NEP-171 NFT. +There are so many examples where NFT standards are poorly or improperly implemented, adding another standard with differing functionality but equal naming in there will cause lots of misclassifications between NFTs/SBTs, and then recover methods called on NFT contracts, SBTs attempted to be listed on NFT marketplaces and so on. ## Copyright From 89aa83ab9d50baf6847c17285b7c84a33d18598e Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Sat, 25 Mar 2023 14:44:37 +0100 Subject: [PATCH 13/89] adding token kind --- specs/Standards/Sould Bound Token/README.md | 69 ++++++++++++++++----- 1 file changed, 53 insertions(+), 16 deletions(-) diff --git a/specs/Standards/Sould Bound Token/README.md b/specs/Standards/Sould Bound Token/README.md index 56368e76d..12cb0db44 100644 --- a/specs/Standards/Sould Bound Token/README.md +++ b/specs/Standards/Sould Bound Token/README.md @@ -30,7 +30,7 @@ _Verifiable Credentials_ (VC) could be seen as subset of SBT. However there is a ## Specification -Main requirement for Soubound tokens is to make it bound to a human: +Main requirement for Soulbound tokens is to make it bound to a human: - moving tokens from one account to another should be strictly limited to case of _recoverability_ (eg in case a user's private key is compromised due to extortion, loss, etc) or user account merge (_soul transfer_). This becomes especially important for proof-of-human stamps that can only be issued once per user. @@ -39,23 +39,51 @@ Main requirement for Soubound tokens is to make it bound to a human: Atomicity of `soul_transfer` in current NEAR runtime is not possible if the token balance is kept separately in each SBT smart contract. To provide atomic transfer of all user tokens, we need additional contract: the `SBT Registry`. // TODO: add more details about the registry. -Two safeguards against misuse of recovery are contemplated. 1) Users cannot recover an SBT by themselves. The issuer, a DAO or a smart contract (eg: multisig) dedicated to manage the recovery should be assigned. 2) Whenever a recovery is triggered then the Sould registry emits _SoulKill_ event (indicator that the account identity is burned). Recovering one SBT triggers registry blocklist that should apply for SBTs that share the same address. +Two safeguards against misuse of recovery are contemplated. 1) Users cannot recover an SBT by themselves. The issuer, a DAO or a smart contract (eg: multisig) dedicated to manage the recovery should be assigned. 2) Whenever a recovery is triggered then the Soul registry emits _SoulKill_ event (indicator that the account identity is burned). Recovering one SBT triggers registry blocklist that should apply for SBTs that share the same address. Soulbound tokens can have an expire date. This is useful for tokens which are related to real world certificates with expire time, or social mechanisms (eg, community membership). Such tokens SHOULD have an option to be renewable. Examples include mandatory renewal with some frequency to check that the owner is still alive, or renew membership to a DAO that uses SBTs as membership gating. +### Token Kind + +SBT tokens can't be fractionized. Also, by definition there should be only one of a token per token kind per user. Examples: user should not be able to receive few badges of the same kind, or few proof of attendance to the same event. +However we identify a need for having few token kinds in a single contract: + +- badges: one contract with multiple badge kind (community lead, OG...); +- certificates: one issuer can create certificates of a different kind (eg school department can create diplomas for each major and each graduation year). + +We also see a trend in the NFT community and demand for market places to support multi token contracts. + +- In Ethereum community many projects are using [ERC-1155 Multi Token Standard](https://eips.ethereum.org/EIPS/eip-1155). NFT projects are using it for fraction ownership: each token id can have many fungible fractions. +- NEAR [NEP-246](https://github.com/near/NEPs/blob/master/neps/nep-0245.md) has elaborated similar interface for both bridge compatibility with EVM chains as well as flexibility to define different token types with different behavior in a single contract. +- [NEP-454](https://github.com/near/NEPs/pull/454) proposes royalties support for multi token contracts. + +We propose that the SBT Standard will support the multi-token idea from the get go. This won't increase the complexity of the contract (in a traditional case, where one contract will only issue tokens of the single kind, the `kind` argument is simply ignored in the state, and in the functions it's required to be of a constant value, eg `1`) but will unify the interface. +It's up to the smart contract design how the token kinds is managed. A smart contract can expose an admin function (example: `sbt_new_kind() -> KindId`) or hard code the pre-registered kinds. + +Finally, we require that each token ID is unique within the smart contract. This will allow us to query token only by token ID, without knowing it's kind. + ### Smart contract interface For the Token ID type we propose `u64` rather than `U128`. `u64` capacity is more than 1e19. If we will mint 10'000 SBTs per second, than it will take us 58'494'241 years to get into the capacity. -Today, the JS integer limit is `2^53-1 ~ 9e15`. It will take us 28561 years to fill that when minting 10'000 SBTs per second. So, we don't need to u128 nor a String type. However, if for some reason, we will need to get u64 support for JS, then we can always add another set of methods which will return String, so making it compatible with NFT standar (which is using `U128`, which is a string). +Today, the JS integer limit is `2^53-1 ~ 9e15`. It will take us 28561 years to fill that when minting 10'000 SBTs per second. So, we don't need to u128 nor a String type. However, if for some reason, we will need to get u64 support for JS, then we can always add another set of methods which will return String, so making it compatible with NFT standard (which is using `U128`, which is a string). ```rust +// TokenId and Kind Id must be positive (0 is not a valid id) pub type TokenId = u64; +pub type KindId = u64; + +pub struct Token { + pub token_id: TokenId, + pub owner_id: AccountId, + pub metadata: TokenMetadata, +} ``` The Soulbound Token follows the NFT [NEP-171](https://github.com/near/NEPs/blob/master/neps/nep-0171.md) interface, with few differences: - token ID is `u64` (as discussed above). -- `TokenMetadata` doesn't have `title`, `description`, `media`, `media_hash`, `copies`, `extra`, `starts_at` nor `updated_at`. All that attributes except the `updated_at` can be part of the document stored at `reference`. `updated_at` can be tracked on blockchain. +- token kind is `u64`, it's required when minting and it's part of the token metadata. +- `TokenMetadata` doesn't have `title`, `description`, `media`, `media_hash`, `copies`, `extra`, `starts_at` nor `updated_at`. All that attributes except the `updated_at` can be part of the document stored at `reference`. `updated_at` can be tracked easily by indexers. - only NEP-171 Mint and Burn events are reused. Since we don't have normal transferability, we propose to use more targeted events, to better reflect the event nature. - TODO: since the native token ID is different in SBT and NFT, maybe we should use SBT special events for mint and revoke (burn)? @@ -73,6 +101,7 @@ pub struct ContractMetadata { /// TokenMetadata defines attributes for each SBT token. pub struct TokenMetadata { + pub kind: KindId, // Kind of a token pub issued_at: Option, // When token was issued or minted, Unix epoch in milliseconds pub expires_at: Option, // When token expires, Unix epoch in milliseconds pub reference: Option, // URL to an off-chain JSON file with more info. @@ -91,30 +120,35 @@ trait SBT { /// returns total amount of tokens minted by this contract fn sbt_total_supply(&self) -> u64; + /// returns total amount of tokens of given kind minted by this contract + fn sbt_total_supply_by_kind(&self, kind: KindId) -> u64; + + /// returns total supply of SBTs for a given owner - fn sbt_supply_for_owner(&self, account: AccountId) -> u64; + fn sbt_supply_by_owner(&self, account: AccountId) -> u64; + + /// returns true if the `account` has a token of a given `kind`. + fn sbt_supply_by_kind(&self, account: AccountId, kind: KindId) -> bool; /// Query for sbt tokens + /// If `from_index` is not specified, then `from_index` should be assumed to be the first + /// valid token id. fn sbt_tokens(&self, from_index: Option, limit: Option) -> Vec; /// Query sbt tokens by owner - fn sbt_tokens_for_owner( + /// If `from_kind` is not specified, then `from_kind` should be assumed to be the first + /// valid kind id. + fn sbt_tokens_by_owner( &self, account: AccountId, - from_index: Option, + from_kind: Option, limit: Option, ) -> Vec; - /// Optional. If the SBT implementaiton assures that one account can have maximum one SBT - /// the the following function should be implemented. - /// Returns Some(Token) if an `account` owns an SBT, otherwise returns None. - fn sbt_token_for_owner(&self, account: AccountId) -> Option {} - /* * Transactions are not part of the standard. Instead, in the section below, we provide * recommended interfaces for the functions which are related to the SBT Standard Events. **/ - } ``` @@ -123,11 +157,12 @@ SBT smart contracts can implement NFT query interface to make it compatible with ```rust trait SBTNFT { fn nft_total_supply(&self) -> U64 + // here we index by token id instead of by kind id (as done in `sbt_tokens_by_owner`) fn nft_tokens_for_owner(&self, account_id: AccountId, from_index: Option, limit: Option) -> Vec fn nft_supply_for_owner(&self, account_id: AccountId) -> U64 ``` -### Logs +### Events ```typescript struct SbtEventKind { @@ -187,8 +222,9 @@ trait SBTTxs { /// Must emit NEP-171 compatible Mint event. /// Must provide enough NEAR to cover registry storage cost. /// The arguments to this function can vary, depending on the use-case. + /// `kind` is provided as an explicit argument and it must overwrite `metadata.kind`. #[payable] - fn sbt_mint(&mut self, metadata: TokenMetadata, receiver: AccountId); + fn sbt_mint(&mut self, kind: KindId, metadata: TokenMetadata, receiver: AccountId); /// sbt_recover reassigns all tokens from the old owner to a new owner, /// and registers `old_owner` to a burned addresses registry. @@ -212,6 +248,7 @@ trait SBTTxs { ## Reference Implementation +- Common [type definitions](https://github.com/alpha-fi/i-am-human/tree/master/contracts/sbt) (events, traits). - https://github.com/alpha-fi/i-am-human/tree/master/contracts/soulbound ## Example Flow @@ -228,7 +265,7 @@ trait SBTTxs { - SBT are considered as a basic primitive for Decentralized Societies. - new way to implement Sybil attack resistance. -### Netural +### Neutral - The API partially follows the NEP-171 (NFT) standard. The proposed design is to have native SBT API and also support NFT based queries. NOTE: we can decide to use `nft_` prefix whenever possible. From 13b152cae57eabe3dfe459bafca137c9f88c84a1 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Sat, 25 Mar 2023 16:37:16 +0100 Subject: [PATCH 14/89] update documentation about soul_transfer, recover and revoke --- specs/Standards/Sould Bound Token/README.md | 96 +++++++++++++-------- 1 file changed, 58 insertions(+), 38 deletions(-) diff --git a/specs/Standards/Sould Bound Token/README.md b/specs/Standards/Sould Bound Token/README.md index 12cb0db44..f18a8d4e6 100644 --- a/specs/Standards/Sould Bound Token/README.md +++ b/specs/Standards/Sould Bound Token/README.md @@ -30,18 +30,19 @@ _Verifiable Credentials_ (VC) could be seen as subset of SBT. However there is a ## Specification -Main requirement for Soulbound tokens is to make it bound to a human: +Main requirement for Soulbound tokens is to make it bound to a human. Moving tokens from one account to another should be strictly limited to case of **recoverability** (eg in case a user's private key is compromised due to extortion, loss, etc) or user account merge (**soul transfer**). This becomes especially important for proof-of-human stamps that can only be issued once per user. -- moving tokens from one account to another should be strictly limited to case of _recoverability_ (eg in case a user's private key is compromised due to extortion, loss, etc) or user account merge (_soul transfer_). - This becomes especially important for proof-of-human stamps that can only be issued once per user. -- there should be an inherit cost when a token is moved. We propose an SBT registry, which will assure that a true _soul transfer_ is performed, and the source account will be blocked to house any new soul. +Two safeguards against misuse of recovery are contemplated. -Atomicity of `soul_transfer` in current NEAR runtime is not possible if the token balance is kept separately in each SBT smart contract. To provide atomic transfer of all user tokens, we need additional contract: the `SBT Registry`. -// TODO: add more details about the registry. +1. Users cannot recover an SBT by themselves. The issuer, a DAO or a smart contract (eg: multisig) dedicated to manage the recovery should be assigned. +2. Whenever a _soul transfer_ is triggered then the SBT registry emits `Kill` event. It creates an inherit cost for such action: the account identity is burned and can't receive any SBT in the future. -Two safeguards against misuse of recovery are contemplated. 1) Users cannot recover an SBT by themselves. The issuer, a DAO or a smart contract (eg: multisig) dedicated to manage the recovery should be assigned. 2) Whenever a recovery is triggered then the Soul registry emits _SoulKill_ event (indicator that the account identity is burned). Recovering one SBT triggers registry blocklist that should apply for SBTs that share the same address. +SBT recover MUST not trigger `SoulTransfer` nor `Kill`: malicious issuer could compromise the system by faking the token recovery and take over all other SBTs from a user. +Only the owner of the account can call `soul_transfer` and merge 2 accounts he owns. -Soulbound tokens can have an expire date. This is useful for tokens which are related to real world certificates with expire time, or social mechanisms (eg, community membership). Such tokens SHOULD have an option to be renewable. Examples include mandatory renewal with some frequency to check that the owner is still alive, or renew membership to a DAO that uses SBTs as membership gating. +Soulbound tokens can have an _expire date_. This is useful for tokens which are related to real world certificates with expire time, or social mechanisms (eg, community membership). Such tokens SHOULD have an option to be renewable. Examples include mandatory renewal with a frequency to check that the owner is still alive, or renew membership to a DAO that uses SBTs as membership gating. + +An issuer can provide a _sbt revocation_ in his contract (eg, when a related certificate or membership should be revoked). When doing so, the SBT registry MUST be updated and `Revoke` event must be emitted. It's up to the registry to define revocation handling (either by burning a token, or changing expire date of it's metadata). ### Token Kind @@ -164,46 +165,65 @@ trait SBTNFT { ### Events +CALL FOR ACTION: Shall the events be issued by the registry or by the issuing contract? + +- registry: we query registry, so it makes sense to use registry. If we emit the event by the registry, we need to add smart contract argument as a field of the events. +- issuing contract: all actions, except soul transfer, are initiated through the issuing contract. So maybe only `SoulTransfer` event should be emitted by a registry. +- maybe we should reduce amount of tokens: merge {mint, renew}? +- maybe we can remove Kill event -- currently it's implicit by the SoulTransfer event... but maybe future use cases will require it? +- Should we use NEP-171 Mint and NEP-171 Burn (instead of revoke) events? If the events will be emitted by registry, then we need new events to include the contract address. + ```typescript -struct SbtEventKind { +type SbtEventKind { standard: "nep393"; version: "1.0.0"; - event: "mint" | "recover" | "renew"; - data: Recover | Renew | SoulTransfer; + event: "mint" | "recover" | "renew" | "revoke" | "kill"; + data: Mint | Recover | Renew | Revoke | SoulTransfer | Kill; } // Note: for token minting, NEP-171 compatible Mint event is used. +type Mint = NftMint + -/// An event emitted when a recovery process succeeded to reassign SBT due to account access -/// loss. This action is usually requested by the owner, but executed by an issuer, and doesn't -/// trigger Soul Transfer. -/// Arguments -/// * `old_owner`: current holder of the SBT, whose account was compromised or lost. -/// * `new_owner`: destination account. -/// * `token_ids`: list of token ids. -/// * `memo`: optional message -struct Recover { - old_owner: AccountId; - new_owner: AccountId; - tokens: []u64; - memo: Option; +/// An event emitted when a recovery process succeeded to reassign SBT, usually due to account +/// access loss. This action is usually requested by the owner, but executed by an issuer, +/// and doesn't trigger Soul Transfer. +type Recover { + old_owner: AccountId; // current holder of the SBT + new_owner: AccountId; // destination account. + tokens: []u64; // list of token ids. + memo?: string; // optional message } /// An event emitted when a existing tokens are renewed. -/// Arguments -/// * `tokens`: list of token ids. -/// * `memo`: optional message -struct Renew { - tokens: []u64; - memo: string; +type Renew { + tokens: []u64; // list of token ids. + memo?: string; // optional message } -/// An evenet emitted when soul transfer is happening: all SBTs owned by `from` are transferred -/// to `to`, and the `from` account is _killed_ (can't receive any new SBT). -struct SoulTransfer { +/// An event emitted when a existing tokens are revoked. +/// Revoked tokens should not be listed in a wallet. +type Revoke { + tokens: []u64; // list of token ids. + memo?: string; // optional message +} + +/// An event emitted when soul transfer is happening: all SBTs owned by `from` are transferred +/// to `to`, and the `from` account is killed (can't receive any new SBT). +/// Registry MUST emit `Kill` whenever the soul transfer happens. +type SoulTransfer { from: AccountId; to: AccountId; - memo: Option; + memo?: string; // optional message +} + +/// An event emitted when the `account` is killed within the emitting registry. +/// Must be emitted by an SBT registry. +/// Registry must add the `account` to a blocklist and prohibit issuing SBTs to this account +/// in the future +type Kill { + account: AccountId; + memo?: string; // optional message } ``` @@ -219,7 +239,7 @@ These functions should emit appropriate events. trait SBTTxs { /// Creates a new, unique token and assigns it to the `receiver`. - /// Must emit NEP-171 compatible Mint event. + /// Must emit NEP-171 compatible `Mint` event. /// Must provide enough NEAR to cover registry storage cost. /// The arguments to this function can vary, depending on the use-case. /// `kind` is provided as an explicit argument and it must overwrite `metadata.kind`. @@ -228,7 +248,7 @@ trait SBTTxs { /// sbt_recover reassigns all tokens from the old owner to a new owner, /// and registers `old_owner` to a burned addresses registry. - /// Must emit SbtRecover event. + /// Must emit `Recover` event. /// Must be called by an operator. /// Must provide enough NEAR to cover registry storage cost. #[payable] @@ -236,11 +256,11 @@ trait SBTTxs { /// sbt_renew will update the expire time of provided tokens. /// `expires_at` is a unix timestamp (in seconds). - /// Must emit SbtRenew event. + /// Must emit `Renew` event. pub fn sbt_renew(&mut self, tokens: Vec, expires_at: u64, memo: Option); /// Revokes and burn SBT. - /// Must emit NEP-171 compatible Burn event. + /// Must emit `Revoke` event. /// Returns true if a token_id is a valid, active SBT. Otherwise returns false. pub fn sbt_revoke(token_id: uint64): bool; } From afab9e8a0cb7989ac7265bf39a55a2a424b646c8 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Sat, 25 Mar 2023 17:02:23 +0100 Subject: [PATCH 15/89] sbt registry --- specs/Standards/Sould Bound Token/README.md | 158 +++++++++++++------- 1 file changed, 108 insertions(+), 50 deletions(-) diff --git a/specs/Standards/Sould Bound Token/README.md b/specs/Standards/Sould Bound Token/README.md index f18a8d4e6..d4697c642 100644 --- a/specs/Standards/Sould Bound Token/README.md +++ b/specs/Standards/Sould Bound Token/README.md @@ -36,6 +36,7 @@ Two safeguards against misuse of recovery are contemplated. 1. Users cannot recover an SBT by themselves. The issuer, a DAO or a smart contract (eg: multisig) dedicated to manage the recovery should be assigned. 2. Whenever a _soul transfer_ is triggered then the SBT registry emits `Kill` event. It creates an inherit cost for such action: the account identity is burned and can't receive any SBT in the future. +3. The recovery function is additional economical risk preventing account trading: user should always be able to recover his SBT, and move to another, not killed account. SBT recover MUST not trigger `SoulTransfer` nor `Kill`: malicious issuer could compromise the system by faking the token recovery and take over all other SBTs from a user. Only the owner of the account can call `soul_transfer` and merge 2 accounts he owns. @@ -63,6 +64,21 @@ It's up to the smart contract design how the token kinds is managed. A smart con Finally, we require that each token ID is unique within the smart contract. This will allow us to query token only by token ID, without knowing it's kind. +### SBT Registry + +Atomicity of _soul transfer_ in current NEAR runtime is not possible if the token balance is kept separately for each SBT smart contract. We need an additional contract: the `SBT Registry`, to provide atomic transfer of all user tokens and efficient way to block accounts in relation to a Kill event. The registry will provide a balance book for all associated SBT tokens. + +An SBT smart contract, SHOULD opt-in to a registry using `opt_in` function. One SBT smart contract can opt-in to: + +- many registries: it MUST relay all state change functions to all registries. +- or to no registry: it MUST issue the SBT state change emits by itself. + +Moreover, a registry will provide an efficient way to query multiple tokens for a single user. This will allow implementation of use cases such us: + +- SBT based identities (main use case of the `i-am-human` protocol) +- decentralized societies +- SBT classes. + ### Smart contract interface For the Token ID type we propose `u64` rather than `U128`. `u64` capacity is more than 1e19. If we will mint 10'000 SBTs per second, than it will take us 58'494'241 years to get into the capacity. @@ -110,46 +126,91 @@ pub struct TokenMetadata { } -trait SBT { +trait SBTRegistry { /********** * QUERIES **********/ /// get the information about specific token ID - fn sbt(&self, token_id: TokenId) -> Option; + fn sbt(&self, ctr: AccountId, token_id: TokenId) -> Option; /// returns total amount of tokens minted by this contract - fn sbt_total_supply(&self) -> u64; + fn sbt_total_supply(&self, ctr: AccountId) -> u64; /// returns total amount of tokens of given kind minted by this contract - fn sbt_total_supply_by_kind(&self, kind: KindId) -> u64; - + fn sbt_total_supply_by_kind(&self, ctr: AccountId, kind: KindId) -> u64; /// returns total supply of SBTs for a given owner - fn sbt_supply_by_owner(&self, account: AccountId) -> u64; + fn sbt_supply_by_owner(&self, ctr: AccountId, account: AccountId) -> u64; /// returns true if the `account` has a token of a given `kind`. - fn sbt_supply_by_kind(&self, account: AccountId, kind: KindId) -> bool; + fn sbt_supply_by_kind(&self, ctr: AccountId, account: AccountId, kind: KindId) -> bool; - /// Query for sbt tokens - /// If `from_index` is not specified, then `from_index` should be assumed to be the first - /// valid token id. - fn sbt_tokens(&self, from_index: Option, limit: Option) -> Vec; + /// Query sbt tokens. If `from_index` is not specified, then `from_index` should be assumed + /// to be the first valid token id. + fn sbt_tokens( + &self, + ctr: AccountId, + from_index: Option, + limit: Option, + ) -> Vec; /// Query sbt tokens by owner /// If `from_kind` is not specified, then `from_kind` should be assumed to be the first /// valid kind id. fn sbt_tokens_by_owner( &self, + ctr: AccountId, account: AccountId, - from_kind: Option, + from_kind: Option, limit: Option, - ) -> Vec; + ) -> Vec; + + /************* + * Transactions + *************/ + + /// Creates a new, unique token and assigns it to the `receiver`. + /// Must be called by an SBT contract. + /// Must emit NEP-171 compatible `Mint` event. + /// Must provide enough NEAR to cover registry storage cost. + /// The arguments to this function can vary, depending on the use-case. + /// `kind` is provided as an explicit argument and it must overwrite `metadata.kind`. + /// Requires attaching enough tokens to cover the storage growth. + // #[payable] + fn sbt_mint( + &mut self, + account: AccountId, + kind: Option, + metadata: TokenMetadata, + ) -> TokenId; - /* - * Transactions are not part of the standard. Instead, in the section below, we provide - * recommended interfaces for the functions which are related to the SBT Standard Events. - **/ + /// sbt_recover reassigns all tokens from the old owner to a new owner, + /// and registers `old_owner` to a burned addresses registry. + /// Must be called by an SBT contract. + /// Must emit `Recover` event. + /// Must be called by an operator. + /// Must provide enough NEAR to cover registry storage cost. + /// Requires attaching enough tokens to cover the storage growth. + // #[payable] + fn sbt_recover(&mut self, from: AccountId, to: AccountId); + + /// sbt_renew will update the expire time of provided tokens. + /// `expires_at` is a unix timestamp (in seconds). + /// Must be called by an SBT contract. + /// Must emit `Renew` event. + fn sbt_renew(&mut self, tokens: Vec, expires_at: u64, memo: Option); + + /// Revokes SBT, could potentailly burn it or update the expire time. + /// Must be called by an SBT contract. + /// Must emit `Revoke` event. + /// Returns true if a token_id is a valid, active SBT. Otherwise returns false. + fn sbt_revoke(&mut self, token_id: u64) -> bool; + + /// Transfers atomically all SBT tokens from one account to another account. + /// Must be an SBT holder. + // #[payable] + fn sbt_soul_transfer(&mut self, to: AccountId) -> bool; } ``` @@ -165,14 +226,6 @@ trait SBTNFT { ### Events -CALL FOR ACTION: Shall the events be issued by the registry or by the issuing contract? - -- registry: we query registry, so it makes sense to use registry. If we emit the event by the registry, we need to add smart contract argument as a field of the events. -- issuing contract: all actions, except soul transfer, are initiated through the issuing contract. So maybe only `SoulTransfer` event should be emitted by a registry. -- maybe we should reduce amount of tokens: merge {mint, renew}? -- maybe we can remove Kill event -- currently it's implicit by the SoulTransfer event... but maybe future use cases will require it? -- Should we use NEP-171 Mint and NEP-171 Burn (instead of revoke) events? If the events will be emitted by registry, then we need new events to include the contract address. - ```typescript type SbtEventKind { standard: "nep393"; @@ -188,7 +241,9 @@ type Mint = NftMint /// An event emitted when a recovery process succeeded to reassign SBT, usually due to account /// access loss. This action is usually requested by the owner, but executed by an issuer, /// and doesn't trigger Soul Transfer. +/// Must be emitted by an SBT registry. type Recover { + ctr: AccountId // SBT Contract recovering the tokens old_owner: AccountId; // current holder of the SBT new_owner: AccountId; // destination account. tokens: []u64; // list of token ids. @@ -196,20 +251,25 @@ type Recover { } /// An event emitted when a existing tokens are renewed. +/// Must be emitted by an SBT registry. type Renew { + ctr: AccountId // SBT Contract renewing the tokens tokens: []u64; // list of token ids. memo?: string; // optional message } /// An event emitted when a existing tokens are revoked. /// Revoked tokens should not be listed in a wallet. +/// Must be emitted by an SBT registry. type Revoke { + ctr: AccountId // SBT Contract revoking the tokens tokens: []u64; // list of token ids. memo?: string; // optional message } /// An event emitted when soul transfer is happening: all SBTs owned by `from` are transferred /// to `to`, and the `from` account is killed (can't receive any new SBT). +/// Must be emitted by an SBT registry. /// Registry MUST emit `Kill` whenever the soul transfer happens. type SoulTransfer { from: AccountId; @@ -221,6 +281,7 @@ type SoulTransfer { /// Must be emitted by an SBT registry. /// Registry must add the `account` to a blocklist and prohibit issuing SBTs to this account /// in the future +/// Must be emitted by an SBT registry. type Kill { account: AccountId; memo?: string; // optional message @@ -231,38 +292,26 @@ Whenever a recovery is made in a way that an existing SBT is burned, the `NftBur ### Recommended functions -Although the transaction functions below are not part of the standard (depending on a use case, they may need different parameters), we recommend them as a part of implementation and we also provide them in the reference implementation. -These functions should emit appropriate events. +Although the transaction functions below are not part of the SBT smart contract standard (depending on a use case, they may have different parameters), we recommend them as a part of implementation and we also provide them in the reference implementation. +These functions should emit appropriate events and relay calls to a SBT registry. ```rust -trait SBTTxs { - - /// Creates a new, unique token and assigns it to the `receiver`. - /// Must emit NEP-171 compatible `Mint` event. - /// Must provide enough NEAR to cover registry storage cost. - /// The arguments to this function can vary, depending on the use-case. - /// `kind` is provided as an explicit argument and it must overwrite `metadata.kind`. - #[payable] - fn sbt_mint(&mut self, kind: KindId, metadata: TokenMetadata, receiver: AccountId); +trait SBT { + // #[payable] + fn sbt_mint( + &mut self, + account: AccountId, + kind: Option, + metadata: TokenMetadata, + ) -> Vec; - /// sbt_recover reassigns all tokens from the old owner to a new owner, - /// and registers `old_owner` to a burned addresses registry. - /// Must emit `Recover` event. - /// Must be called by an operator. - /// Must provide enough NEAR to cover registry storage cost. - #[payable] + // #[payable] fn sbt_recover(&mut self, from: AccountId, to: AccountId); - /// sbt_renew will update the expire time of provided tokens. - /// `expires_at` is a unix timestamp (in seconds). - /// Must emit `Renew` event. - pub fn sbt_renew(&mut self, tokens: Vec, expires_at: u64, memo: Option); + fn sbt_renew(&mut self, tokens: Vec, expires_at: u64, memo: Option); - /// Revokes and burn SBT. - /// Must emit `Revoke` event. - /// Returns true if a token_id is a valid, active SBT. Otherwise returns false. - pub fn sbt_revoke(token_id: uint64): bool; + fn sbt_revoke(token_id: u64) -> bool; } ``` @@ -302,6 +351,15 @@ Being fully compatible with NFT standard is a desirable. However, given the requ Give that our requirements are much striker, we need to reconsider the level of compatibility with NEP-171 NFT. There are so many examples where NFT standards are poorly or improperly implemented, adding another standard with differing functionality but equal naming in there will cause lots of misclassifications between NFTs/SBTs, and then recover methods called on NFT contracts, SBTs attempted to be listed on NFT marketplaces and so on. +CALL FOR ACTION: Shall the events be issued by the registry or by the issuing contract? + +- registry: we query registry, so it makes sense to use registry. If we emit the event by the registry, we need to add smart contract argument as a field of the events. +- issuing contract: all actions, except soul transfer, are initiated through the issuing contract. So maybe only `SoulTransfer` event should be emitted by a registry. +- maybe we should reduce amount of tokens: merge {mint, renew}? +- maybe we can remove Kill event -- currently it's implicit by the SoulTransfer event... but maybe future use cases will require it? +- Should we use NEP-171 Mint and NEP-171 Burn (instead of revoke) events? If the events will be emitted by registry, then we need new events to include the contract address. +- Decide if we need to keep memo in the events. + ## Copyright [Creative Commons Attribution 4.0 International Public License (CC BY 4.0)](https://creativecommons.org/licenses/by/4.0/) From e0d92fb896cc89255b117b6753da3f0229e6e941 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Sat, 25 Mar 2023 18:37:57 +0100 Subject: [PATCH 16/89] adding a mint flow --- specs/Standards/Sould Bound Token/README.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/specs/Standards/Sould Bound Token/README.md b/specs/Standards/Sould Bound Token/README.md index d4697c642..407fad877 100644 --- a/specs/Standards/Sould Bound Token/README.md +++ b/specs/Standards/Sould Bound Token/README.md @@ -320,7 +320,24 @@ trait SBT { - Common [type definitions](https://github.com/alpha-fi/i-am-human/tree/master/contracts/sbt) (events, traits). - https://github.com/alpha-fi/i-am-human/tree/master/contracts/soulbound -## Example Flow +## Example Flows + +```mermaid +sequenceDiagram + actor Alice + actor Issuer1 as SBT_1 Issuer + participant SBT1 as SBT_1 Contract + participant SBT_Registry + + Issuer1->>SBT_1_Contract: sbt_mint(alice, 1, metadata) + SBT1-)SBT_Registry: sbt_mint(alice, 1, metadata) + SBT_Registry-)SBT1: token_id: 238 + + Note over Issuer1,SBT1,SBT_Registry: now Alice can query registry to check her SBT + + Alice-->SBT_Registry: sbt(SBT_1_Contract, 238) + SBT_Registry-->Alice: {token_id: 238, owner_id: alice, metadata} +``` ## Consequences From 664573f16738e3c24a5e46d9cefcd00c48ef21c6 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Sun, 26 Mar 2023 21:27:01 +0200 Subject: [PATCH 17/89] adding mint event (non nep-171) + few cosmetic updates --- specs/Standards/Sould Bound Token/README.md | 29 +++++++++++++-------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/specs/Standards/Sould Bound Token/README.md b/specs/Standards/Sould Bound Token/README.md index 407fad877..1c5c60b40 100644 --- a/specs/Standards/Sould Bound Token/README.md +++ b/specs/Standards/Sould Bound Token/README.md @@ -22,7 +22,7 @@ SBTs are well suited for carrying proof-of-attendance, proof-of-unique-human "st ## Motivation -Recent Decentralized Society trends open a new area of Web3 research to model various aspects of what characterizes humans. Economic and governance value is generated by humans and their relationship. Creating a strong primitive is necessary to model new innovative systems and decentralized societies. Examples include reputation protocols, one-person-one-vote, fair airdrops & ICOs, universal basic income, non KYC identity systems, Human DAOs as well as methods for Sybil attack resistance. +Recent [Decentralized Society](https://www.bankless.com/decentralized-society-desoc-explained) trends open a new area of Web3 research to model various aspects of what characterizes humans. Economic and governance value is generated by humans and their relationship. Creating a strong primitives is necessary to model new innovative systems and decentralized societies. Examples include reputation protocols, one-person-one-vote, fair airdrops & ICOs, universal basic income, non KYC identity systems, Human DAOs and methods for Sybil attack resistance. We propose a SBT standard to model protocols described above. @@ -73,11 +73,13 @@ An SBT smart contract, SHOULD opt-in to a registry using `opt_in` function. One - many registries: it MUST relay all state change functions to all registries. - or to no registry: it MUST issue the SBT state change emits by itself. +If an SBT smart contract doesn't opt-in to any registry, then we should think about it as a single token registry, and it MUST strictly implement all SBT Registry query functions by itself: the contract address must be part of the arguments, and it must check that it equals to the deployed account address (`require!(ctr == env::current_account_id())`). + Moreover, a registry will provide an efficient way to query multiple tokens for a single user. This will allow implementation of use cases such us: -- SBT based identities (main use case of the `i-am-human` protocol) -- decentralized societies -- SBT classes. +- SBT based identities (main use case of the `i-am-human` protocol); +- SBT classes; +- decentralized societies. ### Smart contract interface @@ -101,8 +103,7 @@ The Soulbound Token follows the NFT [NEP-171](https://github.com/near/NEPs/blob/ - token ID is `u64` (as discussed above). - token kind is `u64`, it's required when minting and it's part of the token metadata. - `TokenMetadata` doesn't have `title`, `description`, `media`, `media_hash`, `copies`, `extra`, `starts_at` nor `updated_at`. All that attributes except the `updated_at` can be part of the document stored at `reference`. `updated_at` can be tracked easily by indexers. -- only NEP-171 Mint and Burn events are reused. Since we don't have normal transferability, we propose to use more targeted events, to better reflect the event nature. - - TODO: since the native token ID is different in SBT and NFT, maybe we should use SBT special events for mint and revoke (burn)? +- We don't have normal transferability, we propose to use more targeted events, to better reflect the event nature. Moreover events are emitted by the registry, so we need to include issuer contract address in the event. ```rust /// ContractMetadata defines contract wide attributes, which describes the whole contract. @@ -172,7 +173,7 @@ trait SBTRegistry { /// Creates a new, unique token and assigns it to the `receiver`. /// Must be called by an SBT contract. - /// Must emit NEP-171 compatible `Mint` event. + /// Must emit `Mint` event. /// Must provide enough NEAR to cover registry storage cost. /// The arguments to this function can vary, depending on the use-case. /// `kind` is provided as an explicit argument and it must overwrite `metadata.kind`. @@ -234,8 +235,13 @@ type SbtEventKind { data: Mint | Recover | Renew | Revoke | SoulTransfer | Kill; } -// Note: for token minting, NEP-171 compatible Mint event is used. -type Mint = NftMint +/// An event minted by the Registry when new SBT is created. +type Mint { + ctr: AccountId // SBT Contract recovering the tokens + owner: AccountId, // holder of the newly minted SBT + tokens: []u64, // newly minted token ids + memo?: string, // optional message +} /// An event emitted when a recovery process succeeded to reassign SBT, usually due to account @@ -358,7 +364,7 @@ sequenceDiagram ### Negative -- new set of events to be handled by the indexer. +- new set of events to be handled by the indexer and wallets. - complexity of integration with a registry: all SBT related transactions must go through Registry. ## Considerations @@ -374,7 +380,8 @@ CALL FOR ACTION: Shall the events be issued by the registry or by the issuing co - issuing contract: all actions, except soul transfer, are initiated through the issuing contract. So maybe only `SoulTransfer` event should be emitted by a registry. - maybe we should reduce amount of tokens: merge {mint, renew}? - maybe we can remove Kill event -- currently it's implicit by the SoulTransfer event... but maybe future use cases will require it? -- Should we use NEP-171 Mint and NEP-171 Burn (instead of revoke) events? If the events will be emitted by registry, then we need new events to include the contract address. +- Should we use NEP-171 Mint and NEP-171 Burn (instead of revoke) events? If the events will be emitted by registry, then we need new events to include the contract address. Since the native token ID is different in SBT and NFT, we should probably keep SBT events version +- Also confirm that only the registry emits the events. - Decide if we need to keep memo in the events. ## Copyright From e1340c827b2cb092a8e87defc2c84ace9550f82e Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Sun, 26 Mar 2023 21:39:45 +0200 Subject: [PATCH 18/89] Recovery within a SBT Registry --- specs/Standards/Sould Bound Token/README.md | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/specs/Standards/Sould Bound Token/README.md b/specs/Standards/Sould Bound Token/README.md index 1c5c60b40..7f9019526 100644 --- a/specs/Standards/Sould Bound Token/README.md +++ b/specs/Standards/Sould Bound Token/README.md @@ -16,13 +16,17 @@ Requires: -- ## Summary -Soulbound Token (SBT) is a form of a NFT which represents an aspect of an account (_soul_). Transferability is limited only to a case of recoverability or transferring the whole _soul_ - a `sbt_soul_transfer` should coordinate with a registry to transfer all SBTs from one account to another, and _killing_ the source account. +Soulbound Token (SBT) is a form of a NFT which represents an aspect of an account: _soul_. Transferability is limited only to a case of recoverability or transferring the whole _soul_ - a `sbt_soul_transfer` should coordinate with a registry to transfer all SBTs from one account to another, and _killing_ the source account. SBTs are well suited for carrying proof-of-attendance, proof-of-unique-human "stamps" and other similar credibility-carriers. ## Motivation -Recent [Decentralized Society](https://www.bankless.com/decentralized-society-desoc-explained) trends open a new area of Web3 research to model various aspects of what characterizes humans. Economic and governance value is generated by humans and their relationship. Creating a strong primitives is necessary to model new innovative systems and decentralized societies. Examples include reputation protocols, one-person-one-vote, fair airdrops & ICOs, universal basic income, non KYC identity systems, Human DAOs and methods for Sybil attack resistance. +Recent [Decentralized Society](https://www.bankless.com/decentralized-society-desoc-explained) trends open a new area of Web3 research to model various aspects of what characterizes humans. Economic and governance value is generated by humans and their relationship. + +> Decentralized Society, or DeSoc, would lean on SBTs to allow “Souls and communities to come together bottom-up, as emergent properties of each other to co-create plural network goods and intelligences, at a range of scales.” + +Creating a strong primitives is necessary to model new innovative systems and decentralized societies. Examples include reputation protocols, undercollaterized lending, one-person-one-vote, fair airdrops & ICOs, universal basic income, non KYC identity systems, Human DAOs and methods for Sybil attack resistance. We propose a SBT standard to model protocols described above. @@ -81,6 +85,17 @@ Moreover, a registry will provide an efficient way to query multiple tokens for - SBT classes; - decentralized societies. +#### Recovery within a SBT Registry + +SBT registry can define it's own mechanism to atomically recover all tokens related to one account and execute soul transfer to another account, without going one by one through each SBT issuer (sometimes that might be even not possible). Example mechanisms a Registry can implement to recovery mechanism: + +- KYC based recovery +- Social recovery + +![Social Recovery, Image via “Decentralized Society”](https://bankless.ghost.io/content/images/public/images/cdb1fc23-6179-44f0-9bfe-e5e5831492f7_1399x680.png) + +SBT Registry based recovery is not part of this specification. + ### Smart contract interface For the Token ID type we propose `u64` rather than `U128`. `u64` capacity is more than 1e19. If we will mint 10'000 SBTs per second, than it will take us 58'494'241 years to get into the capacity. From e8ade974691271650df3ec289ca2e3ca8ca40510 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Mon, 27 Mar 2023 11:43:14 +0200 Subject: [PATCH 19/89] update nep-245 note --- specs/Standards/Sould Bound Token/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/specs/Standards/Sould Bound Token/README.md b/specs/Standards/Sould Bound Token/README.md index 7f9019526..01353406d 100644 --- a/specs/Standards/Sould Bound Token/README.md +++ b/specs/Standards/Sould Bound Token/README.md @@ -60,7 +60,7 @@ However we identify a need for having few token kinds in a single contract: We also see a trend in the NFT community and demand for market places to support multi token contracts. - In Ethereum community many projects are using [ERC-1155 Multi Token Standard](https://eips.ethereum.org/EIPS/eip-1155). NFT projects are using it for fraction ownership: each token id can have many fungible fractions. -- NEAR [NEP-246](https://github.com/near/NEPs/blob/master/neps/nep-0245.md) has elaborated similar interface for both bridge compatibility with EVM chains as well as flexibility to define different token types with different behavior in a single contract. +- NEAR [NEP-245](https://github.com/near/NEPs/blob/master/neps/nep-0245.md) has elaborated similar interface for both bridge compatibility with EVM chains as well as flexibility to define different token types with different behavior in a single contract. [DevGovGigs Board](https://near.social/#/mob.near/widget/MainPage.Post.Page?accountId=devgovgigs.near&blockHeight=87938945) recently also shows growing interest to move NEP-245 adoption forward. - [NEP-454](https://github.com/near/NEPs/pull/454) proposes royalties support for multi token contracts. We propose that the SBT Standard will support the multi-token idea from the get go. This won't increase the complexity of the contract (in a traditional case, where one contract will only issue tokens of the single kind, the `kind` argument is simply ignored in the state, and in the functions it's required to be of a constant value, eg `1`) but will unify the interface. @@ -398,6 +398,7 @@ CALL FOR ACTION: Shall the events be issued by the registry or by the issuing co - Should we use NEP-171 Mint and NEP-171 Burn (instead of revoke) events? If the events will be emitted by registry, then we need new events to include the contract address. Since the native token ID is different in SBT and NFT, we should probably keep SBT events version - Also confirm that only the registry emits the events. - Decide if we need to keep memo in the events. +- Should we keep the proposed multi-token approach? ## Copyright From a4a6e5ca5e039e941699dc7f78c9fdc715e89816 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Mon, 27 Mar 2023 12:53:42 +0200 Subject: [PATCH 20/89] fix flow --- specs/Standards/Sould Bound Token/README.md | 36 ++++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/specs/Standards/Sould Bound Token/README.md b/specs/Standards/Sould Bound Token/README.md index 01353406d..26a843d45 100644 --- a/specs/Standards/Sould Bound Token/README.md +++ b/specs/Standards/Sould Bound Token/README.md @@ -347,17 +347,45 @@ trait SBT { sequenceDiagram actor Alice actor Issuer1 as SBT_1 Issuer + participant SBT1 as SBT_1 Contract participant SBT_Registry - Issuer1->>SBT_1_Contract: sbt_mint(alice, 1, metadata) + Issuer1->>SBT1: sbt_mint(alice, 1, metadata) + activate SBT1 SBT1-)SBT_Registry: sbt_mint(alice, 1, metadata) + SBT1->>SBT1: emit Mint(SBT_1_Contract, alice, [238]) SBT_Registry-)SBT1: token_id: 238 + deactivate SBT1 + + Note over Alice,SBT_Registry: now Alice can query registry to check her SBT + + Alice-->>SBT_Registry: sbt(SBT_1_Contract, 238) + SBT_Registry-->>Alice: {token_id: 238, owner_id: alice, metadata} + + Note over Alice,SBT_Registry: Alice partiicpates in other community and applies for another SBT.
However she will use a differnt wallet: alice2 + + participant SBT2 as SBT_2 Contract + actor Issuer2 as SBT_2 Issuer + + Issuer2->>SBT2: sbt_mint(alice2, 1, metadata) + activate SBT2 + SBT2-)SBT_Registry: sbt_mint(alice2, 1, metadata) + SBT2->>SBT2: emit Mint(SBT_2_Contract, alice2, [7991]) + SBT_Registry-)SBT2: token_id: 7991 + deactivate SBT2 + + Alice-->>SBT_Registry: sbt(SBT_2_Contract, 7991) + SBT_Registry-->>Alice: {token_id: 7991, owner_id: alice2, metadata} + + Note over Alice,SBT_Registry: After a while Alice decides to merge her `alice2` account into `alice2` + Alice->>SBT_Registry: sbt_soul_transfer(alice) --accountId alice2 - Note over Issuer1,SBT1,SBT_Registry: now Alice can query registry to check her SBT + Alice-->>+SBT_Registry: sbt_tokens_by_owner(alice2) + SBT_Registry-->>-Alice: {} - Alice-->SBT_Registry: sbt(SBT_1_Contract, 238) - SBT_Registry-->Alice: {token_id: 238, owner_id: alice, metadata} + Alice-->>+SBT_Registry: sbt_tokens_by_owner(alice1) + SBT_Registry-->>-Alice: {SBT_1_Contract: [238], SBT_2_Contract: [7991]} ``` ## Consequences From 99e9f99e01144987337cda333c0415429aaaaa27 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Mon, 27 Mar 2023 13:16:01 +0200 Subject: [PATCH 21/89] registry mint authorization and update query functions --- specs/Standards/Sould Bound Token/README.md | 22 +++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/specs/Standards/Sould Bound Token/README.md b/specs/Standards/Sould Bound Token/README.md index 26a843d45..a2c5972ad 100644 --- a/specs/Standards/Sould Bound Token/README.md +++ b/specs/Standards/Sould Bound Token/README.md @@ -96,6 +96,10 @@ SBT registry can define it's own mechanism to atomically recover all tokens rela SBT Registry based recovery is not part of this specification. +#### Minting authorization + +A registry can limit which contracts can mint SBTs by implementing a custom issuer registration methods. Example: a simple access control list managed by a DAO. + ### Smart contract interface For the Token ID type we propose `u64` rather than `U128`. `u64` capacity is more than 1e19. If we will mint 10'000 SBTs per second, than it will take us 58'494'241 years to get into the capacity. @@ -147,22 +151,23 @@ trait SBTRegistry { * QUERIES **********/ - /// get the information about specific token ID + /// get the information about specific token ID issued by `ctr` SBT contract. fn sbt(&self, ctr: AccountId, token_id: TokenId) -> Option; - /// returns total amount of tokens minted by this contract - fn sbt_total_supply(&self, ctr: AccountId) -> u64; + /// returns total amount of tokens issued by `ctr` SBT contract. + fn sbt_supply(&self, ctr: AccountId) -> u64; /// returns total amount of tokens of given kind minted by this contract - fn sbt_total_supply_by_kind(&self, ctr: AccountId, kind: KindId) -> u64; + fn sbt_supply_by_kind(&self, ctr: AccountId, kind: KindId) -> u64; - /// returns total supply of SBTs for a given owner + /// returns total supply of SBTs for a given owner. fn sbt_supply_by_owner(&self, ctr: AccountId, account: AccountId) -> u64; /// returns true if the `account` has a token of a given `kind`. fn sbt_supply_by_kind(&self, ctr: AccountId, account: AccountId, kind: KindId) -> bool; - /// Query sbt tokens. If `from_index` is not specified, then `from_index` should be assumed + /// Query sbt tokens issued by a given contract. + /// If `from_index` is not specified, then `from_index` should be assumed /// to be the first valid token id. fn sbt_tokens( &self, @@ -174,13 +179,14 @@ trait SBTRegistry { /// Query sbt tokens by owner /// If `from_kind` is not specified, then `from_kind` should be assumed to be the first /// valid kind id. + /// Returns list of pairs: `(Contract address, list of token IDs)`. fn sbt_tokens_by_owner( &self, - ctr: AccountId, account: AccountId, + ctr: Option, from_kind: Option, limit: Option, - ) -> Vec; + ) -> Vec<(AccountId, Vec)>; /************* * Transactions From 1e9a6b32e7c691ea63c0550e422c84591edba159 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Mon, 27 Mar 2023 13:27:12 +0200 Subject: [PATCH 22/89] change sbt_mint to support minting many tokens at once --- specs/Standards/Sould Bound Token/README.md | 34 +++++++++++++-------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/specs/Standards/Sould Bound Token/README.md b/specs/Standards/Sould Bound Token/README.md index a2c5972ad..a73261d02 100644 --- a/specs/Standards/Sould Bound Token/README.md +++ b/specs/Standards/Sould Bound Token/README.md @@ -193,19 +193,17 @@ trait SBTRegistry { *************/ /// Creates a new, unique token and assigns it to the `receiver`. + /// `token_spec` is a vector of pairs: owner AccountId and TokenMetadata. + /// each TokenMetadata must have non zero kind. /// Must be called by an SBT contract. /// Must emit `Mint` event. /// Must provide enough NEAR to cover registry storage cost. - /// The arguments to this function can vary, depending on the use-case. - /// `kind` is provided as an explicit argument and it must overwrite `metadata.kind`. /// Requires attaching enough tokens to cover the storage growth. // #[payable] fn sbt_mint( &mut self, - account: AccountId, - kind: Option, - metadata: TokenMetadata, - ) -> TokenId; + token_spec: Vec<(AccountId, TokenMetadata)>, + ) -> Vec; /// sbt_recover reassigns all tokens from the old owner to a new owner, /// and registers `old_owner` to a burned addresses registry. @@ -323,14 +321,26 @@ Although the transaction functions below are not part of the SBT smart contract These functions should emit appropriate events and relay calls to a SBT registry. ```rust - trait SBT { + /// the function should overwrite `metadata.kind = kind`. // #[payable] fn sbt_mint( &mut self, account: AccountId, - kind: Option, + kind: u64, metadata: TokenMetadata, + ) -> TokenId; + + /// Creates a new, unique token and assigns it to the `receiver`. + /// `token_spec` is a vector of pairs: owner AccountId and TokenMetadata. + /// Must be called by an SBT contract. + /// Must emit `Mint` event. + /// Must provide enough NEAR to cover registry storage cost. + /// Requires attaching enough tokens to cover the storage growth. + // #[payable] + fn sbt_mint_multi( + &mut self, + token_spec: Vec<(AccountId, TokenMetadata)>, ) -> Vec; // #[payable] @@ -359,7 +369,7 @@ sequenceDiagram Issuer1->>SBT1: sbt_mint(alice, 1, metadata) activate SBT1 - SBT1-)SBT_Registry: sbt_mint(alice, 1, metadata) + SBT1-)SBT_Registry: sbt_mint([[alice, metadata]) SBT1->>SBT1: emit Mint(SBT_1_Contract, alice, [238]) SBT_Registry-)SBT1: token_id: 238 deactivate SBT1 @@ -376,7 +386,7 @@ sequenceDiagram Issuer2->>SBT2: sbt_mint(alice2, 1, metadata) activate SBT2 - SBT2-)SBT_Registry: sbt_mint(alice2, 1, metadata) + SBT2-)SBT_Registry: sbt_mint([[alice2, metadata]]) SBT2->>SBT2: emit Mint(SBT_2_Contract, alice2, [7991]) SBT_Registry-)SBT2: token_id: 7991 deactivate SBT2 @@ -388,10 +398,10 @@ sequenceDiagram Alice->>SBT_Registry: sbt_soul_transfer(alice) --accountId alice2 Alice-->>+SBT_Registry: sbt_tokens_by_owner(alice2) - SBT_Registry-->>-Alice: {} + SBT_Registry-->>-Alice: [] Alice-->>+SBT_Registry: sbt_tokens_by_owner(alice1) - SBT_Registry-->>-Alice: {SBT_1_Contract: [238], SBT_2_Contract: [7991]} + SBT_Registry-->>-Alice: [[SBT_1_Contract, [238]], [SBT_2_Contract, [7991]]]} ``` ## Consequences From 31d3ea8ae2ccb17bbdaf973136362a45a67eef4a Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Mon, 27 Mar 2023 13:28:11 +0200 Subject: [PATCH 23/89] fix tags --- specs/Standards/Sould Bound Token/README.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/specs/Standards/Sould Bound Token/README.md b/specs/Standards/Sould Bound Token/README.md index a73261d02..96b2e8a87 100644 --- a/specs/Standards/Sould Bound Token/README.md +++ b/specs/Standards/Sould Bound Token/README.md @@ -1,7 +1,4 @@ -# NEP: Soulbound Token - --- - NEP: 393 Title: Soulbound Token Authors: Robert Zaremba <@robert-zaremba> @@ -10,10 +7,11 @@ Status: Draft Type: Standards Track Category: Contract Created: 12-Sep-2022 -Requires: -- - +Requires: --- +# NEP: Soulbound Token + ## Summary Soulbound Token (SBT) is a form of a NFT which represents an aspect of an account: _soul_. Transferability is limited only to a case of recoverability or transferring the whole _soul_ - a `sbt_soul_transfer` should coordinate with a registry to transfer all SBTs from one account to another, and _killing_ the source account. @@ -194,7 +192,7 @@ trait SBTRegistry { /// Creates a new, unique token and assigns it to the `receiver`. /// `token_spec` is a vector of pairs: owner AccountId and TokenMetadata. - /// each TokenMetadata must have non zero kind. + /// each TokenMetadata must have non zero `kind`. /// Must be called by an SBT contract. /// Must emit `Mint` event. /// Must provide enough NEAR to cover registry storage cost. From 08b0e2aa95a5bf921a0cd102cb89f712e0098b72 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Mon, 27 Mar 2023 15:26:47 +0200 Subject: [PATCH 24/89] use token instead of token_id, update diagram --- specs/Standards/Sould Bound Token/README.md | 32 ++++++++++----------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/specs/Standards/Sould Bound Token/README.md b/specs/Standards/Sould Bound Token/README.md index 96b2e8a87..6d7450c58 100644 --- a/specs/Standards/Sould Bound Token/README.md +++ b/specs/Standards/Sould Bound Token/README.md @@ -109,8 +109,8 @@ pub type TokenId = u64; pub type KindId = u64; pub struct Token { - pub token_id: TokenId, - pub owner_id: AccountId, + pub token: TokenId, + pub owner: AccountId, pub metadata: TokenMetadata, } ``` @@ -150,7 +150,7 @@ trait SBTRegistry { **********/ /// get the information about specific token ID issued by `ctr` SBT contract. - fn sbt(&self, ctr: AccountId, token_id: TokenId) -> Option; + fn sbt(&self, ctr: AccountId, token: TokenId) -> Option; /// returns total amount of tokens issued by `ctr` SBT contract. fn sbt_supply(&self, ctr: AccountId) -> u64; @@ -222,8 +222,8 @@ trait SBTRegistry { /// Revokes SBT, could potentailly burn it or update the expire time. /// Must be called by an SBT contract. /// Must emit `Revoke` event. - /// Returns true if a token_id is a valid, active SBT. Otherwise returns false. - fn sbt_revoke(&mut self, token_id: u64) -> bool; + /// Returns true if a token is a valid, active SBT. Otherwise returns false. + fn sbt_revoke(&mut self, token: u64) -> bool; /// Transfers atomically all SBT tokens from one account to another account. /// Must be an SBT holder. @@ -346,7 +346,7 @@ trait SBT { fn sbt_renew(&mut self, tokens: Vec, expires_at: u64, memo: Option); - fn sbt_revoke(token_id: u64) -> bool; + fn sbt_revoke(token: u64) -> bool; } ``` @@ -368,29 +368,29 @@ sequenceDiagram Issuer1->>SBT1: sbt_mint(alice, 1, metadata) activate SBT1 SBT1-)SBT_Registry: sbt_mint([[alice, metadata]) - SBT1->>SBT1: emit Mint(SBT_1_Contract, alice, [238]) - SBT_Registry-)SBT1: token_id: 238 + SBT_Registry->>SBT_Registry: emit Mint(SBT_1_Contract, alice, [238]) + SBT_Registry-)SBT1: [238] deactivate SBT1 Note over Alice,SBT_Registry: now Alice can query registry to check her SBT Alice-->>SBT_Registry: sbt(SBT_1_Contract, 238) - SBT_Registry-->>Alice: {token_id: 238, owner_id: alice, metadata} + SBT_Registry-->>Alice: {token: 238, owner: alice, metadata} - Note over Alice,SBT_Registry: Alice partiicpates in other community and applies for another SBT.
However she will use a differnt wallet: alice2 + Note over Alice,SBT_Registry: Alice participates in other community and applies for another SBT.
She will get 2 SBTs. However she uses a differnt wallet: alice2. participant SBT2 as SBT_2 Contract actor Issuer2 as SBT_2 Issuer - Issuer2->>SBT2: sbt_mint(alice2, 1, metadata) + Issuer2->>SBT2: sbt_mint_multi([[alice2, metadata2], [alice2, metadata3]]) activate SBT2 - SBT2-)SBT_Registry: sbt_mint([[alice2, metadata]]) - SBT2->>SBT2: emit Mint(SBT_2_Contract, alice2, [7991]) - SBT_Registry-)SBT2: token_id: 7991 + SBT2-)SBT_Registry: sbt_mint([[alice2, metadata2], [alice2, metadata3]]) + SBT_Registry->>SBT_Registry: emit Mint(SBT_2_Contract, alice2, [7991, 17992]) + SBT_Registry-)SBT2: [7991, 1992] deactivate SBT2 Alice-->>SBT_Registry: sbt(SBT_2_Contract, 7991) - SBT_Registry-->>Alice: {token_id: 7991, owner_id: alice2, metadata} + SBT_Registry-->>Alice: {token: 7991, owner: alice2, metadata: metadata2} Note over Alice,SBT_Registry: After a while Alice decides to merge her `alice2` account into `alice2` Alice->>SBT_Registry: sbt_soul_transfer(alice) --accountId alice2 @@ -399,7 +399,7 @@ sequenceDiagram SBT_Registry-->>-Alice: [] Alice-->>+SBT_Registry: sbt_tokens_by_owner(alice1) - SBT_Registry-->>-Alice: [[SBT_1_Contract, [238]], [SBT_2_Contract, [7991]]]} + SBT_Registry-->>-Alice: [[SBT_1_Contract, [238]], [SBT_2_Contract, [7991, 7992]]]} ``` ## Consequences From 9f1266f80fd2e0c0a36e80f2eed37fb9cd4eb099 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Tue, 28 Mar 2023 16:53:29 +0200 Subject: [PATCH 25/89] review --- specs/Standards/Sould Bound Token/README.md | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/specs/Standards/Sould Bound Token/README.md b/specs/Standards/Sould Bound Token/README.md index 6d7450c58..afeb72ad8 100644 --- a/specs/Standards/Sould Bound Token/README.md +++ b/specs/Standards/Sould Bound Token/README.md @@ -14,15 +14,15 @@ Requires: ## Summary -Soulbound Token (SBT) is a form of a NFT which represents an aspect of an account: _soul_. Transferability is limited only to a case of recoverability or transferring the whole _soul_ - a `sbt_soul_transfer` should coordinate with a registry to transfer all SBTs from one account to another, and _killing_ the source account. +Soulbound Token (SBT) is a form of a NFT which represents an aspect of an account: _soul_. Transferability is limited only to a case of recoverability or transferring the soul - a _soul transfer_ should coordinate with a registry to transfer all SBTs from one account to another, and _killing_ the source account. SBTs are well suited for carrying proof-of-attendance, proof-of-unique-human "stamps" and other similar credibility-carriers. ## Motivation -Recent [Decentralized Society](https://www.bankless.com/decentralized-society-desoc-explained) trends open a new area of Web3 research to model various aspects of what characterizes humans. Economic and governance value is generated by humans and their relationship. +Recent [Decentralized Society](https://www.bankless.com/decentralized-society-desoc-explained) trends open a new area of Web3 research to model various aspects of what characterizes humans. Economic and governance value is generated by humans and their relationship. SBTs can represent the commitments, credentials, and affiliations of “Souls” that encode the trust networks of the real economy to establish provenance and reputation. -> Decentralized Society, or DeSoc, would lean on SBTs to allow “Souls and communities to come together bottom-up, as emergent properties of each other to co-create plural network goods and intelligences, at a range of scales.” +> More importantly, SBTs enable other applications of increasing ambition, such as community wallet recovery, sybil-resistant governance, mechanisms for decentralization, and novel markets with decomposable, shared rights. We call this richer, pluralistic ecosystem “Decentralized Society” (DeSoc)—a co-determined sociality, where Souls and communities come together bottom-up, as emergent properties of each other to co-create plural network goods and intelligences, at a range of scales. Creating a strong primitives is necessary to model new innovative systems and decentralized societies. Examples include reputation protocols, undercollaterized lending, one-person-one-vote, fair airdrops & ICOs, universal basic income, non KYC identity systems, Human DAOs and methods for Sybil attack resistance. @@ -32,16 +32,18 @@ _Verifiable Credentials_ (VC) could be seen as subset of SBT. However there is a ## Specification -Main requirement for Soulbound tokens is to make it bound to a human. Moving tokens from one account to another should be strictly limited to case of **recoverability** (eg in case a user's private key is compromised due to extortion, loss, etc) or user account merge (**soul transfer**). This becomes especially important for proof-of-human stamps that can only be issued once per user. +Main requirement for Soulbound tokens is to bound an account to a human. A **Soul** is an account with SBTs, which are used to define account identity. Moving SBTs from one account to another should be strictly limited to: -Two safeguards against misuse of recovery are contemplated. +- **recoverability** in case a user's private key is compromised due to extortion, loss, etc; +- **soul transfer** - when a user needs to merge his accounts merge (e.g. he started with few different account but later decides to merge them to increase an account reputation). -1. Users cannot recover an SBT by themselves. The issuer, a DAO or a smart contract (eg: multisig) dedicated to manage the recovery should be assigned. -2. Whenever a _soul transfer_ is triggered then the SBT registry emits `Kill` event. It creates an inherit cost for such action: the account identity is burned and can't receive any SBT in the future. +This becomes especially important for proof-of-human stamps that can only be issued once per user. Few safeguards against misuse of recovery are contemplated: + +1. Users cannot recover an SBT by themselves. The issuer, a DAO or a smart contract (e.g. multisig) dedicated to manage the recovery should be assigned. +2. Whenever a soul transfer is triggered then the SBT registry emits `SoulTransfer` and `Kill` events. The latter signals that the account can't host and receive any SBT in the future. It creates an inherit cost for such action: the account identity is burned. 3. The recovery function is additional economical risk preventing account trading: user should always be able to recover his SBT, and move to another, not killed account. -SBT recover MUST not trigger `SoulTransfer` nor `Kill`: malicious issuer could compromise the system by faking the token recovery and take over all other SBTs from a user. -Only the owner of the account can call `soul_transfer` and merge 2 accounts he owns. +SBT recover MUST not trigger `SoulTransfer` nor `Kill`: malicious issuer could compromise the system by faking the token recovery and take over all other SBTs from a user. Only the owner of the account can call `soul_transfer` and merge 2 accounts he owns. Soulbound tokens can have an _expire date_. This is useful for tokens which are related to real world certificates with expire time, or social mechanisms (eg, community membership). Such tokens SHOULD have an option to be renewable. Examples include mandatory renewal with a frequency to check that the owner is still alive, or renew membership to a DAO that uses SBTs as membership gating. @@ -227,6 +229,7 @@ trait SBTRegistry { /// Transfers atomically all SBT tokens from one account to another account. /// Must be an SBT holder. + /// Must emit `Revoke` event. // #[payable] fn sbt_soul_transfer(&mut self, to: AccountId) -> bool; } From a8f4f491fc4512cfb632e2d86eb90cc651664f32 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Tue, 28 Mar 2023 18:33:51 +0200 Subject: [PATCH 26/89] add more info about Kill event and updated Considerations --- specs/Standards/Sould Bound Token/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/specs/Standards/Sould Bound Token/README.md b/specs/Standards/Sould Bound Token/README.md index afeb72ad8..d15347cad 100644 --- a/specs/Standards/Sould Bound Token/README.md +++ b/specs/Standards/Sould Bound Token/README.md @@ -49,6 +49,8 @@ Soulbound tokens can have an _expire date_. This is useful for tokens which are An issuer can provide a _sbt revocation_ in his contract (eg, when a related certificate or membership should be revoked). When doing so, the SBT registry MUST be updated and `Revoke` event must be emitted. It's up to the registry to define revocation handling (either by burning a token, or changing expire date of it's metadata). +A registry can emit a `Kill` event without doing soul transfer. Handling it depends on the registry governance or registry use cases. One example is to use social governance to identify fake accounts (like bots) - in that case the registry should allow to emit `Kill` and block a scam soul and block future transfers. + ### Token Kind SBT tokens can't be fractionized. Also, by definition there should be only one of a token per token kind per user. Examples: user should not be able to receive few badges of the same kind, or few proof of attendance to the same event. @@ -231,7 +233,7 @@ trait SBTRegistry { /// Must be an SBT holder. /// Must emit `Revoke` event. // #[payable] - fn sbt_soul_transfer(&mut self, to: AccountId) -> bool; + fn osbt_soul_transfer(&mut self, to: AccountId) -> bool; } ``` @@ -437,12 +439,10 @@ There are so many examples where NFT standards are poorly or improperly implemen CALL FOR ACTION: Shall the events be issued by the registry or by the issuing contract? - registry: we query registry, so it makes sense to use registry. If we emit the event by the registry, we need to add smart contract argument as a field of the events. -- issuing contract: all actions, except soul transfer, are initiated through the issuing contract. So maybe only `SoulTransfer` event should be emitted by a registry. - maybe we should reduce amount of tokens: merge {mint, renew}? -- maybe we can remove Kill event -- currently it's implicit by the SoulTransfer event... but maybe future use cases will require it? -- Should we use NEP-171 Mint and NEP-171 Burn (instead of revoke) events? If the events will be emitted by registry, then we need new events to include the contract address. Since the native token ID is different in SBT and NFT, we should probably keep SBT events version -- Also confirm that only the registry emits the events. -- Decide if we need to keep memo in the events. +- Should we use NEP-171 Mint and NEP-171 Burn (instead of revoke) events? If the events will be emitted by registry, then we need new events to include the contract address. Since the native token ID is different in SBT and NFT, we should probably keep SBT events version. Also, with registry, it makes less sense to issue NEP-171 Mint and Burn. +- Confirm that only the registry emits the events. +- Decide if we need to keep memo in the events (it's already in the transaction). - Should we keep the proposed multi-token approach? ## Copyright From 8acb1924e22e32d74dddfedeed8d617a387490c0 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Wed, 29 Mar 2023 00:04:47 +0200 Subject: [PATCH 27/89] add Burn event --- specs/Standards/Sould Bound Token/README.md | 48 ++++++++++++--------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/specs/Standards/Sould Bound Token/README.md b/specs/Standards/Sould Bound Token/README.md index d15347cad..91b28255f 100644 --- a/specs/Standards/Sould Bound Token/README.md +++ b/specs/Standards/Sould Bound Token/README.md @@ -79,7 +79,7 @@ An SBT smart contract, SHOULD opt-in to a registry using `opt_in` function. One - many registries: it MUST relay all state change functions to all registries. - or to no registry: it MUST issue the SBT state change emits by itself. -If an SBT smart contract doesn't opt-in to any registry, then we should think about it as a single token registry, and it MUST strictly implement all SBT Registry query functions by itself: the contract address must be part of the arguments, and it must check that it equals to the deployed account address (`require!(ctr == env::current_account_id())`). +If a SBT smart contract doesn't opt-in to any registry, then we should think about it as a single token registry, and it MUST strictly implement all SBT Registry query functions by itself: the contract address must be part of the arguments, and it must check that it equals to the deployed account address (`require!(ctr == env::current_account_id())`). Moreover, a registry will provide an efficient way to query multiple tokens for a single user. This will allow implementation of use cases such us: @@ -197,10 +197,9 @@ trait SBTRegistry { /// Creates a new, unique token and assigns it to the `receiver`. /// `token_spec` is a vector of pairs: owner AccountId and TokenMetadata. /// each TokenMetadata must have non zero `kind`. - /// Must be called by an SBT contract. + /// Must be called by a SBT contract. /// Must emit `Mint` event. /// Must provide enough NEAR to cover registry storage cost. - /// Requires attaching enough tokens to cover the storage growth. // #[payable] fn sbt_mint( &mut self, @@ -209,7 +208,7 @@ trait SBTRegistry { /// sbt_recover reassigns all tokens from the old owner to a new owner, /// and registers `old_owner` to a burned addresses registry. - /// Must be called by an SBT contract. + /// Must be called by a SBT contract. /// Must emit `Recover` event. /// Must be called by an operator. /// Must provide enough NEAR to cover registry storage cost. @@ -219,18 +218,18 @@ trait SBTRegistry { /// sbt_renew will update the expire time of provided tokens. /// `expires_at` is a unix timestamp (in seconds). - /// Must be called by an SBT contract. + /// Must be called by a SBT contract. /// Must emit `Renew` event. fn sbt_renew(&mut self, tokens: Vec, expires_at: u64, memo: Option); /// Revokes SBT, could potentailly burn it or update the expire time. - /// Must be called by an SBT contract. - /// Must emit `Revoke` event. + /// Must be called by a SBT contract. + /// Must emit one of `Revoke` or `Burn` event. /// Returns true if a token is a valid, active SBT. Otherwise returns false. fn sbt_revoke(&mut self, token: u64) -> bool; /// Transfers atomically all SBT tokens from one account to another account. - /// Must be an SBT holder. + /// Must be a SBT holder. /// Must emit `Revoke` event. // #[payable] fn osbt_soul_transfer(&mut self, to: AccountId) -> bool; @@ -253,8 +252,8 @@ trait SBTNFT { type SbtEventKind { standard: "nep393"; version: "1.0.0"; - event: "mint" | "recover" | "renew" | "revoke" | "kill"; - data: Mint | Recover | Renew | Revoke | SoulTransfer | Kill; + event: "mint" | "recover" | "renew" | "revoke" | "burn" | "soul_transfer" | "kill"; + data: Mint | Recover | Renew | Revoke | Burn | SoulTransfer | Kill; } /// An event minted by the Registry when new SBT is created. @@ -269,7 +268,7 @@ type Mint { /// An event emitted when a recovery process succeeded to reassign SBT, usually due to account /// access loss. This action is usually requested by the owner, but executed by an issuer, /// and doesn't trigger Soul Transfer. -/// Must be emitted by an SBT registry. +/// Must be emitted by a SBT registry. type Recover { ctr: AccountId // SBT Contract recovering the tokens old_owner: AccountId; // current holder of the SBT @@ -279,25 +278,34 @@ type Recover { } /// An event emitted when a existing tokens are renewed. -/// Must be emitted by an SBT registry. +/// Must be emitted by a SBT registry. type Renew { ctr: AccountId // SBT Contract renewing the tokens tokens: []u64; // list of token ids. memo?: string; // optional message } -/// An event emitted when a existing tokens are revoked. +/// An event emitted when an existing tokens are revoked. /// Revoked tokens should not be listed in a wallet. -/// Must be emitted by an SBT registry. +/// Must be emitted by a SBT registry. type Revoke { ctr: AccountId // SBT Contract revoking the tokens tokens: []u64; // list of token ids. memo?: string; // optional message } +/// An event emitted when an existing tokens are burned. +/// Must be emitted by a SBT registry. +type Burn { + ctr: AccountId // SBT Contract revoking the tokens + tokens: []u64; // list of token ids. + memo?: string; // optional message +} + + /// An event emitted when soul transfer is happening: all SBTs owned by `from` are transferred /// to `to`, and the `from` account is killed (can't receive any new SBT). -/// Must be emitted by an SBT registry. +/// Must be emitted by a SBT registry. /// Registry MUST emit `Kill` whenever the soul transfer happens. type SoulTransfer { from: AccountId; @@ -306,17 +314,17 @@ type SoulTransfer { } /// An event emitted when the `account` is killed within the emitting registry. -/// Must be emitted by an SBT registry. +/// Must be emitted by a SBT registry. /// Registry must add the `account` to a blocklist and prohibit issuing SBTs to this account /// in the future -/// Must be emitted by an SBT registry. +/// Must be emitted by a SBT registry. type Kill { account: AccountId; memo?: string; // optional message } ``` -Whenever a recovery is made in a way that an existing SBT is burned, the `NftBurn` event must be emitted. +Whenever a recovery is made in a way that an existing SBT is burned, the `Burn` event MUST be emitted. If `Revoke` burns token then `Burn` event MUST be emitted instead of `Revoke`. ### Recommended functions @@ -326,6 +334,7 @@ These functions should emit appropriate events and relay calls to a SBT registry ```rust trait SBT { /// the function should overwrite `metadata.kind = kind`. + /// Must provide enough NEAR to cover registry storage cost. // #[payable] fn sbt_mint( &mut self, @@ -336,10 +345,7 @@ trait SBT { /// Creates a new, unique token and assigns it to the `receiver`. /// `token_spec` is a vector of pairs: owner AccountId and TokenMetadata. - /// Must be called by an SBT contract. - /// Must emit `Mint` event. /// Must provide enough NEAR to cover registry storage cost. - /// Requires attaching enough tokens to cover the storage growth. // #[payable] fn sbt_mint_multi( &mut self, From 893f7a68f2327d87d896907959d1ff42c8549a78 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Thu, 30 Mar 2023 11:20:50 +0200 Subject: [PATCH 28/89] typos --- specs/Standards/Sould Bound Token/README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/specs/Standards/Sould Bound Token/README.md b/specs/Standards/Sould Bound Token/README.md index 91b28255f..a5a7d3228 100644 --- a/specs/Standards/Sould Bound Token/README.md +++ b/specs/Standards/Sould Bound Token/README.md @@ -14,7 +14,7 @@ Requires: ## Summary -Soulbound Token (SBT) is a form of a NFT which represents an aspect of an account: _soul_. Transferability is limited only to a case of recoverability or transferring the soul - a _soul transfer_ should coordinate with a registry to transfer all SBTs from one account to another, and _killing_ the source account. +Soulbound Token (SBT) is a form of a NFT which represents an aspect of an account: _soul_. Transferability is limited only to a case of recover-ability or transferring the soul - a _soul transfer_ should coordinate with a registry to transfer all SBTs from one account to another, and _killing_ the source account. SBTs are well suited for carrying proof-of-attendance, proof-of-unique-human "stamps" and other similar credibility-carriers. @@ -22,9 +22,9 @@ SBTs are well suited for carrying proof-of-attendance, proof-of-unique-human "st Recent [Decentralized Society](https://www.bankless.com/decentralized-society-desoc-explained) trends open a new area of Web3 research to model various aspects of what characterizes humans. Economic and governance value is generated by humans and their relationship. SBTs can represent the commitments, credentials, and affiliations of “Souls” that encode the trust networks of the real economy to establish provenance and reputation. -> More importantly, SBTs enable other applications of increasing ambition, such as community wallet recovery, sybil-resistant governance, mechanisms for decentralization, and novel markets with decomposable, shared rights. We call this richer, pluralistic ecosystem “Decentralized Society” (DeSoc)—a co-determined sociality, where Souls and communities come together bottom-up, as emergent properties of each other to co-create plural network goods and intelligences, at a range of scales. +> More importantly, SBTs enable other applications of increasing ambition, such as community wallet recovery, Sybil-resistant governance, mechanisms for decentralization, and novel markets with decomposable, shared rights. We call this richer, pluralistic ecosystem “Decentralized Society” (DeSoc)—a co-determined sociality, where Souls and communities come together bottom-up, as emergent properties of each other to co-create plural network goods and intelligences, at a range of scales. -Creating a strong primitives is necessary to model new innovative systems and decentralized societies. Examples include reputation protocols, undercollaterized lending, one-person-one-vote, fair airdrops & ICOs, universal basic income, non KYC identity systems, Human DAOs and methods for Sybil attack resistance. +Creating a strong primitives is necessary to model new innovative systems and decentralized societies. Examples include reputation protocols, undercollateralized lending, one-person-one-vote, fair airdrops & ICOs, universal basic income, non KYC identity systems, Human DAOs and methods for Sybil attack resistance. We propose a SBT standard to model protocols described above. @@ -34,7 +34,7 @@ _Verifiable Credentials_ (VC) could be seen as subset of SBT. However there is a Main requirement for Soulbound tokens is to bound an account to a human. A **Soul** is an account with SBTs, which are used to define account identity. Moving SBTs from one account to another should be strictly limited to: -- **recoverability** in case a user's private key is compromised due to extortion, loss, etc; +- **recover-ability** in case a user's private key is compromised due to extortion, loss, etc; - **soul transfer** - when a user needs to merge his accounts merge (e.g. he started with few different account but later decides to merge them to increase an account reputation). This becomes especially important for proof-of-human stamps that can only be issued once per user. Few safeguards against misuse of recovery are contemplated: @@ -45,7 +45,7 @@ This becomes especially important for proof-of-human stamps that can only be iss SBT recover MUST not trigger `SoulTransfer` nor `Kill`: malicious issuer could compromise the system by faking the token recovery and take over all other SBTs from a user. Only the owner of the account can call `soul_transfer` and merge 2 accounts he owns. -Soulbound tokens can have an _expire date_. This is useful for tokens which are related to real world certificates with expire time, or social mechanisms (eg, community membership). Such tokens SHOULD have an option to be renewable. Examples include mandatory renewal with a frequency to check that the owner is still alive, or renew membership to a DAO that uses SBTs as membership gating. +Soulbound tokens can have an _expire date_. This is useful for tokens which are related to real world certificates with expire time, or social mechanisms (e.g. community membership). Such tokens SHOULD have an option to be renewable. Examples include mandatory renewal with a frequency to check that the owner is still alive, or renew membership to a DAO that uses SBTs as membership gating. An issuer can provide a _sbt revocation_ in his contract (eg, when a related certificate or membership should be revoked). When doing so, the SBT registry MUST be updated and `Revoke` event must be emitted. It's up to the registry to define revocation handling (either by burning a token, or changing expire date of it's metadata). @@ -222,7 +222,7 @@ trait SBTRegistry { /// Must emit `Renew` event. fn sbt_renew(&mut self, tokens: Vec, expires_at: u64, memo: Option); - /// Revokes SBT, could potentailly burn it or update the expire time. + /// Revokes SBT, could potentially burn it or update the expire time. /// Must be called by a SBT contract. /// Must emit one of `Revoke` or `Burn` event. /// Returns true if a token is a valid, active SBT. Otherwise returns false. @@ -232,7 +232,7 @@ trait SBTRegistry { /// Must be a SBT holder. /// Must emit `Revoke` event. // #[payable] - fn osbt_soul_transfer(&mut self, to: AccountId) -> bool; + fn sbt_soul_transfer(&mut self, to: AccountId) -> bool; } ``` @@ -388,7 +388,7 @@ sequenceDiagram Alice-->>SBT_Registry: sbt(SBT_1_Contract, 238) SBT_Registry-->>Alice: {token: 238, owner: alice, metadata} - Note over Alice,SBT_Registry: Alice participates in other community and applies for another SBT.
She will get 2 SBTs. However she uses a differnt wallet: alice2. + Note over Alice,SBT_Registry: Alice participates in other community and applies for another SBT.
She will get 2 SBTs. However she uses a different wallet: alice2. participant SBT2 as SBT_2 Contract actor Issuer2 as SBT_2 Issuer @@ -409,7 +409,7 @@ sequenceDiagram Alice-->>+SBT_Registry: sbt_tokens_by_owner(alice2) SBT_Registry-->>-Alice: [] - Alice-->>+SBT_Registry: sbt_tokens_by_owner(alice1) + Alice-->>+SBT_Registry: sbt_tokens_by_owner(alice) SBT_Registry-->>-Alice: [[SBT_1_Contract, [238]], [SBT_2_Contract, [7991, 7992]]]} ``` @@ -421,7 +421,7 @@ sequenceDiagram - Ability to create SBT aggregators. - Ability to use SBT as a primitive to model non KYC identity, badges, certificates etc... - SBT can be further used for "lego" protocols, like: Proof of Humanity (discussed for NDC Governance), undercollateralized lending, role based authentication systems, innovative economic and social applications... -- Standarized recoverability mechanism. +- Standard recover-ability mechanism. - SBT are considered as a basic primitive for Decentralized Societies. - new way to implement Sybil attack resistance. From 2d65c82c743ecf45d148673abaa3aaf1f3b9c647 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Fri, 31 Mar 2023 19:54:11 +0200 Subject: [PATCH 29/89] typos --- specs/Standards/Sould Bound Token/README.md | 42 ++++++++++----------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/specs/Standards/Sould Bound Token/README.md b/specs/Standards/Sould Bound Token/README.md index a5a7d3228..da790ca84 100644 --- a/specs/Standards/Sould Bound Token/README.md +++ b/specs/Standards/Sould Bound Token/README.md @@ -14,7 +14,7 @@ Requires: ## Summary -Soulbound Token (SBT) is a form of a NFT which represents an aspect of an account: _soul_. Transferability is limited only to a case of recover-ability or transferring the soul - a _soul transfer_ should coordinate with a registry to transfer all SBTs from one account to another, and _killing_ the source account. +Soulbound Token (SBT) is a form of a NFT which represents an aspect of an account: _soul_. Transferability is limited only to a case of recoverability or transferring the soul - a _soul transfer_ should coordinate with a registry to transfer all SBTs from one account to another, and _killing_ the source account. SBTs are well suited for carrying proof-of-attendance, proof-of-unique-human "stamps" and other similar credibility-carriers. @@ -26,7 +26,7 @@ Recent [Decentralized Society](https://www.bankless.com/decentralized-society-de Creating a strong primitives is necessary to model new innovative systems and decentralized societies. Examples include reputation protocols, undercollateralized lending, one-person-one-vote, fair airdrops & ICOs, universal basic income, non KYC identity systems, Human DAOs and methods for Sybil attack resistance. -We propose a SBT standard to model protocols described above. +We propose an SBT standard to model protocols described above. _Verifiable Credentials_ (VC) could be seen as subset of SBT. However there is an important distinction: VC require set of claims and privacy protocols. It would make more sense to model VC with relation to W3 DID standard. SBT is different, it doesn't require a [resolver](https://www.w3.org/TR/did-core/#dfn-did-resolvers) nor [method](https://www.w3.org/TR/did-core/#dfn-did-methods) registry. For SBT, we need something more elastic than VC. @@ -34,7 +34,7 @@ _Verifiable Credentials_ (VC) could be seen as subset of SBT. However there is a Main requirement for Soulbound tokens is to bound an account to a human. A **Soul** is an account with SBTs, which are used to define account identity. Moving SBTs from one account to another should be strictly limited to: -- **recover-ability** in case a user's private key is compromised due to extortion, loss, etc; +- **recoverability** in case a user's private key is compromised due to extortion, loss, etc; - **soul transfer** - when a user needs to merge his accounts merge (e.g. he started with few different account but later decides to merge them to increase an account reputation). This becomes especially important for proof-of-human stamps that can only be issued once per user. Few safeguards against misuse of recovery are contemplated: @@ -79,7 +79,7 @@ An SBT smart contract, SHOULD opt-in to a registry using `opt_in` function. One - many registries: it MUST relay all state change functions to all registries. - or to no registry: it MUST issue the SBT state change emits by itself. -If a SBT smart contract doesn't opt-in to any registry, then we should think about it as a single token registry, and it MUST strictly implement all SBT Registry query functions by itself: the contract address must be part of the arguments, and it must check that it equals to the deployed account address (`require!(ctr == env::current_account_id())`). +If an SBT smart contract doesn't opt-in to any registry, then we should think about it as a single token registry, and it MUST strictly implement all SBT Registry query functions by itself: the contract address must be part of the arguments, and it must check that it equals to the deployed account address (`require!(ctr == env::current_account_id())`). Moreover, a registry will provide an efficient way to query multiple tokens for a single user. This will allow implementation of use cases such us: @@ -87,7 +87,7 @@ Moreover, a registry will provide an efficient way to query multiple tokens for - SBT classes; - decentralized societies. -#### Recovery within a SBT Registry +#### Recovery within an SBT Registry SBT registry can define it's own mechanism to atomically recover all tokens related to one account and execute soul transfer to another account, without going one by one through each SBT issuer (sometimes that might be even not possible). Example mechanisms a Registry can implement to recovery mechanism: @@ -178,7 +178,7 @@ trait SBTRegistry { limit: Option, ) -> Vec; - /// Query sbt tokens by owner + /// Query SBT tokens by owner /// If `from_kind` is not specified, then `from_kind` should be assumed to be the first /// valid kind id. /// Returns list of pairs: `(Contract address, list of token IDs)`. @@ -196,8 +196,8 @@ trait SBTRegistry { /// Creates a new, unique token and assigns it to the `receiver`. /// `token_spec` is a vector of pairs: owner AccountId and TokenMetadata. - /// each TokenMetadata must have non zero `kind`. - /// Must be called by a SBT contract. + /// Each TokenMetadata must have non zero `kind`. + /// Must be called by an SBT contract. /// Must emit `Mint` event. /// Must provide enough NEAR to cover registry storage cost. // #[payable] @@ -208,7 +208,7 @@ trait SBTRegistry { /// sbt_recover reassigns all tokens from the old owner to a new owner, /// and registers `old_owner` to a burned addresses registry. - /// Must be called by a SBT contract. + /// Must be called by an SBT contract. /// Must emit `Recover` event. /// Must be called by an operator. /// Must provide enough NEAR to cover registry storage cost. @@ -218,18 +218,18 @@ trait SBTRegistry { /// sbt_renew will update the expire time of provided tokens. /// `expires_at` is a unix timestamp (in seconds). - /// Must be called by a SBT contract. + /// Must be called by an SBT contract. /// Must emit `Renew` event. fn sbt_renew(&mut self, tokens: Vec, expires_at: u64, memo: Option); /// Revokes SBT, could potentially burn it or update the expire time. - /// Must be called by a SBT contract. + /// Must be called by an SBT contract. /// Must emit one of `Revoke` or `Burn` event. /// Returns true if a token is a valid, active SBT. Otherwise returns false. fn sbt_revoke(&mut self, token: u64) -> bool; /// Transfers atomically all SBT tokens from one account to another account. - /// Must be a SBT holder. + /// The caller must be an SBT holder and the `to` must not be a banned account. /// Must emit `Revoke` event. // #[payable] fn sbt_soul_transfer(&mut self, to: AccountId) -> bool; @@ -268,7 +268,7 @@ type Mint { /// An event emitted when a recovery process succeeded to reassign SBT, usually due to account /// access loss. This action is usually requested by the owner, but executed by an issuer, /// and doesn't trigger Soul Transfer. -/// Must be emitted by a SBT registry. +/// Must be emitted by an SBT registry. type Recover { ctr: AccountId // SBT Contract recovering the tokens old_owner: AccountId; // current holder of the SBT @@ -278,7 +278,7 @@ type Recover { } /// An event emitted when a existing tokens are renewed. -/// Must be emitted by a SBT registry. +/// Must be emitted by an SBT registry. type Renew { ctr: AccountId // SBT Contract renewing the tokens tokens: []u64; // list of token ids. @@ -287,7 +287,7 @@ type Renew { /// An event emitted when an existing tokens are revoked. /// Revoked tokens should not be listed in a wallet. -/// Must be emitted by a SBT registry. +/// Must be emitted by an SBT registry. type Revoke { ctr: AccountId // SBT Contract revoking the tokens tokens: []u64; // list of token ids. @@ -295,7 +295,7 @@ type Revoke { } /// An event emitted when an existing tokens are burned. -/// Must be emitted by a SBT registry. +/// Must be emitted by an SBT registry. type Burn { ctr: AccountId // SBT Contract revoking the tokens tokens: []u64; // list of token ids. @@ -305,7 +305,7 @@ type Burn { /// An event emitted when soul transfer is happening: all SBTs owned by `from` are transferred /// to `to`, and the `from` account is killed (can't receive any new SBT). -/// Must be emitted by a SBT registry. +/// Must be emitted by an SBT registry. /// Registry MUST emit `Kill` whenever the soul transfer happens. type SoulTransfer { from: AccountId; @@ -314,10 +314,10 @@ type SoulTransfer { } /// An event emitted when the `account` is killed within the emitting registry. -/// Must be emitted by a SBT registry. +/// Must be emitted by an SBT registry. /// Registry must add the `account` to a blocklist and prohibit issuing SBTs to this account /// in the future -/// Must be emitted by a SBT registry. +/// Must be emitted by an SBT registry. type Kill { account: AccountId; memo?: string; // optional message @@ -329,7 +329,7 @@ Whenever a recovery is made in a way that an existing SBT is burned, the `Burn` ### Recommended functions Although the transaction functions below are not part of the SBT smart contract standard (depending on a use case, they may have different parameters), we recommend them as a part of implementation and we also provide them in the reference implementation. -These functions should emit appropriate events and relay calls to a SBT registry. +These functions should emit appropriate events and relay calls to an SBT registry. ```rust trait SBT { @@ -421,7 +421,7 @@ sequenceDiagram - Ability to create SBT aggregators. - Ability to use SBT as a primitive to model non KYC identity, badges, certificates etc... - SBT can be further used for "lego" protocols, like: Proof of Humanity (discussed for NDC Governance), undercollateralized lending, role based authentication systems, innovative economic and social applications... -- Standard recover-ability mechanism. +- Standard recoverability mechanism. - SBT are considered as a basic primitive for Decentralized Societies. - new way to implement Sybil attack resistance. From 389a4bc2eb6e37e15b31c6e59c0a55bd2a16c30f Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Fri, 31 Mar 2023 19:55:54 +0200 Subject: [PATCH 30/89] rename event: Kill -> Ban --- specs/Standards/Sould Bound Token/README.md | 24 ++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/specs/Standards/Sould Bound Token/README.md b/specs/Standards/Sould Bound Token/README.md index da790ca84..133c4b858 100644 --- a/specs/Standards/Sould Bound Token/README.md +++ b/specs/Standards/Sould Bound Token/README.md @@ -14,7 +14,7 @@ Requires: ## Summary -Soulbound Token (SBT) is a form of a NFT which represents an aspect of an account: _soul_. Transferability is limited only to a case of recoverability or transferring the soul - a _soul transfer_ should coordinate with a registry to transfer all SBTs from one account to another, and _killing_ the source account. +Soulbound Token (SBT) is a form of a NFT which represents an aspect of an account: _soul_. Transferability is limited only to a case of recoverability or transferring the soul - a _soul transfer_ should coordinate with a registry to transfer all SBTs from one account to another, and _banning_ the source account. SBTs are well suited for carrying proof-of-attendance, proof-of-unique-human "stamps" and other similar credibility-carriers. @@ -40,16 +40,16 @@ Main requirement for Soulbound tokens is to bound an account to a human. A **Sou This becomes especially important for proof-of-human stamps that can only be issued once per user. Few safeguards against misuse of recovery are contemplated: 1. Users cannot recover an SBT by themselves. The issuer, a DAO or a smart contract (e.g. multisig) dedicated to manage the recovery should be assigned. -2. Whenever a soul transfer is triggered then the SBT registry emits `SoulTransfer` and `Kill` events. The latter signals that the account can't host and receive any SBT in the future. It creates an inherit cost for such action: the account identity is burned. -3. The recovery function is additional economical risk preventing account trading: user should always be able to recover his SBT, and move to another, not killed account. +2. Whenever a soul transfer is triggered then the SBT registry emits `SoulTransfer` and `Ban` events. The latter signals that the account can't host and receive any SBT in the future. It creates an inherit cost for such action: the account identity is burned. +3. The recovery function is additional economical risk preventing account trading: user should always be able to recover his SBT, and move to another, not banned account. -SBT recover MUST not trigger `SoulTransfer` nor `Kill`: malicious issuer could compromise the system by faking the token recovery and take over all other SBTs from a user. Only the owner of the account can call `soul_transfer` and merge 2 accounts he owns. +SBT recover MUST not trigger `SoulTransfer` nor `Ban`: malicious issuer could compromise the system by faking the token recovery and take over all other SBTs from a user. Only the owner of the account can call `soul_transfer` and merge 2 accounts he owns. Soulbound tokens can have an _expire date_. This is useful for tokens which are related to real world certificates with expire time, or social mechanisms (e.g. community membership). Such tokens SHOULD have an option to be renewable. Examples include mandatory renewal with a frequency to check that the owner is still alive, or renew membership to a DAO that uses SBTs as membership gating. An issuer can provide a _sbt revocation_ in his contract (eg, when a related certificate or membership should be revoked). When doing so, the SBT registry MUST be updated and `Revoke` event must be emitted. It's up to the registry to define revocation handling (either by burning a token, or changing expire date of it's metadata). -A registry can emit a `Kill` event without doing soul transfer. Handling it depends on the registry governance or registry use cases. One example is to use social governance to identify fake accounts (like bots) - in that case the registry should allow to emit `Kill` and block a scam soul and block future transfers. +A registry can emit a `Ban` event without doing soul transfer. Handling it depends on the registry governance or registry use cases. One example is to use social governance to identify fake accounts (like bots) - in that case the registry should allow to emit `Ban` and block a scam soul and block future transfers. ### Token Kind @@ -72,7 +72,7 @@ Finally, we require that each token ID is unique within the smart contract. This ### SBT Registry -Atomicity of _soul transfer_ in current NEAR runtime is not possible if the token balance is kept separately for each SBT smart contract. We need an additional contract: the `SBT Registry`, to provide atomic transfer of all user tokens and efficient way to block accounts in relation to a Kill event. The registry will provide a balance book for all associated SBT tokens. +Atomicity of _soul transfer_ in current NEAR runtime is not possible if the token balance is kept separately for each SBT smart contract. We need an additional contract: the `SBT Registry`, to provide atomic transfer of all user tokens and efficient way to block accounts in relation to a Ban event. The registry will provide a balance book for all associated SBT tokens. An SBT smart contract, SHOULD opt-in to a registry using `opt_in` function. One SBT smart contract can opt-in to: @@ -252,8 +252,8 @@ trait SBTNFT { type SbtEventKind { standard: "nep393"; version: "1.0.0"; - event: "mint" | "recover" | "renew" | "revoke" | "burn" | "soul_transfer" | "kill"; - data: Mint | Recover | Renew | Revoke | Burn | SoulTransfer | Kill; + event: "mint" | "recover" | "renew" | "revoke" | "burn" | "soul_transfer" | "ban"; + data: Mint | Recover | Renew | Revoke | Burn | SoulTransfer | Ban; } /// An event minted by the Registry when new SBT is created. @@ -304,21 +304,21 @@ type Burn { /// An event emitted when soul transfer is happening: all SBTs owned by `from` are transferred -/// to `to`, and the `from` account is killed (can't receive any new SBT). +/// to `to`, and the `from` account is banned (can't receive any new SBT). /// Must be emitted by an SBT registry. -/// Registry MUST emit `Kill` whenever the soul transfer happens. +/// Registry MUST emit `Ban` whenever the soul transfer happens. type SoulTransfer { from: AccountId; to: AccountId; memo?: string; // optional message } -/// An event emitted when the `account` is killed within the emitting registry. +/// An event emitted when the `account` is banned within the emitting registry. /// Must be emitted by an SBT registry. /// Registry must add the `account` to a blocklist and prohibit issuing SBTs to this account /// in the future /// Must be emitted by an SBT registry. -type Kill { +type Ban { account: AccountId; memo?: string; // optional message } From ad558dc8feb2b16009f729f611ba37d425c549ae Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Fri, 31 Mar 2023 19:59:34 +0200 Subject: [PATCH 31/89] make kind optional in sbt_supply_by_owner query --- specs/Standards/Sould Bound Token/README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/specs/Standards/Sould Bound Token/README.md b/specs/Standards/Sould Bound Token/README.md index 133c4b858..ddab2d661 100644 --- a/specs/Standards/Sould Bound Token/README.md +++ b/specs/Standards/Sould Bound Token/README.md @@ -163,10 +163,8 @@ trait SBTRegistry { fn sbt_supply_by_kind(&self, ctr: AccountId, kind: KindId) -> u64; /// returns total supply of SBTs for a given owner. - fn sbt_supply_by_owner(&self, ctr: AccountId, account: AccountId) -> u64; - - /// returns true if the `account` has a token of a given `kind`. - fn sbt_supply_by_kind(&self, ctr: AccountId, account: AccountId, kind: KindId) -> bool; + /// If kind is specified, returns only owner supply of the given kind -- must be 0 or 1. + fn sbt_supply_by_owner(&self, ctr: AccountId, account: AccountId, kind: Option) -> u64; /// Query sbt tokens issued by a given contract. /// If `from_index` is not specified, then `from_index` should be assumed From dbc20d84d498c8f996bb069f3d3da04d29bdd597 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Sun, 2 Apr 2023 13:23:43 +0200 Subject: [PATCH 32/89] review --- specs/Standards/Sould Bound Token/README.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/specs/Standards/Sould Bound Token/README.md b/specs/Standards/Sould Bound Token/README.md index ddab2d661..dd1f4ba0c 100644 --- a/specs/Standards/Sould Bound Token/README.md +++ b/specs/Standards/Sould Bound Token/README.md @@ -104,8 +104,8 @@ A registry can limit which contracts can mint SBTs by implementing a custom issu ### Smart contract interface -For the Token ID type we propose `u64` rather than `U128`. `u64` capacity is more than 1e19. If we will mint 10'000 SBTs per second, than it will take us 58'494'241 years to get into the capacity. -Today, the JS integer limit is `2^53-1 ~ 9e15`. It will take us 28561 years to fill that when minting 10'000 SBTs per second. So, we don't need to u128 nor a String type. However, if for some reason, we will need to get u64 support for JS, then we can always add another set of methods which will return String, so making it compatible with NFT standard (which is using `U128`, which is a string). +For the Token ID type we propose `u64` rather than `U128`. `u64` capacity is more than 1e19. If we will mint 10'000 SBTs per second, then it will take us 58'494'241 years to fill the capacity. +Today, the JS integer limit is `2^53-1 ~ 9e15`. Similarly, when minting 10'000 SBTs per second, it will take us 28'561 years to reach the limit. So, we don't need u128 nor the String type. However, if for some reason, we will need to get u64 support for JS, then we can always add another set of methods which will return String, so making it compatible with NFT standard (which is using `U128`, which is a string). Also, it's worth to note, that in 28'000 years JS (if it will still exists) will be completely different. ```rust // TokenId and Kind Id must be positive (0 is not a valid id) @@ -425,13 +425,12 @@ sequenceDiagram ### Neutral -- The API partially follows the NEP-171 (NFT) standard. The proposed design is to have native SBT API and also support NFT based queries. - NOTE: we can decide to use `nft_` prefix whenever possible. +- The API partially follows the NEP-171 (NFT) standard. The proposed design is to have native SBT API and make it possible for issuer contracts to support NFT based queries if needed (such contract will have a limitation of only issuing SBTs with one `KindID` only). ### Negative -- new set of events to be handled by the indexer and wallets. -- complexity of integration with a registry: all SBT related transactions must go through Registry. +- New set of events to be handled by the indexer and wallets. +- Complexity of integration with a registry: all SBT related transactions must go through Registry. ## Considerations From 9a28f96f95d28bc2d38d67f40ffad3f6c29d438d Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Sun, 2 Apr 2023 13:27:49 +0200 Subject: [PATCH 33/89] rename KindId to ClassId --- specs/Standards/Sould Bound Token/README.md | 50 ++++++++++----------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/specs/Standards/Sould Bound Token/README.md b/specs/Standards/Sould Bound Token/README.md index dd1f4ba0c..c745c29f1 100644 --- a/specs/Standards/Sould Bound Token/README.md +++ b/specs/Standards/Sould Bound Token/README.md @@ -51,13 +51,13 @@ An issuer can provide a _sbt revocation_ in his contract (eg, when a related cer A registry can emit a `Ban` event without doing soul transfer. Handling it depends on the registry governance or registry use cases. One example is to use social governance to identify fake accounts (like bots) - in that case the registry should allow to emit `Ban` and block a scam soul and block future transfers. -### Token Kind +### Token Class -SBT tokens can't be fractionized. Also, by definition there should be only one of a token per token kind per user. Examples: user should not be able to receive few badges of the same kind, or few proof of attendance to the same event. -However we identify a need for having few token kinds in a single contract: +SBT tokens can't be fractionized. Also, by definition there should be only one of a token per token class per user. Examples: user should not be able to receive few badges of the same class, or few proof of attendance to the same event. +However we identify a need for having few token classes in a single contract: -- badges: one contract with multiple badge kind (community lead, OG...); -- certificates: one issuer can create certificates of a different kind (eg school department can create diplomas for each major and each graduation year). +- badges: one contract with multiple badge class (community lead, OG...); +- certificates: one issuer can create certificates of a different class (eg school department can create diplomas for each major and each graduation year). We also see a trend in the NFT community and demand for market places to support multi token contracts. @@ -65,10 +65,10 @@ We also see a trend in the NFT community and demand for market places to support - NEAR [NEP-245](https://github.com/near/NEPs/blob/master/neps/nep-0245.md) has elaborated similar interface for both bridge compatibility with EVM chains as well as flexibility to define different token types with different behavior in a single contract. [DevGovGigs Board](https://near.social/#/mob.near/widget/MainPage.Post.Page?accountId=devgovgigs.near&blockHeight=87938945) recently also shows growing interest to move NEP-245 adoption forward. - [NEP-454](https://github.com/near/NEPs/pull/454) proposes royalties support for multi token contracts. -We propose that the SBT Standard will support the multi-token idea from the get go. This won't increase the complexity of the contract (in a traditional case, where one contract will only issue tokens of the single kind, the `kind` argument is simply ignored in the state, and in the functions it's required to be of a constant value, eg `1`) but will unify the interface. -It's up to the smart contract design how the token kinds is managed. A smart contract can expose an admin function (example: `sbt_new_kind() -> KindId`) or hard code the pre-registered kinds. +We propose that the SBT Standard will support the multi-token idea from the get go. This won't increase the complexity of the contract (in a traditional case, where one contract will only issue tokens of the single class, the `class` argument is simply ignored in the state, and in the functions it's required to be of a constant value, eg `1`) but will unify the interface. +It's up to the smart contract design how the token classes is managed. A smart contract can expose an admin function (example: `sbt_new_class() -> ClassId`) or hard code the pre-registered classes. -Finally, we require that each token ID is unique within the smart contract. This will allow us to query token only by token ID, without knowing it's kind. +Finally, we require that each token ID is unique within the smart contract. This will allow us to query token only by token ID, without knowing it's class. ### SBT Registry @@ -108,9 +108,9 @@ For the Token ID type we propose `u64` rather than `U128`. `u64` capacity is mor Today, the JS integer limit is `2^53-1 ~ 9e15`. Similarly, when minting 10'000 SBTs per second, it will take us 28'561 years to reach the limit. So, we don't need u128 nor the String type. However, if for some reason, we will need to get u64 support for JS, then we can always add another set of methods which will return String, so making it compatible with NFT standard (which is using `U128`, which is a string). Also, it's worth to note, that in 28'000 years JS (if it will still exists) will be completely different. ```rust -// TokenId and Kind Id must be positive (0 is not a valid id) +// TokenId and ClassId must be positive (0 is not a valid ID) pub type TokenId = u64; -pub type KindId = u64; +pub type ClassId = u64; pub struct Token { pub token: TokenId, @@ -122,7 +122,7 @@ pub struct Token { The Soulbound Token follows the NFT [NEP-171](https://github.com/near/NEPs/blob/master/neps/nep-0171.md) interface, with few differences: - token ID is `u64` (as discussed above). -- token kind is `u64`, it's required when minting and it's part of the token metadata. +- token class is `u64`, it's required when minting and it's part of the token metadata. - `TokenMetadata` doesn't have `title`, `description`, `media`, `media_hash`, `copies`, `extra`, `starts_at` nor `updated_at`. All that attributes except the `updated_at` can be part of the document stored at `reference`. `updated_at` can be tracked easily by indexers. - We don't have normal transferability, we propose to use more targeted events, to better reflect the event nature. Moreover events are emitted by the registry, so we need to include issuer contract address in the event. @@ -140,7 +140,7 @@ pub struct ContractMetadata { /// TokenMetadata defines attributes for each SBT token. pub struct TokenMetadata { - pub kind: KindId, // Kind of a token + pub class: ClassId, // token class pub issued_at: Option, // When token was issued or minted, Unix epoch in milliseconds pub expires_at: Option, // When token expires, Unix epoch in milliseconds pub reference: Option, // URL to an off-chain JSON file with more info. @@ -159,12 +159,12 @@ trait SBTRegistry { /// returns total amount of tokens issued by `ctr` SBT contract. fn sbt_supply(&self, ctr: AccountId) -> u64; - /// returns total amount of tokens of given kind minted by this contract - fn sbt_supply_by_kind(&self, ctr: AccountId, kind: KindId) -> u64; + /// returns total amount of tokens of given class minted by this contract + fn sbt_supply_by_class(&self, ctr: AccountId, class: ClassId) -> u64; /// returns total supply of SBTs for a given owner. - /// If kind is specified, returns only owner supply of the given kind -- must be 0 or 1. - fn sbt_supply_by_owner(&self, ctr: AccountId, account: AccountId, kind: Option) -> u64; + /// If class is specified, returns only owner supply of the given class -- must be 0 or 1. + fn sbt_supply_by_owner(&self, ctr: AccountId, account: AccountId, class: Option) -> u64; /// Query sbt tokens issued by a given contract. /// If `from_index` is not specified, then `from_index` should be assumed @@ -177,14 +177,14 @@ trait SBTRegistry { ) -> Vec; /// Query SBT tokens by owner - /// If `from_kind` is not specified, then `from_kind` should be assumed to be the first - /// valid kind id. + /// If `from_class` is not specified, then `from_class` should be assumed to be the first + /// valid class id. /// Returns list of pairs: `(Contract address, list of token IDs)`. fn sbt_tokens_by_owner( &self, account: AccountId, ctr: Option, - from_kind: Option, + from_class: Option, limit: Option, ) -> Vec<(AccountId, Vec)>; @@ -194,7 +194,7 @@ trait SBTRegistry { /// Creates a new, unique token and assigns it to the `receiver`. /// `token_spec` is a vector of pairs: owner AccountId and TokenMetadata. - /// Each TokenMetadata must have non zero `kind`. + /// Each TokenMetadata must have non zero `class`. /// Must be called by an SBT contract. /// Must emit `Mint` event. /// Must provide enough NEAR to cover registry storage cost. @@ -239,7 +239,7 @@ SBT smart contracts can implement NFT query interface to make it compatible with ```rust trait SBTNFT { fn nft_total_supply(&self) -> U64 - // here we index by token id instead of by kind id (as done in `sbt_tokens_by_owner`) + // here we index by token id instead of by class id (as done in `sbt_tokens_by_owner`) fn nft_tokens_for_owner(&self, account_id: AccountId, from_index: Option, limit: Option) -> Vec fn nft_supply_for_owner(&self, account_id: AccountId) -> U64 ``` @@ -247,7 +247,7 @@ trait SBTNFT { ### Events ```typescript -type SbtEventKind { +type SbtEventClass { standard: "nep393"; version: "1.0.0"; event: "mint" | "recover" | "renew" | "revoke" | "burn" | "soul_transfer" | "ban"; @@ -331,13 +331,13 @@ These functions should emit appropriate events and relay calls to an SBT registr ```rust trait SBT { - /// the function should overwrite `metadata.kind = kind`. + /// the function should overwrite `metadata.class = class`. /// Must provide enough NEAR to cover registry storage cost. // #[payable] fn sbt_mint( &mut self, account: AccountId, - kind: u64, + class: u64, metadata: TokenMetadata, ) -> TokenId; @@ -425,7 +425,7 @@ sequenceDiagram ### Neutral -- The API partially follows the NEP-171 (NFT) standard. The proposed design is to have native SBT API and make it possible for issuer contracts to support NFT based queries if needed (such contract will have a limitation of only issuing SBTs with one `KindID` only). +- The API partially follows the NEP-171 (NFT) standard. The proposed design is to have native SBT API and make it possible for issuer contracts to support NFT based queries if needed (such contract will have a limitation of only issuing SBTs with one `ClassId` only). ### Negative From 00649b036a7d970e99352e1c75df8d44c9552286 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Sun, 2 Apr 2023 20:08:02 +0200 Subject: [PATCH 34/89] formatting --- specs/Standards/Sould Bound Token/README.md | 54 ++++++++++----------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/specs/Standards/Sould Bound Token/README.md b/specs/Standards/Sould Bound Token/README.md index c745c29f1..9a808c42f 100644 --- a/specs/Standards/Sould Bound Token/README.md +++ b/specs/Standards/Sould Bound Token/README.md @@ -124,7 +124,8 @@ The Soulbound Token follows the NFT [NEP-171](https://github.com/near/NEPs/blob/ - token ID is `u64` (as discussed above). - token class is `u64`, it's required when minting and it's part of the token metadata. - `TokenMetadata` doesn't have `title`, `description`, `media`, `media_hash`, `copies`, `extra`, `starts_at` nor `updated_at`. All that attributes except the `updated_at` can be part of the document stored at `reference`. `updated_at` can be tracked easily by indexers. -- We don't have normal transferability, we propose to use more targeted events, to better reflect the event nature. Moreover events are emitted by the registry, so we need to include issuer contract address in the event. +- We don't have normal transferability. +- We propose to use more targeted events, to better reflect the event nature. Moreover events are emitted by the registry, so we need to include issuer contract address in the event. ```rust /// ContractMetadata defines contract wide attributes, which describes the whole contract. @@ -164,7 +165,12 @@ trait SBTRegistry { /// returns total supply of SBTs for a given owner. /// If class is specified, returns only owner supply of the given class -- must be 0 or 1. - fn sbt_supply_by_owner(&self, ctr: AccountId, account: AccountId, class: Option) -> u64; + fn sbt_supply_by_owner( + &self, + ctr: AccountId, + account: AccountId, + class: Option, + ) -> u64; /// Query sbt tokens issued by a given contract. /// If `from_index` is not specified, then `from_index` should be assumed @@ -199,10 +205,7 @@ trait SBTRegistry { /// Must emit `Mint` event. /// Must provide enough NEAR to cover registry storage cost. // #[payable] - fn sbt_mint( - &mut self, - token_spec: Vec<(AccountId, TokenMetadata)>, - ) -> Vec; + fn sbt_mint(&mut self, token_spec: Vec<(AccountId, TokenMetadata)>) -> Vec; /// sbt_recover reassigns all tokens from the old owner to a new owner, /// and registers `old_owner` to a burned addresses registry. @@ -250,19 +253,18 @@ trait SBTNFT { type SbtEventClass { standard: "nep393"; version: "1.0.0"; - event: "mint" | "recover" | "renew" | "revoke" | "burn" | "soul_transfer" | "ban"; - data: Mint | Recover | Renew | Revoke | Burn | SoulTransfer | Ban; + event: "mint" | "recover" | "renew" | "revoke" | "burn" | "ban" | "soul_transfer" ; + data: Mint[] | Recover[] | Renew | Revoke | Burn | Ban[] | SoulTransfer[]; } /// An event minted by the Registry when new SBT is created. type Mint { - ctr: AccountId // SBT Contract recovering the tokens - owner: AccountId, // holder of the newly minted SBT - tokens: []u64, // newly minted token ids - memo?: string, // optional message + ctr: AccountId; // SBT Contract recovering the tokens + owner: AccountId; // holder of the newly minted SBT + tokens: []u64; // newly minted token ids + memo?: string; // optional message } - /// An event emitted when a recovery process succeeded to reassign SBT, usually due to account /// access loss. This action is usually requested by the owner, but executed by an issuer, /// and doesn't trigger Soul Transfer. @@ -278,7 +280,7 @@ type Recover { /// An event emitted when a existing tokens are renewed. /// Must be emitted by an SBT registry. type Renew { - ctr: AccountId // SBT Contract renewing the tokens + ctr: AccountId; // SBT Contract renewing the tokens tokens: []u64; // list of token ids. memo?: string; // optional message } @@ -287,7 +289,7 @@ type Renew { /// Revoked tokens should not be listed in a wallet. /// Must be emitted by an SBT registry. type Revoke { - ctr: AccountId // SBT Contract revoking the tokens + ctr: AccountId; // SBT Contract revoking the tokens tokens: []u64; // list of token ids. memo?: string; // optional message } @@ -295,31 +297,29 @@ type Revoke { /// An event emitted when an existing tokens are burned. /// Must be emitted by an SBT registry. type Burn { - ctr: AccountId // SBT Contract revoking the tokens + ctr: AccountId; // SBT Contract burning the tokens tokens: []u64; // list of token ids. memo?: string; // optional message } +/// An event emitted when the `account` is banned within the emitting registry. +/// Registry must add the `account` to a list of accounts that are not allowed to get any SBT +/// in the future. +/// Must be emitted by an SBT registry. +type Ban { + account: AccountId; + memo?: string; // optional message +} /// An event emitted when soul transfer is happening: all SBTs owned by `from` are transferred /// to `to`, and the `from` account is banned (can't receive any new SBT). /// Must be emitted by an SBT registry. -/// Registry MUST emit `Ban` whenever the soul transfer happens. +/// Registry MUST also emit `Ban` whenever the soul transfer happens. type SoulTransfer { from: AccountId; to: AccountId; memo?: string; // optional message } - -/// An event emitted when the `account` is banned within the emitting registry. -/// Must be emitted by an SBT registry. -/// Registry must add the `account` to a blocklist and prohibit issuing SBTs to this account -/// in the future -/// Must be emitted by an SBT registry. -type Ban { - account: AccountId; - memo?: string; // optional message -} ``` Whenever a recovery is made in a way that an existing SBT is burned, the `Burn` event MUST be emitted. If `Revoke` burns token then `Burn` event MUST be emitted instead of `Revoke`. From 22138142b0b161b1259a9d3b294828528a191f25 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Sun, 2 Apr 2023 23:28:11 +0200 Subject: [PATCH 35/89] add missing memo type and update events --- specs/Standards/Sould Bound Token/README.md | 30 ++++++++++++--------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/specs/Standards/Sould Bound Token/README.md b/specs/Standards/Sould Bound Token/README.md index 9a808c42f..aae39317c 100644 --- a/specs/Standards/Sould Bound Token/README.md +++ b/specs/Standards/Sould Bound Token/README.md @@ -205,17 +205,16 @@ trait SBTRegistry { /// Must emit `Mint` event. /// Must provide enough NEAR to cover registry storage cost. // #[payable] - fn sbt_mint(&mut self, token_spec: Vec<(AccountId, TokenMetadata)>) -> Vec; + fn sbt_mint(&mut self, token_spec: Vec<(AccountId, TokenMetadata)>, memo: Option) -> Vec; /// sbt_recover reassigns all tokens from the old owner to a new owner, /// and registers `old_owner` to a burned addresses registry. /// Must be called by an SBT contract. /// Must emit `Recover` event. - /// Must be called by an operator. /// Must provide enough NEAR to cover registry storage cost. /// Requires attaching enough tokens to cover the storage growth. // #[payable] - fn sbt_recover(&mut self, from: AccountId, to: AccountId); + fn sbt_recover(&mut self, from: AccountId, to: AccountId, memo: Option); /// sbt_renew will update the expire time of provided tokens. /// `expires_at` is a unix timestamp (in seconds). @@ -227,13 +226,13 @@ trait SBTRegistry { /// Must be called by an SBT contract. /// Must emit one of `Revoke` or `Burn` event. /// Returns true if a token is a valid, active SBT. Otherwise returns false. - fn sbt_revoke(&mut self, token: u64) -> bool; + fn sbt_revoke(&mut self, token: u64, memo: Option) -> bool; /// Transfers atomically all SBT tokens from one account to another account. /// The caller must be an SBT holder and the `to` must not be a banned account. /// Must emit `Revoke` event. // #[payable] - fn sbt_soul_transfer(&mut self, to: AccountId) -> bool; + fn sbt_soul_transfer(&mut self, to: AccountId, memo: Option) -> bool; } ``` @@ -249,31 +248,36 @@ trait SBTNFT { ### Events +Event design principles: + +- Events don't need to repeat all function arguments - these are easy to retrieve by indexer (events are consumed by indexers anyway). +- Events must include fields necessary to identify subject matters related to use case. +- When possible, events should contain aggregated data, with respect to the standard function related to the event. + ```typescript type SbtEventClass { standard: "nep393"; version: "1.0.0"; event: "mint" | "recover" | "renew" | "revoke" | "burn" | "ban" | "soul_transfer" ; - data: Mint[] | Recover[] | Renew | Revoke | Burn | Ban[] | SoulTransfer[]; + data: Mint | Recover | Renew | Revoke | Burn | Ban[] | SoulTransfer; } /// An event minted by the Registry when new SBT is created. type Mint { ctr: AccountId; // SBT Contract recovering the tokens - owner: AccountId; // holder of the newly minted SBT - tokens: []u64; // newly minted token ids + tokens: [[AccountId, []u64]]; // list of pairs (token owner, TokenId[]) memo?: string; // optional message } /// An event emitted when a recovery process succeeded to reassign SBT, usually due to account /// access loss. This action is usually requested by the owner, but executed by an issuer, -/// and doesn't trigger Soul Transfer. +/// and doesn't trigger Soul Transfer. Registry recovers all tokens assigned to `old_owner`, +/// hence we don't need to enumerate them. /// Must be emitted by an SBT registry. type Recover { ctr: AccountId // SBT Contract recovering the tokens old_owner: AccountId; // current holder of the SBT new_owner: AccountId; // destination account. - tokens: []u64; // list of token ids. memo?: string; // optional message } @@ -339,6 +343,7 @@ trait SBT { account: AccountId, class: u64, metadata: TokenMetadata, + memo: Option, ) -> TokenId; /// Creates a new, unique token and assigns it to the `receiver`. @@ -348,14 +353,15 @@ trait SBT { fn sbt_mint_multi( &mut self, token_spec: Vec<(AccountId, TokenMetadata)>, + memo: Option, ) -> Vec; // #[payable] - fn sbt_recover(&mut self, from: AccountId, to: AccountId); + fn sbt_recover(&mut self, from: AccountId, to: AccountId, memo: Option); fn sbt_renew(&mut self, tokens: Vec, expires_at: u64, memo: Option); - fn sbt_revoke(token: u64) -> bool; + fn sbt_revoke(token: u64, memo: Option) -> bool; } ``` From 2ea6fef0c9740621c4ee54cedf2679d3997ab379 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Sun, 2 Apr 2023 23:34:14 +0200 Subject: [PATCH 36/89] remove memo from registry functions and events --- specs/Standards/Sould Bound Token/README.md | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/specs/Standards/Sould Bound Token/README.md b/specs/Standards/Sould Bound Token/README.md index aae39317c..4d3c70e0c 100644 --- a/specs/Standards/Sould Bound Token/README.md +++ b/specs/Standards/Sould Bound Token/README.md @@ -205,7 +205,7 @@ trait SBTRegistry { /// Must emit `Mint` event. /// Must provide enough NEAR to cover registry storage cost. // #[payable] - fn sbt_mint(&mut self, token_spec: Vec<(AccountId, TokenMetadata)>, memo: Option) -> Vec; + fn sbt_mint(&mut self, token_spec: Vec<(AccountId, TokenMetadata)>) -> Vec; /// sbt_recover reassigns all tokens from the old owner to a new owner, /// and registers `old_owner` to a burned addresses registry. @@ -214,25 +214,25 @@ trait SBTRegistry { /// Must provide enough NEAR to cover registry storage cost. /// Requires attaching enough tokens to cover the storage growth. // #[payable] - fn sbt_recover(&mut self, from: AccountId, to: AccountId, memo: Option); + fn sbt_recover(&mut self, from: AccountId, to: AccountId); /// sbt_renew will update the expire time of provided tokens. /// `expires_at` is a unix timestamp (in seconds). /// Must be called by an SBT contract. /// Must emit `Renew` event. - fn sbt_renew(&mut self, tokens: Vec, expires_at: u64, memo: Option); + fn sbt_renew(&mut self, tokens: Vec, expires_at: u64); /// Revokes SBT, could potentially burn it or update the expire time. /// Must be called by an SBT contract. /// Must emit one of `Revoke` or `Burn` event. /// Returns true if a token is a valid, active SBT. Otherwise returns false. - fn sbt_revoke(&mut self, token: u64, memo: Option) -> bool; + fn sbt_revoke(&mut self, token: u64) -> bool; /// Transfers atomically all SBT tokens from one account to another account. /// The caller must be an SBT holder and the `to` must not be a banned account. /// Must emit `Revoke` event. // #[payable] - fn sbt_soul_transfer(&mut self, to: AccountId, memo: Option) -> bool; + fn sbt_soul_transfer(&mut self, to: AccountId) -> bool; } ``` @@ -266,7 +266,6 @@ type SbtEventClass { type Mint { ctr: AccountId; // SBT Contract recovering the tokens tokens: [[AccountId, []u64]]; // list of pairs (token owner, TokenId[]) - memo?: string; // optional message } /// An event emitted when a recovery process succeeded to reassign SBT, usually due to account @@ -278,7 +277,6 @@ type Recover { ctr: AccountId // SBT Contract recovering the tokens old_owner: AccountId; // current holder of the SBT new_owner: AccountId; // destination account. - memo?: string; // optional message } /// An event emitted when a existing tokens are renewed. @@ -286,7 +284,6 @@ type Recover { type Renew { ctr: AccountId; // SBT Contract renewing the tokens tokens: []u64; // list of token ids. - memo?: string; // optional message } /// An event emitted when an existing tokens are revoked. @@ -295,7 +292,6 @@ type Renew { type Revoke { ctr: AccountId; // SBT Contract revoking the tokens tokens: []u64; // list of token ids. - memo?: string; // optional message } /// An event emitted when an existing tokens are burned. @@ -303,7 +299,6 @@ type Revoke { type Burn { ctr: AccountId; // SBT Contract burning the tokens tokens: []u64; // list of token ids. - memo?: string; // optional message } /// An event emitted when the `account` is banned within the emitting registry. @@ -312,7 +307,6 @@ type Burn { /// Must be emitted by an SBT registry. type Ban { account: AccountId; - memo?: string; // optional message } /// An event emitted when soul transfer is happening: all SBTs owned by `from` are transferred @@ -322,7 +316,6 @@ type Ban { type SoulTransfer { from: AccountId; to: AccountId; - memo?: string; // optional message } ``` @@ -332,6 +325,7 @@ Whenever a recovery is made in a way that an existing SBT is burned, the `Burn` Although the transaction functions below are not part of the SBT smart contract standard (depending on a use case, they may have different parameters), we recommend them as a part of implementation and we also provide them in the reference implementation. These functions should emit appropriate events and relay calls to an SBT registry. +We recommend that the all functions related to an event will take an optional `memo: Option` argument for accounting purposes. ```rust trait SBT { @@ -361,7 +355,7 @@ trait SBT { fn sbt_renew(&mut self, tokens: Vec, expires_at: u64, memo: Option); - fn sbt_revoke(token: u64, memo: Option) -> bool; + fn sbt_revoke(token: Vec, memo: Option) -> bool; } ``` From 3d809fabf9ad90f72e91014b40719050ea6351d2 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Mon, 3 Apr 2023 10:32:26 +0200 Subject: [PATCH 37/89] adding is_banned to registry trait --- specs/Standards/Sould Bound Token/README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/specs/Standards/Sould Bound Token/README.md b/specs/Standards/Sould Bound Token/README.md index 4d3c70e0c..185bcb077 100644 --- a/specs/Standards/Sould Bound Token/README.md +++ b/specs/Standards/Sould Bound Token/README.md @@ -50,6 +50,7 @@ Soulbound tokens can have an _expire date_. This is useful for tokens which are An issuer can provide a _sbt revocation_ in his contract (eg, when a related certificate or membership should be revoked). When doing so, the SBT registry MUST be updated and `Revoke` event must be emitted. It's up to the registry to define revocation handling (either by burning a token, or changing expire date of it's metadata). A registry can emit a `Ban` event without doing soul transfer. Handling it depends on the registry governance or registry use cases. One example is to use social governance to identify fake accounts (like bots) - in that case the registry should allow to emit `Ban` and block a scam soul and block future transfers. +NOTE: SBT smart contract (issuer) can have it's own list of blocked accounts or allowed only accounts. ### Token Class @@ -72,7 +73,7 @@ Finally, we require that each token ID is unique within the smart contract. This ### SBT Registry -Atomicity of _soul transfer_ in current NEAR runtime is not possible if the token balance is kept separately for each SBT smart contract. We need an additional contract: the `SBT Registry`, to provide atomic transfer of all user tokens and efficient way to block accounts in relation to a Ban event. The registry will provide a balance book for all associated SBT tokens. +Atomicity of _soul transfer_ in current NEAR runtime is not possible if the token balance is kept separately for each SBT smart contract. We need an additional contract: the `SBT Registry`, to provide atomic transfer of all user tokens and efficient way to block accounts in relation to a Ban event. The registry provides a balance book of all associated SBT tokens. An SBT smart contract, SHOULD opt-in to a registry using `opt_in` function. One SBT smart contract can opt-in to: @@ -194,6 +195,10 @@ trait SBTRegistry { limit: Option, ) -> Vec<(AccountId, Vec)>; + /// checks if an `account` was banned by the registry. + fn is_banned(&self, account: AccountId) -> bool; + + /************* * Transactions *************/ From 6c513a6c09002d0de71442385e3d5c3658ae78f3 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Mon, 3 Apr 2023 22:35:09 +0200 Subject: [PATCH 38/89] update registry motivation --- specs/Standards/Sould Bound Token/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specs/Standards/Sould Bound Token/README.md b/specs/Standards/Sould Bound Token/README.md index 185bcb077..7fef81c5f 100644 --- a/specs/Standards/Sould Bound Token/README.md +++ b/specs/Standards/Sould Bound Token/README.md @@ -73,9 +73,9 @@ Finally, we require that each token ID is unique within the smart contract. This ### SBT Registry -Atomicity of _soul transfer_ in current NEAR runtime is not possible if the token balance is kept separately for each SBT smart contract. We need an additional contract: the `SBT Registry`, to provide atomic transfer of all user tokens and efficient way to block accounts in relation to a Ban event. The registry provides a balance book of all associated SBT tokens. +Atomicity of _soul transfer_ in current NEAR runtime is not possible if the token balance is kept separately for each SBT smart contract. We need an additional contract: the `SBT Registry`, to provide atomic transfer of all user SBTs and efficient way to ban accounts in relation to the Ban event. The registry provides a balance book of all associated SBT tokens. So, each SBT is requested by a smart contract (issuer), however it's is created in the registry. -An SBT smart contract, SHOULD opt-in to a registry using `opt_in` function. One SBT smart contract can opt-in to: +We can have multiple competing registries (with different purpose or different management scheme). Registries can utilize the same SBT contracts. An SBT smart contract, SHOULD opt-in to a registry using `opt_in` function. One SBT smart contract can opt-in to: - many registries: it MUST relay all state change functions to all registries. - or to no registry: it MUST issue the SBT state change emits by itself. From 4e300e602e6db5bc3af58a7aa4bd58ff55716661 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Mon, 3 Apr 2023 22:35:51 +0200 Subject: [PATCH 39/89] move to nep-0393.md --- specs/Standards/Sould Bound Token/README.md => neps/nep-0393.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename specs/Standards/Sould Bound Token/README.md => neps/nep-0393.md (100%) diff --git a/specs/Standards/Sould Bound Token/README.md b/neps/nep-0393.md similarity index 100% rename from specs/Standards/Sould Bound Token/README.md rename to neps/nep-0393.md From e8ea5ec18764c4db527f2c9b0bfdbfc706ffcd3f Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Mon, 3 Apr 2023 22:46:35 +0200 Subject: [PATCH 40/89] fix snippet --- neps/nep-0393.md | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index 7fef81c5f..4e9276b7e 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -245,10 +245,11 @@ SBT smart contracts can implement NFT query interface to make it compatible with ```rust trait SBTNFT { - fn nft_total_supply(&self) -> U64 - // here we index by token id instead of by class id (as done in `sbt_tokens_by_owner`) - fn nft_tokens_for_owner(&self, account_id: AccountId, from_index: Option, limit: Option) -> Vec - fn nft_supply_for_owner(&self, account_id: AccountId) -> U64 + fn nft_total_supply(&self) -> U64; + // here we index by token id instead of by class id (as done in `sbt_tokens_by_owner`) + fn nft_tokens_for_owner(&self, account_id: AccountId, from_index: Option, limit: Option) -> Vec; + fn nft_supply_for_owner(&self, account_id: AccountId) -> U64; +} ``` ### Events @@ -444,8 +445,6 @@ Being fully compatible with NFT standard is a desirable. However, given the requ Give that our requirements are much striker, we need to reconsider the level of compatibility with NEP-171 NFT. There are so many examples where NFT standards are poorly or improperly implemented, adding another standard with differing functionality but equal naming in there will cause lots of misclassifications between NFTs/SBTs, and then recover methods called on NFT contracts, SBTs attempted to be listed on NFT marketplaces and so on. -CALL FOR ACTION: Shall the events be issued by the registry or by the issuing contract? - - registry: we query registry, so it makes sense to use registry. If we emit the event by the registry, we need to add smart contract argument as a field of the events. - maybe we should reduce amount of tokens: merge {mint, renew}? - Should we use NEP-171 Mint and NEP-171 Burn (instead of revoke) events? If the events will be emitted by registry, then we need new events to include the contract address. Since the native token ID is different in SBT and NFT, we should probably keep SBT events version. Also, with registry, it makes less sense to issue NEP-171 Mint and Burn. @@ -453,6 +452,22 @@ CALL FOR ACTION: Shall the events be issued by the registry or by the issuing co - Decide if we need to keep memo in the events (it's already in the transaction). - Should we keep the proposed multi-token approach? +## Changelog + +### v1.0.0 + +#### Benefits + +- Benefit +- Benefit + +#### Concerns + +| # | Concern | Resolution | Status | +| --- | ------- | ---------- | ------ | +| 1 | Concern | Resolution | Status | +| 2 | Concern | Resolution | Status | + ## Copyright [Creative Commons Attribution 4.0 International Public License (CC BY 4.0)](https://creativecommons.org/licenses/by/4.0/) From 643cb895638f4f68c53a48874045637c7d2b5ec2 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Mon, 3 Apr 2023 22:55:04 +0200 Subject: [PATCH 41/89] fix typescript event type definitions --- neps/nep-0393.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index 4e9276b7e..c69ae7fa0 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -261,7 +261,10 @@ Event design principles: - When possible, events should contain aggregated data, with respect to the standard function related to the event. ```typescript -type SbtEventClass { +// only valid integer numbers (without rounding errors). +type u64 = number; + +type Nep393Event { standard: "nep393"; version: "1.0.0"; event: "mint" | "recover" | "renew" | "revoke" | "burn" | "ban" | "soul_transfer" ; @@ -271,7 +274,7 @@ type SbtEventClass { /// An event minted by the Registry when new SBT is created. type Mint { ctr: AccountId; // SBT Contract recovering the tokens - tokens: [[AccountId, []u64]]; // list of pairs (token owner, TokenId[]) + tokens: [[AccountId, u64[]]]; // list of pairs (token owner, TokenId[]) } /// An event emitted when a recovery process succeeded to reassign SBT, usually due to account @@ -289,7 +292,7 @@ type Recover { /// Must be emitted by an SBT registry. type Renew { ctr: AccountId; // SBT Contract renewing the tokens - tokens: []u64; // list of token ids. + tokens: u64[]; // list of token ids. } /// An event emitted when an existing tokens are revoked. @@ -297,14 +300,14 @@ type Renew { /// Must be emitted by an SBT registry. type Revoke { ctr: AccountId; // SBT Contract revoking the tokens - tokens: []u64; // list of token ids. + tokens: u64[]; // list of token ids. } /// An event emitted when an existing tokens are burned. /// Must be emitted by an SBT registry. type Burn { ctr: AccountId; // SBT Contract burning the tokens - tokens: []u64; // list of token ids. + tokens: u64[]; // list of token ids. } /// An event emitted when the `account` is banned within the emitting registry. From 08bd3ac33294c3dfe55375f9b39b1e9145c8fb12 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Wed, 5 Apr 2023 19:13:32 +0200 Subject: [PATCH 42/89] update comments --- neps/nep-0393.md | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index c69ae7fa0..643dad5da 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -155,16 +155,20 @@ trait SBTRegistry { * QUERIES **********/ - /// get the information about specific token ID issued by `ctr` SBT contract. + /// Get the information about specific token ID issued by `ctr` SBT contract. fn sbt(&self, ctr: AccountId, token: TokenId) -> Option; - /// returns total amount of tokens issued by `ctr` SBT contract. + /// Returns total amount of tokens issued by `ctr` SBT contract, including expired tokens. + /// Depending on the implementation, if a revoke removes a token, it then is should not be + /// included in the supply. fn sbt_supply(&self, ctr: AccountId) -> u64; - /// returns total amount of tokens of given class minted by this contract + /// Returns total amount of tokens of given class minted by `ctr`. See `sbt_supply` for + /// information about revoked tokens. fn sbt_supply_by_class(&self, ctr: AccountId, class: ClassId) -> u64; - /// returns total supply of SBTs for a given owner. + /// Returns total supply of SBTs for a given owner. See `sbt_supply` for information about + /// revoked tokens. /// If class is specified, returns only owner supply of the given class -- must be 0 or 1. fn sbt_supply_by_owner( &self, @@ -330,10 +334,10 @@ type SoulTransfer { Whenever a recovery is made in a way that an existing SBT is burned, the `Burn` event MUST be emitted. If `Revoke` burns token then `Burn` event MUST be emitted instead of `Revoke`. -### Recommended functions +### Example SBT Contract functions -Although the transaction functions below are not part of the SBT smart contract standard (depending on a use case, they may have different parameters), we recommend them as a part of implementation and we also provide them in the reference implementation. -These functions should emit appropriate events and relay calls to an SBT registry. +Although the transaction functions below are not part of the SBT smart contract standard (depending on a use case, they may have different parameters), we present here an example interface for SBT issuance and we also provide a reference implementation. +These functions must relay calls to an SBT registry, which will emit appropriate events. We recommend that the all functions related to an event will take an optional `memo: Option` argument for accounting purposes. ```rust From c18f2b4486d807595815fca0ac34edbbdbbe60eb Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Thu, 6 Apr 2023 13:46:15 +0200 Subject: [PATCH 43/89] considerations cleanup --- neps/nep-0393.md | 35 ++++++++++++++++------------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index 643dad5da..e0f3506de 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -128,6 +128,8 @@ The Soulbound Token follows the NFT [NEP-171](https://github.com/near/NEPs/blob/ - We don't have normal transferability. - We propose to use more targeted events, to better reflect the event nature. Moreover events are emitted by the registry, so we need to include issuer contract address in the event. +All time related attributes are defined in milliseconds (as per NEP-171). + ```rust /// ContractMetadata defines contract wide attributes, which describes the whole contract. pub struct ContractMetadata { @@ -226,7 +228,7 @@ trait SBTRegistry { fn sbt_recover(&mut self, from: AccountId, to: AccountId); /// sbt_renew will update the expire time of provided tokens. - /// `expires_at` is a unix timestamp (in seconds). + /// `expires_at` is a unix timestamp (in milliseconds). /// Must be called by an SBT contract. /// Must emit `Renew` event. fn sbt_renew(&mut self, tokens: Vec, expires_at: u64); @@ -426,6 +428,11 @@ sequenceDiagram ## Consequences +Being fully compatible with NFT standard is a desirable. However, given the requirements related to _soul transfer_ we didn't find an applaudable solution. Also we decided to use u64 as a Token ID, diverging further from the NFT NEP-171 standard. + +Give that our requirements are much striker, we had to reconsider the level of compatibility with NEP-171 NFT. +There are many examples where NFT standards are poorly or improperly implemented, adding another standard with different functionality but equal naming will cause lots of problems and misclassifications between NFT and SBT. + ### Positive - Template and set of guidelines for creating SBT tokens. @@ -445,20 +452,6 @@ sequenceDiagram - New set of events to be handled by the indexer and wallets. - Complexity of integration with a registry: all SBT related transactions must go through Registry. -## Considerations - -Being fully compatible with NFT standard is a desirable. However, given the requirements related to _soul transfer_ we didn't find an applaudable solution. Also we decided to use u64 as a Token ID, diverging further from the NFT NEP-171 standard. - -Give that our requirements are much striker, we need to reconsider the level of compatibility with NEP-171 NFT. -There are so many examples where NFT standards are poorly or improperly implemented, adding another standard with differing functionality but equal naming in there will cause lots of misclassifications between NFTs/SBTs, and then recover methods called on NFT contracts, SBTs attempted to be listed on NFT marketplaces and so on. - -- registry: we query registry, so it makes sense to use registry. If we emit the event by the registry, we need to add smart contract argument as a field of the events. -- maybe we should reduce amount of tokens: merge {mint, renew}? -- Should we use NEP-171 Mint and NEP-171 Burn (instead of revoke) events? If the events will be emitted by registry, then we need new events to include the contract address. Since the native token ID is different in SBT and NFT, we should probably keep SBT events version. Also, with registry, it makes less sense to issue NEP-171 Mint and Burn. -- Confirm that only the registry emits the events. -- Decide if we need to keep memo in the events (it's already in the transaction). -- Should we keep the proposed multi-token approach? - ## Changelog ### v1.0.0 @@ -470,10 +463,14 @@ There are so many examples where NFT standards are poorly or improperly implemen #### Concerns -| # | Concern | Resolution | Status | -| --- | ------- | ---------- | ------ | -| 1 | Concern | Resolution | Status | -| 2 | Concern | Resolution | Status | +| # | Concern | Resolution | Status | +| --- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | ------ | +| 1 | Should we Emit NEP-171 Mint and NEP-171 Burn by the SBT contract (in addition to SBT native events emitted by the registry)? If the events will be emitted by registry, then we need new events to include the contract address. | - | open | +| 2 | Should we reduce amount of events? | - | open | +| 3 | Decide if we should include memo in the events (it's already in the transaction). | - | open | +| 4 | Approve the proposed multi-token approach. | - | open | +| 5 | Use of seconds vs milliseconds as a time unit. | - | open | +| x | | - | open | ## Copyright From 99943a3f5e50f9ff2f5f5c91fbbea090e8e9838b Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Thu, 6 Apr 2023 14:17:39 +0200 Subject: [PATCH 44/89] review --- neps/nep-0393.md | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index e0f3506de..fc35cce9d 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -35,15 +35,15 @@ _Verifiable Credentials_ (VC) could be seen as subset of SBT. However there is a Main requirement for Soulbound tokens is to bound an account to a human. A **Soul** is an account with SBTs, which are used to define account identity. Moving SBTs from one account to another should be strictly limited to: - **recoverability** in case a user's private key is compromised due to extortion, loss, etc; -- **soul transfer** - when a user needs to merge his accounts merge (e.g. he started with few different account but later decides to merge them to increase an account reputation). +- **soul transfer** - when a user needs to merge his accounts merge (e.g. they started with few different account but later decides to merge them to increase an account reputation). -This becomes especially important for proof-of-human stamps that can only be issued once per user. Few safeguards against misuse of recovery are contemplated: +This becomes especially important for proof-of-human stamps that can only be issued once per user. Few safeguards against misuse of SBT transfer (soul transfer or recovery) are contemplated: 1. Users cannot recover an SBT by themselves. The issuer, a DAO or a smart contract (e.g. multisig) dedicated to manage the recovery should be assigned. 2. Whenever a soul transfer is triggered then the SBT registry emits `SoulTransfer` and `Ban` events. The latter signals that the account can't host and receive any SBT in the future. It creates an inherit cost for such action: the account identity is burned. 3. The recovery function is additional economical risk preventing account trading: user should always be able to recover his SBT, and move to another, not banned account. -SBT recover MUST not trigger `SoulTransfer` nor `Ban`: malicious issuer could compromise the system by faking the token recovery and take over all other SBTs from a user. Only the owner of the account can call `soul_transfer` and merge 2 accounts he owns. +SBT recover MUST not trigger `SoulTransfer` nor `Ban`: malicious issuer could compromise the system by faking the token recovery and take over all other SBTs from a user. Only the owner of the account can call `soul_transfer` and merge 2 accounts they owns. Soulbound tokens can have an _expire date_. This is useful for tokens which are related to real world certificates with expire time, or social mechanisms (e.g. community membership). Such tokens SHOULD have an option to be renewable. Examples include mandatory renewal with a frequency to check that the owner is still alive, or renew membership to a DAO that uses SBTs as membership gating. @@ -218,8 +218,9 @@ trait SBTRegistry { // #[payable] fn sbt_mint(&mut self, token_spec: Vec<(AccountId, TokenMetadata)>) -> Vec; - /// sbt_recover reassigns all tokens from the old owner to a new owner, - /// and registers `old_owner` to a burned addresses registry. + /// sbt_recover reassigns all tokens from the old owner to a new owner that were ONLY + /// issued by the `ctr` SBT Contract, and registers `old_owner` to a list of banned + /// accounts. /// Must be called by an SBT contract. /// Must emit `Recover` event. /// Must provide enough NEAR to cover registry storage cost. @@ -279,14 +280,15 @@ type Nep393Event { /// An event minted by the Registry when new SBT is created. type Mint { - ctr: AccountId; // SBT Contract recovering the tokens + ctr: AccountId; // SBT Contract minting the tokens tokens: [[AccountId, u64[]]]; // list of pairs (token owner, TokenId[]) } -/// An event emitted when a recovery process succeeded to reassign SBT, usually due to account +/// An event emitted when a recovery process succeeded to reassign SBTs, usually due to account /// access loss. This action is usually requested by the owner, but executed by an issuer, -/// and doesn't trigger Soul Transfer. Registry recovers all tokens assigned to `old_owner`, -/// hence we don't need to enumerate them. +/// and doesn't trigger Soul Transfer. Registry reassigns all tokens assigned to `old_owner` +/// that were ONLY issued by the `ctr` SBT Contract (hence we don't need to enumerate the +/// token IDs). /// Must be emitted by an SBT registry. type Recover { ctr: AccountId // SBT Contract recovering the tokens @@ -302,14 +304,15 @@ type Renew { } /// An event emitted when an existing tokens are revoked. -/// Revoked tokens should not be listed in a wallet. +/// Revoked tokens will continue to be listed by the registry but they should not be listed in +/// a wallet. See also `Burn` event. /// Must be emitted by an SBT registry. type Revoke { ctr: AccountId; // SBT Contract revoking the tokens tokens: u64[]; // list of token ids. } -/// An event emitted when an existing tokens are burned. +/// An event emitted when an existing tokens are burned and removed from the registry. /// Must be emitted by an SBT registry. type Burn { ctr: AccountId; // SBT Contract burning the tokens From 549546ad76bdc400b43aea3e6169525f8c1fd48d Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Wed, 12 Apr 2023 23:35:16 +0200 Subject: [PATCH 45/89] update return types --- neps/nep-0393.md | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index fc35cce9d..30ab16e6a 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -174,20 +174,16 @@ trait SBTRegistry { /// If class is specified, returns only owner supply of the given class -- must be 0 or 1. fn sbt_supply_by_owner( &self, - ctr: AccountId, account: AccountId, + ctr: AccountId, class: Option, ) -> u64; /// Query sbt tokens issued by a given contract. - /// If `from_index` is not specified, then `from_index` should be assumed + /// If `from_token` is not specified, then `from_token` should be assumed /// to be the first valid token id. - fn sbt_tokens( - &self, - ctr: AccountId, - from_index: Option, - limit: Option, - ) -> Vec; + fn sbt_tokens(&self, ctr: AccountId, from_token: Option, limit: Option) + -> Vec; /// Query SBT tokens by owner /// If `from_class` is not specified, then `from_class` should be assumed to be the first @@ -199,12 +195,11 @@ trait SBTRegistry { ctr: Option, from_class: Option, limit: Option, - ) -> Vec<(AccountId, Vec)>; + ) -> Vec<(AccountId, Vec)>; /// checks if an `account` was banned by the registry. fn is_banned(&self, account: AccountId) -> bool; - /************* * Transactions *************/ @@ -216,20 +211,20 @@ trait SBTRegistry { /// Must emit `Mint` event. /// Must provide enough NEAR to cover registry storage cost. // #[payable] - fn sbt_mint(&mut self, token_spec: Vec<(AccountId, TokenMetadata)>) -> Vec; + fn sbt_mint(&mut self, token_spec: Vec<(AccountId, Vec)>) -> Vec; - /// sbt_recover reassigns all tokens from the old owner to a new owner that were ONLY - /// issued by the `ctr` SBT Contract, and registers `old_owner` to a list of banned - /// accounts. + /// sbt_recover reassigns all tokens from the old owner to a new owner, + /// and registers `old_owner` to a burned addresses registry. /// Must be called by an SBT contract. /// Must emit `Recover` event. + /// Must be called by an operator. /// Must provide enough NEAR to cover registry storage cost. /// Requires attaching enough tokens to cover the storage growth. // #[payable] fn sbt_recover(&mut self, from: AccountId, to: AccountId); /// sbt_renew will update the expire time of provided tokens. - /// `expires_at` is a unix timestamp (in milliseconds). + /// `expires_at` is a unix timestamp (in miliseconds). /// Must be called by an SBT contract. /// Must emit `Renew` event. fn sbt_renew(&mut self, tokens: Vec, expires_at: u64); From 4c4231d2a386e398f946c8785c206f31762527b0 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Wed, 12 Apr 2023 23:55:05 +0200 Subject: [PATCH 46/89] update diagram --- neps/nep-0393.md | 42 ++++++++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index 30ab16e6a..5c994a847 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -379,6 +379,10 @@ trait SBT { ## Example Flows +In the diagram below we explain a Soul Transfer use case. Alice has two accounts: `alice1` and `alice2`. She is getting tokens from 2 SBT contracts which use the same registry. Later she decides to merge the accounts by doing Soul Transfer. + +Alice uses her `alice1` account to interact with `SBT_1 Issuer` and receives an SBT with token ID = 238: + ```mermaid sequenceDiagram actor Alice @@ -387,40 +391,54 @@ sequenceDiagram participant SBT1 as SBT_1 Contract participant SBT_Registry - Issuer1->>SBT1: sbt_mint(alice, 1, metadata) + Issuer1->>SBT1: sbt_mint(alice1, class5, metadata) activate SBT1 - SBT1-)SBT_Registry: sbt_mint([[alice, metadata]) - SBT_Registry->>SBT_Registry: emit Mint(SBT_1_Contract, alice, [238]) + SBT1-)SBT_Registry: sbt_mint([[alice1, [metadata]]]) + SBT_Registry->>SBT_Registry: emit Mint(SBT_1_Contract, alice1, [238]) SBT_Registry-)SBT1: [238] deactivate SBT1 Note over Alice,SBT_Registry: now Alice can query registry to check her SBT Alice-->>SBT_Registry: sbt(SBT_1_Contract, 238) - SBT_Registry-->>Alice: {token: 238, owner: alice, metadata} + SBT_Registry-->>Alice: {token: 238, owner: alice1, metadata} +``` - Note over Alice,SBT_Registry: Alice participates in other community and applies for another SBT.
She will get 2 SBTs. However she uses a different wallet: alice2. +With `SBT_2 Issuer`, Alice uses her `alice2` account. Note that `SBT_2 Contract` has different mint function (can mint many tokens at once), and validates a proof prior to requesting the registry to mint the tokens. - participant SBT2 as SBT_2 Contract +```mermaid +sequenceDiagram + actor Alice actor Issuer2 as SBT_2 Issuer - Issuer2->>SBT2: sbt_mint_multi([[alice2, metadata2], [alice2, metadata3]]) + participant SBT2 as SBT_2 Contract + participant SBT_Registry + + Issuer2->>SBT2: sbt_mint_multi([[alice2, metadata2], [alice2, metadata3]], proof) activate SBT2 - SBT2-)SBT_Registry: sbt_mint([[alice2, metadata2], [alice2, metadata3]]) - SBT_Registry->>SBT_Registry: emit Mint(SBT_2_Contract, alice2, [7991, 17992]) + SBT2-)SBT_Registry: sbt_mint([[alice2, [metadata2, metadata3]]]) + SBT_Registry->>SBT_Registry: emit Mint(SBT_2_Contract, alice2, [7991, 7992]) SBT_Registry-)SBT2: [7991, 1992] deactivate SBT2 + Note over Alice,SBT_Registry: Alice queries one of her new tokens Alice-->>SBT_Registry: sbt(SBT_2_Contract, 7991) SBT_Registry-->>Alice: {token: 7991, owner: alice2, metadata: metadata2} +``` + +After a while Alice decides to transfer her `alice2` soul into `alice1` + +```mermaid +sequenceDiagram + actor Alice + participant SBT_Registry - Note over Alice,SBT_Registry: After a while Alice decides to merge her `alice2` account into `alice2` - Alice->>SBT_Registry: sbt_soul_transfer(alice) --accountId alice2 + Alice->>SBT_Registry: sbt_soul_transfer(alice1) --accountId alice2 Alice-->>+SBT_Registry: sbt_tokens_by_owner(alice2) SBT_Registry-->>-Alice: [] - Alice-->>+SBT_Registry: sbt_tokens_by_owner(alice) + Alice-->>+SBT_Registry: sbt_tokens_by_owner(alice1) SBT_Registry-->>-Alice: [[SBT_1_Contract, [238]], [SBT_2_Contract, [7991, 7992]]]} ``` From d2cbbdf50a3a65cc5ca24bebe1f347bb5f9b4b03 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Thu, 13 Apr 2023 15:12:01 +0200 Subject: [PATCH 47/89] review, adding notes about possible burning mechanism --- neps/nep-0393.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index 5c994a847..21f6c30aa 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -14,7 +14,7 @@ Requires: ## Summary -Soulbound Token (SBT) is a form of a NFT which represents an aspect of an account: _soul_. Transferability is limited only to a case of recoverability or transferring the soul - a _soul transfer_ should coordinate with a registry to transfer all SBTs from one account to another, and _banning_ the source account. +Soulbound Token (SBT) is a form of a NFT which represents an aspect of an account: _soul_. Transferability is limited only to a case of recoverability or a _soul transfer_. The latter, must coordinate with a registry to transfer all SBTs from one account to another, and _banning_ the source account. SBTs are well suited for carrying proof-of-attendance, proof-of-unique-human "stamps" and other similar credibility-carriers. @@ -35,7 +35,7 @@ _Verifiable Credentials_ (VC) could be seen as subset of SBT. However there is a Main requirement for Soulbound tokens is to bound an account to a human. A **Soul** is an account with SBTs, which are used to define account identity. Moving SBTs from one account to another should be strictly limited to: - **recoverability** in case a user's private key is compromised due to extortion, loss, etc; -- **soul transfer** - when a user needs to merge his accounts merge (e.g. they started with few different account but later decides to merge them to increase an account reputation). +- **soul transfer** - when a user needs to merge his accounts (e.g. they started with few different account but later decides to merge them to increase an account reputation). Soul transfer is different than a token transfer. The standard forbids a traditional token transfer functionality, where a user can transfer individual tokens. That being said, a registry can have extension functions for more advanced scenarios, which could require a governance mechanism. This becomes especially important for proof-of-human stamps that can only be issued once per user. Few safeguards against misuse of SBT transfer (soul transfer or recovery) are contemplated: @@ -43,7 +43,7 @@ This becomes especially important for proof-of-human stamps that can only be iss 2. Whenever a soul transfer is triggered then the SBT registry emits `SoulTransfer` and `Ban` events. The latter signals that the account can't host and receive any SBT in the future. It creates an inherit cost for such action: the account identity is burned. 3. The recovery function is additional economical risk preventing account trading: user should always be able to recover his SBT, and move to another, not banned account. -SBT recover MUST not trigger `SoulTransfer` nor `Ban`: malicious issuer could compromise the system by faking the token recovery and take over all other SBTs from a user. Only the owner of the account can call `soul_transfer` and merge 2 accounts they owns. +SBT recover MUST not trigger `SoulTransfer` nor `Ban`: malicious issuer could compromise the system by faking the token recovery and take over all other SBTs from a user. Only the owner of the account can call `sbt_soul_transfer` and merge 2 accounts they owns. Soulbound tokens can have an _expire date_. This is useful for tokens which are related to real world certificates with expire time, or social mechanisms (e.g. community membership). Such tokens SHOULD have an option to be renewable. Examples include mandatory renewal with a frequency to check that the owner is still alive, or renew membership to a DAO that uses SBTs as membership gating. @@ -75,14 +75,14 @@ Finally, we require that each token ID is unique within the smart contract. This Atomicity of _soul transfer_ in current NEAR runtime is not possible if the token balance is kept separately for each SBT smart contract. We need an additional contract: the `SBT Registry`, to provide atomic transfer of all user SBTs and efficient way to ban accounts in relation to the Ban event. The registry provides a balance book of all associated SBT tokens. So, each SBT is requested by a smart contract (issuer), however it's is created in the registry. -We can have multiple competing registries (with different purpose or different management scheme). Registries can utilize the same SBT contracts. An SBT smart contract, SHOULD opt-in to a registry using `opt_in` function. One SBT smart contract can opt-in to: +We can have multiple competing registries (with different purpose or different management scheme). Registries can utilize the same SBT contracts. An SBT smart contract, SHOULD opt-in to a registry. Registries may develop different opt-in mechanisms (they could differ by the approval mechanism, be fully permissioned etc..). One SBT smart contract can opt-in to: - many registries: it MUST relay all state change functions to all registries. - or to no registry: it MUST issue the SBT state change emits by itself. If an SBT smart contract doesn't opt-in to any registry, then we should think about it as a single token registry, and it MUST strictly implement all SBT Registry query functions by itself: the contract address must be part of the arguments, and it must check that it equals to the deployed account address (`require!(ctr == env::current_account_id())`). -Moreover, a registry will provide an efficient way to query multiple tokens for a single user. This will allow implementation of use cases such us: +Moreover, a registry provides an efficient way to query multiple tokens for a single user. This will allow implementation of use cases such us: - SBT based identities (main use case of the `i-am-human` protocol); - SBT classes; @@ -103,6 +103,10 @@ SBT Registry based recovery is not part of this specification. A registry can limit which contracts can mint SBTs by implementing a custom issuer registration methods. Example: a simple access control list managed by a DAO. +#### Burning tokens + +Registry MAY expose a mechanism to allow an account to burn an unwanted token. The exact mechanism is not part of the standard and it will depend on the registry implementation. We only define a standard `Burn` event, which must be emitted each time a token is removed from existence. Some registries may not allow to let accounts burning their tokens in a spirit of preserving specific claims. Ultimately, NEAR is a public blockchain, and even if a token is burned, it's trace will be preserved. + ### Smart contract interface For the Token ID type we propose `u64` rather than `U128`. `u64` capacity is more than 1e19. If we will mint 10'000 SBTs per second, then it will take us 58'494'241 years to fill the capacity. @@ -453,7 +457,7 @@ There are many examples where NFT standards are poorly or improperly implemented - Template and set of guidelines for creating SBT tokens. - Ability to create SBT aggregators. -- Ability to use SBT as a primitive to model non KYC identity, badges, certificates etc... +- An SBT standard with recoverability mechanism provides a unified model for multiple primitives such as non KYC identities, badges, certificates etc... - SBT can be further used for "lego" protocols, like: Proof of Humanity (discussed for NDC Governance), undercollateralized lending, role based authentication systems, innovative economic and social applications... - Standard recoverability mechanism. - SBT are considered as a basic primitive for Decentralized Societies. From 1762ba2ef8324562b8ba9b101f89eb971b726847 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Thu, 13 Apr 2023 15:25:02 +0200 Subject: [PATCH 48/89] adding note about PII --- neps/nep-0393.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index 21f6c30aa..63bf6bc36 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -107,6 +107,10 @@ A registry can limit which contracts can mint SBTs by implementing a custom issu Registry MAY expose a mechanism to allow an account to burn an unwanted token. The exact mechanism is not part of the standard and it will depend on the registry implementation. We only define a standard `Burn` event, which must be emitted each time a token is removed from existence. Some registries may not allow to let accounts burning their tokens in a spirit of preserving specific claims. Ultimately, NEAR is a public blockchain, and even if a token is burned, it's trace will be preserved. +#### Personal Identifiable Information + +Issuers must not include any PII into any SBT. + ### Smart contract interface For the Token ID type we propose `u64` rather than `U128`. `u64` capacity is more than 1e19. If we will mint 10'000 SBTs per second, then it will take us 58'494'241 years to fill the capacity. @@ -247,7 +251,7 @@ trait SBTRegistry { } ``` -SBT smart contracts can implement NFT query interface to make it compatible with NFT tools. Note, we use U64 type rather than U128. +SBT issuer smart contracts can implement NFT query interface to make it compatible with NFT tools. Note, we use U64 type rather than U128. However, SBT issuer must not emit NFT related events. ```rust trait SBTNFT { From 3a8ceff72f36542c7e12b49e1004fadd792273da Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Thu, 13 Apr 2023 16:10:13 +0200 Subject: [PATCH 49/89] remove sbt_soul_transfer from required interface, and document required behavior --- neps/nep-0393.md | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index 63bf6bc36..579f82734 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -43,7 +43,7 @@ This becomes especially important for proof-of-human stamps that can only be iss 2. Whenever a soul transfer is triggered then the SBT registry emits `SoulTransfer` and `Ban` events. The latter signals that the account can't host and receive any SBT in the future. It creates an inherit cost for such action: the account identity is burned. 3. The recovery function is additional economical risk preventing account trading: user should always be able to recover his SBT, and move to another, not banned account. -SBT recover MUST not trigger `SoulTransfer` nor `Ban`: malicious issuer could compromise the system by faking the token recovery and take over all other SBTs from a user. Only the owner of the account can call `sbt_soul_transfer` and merge 2 accounts they owns. +SBT recover MUST not trigger `SoulTransfer` nor `Ban`: malicious issuer could compromise the system by faking the token recovery and take over all other SBTs from a user. Only the owner of the account can make a Soul Transfer transaction and merge 2 accounts they owns. Soulbound tokens can have an _expire date_. This is useful for tokens which are related to real world certificates with expire time, or social mechanisms (e.g. community membership). Such tokens SHOULD have an option to be renewable. Examples include mandatory renewal with a frequency to check that the owner is still alive, or renew membership to a DAO that uses SBTs as membership gating. @@ -133,7 +133,7 @@ The Soulbound Token follows the NFT [NEP-171](https://github.com/near/NEPs/blob/ - token ID is `u64` (as discussed above). - token class is `u64`, it's required when minting and it's part of the token metadata. - `TokenMetadata` doesn't have `title`, `description`, `media`, `media_hash`, `copies`, `extra`, `starts_at` nor `updated_at`. All that attributes except the `updated_at` can be part of the document stored at `reference`. `updated_at` can be tracked easily by indexers. -- We don't have normal transferability. +- We don't have traditional transferability. - We propose to use more targeted events, to better reflect the event nature. Moreover events are emitted by the registry, so we need to include issuer contract address in the event. All time related attributes are defined in milliseconds (as per NEP-171). @@ -242,16 +242,31 @@ trait SBTRegistry { /// Must emit one of `Revoke` or `Burn` event. /// Returns true if a token is a valid, active SBT. Otherwise returns false. fn sbt_revoke(&mut self, token: u64) -> bool; +} +``` + +**Soul Transfer** MUST be part of the registry implementation. It is essential part of the standard, but the exact interface of the method is not part of the standard, because registries may adopt different mechanism and require different arguments. Moreover, big registries may require to implement the Soul Transfer operation in stages (and block the account in the meantime). +Smart contract must keep track of ongoing Soul Transfer operations. For example, in the first call, contract should lock the account, and do maximum X amount of transfers. If the list of to be transferred SBTs has not been exhausted, the contract should keep locking the account and remember the last transferred SBT. Subsequent calls by the same user will resume the operation until the list is exhausted. +Soult Transfer must emit the `SoulTransfer` event. + +Example interface: +```rust /// Transfers atomically all SBT tokens from one account to another account. /// The caller must be an SBT holder and the `to` must not be a banned account. - /// Must emit `Revoke` event. - // #[payable] - fn sbt_soul_transfer(&mut self, to: AccountId) -> bool; -} + /// Returns the lastly moved SBT identified by it's contract issuer and token ID as well + /// a boolean: `true` if the whole process has finished, `false` when the process has not + /// finished and should be continued by a subsequent call. + /// User must keeps calling `sbt_soul_transfer` until `true` is returned. + /// Must emit `SoulTransfer` event. + #[payable] + fn sbt_soul_transfer( + &mut self, + to: AccountId, + ) -> (AccountId, TokenId, bool); ``` -SBT issuer smart contracts can implement NFT query interface to make it compatible with NFT tools. Note, we use U64 type rather than U128. However, SBT issuer must not emit NFT related events. +SBT issuer smart contracts may implement NFT query interface to make it compatible with NFT tools. In that case, the contract should proxy the calls to the related registry. Note, we use U64 type rather than U128. However, SBT issuer must not emit NFT related events. ```rust trait SBTNFT { From 786d4a1ac4977354fae8d34864afea1af2d93a11 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Mon, 24 Apr 2023 16:16:02 +0200 Subject: [PATCH 50/89] remove table --- neps/nep-0393.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index 579f82734..4f730d5e2 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -502,15 +502,6 @@ There are many examples where NFT standards are poorly or improperly implemented #### Concerns -| # | Concern | Resolution | Status | -| --- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | ------ | -| 1 | Should we Emit NEP-171 Mint and NEP-171 Burn by the SBT contract (in addition to SBT native events emitted by the registry)? If the events will be emitted by registry, then we need new events to include the contract address. | - | open | -| 2 | Should we reduce amount of events? | - | open | -| 3 | Decide if we should include memo in the events (it's already in the transaction). | - | open | -| 4 | Approve the proposed multi-token approach. | - | open | -| 5 | Use of seconds vs milliseconds as a time unit. | - | open | -| x | | - | open | - ## Copyright [Creative Commons Attribution 4.0 International Public License (CC BY 4.0)](https://creativecommons.org/licenses/by/4.0/) From dba3b4efc4ef8a45da62e6199c8e0aceed7bcb57 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Mon, 24 Apr 2023 21:28:42 +0200 Subject: [PATCH 51/89] interface documentation update and review updates --- neps/nep-0393.md | 55 ++++++++++++++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index 4f730d5e2..1e0bdf212 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -47,7 +47,7 @@ SBT recover MUST not trigger `SoulTransfer` nor `Ban`: malicious issuer could co Soulbound tokens can have an _expire date_. This is useful for tokens which are related to real world certificates with expire time, or social mechanisms (e.g. community membership). Such tokens SHOULD have an option to be renewable. Examples include mandatory renewal with a frequency to check that the owner is still alive, or renew membership to a DAO that uses SBTs as membership gating. -An issuer can provide a _sbt revocation_ in his contract (eg, when a related certificate or membership should be revoked). When doing so, the SBT registry MUST be updated and `Revoke` event must be emitted. It's up to the registry to define revocation handling (either by burning a token, or changing expire date of it's metadata). +An issuer can provide an _sbt revocation_ in his contract (for example: when a related certificate or membership should be revoked, when an issuer finds out that there was an abuse or a scam, etc...). When doing so, the SBT registry MUST update token metadata and must emit the `Revoke` event. It's up to the registry to define revocation handling (either by burning a token, or changing expire date of it's metadata). A registry can emit a `Ban` event without doing soul transfer. Handling it depends on the registry governance or registry use cases. One example is to use social governance to identify fake accounts (like bots) - in that case the registry should allow to emit `Ban` and block a scam soul and block future transfers. NOTE: SBT smart contract (issuer) can have it's own list of blocked accounts or allowed only accounts. @@ -162,20 +162,20 @@ pub struct TokenMetadata { trait SBTRegistry { /********** - * QUERIES - **********/ + * QUERIES + **********/ - /// Get the information about specific token ID issued by `ctr` SBT contract. - fn sbt(&self, ctr: AccountId, token: TokenId) -> Option; + /// Get the information about specific token ID issued by `issuer` SBT contract. + fn sbt(&self, issuer: AccountId, token: TokenId) -> Option; - /// Returns total amount of tokens issued by `ctr` SBT contract, including expired tokens. - /// Depending on the implementation, if a revoke removes a token, it then is should not be - /// included in the supply. - fn sbt_supply(&self, ctr: AccountId) -> u64; + /// Returns total amount of tokens issued by `issuer` SBT contract, including expired + /// tokens. Depending on the implementation, if a revoke removes a token, it then is should + /// not be included in the supply. + fn sbt_supply(&self, issuer: AccountId) -> u64; - /// Returns total amount of tokens of given class minted by `ctr`. See `sbt_supply` for + /// Returns total amount of tokens of given class minted by `issuer`. See `sbt_supply` for /// information about revoked tokens. - fn sbt_supply_by_class(&self, ctr: AccountId, class: ClassId) -> u64; + fn sbt_supply_by_class(&self, issuer: AccountId, class: ClassId) -> u64; /// Returns total supply of SBTs for a given owner. See `sbt_supply` for information about /// revoked tokens. @@ -183,15 +183,19 @@ trait SBTRegistry { fn sbt_supply_by_owner( &self, account: AccountId, - ctr: AccountId, + issuer: AccountId, class: Option, ) -> u64; /// Query sbt tokens issued by a given contract. /// If `from_token` is not specified, then `from_token` should be assumed /// to be the first valid token id. - fn sbt_tokens(&self, ctr: AccountId, from_token: Option, limit: Option) - -> Vec; + fn sbt_tokens( + &self, + issuer: AccountId, + from_token: Option, + limit: Option, + ) -> Vec; /// Query SBT tokens by owner /// If `from_class` is not specified, then `from_class` should be assumed to be the first @@ -200,7 +204,7 @@ trait SBTRegistry { fn sbt_tokens_by_owner( &self, account: AccountId, - ctr: Option, + issuer: Option, from_class: Option, limit: Option, ) -> Vec<(AccountId, Vec)>; @@ -221,8 +225,8 @@ trait SBTRegistry { // #[payable] fn sbt_mint(&mut self, token_spec: Vec<(AccountId, Vec)>) -> Vec; - /// sbt_recover reassigns all tokens from the old owner to a new owner, - /// and registers `old_owner` to a burned addresses registry. + /// sbt_recover reassigns all tokens from the old owner to a new owner, and saves + /// `old_owner` to set of banned addresses, which is shared for the entire registry. /// Must be called by an SBT contract. /// Must emit `Recover` event. /// Must be called by an operator. @@ -237,11 +241,11 @@ trait SBTRegistry { /// Must emit `Renew` event. fn sbt_renew(&mut self, tokens: Vec, expires_at: u64); - /// Revokes SBT, could potentially burn it or update the expire time. + /// Revokes SBT, could potentially burn an SBT or update SBT expire time. /// Must be called by an SBT contract. /// Must emit one of `Revoke` or `Burn` event. /// Returns true if a token is a valid, active SBT. Otherwise returns false. - fn sbt_revoke(&mut self, token: u64) -> bool; + fn sbt_revoke(&mut self, token: Vec) -> bool; } ``` @@ -266,6 +270,17 @@ Example interface: ) -> (AccountId, TokenId, bool); ``` +### SBT Issuer interface + +SBTContract is the minimum required interface to be implemented by issuer. Other methods, such as a mint function, which requests the registry to proceed with token minting, is specific to an Issuer implementation (similarly, mint is not part of the FT standard). + +```rust +pub trait SBTContract { + /// returns contract metadata + fn sbt_metadata(&self) -> ContractMetadata; +} +``` + SBT issuer smart contracts may implement NFT query interface to make it compatible with NFT tools. In that case, the contract should proxy the calls to the related registry. Note, we use U64 type rather than U128. However, SBT issuer must not emit NFT related events. ```rust @@ -469,7 +484,7 @@ sequenceDiagram Being fully compatible with NFT standard is a desirable. However, given the requirements related to _soul transfer_ we didn't find an applaudable solution. Also we decided to use u64 as a Token ID, diverging further from the NFT NEP-171 standard. -Give that our requirements are much striker, we had to reconsider the level of compatibility with NEP-171 NFT. +Given that our requirements are much striker, we had to reconsider the level of compatibility with NEP-171 NFT. There are many examples where NFT standards are poorly or improperly implemented, adding another standard with different functionality but equal naming will cause lots of problems and misclassifications between NFT and SBT. ### Positive From ee2317b49aac199873e7bcecf863a7ca00cbb7ef Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Tue, 9 May 2023 18:26:58 +0200 Subject: [PATCH 52/89] updates --- neps/nep-0393.md | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index 1e0bdf212..53475ef34 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -136,6 +136,8 @@ The Soulbound Token follows the NFT [NEP-171](https://github.com/near/NEPs/blob/ - We don't have traditional transferability. - We propose to use more targeted events, to better reflect the event nature. Moreover events are emitted by the registry, so we need to include issuer contract address in the event. +-- TODO add mention that events will trigger metadata change + All time related attributes are defined in milliseconds (as per NEP-171). ```rust @@ -225,9 +227,10 @@ trait SBTRegistry { // #[payable] fn sbt_mint(&mut self, token_spec: Vec<(AccountId, Vec)>) -> Vec; - /// sbt_recover reassigns all tokens from the old owner to a new owner, and saves - /// `old_owner` to set of banned addresses, which is shared for the entire registry. - /// Must be called by an SBT contract. + /// sbt_recover reassigns all tokens issued by the caller, from the old owner to a new owner. + /// Adds `old_owner` to a banned accounts list. + /// Caller must be a valid issuer. + /// Must be called by a valid SBT issuer. /// Must emit `Recover` event. /// Must be called by an operator. /// Must provide enough NEAR to cover registry storage cost. @@ -241,11 +244,12 @@ trait SBTRegistry { /// Must emit `Renew` event. fn sbt_renew(&mut self, tokens: Vec, expires_at: u64); - /// Revokes SBT, could potentially burn an SBT or update SBT expire time. + /// Revokes SBT by burning the token or updating its expire time. /// Must be called by an SBT contract. - /// Must emit one of `Revoke` or `Burn` event. + /// Must emit `Revoke` event. + /// Must also emit `Burn` event if the SBT tokens are burned (removed). /// Returns true if a token is a valid, active SBT. Otherwise returns false. - fn sbt_revoke(&mut self, token: Vec) -> bool; + fn sbt_revoke(&mut self, token: Vec, burn: bool) -> bool; } ``` @@ -258,16 +262,18 @@ Example interface: ```rust /// Transfers atomically all SBT tokens from one account to another account. /// The caller must be an SBT holder and the `to` must not be a banned account. - /// Returns the lastly moved SBT identified by it's contract issuer and token ID as well - /// a boolean: `true` if the whole process has finished, `false` when the process has not + /// Returns amount of tokens transferred as well a boolean: `true` if the whole + /// process has finished, `false` when the process has not /// finished and should be continued by a subsequent call. /// User must keeps calling `sbt_soul_transfer` until `true` is returned. - /// Must emit `SoulTransfer` event. + /// Must emit `SoulTransfer` event only if the caller was in possession of at least one SBT + /// (if caller doesn't have any tokens, the function won't transfer anything, ban the + /// recipient, emit Ban, and NOT emit SoulTransfer). #[payable] fn sbt_soul_transfer( &mut self, - to: AccountId, - ) -> (AccountId, TokenId, bool); + recipient: AccountId, + ) -> (u32, bool); ``` ### SBT Issuer interface @@ -352,13 +358,11 @@ type Burn { tokens: u64[]; // list of token ids. } -/// An event emitted when the `account` is banned within the emitting registry. +/// An event emitted when an account is banned within the emitting registry. /// Registry must add the `account` to a list of accounts that are not allowed to get any SBT /// in the future. /// Must be emitted by an SBT registry. -type Ban { - account: AccountId; -} +type Ban = AccountId; /// An event emitted when soul transfer is happening: all SBTs owned by `from` are transferred /// to `to`, and the `from` account is banned (can't receive any new SBT). From 54e9a9deef09c7efcb56a8bbdc4c975e8e65f278 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Tue, 9 May 2023 21:42:56 +0200 Subject: [PATCH 53/89] remove return value from sbt_revoke --- neps/nep-0393.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index 53475ef34..0dba0f4fb 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -248,8 +248,7 @@ trait SBTRegistry { /// Must be called by an SBT contract. /// Must emit `Revoke` event. /// Must also emit `Burn` event if the SBT tokens are burned (removed). - /// Returns true if a token is a valid, active SBT. Otherwise returns false. - fn sbt_revoke(&mut self, token: Vec, burn: bool) -> bool; + fn sbt_revoke(&mut self, token: Vec, burn: bool); } ``` From 07021ea2bbd0b37edd815b0004b25b06c9c2ef80 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Tue, 9 May 2023 22:38:23 +0200 Subject: [PATCH 54/89] privacy note, and few typos/wording bugs --- neps/nep-0393.md | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index 0dba0f4fb..cec34cb61 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -171,7 +171,7 @@ trait SBTRegistry { fn sbt(&self, issuer: AccountId, token: TokenId) -> Option; /// Returns total amount of tokens issued by `issuer` SBT contract, including expired - /// tokens. Depending on the implementation, if a revoke removes a token, it then is should + /// tokens. Tokens removed through revoke (with burn=true) or other burning methods should /// not be included in the supply. fn sbt_supply(&self, issuer: AccountId) -> u64; @@ -267,7 +267,7 @@ Example interface: /// User must keeps calling `sbt_soul_transfer` until `true` is returned. /// Must emit `SoulTransfer` event only if the caller was in possession of at least one SBT /// (if caller doesn't have any tokens, the function won't transfer anything, ban the - /// recipient, emit Ban, and NOT emit SoulTransfer). + /// original owner, emit Ban, and NOT emit SoulTransfer). #[payable] fn sbt_soul_transfer( &mut self, @@ -509,6 +509,23 @@ There are many examples where NFT standards are poorly or improperly implemented - New set of events to be handled by the indexer and wallets. - Complexity of integration with a registry: all SBT related transactions must go through Registry. +### Privacy Notes + +> Blockchain-based systems are public by default. Any relationship that is recorded on-chain is immediately visible not just to the participants, but also to anyone in the entire world. Some privacy can be retained by having multiple pseudonyms: a family Soul, a medical Soul, a professional Soul, a political Soul each carrying different SBTs. But done naively, it could be very easy to correlate these Souls to each other. +> The consequences of this lack of privacy are serious. Indeed, without explicit measures taken to protect privacy, the “naive” vision of simply putting all SBTs on-chain may well make too much information public for many applications. + +-- Decentralized Society + +There are multiple ways how a an identity can be doxxed using chain data. SBT, indeed provides more data about account. +The standard allows for few anonymization methods: + +- not providing any data in the token metadata (reference...) or encrypt the reference. +- anonymize issuers (standard allows to have many issues for the same entity) and mix it with different class ids. These are just a numbers. + +Perfect privacy can only be done with solid ZKP, not off-chain walls. + +Implementations must not store any personal information on chain. + ## Changelog ### v1.0.0 From 34fc8939dafb34559d169729feaed25d69e29d0f Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Thu, 8 Jun 2023 17:46:47 +0200 Subject: [PATCH 55/89] review updates --- neps/nep-0393.md | 54 +++++++++++++++++++++++++----------------------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index cec34cb61..fb509eb77 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -47,17 +47,17 @@ SBT recover MUST not trigger `SoulTransfer` nor `Ban`: malicious issuer could co Soulbound tokens can have an _expire date_. This is useful for tokens which are related to real world certificates with expire time, or social mechanisms (e.g. community membership). Such tokens SHOULD have an option to be renewable. Examples include mandatory renewal with a frequency to check that the owner is still alive, or renew membership to a DAO that uses SBTs as membership gating. -An issuer can provide an _sbt revocation_ in his contract (for example: when a related certificate or membership should be revoked, when an issuer finds out that there was an abuse or a scam, etc...). When doing so, the SBT registry MUST update token metadata and must emit the `Revoke` event. It's up to the registry to define revocation handling (either by burning a token, or changing expire date of it's metadata). +An issuer can provide an _sbt revocation_ in his contract (for example: when a related certificate or membership should be revoked, when an issuer finds out that there was an abuse or a scam, etc...). Registry, when receiving `sbt_revoke` request from an issuer must always emit the `Revoke` event. Registry must only accept revoke requests from a valid issuer, and only revoke tokens from that issuer. If `burn=true` is set in the request, then the token should be burned and additionally `Burn` event must be emitted. Otherwise (when `burn=false`) the registry must update token metadata and set expire date in the past. Registry must not ban nor emit `Ban` event when revoking a contract. That would create an attack vector, when a malicious registry would thread the registry by banning accounts. A registry can emit a `Ban` event without doing soul transfer. Handling it depends on the registry governance or registry use cases. One example is to use social governance to identify fake accounts (like bots) - in that case the registry should allow to emit `Ban` and block a scam soul and block future transfers. NOTE: SBT smart contract (issuer) can have it's own list of blocked accounts or allowed only accounts. -### Token Class +### Token Class (multitoken approach) SBT tokens can't be fractionized. Also, by definition there should be only one of a token per token class per user. Examples: user should not be able to receive few badges of the same class, or few proof of attendance to the same event. -However we identify a need for having few token classes in a single contract: +However we identify a need for having to support token classes (aka multitoken interface) in a single contract: -- badges: one contract with multiple badge class (community lead, OG...); +- badges: one contract. Each badge will have a class (community lead, OG...), and each token will belong to a specific class; - certificates: one issuer can create certificates of a different class (eg school department can create diplomas for each major and each graduation year). We also see a trend in the NFT community and demand for market places to support multi token contracts. @@ -73,7 +73,7 @@ Finally, we require that each token ID is unique within the smart contract. This ### SBT Registry -Atomicity of _soul transfer_ in current NEAR runtime is not possible if the token balance is kept separately for each SBT smart contract. We need an additional contract: the `SBT Registry`, to provide atomic transfer of all user SBTs and efficient way to ban accounts in relation to the Ban event. The registry provides a balance book of all associated SBT tokens. So, each SBT is requested by a smart contract (issuer), however it's is created in the registry. +Atomicity of _soul transfer_ in current NEAR runtime is not possible if the token balance is kept separately for each SBT smart contract. We need an additional contract: the `SBT Registry`, to provide atomic transfer of all user SBTs and efficient way to ban accounts in relation to the Ban event. The registry provides a balance book of all associated SBT tokens. So, each SBT is requested by a smart contract (issuer), however it is created in the registry. We can have multiple competing registries (with different purpose or different management scheme). Registries can utilize the same SBT contracts. An SBT smart contract, SHOULD opt-in to a registry. Registries may develop different opt-in mechanisms (they could differ by the approval mechanism, be fully permissioned etc..). One SBT smart contract can opt-in to: @@ -90,7 +90,7 @@ Moreover, a registry provides an efficient way to query multiple tokens for a si #### Recovery within an SBT Registry -SBT registry can define it's own mechanism to atomically recover all tokens related to one account and execute soul transfer to another account, without going one by one through each SBT issuer (sometimes that might be even not possible). Example mechanisms a Registry can implement to recovery mechanism: +SBT registry can define it's own mechanism to atomically recover all tokens related to one account and execute soul transfer to another account, without going one by one through each SBT issuer (sometimes that might be even not possible). Below we list few ideas a Registry can use to implement recovery: - KYC based recovery - Social recovery @@ -105,7 +105,7 @@ A registry can limit which contracts can mint SBTs by implementing a custom issu #### Burning tokens -Registry MAY expose a mechanism to allow an account to burn an unwanted token. The exact mechanism is not part of the standard and it will depend on the registry implementation. We only define a standard `Burn` event, which must be emitted each time a token is removed from existence. Some registries may not allow to let accounts burning their tokens in a spirit of preserving specific claims. Ultimately, NEAR is a public blockchain, and even if a token is burned, it's trace will be preserved. +Registry MAY expose a mechanism to allow an account to burn an unwanted token. The exact mechanism is not part of the standard and it will depend on the registry implementation. We only define a standard `Burn` event, which must be emitted each time a token is removed from existence. Some registries may forbid accounts to burn their tokens in order to preserve specific claims. Ultimately, NEAR is a public blockchain, and even if a token is burned, it's trace will be preserved. #### Personal Identifiable Information @@ -171,8 +171,7 @@ trait SBTRegistry { fn sbt(&self, issuer: AccountId, token: TokenId) -> Option; /// Returns total amount of tokens issued by `issuer` SBT contract, including expired - /// tokens. Tokens removed through revoke (with burn=true) or other burning methods should - /// not be included in the supply. + /// tokens. If a revoke removes a token, it must not be included in the supply. fn sbt_supply(&self, issuer: AccountId) -> u64; /// Returns total amount of tokens of given class minted by `issuer`. See `sbt_supply` for @@ -190,25 +189,30 @@ trait SBTRegistry { ) -> u64; /// Query sbt tokens issued by a given contract. + /// `limit` specifies the upper limit of how many tokens we want to return. /// If `from_token` is not specified, then `from_token` should be assumed - /// to be the first valid token id. + /// to be the first valid token id. If `with_expired=false` then expired tokens are not + /// included in the result. fn sbt_tokens( &self, issuer: AccountId, from_token: Option, limit: Option, + with_expired: bool, ) -> Vec; - /// Query SBT tokens by owner + /// Query SBT tokens by owner. + /// `limit` specifies the upper limit of how many tokens we want to return. /// If `from_class` is not specified, then `from_class` should be assumed to be the first - /// valid class id. - /// Returns list of pairs: `(Contract address, list of token IDs)`. + /// valid class id. If `with_expired=false` then expired tokens are not included in the + /// result. Returns list of pairs: `(Contract address, list of token IDs)`. fn sbt_tokens_by_owner( &self, account: AccountId, issuer: Option, from_class: Option, limit: Option, + with_expired: bool, ) -> Vec<(AccountId, Vec)>; /// checks if an `account` was banned by the registry. @@ -227,16 +231,14 @@ trait SBTRegistry { // #[payable] fn sbt_mint(&mut self, token_spec: Vec<(AccountId, Vec)>) -> Vec; - /// sbt_recover reassigns all tokens issued by the caller, from the old owner to a new owner. + /// sbt_recover reassigns all tokens issued by the caller, from the old owner to a new one. /// Adds `old_owner` to a banned accounts list. - /// Caller must be a valid issuer. + /// Must not add `old_owner` to a ban list. /// Must be called by a valid SBT issuer. /// Must emit `Recover` event. - /// Must be called by an operator. - /// Must provide enough NEAR to cover registry storage cost. - /// Requires attaching enough tokens to cover the storage growth. + /// Requires attaching enough NEAR to cover the storage growth. // #[payable] - fn sbt_recover(&mut self, from: AccountId, to: AccountId); + fn sbt_recover(&mut self, from: AccountId, to: AccountId) -> (u32, bool); /// sbt_renew will update the expire time of provided tokens. /// `expires_at` is a unix timestamp (in miliseconds). @@ -248,7 +250,7 @@ trait SBTRegistry { /// Must be called by an SBT contract. /// Must emit `Revoke` event. /// Must also emit `Burn` event if the SBT tokens are burned (removed). - fn sbt_revoke(&mut self, token: Vec, burn: bool); + fn sbt_revoke(&mut self, tokens: Vec, burn: bool); } ``` @@ -261,7 +263,7 @@ Example interface: ```rust /// Transfers atomically all SBT tokens from one account to another account. /// The caller must be an SBT holder and the `to` must not be a banned account. - /// Returns amount of tokens transferred as well a boolean: `true` if the whole + /// Returns amount of tokens transferred and a boolean: `true` if the whole /// process has finished, `false` when the process has not /// finished and should be continued by a subsequent call. /// User must keeps calling `sbt_soul_transfer` until `true` is returned. @@ -334,14 +336,14 @@ type Recover { new_owner: AccountId; // destination account. } -/// An event emitted when a existing tokens are renewed. +/// An event emitted when existing tokens are renewed. /// Must be emitted by an SBT registry. type Renew { ctr: AccountId; // SBT Contract renewing the tokens tokens: u64[]; // list of token ids. } -/// An event emitted when an existing tokens are revoked. +/// An event emitted when existing tokens are revoked. /// Revoked tokens will continue to be listed by the registry but they should not be listed in /// a wallet. See also `Burn` event. /// Must be emitted by an SBT registry. @@ -350,7 +352,7 @@ type Revoke { tokens: u64[]; // list of token ids. } -/// An event emitted when an existing tokens are burned and removed from the registry. +/// An event emitted when existing tokens are burned and removed from the registry. /// Must be emitted by an SBT registry. type Burn { ctr: AccountId; // SBT Contract burning the tokens @@ -379,7 +381,7 @@ Whenever a recovery is made in a way that an existing SBT is burned, the `Burn` Although the transaction functions below are not part of the SBT smart contract standard (depending on a use case, they may have different parameters), we present here an example interface for SBT issuance and we also provide a reference implementation. These functions must relay calls to an SBT registry, which will emit appropriate events. -We recommend that the all functions related to an event will take an optional `memo: Option` argument for accounting purposes. +We recommend that all functions related to an event will take an optional `memo: Option` argument for accounting purposes. ```rust trait SBT { @@ -516,7 +518,7 @@ There are many examples where NFT standards are poorly or improperly implemented -- Decentralized Society -There are multiple ways how a an identity can be doxxed using chain data. SBT, indeed provides more data about account. +There are multiple ways how an identity can be doxxed using chain data. SBT, indeed provides more data about account. The standard allows for few anonymization methods: - not providing any data in the token metadata (reference...) or encrypt the reference. From 876d9d333dd2b0dc7ab8d7bf63c485fcfe1c61a2 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Thu, 8 Jun 2023 18:06:18 +0200 Subject: [PATCH 56/89] udpated motiviation about registry --- neps/nep-0393.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index fb509eb77..b7b47762c 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -24,7 +24,7 @@ Recent [Decentralized Society](https://www.bankless.com/decentralized-society-de > More importantly, SBTs enable other applications of increasing ambition, such as community wallet recovery, Sybil-resistant governance, mechanisms for decentralization, and novel markets with decomposable, shared rights. We call this richer, pluralistic ecosystem “Decentralized Society” (DeSoc)—a co-determined sociality, where Souls and communities come together bottom-up, as emergent properties of each other to co-create plural network goods and intelligences, at a range of scales. -Creating a strong primitives is necessary to model new innovative systems and decentralized societies. Examples include reputation protocols, undercollateralized lending, one-person-one-vote, fair airdrops & ICOs, universal basic income, non KYC identity systems, Human DAOs and methods for Sybil attack resistance. +Creating a strong primitives is necessary to model new innovative systems and decentralized societies. Examples include reputation protocols, undercollateralized lending, proof-of-personhood, proof-of-attendance, proof-of-skill, reputation protocols, one-person-one-vote, fair airdrops & ICOs, universal basic income, non KYC identity systems, Human DAOs and methods for Sybil attack resistance. We propose an SBT standard to model protocols described above. @@ -40,8 +40,9 @@ Main requirement for Soulbound tokens is to bound an account to a human. A **Sou This becomes especially important for proof-of-human stamps that can only be issued once per user. Few safeguards against misuse of SBT transfer (soul transfer or recovery) are contemplated: 1. Users cannot recover an SBT by themselves. The issuer, a DAO or a smart contract (e.g. multisig) dedicated to manage the recovery should be assigned. -2. Whenever a soul transfer is triggered then the SBT registry emits `SoulTransfer` and `Ban` events. The latter signals that the account can't host and receive any SBT in the future. It creates an inherit cost for such action: the account identity is burned. -3. The recovery function is additional economical risk preventing account trading: user should always be able to recover his SBT, and move to another, not banned account. +2. Whenever a soul transfer is triggered then the SBT registry emits `SoulTransfer` and `Ban` events. The latter signals that the account can't host nor receive any SBT in the future. It creates an inherit cost for such action: the account identity is burned. +3. `SoulTransfer` event can also trigger similar actions in other registries (specification for this is out of the scope of this NEP). +4. The recovery function is additional economical risk preventing account trading: user should always be able to recover his SBT, and move to another, not banned account. SBT recover MUST not trigger `SoulTransfer` nor `Ban`: malicious issuer could compromise the system by faking the token recovery and take over all other SBTs from a user. Only the owner of the account can make a Soul Transfer transaction and merge 2 accounts they owns. @@ -75,19 +76,23 @@ Finally, we require that each token ID is unique within the smart contract. This Atomicity of _soul transfer_ in current NEAR runtime is not possible if the token balance is kept separately for each SBT smart contract. We need an additional contract: the `SBT Registry`, to provide atomic transfer of all user SBTs and efficient way to ban accounts in relation to the Ban event. The registry provides a balance book of all associated SBT tokens. So, each SBT is requested by a smart contract (issuer), however it is created in the registry. -We can have multiple competing registries (with different purpose or different management scheme). Registries can utilize the same SBT contracts. An SBT smart contract, SHOULD opt-in to a registry. Registries may develop different opt-in mechanisms (they could differ by the approval mechanism, be fully permissioned etc..). One SBT smart contract can opt-in to: +We can have multiple competing registries (with different purpose or different management scheme). Registries can utilize the same SBT issuers. An SBT issuer, SHOULD opt-in to a registry before being able to use registry. Registries may develop different opt-in mechanisms (they could differ by the approval mechanism, be fully permissioned etc..). One SBT smart contract can opt-in to: - many registries: it MUST relay all state change functions to all registries. -- or to no registry: it MUST issue the SBT state change emits by itself. +- or to no registry: it MUST emit related events by itself. If an SBT smart contract doesn't opt-in to any registry, then we should think about it as a single token registry, and it MUST strictly implement all SBT Registry query functions by itself: the contract address must be part of the arguments, and it must check that it equals to the deployed account address (`require!(ctr == env::current_account_id())`). +We recommend that each issuer will use only one registry to avoid complex reconciliation and assure single source of truth. + Moreover, a registry provides an efficient way to query multiple tokens for a single user. This will allow implementation of use cases such us: - SBT based identities (main use case of the `i-am-human` protocol); - SBT classes; - decentralized societies. +The registry fills a central and important role. But it is **not centralized**, as anyone can create their own registry and SBT issuers can choose which registry to use. It's also not too powerful, as almost all of the power (mint, revoke, burn, recover, etc) still remains with the SBT issuer and not with the registry. + #### Recovery within an SBT Registry SBT registry can define it's own mechanism to atomically recover all tokens related to one account and execute soul transfer to another account, without going one by one through each SBT issuer (sometimes that might be even not possible). Below we list few ideas a Registry can use to implement recovery: @@ -318,7 +323,7 @@ type Nep393Event { data: Mint | Recover | Renew | Revoke | Burn | Ban[] | SoulTransfer; } -/// An event minted by the Registry when new SBT is created. +/// An event emitted by the Registry when new SBT is created. type Mint { ctr: AccountId; // SBT Contract minting the tokens tokens: [[AccountId, u64[]]]; // list of pairs (token owner, TokenId[]) From 808e0474024951af5bf93795a917f22c978fbd70 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Thu, 8 Jun 2023 18:44:36 +0200 Subject: [PATCH 57/89] add sbt_revoke_by_owner method --- neps/nep-0393.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index b7b47762c..b711432b6 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -256,6 +256,9 @@ trait SBTRegistry { /// Must emit `Revoke` event. /// Must also emit `Burn` event if the SBT tokens are burned (removed). fn sbt_revoke(&mut self, tokens: Vec, burn: bool); + + /// Similar to `sbt_revoke`, but revokes all `owner`s tokens issued by the caller. + fn sbt_revoke_by_owner(&mut self, owner: AccountId, burn: bool); } ``` From f76f85db49c68bbb5d436532ee47bfee32d3c105 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Tue, 13 Jun 2023 00:11:52 +0200 Subject: [PATCH 58/89] update doc string --- neps/nep-0393.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index b711432b6..6bd12f3c9 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -196,8 +196,8 @@ trait SBTRegistry { /// Query sbt tokens issued by a given contract. /// `limit` specifies the upper limit of how many tokens we want to return. /// If `from_token` is not specified, then `from_token` should be assumed - /// to be the first valid token id. If `with_expired=false` then expired tokens are not - /// included in the result. + /// to be the first valid token id. If `with_expired` if is set to `false` then + /// all tokens are returned. fn sbt_tokens( &self, issuer: AccountId, @@ -209,8 +209,8 @@ trait SBTRegistry { /// Query SBT tokens by owner. /// `limit` specifies the upper limit of how many tokens we want to return. /// If `from_class` is not specified, then `from_class` should be assumed to be the first - /// valid class id. If `with_expired=false` then expired tokens are not included in the - /// result. Returns list of pairs: `(Contract address, list of token IDs)`. + /// valid class id. If `with_expired` if is set to `false` then + /// all tokens are returned. Returns list of pairs: `(Contract address, list of token IDs)`. fn sbt_tokens_by_owner( &self, account: AccountId, From 45b73c9a1ff28bd03f6ee3934dc6b56055851f70 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Mon, 19 Jun 2023 23:10:07 +0200 Subject: [PATCH 59/89] One more comment to state that class_id > 0 --- neps/nep-0393.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index 6bd12f3c9..8ed243e3b 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -159,7 +159,7 @@ pub struct ContractMetadata { /// TokenMetadata defines attributes for each SBT token. pub struct TokenMetadata { - pub class: ClassId, // token class + pub class: ClassId, // token class. Required. Must be non zero. pub issued_at: Option, // When token was issued or minted, Unix epoch in milliseconds pub expires_at: Option, // When token expires, Unix epoch in milliseconds pub reference: Option, // URL to an off-chain JSON file with more info. From 670cf5d4aba2b47a048aa894b948938e51f04eed Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Thu, 22 Jun 2023 17:24:51 +0200 Subject: [PATCH 60/89] adding more diagrams --- neps/nep-0393.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index 8ed243e3b..0005509c8 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -432,6 +432,22 @@ trait SBT { In the diagram below we explain a Soul Transfer use case. Alice has two accounts: `alice1` and `alice2`. She is getting tokens from 2 SBT contracts which use the same registry. Later she decides to merge the accounts by doing Soul Transfer. +An `SBT_1 Issuer` wants to mint tokens using the `SBT_Registry`. Each registry has some way to whitelist an issuer. For this example, let's say that the `SBT_Registry` has a DAO which votes on adding a new issuer: + +```mermaid +sequenceDiagram + actor Issuer1 as SBT_1 Issuer + + participant SBT_Registry + + actor DAO + + Note over Issuer1,DAO: Issuer1 connects with the DAO and asks for being whitelisted in the registry + Issuer1-.request whitelist->DAO + + DAO->>SBT_Registry: whitelist(SBT_1 Issuer) +``` + Alice uses her `alice1` account to interact with `SBT_1 Issuer` and receives an SBT with token ID = 238: ```mermaid From dd7ebc2df1399ff260a895ef86814093d530a323 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Thu, 22 Jun 2023 17:33:32 +0200 Subject: [PATCH 61/89] fix --- neps/nep-0393.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index 0005509c8..92354d9e5 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -437,14 +437,13 @@ An `SBT_1 Issuer` wants to mint tokens using the `SBT_Registry`. Each registry h ```mermaid sequenceDiagram actor Issuer1 as SBT_1 Issuer + actor DAO participant SBT_Registry - actor DAO - - Note over Issuer1,DAO: Issuer1 connects with the DAO and asks for being whitelisted in the registry - Issuer1-.request whitelist->DAO + Note over Issuer1,DAO: Issuer1 connects with the DAO
to be whitelisted. + Issuer1-->>DAO: request whitelist DAO->>SBT_Registry: whitelist(SBT_1 Issuer) ``` From 8a7c58403afa3a7d85f6a4efe9bcfb2976c9db8f Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Thu, 22 Jun 2023 17:35:26 +0200 Subject: [PATCH 62/89] add headers --- neps/nep-0393.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index 92354d9e5..c02ea7293 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -432,6 +432,8 @@ trait SBT { In the diagram below we explain a Soul Transfer use case. Alice has two accounts: `alice1` and `alice2`. She is getting tokens from 2 SBT contracts which use the same registry. Later she decides to merge the accounts by doing Soul Transfer. +#### Whitelist a new issuer + An `SBT_1 Issuer` wants to mint tokens using the `SBT_Registry`. Each registry has some way to whitelist an issuer. For this example, let's say that the `SBT_Registry` has a DAO which votes on adding a new issuer: ```mermaid @@ -447,6 +449,8 @@ sequenceDiagram DAO->>SBT_Registry: whitelist(SBT_1 Issuer) ``` +#### Mint tokens + Alice uses her `alice1` account to interact with `SBT_1 Issuer` and receives an SBT with token ID = 238: ```mermaid @@ -492,6 +496,8 @@ sequenceDiagram SBT_Registry-->>Alice: {token: 7991, owner: alice2, metadata: metadata2} ``` +#### Soul transfer + After a while Alice decides to transfer her `alice2` soul into `alice1` ```mermaid From 066d53b7f8f3d62b92aade6f62d89f75be61b276 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Thu, 22 Jun 2023 17:36:25 +0200 Subject: [PATCH 63/89] todos --- neps/nep-0393.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index c02ea7293..9f7eea886 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -141,8 +141,6 @@ The Soulbound Token follows the NFT [NEP-171](https://github.com/near/NEPs/blob/ - We don't have traditional transferability. - We propose to use more targeted events, to better reflect the event nature. Moreover events are emitted by the registry, so we need to include issuer contract address in the event. --- TODO add mention that events will trigger metadata change - All time related attributes are defined in milliseconds (as per NEP-171). ```rust From ba4a5db33190a50da68ecc84e7671e9eb6a4bb92 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Thu, 22 Jun 2023 17:47:25 +0200 Subject: [PATCH 64/89] review --- neps/nep-0393.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index 9f7eea886..de1b997c3 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -183,7 +183,7 @@ trait SBTRegistry { /// Returns total supply of SBTs for a given owner. See `sbt_supply` for information about /// revoked tokens. - /// If class is specified, returns only owner supply of the given class -- must be 0 or 1. + /// If `class` is specified, returns only owner supply of the given class (either 0 or 1). fn sbt_supply_by_owner( &self, account: AccountId, @@ -397,7 +397,6 @@ trait SBT { fn sbt_mint( &mut self, account: AccountId, - class: u64, metadata: TokenMetadata, memo: Option, ) -> TokenId; @@ -423,8 +422,8 @@ trait SBT { ## Reference Implementation -- Common [type definitions](https://github.com/alpha-fi/i-am-human/tree/master/contracts/sbt) (events, traits). -- https://github.com/alpha-fi/i-am-human/tree/master/contracts/soulbound +- Common [type definitions](https://github.com/https://github.com/near-ndc/i-am-human/tree/main/contracts/sbt) (events, traits). +- [I Am Human](https://github.com/https://github.com/near-ndc/i-am-human) registry and issuers. ## Example Flows From 8b91fb7668713875b08575d7bcc4ed4c0a8185b4 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Thu, 22 Jun 2023 20:00:55 +0200 Subject: [PATCH 65/89] typo --- neps/nep-0393.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index de1b997c3..f39614f27 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -55,7 +55,7 @@ NOTE: SBT smart contract (issuer) can have it's own list of blocked accounts or ### Token Class (multitoken approach) -SBT tokens can't be fractionized. Also, by definition there should be only one of a token per token class per user. Examples: user should not be able to receive few badges of the same class, or few proof of attendance to the same event. +SBT tokens can't be fractionize. Also, by definition there should be only one of a token per token class per user. Examples: user should not be able to receive few badges of the same class, or few proof of attendance to the same event. However we identify a need for having to support token classes (aka multitoken interface) in a single contract: - badges: one contract. Each badge will have a class (community lead, OG...), and each token will belong to a specific class; From 098d89c04a2e17d5d496e83afacce7f8436506e7 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Fri, 23 Jun 2023 00:26:04 +0200 Subject: [PATCH 66/89] update diagram --- neps/nep-0393.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index f39614f27..63600b128 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -458,7 +458,7 @@ sequenceDiagram participant SBT1 as SBT_1 Contract participant SBT_Registry - Issuer1->>SBT1: sbt_mint(alice1, class5, metadata) + Issuer1->>SBT1: sbt_mint(alice1, metadata) activate SBT1 SBT1-)SBT_Registry: sbt_mint([[alice1, [metadata]]]) SBT_Registry->>SBT_Registry: emit Mint(SBT_1_Contract, alice1, [238]) From 9104c0e3ec19b9ed380039fc079a38354c289a8c Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Thu, 29 Jun 2023 16:58:57 +0200 Subject: [PATCH 67/89] Update neps/nep-0393.md Co-authored-by: Vlad Frolov --- neps/nep-0393.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index 63600b128..e96738776 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -422,8 +422,8 @@ trait SBT { ## Reference Implementation -- Common [type definitions](https://github.com/https://github.com/near-ndc/i-am-human/tree/main/contracts/sbt) (events, traits). -- [I Am Human](https://github.com/https://github.com/near-ndc/i-am-human) registry and issuers. +- Common [type definitions](https://github.com/near-ndc/i-am-human/tree/main/contracts/sbt) (events, traits). +- [I Am Human](https://github.com/near-ndc/i-am-human) registry and issuers. ## Example Flows From b7c4d4418bd8b011e56f81736255e1d64d95684a Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Fri, 30 Jun 2023 01:50:28 +0200 Subject: [PATCH 68/89] remove 'poorly' --- neps/nep-0393.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index e96738776..9f10bb689 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -516,7 +516,7 @@ sequenceDiagram Being fully compatible with NFT standard is a desirable. However, given the requirements related to _soul transfer_ we didn't find an applaudable solution. Also we decided to use u64 as a Token ID, diverging further from the NFT NEP-171 standard. Given that our requirements are much striker, we had to reconsider the level of compatibility with NEP-171 NFT. -There are many examples where NFT standards are poorly or improperly implemented, adding another standard with different functionality but equal naming will cause lots of problems and misclassifications between NFT and SBT. +There are many examples where NFT standards are improperly implemented, adding another standard with different functionality but equal naming will cause lots of problems and misclassifications between NFT and SBT. ### Positive From 785012569be9d37818ec9826c52162c6d0b61554 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Fri, 30 Jun 2023 11:41:16 +0200 Subject: [PATCH 69/89] improving the flows and spec --- neps/nep-0393.md | 306 +++++++++++++++++++++++++---------------------- 1 file changed, 166 insertions(+), 140 deletions(-) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index 9f10bb689..e2ba32333 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -24,7 +24,7 @@ Recent [Decentralized Society](https://www.bankless.com/decentralized-society-de > More importantly, SBTs enable other applications of increasing ambition, such as community wallet recovery, Sybil-resistant governance, mechanisms for decentralization, and novel markets with decomposable, shared rights. We call this richer, pluralistic ecosystem “Decentralized Society” (DeSoc)—a co-determined sociality, where Souls and communities come together bottom-up, as emergent properties of each other to co-create plural network goods and intelligences, at a range of scales. -Creating a strong primitives is necessary to model new innovative systems and decentralized societies. Examples include reputation protocols, undercollateralized lending, proof-of-personhood, proof-of-attendance, proof-of-skill, reputation protocols, one-person-one-vote, fair airdrops & ICOs, universal basic income, non KYC identity systems, Human DAOs and methods for Sybil attack resistance. +Creating a strong primitives is necessary to model new innovative systems and decentralized societies. Examples include reputation protocols, non-transferrable certificates, non-transferrable rights, undercollateralized lending, proof-of-personhood, proof-of-attendance, proof-of-skill, one-person-one-vote, fair airdrops & ICOs, universal basic income, non KYC identity systems, Human DAOs and methods for Sybil attack resistance. We propose an SBT standard to model protocols described above. @@ -32,68 +32,144 @@ _Verifiable Credentials_ (VC) could be seen as subset of SBT. However there is a ## Specification -Main requirement for Soulbound tokens is to bound an account to a human. A **Soul** is an account with SBTs, which are used to define account identity. Moving SBTs from one account to another should be strictly limited to: +Main requirement for Soulbound tokens is to bound an account to a human. A **Soul** is an account with SBTs, which are used to define account identity. +Often non transferrable NFT (an NFT token with a no-op transfer function) is used to implement SBTs. However, such model is rather shortsighted. Transferability is required to allow users to either recover their SBTs or merge between the accounts they own. At the same time, we need to limit transferability to assure that an SBTs is kept bound to the same _soul_. +We also need an efficient on way to make composed ownership queries (for example: check if an account owns SBT of class C1, C2 and C3 issued by issuer I1, I2 and I3 respectively) - this is needed to model emergent properties discussed above. -- **recoverability** in case a user's private key is compromised due to extortion, loss, etc; -- **soul transfer** - when a user needs to merge his accounts (e.g. they started with few different account but later decides to merge them to increase an account reputation). Soul transfer is different than a token transfer. The standard forbids a traditional token transfer functionality, where a user can transfer individual tokens. That being said, a registry can have extension functions for more advanced scenarios, which could require a governance mechanism. +We introduce a **soul transfer**: an ability for user to move ALL SBT tokens from one account to another in a [semi atomic](#soul-transfer) way, while keeping the SBT bounded to the same _soul_. This happens when a user needs to merge his accounts (e.g. they started with few different account but later decides to merge them to increase an account reputation). Soul transfer is different than a token transfer. The standard forbids a traditional token transfer functionality, where a user can transfer individual tokens. That being said, a registry can have extension functions for more advanced scenarios, which could require a governance approval. -This becomes especially important for proof-of-human stamps that can only be issued once per user. Few safeguards against misuse of SBT transfer (soul transfer or recovery) are contemplated: +SBT standard separates the token issuer concept from the token registry in order to meet the requirements listed above. +In the following sections we discuss the functionality of an issuer and registry. -1. Users cannot recover an SBT by themselves. The issuer, a DAO or a smart contract (e.g. multisig) dedicated to manage the recovery should be assigned. -2. Whenever a soul transfer is triggered then the SBT registry emits `SoulTransfer` and `Ban` events. The latter signals that the account can't host nor receive any SBT in the future. It creates an inherit cost for such action: the account identity is burned. -3. `SoulTransfer` event can also trigger similar actions in other registries (specification for this is out of the scope of this NEP). -4. The recovery function is additional economical risk preventing account trading: user should always be able to recover his SBT, and move to another, not banned account. +### SBT Registry -SBT recover MUST not trigger `SoulTransfer` nor `Ban`: malicious issuer could compromise the system by faking the token recovery and take over all other SBTs from a user. Only the owner of the account can make a Soul Transfer transaction and merge 2 accounts they owns. +Traditional Token model in NEAR blockchain assumes that each token has it's own balance book and implements the authorization and issuance mechanism in the same smart contract. Such model prevents atomic _soul transfer_ in the current NEAR runtime. When token balance is kept separately in each SBT smart contract, synchronizing transfer calls to all such contracts to assure atomicity is not possible at scale. We need an additional contract: the `SBT Registry`, to provide atomic transfer of all user SBTs and efficient way to ban accounts in relation to the Ban event discussed below. +This, and efficient cross-issuer queries are the main reasons SBT standards separates that token registry and token issuance concerns. -Soulbound tokens can have an _expire date_. This is useful for tokens which are related to real world certificates with expire time, or social mechanisms (e.g. community membership). Such tokens SHOULD have an option to be renewable. Examples include mandatory renewal with a frequency to check that the owner is still alive, or renew membership to a DAO that uses SBTs as membership gating. +Issuer is an entity which issues new tokens and potentially can update the tokens (for example execute renewal). All standard modification options are discussed in the sections below. -An issuer can provide an _sbt revocation_ in his contract (for example: when a related certificate or membership should be revoked, when an issuer finds out that there was an abuse or a scam, etc...). Registry, when receiving `sbt_revoke` request from an issuer must always emit the `Revoke` event. Registry must only accept revoke requests from a valid issuer, and only revoke tokens from that issuer. If `burn=true` is set in the request, then the token should be burned and additionally `Burn` event must be emitted. Otherwise (when `burn=false`) the registry must update token metadata and set expire date in the past. Registry must not ban nor emit `Ban` event when revoking a contract. That would create an attack vector, when a malicious registry would thread the registry by banning accounts. +Registry is a smart contract, where issuers register the tokens. Registry provides a balance book of all associated SBTs. Registry must ensure that each issuer has it's own "sandbox" and issuers won't overwrite each other. A registry provides an efficient way to query multiple tokens for a single user. This will allow implementation of use cases such us: -A registry can emit a `Ban` event without doing soul transfer. Handling it depends on the registry governance or registry use cases. One example is to use social governance to identify fake accounts (like bots) - in that case the registry should allow to emit `Ban` and block a scam soul and block future transfers. -NOTE: SBT smart contract (issuer) can have it's own list of blocked accounts or allowed only accounts. +- SBT based identities (main use case of the `i-am-human` protocol); +- SBT classes; +- decentralized societies. -### Token Class (multitoken approach) +```mermaid +graph TB + Issuer1--uses--> Registry + Issuer2--uses--> Registry + Issuer3--uses--> Registry +``` -SBT tokens can't be fractionize. Also, by definition there should be only one of a token per token class per user. Examples: user should not be able to receive few badges of the same class, or few proof of attendance to the same event. -However we identify a need for having to support token classes (aka multitoken interface) in a single contract: +We can have multiple competing registries (with different purpose or different management scheme). An SBT issuer, SHOULD opt-in to a registry before being able to use registry. Registries may develop different opt-in mechanisms (they could differ by the approval mechanism, be fully permissioned etc..). One SBT smart contract can opt-in to: -- badges: one contract. Each badge will have a class (community lead, OG...), and each token will belong to a specific class; -- certificates: one issuer can create certificates of a different class (eg school department can create diplomas for each major and each graduation year). +- many registries: it MUST relay all state change functions to all registries. +- or to no registry. We should think about it as a single token registry, and it MUST strictly implement all SBT Registry query functions by itself. The contract address must be part of the arguments, and it must check that it equals to the deployed account address (`require!(ctr == env::current_account_id())`). It also MUST emit related events by itself. -We also see a trend in the NFT community and demand for market places to support multi token contracts. +We recommend that each issuer will use only one registry to avoid complex reconciliation and assure single source of truth. -- In Ethereum community many projects are using [ERC-1155 Multi Token Standard](https://eips.ethereum.org/EIPS/eip-1155). NFT projects are using it for fraction ownership: each token id can have many fungible fractions. -- NEAR [NEP-245](https://github.com/near/NEPs/blob/master/neps/nep-0245.md) has elaborated similar interface for both bridge compatibility with EVM chains as well as flexibility to define different token types with different behavior in a single contract. [DevGovGigs Board](https://near.social/#/mob.near/widget/MainPage.Post.Page?accountId=devgovgigs.near&blockHeight=87938945) recently also shows growing interest to move NEP-245 adoption forward. -- [NEP-454](https://github.com/near/NEPs/pull/454) proposes royalties support for multi token contracts. +The registry fills a central and important role. But it is **not centralized**, as anyone can create their own registry and SBT issuers can choose which registry to use. It's also not too powerful, as almost all of the power (mint, revoke, burn, recover, etc) still remains with the SBT issuer and not with the registry. -We propose that the SBT Standard will support the multi-token idea from the get go. This won't increase the complexity of the contract (in a traditional case, where one contract will only issue tokens of the single class, the `class` argument is simply ignored in the state, and in the functions it's required to be of a constant value, eg `1`) but will unify the interface. -It's up to the smart contract design how the token classes is managed. A smart contract can expose an admin function (example: `sbt_new_class() -> ClassId`) or hard code the pre-registered classes. +#### Issuer authorization -Finally, we require that each token ID is unique within the smart contract. This will allow us to query token only by token ID, without knowing it's class. +A registry can limit which issuers can use registry to mint SBTs by implementing a custom issuer whitelist methods (for example simple access control list managed by a DAO) or keep it fully open (allowing any issuer minting withing the registry). -### SBT Registry +Example: an `SBT_1 Issuer` wants to mint tokens using the `SBT_Registry`. The `SBT_Registry` has a DAO which votes on adding a new issuer: + +```mermaid +sequenceDiagram + actor Issuer1 as SBT_1 Issuer + actor DAO -Atomicity of _soul transfer_ in current NEAR runtime is not possible if the token balance is kept separately for each SBT smart contract. We need an additional contract: the `SBT Registry`, to provide atomic transfer of all user SBTs and efficient way to ban accounts in relation to the Ban event. The registry provides a balance book of all associated SBT tokens. So, each SBT is requested by a smart contract (issuer), however it is created in the registry. + participant SBT_Registry -We can have multiple competing registries (with different purpose or different management scheme). Registries can utilize the same SBT issuers. An SBT issuer, SHOULD opt-in to a registry before being able to use registry. Registries may develop different opt-in mechanisms (they could differ by the approval mechanism, be fully permissioned etc..). One SBT smart contract can opt-in to: + Note over Issuer1,DAO: Issuer1 connects with the DAO
to be whitelisted. -- many registries: it MUST relay all state change functions to all registries. -- or to no registry: it MUST emit related events by itself. + Issuer1-->>DAO: request whitelist + DAO->>SBT_Registry: whitelist(SBT_1 Issuer) +``` -If an SBT smart contract doesn't opt-in to any registry, then we should think about it as a single token registry, and it MUST strictly implement all SBT Registry query functions by itself: the contract address must be part of the arguments, and it must check that it equals to the deployed account address (`require!(ctr == env::current_account_id())`). +#### Personal Identifiable Information -We recommend that each issuer will use only one registry to avoid complex reconciliation and assure single source of truth. +Issuers must not include any PII into any SBT. -Moreover, a registry provides an efficient way to query multiple tokens for a single user. This will allow implementation of use cases such us: +### Account Ban -- SBT based identities (main use case of the `i-am-human` protocol); -- SBT classes; -- decentralized societies. +`Ban` is an event emitted by a registry signaling that the account is banned, and can't own any SBT. Registry must return zero for every SBT supply query of a banned account. Operations which trigger soul transfer must emit Ban. -The registry fills a central and important role. But it is **not centralized**, as anyone can create their own registry and SBT issuers can choose which registry to use. It's also not too powerful, as almost all of the power (mint, revoke, burn, recover, etc) still remains with the SBT issuer and not with the registry. +A registry can emit a `Ban` for use cases not discussed in this standard. Handling it depends on the registry governance. One example is to use social governance to identify fake accounts (like bots) - in that case the registry should allow to emit `Ban` and block a scam soul and block future transfers. +NOTE: an SBT Issuer can have it's own list of blocked accounts or allowed only accounts. -#### Recovery within an SBT Registry +### Minting + +Minting is done by issuer calling `registry.sbt_mint(tokens_to_mint)` method. Standard doesn't specify how a registry authorizes an issuer. A classical approach is a whitelist of issuers: any whitelisted issuer can mint any amount of new tokens. Registry must keep the balances and assign token IDs to newly minted tokens. + +Example: Alice has two accounts: `alice1` and `alice2` which she used to mint tokens. She is getting tokens from 2 issuers that use the same registry. Alice uses her `alice1` account to interact with `SBT_1 Issuer` and receives an SBT with token ID = 238: + +```mermaid +sequenceDiagram + actor Alice + actor Issuer1 as SBT_1 Issuer + + participant SBT1 as SBT_1 Contract + participant SBT_Registry + + Issuer1->>SBT1: sbt_mint(alice1, metadata) + activate SBT1 + SBT1-)SBT_Registry: sbt_mint([[alice1, [metadata]]]) + SBT_Registry->>SBT_Registry: emit Mint(SBT_1_Contract, alice1, [238]) + SBT_Registry-)SBT1: [238] + deactivate SBT1 + + Note over Alice,SBT_Registry: now Alice can query registry to check her SBT + + Alice-->>SBT_Registry: sbt(SBT_1_Contract, 238) + SBT_Registry-->>Alice: {token: 238, owner: alice1, metadata} +``` + +With `SBT_2 Issuer`, Alice uses her `alice2` account. Note that `SBT_2 Contract` has different mint function (can mint many tokens at once), and validates a proof prior to requesting the registry to mint the tokens. + +```mermaid +sequenceDiagram + actor Alice + actor Issuer2 as SBT_2 Issuer + + participant SBT2 as SBT_2 Contract + participant SBT_Registry + + Issuer2->>SBT2: sbt_mint_multi([[alice2, metadata2], [alice2, metadata3]], proof) + activate SBT2 + SBT2-)SBT_Registry: sbt_mint([[alice2, [metadata2, metadata3]]]) + SBT_Registry->>SBT_Registry: emit Mint(SBT_2_Contract, alice2, [7991, 7992]) + SBT_Registry-)SBT2: [7991, 1992] + deactivate SBT2 + + Note over Alice,SBT_Registry: Alice queries one of her new tokens + Alice-->>SBT_Registry: sbt(SBT_2_Contract, 7991) + SBT_Registry-->>Alice: {token: 7991, owner: alice2, metadata: metadata2} +``` + +### Transferability + +Safeguards are set against misuse of SBT transfer and keep the _soul bound_ property. SBT transfer from one account to another should be strictly limited to: + +- **revocation** allows issuer to invalidate or burn an SBT in case a token issuance should be reverted (for example the recipient is a Sybil account, that is an account controlled by an entity trying to create the false appearance); +- **recoverability** in case a user's private key is compromised due to extortion, loss, etc. Users cannot recover an SBT only by themselves. Users must connect with issuer to request recoverability or use more advanced mechanism (like social recoverability). The recovery function provides additional economical cost preventing account trading: user should always be able to recover his SBT, and move to another, not banned account. +- **soul transfer** - moving all SBT tokens from a source account (issued by all issuers) to a destination account. During such transfer, SBT registry emits `SoulTransfer` and `Ban` events. The latter signals that the account can't host nor receive any SBT in the future, effectively burning the identity of the source account. This creates an inherit cost for the source account: it's identity can't be used any more. Registry can have extension functions for more advanced scenarios, which could require a governance mechanism. `SoulTransfer` event can also trigger similar actions in other registries (specification for this is out of the scope of this NEP). + +This becomes especially important for proof-of-human stamps that can only be issued once per user. + +#### Revocation + +An issuer can revoke SBTs by calling `registry.sbt_revoke(tokens_to_revoke, burn)`. Example: when a related certificate or membership should be revoked, when an issuer finds out that there was an abuse or a scam, etc...). Registry, when receiving `sbt_revoke` request from an issuer must always emit the `Revoke` event. Registry must only accept revoke requests from a valid issuer, and only revoke tokens from that issuer. If `burn=true` is set in the request, then the token should be burned and `Burn` event must be emitted. Otherwise (when `burn=false`) the registry must update token metadata and set expire date to a time in the past. Registry must not ban nor emit `Ban` event when revoking a contract. That would create an attack vector, when a malicious registry would thread the registry by banning accounts. + +#### Recoverability + +Standard defines issuer recoverability. At minimum, the standard registry exposes `sbt_recover` method, which allows issuer to reassign a token issued by him from one account to another. + +SBT recovery MUST not trigger `SoulTransfer` nor `Ban` event: malicious issuer could compromise the system by faking the token recovery and take over all other SBTs from a user. Only the owner of the account can make a Soul Transfer transaction and merge 2 accounts they owns. + +#### Recoverability within an SBT Registry SBT registry can define it's own mechanism to atomically recover all tokens related to one account and execute soul transfer to another account, without going one by one through each SBT issuer (sometimes that might be even not possible). Below we list few ideas a Registry can use to implement recovery: @@ -104,19 +180,58 @@ SBT registry can define it's own mechanism to atomically recover all tokens rela SBT Registry based recovery is not part of this specification. -#### Minting authorization +#### Soul Transfer + +The basic use case is described above. Registry MUST provide a permissionless method to allow any user to execute soul transfer. It is essential part of the standard, but the exact interface of the method is not part of the standard, because registries may adopt different mechanism and require different arguments. + +Soul transfers must be _semi atomic_. That is, the holder account must be non operational (in terms of SBT supply) until the soul transfer is completed. Given the nature of NEAR blockchain, where transactions are limited by gas, big registries may require to implement the Soul Transfer operation in stages. Source and destination accounts should act as a non-soul accounts while the soul transfer operation is ongoing. For example, in the first call, contract can lock the account, and do maximum X amount of transfers. If the list of to be transferred SBTs has not been exhausted, the contract should keep locking the account and remember the last transferred SBT. Subsequent calls by the same user will resume the operation until the list is exhausted. +Soul Transfer must emit the `SoulTransfer` event. -A registry can limit which contracts can mint SBTs by implementing a custom issuer registration methods. Example: a simple access control list managed by a DAO. +Example: Alice has two accounts: `alice1` and `alice2` which she used to mint tokens (see [mint diagram](#minting)). She decides to merge the accounts by doing Soul Transfer. -#### Burning tokens +```mermaid +sequenceDiagram + actor Alice + participant SBT_Registry + + Alice->>SBT_Registry: sbt_soul_transfer(alice1) --accountId alice2 + + Alice-->>+SBT_Registry: sbt_tokens_by_owner(alice2) + SBT_Registry-->>-Alice: [] + + Alice-->>+SBT_Registry: sbt_tokens_by_owner(alice1) + SBT_Registry-->>-Alice: [[SBT_1_Contract, [238]], [SBT_2_Contract, [7991, 7992]]]} +``` + +### Renewal + +Soulbound tokens can have an _expire date_. This is useful for tokens which are related to real world certificates with expire time, or social mechanisms (e.g. community membership). Such tokens SHOULD have an option to be renewable. Examples include mandatory renewal with a frequency to check that the owner is still alive, or renew membership to a DAO that uses SBTs as membership gating. +Registry defines `sbt_renew` method which allows issuers to update the token expire date. + +### Burning tokens Registry MAY expose a mechanism to allow an account to burn an unwanted token. The exact mechanism is not part of the standard and it will depend on the registry implementation. We only define a standard `Burn` event, which must be emitted each time a token is removed from existence. Some registries may forbid accounts to burn their tokens in order to preserve specific claims. Ultimately, NEAR is a public blockchain, and even if a token is burned, it's trace will be preserved. -#### Personal Identifiable Information +### Token Class (multitoken approach) -Issuers must not include any PII into any SBT. +SBT tokens can't be fractionize. Also, by definition there should be only one of a token per token class per user. Examples: user should not be able to receive few badges of the same class, or few proof of attendance to the same event. +However we identify a need for having to support token classes (aka multitoken interface) in a single contract: + +- badges: one contract. Each badge will have a class (community lead, OG...), and each token will belong to a specific class; +- certificates: one issuer can create certificates of a different class (eg school department can create diplomas for each major and each graduation year). + +We also see a trend in the NFT community and demand for market places to support multi token contracts. + +- In Ethereum community many projects are using [ERC-1155 Multi Token Standard](https://eips.ethereum.org/EIPS/eip-1155). NFT projects are using it for fraction ownership: each token id can have many fungible fractions. +- NEAR [NEP-245](https://github.com/near/NEPs/blob/master/neps/nep-0245.md) has elaborated similar interface for both bridge compatibility with EVM chains as well as flexibility to define different token types with different behavior in a single contract. [DevGovGigs Board](https://near.social/#/mob.near/widget/MainPage.Post.Page?accountId=devgovgigs.near&blockHeight=87938945) recently also shows growing interest to move NEP-245 adoption forward. +- [NEP-454](https://github.com/near/NEPs/pull/454) proposes royalties support for multi token contracts. + +We propose that the SBT Standard will support the multi-token idea from the get go. This won't increase the complexity of the contract (in a traditional case, where one contract will only issue tokens of the single class, the `class` argument is simply ignored in the state, and in the functions it's required to be of a constant value, eg `1`) but will unify the interface. +It's up to the smart contract design how the token classes is managed. A smart contract can expose an admin function (example: `sbt_new_class() -> ClassId`) or hard code the pre-registered classes. + +Finally, we require that each token ID is unique within the smart contract. This will allow us to query token only by token ID, without knowing it's class. -### Smart contract interface +## Smart contract interface For the Token ID type we propose `u64` rather than `U128`. `u64` capacity is more than 1e19. If we will mint 10'000 SBTs per second, then it will take us 58'494'241 years to fill the capacity. Today, the JS integer limit is `2^53-1 ~ 9e15`. Similarly, when minting 10'000 SBTs per second, it will take us 28'561 years to reach the limit. So, we don't need u128 nor the String type. However, if for some reason, we will need to get u64 support for JS, then we can always add another set of methods which will return String, so making it compatible with NFT standard (which is using `U128`, which is a string). Also, it's worth to note, that in 28'000 years JS (if it will still exists) will be completely different. @@ -194,8 +309,8 @@ trait SBTRegistry { /// Query sbt tokens issued by a given contract. /// `limit` specifies the upper limit of how many tokens we want to return. /// If `from_token` is not specified, then `from_token` should be assumed - /// to be the first valid token id. If `with_expired` if is set to `false` then - /// all tokens are returned. + /// to be the first valid token id. If `with_expired` is set to `true` then all the tokens are returned + /// including expired ones otherwise only non-expired tokens are returned. fn sbt_tokens( &self, issuer: AccountId, @@ -207,8 +322,9 @@ trait SBTRegistry { /// Query SBT tokens by owner. /// `limit` specifies the upper limit of how many tokens we want to return. /// If `from_class` is not specified, then `from_class` should be assumed to be the first - /// valid class id. If `with_expired` if is set to `false` then - /// all tokens are returned. Returns list of pairs: `(Contract address, list of token IDs)`. + /// valid class id. If `with_expired` is set to `true` then all the tokens are returned + /// including expired ones otherwise only non-expired tokens are returned. + /// Returns list of pairs: `(Contract address, list of token IDs)`. fn sbt_tokens_by_owner( &self, account: AccountId, @@ -260,11 +376,7 @@ trait SBTRegistry { } ``` -**Soul Transfer** MUST be part of the registry implementation. It is essential part of the standard, but the exact interface of the method is not part of the standard, because registries may adopt different mechanism and require different arguments. Moreover, big registries may require to implement the Soul Transfer operation in stages (and block the account in the meantime). -Smart contract must keep track of ongoing Soul Transfer operations. For example, in the first call, contract should lock the account, and do maximum X amount of transfers. If the list of to be transferred SBTs has not been exhausted, the contract should keep locking the account and remember the last transferred SBT. Subsequent calls by the same user will resume the operation until the list is exhausted. -Soult Transfer must emit the `SoulTransfer` event. - -Example interface: +Example **Soul Transfer** interface: ```rust /// Transfers atomically all SBT tokens from one account to another account. @@ -425,92 +537,6 @@ trait SBT { - Common [type definitions](https://github.com/near-ndc/i-am-human/tree/main/contracts/sbt) (events, traits). - [I Am Human](https://github.com/near-ndc/i-am-human) registry and issuers. -## Example Flows - -In the diagram below we explain a Soul Transfer use case. Alice has two accounts: `alice1` and `alice2`. She is getting tokens from 2 SBT contracts which use the same registry. Later she decides to merge the accounts by doing Soul Transfer. - -#### Whitelist a new issuer - -An `SBT_1 Issuer` wants to mint tokens using the `SBT_Registry`. Each registry has some way to whitelist an issuer. For this example, let's say that the `SBT_Registry` has a DAO which votes on adding a new issuer: - -```mermaid -sequenceDiagram - actor Issuer1 as SBT_1 Issuer - actor DAO - - participant SBT_Registry - - Note over Issuer1,DAO: Issuer1 connects with the DAO
to be whitelisted. - - Issuer1-->>DAO: request whitelist - DAO->>SBT_Registry: whitelist(SBT_1 Issuer) -``` - -#### Mint tokens - -Alice uses her `alice1` account to interact with `SBT_1 Issuer` and receives an SBT with token ID = 238: - -```mermaid -sequenceDiagram - actor Alice - actor Issuer1 as SBT_1 Issuer - - participant SBT1 as SBT_1 Contract - participant SBT_Registry - - Issuer1->>SBT1: sbt_mint(alice1, metadata) - activate SBT1 - SBT1-)SBT_Registry: sbt_mint([[alice1, [metadata]]]) - SBT_Registry->>SBT_Registry: emit Mint(SBT_1_Contract, alice1, [238]) - SBT_Registry-)SBT1: [238] - deactivate SBT1 - - Note over Alice,SBT_Registry: now Alice can query registry to check her SBT - - Alice-->>SBT_Registry: sbt(SBT_1_Contract, 238) - SBT_Registry-->>Alice: {token: 238, owner: alice1, metadata} -``` - -With `SBT_2 Issuer`, Alice uses her `alice2` account. Note that `SBT_2 Contract` has different mint function (can mint many tokens at once), and validates a proof prior to requesting the registry to mint the tokens. - -```mermaid -sequenceDiagram - actor Alice - actor Issuer2 as SBT_2 Issuer - - participant SBT2 as SBT_2 Contract - participant SBT_Registry - - Issuer2->>SBT2: sbt_mint_multi([[alice2, metadata2], [alice2, metadata3]], proof) - activate SBT2 - SBT2-)SBT_Registry: sbt_mint([[alice2, [metadata2, metadata3]]]) - SBT_Registry->>SBT_Registry: emit Mint(SBT_2_Contract, alice2, [7991, 7992]) - SBT_Registry-)SBT2: [7991, 1992] - deactivate SBT2 - - Note over Alice,SBT_Registry: Alice queries one of her new tokens - Alice-->>SBT_Registry: sbt(SBT_2_Contract, 7991) - SBT_Registry-->>Alice: {token: 7991, owner: alice2, metadata: metadata2} -``` - -#### Soul transfer - -After a while Alice decides to transfer her `alice2` soul into `alice1` - -```mermaid -sequenceDiagram - actor Alice - participant SBT_Registry - - Alice->>SBT_Registry: sbt_soul_transfer(alice1) --accountId alice2 - - Alice-->>+SBT_Registry: sbt_tokens_by_owner(alice2) - SBT_Registry-->>-Alice: [] - - Alice-->>+SBT_Registry: sbt_tokens_by_owner(alice1) - SBT_Registry-->>-Alice: [[SBT_1_Contract, [238]], [SBT_2_Contract, [7991, 7992]]]} -``` - ## Consequences Being fully compatible with NFT standard is a desirable. However, given the requirements related to _soul transfer_ we didn't find an applaudable solution. Also we decided to use u64 as a Token ID, diverging further from the NFT NEP-171 standard. From 0ec0dca6ba0d771878167a04bfc5f7165cbcaad4 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Fri, 30 Jun 2023 16:16:16 +0200 Subject: [PATCH 70/89] more notes about token ID and the available space --- neps/nep-0393.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index e2ba32333..182f0cccd 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -235,6 +235,9 @@ Finally, we require that each token ID is unique within the smart contract. This For the Token ID type we propose `u64` rather than `U128`. `u64` capacity is more than 1e19. If we will mint 10'000 SBTs per second, then it will take us 58'494'241 years to fill the capacity. Today, the JS integer limit is `2^53-1 ~ 9e15`. Similarly, when minting 10'000 SBTs per second, it will take us 28'561 years to reach the limit. So, we don't need u128 nor the String type. However, if for some reason, we will need to get u64 support for JS, then we can always add another set of methods which will return String, so making it compatible with NFT standard (which is using `U128`, which is a string). Also, it's worth to note, that in 28'000 years JS (if it will still exists) will be completely different. +The number of combinations for a single issuer is much higher in fact: the token standard uses classes. So technically that makes the number of all possible combinations for a single issuer equal `(2^64)^2 ~ 1e38`. For "today" JS it is `(2^53-1)^2 ~ 1e31`. + +Token IDs should be created in a sequence to make sure the ID space is not exhausted locally (eg if a registry would decide to introduce segments, it would potentially get into a trap where one of the segments is filled up very quickly). ```rust // TokenId and ClassId must be positive (0 is not a valid ID) From c57a4d21435cb7550538c36c9e6948f278190435 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Fri, 30 Jun 2023 16:33:51 +0200 Subject: [PATCH 71/89] language --- neps/nep-0393.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index 182f0cccd..871642fdf 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -14,7 +14,7 @@ Requires: ## Summary -Soulbound Token (SBT) is a form of a NFT which represents an aspect of an account: _soul_. Transferability is limited only to a case of recoverability or a _soul transfer_. The latter, must coordinate with a registry to transfer all SBTs from one account to another, and _banning_ the source account. +Soulbound Token (SBT) is a form of a NFT which represents an aspect of an account: _soul_. Transferability is limited only to a case of recoverability or a _soul transfer_. The latter must coordinate with a registry to transfer all SBTs from one account to another, and _banning_ the source account. SBTs are well suited for carrying proof-of-attendance, proof-of-unique-human "stamps" and other similar credibility-carriers. @@ -36,14 +36,14 @@ Main requirement for Soulbound tokens is to bound an account to a human. A **Sou Often non transferrable NFT (an NFT token with a no-op transfer function) is used to implement SBTs. However, such model is rather shortsighted. Transferability is required to allow users to either recover their SBTs or merge between the accounts they own. At the same time, we need to limit transferability to assure that an SBTs is kept bound to the same _soul_. We also need an efficient on way to make composed ownership queries (for example: check if an account owns SBT of class C1, C2 and C3 issued by issuer I1, I2 and I3 respectively) - this is needed to model emergent properties discussed above. -We introduce a **soul transfer**: an ability for user to move ALL SBT tokens from one account to another in a [semi atomic](#soul-transfer) way, while keeping the SBT bounded to the same _soul_. This happens when a user needs to merge his accounts (e.g. they started with few different account but later decides to merge them to increase an account reputation). Soul transfer is different than a token transfer. The standard forbids a traditional token transfer functionality, where a user can transfer individual tokens. That being said, a registry can have extension functions for more advanced scenarios, which could require a governance approval. +We introduce a **soul transfer**: an ability for user to move ALL SBT tokens from one account to another in a [semi atomic](#soul-transfer) way, while keeping the SBT bounded to the same _soul_. This happens when a user needs to merge his accounts (e.g. they started with a few different accounts but later decides to merge them to increase an account reputation). Soul transfer is different than a token transfer. The standard forbids a traditional token transfer functionality, where a user can transfer individual tokens. That being said, a registry can have extension functions for more advanced scenarios, which could require a governance approval. SBT standard separates the token issuer concept from the token registry in order to meet the requirements listed above. In the following sections we discuss the functionality of an issuer and registry. ### SBT Registry -Traditional Token model in NEAR blockchain assumes that each token has it's own balance book and implements the authorization and issuance mechanism in the same smart contract. Such model prevents atomic _soul transfer_ in the current NEAR runtime. When token balance is kept separately in each SBT smart contract, synchronizing transfer calls to all such contracts to assure atomicity is not possible at scale. We need an additional contract: the `SBT Registry`, to provide atomic transfer of all user SBTs and efficient way to ban accounts in relation to the Ban event discussed below. +Traditional Token model in NEAR blockchain assumes that each token has it's own balance book and implements the authorization and issuance mechanism in the same smart contract. Such model prevents atomic _soul transfer_ in the current NEAR runtime. When token balance is kept separately in each SBT smart contract, synchronizing transfer calls to all such contracts to assure atomicity is not possible at scale. We need an additional contract, the `SBT Registry`, to provide atomic transfer of all user SBTs and efficient way to ban accounts in relation to the Ban event discussed below. This, and efficient cross-issuer queries are the main reasons SBT standards separates that token registry and token issuance concerns. Issuer is an entity which issues new tokens and potentially can update the tokens (for example execute renewal). All standard modification options are discussed in the sections below. @@ -205,8 +205,8 @@ sequenceDiagram ### Renewal -Soulbound tokens can have an _expire date_. This is useful for tokens which are related to real world certificates with expire time, or social mechanisms (e.g. community membership). Such tokens SHOULD have an option to be renewable. Examples include mandatory renewal with a frequency to check that the owner is still alive, or renew membership to a DAO that uses SBTs as membership gating. -Registry defines `sbt_renew` method which allows issuers to update the token expire date. +Soulbound tokens can have an _expire date_. It is useful for tokens which are related to real world certificates with expire time, or social mechanisms (e.g. community membership). Such tokens SHOULD have an option to be renewable. Examples include mandatory renewal with a frequency to check that the owner is still alive, or renew membership to a DAO that uses SBTs as membership gating. +Registry defines `sbt_renew` method allowing issuers to update the token expire date. ### Burning tokens @@ -214,8 +214,8 @@ Registry MAY expose a mechanism to allow an account to burn an unwanted token. T ### Token Class (multitoken approach) -SBT tokens can't be fractionize. Also, by definition there should be only one of a token per token class per user. Examples: user should not be able to receive few badges of the same class, or few proof of attendance to the same event. -However we identify a need for having to support token classes (aka multitoken interface) in a single contract: +SBT tokens can't be fractionized. Also, by definition there should be only one of a token per token class per user. Examples: user should not be able to receive few badges of the same class, or few proof of attendance to the same event. +However, we identify a need for having to support token classes (aka multitoken interface) in a single contract: - badges: one contract. Each badge will have a class (community lead, OG...), and each token will belong to a specific class; - certificates: one issuer can create certificates of a different class (eg school department can create diplomas for each major and each graduation year). @@ -545,7 +545,7 @@ trait SBT { Being fully compatible with NFT standard is a desirable. However, given the requirements related to _soul transfer_ we didn't find an applaudable solution. Also we decided to use u64 as a Token ID, diverging further from the NFT NEP-171 standard. Given that our requirements are much striker, we had to reconsider the level of compatibility with NEP-171 NFT. -There are many examples where NFT standards are improperly implemented, adding another standard with different functionality but equal naming will cause lots of problems and misclassifications between NFT and SBT. +There are many examples where NFT standards are improperly implemented. Adding another standard with different functionality but equal naming will cause lots of problems and misclassifications between NFT and SBT. ### Positive From eece830354fef915ba2ac4b3825c6121c792b478 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Fri, 30 Jun 2023 16:57:43 +0200 Subject: [PATCH 72/89] gramma --- neps/nep-0393.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index 871642fdf..7965fecf2 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -214,7 +214,7 @@ Registry MAY expose a mechanism to allow an account to burn an unwanted token. T ### Token Class (multitoken approach) -SBT tokens can't be fractionized. Also, by definition there should be only one of a token per token class per user. Examples: user should not be able to receive few badges of the same class, or few proof of attendance to the same event. +SBT tokens can't be fractionized. Also, by definition, there should be only one SBT per token class per user. Examples: user should not be able to receive few badges of the same class, or few proof of attendance to the same event. However, we identify a need for having to support token classes (aka multitoken interface) in a single contract: - badges: one contract. Each badge will have a class (community lead, OG...), and each token will belong to a specific class; From b8167930714ddc28823ba31259ec48c1ecafbd6c Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Fri, 30 Jun 2023 17:23:31 +0200 Subject: [PATCH 73/89] gramma --- neps/nep-0393.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index 7965fecf2..92cd965e4 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -61,7 +61,7 @@ graph TB Issuer3--uses--> Registry ``` -We can have multiple competing registries (with different purpose or different management scheme). An SBT issuer, SHOULD opt-in to a registry before being able to use registry. Registries may develop different opt-in mechanisms (they could differ by the approval mechanism, be fully permissioned etc..). One SBT smart contract can opt-in to: +We can have multiple competing registries (with different purpose or different management scheme). An SBT issuer SHOULD opt-in to a registry before being able to use registry. Registries may develop different opt-in mechanisms (they could differ by the approval mechanism, be fully permissioned etc..). One SBT smart contract can opt-in to: - many registries: it MUST relay all state change functions to all registries. - or to no registry. We should think about it as a single token registry, and it MUST strictly implement all SBT Registry query functions by itself. The contract address must be part of the arguments, and it must check that it equals to the deployed account address (`require!(ctr == env::current_account_id())`). It also MUST emit related events by itself. From 57b4ac61d3ec9a3c72839e0f72b6f247767ca63a Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Tue, 11 Jul 2023 18:40:57 +0200 Subject: [PATCH 74/89] small updates --- neps/nep-0393.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index 92cd965e4..af233ff12 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -14,7 +14,7 @@ Requires: ## Summary -Soulbound Token (SBT) is a form of a NFT which represents an aspect of an account: _soul_. Transferability is limited only to a case of recoverability or a _soul transfer_. The latter must coordinate with a registry to transfer all SBTs from one account to another, and _banning_ the source account. +Soulbound Token (SBT) is a form of a non-fungible token which represents an aspect of an account: _soul_. [Transferability](#transferability) is limited only to a case of recoverability or a _soul transfer_. The latter must coordinate with a registry to transfer all SBTs from one account to another, and _banning_ the source account. SBTs are well suited for carrying proof-of-attendance, proof-of-unique-human "stamps" and other similar credibility-carriers. @@ -237,7 +237,7 @@ For the Token ID type we propose `u64` rather than `U128`. `u64` capacity is mor Today, the JS integer limit is `2^53-1 ~ 9e15`. Similarly, when minting 10'000 SBTs per second, it will take us 28'561 years to reach the limit. So, we don't need u128 nor the String type. However, if for some reason, we will need to get u64 support for JS, then we can always add another set of methods which will return String, so making it compatible with NFT standard (which is using `U128`, which is a string). Also, it's worth to note, that in 28'000 years JS (if it will still exists) will be completely different. The number of combinations for a single issuer is much higher in fact: the token standard uses classes. So technically that makes the number of all possible combinations for a single issuer equal `(2^64)^2 ~ 1e38`. For "today" JS it is `(2^53-1)^2 ~ 1e31`. -Token IDs should be created in a sequence to make sure the ID space is not exhausted locally (eg if a registry would decide to introduce segments, it would potentially get into a trap where one of the segments is filled up very quickly). +Token IDs must be created in a sequence to make sure the ID space is not exhausted locally (eg if a registry would decide to introduce segments, it would potentially get into a trap where one of the segments is filled up very quickly). ```rust // TokenId and ClassId must be positive (0 is not a valid ID) From 3a410f9231e14df3228c6b779602f4abf678a602 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Wed, 12 Jul 2023 00:13:54 +0200 Subject: [PATCH 75/89] review --- neps/nep-0393.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index af233ff12..37122ab86 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -3,7 +3,7 @@ NEP: 393 Title: Soulbound Token Authors: Robert Zaremba <@robert-zaremba> DiscussionsTo: -Status: Draft +Status: Approved Type: Standards Track Category: Contract Created: 12-Sep-2022 @@ -594,6 +594,19 @@ Implementations must not store any personal information on chain. #### Concerns +| # | Concern | Resolution | Status | +| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | +| 1 | [Robert] Should we Emit NEP-171 Mint and NEP-171 Burn by the SBT contract (in addition to SBT native events emitted by the registry)? If the events will be emitted by registry, then we need new events to include the contract address. | Don't emit NFT events. SBT is not NFT. Support: @alexastrum | resolved | +| 2 | [Robert] remove `memo` in events. The `memo` is already part of the transaction, and should not be needed to identify transactions. Processes looking for events, can easily track transaction through event and recover `memo` if needed. | Removed, consequently also removed from registry transactions . Support: @alexastrum | resolved | +| 3 | [Token Spam](https://github.com/near/NEPs/pull/393/#discussion_r1163938750) | We have a `Burn` event. Added example `sbt_burn` function, but keeping it not as a part of required interface. Event should be enough. | resolved | +| 4 | [Multiple registries](https://github.com/near/NEPs/pull/393/#discussion_r1163951624). Registry source of truth [comment](https://github.com/near/NEPs/pull/393/#issuecomment-1531766643) | This is a part of the design: permissionless approach. [Justification for registry](https://github.com/near/NEPs/pull/393/#issuecomment-1540621077) | resolved | +| 5 | [Robert] Approve the proposed multi-token | Support: @alexastrum | resolved | +| 6 | [Robert] Use of milliseconds as a time unit. | Use milliseconds. | resolved | +| 7 | Should a `burn` function be part of a standard or a recommendation? | We already have the Burn event. A call method should not be part of the standard interface (similarly to FT and NFT). | resolved | +| 8 | [Robert] Don't include `sbt_soul_transfer` in the standard interface, [comment](https://github.com/near/NEPs/pull/393#issuecomment-1506969996). | Moved outside of the required interface. | resolved | +| 9 | [Privacy](https://github.com/near/NEPs/pull/393/#issuecomment-1504309947) | Concerns have been addressed: [comment-1](https://github.com/near/NEPs/pull/393/#issuecomment-1504485420) and [comment2](https://github.com/near/NEPs/pull/393/#issuecomment-1505958549) | resolved | +| 10 | @frol [suggested](https://github.com/near/NEPs/pull/393/#discussion_r1247879778) to use a struct in `sbt_recover` and `sbt_soul_transfer`. | Motivation to use pair `(number, bool)` rather than follow a common Iterator Pattern. Rust uses `Option` type for that, that works perfectly for languages with native Option type, but creates a "null" problem for anything else. Other common way to implement Iterator is the presented pair, which doesn't require extra type definition and reduces code size. | new | + ## Copyright [Creative Commons Attribution 4.0 International Public License (CC BY 4.0)](https://creativecommons.org/licenses/by/4.0/) From 1f5001ea4728dd31c7f24b1af0799fd5203150ee Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Wed, 12 Jul 2023 00:23:07 +0200 Subject: [PATCH 76/89] language --- neps/nep-0393.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index 37122ab86..b01c33333 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -24,7 +24,7 @@ Recent [Decentralized Society](https://www.bankless.com/decentralized-society-de > More importantly, SBTs enable other applications of increasing ambition, such as community wallet recovery, Sybil-resistant governance, mechanisms for decentralization, and novel markets with decomposable, shared rights. We call this richer, pluralistic ecosystem “Decentralized Society” (DeSoc)—a co-determined sociality, where Souls and communities come together bottom-up, as emergent properties of each other to co-create plural network goods and intelligences, at a range of scales. -Creating a strong primitives is necessary to model new innovative systems and decentralized societies. Examples include reputation protocols, non-transferrable certificates, non-transferrable rights, undercollateralized lending, proof-of-personhood, proof-of-attendance, proof-of-skill, one-person-one-vote, fair airdrops & ICOs, universal basic income, non KYC identity systems, Human DAOs and methods for Sybil attack resistance. +Creating strong primitives is necessary to model new innovative systems and decentralized societies. Examples include reputation protocols, non-transferrable certificates, non-transferrable rights, undercollateralized lending, proof-of-personhood, proof-of-attendance, proof-of-skill, one-person-one-vote, fair airdrops & ICOs, universal basic income, non KYC identity systems, Human DAOs and methods for Sybil attack resistance. We propose an SBT standard to model protocols described above. From 673beb60b94cf0068fdf890ca27ae85765559f79 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Wed, 12 Jul 2023 00:26:39 +0200 Subject: [PATCH 77/89] sbt_mint example update --- neps/nep-0393.md | 1 - 1 file changed, 1 deletion(-) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index b01c33333..4f0b62a06 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -506,7 +506,6 @@ We recommend that all functions related to an event will take an optional `memo: ```rust trait SBT { - /// the function should overwrite `metadata.class = class`. /// Must provide enough NEAR to cover registry storage cost. // #[payable] fn sbt_mint( From 3fc344757859bcd6280b92d00c43ffe8d1fd6475 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Wed, 12 Jul 2023 00:41:48 +0200 Subject: [PATCH 78/89] function comments --- neps/nep-0393.md | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index 4f0b62a06..53cc71526 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -353,12 +353,14 @@ trait SBTRegistry { // #[payable] fn sbt_mint(&mut self, token_spec: Vec<(AccountId, Vec)>) -> Vec; - /// sbt_recover reassigns all tokens issued by the caller, from the old owner to a new one. - /// Adds `old_owner` to a banned accounts list. - /// Must not add `old_owner` to a ban list. + /// sbt_recover reassigns all tokens issued by the caller, from the old owner to a new owner. /// Must be called by a valid SBT issuer. - /// Must emit `Recover` event. - /// Requires attaching enough NEAR to cover the storage growth. + /// Must emit `Recover` event once all the tokens have been recovered. + /// Requires attaching enough tokens to cover the storage growth. + /// Returns the amount of tokens recovered and a boolean: `true` if the whole + /// process has finished, `false` when the process has not finished and should be + /// continued by a subsequent call. User must keep calling the `sbt_recover` until `true` + /// is returned. // #[payable] fn sbt_recover(&mut self, from: AccountId, to: AccountId) -> (u32, bool); @@ -383,14 +385,16 @@ Example **Soul Transfer** interface: ```rust /// Transfers atomically all SBT tokens from one account to another account. - /// The caller must be an SBT holder and the `to` must not be a banned account. - /// Returns amount of tokens transferred and a boolean: `true` if the whole - /// process has finished, `false` when the process has not - /// finished and should be continued by a subsequent call. - /// User must keeps calling `sbt_soul_transfer` until `true` is returned. - /// Must emit `SoulTransfer` event only if the caller was in possession of at least one SBT - /// (if caller doesn't have any tokens, the function won't transfer anything, ban the - /// original owner, emit Ban, and NOT emit SoulTransfer). + /// The caller must be an SBT holder and the `recipient` must not be a banned account. + /// Returns the amount of tokens transferred and a boolean: `true` if the whole + /// process has finished, `false` when the process has not finished and should be + /// continued by a subsequent call. + /// Emits `Ban` event for the caller at the beginning of the process. + /// Emits `SoulTransfer` event only once all the tokens from the caller were transferred + /// and at least one token was trasnfered (caller had at least 1 sbt). + /// + User must keep calling the `sbt_soul_transfer` until `true` is returned. + /// + If caller does not have any tokens, nothing will be transfered, the caller + /// will be banned and `Ban` event will be emitted. #[payable] fn sbt_soul_transfer( &mut self, From 1149e982cf402841f7f361fe6807bb50925f41f9 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Thu, 13 Jul 2023 00:23:55 +0200 Subject: [PATCH 79/89] bold must --- neps/nep-0393.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index 53cc71526..3737db506 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -237,7 +237,7 @@ For the Token ID type we propose `u64` rather than `U128`. `u64` capacity is mor Today, the JS integer limit is `2^53-1 ~ 9e15`. Similarly, when minting 10'000 SBTs per second, it will take us 28'561 years to reach the limit. So, we don't need u128 nor the String type. However, if for some reason, we will need to get u64 support for JS, then we can always add another set of methods which will return String, so making it compatible with NFT standard (which is using `U128`, which is a string). Also, it's worth to note, that in 28'000 years JS (if it will still exists) will be completely different. The number of combinations for a single issuer is much higher in fact: the token standard uses classes. So technically that makes the number of all possible combinations for a single issuer equal `(2^64)^2 ~ 1e38`. For "today" JS it is `(2^53-1)^2 ~ 1e31`. -Token IDs must be created in a sequence to make sure the ID space is not exhausted locally (eg if a registry would decide to introduce segments, it would potentially get into a trap where one of the segments is filled up very quickly). +Token IDs MUST be created in a sequence to make sure the ID space is not exhausted locally (eg if a registry would decide to introduce segments, it would potentially get into a trap where one of the segments is filled up very quickly). ```rust // TokenId and ClassId must be positive (0 is not a valid ID) From 66c794f08992a9fb64a73eb83e5c2fdd829e30e1 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Mon, 24 Jul 2023 16:32:22 +0200 Subject: [PATCH 80/89] add sbts method --- neps/nep-0393.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index 3737db506..8960c01fc 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -291,6 +291,10 @@ trait SBTRegistry { /// Get the information about specific token ID issued by `issuer` SBT contract. fn sbt(&self, issuer: AccountId, token: TokenId) -> Option; + /// Get the information about list of token IDs issued by the `issuer` SBT contract. + /// If token ID is not found `None` is set in the specific return index. + fn sbts(&self, issuer: AccountId, token: Vec) -> Vec>; + /// Returns total amount of tokens issued by `issuer` SBT contract, including expired /// tokens. If a revoke removes a token, it must not be included in the supply. fn sbt_supply(&self, issuer: AccountId) -> u64; From fe352b8f3d8544bcca6e0b5d855c8515628a95b7 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Wed, 26 Jul 2023 12:26:11 +0200 Subject: [PATCH 81/89] query: sbt_classes and docs --- neps/nep-0393.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index 8960c01fc..c51e3eb93 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -295,6 +295,10 @@ trait SBTRegistry { /// If token ID is not found `None` is set in the specific return index. fn sbts(&self, issuer: AccountId, token: Vec) -> Vec>; + /// Query class ID for each token ID issued by the SBT `issuer`. + /// If token ID is not found, `None` is set in the specific return index. + fn sbt_classes(&self, issuer: AccountId, tokens: Vec) -> Vec>; + /// Returns total amount of tokens issued by `issuer` SBT contract, including expired /// tokens. If a revoke removes a token, it must not be included in the supply. fn sbt_supply(&self, issuer: AccountId) -> u64; From 7e784a26e16b1314163a4a85e6c365dff415adc6 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Wed, 6 Sep 2023 14:02:44 +0200 Subject: [PATCH 82/89] more docs about sbt_renew --- neps/nep-0393.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index c51e3eb93..507fb6051 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -206,7 +206,7 @@ sequenceDiagram ### Renewal Soulbound tokens can have an _expire date_. It is useful for tokens which are related to real world certificates with expire time, or social mechanisms (e.g. community membership). Such tokens SHOULD have an option to be renewable. Examples include mandatory renewal with a frequency to check that the owner is still alive, or renew membership to a DAO that uses SBTs as membership gating. -Registry defines `sbt_renew` method allowing issuers to update the token expire date. +Registry defines `sbt_renew` method allowing issuers to update the token expire date. The issuer can set a the _expire date_ in the past. This is useful if an issuer wants to invalidate the token without removing it. ### Burning tokens From 51b14d9f9be6e9483e5afd990ff44f2d363c8ce4 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Wed, 6 Sep 2023 15:03:35 +0200 Subject: [PATCH 83/89] update metadata struct comments --- neps/nep-0393.md | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index 507fb6051..08e234665 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -264,22 +264,35 @@ All time related attributes are defined in milliseconds (as per NEP-171). ```rust /// ContractMetadata defines contract wide attributes, which describes the whole contract. pub struct ContractMetadata { - pub spec: String, // required, essentially a version like "sbt-1.0.0" - pub name: String, // required, ex. "Mosaics" - pub symbol: String, // required, ex. "MOSAIC" - pub icon: Option, // Data URL - pub base_uri: Option, // Centralized gateway known to have reliable access to decentralized storage assets referenced by `reference` or `media` URLs - pub reference: Option, // URL to a JSON file with more info - pub reference_hash: Option, // Base64-encoded sha256 hash of JSON from reference field. Required if `reference` is included. + /// Version with namespace, example: "sbt-1.0.0". Required. + pub spec: String, + /// Issuer Name, required, ex. "Mosaics" + pub name: String, + /// Issuer symbol which can be used as a token symbol, eg Ⓝ, ₿, BTC, MOSAIC ... + pub symbol: String, + /// Icon content (SVG) or a link to an Icon. If it doesn't start with a scheme (eg: https://) + /// then `base_uri` should be prepended. + pub icon: Option, + /// URI prefix which will be prepended to other links which don't start with a scheme + /// (eg: ipfs:// or https:// ...). + pub base_uri: Option, + /// JSON or an URL to a JSON file with more info. If it doesn't start with a scheme + /// (eg: https://) then base_uri should be prepended. + pub reference: Option, + /// Base64-encoded sha256 hash of JSON from reference field. Required if `reference` is included. + pub reference_hash: Option, } /// TokenMetadata defines attributes for each SBT token. pub struct TokenMetadata { pub class: ClassId, // token class. Required. Must be non zero. - pub issued_at: Option, // When token was issued or minted, Unix epoch in milliseconds - pub expires_at: Option, // When token expires, Unix epoch in milliseconds - pub reference: Option, // URL to an off-chain JSON file with more info. - pub reference_hash: Option, // Base64-encoded sha256 hash of JSON from reference field. Required if `reference` is included. + pub issued_at: Option, // When token was issued or minted, Unix time in milliseconds + pub expires_at: Option, // When token expires, Unix time in milliseconds + /// JSON or an URL to a JSON file with more info. If it doesn't start with a scheme + /// (eg: https://) then base_uri should be prepended. + pub reference: Option, + /// Base64-encoded sha256 hash of JSON from reference field. Required if `reference` is included. + pub reference_hash: Option, } From ad56e5f4d95100a7d051a2f6c80952f960e1ce1d Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Thu, 7 Sep 2023 00:34:30 +0200 Subject: [PATCH 84/89] add sbt_update_token_references method and event to the standard --- neps/nep-0393.md | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index 08e234665..33d876104 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -399,6 +399,15 @@ trait SBTRegistry { /// Similar to `sbt_revoke`, but revokes all `owner`s tokens issued by the caller. fn sbt_revoke_by_owner(&mut self, owner: AccountId, burn: bool); + + /// Allows issuer to update token metadata reference and reference_hash. + /// * `updates` is a list of triples: (token ID, reference, reference base64-encoded sha256 hash). + /// Must emit `token_reference` event. + /// Panics if any of the token Ids don't exists. + fn sbt_update_token_references( + &mut self, + updates: Vec<(TokenId, Option, Option)>, + ); } ``` @@ -460,14 +469,14 @@ type u64 = number; type Nep393Event { standard: "nep393"; version: "1.0.0"; - event: "mint" | "recover" | "renew" | "revoke" | "burn" | "ban" | "soul_transfer" ; - data: Mint | Recover | Renew | Revoke | Burn | Ban[] | SoulTransfer; + event: "mint" | "recover" | "renew" | "revoke" | "burn" | "ban" | "soul_transfer" | "token_reference" ; + data: Mint | Recover | Renew | Revoke | Burn | Ban[] | SoulTransfer | TokenReference; } /// An event emitted by the Registry when new SBT is created. type Mint { ctr: AccountId; // SBT Contract minting the tokens - tokens: [[AccountId, u64[]]]; // list of pairs (token owner, TokenId[]) + tokens: (AccountId, u64[])[]; // list of pairs (token owner, TokenId[]) } /// An event emitted when a recovery process succeeded to reassign SBTs, usually due to account @@ -519,6 +528,9 @@ type SoulTransfer { from: AccountId; to: AccountId; } + +/// An event emitted when existing token metadata references are updated. +type TokenReference = u64[]; // list of token ids. ``` Whenever a recovery is made in a way that an existing SBT is burned, the `Burn` event MUST be emitted. If `Revoke` burns token then `Burn` event MUST be emitted instead of `Revoke`. From 3ea249b5c23d0d79c3690b88ccce870232d84449 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Thu, 7 Sep 2023 00:53:30 +0200 Subject: [PATCH 85/89] fix: events: should use issuer rather than ctr as per reference implementation --- neps/nep-0393.md | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index 33d876104..c68d36b89 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -475,7 +475,7 @@ type Nep393Event { /// An event emitted by the Registry when new SBT is created. type Mint { - ctr: AccountId; // SBT Contract minting the tokens + issuer: AccountId; // SBT Contract minting the tokens tokens: (AccountId, u64[])[]; // list of pairs (token owner, TokenId[]) } @@ -486,7 +486,7 @@ type Mint { /// token IDs). /// Must be emitted by an SBT registry. type Recover { - ctr: AccountId // SBT Contract recovering the tokens + issuer: AccountId // SBT Contract recovering the tokens old_owner: AccountId; // current holder of the SBT new_owner: AccountId; // destination account. } @@ -494,7 +494,7 @@ type Recover { /// An event emitted when existing tokens are renewed. /// Must be emitted by an SBT registry. type Renew { - ctr: AccountId; // SBT Contract renewing the tokens + issuer: AccountId; // SBT Contract renewing the tokens tokens: u64[]; // list of token ids. } @@ -503,14 +503,14 @@ type Renew { /// a wallet. See also `Burn` event. /// Must be emitted by an SBT registry. type Revoke { - ctr: AccountId; // SBT Contract revoking the tokens + issuer: AccountId; // SBT Contract revoking the tokens tokens: u64[]; // list of token ids. } /// An event emitted when existing tokens are burned and removed from the registry. /// Must be emitted by an SBT registry. type Burn { - ctr: AccountId; // SBT Contract burning the tokens + issuer: AccountId; // SBT Contract burning the tokens tokens: u64[]; // list of token ids. } @@ -529,6 +529,13 @@ type SoulTransfer { to: AccountId; } +/// An event emitted when an issuer updates token metadata reference of existing SBTs. +/// Must be emitted by an SBT registry. +type TokenReference { + issuer: AccountId; // Issuer account + tokens: u64[]; // list of token ids. +} + /// An event emitted when existing token metadata references are updated. type TokenReference = u64[]; // list of token ids. ``` From 9f4c86e51c099353847dc58b060e566e0e0aa767 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Thu, 7 Sep 2023 17:41:14 +0200 Subject: [PATCH 86/89] language --- neps/nep-0393.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index c68d36b89..2c2d895f4 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -403,7 +403,7 @@ trait SBTRegistry { /// Allows issuer to update token metadata reference and reference_hash. /// * `updates` is a list of triples: (token ID, reference, reference base64-encoded sha256 hash). /// Must emit `token_reference` event. - /// Panics if any of the token Ids don't exists. + /// Panics if any of the token IDs don't exist. fn sbt_update_token_references( &mut self, updates: Vec<(TokenId, Option, Option)>, From 7b753f1fbe7fd6c2cb6eef4b1e9899eb5a54f619 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Fri, 8 Sep 2023 03:09:26 +0200 Subject: [PATCH 87/89] add soul_transfer implementation notes --- neps/nep-0393.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index 2c2d895f4..e478da1e7 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -203,6 +203,13 @@ sequenceDiagram SBT_Registry-->>-Alice: [[SBT_1_Contract, [238]], [SBT_2_Contract, [7991, 7992]]]} ``` +Implementation Notes: + +- There is a risk of conflict. The standard requires that one account can't have more than one SBT of the same (issuer, class) pair. +- When both `alice1` and `alice2` have SBT of the same (issuer, class) pair, then the transfer should fail. One of the accounts should burn conflicting tokens to be able to continue the soul transfer. +- Soul transfer may require extra confirmation before executing a transfer. For example, if `alice1` wants to do a soul transfer to `alice2`, the contract my require `alice2` approval before continuing the transfer. +- Other techniques may be used to enforce that the source account will be deleted. + ### Renewal Soulbound tokens can have an _expire date_. It is useful for tokens which are related to real world certificates with expire time, or social mechanisms (e.g. community membership). Such tokens SHOULD have an option to be renewable. Examples include mandatory renewal with a frequency to check that the owner is still alive, or renew membership to a DAO that uses SBTs as membership gating. From 912803f446ca47ec86ea994dc132b0e90dca1a10 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Mon, 11 Sep 2023 23:23:33 +0200 Subject: [PATCH 88/89] add benefits --- neps/nep-0393.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index e478da1e7..68dbf48ac 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -639,8 +639,9 @@ Implementations must not store any personal information on chain. #### Benefits -- Benefit -- Benefit +- SBTs as any other kind of a token are essential primitive to represent real world use cases. This standards provides a model and a guideline for developers to build SBT based solutions. +- Token standards are key for composability. +- Wallet and tools needs a common interface to operate tokens. #### Concerns From ceabc867388576b6453b37557ee621bd74b75de1 Mon Sep 17 00:00:00 2001 From: Vlad Frolov Date: Tue, 12 Sep 2023 17:58:28 +0200 Subject: [PATCH 89/89] Added the decision statement and link to the video recording to the NEP document --- neps/nep-0393.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/neps/nep-0393.md b/neps/nep-0393.md index 68dbf48ac..67b3361c2 100644 --- a/neps/nep-0393.md +++ b/neps/nep-0393.md @@ -637,6 +637,8 @@ Implementations must not store any personal information on chain. ### v1.0.0 +The Contract Standards Working Group members approved this NEP on June 30, 2023 ([meeting recording](https://youtu.be/S1An5CDG154)). + #### Benefits - SBTs as any other kind of a token are essential primitive to represent real world use cases. This standards provides a model and a guideline for developers to build SBT based solutions.