Approved Action: Exploring Exotic Cobuild Actions

中文版本

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.

2 Likes

Cobuild 协议中引入了 Action 的概念,用来表述一个 CKB transaction 想要完成的一个或多个操作。在实际合约实现中,我们也会建议一个 type script 尽可能要求它对对应的 cobuild action 一定存在,以减少外部钱包或其他工具解析交易时所需要完成的工作量。

考虑到 Cobuild 是一个全新引入的概念,大家对 cobuild action 的理解可能仅限于简单的文字描述。目前 Spore 合约中提供了对 cobuild action 的定义,以及相对应的校验实现。但是因为 Spore 合约本身的设计与实现需求所限,Spore 中涉及 cobuild action 的的部分代码,是有很强的局限性的,可能并不能很好的作为一个例子,来帮助大家窥探可能存在的种种 cobuild action 设计。

因此便有了这篇文章:我们期望通过对 AMM 场景下,cobuild action 的设计,向大家展示除可以简单想到的 transfer / mint / burn 操作之外,其实 cobuild action 也可以被应用于更广泛的场景,支撑多种多样的设计需求。

When Cobuild OTX meets AMM

在目前的框架中,我们选择了基于 cell 来构建 OTX,而不是基于一个 cobuild action。即使 Message 已经可以确定一个 action,在实际的 OTX 签名中,我们仍旧把 Message,和以 cell 来表达的 Otx 结构一起签名,确保 Message,和 OTX 中的任意一个 cell 都不得被篡改。这样的设计选择,在某些场景中,也就会带来一些困扰:

  • 在真正 L1 上链时,才能确定某些数据的操作,比如 AMM swap
  • 需要某些不属于 Otx 中的合约执行才能确定安全的场景,比如 Alice 通过类似 aave 协议做 deposit 时,aave 本身元数据的更新(注意这里只是一种可能的 CKB 上实现 aave 的方法,实际也有可能只是单纯的把 Alice 的 deposit 存入某个特定的 lock)

对这些场景,我们会引入一个一个特殊的设计模式解决问题,我们称之为 approved action

要先说明的是,取决于不同的视角,approved action 可能并不是一个非常理想的名字。Approved action 最初起源于 ERC-20 中的 approve 操作,也因此我们把它命名为 approved action。但是实际上下文可以看出,approved action 实际的设计与 ERC-20 中的 approve 操作有着很大的不同。所以其实在 cobuild 中可能应该取一个不同的名字。在这里我们暂且先把它叫做 approved action。

AMM Entity & Asset Cell

这里我们以一个 AMM swap 为例。AMM 需要维护自身交易对中,参与交易的两个币种在 pool 中各自当前的数量。我们这里以一个 entity cell(我们会有一篇后续文章来展开关于 entity cell 的讨论) 来表达一个特定的交易对:

lock: <always success script>
type: <AMM entity type script>
data: <UDT 1 script hash> <UDT 1 amount> <UDT 2 script hash> <UDT 2 amount>

出于简化讨论的考虑,我们定义了如下规则:

  • 一个 AMM entity cell 只处理一个交易对的交易
  • 交易对所需的最基本信息放在cell data 中平铺存放

实际场景可能远比这里复杂:一个 AMM entity cell 可能会用于存放多个交易对的数据,同时每个涉及的交易对,也可能有更多的元数据,以更复杂的数据结构来存放。但是我们简单的 entity cell 定义,已经足够用于当前的讨论。

针对上面定义的 AMM entity cell,可以有两个或更多的 AMM asset cell 用于保存一个交易对所对应的实际资产:

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>

通常情况下,这里的 AMM asset lock script,只需要检查当前 CKB transaction 中包含一个使用 AMM entity type script 作为 type script 的 input / output cell 即可。实际这样一个 AMM 运作时所有的校验逻辑,都由 AMM entity type script 完成。

AMM OTX

然后我们考虑 AMM swap 中,Cobuild OTX 如何构建。假设 Alice 想用 1000 USDC 换 USDT,他能接受的最大滑点为 2%,即该用户可以接受 1 USDT 换到 0.98 USDC(当然针对不同 AMM 的实现,滑点可能有不同的计算定义方式,这里我们使用最简单直观的定义方式)。按照最直接的思路,我们只能构建出一个初步的 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

这里很显然需要一个 output cell 1,承载 Alice 收到的 USDT,但是我们在这里面临问题:

  • 在构建这个 OTX,并进行签名时,我们并不知道实际在 AMM swap 中,Alice 可以换得多少 USDT
  • 同时,Alice 需要一个位置来 encode 滑点信息,告诉 AMM swap,如果滑点超过 2%,就不要打包当前的 Otx(注意这里跟以太上的 AMM 并不相同,如果滑点不满足,在 CKB 的 AMM 中,不会失败,而只会不去打包)

Approved Action Pattern

通常情况下,我们会认为一个 UDT 对应的 Cobuild Action,有如下的类似定义:

table Transfer {
		from: Address,
		to: Address,
		amount: Uint64,
}

table Mint {
		to: Address,
		amount: Uint64,
}

table Burn {
		amount: Uint64,
}

union USDCAction {
		Transfer,
		Mint,
		Burn,
}

上面的 OTX 也的确是基于类似这样的 schema 进行实际构建的。

但是其实一个 UDT 对应的 Cobuild Action schema,并不仅限于此。这里其实有更多的操作空间,我们可以在 schema 上做一些修改:

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,
}

与之前的 schema 相比,我们在这里增加了一个新的 Approve 类型的 schema。同时我们可以在 USDC UDT type script 中增加相应的规则:如果当前 TX / OTX 中,对应 USDC UDT type script 的 cobuild action 为 Approve 类型的话,首先我们依然按照校验 Transfer 时同样的规则,对 fromamount 中的内容进行验证。然后与 Transfer 不同的是,接下来 USDC UDT type script 会校验当前完整的 L1 CKB transaction 中,是否有一个被执行的 Script(包含 input cells 中的 lock script,以及所有的 type script)其 script hash 与 to 中包含的数据相同。如果当前完整的交易中存在这样一个 script,则 USDC UDT type script 校验成功,否则认为 Message 的校验失败。

本质上, Approve action 用来表达两件事情:

  • 当前完整的 CKB transaction 中,存在某一个确定性的 script 会被执行(在 CKB 的场景下,要求当前交易中存在一个 type script,或者存在一个 input cell 的 lock script,本质上其实是在表达希望这个 script 对应的代码,会被执行)
  • 当前 action 的签名方,愿意提供一定数量的 UDT

进一步理解的话,我们可以把这两件事情合并起来:当前 action 的签名方 approve 了一定数量的 UDT 以便某个 script 可以进行进一步的操作。因此在 AMM 的背景下,如果某个签名方通过 UDT 的 Approve action,来向 AMM entity type script(由 Approve action 的 to 指定) approve 了一定数量的 UDT 的话,我们便可以在 OTX 中表达一个提供 token 给 AMM 的概念。

AMM OTX via Approved Action

有了 Approve action,我们就可以把 UDT 与一系列的 DeFi 应用组合在一起,比如上面讨论的 AMM 应用。首先我们来定义一个最简单 AMM 所使用的 molecule schema:

table Swap {
		slippage: Uint64,
		reserved_ckb_for_storage: Uint64,
		source_coin: Byte32,
		target_coin: Byte32,
}

union AMMAction {
		Swap,
}

以及 DappInfo

name Spore
url Aha Swap
script_hash https://aha-swap.example.com
schema
message_type AMMAction

然后基于上述的讨论,我们来完善 AMM swap otx:

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>

注意:在 CKB 上的 AMM swap 中,仍旧有一个可能跟每一个 AMM 设计相关的信息,就是保存交易中得到的 UDT 所对应的 CKB 由谁提供。因为这个问题与当前文章主要的讨论无关,所以我们在这里选择了一个最简单的做法:OTX 发起方自行提供 CKB 来用于保存交易得到的 UDT。

当前 OTX 中其实并不包含 AMM type script,但是在 Cobuild Message 中,我们依然放入了 AMM swap 操作对应的 action。USDC UDT type script 在校验自己的 cobuild action 时,会确保 AMM entity type script 在当前完整的 L1 CKB transaction 中依然存在。

实际 AMM entity type script 执行时,会依次遍历当前 L1 CKB transaction 中包含的每一个 Cobuild OTX,判断每一个 OTX 对应的 Cobuild message 中,是否包含针对自己的 Cobuild action。如果存在的话,当前的 OTX 即为一个 AMM 操作,AMM entity type script 于是会针对滑点以及用户通过 Approve action 提供的 amount,来进行实际 AMM swap 的校验,确保当前的交易符合 OTX 签名方的预期。

如果只看当前 OTX 的话,Action 1 中的数据并不会被 OTX 内部包含的 script 所使用,某种程度上,对于当前的 OTX 可能是冗余数据。但是如果当前的 OTX 想被收纳在一个完整的 CKB L1 transaction 中,并最后被打包上链,Action 1 中的数据在 AMM 校验中是不可或缺的。同时签名放在构造当前 OTX 时,也会通过签名来确保 Action 1 中的数据不会被篡改。

如下是包含了上述 Otx 的一个完整 AMM swap 例子:

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>

我们在 cobuild 的背景下,便有了一个可能的 AMM 实现。

Recap

Approved action 实际上是一个十分通用的 pattern,除 AMM 之外, Approved action 也同样可以用于 CKB 上的 UDT 与其他多种多样的 DeFi 甚至非 DeFi 的协议之间的交互。 Approved action 只提供了一个通用的渠道,用于表述某个签名方愿意向某个 script 授权有一定上限的 UDT,而具体这些 UDT 如何流通操作,完全由实际的合约来决定。某种程度上, Approved action 与 ERC-20 中的 approve 功能的确有些类似,它也因此而得名。但是从实际使用方式上,我们认为 cobuild 中的 Approved action,与 ERC-20 中的 approve,还是有一定区别的。

Approved action 中,我们也可以感觉到 Cobuild 中 OTX 与之前在 CKB 上设计的 OTX 中的区别。在以往的设计中,我们总是希望 OTX 能完整的表述一个确定的行为,比如 Alice 送出 1000 USDT,得到 998 USDC。我们在 “送出” 和 “得到” 这两个维度上,都想做到覆盖。但是实际上,一个 OTX 是否应该覆盖一个交易的两端?就像 AMM 这样的场景,我们最多只能确保一个 OTX 只能送出 1000 USDT,而这个 OTX 得到的是 998 USDC 还是 999 USDC,我们并不能确定。所以在 Cobuild OTX 中,通过 Approved action 的引入,我们希望在某种程度上,拓宽 OTX 的适用范围:一个 OTX 完全可以确定性的覆盖所有的 input / output cell,来锁定交易的两端所有的数据,但是一个 OTX 也应该同样可以用来表达,我最多只想花费 1000 USDT 的功能,把交易的另一端,交给一个链上合约来决定。

有了这样的设计,我相信,cobuild 其实是可以通过 action 涵盖绝大部分已知以及未知的 UDT / NFT 使用场景的。而其实 cobuild 的框架下,对于其他的合约,其他的资产,可能也会有非常规的,类似 Approved action 一样的 cobuild action 类型,来解决必要的一些特殊问题。

1 Like

在应对AMM的场景下,这种机制是合理的,但是,它依然还存在以下两个问题:

  1. 基于OTX的订单簿将无法实施,同时使用UDT的手续费代付也不再可能,即用户可以在一笔OTX交易中留一些UDT,并由任何将这笔交易上链的人获得。
  2. 资产协议需要整合特别多的 Cobuild 逻辑,因为资产无处不在,链上协议本身是为资产服务的。那一个资产协议就需要对接非常多的Cobuild版本,比如SighashAll,OTX:
    1. 首先,这提升了复杂度,而我觉得资产协议应该简单而抽象。
    2. 未来也许会有可以细粒度控制的OTX或者别的Witness编排协议,那么资产协议是不是也要跟着升级呢?但是大部分资产脚本都是使用不可升级方式部署的。如果不支持,那依然会出现兼容性问题。

我的建议是,资产协议支持最基础的SigHashAll操作,这样,用户在日常使用钱包的时候,使用Action + SigHashAll,而如果没有SigHashAll的Message,这时资产协议不约束Witness中的Action。

First, some points on the technical bits:

I don’t agree with this, orderbook DEX is something we definitely have thought about in the process of designing cobuild, and UDT-based fee is definitely possible in Cobuild OTX.

Sighash / OTX is totally orthogonal to Action design. It is totally possible to use Approved Action with Sighash, given enough motives.

And now the controversial part where personal preference plays a more important role:

I do want to stress my point one more than:

Complexity is, well, a complicated thing. In a protocol, it makes no sense to look at the complexity of one particular aspect alone. Everything we do here involves tradeoffs, to me it is better to optimize for overall complexity of the stack, than focusing on the complexity of one component. I believe it is better if we can eliminate transaction parsing in wallets / transactions as much as we can, an external tool, should have the ability to focus on cobuild action alone, while ignoring a major part of the actual transaction when it can. This, to me, eliminates the most complexity of the overall stack, at the expense of slightly complicating one bit in the design. To me it is a worthwhile tradeoff.

But again, like we discussed before on discord, this is not a technical problem, but a preference problem. To me there is really no point in arguing on a preference problem. My personal preference does not matter, your personal preference might not matter that much. What really matters, is the joint preference of the community. Let me just say that cobuild started as a design of my preference, but there have been enough discussions and changes internally at Cryptape before we made it public, so at its current state, cobuild really represents the collective preference of Cryptape team, and we are now hoping to make it the collective preference of the whole CKB community. So I will just explain my motive in the current design, and then leave it here, for anyone who might have an opinion here to tell, a joint decision can then be reached.

Again, I will request a reference on this. My past experience is that most scripts used for tokens are deployed with type ID, allowing a proper path of upgrading. Spore is really the first of its kind that embraces imperfection and be deployed without type ID.

And if you really ask my personal opinion, I must say when I first discovered type ID, I envision a different way than what we typically employ right now:

  • A script can be initially deployed using type ID together with a single-sign / multi-sign lock
  • At early stages, there might be bugs discovered from the script, or features requested to the script, which requires upgrading the script a few times
  • Sooner or later a time will come where the community decides too much value is managed by the script, and an upgradable script presents more risks than merits. At this stage we might also be quite familiar with the script to know that all known bugs have been resolved. At this stage, the developer can do one last upgrade on the cell containing the script, changing the lock to an always-failure script, or we can also alter the hash type part in the type script. This way, we mark the script as not upgradable, providing more confidence to everyone using the script

Really, this is my originally designed way on how type ID should be used. And if we use it like this way, we do avoid most, if not all problems here.

It remains a question to me how many scripts will choose the design choice made by Spore. I seriously doubt that it will be the majority.

1 Like

How to do it? Note that we should not introduce any cell competition.

Currently, both xUDT and Spore are deployed based on non-upgradeable mode, which means that the current most important NFT protocol and FT protocol are not upgradeable.

Considering that the total asset market value of existing Spore and xUDT is close to $100 million , the new asset protocol should remain backwards compatible, as all future infrastructure already has to be compatible with this situation, namely:

  • Use type_script+amount to determine UDT
  • and use type_script with type_id args to determine NFT.

Whether to force Cobuild Action or not has its own trade-offs, and keeping this decision is at least beneficial in terms of backward compatibility, forward compatibility, and interaction with other applications. If this is just a taste preference, I think we need to do this. trade off. Note that it still supports the full Cobuild Action at this time.

Like the above AMM example shown here, cell competition is a problem of OTX, and OTX processors will deal with the cell contention issue. When we are designing an app in the OTX pattern, the app itself does not need to deal with the contention issue.

I was actually surprised that you consider this is a problem, cuz AMM is really the harder part in an UTXO design. If this still feels like an issue for me, I can find a time later to write a draft design. I’m also trying to get a DEX demo in cobuild OTX’s design anyway.

Please let’s talk about different problems differently.

My question really here is: will non-upgradable scripts be the majority, or will upgradable scripts be the majority? Spore and xUDT are currently deployed as non-upgradable, I get it. But is this a widely received consensus? Or is it just design choice for now? I’m asking a reference on this part.

Again, let’s not mix Spore and cobuild together, as they are totally different thing. Spore is a asset protocol that is designed to be non-upgradable by choice, all future protocols, if they want Spore in the game, will need to make itself compatible with Spore, and this issue here is actually bigger than the cobuild design part we think about here. What if a future protocol wishes to use a different data layout than Spore currently utilities? Fundamentally, if you want to be able to leverage Spore in a new protocol, you have to make sure your protocol is compatible with Spore, there is no other choice.

And really, I want to stress this part one more time: Spore is a different thing from cobuild, they share some similarities but they are fundamentally different. Spore is non-upgradable, does not equal to the fact that all scripts following cobuild protocol will not be upgrdable. Please do not jump to the wrong conclusion based on invalid assumptions here. We could still argue if it makes sense for cobuild to enforce Cobuild Action validation in its recommended proposal, but please do not use Spore nor xUDT as arguments to prove or disprove one point, cuz really a shared protocol, is quite different from a concrete script with its own added design choices.

If we consider a centralized sequencer, then of course it would be OK. But what I’m thinking is, unless a sequencer must be used, in the Cell/UTXO model, any contention over cells should be avoided.

From a purely design perspective, that’s how it is, but when considering engineering and reality, we have to think about future asset protocols from the perspective of existing mainstream asset protocols. Unless we believe that asset protocols should not have general interface standards like ERC20 and ERC721, but can be designed arbitrarily by asset protocol designers. If designers can freely design the Actions of asset protocols, considering the special nature of asset protocols, I think the problems caused by Cobuild on asset protocols outweigh the benefits. If the asset protocol requires interface standards, I cannot discuss it without considering the existing sUDT, xUDT, and Spore.

After giving up backward compatibility and bringing great difficulties to forward compatibility, and needing various patches to meet the interaction of various protocols and asset protocols, apart from differences in taste, what engineering benefits does enforcing the use of Cobuild Actions bring to assets protocol compared to simultaneously supporting Cobuild and non-Cobuild? Currently, I understand that it only has the benefit of off-chain parsing, but off-chain parsers have too many capabilities at their disposal and can be upgraded arbitrarily.

Once more, let’s not mix totally different problems together:

The design philosophy of cobuild OTX, is that cobuild OTX the backbone itself, will handle the cell contention problem. And if you want, we can talk about this problem separately: I do believe in the cobuild OTX world, mining pools will have an advantage when acting as a sequencer, since a mining pool has access to internal state within its produced blocks, allowing for packing more OTXs when possible. But that does not really mean that individual OTX processor will not be available. An analogy here, is that Flashbots exist in the Ethereum world, independent from mining pools.

However, that does not really mean that OTX processors will become centralized sequencer. Yes within a single CKB transaction, it might be that one OTX processor decides on orders for a small part of transactions but: 1) there are more than one OTX processor, I would not call it centralization; 2) it is still CKB consensus that decides orders between different L1 TXs sent by different OTX processors.

But I do want to stress more, that the above discussion is merely put together for completeness, it is not relevant to the discussion for this post. When one is designing an app (AMM dex or orderbook dex or others) following the cobuild OTX pattern, it is safe to assume that the cell contention problem is already solved in another layer. So I do recommend we stick to the point we want to discuss here.

I feel like I’ve been saying this over and over and over and over and over again:

All designs are tradeoffs, I see great merits when wallets / external tools can focus solely on cobuild actions, to me it would bring a huge deal of benefits when they don’t have to dissect individual transactions again. Maybe you disagree, and it’s fine. To me this is a huge benefit we win by enforcing cobuild actions to be present.

And personally, I wouldn’t call this “great difficulties”, and I seriously doubt if we will have the luxury of defining backward incompatible protocols, so I would not consider this a big problem. A tradeoff is a tradeoff, there are wins, there are losses. If you disagree, that is totally fine. I just want to explain the rationale I have when designing cobuild in the first place.

If it is a wallet and tool based on the Cobuild toolchain, then when asset protocols supporting both modes(Cobuild & no-cobuild) at the same time, it can still force the use of Cobuild, but this enforcement is not at the contract layer, it seems to be fine.

I will just say that I have different opinions. I think we’ve learned enough lessons, so in a way, cobuild actually adds rules, reducing flexibility in exchange for clarity and simplicity.

We’ve had too many cases where we want both ends in a decision, and end up pretty badly.

To me, I don’t want this level of flexibility, I believe it will do more harm than good.

I think the design of asset protocol standards (which is extremely important for public chains) needs to involve more product managers and application developers in the design discussion, while all previous design decisions came almost exclusively from protocol developers.

Even if we learn from history, we also need to consider the impact on users and application developers, not just from the perspective of protocol developers.

Many attributions learned from history may not be correct, it might just make more mistakes.

If you really think that the current consensus on asset interface standards needs to be broken and no longer backwards compatible, I think this needs to be written into an RFC for widespread discussion.

It is probably difficult to accept the backward incompatibility of asset interface standards determined by a few people. The asset protocol is not simply another common application.