Tutorial: Simple UDT Script
Tutorial Overview
User Defined Token(UDT) is a fungible token standard on CKB blockchain.
In this tutorial, we will create a UDT Script which is a simplify version of XUDT standard to helps you gain better understanding of how fungible token works on CKB.
It is highly recommend to go through the dApp tutorial Create a Fungible Token first before writing your own UDT Script.
The full code of the UDT Script in this tutorial can be found at Github.
Data Structure for simple UDT Cellβ
data:
<amount: uint128>
type:
code_hash: UDT type script hash
args: <owner lock script hash>
lock: <user_defined>
Here the issuer's Lock Script Hash
works like the unique ID for the custom token.
Different Lock Script Hash means a different kind of token issued by different owner.
It is also used as a checkpoint to tell that a transaction is triggered by the token issuer
or a regular token holder to apply different security validation.
- For the token owner, they can perform any operation.
- For regular token holders, the UDT Script ensures that the amount in the output cells does not exceed the amount in the input cells. For the more detail explanation of UDT idea, please refer to create-a-token or sudt RFC
Now let's create a new project to build the UDT Script. We will use offckb and ckb-script-templates for this purpose.
Init a Script Projectβ
Let's run the command to generate a new Script project called sudt-script
(shorthand for simple UDT):
- Command
- Response
offckb create --script sudt-script
β οΈ Favorite `gh:cryptape/ckb-script-templates` not found in config, using it as a git repository: https://github.com/cryptape/ckb-script-templates.git
π€· Project Name: sudt-script
π§ Destination: /tmp/sudt-script ...
π§ project-name: sudt-script ...
π§ Generating template ...
π§ Moving generated files into: `/tmp/sudt-script`...
π§ Initializing a fresh Git repository
β¨ Done! New project created /tmp/sudt-script
Create a New Scriptβ
Letβs create a new Script called sudt
inside the project.
- Command
- Response
cd sudt-script
make generate
π€· Project Name: sudt
π§ Destination: /tmp/sudt-script/contracts/sudt ...
π§ project-name: sudt ...
π§ Generating template ...
π§ Moving generated files into: `/tmp/sudt-script/contracts/sudt`...
π§ Initializing a fresh Git repository
β¨ Done! New project created /tmp/sudt-script/contracts/sudt
Our project is successfully setup! You can run tree .
to show the project structure:
- Command
- Response
tree .
.
βββ Cargo.toml
βββ Makefile
βββ README.md
βββ contracts
βΒ Β βββ sudt
βΒ Β βββ Cargo.toml
βΒ Β βββ Makefile
βΒ Β βββ README.md
βΒ Β βββ src
βΒ Β βββ main.rs
βββ scripts
βΒ Β βββ find_clang
βΒ Β βββ reproducible_build_docker
βββ tests
βββ Cargo.toml
βββ src
βββ lib.rs
βββ tests.rs
7 directories, 12 files
Here's a little introduction: contracts/sudt/src/main.rs
contains the source code of the sudt Script, while tests/tests.rs
provides unit tests for our Scripts. We will introduce the tests after we wrote the Script.
Implement SUDT Script Logicβ
The sudt Script is implemented in contracts/sudt/src/main.rs. This script is designed to manage the transfer of UDTs on the CKB blockchain.
Let's break down the high-level logic of how this script works:
-
Owner Mode Check: The script first checks if it is being executed in owner mode. This is important because if the script is running in owner mode, it means the owner has special permissions, and the script can return success immediately without further checks.
-
Input and Output Amount Validation: Next, the script collects the total amounts of UDTs from both input and output cells. It ensures that the total amount of UDTs being sent (outputs) does not exceed the amount being received (inputs). This is crucial for maintaining the integrity of the token transfers.
Hereβs a snippet of the code that illustrates these checks:
pub fn program_entry() -> i8 {
ckb_std::debug!("This is a sample UDT contract!");
let script = load_script().unwrap();
let args: Bytes = script.args().unpack();
ckb_std::debug!("script args is {:?}", args);
// Check if the script is in owner mode
if check_owner_mode(&args) {
return 0; // Success if in owner mode
}
// Collect amounts from input and output cells
let inputs_amount: u128 = match collect_inputs_amount() {
Ok(amount) => amount,
Err(err) => return err as i8,
};
let outputs_amount = match collect_outputs_amount() {
Ok(amount) => amount,
Err(err) => return err as i8,
};
// Validate that inputs are greater than or equal to outputs
if inputs_amount < outputs_amount {
return Error::InvalidAmount as i8; // Error if invalid amount
}
0 // Success
}
- Error Handling: The script also includes robust error handling. It defines a custom
Error
enum to manage various error conditions, such as when the input amount is invalid or when there are issues with encoding. This helps ensure that any problems are clearly communicated.
This implementation ensures that UDT transactions are validated correctly, maintaining the integrity of token transfers on the CKB blockchain. By checking both the owner mode and the amounts, the script helps prevent errors and ensures smooth operation.
Collecting UDT Amountβ
In the sudt Script, the functions collect_inputs_amount
and collect_outputs_amount
play a crucial role in gathering the total amounts of UDTs from the respective input and output cells.
Hereβs how they work:
Both functions iterate through the relevant cells and collect the cell_data
to calculate the total UDT amount. This is done using the following syscalls:
Source::GroupInput
: This ensures that only the cells with the same script as the current running script are iteratingSource::GroupOutput
: Similarly, this syscall only the cells with the same script as the current running script are iterating
By using these specific sources, the script avoids issues related to different UDT cell types, ensuring that only the appropriate cells are processed. This is particularly important in a blockchain environment where multiple UDTs may exist, and we want to ensure that the calculations are accurate and relevant to the current UDT Script.
Hereβs a brief look at how these functions are implemented:
fn collect_inputs_amount() -> Result<u128, Error> {
let mut buf = [0u8; UDT_AMOUNT_LEN];
let udt_list = QueryIter::new(load_cell_data, Source::GroupInput)
.map(|data| {
if data.len() >= UDT_AMOUNT_LEN {
buf.copy_from_slice(&data);
Ok(u128::from_le_bytes(buf))
} else {
Err(Error::AmountEncoding)
}
})
.collect::<Result<Vec<_>, Error>>()?;
Ok(udt_list.into_iter().sum::<u128>())
}
fn collect_outputs_amount() -> Result<u128, Error> {
let mut buf = [0u8; UDT_AMOUNT_LEN];
let udt_list = QueryIter::new(load_cell_data, Source::GroupOutput)
.map(|data| {
if data.len() >= UDT_AMOUNT_LEN {
buf.copy_from_slice(&data);
Ok(u128::from_le_bytes(buf))
} else {
Err(Error::AmountEncoding)
}
})
.collect::<Result<Vec<_>, Error>>()?;
Ok(udt_list.into_iter().sum::<u128>())
}
In summary, these functions are essential for accurately calculating the total UDT amounts involved in the transaction while ensuring that only the relevant cells are considered, thus maintaining the integrity of the UDT transfer process.
Full code: contracts/sudt/src/main.rs
Writing Test For UDT Scriptβ
Testing is a crucial part of developing any smart contract, including the sudt script.
The test_transfer_sudt
test case is designed to simulate a transfer of UDTs from one address to another.
It sets up the necessary environment, including creating input and output cells, and then invokes the sudt Script to perform the transfer.
The test checks whether the transfer transaction is successfully verified.
Hereβs a code of the transfer_sudt
test case:
#[test]
fn test_transfer_sudt() {
let mut context = Context::default();
// build lock script
let always_success_out_point = context.deploy_cell(ALWAYS_SUCCESS.clone());
let lock_script = context
.build_script(&always_success_out_point, Default::default())
.expect("script");
let lock_script_dep = CellDep::new_builder()
.out_point(always_success_out_point)
.build();
// prepare scripts
let contract_bin: Bytes = Loader::default().load_binary("sudt");
let out_point = context.deploy_cell(contract_bin);
let type_script = context
.build_script(&out_point, Bytes::from(vec![42]))
.expect("script");
let type_script_dep = CellDep::new_builder().out_point(out_point).build();
let input_token: u128 = 400;
let output_token1: u128 = 300;
let output_token2: u128 = 100;
// prepare cells
let input_out_point = context.create_cell(
CellOutput::new_builder()
.capacity(1000u64.pack())
.lock(lock_script.clone())
.type_(Some(type_script.clone()).pack())
.build(),
input_token.to_le_bytes().to_vec().into(),
);
let input = CellInput::new_builder()
.previous_output(input_out_point)
.build();
let outputs = vec![
CellOutput::new_builder()
.capacity(500u64.pack())
.lock(lock_script.clone())
.type_(Some(type_script.clone()).pack())
.build(),
CellOutput::new_builder()
.capacity(500u64.pack())
.lock(lock_script)
.type_(Some(type_script).pack())
.build(),
];
let outputs_data = vec![
Bytes::from(output_token1.to_le_bytes().to_vec()),
Bytes::from(output_token2.to_le_bytes().to_vec()),
];
// build transaction
let tx = TransactionBuilder::default()
.input(input)
.outputs(outputs)
.outputs_data(outputs_data.pack())
.cell_dep(lock_script_dep)
.cell_dep(type_script_dep)
.build();
let tx = context.complete_tx(tx);
// run
let cycles = context
.verify_tx(&tx, 10_000_000)
.expect("pass verification");
println!("consume cycles: {}", cycles);
}
In this test case:
- We deploy the sudt Script we wrote and use it to build cell's type script.
- We set up the initial conditions by defining the valid input and output amounts.
- Finally, We build the full transaction and ensure that the transaction can be verified.
Notice that in the type script args we use a random bytes which is 42 indicating that we are not under the owner_mode
in the test_transfer_sudt
test case.
For ownermode testcase, you can checkout the full tests code for reference.
By writing tests like test_transfer_sudt
, we can ensure that our sudt script behaves correctly under various scenarios, helping to catch any issues before deployment.
Congratulations!β
By following this tutorial so far, you have mastered how to write a simple UDT Script that brings fungible tokens to CKB. Here's a quick recap:
- Use
offckb
andckb-script-templates
to init a Script project - Use
ckb_std
to leverage CKB syscallsSource::GroupIuput
/Source::GroupOutput
for performing relevant cells iteration. - Write unit tests to make sure the UDT Script works as expected.
Additional Resourcesβ
- Full source code of this tutorial: sudt-script
- CKB syscalls specs: RFC-0009
- Script templates: ckb-script-templates
- CKB transaction structure: RFC-0022-transaction-structure
- CKB data structure basics: RFC-0019-data-structure