This document puts forth a proposed standard for Base Fungible user defined tokens (UDT) on Nervos Network’s Common Knowledge Base (CKB).
Transactions are the interface in CKB by which users perform operations (produce state changes). Transactions are composed of cells and cells can define type scripts that enforce constraints on any transaction in which the cell is included. Therefore descriptions of CKB operations are descriptions of transaction patterns, which in turn describe specific input, output, and dependency cells, as well as the constraints the input & output cells introduce into a transaction (via their type & lock scripts). The operations and their corresponding transaction patterns are the first two aspects of the standard that this document defines. The third aspect that this draft defines is the UDT extension mechanism through Lock.
The base standard is simple, defining only the bare minimum constraints for a fungible token. This will allow greater flexibility and opportunities to innovate on token models while still reaping the benefits of standard-compliance, namely: interoperability and predictable behavior.
To understand the material in this paper, readers should have an understanding of the programming model - “Cell” model - of Nervos Network’s CKB. More specifically, readers should have an understanding of the following concepts:
- Cell model & fundamental CKB data structures
- Type ID
- Dep types
- Hash types on scripts
- Transaction verification workflow
- Molecule serialization format
The many design concerns can be classified into three types: usability concerns, cost concerns, and security concerns. The standard should be usable in so far that it should be pleasant and easy to develop standard-compliant tokens as well extensions. It should not be too difficult to identify the right cells to use to generate transactions (i.e., perform an operation) and should therefore also be easy to query, parse, and understand on-chain data by off-chain parties such as wallets and exchanges. It should be convenient for UDT holders/users to perform the operations supported by the base standard and any custom extensions. Corollary to this, since holders will probably use a wallet or other tool to interface with the token, it should be easy for interface tools to add support for new UDTs and support for new extensions to an already-supported UDT.
The cost of UDT comes in the form of cost of operations due to transaction size & compute cycles utilized by the scripts themselves, as well as from the amount of storage it takes to store the required on-chain metadata, UDT scripts, and UDT instances themselves, as described in the Architecture section below. These should be minimized.
- Definition Script
- Info Cell Script
- UDT Information Cell
- UDT Instance
It’s a Type script to constrain the behavior of UDT Instance.
It’s a Type script to constrain the behavior of Info Cell.
A UDT Instance is any cell that has a UDT Definition type and contains the amount description. So, holders or users of some UDT would be most often operating on UDT instances; they are the deliverable, so to speak, of UDT development.
data:
amount: uint256
type:
code_hash: definition_script_type_hash
hash_type: type
args: UUID
lock:
<user_defined>
The UDT information cell is important, as it acts as an management and infomation center for specific UDT.
data:
meta_info_dictionary : molecule bytes
type:
code_hash: info_cell_script_type_hash
hash_type: type
args: UUID
lock:
<operator_defined>
The meta_info_dictionary
in the info_cell includes optional fields for operators to publish necessary data to users. None of the data in this field is mandatory. Here we list some recommended descriptors for meta_info_dictionary
.
name: string
symbol: char[4]
decimal: uint8
total_supply: uint256
description: string
image: string // URI
icon_uri: string // URI
icon_binary: bytes[]
binaries:
definition_script => UDT_definition_script_cell
info_cell_script => UDT_info_script_cell
To create a new fungible token:
// Issue new UDT
Deps:
outpoint_of_UDT_definition_script
outpoint_of_UDT_info_script
outpoint_of_user_lock_script
Inputs:
<...>
Outputs:
Info_Cell:
Data:
[optional] meta_info_dictionary : molecule
Type:
code_hash: info_cell_script_type_hash
hash_type: type
args: Hash(input0_outpoint) as UUID
Lock:
<user_defined_governance_lock>
UDT_Instance:
Data:
amount: uint256
Type:
code_hash: definition_script_type_hash
hash_type: type
args: UUID
Lock:
<user_defined_custodian_lock>
[optional] <vec>UDT_Instance
// Transfer
Deps:
outpoint_of_UDT_definition_script
outpoint_of_user_lock_script
Inputs:
<vec> UDT_Instance
<...>
Outputs:
<vec> UDT_Instance
<...>
// Mint
Deps:
outpoint_of_UDT_definition_script
outpoint_of_UDT_info_script
outpoint_of_user_defined_governance_lock
Inputs:
Info_Cell
<...>
Outputs:
Info_Cell:
Data:
<update_if_necessary>
Type:
<unchanged>
Lock:
<unchanged>
UDT_Instance:
Data:
amount: uint256
Type:
code_hash: definition_script_type_hash
hash_type: type
args: UUID
Lock:
<user_defined_custodian_lock>
<...>
// Burn
Deps:
outpoint_of_UDT_definition_script
outpoint_of_UDT_info_script
outpoint_of_user_defined_governance_lock
Inputs:
Info_Cell
UDT_Instance:
Data:
amount: uint256
Type:
code_hash: definition_script_type_hash
hash_type: type
args: UUID
Lock:
<user_defined_custodian_lock>
<...>
Outputs:
Info_Cell:
Data:
<update_if_necessary>
Type:
<unchanged>
Lock:
<unchanged>
UDT_Instance:
Data:
amount: uint256
Type:
code_hash: definition_script_type_hash
hash_type: type
args: UUID
Lock:
<user_defined_custodian_lock>
<...>
Notice: all the logic mentioned below is only valid in the same type verification group (by default).
All the logic designed here should not conflict with other potential parallel logic existing in the same UTXO transaction. Potential parallel logic includes:
- Different Type logic
- The same UDT Type logic with a different UDT UUID
Thanks to the group mechanisms in CKB transaction verification, it is possible to meet these demands.
This logic is triggered when operators issue a new type of fungible UDT.
The output info_cell
must follow this logic:
- Trigger conditions:
info_cell
ONLY appears on the output side of the TX
Data
field MUST be empty or a molecule-formatted dictionaryType
field MUST follow these rules:code_hash
is equal toinfo_cell_script_type_hash
hash_type
is equal totype
args
is equal toblake2b(outpoint_of_input_0)[:20]
(UUID)
- There are no restrictions on the
Lock
field.
This logic is triggered when token operators utilize the governance functions of the UDT: minting, burning, modifying information and so on.
The output info_cell
must follow the following logic:
- Trigger conditions:
info_cell
MUST appear on both the input and output side of the TX
Data
field MUST be empty or a molecule-formatted dictionaryType
field MUST be equal to theType
field of inputinfo_cell
- There are no restrictions on the
Lock
field.
This logic is triggered when a transaction contains a malicious or forbidden action.
- Trigger conditions:
info_cell
ONLY appears on the input side of the transaction.
- Result
- return
ERROR
- return
This logic is triggered when a token holder initiates a token transfer.
The output UDT_instance_cell
must follow this logic:
- Trigger conditions:
- No
info_cell
with the same UUID appears in inputs or outputs (user cannot modifyinfo_cell
in this operation)
- No
- The sum of
amount
fields inUDT_instance_cell
’s input MUST be greater than or equal toamount
fields in output cells- the “input greater than output” operation could help users get rid of spam tokens and re-claim their occupied capacity
This logic is triggered when an info_cell
appears in outputs
- Trigger conditions:
- An
info_cell
with the same UUID appears in outputs (some operations would includeinfo_cell
in inputs as well)
- An
- No restrictions on the sum of
UDT_instance_cell
’s amount in output
There are two kinds of cells defined in this proposal: info_cell
and UDT_instance
. Each type of cell has different lock functionalities and both are highly user definable.
The governance lock is the lock
on the info_cell
. By operating the info_cell
, governance operations are enabled for the UDT. Below we have listed several governance lock examples for info_cell
.
An empty lock on an info_cell
would disable the privilege/responsibility of UDT governance. Without this function, there will never be additional issuance of the UDT and the UDT info will never be modified after being created.
Using a pure signature algorithm as the lock of info_cell
allows the owner of the lock to do anything to the UDT, including mint, burn and modify the info_cell
. This is useful when the operator is an asset gateway, such as Tether.
This design allows for the implementation of complex logic to make decisions, such as holder voting and governing committees. Constraints to bound the possibilities of info_cell
modification and mint/burn limitations can also be added.
For example, minting logic can be embedded in a complex governance lock script with the following rules:
- An aggregated signature of 2/3 of committees is required to alter the UDT
- New minted token amount should not exceed 10% of current supply
- The
total_supply
field in theinfo_cell
MUST also be updated
The usage lock is the lock of the UDT_instance
, there are no restrictions placed on this field, it is free to be defined by token holders. This usage lock could interact with other Type logic to implement functionality for various use cases.
Users may use any simple lock to secure the cells that store their UDT balances, such as a secp256k1 lock. This lock would only verify cell ownership and a signature.
To create a sub-token, we can create a special lock with two args: the first is a sub_id
and the second is the holder’s public_key
.
A condition will be added to the UDT transfer function requiring that the output lock includes the sub_id
provided in the first arg. A single UDT Instance is effectively divided into multiple sub tokens and this design can be useful when the same UDT is being used across different regions with different regulations.
Other dApps such as a DEX may have their own specific token processing logic, which would exist as a superset of the Base Fungible UDT standard. In this model, we could leverage a Usage Lock to implement processing logic changes.
Take a DEX as an example. Firstly, token holders would deposit their UDTs to a cell using the DEX’s lock, and receive a new DEX token as receipt.
// Deposit to DEX
Inputs:
UDT_Instance:
Lock:
<holder_lock>
Outputs:
UDT_Instance:
Lock:
<DEX_DEPOSIT_LOCK>
DEX_Receipt_Cell:
Data:
amount,
UDT UUID,
...
Type:
<DEX_Type>
Lock:
<DEX_Lock with holder_lock as args>
After deposit, users could conduct exchanges under the DEX logic.
// Exchange
Inputs:
DEX_Receipt_Cell
<...>
Outputs:
DEX_Receipt_Cell
<...>
Finally, users could withdraw from DEX.
// Withdraw
Inputs:
DEX_Receipt_Cell
Lock:
<holder_lock>
UDT_Instance:
Data:
reserve_amount
Lock:
<DEX_DEPOSIT_LOCK>
Outputs:
UDT_Instance:
Data:
withdraw_amount
Lock:
<holder_lock>
UDT_Instance:
Data:
reserve_amount - withdraw_amount
Lock:
<DEX_DEPOSIT_LOCK>
Author: Cipher Wang
Editors: Matt Quinn, Tannr Allard