Skip to content
Last updated
Edit

This tutorial shows you how to claw back tokens from a LoanBroker on the XRP Ledger. The clawback feature enables token issuers (Multi-Purpose or Trust Line) to meet regulatory standards and claw back funds even if those funds have been deposited as first-loss capital.

Requires the LendingProtocol amendment. Loading...

Goals

By the end of this tutorial, you will be able to:

  • Deposit an MPT as first-loss capital into a LoanBroker entry.
  • Verify the MPT issuer.
  • Claw back all first-loss capital from the LoanBroker entry.
  • Check the cover available for the LoanBroker entry.

Prerequisites

To complete this tutorial, you should:

Source Code

You can find the complete source code for this tutorial's examples in the code samples section of this website's repository.

Steps

1. Install dependencies

From the code sample folder, use npm to install dependencies.

npm install xrpl

2. Set up client and accounts

To get started, import the necessary libraries and instantiate a client to connect to the XRPL. This example imports:

  • xrpl: Used for XRPL client connection, transaction submission, and wallet handling.
  • fs and child_process: Used to run tutorial set up scripts.
// IMPORTANT: This example deposits and claws back first-loss capital from a
// preconfigured LoanBroker entry. The first-loss capital is an MPT
// with clawback enabled.

import fs from 'fs'
import { execSync } from 'child_process'
import xrpl from 'xrpl'

// Connect to the network ----------------------
const client = new xrpl.Client('wss://s.devnet.rippletest.net:51233')
await client.connect()

Next, load the loan broker account, MPT issuer account, loan broker ID, and MPT ID.

// This step checks for the necessary setup data to run the lending protocol tutorials.
// If missing, lendingSetup.js will generate the data.
if (!fs.existsSync('lendingSetup.json')) {
  console.log(`\n=== Lending tutorial data doesn't exist. Running setup script... ===\n`)
  execSync('node lendingSetup.js', { stdio: 'inherit' })
}

// Load preconfigured accounts and LoanBrokerID.
const setupData = JSON.parse(fs.readFileSync('lendingSetup.json', 'utf8'))

// You can replace these values with your own
const loanBroker = xrpl.Wallet.fromSeed(setupData.loanBroker.seed)
const mptIssuer = xrpl.Wallet.fromSeed(setupData.depositor.seed)
const loanBrokerID = setupData.loanBrokerID
const mptID = setupData.mptID

console.log(`\nLoan broker address: ${loanBroker.address}`)
console.log(`MPT issuer address: ${mptIssuer.address}`)
console.log(`LoanBrokerID: ${loanBrokerID}`)
console.log(`MPT ID: ${mptID}`)

This example uses preconfigured accounts, MPTs, and loan broker data from the lendingSetup.js script, but you can replace loanBroker, mptIssuer, loanBrokerID, and mptID with your own values.

3. Check initial cover available

Check the initial cover (first-loss capital) available using the ledger_entry method.

// Check cover available ----------------------
console.log(`\n=== Cover Available ===\n`)
const coverInfo = await client.request({
  command: 'ledger_entry',
  index: loanBrokerID,
  ledger_index: 'validated'
})

let currentCoverAvailable = coverInfo.result.node.CoverAvailable || '0'
console.log(`${currentCoverAvailable} TSTUSD`)

If the CoverAvailable field is missing, it means no first-loss capital has been deposited.

4. Prepare LoanBrokerCoverDeposit transaction

Create the LoanBrokerCoverDeposit transaction object.

// Prepare LoanBrokerCoverDeposit transaction ----------------------
console.log(`\n=== Preparing LoanBrokerCoverDeposit transaction ===\n`)
const coverDepositTx = {
  TransactionType: 'LoanBrokerCoverDeposit',
  Account: loanBroker.address,
  LoanBrokerID: loanBrokerID,
  Amount: {
    mpt_issuance_id: mptID,
    value: '1000'
  }
}

// Validate the transaction structure before submitting
xrpl.validate(coverDepositTx)
console.log(JSON.stringify(coverDepositTx, null, 2))

The Amount field specifies the MPT and amount to deposit as first-loss capital.

If the transaction succeeds, the amount is deposited and held in the pseudo-account associated with the LoanBroker entry.

5. Submit LoanBrokerCoverDeposit transaction

Sign and submit the LoanBrokerCoverDeposit transaction to the XRP Ledger.

// Sign, submit, and wait for deposit validation ----------------------
console.log(`\n=== Submitting LoanBrokerCoverDeposit transaction ===\n`)
const depositResponse = await client.submitAndWait(coverDepositTx, {
  wallet: loanBroker,
  autofill: true
})

if (depositResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
  const resultCode = depositResponse.result.meta.TransactionResult
  console.error('Error: Unable to deposit cover:', resultCode)
  await client.disconnect()
  process.exit(1)
}
console.log('Cover deposit successful!')

Verify that the transaction succeeded by checking for a tesSUCCESS result code.

6. Check updated cover available

Retrieve the cover available from the transaction result by checking the LoanBroker entry in the transaction metadata.

// Extract updated cover available after deposit ----------------------
console.log(`\n=== Cover Available After Deposit ===\n`)
let loanBrokerNode = depositResponse.result.meta.AffectedNodes.find(node =>
  node.ModifiedNode?.LedgerEntryType === 'LoanBroker'
)

currentCoverAvailable = loanBrokerNode.ModifiedNode.FinalFields.CoverAvailable
console.log(`${currentCoverAvailable} TSTUSD`)

7. Verify the asset issuer

Before executing a clawback, verify that the account submitting the transaction is the same as the asset issuer.

// Verify issuer of cover asset matches ----------------------
// Only the issuer of the asset can submit clawback transactions.
// The asset must also have clawback enabled.
console.log(`\n=== Verifying Asset Issuer ===\n`)
const assetIssuerInfo = await client.request({
  command: 'ledger_entry',
  mpt_issuance: mptID,
  ledger_index: 'validated'
})

if (assetIssuerInfo.result.node.Issuer !== mptIssuer.address) {
  console.error(`Error: ${assetIssuerInfo.result.node.Issuer} does not match account (${mptIssuer.address}) attempting clawback!`)
  await client.disconnect()
  process.exit(1)
}

console.log(`MPT issuer account verified: ${mptIssuer.address}. Proceeding to clawback.`)

Clawback functionality is disabled by default. In the case of MPTs, the tfMPTCanClawback flag must be enabled when the MPTokenIssuanceCreate transaction is submitted. This tutorial uses an MPT that is already configured for clawback.

8. Prepare LoanBrokerCoverClawback transaction

Create the LoanBrokerCoverClawback transaction object.

// Prepare LoanBrokerCoverClawback transaction ----------------------
console.log(`\n=== Preparing LoanBrokerCoverClawback transaction ===\n`)
const coverClawbackTx = {
  TransactionType: 'LoanBrokerCoverClawback',
  Account: mptIssuer.address,
  LoanBrokerID: loanBrokerID,
  Amount: {
    mpt_issuance_id: mptID,
    value: currentCoverAvailable
  }
}

// Validate the transaction structure before submitting
xrpl.validate(coverClawbackTx)
console.log(JSON.stringify(coverClawbackTx, null, 2))

In this example we claw back the entire amount, but you can specify any amount so long as it doesn't exceed the available cover or reduce the cover below the minimum required by the LoanBroker.

9. Submit LoanBrokerCoverClawback transaction

Sign and submit the LoanBrokerCoverClawback transaction to the XRP Ledger.

// Sign, submit, and wait for clawback validation ----------------------
console.log(`\n=== Submitting LoanBrokerCoverClawback transaction ===\n`)
const clawbackResponse = await client.submitAndWait(coverClawbackTx, {
  wallet: mptIssuer,
  autofill: true
})

if (clawbackResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
  const resultCode = clawbackResponse.result.meta.TransactionResult
  console.error('Error: Unable to clawback cover:', resultCode)
  await client.disconnect()
  process.exit(1)
}

console.log(`Successfully clawed back ${currentCoverAvailable} TSTUSD!`)

Verify that the transaction succeeded by checking for a tesSUCCESS result code.

10. Check final cover available

Retrieve the final cover available from the transaction result.

// Extract final cover available ----------------------
console.log(`\n=== Final Cover Available After Clawback ===\n`)
loanBrokerNode = clawbackResponse.result.meta.AffectedNodes.find(node =>
  node.ModifiedNode?.LedgerEntryType === 'LoanBroker'
)

console.log(`${loanBrokerNode.ModifiedNode.FinalFields.CoverAvailable || '0'} TSTUSD`)

await client.disconnect()

See Also

Concepts:

Tutorials:

References: