Skip to content
LogoLogo

TIP-1003: Client order IDs

Abstract

This TIP adds support for optional client order IDs (clientOrderId) to the Stablecoin DEX. Users can specify a uint128 identifier when placing orders, which serves as an idempotency key and a predictable handle for the order. The system-generated orderId is not predictable before transaction execution, making client order IDs useful for order management.

Motivation

Traditional exchanges allow users to specify a client order ID (called ClOrdID in FIX protocol, cloid in Hyperliquid) for several reasons:

  1. Idempotency: If a transaction is submitted twice (e.g., due to network issues), the duplicate can be detected and rejected
  2. Predictable reference: Users know the order identifier before the transaction confirms, enabling them to prepare cancel requests or track orders without waiting for confirmation
  3. Integration: External systems can use their own ID schemes to correlate orders

Specification

New storage

A new mapping tracks active client order IDs per user:

mapping(address user => mapping(uint128 clientOrderId => uint128 orderId)) public clientOrderIds;

Modified functions

All order placement functions gain an optional clientOrderId parameter:

/// @notice Places an order with an optional client order ID
/// @param token The base token of the pair
/// @param amount The order amount in base tokens
/// @param isBid True for buy orders, false for sell orders
/// @param tick The price tick for the order
/// @param clientOrderId Optional client-specified ID (0 for none)
/// @return orderId The system-assigned order ID
function place(
    address token,
    uint128 amount,
    bool isBid,
    int16 tick,
    uint128 clientOrderId
) external returns (uint128 orderId);
 
/// @notice Places an order on a specific pair with an optional client order ID
/// @dev Overload from TIP-1001
function place(
    bytes32 bookKey,
    address token,
    uint128 amount,
    bool isBid,
    int16 tick,
    uint128 clientOrderId
) external returns (uint128 orderId);
 
/// @notice Places a flip order with an optional client order ID
function placeFlip(
    address token,
    uint128 amount,
    bool isBid,
    int16 tick,
    int16 flipTick,
    bool internalBalanceOnly,
    uint128 clientOrderId
) external returns (uint128 orderId);
 
/// @notice Places a flip order on a specific pair with an optional client order ID
/// @dev Overload from TIP-1001
function placeFlip(
    bytes32 bookKey,
    address token,
    uint128 amount,
    bool isBid,
    int16 tick,
    int16 flipTick,
    bool internalBalanceOnly,
    uint128 clientOrderId
) external returns (uint128 orderId);

New functions

/// @notice Cancels an order by its client order ID
/// @param clientOrderId The client-specified order ID
function cancelByClientOrderId(uint128 clientOrderId) external;
 
/// @notice Gets the system order ID for a client order ID
/// @param user The user who placed the order
/// @param clientOrderId The client-specified order ID
/// @return orderId The system-assigned order ID, or 0 if not found
function getOrderByClientOrderId(address user, uint128 clientOrderId) external view returns (uint128 orderId);

Behavior

Placing orders with clientOrderId

When clientOrderId is non-zero:

  1. Check if clientOrderIds[msg.sender][clientOrderId] maps to an active order
  2. If it does, revert with DUPLICATE_CLIENT_ORDER_ID
  3. Otherwise, proceed with order placement and set clientOrderIds[msg.sender][clientOrderId] = orderId

When clientOrderId is zero, no client order ID tracking occurs.

Uniqueness and reuse

A clientOrderId must be unique among a user's active orders. Once an order is filled or cancelled, its clientOrderId can be reused. This matches the standard FIX protocol behavior where ClOrdID uniqueness is required only for working orders.

When an order reaches a terminal state (filled or cancelled), the clientOrderIds mapping entry is cleared.

Flip orders

When a flip order is filled and creates a new order on the opposite side:

  1. The new (flipped) order inherits the original order's clientOrderId
  2. The clientOrderIds mapping is updated to point to the new order ID
  3. This allows users to track their position across flips using a single clientOrderId

If the original order had no clientOrderId (was zero), the flipped order also has no clientOrderId.

Cancellation

cancelByClientOrderId(clientOrderId) looks up clientOrderIds[msg.sender][clientOrderId] and cancels that order. It reverts if no active order exists for that clientOrderId.

New event

/// @notice Emitted when an order is placed (V2 with clientOrderId)
/// @dev Replaces OrderPlaced for new orders
event OrderPlacedV2(
    uint128 indexed orderId,
    address indexed maker,
    address token,
    uint128 amount,
    bool isBid,
    int16 tick,
    bool isFlipOrder,
    int16 flipTick,
    uint128 clientOrderId
);

OrderPlacedV2 is identical to OrderPlaced but adds the clientOrderId field. When an order is placed, only OrderPlacedV2 is emitted (not both events).

New errors

/// @notice The client order ID is already in use by an active order
error DUPLICATE_CLIENT_ORDER_ID();
 
/// @notice No active order found for the given client order ID
error CLIENT_ORDER_ID_NOT_FOUND();

Invariants

  • A non-zero clientOrderId maps to at most one active order per user
  • clientOrderIds[user][clientOrderId] is cleared when the order is filled or cancelled
  • Flip orders inherit clientOrderId and update the mapping atomically
  • clientOrderId = 0 is reserved to mean "no client order ID"