订单生命周期:从创建到消亡的完整旅程
做链上订单簿,说白了就是管理订单的「生老病死」。
我刚开始接触这个领域时,觉得订单不就是挂上去、成交、取消嘛,能有多复杂?直到我在一个项目中遇到内存池拥堵导致的订单状态混乱,才意识到——订单状态机设计不好,整个系统都会崩。
今天咱们就聊聊订单的完整生命周期。我会结合自己踩过的坑,把每个环节掰开揉碎了讲。
订单状态机:核心骨架
先看一张我画的订单状态流转图。这张图我每次做新项目都会贴在墙上——
这张图我看了不下百遍。每个箭头背后都有一段故事。
创建订单:一切从这里开始
创建订单,就是把用户的意图「刻」到链上。
用户说「我想用 2000 USDC 买 1 个 ETH,单价 2000」,我们就得把这个意图打包成一笔交易发出去。
代码层面,核心逻辑是这样的——
function createOrder(
address maker,
uint256 price,
uint256 amount,
bool isBuy
) external returns (uint256 orderId) {
// 检查用户余额
require(balanceOf[maker] >= amount, "余额不足");
// 锁定资产
_lockAsset(maker, amount);
// 生成订单ID
orderId = nextOrderId++;
// 存储订单
orders[orderId] = Order({
maker: maker,
price: price,
amount: amount,
filled: 0,
isBuy: isBuy,
status: OrderStatus.ACTIVE,
createdAt: block.timestamp
});
// 插入价格队列
_insertIntoOrderBook(orderId, price, isBuy);
emit OrderCreated(orderId, maker, price, amount, isBuy);
}
取消订单:不是想撤就能撤
取消订单看似简单,但有几个坑要注意。
首先,只有订单创建者才能取消。这个权限检查不能少。
其次,取消时要归还锁定的资产。我见过有人忘了这步,资产直接锁死在合约里——
function cancelOrder(uint256 orderId) external {
Order storage order = orders[orderId];
// 权限检查
require(order.maker == msg.sender, "不是你的订单");
// 状态检查
require(order.status == OrderStatus.ACTIVE, "订单已结束");
// 更新状态
order.status = OrderStatus.CANCELLED;
// 归还资产
_unlockAsset(order.maker, order.amount - order.filled);
// 从订单簿移除
_removeFromOrderBook(orderId, order.price, order.isBuy);
emit OrderCancelled(orderId);
}
部分成交:最常见的状态
说实话,完全成交的订单其实不多。大部分订单都是部分成交,然后慢慢「啃」完。
为什么会这样?因为链上流动性是碎片化的。你挂一个 100 ETH 的买单,可能对面只有 0.5 ETH 的卖单。那就先吃 0.5,剩下的继续挂着。
部分成交的逻辑——
function _matchOrder(uint256 takerOrderId, uint256 makerOrderId, uint256 matchAmount) internal {
Order storage makerOrder = orders[makerOrderId];
// 更新已成交数量
makerOrder.filled += matchAmount;
// 判断是否完全成交
if (makerOrder.filled >= makerOrder.amount) {
makerOrder.status = OrderStatus.FULLY_FILLED;
_removeFromOrderBook(makerOrderId, makerOrder.price, makerOrder.isBuy);
}
// 转移资产
_transferAsset(makerOrder.maker, takerOrderId, matchAmount);
}
关键点:部分成交时,订单状态仍然是 ACTIVE,只是 filled 字段增加了。直到 filled == amount,才变成 FULLY_FILLED。
完全成交:订单的终点
完全成交意味着订单的 amount 全部被吃掉了。这时候要做三件事——
- 更新状态为 FULLY_FILLED
- 从订单簿中移除
- 释放所有锁定的资产(其实已经转移走了)
我习惯在完全成交时触发一个事件,方便前端做 UI 更新——
event OrderFullyFilled(
uint256 indexed orderId,
address indexed maker,
uint256 totalAmount,
uint256 totalValue
);
订单状态机:一张表说清楚
| 当前状态 | 触发事件 | 下一状态 | 备注 |
|---|---|---|---|
| PENDING | 交易确认 | ACTIVE | 创建后立即进入 |
| ACTIVE | 部分匹配 | ACTIVE | filled 增加,状态不变 |
| ACTIVE | 完全匹配 | FULLY_FILLED | 订单完成 |
| ACTIVE | 用户取消 | CANCELLED | 归还剩余资产 |
| ACTIVE | 超时 | EXPIRED | 可选功能 |
实战中的几个细节
说几个我实际项目中遇到的坑——
- Gas 优化:创建订单时,尽量把数据打包成 struct 存储,减少 storage 操作次数。我见过有人每个字段单独存,Gas 费直接翻倍。
- 重入攻击:取消订单时,先更新状态再转账。顺序反了就可能被重入攻击。Solidity 的 Checks-Effects-Interactions 模式不是白说的。
- 精度问题:部分成交时,如果金额不能整除,剩下的零头怎么处理?我习惯把零头退还给 maker,而不是吃掉。公平第一。
订单生命周期,说白了就是管理好「创建→匹配→成交/取消」这条线。状态机设计好了,后面的事情就顺了。
你想想看,如果状态机有漏洞,用户就能钻空子。轻则损失手续费,重则合约被掏空。所以这块一定要慎之又慎。
公众号:蓝海资料掘金营,微信deep3321