Repository navigation
feat(platform)!: non-transferable tokens (PV14) #5353
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
QuantumExplorer
merged 3 commits into
v5.0-dev
from
claude/yapp-non-transferable-token-36a052
Oct 9, 2026
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| # Non-Transferable Tokens | ||
|
|
||
| From protocol version 14 a token can be non-transferable: once an identity holds it, no one can | ||
| move it to another identity, directly or through a contract. The token can still be minted, | ||
| claimed, bought, burned, frozen and paused as its rules allow, and it can still pay for the | ||
| documents of its own contract, by being burned. Reach for it when a token is a usage allowance, | ||
| points or a reputation score rather than a currency: an app hands it out, users spend it on the | ||
| app's own actions, and it never becomes something to sell or give away. | ||
|
|
||
| ## Configuration | ||
|
|
||
| `TokenConfiguration` format version 1 carries `transferable: bool`, `true` when absent. A | ||
| version 0 configuration is always transferable, so only a format version 1 configuration can set | ||
| it to `false`, and protocol versions before 14 refuse that format at contract create and update. | ||
|
|
||
| ```json | ||
| { | ||
| "$formatVersion": "1", | ||
| "baseSupply": 0, | ||
| "transferable": false, | ||
| ... | ||
| } | ||
| ``` | ||
|
|
||
| The flag is fixed when the token is created. No `TokenConfigUpdate` item changes it, and a | ||
| contract update cannot change an existing token's configuration. | ||
|
|
||
| ## What a non-transferable token still does | ||
|
|
||
| - **Minting.** The minting rules decide who mints and whether they may choose the destination, | ||
| as for any token. This is how an issuer hands the token out; a base supply is minted to | ||
| `newTokensDestinationIdentity`, or to the contract owner when that is unset, and whoever | ||
| receives it cannot send it on either. | ||
| - **Distributions and claims.** Perpetual, pre-programmed and once-per-identity distributions mint | ||
| to their recipients. | ||
| - **Direct purchase.** A purchase mints the tokens to the buyer and pays the price in credits. | ||
| - **Burning, freezing, unfreezing, destroying frozen funds and pausing**, under their own rules. | ||
| - **Paying for documents by burning.** A document type of the token's own contract may charge it | ||
| with `effect: 1` ([`tokenCost`](../contract-keywords/token-cost.md#effect-transfer-or-burn)). | ||
|
|
||
| ## What is refused | ||
|
|
||
| | Operation | Refused with | Where | | ||
| |---|---|---| | ||
| | A `TokenTransfer` of the token | `TokenNotTransferableError` (40726) | Batch advanced structure validation, from the contract the action carries: the mempool refuses it, and a block charges the signer and bumps the nonce | | ||
| | A document type of the token's own contract charging it with `effect: 0` (pay the contract owner) | `NonTransferableTokenPaymentMustBurnError` (10280) | Contract create and update, when the document type is parsed. A document payment refuses it again (`TokenNotTransferableError`, 40726) from the configuration the contract already carries, so a contract that bypassed registration cannot pay with it either | | ||
| | A document type of another contract charging it | `TokenNotTransferableError` (40726) | Contract create and update state validation, which already reads the token's contract to check the token exists. Such a cost always pays the contract owner, since another contract's token cannot be burned (10261) | | ||
| | `hasShieldedPool: true` | `NonTransferableTokenShieldedPoolError` (10279) | Contract create and update, before anything is charged | | ||
|
|
||
| A shielded pool is refused because a note spent inside the pool can be unshielded to any | ||
| identity, so shielding and unshielding would move the token between holders. | ||
|
|
||
| Every rule is checked when the contract is registered or when the transfer arrives, and the flag | ||
| cannot change afterwards: a document type that could pay the owner in a non-transferable token | ||
| is never registered. A payment in the contract's own token checks the flag again at no read | ||
| cost; another contract's token is not read at payment time, since a cost in it was refused at | ||
| registration. | ||
|
|
||
| A non-transferable token has no pool, so the `TokenConfigUpdate` items that change the pool's | ||
| `minimumPoolNotesForOutgoing` and its rules are refused for it, even though its configuration is | ||
| format version 1. | ||
|
|
||
| ## Why not pause the token | ||
|
|
||
| A paused token refuses transfers, but from protocol version 14 a paused token also refuses every | ||
| document payment, burns included (`TokenIsPausedError`, 40711). Before protocol version 14 a | ||
| paused token could still pay for documents, and since any contract may charge another contract's | ||
| token and pay it to its own owner, anyone could move a paused token by registering such a | ||
| contract. A non-transferable token closes that path at registration and keeps its document | ||
| payments working. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
11 changes: 9 additions & 2 deletions
11
packages/rs-dpp/src/data_contract/associated_token/token_configuration/accessors/v1/mod.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,18 +1,25 @@ | ||
| /// Getters introduced by `TokenConfigurationV1`. | ||
| /// | ||
| /// Implemented for every configuration version: a V0 configuration answers `false` and 0, | ||
| /// since neither the shielded pool opt-in nor its threshold existed before V1. | ||
| /// Implemented for every configuration version: a V0 configuration answers `false`, 0 and | ||
| /// transferable, since neither the shielded pool opt-in, its threshold nor non-transferable | ||
| /// tokens existed before V1. | ||
| pub trait TokenConfigurationV1Getters { | ||
| /// Whether the token has its own shielded pool. | ||
| fn has_shielded_pool(&self) -> bool; | ||
|
|
||
| /// The fewest notes the token's shielded pool must hold before tokens may leave it for a | ||
| /// visible destination; 0 when the configuration sets none. | ||
| fn minimum_pool_notes_for_outgoing(&self) -> u64; | ||
|
|
||
| /// Whether holders may move the token to another identity. | ||
| fn is_transferable(&self) -> bool; | ||
| } | ||
|
|
||
| /// Setters introduced by `TokenConfigurationV1`. | ||
| pub trait TokenConfigurationV1Setters { | ||
| /// Sets whether the token has its own shielded pool. | ||
| fn set_has_shielded_pool(&mut self, has_shielded_pool: bool); | ||
|
|
||
| /// Sets whether holders may move the token to another identity. | ||
| fn set_transferable(&mut self, transferable: bool); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.