I must say that I still don’t see a detailed design here. We don’t need a fully compete proposal to start talking, but I would personally expect something like the spore examples put together, so you can see what a complete CKB transaction with iCKB interactions might be. Unfortunately what we have here is just a snippet of a small data structure, I must say I still don’t have a picture on the full iCKB design to comment if it is viable.
Instead, I’m gonna do a slightly different way: I will put together a small design if I’m doing a liquidable Nervos DAO UDT in the context of cobuild. I will first list a few assumptions I will use in my UDT, then come up with a design from the assumptions. It’s possible that my assumptions here will be different from iCKB’s assumptions, hence leading to different designs. But with the information I have at hand, this is the best I can do.
So without further ado, here are the assumptions I will use:
- A special UDT token will be introduced. I’m gonna name it xDAOCKB. xDAOCKB can be minted when a Nervos DAO deposit into a special lock address is included in the same CKB transaction. One can only mint as many as xDAOCKB as the DAO deposit.
- Smaller deposits and bigger withdraws are supported by xDAOCKB, but as we shall see later, they will be implemented via Cobuild OTXs, not as Nervos DAO deposits / withdraws directly.
- xDAOCKB can be freely transferred to any cobuild-compatible accounts(this has a potential BIG issue we will address later).
- Any xDAOCKB owners can put up limit orders selling xDAOCKB in exchange for (could be slightly smaller than typical DAO withdrawals to give liquidity provider incentives) corresponding CKBs with some interests. A limit order will also be in the form of Cobuild OTX.
- A liquidity providers can fully / partially fulfill limit orders in the form of Cobuild OTX, buying and burning xDAOCKBs from limit orders, providing limit order creators with specified CKBs, and initialize CKB withdraw requests in the same CKB transaction.
I personally believe the above assumptions already make a decent liquidable Nervos DAO app but if you have a specific requirement you want in mind, feel free to add via more comments.
And there is also one big issue with xDAOCKB that could potential lead to drastically different design: not all xDAOCKB tokens are created equal. Assuming that 1000 xDAOCKBs are issued at block 10000 with DAO deposit cell A, but 2000 xDAOCKBs are then issued at block 20000 with DAO deposit cell B, what if people combine the xDAOCKBs issued at block 20000 with DAO deposit cell A for withdrawals? This would create an unfair situation. Here I’m gonna talk about 2 different designs, they all have their own pros & cons. Different people would favor different one, but as protocol designers, I would simply list both designs without preferences.
- We add a validation rule to xDAOCKB: the creation order of Nervos DAO deposit cells will be maintained by xDAOCKB, so that a liquidity provider can only provide the earliest Nervos DAO deposit cell to withdraw from. The market will then adjust the limit order price, so a reasonable balance between Nervos DAO interest and the buy-out price can be reached.
- Take inspiration from Bitcoin Inscription and Spore: what if we treat each issuance of xDAOCKB as a NFT, not UDTs? Different xDAOCKB are issued at different time for different Nervos DAO deposits. What if we treat the whole issuance of xDAOCKB as an NFT, and only allowing the transfer of a batch of xDAOCKBs as a group? That will essentially simply withdrawing by a large margin.
Again, let me stress the point that it is not my position nor my interest to debate which of the above 2 designs are a better one, I’m just saying I see 2 solutions here, both work from a technical perspective, and I will explain both here.
Ordering withdrawals
Let’s first talk about the case where we can treat xDAOCKB as true UDTs and can be freely transferred in any amount.
For xDAOCKB to work on chain, there are 3 different types of cells involved:
- xDAOCKB entity cell: a unique(could be ensured via type id) entity cell exists on chain for a particular deployment of the xDAOCKB app. It contains certain metadata information(such as the minimal CKBytes per Nervos CKB deposit batch), or bookkeeping information(all Nervos DAO deposits into xDAOCKB app). This cell will use an always-success script as the lock script, and a xDAOCKB app script as the type script. Note that xDAOCKB app script does the majority of the validation work of xDAOCKB.
- xDAOCKB asset cell: a series of cells used to hold CKBytes deposited into xDAOCKB app. A xDAOCKB app might have a series of xDAOCKB asset cells depending on actual usage. A xDAOCKB asset cell will use a xDAOCKB ensuring script as the lock script, and the Nervos DAO type script as the type script. The xDAOCKB ensuring script only does one thing: it validates that current CKB transaction has a cell using xDAOCKB app script as the type script
- xDAOCKB cell: this just refers to cells containing xDAOCKB UDTs. Per UDT’s design, a xDAOCKB cell can use any lock script as the lock, it uses xDAOCKB UDT script as the type script. There can be as many xDAOCKB cells as possible on chain, one is also free to split xDAOCKB cell into multiple xDAOCKB cells each holding a portion of xDAOCKB UDTs. I believe we can simply use xDAOCKB app script as the owner lock for xDAOCKB UDT script.
Deposit OTX
Assuming Alice wants to deposit 1100 CKB into xDAOCKB app, Alice can create an OTX like the following:
inputs:
input 0:
capacity: 5000 CKB
lock: Any OTX-compatible lock of Alice's (let's call it lock A)
type: <EMPTY>
data: <EMPTY>
outputs:
output 0:
capacity: 3499.99 CKB
lock: Any lock of Alice's
type: <EMPTY>
data: <EMPTY>
output 1:
capacity: 400 CKB
lock: Any lock of Alice's
type: xDAOCKB UDT script
data: 1100 xDAOCKB
witnesses:
witness 0: WitnessLayout format, Otx variant
seals:
0: Seal format
script_hash: lock A's hash
seal: signature for lock A
input_cells: 1
output_cells: 2
message: Message format
actions:
0: Action format
script_info_hash: xDAOCKB UDT ScriptInfo's hash
script_hash: xDAOCKB UDT script's hash
data: cobuild message for issuing 1100 xDAOCKB
1: Action format
script_info_hash: xDAOCKB app ScriptInfo's hash
script_hash: xDAOCKB app script's hash
data: cobuild message for depositing 1100 CKB
Several interesting points of this OTX:
- It does not use Nervos DAO script at all. There is no deposit cells. There is only a cell for issuing 1100 xDAOCKB. So there is no concern about Nervos DAO’s script limits, a user can deposit any amount of CKBytes as he / she wants.
- The user is free to use any lock as he / she wants. The input cell must use an OTX-compatible lock since this is an otx, the output cells are free to use any lock, even non-OTX-compatible ones.
- The user is in charge of providing capacity cost for the xDAOCKB cells(in reality this can change), the user also provides 0.01 CKBytes as incentives for OTX processors to process this OTX
- A signature is included in
seals
to ensure no one can alter this OTX
- The cobuild message part has 2 Action objects: one for issuing UDT script, one containing the action for xDAOCKB app
Merging Deposit OTXs into CKB Tx
With a few gathered deposit OTXs, an OTX processor can then merge then into one CKB transaction, creating a single Nervos DAO deposit request:
inputs:
input 0(otx #0):
capacity: 5000 CKB
lock: Any OTX-compatible lock of Alice's (let's call it lock A)
type: <EMPTY>
data: <EMPTY>
input 1(otx #1):
capacity: 3000 CKB
lock: Any OTX-compatible lock of Bob's (let's call it lock B)
type: <EMPTY>
data: <EMPTY>
input 2:
capacity: xDAOCKB app capacity
lock: always-success lock
type: xDAOCKB app script
data: xDAOCKB app data
outputs:
output 0(otx #0):
capacity: 3499.99 CKB
lock: Any lock of Alice's
type: <EMPTY>
data: <EMPTY>
output 1(otx #0):
capacity: 400 CKB
lock: Any lock of Alice's
type: xDAOCKB UDT script
data: 1100 xDAOCKB
output 2(otx #1):
capacity: 499.99 CKB
lock: Any lock of Bob's
type: xDAOCKB UDT script
data: 2500 xDAOCKB
output 3:
capacity: 3600 CKB
lock: xDAOCKB ensuring script
type: Nervos DAO script
data: Nervos DAO data
output 4:
capacity: updated xDAOCKB app capacity
lock: always-success lock
type: xDAOCKB app script
data: updated xDAOCKB app data
witnesses:
witness 0: Witness for Otx #0
witness 1: Witness for Otx #1
This CKB transaction merges 2 OTXs together: Alice deposits 1100 CKB to xDAOCKB via OTX #0, Bob deposits 2500 CKB to xDAOCKB via OTX #1. But as the CKB Tx shows, no matter how many deposit OTXs there are, there is only one Nervos DAO deposit cell created(output 3 above), it combines all the individual deposits from each OTX together into a single Nervos DAO deposits. xDAOCKB entity cell(input 2 & output 4) is also included in the CKB transaction, so xDAOCKB app script can perform all the checking needed, and update all the bookkeeping information(current Nervos DAO deposit information must also be included in xDAOCKB app data for maintaining deposit ordering). Also sice xDAOCKB app script is included in current Tx, xDAOCKB ensuring script can succeed in the execution. xDAOCKB UDT script can also successfully issuing xDAOCKBs to Alice and Bob.
Withdraw OTX via a limit order
With xDAOCKB, there is no direct withdrawing from Nervos DAO, instead a withdraw request is handled as a limit order:
inputs:
input 0:
capacity: 400 CKB
lock: Any OTX-compatible lock of Alice's (let's call it lock A)
type: xDAOCKB UDT script
data: 1100 xDAOCKB
outputs:
witnesses:
witness 0: WitnessLayout format, Otx variant
seals:
0: Seal format
script_hash: lock A's hash
seal: signature for lock A
input_cells: 1
output_cells: 0
message: Message format
actions:
0: Action format
script_info_hash: xDAOCKB UDT ScriptInfo's hash
script_hash: xDAOCKB UDT script's hash
data: cobuild message for burning (at most) 1100 xDAOCKB
1: Action format
script_info_hash: xDAOCKB app ScriptInfo's hash
script_hash: xDAOCKB app script's hash
data: cobuild message for withdrawing limit order
ask_interest: 0.00005 CKB
partial_fulfill_enabled: true
Like the deposit OTX, the withdraw OTX does not contain any Nervos DAO cells. It merely provides an input cell providing xDAOCKB, and an Action object(at action #1) containing limit order information. Of course different app would want to use different parameter formats as limit order information, here I merely include the absolutely necessary part:
ask_interest
: for each xDAOCKB sold by the limit order, we ask for 1.00005 CKB
partial_fulfill_enabled
: whether you can partially fulfill the order, or you can only fulfill the complete order.
The total tradable xDAOCKB, is thus read from the OTX itself. In the above OTX, Alice wants to trade at most 1100 xDAOCKB for CKB. But if Alice only wants to trade less xDAOCKB, a slightly different OTX can be used:
inputs:
input 0:
capacity: 400 CKB
lock: Any OTX-compatible lock of Alice's (let's call it lock A)
type: xDAOCKB UDT script
data: 1100 xDAOCKB
outputs:
capacity: 400 CKB
lock: Any lock of Alice's
type: xDAOCKB UDT script
data: 600 xDAOCKB
witnesses:
witness 0: WitnessLayout format, Otx variant
seals:
0: Seal format
script_hash: lock A's hash
seal: signature for lock A
input_cells: 1
output_cells: 1
message: Message format
actions:
0: Action format
script_info_hash: xDAOCKB UDT ScriptInfo's hash
script_hash: xDAOCKB UDT script's hash
data: cobuild message for burning (at most) 1100 xDAOCKB
1: Action format
script_info_hash: xDAOCKB app ScriptInfo's hash
script_hash: xDAOCKB app script's hash
data: cobuild message for withdrawing limit order
ask_interest: 0.00005 CKB
partial_fulfill_enabled: true
In this OTX, Alice only wants to trade 500 xDAOCKB.
Merging Withdraw OTXs into CKB Tx
In xDAOCKB, Liquidity providers act as OTX processors, they monitor withdraw OTXs submitted by users, and when they see orders they want to fulfill, they can merge those withdraw OTXs into a proper CKB Tx, like the following:
inputs:
input 0(otx #0):
capacity: 400 CKB
lock: Any OTX-compatible lock of Alice's (let's call it lock A)
type: xDAOCKB UDT script
data: 1100 xDAOCKB
input 1(otx #1):
capacity: 400 CKB
lock: Any OTX-compatible lock of Bob's (let's call it lock B)
type: xDAOCKB UDT script
data: 3000 xDAOCKB
input 2:
capacity: 10000 CKB
lock: xDAOCKB ensuring script
type: Nervos DAO type script
data: Nervos DAO data
input 3:
capacity: xDAOCKB app capacity
lock: always-success lock
type: xDAOCKB app script
data: xDAOCKB app data
any required cells from LP to make CKBytes balanced
outputs:
output 0(otx #1):
capacity: 400 CKB
lock: Any lock of Bob's
type: xDAOCKB UDT script
data: 1000 xDAOCKB
output 1(fulfilling otx #0):
capacity: 1100.1 CKB
lock: Recipient lock specified by Alice
type: <EMPTY>
data: <EMPTY>
output 2(partially fulfilling otx #1):
capacity: 1000.1 CKB
lock: Recipient lock specified by Bob
type: xDAOCKB UDT script
data: 1000 xDAOCKB
output 3:
capacity: 7900 CKB
lock: xDAOCKB ensuring script
type: Nervos DAO type script
data: Nervos DAO data
output 4:
capacity: updated xDAOCKB app capacity
lock: always-success lock
type: xDAOCKB app script
data: updated xDAOCKB app data
output 5:
capacity: 10000 CKB
lock: Any lock of LP's
type: Nervos DAO type script
data: Nervos DAO data for withdrawing cell
any changed cells to LP
In the above transaction, we have completely fulfill OTX #0(1100.1 CKB for 1000 xDAOCKB), while partially fulfill OTX #1(1000.1 CKB for 1000 xDAOCKB, while Bob wants to trade for 2000 xDAOCKB). Even though there are multiple limit orders, we are only doing withdrawing from one Nervos DAO deposit cell(input 2). For security reason, if burned xDAOCKB is less than CKBytes in withdrawed Nervos DAO cell, a new Nervos DAO deposit cell must be created(output 4 here). xDAOCKB entity cell is also included(input 3 & output 4) which does the bulk validation and bookkeeping work.
Recall the previous dicussion that xDAOCKB issued at different time is not equal, some might be issued from DAO deposit cells that exist longer than others, hence bearing more interested. To prevent attackers from exploiting this behavior, xDAOCKB entity cell will validate that the withdrawed Nervos DAO cell(input 2) is the oldest DAO deposit cell for current xDAOCKB app.
Apart from displayed cells, above, an LP might need to include other cells in the transaction as well, so as to provide CKBytes for fulfilled limit orders, as well as L1 transaction fees. The LP can then claim the withdrawed Nervos DAO cell(output 5), and gather the locked CKBytes with interests after a certain lock period.
Till this point, we’ve designed a liquidable Nervos DAO app that supports small & large deposits / withdraws, is not affected by Nervos DAO’s script limitation, and also supports complete / partial fulfillment of limit orders. Such an app is also cobuild-compatible, and is designed to leverage cobuild’s protocol from ground up.
NFT-style xDAOCKB
NFT-style xDAOCKB is similar to the above, but would be different in certain points:
- The issued xDAOCKB must keep the block number of the DAO deposit cell, in its cell’s data storage. This can be done in several ways:
- A 2-phase design can be used: a first transaction deposits CKBytes into Nervos DAO, and a follow-up transaction issues the xDAOCKB tokens
- A proximation can also be used: we can set the since value of one input in the transaction, then use block number / epoch number contained in since value as the block number used for issuing xDAOCKB tokens, this would ensure that the DAO deposit cell can only be committed at or after the since value. Also providing security guarentees.
- A cell containing xDAOCKB can only be tranferred as a single unit, there is no way to break an xDAOCKB cell into multiple cells, each containing a part of xDAOCKB tokens
- There is no need to maintain the order of deposited DAO cells in xDAOCKB entity cell
- When building withdrawing CKB transaction, xDAOCKB app script must check that for each consumed xDAOCKB cell, the included block number in xDAOCKB cell, must be no bigger than the block number where withdrawing DAO cell is committed.
This way we are limiting xDAOCKB cells so they can only earn interests starting from the point they are first issued, but not before that particular time point.
Recap
Here we have shown that how to build a liquidable Nervos DAO app in the context of cobuild protocol. What’s best about this protocol, is that we are not hardcoding against any particular lock script, one is free to use any lock to interact with xDAOCKB, such as secp256k1-sighash-all included in genesis block, omnilock, Unipass, dotbit(I’m not 100% on the dotbit part, since last time I checked dotbit still uses a lot of type scripts, and this app requires a wallet to only use lock script), Joy ID, etc. Hopefully this can shed some light on future CKB app development.