This tutorial shows you how to claw back a holder's confidential Multi-Purpose Token (MPT) balance. A confidential clawback takes the holder's total confidential balance (spending + inbox balance), so there is no partial clawback. The clawback amount is publicly visible, so the holder's total confidential balance at that moment can be seen by everyone.
ConfidentialTransfer amendmentが必要です。 Loading...
By the end of this tutorial, you will be able to:
- Lock an MPT issuance for a holder so that a clawback proof stays valid.
- Claw back the holder's entire confidential balance, and verify the result as the holder, the issuer, and the auditor.
To complete this tutorial, you should:
- Have a basic understanding of the XRP Ledger and the Confidential Transfers concept, in particular Confidential Clawback.
- Have an MPT issued with both the Can Clawback and Can Lock flags, the issuer and auditor encryption keys registered on it, and a holder with a confidential balance. See Issue an MPT for Confidential Transfers. The setup script for this tutorial creates them for you.
- Have an XRP Ledger client library set up in your development environment. This page provides examples for the following:
- JavaScript with the xrpl.js library. See Get Started Using JavaScript for setup steps.
- Python with the xrpl-py library. See Get Started Using Python for setup steps.
You can find the complete source code for this tutorial's examples in the code samples section of this website's repository.
From the js/ folder, use npm to install dependencies.
npm installTo get started, import the necessary libraries and instantiate a client to connect to the XRP Ledger. This example imports:
fs: Used to check for and load the tutorial setup data.xrpl: Used for XRPL client connection, transaction submission, and wallet handling../confidentialTransfersSetup.js: The tutorial setup script, imported and called directly.
import fs from 'fs'
import {
Client,
MPTokenIssuanceSetFlags,
Wallet,
deriveConfidentialKeypair,
fetchMPToken,
fetchMPTokenIssuance,
loadMptCrypto,
prepareConfidentialClawback
} from 'xrpl'
import { setup } from './confidentialTransfersSetup.js'
// Connect to the network ----------------------
const client = new Client('wss://s.devnet.rippletest.net:51233')
await client.connect()
const EXPLORER = 'https://devnet.xrpl.org'Load the issuer, the holder, the auditor, and the issuance the clawback applies to. Only the issuer can claw back, and it needs its own encryption keys to read the holder's balance and build the proof.
// Load setup data ----------------------
// This step checks for the necessary setup data to run the tutorial.
// If missing, confidentialTransfersSetup.js will generate it.
if (!fs.existsSync('confidentialTransfersSetup.json')) {
console.log(`\n=== Setup data doesn't exist. Running setup script... ===\n`)
await setup()
}
const setupData = JSON.parse(
fs.readFileSync('confidentialTransfersSetup.json', 'utf8')
)
// Set up accounts ----------------------
console.log(`\n=== Getting accounts... ===`)
const issuer = Wallet.fromSeed(setupData.issuer.seed)
const auditor = Wallet.fromSeed(setupData.auditor.seed)
const holder = Wallet.fromSeed(setupData.flaggedHolder.seed)
const issuerKeys = deriveConfidentialKeypair(issuer.seed)
const auditorKeys = deriveConfidentialKeypair(auditor.seed)
const holderKeys = deriveConfidentialKeypair(holder.seed)
const { ticker, mptIssuanceID } = setupData.stablecoin
console.log(`Issuer address: ${issuer.address}`)
console.log(`Holder address: ${holder.address}`)
console.log(`Auditor address: ${auditor.address}`)This example uses pre-configured data from a setup script, including public and private keys, but you can replace these with your own values.
For testing purposes, the example's private keys are stored in a JSON file by the setup script. In production, private keys should be stored in a secure, encrypted key store.
A clawback proof is built against the balance the issuer reads. If the holder spends or receives anything before the clawback, the balance changes, the proof goes stale, and the clawback fails.
Submit an MPTokenIssuanceSet transaction with the tfMPTLock flag to lock the MPT issuance for the holder. This requires the issuance to have the Can Lock flag enabled.
// Lock the issuance for the holder ----------------------
console.log(`\n=== Locking ${ticker} for the holder... ===`)
const lockTx = {
TransactionType: 'MPTokenIssuanceSet',
Account: issuer.address,
MPTokenIssuanceID: mptIssuanceID,
Holder: holder.address,
Flags: MPTokenIssuanceSetFlags.tfMPTLock
}
const lockResponse = await client.submitAndWait(lockTx, {
wallet: issuer,
autofill: true
})
if (lockResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
const resultCode = lockResponse.result.meta.TransactionResult
console.error('Error: Unable to lock the issuance:', resultCode)
await client.disconnect()
process.exit(1)
}
console.log(`${ticker} is locked for ${holder.address}.`)
console.log(`${EXPLORER}/transactions/${lockResponse.result.hash}`)The issuance's ConfidentialOutstandingAmount is the total confidential supply and is public, so the issuer can watch the clawback take effect without decrypting anything.
// Read the confidential supply before the clawback ----------------------
console.log(`\n=== Reading the confidential supply... ===`)
const mptIssuance = await fetchMPTokenIssuance(client, mptIssuanceID)
const confidentialSupply = BigInt(mptIssuance.ConfidentialOutstandingAmount)
console.log(`Confidential supply before clawback: ${confidentialSupply}`)The holder's MPToken entry also carries an IssuerEncryptedBalance, which holds the holder's spending and inbox balances added together, encrypted with the issuer's public key. That single total is what makes an unassisted clawback possible, because the issuer can read the exact amount without any cooperation from the holder.
Submit a ConfidentialMPTClawback transaction to claw back the holder's total confidential balance. The transaction carries the amount in the clear, along with a Zero-Knowledge Proof (ZKP) that the amount matches the IssuerEncryptedBalance it was read from. The two libraries split that work differently.
prepareConfidentialClawback needs only the issuer's keypair. It fetches the holder's MPToken entry, decrypts IssuerEncryptedBalance, and sets MPTAmount on the prepared transaction, which is where this example reads the amount from.
// Claw back the confidential balance ----------------------
console.log(`\n=== Clawing back the holder's confidential balance... ===`)
// prepareConfidentialClawback attaches the amount the issuer read and a
// Zero-Knowledge Proof (ZKP) that the amount matches the encrypted balance.
const clawbackTx = await prepareConfidentialClawback(client, {
account: issuer.address,
holder: holder.address,
mptIssuanceID,
issuerKeypair: issuerKeys
})
// A clawback takes the whole balance, so there is nothing left to reclaim if
// this sample has already run against this setup data. The protocol rejects a
// clawback of zero, so stop before submitting.
if (clawbackTx.MPTAmount === '0') {
console.error(
`Error: The confidential balance of ${holder.address} is already zero.`
)
console.error(
'Delete confidentialTransfersSetup.json and run the setup script again.'
)
await client.disconnect()
process.exit(1)
}
console.log(JSON.stringify(clawbackTx, null, 2))
const clawbackResponse = await client.submitAndWait(clawbackTx, {
wallet: issuer,
autofill: true
})
if (clawbackResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
const resultCode = clawbackResponse.result.meta.TransactionResult
console.error('Error: Unable to claw back the balance:', resultCode)
await client.disconnect()
process.exit(1)
}
console.log(`Clawed back ${clawbackTx.MPTAmount} ${ticker} from ${holder.address}.`)
console.log(`${EXPLORER}/transactions/${clawbackResponse.result.hash}`)The clawback sets both the spending balance and the inbox to encrypted zero, so every key that could read the holder's balance now reads zero. Decrypt all four copies to confirm this, one per key that has access.
// Verify the clawback ----------------------
console.log(`\n=== Verifying the clawback... ===`)
const mptoken = await fetchMPToken(client, holder.address, mptIssuanceID)
console.log(`MPToken entry:`)
console.log(JSON.stringify(mptoken, null, 2))
const crypto = await loadMptCrypto()
const [spendingBalance, inboxBalance, issuerView, auditorView] =
await Promise.all([
crypto.decryptAmount(
mptoken.ConfidentialBalanceSpending,
holderKeys.privateKey,
confidentialSupply
),
crypto.decryptAmount(
mptoken.ConfidentialBalanceInbox,
holderKeys.privateKey,
confidentialSupply
),
crypto.decryptAmount(
mptoken.IssuerEncryptedBalance,
issuerKeys.privateKey,
confidentialSupply
),
crypto.decryptAmount(
mptoken.AuditorEncryptedBalance,
auditorKeys.privateKey,
confidentialSupply
)
])
console.log(`\nHolder reads its spending balance as ${spendingBalance} ${ticker}.`)
console.log(`Holder reads its inbox balance as ${inboxBalance} ${ticker}.\n`)
console.log(`Issuer reads the holder's balance as ${issuerView} ${ticker}.`)
console.log(`Auditor reads the holder's balance as ${auditorView} ${ticker}.\n`)
const issuanceAfter = await fetchMPTokenIssuance(client, mptIssuanceID)
const supplyAfter = issuanceAfter.ConfidentialOutstandingAmount
console.log(`Confidential supply after the clawback: ${supplyAfter}`)
console.log(`Total supply in circulation (public + confidential): ${issuanceAfter.OutstandingAmount}`)
await client.disconnect()The clawed back tokens leave circulation entirely, which the issuance shows in public. ConfidentialOutstandingAmount drops by the amount clawed back, and OutstandingAmount drops with it.