Skip to content
This repository was archived by the owner on Feb 23, 2023. It is now read-only.

Latest commit

 

History

History
404 lines (313 loc) · 16.5 KB

clawback.mdx

File metadata and controls

404 lines (313 loc) · 16.5 KB
title order
Clawbacks

import { CodeExample } from "components/CodeExample"; import { Alert } from "components/Alert";

Make some changes.

Protocol 17 introduces operations that allow asset issuers to maintain tighter control over how their asset is distributed to the world. Specifically, it gives them power to burn their asset from a trustline or claimable balance, effectively removing it from the recipient's balance sheet:

The amount of the asset clawed back is burned and is not sent to any other address. The issuer may reissue the asset to the same account or to another account if the intent of the clawback is to move the asset to another account.

This allows for things like regulatory enforcement, safety and control over certain assets, etc. You can refer to CAP-35 for more motivations or technical details behind these new features.

Relevant operations

Configuring and performing a clawback involves several discrete steps:

  • the issuer: sets up their account to enable clawbacks (via SetOptions),
  • the recipients: establish trustlines for the issuer's asset (via ChangeTrust),
  • someone transfers value with the issued asset (e.g. via a Payment or other mechanism), and finally
  • the issuer claws back some or all of the asset (via Clawback or Clawback Claimable Balance).

Setting Account Options

In order to enable clawback, the issuer account must first set the AUTH_CLAWBACK_ENABLED flag on itself. This will cause every subsequent trustline established from this account to have its corresponding TRUSTLINE_CLAWBACK_ENABLED_FLAG set automatically.

Note an important corequirement described in the CAP:

If an issuer wishes to set AUTH_CLAWBACK_ENABLED_FLAG, it must also have AUTH_REVOCABLE_FLAG set.

This allows an asset issuer to clawback balances locked up in offers by first revoking authorization from a trustline, which pulls all offers that involve that trustline. The issuer can then perform the clawback.

If you (as an asset issuer) would like to forgo clawback capabilities on a specific trustline, you can clear its TRUSTLINE_CLAWBACK_ENABLED_FLAG flag via SetTrustLineFlags. Note that you can only clear this flag, not set it, in order to give asset holders perpetual confidence about the future state of their holdings.

Clawback

Once an account holds a particular asset for which clawbacks have been enabled, you can claw it back (provided you are the issuer, obviously). You need to provide the asset, a quantity, and the account from which you're clawing back the asset.

Clawback Claimable Balance

Claimable balances, introduced in Protocol 15, provide another way for accounts to acquire assets. They are a special, two-part payment, and thus need to be clawed back differently. All you need to do is provide the claimable balance ID.

Examples

Here we'll cover the two main approaches to clawing back an asset. An issuing account ("Account A") will create an asset and send some to a top-level recipient ("Account B"), who will then make it available to a third party ("Account C"). We'll add this level of indirection to demonstrate that clawbacks don't rely on direct relationships between accounts.

Finally, A will perform a clawback on the asset from C. In one scenario, Account B will pay Account C directly. In the other, Account B will transfer the asset to Account C via a claimable balance.

In the following code samples, proper error checking is omitted for brevity. However, you should always validate your results, as there are many ways that requests can fail. You can refer to the guide on Handling Errors Gracefully for tips on error management strategies.

Preamble: Issuing a Clawback-able Asset

First, we'll set up an account to enable clawbacks and issue an asset accordingly. Note that properly issuing an asset (with separate issuing and distribution accounts) is a little more involved, but you can refer to the tutorial for more; we use a simpler method here for brevity.

const sdk = require("stellar-sdk");

let server = new sdk.Server("https://horizon-testnet.stellar.org");

const A = sdk.Keypair.fromSecret("SAQLZCQA6AYUXK6JSKVPJ2MZ5K5IIABJOEQIG4RVBHX4PG2KMRKWXCHJ");
const B = sdk.Keypair.fromSecret("SAAY2H7SANIS3JLFBFPLJRTYNLUYH4UTROIKRVFI4FEYV4LDW5Y7HDZ4");
const C = sdk.Keypair.fromSecret("SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4");

const ASSET = new sdk.Asset("CLAW", A.publicKey());

/// Enables AuthClawbackEnabledFlag on an account.
function enableClawback(account, keys) {
  return server.submitTransaction(buildTx(
    account, keys, [
      sdk.Operation.setOptions({
        setFlags: sdk.AuthClawbackEnabledFlag | sdk.AuthRevocableFlag,
      }),
    ],
  ));
}

/// Establishes a trustline for `recipient` for ASSET (from above).
const establishTrustline = function (recipient, key) {
  return server.submitTransaction(buildTx(
    recipient, key, [
      sdk.Operation.changeTrust({
        asset: ASSET,
        limit: "5000", // arbitrary
      }),
    ],
  ));
};

/// Retrieves latest account info for all accounts.
function getAccounts() {
  return Promise.all([
    server.loadAccount(A.publicKey()),
    server.loadAccount(B.publicKey()),
    server.loadAccount(C.publicKey()),
  ]);
}

/// Enables clawback on A, and establishes trustlines from C, B -> A.
function preamble() {
  return getAccounts().then(function (accounts) {
    let [ accountA, accountB, accountC ] = accounts;
    return enableClawback(accountA, A)
      .then(Promise.all([
        establishTrustline(accountB, B),
        establishTrustline(accountC, C),
      ])
    );
  });
}

/// Helps simplify creating & signing a transaction.
function buildTx(source, signer, ops) {
  var tx = new StellarSdk.TransactionBuilder(source, {
    fee: 100,
    networkPassphrase: StellarSdk.Networks.TESTNET,
  });
  ops.forEach((op) => tx.addOperation(op));
  tx = tx.setTimeout(30).build();
  tx.sign(signer);
  return tx;
}

/// Prints the balances of a list of accounts.
function showBalances(accounts) {
  accounts.forEach((acc) => {
    console.log(`${acc.accountId().substring(0, 5)}: ${getBalance(acc)}`);
  });
}

Note that above we first enabled clawback and then established trustlines, since you cannot retroactively enable clawback on existing trustlines.

Example: Payments

With the shared setup code out of the way, we can now demonstrate how clawback works for payments. This example will highlight how the asset issuer holds control over their asset regardless of how it gets distributed to the world.

In our scenario, Account A will pay Account B with 1000 tokens of its custom asset; then, B will pay Account C 500 tokens in turn. Finally, A will "claw back" half of C's balance, burning 250 tokens forever. Let's dive in to the helper functions:

/// Make a payment to `toAccount` from `fromAccount` for `amount`.
function makePayment(toAccount, fromAccount, fromKey, amount) {
  return server.submitTransaction(buildTx(
    fromAccount, fromKey, [
      sdk.Operation.payment({
        destination: toAccount.accountId(),
        asset: ASSET,   // defined in preamble
        amount: amount,
      }),
    ],
  ));
};

/// Perform a clawback by `byAccount` of `amount` from `fromAccount`.
function doClawback(byAccount, byKey, fromAccount, amount) {
  return server.submitTransaction(buildTx(
    byAccount, byKey, [
      sdk.Operation.clawback({
        from: fromAccount.accountId(),
        asset: ASSET,   // defined in preamble
        amount: amount,
      }),
    ],
  ));
};

/// Retrieves the balance of ASSET in `account`.
function getBalance(account) {
  const balances = account.balances.filter((balance) => {
    return (balance.asset_code   == ASSET.code && 
            balance.asset_issuer == ASSET.issuer);
  });
  return balances.length > 0 ? balances[0].balance : "0";
}

These snippets will help us with the final composition: making some payments to distribute the asset to the world and clawing some of it back.

function examplePaymentClawback() {
  return getAccounts().then(function(accounts) {
    let [ accountA, accountB, accountC ] = accounts;
    return  makePayment(accountB, accountA, A, "1000")
      .then(makePayment(accountC, accountB, B, "500"))
      .then(doClawback( accountA, A, accountC, "250"));
  })
  .then(getAccounts)
  .then(showBalances);
}

preamble().then(examplePaymentClawback);

After running our example, we should see the balances reflect the example flow:

GCIHA: 0
GDS5N: 500
GC2BK: 250

Notice that GCIHA (this is Account A, the issuer) holds none of the asset, despite clawing back 250 from Account C. This should drive home the fact that clawed-back assets are burned, not transferred.

(It may be strange that A never holds any tokens of its custom asset, but that's exactly how issuing works: you create value where there used to be none. Sending an asset to its issuing account is equivalent to burning it, and auditing the total amount of an asset in existence is one of the benefits of properly distributing an asset via a distribution account, which we avoid doing here for example brevity.)

Example: Claimable Balances

Direct payments aren't the only way to transfer assets between accounts: claimable balances essentially create a "two-part payment" in which a valid claimant must actively accept the transfer. As a separate payment mechanism, they also need a separate clawback mechanism.

We need some additional helper methods to get started working efficiently with claimable balances:

function createClaimable(fromAccount, fromKey, toAccount, amount) {
  return server.submitTransaction(buildTx(
    fromAccount, fromKey, [
      sdk.Operation.createClaimableBalance({
        asset: ASSET,
        amount: amount,
        claimants: [
          new sdk.Claimant(toAccount.accountId()),
        ],
      }),
    ],
  ));
}

// https://developers.stellar.org/docs/glossary/claimable-balance/#example
function getBalanceId(txResponse) {
  const txResult = sdk.xdr.TransactionResult.fromXDR(
    txResponse.result_xdr, "base64");
  const operationResult = txResult.result().results()[0];

  let creationResult = operationResult.value().createClaimableBalanceResult();
  return creationResult.balanceId().toXDR("hex");
}

function clawbackClaimable(issuerAccount, issuerKey, balanceId) {
  return server.submitTransaction(buildTx(
    issuerAccount, issuerKey, [
      sdk.Operation.clawbackClaimableBalance({ balanceId })
    ],
  ));
}

Now, we can fulfill the flow: A pays B, who sends a claimable balance to C, who gets it clawed back by A. (Note that we rely on the makePayment helper from the earlier example.)

function exampleClaimableBalanceClawback() {
  return getAccounts()
    .then(function(accounts) {
      let [ accountA, accountB, accountC ] = accounts;

      return makePayment(accountB, accountA, A, "1000")
        .then(() => createClaimable(accountB, B, accountC, "500"))
        .then((txResp) => clawbackClaimable(accountA, A, getBalanceId(txResp)));
    })
    .then(getAccounts)
    .then(showBalances);
}

After running preamble().then(examplePaymentClawback), we should see the balances reflect our flow:

GCIHA: 0
GDS5N: 500
GC2BK: 0

Note that after a particular claimable balance has been claimed, this method no longer applies: you should claw back your asset directly using the previous method, via Operation.clawback. This method is for unclaimed, pending claimable balances.

Further note the divergence from the earlier example: you can't do a partial clawback of the claimable balance. It's an all-or-nothing operation.

Example: Selectively Enabling Clawback

When you enable the AUTH_CLAWBACK_ENABLED_FLAG on your account, it will make all future trustlines have clawback enabled for any of your issued assets. This may not always be desireable: you may want certain assets to behave as they did before. Though you could work around this by reissuing assets from a "dedicated clawback" account, you can also simply disable clawbacks for certain trustlines by clearing the TRUST_LINE_CLAWBACK_ENABLED_FLAG on a trustline.

In the following example, we'll have an account (Account A, as before) issue a new asset and distribute it to a second account (Account B). We won't reintroduce Account C like in the earlier examples in order to keep things simple. Then, we'll demonstrate how A claws back some of the asset from B, then clears the trustline and can no longer claw back the asset.

First, let's prepare the accounts (note that we are relying here on helper functions defined in the earlier examples):

function getAccounts() {
  return Promise.all([
    server.loadAccount(A.publicKey()),
    server.loadAccount(B.publicKey()),
  ]);
}

function preambleRedux() {
  return getAccounts().then((accounts) => {
    return enableClawback(accounts[0], A)
      .then(() => establishTrustline(accounts[1], B));
  });
}

Now, let's distribute some of our asset to Account B, just to claw it back. Then, we'll clear the flag from the trustline and show that another clawback isn't possible:

function disableClawback(issuerAccount, issuerKeys, forTrustor) {
  return server.submitTransaction(buildTx(
    issuerAccount, issuerKeys, [
      sdk.Operation.setTrustLineFlags({
        trustor: forTrustor.accountId(), 
        asset: ASSET,   // defined in the (original) preamble
        flags: {
          clawbackEnabled: false,
        },
      }),
    ]
  ));
}

function exampleSelectiveClawback() {
  return getAccounts().then((accounts) => {
    let [ accountA, accountB ] = accounts;
    return makePayment(accountB, accountA, A, "1000")
      .then(getAccounts).then(showBalances)
      .then(() => doClawback(accountA, A, accountB, "500"))
      .then(getAccounts).then(showBalances)
      .then(() => disableClawback(accountA, A, accountB))
      .then(() => doClawback(accountA, A, accountB, "500"))
      .catch((err) => {
        if (err.response && err.response.data) {
          // Note that this is a *very* specific way to check for an error, and
          // you should probably never do it this way.
          // We do this here to demonstrate that the clawback error *does*
          // occur as expected.
          const opErrors = err.response.data.extras.result_codes.operations;
          if (opErrors && opErrors.length > 0 &&
              opErrors[0] === "op_no_clawback_enabled") {
            console.info("Clawback failed, as expected!");
          } else {
            console.error("Uh-oh, some other failure occurred:", err.response.data.extras);
          }
        } else {
          console.error("Uh-oh, unknown failure:", err);
        }
      });
  })
  .then(getAccounts)
  .then(showBalances);
}

Run the example (e.g. via preambleRedux().then(exampleSelectiveClawback)) and observe its result:

GCIHA: 0
GDS5N: 1000
GCIHA: 0
GDS5N: 500
Clawback failed, as expected!
GCIHA: 0
GDS5N: 500

It's important to note that clearing the clawback flag on a trustline is irreversible: you cannot set the flag, only clear it. This behavior is intended and aligns with the earlier caveat that only new trustlines follow the account's clawback flag: it's done so that you cannot retroactively "change the rules" on your asset's holders after they've established trustlines. If you'd like to enable clawback again, holders need to reissue their trustlines.