Introduction
The UDT (User-Defined Token) standard has been a long-standing issue in the CKB community. Previously, there were two standards: sUDT and xUDT. The sUDT was too simple, while the extension mode of xUDT was hardly used due to compatibility issues with other applications. Furthermore, until now, there has been no standard for binding UDT information on-chain.
A practical approach is that UDT should focus on implementation like Solana’s SPL token standard, rather than focusing on abstract interfaces like ERC20.
Why is it difficult to advance efforts like xUDT, which defines an abstract interface for extensions to expand the token standard? This is because the EVM is an on-chain computation model, while the Cell model is not. Arbitrary implementation of extensions would make it difficult for other applications to integrate. Therefore, a reasonable approach is for the UDT designer to decide what functions the corresponding UDT script should have, and provide both on-chain script implementation and off-chain SDK implementation. This ensures that various applications such as wallets, DEXs, and lending platforms won’t encounter problems when interfacing. Token issuers can select and combine the needed functions from the existing UDT feature set.
When the demand for a new feature becomes strong enough, and existing UDTs fail to provide the required functionality, a new UDT implementation can be designed. This implementation should ensure backward compatibility as much as possible, allowing most applications to integrate with token transfers simply by replacing the code_hash.
Solana currently has two token standards:
-
SPL Token, with the following features:
- Mint authority
- Freeze authority (ability to freeze accounts)
- Decimals information
- Metadata: name, decimal, URI
-
SPL Token 2022, which adds the following features based on SPL Token:
- Confidential transfers (hiding transaction amounts)
- Transfer hooks: calling a specific program for each transfer
- Transfer fees: ability to tax transfers
- Closing mint accounts, reclaiming storage rent
- Interest-bearing tokens: token balances can grow at a certain ratio
- Non-transferable tokens: only the issuer can change token ownership
- Transfer memos: for compliance and audit tracing
- Closing mint authority
It can be said that most extension needs for token standards would not exceed the set outlined above.
Considering the importance and difficulty of implementation in the Cell model, I believe it’s reasonable to first implement the basic SPL Token functionality. Other features are challenging to implement in the Cell model. For example:
- Transfer fees require consideration of state contention or state cost issues.
- Interest-bearing tokens need to address the problem of obtaining on-chain time.
Therefore, for a simple and practical UDT standard implementation, it should have the following features, with metadata binding for easy application integration and freeze authority for centralized stablecoin issuance:
- Programmability of mint authority, which AMM LP tokens and collateralized stablecoins depend on.
- On-chain binding of metadata, allowing indexing of token metadata information from the token Cell.
- Freeze authority, the ability to blacklist a specific lockscript.
- Ability to modify mint authority.
- Ability to modify metadata.
- Ability to modify freeze authority.
- Supply control capability.
To simplify usage and address different scenarios, this proposal will introduce two script implementations: Enhanced sUDT and Enhanced xUDT.
Enhanced sUDT will be consistent with sUDT in terms of transfer usage, allowing it to integrate with existing applications and infrastructure without code modifications.
Enhanced xUDT will be able to implement additional constraints on UDT Transfers, such as the freeze function. The args for both UDT standards will be 32 bytes to ensure full compatibility with current UDTs.
Enhanced sUDT will include the following features:
- Programmability of mint authority.
- On-chain binding of metadata.
- Ability to modify mint authority.
- Ability to modify metadata.
- Supply control capability.
Enhanced xUDT will add the following features based on Enhanced sUDT:
- Freeze authority.
- Ability to modify freeze authority.
- Plugin extension capability.
Basic Data Structures
struct ScriptAttr {
location: byte,
script_hash: Byte32,
script: ScriptOpt,
}
option ScriptAttrOpt (ScriptAttr);
vector ScriptAttrVec <ScriptAttr>;
ScriptAttr represents on-chain permissions, with Location defined as follows:
- 0: input_lock, meaning the constraint check should verify if there exists an input_cell’s lock_hash that satisfies the rule.
- 1: input_type, similar check as above.
- 2: output_type, similar check as above.
- 3: dynamic linking, this script must have a value and execute according to xUDT rules using dynamic linking.
- 4: spawn, this script must have a value and execute the corresponding script according to spawn rules. (Awaiting hard fork to enable)
Among these types, 0, 1, and 2 must rely on an input or output during execution and may have contention issues, while 3 and 4 only execute the corresponding code without state contention issues.
Enhanced sUDT
Data Definition
When creating a token, first establish an sUDT Meta Cell, with its type script’s code_hash as sUDT Meta Type and args as type_id.
struct ScriptAttr {
location: byte,
script_hash: Byte32,
script: ScriptOpt,
}
option ScriptAttrOpt (ScriptAttr);
table sUDTMetadata {
flag: byte,
mint_authority: ScriptAttrOpt,
metadata_authority: ScriptAttrOpt,
supply: Uint128,
decimals: byte,
name: Bytes,
symbol: Bytes,
uri: Bytes,
extra_data: Bytes,
}
The flag will indicate the configuration used by the UDT:
- Bit 0 set to 1 indicates that the supply mode is enabled.
- Other bits are reserved and set to 0.
Token Creation
The token creator should first generate an sUDT Meta Cell
, filling in the corresponding information. The UDT Cell’s type code_hash should be Enhanced sUDT_code_hash
, and args should be sUDT Meta type_hash
, which is 32 bytes in length.
Token Issuance
When issuing tokens, there are two scenarios:
- If Supply mode is enabled, the
sUDT Meta Cell
needs to be included as an Input, checking the mint_authority, and verifying the increase or decrease in UDT quantity relative to the input, modifying the supply value. The token issuer can first issue the total supply of tokens, then set mint_authority to None, meaning no further token issuance is possible. - If Supply mode is not enabled, the
sUDT Meta Cell
needs to be included as CellDeps, and the mint_authority is checked.
Token Transfer and Destruction
Token transfer and destruction follow the same rules as ordinary sUDT, entirely controlled by its lock, allowing free operation.
Meta Cell Modification
After mint_authority verification passes, the mint authority can be transferred.
After metadata_authority verification passes, the metadata modification authority can be transferred, and name, symbol, URI, and decimals can be modified. Modifying decimals can be understood as splitting tokens or the reverse.
Once any authority is transferred to None, it cannot be reclaimed.
Enhanced xUDT
When creating a token, first establish an xUDT Meta Cell, with its type script’s code_hash as sUDT Meta Type and args as type_id.
struct ScriptAttr {
location: byte,
script_hash: Byte32,
script: ScriptOpt,
}
option ScriptAttrOpt (ScriptAttr);
vector ScriptAttrVec <ScriptAttr>;
table xUDTMetadata {
flag: byte,
paused: byte,
mint_authority: ScriptAttrOpt,
pause_authority: ScriptAttrOpt,
metadata_authority: ScriptAttrOpt,
freeze_authority: ScriptAttrOpt,
extensions: ScriptAttrVec,
supply: Uint128,
decimals: byte,
name: Bytes,
symbol: Bytes,
uri: Bytes,
extra_data: Bytes,
}
The flag will indicate the configuration used by the UDT:
- Bit 0 set to 1 indicates that the supply mode is enabled.
- Bit 1 set to 1 indicates that the freeze mode is enabled.
- Other bits are reserved and set to 0.
Compared to Enhanced sUDT, Enhanced xUDT’s metadata has two additional items: paused, paused_authority, freeze_authority and extensions. The ‘paused’ indicates whether the contract is paused; the pause_authority can modify the value of paused, limited to 0 or 1 only; the freeze_authority has the right to add and remove from the blacklist; while extensions are additional checks applied to each UDT Transfer, which can be more than one.
Token Creation
The token creator should first generate an xUDT Meta Cell
, filling in the corresponding information. The UDT Cell’s type code_hash should be Enhanced xUDT_code_hash
, and args should be xUDT Meta type_hash
, which is 32 bytes in length.
Token Issuance
When issuing tokens, there are two scenarios:
- If Supply mode is enabled, the
sUDT Meta Cell
needs to be included as an Input, checking the mint_authority, and verifying the increase or decrease in UDT quantity relative to the input, modifying the supply value. The token issuer can first issue the total supply of tokens, then set mint_authority to None, meaning no further token issuance is possible. - If Supply mode is not enabled, the
sUDT Meta Cell
needs to be included as CellDeps, and the mint_authority is checked.
Token issuance should also go through extensions checks, but passing information that the current operation has passed mint_authority checks. Most extensions can directly return verification success.
During token issuance, paused should be 0, otherwise new token issuance is not allowed.
Freeze Functionality
The token’s blacklist will be implemented using a linked list. When the flag indicates that the corresponding token has freeze_authority, it needs to ensure that a linked list cell is created simultaneously, with the range of this linked list cell covering 000…000 to FFF…FFF.
array BlackListRange [Byte32; 2];
table BlackListData {
range: BlackListRange,
blacklist: Byte32Vec,
}
Each blacklist’s data is a Bytes32Vec, representing the frozen lock_hash. There are four operations:
- Insert some items within the range.
- Delete some items within the range.
- Split the Cell, turning one range into two ranges.
- Merge Cells, combining two ranges into one range.
The typescript of this linked list cell has a code_hash of blacklist code_hash, and args is UDT Meta type_hash. When inserting new items into the linked list or splitting a linked list cell into multiple ones.
The script execution check reads a cell from cellDeps whose type_hash equals the args of blacklist_lock_script, uses UDTMetadata to parse, obtains freeze_authority, then verifies permissions, and simultaneously verifies the correctness of insertion, deletion, splitting, and merging.
When the flag has freeze functionality, during transfers, the corresponding blacklist_cell needs to be placed in cellDeps, with its range covering all input and output UDT Cell lockhashes. If distributed across multiple blacklist linked list cells, multiple cellDeps need to be placed.
UDT needs to check that all lockhashes involved in UDT are not present in the blacklist_cell for the transaction to successfully go on-chain.
Scripts with freeze_authority can modify the freeze_authority in UDTMetaData, i.e., transfer authority.
Compared to SMT-based blacklist implementation, the linked list is a pure on-chain implementation, with all information queryable on-chain, and adding/removing from the blacklist is also simple.
At the same time, paused can be directly set to 1 by pause_authority, which disallows any transfers.
Token Transfer and Destruction
Both token transfer and destruction need to pass blacklist and extensions checks.
- The
sUDT Meta Cell
needs to be included as CellDeps. - If the metadata’s flag indicates that freeze mode is enabled, all blacklist Cells need to be read from cellDeps, and all lock_hashes in inputs and outputs need to be checked to ensure they don’t exist in the blacklist.
- If the length of extensions in the metadata is non-zero, all extensions checks are executed one by one.
- If paused in the meta is equal to 1, no transfers are allowed to be initiated.
Meta Cell Modification
After mint_authority verification passes, mint authority can be transferred, and extensions settings can be modified, such as adding or removing extensions.
After freeze_authority verification passes, freeze authority can be transferred, and blacklist cells can be added or removed.
After metadata_authority verification passes, metadata modification authority can be transferred, and name, symbol, URI, and decimals can be modified. Modifying decimals can be understood as splitting tokens or the reverse.
Once any authority is transferred to None, it cannot be reclaimed.
Script Analysis
For Enhanced sUDT, there are two Scripts:
One is the Meta type, responsible for checking permissions for modifying Meta information and format checks. Since all checks for MetaCell are executed by type, it is recommended that its lock be forced to always_success, such as the non-upgradable deployment in ckb-ecofund/ckb-proxy-locks. It is recommended that always_success have a recognized deployment.
The other is Enhanced sUDT type, responsible for checking UDT transfers, while for minting, it obtains MetaCell information and performs proxy checks.
There is a dependency relationship here. Due to SupplyMode, Meta type needs to be able to read the quantity of UDT in inputs and outputs, so it needs to hard-code the code_hash and hash_type of Enhanced sUDT type in the code.
However, analysis shows that only Meta type needs to know the deployment-time information of Enhanced sUDT type, while Enhanced sUDT type doesn’t need to know Meta type’s deployment information. It only needs to get its own args and find a type that meets the requirements in inputs or celldeps, read its CellData according to the sUDT Meta format, and use it.
Therefore, both scripts can use non-upgradable data_hash deployment.
For Enhanced sUDT, there are three Scripts:
One is the Meta type, responsible for checking permissions for modifying Meta information and format checks. Since all checks for MetaCell are executed by type, it is recommended that its lock be forced to always_success, such as the non-upgradable deployment in ckb-ecofund/ckb-proxy-locks. It is recommended that always_success have a recognized deployment.
The second is Enhanced xUDT type, responsible for checking UDT transfers, while for minting, it obtains MetaCell information and performs proxy checks. It also needs to read blacklist Cells to determine if the transaction is legal.
The third is BlackList type, responsible for checking the addition and removal of BlackList, and the splitting and merging of Cells.
When considering the deployment dependency relationships of the three scripts, first, Meta type depends on the deployment of Enhanced xUDT type code, while Enhanced xUDT type depends on the deployment of BlackList type, and Meta type also depends on the deployment of BlackList type.
Because when the Enhanced xUDT script executes, how does it find the blacklist cell? A simple method is to directly extract the blacklist code_hash and hash_type and concatenate them with its own args to find the blacklist.
Why does MetaCell also need to depend on the deployment of BlackList Cell? Because when creating MetaCell, if the Freeze function is enabled, it must ensure that a BlackList covering the full range is created.
BlackList type and Enhanced xUDT type, like Meta Cell, only need to obtain Meta Cell information through args, parse and read, and perform permission verification, so there is no circular dependency. They can all be deployed using data deployment, just deployed in the order of dependencies.
For MetaCell and Blacklist Cell, since their constraints are entirely completed by type, the function of lock will affect functionality, so AlwaysSuccess script can be hard-coded in the code, and it is mandatory to require that the lock of these two types of Cells must be AlwaysSuccess.
Comparison with SMT-based Freeze Mechanism
In RFC: Regulation Compliance Extension - English / CKB Development & Technical Discussion - Nervos Talk, an SMT-based freeze mechanism was proposed. Its advantage is that the on-chain state occupation is extremely small, so the cost of adding to the blacklist is very low.
The disadvantages are as follows:
- For each token adopting this mechanism, all involved applications need to index and store the latest state of the SMT tree, while the linked list-based approach doesn’t require maintaining off-chain state and is easier to integrate.
- It has strong invasiveness to applications. Transfer transactions need to place proofs in the Witness, which becomes problematic when dealing with OTX transactions. Since user signatures include the corresponding Witness, and the final Aggregator may not initially know which lockscripts will be involved, it requires careful selection of combination modes. The linked list-based approach only adds CellDeps, which has almost no impact on application composition.
Moreover, based on the extensions mechanism, if the cost of blacklists becomes very high in the future, for example, a blacklist involving 10 thousands locks would require 320,000 CKB.
If this cost is deemed too high at that time, a more easily combinable SMT-based freeze scheme can be designed, migrated from the linked list scheme, and all CKB can be reclaimed.
The migration process is as follows:
- mint_authority adds an SMT-based freeze extension, with its args being the type_hash of the SMTRootCell. The execution process of this plugin is:
- Read the Cell pointed to by the type_hash and read its data. The data should include two pieces of information: 1. SMT Root; 2. UDT type hash to be checked
- Read the SMT Proof from the Witness
- Scan the locks involved in the UDT Type hash corresponding to inputs and outputs.
- Check that all locks comply with the rules according to the SMT Proof.
- First, add all existing blacklisted locks to the SMT blacklist. At this point, each transfer will undergo two checks, one freeze mode and one freeze extension.
- Gradually clear and merge all blacklists from the blacklist cell, and reclaim CKB, until returning to the full range empty linked list.
- At this point, CKB has been reclaimed, and the blacklist linked list is empty, equivalent to only being subject to the extension’s blacklist check.
Analysis of Permission Management
According to the Principle of least privilege, regarding UDT, especially compliant UDT, the permission control in the final implementation can be more fine-grained, not limited to the details in this document.
In the Enhanced xUDT of this proposal, there are four types of permissions:
- mint_authority: Permission to mint tokens
- pause_authority: Permission to pause minting and transfers
- metadata_authority: Permission to modify metadata
- freeze_authority: Permission to freeze accounts
However, sometimes more fine-grained layered permission control can be added. For example, similar to USDC’s contract design on ETH, there exists a MasterMinter that can add minters and give each minter a certain mint limit. After minting a certain amount of tokens each time, the limit will decrease accordingly to isolate risks.
Then the metadata could become like this:
table xUDTMetadata {
flag: byte,
paused: byte,
master_mint_authority: ScriptAttrOpt,
minter: ScriptAttrVec,
mint_allowance: Uint128Vec,
pause_authority: ScriptAttrOpt,
metadata_authority: ScriptAttrOpt,
freeze_authority: ScriptAttrOpt,
extensions: ScriptAttrVec,
supply: Uint128,
decimals: byte,
name: Bytes,
symbol: Bytes,
uri: Bytes,
extra_data: Bytes,
}
Of course, the best approach is to reference the product design of relevant compliant asset and investigate requirements to design a more general applicable security permission management system.