forked from hashgraph/hedera-improvement-proposal
-
Notifications
You must be signed in to change notification settings - Fork 0
/
hip-796.md
728 lines (556 loc) · 55.2 KB
/
hip-796.md
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
---
hip: 796 \
Title: Lockable Fractional Amounts of Fungible Tokens on Hedera\
Author: Sam Wood <sam.wood@luthersystems.com>, Stephanie Yi <stephanie.yi@luthersystems.com>,
Eleonora Odorizzi <eleonora.odorizzi@toko.network>, Vicky Lio <vicky.lio@toko.network>\
Working Group: Atul Mahamuni <@atul-hedera>, Leemon Baird <@lbaird>, Richard Bair <@rbair23>, Jasper
Potts <@jasperpotts>\
type: Standards Track\
Needs-council-approval: Yes\
Category: Service\
Status: Idea\
Created: 2023-08-30\
Discussions-to: https://github.com/hashgraph/hedera-improvement-proposal/discussions/797\
replaces: 751, 752\
Updated: 2023-08-30\
---
## 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 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 `AcmeToken`s, 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` or on the `partition-definition` (but not both), 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-definition`s 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-definition`s for my `token-definition`.
- **Partitions-2**: As a `partition-administrator`, I want to update existing `partition-definition`s for my `token-definition`, such as the memo, of a `partition-definition`.
- **Partitions-3**: As a `partition-administrator`, I want to delete existing `partition-definition`s 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-definition`s 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-definition`s 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.
### 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-definition`s 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 or paused, 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-definition`s for the token-definition `0.0.123456`. Hedera creates these three definitions and returns `TokenID`s 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: [
{partition_number:1, metadata: “ipfs://xxx”, partition_id: 0.0.200001},
{partition_number:2, metadata: “ipfs://yyy”, partition_id: 0.0.200002},
{partition_number:3, 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: 1, partition_id: 0.0.200001, balance: 10, locked: 5 },
{partition_id: 3, 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 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-definition`s. 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-definition`s 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 `tokenID`s 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.
```protobuf
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`.
```protobuf
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 | Y | \* | \* | 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 and 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 |
```protobuf
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. 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`.
```protobuf
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`.
```protobuf
/**
* 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
```protobuf
/**
* 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_id = 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 = 6;
}
```
```protobuf
/**
* 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;
}
```
```protobuf
/**
* 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
```protobuf
/**
* 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_id = 1;
TokenID token_id = 2; // token-definition-id or partition-definition-id
oneOf amountOrSerialNumber {
int64 amount = 3; // if token-definition is FUNGIBLE_COMMON
int64 serialNumber = 4; // if the token-definition is NON_FUNGIBLE_UNIQUE
}
}
```
```protobuf
/**
* 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 accoun_id = 1;
TokenID token_id = 2; // token-definition-id or partition-definition-id
oneOf amountOrSerialNumber {
int64 amount = 3; // if token-definition is FUNGIBLE_COMMON
int64 serialNumber = 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 `AcmeToken`s (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.
```protobuf
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 **not** to model partitions as tokens, but rather 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-definition`s as specialized `token-definition`s, 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](../LICENSE) or (https://www.apache.org/licenses/LICENSE-2.0)
PartitionableAndLockableToken_HIP_v5.md
Open with
Displaying PartitionableAndLockableToken_HIP_v5.md.