GitHub Logo HIP-796: Lockable Fractional Amounts of Fungible Tokens on Hedera

Author Sam Wood, Stephanie Yi, Eleonora Odorizzi, Vicky Lio
Working Group Atul Mahamuni, Leemon Baird, Richard Bair, Jasper Potts
Requested By TOKO
Discussions-To https://github.com/hashgraph/hedera-improvement-proposal/discussions/797
Status Accepted
Needs Council Approval Yes
Type Standards Track
Category Service
Created 2023-08-30
Updated 2024-02-06

Abstract

Adds the ability to partition fungible and non-fungible tokens held by an account, and to lock a subset of tokens in an account or account partition, preventing those locked tokens from being transferred.

Motivation

Regulatory and compliance requirements for issuers of securities and derivatives often require that subsets of tokens owned by a single entity be tracked and treated differently. For example, the tokens owned by an account may need to be partitioned into different tranches, each with different lockup periods, or different tax treatment. By partitioning the token balance of an account, and managing how many of those tokens are locked, it is possible to model complex legal requirements for tokens directly on the ledger. Partitioning and locking make it possible to attract more use cases and developers from “traditional finance” to the Hedera Token Service. It is particularly beneficial for regulatory compliance, as it enables the token issuer to maintain precise control over the token supply and manage it in accordance with regulatory requirements directly on the ledger. It will enable new applications with needs such as fractional ownership in real-world assets and complex token mechanics for securities and derivatives.

The ability to lock individual tokens held by an account provides increased flexibility to customize tokens to meet specific needs while maintaining a high level of security and compliance. It enables token issuers to temporarily halt the transfer or trading of specific tokens within a type without affecting the entire supply of that type of token owned by the account or by a partition of the account. This temporary halt to the transfer or trading of specific tokens will be especially useful in situations where token issuers need to quickly respond to regulatory compliance, or technical issues without disrupting the overall functionality of the supply, other accounts, or even all tokens of a specific kind within an account or partition.

Some use cases that can now be supported by locking are:

  • Time-based access: A token can be locked, and a program can be used to unlock some of those tokens at some specific date or time, allowing access to a particular asset or service only after a predetermined period.
  • Joint escrow: A token can be locked until multiple parties agree to unlock it, allowing for a virtual joint escrow of an asset. Joint escrow in an LFT enables the locking of an asset until multiple parties agree to unlock it. This provides an added layer of security for transactions.
  • Conditional transfers: A token can be locked until certain conditions are met, such as the receipt of payment, completion of a task, or other criteria. This could be implemented via a smart contract that has the lock key.

Overall, introducing the ability to partition and lock/unlock tokens would enable the Hedera network to better cater to multiple industries and would provide:

  1. Flexibility This will allow greater flexibility with potential access to new use cases and markets, allowing Hedera to meet the diverse needs of different industries and additional applications that the existing specifications and services don’t currently meet.

  2. Regulatory Compliance Traditional financial institutions and enterprises often require a high level of regulatory compliance to ensure security, transparency, and accuracy that existing specifications and services do not address.

  3. Scalability The proposed partitioning functionality for Hedera Fungible Tokens improves scalability by allowing multiple tokens to be grouped and managed within a single token-definition, rather than individually. This significantly reduces computational load and transaction costs, particularly for operations like metadata updates or token transfers that are executed at the partition level, as opposed to individual shares or units when modeled as an NFT. By reducing the number of transactions required for large-scale operations, this approach mitigates potential network congestion and ensures faster transaction processing, making partitioning an efficient solution for managing diverse token supplies at scale.

  4. User Experience Partitioning would allow users to manage multiple tokens with different properties and specifications within the same supply, greatly improving user experience.

Terminology

  1. token-definition: The definition of the token, as defined by the TokenCreateTransactionBody and TokenUpdateTransactionBody and stored in state on the ledger. For example, the AcmeToken.
  2. token-issuer: The user that created the token-definition.
  3. token-administrator: The user holding the token-definition’s admin-key.
  4. partition-definition: The definition of a partition of a token-definition. For example, AcmeToken.Tranche-A, or AcmeToken.Tranche-B. Partition definitions are a type of token definition.
  5. partition: An “instance” of a partition-definition, bound to a particular account. For example, Alice may have 1000 AcmeToken.Tranche-A tokens, and 2000 AcmeToken.Tranche-B tokens, giving a total ownership of 3000 AcmeTokens, while Bob may have 500 AcmeToken.Tranche-A tokens. All three of these are different partitions, two of them defined by the AcmeToken.Trance-A partition definition, and one of them defined by the AcmeToken.Tranche-B partition definition.
  6. lock-key: A key on the token-definition used to authorize the locking and unlocking of tokens, or the transfer of locked tokens, on balances held by the user either on their account directly, or in the case of a partition, on the partition in their account.
  7. partition-key: A key on the token-definition used to authorize the creation, deletion, or updating of partition-definitions owned by the token-definition.
  8. partition-move-key: A key on the token-definition used to authorize the movement of tokens between partitions.
  9. partition-administrator: The user holding the partition-key on the token-definition.
  10. partition-move-manager: The user holding the partition-move-key on the token-definition.

User stories

General

  • General-1: As a token-issuer, I want to create a fungible token definition with locking and/or partitioning capabilities.
  • General-2: As a token-issuer, I want to create a non-fungible token definition with locking and/or partitioning capabilities.

Token Definition Keys

  • Keys-1: As a token-administrator, I want to administer (set, rotate/update, or remove) a lock-key on the token-definition
  • Keys-2: As a token-administrator, I want to administer (set, rotate/update, or remove) a partition-key on the token-definition
  • Keys-3: As a token-administrator, I want to administer (set, rotate/update, or remove) a partition-move-key on the token-definition.
  • Keys-4: As a token-administrator smart contract, I want to administer each of the above-mentioned keys.

Partitions

  • Partitions-1: As a partition-administrator, I want to create new partition-definitions for my token-definition.
  • Partitions-2: As a partition-administrator, I want to update existing partition-definitions for my token-definition, such as the memo, of a partition-definition.
  • Partitions-3: As a partition-administrator, I want to delete existing partition-definitions of my token-definition.
  • Partitions-4: As the holder of a partition-move-key, I want to transfer independent fungible token balances within partitions of an account.
  • Partitions-5: As the holder of a partition-move-key, I want to transfer independent NFT serials within partitions of an account.
  • Partitions-6: As a token-administrator, I want to pause all token transfers for my token-definition, including for all partitions, by pausing the token-definition itself.
  • Partitions-7: As a token-administrator, I want to freeze all token transfers for my token-definition on a particular account, including for all partitions of the token-definition, by freezing the token-definition itself.
  • Partitions-8: As a token-administrator, I want to require kyc to be set on the account for the association with my token-definition to enable transfers of any tokens in partitions of the token-definition.
  • Partitions-9: As a token-administrator, I want to pause all token transfers for a specific partition-definition of my token-definition.
  • Partitions-10: As a token-administrator, I want to freeze all token transfers for a specific partition-definition of my token-definition on a particular account.
  • Partitions-11: As a partition-administrator, I want to require a kyc flag to be set on the partition of an account to enable transfers of tokens in that partition.
  • Partitions-12: As a token-administrator, I want to be able to create a new token-definition with a fixed supply and a partition-key.
  • Partitions-13: As a node operator, I do not want to honor deletion of a token-definition that has any partition-definition that is not also already deleted.
  • Partitions-14: As a supply-key holder, I want to mint tokens into a specific partition of the treasury account.
  • Partitions-15: As a supply-key holder, I want to burn tokens from a specific partition of the treasury account.
  • Partitions-16: As a wipe-key holder, I want to wipe tokens from a specific partition in the user’s account.
  • Partitions-17: As a token-administrator smart contract, I want to create, update, and delete partitions, and in all other ways work with partitions as I would using the HAPI.
  • Partitions-18: If freeze or pause is set at the token-definition level then it takes precedence over the partition-definition level.

Association of Partitions

  • Association-1: As a user, I want to associate with a token-definition that has partition-definitions. When tokens are sent to my account for a partition of that token-definition, then I want to automatically associate with that partition-definition.
  • Association-2: As a user, I want to associate with a partition-definition, exactly as I would for associating with any other token-definition, and automatically get the token-definition associated too with extra cost, without using the auto-association slots.
  • Association-3: As a user, once associated with a partition-definition, I want transfers into my account for “sibling” partition-definitions to be auto-partition-associated with extra cost, without using the auto-association slots.
  • Association-4: As a user, once associated with a token-definition, I want any transfers into my account for “child” partition-definitions to be auto-partition-associated with extra cost without using the auto-association slots.
  • Association-5: As a user, if a partition in my account holds no tokens, I want to disassociate from that partition-definition.
  • Association-6: As a user, if a partition in my account hold tokens, I do not want to permit disassociation from that partition-definition.
  • Association-7: As a node operator, I do not want to permit a user to disassociate from a token-definition if the user account has any related partitions. The partitions must be removed first.

Moving Fungible Tokens Between Partitions

  • Move-1: As a partition-move-manager, I want to move fungible tokens from one partition (existing or deleted) to a different (new or existing) partition on the same user account, without requiring a signature from the user holding the balance.
  • Move-2: As a partition-move-manager, I want to move fungible tokens from one partition (existing or deleted) to a different (new or existing) partition on a different user account, but requiring a signature from the user’s account being debited.
  • Move-3: As a partition-move-manager, I want to move non-fungible tokens from one partition (existing or deleted) to another (new or existing) partition on the same user account, without requiring a signature from the user.
  • Move-4: As a partition-move-manager, I want to move non-fungible tokens from one partition (existing or deleted) to another (new or existing) partition on a different user account, but requiring a signature from the user.
  • Move-5: As a token-administrator smart contract, I want to move tokens from one partition to another, in the same account or to a different account, if my contract ID is specified as the partition-move-key, and all other conditions are met.

Locking

  • Lock-1: As a lock-key holder, I want to lock a subset of the currently held unpartitioned unlocked fungible tokens held by a user’s account without requiring the user’s signature. If an account has x unlocked tokens, then the number of tokens that can be additionally locked is governed by: 0 <= number_of_tokens_to_be_locked <= x.
  • Lock-2: As a lock-key holder, I want to lock a subset of the currently held unlocked fungible tokens held by a user’s account in a partition without requiring the user’s signature. If an account has x unlocked tokens, then the number of tokens that can be additionally locked is governed by: 0 <= number_of_tokens_to_be_locked <= x.
  • Lock-3: As a lock-key holder, I want to unlock a subset of the currently held unpartitioned locked fungible tokens held by a user’s account without requiring the user’s signature. If an account has x locked tokens, then the number of tokens that can be additionally unlocked is governed by: 0 <= number_of_locked_tokens <= x.
  • Lock-4: As a lock-key holder, I want to unlock a subset of the currently held locked fungible tokens held by a user’s account in a partition without requiring the user’s signature. If an account has x locked tokens, then the number of tokens that can be additionally unlocked is governed by: 0 <= number_of_locked_tokens <= x.
  • Lock-5: As a lock-key holder, I want to lock specific NFT serials currently unlocked in a user’s account without requiring the user’s signature.
  • Lock-6: As a lock-key holder, I want to lock specific NFT serials currently unlocked in a user’s account in a partition without requiring the user’s signature.
  • Lock-7: As a lock-key holder, I want to unlock specific NFT serials currently locked in a user’s account without requiring the user’s signature.
  • Lock-8: As a lock-key holder, I want to unlock specific NFT serials currently locked in a user’s account in a partition without requiring the user’s signature.
  • Lock-9: As a lock-key holder, I want to transfer fungible or non-fungible tokens that are currently locked in a user’s account, if the transaction is signed by both the lock-key and the key on the debited account. These tokens will remain locked in the recipient’s account.

Transfers

  • Transfer-1: As an owner of an account with a partition, I want to transfer tokens to another user with the same partition.
  • Transfer-2: As an owner of an account with a partition, I want to transfer tokens to another user that does not already have the same partition, but can have the same partition auto-associated.
  • Transfer-3: As an owner of an account with a partition with locked tokens, I want to transfer tokens to another user with the same partition, either new (with auto-association) or existing. This cannot be done atomically at this time. The tokens must be unlocked, transferred, and then locked again. Using HIP 551 (atomic batch transactions), I would be able to unlock, transfer, and lock atomically. This has to be done in coordinate with the lock-key holder.

Other existing operations on the token-definition

  • Misc-1: As a token-administrator, I would like all operations on the token-definition, such as freeze, pause, metadata updates, kyc-flag updates, etc., to function unchanged from prior releases, even if partition-definitions are specified, since they operate at the token-definition level and are not specific to any single partition.
  • Misc-2: Rent: As a node operator, I want to charge rent for each partition and partition-definition on the ledger. The account pays for partition rent unless an auto-renew-payer is specified on the account.
  • Misc-3: Approval/Allowance: As a user, I want to grant an allowance to another user for a specific amount in a specific partition of my token balance (for fungible tokens).
    • The allowance at a token-definition level will not be interpreted at a given partition level. Each partition should provide its own allowance. If I have a partitioned token and I have granted allowances to another user at a token-definition level (and not at the partition level), then an allowance-based transfer transaction that tries to transfer tokens from a specific partition will fail.
  • Misc-4: Account expiry: As a node operator, I want to reclaim the memory used by expired accounts that haven’t paid their rent.
    • Before Hedera implements archiving: When a user account expires, the tokens of each partition will be moved to the treasury account of the associated token-definition. This is consistent with how Hedera intends to treat the expiry of accounts that hold any tokens.
    • After Hedera implements archiving: When a user account expires, the partitions will be archived along with the account. This is consistent with how Hedera intends to treat the expiry of accounts that hold any tokens after archiving is implemented.
    • When a treasury account expires, the token-definition will be deemed as expired and the token-definition and all partition-definitions within that token-definition will be deleted/archived. This is consistent with how Hedera intends to treat the expiry of treasury accounts for any tokens.
  • Misc-5: Account deletion: As a node operator, I do not want to honor account deletion requests if the account holds tokens, including in any partition. The user must dispose of their tokens from their account before the account can be deleted.
  • Misc-6: As a token-issuer, I want to set custom fees at the token-definition level and not at the partition level. The fees will be applied to all partitions of the token-definition. Custom fees will not be applied when moving tokens between partitions of the same account.

Mirror node

  • Mirror-1: As a mirror node user, I want to query a partitioned token-definition and receive information about all the child partition-definitions, and their lock status.
  • Mirror-2: As a mirror node user, I want to query any account (a user account or a treasury account), and receive information about locked as well as unlocked balances for any token or partition that the user has associated with.
  • Mirror-3: As a mirror node user, I want to see a simple number indicating the number of tokens that can be transferred from a given token or partition. The number is 0 if paused or frozen, or is the unlocked number of tokens in the account or partition.
  • Mirror-4: As a mirror node user, I want to see a rollup of the balance of all my partitions across a token-definition
  • Mirror-5: As a mirror node user, I want to know the distribution of the supply of a partitioned token-definition across all of its partition-definitions. For example, if there are 1M users associated with partition-definition P1 and P2, and if the total supply is 100M tokens, and if the sum of balances held by all 1M users of P1 is 20M and the sum of balances held by all 1M users of P2 is 80M, then I should see that the total supply of 100M is distributed with 20M in partition-definition P1 and 80M in partition-definition P2.
  • Mirror-6: As a mirror node user, I want to know all NFTs that are part of a partition of an account.

SDKs

  • SDK-1: As an SDK user, I want to have an API to tell me the lock, pause, and freeze status of any token (including partitions), and the number of tokens that are available for transfer.
  • SDK-2: As an SDK user, I want to have an API for working with partitions and locks as defined by the HAPI.

Wallets

  • WALLET-1: As a wallet user, I want my wallet to show me whether a given token (including partitions) are frozen, paused, or locked, and the number of tokens that are available for transfer.
  • WALLET-2: As a wallet user, I want my wallet to show me the relationship between partitions in my account and the token-definition that they belong to, and the metadata associated with each partition.

Mirror Node Explorer

  • EXPLORER-1: As a user of the mirror node explorer, I want to see, for any given account’s tokens, the relationship of partitions held by that account, and the status of each partition or token (locked, unlocked, paused, frozen, etc).

Example

The concept of partitioning and locking can be illustrated using an example. Consider a token-definition for AcmeToken that represents investments in a fund.

  • The token issuer creates this token-definition with a partition-key. Let’s say this has a TokenID of 0.0.123456.
  • The fund has different tranches with three different timelines until which these funds are locked (i.e. cannot be transferred or traded by a holder). Let’s call these tranches Tranche-A (locked until Jan 1, 2024), Tranche-B (locked until Jan 1, 2025), and Tranche-C (locked until Jan 1, 2026).
  • The token issuer creates three partition-definitions for the token-definition 0.0.123456. Hedera creates these three definitions and returns TokenIDs of 0.0.200001, 0.0.200002, and 0.0.200003 that represent AcmeToken.Tranche-A, AcmeToken.Tranche-B, AcmeToken.Tranche-C respectively.
  • The token issuer sets three different metadata on these partition definitions independently of each other.
  • When anybody queries the mirror node for this token id 0.0.123456, they get an equivalent of:
{
    id: 0.0.123456,
    name: AcmeToken,
    symbol: ACME,
    lockKey: 0x111abc..,
    partitionKey: 0x222abc..,
    partitionMoveKey: 0x333ace…,
    partitions: [
        {metadata: “ipfs://xxx”, partition_id: 0.0.200001},
        {metadata: “ipfs://yyy”, partition_id: 0.0.200002},
        {metadata: “ipfs://zzz”, partition_id: 0.0.200003}, …
    ]
}
  • Alice makes investments in this fund in two of these tranches - Tranche-A and Tranche-C. She skips Tranche-B.
  • A query to the mirror node for her account balance shows the equivalent of:
{
…
tokens: {[
    {
        token_id: 0.0.123456,
        balance: [
            {partition_id: 0.0.200001, balance: 10, locked: 5 },
            {partition_id: 0.0.200003, balance: 10, locked: 7 },
        ]
    }
]}
…
}
  • On 1 June 2024 the holder of the lock-key signs a transaction to unlock the tokens in Tranche-A.
  • Alice can attempt to transfer tokens in an individual partition on 1 June 2024.
  • If she transfers tokens from partition-1, that transfer will succeed.
  • If she transfers tokens from partition-3, that transfer will return an appropriate error informing her that her tokens are locked.
  • To make the transfer, she will use the simple form of the current crypto transfer as the existing tokenTransferList. If she wants to transfer tokens from partition-3, she (or the SDK) queries the mirror nodes or the wallets and maps partition-3 to the TokenID of 0.0.200003 and fills that in the token transfer list of the cryptoTransfer transaction.

Specification

This HIP will introduce two new features to the Hedera Token Service API that allows token issuers to create and manage partitions and to lock or unlock a subset of tokens. These functions will be accessible through the Hedera Token Service API.

Overview of Partitions

From an implementation perspective, a partition-definition can be thought of as a special type of token-definition. It is created using the partition-key of a token-definition. It has a tokenID, just like any other token. Indeed, the HAPI TokenType will be extended to include a new TokenType of PARTITION. Almost anywhere in HAPI that takes a tokenID, will also work with partitions.

For example, the TokenAssociateTransactionBody and TokenFreezeAccountTransactionBody both take a TokenID as one of their arguments, and will support FUNGIBLE_COMMON, NON_FUNGIBLE_UNIQUE, and PARTITION token types.

There are many features of FUNGIBLE_COMMON and NON_FUNGIBLE_UNIQUE token definitions that are NOT supported by a partition-definition, since the partition-definition inherits these values from its parent token-definition. For example, the freeze-key is specified on the parent token-definition, but applies also to all partition instances of the child partition-definitions. For this reason, distinct APIs were created for managing the lifecycle of partitions, such as TokenCreatePartitionTransactionBody, instead of reusing TokenCreateTransactionBody.

Partitioning works for both fungible and non-fungible tokens. A partition of fungible tokens contains a balance, while a partition of non-fungible tokens contains a list of serial numbers, all within a single supply. Only those token definitions created with a partition-key are capable of being partitioned.

As with other token types, an account must be associated with a partition to hold a balance of that partition (for fungible tokens, or a list of serial numbers for non-fungible tokens). Once associated with the partition’s root token type, or with the partition itself, all other partitions of that token type can automatically associate with the account. There is a price, paid for by the transaction causing the auto-association to take place, for that association, and the account that is auto-associated will be credited one association slot in their next account renewal. In this way, each association is paid for ahead of time for at least one renewal period.

Since partition-definitions have a tokenID, they can also be used in token transfers, as part of the CryptoTransferTransactionBody. Transferring tokens from one partition to another, where both have the same TokenID, works without any changes to the HAPI. The services code that handles the transaction must be aware of the fact it is working with a PARTITION rather than a FUNGIBLE_COMMON or NON_FUNGIBLE_UNIQUE token type, but the client does not have to be aware of this distinction. This is critical, for it allows existing wallets and dApps to work with partitions just like they do with all other token types.

One critical, new feature of partitions is the ability to move tokens between partitions. Today, you can transfer a balance between two accounts for a single tokenID, but you cannot transfer a balance from one tokenID to another. This is enforced by the services code by validating that the balances of all tokenIDs are zero in the transfer list.

This specification introduces a slight modification of this rule. All partitions within a token-definition are inherently fungible. Thus, if I have 1000 tokens in partition 1, and 2000 tokens in partition 2, I can transfer 500 tokens from partition 1 to partition 2. In the transfer logic, we will recognize that tokenID 1 and tokenID 2 are both sibling partitions, and make sure that the sum of all transfers involving sibling partitions balances to zero, and not just that the sum of all transfers by tokenID balance to zero.

Overview of Locking

Conceptually, locking is a natural extension to “pausing” and “freezing”.

For FUNGIBLE_COMMON token-definitions, pausing affects the supply across the entire ledger. Freezing affects the balances owned by a single account. And locking affects a subset of the balance held by an account. Likewise for NON_FUNGIBLE_UNIQUE types, pausing affects all serials ledger-wide, while freezing only affects those serials owned by the account, and locking affects a specific subset of serials owned by the account.

Since a partition-definition is a special type of token-definition, it inherits the ability to pause and freeze and lock, but only for the tokens held by the account in associated partitions. So pausing will affect all balances or serials ledger-wide that are held in any partition defined by that partition-definition, while freezing affects only the balance or serials of a specific partition of a specific account, and locking applies to a subset of the balance or serials of a specific partition of a specific account.

Just as with pausing and freezing, the owner of the token has no say in whether the token is locked or unlocked. It is the sole discretion of the lock-key holder to manage which tokens are locked or unlocked.

Enhancements to Core Concepts

Here are some additions required to some of the core concepts/components:

  • Token-Definition

    • Three new types of keys are defined on the token-definition: partition-key, partition-move-key and lock-key.
    • Partition-definitions can be created in a token-definition using the partition-key.
    • Each partition-definition will have its own metadata.
  • Accounts

    • For fungible tokens, any account (e.g. user accounts or treasury account) holding a balance of a partitioned token-definition will have multiple entries, one for each partition of which it holds a balance.
    • Each of these entries will have two sub-entries: the total balance within that partition, and the locked balance within that partition. The unlocked balance is simply balance - lockedBalance.
  • Mirror nodes, SDK, Explorer, Wallets

    • Will be aware of partitioning and locking features.
  • dApps, DEXes, NFT marketplaces

    • In general, these will benefit from being aware of partitioning and locking features. However, since partitions are modeled as a new type of token, they can treat partitions as a new kind of token without having to know about partitioning. Likewise, locking is essentially a more specialized pause or freeze, and these concepts work reasonably with cross-chain programs already.

HAPI (Hedera API)

TokenService gRPC Endpoints

The following additions to TokenService expose new gRPC endpoints for locking and partitioning tokens.

syntax = "proto3";
message Transaction {
  // ...
}

message TransactionResponse {
  // ...
}

/**
 * Transactions and queries for the Token Service
 */
service TokenService {
  // ...

  /**
   * Locks an amount of the token in a user's account or partition of their account.
   */
  rpc lockToken (Transaction) returns (TransactionResponse);

  /**
   * Unlocks an amount of the token in a user's account or partition of their account.
   */
  rpc unlockToken (Transaction) returns (TransactionResponse);

  /**
   * Creates a new partition definition on a token definition. After the entity is created,
   * the TokenID for it is in the receipt.
   */
  rpc createTokenPartitionDefinition (Transaction) returns (TransactionResponse);

  /**
   * Updates an already created token partition definition to the given values.
   */
  rpc updateTokenPartitionDefinition (Transaction) returns (TransactionResponse);

  /**
   * Marks a token partition definition as deleted, though it will remain in the ledger.
   */
  rpc deleteTokenPartitionDefinition (Transaction) returns (TransactionResponse);
}

Changes to Existing HTS API

A token’s partition is defined as a new type of token. We therefore add a new PARTITION enumeration to the TokenType.

enum TokenType {
  // ...

  /**
   * A special type of token that holds a subset of the supply of a FUNGIBLE_COMMON token, or
   * a subset of the serial numbers of a NON_FUNGIBLE_UNIQUE token. Partitions are always "children"
   * of another token such as a FUNGIBLE_COMMON or NON_FUNGIBLE_UNIQUE token.
   */
  PARTITION = 2;
}

The TokenCreateTransactionBody is extended to allow the token creator to specify the keys for locking and partitioning, and for moving balances between partitions. A token-definition with no partition-key but with a partition-move-key is valid, if meaningless. A token-definition with no partition-move-key but with a partition-key is also valid, and means that while partitions can be created, no balances can be moved between partitions. Assuming there is a supply-key, it is possible to mint into a partition or burn from a partition but not to move balances between partitions.

Partition Key? Partition-Move-Key? Supply-Key? Initial Supply? Behavior
N * * * Acts like a normal token-def, no partition behavior
Y N N Y No capabilities over a normal token-def, yet is more complex. Initial partition created with the initial supply
Y N N N Not useful. Initial partition created with no supply and unable to mint
Y N Y Y Initial partition created with initial supply, new partitions can be created and minted into. No transfer between partitions
Y N Y N No initial partition or initial supply, but partitions can be created and minted into. No transfer between partitions
Y Y N N Not useful. Can create partitions and transfer between partitions , but no tokens can exist
Y Y N Y Initial partition created with initial supply, new partitions can be created, and initial supply can be transferred between partitions
Y Y Y N Partitions can be created, tokens can be minted into partitions, and tokens can be transferred between partitions
Y Y Y Y Initial partition created with initial supply, new partitions can be created, tokens can be minted into partitions, and tokens can be transferred between partitions
syntax = "proto3";

message Key {
  // ...
}

message TokenCreateTransactionBody {
  // ...

  /**
   * The key which can lock, unlock, or transfer locked tokens in an account. Each fungible token
   * balance of a token-definition with a lock_key will have both a balance, and a quantity of
   * locked tokens, where the quantity of locked tokens may be 0. If this key is desired, it
   * must be set at the time the token-definition is created. It can only be set for token
   * definitions with a TokenType of FUNGIBLE_COMMON and NON_FUNGIBLE_UNIQUE. If set, it may be updated, but only if the
   * update transaction is signed both by the lock key and the new lock key. Once null, it
   * cannot be set again.
   *
   * If set on a token-definition that also sets the partition_key, then the lock_key may also be
   * used to lock balances on those partitions.
   */
  Key lock_key = 23;

  /**
   * The key which can create, update, and delete partitions of this token type. If this key is
   * desired, it must be set at the time the token-definition is created. It is applicable to both
   * FUNGIBLE_COMMON and NON_FUNGIBLE_UNIQUE token types. If set, it may be updated, but only if
   * the update transaction is signed both by the old partition key and the new partition key.
   * Once null, it cannot be set again.
   */
  Key partition_key = 24;

  /**
   * The key which can move balances from the token type's supply into any partition of any user,
   * or move balance from one partition to another of different types, either in the same account,
   * or in different accounts.
   *
   * For example, if two users both have partitions "tranche-A" and "tranche-B", then either user
   * could move tokens from their "tranche-A" to the other user's "tranche-A", or from their
   * "tranche-B" to the other user's "tranche-B", but they cannot transfer from their "tranche-A"
   * to the other user's "tranche-B", or from their "tranche-A" to their own "tranche-B". That is,
   * under normal circumstances, you can transfer funds between partitions of the same type, but not
   * between partitions of different types.
   *
   * However, a transaction signed by this key *can* transfer funds between partitions of different
   * types, either for the same user, or for different users. So user Alice can transfer balance
   * from her "Tranche-A" to user Bob's "Tranche-B", if the transaction is signed both by Alice,
   * and by the partition-move-key. In addition, balance may be transferred from Alice's "Tranche-A"
   * into Alice's "Tranche-B", if the transaction is signed by the partition-move-key. Transferring
   * balances across partitions in the user's account does not require the user to sign the
   * transaction.
   *
   * If this key is desired, it must be set at the time the token-definition is created. It is
   * applicable to both FUNGIBLE_COMMON and NON_FUNGIBLE_UNIQUE token types. If set, it may be
   * updated, but only if the update transaction is signed both by the old partition move key
   * and the new partition move key. Once null, it cannot be set again.
   */
  Key partition_move_key = 25;
}

The TokenUpdateTransactionBody will support updating the new keys added to TokenCreateTransactionBody.

syntax = "proto3";
message TokenUpdateTransactionBody {
  /**
   * The key which can lock, unlock, or transfer locked tokens in an account. If the Token does not
   * currently have a lock key, transaction will resolve to TOKEN_HAS_NO_LOCK_KEY
   */
  Key lock_key = 23;

  /**
   * The key which can create, update, and delete partitions of this token type. If the Token does
   * not currently have a partition key, transaction will resolve to TOKEN_HAS_NO_PARTITION_KEY
   */
  Key partition_key = 24;

  /**
   * The key which can move balances from the token type's supply into any partition of any user,
   * or move balance from one partition to another of different types, either in the same account,
   * or in different accounts.
   *
   * If the Token does not currently have a partition move key, transaction will resolve to
   * TOKEN_HAS_NO_PARTITION_MOVE_KEY
   */
  Key partition_move_key = 25;
}

No actual change is made to the API definition for TokenDeleteTransactionBody, but the specification is updated to indicate that the token definition cannot be deleted if it has any partition definitions that are not also deleted.

Note that although a partition-definition is identified by TokenID, it cannot be used with TokenDeleteTransactionBody. Instead, it must be used with TokenDeletePartitionDefinitionTransactionBody.

/**
 * Marks a token definition as deleted, though it will remain in the ledger until no longer used.
 * The operation must be signed by the specified Admin Key of the Token. If admin key is not set,
 * Transaction will result in TOKEN_IS_IMMUTABlE. Once deleted update, mint, burn, wipe, freeze,
 * unfreeze, grant kyc, revoke kyc and token transfer transactions will resolve to
 * TOKEN_WAS_DELETED. A token cannot be deleted if it has any partition definitions that are not
 * also deleted. In that case, the transaction will resolve to TOKEN_PARTITIONS_STILL_EXIST.
 */
message TokenDeleteTransactionBody {
  /**
   * The token to be deleted. If invalid token is specified, transaction will result in
   * INVALID_TOKEN_ID
   */
  TokenID token = 1;
}

NOTE: TokenAssociateTransactionBody and TokenDisassociateTransactionBody, TokenFreezeAccountTransactionBody, TokenGrantKycTransactionBody, TokenPauseTransactionBody, TokenRevokeKycTransactionBody, TokenUnfreezeAccountTransactionBody, TokenUnpauseTransactionBody, CryptoApproveAllowanceTransactionBody, and CryptoDeleteAllowanceTransactionBody do not have any API changes, but their documentation will be updated to refer to partitions, and the implementation has to also deal with partitions.

NOTE: TokenGetAccountNftInfosQuery, TokenGetInfoQuery, TokenGetNftInfoQuery, and TokenGetNftInfosQuery, do not return results for partitions. They will be deprecated in favor of using the mirror node instead.

API for managing partitions

/**
 * Create a new partition type on a token. After the entity is created, the TokenID for it is in
 * the receipt.
 */
message TokenCreatePartitionDefinitionTransactionBody {
    /**
     * The token (either FUNGIBLE_COMMON or NON_FUNGIBLE_UNIQUE) that this partition is a part of.
     */
    TokenID parent_token = 1;

    /**
     * The publicly visible name of the partition. The partition name is specified as a Unicode
     * string. Its UTF-8 encoding cannot exceed 100 bytes, and cannot contain the 0 byte (NUL).
     * This name is not unique within the ledger.
     */
    string name = 2;

    /**
     * The memo associated with the partition (UTF-8 encoding max 100 bytes)
     */
    string memo = 3;
}
/**
 * Marks a token partition as deleted, though it will remain in the ledger.
 * The operation must be signed by the specified partition key of the parent Token. If
 * the partition key is not set, the Transaction will result in TOKEN_IS_IMMUTABlE.
 * Once deleted update, freeze, unfreeze, grant kyc, revoke kyc and token transfer
 * transactions will resolve to TOKEN_WAS_DELETED.
 */
message TokenDeletePartitionDefinitionTransactionBody {
    /**
     * The token partition to be deleted. If an invalid token is specified, the transaction will
     * result in INVALID_TOKEN_ID
     */
    TokenID token = 1;
}
/**
 * At consensus, updates an already created token partition to the given values.
 */
message TokenUpdatePartitionTransactionBody {
    /**
     * The Token partition to be updated
     */
    TokenID token = 1;

    /**
     * The new publicly visible name of the token. The token name is specified as a Unicode string.
     * Its UTF-8 encoding cannot exceed 100 bytes, and cannot contain the 0 byte (NUL).
     */
    string name = 2;

    /**
     * If set, the new memo to be associated with the token (UTF-8 encoding max 100 bytes)
     */
    google.protobuf.StringValue memo = 5;
}

NOTE: No query API is provided for partitions. Records will be provided to the record stream, and mirror nodes will provide the necessary API to query partitions.

API for locking and unlocking tokens

/**
 * Lock a certain amount of tokens in an account. The TokenID must refer either to a
 * FUNGIBLE_COMMON token type, or a partition of such a token type.
 */
message TokenLockTransactionBody {
    AccountID account = 1;
    TokenID token = 2; // token-definition-id or partition-definition-id
    int64 amount = 3; // if token-definition is FUNGIBLE_COMMON
    repeated int64 serialNumbers = 4; // if the token-definition is NON_FUNGIBLE_UNIQUE
}
/**
 * Unlock a certain amount of tokens in an account. The TokenID must either refer to a
 * FUNGIBLE_COMMON token type, or a partition of such a token type.
 */
message TokenUnlockTransactionBody {
    AccountID account = 1;
    TokenID token = 2; // token-definition-id or partition-definition-id
    int64 amount = 3; // if token-definition is FUNGIBLE_COMMON
    repeated int64 serialNumbers = 4; // if the token-definition is NON_FUNGIBLE_UNIQUE
}

CryptoTransfer Handling Changes

When validating a crypto transfer, the consensus node will validate that the sum of all transfers balances to zero. It is not legal to debit one account by 10 hbars and fail to credit another account by the same amount. Likewise, it is not legal to remove an NFT from one account without also adding it to another, or debit some fungible tokens from one account and fail to credit another.

However, since all partitions of the same token-definition are fungible, we need to alter the rule for checking that all transfers balance to zero. When signed by the appropriate partition-move-key, the balance check will verify that the sum of all token-definition transfers will balance to zero.

For example, suppose Alice (accountID: 0.0.1234) has 1000 tokens in partition 1 (tokenID: 0.0. 1001) and 50 tokens in partition 2 (tokenID: 0.0.1002), and she wants to transfer 500 tokens from partition 1 to partition 2. And suppose both of these are AcmeTokens (tokenID: 0.0.1000). The transfer list will look like this:

{
    tokenTransfers: [
        { token: 0.0.1001, transfers: [{ accountID: 0.0.1234, amount: -500 }] },
        { token: 0.0.1002, transfers: [{ accountID: 0.0.1234, amount: 500 }] }
    ]
}

Normally this would fail, because token 1001 and 1002 are different tokenIDs. However, after this HIP is implemented, the transfer logic will look up tokens 1001 and 1002 and realize they are both partitions of 1000. Balancing -500 with +500 for tokenID 1000 does balance to zero, so this transfer will be permitted (assuming it is signed by the partition-move-key, since it is movement from one partition to another).

In another example, suppose Alice wants to transfer 500 tokens from her partition 1 to Bob’s account (accountID: 0.0.5678), also into partition 1. The transfer list will look like this:

{
    tokenTransfers: [
        { token: 0.0.1001, transfers: [{ accountID: 0.0.1234, amount: -500 }] },
        { token: 0.0.1001, transfers: [{ accountID: 0.0.5678, amount: 500 }] }
    ]
}

In this case, since both transfers are for the same tokenID, the transaction only has to be signed by Alice’s key, and not the partition-move-key. In addition, the transfer logic will succeed both because the amounts balance to zero.

Finally, consider this example. Suppose Alice wants to transfer 500 tokens from her partition 1 to Bob’s partition 2. The transfer list will look like this:

{
    tokenTransfers: [
        { token: 0.0.1001, transfers: [{ accountID: 0.0.1234, amount: -500 }] },
        { token: 0.0.1002, transfers: [{ accountID: 0.0.5678, amount: 500 }] }
    ]
}

Once again, today, this would fail because the transfer list does not balance to zero. However, after this HIP is implemented, the transfer logic will look up tokens 1001 and 1002 and realize they are both partitions of 1000. Balancing -500 with +500 for tokenID 1000 does balance to zero, so this transfer will be permitted (assuming it is signed by the partition-move-key).

Receipt and Record Changes

If a token-definition has a partition-key, then ALL balances / serials for that token-definition must live in a partition. If a token-definition is created with a partition-key, but without a supply-key, and with an initial supply, then that initial supply has to be assigned to some partition. For this reason, an automatically generated partition-definition will be created, and using the partition-key, the partition-administrator can later update the name and memo of that partition-definition.

For this reason, the TransactionReceipt must be extended to record both the created tokenID, and the initial ID of the generated partition definition.

message TransactionReceipt {

    /**
     * In the receipt of a CreateToken, the id of the newly created token, or in the case of a
     * CreateTokenPartitionDefinition, the tokenID of the newly created partition definition.
     */
    TokenID tokenID = 10;

    /**
     * If during CreateToken, a partition_key is specified but no supply_key is specified, then a single initial
     * partition is created in addition to the token definition. This is the ID of that partition definition.
     */
    TokenID initialPartitionID = 15;
}

Smart Contract Changes

The Smart Contract system contracts for HTS must be updated to support locking and partitions.

MAPI (Mirror Node API)

  • GET /api/v1/tokens/{tokenId}/partitions
    • Should return all the partitions in that tokenId
  • GET /api/v1/tokens/{tokenPartitionEntityId}
    • Should return the information about this partition, such as metadata, lockedUntil date, etc.
  • GET /api/v1/accounts/{idOrAliasOrEvmAddress}
    • Should return locked and unlocked balances, if no partitions are used
  • GET /api/v1/accounts/{idOrAliasOrEvmAddress}/tokens
    • Should return the information about balances held in all partitions and/or their locked status
  • GET /api/v1/accounts/{idOrAliasOrEvmAddress}/allowances/tokens
    • Should return allowances for each partition

Rationale

This section contains the rationale for the design decisions made in this HIP. The following design philosophy was used as guideposts for the design decisions.

Total Supply is Ledger Enforced

It is important that the total supply of a token be ledger enforced. Indeed, we want to enforce as many rules as possible on the ledger. For example, moving tokens from one partition to another could have been achieved using batch transactions, and burning tokens from one partition and minting in another. However, this requires a token definition to have a supply-key, even if it never intends to mint or burn other than for transferring between partitions. This violates the principle of using the ledger to enforce guarantees about what can happen with tokens. For this reason, we decided to create a new partition concept on the ledger.

Compatibility with Existing Wallets and dApps

We tried very hard to make sure the partition concept would fit naturally into the HAPI, into the code, and into the ecosystem. By modeling partitions as tokens, wallets, exchanges, explorers, and other ecosystem components can view and handle partitions without having to know about the concept of partitions.

If instead we had modeled partitions as a new concept separate from tokens, then wallets and exchanges would have had to be updated to support them. For wallets such as Metamask, this would have been cumbersome, or perhaps even impossible.

(Loose) Compatibility with Existing Token Types

The concept of partitioning tokens is not entirely new. Ethereum’s ERC-1410 standard provides a similar functionality, even if not widely adopted. Naming the feature “partitioning” was done to align in concept with ERC-1410 (even though many of the details differ). It may be possible to automatically map ERC-1410 tokens to Hedera tokens, but this is not an explicit goal of this HIP, since ERC-1410 was abandoned.

Reusing Existing Transactions and API

By modeling partition-definitions as specialized token-definitions, we can reuse many existing transactions by also reusing the TokenID. This allows us to get a very rich behavior for partitions without adding tremendous complexity in the API.

With that being said, as can be seen from this HIP, there are many touch points in the HAPI and in the implementation that have to be reviewed, tested, and possibly updated.

The locking feature was also designed to fit naturally with the existing TokenPause and TokenFreeze concepts. This allows the locking feature to be implemented with minimal changes to existing wallets and provides for an orthogonal API.

For example, in a wallet today you have a token balance of 100 ACME tokens. You may attempt to transfer them, and it may fail, either because the ACME token definition has been paused (which prevents crypto transfers of that token ledger-wide), or it may have been frozen on your account. There may be many reasons why transferring those 100 tokens won’t succeed.

In the same way, if 50 of those tokens are locked, and you attempt to transfer some of those locked tokens, then the transfer will fail. Thus, locking fits in naturally with the existing infrastructure.

Had we modeled locked and unlocked balances as two separate tokens, it would have caused more violence to the API, and would have required more changes to wallets and exchanges. “Unlocking” and “locking” quantities would have to be new, separate APIs, or very special crypto transfers.

Auto-association

Each partition must be associated with the account that holds a balance or serial number for that partition. In Hedera today, there are multiple methods for handling association, from having association slots to manual association. However, to reduce the burden of having to associate manually with each and every partition of a token, we allow partition-types of an already-associated token-definition to be automatically associated with the account.

To prevent misuse, the transaction that causes the auto-association to take place must pay a surcharge for this association, which includes enough to cover the cost of the association for at least one full renewal period. Thus, for such auto-associated partitions, the account is given a credit of one association to be used at the next renewal, since it was essentially pre-paid by the transaction that caused the association.

If an association is made, and a credit given, and the association is then removed, the credit remains, because it was already paid forward.

Backward Compatibility

This feature is fully backward compatible with the existing tokens on the Hedera network. Token issuers can choose to use the feature or continue to operate their tokens as they currently do. Wallets, exchanges, and other ecosystem components can continue to operate as they do today, including working with partitions as though they were tokens, and locking as though the tokens were frozen.

Security Implications

It is imperative for the explorers to clearly specify the “lockability” of a token and the precise time when funds are placed in a locked state. This measure is vital in preventing any deceitful attempts by individuals who might attempt to present their locked funds/proof of reserves as readily available to others.

Open Issues

N/A

References

  • HIP-24: Pause feature on Hedera Token Service https://github.com/hashgraph/hedera-improvement-proposal/blob/master/HIP/hip-24.md
  • HIP-423: Long-term Scheduled Transactions https://hips.hedera.com/hip/hip-423
  • ERC-20 Vesting Wallet https://docs.openzeppelin.com/contracts/4.x/api/finance#VestingWallet
  • ERC-20 Token Time Lock https://docs.openzeppelin.com/contracts/4.x/api/token/erc20#TokenTimelock
  • Liquidity Mining https://docs.uniswap.org/contracts/v3/guides/liquidity-mining/overview
  • Claimable Balances https://developers.stellar.org/docs/encyclopedia/claimable-balances

Copyright/license

This document is licensed under the Apache License, Version 2.0 – see LICENSE or (https://www.apache.org/licenses/LICENSE-2.0)

Citation

Please cite this document as: