基于 Layer 2 订单聚合模式的 DEX 设计

Background

随着 DeFi 和链上金融活动的演化,DEX 作为其基础设施目前看大概有这样几种大的类型。其一是以 etherdelta、0x 为代表订单簿类型,细分为链上撮合和链外撮合两种;其二是以 Uniswap 为代表的 AMM 类型,通过某种数学关系自动设置成交价格从而提供无限深度;其三是以 Kyber 为代表的外部储备池自动报价的做市商类型。三种模式的统一特点是用户的资产是自托管的,对服务商无需信任。因此在安全性上大幅超越传统的中心化交易所。最近一段时间,随着 rollup 技术的发展,人们开始尝试用该技术及其改进的 zk-rollup 和 optimistic rollup 来进行优化,引入了链外聚合者的角色,将大量交易在链外聚合清算,然后定期到 Layer 1 上做结算。这种方法也大幅提高了 Layer 1 的交易吞吐量,降低了交易的平均成本。

CKB 上采用的 Cell 编程模型在产品和协议设计上与以太坊等账户模型有很大的不同,这就给面向账户设计的这几种 DEX 的实现带来了挑战。然而 rollup 这种链外聚合链上结算的模式又绕开了在链上根据用户的动作更新账户数据的问题,只需要验证链外的清算结果是否正确即可。这种模式非常符合 CKB 的业务逻辑,因此本文将深入探讨这种 Layer 2 订单聚合模式在 CKB 上的实现。

基本业务流程

充值 & 提现

用户需要将自己待交易的资产的操作权限授权给 DEX 合约,该过程可以类比传统中心化交易所的充值过程,但资产所属权仍然归用户,它是非托管的。

充值过程是把用户的资产改为合约管理,并且生成一个对应的 account cell,其中记录用户的资产数值。Account cell 本身的业务逻辑由对应的 dex clear type script 来保证,它本身记录了用户的 pk_hash 用来验证来自用户的交易指令,同时也允许合约本身按照预定规则修改其余额。Account cell 的 Data 字段记录了用户的各种资产的余额,同时根据 DEX 类型不同,可能还需要记录用户的 order book。

用户的提现动作和充值动作相反,用户将自己的 Account Cell 与 一个或多个 Deposit Cell 组合,并生成一个或多个资产 cell。但这里有一个细节,即如果用户还没有结束自己在 Layer 2 聚合器的业务,聚合器可能会同时发送一个引用该用户 account cell 的聚合交易,该交易将会与用户的提现交易冲突,导致二者只有一个可以上链。为了防止这种情况发生,我们需要对提现做额外的限制,例如在提现前用户需要先公布一个预提现 cell,正式提现则需要引用这个预提现 cell,同时聚合器如果看到这个预提现 cell,它将自动将该用户的 account cell 从状态更新中排除。

交易指令与撮合

我们把用户在链上的 account cell 称为初始状态(pervious states),对于 AMM 类的 DEX,会存在类似 Asset pool cell 的额外初始状态。用户与 Layer 2 的聚合者建立连接,获取当前的报价和订单信息,并且发送交易订单。用户发送的交易订单内容包括对交易的描述(order),例如 “0.1 cBTC 置换 1000 cUSD”,以及交易签名。聚合器收集用户的报价订单后,立即反馈用户,让用户看到自己的交易已经被接受。

接着,聚合器实时对收到的订单进行撮合,随时更新每个参与这的 account cell 状态(new states),并定时把新的 account cell commit 到链上实现链上结算。每个上链的交易由初始状态、新状态、用户的数字签名构成。Account Cell 的 type script 负责验证整个链上结算的有效性。

用户的手续费收取在这个过程中是比较明确的,即按百分比收取交易资产的数量,并由聚合器负责为 ckb 矿工提供以 ckb 为计价的矿工费。

AMM 模式与撮合流程

类似 Uniswap 的 AMM 需要为流动性提供方设置资产池,用来提供算法支持下的无限流动性。下面以 ckb-sUDT 交易对为例,描述资产池操作的生命周期。

创建交易对并注入初始流动性

为了防止重复创建交易对,这里需要引入交易对注册机制。

用户注册时需要检测 Registry Cell 中是否已经有了相应的交易对,并以交易对的 token id 的 hash 作为交易对标识符。新增交易对将在注册中心新增一条记录。目前没有设计反注册功能。

用户注册交易对的同时需要注入初始流动性。初始流动性根据 CKB/sUDT 的市场比价等比注入,例如 1CKB = 0.01USDT 时,用户同时注入 50k CKB 和 500 USDT. 这些资产生成一个单一的资产池 Cell(Asset Pool Cell)用来进行后续交易。

注入流动性将获得 Liquidity Token,这是一种符合 sUDT 标准的扩展 UDT,其合约代码与 sUDT 略有区别,资产标签为交易对的哈希。Liquidity Token 总是在注入流动性时被自动创建,并在提出流动性时销毁。具体的业务逻辑与 Uniswap 一致。

增加/退出流动性

流动性增加与退出方式与 Uniswap 也是一致的。具体来说用户注入资产可以获得 Liquidity Token,反向则可以提取资产。

为了防止频繁操作资产池导致交易冲突或 DDoS 问题,可能需要引入一些限制。例如加入阈值要求、提现手续费要求等。

交易撮合与手续费计算

聚合服务器收集一段时间内的用户订单并统一聚合,这些订单包括:买单、卖单和流动性增减订单。考虑到交易的公平性,聚合后的订单按照如下规则(次序)执行。这种处理方法可以最小化交易滑点。

  1. 处理增加资产池的订单
  2. 所有的买单、卖单按照当前资产池价格直接匹配
  3. 买卖单差额再按照预定的价格曲线与资产池进行清算( 这里可以根据订单的滑点要求按需处理)
  4. 处理减少资产池的订单

手续费方面,用户的买单卖单需要缴纳一定额度(如 0.3% )的手续费。这部分手续费一部分(例如 0.2%)直接进入资产池,作为流动性提供方的收益;另一部分(例如 0.1% 作为聚合器运营费用支付给指定地址)。

Orderbook 模式与撮合流程

与 AMM 模式相比,订单簿模式有两个重要区别:其一是它的订单一般是限价单而不是市价单,即用户自己控制交易价格;其二是它无法提供无限流动性,用户必须等待匹配订单出现。从数据结构上将,订单簿模式比 AMM 模式在链上增加了订单数据,同时减少了流动性代币和资金池。

限价单更新

用户将资产“存入” DEX 后,即可下限价单,限价单的内容包括:交易对象、交易价格、交易数量等。为了便于跟踪,每个限价单会生成唯一标识符 oid(可以通过交易的第一个 input 的 outpoint hash 实现。

交易撮合与手续费计算

注意用户的限价单订单下单操作会首先发送给聚合器,聚合器会直接将可匹配的订单处理,最后留下暂时无法匹配的订单才会真正在链上更新其订单列表。下一个聚合周期,这些未完成的订单会作为 input 参与下一轮撮合,但这时他们就无需用户提交新的交易签名了。

撮合引擎按照如下逻辑处理:

  • 所有通过 witness 新增的订单和留在 cell data 里面的老订单一起处理
  • 按照买卖订单分成两个队列并根据价格降序、升序排列
  • 依次撮合两个队列的头部订单,如果价格匹配,则按照平均价格成交,更新订单数量信息、用户账户余额信息
  • 继续处理下一个头部订单,直到无法匹配为止

交易手续费按照交易币种的等比收取(例如 0.1%),由聚合器运营方收取。

总结

本文初步设计了两种 Layer 2 DEX 的 设计方案。可以看到他们都充分利用了 UTXO 模型天然的聚合特性,在链外完成了交易聚合和撮合,降低了链上的交易量和用户的认知成本。同时保证了资产托管的去中心化性和去信任性。

类比 Rollup 系列技术,本文的交易聚合方案并没有使用零知识证明或乐观假设加挑战等方案对用户的交易验证进行运算复杂度降低。因此实际的运行效率和区块 cycle 上限与单次非对称验签的 cycle 比值有关,预计在 200~300 tps。尽管订单簿模式中老订单不需要再次验证交易签名,其对交易吞吐量的提升有限。但相信我们可以通过零知识证明技术,聚合签名技术等密码学方案大幅提高交易吞吐量。

11 Likes

一种更简单的模式是不区分 deposit/account cell:

  • deposit&make order: Alice 生成一个新的 udt cell (token A, amount X) , lock 设置为 DEX lock, lock args 说明想要交换的 token B, amount Y. 这个交易既是充值又是挂单。这个 cell 的解锁条件是 output 中有一个 token B cell, amount = Y, 且 lock 是 Alice lock.
  • take order: Bob 观察链上具有 DEX lock 的所有 cells (orders),选择其中一个或者多个进行交易。

这个模式的优点是简单,只需要实现一个 dex lock script. 应用层结合不同的链外逻辑既可以实现 OTC 交易也可以实现集中撮合交易。

6 Likes

期待安比他们的 zk 工具箱好了之后上 rollup。
BTW 我自己很可能会在 ETH 上搞个 uniswap in zksnark。欢迎交流。https://github.com/zkswap/docs/tree/master/cn/daily_notes

如果没有引入 optimistic 的挑战机制和单一 operator 的话,Asset Pool Cell 是不是有热点问题?不同用户在同一个块中对同一个交易对进行 增加/退出流动性、交易 只会有一个成功?

这个能不能做成“部分”take的形式,比如A挂一个10UDT的单子,但是我手上只有5UDT,那我就换5个UDT,然后剩下的5个还是以DEX lock的形式存在,这样对流动性可能会有帮助。

可以实现,但是逻辑会复杂一些。

DEX lock 本来是 (target_token, target_amount),需要改成 (target_token, price),这样在 partial filled 之后也可以保持价格不变;
解锁逻辑改成 output 中有一个 target_token cell, 其 amount > (sum(amount in output group) - sum(amount in input group)) * price。