Restricting token transfers

Limit how accounts can transact with Token Restrictions.

Use case

Let’s say a company, CharlieChocolateFactory, wants to go public by tokenizing their shares and conducting an STO. They create a token ccf.shares and configure it to be restrictable. To comply with regulations, the company wants only the participants that have passed the KYC/AML process to buy and transact their stock.

This guide shows you how to use Bitxor’s Token Restriction feature to define rules that determine which participants can transact with ccf.shares.

Prerequisites

  • Complete creating a token guide.

  • Create accounts for CharlieChocolateFactory, Alice, And Bob.

  • Load CharlieChocolateFactory’s account with enough bitxor to pay for the transactions fees and creation of tokens.

Creating a restrictable token

Before starting to work with Token Restrictions, we need to have created a restrictable token. Only tokens with the restrictable property set to true at the moment of their creation can accept token restrictions.

  1. Start creating the new restrictable token ccf.shares.

bitxor-cli transaction token --profile ccfactory --sync

Do you want a non-expiring token? [y/n]: y
Enter token divisibility: 0
Do you want token to have supply mutable? [y/n]: y
Do you want token to be transferable? [y/n]: y
Do you want token to be restrictable? [y/n]: y
Enter amount of tokens: 1000
Transaction confirmed

The new token id is: 634a8ac3fc2b65b3
  1. Then, copy and save the token identifier. We will need it later to define restrictions.

Setting a Token Global Restriction

The company wants to add a restriction to only permit accounts with elevated statuses to interact with the asset. To achieve this, the company will add a token global restriction as {ccf.shares, KYC, EQ = 1}, which can be read as “only allow accounts to transact with the ccf.shares token if their KYC restriction key for it has a value equal to 1”.

../../_images/token-restriction-sto.png

Use case diagram

  1. Open a new file and place the token identifier value you got while creating the token in a variable named tokenId. Also, you should represent the key KYC with a numeric value encoded as a UInt64.

// replace with token id
const tokenIdHex = '634a8ac3fc2b65b3';
const tokenId = new TokenId(tokenIdHex);
const key = KeyGenerator.generateUInt64Key('KYC'.toLowerCase());
// replace with token id
const tokenIdHex = '634a8ac3fc2b65b3';
const tokenId = new bitxor_sdk_1.TokenId(tokenIdHex);
const key = bitxor_sdk_1.KeyGenerator.generateUInt64Key('KYC'.toLowerCase());
  1. Then, define a new TokenGlobalRestrictionTransaction. Pass the token id and keys you have defined in the previous step as arguments.

The SDK will also request the previous token restriction value and type for this key and token. As it is the first global restriction we are announcing, set the previousRestrictionValue to 0 and the tokenRestrictionType to None.

// replace with network type
const networkType = NetworkType.TEST_NET;

const transaction = TokenGlobalRestrictionTransaction.create(
  Deadline.create(epochAdjustment),
  tokenId, // tokenId
  key, // restrictionKey
  UInt64.fromUint(0), // previousRestrictionValue
  TokenRestrictionType.NONE, // previousRestrictionType
  UInt64.fromUint(1), // newRestrictionValue
  TokenRestrictionType.EQ, // newRestrictionType
  networkType,
  undefined,
  UInt64.fromUint(2000000),
);
// replace with network type
const networkType = bitxor_sdk_1.NetworkType.TEST_NET;
const transaction = bitxor_sdk_1.TokenGlobalRestrictionTransaction.create(
  bitxor_sdk_1.Deadline.create(epochAdjustment),
  tokenId, // tokenId
  key, // restrictionKey
  bitxor_sdk_1.UInt64.fromUint(0), // previousRestrictionValue
  bitxor_sdk_1.TokenRestrictionType.NONE, // previousRestrictionType
  bitxor_sdk_1.UInt64.fromUint(1), // newRestrictionValue
  bitxor_sdk_1.TokenRestrictionType.EQ, // newRestrictionType
  networkType,
  undefined,
  bitxor_sdk_1.UInt64.fromUint(2000000),
);
  1. After defining the global restriction, sign the transaction with the token creator’s account—CharlieChocolateFactory—and announce it to the network.

// replace with company private key
const privateKey =
  'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
const account = Account.createFromPrivateKey(privateKey, networkType);
// replace with meta.networkGenerationHash (nodeUrl + '/node/info')
const networkGenerationHash =
  '1DFB2FAA9E7F054168B0C5FCB84F4DEB62CC2B4D317D861F3168D161F54EA78B';
const signedTransaction = account.sign(transaction, networkGenerationHash);
// replace with node endpoint
const nodeUrl = 'NODE_URL';
const repositoryFactory = new RepositoryFactoryHttp(nodeUrl);
const transactionHttp = repositoryFactory.createTransactionRepository();

transactionHttp.announce(signedTransaction).subscribe(
  (x) => console.log(x),
  (err) => console.error(err),
);
// replace with company private key
const privateKey =
  'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
const account = bitxor_sdk_1.Account.createFromPrivateKey(
  privateKey,
  networkType,
);
// replace with meta.networkGenerationHash (nodeUrl + '/node/info')
const networkGenerationHash =
  '1DFB2FAA9E7F054168B0C5FCB84F4DEB62CC2B4D317D861F3168D161F54EA78B';
const signedTransaction = account.sign(transaction, networkGenerationHash);
// replace with node endpoint
const nodeUrl = 'NODE_URL';
const repositoryFactory = new bitxor_sdk_1.RepositoryFactoryHttp(nodeUrl);
const transactionHttp = repositoryFactory.createTransactionRepository();
transactionHttp.announce(signedTransaction).subscribe(
  (x) => console.log(x),
  (err) => console.error(err),
);

Assigning Token Address Restrictions

When investors complete the KYC/AML process, the CharlieChocolateFactory alters their accounts with a TokenAddressRestrictionTransaction with parameters ccf.shares, KYC, 1, allowing certified investors to participate in the STO. Others who have not provided the necessary information will not be able to receive or trade the asset.

Alice, a potential investor, passes the KYC process. Once Alice has been verified, the company tags Alice’s account with the token address restriction {ccf.shares, Alice, KYC, 1}. On the other hand, Bob, another interested investor, did not pass the KYC process. Bob’s account is not eligible to receive ccf.shares as it does not meet the token global restriction requirements. Nevertheless, CharlieCholocalteFatory decides to tag the account with the token address restriction {ccf.shares, Bob, KYC, 0}. Doing so, they know that Bob has attempted and failed the KYC process.

  1. Define both TokenAddressRestrictionTransaction for Alice and Bob accounts as follows:

  • Alice: {ccf.shares, Alice, KYC, 1}

  • Bob: {ccf.shares, Bob, KYC, 0}

// replace with network type
const networkType = NetworkType.TEST_NET;
// replace with token id
const tokenIdHex = '634a8ac3fc2b65b3';
const tokenId = new TokenId(tokenIdHex);
// replace with address
const aliceRawAddress = 'BXRBDE-NCLKEB-ILBPWP-3JPB2X-NY64OE-7PYHHE-32I';
const aliceAddress = Address.createFromRawAddress(aliceRawAddress);
// replace with address
const bobRawAddress = 'BXRQ5E-YACWBP-CXKGIL-I6XWCH-DRFLTB-KUK34I-YJQ';
const bobAddress = Address.createFromRawAddress(bobRawAddress);

const key = KeyGenerator.generateUInt64Key('KYC'.toLowerCase());

const aliceTokenAddressRestrictionTransaction = TokenAddressRestrictionTransaction.create(
  Deadline.create(epochAdjustment),
  tokenId, // tokenId
  key, // restrictionKey
  aliceAddress, // address
  UInt64.fromUint(1), // newRestrictionValue
  networkType,
);

const bobTokenAddressRestrictionTransaction = TokenAddressRestrictionTransaction.create(
  Deadline.create(epochAdjustment),
  tokenId, // tokenId
  key, // restictionKey
  bobAddress, // address
  UInt64.fromUint(0), // newRestrictionValue
  networkType,
);
// replace with network type
const networkType = bitxor_sdk_1.NetworkType.TEST_NET;
// replace with token id
const tokenIdHex = '634a8ac3fc2b65b3';
const tokenId = new bitxor_sdk_1.TokenId(tokenIdHex);
// replace with address
const aliceRawAddress = 'BXRBDE-NCLKEB-ILBPWP-3JPB2X-NY64OE-7PYHHE-32I';
const aliceAddress = bitxor_sdk_1.Address.createFromRawAddress(aliceRawAddress);
// replace with address
const bobRawAddress = 'BXRQ5E-YACWBP-CXKGIL-I6XWCH-DRFLTB-KUK34I-YJQ';
const bobAddress = bitxor_sdk_1.Address.createFromRawAddress(bobRawAddress);
const key = bitxor_sdk_1.KeyGenerator.generateUInt64Key('KYC'.toLowerCase());
const aliceTokenAddressRestrictionTransaction = bitxor_sdk_1.TokenAddressRestrictionTransaction.create(
    bitxor_sdk_1.Deadline.create(epochAdjustment),
    tokenId, // tokenId
    key, // restrictionKey
    aliceAddress, // address
    bitxor_sdk_1.UInt64.fromUint(1), // newRestrictionValue
    networkType,
);
const bobTokenAddressRestrictionTransaction = bitxor_sdk_1.TokenAddressRestrictionTransaction.create(
    bitxor_sdk_1.Deadline.create(epochAdjustment),
    tokenId, // tokenId
    key, // restictionKey
    bobAddress, // address
    bitxor_sdk_1.UInt64.fromUint(0), // newRestrictionValue
    networkType,
);
  1. Now, you can announce the transactions to the network. To do so, try to announce both transactions together using an aggregate transaction. Remember that you will have to announce the transactions from the token’s creator account.

// replace with company private key
const privateKey =
  'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
const account = Account.createFromPrivateKey(privateKey, networkType);

const aggregateTransaction = AggregateTransaction.createComplete(
  Deadline.create(epochAdjustment),
  [
    aliceTokenAddressRestrictionTransaction.toAggregate(account.publicAccount),
    bobTokenAddressRestrictionTransaction.toAggregate(account.publicAccount),
  ],
  networkType,
  [],
  UInt64.fromUint(2000000),
);

// replace with meta.networkGenerationHash (nodeUrl + '/node/info')
const networkGenerationHash =
  '1DFB2FAA9E7F054168B0C5FCB84F4DEB62CC2B4D317D861F3168D161F54EA78B';
const signedTransaction = account.sign(
  aggregateTransaction,
  networkGenerationHash,
);
console.log(signedTransaction.hash);
// replace with node endpoint
const nodeUrl = 'NODE_URL';
const repositoryFactory = new RepositoryFactoryHttp(nodeUrl);
const transactionHttp = repositoryFactory.createTransactionRepository();

transactionHttp.announce(signedTransaction).subscribe(
  (x) => console.log(x),
  (err) => console.error(err),
);
// replace with company private key
const privateKey =
    'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
const account = bitxor_sdk_1.Account.createFromPrivateKey(
    privateKey,
    networkType,
);
const aggregateTransaction = bitxor_sdk_1.AggregateTransaction.createComplete(
    bitxor_sdk_1.Deadline.create(epochAdjustment), [
        aliceTokenAddressRestrictionTransaction.toAggregate(account.publicAccount),
        bobTokenAddressRestrictionTransaction.toAggregate(account.publicAccount),
    ],
    networkType, [],
    bitxor_sdk_1.UInt64.fromUint(2000000),
);
// replace with meta.networkGenerationHash (nodeUrl + '/node/info')
const networkGenerationHash =
    '1DFB2FAA9E7F054168B0C5FCB84F4DEB62CC2B4D317D861F3168D161F54EA78B';
const signedTransaction = account.sign(
    aggregateTransaction,
    networkGenerationHash,
);
console.log(signedTransaction.hash);
// replace with node endpoint
const nodeUrl = 'NODE_URL';
const repositoryFactory = new bitxor_sdk_1.RepositoryFactoryHttp(nodeUrl);
const transactionHttp = repositoryFactory.createTransactionRepository();
transactionHttp.announce(signedTransaction).subscribe(
    (x) => console.log(x),
    (err) => console.error(err),
);
  1. Once the transaction gets confirmed, try to send tokens to Alice’s and Bob’s accounts.

Now, you should be able to send ccf.shares to Alice without any problems. Additionally, Alice will be able to transfer tokens with other accounts with restrictions set to {ccf.shares, KYC, 1}.

bitxor-cli transaction transfer --recipient-address BXRBDE-NCLKEB-ILBPWP-3JPB2X-NY64OE-7PYHHE-32I --tokens 634a8ac3fc2b65b3::1 --sync

However, when you send the same token to Bob’s account, you should get the error Failure_RestrictionToken_Account_Unauthorized through the status error channel because he is not allowed to transact with ccf.shares.