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, 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.
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, and manage internal balances.
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.
Flip orders
A flip order behaves like a normal resting order until it is fully filled. When filled, the exchange places a new 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.
When a flip order flips, it draws escrow exclusively from the maker's internal exchange balance. Unlike initial order placement, the exchange does not fall back to transferFrom if the internal balance is insufficient—the flip simply does not occur. This ensures that flip execution is self-contained and does not require additional token approvals or external balance checks at fill time.
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 DEX 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 - 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);Places a limit order against the pair of token and its quote, immediately adding it to the active book. Escrows funds: bids escrow quote at tick price; asks escrow base.
Notes:
tickmust be within[MIN_TICK, MAX_TICK]and divisible byTICK_SPACING(10).- The maker must be authorized by the TIP-403 transfer policies of both the base and quote tokens. This ensures makers cannot place orders to buy or sell tokens they are not permitted to transfer.
- Additionally, the DEX contract itself must be authorized by the TIP-20 transfer policies of both the base and quote tokens. This allows token issuers to prevent their tokens from being traded on the DEX.
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 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). - When the order flips, escrow is drawn exclusively from the maker's internal exchange balance. If the internal balance is insufficient, the flip silently fails—no
transferFromis attempted, even if the maker has sufficient external balance and approval. - The maker must be authorized by the TIP-403 transfer policies of both the base and quote tokens, both at initial placement and when the order flips. If the maker becomes unauthorized before a flip, the flip silently fails and no new order is created (although the existing order is executed).
function cancel(uint128 orderId) external;Cancels an order owned by the caller. When canceled, the order is removed from the tick queue, liquidity is decremented, and remaining escrow is refunded to the order owner's exchange balance which can then be withdrawn.
function cancelStaleOrder(uint128 orderId) external;Cancels an order where the maker is forbidden by the escrowed token's TIP-403 transfer policy. Unlike cancel, this function can be called by anyone—not just the order maker—but only succeeds if the maker is no longer authorized to transfer the escrowed token (e.g., the maker has been blacklisted). This allows third parties to clean up stale orders from the book.
When canceled, the order is removed from the tick queue, liquidity is decremented, and remaining escrow is refunded to the order maker's exchange balance. Reverts with OrderNotStale if the maker is still authorized.
function nextOrderId() external view returns (uint128);Monotonic counter for next orderId.
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, bool isFlipOrder, 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();- 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) - Stale orders:
ORDER_NOT_STALE(cancelStaleOrder when maker is still authorized) - Balance:
INSUFFICIENT_BALANCE(withdraw)