Tutorial: Simple UDT Script
User-Defined Token (UDT) is a fungible token standard on CKB blockchain.
In this tutorial, we will create a UDT Script, a simplified version of XUDT standard to help you better understand how fungible tokens work on CKB.
It is highly recommended 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 Hashes correspond to different types of token issued by different owners.
This hash is also used to determine if a transaction is initiated by the token issuer or a regular token holder, to apply different security checks.
- Token Owner: Can perform any operation.
- Regular Holders: The UDT Script ensures that the amount in the output Cells does not exceed that in the input Cells. For a more detailed explanation, please refer to Create a Fungible Token or RFC0025: Simple UDT
Now, let's create a new project to build the UDT Script. We will use OFFCKB and ckb-script-templates.
Initialize a Script Projectβ
Let's run the command to generate a new Script project called sudt-script
(short 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 view 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
contracts/sudt/src/main.rs
contains the source code of the sUDT Script, and tests/tests.rs
provides the unit tests for our Scripts. We will introduce the tests after completing 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 running in owner mode means that the owner has special permissions, allowing the Script to return success immediately without further checks.
-
Input and Output Amount Validation: Next, the Script collects the total UDTs amounts from the input and output Cells to ensure that the UDTs being sent (outputs) do not exceed that being received (inputs). This is crucial for maintaining the integrity of token transfers.
Hereβs the code snippet illustrating 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 includes robust error handling through a custom
Error
enum to manage various error conditions, such as invalid input or encoding issues. This ensures clear communication of any problems.
Together, this implementation validates UDT transactions effectively, maintaining the integrity of token transfers on the CKB blockchain. By checking both the owner mode and the amounts, the Script helps preventing errors and promotes smooth operation.
Collecting UDT Amountβ
In the sUDT Script, the collect_inputs_amount
and collect_outputs_amount
functions 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, done with the following syscalls:
Source::GroupInput
Source::GroupOutput
For source group input and output respectively, they ensure that only the Cells with the same Script as the current running Script are involved in the iteration.
By using these specific sources, the Script avoids issues related to different UDT Cell types, ensuring that only the appropriate Cells can be processed. This is particularly important in a blockchain environment where multiple UDTs may exist, to ensure accurate iteration of the relevant 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 the accurate calculation of 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
Write Tests 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, then invoking the sUDT Script to perform the transfer.
The test checks whether the transfer transaction is successfully verified.
Hereβs the code for 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 the 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 its validity.
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 owner mode test case, refer to the full tests code for reference.
By writing tests like test_transfer_sudt
, we can ensure that our sUDT Script functions correctly under various scenarios, helping to identity 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 Cell 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