Stablecoin DEX
Abstract
This specification defines an enshrined decentralized exchange for trading between TIP-20 stablecoins. The exchange currently only supports trading between TIP-20 stablecoins with USD as their currency. By only allowing each stablecoin to be paired against its designated "quote token" the exchange enforces that there is only one route for trading between any two tokens.
The exchange maintains price‑time priority at each discrete price tick, defers order placement to the end of the block, executes swaps immediately against the active book, and supports auto‑replenishing “flip orders” that recreate themselves on the opposite side after being fully filled.
Users maintain internal balances per token on the exchange. Order placement escrows funds from these balances (or transfers from the user if necessary), fills credit makers internally, and withdrawals transfer tokens out.
Motivation
Tempo aims to provide high‑quality execution for cross‑stablecoin payments while avoiding unnecessary chain load and minimizing mid‑block MEV surfaces.
A simple, on‑chain, price‑time‑priority orderbook tailored to stable pairs encourages passive liquidity to rest on chain and allows takers to execute deterministically at the best available ticks. Deferring new order placement to the end of the block reduces backrunning and JIT strategies.
Another design goal is to avoid fragmentation of liquidity across many different pairs. By enforcing that each stablecoin only trades against a single quote token, the system guarantees that there is only one path between any two tokens.
Specification
Contract and scope
The exchange is a singleton contract deployed at 0xdec0000000000000000000000000000000000000. It exposes functions to create trading pairs, place and cancel orders (including flip orders), execute swaps, produce quotes, manage internal balances, and process end‑of‑block placement.
Places are delayed until the end of each block, and are processed in an executeBlock() function. This function can only be called by a system transaction.
Key concepts
Internal balances
The contract maintains per‑user, per‑token internal balances. Order placement escrows funds from these balances (or transfers any shortfall from the user). When an order fills, the maker is credited internally with the counter‑asset at the order’s tick price. Users can withdraw available balances at any time.
Delayed order placement
Orders placed during a block are recorded as pending and are not visible on the active book until the block ends. A system transaction calls executeBlock() to insert all pending orders into their ticks in creation order within the block. This reduces mid‑block MEV surfaces and JIT strategies.
Flip orders
A flip order behaves like a normal resting order until it is fully filled. When filled, the exchange schedules a new pending order for the same maker on the opposite side at a configured flipTick (which must be greater than tick for bids and less for asks). This enables passive liquidity with flexible strategies.
Pairs, ticks, and prices
Pairs are identified deterministically from the two token addresses (the base token is any TIP‑20, and its quoteToken() function points to the quote token). Prices are discretized into integer ticks with a tick size of 0.1 bps: with PRICE_SCALE = 100_000, price = PRICE_SCALE + tick. Orders may only be placed at ticks divisible by TICK_SPACING = 10 (effectively setting a 1 bp tick size). The orderbook tracks best bid (highest active bid tick) and best ask (lowest active ask tick), and uses bitmaps over tick words for efficient discovery of the next initialized tick.
Quote tokens
Each TIP‑20 token specifies a single quote token in its metadata via quoteToken(). A trading pair on the stablecoin exchange exists only between a base token and its designated quote token, and prices for the pair are denominated in units of the quote token.
This design reduces liquidity fragmentation by giving every token exactly one paired asset.
It also simplifies routing. We require that:
- each token picks a single other stablecoin as its quote token, and,
- quote token relationships cannot have circular dependencies.
This forces liquidity into a tree structure, which in turn implies that there is only one path between any two stablecoins
USD tokens can only choose USD tokens as their quote token. Non-USD TIP-20 tokens can pick any token as their quote token, but currently there is no support for cross-currency trading, or same-currency trading of non-USD tokens, on the DEX.
The platform offers a neutral USD stablecoin, pathUSD, as an option for quote token. PathUSD is the first stablecoin deployed on the chain, which means it has no quote token. Use of pathUSD is optional.
Swaps
Swaps execute immediately against the active book. Selling base for quote starts at the current best bid and walks downward as ticks are exhausted; selling quote for base starts at the best ask and walks upward. Within a tick, fills are FIFO and decrement the tick’s total liquidity. When a tick empties, it is de‑initialized.
Callers can swap between any two USD TIP-20 tokens. If tokenIn and tokenOut are not directly paired, the implementation finds the unique path between them through quote‑token relationships, and performs a multi‑hop swap/quote.
Crossed books
Crossed books are permitted; the implementation does not enforce that best bid ≤ best ask. This primarily supports flip‑order scenarios.
Constraints
- Only USD‑denominated tokens are supported, and their quotes must also be USD
- Orders must specify ticks within the configured bounds (±2000)
- Tick spacing is 10:
tick % 10 == 0for orders and flip orders executeBlock()is system‑only (called in a system transaction at end of block)- Withdrawals require sufficient internal balance
Interface
Below is the complete on‑chain interface, organized by function. Behavior notes and constraints are included with each function where relevant.
Constants and pricing
function PRICE_SCALE() external view returns (uint32);Scaling factor for tick‑based prices. One tick equals 1/PRICE_SCALE above or below the peg. Current value: 100_000 (0.001% per tick).
function TICK_SPACING() external view returns (int16);Orders must be placed on ticks divisible by TICK_SPACING. Current value: 10 (i.e., 1 bp grid).
function MIN_TICK() external view returns (int16);
function MAX_TICK() external view returns (int16);Inclusive tick bounds for order placement. Current range: ±2000 ticks (±2%).
function MIN_PRICE() external view returns (uint32);
function MAX_PRICE() external view returns (uint32);Price bounds implied by tick bounds and PRICE_SCALE.
function tickToPrice(int16 tick) external pure returns (uint32 price);
function priceToTick(uint32 price) external pure returns (int16 tick);Convert between discrete ticks and scaled prices. priceToTick reverts if price is out of bounds.
Pairing and orderbook
function pairKey(address tokenA, address tokenB) external pure returns (bytes32 key);Deterministic key for a pair derived from the two token addresses (order‑independent).
function createPair(address base) external returns (bytes32 key);Creates the pair between base and its quoteToken() (from TIP‑20). Both must be USD‑denominated. Reverts if the pair already exists or tokens are not USD.
function books(bytes32 pairKey) external view returns (address base, address quote, int16 bestBidTick, int16 bestAskTick);Returns pair metadata and current best‑of‑side ticks. Best ticks may be sentinel values when no liquidity exists.
function getTickLevel(address base, int16 tick, bool isBid) external view returns (uint128 head, uint128 tail, uint128 totalLiquidity);Returns FIFO head/tail order IDs and aggregate liquidity for a tick on a side, allowing indexers to reconstruct the active book.
Internal balances
function balanceOf(address user, address token) external view returns (uint128);Returns a user’s internal balance for token held on the exchange.
function withdraw(address token, uint128 amount) external;Transfers amount of token from the caller’s internal balance to the caller. Reverts if insufficient internal balance.
Order placement and lifecycle
function place(address token, uint128 amount, bool isBid, int16 tick) external returns (uint128 orderId);Queues a limit order against the pair of token and its quote. Escrows funds: bids escrow quote at tick price; asks escrow base.
Notes:
tickmust be within[MIN_TICK, MAX_TICK]and divisible byTICK_SPACING(10).
function placeFlip(address token, uint128 amount, bool isBid, int16 tick, int16 flipTick) external returns (uint128 orderId);Like place, but marks the order as a flip order. When fully filled, a new pending order for the same maker is scheduled on the opposite side at flipTick (which must be greater than tick for bids and less for asks).
Notes:
- Both
tickandflipTickmust be within[MIN_TICK, MAX_TICK]and divisible byTICK_SPACING(10).
function cancel(uint128 orderId) external;Cancels a pending or active order owned by the caller. Pending orders refund escrow to internal balance. Active orders are removed from the tick queue, liquidity is decremented, and remaining escrow is refunded.
function executeBlock() external;Processes all orders that were pending at call time and inserts them into the active book in creation order within the block. Only callable by the protocol, and is called in a system transaction at the end of each block.
function activeOrderId() external view returns (uint128);
function pendingOrderId() external view returns (uint128);Monotonic counters for last processed active order and latest pending order.
Swaps and quoting
function quoteSwapExactAmountIn(address tokenIn, address tokenOut, uint128 amountIn) external view returns (uint128 amountOut);Simulates an exact‑in swap walking initialized ticks and returns the expected output. Reverts if the pair path lacks sufficient liquidity.
function quoteSwapExactAmountOut(address tokenIn, address tokenOut, uint128 amountOut) external view returns (uint128 amountIn);Simulates an exact‑out swap and returns the required input. Reverts if insufficient liquidity.
function swapExactAmountIn(address tokenIn, address tokenOut, uint128 amountIn, uint128 minAmountOut) external returns (uint128 amountOut);Executes an exact‑in swap against the active book. Deducts amountIn from caller’s internal balance (transferring any shortfall) and transfers output to the caller. Reverts if resulting amountOut is below minAmountOut or liquidity is insufficient.
function swapExactAmountOut(address tokenIn, address tokenOut, uint128 amountOut, uint128 maxAmountIn) external returns (uint128 amountIn);Executes an exact‑out swap. Deducts the actual input from the caller’s internal balance (transferring any shortfall from the user) and transfers amountOut to the caller. Reverts if required input exceeds maxAmountIn or liquidity is insufficient.
Events
event PairCreated(bytes32 indexed key, address indexed base, address indexed quote);
event OrderPlaced(uint128 indexed orderId, address indexed maker, address indexed token, uint128 amount, bool isBid, int16 tick);
event FlipOrderPlaced(uint128 indexed orderId, address indexed maker, address indexed token, uint128 amount, bool isBid, int16 tick, int16 flipTick);
event OrderCancelled(uint128 indexed orderId);
event OrderFilled(uint128 indexed orderId, address indexed maker, address indexed taker, uint128 amountFilled, bool partialFill);Errors
error Unauthorized(); // Non‑system caller attempted to call executeBlockOther notable revert reasons (string reverts in the reference implementation):
- Pair creation or usage:
PAIR_EXISTS,PAIR_NOT_EXISTS,ONLY_USD_PAIRS - Bounds:
TICK_OUT_OF_BOUNDS,FLIP_TICK_OUT_OF_BOUNDS,FLIP_TICK_MUST_BE_GREATER_FOR_BID,FLIP_TICK_MUST_BE_LESS_FOR_ASK, "Price out of bounds" - Tick spacing:
TICK_NOT_MULTIPLE_OF_SPACING,FLIP_TICK_NOT_MULTIPLE_OF_SPACING - Liquidity and limits:
INSUFFICIENT_LIQUIDITY,MAX_IN_EXCEEDED,INSUFFICIENT_OUTPUT - Authorization:
UNAUTHORIZED(cancel not by maker),INSUFFICIENT_BALANCE(withdraw)