ZEOS Hybrid Order Book Exchange

Matthias Schönebeck
20 min readDec 8, 2023

--

When Daniel Larimer came up with his concept for a decentralized hybrid order book exchange a lot of EOS community members became very excited, expecting this new and innovative DEX would launch on the EOS blockchain. Backed by investor legends like Peter Thiel, block.one raised the incredible amount of $10 billion Dollars in seed funding to develop and launch what later became known as “Bullish”.

However, with pretty much everything block.one was involved in, Bullish ended up as a massive disappointment for the EOS community: It never launched on EOS and was also never meant to. But Larimer’s idea of a hybrid order book exchange — a combination of both: traditional limit-order book and liquidity pool with automated market maker — was out in the open. For some reason though, no one ever built such an exchange for EOSIO blockchains — at least not until now.

The ZEOS Hybrid Order Book Exchange is the first implementation of such an exchange independent from block.one and Bullish. This article highlights the most interesting features which go far beyond of just being a hybrid order book exchange.

Features

The following sections explain all the top features of the ZEOS Hybrid Order Book Exchange which is already deployed on the Jungle 4 testnet and can be found here: dex.zeos.one.

Hybrid Order Book

Each market on the exchange always consists of both: A traditional limit-order book as well as an automated market maker managing a liquidity pool based on the constant product market-making (CPMM) formula. As a result orders are always matched against both — order book and liquidity pool — depending on which provides the better price. It can be thought of as having instant arbitrage between order book and liquidity pool without having to trade on two different platforms manually. This essentially means that the pool is “sitting” between the bid and ask sides of the order book eliminating any spread between those two.

The order matching algorithm works as follows: If a market order is executed the algorithm determines the cheaper price between book and pool and starts to match it against the currently available liquidity. If the pool price is better than the cheapest available book price the order is filled by the liquidity pool until the slippage reaches the point at which the next book order becomes the cheaper deal. At that point the algorithm automatically switches to the book and matches against the current row until either the order is fully filled or the book row at this particular price is fully consumed. The algorithm then switches back to the pool and keeps filling it until — again — the slippage becomes so high that the next book order becomes the cheaper deal. This process repeats until the order is fully filled.

High-Performance Order Execution

The exchange is designed to be as efficient as possible when it comes to order execution. Each market — with all its data — persists in only a single multi-index table row. This means that for every order which gets executed on a particular market only a single multi-index table access is required by the smart contract (i.e. fetching, de-serializing, modifying and serializing of a single table row only). Having to access only one table row per trade makes the exchange very efficient on the smart contract side but also on the front-end: The client-side user interface only has to query a single table row through the chain API as well in order to retrieve all data related to a particular market. This includes both sides of the limit-order book but also the liquidity pool data which can all be fetched through just a single API call. Simplicity is beauty and sometimes also the most efficient way.

Fully on-chain History Solution

The exchange introduces a new concept for a fully on-chain history solution for market data which does not require anyone to run an expensive blockchain node with history-plugin. While the most recent “tick” data (i.e. the latest deals) of a market is always held inside chain state (as part of the “markets” multi-index table row as mentioned above), an “archive” action can be executed by anyone as soon as enough tick-data has been accumulated inside the smart contract’s state. In order to execute the “archive” action the following conditions must be met:

  • Enough tick-data must exist in chain state (the minimum number of “ticks” required to be archived can be configured per market by the owner of the exchange)
  • There must be at least two “ticks” remaining in the chain state market history and the oldest “tick” must be more than 24 hours of blocks in the past (this is to ensure that the user interface always has enough market history available to calculate the 24-hour changes of a market by just fetching the corresponding multi-index table row of that particular market)
  • The correct tick-data to be archived needs to be send as a parameter of the “archive” action
  • The “archive” action must not be executed as an inline action but must be found at the top-level of the executing transaction
  • For security reasons (i.e. to prevent abuse) the “archive” action must be the only action inside the particular transaction it gets executed with.

Executing the “archive” action clears the smart contract’s state from all the submitted tick-data and replaces it with the four byte value of the current block number the “archive” action gets included in. This means that the corresponding allocated RAM for all the archived market history is freed up while the market history itself is archived on-chain in that particular block. Because all the above conditions must be met in order to be able to execute the “archive” action in the first place it is guaranteed that the market history is now included in the top-level of a block. Only the block number of that particular block needs to be held inside the smart contract’s state as part of the market’s history. That way the user interface is able to query all the archived blocks cheaply through the chain API as well and extract all recorded history from those blocks without having to rely on access to a node with full history-plugin (which is an expensive to run and usually not freely available API service).

Stop/Stop-Limit Order Types

As probably the first decentralized exchange in all of crypto it offers traders the ability to set stop and stop-limit orders. However, in order to enable this feature users are required to deploy the exchange’s “stop contract” to their account. This can be done by the click of a button though which executes the necessary transaction to deploy the “stop contract” to the user’s account and also configures the user account’s permissions accordingly.

The main reason for the “stop contract” is the fact that users cannot whitelist actions in their Anchor wallet for auto-execution. This means, if the exchange’s user interface wanted to execute a stop-order on the user’s behalf it would always have to send a signing request to the user’s Anchor wallet which would require the user to manually sign the transaction using his “active” permission. But stop-orders should get executed automatically without any additional action required by the user as soon as the particular stop-price is hit — even if the user is not on his device. In order to achieve that, a special permission (i.e. private key) is created and linked to the “stop contract’s” “executeorder” action, just for the user interface to be able to execute stop-orders on the user’s behalf. This is all the permission is allowed to do. It can only execute but not create or delete stop-orders. For the latter two the user’s “active” permission is still needed which requires active signing through Anchor by the user.

Here is how it works:

If a user deploys the “stop contract” to his account the user interface creates a new private key which will be linked to the “executeorder” action of the “stop contract”. This private key is being held in browser storage and can only be used by the user interface to execute a previously created stop-order. There is no risk of loosing funds in case this key gets compromised (for example, if an attacker is somehow able to read out this key from the user’s browser). In addition to creating this new key, the user needs to set an encryption password. This password is used to encrypt all the stop-orders on-chain which are held in the user account’s RAM, so no one else can see what those (pending) stop-orders are. After all, stop-orders should remain private and hidden to the rest of the world until the moment they get executed.

In fact, it’s even a little bit more complicated than that: Each stop order comes with its very own ephemeral encryption key (which is transparent to the user) so that when a stop-order gets executed by the user interface only the encryption key of that particular stop-order gets revealed to the public. The private key to execute stop-orders is also stored on-chain in encrypted form inside the user account’s RAM. That way, the user interface can always restore all of the user’s stop-orders and its permission to execute them in case the user re-connects to the user interface (or if the user reconnects using a different device).

When a stop-order gets triggered, the user interface executes the “executeorder” action of the user’s “stop contract” using its special private key for that. The encrypted order is sent as an action parameter together with it’s very own ephemeral encryption key so that the “stop contract” can decrypt and execute it instantly. All the user needs to ensure is that his account has the necessary blockchain resources (i.e. CPU and NET) allocated as well as sufficient funds in his account in order for the “stop contract” to be able to execute his order. And that’s it. This is how it works.

Keep in mind that in order for stop-orders to be able to get executed the user needs to keep the user interface running on his device. After all, this is crypto: There is no middle-man between you and the markets like a bank or a traditional stock broker who would execute those stop-orders for you. You access the markets directly through a decentralized exchange meaning you are your own bank (or broker) so you need to make sure your trading infrastructure (i.e. your browser running the user interface) is always up and running!

Takers pay, Makers earn

Unlike other limit-order book exchanges in the space who charge fees on both sides, this exchange rewards you for market-making. This means, no matter if you provide liquidity through order book (i.e. limit orders) or liquidity pool: As a market maker you earn a share of the trading fees which only the market takers pay.

There are two fee types on the exchange: The maker fee and the platform fee. Both can be configured for each market individually. In sum, they equal the total trading fee a market taker has to pay. As the name indicates, the maker fee is the share that goes to the market maker for providing liquidity. The platform fee is a percentage that goes into the exchange’s account and can only be claimed by the owner of the exchange.

Reasonable initial values for those fees could be 0.2% maker fee and 0.1% platform fee. But again, it is totally up to the owner of the exchange to set those fees for each market when they get opened. Of course, fees can always be adjusted at any point later in time by the owner of the exchange by executing the “setfees” action of the exchange’s smart contract for a particular market, providing a new pair of fee values.

Private Trading (coming soon)

The only missing feature (at this point) is probably also the most exciting one: As soon as the ZEOS Shielded Protocol goes live users will be able to not only trade on the exchange using traditional EOSIO blockchain accounts but will also be able to connect their ZEOS private wallet to the exchange. This will — most likely as the first crypto exchange ever — enable private trading for ZEOS protocol users.

The user experience of private trading will be very similar to the one if using your Anchor wallet. The only difference: It’s not going to be Anchor wallet which is going to pop-up on a signing request (to sign an EOSIO transaction) but the ZEOS wallet will pop-up and ask you to sign a private ZEOS transaction. This will give users full anonymity when trading on the exchange leaving observers of the underlying public blockchain completely clueless of who is actually using the exchange.

All information, however, of what is being exchanged and how much of it, remains public. In fact, this data has to remain public in order for the exchange to be able to match orders. But this also implies that global data like daily trading volume remains publicly detectable which is actually a good thing. However, it won’t be possible to determine the total number of users anymore because all traders who access the exchange privately will access it through the ZEOS protocol contract which acts as a proxy for all private trading. A public observer would just see a lot of trading activity coming from the ZEOS protocol account but it would be impossible to tell who is actually behind certain trades.

This feature is currently being developed and will most-likely go live on the same day the ZEOS Shielded Protocol will launch.

User Interface

Since this is the first hybrid order book exchange in crypto which combines both — traditional order book as well as AMM liquidity pool — the slightly unusual looking user interface might require some more detailed explanation.

Header

At the top right of the window (as part of the header) there are four clickable icons: The first one is the login button used to connect a crypto wallet to the application. Connecting a wallet is required in order to be able to use the exchange. The second icon is to toggle between the light and dark theme of the exchange. The third button opens the download-modal through which a user can download this entire user interface to run it locally on his machine. This option makes the exchange truly censorship-resistant as users do not depend on a traditional web service where this application is hosted. It is built in such a way that users can easily run it locally on their machine without any major requirements other than having NodeJS installed on their device. The fourth button opens the info-modal which displays a short welcome message and highlights the exchange’s unique features.

TradingView Chart Widget

The first element that sticks out right away and catches the eye is the TradingView chart widget which is used to render all market data into nice candle stick charts of any resolution. The more the user zooms out the more history data is automatically loaded into the chart through the unique history solution explained above. In addition to that, the “Latest Deals” table which shows the actual tick-data, is expanded to display all the history “ticks” (i.e. deals from the past) from which the currently displayed chart is created from.

The TradingView chart widget is very feature-rich and comes with a ton of indicators and tools. All credit for this amazing chart library goes to the great developers at TradingView.

Hybrid Order Book

On the left side the order book is displayed together with the liquidity pool data. Since the liquidity pool always fills the gap between the bid and ask sides of the book it is placed in-between them. This also makes it very clear that the current pool price always equals the latest market price. It might equal either the lowest ask or the highest bid price. In case it equals the lowest ask price the book offers a better price (with more liquidity) for buyers than the pool but the pool offers a better price for sellers. In case it equals the highest bid price the book offers a better price for sellers (with more liquidity) than the pool but the pool offers a better price for buyers. However, the pool might also just mark a price in-between highest bid and lowest ask. If that’s the case the pool price is better than the book price for both — buyers and sellers.

Both, book and pool, have red and green horizontal bars overlaying their rows indicating the current liquidity depth at that particular price level (relative to the highest liquidity point available). In case of the pool the bars indicate the depth of the liquidity available inside the pool until the first book row is “tapped into”. This means, the bars overlaying the pool always indicate the amount of sell or buy liquidity between the current pool price and the first bid or ask row of the book. In case this liquidity is bought from (or sold into) the pool, the price would hit the next order book row above or below the pool. Book rows are clickable, setting the order price and amount according to how much buy/sell volume would be required in order match all the available liquidity up until that particular book row (including the clicked row itself).

Ticker List

The ticker list on the right side shows all the trading pairs which are available for trading on the exchange including their last price at which a deal was made and their 24-hour price change. The rows in this table are clickable, making the user interface switch to the corresponding market.

Trading Interface

This is the place where actual trades are being entered and executed by the user. The input mask on the left is for buying the market (i.e. buy base asset with quote asset) and the input mask on the right is for selling the market (i.e. sell base asset for quote asset). It shows the user’s token balances at the top of the buy and sell input masks indicating the amount of tokens the user has available for trading each side of the market.

The first input field below the balance is used to select the order type. There are four different types of orders on the exchange: limit-orders, market-orders, stop-orders and stop-limit-orders. For the latter two the user is required to deploy the exchange’s “stop contract” to his account as explained above. If the user doesn’t have this contract deployed (yet) and chooses either one of the two stop-order types the user interface will hide the input masks and instead show a button through which the user can easily deploy the “stop contract” to his account.

  1. In case of limit-order the input mask shows a “price” field, an “amount” field and a “total” field. If the price entered by the user is above the current market price (in case of buy, or below the current market price in case of sell, respectively) his limit-order gets filled at the best possible price available until the slippage exceeds his desired limit-price. With the remaining amount that couldn’t be filled a limit-order will be opened and placed into the order book at that particular price level. For the whole amount that gets filled immediately the trader pays the taker fee. For the remaining amount of his order that ends up in the book as a limit-order he will earn the maker fee in case someone matches against it.
  2. In case of market-order the input mask only shows the “total” field (in case of buy) or the “amount” field (in case of sell, respectively). The user then puts in the desired amount of tokens he wants to swap immediately (i.e. his order gets fully filled at the best price possible). The “amount” field (in case of buy, or the “total” field in case of sell) will then display to the user the exact amount of tokens he will receive in return under current market conditions in case he executes his order. The price field will display the average price he will receive for this trade. This gives market takers full control and the most transparency possible over the outcome of a token swap via market-order as they are able to see in advance what the outcome of a particular trade is going to be.
  3. In case of stop-order (also commonly known as “stop-loss”) the input mask only shows a “stop-price” field and the “total” field (in case of buy or the “amount” field in case of sell, respectively). The user enters his desired stop-price at which he wants to buy or sell a certain amount of tokens. Since stop-orders turn into market-orders the moment they get executed it is impossible to determine the outcome of a stop-order at the time of its creation. It is not guaranteed that a user’s order will get executed at the desired stop-price as the market might slip further than expected before the order gets triggered by the user interface. Also, the user has to make sure he has enough blockchain resources allocated to his account in order for his order to be executable by the “stop contract” at the time the market hits his desired stop-price. Finally, the user interface must be running inside the user’s browser because there is no one else who could possibly execute a stop-order on his behalf (other than the user interface itself). Needless to say, the user also must ensure he has sufficient funds inside his account in order for his stop-order to be executable.
  4. Finally, in case of stop-limit-order the input masks shows all available fields: “stop-price”, “(limit-)price”, “amount” and “total”. Like normal stop-orders also stop-limit-orders get triggered asynchronously by the user interface as soon as the market hits the stop-price. Instead of turning into a market-order though (as with normal stop-orders the case) a stop-limit-order turns into a limit-order which gets executed the same way as described above (see first point for limit-orders). As for stop-orders the user must make sure that the user interface is running in his browser and that his account has the sufficient funds and blockchain resources available for a successful order execution in case the market slips through the stop-price and triggers the stop-limit-order execution.

Most of the trading interface should be self-explanatory to the experienced trader. There is nothing special to note here. What’s very convenient in case of market-orders is the fact that “what you see is what you get” meaning you have full transparency over how much tokens you receive as a result from an immediate token swap in case of market-orders. Even if the market changes while the user enters his order into the input mask: The user interface will always keep the user up-to-date showing the exact amount of tokens the user is going to receive before he executes the trade.

Liquidity Interface

This part of the user interface is related to the liquidity pool of the currently selected market. It is the place where users are able to add or remove liquidity to/from a market and receive (or burn) pool shares. On the left side the input mask to add liquidity to the pool is displayed. Entering a certain amount of tokens in one of the two input fields automatically sets the required amount of the second asset. The field on the bottom shows the amount of pool shares the user would receive for his liquidity deposit. On the right side the input mask to burn pool shares is displayed. Entering an amount of shares will show both resulting amounts of tokens the user receives back in return for burning (part of) his shares. The middle part shows the amount of pool shares the user currently holds in his account. Below it the user’s pool share percentage is displayed as well as the current amounts of both — base and quote — asset, represented by the number of his pool shares.

The symbol names of all shares on the exchange start with the prefix “ZLP” (ZEOS Liquidity Pool) followed by four letters which basically represent the market id starting at “AAAA” for the pool shares of the market with id “0” and then increase according to the market id. So for example the second market on the exchange with id “1” would have the share token symbol “ZLPAAAB” the third one (id “2”) would have the share token symbol “ZLPAAAC” and so on and so forth. Market ids are transparent to the user but can be determined through the ticker list on the top right side of the user interface: The first market listed there has the id “0”, the second one has id “1” and so on and so forth.

Latest Deals

To the right side of the trading and liquidity interface the tick-data of the market is displayed in the “Latest Deals” table. It lists all the deals that have been made through order matches. The first column, named “LP”, indicates where the liquidity of a particular deal originated from. This can be either “book” or “pool”. The second column shows the (last) price at which the order was matched (and indicates if there was price slippage). The third column shows the volume of a particular deal and the fourth column shows the time at which a deal was made. All time values in this table are translated from actual block numbers as this is the only real time scale that exists for smart contracts on a blockchain.

Next to the table caption “Latest Deals” there is a small upload icon. This is the button a user needs to click in order to archive the latest tick-data on chain (i.e. clearing the exchange contract’s RAM from old market history data). Note that this action is only executable if there is enough accumulated market history available in chain state. See the section “Fully on-chain History Solution” above to learn more about the conditions that must be met in order to execute the “archive” action.

Open Orders

Finally on the very bottom of the user interface all open orders of the currently logged-in user and the currently selected market are listed. The table should be self-explanatory but just in case: The first column shows a unique order id (unique only for this particular market and the particular market side, i.e. buy or sell) and is chosen by the user interface at the time it is created. The second column shows the type of a particular order. This can be either limit-buy or limit-sell, stop-buy or stop-sell (also commonly known as “stop-loss”) and stop-limit-buy or stop-limit-sell. The third column indicates the stop-price (if any), the fourth column indicates the limit-price (if any). The fifth column shows the open amount in quote asset (in case of buy) or base asset (in case of sell). The sixth column shows the amount that was already dealt in base asset (in case of buy) or quote asset (in case of sell). The seventh column shows the status of the order which is usually “open” (as this is the “Open Orders” table) but it can also indicate if an order is already partly filled. In case an order gets fully filled it is automatically removed and the resulting tokens are sent back to the user’s account. It will thus disappear from the table automatically. The eighth column shows the time at which an order was created and the ninth column displays a “cancel” button through which the user is able to cancel a particular order. In case an order gets cancelled all tokens that have already been sent to or dealt by the exchange are going to be transferred back into the user’s account.

Additional Notes

The current user interface is not supported by Firefox but should work fine in all other web browsers on desktop and mobile.

--

--

Matthias Schönebeck

zeos.one | Computer Engineer, C++ Developer, Austrian Economist, Libertarian, Perpetual Traveler. You don’t know how to party if you’ve never been to Berlin.