SDK
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 amounttoken_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.
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; } elseif post_only {msg!("Order could not be placed due to PostOnly"); post_target =None;break; // return silently to not fail other instructions in tx } elseif 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 behaviourif 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) asu64); } 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 brevitylet 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 tokentoken_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 tokenif 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 amountif 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 baselet (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 zeroif base_amount_transfer >0 {// transfer base tokentoken_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 tokenif 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