Skip to main content

Tutorial: Simple UDT Script

Tutorial Overview

⏰ Estimated Time: 5 - 7 min
πŸ’‘ Topics: UDT, Script, Transaction
πŸ”§ Tools You Need:
  • offckb, version >= v0.3.0-rc2
  • git,make,sed,bash,sha256sum and others Unix utilities.
  • Rust and riscv64 target: rustup target add riscv64imac-unknown-none-elf
  • Clang 16+
    • Debian / Ubuntu: wget https://apt.llvm.org/llvm.sh && chmod +x llvm.sh && sudo ./llvm.sh 16 && rm llvm.sh
    • Fedora 39+: sudo dnf -y install clang
    • Archlinux: sudo pacman --noconfirm -Syu clang
    • MacOS (with Homebrew): brew install llvm@16
    • Windows (with Scoop): scoop install llvm yasm

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):

offckb create --script sudt-script

Create a New Script​

Let’s create a new Script called sudt inside the project.

cd sudt-script
make generate

Our project is successfully setup! You can run tree . to show the project structure:

tree .

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:

  1. 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.

  2. 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
}
  1. 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 iterating
  • Source::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 and ckb-script-templates to init a Script project
  • Use ckb_std to leverage CKB syscalls Source::GroupIuput/Source::GroupOutput for performing relevant cells iteration.
  • Write unit tests to make sure the UDT Script works as expected.

Additional Resources​