Fermi DEX- Docs
  • ๐Ÿ‘‹Intro to Fermi
  • Overview
    • ๐Ÿ‘๏ธโ€๐Ÿ—จ๏ธVision
    • ๐Ÿ’กWhat we do
    • โœจFermi DEX: Features
    • ๐Ÿˆโ€โฌ›Background
  • Fermi DEX Usage Guide
    • Fermi DEX: How it works
      • ๐Ÿ“ชPosting a Limit Order
      • โœ…Finalizing Orders
      • ๐Ÿ”„Withdrawing Funds
    • ๐Ÿ› ๏ธGetting set up
      • ๐Ÿ“Getting tokens
      • ๐Ÿ”ซTrobleshooting
    • ๐ŸชTransfer Hooks Redefined
    • Custom settlement periods
  • Technical Overview
    • Overview v0.2 (DEPRECATED)
    • Technical Overview v1
      • Market Addresses (Devnet)
      • Key Structs
      • Instructions
      • Events
      • SDK
    • ๐ŸšงArchitecture
      • ๐Ÿ‘ฝJust In Time Liquidity
      • ๐ŸคMatching Logic
      • ๐Ÿ›ธLiquidity Abstraction
      • ๐ŸŒŠLiquidity Management Programs
    • ๐ŸงชTesting Locally
    • ๐Ÿ•น๏ธDemo - Fermi v0.2
    • ๐Ÿ“ฝ๏ธDemo - Fermi v1
    • ๐Ÿ“‡Github, Devnet Programs and PDAs
    • ๐Ÿ‘ฉโ€๐ŸŽคActors and Incentives
  • Use cases & Benefits
    • ๐Ÿ–‡๏ธLower Slippage than traditional DEXes
    • โ›๏ธPortfolio Margining Strategies
    • ๐ŸMarket Making on Fermi DEX
  • About us
    • ๐Ÿ‘‹Meet the Team
    • ๐Ÿ›ฃ๏ธRoadmap
    • ๐Ÿš“Next Steps
Powered by GitBook
On this page
  • Introduction
  • Order flow Logic
  • 1. Order Placement
  • 2. Order Matching Engine
  • 3. Order Finalization
  • 4. Handling Settlement Failure: Cancel With Penalty
  1. Technical Overview

Technical Overview v1

Code snippets capturing the core logic behind order placement, matching, finalisation, and settlement in Fermi DEX v1

PreviousOverview v0.2 (DEPRECATED)NextMarket Addresses (Devnet)

Last updated 1 year ago

Introduction

You can learn more about the technical architecture on the following pages:

Here we outline the order flow process with simplified code snippets, illustrating the core logic behind the implementation of Fermi's intent-based DLOB.

Order flow Logic

1. Order Placement

When placing an order, 1% of the order value is deposited as security to ensure credibility. The remaining amount of tokens needed for the trade are delegated from the user to the Market Authority, to be conditionally transferred later if the order is filled. A counter is maintained to account for cumulative approvals of a token for a user across all of their open orders on a market.

    token_transfer(
        deposit_amount / 100,
        &ctx.accounts.token_program,
        &ctx.accounts.user_token_account,
        &ctx.accounts.market_vault,
        &ctx.accounts.signer,
    )?;

    //already credited user's open orders with locked penalty amount

    token_approve(
        deposit_amount,
        &ctx.accounts.token_program,
        &ctx.accounts.user_token_account,
        &ctx.accounts.market_authority,
        &ctx.accounts.signer,
    )?;

In case of a limit order, the user's position (tokens deposited & tokens approved) is recorded in their user-specific openOrders struct. In case of a taker order the order is immediately filled and added to eventQ, or cancelled.

let oo_account = &mut open_orders_account;
let position = oo_account.position;
let deposit_amount = match order.side {
    Side::Bid => {
            let free_quote = position.quote_free_native;
            let max_quote_including_fees =
                total_quote_taken_native + posted_quote_native + taker_fees + maker_fees;

            let free_qty_to_lock = cmp::min(max_quote_including_fees, free_quote);
            
            // new total approved

            oo_account.total_approved_quote += max_quote_including_fees;

            let total_quote_approved = &oo_account.total_approved_quote;

            let deposit_amount = *total_quote_approved;

            market.quote_deposit_total += max_quote_including_fees;

            deposit_amount
        }

let oo_account = &mut open_orders_account;
let position = oo_account.position;
let deposit_amount = match order.side {
        ...
        Side::Ask => {
            let free_base = position.base_free_native;
            let max_base_native = total_base_taken_native + posted_base_native;

            // add additional amt to oo.total approved base
            oo_account.total_approved_base += max_base_native;

            let total_base_approved = &oo_account.total_approved_base;
            //let deposit_amount = max_base_native - free_qty_to_lock;

            let deposit_amount = *total_base_approved;
 
            market.base_deposit_total += max_base_native;

            deposit_amount
        }
    };

2. Order Matching Engine

Any limit orders placed are matched against the opposing bookSide to see if they can be partially or fully filled:

let OrderWithAmounts {
        order_id,
        total_base_taken_native,
        total_quote_taken_native,
        posted_base_native,
        posted_quote_native,
        taker_fees,
        maker_fees,
        ..
    } = book.new_order(
        &order,
        &mut market,
        &mut event_heap,
        oracle_price,
        Some(&mut open_orders_account),
        &open_orders_account_pk,
        now_ts,
        limit,
        ctx.remaining_accounts,
    )?;

The following depicts key parts of the matching code (book.new_order):

let opposing_bookside = self.bookside_mut(other_side);

        msg!("opposing_bookside:");
        for best_opposing in opposing_bookside.iter_all_including_invalid(now_ts, oracle_price_lots)
        {
            msg!("best_opposing: {}", best_opposing.node.quantity);

            if remaining_base_lots == 0 || remaining_quote_lots == 0 {
                msg!("Order matching limit reached");
                break;
            }

            if !best_opposing.is_valid() {
                // Remove the order from the book unless we've done that enough
                // excluded for brevity
            }

            let best_opposing_price = best_opposing.price_lots;

            if !side.is_price_within_limit(best_opposing_price, price_lots) {
                break;
            } else if post_only {
                msg!("Order could not be placed due to PostOnly");
                post_target = None;
                break; // return silently to not fail other instructions in tx
            } else if limit == 0 {
                msg!("Order matching limit reached");
                post_target = None;
                break;
            }

            let max_match_by_quote = remaining_quote_lots / best_opposing_price;
            if max_match_by_quote == 0 {
                break;
            }

            let match_base_lots = remaining_base_lots
                .min(best_opposing.node.quantity)
                .min(max_match_by_quote);
            let match_quote_lots = match_base_lots * best_opposing_price;

            // Self-trade behaviour
            if open_orders_account.is_some() && owner == &best_opposing.node.owner {
                msg!("Self-trade detected");
                match order.self_trade_behavior {
                   // excluded for brevity
                }
                assert!(order.self_trade_behavior == SelfTradeBehavior::DecrementTake);
            } else {
                maker_rebates_acc +=
                    market.maker_rebate_floor((match_quote_lots * market.quote_lot_size) as u64);
            }

            remaining_base_lots -= match_base_lots;
            remaining_quote_lots -= match_quote_lots;
            assert!(remaining_quote_lots >= 0);

            let new_best_opposing_quantity = best_opposing.node.quantity - match_base_lots;
            let maker_out = new_best_opposing_quantity == 0;
            if maker_out {
                matched_order_deletes
                    .push((best_opposing.handle.order_tree, best_opposing.node.key));
            } else {
                matched_order_changes.push((best_opposing.handle, new_best_opposing_quantity));
            }

           // Fill event

If an order is partially or fully filled, a fillEvent is generated and added to the EventQueue. This fill event contains all the information needed to "Finalize" the order, and execute just-in-time transfers from both counterparties.

let fill = FillEvent::new(
                side,
                maker_out,
                best_opposing.node.owner_slot,
                now_ts,
                event_heap.header.seq_num,
                best_opposing.node.owner,
                best_opposing.node.client_order_id,
                best_opposing.node.timestamp,
                *owner,
                order.client_order_id,
                best_opposing_price,
                best_opposing.node.peg_limit,
                match_base_lots,
            );

            msg!("processing fill event!");

            process_fill_event(
                fill,
                market,
                event_heap,
                remaining_accs,
                &mut number_of_processed_fill_events,
            )?;

            limit -= 1;
        }

3. Order Finalization

Can be called by either counterparty/ a third party "cranker". Unlike Openbooks' consume_events function, this is an atomic function, so a trade can be finalised sequence independently, even if there are other matched orders before/after this order on the eventQueue that have not yet been finalized. A. atomic_finalize_events

                ...
                // Calculations ommitted for brevity
                let from_account_base = match side {
                    Side::Ask => maker_ata,
                    Side::Bid => taker_ata,
                };
                let to_account_base = market_base_vault;

                let from_account_quote = match side {
                    Side::Ask => taker_ata,
                    Side::Bid => maker_ata,
                };

                let to_account_quote = market_quote_vault;
                
                if base_amount_transfer > 0 {
                    msg!("{} tokens of base mint {} transferring from user's account {} to market's vault {}", base_amount_transfer,  from_account_base.mint, from_account_base.key(), market_base_vault.key());

                    // transfer base token
                    token_transfer_signed(
                        base_amount_transfer,
                        &ctx.accounts.token_program,
                        from_account_base,
                        to_account_base,
                        &ctx.accounts.market_authority,
                        seeds,
                    )?;
                    // Bid recieves base, ASKER recieves quote
                    // credit base to counterparty
                } else {
                    msg!("base transfer amount is 0");
                }
                //transfer quote token
                if quote_amount_transfer > 0 {
  
                    token_transfer_signed(
                        quote_amount_transfer,
                        &ctx.accounts.token_program,
                        from_account_quote,
                        to_account_quote,
                        &ctx.accounts.market_authority,
                        seeds,
                    )?;
                    // Bid recieves base, ASKER recieves quote
                    // credit quote to counterparty
                } else {
                    msg!("quote transfer amount is 0");
                }
                ... // accounting
               

Accounting

After executing just-in-time transfers from both parties, their openorders balances are updated to reflect the completed trade:

                // CREDIT the maker and taker with the filled amount
                if side == Side::Bid {
                    taker.position.quote_free_native += quote_amount;
                    maker.position.base_free_native += base_amount;
                } else {
                    maker.position.quote_free_native += quote_amount;
                    taker.position.base_free_native += base_amount;
                }

B. atomicFinalizeEventsDirect

This function is used to conduct JIT transfers directly into each counterparties' wallet, without using the intermediate openorders for accounting. The purpose of this is convinience for taker orders, and easier integration with aggregators.

  let (from_account_base, to_account_base) = match side {
                    Side::Ask => (maker_base_account, taker_base_account),
                    Side::Bid => (taker_base_account, maker_base_account),
                };
                //let to_account_base = market_base_vault;

                // if maker is ASK, maker sends base, gets quote. If maker is BID, maker sends quote, gets base
                let (from_account_quote, to_account_quote) = match side {
                    Side::Ask => (taker_quote_account, maker_quote_account),
                    Side::Bid => (maker_quote_account, taker_quote_account),
                };

                let seeds = market_seeds!(market, ctx.accounts.market.key());
                msg!(
                    "transferrring {} tokens from user's ata {} to market's vault {}",
                    base_amount_transfer,
                    from_account_base.to_account_info().key(),
                    market_base_vault.to_account_info().key()
                );
                // Perform the transfer if the amount is greater than zero
                if base_amount_transfer > 0 {
                    // transfer base token
                    token_transfer_signed(
                        base_amount_transfer,
                        &ctx.accounts.token_program,
                        from_account_base.as_ref(),
                        to_account_base.as_ref(),
                        &ctx.accounts.market_authority,
                        seeds,
                    )?;
                    // Bid recieves base, ASKER recieves quote
                    // credit base to counterparty
                } else {
                    msg!("base transfer amount is 0");
                }
                //transfer quote token
                if quote_amount_transfer > 0 {

                    token_transfer_signed(
                        quote_amount_transfer,
                        &ctx.accounts.token_program,
                        from_account_quote.as_ref(),
                        to_account_quote.as_ref(),
                        &ctx.accounts.market_authority,
                        seeds,
                    )?;
                    // Bid recieves base, ASKER recieves quote
                    // credit quote to counterparty
                } else {
                    msg!("quote transfer amount is 0");
                }

4. Handling Settlement Failure: Cancel With Penalty

Key Data Structures
Instructions
Events
SDK