Mint SUDT via Contract

Simple User Defined Tokens(Simple UDT or SUDT) is a simple specification which provides a way for dapp developers to issue custom tokens on Nervos CKB.

It may be familiar for users to use SECP256K1/blake160 and SECP256K1/multisig to guard tokens, just like what we do with CKB. But how can we mint sUDT via contract?

We encountered this problem when we are developing toCKB, a trustless way maps assets on other blockchains to CKB. Here we will share a pattern to do this.

SUDT review

Let’s recall the SUDT spec first before we dig into the mint programming pattern.

A SUDT cell in Simple SUDT specification looks like following:

data:
    amount: uint128
type:
    code_hash: simple_udt type script
    args: owner lock script hash (...)
lock:
    <user_defined>

The following rules should be met in a SUDT Cell:

  • Simple UDT Rule 1: a SUDT cell must store SUDT amount in the first 16 bytes of cell data segment, the amount should be stored as little endian, 128-bit unsigned integer format. In the case of composable scripts, the SUDT amount must still be located at the initial 16 bytes in the data segment which corresponds to the composed SUDT script
  • Simple UDT Rule 2: the first 32 bytes of the SUDT cell’s type script args must store the lock script hash of owner lock.
  • Simple UDT Rule 3: each SUDT must have unique type script, in other words, 2 SUDT cells using the same type script are considered to be the same SUDT.

User shall use any lock script as they wish in the SUDT Cell.

You can find more details here.

The owner lock script hash in type args specify the owner. As long as the lock script shows in transaction inputs, we should consider it as owner mode. In owner mode, the SUDT script will not check the equality of amount between inputs and outputs, which means we can mint or burn token in this mode.

If we use SECP256K1/blake160 as SUDT type args lockscript, the one who has the associated private key can mint this token.

If we use SECP256K1/multisig as SUDT type args lockscript, the majority of the committee can mint this token.

If we want to mint token via contract, we should translate our contract logic into a lockscript.

lockscript as contract

On CKB, contracts are scripts which will be run during the transaction.

If our business is stateless, we can use a single lockscript to do this.
For example, if you want to crate a new token, any one who provides the answer to life, the universe and everything can be the token owner.
We can use a lockscript like below as our token args:

let arg = load_first_witness();
if arg == 42 {
    return 0;
} else {
    return 1;
}

If you create and publish this token, anyone can compose a transaction, provide 42 as the first witness to enter the owner mode, mint or burn any amount of token as they wish.

More often our business logics are associated with some states.
E.g. in a uniswap like DEX, when someone deposit some liquidity, the contract mint liquidity token, in a cross chain bridge, when someone relay a legal spv proof to the contract, the contract mint the mirror token. How can we achieve this?

We can delegate the verify logic of the lockscript to a typescript, use the cell data as our state and verify the state transfer in the typescript.

Let’s go through a simple example to see how to do this.

simplified ETH bridge

Let’s take a simplified ETH bridge as an example.
When someone relays a spv proof which indicates he locks some token into the bridge contract on ETH, we mint the mirror token(we can name it cETH) on CKB.

We will need 3 scripts here:

  • A eth-bridge-typescript to handle the spv proof verification logic.
  • A eth-bridge-lockscript to be used as SUDT typescript args and delegate the verify logic to eth-bridge typescript.
  • A SUDT typescript which represents cETH.

The transaction structure which mints cETH is like below:

Inputs:
    eth-bridge-cell:
        Type:
            code: eth-bridge-typescript
            args: id
        Lock:
            code: eth-bridge-lockscript
            args: eth-bridge-typescript-code-hash
        Data:
            eth_light_client_data
            records: [(block_hash, tx_index)]
    provide-capacity-cell
        Type: None
        Lock: <User Lockscript>
Outputs:
    eth-bridge-cell:
        Type:
            code: eth-bridge-typescript
            args: id
        Lock:
            code: eth-bridge-lockscript
            args: eth-bridge-typescript-code-hash
        Data:
            eth_light_client_data
            records: [(block_hash, tx_index)]
    cETH-token-cell:
        Type:
            code: SUDT-typescript
            args: eth-bridge-lockscript-hash
            data: amount
        Lock:
            <User Lockscript>
Witnesses:
    0: eth-spv-proof
    1: signature to unlock provide-capacity-cell

The scripts verify logics below:

  • eth-bridge-typescript
    • ensure the spv proof in associated witness is valid
    • ensure the handled spv proof is recorded in the cell data to avoid using a single proof to mint cETH multiple times.
    • ensure the ETH amount locked on Ethereum equals the amount of cETH we mint
  • eth-bridge-lockscript
    • ensure the associated eth-bridge-typescript represents in outputs, then we know the typescript script will be run in the transaction, it makes the logic delegation work

But we encountered a new problem.
We need the eth-bridge-typescript hash as the eth-bridge-lockscript args, then the eth-bridge-lockscript hash as SUDT-typescript args, but we need to know the cETH token script to check the token amount in eth-bridge-typescript. It we locate cETH token typescript by it’s script hash, there will be a cycle dependency.

We can use load_lockscript_hash in eth-bridge-typescript and then check the entire script (which includes code_hash, args and type) part by part instead of cETH token script hash.

The pseudocode is like below:

let lockscript_hash = load_lockscript_hash(0, Source::Output);
for script in load_output_typescripts {
    if script.args = lockscript_hash \
        && script.code_hash = SUDT_CODE_HASH \
        && script.type = 0 {
        // This is a cETH token cell
    }
}

You can check the complete demo here.
The test file shows how to construct a mint transaction using this pattern.

6 Likes

How to cross back Eth from cETH? Could something like state-root could help on it? I have a rough impression that some L2 on Eth using that to make sure Ethereum could verify the state on L2.

I did not mention this because this post is mainly about how to mint token via contract.
If you want to cross back Eth from cETH, you can burn the cETH token in a similar way we mint it, relay the transaction along with its proof to ETH, verify the proof in the ETH contract which locks the ETH, then unlock the associated amount of ETH if the verification passes.

But it rely on an implementation of ETH contract to parse CKB data structure, maintain legal headers and verify CKB spv proof.

1 Like

Do we have some articles or materials about how toCKB works? I’m very interested in this direction.

I am working on that. There will be some articles to introduce the design released on talk for discussion.

2 Likes