iCKB journey into CoBuild

Yes, a proper OTX collector/processor can and should indeed do this.

1 Like

Yes, this PR is in progress. We will publish it soon when it’s ready.

2 Likes

Took a while, but I was able to bring the iCKB proposal up to date with designs are both L1 Classic and L1 CoBuild OTX compatible. CoBuild Actions are missing at the moment.

Highlights

Deposit receipt is now positional independent and it allows for multiple receipts in output.

Note: The receipt only memorizes deposit amount and quantity, but it doesn’t specify which NervosDAO deposits that is accounting. A possible future OTX design may be: the user unlocks the necessary funds & create a receipt in output, while the aggregator transforms these funds into the intended NervosDAO deposits. Then again, currently I see no benefit in this possible OTX design.

Withdrawal requests are now handled by the script Owned Owner. While iCKB Logic script is independent to the withdrawal request lock choice, this lock has some pretty restrictive constraints, as no information can be stored in its lock args nor in its cell data. For this reason has been developed Owned Owner Script which split the ownership of the withdrawal request into two cells:

  1. The owned cell with this script as lock and NervosDAO withdrawal request as type.
  2. The owner cell with this script as type and a lock that identifies the user.

To unlock both cells the input must contain both the owned cell and the owner cell.

Similarly to Owned Owner, a Limit Order is now a two cell design:

  1. The limit order cell with this script as lock and UDT as type.
  2. The master cell with this script as type and a lock that identifies the user.

While the limit order cell can be updated following its logic, to effectively consume both cells the input must contain both the owned cell and the owner cell.

Follows the updated proposal sections.

Deposit

In NervosDAO, a deposit is a single transaction in which a CKB holder locks his CKB in exchange for a NervosDAO receipt of that specific deposit.

In the proposed protocol, a deposit is the process in which a CKB holder locks his 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, then again 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 in two phases:

  1. In the first phase, the CKB holder locks his 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 NervosDAO deposit cells locked by iCKB Logic Script, in short a deposit.
  • Awards to the user a protocol receipt of the deposits, effectively wrapping them.

Given the impossibility to access the header in this phase, it cannot exist a strict requirement on deposits iCKB-equivalent size. On the other hand, to achieve higher deposits 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.

On the other side, deposits smaller than the standard deposit size are intrinsically disincentivized by L1 dynamics. As deposits gets smaller they incur a bigger penalty in form of unaccounted occupied capacity, up to 10% of the deposit. This translates to a minimum deposit of 820 CKB:

  • 82 CKB of fixed occupied capacity, used for state rent of the deposit cell with iCKB Logic Script.
  • 738 CKB of minimum unoccupied capacity, to be accounted by the receipt and later on converted in iCKB.

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

Since having a separate receipt per deposit cell would be capital inefficient, the protocol allows to account multiple deposit 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.

In a receipt cell data:

  • The first 4 bytes, currently zeroed, are reserved as unionID in case of future iCKB updates.
  • The single deposit unoccupied CKB capacity is stored in 6 bytes, this poses an hard-cap of ~2.8M CKB per single deposit. This hard-cap prevents a certain form of DoS, while still leaving enough slack for the standard deposit CKB size to grow for well over a hundred of years.
  • The quantity of deposits is stored in 2 bytes, this poses an hard-cap of 65535 deposits per receipt. On the other side for simplicity a transaction containing NervosDAO script is currently limited to 64 output cells so that processing is simplified. This limitation may be relaxed later on in a future NervosDAO script update.

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 Type ID, HashType: Type, Args: Empty}.
  • 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 Type ID, HashType: Type, Args: Empty}, the first 12 bytes of cell data are reserved for:
    • union_id all zero, it’s reserved for future updates to data encoding (4 bytes)
    • receipt_count keeps track of the quantity of deposits (2 bytes)
    • receipt_amount keeps track of the single deposit unoccupied capacity (6 bytes)
  • No more than 64 output cells are allowed, due to the current NervosDAO restriction.
  • CellDeps must contain iCKB Dep Group comprising of: iCKB Logic Script and Nervos DAO Script.

Receipt data molecule encoding:

array Uint16 <byte; 2>;
array Uint48 <byte; 6>;

struct ReceiptDataV0 {
    receipt_count: Uint16,
    receipt_amount: Uint48,
}

union ReceiptData {
    ReceiptDataV0,
}

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 Type ID
            HashType: Type
            Args: Empty
    - ...
    - Receipt:
        Data: ReceiptData
            union_id: All zero, reserved for future updates (4 bytes)
            receipt_count: Quantity of deposits (2 bytes)
            receipt_amount: Single deposit unoccupied capacity (6 bytes)
        Type:
            CodeHash: iCKB Logic Type ID
            HashType: Type
            Args: Empty
        Lock: A lock that identifies the user

Deposit Phase 2

A receipt accrues interests and it can be used to withdraw, but it’s not liquid nor transferrable.

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

As seen in iCKB/CKB Exchange Rate Calculation for each receipt the equivalent amount of iCKB is well defined. The only difference being the incentivization: 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(receipt_count, receipt_amount, AR_m) {
    return receipt_count * iCKB_value(receipt_amount, AR_m);
}
  • The total iCKB value of input tokens and input receipts must be bigger or equal to the total iCKB value of output tokens.
  • iCKB xUDT flags are set to 0x80000000 to enable xUDT owner mode by input type.
  • HeaderDeps must contain the transaction hash of the deposit block for each receipt.
  • CellDeps must contain iCKB Dep Group comprising of: iCKB Logic Script, Standard xUDT Script and Nervos DAO Script.

Example of deposit phase 2:

CellDeps:
    - iCKB Dep Group cell
    - ...
HeaderDeps: 
    - Deposit block
    - ...
Inputs:
    - Receipt:
        Data: ReceiptData
        Type:
            CodeHash: iCKB Logic Type ID
            HashType: Type
            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 Type ID, 0x80000000]
        Lock: A lock that identifies the user

Withdrawal

In NervosDAO time is slotted in batches of 180 epochs depending on the initial deposit timing, a withdrawal is split in two steps:

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

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

The proposed protocol instead proceed by un-wrapping iCKB tokens into NervosDAO withdrawal cells:

  1. In the first transaction the user:
    • Requests the withdrawal from some protocol controlled deposits.
    • Respectfully to that quantity, burns a bigger or equal 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 being the incentivization: requesting the withdrawal from an oversized deposit is incentivized by a 10% discount on the amount exceeding a standard deposit.

An additional NervosDAO constraint is that if deposit lock and withdrawal request lock differs, as in iCKB case, then NervosDAO requires the deposit lock and withdrawal request lock to have the same size. A non solution would be to use a lock with zero padded args in the deposit, then again different user locks would have different sizes, so it wouldn’t solve the problem at hand. While iCKB Logic script is independent to the withdrawal request lock choice, this lock has some pretty restrictive constraints, as no information can be stored in its lock args nor in its cell data. For this reason has been developed Owned Owner Script.

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(receipt_count, receipt_amount, AR_m) {
    return receipt_count * iCKB_value(receipt_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 bigger or equal to the total iCKB value of output tokens and input deposits, the deposits being withdrawn.
  • Withdrawal Request lock must have zero args length and no information stored in the data cell.
  • No more than 64 output cells are allowed, due to the current NervosDAO restriction.
  • HeaderDeps must contain the transaction hash of the deposit block for each deposit being used to withdraw and each receipt cashed out.
  • CellDeps must contain iCKB Dep Group comprising of: iCKB Logic Script, Standard xUDT Script and Nervos DAO Script.

Example of withdrawal phase 1:

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

Ancillary Scripts

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

These scripts offers solutions to specific lock needs, while supporting all users locks. The natural choice to prove user ownership would be to use the delegated signature validation pattern, then again given the incumbent OTX era this pattern has some specific OTX pitfalls. Let’s assume that:

  • The user lock is OTX signature based.
  • The user unlocks some cells with signature in an OTX transaction, first OTX.

An attacker could do the following:

  • Attacker includes all other cells locked with delegated user signature in a second OTX.
  • Attacker packs this second OTX together with the first OTX in the same transaction.
  • These second cells will unlock thanks to delegated signature validation.
  • 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 mint both a controlled cell and controller cell.
  • In a transaction there may be multiple controlled cell and controller cell.
  • At minting time one of the cells reference the other one using the signed relative index distance between each other.
  • 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 iCKB Logic script is independent to the withdrawal request lock choice, this lock has some pretty restrictive constraints, as no information can be stored in its lock args nor in its cell data. For this reason has been developed Owned Owner Script. In a transaction there may be multiple owned cells and owner cells. This script lifecycle consists of two transactions: Mint and Melt.

Owner data molecule encoding:

array Int32 <byte; 4>;

struct OwnerOwnedData {
    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 memorizes in 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 + signed_distance

Example of withdrawal phase 1 using Owned Owner:

CellDeps:
    - iCKB Dep Group cell
    - Owned Owner data cell
    - ...
HeaderDeps: 
    - Deposit block
    - ...
Inputs:
    - Nervos DAO deposit cell with iCKB Logic Script:
        Data: 8 bytes filled with zeros
        Type: Nervos DAO
        Lock:
            CodeHash: iCKB Logic Type ID
            HashType: Type
            Args: Empty
    - Token:
        Data: amount (16 bytes)
        Type:
            CodeHash: Standard xUDT Script
            HashType: Data1
            Args: [iCKB Logic Type ID, 0x80000000]
        Lock: A lock that identifies the user
    - ...
Outputs:
    - Nervos DAO phase 1 withdrawal cell:
        Data: Deposit cell's including 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 does’t validate.

Example of withdrawal phase 2 using Owned Owner:

CellDeps:
    - iCKB Dep Group cell
    - Owned Owner data cell
    - ...
HeaderDeps: 
    - Deposit block
    - ...
Inputs:
    - Nervos DAO phase 1 withdrawal cell:
        Data: Deposit cell's including 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 NervosDAO script, no more than 64 output cells are allowed.
  • 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.
  • NervosDAO 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 NervosDAO and iCKB protocol limitations, it has been created a lock that implements limit order logic, abstracting user intentions, and that anyone can match partially or fulfill completely, similarly to an ACP lock. This lock aims to be compatible with all types that follows the sUDT convention of storing the amount in the first 16 bytes of cell data, at the moment sUDT and xUDT. In a transaction there may be multiple orders cells. This script lifecycle consists of three kind of transactions: Mint, Match and Melt.

Limit Order Args molecule encoding:

array Hash <byte; 32>;
array Uint64 <byte; 8>;
array Uint32 <byte; 4>;
array Int32 <byte; 4>;

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

struct OrderInfo {
    is_udt_to _ckb: byte,
    ckb_multiplier: Uint64,
    udt_multiplier: Uint64,
    log_min_match: byte,
}

struct MintOrderArgs {
    orderInfo: OrderInfo,
    master_distance: Int32,
}

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

struct FulfillOrderArgs {
    master_outpoint: OutPoint,
}

union OrderArgs {
    MintOrderArgs,
    MatchOrderArgs,
    FulfillOrderArgs,
}

Mint Limit Order

In the Mint transaction, the output contains:

  1. The limit order cell itself with an UDT as type and this script as lock. This lock args memorizes:

    • is_udt_to _ckb expresses the order direction.
    • ckb_multiplier and udt_multiplier expresses the order exchange ratio.
    • log_min_match expresses the logarithm in base 2 of the minimum partial match of the wanted asset.
    • master_distance expresses the signed relative index distance between this cell and the master cell.
  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:

  • orders_index + master_distance == master_index
  • Order cell data length must be at least 16 bytes.

Example of Limit Order mint:

CellDeps:
    - Limit Order data cell
    - ...
Inputs:
    - ...
Outputs:
    - Limit Order cell:
        Data: [amount (16 bytes), ...]
        Type: UDT
        Lock: Limit Order role
            CodeHash: Limit Order Type ID
            HashType: Type
            Args: MintOrderArgs variant of OrderArgs
    - Master cell:
        Data: ...
        Lock: Master role
            CodeHash: Limit Order Type ID
            HashType: Type
            Args: Empty
        Lock: A lock that identifies the user

Match and Fulfill Limit Order

In Match and Fulfill transactions the allowed input limit OrderArgs variants are MintOrderArgs and MatchOrdersArgs. While the allowed output limit OrderArgs variants are MatchOrdersArgs and FulfillOrdersArgs.

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
  • excluding the first 16 bytes that encode the amount, the rest of data bytes must be equal between input and output order.
  • FulfillOrdersArgs is not allowed as input order.
  • MintOrderArgs is not allowed as output order.

Example of Limit Order Match:

CellDeps:
    - Limit Order data cell
    - ...
Inputs:
    - Limit Order cell:
        Data: [amount (16 bytes), ...]
        Type: UDT
        Lock: Limit Order role
            CodeHash: Limit Order Type ID
            HashType: Type
            Args: MintOrderArgs variant of OrderArgs
Outputs:
    - Limit Order cell:
        Data: [amount (16 bytes), ...]
        Type: UDT
        Lock: Limit Order role
            CodeHash: Limit Order Type ID
            HashType: Type
            Args: MatchOrderArgs variant of OrderArgs

Melt Limit Order

In the Melt transaction, the input contains both the order cell and the master cell. If one of the two is missing the script does’t validate. Any limit OrderArgs variant is allowed as input.

Example of Limit Order melt:

CellDeps:
    - Limit Order data cell
    - ...
Inputs:
    - Limit Order cell:
        Data: [amount (16 bytes), ...]
        Type: UDT
        Lock: Limit Order role
            CodeHash: Limit Order Type ID
            HashType: Type
            Args: FulfillOrdersArgs variant of OrderArgs
    - Master cell:
        Data: ...
        Lock: Master role
            CodeHash: Limit Order Type ID
            HashType: Type
            Args: Empty
        Lock: A lock that identifies the user
Outputs:
    - ...
4 Likes

Thanks for putting together a complete design! There are parts where I would personally design differently, but that is really an irrelevant question. What matters here, is that a complete design allows us to derive certain assumptions that are mis-communicated before:

  • The iCKB issued from each deposit, is tied to the commit block header of the deposited cell, hence 2-phase deposit is required(I must say I missed this part earlier)
  • The core of iCKB is only about issuing iCKB together from a DAO deposit, and then requiring burning iCKB when deposit cells are withdrawed. The limit order of iCKB for CKB, is really an add-on part, that can be done separately, possibly via a standalone DEX.

Those assumptions would indeed affect the design, and I agree with you now that a 2-phase deposit might be needed, and that OTX is not needed at least at the core part of iCKB.

Though I still have some questions, and if I were doing this design, I might do something different, I will include them here just as suggestions. Feel free to think about it and absorb/ignore/criticize them as you want.

Questions

OTX

I don’t really understand this part. A second OTX would require a separate signature signed by the user, so as to unlock cells from the original user. An attacker cannot just add a second OTX with the first OTX in the same transaction, hoping that the second OTX would succeed in validation. Each OTX will have clear boundaries, can you explain more on how this attack is performed?

What Is Owned Distance

This part really confuses me, what is owned distance here? To me Mint is just withdraw phase 1, and Melt is just withdraw phase 2.

And what is Owner cell used in Melt transaction?

Suggestions

Automating the conversion from Receipt to iCKB tokens

This is one part that I believe could use some optimization: right now a receipt cell uses a lock from the original user making the deposit, and the user himself/herself has to sign a different transaction to issue the iCKB tokens. But if you think about it, the included iCKB Logic Type ID type script of the receipt cell, already handles all verification work. So if I were designing the workflow, I would use an always-success lock as the lock of receipt cell created from deposit phase 1. Then a bot can handle the conversion from a receipt cell, to the actual iCKB tokens. Of course 2 additional points are needed:

  • ReceiptData will need to hold a lock script used as the lock of the final iCKB token cell
  • An incentive in the form of CKB is probably needed to be taken from the receipt cell so some bot would want to do the conversion for the user

A user can still manually do the conversion from a receipt cell to the actual iCKB tokens, but in most cases, bot will help take care of this process without any potential issues.

Simplifying scripts

I’m sure you might also want to clean this up when the actual development work started, still I want to point out some inconsistencies:

  • The DAO deposit step creates a DAO deposit cell using iCKB Logic Type ID as the lock
  • Withdrawal phase 1 consumes a DAO deposit cell using iCKB Logic Type ID as the lock, then creates a DAO withdrawing cell using Owned Owner Hash as the lock

To me, it might simply be simpler if DAO deposit cell simply use Owned Owner Hash as the lock script here. Which will result in a cleaner(to me) design:

  • iCKB Logic Type ID is only used as the type script of receipt cell, it validates the legit creation of receipt cell from DAO deposits into Owned Owner Hash, then validates the issuance of iCKB tokens from the consumption of receipt cell
  • Owned Owner Hash is only used as the lock script of DAO deposit/withdrawing cells, it validates the withdrawing of DAO deposits, together with the burning of enough iCKB tokens

You might want to revisit the names used here for the scripts, but to me this way we have a pair of scripts, each with clearly defined responsibility and a cleaner design. In general, I would not personally recommend using a single script both as lock and type. In theory it works, but in practice it would probably messes your head so chances are you might miss something.

And one more question here: I noticed iCKB Logic Type ID uses type as hash type, but Owned Owner Hash uses Data1 as hash type. What’s the rationale behind this? I would personally recommend to stick to one hash type choice across iCKB. And really don’t make any design that fixates on a particular hash type, this is really one field that should stay flexible for future changes.

Don’t Over Simplify Data Structures

I personally would not recommend using Uint16 and Uint48 in ReceiptData. I get it you want to have smaller cell data, but my past experience is that those excessive saving of bytes, will only incur more problems in toolings. If I were you, I would simply go with Uint64, even if you are only using a small fraction of Uint64. Yes contracts can be upgraded but it really is all those off-chain SDKs & toolings that worry me.

Limit Order Can Be Implemented In Different Ways

In this design here, Limit Order is just really a separate thing from iCKB. It really depends on the lock of the iCKB token cells to say what limit order design can be used here. You are essentially designing a small DEX here, the proposed solution works, we can also use other solutions like OTX, or any other solution. I would personally not consider this a part of iCKB, in fact I would not consider locking to a particular design. The choices here are really numerous, feel free to look at other DEX solutions in the ecosystem.

2 Likes

Hey @xxuejie, thank you for taking your time to read the updated proposal, I appreciate a lot :hugs:

If we assume that every script is CoBuild OTX, no such attack can occur.

Then again this assumption may fail in the following scenarios:

  1. In the future exists a second form of signature OTX with different rules and developer/user unwittingly mix and match CoBuild OTX and non-CoBuild OTX.

  2. The script who performs delegated signature validation is not CoBuild OTX aware and developer/user unwittingly uses a CoBuild OTX signature lock.

While 1 is what worries me in the ancillary scripts section, 2 is more realistic than one may expect. In the past to perform delegated signature validation a script would only need to check that the input contains a specific lock. Crucially sUDT and xUDT adopt this patter and they are not CoBuild OTX aware. Now, in the unlucky scenario that a developer/user chooses a CoBuild OTX signature lock as owner lock of his sparkling new UDT, then the attack can occur.

Let’s start by clarifying that Mint and Melt are context dependent. There are at least four different kinds of Mints:

  1. CKB → Receipt, so a Receipt mint (not named mint in the proposal)

  2. Receipt → iCKB xUDT, so a iCKB xUDT mint (just mentioned as verb in the proposal)

  3. An Owned Owner mint (as per section title)

  4. A Limit Order mint (as per section title)

Same goes with Melt. Maybe using instead synonyms would help, do you have suggestions for the synonyms?

My typo, sorry, owned_distance and signed_distance are the same thing. Let’s say the outputs of a TX/OTX are arranged like this:

  • index 0: Cell

  • index 1: Owned Cell

  • index 2: Cell

  • index 3: Cell

  • index 4: Owner Cell of 2 Owned Cell

  • index 5: Cell

  • index 6: Cell

The validation rule: owned_index == owner_index + signed_distance

In this case:

  • owned_index = 2

  • owner_index = 4

  • signed_distance = -2

What you propose is indeed doable and in the past I consider it, then again:

I’d like to point out that:

  • The old design adopted the same lock for deposits and withdrawal requests, this brought unnecessary complications, that’s why in this design I switched to xUDT.

  • Not inconsistent, intentional. A deposit cell is owned by the protocol, a withdrawal request cell is owned by a specific user. A user may use other locks too.

  • Splitting deposit lock and iCKB Logic Type ID is troublesome as it’s a circular dependency between the two scripts. So the deposit lock now has to memorize as Args the hash of the script of the other one. Using a script as both lock and type solves elegantly the issue.

Actually it’s one of the designs that I like most as it’s inherently safe and it avoids circular dependencies.

Agreed, to be honest I don’t like the name Owned Owner either, but it’s short and gives the idea. Do you have any suggestion?

Well spotted! Limit Order and iCKB Logic are meant to be upgradable, while Owned Owner Hash has a really simple function, so it is not meant to be upgradable. Then again yeah, I’m pretty neutral on this, I wouldn’t mind to switch to type for Owner Owned script :+1:

I agree, the non standard Uint48 may bring future tooling headaches to third parties, so it’s a good idea to change it to Uint64 :+1: (and limit the max within the script itself). Then again, any tooling that doesn’t support the standard Uint8, Uint16 or Uint32 has a long way to go.

Exactly!! :raised_hands:

Could you point me to existing alternatives?

Personally I think this is being over paranoid, since the issue is actually much bigger if we follow down this path:

CKB is a permissionless blockchain, meaning anyone is free to deploy any script on chain, and use it as the lock / type script of a cell. Such a script used, can really execute any logic as one wishes. This brings a question: in such a chaos world of CKB, how can we be sure a cell is secure?

We first define a set of conventions, then we develop scripts using this set of conventions, those scripts are then used as locks and types of cells. Finally, when a wallet / dapp generates a new transaction for user to sign, the wallet / dapp also follows this very convention. To ensure his / her cells are secure, a user is also required to ONLY use scripts that conform to the above conventions.

We can now reach a conclusion: it’s not that any CKB cell using any script is secure, only when everyone follows a clearly defined set of conventions, will everyone be sure that his / her cells are secure and can work in ways he / she wants. This is the KEY for building protocols on CKB.

We don’t have to look at cobuild OTX now, we can look at the sighash-all script included in genesis cell. This script does not define a rule of CKB consensus. CKB nodes will perfectly run without this script. Instead, it merely defines the following set of conventions:

  • A lock script guards the tx hash derived from current transaction
  • A lock script guards witnesses of its own script group
  • A lock script guards those witnesses that do not have matching input cells

This is a set of rules we defined to make a cell secure, but it really is only one set of all possible rules. It’s totally fine someone is building a different set of rules(for example, we can have a lock script that only limits and uses one witness) and it could also work on CKB. It’s just that we as a community choose to build more scripts / wallets / tools following conventions used by scripts in genesis cells.

And really, every wallet, every tool, every user has to recognize and conform to the above set of conventions, so as to build secure cells and transactions. Just like your example, if someone uses a different script using a different signature that do not conform to the above rules, we might have cells / transactions that are insecure, and this is really by design. It is not a risk. Developers / users have to work under a single set of conventions, so as to securely move cells on CKB.

Cobuild, with and without OTX, is also designed following the same philosophy: nothing we defined in cobuild is consensus rules, and theoretically one is free to either obey/disobey/partially-obey rules defined in the cobuild protocol. This is just what a permissionless blockchain really mean. But fundamentally, we can only be sure that cells / transactions will be secure when all the defined rules in the cobuild protocol are obeyed by scripts, wallets, tools and users. It’s just like any good law(some would want to argue all laws are bad but we will not go that far today): with defined responsibilities you can have rights, if you choose to misbehave, some rights might then be deprived from you.

So I would not worry now about a second form of OTX signature, if the newer form of OTX is compatible with the current one, they will be happily used together. If there are some conflicting parts, then they are not designed to be used together, anyone forcing them together will risk their cells / transactions being insecure, and this is totally expected in the CKB world.

Cobuild is designed with a lot of considerations in backward compatibility, when I say this sentence repeatedly, I really mean it. Yes current sUDT and xUDT are not cobuild OTX compatible, but that does not prevent you from using current versions of sUDT and xUDT in a cobuild OTX. sUDT and xUDT will continue to validate its rules in the full CKB L1 transaction. No validation rules will change here. In other words, you can have a cobuild OTX that receives 1000 sUDT and it is perfectly fine. It is just required that in the final assembled L1 CKB transaction, there will be other cells providing this 1000 sUDT.

There is work needed to make sUDT / xUDT script cobuild-compatible, but we won’t touch current validation rules. I believe there will only be an additional logic, that validates the cobuild action for sUDT / xUDT in each individual cobuild OTX. The core validation logic of sUDT / xUDT, stays the same, and will still be performed in the full L1 transaction.

So I don’t really see a scenario for attacks. Feel free to follow-up if you think otherwise.

Personally I don’t get the reasoning here. It’s just one step depositing and in the same time creating a receipt. I do feel like we are manually adding a second step for no apparent gain.

And see above discussion, I think you don’t need to worry about script with slight different rules. Yes a user might use any different locks but really there is just one type script controlled by iCKB which is considered to be valid receipt.

I guess this is another thing that fundamentally comes down to personal preference. I do respect that you have your own likes and dislikes, but do want to point out one inconvenience: right now deposit cell and withdrawing cell must use lock of the same size, in your current design, iCKB Logic Type ID has no script args, but Owned Owner Hash do have script args so obviously they will have different script size. If I were doing this I would use the same script as lock for deposit cell and withdrawing cell, then I can always read the cell data to tell the different between 2 kinds of cells. But in the end the decision is yours.

Again this is depending on personal viewpoint, I will just point it out and leave the decision to you.

Personally I don’t really see the circular dependency, care to weigh in?

The only thing I would recommend, is sticking to one decision across your app. Yes Owned Owner Hash seems simple for now, but you might never now one year from now.

One more thing: I will need to read more before I can comment on the distance part, seems pretty odd to me.

2 Likes

I don’t mind being over-paranoid if it can even marginally avoid issues down the line. That’s also the reason why instead of rushing to complete iCKB and moving on, I’m here kindly asking your valued opinion :pray:

You know, a couple years ago, when I first heard about OTX I started worrying about how an sUDT OTX Owner Lock would be able to handle it safely. I’m reassured that (while this particular about UDT was not mentioned in the proposal) you have a plan for this! :+1::+1:

As for the technical implementation, if I have to guess, you’ll require for UDT owners at minting time to include the additional CoBuild OTX logic. Failing to do so may render the CoBuild OTX unsecure, but it will be their fault.

Same goes for any script that cannot be upgraded and that currently uses delegated validation, right?

Actually I have a question about Omnilock delegated signature handling. Maybe there was some misunderstanding, but from this Omnilock CoBuild OTX issue seems that only a single cell with matching signature script is required in the the fully assembled Transaction to validate all OTX using that delegated signature validation. Please, could you tell me how the attack proposed in the issue is handled?

I mean, it’s a valid idea, but this additional logic don’t have to reside in the main iCKB Logic, it can reside in a separate Lock (instead of Always Successful).

Yup yup, I considered this and I thought: Well, we can just deploy a V1 script, without touching V0 right away and eventually consuming the v0 cell when there are no on-chain cells referencing it. Then again, as you suggested, using a reference by Type ID is just more straight forward and it doesn’t compromise more from the security standpoint! :+1::+1:

Owned Owner has zero len args in the lock of the withdrawal request, otherwise as you pointed out, not only it would be inconvenient, it would not validate at all. As documented in the proposal, the trick is that it stores User lock information in a secondary cell.

Another completely different way to go around the zero length args issue is to create a brand new script that does exactly two things in its code:

  • Stores/hardcodes a particular user public key.

  • Invokes/delegates the verification (dynlib, exec or spawn).

Generally we can design a signature-based lock in a way that:

  • It hardcodes the information about the user public key in the script code itself (so every User has to first deploy this script hardcoding a public key that he controls)

  • It minimizes the code state rent by relying on already deployed library cells for signature validation logic.

  • It can be referenced in a lock with empty args, so it can be used instead of the Owned Owner Script.

  • It could be referenced by Type ID, so the code of the script itself could be updated. (This open interesting possibilities for automated Bots that are otherwise unable to store securely their private keys. This way they can use a session based private key)

Given all this reasoning, we could keep the zero length args, drop Owned Owner Lock and use this class of scripts, which also work nicely with automated Bots. In your opinion, would this improve the overall iCKB design?

Yes yes, of course!! :hugs: In iCKB protocol we have three entities: iCKB xUDT, Receipt Script and Deposit Script. In particular:

  • iCKB xUDT depends on Receipt Logic (by using xUDT args, ok)

  • Receipt Script depends on iCKB xUDT Hash (!) and Deposit Script Hash (!)

  • Deposit Script depends on iCKB xUDT Hash (!) and Receipt Script Hash (!)

Considerations:

  1. xUDT hash is fixed, so its hash can be hardcoded into the scripts.

  2. Given xUDT hash, if we know Receipt Script hash, then we know iCKB xUDT hash.

So it boils down to the following circular dependency:

  • Receipt Script depends on Deposit Script Hash (!)

  • Deposit Script depends on Receipt Script Hash (!)

We can break this circular dependency by either:

  1. Using the proposed design (so a single script used both as lock and type).

  2. Memorizing one script hash as args (so same approach as xUDT).

  3. Deploy both scripts as type ID and update them until they reference each other. (This option didn’t existed when I first considered this design as then I was fully committed to reference by data).

  4. Other more convoluted ways like using external type ID cells with this data…

If I understand correctly, the design you propose follows 2, right? Could you explain again the advantages it has over 1?

If it’s about fixing the size of withdrawal receipt args, even with 1 we could just pad the deposit lock args with zeros…

Which brings the question, do we know the exact size of the withdrawal request lock args length?

Not really, but we could fix it to a reasonable non-zero value, just we must keep in mind that:

  • We are basically wasting some CKB for state rent in Deposits (around 20%-30% increase in state rent)

  • We are giving a preferential treatment to some user locks respectfully to others (still different from the protocol being tied up to a singular external lock)

Personally I don’t consider either of them deal breakers, so let’s examine this path!

For example, we could target base Omnilock args length, so args length could be 21 bytes. Omnilock is a nice choice as it offers both direct signature unlocking and delegated signature unlocking (in case both user wallet does not support directly Omnilock and user wallet lock args length is not 21). Also it would be CoBuild ready for free.

Given all this reasoning, we could pad deposit args with 21 bytes, so that withdrawal requests can directly use Omnilock. In your opinion, would this improve the overall iCKB design?