Dutch Auctions

Generalized Order Book

Common implements a fully on-chain order book, where each order is, in a sense, a separate dutch auction (or even more general, see the details below).

High Level Overview

Traditional Limit Orders

In a traditional order book, the user typically submits a LimitOrder having the following parameters:

  • token_in - token the user wants to sell

  • token_out - token the user wants to buy

  • amount_in - how many tokens user wants to sell

  • price - determines how many tokens user wants to buy, specifically, the want at least price*amount_in tokens of type token_out.

Such an order sits in the order book until matched by a matching engine. Matching happens against orders in the opposite direction. Orders are matched if they are "crossing", i.e., when both price constraints can be satisfied at the same time. The price at which the trade is executed depends on the matching engine. Orders can be matched partially. Typically some kind of fee is deducted after a trade happens.

On-chain "Dutch" Orders

A Dutch auction is an auction mechanism for selling an asset, in which an initial price p_start is selected for the auctioned item, and then, as the auction progresses, the price gradually decreases, little by little. At the moment when a buyer is satisfied by the current price, they bid, and win the auction right away, paying the current price for the item. Assuming that the initial price p_start was set correctly (higher than what's the item worth) then the auction finishes with the item being sold at an optimal price.

Common uses the Dutch auction mechanism to implement generalized limit orders. Roughly speaking, if the user has some token_in asset and they want to sell amount_in of them for token_out asset, then they can set some initial price p_start and limit price p_stop and let the tokens be traded over some time, at the price varying linearly between p_start and p_stop over a selected time segment. Of course, it is natural to set p_start to be a price that is higher than the current stop market price, so that the auction achieves its goal of trading the assets at a price favorable to the user.

The order book consists then of a set of "living" orders – orders whose prices change in time.

Orders are matched not via a centralized matching engine but via a permissionless group of "fillers" who are actors who can fill one or more orders at the same time. Fillers compete with each other to make profit from arbitrage opportunities.

Details of Dutch Orders

Creating Orders

To create an order user sends the following transaction

fn create_order(token_in, token_out, amount_in, price_curve, expiration_time) {...}

Where token_in, token_out, amount_in are self-explanatory, and:

  • price_curve is a function that takes timestamp as an argument and outputs a price that is acceptable for this order at this time. For instance:

    • For limit orders we have price_curve(t) = const

    • For dutch auctions we have price_curve(t) = ((t1 - t)p1 + (t-t2)p2)/(t1-t2) i.e., a linear function with a specific timestamp t1, t2 and price p1, p2 parameters

    • We could have more general price curves too, for instance exponential, which would be useful for price discovery of very exotic tokens.

  • expiration_time is a timestamp when the order is automatically cancelled. It can be set to infty in order to make the order live till being filled, or manually canceled.

As a result, an order, with a unique order_id is created and stored in the contract.

Note that as part of the create_order transaction, the user locks amount_in tokens token_in in the order book contract.

Filling Orders

To fill an order or multiple orders one uses the following contract call:

fn fill(order_ids: Vec<OrderId>, executor_contract: Address) {
    let orders = {order[order_id] for order_id in order_ids}
    let prices = current prices of orders;
    for order in orders {
        transfer order.amount_in of order.token_in to executor_contract;
    }
    // The results array contains info on what amount of which order was filled
    let results = executor_contract.execute(orders, prices);
    
    Validate results (details below) 
    // Validate that the executor_contract transferred enough token_in and token_out 
    // for each order. Specifically:
    // 1. >= (amount_in-filled) of token_in
    // 2. >= (amount_in-filled)*price of token_out.
    // If validation fails, rollback.
    
    Update orders to take into account filled amounts.
}

Note that the above requires developing an executor contract, so it is expected to be used by a professional filler only. A simplified call for casual traders will be also provided to make it simpler.

The fill method can be logically split into 3 steps:

  1. Send input funds to the executor_contract

  2. Execute a callback by the executor_contract

  3. Receive the swapped funds from the executor_contract

There are a few reasons why the proposed implementation of fill (based on steps 1. 2. 3. as above) is convenient for the fillers:

  • When creating a transaction, the filler cannot predict: 1) if some of the orders they try to fill will not get filled (partially) before the transaction lands on chain, 2) what will be the timestamp of the block their transaction is included. This implies that the filler doesn't know the final amounts of the order and the price at which it will be executed. That's why the execute call includes both orders and prices and is expected to make the final decision on what to trade and how.

  • In case when an order is filled, or the price is not the expected one, the execute method can simply abort, in order to rollback the transaction.

  • Thanks to the fact that the step 1. happens before 3. it is possible for the filler to fill orders without any liquidity held by the filler. Specifically, fill is a variant of a flash-loan. What the filler can do for instance is:

    • Fill one order by trading it against an AMM

    • Fill two orders by trading them against each other.

    • Any combination of these, as long as it's atomic.

Cancelling Orders

Orders can be cancelled at any time by the user who submitted them, in which case they get a full refund on what remains untraded (token_in) + the amount in token_out that was traded.

Fees

Incentives for Fillers

Last updated