- 1. Introduction
- 2. Ledger Entries
- 3. Transactions
- 4. MPT Payment Execution
- 5. Confidential MPT Balances (ConfidentialTransfer Amendment)
Multi-Purpose Tokens (MPTs) are a native asset type on the XRP Ledger designed to provide an alternative to traditional trust line-based tokens. An MPT consists of an MPTokenIssuance that defines the MPT's properties and configuration, and individual MPToken entries that track each holder's balance and holder-specific settings. The issuer does not hold an MPToken for its own issuance. The total amount in circulation is tracked on the MPTokenIssuance via OutstandingAmount, which is adjusted directly when the issuer mints to or burns from holders.
An MPT moves through the following lifecycle:
- Create the issuance. The issuer sends an
MPTokenIssuanceCreatetransaction, which defines the MPT's capability flags (such aslsfMPTRequireAuth,lsfMPTCanTransfer, andlsfMPTCanClawback) and optional properties likeTransferFeeandMaximumAmount. Capability flags are immutable by default, but the issuer can grant mutability permissions at creation time to allow changing them later viaMPTokenIssuanceSet. See MPTokenIssuance Fields. - Authorize holders. A holder sends an
MPTokenAuthorizetransaction to create theirMPTokenentry. If the issuance haslsfMPTRequireAuthset, the entry is created but not yet authorized. The issuer must then send their ownMPTokenAuthorizenaming the holder to setlsfMPTAuthorized(on the holder'sMPToken) before the holder can receive MPTs. IflsfMPTRequireAuthis not set, the holder can receive MPTs as soon as the entry exists. See MPToken Fields and Reserves. - Transfer. Once authorized (if required), MPTs are transferred between holders via the standard
Paymenttransaction, applying the issuer'sTransferFeeif configured. See MPT Payment Execution.
Throughout the MPT's life, the issuer retains control through the MPTokenIssuanceSet transaction: global locking (the lsfMPTLocked flag on the MPTokenIssuance) prevents all holders from transferring the MPT, and individual holder locking (the lsfMPTLocked flag on a holder's MPToken) prevents a single holder from transferring it. Both require the issuance to have the lsfMPTCanLock capability flag; neither prevents the issuer from minting to or redeeming from holders (issuer <-> holder transfers remain available even when locked). See Flags and Payment validation rules.
The MPTokenIssuance ledger entry has an optional DomainID field that controls MPT authorization - determining who can hold and receive the MPT. When DomainID is set on an MPTokenIssuance:
- Accounts must have valid, non-expired credentials for the specified PermissionedDomain to be authorized as MPT holders
- Authorization is verified whenever an account would receive or acquire the MPT. This includes
Payment, offer creation and crossing, AMM operations, escrow, and cross-currency flow steps - Requires both
lsfMPTRequireAuthflag (see Flags) and thefeaturePermissionedDomainsandfeatureSingleAssetVaultamendments
This MPT authorization mechanism is separate and independent from PermissionedDEX domain restrictions on offers. The MPTokenIssuance.DomainID controls who can hold and receive the MPT (verified whenever an account receives the MPT), while Offer.DomainID controls which order book an offer is placed in and restricts liquidity consumption to that domain's order book during payments and offer crossing (see Domain Payments). MPTs can be traded in domain offers, open offers, and hybrid offers regardless of whether the MPTokenIssuance has a DomainID set - creating an offer with Offer.DomainID requires the offer creator to have domain access, but does not require the MPT itself to have a DomainID.
MPTs use two ledger entry types: MPTokenIssuance for the MPT definition and MPToken for individual holder balances.
classDiagram
class MPTokenIssuance {
+AccountID Issuer
+uint32 Sequence
+uint16 TransferFee
+uint64 OwnerNode
+uint8 AssetScale
+uint64 MaximumAmount
+uint64 OutstandingAmount
+uint64 LockedAmount
+Blob MPTokenMetadata
+uint32 Flags
+uint32 ImmutableFlags
+uint256 DomainID
}
class MPToken {
+AccountID Account
+uint192 MPTokenIssuanceID
+uint64 MPTAmount
+uint64 LockedAmount
+uint64 OwnerNode
+uint32 Flags
}
class IssuerAccount {
+AccountID Account
+XRPAmount Balance
+uint32 OwnerCount
}
class HolderAccount {
+AccountID Account
+XRPAmount Balance
+uint32 OwnerCount
}
class DirectoryNode {
}
MPTokenIssuance --> IssuerAccount : issued by
MPToken --> HolderAccount : held by
MPToken --> MPTokenIssuance : references
IssuerAccount --> DirectoryNode : owner directory
HolderAccount --> DirectoryNode : owner directory
DirectoryNode --> MPTokenIssuance : contains
DirectoryNode --> MPToken : contains
The MPTokenIssuance ledger entry (type ltMPTOKEN_ISSUANCE = 0x007e) represents the definition and global state of a specific MPT. An "issuance" is a single MPT type created by an issuer - it defines the token's properties (transfer fee, maximum supply, decimal places, capability flags) and tracks the token's global state (total outstanding amount, lock status). Each issuance is uniquely identified by its MPTID.
An issuer can create multiple separate MPT issuances, each with its own unique MPTID, independent configuration, and distinct set of holders. Each issuance tracks its own OutstandingAmount and has its own capability flags.
The key of the MPTokenIssuance object is the result
of SHA512-Half of the following values
concatenated in order:
- The
MPTokenIssuancespace key0x007E - The MPTID (192-bit identifier)
The MPTID is calculated as:
MPTID = sequence (32 bits, big-endian) || issuer AccountID (160 bits)
Where sequence is the issuer's sequence number at creation time and issuer is the 160-bit AccountID
| Field | Type | Required | Description |
|---|---|---|---|
Issuer |
AccountID | Yes | The account that created this MPT issuance |
Sequence |
UInt32 | Yes | The issuer's sequence number at creation (used in MPTID calculation) |
TransferFee |
UInt16 | Default | Transfer fee in 1/10 of basis points (0-50000, representing 0%-50%) |
OwnerNode |
UInt64 | Yes | Index of the owner directory page for this issuance |
AssetScale |
UInt8 | Default | Number of decimal places for display. Display hint only; does not affect on-ledger integer arithmetic. |
MaximumAmount |
UInt64 | Optional | Maximum supply cap (must be > 0, cannot exceed 0x7FFFFFFFFFFFFFFF) |
OutstandingAmount |
UInt64 | Yes | Total amount in circulation. Equals the sum of every holder's balance, including escrow-locked amounts.1 |
LockedAmount |
UInt64 | Optional | Amount currently locked in escrows |
MPTokenMetadata |
Blob | Optional | Arbitrary metadata (1-1024 bytes) |
ImmutableFlags |
UInt32 | Default | Capability flags and fields declared immutable (see Flags, requires the DynamicMPT amendment) |
IssuerEncryptionKey |
Blob | Optional | Issuer's ElGamal public key for confidential balances (ConfidentialTransfer amendment, see section 5) |
AuditorEncryptionKey |
Blob | Optional | Auditor's ElGamal public key, installed together with the issuer key. Requires an AuditorEncryptedAmount on Convert, ConvertBack, and Send (ConfidentialTransfer amendment) |
ConfidentialOutstandingAmount |
UInt64 | Default | Supply currently held in confidential form. Counted within OutstandingAmount and never exceeds it (ConfidentialTransfer amendment) |
DomainID |
UInt256 | Optional | Permissioned domain identifier for MPT authorization (see DomainID and Authorization) |
ReferenceHolding |
UInt256 | Optional | Vault-share issuances only. Points to the vault pseudo-account's holding of the underlying asset (an MPToken or RippleState). Set internally by VaultCreate. canTrade and canTransfer follow it so the share inherits the underlying asset's tradability and transferability. |
PreviousTxnID |
UInt256 | Yes | Transaction hash that most recently modified this entry |
PreviousTxnLgrSeq |
UInt32 | Yes | Ledger sequence of the transaction that most recently modified this entry |
Field constraints:
TransferFee: If non-zero, requireslsfMPTCanTransferflag and is incompatible withlsfMPTCanHoldConfidentialBalanceMaximumAmount: If set,OutstandingAmountcannot exceed itMPTokenMetadata: Maximum 1024 bytesDomainID: If set, requireslsfMPTRequireAuthand bothfeaturePermissionedDomainsandfeatureSingleAssetVaultamendments
| Flag Name | Hex Value | Description |
|---|---|---|
lsfMPTLocked |
0x00000001 |
Global lock. Prevents all holders from transferring the MPT. Direct issuer/holder transfers still allowed. |
lsfMPTCanLock |
0x00000002 |
Permits the issuer to lock the MPT, globally or per holder, via MPTokenIssuanceSet. |
lsfMPTRequireAuth |
0x00000004 |
Holders must be authorized by issuer before transacting |
lsfMPTCanEscrow |
0x00000008 |
MPT can be held in escrow |
lsfMPTCanTrade |
0x00000010 |
MPT can be traded on the decentralized exchange |
lsfMPTCanTransfer |
0x00000020 |
MPT can be transferred between accounts |
lsfMPTCanClawback |
0x00000040 |
Issuer can claw back MPTs from holders |
lsfMPTCanHoldConfidentialBalance |
0x00000080 |
Holders can keep part of their balance in encrypted form (ConfidentialTransfer amendment, see section 5) |
Flag Mutability:
-
The
lsfMPTLockedflag is always mutable and can be set/cleared by the issuer via theMPTokenIssuanceSettransaction. -
All other capability flags,
MPTokenMetadata, andTransferFeecannot be changed after creation without the DynamicMPT amendment: anyMPTokenIssuanceSetcarrying such a change fails withtemDISABLED. With the amendment enabled, previously unset capability flags can be enabled later (never cleared), and the metadata and transfer fee can be changed, unless declared immutable. The issuer declares immutability per flag or field through theImmutableFlagsfield, at creation or later. Immutability bits can only be added, never removed. The checks read the bits already stored on the ledger entry, so a single transaction can enable a capability and declare it immutable at the same time.
lsfMPTCanTrade and lsfMPTCanTransfer distinction:
These flags control different, independent aspects of MPT movement:
-
lsfMPTCanTrade: Required for all DEX operations. When set, the MPT can be listed in offers via OfferCreate (MPT/XRP, MPT/IOU, MPT/MPT pairs), deposited into AMM pools, and used in cross-currency payments through order books and AMMs. Without this flag, all DEX operations fail withtecNO_PERMISSION, regardless of whetherlsfMPTCanTransferis set. DEX operations validate this flag throughcanTrade(); lock status and holder authorization are checked separately byisFrozenandrequireAuth. See Section 3.6 for complete validation details. -
lsfMPTCanTransfer: Required for direct holder-to-holder transfers without DEX involvement. When set, holders can send MPTs directly to other holders via Payment transactions using direct balance update logic (no pathfinding, no order books). This flag is only required when neither the sender nor the receiver is the issuer. Transfers involving the issuer (minting from issuer to holder, or burning from holder to issuer) do not require this flag. See MPT Payment Execution for transfer mechanics.
lsfMPTCanTrade |
lsfMPTCanTransfer |
Holder-to-holder transfers | DEX operations (offers, AMMs, cross-currency) |
|---|---|---|---|
| ✅ Set | ✅ Set | ✅ Allowed | ✅ Allowed |
| ✅ Set | ❌ Not set | ❌ Blocked (tecNO_AUTH) |
tecNO_AUTH |
| ❌ Not set | ✅ Set | ✅ Allowed | ❌ Blocked (tecNO_PERMISSION) |
| ❌ Not set | ❌ Not set | ❌ Blocked (tecNO_AUTH) |
❌ Blocked (tecNO_PERMISSION) |
Transfers involving the issuer (minting and burning) are always allowed and require neither flag. A DEX trade runs both checks: canTrade gates the trade, and canTransfer gates the delivery leg to a non-issuer holder.
MPTokenIssuance entries do not require pseudo-accounts. The issuer is always a regular account on the ledger.
The MPTokenIssuance entry is owned by the issuer account and linked to the issuer's owner directory. The OwnerNode field stores the directory page index where this issuance appears.
The owner directory is calculated as:
OwnerDirectory Key = SHA512-Half(0x004F, Issuer AccountID)
Where 0x004F is the OwnerDirectory space key (uppercase 'O').
Creating an MPTokenIssuance always requires one owner reserve. The issuer's OwnerCount is incremented when the issuance is created and decremented when it is destroyed.
Under the Sponsor amendment, the issuance's reserve can be covered by a reserve sponsor recorded in the entry's Sponsor field. The sponsor, rather than the issuer, then bears the reserve, and destroying the issuance releases it against the recorded sponsor. The sponsorship mechanism is described in the transactions documentation.2
The MPToken ledger entry (type ltMPTOKEN = 0x007f)3 tracks an individual holder's balance and settings for a specific MPT issuance.
The key of the MPToken object is the result
of SHA512-Half of the following values
concatenated in order:
- The
MPTokenspace key0x0074 - The
MPTokenIssuancekey - The holder's
AccountID
The MPTokenIssuance key is calculated as SHA512-Half(0x007E, MPTID) where 0x007E is the MPTokenIssuance space key and MPTID is the 192-bit issuance identifier.4
| Field | Type | Required | Description |
|---|---|---|---|
Account |
AccountID | Yes | The holder's account |
MPTokenIssuanceID |
UInt192 | Yes | Reference to the MPT issuance (MPTID) |
MPTAmount |
UInt64 | Default | Available (spendable) balance held by this account (max 0x7FFFFFFFFFFFFFFF) |
LockedAmount |
UInt64 | Optional | Portion of the holder's balance locked in MPT escrows, held separately from MPTAmount |
HolderEncryptionKey |
Blob | Optional | Holder's ElGamal public key, registered on first conversion (ConfidentialTransfer amendment) |
ConfidentialBalanceSpending |
Blob | Optional | Spendable confidential balance, encrypted to the holder's key (ConfidentialTransfer amendment) |
ConfidentialBalanceInbox |
Blob | Optional | Received confidential funds awaiting merge (ConfidentialTransfer amendment) |
IssuerEncryptedBalance |
Blob | Optional | Mirror of the confidential balance encrypted to the issuer's key (ConfidentialTransfer amendment) |
AuditorEncryptedBalance |
Blob | Optional | Mirror encrypted to the auditor's key, when the issuance has one (ConfidentialTransfer amendment) |
ConfidentialBalanceVersion |
UInt32 | Default | Zero on first conversion, incremented whenever the confidential spending state is operated on, protecting proofs against replay (ConfidentialTransfer amendment) |
OwnerNode |
UInt64 | Yes | Index of the owner directory page for this holder |
PreviousTxnID |
UInt256 | Yes | Transaction hash that most recently modified this entry |
PreviousTxnLgrSeq |
UInt32 | Yes | Ledger sequence of the transaction that most recently modified this entry |
Field constraints:
MPTAmount: bounded bykMaxMpTokenAmount(2^63-1,0x7FFFFFFFFFFFFFFF). The issuance-wideMaximumAmount(if set) caps the issuance's totalOutstandingAmountacross all holders, not any single holder's balance.5LockedAmount: only ever populated by MPT escrows. Escrow creation requireslsfMPTCanEscrowon the issuance, so a non-zeroLockedAmountimplies the issuance haslsfMPTCanEscrowset.6
| Flag Name | Hex Value | Description |
|---|---|---|
lsfMPTLocked |
0x00000001 |
Individual lock (holder cannot send or receive) |
lsfMPTAuthorized |
0x00000002 |
Authorized by issuer (for lsfMPTRequireAuth issuances) |
lsfMPTAMM |
0x00000004 |
MPToken is held by an AMM pseudo-account |
The lsfMPTLocked flag can only be set if the issuance has lsfMPTCanLock flag set.
The lsfMPTAuthorized flag is only relevant when the issuance has lsfMPTRequireAuth flag set.
The lsfMPTAMM flag is automatically set when an AMM creates an MPToken for an MPT in its pool (which requires the MPTokensV2 amendment). The AMM's MPToken is created with both lsfMPTAMM and lsfMPTAuthorized, even when the issuance has lsfMPTRequireAuth. AMM pseudo-accounts, like Vault and LoanBroker pseudo-accounts, are implicitly authorized, so requireAuth succeeds for them and they can hold a require-auth MPT. This implicit authorization cannot be revoked. Under the fixCleanup3_3_0 amendment, the exemption applies at the start of the authorization check: a pseudo-account passes requireAuth even without an existing MPToken entry and skips the vault-share and permissioned-domain checks. Without the amendment, the exemption only bypasses the final lsfMPTAuthorized flag test, so a pseudo-account with no MPToken entry fails strong authorization with tecNO_AUTH.7 An MPTokenAuthorize transaction naming an AMM, Vault, or LoanBroker pseudo-account as Holder fails with tecNO_PERMISSION. See AMM documentation for AMM pseudo-account behavior.8
MPToken entries do not require pseudo-accounts. The holder is a regular ledger account.
The MPToken entry is owned by the holder account and linked to the holder's owner directory. The OwnerNode field stores the directory page index where this entry appears.
MPToken reserves are determined by the account's total OwnerCount:
- OwnerCount < 2: No reserve required for creating
MPToken - OwnerCount >= 2: One owner reserve per
MPToken
The holder's OwnerCount is always incremented when an MPToken is created and decremented when deleted. This differs from trust lines, which only increment OwnerCount when in non-default state. The reserve grace for the first two owned items, however, mirrors trust-line reserve behavior: no incremental reserve is enforced while OwnerCount is below 2.9
Under the Sponsor amendment, an MPToken created by a sponsored MPTokenAuthorize records the sponsor in its Sponsor field, and the sponsor bears the reserve. The reserve waiver for fewer than two owned objects does not apply to a sponsored transaction. MPToken entries auto-created during payment execution and offer crossing are never sponsored.10
The MPTokenIssuanceCreate transaction creates a new MPT issuance with specified properties and capabilities.
| Field Name | Required? | Modifiable? | JSON Type | Internal Type | Default Value | Description |
|---|---|---|---|---|---|---|
TransactionType |
✔️ | No |
String |
UInt16 |
Must be "MPTokenIssuanceCreate" |
|
Account |
✔️ | No |
String |
AccountID |
Account creating the issuance (becomes the issuer) | |
AssetScale |
No |
Number |
UInt8 |
0 |
Number of decimal places for display. Any UInt8 value (0-255) is accepted. |
|
TransferFee |
Conditional |
Number |
UInt16 |
0 |
Transfer fee in tenths of a basis point (0-50000 inclusive, representing 0%-50%). Must not be present if tfMPTCanTransfer is not set. |
|
MaximumAmount |
No |
String - Number |
UInt64 |
Maximum supply cap. Valid range: 1 to 2^63-1. | ||
MPTokenMetadata |
Conditional |
String - Hexadecimal |
Blob |
Arbitrary metadata (1-1024 bytes). By convention, should decode to JSON describing what the MPT represents. | ||
DomainID |
No |
String - Hexadecimal |
UInt256 |
Permissioned domain identifier (requires amendments) | ||
Flags |
Conditional |
Number |
UInt32 |
0 |
Capability flags | |
ImmutableFlags |
No |
Number |
UInt32 |
0 |
Capability flags and fields declared immutable. Requires DynamicMPT amendment. |
Conditional modifiability (in the table above) means the field or its capability flags can be changed after creation via MPTokenIssuanceSet under the DynamicMPT amendment, unless the matching tif* bit is declared in ImmutableFlags. Capability flags can only be enabled later, never cleared.
Transaction Flags (Capability Flags):
| Flag Name | Hex Value | Description |
|---|---|---|
tfMPTCanLock |
0x00000002 |
Enable individual holder locking |
tfMPTRequireAuth |
0x00000004 |
Require authorization before holders can transact |
tfMPTCanEscrow |
0x00000008 |
Enable escrow functionality |
tfMPTCanTrade |
0x00000010 |
Enable trading on DEX |
tfMPTCanTransfer |
0x00000020 |
Enable transfers between accounts |
tfMPTCanClawback |
0x00000040 |
Enable issuer clawback |
tfMPTCanHoldConfidentialBalance |
0x00000080 |
Enable confidential balances (ConfidentialTransfer amendment) |
There is no flag to lock the issuance at creation. lsfMPTLocked is intentionally not settable here; an issuance is always created unlocked and can be locked later via MPTokenIssuanceSet.
ImmutableFlags (Immutability Declarations):
These bits declare a capability flag or field immutable. Anything not declared immutable can be changed later via MPTokenIssuanceSet under the DynamicMPT amendment (capability flags can only be enabled, never cleared):
| Flag Name | Hex Value | Description |
|---|---|---|
tifMPTCanLock |
0x00000002 |
lsfMPTCanLock can never be enabled later |
tifMPTRequireAuth |
0x00000004 |
lsfMPTRequireAuth can never be enabled later |
tifMPTCanEscrow |
0x00000008 |
lsfMPTCanEscrow can never be enabled later |
tifMPTCanTrade |
0x00000010 |
lsfMPTCanTrade can never be enabled later |
tifMPTCanTransfer |
0x00000020 |
lsfMPTCanTransfer can never be enabled later |
tifMPTCanClawback |
0x00000040 |
lsfMPTCanClawback can never be enabled later |
tifMPTCanHoldConfidentialBalance |
0x00000080 |
lsfMPTCanHoldConfidentialBalance can never be enabled later (also requires the ConfidentialTransfer amendment) |
tifMPTMetadata |
0x00010000 |
MPTokenMetadata can never be changed |
tifMPTTransferFee |
0x00020000 |
TransferFee can never be changed |
Static validation11
temDISABLED:- MPTokensV1 amendment is not enabled
DomainIDis specified but amendments not enabled (requires both PermissionedDomains and SingleAssetVault)ImmutableFlagsis specified but DynamicMPT amendment is not enabledtfMPTCanHoldConfidentialBalanceortifMPTCanHoldConfidentialBalanceis specified but the ConfidentialTransfer amendment is not enabled
temINVALID_FLAG:Flagscontains a bit outside the allowed capability-flag setImmutableFlagsis present but is zero, or contains a bit outside the allowed immutability-flag set (whenImmutableFlagsis included, at least one valid bit must be set)
temBAD_TRANSFER_FEE:TransferFeeexceeds 50000 (50%), or is non-zero withtfMPTCanHoldConfidentialBalancesettemMALFORMED:TransferFeeis non-zero buttfMPTCanTransferis not setDomainIDis specified but is zero (must omit field if not using domains)DomainIDis specified buttfMPTRequireAuthis not setMPTokenMetadatalength is not between 1 and 1024 bytesMaximumAmountis zero or exceeds0x7FFFFFFFFFFFFFFFReferenceHoldingis present (it is set internally byVaultCreateonly and cannot be supplied by user transactions; gated on thefixCleanup3_2_0amendment)
Validation during doApply12
tecINSUFFICIENT_RESERVE: the account's pre-fee balance is below the reserve required for one additional owned object (base reserve plus per-owner increments). For a sponsored reserve, the sponsor has insufficient XRP or the pre-funded sponsorship has no remaining owner-count allowancetecDIR_FULL: Owner directory is full and cannot accommodate the new issuancetecINTERNAL: Signing account does not exist
-
MPTokenIssuanceobject is created:Issuer: Set to signing accountSequence: Set to the transaction's sequence value (the accountSequence, or theTicketSequenceif submitted with a Ticket). This same value feeds the MPTID derivation.OutstandingAmount: Set to 0OwnerNode: Set to directory page indexFlags: Set to transaction flags (excluding universal flags)ImmutableFlags: Set if providedAssetScale: Set if providedTransferFee: Set if providedMaximumAmount: Set if providedMPTokenMetadata: Set if providedDomainID: Set if providedSponsor: Set to the reserve sponsor (only when the transaction's reserve is sponsored)
-
Issuer's
AccountRootis modified:OwnerCount: Incremented by 1
-
DirectoryNodeis created or modified:- Issuance is added to issuer's owner directory
- If issuer has no owner directory, one is created
The MPTokenIssuanceDestroy transaction deletes an MPT issuance. This can only be done when no MPTs are outstanding (all holders have zero balances and no MPTs are locked in escrow).
| Field Name | Required? | Modifiable? | JSON Type | Internal Type | Default Value | Description |
|---|---|---|---|---|---|---|
TransactionType |
✔️ | No |
String |
UInt16 |
Must be "MPTokenIssuanceDestroy" |
|
Account |
✔️ | No |
String |
AccountID |
Account submitting the transaction (must be the issuer) | |
MPTokenIssuanceID |
✔️ | No |
String |
UInt192 |
The MPTID of the issuance to destroy | |
Flags |
No |
Number |
UInt32 |
0 |
No transaction-specific flags; only universal flags (e.g. tfFullyCanonicalSig) are permitted |
Static validation13
temDISABLED: MPTokensV1 amendment is not enabledtemINVALID_FLAG: Any non-universal flags specified
Validation against the ledger view14
tecOBJECT_NOT_FOUND:MPTokenIssuancewith specified MPTID does not existtecNO_PERMISSION: Signing account is not the issuertecHAS_OBLIGATIONS:OutstandingAmountis non-zero (tokens still held by holders)LockedAmountis non-zero (tokens locked in escrow)
The LockedAmount check is a defensive guard: escrow-locked tokens remain counted in OutstandingAmount, so the OutstandingAmount check already catches an issuance with locked tokens (this branch is unreachable in practice).
Validation during doApply15
tecINTERNAL: Signing account is not the issuertefBAD_LEDGER: Failed to remove issuance from owner directory (indicates ledger corruption)
Both doApply errors are defensive: preclaim already guarantees the issuer matches and the issuance exists, so neither is reachable in normal operation.
-
MPTokenIssuanceobject is deleted:- Issuance is removed from the ledger
-
Issuer's
AccountRootis modified:OwnerCount: Decremented by 1
-
DirectoryNodeis modified:- Issuance is removed from issuer's owner directory
- If owner directory becomes empty, it may be deleted
The MPTokenIssuanceSet transaction is sent by the issuer only to modify mutable properties of an MPT issuance or individual MPToken entries. It can:
- Lock/unlock the entire issuance (global lock)
- Lock/unlock individual holders
- Set/clear the
DomainIDfield - Enable capability flags, change the metadata and transfer fee, and extend the immutability declarations (requires the DynamicMPT amendment; anything declared immutable in
ImmutableFlagscannot be changed)
| Field Name | Required? | Modifiable? | JSON Type | Internal Type | Default Value | Description |
|---|---|---|---|---|---|---|
TransactionType |
✔️ | No |
String |
UInt16 |
Must be "MPTokenIssuanceSet" |
|
Account |
✔️ | No |
String |
AccountID |
Account submitting the transaction (must be the issuer) | |
MPTokenIssuanceID |
✔️ | No |
String |
UInt192 |
The MPTID of the issuance to modify | |
Holder |
No |
String |
AccountID |
If present, modifies holder's MPToken; otherwise modifies MPTokenIssuance |
||
DomainID |
Yes |
String |
UInt256 |
Set/clear domain (only when Holder not present, set to zero to clear) |
||
MPTokenMetadata |
Yes |
String - Hexadecimal |
Blob |
Change metadata (fails if tifMPTMetadata is declared) |
||
TransferFee |
Yes |
Number |
UInt16 |
Change transfer fee (fails if tifMPTTransferFee is declared; a non-zero fee requires lsfMPTCanTransfer, already set or enabled by this transaction) |
||
ImmutableFlags |
No |
Number |
UInt32 |
0 |
Declare additional capability flags or fields immutable (bits are added, never removed) | |
IssuerEncryptionKey |
No |
String - Hexadecimal |
Blob |
Install the issuer's ElGamal key, once (ConfidentialTransfer amendment) | ||
AuditorEncryptionKey |
No |
String - Hexadecimal |
Blob |
Install the auditor's ElGamal key, only together with IssuerEncryptionKey (ConfidentialTransfer amendment) |
||
Flags |
No |
Number |
UInt32 |
0 |
Lock/unlock flags and set-only capability flags (see below) |
Transaction Flags:
| Flag Name | Hex Value | Description |
|---|---|---|
tfMPTLock |
0x00000001 |
Lock the issuance or holder |
tfMPTUnlock |
0x00000002 |
Unlock the issuance or holder |
tfMPTSetCanLock |
0x00000004 |
Enable lsfMPTCanLock (DynamicMPT) |
tfMPTSetRequireAuth |
0x00000008 |
Enable lsfMPTRequireAuth (DynamicMPT) |
tfMPTSetCanEscrow |
0x00000010 |
Enable lsfMPTCanEscrow (DynamicMPT) |
tfMPTSetCanTrade |
0x00000020 |
Enable lsfMPTCanTrade (DynamicMPT) |
tfMPTSetCanTransfer |
0x00000040 |
Enable lsfMPTCanTransfer (DynamicMPT) |
tfMPTSetCanClawback |
0x00000080 |
Enable lsfMPTCanClawback (DynamicMPT) |
tfMPTSetCanHoldConfidentialBalance |
0x00000100 |
Enable lsfMPTCanHoldConfidentialBalance (DynamicMPT and ConfidentialTransfer) |
The tfMPTSet* flags only enable capability flags. A capability flag can never be cleared. Enabling a flag whose tif* bit is declared in ImmutableFlags fails with tecNO_PERMISSION.
Behavior:
- When
Holderis NOT specified: Modifies theMPTokenIssuance(global lock/unlock,DomainID, or DynamicMPT mutations: enabling capability flags, changing metadata or transfer fee, extendingImmutableFlags) - When
Holderis specified: Modifies the holder'sMPToken(individual lock/unlock only)
Lock requirements:
- Lock or unlock (global or individual) requires the issuance to have
lsfMPTCanLock.
Static validation16
temDISABLED:- MPTokensV1 amendment is not enabled
DomainIDspecified but amendments not enabled (requires PermissionedDomains and SingleAssetVault)- Mutations (any
tfMPTSet*flag,ImmutableFlags,MPTokenMetadata, orTransferFee) specified but DynamicMPT amendment is not enabled IssuerEncryptionKey,AuditorEncryptionKey,tfMPTSetCanHoldConfidentialBalance, ortifMPTCanHoldConfidentialBalancespecified but the ConfidentialTransfer amendment is not enabled
temMALFORMED:- Both
DomainIDandHolderspecified (mutually exclusive) AccountequalsHolder(cannot lock own MPToken)- With SingleAssetVault, DynamicMPT, or ConfidentialTransfer, must specify at least one of:
tfMPTLock,tfMPTUnlock,DomainID, encryption keys, or mutations (transaction must change something) Holderfield present with mutations or encryption keys (mutually exclusive)tfMPTLockortfMPTUnlockcombined with mutations (cannot lock/unlock while mutating)MPTokenMetadataexceeds 1024 bytesAuditorEncryptionKeywithoutIssuerEncryptionKeyin the same transaction- An encryption key that is not a valid compressed EC point
- Both
temINVALID_FLAG:- Both
tfMPTLockandtfMPTUnlockspecified - Invalid flags specified
ImmutableFlagsis zero or contains invalid flags
- Both
temBAD_TRANSFER_FEE:TransferFeeexceeds 50000 (50%), or a non-zeroTransferFeeis combined withtfMPTSetCanHoldConfidentialBalance
Validation against the ledger view17
terNO_ACCOUNT: Signing account does not exist (enforced by the base transactor, beforeMPTokenIssuanceSetpreclaim)tecOBJECT_NOT_FOUND:MPTokenIssuancedoes not existHolderspecified butMPTokendoes not existDomainIDspecified (non-zero) but domain does not exist
tecNO_PERMISSION:- Signing account is not the issuer
- Attempting to lock/unlock without
lsfMPTCanLock(when SingleAssetVault or DynamicMPT is enabled). When neither amendment is enabled, an issuance withoutlsfMPTCanLockrejects anyMPTokenIssuanceSetwithtecNO_PERMISSION DomainIDfield is present (to set, or to clear withDomainID= 0) but the issuance does not havelsfMPTRequireAuth- Enabling a capability flag (via a
tfMPTSet*flag) whosetif*bit is declared in the issuance'sImmutableFlags - Changing
MPTokenMetadatawhentifMPTMetadatais declared - Changing
TransferFeewhentifMPTTransferFeeis declared - Setting non-zero
TransferFeewhenlsfMPTCanTransferis not set and not being enabled by the same transaction - Setting non-zero
TransferFeeon an issuance withlsfMPTCanHoldConfidentialBalance, or enabling that capability while a non-zeroTransferFeeis stored - Resubmitting an already-installed encryption key
- Installing an encryption key without the confidential capability enabled or being enabled by the same transaction
- Installing an encryption key, or enabling the confidential capability, while confidential supply is outstanding
tecNO_DST:Holderaccount does not exist
Validation during doApply18
tecINTERNAL:MPTokenIssuancedoes not exist
When Holder is NOT specified (modifying MPTokenIssuance):
MPTokenIssuanceobject is modified:- If
tfMPTLock: SetlsfMPTLockedflag (global lock) - If
tfMPTUnlock: ClearlsfMPTLockedflag (global unlock) - If
DomainIDnon-zero: SetDomainIDfield - If
DomainIDzero: ClearDomainIDfield (remove from entry) - If
MPTokenMetadatapresent: update the field, or clear it (remove from the entry) when the value is empty - If
TransferFeepresent: update the field, or clear it (remove from the entry) when the value is 0 (TransferFeeissoeDEFAULT, so absent means 0) - If any
tfMPTSet*flag present: Set the corresponding capability flags (e.g.,tfMPTSetCanTradesetslsfMPTCanTrade) - If
ImmutableFlagspresent: OR the bits into the entry'sImmutableFlags(immutability extends, never shrinks) - If
IssuerEncryptionKeyorAuditorEncryptionKeypresent: Install the key (each can be installed only once)
- If
When Holder is specified (modifying MPToken):
MPTokenobject is modified:- If
tfMPTLock: SetlsfMPTLockedflag (individual lock) - If
tfMPTUnlock: ClearlsfMPTLockedflag (individual unlock)
- If
The MPTokenAuthorize transaction manages MPToken entries and authorization. It has different behaviors depending on who submits it and which fields are specified:
Holder-initiated (no Holder field):
- Create an
MPTokenentry (opt-in to holding the MPT) - Delete an
MPTokenentry (opt-out, requires zero balance)
Issuer-initiated (Holder field present):
- Authorize a holder by setting
lsfMPTAuthorizedflag - Unauthorize a holder by clearing
lsfMPTAuthorizedflag
| Field Name | Required? | Modifiable? | JSON Type | Internal Type | Default Value | Description |
|---|---|---|---|---|---|---|
TransactionType |
✔️ | No |
String |
UInt16 |
Must be "MPTokenAuthorize" |
|
Account |
✔️ | No |
String |
AccountID |
Account submitting the transaction | |
MPTokenIssuanceID |
✔️ | No |
String |
UInt192 |
The MPTID to authorize for | |
Holder |
No |
String |
AccountID |
If present, issuer is authorizing this holder | ||
Flags |
No |
Number |
UInt32 |
0 |
Unauthorize flag (see below) |
Transaction Flags:
| Flag Name | Hex Value | Description |
|---|---|---|
tfMPTUnauthorize |
0x00000001 |
Delete MPToken entry (holder-initiated only) |
Behavioral Matrix:
| Holder Field | tfMPTUnauthorize |
Who Submits | Action |
|---|---|---|---|
| Not present | Not set | Holder | Create MPToken entry |
| Not present | Set | Holder | Delete MPToken entry (requires zero balance) |
| Present | Not set | Issuer | Set lsfMPTAuthorized flag |
| Present | Set | Issuer | Clear lsfMPTAuthorized flag |
Issuer-initiated authorize/unauthorize only applies when the issuance has lsfMPTRequireAuth; otherwise it fails with tecNO_AUTH.
Static validation19
temDISABLED: MPTokensV1 amendment is not enabledtemINVALID_FLAG: Invalid flags specifiedtemMALFORMED:AccountequalsHolder(issuer cannot create/authorize their ownMPToken)
Validation against the ledger view20
When Holder NOT specified (holder-initiated):
- If
tfMPTUnauthorize:tecOBJECT_NOT_FOUND:MPTokendoes not existtefINTERNAL:MPTokenhas a non-zero balance or locked amount but itsMPTokenIssuancecannot be found (internal inconsistency; should not occur)tecHAS_OBLIGATIONS:MPTAmountis non-zero (cannot delete with balance)LockedAmountis non-zero (cannot delete with locked MPTs)
- With SingleAssetVault:
tecNO_PERMISSION:MPTokenis individually locked
- If NOT
tfMPTUnauthorize:tecOBJECT_NOT_FOUND:MPTokenIssuancedoes not existtecDUPLICATE:MPTokenalready existstecNO_PERMISSION: Signing account is the issuer (issuer cannot create ownMPToken)
When Holder specified (issuer-initiated):
tecNO_DST: Holder account does not existtecOBJECT_NOT_FOUND:MPTokenIssuancedoes not existMPTokendoes not exist (must be created by holder first)
tecNO_PERMISSION: Signing account is not the issuertecNO_PERMISSION:Holderis a pseudo-account (Vault, LoanBroker, or AMM); pseudo-accounts are implicitly always authorized and cannot be unauthorizedtecNO_AUTH: Issuance does not havelsfMPTRequireAuthflag (authorization not required)
Validation during doApply21
tecINTERNAL: Signing account does not exist
When creating MPToken (holder-initiated, no flags):
tecINSUFFICIENT_RESERVE: Account has insufficient XRP balance to cover reserve for creatingMPToken(waived if OwnerCount < 2, but never waived for a sponsored transaction). For a sponsored reserve, the sponsor has insufficient XRP or the pre-funded sponsorship has no remaining owner-count allowancetecDIR_FULL: Owner directory is full and cannot accommodate the newMPToken
When deleting MPToken (holder-initiated, tfMPTUnauthorize):
tecINTERNAL:- Failed to remove
MPTokenfrom owner directory (indicates ledger corruption) MPTokendoes not exist, the balance is not 0, or (withfixCleanup3_1_3) the locked amount is not 0
- Failed to remove
Holder creating MPToken (no Holder field, no tfMPTUnauthorize):
-
MPTokenobject is created:Account: Set to signing accountMPTokenIssuanceID: Set to specified MPTIDMPTAmount: Set to 0OwnerNode: Set to directory page indexFlags: Set to 0 (not authorized initially)Sponsor: Set to the reserve sponsor (only when the transaction's reserve is sponsored)
-
Holder's
AccountRootis modified:OwnerCount: Incremented by 1
-
DirectoryNodeis created or modified:MPTokenis added to holder's owner directory
Holder deleting MPToken (no Holder field, tfMPTUnauthorize set):
-
MPTokenobject is deleted:- Entry is removed from the ledger
-
Holder's
AccountRootis modified:OwnerCount: Decremented by 1
-
DirectoryNodeis modified:MPTokenis removed from holder's owner directory
Issuer authorizing holder (Holder field present, no tfMPTUnauthorize):
MPTokenobject is modified:lsfMPTAuthorizedflag: Set to 1 (authorized)
Issuer unauthorizing holder (Holder field present, tfMPTUnauthorize set):
MPTokenobject is modified:lsfMPTAuthorizedflag: Cleared (unauthorized)
The Clawback transaction allows issuers to claw back MPTs from holders when the lsfMPTCanClawback flag is set. This is the same transaction type used for trust line tokens, but with MPT-specific handling.
Fields:
Transaction fields are described in Clawback Fields.
Note: For trust line token clawback, the holder is specified in the Amount.issuer field. For MPT clawback, the Holder field is required because the MPTID identifies the issuance, not the holder.
Static validation:22
temDISABLED: MPTokensV1 amendment is not enabled (for MPT clawback)temINVALID_FLAG: Any non-universal flags specifiedtemMALFORMED:Holderfield is not specified (required for MPT clawback)AccountequalsHolder(cannot claw back from self)
temBAD_AMOUNT:Amountis zero, negative, or greater than the maximum MPT amount (kMaxMpTokenAmount=0x7FFFFFFFFFFFFFFF)
Validation against the ledger view:23
terNO_ACCOUNT: issuer's or holder's account does not exist.tecAMM_ACCOUNT: Holder account is an AMM account (has ansfAMMIDfield) and the SingleAssetVault amendment is not enabled (when SingleAssetVault is enabled, this case is reported astecPSEUDO_ACCOUNTinstead).tecPSEUDO_ACCOUNT: IfSingleAssetVaultis enabled and holder's account is any pseudo-account.tecOBJECT_NOT_FOUND:MPTokenIssuancedoes not exist- Holder's
MPTokendoes not exist
tecNO_PERMISSION:- Signing account is not the issuer
- Issuance does not have
lsfMPTCanClawbackflag
tecINSUFFICIENT_FUNDS: Holder'sMPTAmountis zero (nothing to claw back)
-
Holder's
MPTokenis modified:24MPTAmount: Decreased by the clawed-back amount (capped at the holder's balance)
-
MPTokenIssuanceis modified:OutstandingAmount: Decreased by the clawed-back amount
The holder's MPToken is not deleted by clawback, even when its MPTAmount reaches zero, and the holder's OwnerCount is unchanged. (This differs from trust-line clawback.)
Notes:
- The clawed back amount is removed from circulation (burned), not transferred to the issuer's balance. If the requested clawback amount exceeds the holder's balance, only the available balance is clawed back (no error).
- Transfer fees are waived during clawback. The full clawed back amount is burned from the holder's balance without deducting any transfer fee, even if a
TransferFeeis set on the issuance. - Clawback ignores lock, freeze, and authorization state: the clawable balance is computed with
IgnoreFreezeandIgnoreAuth, so an issuer can claw back from a locked or unauthorized holder.
MPTs are validated for DEX and transfer operations through helper functions in MPTokenHelpers. The two primary checks are canTrade (DEX tradability) and canTransfer (transferability between two parties), with a convenience wrapper canMPTTradeAndTransfer that runs both. All three return tesSUCCESS for XRP and IOU assets, they only constrain MPTs.
These checks cover tradability and transferability only. Two related concerns are validated separately: lock status by isFrozen (lsfMPTLocked), and holder authorization by requireAuth (lsfMPTRequireAuth/lsfMPTAuthorized and DomainID credentials). A typical call site composes them, e.g. requireAuth(...) together with canTrade(...).
canTrade(view, asset): checks whether an asset may be traded on the DEX.25
Used by: OfferCreate (OfferCreate::preclaim) and cross-currency payment book steps (BookStep, MPTEndpointStep). AMM transactions reach it indirectly via canMPTTradeAndTransfer.
Checks performed (MPT assets only; IOU/XRP return tesSUCCESS):
- MPTokenIssuance exists, else
tecOBJECT_NOT_FOUND lsfMPTCanTradeflag set on the issuance, elsetecNO_PERMISSION- Vault shares: recurses into the underlying asset's tradability via
sfReferenceHolding(when thefixCleanup3_2_0amendment is enabled)
Possible error codes:
tecOBJECT_NOT_FOUND: MPTokenIssuance does not existtecNO_PERMISSION:lsfMPTCanTradeflag not set
canTrade does not check lock status or holder authorization. Those are handled separately by isFrozen and requireAuth.
canTransfer(view, mptIssue, from, to, waive = No) checks whether to may receive the MPT from from.26
Used by: holder-to-holder MPT payments (MPTEndpointStep, Payment), CheckCash/CheckCreate, EscrowCreate, Vault and Lending transactions, and offer-owner validation in BookStep.
Passes when any of:
waiveisWaiveMPTCanTransfer::Yes(recovery paths, e.g. unwinding vault or lending positions after transferability is revoked)fromortois the issuer (issuer-involved minting/burning never requireslsfMPTCanTransfer)lsfMPTCanTransferflag set on the issuance
Otherwise returns tecNO_AUTH. Vault shares recurse into the underlying asset's transferability (when the fixCleanup3_2_0 amendment is enabled).
Possible error codes:
tecOBJECT_NOT_FOUND: MPTokenIssuance does not existtecNO_AUTH:lsfMPTCanTransferflag not set and neither party is the issuer
canMPTTradeAndTransfer(view, asset, from, to): convenience wrapper that runs canTrade then canTransfer, returning the first failure (or tesSUCCESS immediately for non-MPT assets).27
Used by: AMM transactions (AMMCreate, AMMDeposit, AMMWithdraw), which require both DEX tradability and transferability.
MPT payments transfer Multi-Purpose Tokens between accounts. Depending on whether the MPTokensV2 amendment is enabled, these payments execute through one of two paths: a direct transfer mechanism (when MPTokensV2 is not enabled) or the Flow engine (when MPTokensV2 is enabled). Both execution paths use the same underlying transfer logic.
Transfer mechanics: The three transfer scenarios described below apply regardless of which execution path is used, as both ultimately reach the same MPT transfer logic in TokenHelpers.cpp (accountSendMPT, which applies any transfer fee and then mutates the ledger entries via directSendNoFeeMPT). The transfer behavior depends on whether the issuer is involved in the transaction.
Prerequisites: Both source and destination must have MPToken entries (created via MPTokenAuthorize transaction) unless one party is the issuer. The issuer never has an MPToken entry.
If the MPTokenIssuance has a DomainID set, the receiving account must be authorized for that domain to receive the MPT (see MPT DomainID and Authorization). Authorization is verified during payment processing via requireAuth (in doApply for the direct path, and in the strand step for the Flow path): the receiver passes if its existing MPToken has lsfMPTAuthorized set, or if it holds valid, non-expired credentials for the issuance's DomainID. requireAuth does not create an MPToken; the receiver must already have one (see Prerequisites above). Only the receiver's authorization is checked, not the sender's.
When the issuer sends MPTs to a holder, new tokens are minted into circulation:
sequenceDiagram
participant I as Issuer Account
participant IS as MPTokenIssuance
participant H as Holder's MPToken
Note over I,H: Payment: 100 MPT from Issuer to Holder
I->>IS: OutstandingAmount += 100
I->>H: MPTAmount += 100
Note over IS: OutstandingAmount: 1000 -> 1100
Note over H: MPTAmount: 50 -> 150
Operations:
- Increase
OutstandingAmountonMPTokenIssuanceby sent amount - Increase
MPTAmounton holder'sMPTokenby sent amount
Net effect: Total supply increases by the sent amount.
Minting fails with tecPATH_DRY if it would push OutstandingAmount above the issuance's MaximumAmount (which defaults to kMaxMpTokenAmount when unset).28
When a holder sends MPTs back to the issuer, tokens are burned from circulation:
sequenceDiagram
participant H as Holder's MPToken
participant IS as MPTokenIssuance
participant I as Issuer Account
Note over H,I: Payment: 100 MPT from Holder to Issuer
H->>IS: MPTAmount -= 100
H->>IS: OutstandingAmount -= 100
Note over H: MPTAmount: 150 -> 50
Note over IS: OutstandingAmount: 1100 -> 1000
Operations:
- Decrease
MPTAmounton holder'sMPTokenby sent amount - Decrease
OutstandingAmountonMPTokenIssuanceby sent amount
Net effect: Total supply decreases by the sent amount.
When neither party is the issuer, the transfer is executed through a two-step process that applies and burns the transfer fee:
sequenceDiagram
participant S as Sender's MPToken
participant IS as MPTokenIssuance
participant R as Receiver's MPToken
Note over S,R: Payment: 100 MPT from Holder to Holder<br/>TransferFee: 1% (1 MPT fee)
Note over S,R: Step 1: Credit receiver (via issuer)
IS->>R: OutstandingAmount += 100<br/>MPTAmount += 100
Note over S,R: Step 2: Debit sender (via issuer)
S->>IS: MPTAmount -= 101<br/>OutstandingAmount -= 101
Note over S: MPTAmount: 200 -> 99
Note over R: MPTAmount: 50 -> 150
Note over IS: OutstandingAmount: 1000 -> 999<br/>(net: -1 fee burned)
- Calculate the transfer fee:
actualAmount = amount * transferRate, wheretransferRate = 1 + sfTransferFee / 100000(sfTransferFeeis in tenths of a basis point: 1000 = 1%, 50000 = 50%) - Credit step (through issuer to receiver):
- Increase
OutstandingAmountonMPTokenIssuanceby delivery amount - Increase
MPTAmounton receiver'sMPTokenby delivery amount
- Increase
- Debit step (from sender through issuer):
- Decrease
MPTAmounton sender'sMPTokenbyactualAmount(amount + fee) - Decrease
OutstandingAmountonMPTokenIssuancebyactualAmount
- Decrease
Net effect:
- Receiver gets: delivery amount
- Sender pays: delivery amount + fee
- Total supply decreases by: fee amount (burned from circulation)
Under the fixCleanup3_2_0 amendment, every transaction is checked at preflight for canonical MPT amounts. An MPT amount is canonical only when it is non-negative, has a zero exponent, and its mantissa does not exceed kMaxMpTokenAmount (0x7FFFFFFFFFFFFFFF). A transaction carrying a non-canonical MPT amount in any field is rejected with temBAD_AMOUNT. This applies to every MPT-bearing transaction, not just payments.29
A ValidAmounts transaction invariant provides defense in depth: it rejects any ledger entry left with a non-canonical MPT (or XRP) amount after a transaction applies.30
When MPTokensV2 is enabled, MPT payments run through the Flow engine and lock/freeze is enforced per Flow step in MPTEndpointStep. A global lock (lsfMPTLocked on the MPTokenIssuance) is checked on the first step, and an individual lock (lsfMPTLocked on a holder's MPToken) on each step; a locked step returns terLOCKED. A pure issuer-side transfer (minting or burning) is a single Flow step (both first and last) and is exempt from the freeze check, so it succeeds even when the MPT is globally locked.31
The ConfidentialTransfer amendment lets holders of an issuance with lsfMPTCanHoldConfidentialBalance keep part of their balance on the ledger in encrypted form (ElGamal ciphertexts) and move it without revealing amounts. Malformed ciphertexts fail with temBAD_CIPHERTEXT. Cryptographically invalid zero-knowledge proofs fail with tecBAD_PROOF. Every confidential transaction charges nine extra base fees on top of the normal fee.32
ConfidentialMPTConvertmoves public balance into encrypted form: the holder'sMPTAmountdecreases,ConfidentialOutstandingAmountincreases, and the amount lands in the holder's inbox. The first conversion registers the holder's encryption key and initializes the confidential fields. The issuer cannot convert.33ConfidentialMPTMergeInboxfolds the inbox into the spending balance. Received confidential funds cannot be spent or converted back until merged.34ConfidentialMPTSendtransfers between holders fully encrypted, without changing either holder's publicMPTAmountor the issuance'sOutstandingAmount. The issuance must havelsfMPTCanTransfer, and deposit authorization applies, includingCredentialIDs.35ConfidentialMPTConvertBackreveals an amount and returns it to the publicMPTAmount, with a range proof that the remaining confidential balance is non-negative.36ConfidentialMPTClawbacklets the issuer of alsfMPTCanClawbackissuance claw back a holder's entire confidential balance.37
Confidential holdings stay counted in OutstandingAmount, so MPTokenIssuanceDestroy keeps failing with tecHAS_OBLIGATIONS while confidential supply exists. MPTokenAuthorize with tfMPTUnauthorize fails with tecHAS_OBLIGATIONS while the issuance has confidential supply outstanding and the MPToken carries confidential fields. For non-issuer holders, spendable-balance computations count only the public MPTAmount, so confidential funds must be converted back before a regular Payment or DEX operation can move them.38
Footnotes
-
SponsorHelpers.cpp,SponsorHelpers.cpp,MPTokenIssuanceCreate.cpp↩ -
Static validation (preflight):
checkExtraFeatures,getFlagsMask,preflight↩ -
Validation during doApply (reserve, directory, and internal checks in
create):MPTokenIssuanceCreate.cpp↩ -
Static validation (generic preflight gate):
invokePreflight↩ -
Validation against ledger view (preclaim):
MPTokenIssuanceDestroy.cpp↩ -
Validation during doApply:
MPTokenIssuanceDestroy.cpp↩ -
Static validation (preflight):
checkExtraFeatures,getFlagsMask,preflight↩ -
Validation during doApply:
MPTokenIssuanceSet.cpp↩ -
Static validation (preflight):
preflightHelper<MPTIssue>↩ -
Validation against the ledger view (preclaim):
preclaim,preclaimHelper<MPTIssue>↩ -
State changes during doApply:
applyHelper<MPTIssue>↩ -
features.macro,Protocol.h,TER.h,TER.h,ConfidentialTransfer.cpp,ConfidentialTransfer.cpp↩ -
MPTokenIssuanceDestroy.cpp,MPTokenAuthorize.cpp,TokenHelpers.cpp↩