Approved Action: Exploring Exotic Cobuild Actions

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