| id | TIP-1003 |
|---|---|
| title | Client order IDs |
| description | Addition of client order IDs to the Stablecoin DEX, allowing users to specify their own order identifiers for idempotency and easier order tracking. |
| authors | Dan Robinson |
| status | Draft |
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.
Traditional exchanges allow users to specify a client order ID (called ClOrdID in FIX protocol, cloid in Hyperliquid) for several reasons:
- Idempotency: If a transaction is submitted twice (e.g., due to network issues), the duplicate can be detected and rejected
- Predictable reference: Users know the order identifier before the transaction confirms, enabling them to prepare cancel requests or track orders without waiting for confirmation
- Integration: External systems can use their own ID schemes to correlate orders
A new mapping tracks active client order IDs per user:
mapping(address user => mapping(uint128 clientOrderId => uint128 orderId)) public clientOrderIds;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);/// @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);When clientOrderId is non-zero:
- Check if
clientOrderIds[msg.sender][clientOrderId]maps to an active order - If it does, revert with
DUPLICATE_CLIENT_ORDER_ID - Otherwise, proceed with order placement and set
clientOrderIds[msg.sender][clientOrderId] = orderId
When clientOrderId is zero, no client order ID tracking occurs.
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.
When a flip order is filled and creates a new order on the opposite side:
- The new (flipped) order inherits the original order's
clientOrderId - The
clientOrderIdsmapping is updated to point to the new order ID - 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.
cancelByClientOrderId(clientOrderId) looks up clientOrderIds[msg.sender][clientOrderId] and cancels that order. It reverts if no active order exists for that clientOrderId.
/// @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).
/// @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();- A non-zero
clientOrderIdmaps to at most one active order per user clientOrderIds[user][clientOrderId]is cleared when the order is filled or cancelled- Flip orders inherit
clientOrderIdand update the mapping atomically clientOrderId = 0is reserved to mean "no client order ID"