ORC Trustless Market Making Guide

David Zhang
5 min readAug 21, 2023

--

Overview

This document walks through the key concepts necessary for running a trustless on-chain market making operation on an ORC-like protocol (herein referred to as ORC-X) on Bitcoin or other similar chains. This includes the following activities:

  • Constructing an orderbook
  • Posting orders
  • Taking orders

This document assumes familiarity with the ORC-like protocols, such as ORC-20 and ORC-CASH and uses language common to these standards.

Note that this is not the only way to market make an ORC-X token, you can also create PSBTs and rely on an exchange, but that comes with its own set of challenges and tradeoffs that we will discuss in a different document.

Orderbook Construction

Creating an ORC-X orderbook involves two key activities

  1. Watching new blocks for maker and taker orders
  2. Tracking posted order expirations

Posted orders (limit orders) on ORC-X are created via a sell operation. These orders can optionally include an expiration time, in blocks. Constructing an orderbook involves crawling the index for all sell operations, figuring out where to put them via price, and which ones to remove due to expiration.

Examples

For simplicity I will omit most keys, and focus on amt, lim, price, expire, seller, and buyer. Assume that these orders are for the same token on the same protocol.

Normally expire would be an integer that represents how many blocks from the time the transaction is mined before it expires, but for the sake of simplicity in these examples I will be using syntax like C+x or C-x where C is the current head of the chain and x is an integer that represents the how far away expiration is. For example, if an order with expire = 100 was mined 13 blocks ago, I would write C+87 since the order expires in 87 more blocks.

Simple case

This simple example demonstrates the happy case, every order is from a unique seller, nothing has expired.

{
"amt": 100,
"lim": 100,
"price": 5000,
"expire": "C+50",
"seller": "seller1",
"buyer": "anyone"
}
{
"amt": 300,
"lim": 150,
"price": 6000,
"expire": "C+5",
"seller": "seller2",
"buyer": "anyone"
}
{
"amt": 400,
"lim": 200,
"price": 6500,
"expire": "C+10",
"seller": "seller3",
"buyer": "anyone"
}
{
"amt": 600,
"lim": 600,
"price": 6500,
"expire": "C+25",
"seller": "seller4",
"buyer": "anyone"
}
{
"amt": 200,
"lim": 200,
"price": 7000,
"expire": "C+50",
"seller": "seller5",
"buyer": "anyone"
}

Results in the following orderbook:

Price | Amount | Expiration
------|--------|------------
5000 | 100 | C+50
6000 | 150 | C+5
6000 | 150 | C+5
6500 | 200 | C+10
6500 | 200 | C+10
6500 | 600 | C+25
7000 | 200 | C+50

Key observations

  • We split orders up based on lim because a single buyer can only purchase from one seller up to lim at a time

Expired orders

{
"amt": 100,
"lim": 100,
"price": 5000,
"expire": "C-5",
"seller": "seller1",
"buyer": "anyone"
}
{
"amt": 300,
"lim": 150,
"price": 6000,
"expire": "C-500",
"seller": "seller2",
"buyer": "anyone"
}
{
"amt": 400,
"lim": 200,
"price": 6500,
"expire": "C+10",
"seller": "seller3",
"buyer": "anyone"
}
{
"amt": 600,
"lim": 600,
"price": 6500,
"expire": "C-1",
"seller": "seller4",
"buyer": "anyone"
}
{
"amt": 200,
"lim": 200,
"price": 7000,
"expire": "C+50",
"seller": "seller5",
"buyer": "anyone"
}

Results in the following orderbook:

Price | Amount | Expiration
------|--------|------------
6500 | 200 | C+10
6500 | 200 | C+10
7000 | 200 | C+50

Seller multiple orders

Here is how I expect indexers to behave when a seller posts multiple active sell orders. The buyer would fill all orders in parallel, not spreading their purchasing power across each order, but multiplied by the number of orders. In this way the seller is essentially giving away an n for 1 deal, where n is the number of active orders, since any buyer would simultaneously fill all orders at once. This can be used to create a price gradient which can be useful when there are slow block times (larger orders are more expensive than smaller ones) but please double check indexer behavior before assuming this.

{
"amt": 200,
"lim": 200,
"price": 4000,
"expire": "C+5",
"seller": "seller1",
"buyer": "anyone"
}
{
"amt": 400,
"lim": 400,
"price": 6000,
"expire": "C+10",
"seller": "seller1",
"buyer": "anyone"
}

Results in the following orderbook:

Price | Amount | Expiration
------|--------|------------
2400 | 200 | C+5
6000 | 266.67 | C+10

Key observations

  • You can get up to 200 at price 4000 and 6000, so by paying 4000*200 = 800k you will get 200+(800k/6000) = 200 + 133.33 = 333.33 (rounded). This puts your effective price at 2400
  • We take the lesser of the two expirations when computing the combined effective order

Posting Orders

Posting an order is relatively simple, depending on the specifics of orc-x it may look something like this.

{ 
"p": "orc-x",
"tick": "TOKEN",
"id": "1",
"op": "sell",
"amt": "200",
"lim": "200",
"price": "5000",
"expire": "5",
"seller": "seller1",
"buyer": "anyone"
}

Taking Orders

Taking an order is straightforward, simply send native token (e.g. BTC on Bitcoin) to the seller and you will receive tokens at the effective price.

When taking orders keep in mind the risk of front-running (see below).

Considerations

Posting multiple orders

Behavior for seller addresses with more than one active sell is not finalized, so best practice is to use a unique seller address for each posted order. These addresses can be recycled once the posted order expires.

See the above example for how I expect this to behave.

Order expiration

A sell operation cannot be canceled once initiated, so this puts theta risk on the seller. You are essentially writing an option for the open market, so take appropriate premiums as necessary to hedge against volatility.

One-sided orderbook

Since we can only construct a one-sided orderbook (e.g. there are no limit buy orders, only limit sell), we must take this information asymmetry into consideration. There is significant opacity for how much buyer demand there is, and there is no guaranteed liquidity on the buy-side.

Sending too much or getting front-run

This is a tricky and dangerous issue, there is no mechanism to protect orders from getting front-run or if you end up sending too much for a purchase (they reduce down to the same essential problem).

If you send too much for your order, you will only receive up to the max sell order size, but that’s not the worst that can happen, a malicious seller could front-run your transaction and make sure their own order gets filled first, this would mean that there would be no tokens to purchase by the time your native asset made it to the seller’s address, resulting in you paying full price for nothing.

As a buyer, you need to be sure that the seller does not have an economic incentive to frontrun you, e.g. it would be more expensive to fill up a block and crowd out your transaction than it is to let it go through.

Disclaimer

ORC-like protocols are new and experimental in nature, the space evolves quickly so information like this can become out of date very quickly. You should research the official documentation and verify behavior for yourself before implementing anything with real money.

I am a co-founder of Stably, we build stablecoin and on-ramp infrastructure for multiple blockchains. These opinions are my own and do not represent those of Stably.

--

--