Skip to content

ickb/whitepaper

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

142 Commits
 
 
 
 
 
 

Repository files navigation

iCKB

Listenable Introduction

If you would like to listen to an introduction of the project before diving in, Jordan Mack explains iCKB in less than 7 minutes during an episode of Hashing it Out.

DApp

If you would like to try out the DApp to get an idea on how it works, iCKB DApp is live on both mainnet and testnet.

Problem

Nervos DAO Illiquidity

The Nervos DAO is possibly the most important smart-contract of Nervos Layer 1 (L1). A CKB holder can lock their CKB in the Nervos DAO in exchange for a receipt of that specific deposit. Every 180 epochs (~30 days) the depositor has the option of exchanging their receipt to unlock their initial deposit plus accrued interest. This creates an illiquidity for the depositor while the CKB is locked.

Untapped Potential

There exists untapped potential in the Nervos ecosystem for a protocol that can liquify Nervos DAO accrued interest and bridge it from L1 to L2. This protocol could enable CKB-based Initial Stake Pool Offerings (ISPO), where users can lock CKB to support new early stage projects without losing their original CKB deposit.

The protocol could also be used to enable a community voting mechanism with funds locked in the Nervos DAO, as well as a multitude more L1, L2, and bridge applications!

dCKB (Unmaintained)

In the past there has been an effort to tackle this challenge by NexisDAO with dCKB. Their approach is to tokenize the holder receipt, which in turn becomes tradeable, so the holder remains liquid. The issue with their approach is that only the original owner can unlock the deposit. Judging by their GitHub repository's issues, dCKB does not appear to be actively maintained.

wstCKB (Under Development)

Currently there is a new effort to tackle this challenge by Stable++. They are developing a new solution behind closed doors and as such not much information is publicly available on wstCKB, except for:

Stable++ also introduces Liquidity Staking through Nervos DAO. Users can stake CKB in exchange for wstCKB, allowing them to earn staking rewards while still being able to use their wstCKB for investments without losing liquidity.

When asked directly on their public Telegram group, Alive24 explained:

At the moment, as LST is still under development [...] If anything disclosable, we attempt to make wstCKB in a way similar to wstETH in terms of rebasing mechanism and anonymous Nervos DAO cell deposit and withdrawal. Any further details are still under development and adjustment. [...] I've read the proposal today and we found a lot in common! Thanks for the advice and definitely it would be of inspirations.

From the information currently available, wstCKB seems to avoid dCKB's mistakes and closely follow iCKB's approach.

This brings the question: Is it really worth developing an iCKB look-alike and doubling the effort?

If wstCKB is too similar to iCKB, the result would be split liquidity between iCKB and wstCKB, reducing the Deposit Pool size for both and bringing the following downsides for everyone:

  1. Longer withdrawal wait time because the temporal density of deposit maturities depends on Deposit Pool size.
  2. Busiwork Attack feasibility.

Solution

Enter iCKB

The inflation-protected CKB (iCKB) is a Nervos L1 xUDT token responsible for protecting users against Nervos secondary issuance inflation like Nervos DAO, while at the same time being a liquid asset. In this whitepaper, the inflation-protected CKB will be referred to as iCKB.

As with dCKB, iCKB's approach is to tokenize Nervos DAO receipts, but with a twist: the protocol owns all the CKB deposits and maintains a pool of them. This means that all the deposits and withdrawals are shared, so anyone can use anyone else's deposit to exit once it's mature.

This protocol aims to solve two problems with Nervos DAO:

  • CKB locked in the Nervos DAO remains liquid as iCKB can truly be used as a normal currency.
  • iCKB can be converted back to CKB quickly at any time without having to wait for maturity.

Water Mill Analogy

As a water mill has many distinct buckets, each at different wheel positions, in which the water is:

  • Collected
  • Maintained
  • Released

In the same way, the protocol can have many distinct deposits, each of them constantly moving at different stages of maturity:

  • Collected: Users deposit CKB and receive iCKB.
  • Maintained: Deposits accrue interest in the Nervos DAO.
  • Released: If a user wants to exchange iCKB for CKB, they can use any deposit that is at maturity.

Feedback

Jordan Mack's comments on Nervos L1 & iCKB:

In a more abstract sense, this doesn't violate any of intentions of the platform. The CKB that is staked is still out of circulation. iCKB does not grant the holder the ability to store data on the blockchain. In the most pure sense, iCKB is enabling the functionality that dCKB was trying to achieve. It better solves the problem because anyone can unlock the original CKB from the Nervos DAO using iCKB instead of requiring the original owner to unlock it as with dCKB.

Team

Phroi

I'm a developer, going by the pseudonym Phroi. I'd like to spend my time working on projects that give meaning to my life and improve users' lives. Win-win situations. The best way to interact with me is in writing, since English is not my native language. A bit on the over-thinker side, so much that I unwillingly find vulnerabilities in other people's work.

Discovering iCKB

During February 2022, while testing the ground for a Nervos DAO based ISPO, I discovered the untapped need for a token that liquefies and bridges interest from L1 to L2, so with Jordan Mack's help I started researching its feasibility. It's since then that I'm working on iCKB.

Diving Into The Protocol

On-Chain, Trust-Less and Decentralized

This protocol defines a solid way to exchange between CKB and iCKB. The design aims to make iCKB as simple, robust and neutral as possible, making it capable of meeting the current and future needs of Nervos users.

This protocol lives completely on Nervos Layer 1. It works by wrapping Nervos DAO transactions: a deposit is first tracked by its protocol receipt and later it is converted into its equivalent amount of iCKB.

iCKB/CKB Exchange Rate Idea

The iCKB mechanism for wrapping interest is similar to Compound's cTokens. The CKB to iCKB exchange rate is determined by block number. At the genesis block 1 CKB is equal to 1 iCKB. As time passes 1 CKB is slowly worth less than 1 iCKB at a rate that matches the issuance from the Nervos DAO. This is because iCKB is gaining value. An easier way to understand this is to think of:

  • CKB as inflationary
  • iCKB as non-inflationary

Jordan Mack's comment on this method:

That's a clever approach. Thinking of it as iCKB being the base and CKB being what is moving makes it much easier to understand.

The inflation rate of CKB is well defined by the Nervos DAO compensation rate and only depends on:

Therefore, the iCKB/CKB exchange rate will always be precise as determined by the formula and the current block. The only risk to this deterministic peg would be a smart contract exploit to the deposit pool or minting contract. These kinds of attack vectors are greatly mitigated by external audits.

Standard Deposit

As bricks can be used to build houses of any size, it seems natural to establish a reasonably small standard deposit size that can be used to construct deposits of any size.

In this way a few goals are achieved:

  • Big deposits, split into standard deposits, increase the overall protocol liquidity.
  • No size mismatch means anybody can use anybody else's deposit to withdraw.
  • The following form of DoS is prevented:

Let’s assume there is no requirement on deposit size, so as in Nervos DAO users can choose the deposit size they prefer. Then an attacker who can borrow a big enough capital can simply attack by repeating the following two steps:

  • Deposit CKB for iCKB in deposits as big as the entirety of their capital.
  • Exchange iCKB for smaller CKB deposits.

This would greatly reduce the quality of the service for everyone, as the only remaining deposits would be as big as or bigger than the attacker's capital and since it’s impossible to withdraw partially from a Nervos DAO deposit, this would greatly hamper use of the protocol.

Back to the standard deposit definition, its size could be defined in CKB terms or in iCKB terms:

  • Defining it in CKB terms means that as deposits are made over time, every deposit would have a different size due to Nervos DAO interest, so it's not working as intended.
  • Defining it in iCKB terms means that at each block a standard deposit would have the same size both in CKB and iCKB. Of course as time passes, the deposit size would be fixed in iCKB-equivalent terms but gradually increasing in CKB terms.

Let's define the standard deposit size as 100,000 iCKB.

iCKB/CKB Exchange Rate Calculation

Excluding deposit cell occupied capacity, per definition 100,000 iCKB are equal to 100,000 CKB staked in Nervos DAO at the genesis block, let's calculate what this means.

From the last formula from Nervos DAO RFC Calculation section:

Nervos DAO compensation can be calculated for any deposited cell. Assuming a Nervos DAO cell is deposited at block m, i.e. the deposit cell is included at block m. One initiates withdrawal and gets phase 1 withdrawing cell included at block n. The total capacity of the deposit cell is c_t, the occupied capacity for the deposit cell is c_o. [...] The maximum withdrawable capacity one can get from this Nervos DAO input cell is:

( c_t - c_o ) * AR_n / AR_m + c_o

AR_n is defined in the Nervos DAO RFC Calculation section:

CKB's block header has a particular field named dao containing auxiliary information for Nervos DAO's use. Specifically [...] AR_i: the current accumulated rate at block i. AR_j / AR_i reflects the CKByte amount if one deposit 1 CKB to Nervos DAO at block i, and withdraw at block j.

Let's fix a few constants:

  • c_o = 82 CKB (occupied cell capacity of a standard deposit cell)
  • c_t = 100,082 CKB (total cell capacity equals the iCKB-equivalent deposit size plus its occupied capacity)
  • AR_0 = 10 ^ 16 (genesis accumulated rate)

So by depositing 100,082 CKB at block 0, iCKB/CKB exchange ratio at block n is defined as:

  • 100,000 iCKB := 100,000 CKB * AR_n / 10 ^ 16 (excluding 82 CKB of occupied cell capacity)

Conversely, by plugging block m as deposit block and block 0 as withdrawal block in Nervos DAO's formula, it's possible to calculate how much iCKB is worth 100,082 CKB deposited at block m:

  • 100,000 CKB * 10 ^ 16 / AR_m (excluding 82 CKB of occupied cell capacity)

This shows that the iCKB/CKB exchange rate only depends on a few constants and the accumulated rate, defined in the deposit's block header.

Deposit

In Nervos DAO, a deposit is a single transaction in which a CKB holder locks their CKB in exchange for a Nervos DAO receipt of that specific deposit.

In the proposed protocol, a deposit is the process in which a CKB holder locks their CKB in exchange for iCKB tokens.

This process can't happen in a single transaction due to a Nervos L1 technical choice: as seen from the previous section, to mint the iCKB equivalent for a deposit the protocol needs to access the current accumulated rate, which is defined in the deposit's block header. However, Nervos L1 is off-chain deterministic, so the current block header cannot be accessed while validating a transaction.

Thus the protocol is forced to split a deposit into two phases:

  1. In the first phase, the CKB holder locks their CKB in exchange for a protocol receipt of the specific amount deposited.
  2. In the second phase, the deposit's header block is available, so the protocol receipt can be transformed into iCKB tokens.

Deposit Phase 1

In this first phase the protocol:

  • Transforms input CKB into Nervos DAO deposit cells locked by the iCKB Logic Script, in short a deposit.
  • Awards the user a protocol receipt for the deposits, effectively wrapping them.

Given the impossibility of accessing the header in this phase, there cannot be a strict requirement on deposits' iCKB-equivalent size. On the other hand, to achieve higher deposit fungibility and to prevent a certain form of DoS, the protocol needs to incentivize standard deposits.

In particular, deposits bigger than the standard deposit size are actively disincentivized: the user will receive only 90% of the iCKB amount exceeding a standard deposit. The remaining 10% is offered as a discount to whoever is willing to withdraw from the oversized deposits. Additionally, the maximum unoccupied capacity per single deposit is fixed at 1,000,000 CKB. This upper bound prevents a certain form of DoS, while still leaving enough slack for the standard deposit CKB size to grow for well over a hundred years.

On the other side, deposits smaller than the standard deposit size are intrinsically disincentivized by L1 dynamics. As deposits get smaller they incur a bigger penalty in the form of unaccounted occupied capacity. Additionally, the minimum unoccupied capacity per single deposit is fixed at 1,000 CKB. This lower bound prevents users from making deposits too detrimental to themselves.

Taking these incentives into consideration, at least 90% of the deposit amount is always converted. Of course the optimal strategy for a depositor is to split their CKB into standard deposits.

Since having a separate receipt per deposit cell would be capital inefficient, the protocol allows multiple deposits to be accounted for with a single receipt. An iCKB receipt accounts for a group of deposits with the same size, it just contains the single deposit unoccupied CKB capacity and the quantity of the accounted deposits. In a transaction output there can be many receipt cells and possibly more than one receipt for the same deposit size.

For simplicity a transaction containing Nervos DAO script is currently limited to 64 output cells so that processing is simplified. This limitation may be relaxed later on in a future Nervos DAO script update.

In a receipt cell data:

  • The second 8 bytes store the deposit unoccupied capacity, which is the single deposit capacity minus its occupied capacity, the actual deposit_amount. A single receipt tracks a group of deposits with the same unoccupied capacity in the current tx output. Multiple receipts for a specific unoccupied capacity may be created where each one keeps track of a different group of deposits.
  • The first 4 bytes store the quantity of deposits with the same unoccupied capacity being tracked in the tx output. A tx may create many deposits with the same unoccupied capacity. This counter keeps track of how many deposits with the same unoccupied capacity are being tracked in the current tx by this specific receipt.

Summing up, in the first deposit phase, these rules must be followed:

  • A deposit is defined as Nervos DAO deposit with an iCKB Logic Lock {CodeHash: iCKB Logic Hash, HashType: Data1, Args: Empty}.
  • A single deposit unoccupied capacity cannot be lower than 1,000 CKB nor higher than 1,000,000 CKB.
  • A group of same-size deposits must be accounted by a receipt.
  • A receipt is defined as a cell with iCKB Logic Type {CodeHash: iCKB Logic Hash, HashType: Data1, Args: Empty}, the first 12 bytes of cell data are reserved for:
    • deposit_quantity keeps track of the quantity of deposits (4 bytes)
    • deposit_amount keeps track of the single deposit unoccupied capacity (8 bytes)
  • No more than 64 output cells are allowed under the currently deployed Nervos DAO script.
  • CellDeps must contain iCKB Dep Group comprising: iCKB Logic Script and Nervos DAO Script.

Receipt data molecule encoding:

array Uint32           [byte; 4];
array Uint64           [byte; 8];

struct ReceiptData {
    deposit_quantity:  Uint32,
    deposit_amount:    Uint64,
}

Example of deposit phase 1:

CellDeps:
    - iCKB Dep Group cell
    - ...
Inputs:
    - ...
Outputs:
    - Nervos DAO deposit cell with iCKB Logic Lock:
        Data: 8 bytes filled with zeros
        Type: Nervos DAO
        Lock:
            CodeHash: iCKB Logic Hash
            HashType: Data1
            Args: Empty
    - ...
    - Receipt:
        Data: ReceiptData
            deposit_quantity: Quantity of deposits (4 bytes)
            deposit_amount: Single deposit unoccupied capacity (8 bytes)
        Type:
            CodeHash: iCKB Logic Hash
            HashType: Data1
            Args: Empty
        Lock: A lock that identifies the user

Deposit Phase 2

A receipt accrues interest and it can be used to withdraw, but it's not liquid nor transferable.

The second phase of the deposit transforms a receipt into its equivalent amount of iCKB tokens, which in turn is both liquid and transferable. This conversion is now possible because the deposit block header is available.

As seen in iCKB/CKB Exchange Rate Calculation, for each receipt the equivalent amount of iCKB is well defined. The only difference is the incentive mechanism: oversized receipts are subject to a 10% fee on the amount exceeding a standard deposit.

In the second deposit phase, these rules must be followed:

  • The iCKB value of a receipt is calculated as
iCKB_value(unoccupied_capacity, AR_m) {
    let s = unoccupied_capacity * AR_0 / AR_m;
    if s > standard_deposit_size {
        s = s - (s - standard_deposit_size) / 10
    }
    return s;
}

receipt_iCKB_value(deposit_quantity, deposit_amount, AR_m) {
    return deposit_quantity * iCKB_value(deposit_amount, AR_m);
}
  • The total iCKB value of input tokens and input receipts must be equal to the total iCKB value of output tokens.
  • iCKB xUDT flags are set to 0x80000000 to enable xUDT owner mode by input type. This flag must later on be encoded as a Uint32 Little Endian, so its final encoding is 0x00000080.
  • HeaderDeps must include the hash of the header of the on-chain block containing the receipt tx for each receipt being converted into iCKB xUDT.
  • CellDeps must contain iCKB Dep Group comprising: iCKB Logic Script, Standard xUDT Script and Nervos DAO Script.

Example of deposit phase 2:

CellDeps:
    - iCKB Dep Group cell
    - ...
HeaderDeps: 
    - Receipt Block Header Hash
    - ...
Inputs:
    - Receipt:
        Data: ReceiptData
        Type:
            CodeHash: iCKB Logic Hash
            HashType: Data1
            Args: Empty
        Lock: A lock that identifies the user
    - ...
Outputs:
    - Token:
        Data: amount (16 bytes)
        Type:
            CodeHash: Standard xUDT Script
            HashType: Data1
            Args: [iCKB Logic Script Hash, 0x00000080]
        Lock: A lock that identifies the user

Withdrawal

In Nervos DAO, time is slotted in batches of 180 epochs depending on the initial deposit timing, and withdrawal is split into two steps:

  1. In the first transaction the user requests the withdrawal.
  2. In the second transaction the user withdraws the deposit plus interest. This must be after the end of the 180-epoch batch in which the first transaction happened.

As seen in Nervos DAO RFC Calculation section the actual withdrawn CKB amount depends on the deposit block and on the withdrawal request block.

The proposed protocol instead proceeds by unwrapping iCKB tokens into Nervos DAO withdrawal cells:

  1. In the first transaction the user:
    • Requests the withdrawal from some protocol controlled deposits.
    • For that quantity, burns the exact corresponding amount of iCKB tokens and/or receipts.
  2. The second transaction is a Nervos DAO second withdrawal step.

As seen in iCKB/CKB Exchange Rate Calculation, for each deposit and receipt the equivalent amount of iCKB is well defined. The only difference is the incentive mechanism: requesting the withdrawal from an oversized deposit is incentivized by a 10% discount on the amount exceeding a standard deposit.

An additional current CKB constraint is that Nervos DAO deposit cells and phase 1 withdrawal cells must use lock scripts of the same serialized size. This is enforced at the node level by the DaoScriptSizeVerifier, which was added as a temporary mitigation for the Nervos DAO occupied-capacity vulnerability. The public Meepo hardfork note describes the same fix and credits phroi with identifying and reporting the issue. Since iCKB deposits use the empty-args iCKB Logic lock, this leaves little room to encode user-specific ownership directly in the withdrawal request lock. For this reason, when a dedicated owner/owned pairing is useful, Owned Owner Script can wrap a DAO withdrawal request together with a user-owned controller cell.

Summing up, when withdrawing, these rules must be followed:

  • The iCKB value of receipts and deposits is calculated as
iCKB_value(unoccupied_capacity, AR_m) {
    let s = unoccupied_capacity * AR_0 / AR_m;
    if s > standard_deposit_size {
        s = s - (s - standard_deposit_size) / 10
    }
    return s;
}

receipt_iCKB_value(deposit_quantity, deposit_amount, AR_m) {
    return deposit_quantity * iCKB_value(deposit_amount, AR_m);
}

deposit_iCKB_value(capacity, occupied_capacity, AR_m) {
    return iCKB_value(capacity - occupied_capacity, AR_m);
}
  • The total iCKB value of input tokens and input receipts must be equal to the total iCKB value of output tokens and input deposits being withdrawn.
  • The Withdrawal Request lock must have the same serialized size as the consumed deposit lock under the current CKB node-level DAO rule.
  • No more than 64 output cells are allowed under the currently deployed Nervos DAO script.
  • HeaderDeps must include the hash of the header of the on-chain block containing the deposits for each deposit being used to withdraw and each receipt being directly cashed out.
  • CellDeps must contain iCKB Dep Group comprising: iCKB Logic Script, Standard xUDT Script and Nervos DAO Script.

Example of withdrawal phase 1:

CellDeps:
    - iCKB Dep Group cell
    - ...
HeaderDeps: 
    - Deposit Block Header Hash
    - ...
Inputs:
    - Nervos DAO deposit cell with iCKB Logic Script:
        Data: 8 bytes filled with zeros
        Type: Nervos DAO
        Lock:
            CodeHash: iCKB Logic Hash
            HashType: Data1
            Args: Empty
    - Token:
        Data: amount (16 bytes)
        Type:
            CodeHash: Standard xUDT Script
            HashType: Data1
            Args: [iCKB Logic Script Hash, 0x00000080]
        Lock: A lock that identifies the user
    - ...
Outputs:
    - Nervos DAO phase 1 withdrawal cell:
        Data: Deposit cell's inclusion block number
        Type: Nervos DAO
        Lock: A user lock with the same serialized size as the deposit lock
    - ...

Multiple iCKB Actions in a TX

It's possible to include multiple actions from different phases in one transaction.

One transaction can include many actions from different iCKB phases. For example, a single transaction can consist of all the following actions:

  1. Create some new iCKB Deposits and their Receipt.
  2. Transform another Receipt into the xUDT iCKB token.
  3. Using the xUDT iCKB token to withdraw from some iCKB Deposits.

Ancillary Scripts

The iCKB protocol would be difficult to use without additional scripts. This section describes the L1 scripts that have been developed to address iCKB user needs.

These scripts offer solutions to specific lock needs, while supporting all user locks. The current iCKB deployment assumes whole-transaction-binding user locks, but delegated-signature and OTX-style integrations need extra care. For example, let's assume that:

  • The user lock is OTX-signature-based.
  • The user unlocks some cells with a signature in the first OTX transaction.

An attacker could do the following:

  • The attacker includes all other cells locked with the delegated user signature in a second OTX.
  • The attacker packs this second OTX together with the first OTX in the same transaction.
  • These second cells unlock through delegated signature validation.
  • The attacker gains control of this second group of cells.

This is the reason why these scripts are instead designed around a similar but safer pattern:

  • A single transaction mints both a controlled cell and a controller cell.
  • In a transaction, there may be multiple controlled cells and controller cells.
  • At minting time, one of the cells references the other using the signed relative index distance between them.
  • The controlled cell satisfies specific user needs.
  • The controlled cell uses the new script as lock
  • The controlled cell may have an updating method using the new script logic.
  • The controller cell has ownership of the controlled cell.
  • The controller cell uses the new script as type.
  • The controller cell has a lock that identifies the user.
  • Melting both cells in the same transaction is the only way to consume both cells.

Owned Owner Script

While the iCKB Logic Script is independent of the withdrawal request lock choice, a dedicated owner/owned pairing can still be useful. For this reason, the Owned Owner Script was developed. This script pairs DAO withdrawal requests, including but not limited to iCKB-origin withdrawals, with owner cells. In a transaction there may be multiple owned cells and owner cells. This script's lifecycle consists of two transactions: Mint and Melt.

Owner data molecule encoding:

array Int32            [byte; 4];

struct OwnedOwnerData {
    owned_distance:    Int32,
}

Mint Owned Owner

In the Mint transaction, the output contains:

  1. The owned cell with this script as lock.
  2. The owner cell with this script as type and a lock that identifies the user. This cell stores in its data the signed relative index distance between the owned cell and itself as a signed 32-bit integer encoded in little-endian.

Validation rule: owned_index == owner_index + owned_distance

Example of withdrawal phase 1 using Owned Owner:

CellDeps:
    - iCKB Dep Group cell
    - ...
HeaderDeps: 
    - Deposit Block Header Hash
    - ...
Inputs:
    - Nervos DAO deposit cell with iCKB Logic Script:
        Data: 8 bytes filled with zeros
        Type: Nervos DAO
        Lock:
            CodeHash: iCKB Logic Hash
            HashType: Data1
            Args: Empty
    - Token:
        Data: amount (16 bytes)
        Type:
            CodeHash: Standard xUDT Script
            HashType: Data1
            Args: [iCKB Logic Script Hash, 0x00000080]
        Lock: A lock that identifies the user
    - ...
Outputs:
    - Nervos DAO phase 1 withdrawal cell:
        Data: Deposit cell's inclusion block number
        Type: Nervos DAO
        Lock: Owned role
            CodeHash: Owned Owner Hash
            HashType: Data1
            Args: Empty
    - Owner cell:
        Data: Signed distance from Owned cell (4 bytes)
        Type: Owner role
            CodeHash: Owned Owner Hash
            HashType: Data1
            Args: Empty
        Lock: A lock that identifies the user
    - ...

Melt Owned Owner

In the Melt transaction, the input contains both the owned cell and the owner cell. If one of the two is missing, the script fails validation.

Example of withdrawal phase 2 using Owned Owner:

CellDeps:
    - iCKB Dep Group cell
    - ...
HeaderDeps: 
    - Deposit Block Header Hash
    - ...
Inputs:
    - Nervos DAO phase 1 withdrawal cell:
        Data: Deposit cell's inclusion block number
        Type: Nervos DAO
        Lock: Owned role
            CodeHash: Owned Owner Hash
            HashType: Data1
            Args: Empty
    - Owner cell:
        Data: Signed distance from Owned cell (4 bytes)
        Type: Owner role
            CodeHash: Owned Owner Hash
            HashType: Data1
            Args: Empty
        Lock: A lock that identifies the user
    - ...
Outputs:
    - ...

Limit Order Script

Interacting directly with the iCKB protocol has some limitations:

  • In transactions containing Nervos DAO script, no more than 64 output cells are allowed under the currently deployed Nervos DAO script.
  • iCKB Logic discourages deposits bigger or smaller than the standard deposit size.
  • There may be a mismatch between the amount the user wants to withdraw and the deposits available in the iCKB pool.
  • Nervos DAO doesn't allow to partially withdraw from a deposit.
  • There is no easy way to merge multiple user intentions within a single deposit or withdrawal.

To abstract over Nervos DAO and iCKB protocol limitations, the Limit Order Script implements limit order logic, abstracts user intentions, and can be matched partially or completely by anyone, similarly to an ACP lock. This lock aims to be compatible with all types that follow the sUDT convention:

  • Store the amount in the first 16 bytes of cell data.
  • Store no data in the witness.

Currently, this includes sUDT and xUDT without extension data. If a UDT needs to store data in the witness, then it should not be used in conjunction with the limit order script. In a transaction, there may be multiple order cells. This script's lifecycle consists of three kinds of transactions: Mint, Match and Melt.

Limit Order data molecule encoding:

array Uint8            [byte; 1];
array Uint32           [byte; 4];
array Uint64           [byte; 8];

array Int32            [byte; 4];
array Byte32           [byte; 32];

struct OutPoint {
    tx_hash:           Byte32,
    index:             Uint32,
}

struct Ratio {
    ckb_multiplier:    Uint64,
    udt_multiplier:    Uint64,
}

struct OrderInfo {
    ckb_to_udt:        Ratio,
    udt_to_ckb:        Ratio,
    ckb_min_match_log: Uint8,
}

struct MintOrderData {
    padding:           Byte32,
    master_distance:   Int32,
    order_info:        OrderInfo,
}

struct MatchOrderData {
    master_outpoint:   OutPoint,
    order_info:        OrderInfo,
}

union OrderData {
    MintOrderData,
    MatchOrderData,
}

Mint Limit Order

In Mint transactions, the output contains:

  1. The limit order cell itself with a UDT as type and this script as lock. In the cell data field, this lock stores the following information:

    • padding is used to achieve the same OrderData length for both variants.
    • master_distance expresses the signed relative index distance between this cell and the master cell.
    • ckb_to_udt expresses the order exchange ratio from CKB to UDT.
    • udt_to_ckb expresses the order exchange ratio from UDT to CKB
    • ckb_min_match_log expresses the logarithm in base 2 of the minimum partial match of the exchanged asset. The UDT minimum match is calculated using the udt_to_ckb ratio.
  2. The master cell with this script as type and a lock that identifies the user. This cell controls the limit order cell.

Validation rules:

  • Only the MintOrderData variant of OrderData is allowed for a newly minted order.
  • Minted order output index plus MintOrderData.master_distance must be equal to its master cell output index.
  • ckb_min_match_log valid values are from 0 to 64, inclusive.
  • Additional cell data is not allowed in order cells.

Example of Limit Order mint:

CellDeps:
    - iCKB Dep Group cell
    - ...
Inputs:
    - ...
Outputs:
    - Limit Order cell:
        Data:
            - Amount (16 bytes),
            - MintOrderData variant of OrderData
        Type: xUDT
        Lock: Limit Order role
            CodeHash: Limit Order Hash
            HashType: Data1
            Args: Empty
    - Master cell:
        Data: ...
        Type: Master role
            CodeHash: Limit Order Hash
            HashType: Data1
            Args: Empty
        Lock: A lock that identifies the user

Match Limit Order

In Match transactions, the allowed input limit order OrderData variants are MintOrderData and MatchOrderData. The only allowed output variant is MatchOrderData.

The only difference between MintOrderData and MatchOrderData is that padding and master_distance are discarded in favour of master_outpoint, which keeps track of the original master outpoint of the matched order.

Validation rules:

  • in_ckb * ckb_multiplier + in_udt * udt_multiplier <= out_ckb * ckb_multiplier + out_udt * udt_multiplier
  • in_wanted_asset + 2^log_min_match <= out_wanted_asset
  • An order already completely fulfilled cannot be matched.
  • Only the MatchOrderData variant of OrderData is allowed as the matched order output.
  • The implicit Master outpoint must be equal between the input and its matched output order:
    1. If input OrderData is the variant MintOrderData, then input order outpoint.tx_hash must be equal to its matched output order master_outpoint.tx_hash. Additionally, input order outpoint.index + master_distance must be equal to its matched output order master_outpoint.index.
    2. If input OrderData is the variant MatchOrderData, then master_outpoint must be equal between input and its matched output order.
  • ckb_to_udt, udt_to_ckb and ckb_min_match_log must be equal between input and its matched output order.
  • Additional cell data is not allowed in order cells.

Example of Limit Order Match:

CellDeps:
    - iCKB Dep Group cell
    - ...
Inputs:
    - Limit Order cell:
        Data:
            - Amount (16 bytes),
            - MintOrderData variant of OrderData
        Type: xUDT
        Lock: Limit Order role
            CodeHash: Limit Order Hash
            HashType: Data1
            Args: Empty
Outputs:
    - Limit Order cell:
        Data:
            - Amount (16 bytes),
            - MatchOrderData variant of OrderData
        Type: xUDT
        Lock: Limit Order role
            CodeHash: Limit Order Hash
            HashType: Data1
            Args: Empty

Melt Limit Order

In the Melt transaction, the input contains both the order cell and its master cell. If one of the two is missing, the script fails validation. Any limit OrderData variant is allowed as input.

Validation rules:

  • The implicit Master outpoint must be equal between the input order cell and its input master cell:
    1. If the order's OrderData is the variant MintOrderData, then the order's outpoint.tx_hash must be equal to its input master's outpoint.tx_hash. Additionally, the order's outpoint.index + master_distance must be equal to its master's outpoint.index.
    2. If the order's OrderData is the variant MatchOrderData, then the order's master_outpoint must be equal to its master's outpoint.

Example of Limit Order melt:

CellDeps:
    - iCKB Dep Group cell
    - ...
Inputs:
    - Limit Order cell:
        Data:
            - Amount (16 bytes),
            - MatchOrderData variant of OrderData
        Type: xUDT
        Lock: Limit Order role
            CodeHash: Limit Order Hash
            HashType: Data1
            Args: Empty
    - Master cell:
        Data: ...
        Type: Master role
            CodeHash: Limit Order Hash
            HashType: Data1
            Args: Empty
        Lock: A lock that identifies the user
Outputs:
    - ...

Multiple Limit Order Actions in a TX

It's possible to include multiple actions from different phases in one transaction.

One transaction can include many actions from different Limit Order phases. For example, a single transaction can consist of all the following actions:

  1. Mint many new Limit Orders.
  2. Match many existing Limit Orders.
  3. Melt many old Limit Orders.

Exit Under Low Liquidity

The deposit, withdrawal, and limit-order mechanisms above compose into an exit flow for low-liquidity cases. A user should first withdraw the best available combination of whole deposits, then only consider a lossy action for the remaining iCKB:

  • Whole-deposit withdrawal: withdraw the available deposit combination with the greatest total iCKB value that does not exceed the user's balance. In other words, choose deposits where sum(deposit_iCKB_value) <= iCKB_balance and maximize sum(deposit_iCKB_value).
  • Limit-order fallback: if no whole-deposit combination exactly matches the user's balance, offer the remaining iCKB as a limit order from iCKB to CKB. That order lets another participant satisfy the mismatch without creating a non-standard deposit.
  • Receipt top-up: if the order remains unmatched and the minimum and maximum deposit sizes permit it, a user or matcher creates one or more non-standard deposits. After the deposit block header is available, those receipts have exact iCKB values. A withdrawal can then consume the remaining iCKB and receipts together to request withdrawal from an available deposit.

For example, assume only standard 100,000 iCKB deposits are available and a user holds 170,000 iCKB:

  1. The user withdraws one 100,000 iCKB deposit first, leaving 70,000 iCKB.
  2. The user leaves the 70,000 iCKB remainder as a limit order from iCKB to CKB.
  3. If no matcher takes that order, the user creates a non-standard deposit intended to produce a receipt worth slightly more than 30,000 iCKB.
  4. After inclusion, the user combines the remaining 70,000 iCKB and 30,000 iCKB from that receipt to request withdrawal from another 100,000 iCKB deposit.

The 30,000 iCKB receipt share represents CKB already added to the pool by the new deposit. It balances the second 100,000 iCKB withdrawal; the recovered value comes from the old 70,000 iCKB. The unrecovered overhead is:

  • The 82 CKB occupied by the new non-standard deposit cell.
  • Transaction fees.

The user targets slightly above 30,000 iCKB. Any excess iCKB output from that withdrawal/conversion flow can be burned in a separate xUDT-only cleanup to reclaim the token cell's occupied CKB capacity when converting it would cost more than the dust is worth.

This path is a last resort. Small non-standard deposits lose proportionally more value to occupied capacity, and the receipt value cannot be known exactly before inclusion because it depends on the deposit block header.

Audit

This whitepaper and the iCKB Scripts code have been internally reviewed by individuals with deep experience in Nervos L1 and externally audited by the Scalebit team, an internationally recognized blockchain security team.

A later local executable review of the deployed release binaries was completed on 2026-05-01 and is available in the iCKB contracts repository as 20260501-ICKB-Audit-Report.md. That review covers iCKB Logic, Owned Owner, Limit Order, and the shared utils crate.

Unsigned Lock Witnesses Malleability

All the scripts presented in this whitepaper (iCKB Logic Script, Owned Owner Script and Limit Order Script) follow a novel pattern of using a script both as a lock in one cell and as a type in another cell. While the pattern allows great flexibility, it also comes with an implicit weakness: the cell that uses the script as lock doesn't rely on signature-based verification, so the witnesses in the same group (lock, input type and output type) can be modified by an attacker after the user signs. Credits to @XuJiandong for the discovery.

Rule of thumb: if a script in a transaction needs to store data in the witness and this data can be tampered with without making the transaction invalid, then this transaction must not employ the scripts presented in the current whitepaper.

This witness malleability does not affect the current iCKB use cases, because no freely tamperable data is ever stored in witnesses.

Confusion Attack on Limit Order

Due to the architectural design of Nervos L1, output locks are not executed during the transaction validation process. Consequently, an attacker may create a limit order that shares the same master cell as an already existing limit order. This situation may confuse front-end code when identifying the correct limit order. Users must exercise particular caution when melting their limit order and master cell, as selecting the incorrect limit order could result in the permanent locking of the funds associated with their original limit order. More details in the dedicated GitHub issue.

The most practical solution is to fetch the original Mint tx of the Limit Order (LO). With the Mint tx, it's possible to validate (on the front-end) that all the LO parameters are the same between the current LO and the initial Mint LO, but if there are still multiple LOs for the same Master Cell, an additional heuristic is required.

Directional LO Heuristic

Directional LOs do not increase in value; usually, they remain constant throughout their life cycle. They only increase in value when whoever matches them makes a mistake. Normally, once certain progress is reached (for example, the LO has been 50% matched), it is not reversible.

Qualifications for Directional LO:

  • LO has the same parameters as Mint LO
  • LO has at least the same value as Mint LO

Heuristic: if there are multiple LOs with the same Master cell, choose the LO with the best progress.

Counterintuitive property: a 100% matched LO (with value equal to Mint) is preferred over a 0% matched LO (with possibly greater value, but very likely forged by an attacker).

Dual-Sided LO Heuristic

Dual-Sided LOs (those with two ratios) can increase in value. They increase in value when whoever matches them goes back and forth between the two assets. So there is no concept of progress here. Any distribution of assets in the LO is not final and can be altered at any time.

Qualifications for Dual-Sided LO:

  • LO has the same parameters as Mint LO
  • LO has at least the same value as Mint LO

Heuristic: if there are multiple LOs with the same Master cell, choose the LO with the best value.

Implementation

The current stack-side mitigation lives in pinned @ickb/order code:

The current stack keeps the same front-end strategy in @ickb/order, but makes the selection rule explicit in one resolver.

  1. Fetch the original Mint LO for a given Master cell and treat it as the origin.
  2. Reject any candidate LO whose lock script, UDT type, resolved Master outpoint, or order parameters differ from the origin, or whose normalized value is lower than the origin. Here normalized value means the order value computed from unoccupied CKB and UDT with the order multipliers:
    • CKB -> UDT: ckb_unoccupied * ckb_to_udt.ckb_multiplier + udt_value * ckb_to_udt.udt_multiplier
    • UDT -> CKB: ckb_unoccupied * udt_to_ckb.ckb_multiplier + udt_value * udt_to_ckb.udt_multiplier
    • Dual-Sided LO: the stack compares the common-scale average of those two values as implemented by @ickb/order.
  3. For Directional LO, also reject any candidate whose progress is lower than the origin. Here progress means the amount already converted into the target asset, so it is monotonic and favors the real matched lineage over a larger but still unprogressed forgery.
  4. For Dual-Sided LO, there is no irreversible notion of progress, so the stack sets progress := normalized value. This reduces the same resolver to the Dual-Sided heuristic above: the LO with the best normalized value is chosen.

In the current stack, the directional progress scalar is computed from the asset that has already moved to the other side of the order:

  • CKB -> UDT: progress = udt_value * ckb_to_udt.udt_multiplier
  • UDT -> CKB: progress = ckb_unoccupied * udt_to_ckb.ckb_multiplier

This is why the same resolver can implement both heuristics without branching on a second selection algorithm: Directional LO rank by irreversible progress, while Dual-Sided LO rank by normalized value because for that shape progress == normalized value.

If multiple qualified candidates still tie on that primary score, the current stack applies one last tie-break: prefer a newly minted LO over a non-mint LO. In practice this means preferring a candidate that still carries the mint-relative Master reference over one that already points to an absolute Master outpoint. This tie-break is secondary only: it is consulted after the directional-progress or dual-sided-value comparison has already produced a tie. If distinct mint-origin outputs or distinct candidate LOs remain tied after that, the stack skips the group instead of selecting by indexer order.

This remains a best-effort client-side heuristic for immutable deployed behavior, not an on-chain proof that forged higher-progress descendants cannot exist. Consumers should use resolved OrderGroups from @ickb/order rather than hand-pairing order and master cells.

Non-Upgradable Deployment

From the start iCKB has been built in the open as a public good. As such iCKB scripts have been deployed in a non-upgradable way. Concretely, the deployed script references below use hash_type = data1: the scripts load code from cell_deps by exact cell data hash, so validation is pinned to the deployed binary bytes. By contrast, hash_type = type loads code by type-script hash, so the referenced code cell can be replaced by another cell with the same type script and different contents, with the replacement policy then governed by that code cell's lock script. This data1 reference mode is what makes the live deployment non-upgradable.

As a separate deployment detail, the published binary cells themselves are locked with a secp256k1_blake160 zero lock, an unspendable lock. This does not make the live script references upgradable, because under data1 the protocol points to the deployed bytes directly; it only means no trusted operator key remains embedded as an owner of the binary cells.

Additionally, the protocol uses the following dependency group:

  • iCKB Logic
  • Limit Order
  • Owned-Owner
  • xUDT
  • Secp256k1 Blake160
  • Nervos DAO
  • Secp256k1 Data
  • Secp256k1 Blake160 Multisig
parameter value
code_hash 0x2a8100ab5990fa055ab1b50891702e1e895c7bd1df6322cd725c1a6115873bd3
hash_type data1
tx_hash 0x621a6f38de3b9f453016780edac3b26bfcbfa3e2ecb47c2da275471a5d3ed165
index 0x0
dep_type depGroup
parameter value
code_hash 0x49dfb6afee5cc8ac4225aeea8cb8928b150caf3cd92fea33750683c74b13254a
hash_type data1
tx_hash 0x621a6f38de3b9f453016780edac3b26bfcbfa3e2ecb47c2da275471a5d3ed165
index 0x0
dep_type depGroup
parameter value
code_hash 0xacc79e07d107831feef4c70c9e683dac5644d5993b9cb106dca6e74baa381bd0
hash_type data1
tx_hash 0x621a6f38de3b9f453016780edac3b26bfcbfa3e2ecb47c2da275471a5d3ed165
index 0x0
dep_type depGroup

iCKB xUDT Mainnet

parameter value
name iCKB
symbol iCKB
decimals 8
metadata_tx_hash 0x490cd47d7491b8dcb74f22bd7607b176bf7dbe13d4cc9c2d0f50dc7208082f6d
script_hash 0xd485c2271949c232e3f5d46128336c716f90bcbf3cb278696083689fbbcd407a
code_hash 0x50bd8d6680b8b9cf98b73f3c08faf8b2a21914311954118ad6609be6e78a1b95
hash_type data1
args 0xb73b6ab39d79390c6de90a09c96b290c331baf1798ed6f97aed02590929734e800000080
tx_hash 0x621a6f38de3b9f453016780edac3b26bfcbfa3e2ecb47c2da275471a5d3ed165
index 0x0
dep_type depGroup
parameter value
code_hash 0x2a8100ab5990fa055ab1b50891702e1e895c7bd1df6322cd725c1a6115873bd3
hash_type data1
tx_hash 0xf7ece4fb33d8378344cab11fcd6a4c6f382fd4207ac921cf5821f30712dcd311
index 0x0
dep_type depGroup
parameter value
code_hash 0x49dfb6afee5cc8ac4225aeea8cb8928b150caf3cd92fea33750683c74b13254a
hash_type data1
tx_hash 0xf7ece4fb33d8378344cab11fcd6a4c6f382fd4207ac921cf5821f30712dcd311
index 0x0
dep_type depGroup
parameter value
code_hash 0xacc79e07d107831feef4c70c9e683dac5644d5993b9cb106dca6e74baa381bd0
hash_type data1
tx_hash 0xf7ece4fb33d8378344cab11fcd6a4c6f382fd4207ac921cf5821f30712dcd311
index 0x0
dep_type depGroup

iCKB xUDT Testnet

parameter value
name iCKB
symbol iCKB
decimals 8
metadata_tx_hash 0x8b33577d05662003f206fb2886c32810866dca51821f0db31e3a05b06a7e3bf9
script_hash 0xd485c2271949c232e3f5d46128336c716f90bcbf3cb278696083689fbbcd407a
code_hash 0x50bd8d6680b8b9cf98b73f3c08faf8b2a21914311954118ad6609be6e78a1b95
hash_type data1
args 0xb73b6ab39d79390c6de90a09c96b290c331baf1798ed6f97aed02590929734e800000080
tx_hash 0xf7ece4fb33d8378344cab11fcd6a4c6f382fd4207ac921cf5821f30712dcd311
index 0x0
dep_type depGroup

Future

At the inception of iCKB, the following were the intended possible use cases:

  • CKB-based Initial Stake Pool Offerings.
  • The official Nervos DAO community voting mechanism.
  • iCKB as a value-accruing asset for Nervos L2 and bridge applications.
  • A multitude more L1, L2, and bridge applications!

A few things have changed since inception. These are the updated use cases:

  • ISPO is a well-thought-out model, but Nervos currently already has a Community Fund, so the need has been addressed. Developing an ISPO now may provide different paths & rules for accessing funding.
  • iCKB can remain useful in Nervos L2 and bridge contexts because it keeps its value and continues accruing Nervos DAO interest while staying liquid.
  • In the short term the most impactful iCKB result will be that more users will feel comfortable staking into Nervos DAO by using iCKB. More CKB will be locked into Nervos DAO, which is an achievement in itself.
  • In the medium term more decentralized finance protocols will integrate iCKB. So users will be able to receive the interest paid out by both these protocols and Nervos DAO.
  • In the long term, more UTXO chains will be integrated with Nervos thanks to RGB++, and iCKB is ideally positioned to take advantage of this. Users from other chains will be able to receive the interest paid out by Nervos DAO while remaining liquid. Protocols from other chains will also be able to build on top of iCKB, so users will be able to receive the interest paid out by both those protocols and Nervos DAO.

Useful Resources

License

This whitepaper is distributed under MIT License.

About

iCKB whitepaper: a NervosDAO liquid staking token

Topics

Resources

License

Stars

Watchers

Forks

Contributors