Repository navigation
Amendment XLS: Token Pre-Authorization #654
Replies: 3 comments 6 replies
|
The issuer can choose to set lock and denylist in the same MPTokenIssuanceSet transaction. They need to enable those two flags together in a single transaction if they want. So it might be more flexible not to set the lock flag automatically when setting denylist. |
|
I agree it is cleaner and more expandable to use new transaction here. And is easier to delegate. We can also mention in rationale: |
|
I'd push back on the scope of this one, because I think most of it is already covered. On pre-authorization, TokenPreauth is still one issuer transaction per holder per token, and each one now also carries a reserve, so it doesn't fix the problem the abstract describes. MPTokenAuthorize is already delegable, which answers the cold storage concern. If the goal is letting the issuer approve before the holder opts in, I'd make it a flag on MPTokenAuthorize that creates the MPToken already authorized, which is how TrustSet with tfSetfAuth already works for trust lines. On blocking, RequireAuth with a delegated screening account that approves every opt-in except listed addresses already gives you a block list, and a listed address never receives a balance. The only thing it leaves is the empty, unauthorized MPToken a listed account can still create. I'd fix that by tightening who can create an MPToken on a RequireAuth issuance, either issuer-created holdings or valid credentials required at opt-in on domain issuances, rather than adding a second list beside it. A block list also adds ledger state an allowlist doesn't. Under RequireAuth the authorization is a flag on the MPToken the holder already owns, while every TokenBlock is a new issuer-owned entry for an address that may never touch the token, repeated for every token the issuer runs. LEDGER BLOAT That leaves a permissionless MPT that must block addresses before they are funded. Which issuer needs that and can't use RequireAuth? If there is one, I'd still change the design. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Token Pre-Authorization
1. Abstract
Managing who may hold a token on the XRP Ledger is either fully manual or fully reactive.
Issuers that require authorization (
lsfMPTRequireAuthon anMPTokenIssuance) must sign a separate transaction for every holder they approve. This is impractical at scale, and it is at odds with keeping issuer keys in cold storage.Issuers that do not require authorization have the opposite problem. Their token is permissionless: any account can opt in and start receiving it immediately. The controls available today (per-holder lock, global lock, and clawback) only act on a holding after it exists, so an account the issuer must not serve can opt in and receive funds in the window before the issuer reacts.
This specification lets an issuer record one of two decisions about a specific holder of a specific token, each as its own issuer-owned ledger entry:
TokenPreauth(pre-authorized): on a token that requires authorization, the holder is authorized without a second issuer transaction.TokenBlock(blocked): the holder cannot obtain a holding of the token by any means, whether or not the token requires authorization. If the account already has a holding, its authorization is revoked, but the holding is not frozen: freezing it remains a separate Lock.Each entry is managed by a transaction of the same name,
TokenPreauthandTokenBlock, mirroringDepositPreauth.The first release of this amendment covers MPTs and individual holder addresses only. IOUs, pre-authorization by credential or domain, and blocking for Deposit Authorization are left to later amendments (see Section 14).
2. Motivation
2.1. Background: Authorized MPTs
An
MPTokenIssuancewithlsfMPTRequireAuthonly lets an account hold the token once the issuer has authorized that account'sMPToken. The holder first creates theMPTokenwithMPTokenAuthorize; the issuer then sends a secondMPTokenAuthorizewith theHolderfield to approve it.2.2. Background: Permissionless MPTs
Without
lsfMPTRequireAuth, any account can submitMPTokenAuthorize, create anMPToken, and receive the token. Since MPTokensV2, anMPTokenis also created implicitly when an account acquires the token through the DEX, by cashing a Check, by finishing an Escrow, or by withdrawing from an AMM.Issuers subject to sanctions and similar regulatory regimes are expected to be able to prevent specific parties from holding their token at all. The existing controls all act on a holding that already exists:
lsfMPTLocked) restricts movement of an existing balance.Neither prevents a listed account from opting in and receiving funds before the issuer detects and locks it. Requiring authorization is not a substitute for permissionless tokens: it would force the issuer to approve every holder.
A preventive block closes that window. The opt-in itself fails, deterministically and verifiably on-ledger, in the same ledger that the opt-in is attempted.
2.3. Terminology
TokenPreauthentry exists for the holder and issuance. Only has an effect on issuances that require authorization.TokenBlockentry exists for the holder and issuance. Has an effect on every issuance.MPTokenobject for a given account and issuance.A holder is never both pre-authorized and blocked for the same issuance.
A block is scoped to one MPT issuance. It is not an account-level freeze: a blocked account can still hold and transact XRP and every other token.
2.4. Overview
This feature mirrors Deposit Authorization in naming and shape: one issuer-owned ledger entry per counterparty, managed by a transaction of the same name. It applies to holding a token rather than to payments into the issuer's account.
We propose:
TokenPreauthledger entry, recording a pre-authorization.TokenBlockledger entry, recording a block.TokenPreauthtransaction to create or delete aTokenPreauthentry.TokenBlocktransaction to create or delete aTokenBlockentry.MPTokenAuthorize.tecTOKEN_BLOCKED.This feature requires an amendment, tentatively titled
TokenPreauth.2.5. Release Scope
TrustSetchanges)Blocking is never issuer-wide, in this or any later amendment (see Section 10.3).
3. Ledger Entry:
TokenPreauthRecords that an issuer has pre-authorized one holder for one MPT issuance. Like
DepositPreauth, it has no flags: the entry's existence is the pre-authorization.3.1. Object Identifier
Key Space:
0x0077('w')Ledger Entry Type:
ltTOKEN_PREAUTH=0x008AID Calculation Algorithm:
The issuer is not hashed separately:
MPTokenIssuanceIDalready encodes it. Future variants that pre-authorize by something other than an address and an MPT issuance (see Section 14) can use their own key space, asDepositPreauthdoes for credentials, so this key never changes.3.2. Fields
LedgerEntryTypeUINT16TokenPreauthTokenPreauthentry.AccountACCOUNTHolderACCOUNTMPTokenIssuanceIDUINT192OwnerNodeUINT64PreviousTxnIDHASH256PreviousTxnLgrSeqUINT323.3. Flags
None.
3.4. Ownership
Owner:
Account(the issuer)Directory Registration: The entry is registered in the issuer's owner directory only.
The
Holderdoes not own the entry, does not pay its reserve, and does not see it in its owner directory.3.5. Reserves
Reserve Requirement: Standard
Each entry requires one owner reserve increment (currently 0.2 XRP, subject to Fee Voting), paid by the issuer. Reserve sponsorship is not supported for this entry.
3.6. Deletion
Deletion Transactions:
TokenPreauthwithtfUnauthorize,TokenBlock(see Section 6.5),AccountDeleteDeletion Conditions:
TokenPreauthwithtfUnauthorize; orTokenBlockwithouttfUnblock, which replaces the entry with aTokenBlock; orAccountDeleteby the issuer.An entry can be deleted after its
MPTokenIssuancehas been destroyed, so the issuer can recover the reserve.Account Deletion Blocker: No
AccountDeleteby the issuer deletes itsTokenPreauthentries. In practice an issuer cannot delete its account while any of its issuances still exist, so the entries it deletes are always for destroyed issuances. The entry is not a deletion blocker for theHoldereither, since the holder does not own it.3.7. Invariants
Accountis the issuer ofMPTokenIssuanceID, andHolderis not that issuer.TokenBlockentry exists with the sameHolderandMPTokenIssuanceID.3.8. RPC Name
RPC Type Name:
token_preauthledger_entryaccepts either the entry's index or the fields that make up its ID:{ "command": "ledger_entry", "token_preauth": { "holder": "rALICE......", "mpt_issuance_id": "000005F398B624EBD06822198649C920C8B20ADB8EBE745E" }, "ledger_index": "validated" }holderandmpt_issuance_idare both required. If no entry exists, the result isentryNotFound.account_objectswith"type": "token_preauth"lists every pre-authorization an issuer owns.3.9. Example JSON
{ "LedgerEntryType": "TokenPreauth", "Flags": 0, "Account": "rISSUER......", "Holder": "rALICE......", "MPTokenIssuanceID": "000005F398B624EBD06822198649C920C8B20ADB8EBE745E", "OwnerNode": "0000000000000000", "PreviousTxnID": "AA10031C6C21164E7673...17D467CEEE9", "PreviousTxnLgrSeq": 12345700 }4. Ledger Entry:
TokenBlockRecords that an issuer has blocked one account from one MPT issuance. It has the same fields and ownership as
TokenPreauth, in a separate key space.4.1. Object Identifier
Key Space:
0x0057('W')Ledger Entry Type:
ltTOKEN_BLOCK=0x008BID Calculation Algorithm:
As for
TokenPreauth, the issuer is encoded inMPTokenIssuanceID.Checking whether an account is blocked is a single existence lookup of this key. The list is not stored on the
MPTokenIssuanceor on the issuer'sAccountRoot, so neither grows with the number of entries.4.2. Fields
LedgerEntryTypeUINT16TokenBlockTokenBlockentry.AccountACCOUNTHolderACCOUNTMPTokenIssuanceIDUINT192OwnerNodeUINT64PreviousTxnIDHASH256PreviousTxnLgrSeqUINT324.3. Flags
None.
4.4. Ownership
Owner:
Account(the issuer)Directory Registration: The entry is registered in the issuer's owner directory only. The
Holderdoes not own it.4.5. Reserves
Reserve Requirement: Standard
Each entry requires one owner reserve increment, paid by the issuer. Replacing a
TokenPreauthwith aTokenBlockdoes not change the issuer's owner count. Reserve sponsorship is not supported for this entry.4.6. Deletion
Deletion Transactions:
TokenBlockwithtfUnblock,AccountDeleteDeletion Conditions:
TokenBlockwithtfUnblock; orAccountDeleteby the issuer.The
TokenPreauthtransaction never deletes aTokenBlock. An entry can be deleted after itsMPTokenIssuancehas been destroyed.Account Deletion Blocker: No
As with
TokenPreauth,AccountDeleteby the issuer deletes itsTokenBlockentries. The entry is not a deletion blocker for theHolder. If the holder's account is deleted and later re-created at the same address, the block still applies.4.7. Invariants
Accountis the issuer ofMPTokenIssuanceID, andHolderis not that issuer.TokenPreauthentry exists with the sameHolderandMPTokenIssuanceID.MPTokenfor a blocked holder, exceptAMMClawback, which creates it withlsfMPTLocked(see Section 8).MPTokenfor that issuance, if one exists, does not havelsfMPTAuthorizedat the end of any transaction (see Section 6.5.1).4.8. RPC Name
RPC Type Name:
token_blockledger_entryaccepts either the entry's index or the fields that make up its ID:{ "command": "ledger_entry", "token_block": { "holder": "rBLOCKED......", "mpt_issuance_id": "000005F398B624EBD06822198649C920C8B20ADB8EBE745E" }, "ledger_index": "validated" }holderandmpt_issuance_idare both required. If no entry exists, the result isentryNotFound. This lets anyone, including a regulator, independently verify whether a given account is blocked from a given token.account_objectswith"type": "token_block"lists an issuer's complete block list.4.9. Example JSON
{ "LedgerEntryType": "TokenBlock", "Flags": 0, "Account": "rISSUER......", "Holder": "rBLOCKED......", "MPTokenIssuanceID": "000005F398B624EBD06822198649C920C8B20ADB8EBE745E", "OwnerNode": "0000000000000000", "PreviousTxnID": "DD40031C6C21164E7673...17D467CEEE9", "PreviousTxnLgrSeq": 12345678 }5. Transaction:
TokenPreauthPre-authorizes a holder for an MPT issuance by creating a
TokenPreauthentry, or removes a pre-authorization by deleting it. Sent by the issuer.5.1. Fields
TransactionTypestringUINT16TokenPreauthTokenPreauthtransaction.HolderstringACCOUNTMPTokenIssuanceIDstringUINT1925.2. Flags
tfUnauthorize0x00000001TokenPreauthentry. If not set, create the entry.As with
MPTokenAuthorize, creating is the default and a flag selects removal.5.3. Transaction Fee
Fee Structure: Standard
5.4. Failure Conditions
5.4.1. Data Verification
TokenPreauthamendment is not enabled (temDISABLED).tfUnauthorizeand the universal flags is set (temINVALID_FLAG).Holderis the zero account (temINVALID_ACCOUNT_ID).Holderis the same asAccount(temMALFORMED).5.4.2. Protocol-Level Failures
In all cases:
Accountis not the issuer encoded inMPTokenIssuanceID(tecNO_PERMISSION). This is read from the ID itself, so it also applies after theMPTokenIssuanceis destroyed.With
tfUnauthorize:TokenPreauthentry exists for (Holder,MPTokenIssuanceID) (tecNO_ENTRY). TheMPTokenIssuancedoes not need to still exist.Without
tfUnauthorize:MPTokenIssuancedoes not exist (tecOBJECT_NOT_FOUND).Holderis a pseudo-account (AMM, Vault, or LoanBroker) (tecPSEUDO_ACCOUNT).Holderaccount does not exist (tecNO_DST).Holderis blocked. The issuer must remove the block withTokenBlockandtfUnblockfirst (tecNO_PERMISSION).Holderis already pre-authorized (tecDUPLICATE).Accountdoes not have enough reserve for the new entry (tecINSUFFICIENT_RESERVE).5.5. State Changes
On success (
tesSUCCESS):tfUnauthorize: create theTokenPreauthentry and increase the issuer's owner count by one (see also Section 5.5.1).tfUnauthorize: delete theTokenPreauthentry and decrease the issuer's owner count by one.5.5.1. Pre-Authorizing an Existing Holder
If the issuance has
lsfMPTRequireAuthand anMPTokenalready exists for (MPTokenIssuanceID,Holder), creating the pre-authorization also setslsfMPTAuthorizedon it.Otherwise the pre-authorization would never take effect for a holder that opted in before being pre-authorized: that holder has no further transaction to send, since a second
MPTokenAuthorizefails withtecDUPLICATE.tfUnauthorizedoes not clearlsfMPTAuthorized. Revoking a holder's authorization remains an explicitMPTokenAuthorizefrom the issuer withtfMPTUnauthorize.5.6. Delegation
The
TokenPreauthtransaction is delegable under XLS-75 with the transaction-level permissionTokenPreauth, which allows both creating and removing pre-authorizations. See Section 6.6 for how delegation applies to both transactions.5.7. Example JSON
Pre-authorizing an account:
{ "TransactionType": "TokenPreauth", "Account": "rISSUER......", "Holder": "rALICE......", "MPTokenIssuanceID": "000005F398B624EBD06822198649C920C8B20ADB8EBE745E", "Fee": "10", "Sequence": 12347 }Removing the pre-authorization:
{ "TransactionType": "TokenPreauth", "Account": "rISSUER......", "Flags": 1, "Holder": "rALICE......", "MPTokenIssuanceID": "000005F398B624EBD06822198649C920C8B20ADB8EBE745E", "Fee": "10", "Sequence": 12348 }6. Transaction:
TokenBlockBlocks a holder from an MPT issuance by creating a
TokenBlockentry, or removes a block by deleting it. Sent by the issuer.6.1. Fields
TransactionTypestringUINT16TokenBlockTokenBlocktransaction.HolderstringACCOUNTMPTokenIssuanceIDstringUINT1926.2. Flags
tfUnblock0x00000001TokenBlockentry. If not set, create the entry.6.3. Transaction Fee
Fee Structure: Standard
6.4. Failure Conditions
6.4.1. Data Verification
TokenPreauthamendment is not enabled (temDISABLED).tfUnblockand the universal flags is set (temINVALID_FLAG).Holderis the zero account (temINVALID_ACCOUNT_ID).Holderis the same asAccount(temMALFORMED).6.4.2. Protocol-Level Failures
In all cases:
Accountis not the issuer encoded inMPTokenIssuanceID(tecNO_PERMISSION), as forTokenPreauth.With
tfUnblock:TokenBlockentry exists for (Holder,MPTokenIssuanceID) (tecNO_ENTRY). TheMPTokenIssuancedoes not need to still exist.Without
tfUnblock:MPTokenIssuancedoes not exist (tecOBJECT_NOT_FOUND).Holderis a pseudo-account (AMM, Vault, or LoanBroker) (tecPSEUDO_ACCOUNT).Holderis already blocked (tecDUPLICATE).Accountdoes not have enough reserve for it (tecINSUFFICIENT_RESERVE). Replacing a pre-authorization does not need additional reserve.An existing
TokenPreauthentry for the same holder is not a failure condition: the block replaces it in the same transaction (Section 6.5, Section 13.4). Requiring a separateTokenPreauthwithtfUnauthorizefirst would leave a gap between the two transactions, and a delegate holding onlyTokenBlockCreatecould not block a pre-authorized holder at all.Unlike
TokenPreauth,TokenBlockdoes not require theHolderaccount to exist. Sanctions lists routinely include addresses that have not yet been funded on the XRP Ledger. If a block could only be created after the address is funded, that address could be funded and opt in within the same ledger, which is the race this feature exists to remove.6.5. State Changes
On success (
tesSUCCESS):tfUnblock:TokenPreauthentry exists for the same (Holder,MPTokenIssuanceID), delete it. A block takes precedence over a pre-authorization.TokenBlockentry.lsfMPTAuthorizedon the holder'sMPToken, if any (see Section 6.5.1).tfUnblock: delete theTokenBlockentry.Creating an entry increases the issuer's owner count by one and deleting one decreases it by one, so replacing a
TokenPreauthwith aTokenBlockleaves it unchanged.The two transactions together give each (
Holder,MPTokenIssuanceID) at most one state:TokenPreauthexistsTokenBlockexistsTokenPreauthTokenPreauth; see 5.5.1tecDUPLICATE)tecNO_PERMISSION)TokenPreauth+tfUnauthorizetecNO_ENTRY)TokenPreauthtecNO_ENTRY)TokenBlockTokenBlock; see 6.5.1TokenPreauthand createTokenBlock; see 6.5.1tecDUPLICATE)TokenBlock+tfUnblocktecNO_ENTRY)tecNO_ENTRY)TokenBlock6.5.1. Revoking Authorization
If an
MPTokenexists for (MPTokenIssuanceID,Holder), creating the block clearslsfMPTAuthorizedon it, if set, in the same transaction. This only has an effect on an issuance withlsfMPTRequireAuth, where it revokes the issuer's approval of the holder. While the block exists, the holding stays unauthorized:MPTokenAuthorizefrom the issuer fails withtecTOKEN_BLOCKEDfor a blocked holder (Section 7.1).A block does not lock the holding, and does not change its balance. A block stops a holding from being created (Section 8); freezing a holding that already exists is what Lock is for (see Section 10.4). An issuer that needs to stop a blocked holder from moving an existing balance also sends
MPTokenIssuanceSetwithtfMPTLock, which it can submit in the sameBatchas theTokenBlockto apply both atomically. Lock and unlock work on a blocked holder exactly as on any other holder. Removing the balance is done with the existingClawbacktransaction, if the issuance haslsfMPTCanClawback.tfUnblockonly removes the block. It does not authorize the holding again (see Section 10.5). On an issuance withlsfMPTRequireAuth, the issuer re-authorizes the holder explicitly, withMPTokenAuthorizeandHolder, or withTokenPreauth, which also authorizes an existingMPToken(Section 5.5.1).6.6. Delegation
Both transactions are delegable under XLS-75, in the same way as the issuer's other holder controls:
Clawbackis delegable as a whole transaction, and per-holder lock and unlock are delegable through theMPTokenIssuanceLockandMPTokenIssuanceUnlockgranular permissions. An issuer can therefore let a compliance account maintain its lists without holding the issuer's key.Because pre-authorizing and blocking are separate transactions, the transaction-level permissions already keep the two directions apart:
TokenPreauthgrants access, andTokenBlockrestricts it. An issuer can give an automated sanctions-screening accountTokenBlockwithout also letting it pre-authorize holders.Within
TokenBlock, an issuer can also delegate creating blocks without delegating their removal, through one granular permission:TokenBlockCreate65549MPTokenIssuanceID,HolderSo an issuer can grant only
TokenBlockCreateto the screening account, and keep the ability to remove a block with the issuer or an account holding the transaction-levelTokenBlockpermission (see Section 13.5).There is no matching unblock-only permission. A granular permission lists the flags a delegated transaction may set, so one that allows
tfUnblockwould also allow the transaction without it, which creates a block. Removing a block is the more sensitive action in any case.A delegated transaction behaves exactly like one sent by the issuer:
Accountis the issuer, the issuer's owner count and reserve change, and a block revokes an existing holding's authorization (Section 6.5.1). As with any delegated transaction, the delegate pays the fee. The granular permission values are provisional and will be confirmed when the amendment is merged.6.7. Example JSON
Blocking an account from an MPT issuance:
{ "TransactionType": "TokenBlock", "Account": "rISSUER......", "Holder": "rBLOCKED......", "MPTokenIssuanceID": "000005F398B624EBD06822198649C920C8B20ADB8EBE745E", "Fee": "10", "Sequence": 12346 }Removing the block:
{ "TransactionType": "TokenBlock", "Account": "rISSUER......", "Flags": 1, "Holder": "rBLOCKED......", "MPTokenIssuanceID": "000005F398B624EBD06822198649C920C8B20ADB8EBE745E", "Fee": "10", "Sequence": 12349 }7. Transaction:
MPTokenAuthorize(Modified)No fields are added. In this section, the holder is the
Accountsending the transaction when there is noHolderfield (the holder opting in), and theHolderfield when the issuer sends it.7.1. Failure Conditions
All existing failure conditions still apply. When the amendment is enabled:
tfMPTUnauthorizeis not set, and aTokenBlockentry exists for (holder,MPTokenIssuanceID) (tecTOKEN_BLOCKED).This check is evaluated before every other protocol-level check, including the checks that the issuance and the holder's account exist and the existing
tecDUPLICATEcheck. So:tecTOKEN_BLOCKED, whether or not it already has anMPToken; andtfMPTUnauthorizeis not affected:MPTokento recover the reserve, subject to the existing conditions (zero balance, no escrowed amount, no confidential balance). This is allowed even if the issuer has locked theMPToken, which existing rules otherwise reject. It does not let the holder escape the lock, because the block prevents it from creating theMPTokenagain.tfMPTUnauthorizefor a blocked holder. It has no further effect, since the block already revoked the authorization (Section 6.5.1).7.2. State Changes
On success, if the holder is opting in (no
Holderfield, notfMPTUnauthorize), the issuance haslsfMPTRequireAuth, and aTokenPreauthentry exists for the holder, the newMPTokenis created withlsfMPTAuthorized. The holder does not wait for a second issuer transaction.On an issuance without
lsfMPTRequireAuth, a pre-authorization has no effect.7.3. Example JSON
A blocked account attempting to opt in. This fails with
tecTOKEN_BLOCKED, and noMPTokenis created:{ "TransactionType": "MPTokenAuthorize", "Account": "rBLOCKED......", "MPTokenIssuanceID": "000005F398B624EBD06822198649C920C8B20ADB8EBE745E", "Fee": "10", "Sequence": 1 }8. Creating a Holding for a Blocked Account
MPTokenAuthorizeis not the only way to obtain anMPToken. Several transactions create one implicitly for an account that receives the token. If onlyMPTokenAuthorizechecked for blocks, a blocked account could obtain the token through any of these other paths.When the amendment is enabled, no transaction may create an
MPTokenfor an account that is blocked for that issuance. A transaction that checks whether an account may hold the token treats a blocked account that has noMPTokenas not allowed to hold it, and fails withtecTOKEN_BLOCKED. This includes:MPTokenPaymenttecTOKEN_BLOCKEDwhen the blocked account is the destination.OfferCreatetecTOKEN_BLOCKEDwhen the blocked account would receive the token (TakerPays).CheckCashtecTOKEN_BLOCKEDwhen the blocked account cashes a check for the token.EscrowCreate,EscrowFinishtecTOKEN_BLOCKEDwhen the blocked account is the destination. The sender can still cancel the escrow after it expires.AMMWithdrawtecTOKEN_BLOCKEDwhen the blocked account would withdraw the token. A blocked LP can still withdraw its share as the pool's other asset.CheckCreatedoes not check whether the destination can hold the token, and is unchanged. A check written to a blocked account can never be cashed.A blocked account that already has an
MPTokenis not affected by this rule, and its balance can still move unless the issuer locks it (see Section 6.5.1).The one exception is
AMMClawback. To claw back from an AMM pool, the issuer may need to recreate a holder'sMPTokenthat the holder has since deleted.AMMClawbackstill does this for a blocked holder, so the issuer can recover its token, but the recreatedMPTokenis created withlsfMPTLockedand withoutlsfMPTAuthorized, since it only exists for the issuer to seize from.9. Composition with Existing Controls
Together with the existing controls, an issuer can stop an account from opting in to, moving, or holding the token:
MPTokenby any path.Before this amendment, the first stage has no control at all for a permissionless issuance.
10. Rationale
10.1. Why pre-authorizations and blocks are separate entry types and transactions
A pre-authorization and a block are opposite decisions, so they are recorded as two ledger entry types rather than as two flags on one entry, and each is managed by its own transaction:
TokenPreauththat sometimes means "blocked" would be confusing.DepositPreauth.TokenPreauthhas the same shape asDepositPreauth: no flags, and the entry's existence is the pre-authorization. Each entry type is managed by a transaction of the same name, asDepositPreauthis, so a transaction never creates an entry of a different type than its name says.A holder still has at most one state per token. The transactions enforce this (
TokenBlockreplaces a pre-authorization, andTokenPreauthfails on a blocked holder), and an invariant checks it. Replacing a pre-authorization with a block deletes one entry and creates the other in the same transaction, so the issuer's reserve does not change.TokenPreauthonly ever creates or deletes aTokenPreauthentry, and cannot overwrite a block, so a pre-authorization workflow can never accidentally remove a block.10.2. Why new transactions instead of flags on
MPTokenIssuanceSetorMPTokenAuthorizeBoth existing transactions act on objects that already exist:
MPTokenIssuanceSetchanges an issuance or locks a holder'sMPToken, and the issuer'sMPTokenAuthorizeapproves a holder's existingMPToken. Pre-authorizations and blocks are made before anyMPTokenexists, often for an address that has no account yet, and each creates or deletes a separate issuer-owned entry with its own reserve. Adding them as flags would change what these transactions mean, rather than adding an option to them:MPTokenIssuanceSetorMPTokenAuthorizewould gain the power to block and unblock holders, or to pre-authorize them, as soon as the amendment is enabled, without the issuer having granted it. New transactions start with no delegates.MPTokenAuthorizeis sent by holders too. The same transaction is the holder's opt-in. Mixing issuer-only list management into it makes its flags and failure conditions depend on who sends it, and it already requires the holder's account to exist (tecNO_DST), which a block must not.tecDUPLICATE,tecNO_ENTRY, reserve), works after the issuance is destroyed, and interacts with the other entry type (Section 6.5). As flags, these would be special cases inside transactions whose existing rules do not apply to them.TokenPreauthandTokenBlocktransactions can later cover IOUs with aCurrencyfield (Section 14.1). Flags on MPT-only transactions could not, and IOUs would need a second, different interface.DepositPreauth, where list management is a dedicated transaction rather than a flag onAccountSet.10.3. Why blocks are per-token
A block names exactly one issuance. An issuer-wide block would either add a second lookup to every opt-in, or require enumerating all of the issuer's issuances to revoke existing authorizations, which is unbounded work in a single transaction. Per-issuance entries keep both the check and the revocation to one key lookup. An issuer with several tokens sends one
TokenBlocktransaction per token.Blocks name a single address rather than a credential or domain because the primary use case, a sanctions list, is a list of addresses.
10.4. Why a block does not lock an existing holding
Block and Lock are separate controls with separate meanings. A block decides who may obtain a holding; Lock decides whether an existing holding may move. Keeping them separate means each has one clear effect, Lock keeps its existing semantics and flags, and an issuer can apply either one without the other. An issuer that wants both sends both, atomically if needed by placing them in one
Batch.Because a block does not lock, it does not depend on
lsfMPTCanLock: an issuer can block accounts on any issuance.A block does revoke authorization. On an issuance with
lsfMPTRequireAuth,lsfMPTAuthorizedis the issuer's affirmative approval of the holder, and leaving it set on a blocked, typically sanctioned, holder would be contradictory. It would also be inconsistent with replacing a pre-authorization: the block deletes theTokenPreauthentry, so it should not keep the authorization that entry granted. Authorization is the same decision a block makes, whether the account may hold the token, so revoking it is part of the block rather than a separate control.10.5. Why
tfUnblockdoes not re-authorizeRestoring access to a holder who was blocked should be a deliberate decision, so on an issuance that requires authorization the issuer re-authorizes it explicitly rather than having
tfUnblockdo it.10.6. Why enforcement covers every path that creates a holding
Since MPTokensV2, an
MPTokenis created by several transactions besidesMPTokenAuthorize(see Section 8). A block that only checkedMPTokenAuthorizecould be bypassed by placing an offer to buy the token, or by having someone write a check or an escrow to the blocked address.10.7. Why MPT only
The gap this addresses is specific to MPTs: an
MPTokencan only be created by the holder (or implicitly by a transaction that pays it), so an issuer has no object to act on before the holder opts in. An IOU issuer can already create a trustline to a known address and freeze it in advance. IOU support is still valuable for consistency across token standards, and is the first planned extension (see Section 14.1).10.8. Why not Deposit Authorization
Deposit Authorization controls payments into the issuer's own account. Issuers may need to control who holds their token without controlling who may pay their address.
10.9. Why a dedicated result code:
tecTOKEN_BLOCKEDtecTOKEN_BLOCKEDIt is returned by
MPTokenAuthorize(Section 7), by the transactions in Section 8, and never by theTokenPreauthorTokenBlocktransactions themselves.A dedicated code, rather than reusing
tecNO_PERMISSIONortecNO_AUTH, makes enforcement auditable: an observer can tell from the transaction result alone that it failed because of a block, and confirm the block with aledger_entrylookup.11. Backwards Compatibility
TokenPreauthandTokenBlockledger entries and transactions are only recognized after the amendment is enabled.TokenBlockentry exists, and entries can only exist after the amendment is enabled. No existing field is added or changed.MPTokenAuthorizeas before, except for holders it has blocked.12. Test Plan
TokenPreauthandTokenBlocktransactions, with and without their flag, for every transition in Section 6.5, including owner count and reserve, and that a holder is never both pre-authorized and blocked.lsfMPTRequireAuth, and receivestecTOKEN_BLOCKED. Other accounts can still opt in.lsfMPTRequireAuth, blocking clearslsfMPTAuthorized(including one granted through a pre-authorization), the issuer cannot authorize the holder while it is blocked, and afterTokenBlockwithtfUnblockthe holder stays unauthorized until the issuer re-authorizes it.TokenBlocksucceeds on an issuance withoutlsfMPTCanLock, for an existing holder, a funded non-holder, and an unfunded address.MPTokenonce the balance is zero, and cannot create it again.lsfMPTRequireAuthissuance, both before and after the holder opts in; no effect withoutlsfMPTRequireAuth.CheckCash,EscrowCreate/EscrowFinish,Payment,AMMWithdraw, andAMMClawbackrecreating a locked, unauthorizedMPToken.AccountDeleteof the issuer and of a blocked holder.ledger_entryby index and by{holder, mpt_issuance_id}for bothtoken_preauthandtoken_block, andaccount_objectswith each type.TokenBlockCreateallows creating a block but not removing one, and covers neitherTokenPreauthaction; a delegate cannot act for an issuer that did not grant it permission; none of the permissions can be granted while the amendment is disabled.13. Security Considerations
13.1. Scope of a block
A block applies only to the one issuance it names. It does not restrict XRP or any other token, and it is not an account-level freeze; no such primitive exists on the XRP Ledger. Documentation aimed at compliance teams and regulators should state this clearly.
A block also applies only to the address it names. It stops that address from holding the token, not from directing the token to another address. For example, a blocked account with shares in a vault for the token cannot
VaultWithdrawto itself, but can withdraw with another account as theDestination. Likewise, a block does not stop the same party from using a new address. Linking new addresses to a blocked party is the job of the issuer's off-chain screening, which then blocks those addresses too; the block is the on-ledger enforcement of that list.13.2. Reserve and spam
Only the issuer can create entries, and only the issuer pays their reserve. A blocked account cannot cause entries to be created. An issuer with a large list should expect a reserve proportional to the number of (holder, issuance) pairs.
13.3. Timing
A block takes effect in the ledger in which the
TokenBlocktransaction is applied, and is checked when an opt-in in that same ledger or any later one is applied. An account that obtained a holding in an earlier ledger, or earlier in the same ledger, keeps it: the block does not roll it back, and the issuer locks it if the balance must not move. Issuers should add accounts to their list promptly, and should block addresses before they are funded where possible.13.4. Block takes precedence
TokenBlockcan replace a pre-authorization with a block in one transaction;TokenPreauthcan never remove a block. Removing a block is always an explicitTokenBlockwithtfUnblock.13.5. Delegation
Only a delegate with the transaction-level
TokenBlockpermission can remove a block. Issuers should grant the narrowest permission that fits each account, for example onlyTokenBlockCreateto an automated screening service, and grantTokenBlockonly to an account they would trust to lift a sanctions block, or keep it with the issuer. Locking or unlocking a blocked holder's existing holding is not part ofTokenBlock; it requires the separateMPTokenIssuanceLockorMPTokenIssuanceUnlockpermission.14. Future Extensions
These are expected to follow in later amendments. They are recorded here so that the entries and the transactions can be extended without redesign.
14.1. IOUs
TokenPreauthandTokenBlockledger entries and transactions gain an optionalCurrencyfield; exactly one ofCurrencyandMPTokenIssuanceIDis present, and the ID hashes the issuer,Holder, andCurrencyin a key space of their own, since aCurrencycode does not identify its issuer.TrustSetfails withtecTOKEN_BLOCKEDwhen a blocked account would create a trustline to the issuer for that currency, or when the issuer setstfSetfAuthon a trustline with a blocked account. A blocked account can still modify or delete an existing trustline, so it can release the reserve once the balance is zero.CheckCashand offer crossing) must also be covered.TokenBlockdoes not freeze an existing trustline; the issuer freezes it separately if needed. Whether a block should cleartfSetfAuthon an existing trustline, as it clearslsfMPTAuthorizedon anMPToken, is open.lsfRequireAuth, a pre-authorized holder's trustline is authorized when created or modified.An IOU block should name a single
Currency. A block covering all of an issuer's IOUs would make revoking authorization unbounded, since the holder may have a trustline to the issuer for every currency it issues (see Section 10.3).14.2. Credentials and domains
Pre-authorization (but not blocking) by credential (XLS-70) or domain (XLS-80):
TokenPreauthgains optionalCredentialsandDomainIDfields, with exactly one ofHolder,Credentials, andDomainIDpresent, andTrustSet/MPTokenAuthorizegain optionalCredentialIDsandDomainIDfields. A block always takes precedence over any such pre-authorization.14.3. Issuer-wide pre-authorization
A
TokenPreauthentry with noMPTokenIssuanceID(orCurrency) that applies to all of the issuer's tokens. Blocks remain per-token (Section 10.3).14.4. Blocking for Deposit Authorization
Deposit Authorization could gain the same pattern: a separate block entry type, alongside
DepositPreauth, that rejects payments from a specific sender even when the destination does not require deposit authorization. This would be a separate amendment, but it should use the same naming and semantics asTokenBlockso that the two features behave the same way.15. Open Issues
TokenBlockfor both. Alternatives includeTokenDeny,TokenBlocklist, andDenyAuth.tecTOKEN_BLOCKEDwill be assigned when the amendment is merged.Appendix A: FAQ
A.1. Does the holder still send
MPTokenAuthorizeif it is pre-authorized?Yes. Pre-authorization does not skip opt-in. On an issuance that requires authorization, that single holder transaction also authorizes the holding, so the issuer does not send a follow-up. If the holder opted in before being pre-authorized, the
TokenPreauthtransaction authorizes the existing holding.A.2. Can I still authorize holders the old way?
Yes. The issuer's
MPTokenAuthorizewithHolderstill works for any holder that is not blocked.A.3. Does pre-authorization do anything for a token that doesn't require authorization?
No. Anyone can already hold such a token. Blocks apply whether or not the token requires authorization.
A.4. How is blocking different from Lock and Clawback?
Lock restricts movement of a holding that already exists, and Clawback removes a balance that already exists. Blocking prevents the holding from being created at all. See Section 9.
A.5. Can a blocked account get the token some other way, for example through the DEX?
No. Every transaction that would create a holding for a blocked account fails or skips that account. See Section 8.
A.6. Who owns the entries?
The issuer.
Accountis the issuer, which owns the entry and pays its reserve;Holderis the pre-authorized or blocked account. This matchesDepositPreauth, notMPToken.A.7. How do I check an account's status for a token?
Look up
token_blockwith{holder, mpt_issuance_id}: if it exists, the account is blocked. Otherwise look uptoken_preauthwith the same fields: if it exists, the account is pre-authorized. At most one of the two exists.A.8. Why not store the blocked accounts on the
MPTokenIssuance?A list of thousands of addresses on one ledger entry is rewritten in full on every change and is limited by the maximum size of a ledger entry. Separate keyed entries are a single lookup to check and a single write to change.
All reactions