Разработка DEX на базе order book
Order book DEX — это децентрализованная биржа, которая использует модель книги ордеров вместо AMM-пула. Трейдеры выставляют limit ордера, matching engine сводит их. Принципиальный вопрос: где живёт order book и где происходит matching? Ответ определяет всю архитектуру.
Три модели order book DEX
Fully on-chain order book
Order book хранится и матчится прямо в смарт-контракте. Каждый ордер — транзакция. Каждый матч — транзакция.
Проблемы:
- Стоимость газа: выставить ордер = транзакция. Отменить = транзакция. Заполнить = транзакция. Трейдеры платят газ за каждое действие, включая ордера которые никогда не исполнились
- Latency: блок Ethereum = ~12 секунд. Нельзя "быстро" скальпировать
- Front-running: все ордера видны в mempool
Применимо только для: аукционов, batch settlement систем, низкочастотной торговли
Off-chain order book + on-chain settlement
Это доминирующая модель. Order book и matching — off-chain (быстро, бесплатно). Settlement финального трейда — on-chain через смарт-контракт.
Примеры: dYdX v3 (Starkware), Injective Protocol, Serum (Solana)
Hybrid: off-chain orders + on-chain matching
Ордера подписываются off-chain (EIP-712), хранятся в off-chain orderbook, но matching выполняется on-chain при settlement. Пример: 0x Protocol, Hashflow.
Преимущество: maker не платит газ за выставление ордера — только за исполнение. Это критично для market makers.
Смарт-контракты для settlement
EIP-712 подписи ордеров
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract OrderBookSettlement {
bytes32 public constant ORDER_TYPEHASH = keccak256(
"Order(address maker,address taker,address makerToken,address takerToken,"
"uint256 makerAmount,uint256 takerAmount,uint256 nonce,uint256 expiry)"
);
struct Order {
address maker;
address taker; // address(0) = any taker
address makerToken;
address takerToken;
uint256 makerAmount;
uint256 takerAmount;
uint256 nonce;
uint256 expiry;
}
mapping(address => mapping(uint256 => bool)) public usedNonces;
mapping(bytes32 => uint256) public filledAmounts; // partial fills
function fillOrder(
Order calldata order,
bytes calldata signature,
uint256 takerFillAmount // для partial fills
) external {
require(block.timestamp < order.expiry, "ORDER_EXPIRED");
require(
order.taker == address(0) || order.taker == msg.sender,
"INVALID_TAKER"
);
bytes32 orderHash = getOrderHash(order);
require(
filledAmounts[orderHash] + takerFillAmount <= order.takerAmount,
"OVERFILL"
);
// Verify EIP-712 signature
address recovered = recoverSigner(orderHash, signature);
require(recovered == order.maker, "INVALID_SIGNATURE");
// Calculate maker amount proportional to partial fill
uint256 makerFillAmount = (order.makerAmount * takerFillAmount) / order.takerAmount;
filledAmounts[orderHash] += takerFillAmount;
// Atomic swap
IERC20(order.takerToken).transferFrom(msg.sender, order.maker, takerFillAmount);
IERC20(order.makerToken).transferFrom(order.maker, msg.sender, makerFillAmount);
emit OrderFilled(orderHash, order.maker, msg.sender, makerFillAmount, takerFillAmount);
}
}
Ключевые security аспекты:
-
filledAmountsотслеживает частичные заполнения — ордер можно заполнять постепенно -
usedNoncesпредотвращает replay атаки -
expiryобязателен — без него maker не может контролировать когда ордер исполнится -
transferFromтребует предварительногоapproveот обеих сторон
Permit2 (Uniswap)
Проблема стандартного approve: пользователь должен отправить отдельную транзакцию approve перед каждым взаимодействием с контрактом. Permit2 (UniswapLabs) решает это:
// Permit2: подписанное разрешение без on-chain approve транзакции
struct PermitTransferFrom {
TokenPermissions permitted;
uint256 nonce;
uint256 deadline;
}
// Maker подписывает разрешение вместе с ордером — одна подпись = всё
Это улучшает UX для maker-ов: они подписывают ордер + permit одной EIP-712 подписью, без предварительных approve транзакций.
Off-chain matching engine
Архитектура
Matching engine для order book DEX — это высокопроизводительный сервис, похожий на CEX matching, но с особенностями:
- Ордера не хранятся on-chain — они существуют как подписанные сообщения
- Частичное заполнение должно отслеживаться off-chain И on-chain (through filledAmounts mapping)
- Отмена ордера = либо on-chain nonce инвалидация, либо off-chain "cancel" с подтверждением от maker-а
from dataclasses import dataclass
from decimal import Decimal
from typing import Optional
import asyncio
@dataclass
class SignedOrder:
maker: str
taker_token: str
maker_token: str
taker_amount: Decimal
maker_amount: Decimal
nonce: int
expiry: int
signature: str
@property
def price(self) -> Decimal:
"""Цена в единицах maker_token за taker_token"""
return self.maker_amount / self.taker_amount
@property
def is_expired(self) -> bool:
import time
return time.time() > self.expiry
class OffChainOrderBook:
def __init__(self, pair: str):
self.pair = pair
self.bids: list[SignedOrder] = [] # buy orders, sorted by price DESC
self.asks: list[SignedOrder] = [] # sell orders, sorted by price ASC
self._settlement_queue = asyncio.Queue()
async def add_order(self, order: SignedOrder, side: str):
if side == 'bid':
self.bids.append(order)
self.bids.sort(key=lambda x: x.price, reverse=True)
else:
self.asks.append(order)
self.asks.sort(key=lambda x: x.price)
await self.try_match()
async def try_match(self):
while self.bids and self.asks:
best_bid = self.bids[0]
best_ask = self.asks[0]
if best_bid.is_expired:
self.bids.pop(0)
continue
if best_ask.is_expired:
self.asks.pop(0)
continue
if best_bid.price >= best_ask.price:
# Match found
fill_amount = min(best_bid.taker_amount, best_ask.taker_amount)
await self._settlement_queue.put({
'bid': best_bid,
'ask': best_ask,
'fill_amount': fill_amount
})
# Update or remove filled orders
best_bid.taker_amount -= fill_amount
best_ask.taker_amount -= fill_amount
if best_bid.taker_amount == 0:
self.bids.pop(0)
if best_ask.taker_amount == 0:
self.asks.pop(0)
else:
break
Settlement батчинг
On-chain settlement дорогой. Стратегия: батчить матчи и отправлять несколько заполнений в одной транзакции.
function fillOrderBatch(
Order[] calldata orders,
bytes[] calldata signatures,
uint256[] calldata fillAmounts
) external {
require(orders.length == signatures.length, "LENGTH_MISMATCH");
for (uint256 i = 0; i < orders.length; i++) {
fillOrder(orders[i], signatures[i], fillAmounts[i]);
}
}
Батч из 10 заполнений экономит ~70% газа по сравнению с 10 отдельными транзакциями (overhead фиксирован, execution дешевле).
L2 и appchain подходы
dYdX v4 архитектура: cosmos appchain
dYdX v4 перешёл с Starkware L2 на собственный Cosmos SDK appchain. Order book и matching — полностью off-chain на validators. On-chain только финальные settlements.
Преимущества: нулевые fees для трейдеров (газ платит протокол из trading fees), <1ms matching latency, полная децентрализация matching (распределён между validators).
Arbitrum / zkSync L2 подход
Deploying on L2 снижает стоимость газа в 10-100 раз. Для order book DEX это означает: on-chain order placement становится экономически целесообразным для ордеров >$100.
Пример: Lighter.xyz построен на Arbitrum, использует fully on-chain matching, достигая ~1,000 orders/sec при cost ~$0.01/order.
Starkware / zkProofs
dYdX v3 использовал StarkEx: matching off-chain, validity proofs submitted on-chain. ZK-proof подтверждает корректность всех операций без необходимости их replay на-chain. Масштабируемость: 10,000+ trades/sec с on-chain security.
Ликвидность: проблема холодного старта
Order book DEX без market makers = пустой стакан. Решения:
Integrations с AMM: если нет market maker на какой-то паре — router автоматически исполняет через AMM-пул. Гибридная модель: order book + AMM fallback.
Market maker программа: специальные условия для designated market makers — сниженные fees, кредитная линия, приоритет в matching.
RFQ (Request for Quote): вместо публичного order book — запрос котировки напрямую к зарегистрированным market makers. Используется Hashflow, Airswap. Лучший execution для крупных ордеров.
Трейдер → broadcast RFQ → Market Makers
↓
Market Makers → ответные котировки (подписанные off-chain)
↓
Трейдер → выбирает лучшую → on-chain settlement
RFQ идеально подходит для institutional-размера ордеров, где AMM slippage неприемлем.
Сравнение с AMM
| Критерий | Order Book DEX | AMM DEX |
|---|---|---|
| Capital efficiency | Высокая (нет idle ликвидности) | Низкая (V2) / Высокая (V3) |
| UX для трейдеров | Familiar, limit orders | Проще, только market |
| Market making | Нужны профессионалы | Доступно всем LP |
| Front-running risk | Высокий (без protect) | Средний (sandwich) |
| Latency | Зависит от архитектуры | Один блок |
| Сложность | Высокая | Средняя |
Для большинства DeFi-проектов AMM проще в запуске. Order book DEX стоит строить если: целевая аудитория — профессиональные трейдеры, нужны limit orders, или работаете на L2/appchain с низкими fees.







