Symbol KYC Required Mosaic

January 24, 2024

In Symbol, it's possible to create mosaics that can only be sent between preapproved accounts. The key feature that enables this is mosaic restrictions. In this article, we'll explore how to use this feature to restrict a mosaic to only accounts that have passed KYC.

First, please review this article about sending Symbol transactions with the Symbol SDK. We'll be using the prepare_and_send_transaction function to simplify the examples in this article.

Mosaics - Background

By default, a mosaic can only be sent to and from the mosaic owner. While this can be sufficient in some cases, it is limiting in others. Symbol defines common token paradigms as mosaic flags. Upon creation, a mosaic can adopt these behaviors by setting the corresponding flag. The available mosaic flags are:

  • SUPPLY_MUTABLE: Mosaic supports supply changes even when mosaic creator only owns a partial supply
  • TRANSFERABLE: Mosaic supports transfers between arbitrary accounts
  • RESTRICTABLE: Mosaic supports custom restrictions configured by the mosaic creator
  • REVOKABLE: Mosaic supports revocation of tokens by the mosaic creator

Restrictions

Symbol has a feature called mosaic restrictions that was created with security tokens in mind. Mosaic restrictions are composed of two parts:

  1. Global restriction that defines a constraint that must be satisfied by all accounts holding a mosaic
  2. Address restriction that sets a key value pair on an account that can be used to satisfy a global restriction

Mosaic global restrictions can be self-referential or refer to any arbitrary mosaic. The latter allows common attributes to be defined once and reused. For example, a KYC-provider could create a mosaic and assign KYC results using mosaic address restrictions. Any mosaics that want to be limited to KYC accounts could refer to that single list.

Setup

In this article, we'll create two mosaics:

  1. Mosaic that is being restricted. Since we want free transfers between all KYC accounts, it should be transferable. Since we want to restrict it from being held by non-KYC accounts, it should be restrictable.
  2. KYC Mosaic that is used to maintain address restrictions. Since we want to define KYC results via address restrictions associated with this mosaic, it must be restrictable.

Let's write a a few helper functions for creating and sending mosaics:

async def create_mosaic(facade, signer_key_pair, nonce, inital_supply, flags=0):
    embedded_transactions = [
        facade.transaction_factory.create_embedded({
            'type': 'mosaic_definition_transaction_v1',
            'signer_public_key': signer_key_pair.public_key,
            'nonce': nonce,
            'divisibility': 0,
            'flags': flags
        }),
        facade.transaction_factory.create_embedded({
            'type': 'mosaic_supply_change_transaction_v1',
            'signer_public_key': signer_key_pair.public_key,
            'mosaic_id': generate_mosaic_id(facade.network.public_key_to_address(signer_key_pair.public_key), nonce),
            'delta': inital_supply,
            'action': 'increase'
        })
    ]

    merkle_hash = facade.hash_embedded_transactions(embedded_transactions)

    await prepare_and_send_transaction(facade, signer_key_pair, {
        'type': 'aggregate_complete_transaction_v2',
        'signer_public_key': signer_key_pair.public_key,
        'transactions_hash': merkle_hash,
        'transactions': embedded_transactions
    })

async def send_mosaic(facade, signer_key_pair, mosaic_id, recipient_address, amount):
    await prepare_and_send_transaction(facade, signer_key_pair, {
        'type': 'transfer_transaction_v1',
        'recipient_address': recipient_address,
        'mosaics': [
            {'mosaic_id': mosaic_id, 'amount': amount}
        ]
    })

Next, let's prepare some accounts and create the two mosaics:

    facade = SymbolFacade('testnet')
    (signer_key_pair, signer_address) = get_test_account(facade, 1, 'signer')
    (recipient_key_pair, recipient_address) = get_test_account(facade, 2, 'recipient')
    (_, other_address) = get_test_account(facade, 3, 'other')

    nonce = 51
    await create_mosaic(facade, signer_key_pair, nonce, 100000, 'transferable restrictable')
    await create_mosaic(facade, signer_key_pair, nonce + 1, 1000, 'restrictable')

At this point, the main mosaic is fully transferable because no restrictions have been set. We can send the mosaic from the mosaic owner (signer) to a recipient account. In turn, the recipient account can send the mosaic to a third account that does not directly interact with the mosaic owner.

    mosaic_id = generate_mosaic_id(facade.network.public_key_to_address(signer_key_pair.public_key), nonce)
    await send_mosaic(facade, signer_key_pair, mosaic_id, recipient_address, 10)
    await send_mosaic(facade, recipient_key_pair, mosaic_id, signer_address, 5)
    await send_mosaic(facade, recipient_key_pair, mosaic_id, other_address, 3)

Restrictions Example

First, we need to create a global mosaic restriction on the KYC mosaic (reference_mosaic_id):

    reference_mosaic_id = generate_mosaic_id(facade.network.public_key_to_address(signer_key_pair.public_key), nonce + 1)
    await prepare_and_send_transaction(facade, signer_key_pair, {
        'type': 'mosaic_global_restriction_transaction_v1',

        'mosaic_id': reference_mosaic_id,
        'restriction_key': 1,
        'previous_restriction_value': 0,
        'new_restriction_value': 1,
        'previous_restriction_type': 'none',
        'new_restriction_type': 'ge'
    })

restriction_key is the locally defined identifier scoped to the KYC mosaic. The previous values must be 0 (restriction_value) and none (restriction_type) because this is a new restriction. The new restriction values are set to 1 (restriction_value) and ge (restriction_type). Since this is not intended to be used as a transferable mosaic, the values don't really matter.

Next, we set an address mosaic restriction for the recipient account:

    await prepare_and_send_transaction(facade, signer_key_pair, {
        'type': 'mosaic_address_restriction_transaction_v1',

        'mosaic_id': reference_mosaic_id,
        'restriction_key': 1,
        'previous_restriction_value': 0xFFFFFFFFFFFFFFFF,
        'new_restriction_value': 1,
        'target_address': recipient_address
    })

restriction_key must match what was used when creating the global mosaic restriction. The previous value must be 0xFFFFFFFFFFFFFFFF because it was previously unset. The new value is set to 1, but any unsigned 64-bit values are valid. Finally, we use the target_address to associate it with the recipient account.

In a real world scenario, the KYC provider would be setting address mosaic restrictions as accounts pass or fail KYC. Consequently, it would be set on a large number of accounts. For this example, we only set it on a single account.

Finally, we create a global mosaic restriction on the mosaic we want to restrict:

    await prepare_and_send_transaction(facade, signer_key_pair, {
        'type': 'mosaic_global_restriction_transaction_v1',

        'mosaic_id': mosaic_id,
        'reference_mosaic_id': reference_mosaic_id,
        'restriction_key': 1,
        'previous_restriction_value': 0,
        'new_restriction_value': 1,
        'previous_restriction_type': 'none',
        'new_restriction_type': 'ge'
    })

Importantly, reference_mosaic_id is set to the KYC mosaic and restriction_key matches what was used previously. This is what allows mosaic address restrictions to be shared across mosaics. The rule will constrict the mosaic to accounts with an address restriction of greater than or equal to (ge) 1.

ℹ️ new_restriction_value and new_restriction_type can be any valid values. They do not need to match the values used in the mosaic global restriction for the KYC mosaic.

Now that we have set up the restriction, let's see it in action:

    await send_mosaic(facade, signer_key_pair, mosaic_id, recipient_address, 10)
    await send_mosaic(facade, signer_key_pair, mosaic_id, other_address, 5)

The recipient account has an appropriate address restriction value set, so the first transfer succeeds. The other account does not, so the transfer fails with the error Failure_RestrictionMosaic_Account_Unauthorized. Notice that even the sender (mosaic creator) cannot override the restrictions and send the mosaic to ineligible accounts. This is what we were hoping to achieve!

Postscript

In a real implementation, a KYC provider would be responsible for verifying an account's identity. The provider could use a single restriction_key and set different values depending on authorization levels. For example, higher numbers could represent more thorough verifications. Alternatively, a KYC provider could use multiple restriction_keys to indicate various types of identifications. This decision is entirely up to the KYC provider.

A mosaic creator can then choose the appropriate restrictions to apply to his mosaic. Multiple mosaic global restrictions can be defined. All must be satisfied for the transfer to be allowed.