基于内存戳和的区块链交易所合约

合约是基于 Chain33 区块链框架的交易所戳和合约,基于内存完成转账及买卖交易操作

约定

合约内所有数字都是以 1e8 为基数的整数,即现实生活中的 1 在合约内的值为 1000000000.001100000

协议

1
2
3
4
5
6
7
8
9
10
11
message WriteRequest {
oneof value{
RequestOrder order = 1;
RequestCancel cancel = 2;
RequestTransfer transfer = 3;
RequestFeeRate rate = 10;
RequestFroze froze = 13;
RequestUpgrade upgrade = 14;
}
int32 ty = 7;
}

交易所核心业务:

  • transfer:转帐,用户冲提币,合约内完成相应代币的资产转移操作,涉及到用户资产变更
  • order:用户挂单,合约内完成订单生成、戳和,涉及到用户资产变更、交易对深度变更、最新成交单变更
  • cancel:用户撤单,合约内完成订单撤销逻辑,涉及到用户资产变更、交易对深度变更
  • rate:管理员设置手续费率
  • froze:管理员冻结账户资产
  • upgrade:合约升级

账户模型

  • 一个账户 Account 包含多个资产子账户 Balance(现货交易账户、杠杠交易账户、交割合约账户、永续合约账户、托管合约账户)
  • 每个资产子账户 Balance 包含多个币种的资产 WalletInfo
  • 每个币种的资产 WalletInfo 包括活跃资产、冻结资产
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
type Account struct {
balances sync.Map `json:"balances"` // map[int32]*Balance walletId --> Balance
Id []byte `json:"id"` // 账户pub key
Rate int32 `json:"rate"` // 手续费率, 0 代表交易免手续费
Role int32 `json:"role"` // 账户角色,0:member普通账户 1:bank 银行账户 2:admin 管理员账户
Gid int64 `json:"gid"` // 账户所属组,默认都是 0(兼容老版经济商相关逻辑)
}

type Balance struct {
SubAcc WalletType
Wallets []*WalletInfo
}

enum WalletType {
WalletSpot = 0; // 现货账户
WalletLeverage = 1; // 杠杠账户
WalletFuture = 2; // 交割合约账户
WalletPerpetual = 3; // 永续合约账户
WaleltEscrow = 4; // 托管账户
}

type WalletInfo struct {
Frozen int64
Active int64
Currency int32
}

存储结构

交易在合约内执行 Exec 逻辑返回的结果都是 event 列表,event 最终持久化到 state dblocal db 中,各种 event 定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
message Event {
oneof value{
EventOrderPlaced orderPlaced = 1;
EventOrderClosed orderClosed = 2;
EventOrderClosed orderCanceled = 3;
EventMtf mtf = 4;
EventAccount account = 5;
EventMatch match = 6;
EventReceipt receipt = 7;
EventTransfer tranfer = 14;
}
}

state db (datadir/mavltree/store.db) 中的存放的是账户及资产信息 EventAccount、公用的匹配信息 EventMatch

1
2
3
4
5
6
7
.-mvcc-.d.mavl-exchange-acc:4a246cd2a3f41b2bc1d071d2db159a388cd6f5c3547ea592d401f270073133d7.00000000000000000002
.-mvcc-.d.mavl-exchange-acc:deec43814b9985591a53670e3a124d963ead797752e1263c1500be46446c6698.00000000000000000020
.-mvcc-.d.mavl-exchange-matchcommon.00000000000000000002
.-mvcc-.d.mavl-exchange-matchcommon.00000000000000000020
.-mvcc-.l.mavl-exchange-acc:0e03d4dc2289fdaf2a875e3d6438067eed4f3328d55a1fd7bcf4fd93c6b5ccfc
.-mvcc-.l.mavl-exchange-acc:4a246cd2a3f41b2bc1d071d2db159a388cd6f5c3547ea592d401f270073133d7
.-mvcc-.l.mavl-exchange-matchcommon

localdb (datadir/blockchain.db) 中的存放的是活跃订单 orderPlaced 、历史订单 orderCanceled + orderClosed、成交信息 mtf、交易回报信息 receipt、资产转账记录 transfer:

1
2
3
4
5
6
7
8
9
closeorder:0000131087:6bdb231b0aa00f91261e5056c0c01b14327703230115715ab83894cc43c89d66:00000000000000000015
closeorder:0000131087:f6e008a45ffc21a902ed34b4b4de04b743e20677855cc278c70192fb1f476f34:00000000000000000019
LODB-exchange-mtf:0000131087:00000000000000000001
LODB-exchange-mtf:0000131087:00000000000000000002
LODB-exchange-openorder:0000131087:00000000000000000015
LODB-exchange-openorder:0000131087:00000000000000000018
LODB-exchange-openorder:0000131087:00000000000000000019
LODB-exchange-receipt:-0308565501158954506
LODB-exchange-receipt:-0321152478071390370

初始化

合约在内存中维护了一个全局的单例戳和对象 ts,Check、Exec、Query、ExecLocal 之前都要保证该单粒对象已初始化且只初始化一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
type TradeServer struct {
coins map[int32]*etypes.Coin
trades map[string][]etypes.Trade // orderType --> trades
matches map[int32]*matcher.Match // symbolId --> Match
common *matcher.MatchCommon
}

type Match struct {
buy *SkipList //买盘
sell *SkipList //卖盘
orders map[int64]*list.Element // orderId --> Order
symbolQuery etypes.ISymbol
currentMtf *ring.Ring //最近50条成交记录
common *MatchCommon
bank etypes.IAccount
minFee int64
}

//公共的匹配信息,每个货币对共用
type MatchCommon struct {
openOrders sync.Map // uid --> OpenOrderList
groups sync.Map // gid --> Group
accounts sync.Map // uid --> Account
curTime int64 // 最新交易所在区块时间
mtfId int64 // 最新成交记录ID
lastExec int64 // 最新交易记录ID
}

TradeServer 初始化方法 trade.Init

  • 加载配置文件 trade.toml
  • 从 state db 加载并初始化全局戳和对象 MatchCommon
  • 从 local db 加载并初始化各交易对戳和对象 Match

交易检查

  • 签名验证:合约内用 uid 标示这笔交易的发起者,采用 ed25519 算法进行签名和验签。
  • 交易查重:合约内部每笔交易都有一个 instructionId 标识别本次操作, 除了框架本身会根据 hash 进行查重,合约内部还会根据 instructionId 进行查重(考虑后续版本去掉合约内查重,提升交易性能)。

转账

合约内的代币都是由 admin 账户创建,admin 分发给各个 bank:

  • 代币生成: 从 admin 转到 bank
  • 用户冲币: 从 bank 转到用户
  • 用户提币: 从用户转到 bank

此外,还支持:

  • bank 间转账:
    • admin 生成的代币,转到一个大 bank,方便财务人员对交易所内的代币源头进行管控
    • 大 bank 分发代币给各个业务模块(冲币、提币、矿机、手续费、机器人等)的小 bank
  • 普通用户间转账:方便用户站内冲提币

下单

流程如下:

戳和

  • 买卖盘是以价格为 score 的跳跃表 SkipList,买盘按照价格从高到低,卖盘按照价格由低到高
  • 相同价格的订单按照挂单时间先后顺序构成 List
  • MatchCommon 记录的用户活跃订单表中,每个用户的活跃订单是 SkipList

交易对规则

以 BTC/USDT 为例,BTC 为目标币,USDT 为基础货币:

  • 挂买:花费 USDT,得到 BTC
  • 挂卖:花费 BTC,得到 USDT

戳和规则

按照 价格优先、时间优先 顺序

手续费规则

手续费率默认为千分之一(管理员可设置用户的手续费率),对买卖双方都收取基础货币作为手续费。

最低手续费:只要订单有成交,则该笔订单收取的手续费必须不少于最低手续费,不同基础货币的最低手续费根据配置略有不同。比如 USDT 交易对的最低手续费为 0.01 USDT, BTC 交易对的最低手续费为 0.000001 BTC。

假设某用户的手续费率为 0.001, 挂单价格为 P,挂单量为 A,则花费 C=P*A,手续费 F=Min(MinFee, 0.001*C)=Min(MinFee, 0.001*P*A)

  • 买单:挂单时冻结花费和手续费(>=最低手续费),即实际须冻结 B=C+F,用户相关交易对的基础币可用资产必须不少于B
  • 卖单:挂单时冻结 A 数量的目标币,用户相关交易对的目标币可用资产必须不少于 A

虽然最终买卖双方都被平台抽取了手续费(>=最低手续费),买单和卖单对手续费的处理方式略有不同:

  • 对买方而言,挂单时已经把该笔订单的手续费冻结进去了
  • 对卖方而言,挂单只需冻结对应数量的目标币即可,手续费是在戳和成交时才收取
彦祖老师 wechat