Solana生态去中心化交易所Serum源码分析

Solana生态去中心化交易所Serum源码分析

Serum是一个去中心化交易所和生态系统,为去中心化金融带来前所未有高交易速度和低交易成本,其建立在Solana区块链网络之上,无需第三方许可便可使用。Serum的目标是解决现Defi协议弱中心化、低资金使用率、流动性分割、低交易吞吐量以及交易成本高等问题。Serum在链上搭建了中央订单薄以及撮合引擎,提供了非常高的交易吞吐量以及极低的交易延迟。

Serum 一个完整的交易流程包括以下4个阶段:

1. 下单

用户从自己的SPL钱包转移资金到中间账户OpenOrders,创建订单并提交一个Request给撮合引擎。

2. 撮合

撮合引擎收到Request后进行交易的撮合处理,并把结果更新到Orderbook账户中。将撮合结果放入Event Queue。

3. 结算

从Event Queue中取出撮合阶段产生的Event并做相应的处理,将交易结果更新到用户的OpenOrders账户中。

4. 偿付

用户可随时将所属中间账户OpenOrders中未锁定资金和手续费奖励提取到钱包中。

一、源码结构

1. 代码库

代码git仓库地址:https://github.com/project-serum/serum-dex.git

版本号:bcedad83ece3b898103af3fef5d14c5798a4c4f2

2. 目录介绍

dex/src/libs.rs

包声明,Solana程序入口。dex/src/critbit.rs

Critbit Tree的实现,用于订单的存储管理。Critbit Tree是一种深度为O(longest-length)的树,与二叉树类似,做分支检查的代价非常小。Critbit Tree的特性用来做订单的插入、查找、删除、更新等操作非常高效。dex/src/error.rs

自定义错误。dex/src/fees.rs

根据用户持有的SRM代币计算该用户的手续费级别,并获取对应级别要收取的费率。dex/src/instruction.rs

程序指令相关的代码。主要实现了Market、Order相关指令的创建、unpack、pack。dex/src/matching.rs

实现交易撮合逻辑。包括提交新订单、Ask、Bid交易撮合、取消订单等功能,以及限价单、市价单的撮合逻辑。dex/src/state.rs

状态管理的相关逻辑。主要包括:Market的创建、初始化、管理Order的提交、创建、取消等操作结算、费用、偿付Request Queue、以及Event Queue的逻辑dex/src/tests.rs

演示了一个完整订单流程,该代码可以辅助理解整个项目的架构思路。

二、主要账户

Serum通过Solana账户存储链上数据,主要有两类账户:DEX的全局账户以及用户专有账户。

Market: 存储Market的元数据。一个交易对对应一个Market,例如交易对BTC/USDT是一个Market,而SOL/USDT是另一个Market。
pubstruct MarketState{// 0 状态 pubaccount_flags: u64,// Initialized, Market // 1 创建者 pubown_address: [u64;4],// 5 资金库签名nonce pubvault_signer_nonce: u64,// 6 base token地址 pubcoin_mint: [u64;4],// 10 quote token地址 pubpc_mint: [u64;4],// 14 base token账户地址 pubcoin_vault: [u64;4],// 18 付保证金 pubcoin_deposits_total: u64,// 19 累积手续费 pubcoin_fees_accrued: u64,// 20 quote token账户地址 pubpc_vault: [u64;4],// 24 付保证金 pubpc_deposits_total: u64,// 25 累积手续费 pubpc_fees_accrued: u64,// 26 最小订单金额 pubpc_dust_threshold: u64,// 27 request queue账户地址 pubreq_q: [u64;4],// 31 event queue账户地址 pubevent_q: [u64;4],// 35 买单账户地址 pubbids: [u64;4],// 39 卖单账户地址 pubasks: [u64;4],// 43 base token最小精度 pubcoin_lot_size: u64,// 44 quote token最小精度 pubpc_lot_size: u64,// 45 交易费率 pubfee_rate_bps: u64,// 46 Maker获取的交易手续费奖励 pubreferrer_rebates_accrued: u64,}pubstruct MarketStateV2{pubinner: MarketState,// 该pubkey的账号有权限操作open orders pubopen_orders_authority: Pubkey,// 该pubkey的账号有权限清空订单 pubprune_authority: Pubkey,// 该pubkey的账号有权限执行结算操作 pubconsume_events_authority: Pubkey,// Unused bytes for future upgrades. padding: [u8;992],}// v2在v1基础上提供了更详细的权限管理,并加了padding字段,增强扩展性

Base Currency Vault: DEX的资金账户,用于存放基础货币,为Base Token的SPL关联账户。Quote Currency Vault: DEX的资金账户,用于存放报价货币,为Quote Token的SPL关联账户。

说明:交易对BTC/USDT中,BTC为Base Token即基础货币,而USDT为Quote Token即报价货币。Request Queue: 请求队列,环形队列。该帐户存放所有提交但未处理的订单以及取消订单请求。
pubstruct RequestQueueHeader{// 状态标识 account_flags: u64,// Initialized, RequestQueue // 头指针 head: u64,// 个数 count: u64,// 下一个元素的序列号 next_seq_num: u64,}pubtype RequestQueue<a>=Queue<a,RequestQueueHeader>;enum RequestFlag{// 新订单 NewOrder=0x01,// 取消订单 CancelOrder=0x02,// 买单 Bid=0x04,PostOnly=0x08,// 市价委托 ImmediateOrCancel=0x10,DecrementTakeOnSelfTrade=0x20,}pubstruct Request{// 标识 request_flags: u8,owner_slot: u8,// 费率级别 fee_tier: u8,self_trade_behavior: u8,padding: [u8;4],// 市价委托时花费的最大金额,或取消订单时这里是订单ID max_coin_qty_or_cancel_id: u64,// 锁定的询价货币数量 native_pc_qty_locked: u64,// 计算后的订单ID order_id: u128,// 订单用户 owner: [u64;4],// 客户端订单ID client_order_id: u64,}
说明:由于撮合引擎已经改为订单提交后立即撮合,无需由客户端定时提交指令触发;故Request Queue的设计仅仅用于暂存订单以便生成订单ID。Event Queue: Event队列,环形队列。该账户用于存储交易撮合后的Event。
pubstruct EventQueueHeader{// 状态标识 account_flags: u64,// Initialized, EventQueue // 头指针 head: u64,// 个数 count: u64,// 下一个Event的序列号 seq_num: u64,}pubtype EventQueue<a>=Queue<a,EventQueueHeader>;enum EventFlag{// 标识订单撮合成功 Fill=0x1,// 标识订单取消、撮合失败 Out=0x2,Bid=0x4,Maker=0x8,ReleaseFunds=0x10,}pubstruct Event{// 状态标识 event_flags: u8,owner_slot: u8,// 手续费级别 fee_tier: u8,_padding: [u8;5],// 已释放金额 native_qty_released: u64,// 已付金额 native_qty_paid: u64,// 交易费用 native_fee_or_rebate: u64,// 订单ID order_id: u128,// 所属用户 pubowner: [u64;4],// 客户端订单ID client_order_id: u64,}
OpenOrders:用户的订单薄,每个交易用户有一个OpenOrders。用于储存用户锁定未偿付订单或可偿付的基础货币以及报价货币金额;存储用户在该Market上的未完成订单列表。
pubstruct OpenOrders{// 状态标识 pubaccount_flags: u64,// Initialized, OpenOrders // 所属Market pubmarket: [u64;4],// 所属用户 pubowner: [u64;4],// 未锁定的基础货币余额 pubnative_coin_free: u64,// 基础货币总额 pubnative_coin_total: u64,// 未锁定的报价货币余额 pubnative_pc_free: u64,// 基础货币总额 pubnative_pc_total: u64,// 标识128个订单slot是否是空闲状态 pubfree_slot_bits: u128,// 标识128个订单是买单还是卖单 pubis_bid_bits: u128,// 订单数据,一个用户最多128个订单 puborders: [u128;128],// Using Option<NonZeroU64> in a pod type requires nightly // 对应的订单ID pubclient_order_ids: [u64;128],// 退费总额 pubreferrer_rebates_accrued: u64,}

三、 详细流程

创建Market

用户提交InitializeMarket指令创建一个交易对的Market,dex/src/state.rs函数process_initialize_market进行如下处理:

获取账户信息初始化Request Queue初始化Event Queue初始化订单薄存储,按Critbit Tree结构存储根据以上步骤获取的参数,初始化Market账户

如果参数有授权账户,则初始化为v2版Market无授权账户,则初始化为V1版

初始化Market时可以指定用户拥有以下特殊权限:

授权用户可以修改OpenOrders账户授权用户可以清空Market的订单授权用户可以发起结算操作

订单流程

1. 下单

用户提交NewOrderV3指令进行下单,指令内容包括:

订单详情:所属Market、订单金额、价格、订单类型、交易方向用户在该Market中的 OpenOrders账户如果是卖单需传入基本货币的账户,买单则传入报价货币的账户

指令接收后调用dex/src/state.rs的process_new_order_v3函数处理,流程如下:

计算转入与需要锁定的资金:

确定该订单所需的最大资金,如果是卖出则为订单金额,如果是买入则为金额*价格。检查用户OpenOrders中间帐户中指定的相应货币未锁定的余额是否满足交易金额。将OpenOrders帐户中相应货币的总余额加上从用户账户上转移的金额。从请求队列中增加序列号,根据订单价格与检索序列号生成新订单的ID。发送请求(内容为订单金额、价格、订单类型、交易方向)到撮合引擎进行交易撮合,撮合引擎将交易结果放入Event Queue,等待结算处理。将新订单添加到用户的OpenOrders账户数组的可用solt上。转账,则将所需金额和OpenOrders中间户中未锁定余额之间的差额从SPL账户转移到基础货币库或报价货币库。

上面的操作均是原子操作,任何单个步骤失败,则整个交易都会失败。 涉及从用户的SPL帐户转移资金到DEX的SPL资金账户的步骤使用跨程序调用,DEX转移资金需要用户授权后才能转移成功。

订单ID是由请求队列中的序列号和订单价格生存的唯一标识符,它是一个128位的数字,其中前64位是价格,后64位则是序列号(如果是买单,则反转所有位)。订单ID的顺序反映相对价格-时间优先级。

2. 撮合

撮合引擎维护了一个Orderbook结构,使用Critbit Tree分别存储买入、卖出订单,通过下单阶段生成的订单ID能快速地在树结构中找到相应的订单信息。

pubenum Side{// 买单 Bid=0,// 卖单 Ask=1,}pubenum OrderType{// 限价单 Limit=0,// 市价单 ImmediateOrCancel=1,PostOnly=2,}pubstruct OrderBookState<a>{// first byte of a key is 0xaa or 0xbb, disambiguating bids and asks // 买入订单 pubbids: &amutSlab,// 卖出订单 pubasks: &amutSlab,// market state pubmarket_state: &amutMarketState,}

用户提交NewOrderV3、CancelOrderV2指令都会触发撮合引擎,完成撮合后会生成相关Event放入Event Queue,并等待结算阶段处理。dex/src/matching.rs的process_orderbook_request函数是撮合引擎的入口函数,针对每次撮合将做如下的处理:

新订单:

Bid->买单,则执行买单的撮合操作

强制执行IOC和post-only类型的订单对于任何匹配成功的两个订单,生成两个相应的Fil类型Event并添加到Event Queue中对于未匹配成功的交易以及IOC订单未执行成功被取消的订单,生成Out类型Event添加到Event Queue中Ask->卖单,则执行卖单的撮合操作

强制执行IOC和post-only类型的订单对于任何匹配成功的两个订单,生成两个相应的Fil类型Event并添加到Event Queue中对于未匹配成功的交易以及IOC订单未执行成功被取消的订单,生成Out类型Event添加到Event Queue中取消订单:

根据订单ID在Orderbook的树结构上找到相应的叶子节点,并删除生成Out类型的Event放入Event Queue

放入Event Queue中两种类型的Event分别包含如下信息:

Fill:

交易方向(买入还是卖出)交易的发起人是不是Maker(是Maker有手续费奖励)该交易对手支付的数量(如果是买入,该数量为以报价货币计算)该交易对手收到的数量(如果是买入,该数量为以基准货币计算)该交易支付的费用(费用收取的是报价货币)Out:

交易方向(买入还是卖出)需要释放的资金数量剩余锁定在订单中的数量(0时未完全取消,非0时为部分取消)两种Event均包含订单ID和solt以及对应OpenOrders帐户的公钥

3. 结算

客户端监控Event Queue,若有新Event则收集受影响OpenOrders中间账户的公钥,并发送ConsumeEvents指令进行结算处理。该逻辑由dex/src/state.rs的process_consume_event函数实现,流程如下:

使用二分查找到该订单用户的OpenOrders账户,若未找到则处理结束,找到则继续下面的步骤从Event Queue中取出Event并处理:

Fill->撮合成功订单:

从OpenOrders帐户的总余额中减去已支付的数量,根据Event中的数量增加总余额和未锁定余额扣减交易费用,包括Maker奖励的手续费以及交易所收取的费用Out->撮合失败或取消订单:

解锁OpenOrders帐户中锁定的订单额度如果Event的指定锁定数量为零,则从OpenOrders帐户的订单列表中删除该订单。删除处理过的Event,继续下一个Event的处理

4. 偿付

用户可发送SettleFunds指令将OpenOrders中未锁定的资金提取到指定的SPL钱包。 dex/src/state.rs函数process_settle_funds进行该指令的处理,流程如下:

从Market的余额中减去要提取的金额从用户的OpenOrders账户中减去相应的金额计算转账要用的seeds,并执行转账操作若要转出奖励手续费,则转出手续费并扣减Market的奖励手续费

5. 取消订单

用户发送CancelOrderV2指令取消订单,调用dex/src/matching.rs的cancel_order_v2函数,从Orderbook中将订单删除,并发送取消的Event到Event Queue,执行结算中的Out流程。

6. 资金安全

资金账户:PDA地址,权限由程序控制,不存在被第三方恶意转出风险。

用户资金:用户授权后,程序才能转出用户钱包中的资金,不存在风险。

中间账户:源码中OpenOrders中间户开户的源码已经被删除,无法审计其安全性。若初始化过程中可修改中间户中可用余额,攻击者可以通过偿付流程恶意提取资金账户的资金。

四、参考文档

Solana官方文档:https://docs.solana.com/Serum使用文档:Welcome – SerumAwesome Serum: https://github.com/project-serum/awesome-serum

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注