Appearance
L3-5: Order Book(链上订单簿)
1. 问题
订单簿是传统交易所(纳斯达克、NYSE、币安)的核心基础设施,但在以太坊主网上直接实现面临严重的 Gas 障碍。一个活跃的订单簿每秒有数千次挂单、撤单和成交,如果每笔操作都需要 L1 交易,Gas 成本将使小额交易者无法参与。AMM(自动做市商)通过恒定乘积公式巧妙地规避了这个问题,但 AMM 在大额交易中存在滑点问题,且价格发现效率不如订单簿。
如何在链上实现限价订单簿,同时控制 Gas 成本?核心挑战在于:挂单者(maker)需要锁定代币以防止虚假挂单,吃单者(taker)需要找到最优价格的对手方,而撮合过程需要原子化执行以保护双方利益。
2. 原因
随着 L2 的成熟(Base、Arbitrum、Optimism 上 Gas 费低至 $0.01),链上订单簿重新变得可行。相比于 AMM,订单簿提供更精确的价格控制(限价单)、更低的滑点(特别是大额交易),以及更灵活的订单类型(止损单、冰山单等)。理解链上订单簿的设计模式是与中心化交易所交互、构建聚合器和链上衍生品的基础。
订单簿 + AMM 的混合模型正在成为行业趋势:Uniswap V4 引入了 hooks 以支持限价单,CoWSwap 使用链下订单簿 + 链上批量结算。掌握订单簿模式是理解下一代 DEX 架构的关键。
3. 方案
OnChainOrderBook.sol 实现了支持部分成交的限价订单簿。
数据结构:Order 结构体记录 maker(挂单者)、baseToken/quoteToken(交易对)、side(BUY/SELL)、price(价格,精度 1e18)、amount(总数量)、filled(已成交数量)和 status(ACTIVE/PARTIALLY_FILLED/FILLED/CANCELLED)。
挂单(Place Order):placeOrder() 由 maker 调用。挂卖单时,maker 锁定 baseToken;挂买单时,maker 锁定 quoteToken(买入总额 = amount * price / 1e18)。代币在成交或取消前托管在合约中。
吃单(Fill Order):fillOrder() 由 taker 调用,指定要成交的订单 ID 和数量。支持部分成交——如果 fillAmount < remaining,订单状态变为 PARTIALLY_FILLED。资产原子化交换:
- 吃买单:taker 发送 baseToken 给 maker,从托管中接收 quoteToken
- 吃卖单:taker 发送 quoteToken 给 maker,从托管中接收 baseToken
撤单(Cancel Order):cancelOrder() 由 maker 调用,仅对 ACTIVE 或 PARTIALLY_FILLED 状态的订单有效。未成交部分的托管代币退还给 maker。
参考源文件:src/level3/OnChainOrderBook.sol
4. 遭遇的陷阱
- 价格优先级缺失:合约本身不保证价格-时间优先级,任何人都可以任意顺序吃单,可能导致最优价格的订单被跳过
- 部分成交后的退款计算:撤单时退还未成交数量的代币,但买单的退款需要按比例计算
(unfilled * price) / 1e18,整数除法可能导致 1 wei 的舍入差异 - 自成交风险:吃单检查中没有明确禁止 maker == taker,可能导致无意义的自成交和 Gas 浪费
- 订单可被抢跑:恶意行为者可以监听 mempool 中的吃单交易,在 taker 之前提交相同价格的吃单(优先级 Gas 拍卖)
- 订单簿无排序机制:没有有效的方式来按价格排序订单,taker 需要链下扫描所有订单来找到最优价格
5. 陷阱的原因
价格优先级问题是链上订单簿的根本性挑战。在中心化交易所,撮合引擎运行在内存中,可以毫秒级对订单簿进行排序和匹配。在链上,所有数据存储在 EVM 状态中,没有原生的排序数据结构(没有堆、优先队列等)。这意味着最优价格发现必须在链下完成——链下服务扫描订单簿,找到最优价格的订单,然后指导用户在链上执行。
这引入了信任假设:用户需要信任链下服务不会提供错误或次优的价格。在 OnChainOrderBook.sol 中,fillOrder() 接受任意 _orderId,不做任何价格验证。实际上,生产级实现应该允许 taker 传入期望的价格范围,由合约验证被吃的订单价格确实在范围内。
抢跑问题在 AMM 中同样存在,但在订单簿中更严重——因为订单是公开的,攻击者可以扫描所有挂单,发现有利可图的吃单机会后抢先提交。缓解措施包括:使用批量拍卖(batch auction)模型,将多个订单聚合后在同一区块内统一结算;或者使用密封订单(sealed orders)防止 mempool 可见。
6. 如何解决陷阱
链下撮合 + 链上验证:构建链下撮合服务,按价格排序后指导用户吃单。在合约中增加价格范围验证:
solidity
function fillOrder(uint256 _orderId, uint256 _fillAmount, uint256 _maxPrice, uint256 _minPrice) external {
Order storage order = orders[_orderId];
require(order.price >= _minPrice && order.price <= _maxPrice, "Price out of bounds");
// ... 后续逻辑
}
防自成交:
solidity
require(order.maker != msg.sender, "Cannot fill own order");
批量拍卖:对于高活跃度的交易对,考虑使用定期批量结算——收集一段时间内的所有吃单和挂单需求,在区块内统一计算最优匹配和清算价格。CoWSwap 的解决方案就是这种模式。
L2 优化:在 L2 上部署订单簿获得秒级区块时间和亚美分级 Gas 费,使链上订单簿在经济上可行。同时可以利用 L2 的排序器提供的预确认(pre-confirmation)来实现更好的用户体验。
7. 技术要点
| 技术点 | 说明 |
|---|---|
| Maker/Taker 模型 | Maker 提供流动性(挂单),Taker 消费流动性(吃单) |
| 托管机制 | 挂单时锁定代币,防止虚假挂单 |
| 部分成交 | amount - filled 追踪剩余数量 |
| 四状态 | ACTIVE → PARTIALLY_FILLED → FILLED/CANCELLED |
| 双代币对 | baseToken(标的)+ quoteToken(计价) |
| 价格精度 | 1e18 缩放,类似 Uniswap 的 Q64.96 格式 |
| 用户追踪 | userOrders 映射维护每个用户的订单列表 |
| 价格发现 | 链下扫描 + 链上验证(生产环境推荐) |