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:
- The owned cell with this script as lock and NervosDAO withdrawal request as type.
- 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:
- The limit order cell with this script as lock and UDT as type.
- 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:
- In the first phase, the CKB holder locks his CKB in exchange for a protocol receipt of the specific amount deposited.
- 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:
- In the first transaction the user requests the withdrawal.
- 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:
- 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.
- 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:
- The owned cell with this script as lock.
- 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:
-
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.
-
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:
- ...