The concept of “Action” was introduced in the Cobuild protocol to express one or more operations that a CKB transaction intends to perform. In the actual contract implementation, it is also recommended that a type script should ensure that the corresponding Cobuild action must exist, in order to reduce the workload required in external wallets / tools to parse transactions.
Considering that Cobuild is a newly introduced concept, people’s understanding of Cobuild actions may be limited to simple textual descriptions. Currently, the Spore contract provides definitions of Cobuild actions and corresponding verification implementations. However, due to the design and implementation requirements of the Spore contract itself, the code related to Cobuild actions in Spore has strong limitations and may not serve as a good example to explore various possible designs of Cobuild actions.
Therefore, this article aims to showcase, through the design of Cobuild actions in the Automated Market Maker (AMM) scenario, that apart from the typical transfer/mint/burn operations, Cobuild actions can also be applied to a wider range of scenarios, supporting various design requirements.
When Cobuild OTX meets AMM
In the current framework, we have chosen to design the OTX structure based on cells rather than a single Cobuild action. Even though Message already expresses the intended action, in the actual OTX signing process, we still sign both the Message and the OTX structure expressed as cells together to ensure that neither the Message nor any cell in the OTX can be tampered with. However, such design choices can bring some challenges in certain scenarios:
- When transactions are finally executed on Layer 1 (L1) blockchain, certain data operations, such as the result of an AMM swap, can only be determined at that point.
- In scenarios where certain contract executions outside of the OTX are necessary to determine security, such as Alice using a protocol similar to Aave to make a deposit, the update of metadata within Aave itself needs to be confirmed. (Note: This is just one possible method of implementing Aave on CKB; in reality, it may simply involve storing Alice’s deposit in a specific lock.)
For these scenarios, we will introduce a special design pattern to solve the problem, which we call approved action.
It’s important to note that depending on different perspectives, “approved action” may not be the most ideal name. “Approved action” originally stems from the “approve” operation in ERC-20, hence why we named it as such. However, it’s evident from the discussion below that the design of approved action is significantly different from the “approve” operation in ERC-20. Therefore, in the context of Cobuild, it might be more appropriate to choose a different name. For now, let’s refer to it as “approved action.”
AMM Entity & Asset Cell
Here, we’ll use an AMM swap as an example. In an AMM, it’s necessary to maintain the current quantities of the two currencies involved in a trade within the pool. Here, we’ll use an “entity cell” (we will have a subsequent article discussing entity cells in detail) to represent a specific trading pair:
lock: <always success script>
type: <AMM entity type script>
data: <UDT 1 script hash> <UDT 1 amount> <UDT 2 script hash> <UDT 2 amount>
For the sake of simplifying the discussion, we have defined the following rules:
- An AMM entity cell handles transactions for only one trading pair.
- The basic information required for the trading pair is stored flatly in the cell data.
In reality, an AMM app might be far more complex: an AMM entity cell might be used to store data for multiple trading pairs, and each involved trading pair might have more metadata stored in a more complex data structure. However, our simplified definition of the entity cell is sufficient for the current discussion.
With the above definition of the AMM entity cell, there can be two or more AMM asset cells used to store the actual assets corresponding to a trading pair:
lock: <AMM asset lock script>
type: <UDT 1 type script>
data: <UDT 1 amount>
lock: <AMM asset lock script>
type: <UDT 2 type script>
data: <UDT 2 amount>
Typically, the AMM asset lock script here only needs to check whether the current CKB transaction includes an input/output cell that uses an AMM entity type script as its type script. In practice, all verification logic for such an AMM operation is handled by the AMM entity type script.
AMM OTX
Then we consider how to construct a Cobuild OTX for an AMM swap. Suppose Alice wants to exchange 1000 USDC for USDT with a maximum acceptable slippage of 2%. This means Alice can accept 1 USDT being exchanged for 0.98 USDC(of course, different AMM implementations may have different ways of defining slippage calculations. Here, we use the simplest and most intuitive definition). Following the most direct approach, we can only construct a preliminary OTX:
inputs:
input 0:
capacity: 200 CKB
lock: Joy ID lock Alice
type: USDC UDT type script
data: <2000 USDC>
outputs:
output 0:
capacity: 200 CKB
lock: Joy ID lock Alice
type: USDC UDT type script
data: <1000 USDC>
output 1: ?
witnesses:
witness 0: WitnessLayout format, SighashAll variant
seals:
0: SealPair format
script_hash: Joy ID lock Alice's script hash
seal: <Signature for Joy ID lock Alice>
input_cells: 1
output_cells: 2
cell_deps: 0
header_deps: 0
message: Message format
Action 0:
script_hash: <USDC UDT type script hash>
script_info_hash: <USDC UDT dapp info hash>
data: USDCAction format, Transfer variant
from: Joy ID lock Alice
to: <EMPTY>
amount: 1000 USDC
Here, it’s evident that we need an output cell 1 to hold the USDT received by Alice. However, we face the following issues:
- When constructing this OTX and signing it, we do not know how much USDT Alice will actually receive in the AMM swap.
- Additionally, Alice needs a way to encode the slippage information, informing the AMM swap that if the slippage exceeds 2%, the current OTX should not be included in the block. (Note that this is different from Ethereum’s AMM; in CKB’s AMM, if the slippage criterion is not met, the transaction will not fail but simply won’t be included in a block).
Approved Action Pattern
Typically, we would consider a Cobuild Action corresponding to a UDT to have a definition similar to the following:
table Transfer {
from: Address,
to: Address,
amount: Uint64,
}
table Mint {
to: Address,
amount: Uint64,
}
table Burn {
amount: Uint64,
}
union USDCAction {
Transfer,
Mint,
Burn,
}
The above OTX is indeed constructed based on a schema similar to this.
However, the Cobuild Action schema corresponding to a UDT is not limited to this. In fact, there is more room for manipulation in the schema, and we can make some modifications to it:
table Transfer {
from: Address,
to: Address,
amount: Uint64,
}
table Mint {
to: Address,
amount: Uint64,
}
table Burn {
amount: Uint64,
}
table Approve {
from: Address,
amount: Uint64,
to: Byte32,
data: Bytes,
}
union USDCAction {
Transfer,
Mint,
Burn,
Approve,
}
Compared to the previous schema, we have added a new schema type called Approve
here. Additionally, we can add corresponding rules in the USDC UDT type script: if the Cobuild action corresponding to the USDC UDT type script in the current TX/OTX is of type Approve
, we first verify the contents of from
and amount
according to the same rules as when verifying a Transfer
. Then, different from Transfer
, the USDC UDT type script will check the entire L1 CKB transaction to see if there is an executed script (including lock scripts in input cells and all type scripts) whose script hash matches the data contained in to
. If such a script exists in the current transaction, the USDC UDT type script verification is successful; otherwise, it is considered as a failure in Message verification.
Essentially, the Approve
action expresses two things:
- There exists a deterministic script in the current complete CKB transaction that will be executed (in the CKB scenario, requiring either the presence of a type script in the current transaction or the presence of a lock script in an input cell essentially expresses the desire for the code corresponding to this script to be executed).
- The signer of the current action is willing to provide a certain amount of UDT.
To further understand, we can combine these two points: the signer of the current action approves a certain amount of UDT so that a certain script can perform further operations. Therefore, in the context of AMM, if a signer approves a certain amount of UDT to the AMM entity type script (specified by the to
of the Approve
action), we can express the concept of providing tokens to the AMM in the OTX.
AMM OTX via Approved Action
With the Approve
action in place, we can integrate UDT with a series of DeFi applications, such as the AMM application discussed earlier. First, let’s define a simple molecule schema used by the AMM:
table Swap {
slippage: Uint64,
reserved_ckb_for_storage: Uint64,
source_coin: Byte32,
target_coin: Byte32,
}
union AMMAction {
Swap,
}
And DappInfo
:
name | Spore |
---|---|
url | Aha Swap |
script_hash | https://aha-swap.example.com |
schema | |
message_type | AMMAction |
Based on the discussion above, let’s refine the OTX for the AMM swap:
inputs:
input 0:
capacity: 200 CKB
lock: Joy ID lock Alice
type: USDC UDT type script
data: <2000 USDC>
input 1:
capacity: 10000 CKB
lock: Joy ID lock Alice
type: <EMPTY>
data: <EMPTY>
outputs:
output 0:
capacity: 200 CKB
lock: Joy ID lock Alice
type: USDC UDT type script
data: <1000 USDC>
output 1:
capacity: 9700 CKB
lock: Joy ID lock Alice
type: <EMPTY>
data: <EMPTY>
witnesses:
witness 0: WitnessLayout format, Otx variant
seals:
0: SealPair format
script_hash: Joy ID lock Alice's script hash
seal: <Signature for Joy ID lock Alice>
input_cells: 2
output_cells: 2
cell_deps: 0
header_deps: 0
message: Message format
Action 0:
script_hash: <USDC UDT type script hash>
script_info_hash: <USDC UDT dapp info hash>
data: USDCAction format, Approve variant
from: Joy ID lock Alice
amount: 1000 USDC
to: <AMM entity type script hash>
Action 1:
script_hash: <AMM entity type script hash>
script_info_hash: <AMM dapp info hash>
data: AMMAction format, Swap variant
slippage: 0.98 as Uint64
reserved_ckb_for_storage: 300 CKB
source_coin: <USDC UDT type script hash>
target_coin: <USDT UDT type script hash>
Note: In an AMM swap on CKB, there is still potentially relevant information associated with each AMM design, namely, who provides the CKB corresponding to the UDT obtained in the transaction. Because this issue is not relevant to the main discussion of the current article, we have chosen the simplest approach here: the initiator of the OTX provides CKB to be used for storing the UDT obtained in the transaction.
In the current OTX, there is actually no inclusion of the AMM type script. However, in the Cobuild Message, we still include the action corresponding to the AMM swap operation. When the USDC UDT type script verifies its own Cobuild action, it ensures that the AMM entity type script still exists in the current complete L1 CKB transaction.
During the actual execution of the AMM entity type script, it iterates through each Cobuild OTX included in the current L1 CKB transaction, checking whether each OTX’s Cobuild message contains a Cobuild action targeting itself. If such a action exists, the current OTX represents an AMM operation, and the AMM entity type script then verifies the actual AMM swap based on the slippage and the amount provided by the user through the Approve
action, ensuring that the current transaction meets the expectations of the OTX signer.
If we only consider the current OTX, the data in Action 1 will not be used by the scripts contained within the OTX, which in a sense makes it redundant data for the current OTX. However, if the current OTX is intended to be included in a complete CKB L1 transaction and eventually be packaged onto the chain, the data in Action 1 is indispensable for AMM verification. Furthermore, the signatures placed when constructing the current OTX ensure that the data in Action 1 cannot be tampered with.
Below is an example of a complete AMM swap containing the above-mentioned OTX:
inputs:
input 0(otx 0):
capacity: 200 CKB
lock: Joy ID lock Alice
type: USDC UDT type script
data: <2000 USDC>
input 1(otx 0):
capacity: 10000 CKB
lock: Joy ID lock Alice
type: <EMPTY>
data: <EMPTY>
input 2:
capacity: 12345.67 CKB
lock: <always success script>
type: <AMM entity type script>
data:
USDT: 100000 USDT
USDC: 50000 USDC
input 3:
capacity: 200 CKB
lock: <AMM asset lock script>
type: <USDT UDT type script>
data: <100000 USDT>
input 4:
capacity: 200 CKB
lock: <AMM asset lock script>
type: <USDT UDT type script>
data: <50000 USDC>
outputs:
output 0(otx 0):
capacity: 200 CKB
lock: Joy ID lock Alice
type: USDC UDT type script
data: <1000 USDC>
output 2(otx 0):
capacity: 9700 CKB
lock: Joy ID lock Alice
type: <EMPTY>
data: <EMPTY>
output 3:
capacity: 300 CKB
lock: Joy ID lock Alice
type: <USDT type script>
data: <999 USDT>
output 4:
capacity: 12345.66 CKB
lock: <always success script>
type: <AMM entity type script>
data:
USDT: 100000 USDT
USDC: 50000 USDC
input 3:
capacity: 200 CKB
lock: <AMM asset lock script>
type: <USDT UDT type script>
data: <99001 USDT>
input 4:
capacity: 200 CKB
lock: <AMM asset lock script>
type: <USDT UDT type script>
data: <51000 USDC>
witnesses:
witness 0: WitnessLayout format, OtxStart variant
start_input_cell: 0
start_output_cell: 0
start_cell_deps: 0
start_header_deps: 0
witness 1: WitnessLayout format, Otx variant
seals:
0: SealPair format
script_hash: Joy ID lock Alice's script hash
seal: <Signature for Joy ID lock Alice>
input_cells: 2
output_cells: 2
cell_deps: 0
header_deps: 0
message: Message format
Action 0:
script_hash: <USDC UDT type script hash>
script_info_hash: <USDC UDT dapp info hash>
data: USDCAction format, Approve variant
from: Joy ID lock Alice
amount: 1000 USDC
to: <AMM type script hash>
Action 1:
script_hash: <AMM type script hash>
script_info_hash: <AMM dapp info hash>
data: AMMAction format, Swap variant
slippage: 0.98 as Uint64
reserved_ckb_for_storage: 300 CKB
source_coin: <USDC UDT type script hash>
target_coin: <USDT UDT type script hash>
witness 2: <EMPTY>
witness 3: <Data to make AMM asset lock script pass the validation>
In the context of Cobuild, we have a possible implementation of AMM on CKB.
Recap
The Approved
action is actually a highly versatile pattern. Apart from AMM, the Approved
action can also be used for interactions between UDTs on CKB and a wide variety of DeFi or even non-DeFi protocols. The Approved
action simply provides a generic channel for expressing that a signer is willing to authorize a certain upper limit of UDT to a specific script, while the specific operations on these UDTs are determined entirely by the actual contracts. In a way, the Approved
action is somewhat similar to the approve
function in ERC-20, hence its name. However, in terms of practical usage, we believe there are differences between the Approved
action in Cobuild and the approve
function in ERC-20.
From the Approved
action, we can also perceive the difference between OTX in Cobuild and those designed on CKB in the past. In previous designs, we always aimed for OTX to fully represent a specific action, such as Alice sending out 1000 USDT and receiving 998 USDC. We wanted to cover both the “send” and “receive” aspects. However, should an OTX cover both ends of a transaction? In scenarios like AMM, we can at most ensure that an OTX only sends out 1000 USDT, but we cannot determine whether this OTX will receive 998 USDC or 999 USDC. Therefore, with the introduction of the Approved
action in Cobuild OTX, we hope to broaden the scope of OTX to some extent: an OTX can fully deterministically cover all input/output cells to lock in all data at both ends of a transaction, but an OTX should also be able to express the functionality of wanting to spend at most 1000 USDT and leave the other end of the transaction to be decided by a contract on-chain.
With this design, I believe Cobuild can cover the majority of known and unknown UDT/NFT use cases through actions. Furthermore, under the Cobuild framework, for other contracts and assets, there may be unconventional Cobuild action types similar to the Approved
action to address necessary special cases.